Compare commits

...

103 Commits

Author SHA1 Message Date
Tim Zhang e4551b1013 shim-protos: replace ttrpc-codegen to the released crate version
The new version v0.6.0 has been released.

Signed-off-by: Tim Zhang <tim@hyper.sh>
2025-07-15 17:54:05 +00:00
jinda.ljd 0e4ff0c5da truncate backing file path exceeding 64 bytes in LoopInfo
When the backing file path exceeds 64 bytes, an 'out of range' error occurs due to the limitation of the `file_name` field in `LoopInfo`. This commit truncates the file path to ensure it does not exceed the maximum supported length, preventing the error while maintaining usability.

Signed-off-by: jinda.ljd <jinda.ljd@alibaba-inc.com>
2025-07-15 02:56:39 +00:00
zzzzzzzzzy9 afab3c8eba add some test for mount_rootfs and umount_recursive and setup_loop
Signed-off-by: zzzzzzzzzy9 <zhang.yu58@zte.com.cn>
2025-07-07 04:51:59 +00:00
zzzzzzzzzy9 11e97809b8 support loop-dev mount in mount_rootfs
Signed-off-by: zzzzzzzzzy9 <zhang.yu58@zte.com.cn>
2025-07-07 04:51:59 +00:00
zzzzzzzzzy9 15fbabcf8e Unmount the mount point of the container when the container is deleted.
Signed-off-by: zzzzzzzzzy9 <zhang.yu58@zte.com.cn>
2025-07-07 04:51:59 +00:00
zzzzzzzzzy9 6aa801807e move mount to mount_linux and mount_other
Signed-off-by: zzzzzzzzzy9 <zhang.yu58@zte.com.cn>
2025-07-07 04:51:59 +00:00
Maksym Pavlenko 220d6d6a65
Fix runc dependency
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-06-30 10:09:42 -07:00
Maksym Pavlenko b8107d6101
Bump runc crate version
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-06-30 09:54:17 -07:00
Bryant Biggs c0b92c4e96 chore: Update protobuf definitions using latest containerd version v2.1.1 2025-06-11 00:15:32 +00:00
Bryant Biggs 8469d6d7a8 chore: Bump ttrpc-codegen as well 2025-06-11 00:13:54 +00:00
Bryant Biggs ad6d647960 chore: Update protobuf dependency to latest 2025-06-11 00:13:54 +00:00
jokemanfire 055e49bf6f fix ut test
Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2025-06-11 00:13:30 +00:00
jokemanfire 0a07bdde72 runc: split the lib's trait from lib to async and sync
Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2025-06-11 00:13:30 +00:00
Bryant Biggs 468fbc0b4c chore: Move tokio dependency in `snapshots` to dev dependency 2025-06-11 00:09:51 +00:00
Bryant Biggs cd5a84d8a7 chore: Update tonic dependencies to latest 2025-06-11 00:07:41 +00:00
jokemanfire cea5523d20 fix(update): avoid update resource while the process is zombie
add a check in zombie init process

Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2025-05-09 01:42:38 +00:00
dependabot[bot] 9b5727a28e build(deps): bump bnjbvr/cargo-machete from 0.7.0 to 0.8.0
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 0.7.0 to 0.8.0.
- [Release notes](https://github.com/bnjbvr/cargo-machete/releases)
- [Changelog](https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bnjbvr/cargo-machete/compare/v0.7.0...v0.8.0)

---
updated-dependencies:
- dependency-name: bnjbvr/cargo-machete
  dependency-version: 0.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-08 19:31:20 +00:00
Maksym Pavlenko 4744fafcd6 Remove Ubuntu 20.04 runners and bump containerd versions
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-05-08 18:40:27 +00:00
jokemanfire 3b42040af2 optimize: use a more user-friendly interface in ttrpc
Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2025-04-15 03:41:12 +00:00
Jorge Prendes 8130e41939 remove dependency on signal-hook-tokio
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
2025-04-10 16:27:31 +00:00
Jorge Prendes 2a098d9f69 re-enable cargo machete
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
2025-04-09 14:23:23 +00:00
Jorge Prendes 59589df20f ping grcov version
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
2025-04-09 13:47:06 +00:00
Jorge Prendes 18fcf6bc52 remove unused dependencies
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
2025-03-19 23:27:16 +00:00
Jorge Prendes 277a1a65f2 use windows activation strategy on async code
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
2025-03-19 23:27:16 +00:00
Jorge Prendes df1b8f05dd use windows activation strategy on all platforms in sync code
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
2025-03-19 23:27:16 +00:00
Jorge Prendes 35980682a6 Consolidate the NamedPipeLogger into the FifoLogger
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
2025-03-19 23:25:14 +00:00
Jorge Prendes f4fdddc5e5 ignore advisory with proc-macro-error and protobuf to unblock CI
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
2025-03-19 22:54:47 +00:00
Jiaxiao (mossaka) Zhou 8fba47295b feat(shim): make logger module public
Downstream shim implementations can use this module to setup logger by
themselves. One example is runwasi's container process needs to use this
module to setup logger so that logs from the container process can be
populated to containerd.

Signed-off-by: Jiaxiao (mossaka) Zhou <duibao55328@gmail.com>
2025-02-25 23:17:12 +00:00
jokemanfire 906bd0f466 Update publisher err deal.
ref:https://github.com/containerd/ttrpc-rust/pull/259
Due to the inclusion of the latest version of ttrpc, this part of the code has been updated

Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2025-02-24 18:54:43 +00:00
Kyle Kosic 559cc576b9 add experimental flag 2025-02-20 18:20:05 +00:00
Phil Estes 9d9cc05d18 Add example using the transfer service
Pulls an image to the containerd content store

Signed-off-by: Phil Estes <estesp@amazon.com>
2025-02-01 06:55:48 +00:00
Maksym Pavlenko 822a065062 Add Jiaxiao Zhou to reviewers
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-31 19:05:21 +00:00
Maksym Pavlenko cd926a2d89
Remove --files-with-diff
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-31 11:09:25 -08:00
Maksym Pavlenko ae876e3a33
Fix shim dependency
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-28 12:06:30 -08:00
Maksym Pavlenko c016d933ed
Bump shim crate version to 0.8
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-28 11:59:33 -08:00
Maksym Pavlenko 2d999680fe
Fix publish?
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-28 11:49:18 -08:00
Maksym Pavlenko ef81c80577
Bump shim protos to 0.8
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-28 11:30:33 -08:00
jokemanfire e5db241747 Fix complie error
1. update ttrpc version
2. proto3 syntax  "To define 'optional' fields in Proto3, simply remove the 'optional' label, as fields are 'optional' by default"
3. compile fail because "required for the cast from `Arc<std::boxed::Box<<T as asynchronous::Shim>::T>>` to `Arc<(dyn containerd_shim_protos::shim_async::Task + std::marker::Send + Sync + 'static)>`"

Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2025-01-27 20:08:08 +00:00
dependabot[bot] 26b783c0b4 build(deps): bump actions/stale from 9.0.0 to 9.1.0
Bumps [actions/stale](https://github.com/actions/stale) from 9.0.0 to 9.1.0.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](28ca103628...5bef64f19d)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 20:06:30 +00:00
jokemanfire 545b0e789e Add info for get runtimeinfo
This interface is already in goshim.
Use containerd-shim-runc-v2 --info.

Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2025-01-27 20:06:12 +00:00
Maksym Pavlenko 1f1e38dc06
Bump client to 0.8.0
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-16 13:46:49 -08:00
Phil Estes 2d47d88f13 Add a to_any helper in the containerd client crate
This provides a solution to Any->type matching that happens on
the containerd Go-based server side where the Any types in prost
use typeurl as defined in the protobuf spec and the server side
is matching on the fullname property of the type.

Signed-off-by: Phil Estes <estesp@amazon.com>
2025-01-16 20:44:01 +00:00
Maksym Pavlenko 2cb7714e76 Fix CI
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-16 19:22:09 +00:00
jokemanfire a345bac309 OpenOptions in blocking mod is a time-consuming operation.
1.Prevent it from blocking a tokio thread. Change sync to async.
2.Add pipe unit test which I found error in Pipe new.

Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2025-01-09 02:57:43 +00:00
Phil Estes 41d2ded16a Add mod for transfer types
Makes the transfer types available in containerd_client imports

Signed-off-by: Phil Estes <estesp@amazon.com>
2025-01-08 19:50:56 +00:00
Maksym Pavlenko 95964f295e
Bump containerd client to 0.7.0
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-08 10:43:30 -08:00
Maksym Pavlenko ce05587af2 Update vendor
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-08 18:31:52 +00:00
Maksym Pavlenko 30be44066a Update vendor script
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-08 18:31:52 +00:00
Maksym Pavlenko 68bff89898
Don't check labels
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-07 16:05:15 -08:00
Maksym Pavlenko 92e4b8b4fb Update Go version
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-06 22:04:41 +00:00
Maksym Pavlenko c3620b7602 Update containerd versions for Linux integration
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-06 22:04:41 +00:00
Maksym Pavlenko 4c66dd46f5
Add stale bot
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-06 12:55:10 -08:00
Maksym Pavlenko bd5dd4fcbd
Remove unused fields
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-06 10:58:59 -08:00
jokemanfire feac9615e1 Fix publisher debug info is not correct
In the containerd log, it is not print correct
2025-01-06 10:52:50 -08:00
Maksym Pavlenko 314e384806
Fix unused fields on MacOS
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2025-01-06 10:50:54 -08:00
Maksym Pavlenko 346b82e673
Fix unused fields on Mac
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2024-12-31 15:09:58 -08:00
Maksym Pavlenko e27decc5ba
Fix clippy
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2024-12-31 14:33:44 -08:00
Maksym Pavlenko 6ad0a6e0d3
Bump Rust version to 1.81
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2024-12-31 14:33:32 -08:00
jokemanfire 4133fef4c0 Optimize: refine the scope of the lock.
We can discard the lock before reporting the incident.According to Goshim.

Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2024-12-15 03:25:25 +00:00
jokemanfire a0f3bf13a1 Attemp to resolve fifo open deadlock
If long time no read side , may cause shim lock.
Avoid fifo open deadlock,use a easy timeout

Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2024-12-13 20:04:38 +00:00
jokemanfire 6526c28e87 Prevent the init process from exiting and continuing with start
No need to wait for runc return an error

Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2024-12-13 20:02:50 +00:00
jokemanfire eef8806871 fix process exit (not init) event not publish
1.add get id interface in process
2.add process event publish in service

Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2024-12-11 20:10:31 +00:00
fengwei0328 1e17e1495e The task_dir successfully cleans when the file is absent
Signed-off-by: fengwei0328 <feng.wei8@zte.com.cn>
2024-12-06 18:54:16 +00:00
zzzzzzzzzy9 7051e312f5 if init is paused/pausing, exec state should be the same 2024-12-04 21:57:39 +00:00
ningmingxiao 8029a61a2b fix leftover shim processes 2024-12-03 23:34:03 +00:00
jokemanfire c6b623cb8b Attempt to change mutex to rwlock
In high concurrency environments, some operations are read-only and do not require mutex write locks.

Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2024-12-03 23:33:04 +00:00
adsick 3044a44363 `with_namespace!`: fix formatting and accept `expr` instead of `ident` 2024-12-03 22:40:50 +00:00
dependabot[bot] 0db44b965c build(deps): update thiserror requirement from 1.0 to 2.0
Updates the requirements on [thiserror](https://github.com/dtolnay/thiserror) to permit the latest version.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.0...1.0.68)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-03 19:02:07 +00:00
dependabot[bot] 25f06b4c7f build(deps): bump codecov/codecov-action from 4 to 5
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-03 17:59:18 +00:00
Jiaxiao Zhou (Mossaka) f87972b9b0 runc/src/io: added the missing imports to unit tests
Signed-off-by: Jiaxiao Zhou (Mossaka) <duibao55328@gmail.com>
2024-12-03 17:58:52 +00:00
Jiaxiao Zhou (Mossaka) 3bb4e14fe5 .github/workflows/ci: add check to runc shim
The runc shim, like the containerd-shim, has a 'sync' feature that is not checked by the workspace.
Thus we need to add that individually to the CI to get full coverage

Signed-off-by: Jiaxiao Zhou (Mossaka) <duibao55328@gmail.com>
2024-12-03 17:58:52 +00:00
Jiaxiao Zhou (Mossaka) 474db59c81 runc/src/io: expose Io as a public API
Signed-off-by: Jiaxiao Zhou (Mossaka) <duibao55328@gmail.com>
2024-12-03 17:58:52 +00:00
Jiaxiao Zhou (Mossaka) b7de553f9e runc/Cargo: add the missing os_pipe dependency
Signed-off-by: Jiaxiao Zhou (Mossaka) <duibao55328@gmail.com>
2024-12-03 17:58:52 +00:00
jiaxiao zhou 7efe8d8390 runc: split Pipe, Io, and PipedIo to async and sync modules
Signed-off-by: jiaxiao zhou <duibao55328@gmail.com>
2024-12-03 17:58:52 +00:00
Maksym Pavlenko 3da6d80b0b Disable cargo machete
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2024-12-03 09:51:32 -08:00
Maksym Pavlenko 9cfc6454f0 Fix CI
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2024-12-03 09:43:44 -08:00
jokemanfire 6d496ef7fa In delete: Process not found should not return ok
Compatible with goshim

Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2024-11-19 19:31:22 +00:00
Christos Katsakioris ac89daa9b1 snapshots: Add handy `Clone`, `Copy` derivations
Derive `Clone` for `Info`, as well as both `Clone` and `Copy` for `Kind`
and `Usage`, which allows for convenient manipulation of these types.

Signed-off-by: Christos Katsakioris <ckatsak@gmail.com>
2024-11-13 21:41:08 +00:00
jiaxiao zhou 857ace0e3f crates/runc: removed unused dep
Signed-off-by: jiaxiao zhou <duibao55328@gmail.com>
2024-11-06 18:26:42 +00:00
jiaxiao zhou a665cfcfc0 client, snapshots/Cargo.toml: Ignore `prost` in cargo-machete metadata
Signed-off-by: jiaxiao zhou <duibao55328@gmail.com>
2024-11-06 18:26:42 +00:00
jiaxiao zhou b535a030d1 .github/workflows/ci: add machete to CI
this will check if there are unused deps in this repo
2024-11-06 18:26:42 +00:00
dependabot[bot] d584728948 build(deps): update oci-spec requirement from 0.6 to 0.7
Updates the requirements on [oci-spec](https://github.com/containers/oci-spec-rs) to permit the latest version.
- [Release notes](https://github.com/containers/oci-spec-rs/releases)
- [Changelog](https://github.com/containers/oci-spec-rs/blob/main/release.md)
- [Commits](https://github.com/containers/oci-spec-rs/compare/v0.6.0...v0.6.7)

---
updated-dependencies:
- dependency-name: oci-spec
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-05 19:34:27 +00:00
Lei Liu 2689bbf250 Ignore notFound error when collect from pid cgroup
When `pids.current` or `pids.max` not exist in cgroup root
(/sys/fs/cgroup/pids/) collect_metrics will print out error level
log "...unable to read a control group file /sys/fs/cgroup/pids/./
pids.current..." continually, this commit will ignore these not
found errors.

As a supplement,if or not set pids.current file in cgroup controller
root directory decided by `CFTYPE_NOT_ON_ROOT` flag, in many commen
environment it is default to enabled. And as a contrast, go version
code will ignore all not found error when collect cgroupv1 metrics:
b67a788072/runtime/v2/runc/task/service.go (L625)

Co-authored-by: Yukiteru <wfly1998@gmail.com>

Signed-off-by: Lei Liu <liulei.pt@bytedance.com>
2024-11-05 19:33:58 +00:00
yummyMax 7c01662306 change socket path
Some containerd test use shim.SocketAddress directly.
So for Goshim-compatible socket path

Signed-off-by: yummyMax <yu.sihui@zte.com.cn>
2024-10-30 23:15:24 +00:00
jokemanfire 6cb625da04 <rshim>Improve the events report
1.add timeout in ctx
2.add queue in publisher report
3.add reconnect client method
2024-10-30 23:14:55 +00:00
zzzzzzzzzy9 792c2e619e shim: Implement the pause and resume interfaces 2024-10-30 16:53:26 +00:00
Lei Liu c22dba6201 fix: remove meaningless input from other_error macro
Signed-off-by: Lei Liu <liulei.pt@bytedance.com>
2024-10-30 00:05:16 +00:00
ningmingxiao a6b4286542 fix os_pipe doesn't work with async IO 2024-10-21 23:04:57 +00:00
dependabot[bot] 7794b07f16 build(deps): update tower requirement from 0.4 to 0.5
Updates the requirements on [tower](https://github.com/tower-rs/tower) to permit the latest version.
- [Release notes](https://github.com/tower-rs/tower/releases)
- [Commits](https://github.com/tower-rs/tower/compare/tower-0.4.0...tower-0.5.1)

---
updated-dependencies:
- dependency-name: tower
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-08 22:58:49 +00:00
Maksym Pavlenko 304d67069b Bump client to 0.6.0
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2024-10-07 11:35:55 -07:00
Bryant Biggs 3f541b9454 fix: All unused re-export to pass clippy 2024-10-05 14:55:48 +00:00
Bryant Biggs da4d1f48bf fix: Replace `tonic` deprecated methods to fix clippy warnings 2024-10-05 14:55:48 +00:00
Bryant Biggs ece6516da5 fix: Switch `write` to `write_all` to consume 2024-10-05 14:55:48 +00:00
Bryant Biggs 6f2812f4b8 fix: Correct lint warnings, bump toolchain version due to `cargo::key=value` build directive which is reserved for future use 2024-10-05 14:55:48 +00:00
Bryant Biggs fb4cb85d66 chore: Run `cargo fmt` 2024-10-05 14:55:48 +00:00
Bryant Biggs 2265428508 Update lib.rs
Co-authored-by: James Sturtevant <jsturtevant@gmail.com>
Signed-off-by: Bryant Biggs <bryantbiggs@gmail.com>
2024-10-05 14:55:48 +00:00
Bryant Biggs 81f51386f0 chore: Revert change to windows target 2024-10-05 14:55:48 +00:00
Bryant Biggs ece15e9275 fix: Windows client is not awaitable 2024-10-05 14:55:48 +00:00
Bryant Biggs 2050c857d6 chore: Update `tonic` and `prost` dependencies to latest 2024-10-05 14:55:48 +00:00
Maksym Pavlenko 4b8bd82ea9 Bump shim protos and fix dependencies
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2024-10-04 11:57:47 -07:00
Maksym Pavlenko 4e60e8a6fa Bump containerd shim to 0.7.4
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2024-10-04 11:30:19 -07:00
jiaxiao zhou 15f51b0899 crates/shim-protos/Cargo: relex version requirement for protobuf
Signed-off-by: jiaxiao zhou <duibao55328@gmail.com>
2024-10-04 17:40:53 +00:00
Maksym Pavlenko 202c756d26 Fix shim dependency to shim-protos and bump to 0.7.3
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2024-10-03 11:50:13 -07:00
120 changed files with 7027 additions and 1993 deletions

View File

@ -22,22 +22,30 @@ jobs:
- run: ./scripts/install-protobuf.sh
shell: bash
- run: rustup toolchain install nightly --component rustfmt
- run: cargo +nightly fmt --all -- --check
# the "runc" and "containerd-shim" crates have `sync` code that is not covered by the workspace
- run: cargo check -p runc --all-targets
- run: cargo clippy -p runc --all-targets -- -D warnings
- run: cargo check -p containerd-shim --all-targets
- run: cargo clippy -p containerd-shim --all-targets -- -D warnings
# check the workspace
- run: cargo check --examples --tests --all-targets
- run: cargo check --examples --tests --all-targets --all-features
- run: rustup toolchain install nightly --component rustfmt
- run: cargo +nightly fmt --all -- --check --files-with-diff
- run: cargo clippy --all-targets -- -D warnings
- run: cargo clippy --all-targets --all-features -- -D warnings
# the shim has sync code that is not covered when running with --all-features
- run: cargo clippy -p containerd-shim --all-targets -- -D warnings
- run: cargo doc --no-deps --features docs
env:
RUSTDOCFLAGS: -Dwarnings
- name: check unused dependencies
uses: bnjbvr/cargo-machete@v0.8.0
env:
RUSTUP_TOOLCHAIN: "stable"
# TODO: Merge this with the checks job above
windows-checks:
name: Windows Checks
@ -51,7 +59,7 @@ jobs:
- run: cargo check --examples --tests -p containerd-shim -p containerd-shim-protos -p containerd-client
- run: rustup toolchain install nightly --component rustfmt
- run: cargo +nightly fmt -p containerd-shim -p containerd-shim-protos -p containerd-client -- --check --files-with-diff
- run: cargo +nightly fmt -p containerd-shim -p containerd-shim-protos -p containerd-client -- --check
- run: cargo clippy -p containerd-shim -p containerd-shim-protos -- -D warnings
- run: cargo doc --no-deps -p containerd-shim -p containerd-shim-protos -p containerd-client
@ -111,7 +119,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1
- uses: EmbarkStudios/cargo-deny-action@v2
linux-integration:
name: Linux Integration
@ -120,8 +128,8 @@ jobs:
strategy:
matrix:
os: [ubuntu-20.04, ubuntu-22.04]
containerd: [v1.6.28, v1.7.13, v2.0.0-beta.2]
os: [ubuntu-latest]
containerd: [v1.6.38, v1.7.27, v2.1.1]
steps:
- name: Checkout extensions
@ -158,7 +166,7 @@ jobs:
## get latest go version for integrations tests so we can skip runnings tests
- uses: actions/setup-go@v5
with:
go-version: '1.20.12'
go-version: '1.23'
- name: Integration
env:
@ -222,7 +230,7 @@ jobs:
# run the example
cargo run -p containerd-shim --example skeleton -- -namespace default -id 1234 -address "\\.\pipe\containerd-containerd" -publish-binary ./bin/containerd start
ps skeleton
cargo run -p containerd-shim-protos --example shim-proto-connect \\.\pipe\containerd-shim-17630016127144989388-pipe
cargo run -p containerd-shim-protos --example shim-proto-connect \\.\pipe\containerd-shim-bc764c65e177434fcefe8257dc440be8b8acf7c96156320d965938f7e9ae1a35-pipe
$skeleton = get-process skeleton -ErrorAction SilentlyContinue
if ($skeleton) { exit 1 }
- name: Run client

View File

@ -22,7 +22,7 @@ jobs:
- name: Install grcov
run: |
cargo install --locked grcov
cargo install --locked grcov@0.8.24
grcov --version
- name: Tests
@ -47,7 +47,7 @@ jobs:
--output-path ./target/coverage/
- name: Upload coverage data
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: unittests

36
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: 'Close stale issues and PRs'
on:
schedule:
- cron: "0 0 * * *" # Every day at midnight
pull_request:
paths:
- '.github/workflows/stale.yml'
permissions: read-all
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
# All stale bot options: https://github.com/actions/stale#all-options
with:
# Idle number of days before marking issues/PRs stale
days-before-stale: 90
# Idle number of days before closing stale issues/PRs
days-before-close: 7
# Comment on the staled issues
stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. This issue will be closed in 7 days unless new comments are made or the stale label is removed.'
# Comment on the staled PRs
stale-pr-message: 'This PR is stale because it has been open 90 days with no activity. This PR will be closed in 7 days unless new comments are made or the stale label is removed.'
# Comment on the staled issues while closed
close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.'
# Comment on the staled PRs while closed
close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity.'
# Enable dry-run when changing this file from a PR.
debug-only: github.event_name == 'pull_request'

View File

@ -32,20 +32,20 @@ futures = "0.3.19"
libc = "0.2.112"
log = {version = "0.4.2", features=["kv_unstable"]}
nix = "0.29"
oci-spec = "0.6"
oci-spec = "0.7"
os_pipe = "1.1"
prctl = "1.0.0"
prost = "0.12"
prost-build = "0.12"
prost-types = "0.12"
prost = "0.13"
prost-build = "0.13"
prost-types = "0.13"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
simple_logger = { version = "5.0", default-features = false }
tempfile = "3.6"
thiserror = "1.0"
thiserror = "2.0"
time = { version = "0.3.29", features = ["serde", "std", "formatting"] }
tokio = "1.26"
tonic = "0.11"
tonic-build = "0.11"
tower = "0.4"
tonic = "0.13"
tonic-build = "0.13"
tower = "0.5"
uuid = { version = "1.0", features = ["v4"] }

View File

@ -8,3 +8,5 @@
# GitHub ID, Name, Email address
"Burning1020","Zhang Tianyang","burning9699@gmail.com"
"jsturtevant","James Sturtevant","jstur@microsoft.com"
"mossaka","Jiaxiao Zhou","jiazho@microsoft.com"

View File

@ -1,6 +1,6 @@
[package]
name = "containerd-client"
version = "0.5.0"
version = "0.8.0"
authors = [
"Maksym Pavlenko <pavlenko.maksym@gmail.com>",
"The containerd Authors",
@ -23,6 +23,7 @@ name = "version"
path = "examples/version.rs"
[dependencies]
hyper-util = "0.1.6" # https://github.com/hyperium/hyper/issues/3110
prost.workspace = true
prost-types.workspace = true
tokio = { workspace = true, optional = true }
@ -48,3 +49,6 @@ default = ["connect"]
[package.metadata.docs.rs]
features = ["docs"]
[package.metadata.cargo-machete]
ignored = ["prost"]

View File

@ -62,15 +62,17 @@ const FIXUP_MODULES: &[&str] = &[
"containerd.services.tasks.v1",
"containerd.services.containers.v1",
"containerd.services.content.v1",
"containerd.services.events.v1",
];
fn main() {
let mut config = prost_build::Config::new();
config.protoc_arg("--experimental_allow_proto3_optional");
config.enable_type_names();
tonic_build::configure()
.build_server(false)
.compile_with_config(config, PROTO_FILES, &["vendor/"])
.compile_protos_with_config(config, PROTO_FILES, &["vendor/"])
.expect("Failed to generate GRPC bindings");
for module in FIXUP_MODULES {
@ -95,6 +97,7 @@ fn fixup_imports(path: &str) -> Result<(), io::Error> {
let contents = fs::read_to_string(&path)?
.replace("super::super::super::v1::types", "crate::types::v1") // for tasks service
.replace("super::super::super::super::types", "crate::types")
.replace("super::super::super::types", "crate::types")
.replace("super::super::super::super::google", "crate::google")
.replace(

View File

@ -0,0 +1,91 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
use std::env::consts;
use client::{
services::v1::{transfer_client::TransferClient, TransferOptions, TransferRequest},
to_any,
types::{
transfer::{ImageStore, OciRegistry, UnpackConfiguration},
Platform,
},
with_namespace,
};
use containerd_client as client;
use tonic::Request;
const IMAGE: &str = "docker.io/library/alpine:latest";
const NAMESPACE: &str = "default";
/// Make sure you run containerd before running this example.
/// NOTE: to run this example, you must prepare a rootfs.
#[tokio::main(flavor = "current_thread")]
async fn main() {
let arch = match consts::ARCH {
"x86_64" => "amd64",
"aarch64" => "arm64",
_ => consts::ARCH,
};
let channel = client::connect("/run/containerd/containerd.sock")
.await
.expect("Connect Failed");
let mut client = TransferClient::new(channel.clone());
// Create the source (OCIRegistry)
let source = OciRegistry {
reference: IMAGE.to_string(),
resolver: Default::default(),
};
let platform = Platform {
os: "linux".to_string(),
architecture: arch.to_string(),
variant: "".to_string(),
os_version: "".to_string(),
};
// Create the destination (ImageStore)
let destination = ImageStore {
name: IMAGE.to_string(),
platforms: vec![platform.clone()],
unpacks: vec![UnpackConfiguration {
platform: Some(platform),
..Default::default()
}],
..Default::default()
};
let anys = to_any(&source);
let anyd = to_any(&destination);
println!("Pulling image for linux/{} from source: {:?}", arch, source);
// Create the transfer request
let request = TransferRequest {
source: Some(anys),
destination: Some(anyd),
options: Some(TransferOptions {
..Default::default()
}),
};
// Execute the transfer (pull)
client
.transfer(with_namespace!(request, NAMESPACE))
.await
.expect("unable to transfer image");
}

View File

@ -28,6 +28,9 @@ pub mod types {
pub mod v1 {
tonic::include_proto!("containerd.v1.types");
}
pub mod transfer {
tonic::include_proto!("containerd.types.transfer");
}
}
/// Generated `google.rpc` types, containerd services typically use some of these types.
@ -86,23 +89,29 @@ pub async fn connect(
let path = path.as_ref().to_path_buf();
// Taken from https://github.com/hyperium/tonic/blob/eeb3268f71ae5d1107c937392389db63d8f721fb/examples/src/uds/client.rs#L19
// Taken from https://github.com/hyperium/tonic/blob/71fca362d7ffbb230547f23b3f2fb75c414063a8/examples/src/uds/client.rs#L21-L28
// There will ignore this uri because uds do not use it
// and make connection with UnixStream::connect.
let channel = Endpoint::try_from("http://[::]")
.unwrap()
let channel = Endpoint::try_from("http://[::]")?
.connect_with_connector(tower::service_fn(move |_| {
#[cfg(unix)]
{
tokio::net::UnixStream::connect(path.clone())
}
let path = path.clone();
#[cfg(windows)]
{
let client = tokio::net::windows::named_pipe::ClientOptions::new()
.open(path.clone())
.map_err(|e| std::io::Error::from(e));
async move { client }
async move {
#[cfg(unix)]
{
Ok::<_, std::io::Error>(hyper_util::rt::TokioIo::new(
tokio::net::UnixStream::connect(path).await?,
))
}
#[cfg(windows)]
{
let client = tokio::net::windows::named_pipe::ClientOptions::new()
.open(&path)
.map_err(|e| std::io::Error::from(e))?;
Ok::<_, std::io::Error>(hyper_util::rt::TokioIo::new(client))
}
}
}))
.await?;
@ -110,12 +119,25 @@ pub async fn connect(
Ok(channel)
}
use prost::{Message, Name};
use prost_types::Any;
// to_any provides a helper to match the current use of the protobuf "fullname" trait
// in the Go code on the gRPC server side in containerd when handling matching of Any
// types to registered types on the server. Further discussion on future direction
// of typeurl in this issue: https://github.com/containerd/rust-extensions/issues/362
pub fn to_any<T: Message + Name>(m: &T) -> Any {
let mut anyt = Any::from_msg(m).unwrap();
anyt.type_url = T::full_name();
anyt
}
/// Help to inject namespace into request.
///
/// To use this macro, the `tonic::Request` is needed.
#[macro_export]
macro_rules! with_namespace {
($req : ident, $ns: expr) => {{
($req:expr, $ns:expr) => {{
let mut req = Request::new($req);
let md = req.metadata_mut();
// https://github.com/containerd/containerd/blob/main/pkg/namespaces/grpc.go#L27

View File

@ -19,10 +19,10 @@ syntax = "proto3";
package containerd.events;
import "google/protobuf/any.proto";
import "github.com/containerd/containerd/protobuf/plugin/fieldpath.proto";
import "github.com/containerd/containerd/api/types/fieldpath.proto";
option go_package = "github.com/containerd/containerd/api/events;events";
option (containerd.plugin.fieldpath_all) = true;
option (containerd.types.fieldpath_all) = true;
message ContainerCreate {
string id = 1;

View File

@ -18,10 +18,15 @@ syntax = "proto3";
package containerd.events;
import "github.com/containerd/containerd/protobuf/plugin/fieldpath.proto";
import "github.com/containerd/containerd/api/types/fieldpath.proto";
option go_package = "github.com/containerd/containerd/api/events;events";
option (containerd.plugin.fieldpath_all) = true;
option (containerd.types.fieldpath_all) = true;
message ContentCreate {
string digest = 1;
int64 size = 2;
}
message ContentDelete {
string digest = 1;

View File

@ -18,10 +18,10 @@ syntax = "proto3";
package containerd.services.images.v1;
import "github.com/containerd/containerd/protobuf/plugin/fieldpath.proto";
import "github.com/containerd/containerd/api/types/fieldpath.proto";
option go_package = "github.com/containerd/containerd/api/events;events";
option (containerd.plugin.fieldpath_all) = true;
option (containerd.types.fieldpath_all) = true;
message ImageCreate {
string name = 1;

View File

@ -18,10 +18,10 @@ syntax = "proto3";
package containerd.events;
import "github.com/containerd/containerd/protobuf/plugin/fieldpath.proto";
import "github.com/containerd/containerd/api/types/fieldpath.proto";
option go_package = "github.com/containerd/containerd/api/events;events";
option (containerd.plugin.fieldpath_all) = true;
option (containerd.types.fieldpath_all) = true;
message NamespaceCreate {
string name = 1;

View File

@ -18,10 +18,10 @@ syntax = "proto3";
package containerd.events;
import "github.com/containerd/containerd/protobuf/plugin/fieldpath.proto";
import "github.com/containerd/containerd/api/types/fieldpath.proto";
option go_package = "github.com/containerd/containerd/api/events;events";
option (containerd.plugin.fieldpath_all) = true;
option (containerd.types.fieldpath_all) = true;
message SnapshotPrepare {
string key = 1;

View File

@ -20,10 +20,10 @@ package containerd.events;
import "google/protobuf/timestamp.proto";
import "github.com/containerd/containerd/api/types/mount.proto";
import "github.com/containerd/containerd/protobuf/plugin/fieldpath.proto";
import "github.com/containerd/containerd/api/types/fieldpath.proto";
option go_package = "github.com/containerd/containerd/api/events;events";
option (containerd.plugin.fieldpath_all) = true;
option (containerd.types.fieldpath_all) = true;
message TaskCreate {
string container_id = 1;

View File

@ -44,6 +44,8 @@ message ApplyRequest {
repeated containerd.types.Mount mounts = 2;
map<string, google.protobuf.Any> payloads = 3;
// SyncFs is to synchronize the underlying filesystem containing files.
bool sync_fs = 4;
}
message ApplyResponse {
@ -74,8 +76,11 @@ message DiffRequest {
// on content store commit.
map<string, string> labels = 5;
// SourceDateEpoch specifies the timestamp used for whiteouts to provide control for reproducibility.
// SourceDateEpoch specifies the timestamp used to provide control for reproducibility.
// See also https://reproducible-builds.org/docs/source-date-epoch/ .
//
// Since containerd v2.0, the whiteout timestamps are set to zero (1970-01-01),
// not to the source date epoch.
google.protobuf.Timestamp source_date_epoch = 6;
}

View File

@ -18,10 +18,9 @@ syntax = "proto3";
package containerd.services.events.v1;
import "github.com/containerd/containerd/protobuf/plugin/fieldpath.proto";
import "github.com/containerd/containerd/api/types/event.proto";
import "google/protobuf/any.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/containerd/containerd/api/services/events/v1;events";
@ -46,7 +45,7 @@ service Events {
// from all namespaces unless otherwise specified. If this is not desired,
// a filter can be provided in the format 'namespace==<namespace>' to
// restrict the received events.
rpc Subscribe(SubscribeRequest) returns (stream Envelope);
rpc Subscribe(SubscribeRequest) returns (stream containerd.types.Envelope);
}
message PublishRequest {
@ -55,17 +54,9 @@ message PublishRequest {
}
message ForwardRequest {
Envelope envelope = 1;
containerd.types.Envelope envelope = 1;
}
message SubscribeRequest {
repeated string filters = 1;
}
message Envelope {
option (containerd.plugin.fieldpath) = true;
google.protobuf.Timestamp timestamp = 1;
string namespace = 2;
string topic = 3;
google.protobuf.Any event = 4;
}

View File

@ -140,4 +140,10 @@ message DeleteImageRequest {
//
// Default is false
bool sync = 2;
// Target value for image to be deleted
//
// If image descriptor does not match the same digest,
// the delete operation will return "not found" error.
optional containerd.types.Descriptor target = 3;
}

View File

@ -18,6 +18,8 @@ syntax = "proto3";
package containerd.services.introspection.v1;
import "google/protobuf/any.proto";
import "github.com/containerd/containerd/api/types/introspection.proto";
import "github.com/containerd/containerd/api/types/platform.proto";
import "google/rpc/status.proto";
import "google/protobuf/empty.proto";
@ -33,6 +35,8 @@ service Introspection {
rpc Plugins(PluginsRequest) returns (PluginsResponse);
// Server returns information about the containerd server
rpc Server(google.protobuf.Empty) returns (ServerResponse);
// PluginInfo returns information directly from a plugin if the plugin supports it
rpc PluginInfo(PluginInfoRequest) returns (PluginInfoResponse);
}
message Plugin {
@ -111,3 +115,19 @@ message DeprecationWarning {
string message = 2;
google.protobuf.Timestamp last_occurrence = 3;
}
message PluginInfoRequest {
string type = 1;
string id = 2;
// Options may be used to request extra dynamic information from
// a plugin.
// This object is determined by the plugin and the plugin may return
// NotImplemented or InvalidArgument if it is not supported
google.protobuf.Any options = 3;
}
message PluginInfoResponse {
Plugin plugin = 1;
google.protobuf.Any extra = 2;
}

View File

@ -31,6 +31,7 @@ import "google/protobuf/timestamp.proto";
import "github.com/containerd/containerd/api/types/sandbox.proto";
import "github.com/containerd/containerd/api/types/mount.proto";
import "github.com/containerd/containerd/api/types/platform.proto";
import "github.com/containerd/containerd/api/types/metrics.proto";
option go_package = "github.com/containerd/containerd/api/services/sandbox/v1;sandbox";
@ -93,6 +94,8 @@ service Controller {
rpc Wait(ControllerWaitRequest) returns (ControllerWaitResponse);
rpc Status(ControllerStatusRequest) returns (ControllerStatusResponse);
rpc Shutdown(ControllerShutdownRequest) returns (ControllerShutdownResponse);
rpc Metrics(ControllerMetricsRequest) returns (ControllerMetricsResponse);
rpc Update(ControllerUpdateRequest) returns (ControllerUpdateResponse);
}
message ControllerCreateRequest {
@ -100,6 +103,9 @@ message ControllerCreateRequest {
repeated containerd.types.Mount rootfs = 2;
google.protobuf.Any options = 3;
string netns_path = 4;
map<string, string> annotations = 5;
containerd.types.Sandbox sandbox = 6;
string sandboxer = 10;
}
message ControllerCreateResponse {
@ -108,6 +114,7 @@ message ControllerCreateResponse {
message ControllerStartRequest {
string sandbox_id = 1;
string sandboxer = 10;
}
message ControllerStartResponse {
@ -115,10 +122,16 @@ message ControllerStartResponse {
uint32 pid = 2;
google.protobuf.Timestamp created_at = 3;
map<string, string> labels = 4;
// Address of the sandbox for containerd to connect,
// for calling Task or other APIs serving in the sandbox.
// it is in the form of ttrpc+unix://path/to/uds or grpc+vsock://<vsock cid>:<port>.
string address = 5;
uint32 version = 6;
}
message ControllerPlatformRequest {
string sandbox_id = 1;
string sandboxer = 10;
}
message ControllerPlatformResponse {
@ -128,12 +141,14 @@ message ControllerPlatformResponse {
message ControllerStopRequest {
string sandbox_id = 1;
uint32 timeout_secs = 2;
string sandboxer = 10;
}
message ControllerStopResponse {}
message ControllerWaitRequest {
string sandbox_id = 1;
string sandboxer = 10;
}
message ControllerWaitResponse {
@ -144,6 +159,7 @@ message ControllerWaitResponse {
message ControllerStatusRequest {
string sandbox_id = 1;
bool verbose = 2;
string sandboxer = 10;
}
message ControllerStatusResponse {
@ -154,10 +170,35 @@ message ControllerStatusResponse {
google.protobuf.Timestamp created_at = 5;
google.protobuf.Timestamp exited_at = 6;
google.protobuf.Any extra = 7;
// Address of the sandbox for containerd to connect,
// for calling Task or other APIs serving in the sandbox.
// it is in the form of ttrpc+unix://path/to/uds or grpc+vsock://<vsock cid>:<port>.
string address = 8;
uint32 version = 9;
}
message ControllerShutdownRequest {
string sandbox_id = 1;
string sandboxer = 10;
}
message ControllerShutdownResponse {}
message ControllerMetricsRequest {
string sandbox_id = 1;
string sandboxer = 10;
}
message ControllerMetricsResponse {
types.Metric metrics = 1;
}
message ControllerUpdateRequest {
string sandbox_id = 1;
string sandboxer = 2;
containerd.types.Sandbox sandbox = 3;
repeated string fields = 4;
}
message ControllerUpdateResponse {
}

View File

@ -18,10 +18,8 @@ syntax = "proto3";
package containerd.services.events.ttrpc.v1;
import "github.com/containerd/containerd/protobuf/plugin/fieldpath.proto";
import "google/protobuf/any.proto";
import "github.com/containerd/containerd/api/types/event.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/containerd/containerd/api/services/ttrpc/events/v1;events";
@ -35,13 +33,5 @@ service Events {
}
message ForwardRequest {
Envelope envelope = 1;
}
message Envelope {
option (containerd.plugin.fieldpath) = true;
google.protobuf.Timestamp timestamp = 1;
string namespace = 2;
string topic = 3;
google.protobuf.Any event = 4;
containerd.types.Envelope envelope = 1;
}

View File

@ -0,0 +1,33 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types;
import "github.com/containerd/containerd/api/types/fieldpath.proto";
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/containerd/containerd/api/types;types";
message Envelope {
option (containerd.types.fieldpath) = true;
google.protobuf.Timestamp timestamp = 1;
string namespace = 2;
string topic = 3;
google.protobuf.Any event = 4;
}

View File

@ -26,12 +26,12 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
syntax = "proto2";
package containerd.plugin;
syntax = "proto3";
package containerd.types;
import "google/protobuf/descriptor.proto";
option go_package = "github.com/containerd/containerd/protobuf/plugin";
option go_package = "github.com/containerd/containerd/api/types;types";
extend google.protobuf.FileOptions {
optional bool fieldpath_all = 63300;

View File

@ -0,0 +1,46 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types;
import "google/protobuf/any.proto";
option go_package = "github.com/containerd/containerd/api/types;types";
message RuntimeRequest {
string runtime_path = 1;
// Options correspond to CreateTaskRequest.options.
// This is needed to pass the runc binary path, etc.
google.protobuf.Any options = 2;
}
message RuntimeVersion {
string version = 1;
string revision = 2;
}
message RuntimeInfo {
string name = 1;
RuntimeVersion version = 2;
// Options correspond to RuntimeInfoRequest.Options (contains runc binary path, etc.)
google.protobuf.Any options = 3;
// OCI-compatible runtimes should use https://github.com/opencontainers/runtime-spec/blob/main/features.md
google.protobuf.Any features = 4;
// Annotations of the shim. Irrelevant to features.Annotations.
map<string, string> annotations = 5;
}

View File

@ -26,4 +26,5 @@ message Platform {
string os = 1;
string architecture = 2;
string variant = 3;
string os_version = 4;
}

View File

@ -0,0 +1,63 @@
syntax = "proto3";
package containerd.runc.v1;
option go_package = "github.com/containerd/containerd/api/types/runc/options;options";
message Options {
// disable pivot root when creating a container
bool no_pivot_root = 1;
// create a new keyring for the container
bool no_new_keyring = 2;
// place the shim in a cgroup
string shim_cgroup = 3;
// set the I/O's pipes uid
uint32 io_uid = 4;
// set the I/O's pipes gid
uint32 io_gid = 5;
// binary name of the runc binary
string binary_name = 6;
// runc root directory
string root = 7;
// criu binary path.
//
// Removed in containerd v2.0: string criu_path = 8;
reserved 8;
// enable systemd cgroups
bool systemd_cgroup = 9;
// criu image path
string criu_image_path = 10;
// criu work path
string criu_work_path = 11;
// task api address, can be a unix domain socket, or vsock address.
// it is in the form of ttrpc+unix://path/to/uds or grpc+vsock://<vsock cid>:<port>.
string task_api_address = 12;
// task api version, currently supported value is 2 and 3.
uint32 task_api_version = 13;
}
message CheckpointOptions {
// exit the container after a checkpoint
bool exit = 1;
// checkpoint open tcp connections
bool open_tcp = 2;
// checkpoint external unix sockets
bool external_unix_sockets = 3;
// checkpoint terminals (ptys)
bool terminal = 4;
// allow checkpointing of file locks
bool file_locks = 5;
// restore provided namespaces as empty namespaces
repeated string empty_namespaces = 6;
// set the cgroups mode, soft, full, strict
string cgroups_mode = 7;
// checkpoint image path
string image_path = 8;
// checkpoint work path
string work_path = 9;
}
message ProcessDetails {
// exec process id if the process is managed by a shim
string exec_id = 1;
}

View File

@ -0,0 +1,17 @@
// To regenerate api.pb.go run `make protos`
syntax = "proto3";
package runtimeoptions.v1;
option go_package = "github.com/containerd/containerd/api/types/runtimeoptions/v1;runtimeoptions";
message Options {
// TypeUrl specifies the type of the content inside the config file.
string type_url = 1;
// ConfigPath specifies the filesystem location of the config file
// used by the runtime.
string config_path = 2;
// Blob specifies an in-memory TOML blob passed from containerd's configuration section
// for this runtime. This will be used if config_path is not specified.
bytes config_body = 3;
}

View File

@ -41,11 +41,14 @@ message Sandbox {
// bundle directory (similary to OCI spec).
google.protobuf.Any spec = 3;
// Labels provides an area to include arbitrary data on containers.
map<string, string> labels = 4;
map<string, string> labels = 4;
// CreatedAt is the time the container was first created.
google.protobuf.Timestamp created_at = 5;
// UpdatedAt is the last time the container was mutated.
google.protobuf.Timestamp updated_at = 6;
// Extensions allow clients to provide optional blobs that can be handled by runtime.
map<string, google.protobuf.Any> extensions = 7;
// Sandboxer is the name of the sandbox controller who manages the sandbox.
string sandboxer = 10;
}

View File

@ -18,6 +18,8 @@ syntax = "proto3";
package containerd.types.transfer;
import "github.com/containerd/containerd/api/types/descriptor.proto";
option go_package = "github.com/containerd/containerd/api/types/transfer";
message Progress {
@ -26,4 +28,5 @@ message Progress {
repeated string parents = 3;
int64 progress = 4;
int64 total = 5;
containerd.types.Descriptor desc = 6;
}

View File

@ -27,6 +27,16 @@ message OCIRegistry {
RegistryResolver resolver = 2;
}
enum HTTPDebug {
DISABLED = 0;
// Enable HTTP debugging
DEBUG = 1;
// Enable HTTP requests tracing
TRACE = 2;
// Enable both HTTP debugging and requests tracing
BOTH = 3;
}
message RegistryResolver {
// auth_stream is used to refer to a stream which auth callbacks may be
// made on.
@ -35,10 +45,18 @@ message RegistryResolver {
// Headers
map<string, string> headers = 2;
// Allow custom hosts dir?
string host_dir = 3;
string default_scheme = 4;
// Force skip verify
// Force HTTP
// CA callback? Client TLS callback?
// Whether to debug/trace HTTP requests to OCI registry.
HTTPDebug http_debug = 5;
// Stream ID to use for HTTP logs (when logs are streamed to client).
// When empty, logs are written to containerd logs.
string logs_stream = 6;
}
// AuthRequest is sent as a callback on a stream

View File

@ -0,0 +1,46 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.events;
import "google/protobuf/any.proto";
import "github.com/containerd/containerd/api/types/fieldpath.proto";
option go_package = "github.com/containerd/containerd/api/events;events";
option (containerd.types.fieldpath_all) = true;
message ContainerCreate {
string id = 1;
string image = 2;
message Runtime {
string name = 1;
google.protobuf.Any options = 2;
}
Runtime runtime = 3;
}
message ContainerUpdate {
string id = 1;
string image = 2;
map<string, string> labels = 3;
string snapshot_key = 4;
}
message ContainerDelete {
string id = 1;
}

View File

@ -0,0 +1,33 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.events;
import "github.com/containerd/containerd/api/types/fieldpath.proto";
option go_package = "github.com/containerd/containerd/api/events;events";
option (containerd.types.fieldpath_all) = true;
message ContentCreate {
string digest = 1;
int64 size = 2;
}
message ContentDelete {
string digest = 1;
}

View File

@ -0,0 +1,38 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.images.v1;
import "github.com/containerd/containerd/api/types/fieldpath.proto";
option go_package = "github.com/containerd/containerd/api/events;events";
option (containerd.types.fieldpath_all) = true;
message ImageCreate {
string name = 1;
map<string, string> labels = 2;
}
message ImageUpdate {
string name = 1;
map<string, string> labels = 2;
}
message ImageDelete {
string name = 1;
}

View File

@ -0,0 +1,38 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.events;
import "github.com/containerd/containerd/api/types/fieldpath.proto";
option go_package = "github.com/containerd/containerd/api/events;events";
option (containerd.types.fieldpath_all) = true;
message NamespaceCreate {
string name = 1;
map<string, string> labels = 2;
}
message NamespaceUpdate {
string name = 1;
map<string, string> labels = 2;
}
message NamespaceDelete {
string name = 1;
}

View File

@ -0,0 +1,37 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.events;
import "google/protobuf/timestamp.proto";
option go_package = "github.com/containerd/containerd/api/events;events";
message SandboxCreate {
string sandbox_id = 1;
}
message SandboxStart {
string sandbox_id = 1;
}
message SandboxExit {
string sandbox_id = 1;
uint32 exit_status = 2;
google.protobuf.Timestamp exited_at = 3;
}

View File

@ -0,0 +1,41 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.events;
import "github.com/containerd/containerd/api/types/fieldpath.proto";
option go_package = "github.com/containerd/containerd/api/events;events";
option (containerd.types.fieldpath_all) = true;
message SnapshotPrepare {
string key = 1;
string parent = 2;
string snapshotter = 5;
}
message SnapshotCommit {
string key = 1;
string name = 2;
string snapshotter = 5;
}
message SnapshotRemove {
string key = 1;
string snapshotter = 5;
}

View File

@ -0,0 +1,93 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.events;
import "google/protobuf/timestamp.proto";
import "github.com/containerd/containerd/api/types/mount.proto";
import "github.com/containerd/containerd/api/types/fieldpath.proto";
option go_package = "github.com/containerd/containerd/api/events;events";
option (containerd.types.fieldpath_all) = true;
message TaskCreate {
string container_id = 1;
string bundle = 2;
repeated containerd.types.Mount rootfs = 3;
TaskIO io = 4;
string checkpoint = 5;
uint32 pid = 6;
}
message TaskStart {
string container_id = 1;
uint32 pid = 2;
}
message TaskDelete {
string container_id = 1;
uint32 pid = 2;
uint32 exit_status = 3;
google.protobuf.Timestamp exited_at = 4;
// id is the specific exec. By default if omitted will be `""` thus matches
// the init exec of the task matching `container_id`.
string id = 5;
}
message TaskIO {
string stdin = 1;
string stdout = 2;
string stderr = 3;
bool terminal = 4;
}
message TaskExit {
string container_id = 1;
string id = 2;
uint32 pid = 3;
uint32 exit_status = 4;
google.protobuf.Timestamp exited_at = 5;
}
message TaskOOM {
string container_id = 1;
}
message TaskExecAdded {
string container_id = 1;
string exec_id = 2;
}
message TaskExecStarted {
string container_id = 1;
string exec_id = 2;
uint32 pid = 3;
}
message TaskPaused {
string container_id = 1;
}
message TaskResumed {
string container_id = 1;
}
message TaskCheckpointed {
string container_id = 1;
string checkpoint = 2;
}

View File

@ -0,0 +1,181 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.containers.v1;
import "google/protobuf/any.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/containerd/containerd/api/services/containers/v1;containers";
// Containers provides metadata storage for containers used in the execution
// service.
//
// The objects here provide an state-independent view of containers for use in
// management and resource pinning. From that perspective, containers do not
// have a "state" but rather this is the set of resources that will be
// considered in use by the container.
//
// From the perspective of the execution service, these objects represent the
// base parameters for creating a container process.
//
// In general, when looking to add fields for this type, first ask yourself
// whether or not the function of the field has to do with runtime execution or
// is invariant of the runtime state of the container. If it has to do with
// runtime, or changes as the "container" is started and stops, it probably
// doesn't belong on this object.
service Containers {
rpc Get(GetContainerRequest) returns (GetContainerResponse);
rpc List(ListContainersRequest) returns (ListContainersResponse);
rpc ListStream(ListContainersRequest) returns (stream ListContainerMessage);
rpc Create(CreateContainerRequest) returns (CreateContainerResponse);
rpc Update(UpdateContainerRequest) returns (UpdateContainerResponse);
rpc Delete(DeleteContainerRequest) returns (google.protobuf.Empty);
}
message Container {
// ID is the user-specified identifier.
//
// This field may not be updated.
string id = 1;
// Labels provides an area to include arbitrary data on containers.
//
// The combined size of a key/value pair cannot exceed 4096 bytes.
//
// Note that to add a new value to this field, read the existing set and
// include the entire result in the update call.
map<string, string> labels = 2;
// Image contains the reference of the image used to build the
// specification and snapshots for running this container.
//
// If this field is updated, the spec and rootfs needed to updated, as well.
string image = 3;
message Runtime {
// Name is the name of the runtime.
string name = 1;
// Options specify additional runtime initialization options.
google.protobuf.Any options = 2;
}
// Runtime specifies which runtime to use for executing this container.
Runtime runtime = 4;
// Spec to be used when creating the container. This is runtime specific.
google.protobuf.Any spec = 5;
// Snapshotter specifies the snapshotter name used for rootfs
string snapshotter = 6;
// SnapshotKey specifies the snapshot key to use for the container's root
// filesystem. When starting a task from this container, a caller should
// look up the mounts from the snapshot service and include those on the
// task create request.
//
// Snapshots referenced in this field will not be garbage collected.
//
// This field is set to empty when the rootfs is not a snapshot.
//
// This field may be updated.
string snapshot_key = 7;
// CreatedAt is the time the container was first created.
google.protobuf.Timestamp created_at = 8;
// UpdatedAt is the last time the container was mutated.
google.protobuf.Timestamp updated_at = 9;
// Extensions allow clients to provide zero or more blobs that are directly
// associated with the container. One may provide protobuf, json, or other
// encoding formats. The primary use of this is to further decorate the
// container object with fields that may be specific to a client integration.
//
// The key portion of this map should identify a "name" for the extension
// that should be unique against other extensions. When updating extension
// data, one should only update the specified extension using field paths
// to select a specific map key.
map<string, google.protobuf.Any> extensions = 10;
// Sandbox ID this container belongs to.
string sandbox = 11;
}
message GetContainerRequest {
string id = 1;
}
message GetContainerResponse {
Container container = 1;
}
message ListContainersRequest {
// Filters contains one or more filters using the syntax defined in the
// containerd filter package.
//
// The returned result will be those that match any of the provided
// filters. Expanded, containers that match the following will be
// returned:
//
// filters[0] or filters[1] or ... or filters[n-1] or filters[n]
//
// If filters is zero-length or nil, all items will be returned.
repeated string filters = 1;
}
message ListContainersResponse {
repeated Container containers = 1;
}
message CreateContainerRequest {
Container container = 1;
}
message CreateContainerResponse {
Container container = 1;
}
// UpdateContainerRequest updates the metadata on one or more container.
//
// The operation should follow semantics described in
// https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/field-mask,
// unless otherwise qualified.
message UpdateContainerRequest {
// Container provides the target values, as declared by the mask, for the update.
//
// The ID field must be set.
Container container = 1;
// UpdateMask specifies which fields to perform the update on. If empty,
// the operation applies to all fields.
google.protobuf.FieldMask update_mask = 2;
}
message UpdateContainerResponse {
Container container = 1;
}
message DeleteContainerRequest {
string id = 1;
}
message ListContainerMessage {
Container container = 1;
}

View File

@ -0,0 +1,330 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.content.v1;
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
option go_package = "github.com/containerd/containerd/api/services/content/v1;content";
// Content provides access to a content addressable storage system.
service Content {
// Info returns information about a committed object.
//
// This call can be used for getting the size of content and checking for
// existence.
rpc Info(InfoRequest) returns (InfoResponse);
// Update updates content metadata.
//
// This call can be used to manage the mutable content labels. The
// immutable metadata such as digest, size, and committed at cannot
// be updated.
rpc Update(UpdateRequest) returns (UpdateResponse);
// List streams the entire set of content as Info objects and closes the
// stream.
//
// Typically, this will yield a large response, chunked into messages.
// Clients should make provisions to ensure they can handle the entire data
// set.
rpc List(ListContentRequest) returns (stream ListContentResponse);
// Delete will delete the referenced object.
rpc Delete(DeleteContentRequest) returns (google.protobuf.Empty);
// Read allows one to read an object based on the offset into the content.
//
// The requested data may be returned in one or more messages.
rpc Read(ReadContentRequest) returns (stream ReadContentResponse);
// Status returns the status for a single reference.
rpc Status(StatusRequest) returns (StatusResponse);
// ListStatuses returns the status of ongoing object ingestions, started via
// Write.
//
// Only those matching the regular expression will be provided in the
// response. If the provided regular expression is empty, all ingestions
// will be provided.
rpc ListStatuses(ListStatusesRequest) returns (ListStatusesResponse);
// Write begins or resumes writes to a resource identified by a unique ref.
// Only one active stream may exist at a time for each ref.
//
// Once a write stream has started, it may only write to a single ref, thus
// once a stream is started, the ref may be omitted on subsequent writes.
//
// For any write transaction represented by a ref, only a single write may
// be made to a given offset. If overlapping writes occur, it is an error.
// Writes should be sequential and implementations may throw an error if
// this is required.
//
// If expected_digest is set and already part of the content store, the
// write will fail.
//
// When completed, the commit flag should be set to true. If expected size
// or digest is set, the content will be validated against those values.
rpc Write(stream WriteContentRequest) returns (stream WriteContentResponse);
// Abort cancels the ongoing write named in the request. Any resources
// associated with the write will be collected.
rpc Abort(AbortRequest) returns (google.protobuf.Empty);
}
message Info {
// Digest is the hash identity of the blob.
string digest = 1;
// Size is the total number of bytes in the blob.
int64 size = 2;
// CreatedAt provides the time at which the blob was committed.
google.protobuf.Timestamp created_at = 3;
// UpdatedAt provides the time the info was last updated.
google.protobuf.Timestamp updated_at = 4;
// Labels are arbitrary data on snapshots.
//
// The combined size of a key/value pair cannot exceed 4096 bytes.
map<string, string> labels = 5;
}
message InfoRequest {
string digest = 1;
}
message InfoResponse {
Info info = 1;
}
message UpdateRequest {
Info info = 1;
// UpdateMask specifies which fields to perform the update on. If empty,
// the operation applies to all fields.
//
// In info, Digest, Size, and CreatedAt are immutable,
// other field may be updated using this mask.
// If no mask is provided, all mutable field are updated.
google.protobuf.FieldMask update_mask = 2;
}
message UpdateResponse {
Info info = 1;
}
message ListContentRequest {
// Filters contains one or more filters using the syntax defined in the
// containerd filter package.
//
// The returned result will be those that match any of the provided
// filters. Expanded, containers that match the following will be
// returned:
//
// filters[0] or filters[1] or ... or filters[n-1] or filters[n]
//
// If filters is zero-length or nil, all items will be returned.
repeated string filters = 1;
}
message ListContentResponse {
repeated Info info = 1;
}
message DeleteContentRequest {
// Digest specifies which content to delete.
string digest = 1;
}
// ReadContentRequest defines the fields that make up a request to read a portion of
// data from a stored object.
message ReadContentRequest {
// Digest is the hash identity to read.
string digest = 1;
// Offset specifies the number of bytes from the start at which to begin
// the read. If zero or less, the read will be from the start. This uses
// standard zero-indexed semantics.
int64 offset = 2;
// size is the total size of the read. If zero, the entire blob will be
// returned by the service.
int64 size = 3;
}
// ReadContentResponse carries byte data for a read request.
message ReadContentResponse {
int64 offset = 1; // offset of the returned data
bytes data = 2; // actual data
}
message Status {
google.protobuf.Timestamp started_at = 1;
google.protobuf.Timestamp updated_at = 2;
string ref = 3;
int64 offset = 4;
int64 total = 5;
string expected = 6;
}
message StatusRequest {
string ref = 1;
}
message StatusResponse {
Status status = 1;
}
message ListStatusesRequest {
repeated string filters = 1;
}
message ListStatusesResponse {
repeated Status statuses = 1;
}
// WriteAction defines the behavior of a WriteRequest.
enum WriteAction {
// WriteActionStat instructs the writer to return the current status while
// holding the lock on the write.
STAT = 0;
// WriteActionWrite sets the action for the write request to write data.
//
// Any data included will be written at the provided offset. The
// transaction will be left open for further writes.
//
// This is the default.
WRITE = 1;
// WriteActionCommit will write any outstanding data in the message and
// commit the write, storing it under the digest.
//
// This can be used in a single message to send the data, verify it and
// commit it.
//
// This action will always terminate the write.
COMMIT = 2;
}
// WriteContentRequest writes data to the request ref at offset.
message WriteContentRequest {
// Action sets the behavior of the write.
//
// When this is a write and the ref is not yet allocated, the ref will be
// allocated and the data will be written at offset.
//
// If the action is write and the ref is allocated, it will accept data to
// an offset that has not yet been written.
//
// If the action is write and there is no data, the current write status
// will be returned. This works differently from status because the stream
// holds a lock.
WriteAction action = 1;
// Ref identifies the pre-commit object to write to.
string ref = 2;
// Total can be set to have the service validate the total size of the
// committed content.
//
// The latest value before or with the commit action message will be use to
// validate the content. If the offset overflows total, the service may
// report an error. It is only required on one message for the write.
//
// If the value is zero or less, no validation of the final content will be
// performed.
int64 total = 3;
// Expected can be set to have the service validate the final content against
// the provided digest.
//
// If the digest is already present in the object store, an AlreadyExists
// error will be returned.
//
// Only the latest version will be used to check the content against the
// digest. It is only required to include it on a single message, before or
// with the commit action message.
string expected = 4;
// Offset specifies the number of bytes from the start at which to begin
// the write. For most implementations, this means from the start of the
// file. This uses standard, zero-indexed semantics.
//
// If the action is write, the remote may remove all previously written
// data after the offset. Implementations may support arbitrary offsets but
// MUST support reseting this value to zero with a write. If an
// implementation does not support a write at a particular offset, an
// OutOfRange error must be returned.
int64 offset = 5;
// Data is the actual bytes to be written.
//
// If this is empty and the message is not a commit, a response will be
// returned with the current write state.
bytes data = 6;
// Labels are arbitrary data on snapshots.
//
// The combined size of a key/value pair cannot exceed 4096 bytes.
map<string, string> labels = 7;
}
// WriteContentResponse is returned on the culmination of a write call.
message WriteContentResponse {
// Action contains the action for the final message of the stream. A writer
// should confirm that they match the intended result.
WriteAction action = 1;
// StartedAt provides the time at which the write began.
//
// This must be set for stat and commit write actions. All other write
// actions may omit this.
google.protobuf.Timestamp started_at = 2;
// UpdatedAt provides the last time of a successful write.
//
// This must be set for stat and commit write actions. All other write
// actions may omit this.
google.protobuf.Timestamp updated_at = 3;
// Offset is the current committed size for the write.
int64 offset = 4;
// Total provides the current, expected total size of the write.
//
// We include this to provide consistency with the Status structure on the
// client writer.
//
// This is only valid on the Stat and Commit response.
int64 total = 5;
// Digest, if present, includes the digest up to the currently committed
// bytes. If action is commit, this field will be set. It is implementation
// defined if this is set for other actions.
string digest = 6;
}
message AbortRequest {
string ref = 1;
}

View File

@ -0,0 +1,90 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.diff.v1;
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";
import "github.com/containerd/containerd/api/types/mount.proto";
import "github.com/containerd/containerd/api/types/descriptor.proto";
option go_package = "github.com/containerd/containerd/api/services/diff/v1;diff";
// Diff service creates and applies diffs
service Diff {
// Apply applies the content associated with the provided digests onto
// the provided mounts. Archive content will be extracted and
// decompressed if necessary.
rpc Apply(ApplyRequest) returns (ApplyResponse);
// Diff creates a diff between the given mounts and uploads the result
// to the content store.
rpc Diff(DiffRequest) returns (DiffResponse);
}
message ApplyRequest {
// Diff is the descriptor of the diff to be extracted
containerd.types.Descriptor diff = 1;
repeated containerd.types.Mount mounts = 2;
map<string, google.protobuf.Any> payloads = 3;
// SyncFs is to synchronize the underlying filesystem containing files.
bool sync_fs = 4;
}
message ApplyResponse {
// Applied is the descriptor for the object which was applied.
// If the input was a compressed blob then the result will be
// the descriptor for the uncompressed blob.
containerd.types.Descriptor applied = 1;
}
message DiffRequest {
// Left are the mounts which represent the older copy
// in which is the base of the computed changes.
repeated containerd.types.Mount left = 1;
// Right are the mounts which represents the newer copy
// in which changes from the left were made into.
repeated containerd.types.Mount right = 2;
// MediaType is the media type descriptor for the created diff
// object
string media_type = 3;
// Ref identifies the pre-commit content store object. This
// reference can be used to get the status from the content store.
string ref = 4;
// Labels are the labels to apply to the generated content
// on content store commit.
map<string, string> labels = 5;
// SourceDateEpoch specifies the timestamp used to provide control for reproducibility.
// See also https://reproducible-builds.org/docs/source-date-epoch/ .
//
// Since containerd v2.0, the whiteout timestamps are set to zero (1970-01-01),
// not to the source date epoch.
google.protobuf.Timestamp source_date_epoch = 6;
}
message DiffResponse {
// Diff is the descriptor of the diff which can be applied
containerd.types.Descriptor diff = 3;
}

View File

@ -0,0 +1,62 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.events.v1;
import "github.com/containerd/containerd/api/types/event.proto";
import "google/protobuf/any.proto";
import "google/protobuf/empty.proto";
option go_package = "github.com/containerd/containerd/api/services/events/v1;events";
service Events {
// Publish an event to a topic.
//
// The event will be packed into a timestamp envelope with the namespace
// introspected from the context. The envelope will then be dispatched.
rpc Publish(PublishRequest) returns (google.protobuf.Empty);
// Forward sends an event that has already been packaged into an envelope
// with a timestamp and namespace.
//
// This is useful if earlier timestamping is required or when forwarding on
// behalf of another component, namespace or publisher.
rpc Forward(ForwardRequest) returns (google.protobuf.Empty);
// Subscribe to a stream of events, possibly returning only that match any
// of the provided filters.
//
// Unlike many other methods in containerd, subscribers will get messages
// from all namespaces unless otherwise specified. If this is not desired,
// a filter can be provided in the format 'namespace==<namespace>' to
// restrict the received events.
rpc Subscribe(SubscribeRequest) returns (stream containerd.types.Envelope);
}
message PublishRequest {
string topic = 1;
google.protobuf.Any event = 2;
}
message ForwardRequest {
containerd.types.Envelope envelope = 1;
}
message SubscribeRequest {
repeated string filters = 1;
}

View File

@ -0,0 +1,149 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.images.v1;
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";
import "github.com/containerd/containerd/api/types/descriptor.proto";
option go_package = "github.com/containerd/containerd/api/services/images/v1;images";
// Images is a service that allows one to register images with containerd.
//
// In containerd, an image is merely the mapping of a name to a content root,
// described by a descriptor. The behavior and state of image is purely
// dictated by the type of the descriptor.
//
// From the perspective of this service, these references are mostly shallow,
// in that the existence of the required content won't be validated until
// required by consuming services.
//
// As such, this can really be considered a "metadata service".
service Images {
// Get returns an image by name.
rpc Get(GetImageRequest) returns (GetImageResponse);
// List returns a list of all images known to containerd.
rpc List(ListImagesRequest) returns (ListImagesResponse);
// Create an image record in the metadata store.
//
// The name of the image must be unique.
rpc Create(CreateImageRequest) returns (CreateImageResponse);
// Update assigns the name to a given target image based on the provided
// image.
rpc Update(UpdateImageRequest) returns (UpdateImageResponse);
// Delete deletes the image by name.
rpc Delete(DeleteImageRequest) returns (google.protobuf.Empty);
}
message Image {
// Name provides a unique name for the image.
//
// Containerd treats this as the primary identifier.
string name = 1;
// Labels provides free form labels for the image. These are runtime only
// and do not get inherited into the package image in any way.
//
// Labels may be updated using the field mask.
// The combined size of a key/value pair cannot exceed 4096 bytes.
map<string, string> labels = 2;
// Target describes the content entry point of the image.
containerd.types.Descriptor target = 3;
// CreatedAt is the time the image was first created.
google.protobuf.Timestamp created_at = 7;
// UpdatedAt is the last time the image was mutated.
google.protobuf.Timestamp updated_at = 8;
}
message GetImageRequest {
string name = 1;
}
message GetImageResponse {
Image image = 1;
}
message CreateImageRequest {
Image image = 1;
google.protobuf.Timestamp source_date_epoch = 2;
}
message CreateImageResponse {
Image image = 1;
}
message UpdateImageRequest {
// Image provides a full or partial image for update.
//
// The name field must be set or an error will be returned.
Image image = 1;
// UpdateMask specifies which fields to perform the update on. If empty,
// the operation applies to all fields.
google.protobuf.FieldMask update_mask = 2;
google.protobuf.Timestamp source_date_epoch = 3;
}
message UpdateImageResponse {
Image image = 1;
}
message ListImagesRequest {
// Filters contains one or more filters using the syntax defined in the
// containerd filter package.
//
// The returned result will be those that match any of the provided
// filters. Expanded, images that match the following will be
// returned:
//
// filters[0] or filters[1] or ... or filters[n-1] or filters[n]
//
// If filters is zero-length or nil, all items will be returned.
repeated string filters = 1;
}
message ListImagesResponse {
repeated Image images = 1;
}
message DeleteImageRequest {
string name = 1;
// Sync indicates that the delete and cleanup should be done
// synchronously before returning to the caller
//
// Default is false
bool sync = 2;
// Target value for image to be deleted
//
// If image descriptor does not match the same digest,
// the delete operation will return "not found" error.
optional containerd.types.Descriptor target = 3;
}

View File

@ -0,0 +1,133 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.introspection.v1;
import "google/protobuf/any.proto";
import "github.com/containerd/containerd/api/types/introspection.proto";
import "github.com/containerd/containerd/api/types/platform.proto";
import "google/rpc/status.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/containerd/containerd/api/services/introspection/v1;introspection";
service Introspection {
// Plugins returns a list of plugins in containerd.
//
// Clients can use this to detect features and capabilities when using
// containerd.
rpc Plugins(PluginsRequest) returns (PluginsResponse);
// Server returns information about the containerd server
rpc Server(google.protobuf.Empty) returns (ServerResponse);
// PluginInfo returns information directly from a plugin if the plugin supports it
rpc PluginInfo(PluginInfoRequest) returns (PluginInfoResponse);
}
message Plugin {
// Type defines the type of plugin.
//
// See package plugin for a list of possible values. Non core plugins may
// define their own values during registration.
string type = 1;
// ID identifies the plugin uniquely in the system.
string id = 2;
// Requires lists the plugin types required by this plugin.
repeated string requires = 3;
// Platforms enumerates the platforms this plugin will support.
//
// If values are provided here, the plugin will only be operable under the
// provided platforms.
//
// If this is empty, the plugin will work across all platforms.
//
// If the plugin prefers certain platforms over others, they should be
// listed from most to least preferred.
repeated types.Platform platforms = 4;
// Exports allows plugins to provide values about state or configuration to
// interested parties.
//
// One example is exposing the configured path of a snapshotter plugin.
map<string, string> exports = 5;
// Capabilities allows plugins to communicate feature switches to allow
// clients to detect features that may not be on be default or may be
// different from version to version.
//
// Use this sparingly.
repeated string capabilities = 6;
// InitErr will be set if the plugin fails initialization.
//
// This means the plugin may have been registered but a non-terminal error
// was encountered during initialization.
//
// Plugins that have this value set cannot be used.
google.rpc.Status init_err = 7;
}
message PluginsRequest {
// Filters contains one or more filters using the syntax defined in the
// containerd filter package.
//
// The returned result will be those that match any of the provided
// filters. Expanded, plugins that match the following will be
// returned:
//
// filters[0] or filters[1] or ... or filters[n-1] or filters[n]
//
// If filters is zero-length or nil, all items will be returned.
repeated string filters = 1;
}
message PluginsResponse {
repeated Plugin plugins = 1;
}
message ServerResponse {
string uuid = 1;
uint64 pid = 2;
uint64 pidns = 3; // PID namespace, such as 4026531836
repeated DeprecationWarning deprecations = 4;
}
message DeprecationWarning {
string id = 1;
string message = 2;
google.protobuf.Timestamp last_occurrence = 3;
}
message PluginInfoRequest {
string type = 1;
string id = 2;
// Options may be used to request extra dynamic information from
// a plugin.
// This object is determined by the plugin and the plugin may return
// NotImplemented or InvalidArgument if it is not supported
google.protobuf.Any options = 3;
}
message PluginInfoResponse {
Plugin plugin = 1;
google.protobuf.Any extra = 2;
}

View File

@ -0,0 +1,116 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.leases.v1;
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/containerd/containerd/api/services/leases/v1;leases";
// Leases service manages resources leases within the metadata store.
service Leases {
// Create creates a new lease for managing changes to metadata. A lease
// can be used to protect objects from being removed.
rpc Create(CreateRequest) returns (CreateResponse);
// Delete deletes the lease and makes any unreferenced objects created
// during the lease eligible for garbage collection if not referenced
// or retained by other resources during the lease.
rpc Delete(DeleteRequest) returns (google.protobuf.Empty);
// List lists all active leases, returning the full list of
// leases and optionally including the referenced resources.
rpc List(ListRequest) returns (ListResponse);
// AddResource references the resource by the provided lease.
rpc AddResource(AddResourceRequest) returns (google.protobuf.Empty);
// DeleteResource dereferences the resource by the provided lease.
rpc DeleteResource(DeleteResourceRequest) returns (google.protobuf.Empty);
// ListResources lists all the resources referenced by the lease.
rpc ListResources(ListResourcesRequest) returns (ListResourcesResponse);
}
// Lease is an object which retains resources while it exists.
message Lease {
string id = 1;
google.protobuf.Timestamp created_at = 2;
map<string, string> labels = 3;
}
message CreateRequest {
// ID is used to identity the lease, when the id is not set the service
// generates a random identifier for the lease.
string id = 1;
map<string, string> labels = 3;
}
message CreateResponse {
Lease lease = 1;
}
message DeleteRequest {
string id = 1;
// Sync indicates that the delete and cleanup should be done
// synchronously before returning to the caller
//
// Default is false
bool sync = 2;
}
message ListRequest {
repeated string filters = 1;
}
message ListResponse {
repeated Lease leases = 1;
}
message Resource {
string id = 1;
// For snapshotter resource, there are many snapshotter types here, like
// overlayfs, devmapper etc. The type will be formatted with type,
// like "snapshotter/overlayfs".
string type = 2;
}
message AddResourceRequest {
string id = 1;
Resource resource = 2;
}
message DeleteResourceRequest {
string id = 1;
Resource resource = 2;
}
message ListResourcesRequest {
string id = 1;
}
message ListResourcesResponse {
repeated Resource resources = 1 ;
}

View File

@ -0,0 +1,107 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.namespaces.v1;
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
option go_package = "github.com/containerd/containerd/api/services/namespaces/v1;namespaces";
// Namespaces provides the ability to manipulate containerd namespaces.
//
// All objects in the system are required to be a member of a namespace. If a
// namespace is deleted, all objects, including containers, images and
// snapshots, will be deleted, as well.
//
// Unless otherwise noted, operations in containerd apply only to the namespace
// supplied per request.
//
// I hope this goes without saying, but namespaces are themselves NOT
// namespaced.
service Namespaces {
rpc Get(GetNamespaceRequest) returns (GetNamespaceResponse);
rpc List(ListNamespacesRequest) returns (ListNamespacesResponse);
rpc Create(CreateNamespaceRequest) returns (CreateNamespaceResponse);
rpc Update(UpdateNamespaceRequest) returns (UpdateNamespaceResponse);
rpc Delete(DeleteNamespaceRequest) returns (google.protobuf.Empty);
}
message Namespace {
string name = 1;
// Labels provides an area to include arbitrary data on namespaces.
//
// The combined size of a key/value pair cannot exceed 4096 bytes.
//
// Note that to add a new value to this field, read the existing set and
// include the entire result in the update call.
map<string, string> labels = 2;
}
message GetNamespaceRequest {
string name = 1;
}
message GetNamespaceResponse {
Namespace namespace = 1;
}
message ListNamespacesRequest {
string filter = 1;
}
message ListNamespacesResponse {
repeated Namespace namespaces = 1;
}
message CreateNamespaceRequest {
Namespace namespace = 1;
}
message CreateNamespaceResponse {
Namespace namespace = 1;
}
// UpdateNamespaceRequest updates the metadata for a namespace.
//
// The operation should follow semantics described in
// https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/field-mask,
// unless otherwise qualified.
message UpdateNamespaceRequest {
// Namespace provides the target value, as declared by the mask, for the update.
//
// The namespace field must be set.
Namespace namespace = 1;
// UpdateMask specifies which fields to perform the update on. If empty,
// the operation applies to all fields.
//
// For the most part, this applies only to selectively updating labels on
// the namespace. While field masks are typically limited to ascii alphas
// and digits, we just take everything after the "labels." as the map key.
google.protobuf.FieldMask update_mask = 2;
}
message UpdateNamespaceResponse {
Namespace namespace = 1;
}
message DeleteNamespaceRequest {
string name = 1;
}

View File

@ -0,0 +1,204 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
// Sandbox is a v2 runtime extension that allows more complex execution environments for containers.
// This adds a notion of groups of containers that share same lifecycle and/or resources.
// A few good fits for sandbox can be:
// - A "pause" container in k8s, that acts as a parent process for child containers to hold network namespace.
// - (micro)VMs that launch a VM process and executes containers inside guest OS.
// containerd in this case remains implementation agnostic and delegates sandbox handling to runtimes.
// See proposal and discussion here: https://github.com/containerd/containerd/issues/4131
package containerd.services.sandbox.v1;
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";
import "github.com/containerd/containerd/api/types/sandbox.proto";
import "github.com/containerd/containerd/api/types/mount.proto";
import "github.com/containerd/containerd/api/types/platform.proto";
import "github.com/containerd/containerd/api/types/metrics.proto";
option go_package = "github.com/containerd/containerd/api/services/sandbox/v1;sandbox";
// Store provides a metadata storage interface for sandboxes. Similarly to `Containers`,
// sandbox object includes info required to start a new instance, but no runtime state.
// When running a new sandbox instance, store objects are used as base type to create from.
service Store {
rpc Create(StoreCreateRequest) returns (StoreCreateResponse);
rpc Update(StoreUpdateRequest) returns (StoreUpdateResponse);
rpc Delete(StoreDeleteRequest) returns (StoreDeleteResponse);
rpc List(StoreListRequest) returns (StoreListResponse);
rpc Get(StoreGetRequest) returns (StoreGetResponse);
}
message StoreCreateRequest {
containerd.types.Sandbox sandbox = 1;
}
message StoreCreateResponse {
containerd.types.Sandbox sandbox = 1;
}
message StoreUpdateRequest {
containerd.types.Sandbox sandbox = 1;
repeated string fields = 2;
}
message StoreUpdateResponse {
containerd.types.Sandbox sandbox = 1;
}
message StoreDeleteRequest {
string sandbox_id = 1;
}
message StoreDeleteResponse {}
message StoreListRequest {
repeated string filters = 1;
}
message StoreListResponse {
repeated containerd.types.Sandbox list = 1;
}
message StoreGetRequest {
string sandbox_id = 1;
}
message StoreGetResponse {
containerd.types.Sandbox sandbox = 1;
}
// Controller is an interface to manage runtime sandbox instances.
service Controller {
rpc Create(ControllerCreateRequest) returns (ControllerCreateResponse);
rpc Start(ControllerStartRequest) returns (ControllerStartResponse);
rpc Platform(ControllerPlatformRequest) returns (ControllerPlatformResponse);
rpc Stop(ControllerStopRequest) returns (ControllerStopResponse);
rpc Wait(ControllerWaitRequest) returns (ControllerWaitResponse);
rpc Status(ControllerStatusRequest) returns (ControllerStatusResponse);
rpc Shutdown(ControllerShutdownRequest) returns (ControllerShutdownResponse);
rpc Metrics(ControllerMetricsRequest) returns (ControllerMetricsResponse);
rpc Update(ControllerUpdateRequest) returns (ControllerUpdateResponse);
}
message ControllerCreateRequest {
string sandbox_id = 1;
repeated containerd.types.Mount rootfs = 2;
google.protobuf.Any options = 3;
string netns_path = 4;
map<string, string> annotations = 5;
containerd.types.Sandbox sandbox = 6;
string sandboxer = 10;
}
message ControllerCreateResponse {
string sandbox_id = 1;
}
message ControllerStartRequest {
string sandbox_id = 1;
string sandboxer = 10;
}
message ControllerStartResponse {
string sandbox_id = 1;
uint32 pid = 2;
google.protobuf.Timestamp created_at = 3;
map<string, string> labels = 4;
// Address of the sandbox for containerd to connect,
// for calling Task or other APIs serving in the sandbox.
// it is in the form of ttrpc+unix://path/to/uds or grpc+vsock://<vsock cid>:<port>.
string address = 5;
uint32 version = 6;
}
message ControllerPlatformRequest {
string sandbox_id = 1;
string sandboxer = 10;
}
message ControllerPlatformResponse {
containerd.types.Platform platform = 1;
}
message ControllerStopRequest {
string sandbox_id = 1;
uint32 timeout_secs = 2;
string sandboxer = 10;
}
message ControllerStopResponse {}
message ControllerWaitRequest {
string sandbox_id = 1;
string sandboxer = 10;
}
message ControllerWaitResponse {
uint32 exit_status = 1;
google.protobuf.Timestamp exited_at = 2;
}
message ControllerStatusRequest {
string sandbox_id = 1;
bool verbose = 2;
string sandboxer = 10;
}
message ControllerStatusResponse {
string sandbox_id = 1;
uint32 pid = 2;
string state = 3;
map<string, string> info = 4;
google.protobuf.Timestamp created_at = 5;
google.protobuf.Timestamp exited_at = 6;
google.protobuf.Any extra = 7;
// Address of the sandbox for containerd to connect,
// for calling Task or other APIs serving in the sandbox.
// it is in the form of ttrpc+unix://path/to/uds or grpc+vsock://<vsock cid>:<port>.
string address = 8;
uint32 version = 9;
}
message ControllerShutdownRequest {
string sandbox_id = 1;
string sandboxer = 10;
}
message ControllerShutdownResponse {}
message ControllerMetricsRequest {
string sandbox_id = 1;
string sandboxer = 10;
}
message ControllerMetricsResponse {
types.Metric metrics = 1;
}
message ControllerUpdateRequest {
string sandbox_id = 1;
string sandboxer = 2;
containerd.types.Sandbox sandbox = 3;
repeated string fields = 4;
}
message ControllerUpdateResponse {
}

View File

@ -0,0 +1,179 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.snapshots.v1;
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";
import "github.com/containerd/containerd/api/types/mount.proto";
option go_package = "github.com/containerd/containerd/api/services/snapshots/v1;snapshots";
// Snapshot service manages snapshots
service Snapshots {
rpc Prepare(PrepareSnapshotRequest) returns (PrepareSnapshotResponse);
rpc View(ViewSnapshotRequest) returns (ViewSnapshotResponse);
rpc Mounts(MountsRequest) returns (MountsResponse);
rpc Commit(CommitSnapshotRequest) returns (google.protobuf.Empty);
rpc Remove(RemoveSnapshotRequest) returns (google.protobuf.Empty);
rpc Stat(StatSnapshotRequest) returns (StatSnapshotResponse);
rpc Update(UpdateSnapshotRequest) returns (UpdateSnapshotResponse);
rpc List(ListSnapshotsRequest) returns (stream ListSnapshotsResponse);
rpc Usage(UsageRequest) returns (UsageResponse);
rpc Cleanup(CleanupRequest) returns (google.protobuf.Empty);
}
message PrepareSnapshotRequest {
string snapshotter = 1;
string key = 2;
string parent = 3;
// Labels are arbitrary data on snapshots.
//
// The combined size of a key/value pair cannot exceed 4096 bytes.
map<string, string> labels = 4;
}
message PrepareSnapshotResponse {
repeated containerd.types.Mount mounts = 1;
}
message ViewSnapshotRequest {
string snapshotter = 1;
string key = 2;
string parent = 3;
// Labels are arbitrary data on snapshots.
//
// The combined size of a key/value pair cannot exceed 4096 bytes.
map<string, string> labels = 4;
}
message ViewSnapshotResponse {
repeated containerd.types.Mount mounts = 1;
}
message MountsRequest {
string snapshotter = 1;
string key = 2;
}
message MountsResponse {
repeated containerd.types.Mount mounts = 1;
}
message RemoveSnapshotRequest {
string snapshotter = 1;
string key = 2;
}
message CommitSnapshotRequest {
string snapshotter = 1;
string name = 2;
string key = 3;
// Labels are arbitrary data on snapshots.
//
// The combined size of a key/value pair cannot exceed 4096 bytes.
map<string, string> labels = 4;
}
message StatSnapshotRequest {
string snapshotter = 1;
string key = 2;
}
enum Kind {
UNKNOWN = 0;
VIEW = 1;
ACTIVE = 2;
COMMITTED = 3;
}
message Info {
string name = 1;
string parent = 2;
Kind kind = 3;
// CreatedAt provides the time at which the snapshot was created.
google.protobuf.Timestamp created_at = 4;
// UpdatedAt provides the time the info was last updated.
google.protobuf.Timestamp updated_at = 5;
// Labels are arbitrary data on snapshots.
//
// The combined size of a key/value pair cannot exceed 4096 bytes.
map<string, string> labels = 6;
}
message StatSnapshotResponse {
Info info = 1;
}
message UpdateSnapshotRequest {
string snapshotter = 1;
Info info = 2;
// UpdateMask specifies which fields to perform the update on. If empty,
// the operation applies to all fields.
//
// In info, Name, Parent, Kind, Created are immutable,
// other field may be updated using this mask.
// If no mask is provided, all mutable field are updated.
google.protobuf.FieldMask update_mask = 3;
}
message UpdateSnapshotResponse {
Info info = 1;
}
message ListSnapshotsRequest{
string snapshotter = 1;
// Filters contains one or more filters using the syntax defined in the
// containerd filter package.
//
// The returned result will be those that match any of the provided
// filters. Expanded, images that match the following will be
// returned:
//
// filters[0] or filters[1] or ... or filters[n-1] or filters[n]
//
// If filters is zero-length or nil, all items will be returned.
repeated string filters = 2;
}
message ListSnapshotsResponse {
repeated Info info = 1;
}
message UsageRequest {
string snapshotter = 1;
string key = 2;
}
message UsageResponse {
int64 size = 1;
int64 inodes = 2;
}
message CleanupRequest {
string snapshotter = 1;
}

View File

@ -0,0 +1,31 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.streaming.v1;
import "google/protobuf/any.proto";
option go_package = "github.com/containerd/containerd/api/services/streaming/v1;streaming";
service Streaming {
rpc Stream(stream google.protobuf.Any) returns (stream google.protobuf.Any);
}
message StreamInit {
string id = 1;
}

View File

@ -0,0 +1,227 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.tasks.v1;
import "google/protobuf/empty.proto";
import "google/protobuf/any.proto";
import "github.com/containerd/containerd/api/types/mount.proto";
import "github.com/containerd/containerd/api/types/metrics.proto";
import "github.com/containerd/containerd/api/types/descriptor.proto";
import "github.com/containerd/containerd/api/types/task/task.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/containerd/containerd/api/services/tasks/v1;tasks";
service Tasks {
// Create a task.
rpc Create(CreateTaskRequest) returns (CreateTaskResponse);
// Start a process.
rpc Start(StartRequest) returns (StartResponse);
// Delete a task and on disk state.
rpc Delete(DeleteTaskRequest) returns (DeleteResponse);
rpc DeleteProcess(DeleteProcessRequest) returns (DeleteResponse);
rpc Get(GetRequest) returns (GetResponse);
rpc List(ListTasksRequest) returns (ListTasksResponse);
// Kill a task or process.
rpc Kill(KillRequest) returns (google.protobuf.Empty);
rpc Exec(ExecProcessRequest) returns (google.protobuf.Empty);
rpc ResizePty(ResizePtyRequest) returns (google.protobuf.Empty);
rpc CloseIO(CloseIORequest) returns (google.protobuf.Empty);
rpc Pause(PauseTaskRequest) returns (google.protobuf.Empty);
rpc Resume(ResumeTaskRequest) returns (google.protobuf.Empty);
rpc ListPids(ListPidsRequest) returns (ListPidsResponse);
rpc Checkpoint(CheckpointTaskRequest) returns (CheckpointTaskResponse);
rpc Update(UpdateTaskRequest) returns (google.protobuf.Empty);
rpc Metrics(MetricsRequest) returns (MetricsResponse);
rpc Wait(WaitRequest) returns (WaitResponse);
}
message CreateTaskRequest {
string container_id = 1;
// RootFS provides the pre-chroot mounts to perform in the shim before
// executing the container task.
//
// These are for mounts that cannot be performed in the user namespace.
// Typically, these mounts should be resolved from snapshots specified on
// the container object.
repeated containerd.types.Mount rootfs = 3;
string stdin = 4;
string stdout = 5;
string stderr = 6;
bool terminal = 7;
containerd.types.Descriptor checkpoint = 8;
google.protobuf.Any options = 9;
string runtime_path = 10;
}
message CreateTaskResponse {
string container_id = 1;
uint32 pid = 2;
}
message StartRequest {
string container_id = 1;
string exec_id = 2;
}
message StartResponse {
uint32 pid = 1;
}
message DeleteTaskRequest {
string container_id = 1;
}
message DeleteResponse {
string id = 1;
uint32 pid = 2;
uint32 exit_status = 3;
google.protobuf.Timestamp exited_at = 4;
}
message DeleteProcessRequest {
string container_id = 1;
string exec_id = 2;
}
message GetRequest {
string container_id = 1;
string exec_id = 2;
}
message GetResponse {
containerd.v1.types.Process process = 1;
}
message ListTasksRequest {
string filter = 1;
}
message ListTasksResponse {
repeated containerd.v1.types.Process tasks = 1;
}
message KillRequest {
string container_id = 1;
string exec_id = 2;
uint32 signal = 3;
bool all = 4;
}
message ExecProcessRequest {
string container_id = 1;
string stdin = 2;
string stdout = 3;
string stderr = 4;
bool terminal = 5;
// Spec for starting a process in the target container.
//
// For runc, this is a process spec, for example.
google.protobuf.Any spec = 6;
// id of the exec process
string exec_id = 7;
}
message ExecProcessResponse {
}
message ResizePtyRequest {
string container_id = 1;
string exec_id = 2;
uint32 width = 3;
uint32 height = 4;
}
message CloseIORequest {
string container_id = 1;
string exec_id = 2;
bool stdin = 3;
}
message PauseTaskRequest {
string container_id = 1;
}
message ResumeTaskRequest {
string container_id = 1;
}
message ListPidsRequest {
string container_id = 1;
}
message ListPidsResponse {
// Processes includes the process ID and additional process information
repeated containerd.v1.types.ProcessInfo processes = 1;
}
message CheckpointTaskRequest {
string container_id = 1;
string parent_checkpoint = 2;
google.protobuf.Any options = 3;
}
message CheckpointTaskResponse {
repeated containerd.types.Descriptor descriptors = 1;
}
message UpdateTaskRequest {
string container_id = 1;
google.protobuf.Any resources = 2;
map<string, string> annotations = 3;
}
message MetricsRequest {
repeated string filters = 1;
}
message MetricsResponse {
repeated types.Metric metrics = 1;
}
message WaitRequest {
string container_id = 1;
string exec_id = 2;
}
message WaitResponse {
uint32 exit_status = 1;
google.protobuf.Timestamp exited_at = 2;
}

View File

@ -0,0 +1,39 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.transfer.v1;
import "google/protobuf/any.proto";
import "google/protobuf/empty.proto";
option go_package = "github.com/containerd/containerd/api/services/transfer/v1;transfer";
service Transfer {
rpc Transfer(TransferRequest) returns (google.protobuf.Empty);
}
message TransferRequest {
google.protobuf.Any source = 1;
google.protobuf.Any destination = 2;
TransferOptions options = 3;
}
message TransferOptions {
string progress_stream = 1;
// Progress min interval
}

View File

@ -0,0 +1,37 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.events.ttrpc.v1;
import "github.com/containerd/containerd/api/types/event.proto";
import "google/protobuf/empty.proto";
option go_package = "github.com/containerd/containerd/api/services/ttrpc/events/v1;events";
service Events {
// Forward sends an event that has already been packaged into an envelope
// with a timestamp and namespace.
//
// This is useful if earlier timestamping is required or when forwarding on
// behalf of another component, namespace or publisher.
rpc Forward(ForwardRequest) returns (google.protobuf.Empty);
}
message ForwardRequest {
containerd.types.Envelope envelope = 1;
}

View File

@ -0,0 +1,33 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.version.v1;
import "google/protobuf/empty.proto";
// TODO(stevvooe): Should version service actually be versioned?
option go_package = "github.com/containerd/containerd/api/services/version/v1;version";
service Version {
rpc Version(google.protobuf.Empty) returns (VersionResponse);
}
message VersionResponse {
string version = 1;
string revision = 2;
}

View File

@ -0,0 +1,33 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types;
option go_package = "github.com/containerd/containerd/api/types;types";
// Descriptor describes a blob in a content store.
//
// This descriptor can be used to reference content from an
// oci descriptor found in a manifest.
// See https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1#Descriptor
message Descriptor {
string media_type = 1;
string digest = 2;
int64 size = 3;
map<string, string> annotations = 5;
}

View File

@ -0,0 +1,33 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types;
import "github.com/containerd/containerd/api/types/fieldpath.proto";
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/containerd/containerd/api/types;types";
message Envelope {
option (containerd.types.fieldpath) = true;
google.protobuf.Timestamp timestamp = 1;
string namespace = 2;
string topic = 3;
google.protobuf.Any event = 4;
}

View File

@ -0,0 +1,42 @@
// Protocol Buffers for Go with Gadgets
//
// Copyright (c) 2013, The GoGo Authors. All rights reserved.
// http://github.com/gogo/protobuf
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
syntax = "proto3";
package containerd.types;
import "google/protobuf/descriptor.proto";
option go_package = "github.com/containerd/containerd/api/types;types";
extend google.protobuf.FileOptions {
optional bool fieldpath_all = 63300;
}
extend google.protobuf.MessageOptions {
optional bool fieldpath = 64400;
}

View File

@ -0,0 +1,46 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types;
import "google/protobuf/any.proto";
option go_package = "github.com/containerd/containerd/api/types;types";
message RuntimeRequest {
string runtime_path = 1;
// Options correspond to CreateTaskRequest.options.
// This is needed to pass the runc binary path, etc.
google.protobuf.Any options = 2;
}
message RuntimeVersion {
string version = 1;
string revision = 2;
}
message RuntimeInfo {
string name = 1;
RuntimeVersion version = 2;
// Options correspond to RuntimeInfoRequest.Options (contains runc binary path, etc.)
google.protobuf.Any options = 3;
// OCI-compatible runtimes should use https://github.com/opencontainers/runtime-spec/blob/main/features.md
google.protobuf.Any features = 4;
// Annotations of the shim. Irrelevant to features.Annotations.
map<string, string> annotations = 5;
}

View File

@ -0,0 +1,30 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types;
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/containerd/containerd/api/types;types";
message Metric {
google.protobuf.Timestamp timestamp = 1;
string id = 2;
google.protobuf.Any data = 3;
}

View File

@ -0,0 +1,43 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types;
option go_package = "github.com/containerd/containerd/api/types;types";
// Mount describes mounts for a container.
//
// This type is the lingua franca of ContainerD. All services provide mounts
// to be used with the container at creation time.
//
// The Mount type follows the structure of the mount syscall, including a type,
// source, target and options.
message Mount {
// Type defines the nature of the mount.
string type = 1;
// Source specifies the name of the mount. Depending on mount type, this
// may be a volume name or a host path, or even ignored.
string source = 2;
// Target path in container
string target = 3;
// Options specifies zero or more fstab style mount options.
repeated string options = 4;
}

View File

@ -0,0 +1,30 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types;
option go_package = "github.com/containerd/containerd/api/types;types";
// Platform follows the structure of the OCI platform specification, from
// descriptors.
message Platform {
string os = 1;
string architecture = 2;
string variant = 3;
string os_version = 4;
}

View File

@ -0,0 +1,63 @@
syntax = "proto3";
package containerd.runc.v1;
option go_package = "github.com/containerd/containerd/api/types/runc/options;options";
message Options {
// disable pivot root when creating a container
bool no_pivot_root = 1;
// create a new keyring for the container
bool no_new_keyring = 2;
// place the shim in a cgroup
string shim_cgroup = 3;
// set the I/O's pipes uid
uint32 io_uid = 4;
// set the I/O's pipes gid
uint32 io_gid = 5;
// binary name of the runc binary
string binary_name = 6;
// runc root directory
string root = 7;
// criu binary path.
//
// Removed in containerd v2.0: string criu_path = 8;
reserved 8;
// enable systemd cgroups
bool systemd_cgroup = 9;
// criu image path
string criu_image_path = 10;
// criu work path
string criu_work_path = 11;
// task api address, can be a unix domain socket, or vsock address.
// it is in the form of ttrpc+unix://path/to/uds or grpc+vsock://<vsock cid>:<port>.
string task_api_address = 12;
// task api version, currently supported value is 2 and 3.
uint32 task_api_version = 13;
}
message CheckpointOptions {
// exit the container after a checkpoint
bool exit = 1;
// checkpoint open tcp connections
bool open_tcp = 2;
// checkpoint external unix sockets
bool external_unix_sockets = 3;
// checkpoint terminals (ptys)
bool terminal = 4;
// allow checkpointing of file locks
bool file_locks = 5;
// restore provided namespaces as empty namespaces
repeated string empty_namespaces = 6;
// set the cgroups mode, soft, full, strict
string cgroups_mode = 7;
// checkpoint image path
string image_path = 8;
// checkpoint work path
string work_path = 9;
}
message ProcessDetails {
// exec process id if the process is managed by a shim
string exec_id = 1;
}

View File

@ -0,0 +1,17 @@
// To regenerate api.pb.go run `make protos`
syntax = "proto3";
package runtimeoptions.v1;
option go_package = "github.com/containerd/containerd/api/types/runtimeoptions/v1;runtimeoptions";
message Options {
// TypeUrl specifies the type of the content inside the config file.
string type_url = 1;
// ConfigPath specifies the filesystem location of the config file
// used by the runtime.
string config_path = 2;
// Blob specifies an in-memory TOML blob passed from containerd's configuration section
// for this runtime. This will be used if config_path is not specified.
bytes config_body = 3;
}

View File

@ -0,0 +1,54 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types;
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/containerd/containerd/api/types;types";
// Sandbox represents a sandbox metadata object that keeps all info required by controller to
// work with a particular instance.
message Sandbox {
// SandboxID is a unique instance identifier within namespace
string sandbox_id = 1;
message Runtime {
// Name is the name of the runtime.
string name = 1;
// Options specify additional runtime initialization options for the shim (this data will be available in StartShim).
// Typically this data expected to be runtime shim implementation specific.
google.protobuf.Any options = 2;
}
// Runtime specifies which runtime to use for executing this container.
Runtime runtime = 2;
// Spec is sandbox configuration (kin of OCI runtime spec), spec's data will be written to a config.json file in the
// bundle directory (similary to OCI spec).
google.protobuf.Any spec = 3;
// Labels provides an area to include arbitrary data on containers.
map<string, string> labels = 4;
// CreatedAt is the time the container was first created.
google.protobuf.Timestamp created_at = 5;
// UpdatedAt is the last time the container was mutated.
google.protobuf.Timestamp updated_at = 6;
// Extensions allow clients to provide optional blobs that can be handled by runtime.
map<string, google.protobuf.Any> extensions = 7;
// Sandboxer is the name of the sandbox controller who manages the sandbox.
string sandboxer = 10;
}

View File

@ -0,0 +1,55 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.v1.types;
import "google/protobuf/timestamp.proto";
import "google/protobuf/any.proto";
option go_package = "github.com/containerd/containerd/api/types/task";
enum Status {
UNKNOWN = 0;
CREATED = 1;
RUNNING = 2;
STOPPED = 3;
PAUSED = 4;
PAUSING = 5;
}
message Process {
string container_id = 1;
string id = 2;
uint32 pid = 3;
Status status = 4;
string stdin = 5;
string stdout = 6;
string stderr = 7;
bool terminal = 8;
uint32 exit_status = 9;
google.protobuf.Timestamp exited_at = 10;
}
message ProcessInfo {
// PID is the process ID.
uint32 pid = 1;
// Info contains additional process information.
//
// Info varies by platform.
google.protobuf.Any info = 2;
}

View File

@ -0,0 +1,82 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types.transfer;
import "github.com/containerd/containerd/api/types/platform.proto";
option go_package = "github.com/containerd/containerd/api/types/transfer";
message ImageStore {
string name = 1;
map<string, string> labels = 2;
// Content filters
repeated types.Platform platforms = 3;
bool all_metadata = 4;
uint32 manifest_limit = 5;
// Import naming
// extra_references are used to set image names on imports of sub-images from the index
repeated ImageReference extra_references = 6;
// Unpack Configuration, multiple allowed
repeated UnpackConfiguration unpacks = 10;
}
message UnpackConfiguration {
// platform is the platform to unpack for, used for resolving manifest and snapshotter
// if not provided
types.Platform platform = 1;
// snapshotter to unpack to, if not provided default for platform shoudl be used
string snapshotter = 2;
}
// ImageReference is used to create or find a reference for an image
message ImageReference {
string name = 1;
// is_prefix determines whether the Name should be considered
// a prefix (without tag or digest).
// For lookup, this may allow matching multiple tags.
// For store, this must have a tag or digest added.
bool is_prefix = 2;
// allow_overwrite allows overwriting or ignoring the name if
// another reference is provided (such as through an annotation).
// Only used if IsPrefix is true.
bool allow_overwrite = 3;
// add_digest adds the manifest digest to the reference.
// For lookup, this allows matching tags with any digest.
// For store, this allows adding the digest to the name.
// Only used if IsPrefix is true.
bool add_digest = 4;
// skip_named_digest only considers digest references which do not
// have a non-digested named reference.
// For lookup, this will deduplicate digest references when there is a named match.
// For store, this only adds this digest reference when there is no matching full
// name reference from the prefix.
// Only used if IsPrefix is true.
bool skip_named_digest = 5;
}

View File

@ -0,0 +1,52 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types.transfer;
option go_package = "github.com/containerd/containerd/api/types/transfer";
import "github.com/containerd/containerd/api/types/platform.proto";
message ImageImportStream {
// Stream is used to identify the binary input stream for the import operation.
// The stream uses the transfer binary stream protocol with the client as the sender.
// The binary data is expected to be a raw tar stream.
string stream = 1;
string media_type = 2;
bool force_compress = 3;
}
message ImageExportStream {
// Stream is used to identify the binary output stream for the export operation.
// The stream uses the transfer binary stream protocol with the server as the sender.
// The binary data is expected to be a raw tar stream.
string stream = 1;
string media_type = 2;
// The specified platforms
repeated types.Platform platforms = 3;
// Whether to include all platforms
bool all_platforms = 4;
// Skips the creation of the Docker compatible manifest.json file
bool skip_compatibility_manifest = 5;
// Excludes non-distributable blobs such as Windows base layers.
bool skip_non_distributable = 6;
}

View File

@ -0,0 +1,32 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types.transfer;
import "github.com/containerd/containerd/api/types/descriptor.proto";
option go_package = "github.com/containerd/containerd/api/types/transfer";
message Progress {
string event = 1;
string name = 2;
repeated string parents = 3;
int64 progress = 4;
int64 total = 5;
containerd.types.Descriptor desc = 6;
}

View File

@ -0,0 +1,97 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types.transfer;
import "google/protobuf/timestamp.proto";
option go_package = "github.com/containerd/containerd/api/types/transfer";
message OCIRegistry {
string reference = 1;
RegistryResolver resolver = 2;
}
enum HTTPDebug {
DISABLED = 0;
// Enable HTTP debugging
DEBUG = 1;
// Enable HTTP requests tracing
TRACE = 2;
// Enable both HTTP debugging and requests tracing
BOTH = 3;
}
message RegistryResolver {
// auth_stream is used to refer to a stream which auth callbacks may be
// made on.
string auth_stream = 1;
// Headers
map<string, string> headers = 2;
string host_dir = 3;
string default_scheme = 4;
// Force skip verify
// CA callback? Client TLS callback?
// Whether to debug/trace HTTP requests to OCI registry.
HTTPDebug http_debug = 5;
// Stream ID to use for HTTP logs (when logs are streamed to client).
// When empty, logs are written to containerd logs.
string logs_stream = 6;
}
// AuthRequest is sent as a callback on a stream
message AuthRequest {
// host is the registry host
string host = 1;
// reference is the namespace and repository name requested from the registry
string reference = 2;
// wwwauthenticate is the HTTP WWW-Authenticate header values returned from the registry
repeated string wwwauthenticate = 3;
}
enum AuthType {
NONE = 0;
// CREDENTIALS is used to exchange username/password for access token
// using an oauth or "Docker Registry Token" server
CREDENTIALS = 1;
// REFRESH is used to exchange secret for access token using an oauth
// or "Docker Registry Token" server
REFRESH = 2;
// HEADER is used to set the HTTP Authorization header to secret
// directly for the registry.
// Value should be `<auth-scheme> <authorization-parameters>`
HEADER = 3;
}
message AuthResponse {
AuthType authType = 1;
string secret = 2;
string username = 3;
google.protobuf.Timestamp expire_at = 4;
// TODO: Stream error
}

View File

@ -0,0 +1,29 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types.transfer;
option go_package = "github.com/containerd/containerd/api/types/transfer";
message Data {
bytes data = 1;
}
message WindowUpdate {
int32 update = 1;
}

View File

@ -25,18 +25,17 @@ path = "src/main.rs"
doc = false
[dependencies]
containerd-shim = { path = "../shim", version = "0.7.1", features = ["async"] }
containerd-shim = { path = "../shim", version = "0.8.0", features = ["async"] }
libc.workspace = true
log.workspace = true
nix = { workspace = true, features = ["socket", "uio", "term"] }
oci-spec.workspace = true
prctl.workspace = true
runc = { path = "../runc", version = "0.2.0", features = ["async"] }
runc = { path = "../runc", version = "0.3.0", features = ["async"] }
serde.workspace = true
serde_json.workspace = true
time.workspace = true
uuid.workspace = true
# Async dependencies
async-trait.workspace = true
tokio = { workspace = true, features = ["full"] }
@ -44,3 +43,4 @@ tokio = { workspace = true, features = ["full"] }
[target.'cfg(target_os = "linux")'.dependencies]
cgroups-rs.workspace = true
nix = { workspace = true, features = ["event"] }
tokio-eventfd = "0.2.1"

View File

@ -16,21 +16,18 @@
#![cfg(target_os = "linux")]
use std::{
os::unix::io::{AsRawFd, FromRawFd},
path::Path,
};
use std::{os::unix::io::AsRawFd, path::Path};
use containerd_shim::{
error::{Error, Result},
io_error, other_error,
};
use nix::sys::eventfd::{EfdFlags, EventFd};
use tokio::{
fs::{self, read_to_string, File},
fs::{self, read_to_string},
io::AsyncReadExt,
sync::mpsc::{self, Receiver},
};
use tokio_eventfd::EventFd;
pub async fn get_path_from_cgorup(pid: u32) -> Result<String> {
let proc_path = format!("/proc/{}/cgroup", pid);
@ -93,15 +90,13 @@ pub async fn register_memory_event(
let path = cg_dir.join(event_name);
let event_file = fs::File::open(path.clone())
.await
.map_err(other_error!(e, "Error get path:"))?;
let eventfd = EventFd::from_value_and_flags(0, EfdFlags::EFD_CLOEXEC)?;
.map_err(other_error!("Error get path:"))?;
let mut eventfd = EventFd::new(0, false).map_err(other_error!("Error create eventfd:"))?;
let event_control_path = cg_dir.join("cgroup.event_control");
let data = format!("{} {}", eventfd.as_raw_fd(), event_file.as_raw_fd());
fs::write(&event_control_path, data.clone())
.await
.map_err(other_error!(e, "Error write eventfd:"))?;
.map_err(other_error!("Error write eventfd:"))?;
let mut buf = [0u8; 8];
@ -109,9 +104,8 @@ pub async fn register_memory_event(
let key = key.to_string();
tokio::spawn(async move {
let mut eventfd_file = unsafe { File::from_raw_fd(eventfd.as_raw_fd()) };
loop {
match eventfd_file.read(&mut buf).await {
match eventfd.read(&mut buf).await {
Ok(0) => return,
Err(_) => return,
_ => (),

View File

@ -16,7 +16,7 @@
use std::{
env,
fs::File,
future::Future,
io::IoSliceMut,
ops::Deref,
os::{
@ -25,6 +25,7 @@ use std::{
},
path::Path,
sync::Arc,
time::Duration,
};
use containerd_shim::{
@ -59,6 +60,8 @@ pub const INIT_PID_FILE: &str = "init.pid";
pub const LOG_JSON_FILE: &str = "log.json";
pub const FIFO_SCHEME: &str = "fifo";
const TIMEOUT_DURATION: std::time::Duration = Duration::from_secs(3);
#[derive(Deserialize)]
pub struct Log {
pub level: String,
@ -70,8 +73,6 @@ pub struct ProcessIO {
pub uri: Option<String>,
pub io: Option<Arc<dyn Io>>,
pub copy: bool,
pub stdout_r: Option<File>,
pub stderr_r: Option<File>,
}
pub fn create_io(
@ -181,7 +182,7 @@ pub fn create_runc(
}
gopts
.build()
.map_err(other_error!(e, "unable to create runc instance"))
.map_err(other_error!("unable to create runc instance"))
}
#[derive(Default)]
@ -248,3 +249,17 @@ pub(crate) fn xdg_runtime_dir() -> String {
env::var("XDG_RUNTIME_DIR")
.unwrap_or_else(|_| env::temp_dir().to_str().unwrap_or(".").to_string())
}
pub async fn handle_file_open<F, Fut>(file_op: F) -> Result<tokio::fs::File, tokio::io::Error>
where
F: FnOnce() -> Fut,
Fut: Future<Output = Result<tokio::fs::File, tokio::io::Error>> + Send,
{
match tokio::time::timeout(TIMEOUT_DURATION, file_op()).await {
Ok(result) => result,
Err(_) => Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"File operation timed out",
)),
}
}

View File

@ -18,11 +18,12 @@ use std::collections::HashMap;
use async_trait::async_trait;
use containerd_shim::{
api::Status,
error::Result,
protos::{
api::{CreateTaskRequest, ExecProcessRequest, ProcessInfo, StateResponse},
cgroups::metrics::Metrics,
protobuf::{well_known_types::any::Any, Message, MessageDyn},
protobuf::{well_known_types::any::Any, EnumOrUnknown, Message, MessageDyn},
shim::oci::ProcessDetails,
},
Error,
@ -56,6 +57,9 @@ pub trait Container {
async fn stats(&self) -> Result<Metrics>;
async fn all_processes(&self) -> Result<Vec<ProcessInfo>>;
async fn close_io(&mut self, exec_id: Option<&str>) -> Result<()>;
async fn pause(&mut self) -> Result<()>;
async fn resume(&mut self) -> Result<()>;
async fn init_state(&self) -> EnumOrUnknown<Status>;
}
#[async_trait]
@ -93,6 +97,11 @@ where
E: Process + Send + Sync,
P: ProcessFactory<E> + Send + Sync,
{
async fn init_state(&self) -> EnumOrUnknown<Status> {
// Default should be unknown
self.init.state().await.unwrap_or_default().status
}
async fn start(&mut self, exec_id: Option<&str>) -> Result<i32> {
let process = self.get_mut_process(exec_id)?;
process.start().await?;
@ -102,6 +111,12 @@ where
async fn state(&self, exec_id: Option<&str>) -> Result<StateResponse> {
let process = self.get_process(exec_id)?;
let mut resp = process.state().await?;
let init_state = self.init.state().await?.status;
if init_state == EnumOrUnknown::new(Status::PAUSING)
|| init_state == EnumOrUnknown::new(Status::PAUSED)
{
resp.status = init_state;
}
resp.bundle = self.bundle.to_string();
debug!("container state: {:?}", resp);
Ok(resp)
@ -137,7 +152,6 @@ where
let process = self.get_mut_process(exec_id_opt);
match process {
Ok(p) => p.delete().await?,
Err(Error::NotFoundError(_)) => return Ok((pid, code, exited_at)),
Err(e) => return Err(e),
}
if let Some(exec_id) = exec_id_opt {
@ -212,6 +226,14 @@ where
let process = self.get_mut_process(exec_id)?;
process.close_io().await
}
async fn pause(&mut self) -> Result<()> {
self.init.pause().await
}
async fn resume(&mut self) -> Result<()> {
self.init.resume().await
}
}
impl<T, E, P> ContainerTemplate<T, E, P>
@ -238,7 +260,7 @@ where
match exec_id {
Some(exec_id) => {
let p = self.processes.get_mut(exec_id).ok_or_else(|| {
Error::NotFoundError("can not find the exec by id".to_string())
Error::NotFoundError(format!("can not find the exec by id {}", exec_id))
})?;
Ok(p)
}

View File

@ -14,9 +14,14 @@
limitations under the License.
*/
use std::env;
use std::{env, io::Write};
use containerd_shim::{asynchronous::run, parse};
use containerd_shim::{
asynchronous::run,
parse,
protos::protobuf::{well_known_types::any::Any, Message},
run_info,
};
mod cgroup_memory;
mod common;
@ -47,6 +52,30 @@ fn parse_version() {
std::process::exit(0);
}
if flags.info {
let r = run_info();
match r {
Ok(rinfo) => {
let mut info = Any::new();
info.type_url = "io.containerd.runc.v2.Info".to_string();
info.value = match rinfo.write_to_bytes() {
Ok(bytes) => bytes,
Err(e) => {
eprintln!("Failed to write runtime info to bytes: {}", e);
std::process::exit(1);
}
};
std::io::stdout()
.write_all(info.write_to_bytes().unwrap().as_slice())
.expect("Failed to write to stdout");
}
Err(_) => {
eprintln!("Failed to get runtime info");
std::process::exit(1);
}
}
std::process::exit(0);
}
}
#[tokio::main]

View File

@ -39,6 +39,7 @@ use tokio::{
use crate::io::Stdio;
#[allow(dead_code)]
#[async_trait]
pub trait Process {
async fn start(&mut self) -> Result<()>;
@ -55,6 +56,9 @@ pub trait Process {
async fn stats(&self) -> Result<Metrics>;
async fn ps(&self) -> Result<Vec<ProcessInfo>>;
async fn close_io(&mut self) -> Result<()>;
async fn pause(&mut self) -> Result<()>;
async fn resume(&mut self) -> Result<()>;
async fn id(&self) -> &str;
}
#[async_trait]
@ -65,6 +69,8 @@ pub trait ProcessLifecycle<P: Process> {
async fn update(&self, p: &mut P, resources: &LinuxResources) -> Result<()>;
async fn stats(&self, p: &P) -> Result<Metrics>;
async fn ps(&self, p: &P) -> Result<Vec<ProcessInfo>>;
async fn pause(&self, p: &mut P) -> Result<()>;
async fn resume(&self, p: &mut P) -> Result<()>;
}
pub struct ProcessTemplate<S> {
@ -119,6 +125,10 @@ where
self.pid
}
async fn id(&self) -> &str {
self.id.as_str()
}
async fn state(&self) -> Result<StateResponse> {
let mut resp = StateResponse::new();
resp.id = self.id.to_string();
@ -198,4 +208,12 @@ where
}
Ok(())
}
async fn pause(&mut self) -> Result<()> {
self.lifecycle.clone().pause(self).await
}
async fn resume(&mut self) -> Result<()> {
self.lifecycle.clone().resume(self).await
}
}

View File

@ -34,6 +34,7 @@ use containerd_shim::{
asynchronous::monitor::{monitor_subscribe, monitor_unsubscribe, Subscription},
io_error,
monitor::{ExitEvent, Subject, Topic},
mount::umount_recursive,
other, other_error,
protos::{
api::ProcessInfo,
@ -59,12 +60,25 @@ use super::{
};
use crate::{
common::{
check_kill_error, create_io, create_runc, get_spec_from_request, receive_socket,
CreateConfig, Log, ProcessIO, ShimExecutor, INIT_PID_FILE, LOG_JSON_FILE,
check_kill_error, create_io, create_runc, get_spec_from_request, handle_file_open,
receive_socket, CreateConfig, Log, ProcessIO, ShimExecutor, INIT_PID_FILE, LOG_JSON_FILE,
},
io::Stdio,
};
/// check the process is zombie
#[cfg(target_os = "linux")]
fn is_zombie_process(pid: i32) -> bool {
if let Ok(status) = std::fs::read_to_string(format!("/proc/{}/status", pid)) {
for line in status.lines() {
if line.starts_with("State:") && line.contains('Z') {
return true;
}
}
}
false
}
pub type ExecProcess = ProcessTemplate<RuncExecLifecycle>;
pub type InitProcess = ProcessTemplate<RuncInitLifecycle>;
@ -299,6 +313,7 @@ impl ProcessLifecycle<InitProcess> for RuncInitLifecycle {
);
}
}
umount_recursive(Path::new(&self.bundle).join("rootfs").to_str(), 0)?;
self.exit_signal.signal();
Ok(())
}
@ -311,6 +326,15 @@ impl ProcessLifecycle<InitProcess> for RuncInitLifecycle {
p.pid
));
}
// check the process is zombie
if is_zombie_process(p.pid) {
return Err(other!(
"failed to update resources because process {} is a zombie",
p.pid
));
}
containerd_shim::cgroup::update_resources(p.pid as u32, resources)
}
@ -327,6 +351,15 @@ impl ProcessLifecycle<InitProcess> for RuncInitLifecycle {
p.pid
));
}
// check the process is zombie
if is_zombie_process(p.pid) {
return Err(other!(
"failed to collect metrics because process {} is a zombie",
p.pid
));
}
containerd_shim::cgroup::collect_metrics(p.pid as u32)
}
@ -340,7 +373,7 @@ impl ProcessLifecycle<InitProcess> for RuncInitLifecycle {
.runtime
.ps(&p.id)
.await
.map_err(other_error!(e, "failed to execute runc ps"))?;
.map_err(other_error!("failed to execute runc ps"))?;
Ok(pids
.iter()
.map(|&x| ProcessInfo {
@ -349,6 +382,46 @@ impl ProcessLifecycle<InitProcess> for RuncInitLifecycle {
})
.collect())
}
#[cfg(target_os = "linux")]
async fn pause(&self, p: &mut InitProcess) -> Result<()> {
match p.state {
Status::RUNNING => {
p.state = Status::PAUSING;
if let Err(e) = self.runtime.pause(p.id.as_str()).await {
p.state = Status::RUNNING;
return Err(runtime_error(&self.bundle, e, "OCI runtime pause failed").await);
}
p.state = Status::PAUSED;
Ok(())
}
_ => Err(other!("cannot pause when in {:?} state", p.state)),
}
}
#[cfg(not(target_os = "linux"))]
async fn pause(&self, _p: &mut InitProcess) -> Result<()> {
Err(Error::Unimplemented("pause".to_string()))
}
#[cfg(target_os = "linux")]
async fn resume(&self, p: &mut InitProcess) -> Result<()> {
match p.state {
Status::PAUSED => {
if let Err(e) = self.runtime.resume(p.id.as_str()).await {
return Err(runtime_error(&self.bundle, e, "OCI runtime pause failed").await);
}
p.state = Status::RUNNING;
Ok(())
}
_ => Err(other!("cannot resume when in {:?} state", p.state)),
}
}
#[cfg(not(target_os = "linux"))]
async fn resume(&self, _p: &mut InitProcess) -> Result<()> {
Err(Error::Unimplemented("resume".to_string()))
}
}
impl RuncInitLifecycle {
@ -473,6 +546,14 @@ impl ProcessLifecycle<ExecProcess> for RuncExecLifecycle {
async fn ps(&self, _p: &ExecProcess) -> Result<Vec<ProcessInfo>> {
Err(Error::Unimplemented("exec ps".to_string()))
}
async fn pause(&self, _p: &mut ExecProcess) -> Result<()> {
Err(Error::Unimplemented("exec pause".to_string()))
}
async fn resume(&self, _p: &mut ExecProcess) -> Result<()> {
Err(Error::Unimplemented("exec resume".to_string()))
}
}
async fn copy_console(
@ -490,11 +571,14 @@ async fn copy_console(
.try_clone()
.await
.map_err(io_error!(e, "failed to clone console file"))?;
let stdin = OpenOptions::new()
.read(true)
.open(stdio.stdin.as_str())
.await
.map_err(io_error!(e, "failed to open stdin"))?;
let stdin = handle_file_open(|| async {
OpenOptions::new()
.read(true)
.open(stdio.stdin.as_str())
.await
})
.await
.map_err(io_error!(e, "failed to open stdin"))?;
spawn_copy(stdin, console_stdin, exit_signal.clone(), None::<fn()>);
}
@ -539,11 +623,14 @@ pub async fn copy_io(pio: &ProcessIO, stdio: &Stdio, exit_signal: Arc<ExitSignal
if let Some(w) = io.stdin() {
debug!("copy_io: pipe stdin from {}", stdio.stdin.as_str());
if !stdio.stdin.is_empty() {
let stdin = OpenOptions::new()
.read(true)
.open(stdio.stdin.as_str())
.await
.map_err(io_error!(e, "open stdin"))?;
let stdin = handle_file_open(|| async {
OpenOptions::new()
.read(true)
.open(stdio.stdin.as_str())
.await
})
.await
.map_err(io_error!(e, "open stdin"))?;
spawn_copy(stdin, w, exit_signal.clone(), None::<fn()>);
}
}
@ -551,18 +638,24 @@ pub async fn copy_io(pio: &ProcessIO, stdio: &Stdio, exit_signal: Arc<ExitSignal
if let Some(r) = io.stdout() {
debug!("copy_io: pipe stdout from to {}", stdio.stdout.as_str());
if !stdio.stdout.is_empty() {
let stdout = OpenOptions::new()
.write(true)
.open(stdio.stdout.as_str())
.await
.map_err(io_error!(e, "open stdout"))?;
let stdout = handle_file_open(|| async {
OpenOptions::new()
.write(true)
.open(stdio.stdout.as_str())
.await
})
.await
.map_err(io_error!(e, "open stdout"))?;
// open a read to make sure even if the read end of containerd shutdown,
// copy still continue until the restart of containerd succeed
let stdout_r = OpenOptions::new()
.read(true)
.open(stdio.stdout.as_str())
.await
.map_err(io_error!(e, "open stdout for read"))?;
let stdout_r = handle_file_open(|| async {
OpenOptions::new()
.read(true)
.open(stdio.stdout.as_str())
.await
})
.await
.map_err(io_error!(e, "open stdout for read"))?;
spawn_copy(
r,
stdout,
@ -577,18 +670,24 @@ pub async fn copy_io(pio: &ProcessIO, stdio: &Stdio, exit_signal: Arc<ExitSignal
if let Some(r) = io.stderr() {
if !stdio.stderr.is_empty() {
debug!("copy_io: pipe stderr from to {}", stdio.stderr.as_str());
let stderr = OpenOptions::new()
.write(true)
.open(stdio.stderr.as_str())
.await
.map_err(io_error!(e, "open stderr"))?;
let stderr = handle_file_open(|| async {
OpenOptions::new()
.write(true)
.open(stdio.stderr.as_str())
.await
})
.await
.map_err(io_error!(e, "open stderr"))?;
// open a read to make sure even if the read end of containerd shutdown,
// copy still continue until the restart of containerd succeed
let stderr_r = OpenOptions::new()
.read(true)
.open(stdio.stderr.as_str())
.await
.map_err(io_error!(e, "open stderr for read"))?;
let stderr_r = handle_file_open(|| async {
OpenOptions::new()
.read(true)
.open(stdio.stderr.as_str())
.await
})
.await
.map_err(io_error!(e, "open stderr for read"))?;
spawn_copy(
r,
stderr,

View File

@ -14,7 +14,7 @@
limitations under the License.
*/
use std::{env::current_dir, sync::Arc};
use std::{env::current_dir, sync::Arc, time::Duration};
use ::runc::options::DeleteOpts;
use async_trait::async_trait;
@ -27,12 +27,13 @@ use containerd_shim::{
event::Event,
io_error,
monitor::{Subject, Topic},
protos::{events::task::TaskExit, protobuf::MessageDyn},
mount::umount_recursive,
protos::{events::task::TaskExit, protobuf::MessageDyn, ttrpc::context::with_duration},
util::{
convert_to_timestamp, read_options, read_pid_from_file, read_runtime, read_spec, timestamp,
write_str_to_file,
},
Config, Context, DeleteResponse, Error, Flags, StartOpts,
Config, DeleteResponse, Error, Flags, StartOpts,
};
use log::{debug, error, warn};
use tokio::sync::mpsc::{channel, Receiver, Sender};
@ -108,7 +109,7 @@ impl Shim for Service {
let namespace = self.namespace.as_str();
let bundle = current_dir().map_err(io_error!(e, "get current dir"))?;
let opts = read_options(&bundle).await?;
let runtime = read_runtime(&bundle).await?;
let runtime = read_runtime(&bundle).await.unwrap_or_default();
let runc = create_runc(
&runtime,
@ -117,11 +118,15 @@ impl Shim for Service {
&opts,
Some(Arc::new(ShimExecutor::default())),
)?;
let pid = read_pid_from_file(&bundle.join(INIT_PID_FILE)).await?;
let pid = read_pid_from_file(&bundle.join(INIT_PID_FILE))
.await
.unwrap_or_default();
runc.delete(&self.id, Some(&DeleteOpts { force: true }))
.await
.unwrap_or_else(|e| warn!("failed to remove runc container: {}", e));
umount_recursive(bundle.join("rootfs").to_str(), 0)
.unwrap_or_else(|e| warn!("failed to umount recursive rootfs: {}", e));
let mut resp = DeleteResponse::new();
// sigkill
resp.set_exit_status(137);
@ -159,8 +164,10 @@ async fn process_exits(
if let Subject::Pid(pid) = e.subject {
debug!("receive exit event: {}", &e);
let exit_code = e.exit_code;
for (_k, cont) in containers.lock().await.iter_mut() {
for (_k, cont) in containers.write().await.iter_mut() {
let bundle = cont.bundle.to_string();
let container_id = cont.id.clone();
let mut change_process: Vec<&mut (dyn Process + Send + Sync)> = Vec::new();
// pid belongs to container init process
if cont.init.pid == pid {
// kill all children process if the container has a private PID namespace
@ -169,20 +176,30 @@ async fn process_exits(
error!("failed to kill init's children: {}", e)
});
}
// set exit for init process
cont.init.set_exited(exit_code).await;
if let Ok(process_d) = cont.get_mut_process(None) {
change_process.push(process_d);
} else {
break;
}
} else {
// pid belongs to container common process
if let Some((_, p)) = cont.processes.iter_mut().find(|(_, p)| p.pid == pid)
{
change_process.push(p as &mut (dyn Process + Send + Sync));
}
}
let process_len = change_process.len();
for process in change_process {
// set exit for process
process.set_exited(exit_code).await;
let code = process.exit_code().await;
let exited_at = process.exited_at().await;
// publish event
let (_, code, exited_at) = match cont.get_exit_info(None).await {
Ok(info) => info,
Err(_) => break,
};
let ts = convert_to_timestamp(exited_at);
let event = TaskExit {
container_id: cont.id.to_string(),
id: cont.id.to_string(),
pid: cont.pid().await as u32,
container_id: container_id.clone(),
id: process.id().await.to_string(),
pid: process.pid().await as u32,
exit_status: code as u32,
exited_at: Some(ts).into(),
..Default::default()
@ -191,18 +208,10 @@ async fn process_exits(
tx.send((topic.to_string(), Box::new(event)))
.await
.unwrap_or_else(|e| warn!("send {} to publisher: {}", topic, e));
break;
}
// pid belongs to container common process
for (_exec_id, p) in cont.processes.iter_mut() {
// set exit for exec process
if p.pid == pid {
p.set_exited(exit_code).await;
// TODO: publish event
break;
}
//if process has been find , no need to keep search
if process_len != 0 {
break;
}
}
}
@ -218,8 +227,11 @@ async fn forward(
) {
tokio::spawn(async move {
while let Some((topic, e)) = rx.recv().await {
// While ttrpc push the event,give it a 5 seconds timeout.
// Prevent event reporting from taking too long time.
// Learnd from goshim's containerd/runtime/v2/shim/publisher.go
publisher
.publish(Context::default(), &topic, &ns, e)
.publish(with_duration(Duration::from_secs(5)), &topic, &ns, e)
.await
.unwrap_or_else(|e| warn!("publish {} to containerd: {}", topic, e));
}

View File

@ -30,17 +30,18 @@ use containerd_shim::{
PidsResponse, StatsRequest, StatsResponse, UpdateTaskRequest,
},
events::task::{TaskCreate, TaskDelete, TaskExecAdded, TaskExecStarted, TaskIO, TaskStart},
protobuf::MessageDyn,
protobuf::{EnumOrUnknown, MessageDyn},
shim_async::Task,
ttrpc,
ttrpc::r#async::TtrpcContext,
ttrpc::{self, r#async::TtrpcContext},
},
util::{convert_to_any, convert_to_timestamp, AsOption},
TtrpcResult,
};
use log::{debug, info, warn};
use oci_spec::runtime::LinuxResources;
use tokio::sync::{mpsc::Sender, MappedMutexGuard, Mutex, MutexGuard};
use tokio::sync::{
mpsc::Sender, RwLock, RwLockMappedWriteGuard, RwLockReadGuard, RwLockWriteGuard,
};
use super::container::{Container, ContainerFactory};
type EventSender = Sender<(String, Box<dyn MessageDyn>)>;
@ -50,6 +51,10 @@ use std::path::Path;
#[cfg(target_os = "linux")]
use cgroups_rs::hierarchies::is_cgroup2_unified_mode;
use containerd_shim::{
api::{PauseRequest, ResumeRequest},
protos::events::task::{TaskPaused, TaskResumed},
};
#[cfg(target_os = "linux")]
use containerd_shim::{
error::{Error, Result},
@ -69,7 +74,10 @@ use crate::cgroup_memory;
/// parameter of `Service`, and implements their own `ContainerFactory` and `Container`.
pub struct TaskService<F, C> {
pub factory: F,
pub containers: Arc<Mutex<HashMap<String, C>>>,
// In comparison, a Mutex does not distinguish between readers or writers that acquire the lock,
// therefore causing any tasks waiting for the lock to become available to yield.
// An RwLock will allow any number of readers to acquire the lock as long as a writer is not holding the lock.
pub containers: Arc<RwLock<HashMap<String, C>>>,
pub namespace: String,
pub exit: Arc<ExitSignal>,
pub tx: EventSender,
@ -82,7 +90,7 @@ where
pub fn new(ns: &str, exit: Arc<ExitSignal>, tx: EventSender) -> Self {
Self {
factory: Default::default(),
containers: Arc::new(Mutex::new(Default::default())),
containers: Arc::new(RwLock::new(Default::default())),
namespace: ns.to_string(),
exit,
tx,
@ -91,15 +99,27 @@ where
}
impl<F, C> TaskService<F, C> {
pub async fn get_container(&self, id: &str) -> TtrpcResult<MappedMutexGuard<'_, C>> {
let mut containers = self.containers.lock().await;
pub async fn container_mut(&self, id: &str) -> TtrpcResult<RwLockMappedWriteGuard<'_, C>> {
let mut containers = self.containers.write().await;
containers.get_mut(id).ok_or_else(|| {
ttrpc::Error::RpcStatus(ttrpc::get_status(
ttrpc::Code::NOT_FOUND,
format!("can not find container by id {}", id),
))
})?;
let container = MutexGuard::map(containers, |m| m.get_mut(id).unwrap());
let container = RwLockWriteGuard::map(containers, |m| m.get_mut(id).unwrap());
Ok(container)
}
pub async fn container(&self, id: &str) -> TtrpcResult<RwLockReadGuard<'_, C>> {
let containers = self.containers.read().await;
containers.get(id).ok_or_else(|| {
ttrpc::Error::RpcStatus(ttrpc::get_status(
ttrpc::Code::NOT_FOUND,
format!("can not find container by id {}", id),
))
})?;
let container = RwLockReadGuard::map(containers, |m| m.get(id).unwrap());
Ok(container)
}
@ -143,7 +163,7 @@ async fn monitor_oom(id: &String, pid: u32, tx: EventSender) -> Result<()> {
"memory.oom_control",
)
.await
.map_err(other_error!(e, "register_memory_event failed:"))?;
.map_err(other_error!("register_memory_event failed:"))?;
run_oom_monitor(rx, id.to_string(), tx);
}
@ -157,7 +177,7 @@ where
C: Container + Sync + Send + 'static,
{
async fn state(&self, _ctx: &TtrpcContext, req: StateRequest) -> TtrpcResult<StateResponse> {
let container = self.get_container(req.id()).await?;
let container = self.container(req.id()).await?;
let exec_id = req.exec_id().as_option();
let resp = container.state(exec_id).await?;
Ok(resp)
@ -171,17 +191,17 @@ where
info!("Create request for {:?}", &req);
// Note: Get containers here is for getting the lock,
// to make sure no other threads manipulate the containers metadata;
let mut containers = self.containers.lock().await;
let ns = self.namespace.as_str();
let id = req.id.as_str();
let container = self.factory.create(ns, &req).await?;
let mut resp = CreateTaskResponse::new();
let pid = container.pid().await as u32;
resp.pid = pid;
containers.insert(id.to_string(), container);
let pid = {
let mut containers = self.containers.write().await;
let container = self.factory.create(ns, &req).await?;
let pid = container.pid().await as u32;
resp.pid = pid;
containers.insert(id.to_string(), container);
pid
};
self.send_event(TaskCreate {
container_id: req.id.to_string(),
@ -206,8 +226,19 @@ where
async fn start(&self, _ctx: &TtrpcContext, req: StartRequest) -> TtrpcResult<StartResponse> {
info!("Start request for {:?}", &req);
let mut container = self.get_container(req.id()).await?;
let pid = container.start(req.exec_id.as_str().as_option()).await?;
let pid = {
let mut container = self.container_mut(req.id()).await?;
// Prevent the init process from exiting and continuing with start
// Return early to reduce the time it takes to return only when runc encounters an error
if container.init_state().await == EnumOrUnknown::new(Status::STOPPED) {
debug!("container init process has exited, start process should not continue");
return Err(ttrpc::Error::RpcStatus(ttrpc::get_status(
ttrpc::Code::FAILED_PRECONDITION,
format!("container init process has exited {}", container.id().await),
)));
}
container.start(req.exec_id.as_str().as_option()).await?
};
let mut resp = StartResponse::new();
resp.pid = pid as u32;
@ -239,19 +270,17 @@ where
async fn delete(&self, _ctx: &TtrpcContext, req: DeleteRequest) -> TtrpcResult<DeleteResponse> {
info!("Delete request for {:?}", &req);
let mut containers = self.containers.lock().await;
let container = containers.get_mut(req.id()).ok_or_else(|| {
ttrpc::Error::RpcStatus(ttrpc::get_status(
ttrpc::Code::NOT_FOUND,
format!("can not find container by id {}", req.id()),
))
})?;
let id = container.id().await;
let exec_id_opt = req.exec_id().as_option();
let (pid, exit_status, exited_at) = container.delete(exec_id_opt).await?;
self.factory.cleanup(&self.namespace, container).await?;
let (id, pid, exit_status, exited_at) = {
let mut container = self.container_mut(req.id()).await?;
let id = container.id().await;
let exec_id_opt = req.exec_id().as_option();
let (pid, exit_status, exited_at) = container.delete(exec_id_opt).await?;
self.factory.cleanup(&self.namespace, &container).await?;
(id, pid, exit_status, exited_at)
};
if req.exec_id().is_empty() {
containers.remove(req.id());
self.containers.write().await.remove(req.id());
}
let ts = convert_to_timestamp(exited_at);
@ -279,8 +308,7 @@ where
async fn pids(&self, _ctx: &TtrpcContext, req: PidsRequest) -> TtrpcResult<PidsResponse> {
debug!("Pids request for {:?}", req);
let container = self.get_container(req.id()).await?;
let processes = container.all_processes().await?;
let processes = self.container(req.id()).await?.all_processes().await?;
debug!("Pids request for {:?} returns successfully", req);
Ok(PidsResponse {
processes,
@ -288,10 +316,34 @@ where
})
}
async fn pause(&self, _ctx: &TtrpcContext, req: PauseRequest) -> TtrpcResult<Empty> {
info!("pause request for {:?}", req);
self.container_mut(req.id()).await?.pause().await?;
self.send_event(TaskPaused {
container_id: req.id.to_string(),
..Default::default()
})
.await;
info!("pause request for {:?} returns successfully", req);
Ok(Empty::new())
}
async fn resume(&self, _ctx: &TtrpcContext, req: ResumeRequest) -> TtrpcResult<Empty> {
info!("resume request for {:?}", req);
self.container_mut(req.id()).await?.resume().await?;
self.send_event(TaskResumed {
container_id: req.id.to_string(),
..Default::default()
})
.await;
info!("resume request for {:?} returns successfully", req);
Ok(Empty::new())
}
async fn kill(&self, _ctx: &TtrpcContext, req: KillRequest) -> TtrpcResult<Empty> {
info!("Kill request for {:?}", req);
let mut container = self.get_container(req.id()).await?;
container
self.container_mut(req.id())
.await?
.kill(req.exec_id().as_option(), req.signal, req.all)
.await?;
info!("Kill request for {:?} returns successfully", req);
@ -301,11 +353,15 @@ where
async fn exec(&self, _ctx: &TtrpcContext, req: ExecProcessRequest) -> TtrpcResult<Empty> {
info!("Exec request for {:?}", req);
let exec_id = req.exec_id().to_string();
let mut container = self.get_container(req.id()).await?;
container.exec(req).await?;
let container_id = {
let mut container = self.container_mut(req.id()).await?;
container.exec(req).await?;
container.id().await
};
self.send_event(TaskExecAdded {
container_id: container.id().await,
container_id,
exec_id,
..Default::default()
})
@ -319,16 +375,18 @@ where
"Resize pty request for container {}, exec_id: {}",
&req.id, &req.exec_id
);
let mut container = self.get_container(req.id()).await?;
container
self.container_mut(req.id())
.await?
.resize_pty(req.exec_id().as_option(), req.height, req.width)
.await?;
Ok(Empty::new())
}
async fn close_io(&self, _ctx: &TtrpcContext, req: CloseIORequest) -> TtrpcResult<Empty> {
let mut container = self.get_container(req.id()).await?;
container.close_io(req.exec_id().as_option()).await?;
self.container_mut(req.id())
.await?
.close_io(req.exec_id().as_option())
.await?;
Ok(Empty::new())
}
@ -349,9 +407,7 @@ where
format!("failed to parse resource spec: {}", e),
))
})?;
let mut container = self.get_container(&id).await?;
container.update(&resources).await?;
self.container_mut(&id).await?.update(&resources).await?;
Ok(Empty::new())
}
@ -359,7 +415,7 @@ where
info!("Wait request for {:?}", req);
let exec_id = req.exec_id.as_str().as_option();
let wait_rx = {
let mut container = self.get_container(req.id()).await?;
let mut container = self.container_mut(req.id()).await?;
let state = container.state(exec_id).await?;
if state.status() != Status::RUNNING && state.status() != Status::CREATED {
let mut resp = WaitResponse::new();
@ -373,8 +429,11 @@ where
wait_rx.await.unwrap_or_default();
// get lock again.
let container = self.get_container(req.id()).await?;
let (_, code, exited_at) = container.get_exit_info(exec_id).await?;
let (_, code, exited_at) = self
.container(req.id())
.await?
.get_exit_info(exec_id)
.await?;
let mut resp = WaitResponse::new();
resp.set_exit_status(code as u32);
let ts = convert_to_timestamp(exited_at);
@ -385,9 +444,7 @@ where
async fn stats(&self, _ctx: &TtrpcContext, req: StatsRequest) -> TtrpcResult<StatsResponse> {
debug!("Stats request for {:?}", req);
let container = self.get_container(req.id()).await?;
let stats = container.stats().await?;
let stats = self.container(req.id()).await?.stats().await?;
let mut resp = StatsResponse::new();
resp.set_stats(convert_to_any(Box::new(stats))?);
Ok(resp)
@ -399,10 +456,12 @@ where
req: ConnectRequest,
) -> TtrpcResult<ConnectResponse> {
info!("Connect request for {:?}", req);
let mut pid: u32 = 0;
if let Ok(container) = self.get_container(req.id()).await {
pid = container.pid().await as u32;
}
let pid = if let Ok(container) = self.container(req.id()).await {
container.pid().await as u32
} else {
0
};
Ok(ConnectResponse {
shim_pid: std::process::id(),
@ -413,7 +472,7 @@ where
async fn shutdown(&self, _ctx: &TtrpcContext, _req: ShutdownRequest) -> TtrpcResult<Empty> {
debug!("Shutdown request");
let containers = self.containers.lock().await;
let containers = self.containers.read().await;
if containers.len() > 0 {
return Ok(Empty::new());
}

View File

@ -1,6 +1,6 @@
[package]
name = "runc"
version = "0.2.0"
version = "0.3.0"
authors = ["Yuna Tomida <ytomida.mmm@gmail.com>", "The containerd Authors"]
description = "A crate for consuming the runc binary in your Rust applications"
keywords = ["containerd", "containers", "runc"]
@ -20,7 +20,6 @@ libc.workspace = true
log.workspace = true
nix = { workspace = true, features = ["user", "fs"] }
oci-spec.workspace = true
os_pipe.workspace = true
path-absolutize = "3.0.11"
prctl.workspace = true
serde.workspace = true
@ -29,6 +28,7 @@ tempfile.workspace = true
thiserror.workspace = true
time.workspace = true
uuid.workspace = true
os_pipe.workspace = true
# Async dependencies
async-trait = { workspace = true, optional = true }

View File

@ -0,0 +1,219 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
use std::{fmt::Debug, io::Result, os::unix::io::AsRawFd, process::Stdio};
use async_trait::async_trait;
use nix::unistd::{Gid, Uid};
use tokio::fs::OpenOptions;
pub use crate::Io;
use crate::{Command, Pipe, PipedIo};
#[derive(Debug, Clone)]
pub struct IOOption {
pub open_stdin: bool,
pub open_stdout: bool,
pub open_stderr: bool,
}
impl Default for IOOption {
fn default() -> Self {
Self {
open_stdin: true,
open_stdout: true,
open_stderr: true,
}
}
}
impl PipedIo {
pub fn new(uid: u32, gid: u32, opts: &IOOption) -> std::io::Result<Self> {
Ok(Self {
stdin: if opts.open_stdin {
Self::create_pipe(uid, gid, true)?
} else {
None
},
stdout: if opts.open_stdout {
Self::create_pipe(uid, gid, true)?
} else {
None
},
stderr: if opts.open_stderr {
Self::create_pipe(uid, gid, true)?
} else {
None
},
})
}
fn create_pipe(uid: u32, gid: u32, stdin: bool) -> std::io::Result<Option<Pipe>> {
let pipe = Pipe::new()?;
let uid = Some(Uid::from_raw(uid));
let gid = Some(Gid::from_raw(gid));
if stdin {
let rd = pipe.rd.try_clone()?;
nix::unistd::fchown(rd.as_raw_fd(), uid, gid)?;
} else {
let wr = pipe.wr.try_clone()?;
nix::unistd::fchown(wr.as_raw_fd(), uid, gid)?;
}
Ok(Some(pipe))
}
}
/// IO driver to direct output/error messages to /dev/null.
///
/// With this Io driver, all methods of [crate::Runc] can't capture the output/error messages.
#[derive(Debug)]
pub struct NullIo {
dev_null: std::sync::Mutex<Option<std::fs::File>>,
}
impl NullIo {
pub fn new() -> std::io::Result<Self> {
let f = std::fs::OpenOptions::new().read(true).open("/dev/null")?;
let dev_null = std::sync::Mutex::new(Some(f));
Ok(Self { dev_null })
}
}
#[async_trait]
impl Io for NullIo {
async fn set(&self, cmd: &mut Command) -> std::io::Result<()> {
if let Some(null) = self.dev_null.lock().unwrap().as_ref() {
cmd.stdout(null.try_clone()?);
cmd.stderr(null.try_clone()?);
}
Ok(())
}
async fn close_after_start(&self) {
let mut m = self.dev_null.lock().unwrap();
let _ = m.take();
}
}
/// Io driver based on Stdio::inherited(), to direct outputs/errors to stdio.
///
/// With this Io driver, all methods of [crate::Runc] can't capture the output/error messages.
#[derive(Debug)]
pub struct InheritedStdIo {}
impl InheritedStdIo {
pub fn new() -> std::io::Result<Self> {
Ok(InheritedStdIo {})
}
}
#[async_trait]
impl Io for InheritedStdIo {
async fn set(&self, cmd: &mut Command) -> std::io::Result<()> {
cmd.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
Ok(())
}
async fn close_after_start(&self) {}
}
/// Io driver based on Stdio::piped(), to capture outputs/errors from runC.
///
/// With this Io driver, methods of [crate::Runc] may capture the output/error messages.
#[derive(Debug)]
pub struct PipedStdIo {}
impl PipedStdIo {
pub fn new() -> std::io::Result<Self> {
Ok(PipedStdIo {})
}
}
#[async_trait]
impl Io for PipedStdIo {
async fn set(&self, cmd: &mut Command) -> std::io::Result<()> {
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
Ok(())
}
async fn close_after_start(&self) {}
}
/// FIFO for the scenario that set FIFO for command Io.
#[derive(Debug)]
pub struct FIFO {
pub stdin: Option<String>,
pub stdout: Option<String>,
pub stderr: Option<String>,
}
#[async_trait]
impl Io for FIFO {
async fn set(&self, cmd: &mut Command) -> Result<()> {
if let Some(path) = self.stdin.as_ref() {
let stdin = OpenOptions::new()
.read(true)
.custom_flags(libc::O_NONBLOCK)
.open(path)
.await?;
cmd.stdin(stdin.into_std().await);
}
if let Some(path) = self.stdout.as_ref() {
let stdout = OpenOptions::new().write(true).open(path).await?;
cmd.stdout(stdout.into_std().await);
}
if let Some(path) = self.stderr.as_ref() {
let stderr = OpenOptions::new().write(true).open(path).await?;
cmd.stderr(stderr.into_std().await);
}
Ok(())
}
async fn close_after_start(&self) {}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(target_os = "macos"))]
#[test]
fn test_io_option() {
let opts = IOOption {
open_stdin: false,
open_stdout: false,
open_stderr: false,
};
let io = PipedIo::new(1000, 1000, &opts).unwrap();
assert!(io.stdin().is_none());
assert!(io.stdout().is_none());
assert!(io.stderr().is_none());
}
#[tokio::test]
async fn test_null_io() {
let io = NullIo::new().unwrap();
assert!(io.stdin().is_none());
assert!(io.stdout().is_none());
assert!(io.stderr().is_none());
}
}

View File

@ -0,0 +1,115 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pub mod io;
mod pipe;
mod runc;
use std::{fmt::Debug, io::Result, os::fd::AsRawFd};
use async_trait::async_trait;
use log::debug;
pub use pipe::Pipe;
pub use runc::{DefaultExecutor, Spawner};
use tokio::io::{AsyncRead, AsyncWrite};
use crate::Command;
#[async_trait]
pub trait Io: Debug + Send + Sync {
fn stdin(&self) -> Option<Box<dyn AsyncWrite + Send + Sync + Unpin>> {
None
}
fn stdout(&self) -> Option<Box<dyn AsyncRead + Send + Sync + Unpin>> {
None
}
fn stderr(&self) -> Option<Box<dyn AsyncRead + Send + Sync + Unpin>> {
None
}
/// Set IO for passed command.
/// Read side of stdin, write side of stdout and write side of stderr should be provided to command.
async fn set(&self, cmd: &mut Command) -> Result<()>;
/// Only close write side (should be stdout/err "from" runc process)
async fn close_after_start(&self);
}
#[derive(Debug)]
pub struct PipedIo {
pub stdin: Option<Pipe>,
pub stdout: Option<Pipe>,
pub stderr: Option<Pipe>,
}
#[async_trait]
impl Io for PipedIo {
fn stdin(&self) -> Option<Box<dyn AsyncWrite + Send + Sync + Unpin>> {
self.stdin.as_ref().and_then(|pipe| {
let fd = pipe.wr.as_raw_fd();
tokio_pipe::PipeWrite::from_raw_fd_checked(fd)
.map(|x| Box::new(x) as Box<dyn AsyncWrite + Send + Sync + Unpin>)
.ok()
})
}
fn stdout(&self) -> Option<Box<dyn AsyncRead + Send + Sync + Unpin>> {
self.stdout.as_ref().and_then(|pipe| {
let fd = pipe.rd.as_raw_fd();
tokio_pipe::PipeRead::from_raw_fd_checked(fd)
.map(|x| Box::new(x) as Box<dyn AsyncRead + Send + Sync + Unpin>)
.ok()
})
}
fn stderr(&self) -> Option<Box<dyn AsyncRead + Send + Sync + Unpin>> {
self.stderr.as_ref().and_then(|pipe| {
let fd = pipe.rd.as_raw_fd();
tokio_pipe::PipeRead::from_raw_fd_checked(fd)
.map(|x| Box::new(x) as Box<dyn AsyncRead + Send + Sync + Unpin>)
.ok()
})
}
// Note that this internally use [`std::fs::File`]'s `try_clone()`.
// Thus, the files passed to commands will be not closed after command exit.
async fn set(&self, cmd: &mut Command) -> std::io::Result<()> {
if let Some(p) = self.stdin.as_ref() {
let pr = p.rd.try_clone()?;
cmd.stdin(pr);
}
if let Some(p) = self.stdout.as_ref() {
let pw = p.wr.try_clone()?;
cmd.stdout(pw);
}
if let Some(p) = self.stderr.as_ref() {
let pw = p.wr.try_clone()?;
cmd.stdout(pw);
}
Ok(())
}
async fn close_after_start(&self) {
if let Some(p) = self.stdout.as_ref() {
nix::unistd::close(p.wr.as_raw_fd()).unwrap_or_else(|e| debug!("close stdout: {}", e));
}
if let Some(p) = self.stderr.as_ref() {
nix::unistd::close(p.wr.as_raw_fd()).unwrap_or_else(|e| debug!("close stderr: {}", e));
}
}
}

View File

@ -0,0 +1,110 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
use std::os::unix::io::OwnedFd;
use tokio::net::unix::pipe;
/// Struct to represent a pipe that can be used to transfer stdio inputs and outputs.
///
/// With this Io driver, methods of [crate::Runc] may capture the output/error messages.
/// When one side of the pipe is closed, the state will be represented with [`None`].
#[derive(Debug)]
pub struct Pipe {
pub rd: OwnedFd,
pub wr: OwnedFd,
}
impl Pipe {
pub fn new() -> std::io::Result<Self> {
let (tx, rx) = pipe::pipe()?;
let rd = rx.into_blocking_fd()?;
let wr = tx.into_blocking_fd()?;
Ok(Self { rd, wr })
}
}
#[cfg(test)]
mod tests {
use std::os::fd::IntoRawFd;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use super::*;
#[tokio::test]
async fn test_pipe_creation() {
let pipe = Pipe::new().expect("Failed to create pipe");
assert!(
pipe.rd.into_raw_fd() >= 0,
"Read file descriptor is invalid"
);
assert!(
pipe.wr.into_raw_fd() >= 0,
"Write file descriptor is invalid"
);
}
#[tokio::test]
async fn test_pipe_write_read() {
let pipe = Pipe::new().expect("Failed to create pipe");
let mut read_end = pipe::Receiver::from_owned_fd(pipe.rd).unwrap();
let mut write_end = pipe::Sender::from_owned_fd(pipe.wr).unwrap();
let write_data = b"hello";
write_end
.write_all(write_data)
.await
.expect("Failed to write to pipe");
let mut read_data = vec![0; write_data.len()];
read_end
.read_exact(&mut read_data)
.await
.expect("Failed to read from pipe");
assert_eq!(
read_data, write_data,
"Data read from pipe does not match data written"
);
}
#[tokio::test]
async fn test_pipe_async_write_read() {
let pipe = Pipe::new().expect("Failed to create pipe");
let mut read_end = pipe::Receiver::from_owned_fd(pipe.rd).unwrap();
let mut write_end = pipe::Sender::from_owned_fd(pipe.wr).unwrap();
let write_data = b"hello";
tokio::spawn(async move {
write_end
.write_all(write_data)
.await
.expect("Failed to write to pipe");
});
let mut read_data = vec![0; write_data.len()];
read_end
.read_exact(&mut read_data)
.await
.expect("Failed to read from pipe");
assert_eq!(
&read_data, write_data,
"Data read from pipe does not match data written"
);
}
}

View File

@ -0,0 +1,525 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
use std::{fmt::Debug, path::Path, process::ExitStatus};
use async_trait::async_trait;
use log::debug;
use oci_spec::runtime::{LinuxResources, Process};
use crate::{
container::Container,
error::Error,
events,
options::*,
utils::{self, write_value_to_temp_file},
Command, Response, Result, Runc,
};
// a macro tool to cleanup the file with name $filename,
// there is no async drop in async rust, so we have to call remove_file everytime
// after a temp file created, before return of a function.
// with this macro we don't have to write the match case codes everytime.
macro_rules! tc {
($b:expr, $filename: expr) => {
match $b {
Ok(r) => r,
Err(e) => {
let _ = tokio::fs::remove_file($filename).await;
return Err(e);
}
}
};
}
/// Async implementation for [Runc].
///
/// Note that you MUST use this client on tokio runtime, as this client internally use [`tokio::process::Command`]
/// and some other utilities.
impl Runc {
pub(crate) async fn launch(&self, mut cmd: Command, combined_output: bool) -> Result<Response> {
debug!("Execute command {:?}", cmd);
unsafe {
cmd.pre_exec(move || {
#[cfg(target_os = "linux")]
if let Ok(thp) = std::env::var("THP_DISABLED") {
if let Ok(thp_disabled) = thp.parse::<bool>() {
if let Err(e) = prctl::set_thp_disable(thp_disabled) {
debug!("set_thp_disable err: {}", e);
};
}
}
Ok(())
});
}
let (status, pid, stdout, stderr) = self.spawner.execute(cmd).await?;
if status.success() {
let output = if combined_output {
stdout + stderr.as_str()
} else {
stdout
};
Ok(Response {
pid,
status,
output,
})
} else {
Err(Error::CommandFailed {
status,
stdout,
stderr,
})
}
}
/// Create a new container
pub async fn create<P>(
&self,
id: &str,
bundle: P,
opts: Option<&CreateOpts>,
) -> Result<Response>
where
P: AsRef<Path>,
{
let mut args = vec![
"create".to_string(),
"--bundle".to_string(),
utils::abs_string(bundle)?,
];
if let Some(opts) = opts {
args.append(&mut opts.args()?);
}
args.push(id.to_string());
let mut cmd = self.command(&args)?;
match opts {
Some(CreateOpts { io: Some(io), .. }) => {
io.set(&mut cmd).await.map_err(Error::UnavailableIO)?;
let res = self.launch(cmd, true).await?;
io.close_after_start().await;
Ok(res)
}
_ => self.launch(cmd, true).await,
}
}
/// Delete a container
pub async fn delete(&self, id: &str, opts: Option<&DeleteOpts>) -> Result<()> {
let mut args = vec!["delete".to_string()];
if let Some(opts) = opts {
args.append(&mut opts.args());
}
args.push(id.to_string());
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
/// Return an event stream of container notifications
pub async fn events(&self, _id: &str, _interval: &std::time::Duration) -> Result<()> {
Err(Error::Unimplemented("events".to_string()))
}
/// Execute an additional process inside the container
pub async fn exec(&self, id: &str, spec: &Process, opts: Option<&ExecOpts>) -> Result<()> {
let f = write_value_to_temp_file(spec).await?;
let mut args = vec!["exec".to_string(), "--process".to_string(), f.clone()];
if let Some(opts) = opts {
args.append(&mut tc!(opts.args(), &f));
}
args.push(id.to_string());
let mut cmd = self.command(&args)?;
match opts {
Some(ExecOpts { io: Some(io), .. }) => {
tc!(
io.set(&mut cmd)
.await
.map_err(|e| Error::IoSet(e.to_string())),
&f
);
tc!(self.launch(cmd, true).await, &f);
io.close_after_start().await;
}
_ => {
tc!(self.launch(cmd, true).await, &f);
}
}
let _ = tokio::fs::remove_file(&f).await;
Ok(())
}
/// Send the specified signal to processes inside the container
pub async fn kill(&self, id: &str, sig: u32, opts: Option<&KillOpts>) -> Result<()> {
let mut args = vec!["kill".to_string()];
if let Some(opts) = opts {
args.append(&mut opts.args());
}
args.push(id.to_string());
args.push(sig.to_string());
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
/// List all containers associated with this runc instance
pub async fn list(&self) -> Result<Vec<Container>> {
let args = ["list".to_string(), "--format=json".to_string()];
let res = self.launch(self.command(&args)?, true).await?;
let output = res.output.trim();
// Ugly hack to work around golang
Ok(if output == "null" {
Vec::new()
} else {
serde_json::from_str(output).map_err(Error::JsonDeserializationFailed)?
})
}
/// Pause a container
pub async fn pause(&self, id: &str) -> Result<()> {
let args = ["pause".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
/// Resume a container
pub async fn resume(&self, id: &str) -> Result<()> {
let args = ["resume".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
pub async fn checkpoint(&self) -> Result<()> {
Err(Error::Unimplemented("checkpoint".to_string()))
}
pub async fn restore(&self) -> Result<()> {
Err(Error::Unimplemented("restore".to_string()))
}
/// List all the processes inside the container, returning their pids
pub async fn ps(&self, id: &str) -> Result<Vec<usize>> {
let args = [
"ps".to_string(),
"--format=json".to_string(),
id.to_string(),
];
let res = self.launch(self.command(&args)?, true).await?;
let output = res.output.trim();
// Ugly hack to work around golang
Ok(if output == "null" {
Vec::new()
} else {
serde_json::from_str(output).map_err(Error::JsonDeserializationFailed)?
})
}
/// Run the create, start, delete lifecycle of the container and return its exit status
pub async fn run<P>(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result<()>
where
P: AsRef<Path>,
{
let mut args = vec![
"run".to_string(),
"--bundle".to_string(),
utils::abs_string(bundle)?,
];
if let Some(opts) = opts {
args.append(&mut opts.args()?);
}
args.push(id.to_string());
let mut cmd = self.command(&args)?;
if let Some(CreateOpts { io: Some(io), .. }) = opts {
io.set(&mut cmd)
.await
.map_err(|e| Error::IoSet(e.to_string()))?;
};
let _ = self.launch(cmd, true).await?;
Ok(())
}
/// Start an already created container
pub async fn start(&self, id: &str) -> Result<()> {
let args = vec!["start".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
/// Return the state of a container
pub async fn state(&self, id: &str) -> Result<Container> {
let args = vec!["state".to_string(), id.to_string()];
let res = self.launch(self.command(&args)?, true).await?;
serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed)
}
/// Return the latest statistics for a container
pub async fn stats(&self, id: &str) -> Result<events::Stats> {
let args = vec!["events".to_string(), "--stats".to_string(), id.to_string()];
let res = self.launch(self.command(&args)?, true).await?;
let event: events::Event =
serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed)?;
if let Some(stats) = event.stats {
Ok(stats)
} else {
Err(Error::MissingContainerStats)
}
}
/// Update a container with the provided resource spec
pub async fn update(&self, id: &str, resources: &LinuxResources) -> Result<()> {
let f = write_value_to_temp_file(resources).await?;
let args = [
"update".to_string(),
"--resources".to_string(),
f.to_string(),
id.to_string(),
];
let _ = tc!(self.launch(self.command(&args)?, true).await, &f);
let _ = tokio::fs::remove_file(&f).await;
Ok(())
}
}
#[async_trait]
pub trait Spawner: Debug {
async fn execute(&self, cmd: Command) -> Result<(ExitStatus, u32, String, String)>;
}
#[derive(Debug)]
pub struct DefaultExecutor {}
#[async_trait]
impl Spawner for DefaultExecutor {
async fn execute(&self, cmd: Command) -> Result<(ExitStatus, u32, String, String)> {
let mut cmd = cmd;
let child = cmd.spawn().map_err(Error::ProcessSpawnFailed)?;
let pid = child.id().unwrap();
let result = child
.wait_with_output()
.await
.map_err(Error::InvalidCommand)?;
let status = result.status;
let stdout = String::from_utf8_lossy(&result.stdout).to_string();
let stderr = String::from_utf8_lossy(&result.stderr).to_string();
Ok((status, pid, stdout, stderr))
}
}
#[cfg(test)]
#[cfg(target_os = "linux")]
mod tests {
use std::sync::Arc;
use crate::{
error::Error,
io::{InheritedStdIo, PipedStdIo},
options::{CreateOpts, DeleteOpts, GlobalOpts},
Runc,
};
fn ok_client() -> Runc {
GlobalOpts::new()
.command("/bin/true")
.build()
.expect("unable to create runc instance")
}
fn fail_client() -> Runc {
GlobalOpts::new()
.command("/bin/false")
.build()
.expect("unable to create runc instance")
}
fn echo_client() -> Runc {
GlobalOpts::new()
.command("/bin/echo")
.build()
.expect("unable to create runc instance")
}
#[tokio::test]
async fn test_async_create() {
let opts = CreateOpts::new();
let ok_runc = ok_client();
let ok_task = tokio::spawn(async move {
let response = ok_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
.expect("true failed.");
assert_ne!(response.pid, 0);
assert!(response.status.success());
assert!(response.output.is_empty());
});
let opts = CreateOpts::new();
let fail_runc = fail_client();
let fail_task = tokio::spawn(async move {
match fail_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
{
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
});
ok_task.await.expect("ok_task failed.");
fail_task.await.expect("fail_task unexpectedly succeeded.");
}
#[tokio::test]
async fn test_async_start() {
let ok_runc = ok_client();
let ok_task = tokio::spawn(async move {
ok_runc.start("fake-id").await.expect("true failed.");
eprintln!("ok_runc succeeded.");
});
let fail_runc = fail_client();
let fail_task = tokio::spawn(async move {
match fail_runc.start("fake-id").await {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
});
ok_task.await.expect("ok_task failed.");
fail_task.await.expect("fail_task unexpectedly succeeded.");
}
#[tokio::test]
async fn test_async_run() {
let opts = CreateOpts::new();
let ok_runc = ok_client();
tokio::spawn(async move {
ok_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
.expect("true failed.");
eprintln!("ok_runc succeeded.");
});
let opts = CreateOpts::new();
let fail_runc = fail_client();
tokio::spawn(async move {
match fail_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
{
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
})
.await
.expect("tokio spawn falied.");
}
#[tokio::test]
async fn test_async_delete() {
let opts = DeleteOpts::new();
let ok_runc = ok_client();
tokio::spawn(async move {
ok_runc
.delete("fake-id", Some(&opts))
.await
.expect("true failed.");
eprintln!("ok_runc succeeded.");
});
let opts = DeleteOpts::new();
let fail_runc = fail_client();
tokio::spawn(async move {
match fail_runc.delete("fake-id", Some(&opts)).await {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
})
.await
.expect("tokio spawn falied.");
}
#[tokio::test]
async fn test_async_output() {
// test create cmd with inherit Io, expect empty cmd output
let mut opts = CreateOpts::new();
opts.io = Some(Arc::new(InheritedStdIo::new().unwrap()));
let echo_runc = echo_client();
let response = echo_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
.expect("echo failed:");
assert_ne!(response.pid, 0);
assert!(response.status.success());
assert!(response.output.is_empty());
// test create cmd with pipe Io, expect nonempty cmd output
let mut opts = CreateOpts::new();
opts.io = Some(Arc::new(PipedStdIo::new().unwrap()));
let response = echo_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
.expect("echo failed:");
assert_ne!(response.pid, 0);
assert!(response.status.success());
assert!(!response.output.is_empty());
}
}

View File

@ -39,28 +39,32 @@
//! [go-runc](https://github.com/containerd/go-runc) for Go.
use std::{
fmt::{self, Debug, Display},
path::{Path, PathBuf},
path::PathBuf,
process::{ExitStatus, Stdio},
sync::Arc,
};
#[cfg(feature = "async")]
use async_trait::async_trait;
pub use crate::asynchronous::*;
#[cfg(not(feature = "async"))]
pub use crate::synchronous::*;
#[cfg(feature = "async")]
use log::debug;
use oci_spec::runtime::{LinuxResources, Process};
use crate::{container::Container, error::Error, options::*, utils::write_value_to_temp_file};
pub mod asynchronous;
pub mod container;
pub mod error;
pub mod events;
pub mod io;
#[cfg(not(feature = "async"))]
pub mod synchronous;
#[cfg(feature = "async")]
pub mod monitor;
pub mod options;
pub mod utils;
const JSON: &str = "json";
const TEXT: &str = "text";
pub type Result<T> = std::result::Result<T, crate::error::Error>;
/// Response is for (pid, exit status, outputs).
@ -123,910 +127,3 @@ impl Runc {
Ok(cmd)
}
}
#[cfg(not(feature = "async"))]
impl Runc {
fn launch(&self, cmd: Command, combined_output: bool) -> Result<Response> {
let (status, pid, stdout, stderr) = self.spawner.execute(cmd)?;
if status.success() {
let output = if combined_output {
stdout + stderr.as_str()
} else {
stdout
};
Ok(Response {
pid,
status,
output,
})
} else {
Err(Error::CommandFailed {
status,
stdout,
stderr,
})
}
}
/// Create a new container
pub fn create<P>(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result<Response>
where
P: AsRef<Path>,
{
let mut args = vec![
"create".to_string(),
"--bundle".to_string(),
utils::abs_string(bundle)?,
];
if let Some(opts) = opts {
args.append(&mut opts.args()?);
}
args.push(id.to_string());
let mut cmd = self.command(&args)?;
match opts {
Some(CreateOpts { io: Some(io), .. }) => {
io.set(&mut cmd).map_err(|e| Error::IoSet(e.to_string()))?;
let res = self.launch(cmd, true)?;
io.close_after_start();
Ok(res)
}
_ => self.launch(cmd, true),
}
}
/// Delete a container
pub fn delete(&self, id: &str, opts: Option<&DeleteOpts>) -> Result<()> {
let mut args = vec!["delete".to_string()];
if let Some(opts) = opts {
args.append(&mut opts.args());
}
args.push(id.to_string());
self.launch(self.command(&args)?, true)?;
Ok(())
}
/// Execute an additional process inside the container
pub fn exec(&self, id: &str, spec: &Process, opts: Option<&ExecOpts>) -> Result<()> {
let (_temp_file, filename) = write_value_to_temp_file(spec)?;
let mut args = vec!["exec".to_string(), "--process".to_string(), filename];
if let Some(opts) = opts {
args.append(&mut opts.args()?);
}
args.push(id.to_string());
let mut cmd = self.command(&args)?;
match opts {
Some(ExecOpts { io: Some(io), .. }) => {
io.set(&mut cmd).map_err(|e| Error::IoSet(e.to_string()))?;
self.launch(cmd, true)?;
io.close_after_start();
}
_ => {
self.launch(cmd, true)?;
}
}
Ok(())
}
/// Send the specified signal to processes inside the container
pub fn kill(&self, id: &str, sig: u32, opts: Option<&KillOpts>) -> Result<()> {
let mut args = vec!["kill".to_string()];
if let Some(opts) = opts {
args.append(&mut opts.args());
}
args.push(id.to_string());
args.push(sig.to_string());
let _ = self.launch(self.command(&args)?, true)?;
Ok(())
}
/// List all containers associated with this runc instance
pub fn list(&self) -> Result<Vec<Container>> {
let args = ["list".to_string(), "--format=json".to_string()];
let res = self.launch(self.command(&args)?, true)?;
let output = res.output.trim();
// Ugly hack to work around golang
Ok(if output == "null" {
Vec::new()
} else {
serde_json::from_str(output).map_err(Error::JsonDeserializationFailed)?
})
}
/// Pause a container
pub fn pause(&self, id: &str) -> Result<()> {
let args = ["pause".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true)?;
Ok(())
}
/// Resume a container
pub fn resume(&self, id: &str) -> Result<()> {
let args = ["resume".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true)?;
Ok(())
}
pub fn checkpoint(&self) -> Result<()> {
Err(Error::Unimplemented("checkpoint".to_string()))
}
pub fn restore(&self) -> Result<()> {
Err(Error::Unimplemented("restore".to_string()))
}
/// List all the processes inside the container, returning their pids
pub fn ps(&self, id: &str) -> Result<Vec<usize>> {
let args = [
"ps".to_string(),
"--format=json".to_string(),
id.to_string(),
];
let res = self.launch(self.command(&args)?, false)?;
let output = res.output.trim();
// Ugly hack to work around golang
Ok(if output == "null" {
Vec::new()
} else {
serde_json::from_str(output).map_err(Error::JsonDeserializationFailed)?
})
}
/// Run the create, start, delete lifecycle of the container and return its exit status
pub fn run<P>(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result<Response>
where
P: AsRef<Path>,
{
let mut args = vec![
"run".to_string(),
"--bundle".to_string(),
utils::abs_string(bundle)?,
];
if let Some(opts) = opts {
args.append(&mut opts.args()?);
}
args.push(id.to_string());
let mut cmd = self.command(&args)?;
if let Some(CreateOpts { io: Some(io), .. }) = opts {
io.set(&mut cmd).map_err(|e| Error::IoSet(e.to_string()))?;
};
self.launch(cmd, true)
}
/// Start an already created container
pub fn start(&self, id: &str) -> Result<Response> {
let args = ["start".to_string(), id.to_string()];
self.launch(self.command(&args)?, true)
}
/// Return the state of a container
pub fn state(&self, id: &str) -> Result<Container> {
let args = ["state".to_string(), id.to_string()];
let res = self.launch(self.command(&args)?, true)?;
serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed)
}
/// Return the latest statistics for a container
pub fn stats(&self, id: &str) -> Result<events::Stats> {
let args = vec!["events".to_string(), "--stats".to_string(), id.to_string()];
let res = self.launch(self.command(&args)?, true)?;
let event: events::Event =
serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed)?;
if let Some(stats) = event.stats {
Ok(stats)
} else {
Err(Error::MissingContainerStats)
}
}
/// Update a container with the provided resource spec
pub fn update(&self, id: &str, resources: &LinuxResources) -> Result<()> {
let (_temp_file, filename) = write_value_to_temp_file(resources)?;
let args = [
"update".to_string(),
"--resources".to_string(),
filename,
id.to_string(),
];
self.launch(self.command(&args)?, true)?;
Ok(())
}
}
// a macro tool to cleanup the file with name $filename,
// there is no async drop in async rust, so we have to call remove_file everytime
// after a temp file created, before return of a function.
// with this macro we don't have to write the match case codes everytime.
#[cfg(feature = "async")]
macro_rules! tc {
($b:expr, $filename: expr) => {
match $b {
Ok(r) => r,
Err(e) => {
let _ = tokio::fs::remove_file($filename).await;
return Err(e);
}
}
};
}
#[cfg(not(feature = "async"))]
pub trait Spawner: Debug {
fn execute(&self, cmd: Command) -> Result<(ExitStatus, u32, String, String)>;
}
#[cfg(feature = "async")]
#[async_trait]
pub trait Spawner: Debug {
async fn execute(&self, cmd: Command) -> Result<(ExitStatus, u32, String, String)>;
}
/// Async implementation for [Runc].
///
/// Note that you MUST use this client on tokio runtime, as this client internally use [`tokio::process::Command`]
/// and some other utilities.
#[cfg(feature = "async")]
impl Runc {
async fn launch(&self, mut cmd: Command, combined_output: bool) -> Result<Response> {
debug!("Execute command {:?}", cmd);
unsafe {
cmd.pre_exec(move || {
#[cfg(target_os = "linux")]
if let Ok(thp) = std::env::var("THP_DISABLED") {
if let Ok(thp_disabled) = thp.parse::<bool>() {
if let Err(e) = prctl::set_thp_disable(thp_disabled) {
debug!("set_thp_disable err: {}", e);
};
}
}
Ok(())
});
}
let (status, pid, stdout, stderr) = self.spawner.execute(cmd).await?;
if status.success() {
let output = if combined_output {
stdout + stderr.as_str()
} else {
stdout
};
Ok(Response {
pid,
status,
output,
})
} else {
Err(Error::CommandFailed {
status,
stdout,
stderr,
})
}
}
/// Create a new container
pub async fn create<P>(
&self,
id: &str,
bundle: P,
opts: Option<&CreateOpts>,
) -> Result<Response>
where
P: AsRef<Path>,
{
let mut args = vec![
"create".to_string(),
"--bundle".to_string(),
utils::abs_string(bundle)?,
];
if let Some(opts) = opts {
args.append(&mut opts.args()?);
}
args.push(id.to_string());
let mut cmd = self.command(&args)?;
match opts {
Some(CreateOpts { io: Some(io), .. }) => {
io.set(&mut cmd).map_err(Error::UnavailableIO)?;
let res = self.launch(cmd, true).await?;
io.close_after_start();
Ok(res)
}
_ => self.launch(cmd, true).await,
}
}
/// Delete a container
pub async fn delete(&self, id: &str, opts: Option<&DeleteOpts>) -> Result<()> {
let mut args = vec!["delete".to_string()];
if let Some(opts) = opts {
args.append(&mut opts.args());
}
args.push(id.to_string());
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
/// Return an event stream of container notifications
pub async fn events(&self, _id: &str, _interval: &std::time::Duration) -> Result<()> {
Err(Error::Unimplemented("events".to_string()))
}
/// Execute an additional process inside the container
pub async fn exec(&self, id: &str, spec: &Process, opts: Option<&ExecOpts>) -> Result<()> {
let f = write_value_to_temp_file(spec).await?;
let mut args = vec!["exec".to_string(), "--process".to_string(), f.clone()];
if let Some(opts) = opts {
args.append(&mut tc!(opts.args(), &f));
}
args.push(id.to_string());
let mut cmd = self.command(&args)?;
match opts {
Some(ExecOpts { io: Some(io), .. }) => {
tc!(
io.set(&mut cmd).map_err(|e| Error::IoSet(e.to_string())),
&f
);
tc!(self.launch(cmd, true).await, &f);
io.close_after_start();
}
_ => {
tc!(self.launch(cmd, true).await, &f);
}
}
let _ = tokio::fs::remove_file(&f).await;
Ok(())
}
/// Send the specified signal to processes inside the container
pub async fn kill(&self, id: &str, sig: u32, opts: Option<&KillOpts>) -> Result<()> {
let mut args = vec!["kill".to_string()];
if let Some(opts) = opts {
args.append(&mut opts.args());
}
args.push(id.to_string());
args.push(sig.to_string());
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
/// List all containers associated with this runc instance
pub async fn list(&self) -> Result<Vec<Container>> {
let args = ["list".to_string(), "--format=json".to_string()];
let res = self.launch(self.command(&args)?, true).await?;
let output = res.output.trim();
// Ugly hack to work around golang
Ok(if output == "null" {
Vec::new()
} else {
serde_json::from_str(output).map_err(Error::JsonDeserializationFailed)?
})
}
/// Pause a container
pub async fn pause(&self, id: &str) -> Result<()> {
let args = ["pause".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
/// Resume a container
pub async fn resume(&self, id: &str) -> Result<()> {
let args = ["resume".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
pub async fn checkpoint(&self) -> Result<()> {
Err(Error::Unimplemented("checkpoint".to_string()))
}
pub async fn restore(&self) -> Result<()> {
Err(Error::Unimplemented("restore".to_string()))
}
/// List all the processes inside the container, returning their pids
pub async fn ps(&self, id: &str) -> Result<Vec<usize>> {
let args = [
"ps".to_string(),
"--format=json".to_string(),
id.to_string(),
];
let res = self.launch(self.command(&args)?, true).await?;
let output = res.output.trim();
// Ugly hack to work around golang
Ok(if output == "null" {
Vec::new()
} else {
serde_json::from_str(output).map_err(Error::JsonDeserializationFailed)?
})
}
/// Run the create, start, delete lifecycle of the container and return its exit status
pub async fn run<P>(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result<()>
where
P: AsRef<Path>,
{
let mut args = vec![
"run".to_string(),
"--bundle".to_string(),
utils::abs_string(bundle)?,
];
if let Some(opts) = opts {
args.append(&mut opts.args()?);
}
args.push(id.to_string());
let mut cmd = self.command(&args)?;
if let Some(CreateOpts { io: Some(io), .. }) = opts {
io.set(&mut cmd).map_err(|e| Error::IoSet(e.to_string()))?;
};
let _ = self.launch(cmd, true).await?;
Ok(())
}
/// Start an already created container
pub async fn start(&self, id: &str) -> Result<()> {
let args = vec!["start".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
/// Return the state of a container
pub async fn state(&self, id: &str) -> Result<Container> {
let args = vec!["state".to_string(), id.to_string()];
let res = self.launch(self.command(&args)?, true).await?;
serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed)
}
/// Return the latest statistics for a container
pub async fn stats(&self, id: &str) -> Result<events::Stats> {
let args = vec!["events".to_string(), "--stats".to_string(), id.to_string()];
let res = self.launch(self.command(&args)?, true).await?;
let event: events::Event =
serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed)?;
if let Some(stats) = event.stats {
Ok(stats)
} else {
Err(Error::MissingContainerStats)
}
}
/// Update a container with the provided resource spec
pub async fn update(&self, id: &str, resources: &LinuxResources) -> Result<()> {
let f = write_value_to_temp_file(resources).await?;
let args = [
"update".to_string(),
"--resources".to_string(),
f.to_string(),
id.to_string(),
];
let _ = tc!(self.launch(self.command(&args)?, true).await, &f);
let _ = tokio::fs::remove_file(&f).await;
Ok(())
}
}
#[derive(Debug)]
pub struct DefaultExecutor {}
#[cfg(feature = "async")]
#[async_trait]
impl Spawner for DefaultExecutor {
async fn execute(&self, cmd: Command) -> Result<(ExitStatus, u32, String, String)> {
let mut cmd = cmd;
let child = cmd.spawn().map_err(Error::ProcessSpawnFailed)?;
let pid = child.id().unwrap();
let result = child
.wait_with_output()
.await
.map_err(Error::InvalidCommand)?;
let status = result.status;
let stdout = String::from_utf8_lossy(&result.stdout).to_string();
let stderr = String::from_utf8_lossy(&result.stderr).to_string();
Ok((status, pid, stdout, stderr))
}
}
#[cfg(not(feature = "async"))]
impl Spawner for DefaultExecutor {
fn execute(&self, cmd: Command) -> Result<(ExitStatus, u32, String, String)> {
let mut cmd = cmd;
let child = cmd.spawn().map_err(Error::ProcessSpawnFailed)?;
let pid = child.id();
let result = child.wait_with_output().map_err(Error::InvalidCommand)?;
let status = result.status;
let stdout = String::from_utf8_lossy(&result.stdout).to_string();
let stderr = String::from_utf8_lossy(&result.stderr).to_string();
Ok((status, pid, stdout, stderr))
}
}
#[cfg(test)]
#[cfg(all(target_os = "linux", not(feature = "async")))]
mod tests {
use std::sync::Arc;
use super::{
io::{InheritedStdIo, PipedStdIo},
*,
};
fn ok_client() -> Runc {
GlobalOpts::new()
.command("/bin/true")
.build()
.expect("unable to create runc instance")
}
fn fail_client() -> Runc {
GlobalOpts::new()
.command("/bin/false")
.build()
.expect("unable to create runc instance")
}
fn echo_client() -> Runc {
GlobalOpts::new()
.command("/bin/echo")
.build()
.expect("unable to create runc instance")
}
fn dummy_process() -> Process {
serde_json::from_str(
"
{
\"user\": {
\"uid\": 1000,
\"gid\": 1000
},
\"cwd\": \"/path/to/dir\"
}",
)
.unwrap()
}
#[test]
fn test_create() {
let opts = CreateOpts::new();
let ok_runc = ok_client();
let response = ok_runc
.create("fake-id", "fake-bundle", Some(&opts))
.expect("true failed.");
assert_ne!(response.pid, 0);
assert!(response.status.success());
assert!(response.output.is_empty());
let fail_runc = fail_client();
match fail_runc.create("fake-id", "fake-bundle", Some(&opts)) {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
}
#[test]
fn test_run() {
let opts = CreateOpts::new();
let ok_runc = ok_client();
let response = ok_runc
.run("fake-id", "fake-bundle", Some(&opts))
.expect("true failed.");
assert_ne!(response.pid, 0);
assert!(response.status.success());
assert!(response.output.is_empty());
let fail_runc = fail_client();
match fail_runc.run("fake-id", "fake-bundle", Some(&opts)) {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
}
#[test]
fn test_exec() {
let opts = ExecOpts::new();
let ok_runc = ok_client();
let proc = dummy_process();
ok_runc
.exec("fake-id", &proc, Some(&opts))
.expect("true failed.");
eprintln!("ok_runc succeeded.");
let fail_runc = fail_client();
match fail_runc.exec("fake-id", &proc, Some(&opts)) {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
}
#[test]
fn test_delete() {
let opts = DeleteOpts::new();
let ok_runc = ok_client();
ok_runc
.delete("fake-id", Some(&opts))
.expect("true failed.");
eprintln!("ok_runc succeeded.");
let fail_runc = fail_client();
match fail_runc.delete("fake-id", Some(&opts)) {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
}
#[test]
fn test_output() {
// test create cmd with inherit Io, expect empty cmd output
let mut opts = CreateOpts::new();
opts.io = Some(Arc::new(InheritedStdIo::new().unwrap()));
let echo_runc = echo_client();
let response = echo_runc
.create("fake-id", "fake-bundle", Some(&opts))
.expect("echo failed.");
assert_ne!(response.pid, 0);
assert!(response.status.success());
assert!(response.output.is_empty());
// test create cmd with pipe Io, expect nonempty cmd output
let mut opts = CreateOpts::new();
opts.io = Some(Arc::new(PipedStdIo::new().unwrap()));
let echo_runc = echo_client();
let response = echo_runc
.create("fake-id", "fake-bundle", Some(&opts))
.expect("echo failed.");
assert_ne!(response.pid, 0);
assert!(response.status.success());
assert!(!response.output.is_empty());
}
}
/// Tokio tests
#[cfg(test)]
#[cfg(all(target_os = "linux", feature = "async"))]
mod tests {
use std::sync::Arc;
use super::{
io::{InheritedStdIo, PipedStdIo},
*,
};
fn ok_client() -> Runc {
GlobalOpts::new()
.command("/bin/true")
.build()
.expect("unable to create runc instance")
}
fn fail_client() -> Runc {
GlobalOpts::new()
.command("/bin/false")
.build()
.expect("unable to create runc instance")
}
fn echo_client() -> Runc {
GlobalOpts::new()
.command("/bin/echo")
.build()
.expect("unable to create runc instance")
}
#[tokio::test]
async fn test_async_create() {
let opts = CreateOpts::new();
let ok_runc = ok_client();
let ok_task = tokio::spawn(async move {
let response = ok_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
.expect("true failed.");
assert_ne!(response.pid, 0);
assert!(response.status.success());
assert!(response.output.is_empty());
});
let opts = CreateOpts::new();
let fail_runc = fail_client();
let fail_task = tokio::spawn(async move {
match fail_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
{
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
});
ok_task.await.expect("ok_task failed.");
fail_task.await.expect("fail_task unexpectedly succeeded.");
}
#[tokio::test]
async fn test_async_start() {
let ok_runc = ok_client();
let ok_task = tokio::spawn(async move {
ok_runc.start("fake-id").await.expect("true failed.");
eprintln!("ok_runc succeeded.");
});
let fail_runc = fail_client();
let fail_task = tokio::spawn(async move {
match fail_runc.start("fake-id").await {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
});
ok_task.await.expect("ok_task failed.");
fail_task.await.expect("fail_task unexpectedly succeeded.");
}
#[tokio::test]
async fn test_async_run() {
let opts = CreateOpts::new();
let ok_runc = ok_client();
tokio::spawn(async move {
ok_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
.expect("true failed.");
eprintln!("ok_runc succeeded.");
});
let opts = CreateOpts::new();
let fail_runc = fail_client();
tokio::spawn(async move {
match fail_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
{
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
})
.await
.expect("tokio spawn falied.");
}
#[tokio::test]
async fn test_async_delete() {
let opts = DeleteOpts::new();
let ok_runc = ok_client();
tokio::spawn(async move {
ok_runc
.delete("fake-id", Some(&opts))
.await
.expect("true failed.");
eprintln!("ok_runc succeeded.");
});
let opts = DeleteOpts::new();
let fail_runc = fail_client();
tokio::spawn(async move {
match fail_runc.delete("fake-id", Some(&opts)).await {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
})
.await
.expect("tokio spawn falied.");
}
#[tokio::test]
async fn test_async_output() {
// test create cmd with inherit Io, expect empty cmd output
let mut opts = CreateOpts::new();
opts.io = Some(Arc::new(InheritedStdIo::new().unwrap()));
let echo_runc = echo_client();
let response = echo_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
.expect("echo failed:");
assert_ne!(response.pid, 0);
assert!(response.status.success());
assert!(response.output.is_empty());
// test create cmd with pipe Io, expect nonempty cmd output
let mut opts = CreateOpts::new();
opts.io = Some(Arc::new(PipedStdIo::new().unwrap()));
let response = echo_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
.expect("echo failed:");
assert_ne!(response.pid, 0);
assert!(response.status.success());
assert!(!response.output.is_empty());
}
}

View File

@ -39,7 +39,7 @@ use std::{
time::Duration,
};
use crate::{error::Error, io::Io, utils, DefaultExecutor, LogFormat, Runc, Spawner};
use crate::{error::Error, utils, DefaultExecutor, Io, LogFormat, Runc, Spawner};
// constants for log format
pub const JSON: &str = "json";
@ -485,7 +485,7 @@ mod tests {
fn create_opts_test() {
assert_eq!(
CreateOpts::new().args().expect(ARGS_FAIL_MSG),
vec![String::new(); 0]
Vec::<String>::new()
);
assert_eq!(
@ -536,7 +536,7 @@ mod tests {
fn exec_opts_test() {
assert_eq!(
ExecOpts::new().args().expect(ARGS_FAIL_MSG),
vec![String::new(); 0]
Vec::<String>::new()
);
assert_eq!(
@ -576,10 +576,7 @@ mod tests {
#[test]
fn delete_opts_test() {
assert_eq!(
DeleteOpts::new().force(false).args(),
vec![String::new(); 0]
);
assert_eq!(DeleteOpts::new().force(false).args(), Vec::<String>::new());
assert_eq!(
DeleteOpts::new().force(true).args(),
@ -589,7 +586,7 @@ mod tests {
#[test]
fn kill_opts_test() {
assert_eq!(KillOpts::new().all(false).args(), vec![String::new(); 0]);
assert_eq!(KillOpts::new().all(false).args(), Vec::<String>::new());
assert_eq!(KillOpts::new().all(true).args(), vec!["--all".to_string()],);
}

View File

@ -13,8 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
#[cfg(not(feature = "async"))]
use std::io::{Read, Write};
use std::{
fmt::Debug,
fs::{File, OpenOptions},
@ -24,58 +23,10 @@ use std::{
sync::Mutex,
};
use log::debug;
use nix::unistd::{Gid, Uid};
use os_pipe::{PipeReader, PipeWriter};
#[cfg(feature = "async")]
use tokio::io::{AsyncRead, AsyncWrite};
use crate::Command;
pub trait Io: Debug + Send + Sync {
/// Return write side of stdin
#[cfg(not(feature = "async"))]
fn stdin(&self) -> Option<Box<dyn Write + Send + Sync>> {
None
}
/// Return read side of stdout
#[cfg(not(feature = "async"))]
fn stdout(&self) -> Option<Box<dyn Read + Send>> {
None
}
/// Return read side of stderr
#[cfg(not(feature = "async"))]
fn stderr(&self) -> Option<Box<dyn Read + Send>> {
None
}
/// Return write side of stdin
#[cfg(feature = "async")]
fn stdin(&self) -> Option<Box<dyn AsyncWrite + Send + Sync + Unpin>> {
None
}
/// Return read side of stdout
#[cfg(feature = "async")]
fn stdout(&self) -> Option<Box<dyn AsyncRead + Send + Sync + Unpin>> {
None
}
/// Return read side of stderr
#[cfg(feature = "async")]
fn stderr(&self) -> Option<Box<dyn AsyncRead + Send + Sync + Unpin>> {
None
}
/// Set IO for passed command.
/// Read side of stdin, write side of stdout and write side of stderr should be provided to command.
fn set(&self, cmd: &mut Command) -> Result<()>;
/// Only close write side (should be stdout/err "from" runc process)
fn close_after_start(&self);
}
use super::Io;
use crate::{Command, Pipe, PipedIo};
#[derive(Debug, Clone)]
pub struct IOOption {
@ -94,49 +45,28 @@ impl Default for IOOption {
}
}
/// Struct to represent a pipe that can be used to transfer stdio inputs and outputs.
///
/// With this Io driver, methods of [crate::Runc] may capture the output/error messages.
/// When one side of the pipe is closed, the state will be represented with [`None`].
#[derive(Debug)]
pub struct Pipe {
rd: PipeReader,
wr: PipeWriter,
}
#[derive(Debug)]
pub struct PipedIo {
stdin: Option<Pipe>,
stdout: Option<Pipe>,
stderr: Option<Pipe>,
}
impl Pipe {
fn new() -> std::io::Result<Self> {
let (rd, wr) = os_pipe::pipe()?;
Ok(Self { rd, wr })
}
}
impl PipedIo {
pub fn new(uid: u32, gid: u32, opts: &IOOption) -> std::io::Result<Self> {
Ok(Self {
stdin: Self::create_pipe(uid, gid, opts.open_stdin, true)?,
stdout: Self::create_pipe(uid, gid, opts.open_stdout, false)?,
stderr: Self::create_pipe(uid, gid, opts.open_stderr, false)?,
stdin: if opts.open_stdin {
Self::create_pipe(uid, gid, true)?
} else {
None
},
stdout: if opts.open_stdout {
Self::create_pipe(uid, gid, true)?
} else {
None
},
stderr: if opts.open_stderr {
Self::create_pipe(uid, gid, true)?
} else {
None
},
})
}
fn create_pipe(
uid: u32,
gid: u32,
enabled: bool,
stdin: bool,
) -> std::io::Result<Option<Pipe>> {
if !enabled {
return Ok(None);
}
fn create_pipe(uid: u32, gid: u32, stdin: bool) -> std::io::Result<Option<Pipe>> {
let pipe = Pipe::new()?;
let uid = Some(Uid::from_raw(uid));
let gid = Some(Gid::from_raw(gid));
@ -151,99 +81,6 @@ impl PipedIo {
}
}
impl Io for PipedIo {
#[cfg(not(feature = "async"))]
fn stdin(&self) -> Option<Box<dyn Write + Send + Sync>> {
self.stdin.as_ref().and_then(|pipe| {
pipe.wr
.try_clone()
.map(|x| Box::new(x) as Box<dyn Write + Send + Sync>)
.ok()
})
}
#[cfg(feature = "async")]
fn stdin(&self) -> Option<Box<dyn AsyncWrite + Send + Sync + Unpin>> {
self.stdin.as_ref().and_then(|pipe| {
let fd = pipe.wr.as_raw_fd();
tokio_pipe::PipeWrite::from_raw_fd_checked(fd)
.map(|x| Box::new(x) as Box<dyn AsyncWrite + Send + Sync + Unpin>)
.ok()
})
}
#[cfg(not(feature = "async"))]
fn stdout(&self) -> Option<Box<dyn Read + Send>> {
self.stdout.as_ref().and_then(|pipe| {
pipe.rd
.try_clone()
.map(|x| Box::new(x) as Box<dyn Read + Send>)
.ok()
})
}
#[cfg(feature = "async")]
fn stdout(&self) -> Option<Box<dyn AsyncRead + Send + Sync + Unpin>> {
self.stdout.as_ref().and_then(|pipe| {
let fd = pipe.rd.as_raw_fd();
tokio_pipe::PipeRead::from_raw_fd_checked(fd)
.map(|x| Box::new(x) as Box<dyn AsyncRead + Send + Sync + Unpin>)
.ok()
})
}
#[cfg(not(feature = "async"))]
fn stderr(&self) -> Option<Box<dyn Read + Send>> {
self.stderr.as_ref().and_then(|pipe| {
pipe.rd
.try_clone()
.map(|x| Box::new(x) as Box<dyn Read + Send>)
.ok()
})
}
#[cfg(feature = "async")]
fn stderr(&self) -> Option<Box<dyn AsyncRead + Send + Sync + Unpin>> {
self.stderr.as_ref().and_then(|pipe| {
let fd = pipe.rd.as_raw_fd();
tokio_pipe::PipeRead::from_raw_fd_checked(fd)
.map(|x| Box::new(x) as Box<dyn AsyncRead + Send + Sync + Unpin>)
.ok()
})
}
// Note that this internally use [`std::fs::File`]'s `try_clone()`.
// Thus, the files passed to commands will be not closed after command exit.
fn set(&self, cmd: &mut Command) -> std::io::Result<()> {
if let Some(p) = self.stdin.as_ref() {
let pr = p.rd.try_clone()?;
cmd.stdin(pr);
}
if let Some(p) = self.stdout.as_ref() {
let pw = p.wr.try_clone()?;
cmd.stdout(pw);
}
if let Some(p) = self.stderr.as_ref() {
let pw = p.wr.try_clone()?;
cmd.stdout(pw);
}
Ok(())
}
fn close_after_start(&self) {
if let Some(p) = self.stdout.as_ref() {
nix::unistd::close(p.wr.as_raw_fd()).unwrap_or_else(|e| debug!("close stdout: {}", e));
}
if let Some(p) = self.stderr.as_ref() {
nix::unistd::close(p.wr.as_raw_fd()).unwrap_or_else(|e| debug!("close stderr: {}", e));
}
}
}
/// IO driver to direct output/error messages to /dev/null.
///
/// With this Io driver, all methods of [crate::Runc] can't capture the output/error messages.
@ -375,9 +212,10 @@ mod tests {
}
#[cfg(target_os = "linux")]
#[cfg(not(feature = "async"))]
#[test]
fn test_create_piped_io() {
use std::io::{Read, Write};
let opts = IOOption::default();
let uid = nix::unistd::getuid();
let gid = nix::unistd::getgid();

View File

@ -0,0 +1,119 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pub mod io;
mod pipe;
mod runc;
use std::{
fmt::Debug,
io::{Read, Result, Write},
os::fd::AsRawFd,
};
use log::debug;
pub use pipe::Pipe;
pub use runc::{DefaultExecutor, Spawner};
use crate::Command;
pub trait Io: Debug + Send + Sync {
/// Return write side of stdin
fn stdin(&self) -> Option<Box<dyn Write + Send + Sync>> {
None
}
/// Return read side of stdout
fn stdout(&self) -> Option<Box<dyn Read + Send>> {
None
}
/// Return read side of stderr
fn stderr(&self) -> Option<Box<dyn Read + Send>> {
None
}
/// Set IO for passed command.
/// Read side of stdin, write side of stdout and write side of stderr should be provided to command.
fn set(&self, cmd: &mut Command) -> Result<()>;
/// Only close write side (should be stdout/err "from" runc process)
fn close_after_start(&self);
}
#[derive(Debug)]
pub struct PipedIo {
pub stdin: Option<Pipe>,
pub stdout: Option<Pipe>,
pub stderr: Option<Pipe>,
}
impl Io for PipedIo {
fn stdin(&self) -> Option<Box<dyn Write + Send + Sync>> {
self.stdin.as_ref().and_then(|pipe| {
pipe.wr
.try_clone()
.map(|x| Box::new(x) as Box<dyn Write + Send + Sync>)
.ok()
})
}
fn stdout(&self) -> Option<Box<dyn Read + Send>> {
self.stdout.as_ref().and_then(|pipe| {
pipe.rd
.try_clone()
.map(|x| Box::new(x) as Box<dyn Read + Send>)
.ok()
})
}
fn stderr(&self) -> Option<Box<dyn Read + Send>> {
self.stderr.as_ref().and_then(|pipe| {
pipe.rd
.try_clone()
.map(|x| Box::new(x) as Box<dyn Read + Send>)
.ok()
})
}
// Note that this internally use [`std::fs::File`]'s `try_clone()`.
// Thus, the files passed to commands will be not closed after command exit.
fn set(&self, cmd: &mut Command) -> std::io::Result<()> {
if let Some(p) = self.stdin.as_ref() {
let pr = p.rd.try_clone()?;
cmd.stdin(pr);
}
if let Some(p) = self.stdout.as_ref() {
let pw = p.wr.try_clone()?;
cmd.stdout(pw);
}
if let Some(p) = self.stderr.as_ref() {
let pw = p.wr.try_clone()?;
cmd.stdout(pw);
}
Ok(())
}
fn close_after_start(&self) {
if let Some(p) = self.stdout.as_ref() {
nix::unistd::close(p.wr.as_raw_fd()).unwrap_or_else(|e| debug!("close stdout: {}", e));
}
if let Some(p) = self.stderr.as_ref() {
nix::unistd::close(p.wr.as_raw_fd()).unwrap_or_else(|e| debug!("close stderr: {}", e));
}
}
}

View File

@ -14,7 +14,17 @@
limitations under the License.
*/
#[cfg(windows)]
pub(crate) mod windows;
#[cfg(windows)]
pub use crate::sys::windows::NamedPipeLogger;
use os_pipe::{pipe, PipeReader, PipeWriter};
#[derive(Debug)]
pub struct Pipe {
pub rd: PipeReader,
pub wr: PipeWriter,
}
impl Pipe {
pub fn new() -> std::io::Result<Self> {
let (rd, wr) = pipe()?;
Ok(Self { rd, wr })
}
}

View File

@ -0,0 +1,445 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
use std::{fmt::Debug, path::Path, process::ExitStatus};
use oci_spec::runtime::{LinuxResources, Process};
use crate::{
container::Container,
error::Error,
events,
options::*,
utils::{self, write_value_to_temp_file},
Command, Response, Result, Runc,
};
impl Runc {
pub(crate) fn launch(&self, cmd: Command, combined_output: bool) -> Result<Response> {
let (status, pid, stdout, stderr) = self.spawner.execute(cmd)?;
if status.success() {
let output = if combined_output {
stdout + stderr.as_str()
} else {
stdout
};
Ok(Response {
pid,
status,
output,
})
} else {
Err(Error::CommandFailed {
status,
stdout,
stderr,
})
}
}
/// Create a new container
pub fn create<P>(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result<Response>
where
P: AsRef<Path>,
{
let mut args = vec![
"create".to_string(),
"--bundle".to_string(),
utils::abs_string(bundle)?,
];
if let Some(opts) = opts {
args.append(&mut opts.args()?);
}
args.push(id.to_string());
let mut cmd = self.command(&args)?;
match opts {
Some(CreateOpts { io: Some(io), .. }) => {
io.set(&mut cmd).map_err(|e| Error::IoSet(e.to_string()))?;
let res = self.launch(cmd, true)?;
io.close_after_start();
Ok(res)
}
_ => self.launch(cmd, true),
}
}
/// Delete a container
pub fn delete(&self, id: &str, opts: Option<&DeleteOpts>) -> Result<()> {
let mut args = vec!["delete".to_string()];
if let Some(opts) = opts {
args.append(&mut opts.args());
}
args.push(id.to_string());
self.launch(self.command(&args)?, true)?;
Ok(())
}
/// Execute an additional process inside the container
pub fn exec(&self, id: &str, spec: &Process, opts: Option<&ExecOpts>) -> Result<()> {
let (_temp_file, filename) = write_value_to_temp_file(spec)?;
let mut args = vec!["exec".to_string(), "--process".to_string(), filename];
if let Some(opts) = opts {
args.append(&mut opts.args()?);
}
args.push(id.to_string());
let mut cmd = self.command(&args)?;
match opts {
Some(ExecOpts { io: Some(io), .. }) => {
io.set(&mut cmd).map_err(|e| Error::IoSet(e.to_string()))?;
self.launch(cmd, true)?;
io.close_after_start();
}
_ => {
self.launch(cmd, true)?;
}
}
Ok(())
}
/// Send the specified signal to processes inside the container
pub fn kill(&self, id: &str, sig: u32, opts: Option<&KillOpts>) -> Result<()> {
let mut args = vec!["kill".to_string()];
if let Some(opts) = opts {
args.append(&mut opts.args());
}
args.push(id.to_string());
args.push(sig.to_string());
let _ = self.launch(self.command(&args)?, true)?;
Ok(())
}
/// List all containers associated with this runc instance
pub fn list(&self) -> Result<Vec<Container>> {
let args = ["list".to_string(), "--format=json".to_string()];
let res = self.launch(self.command(&args)?, true)?;
let output = res.output.trim();
// Ugly hack to work around golang
Ok(if output == "null" {
Vec::new()
} else {
serde_json::from_str(output).map_err(Error::JsonDeserializationFailed)?
})
}
/// Pause a container
pub fn pause(&self, id: &str) -> Result<()> {
let args = ["pause".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true)?;
Ok(())
}
/// Resume a container
pub fn resume(&self, id: &str) -> Result<()> {
let args = ["resume".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true)?;
Ok(())
}
pub fn checkpoint(&self) -> Result<()> {
Err(Error::Unimplemented("checkpoint".to_string()))
}
pub fn restore(&self) -> Result<()> {
Err(Error::Unimplemented("restore".to_string()))
}
/// List all the processes inside the container, returning their pids
pub fn ps(&self, id: &str) -> Result<Vec<usize>> {
let args = [
"ps".to_string(),
"--format=json".to_string(),
id.to_string(),
];
let res = self.launch(self.command(&args)?, false)?;
let output = res.output.trim();
// Ugly hack to work around golang
Ok(if output == "null" {
Vec::new()
} else {
serde_json::from_str(output).map_err(Error::JsonDeserializationFailed)?
})
}
/// Run the create, start, delete lifecycle of the container and return its exit status
pub fn run<P>(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result<Response>
where
P: AsRef<Path>,
{
let mut args = vec![
"run".to_string(),
"--bundle".to_string(),
utils::abs_string(bundle)?,
];
if let Some(opts) = opts {
args.append(&mut opts.args()?);
}
args.push(id.to_string());
let mut cmd = self.command(&args)?;
if let Some(CreateOpts { io: Some(io), .. }) = opts {
io.set(&mut cmd).map_err(|e| Error::IoSet(e.to_string()))?;
};
self.launch(cmd, true)
}
/// Start an already created container
pub fn start(&self, id: &str) -> Result<Response> {
let args = ["start".to_string(), id.to_string()];
self.launch(self.command(&args)?, true)
}
/// Return the state of a container
pub fn state(&self, id: &str) -> Result<Container> {
let args = ["state".to_string(), id.to_string()];
let res = self.launch(self.command(&args)?, true)?;
serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed)
}
/// Return the latest statistics for a container
pub fn stats(&self, id: &str) -> Result<events::Stats> {
let args = vec!["events".to_string(), "--stats".to_string(), id.to_string()];
let res = self.launch(self.command(&args)?, true)?;
let event: events::Event =
serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed)?;
if let Some(stats) = event.stats {
Ok(stats)
} else {
Err(Error::MissingContainerStats)
}
}
/// Update a container with the provided resource spec
pub fn update(&self, id: &str, resources: &LinuxResources) -> Result<()> {
let (_temp_file, filename) = write_value_to_temp_file(resources)?;
let args = [
"update".to_string(),
"--resources".to_string(),
filename,
id.to_string(),
];
self.launch(self.command(&args)?, true)?;
Ok(())
}
}
pub trait Spawner: Debug {
fn execute(&self, cmd: Command) -> Result<(ExitStatus, u32, String, String)>;
}
#[derive(Debug)]
pub struct DefaultExecutor {}
impl Spawner for DefaultExecutor {
fn execute(&self, cmd: Command) -> Result<(ExitStatus, u32, String, String)> {
let mut cmd = cmd;
let child = cmd.spawn().map_err(Error::ProcessSpawnFailed)?;
let pid = child.id();
let result = child.wait_with_output().map_err(Error::InvalidCommand)?;
let status = result.status;
let stdout = String::from_utf8_lossy(&result.stdout).to_string();
let stderr = String::from_utf8_lossy(&result.stderr).to_string();
Ok((status, pid, stdout, stderr))
}
}
#[cfg(test)]
#[cfg(target_os = "linux")]
mod tests {
use std::sync::Arc;
use oci_spec::runtime::Process;
use crate::{
error::Error,
io::{InheritedStdIo, PipedStdIo},
options::{CreateOpts, DeleteOpts, ExecOpts, GlobalOpts},
Runc,
};
fn ok_client() -> Runc {
GlobalOpts::new()
.command("/bin/true")
.build()
.expect("unable to create runc instance")
}
fn fail_client() -> Runc {
GlobalOpts::new()
.command("/bin/false")
.build()
.expect("unable to create runc instance")
}
fn echo_client() -> Runc {
GlobalOpts::new()
.command("/bin/echo")
.build()
.expect("unable to create runc instance")
}
fn dummy_process() -> Process {
serde_json::from_str(
"
{
\"user\": {
\"uid\": 1000,
\"gid\": 1000
},
\"cwd\": \"/path/to/dir\"
}",
)
.unwrap()
}
#[test]
fn test_create() {
let opts = CreateOpts::new();
let ok_runc = ok_client();
let response = ok_runc
.create("fake-id", "fake-bundle", Some(&opts))
.expect("true failed.");
assert_ne!(response.pid, 0);
assert!(response.status.success());
assert!(response.output.is_empty());
let fail_runc = fail_client();
match fail_runc.create("fake-id", "fake-bundle", Some(&opts)) {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
}
#[test]
fn test_run() {
let opts = CreateOpts::new();
let ok_runc = ok_client();
let response = ok_runc
.run("fake-id", "fake-bundle", Some(&opts))
.expect("true failed.");
assert_ne!(response.pid, 0);
assert!(response.status.success());
assert!(response.output.is_empty());
let fail_runc = fail_client();
match fail_runc.run("fake-id", "fake-bundle", Some(&opts)) {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
}
#[test]
fn test_exec() {
let opts = ExecOpts::new();
let ok_runc = ok_client();
let proc = dummy_process();
ok_runc
.exec("fake-id", &proc, Some(&opts))
.expect("true failed.");
eprintln!("ok_runc succeeded.");
let fail_runc = fail_client();
match fail_runc.exec("fake-id", &proc, Some(&opts)) {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
}
#[test]
fn test_delete() {
let opts = DeleteOpts::new();
let ok_runc = ok_client();
ok_runc
.delete("fake-id", Some(&opts))
.expect("true failed.");
eprintln!("ok_runc succeeded.");
let fail_runc = fail_client();
match fail_runc.delete("fake-id", Some(&opts)) {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
}
#[test]
fn test_output() {
// test create cmd with inherit Io, expect empty cmd output
let mut opts = CreateOpts::new();
opts.io = Some(Arc::new(InheritedStdIo::new().unwrap()));
let echo_runc = echo_client();
let response = echo_runc
.create("fake-id", "fake-bundle", Some(&opts))
.expect("echo failed.");
assert_ne!(response.pid, 0);
assert!(response.status.success());
assert!(response.output.is_empty());
// test create cmd with pipe Io, expect nonempty cmd output
let mut opts = CreateOpts::new();
opts.io = Some(Arc::new(PipedStdIo::new().unwrap()));
let echo_runc = echo_client();
let response = echo_runc
.create("fake-id", "fake-bundle", Some(&opts))
.expect("echo failed.");
assert_ne!(response.pid, 0);
assert!(response.status.success());
assert!(!response.output.is_empty());
}
}

View File

@ -98,6 +98,7 @@ pub async fn write_value_to_temp_file<T: Serialize>(value: &T) -> Result<String,
let filename = format!("{}/runc-process-{}", xdg_runtime_dir(), Uuid::new_v4());
let mut f = tokio::fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&filename)
.await

View File

@ -1,6 +1,6 @@
[package]
name = "containerd-shim-protos"
version = "0.7.1"
version = "0.8.0"
authors = [
"Maksym Pavlenko <pavlenko.maksym@gmail.com>",
"The containerd Authors",
@ -49,11 +49,11 @@ required-features = ["async"]
[dependencies]
async-trait = { workspace = true, optional = true }
protobuf = "=3.5"
ttrpc = "0.8.2"
protobuf = "3.7.2"
ttrpc = "0.8.3"
[build-dependencies]
ttrpc-codegen = "0.4.2"
ttrpc-codegen = "0.6.0"
[dev-dependencies]
ctrlc = { version = "3.0", features = ["termination"] }

View File

@ -32,6 +32,7 @@ fn main() {
"vendor/github.com/containerd/containerd/protobuf/plugin/fieldpath.proto",
"vendor/github.com/containerd/containerd/api/types/mount.proto",
"vendor/github.com/containerd/containerd/api/types/task/task.proto",
"vendor/github.com/containerd/containerd/api/types/introspection.proto",
#[cfg(feature = "sandbox")]
"vendor/github.com/containerd/containerd/api/types/platform.proto",
],

View File

@ -34,6 +34,9 @@ pub mod fieldpath {
include!(concat!(env!("OUT_DIR"), "/types/fieldpath.rs"));
}
pub mod introspection {
include!(concat!(env!("OUT_DIR"), "/types/introspection.rs"));
}
#[cfg(feature = "sandbox")]
pub mod platform {
include!(concat!(env!("OUT_DIR"), "/types/platform.rs"));

View File

@ -0,0 +1,46 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.types;
import "google/protobuf/any.proto";
option go_package = "github.com/containerd/containerd/api/types;types";
message RuntimeRequest {
string runtime_path = 1;
// Options correspond to CreateTaskRequest.options.
// This is needed to pass the runc binary path, etc.
google.protobuf.Any options = 2;
}
message RuntimeVersion {
string version = 1;
string revision = 2;
}
message RuntimeInfo {
string name = 1;
RuntimeVersion version = 2;
// Options correspond to RuntimeInfoRequest.Options (contains runc binary path, etc.)
google.protobuf.Any options = 3;
// OCI-compatible runtimes should use https://github.com/opencontainers/runtime-spec/blob/main/features.md
google.protobuf.Any features = 4;
// Annotations of the shim. Irrelevant to features.Annotations.
map<string, string> annotations = 5;
}

View File

@ -1,6 +1,6 @@
[package]
name = "containerd-shim"
version = "0.7.2"
version = "0.8.0"
authors = [
"Maksym Pavlenko <pavlenko.maksym@gmail.com>",
"The containerd Authors",
@ -19,7 +19,6 @@ async = [
"async-trait",
"containerd-shim-protos/async",
"futures",
"signal-hook-tokio",
"tokio",
]
tracing = ["dep:tracing"]
@ -34,9 +33,11 @@ name = "windows-log-reader"
path = "examples/windows_log_reader.rs"
[dependencies]
containerd-shim-protos = { path = "../shim-protos", version = "0.7.0" }
which = "7.0.1"
containerd-shim-protos = { path = "../shim-protos", version = "0.8.0" }
go-flag = "0.1.0"
lazy_static = "1.4.0"
sha2 = "0.10.2"
libc.workspace = true
log = { workspace = true, features = ["std", "kv_unstable" ] }
nix = { workspace = true, features = [
@ -53,6 +54,7 @@ prctl.workspace = true
signal-hook = "0.3.13"
serde.workspace = true
serde_json.workspace = true
tempfile.workspace = true
thiserror.workspace = true
time.workspace = true
@ -62,20 +64,13 @@ tracing = { version = "0.1", optional = true }
# Async dependencies
async-trait = { workspace = true, optional = true }
futures = { workspace = true, optional = true }
signal-hook-tokio = { version = "0.3.1", optional = true, features = [
"futures-v0_3",
] }
tokio = { workspace = true, features = ["full"], optional = true }
[target.'cfg(target_os = "linux")'.dependencies]
cgroups-rs.workspace = true
[target.'cfg(unix)'.dependencies]
command-fds = "0.3.0"
[target.'cfg(windows)'.dependencies]
mio = { version = "1.0", features = ["os-ext", "os-poll"] }
os_pipe.workspace = true
windows-sys = { version = "0.52.0", features = [
"Win32_Foundation",
"Win32_System_WindowsProgramming",

View File

@ -175,7 +175,7 @@ $ cat log
$env:TTRPC_ADDRESS="\\.\pipe\containerd-containerd.ttrpc"
$ cargo run --example skeleton -- -namespace default -id 1234 -address "\\.\pipe\containerd-containerd" start
\\.\pipe\containerd-shim-17630016127144989388-pipe
\\.\pipe\containerd-shim-bc764c65e177434fcefe8257dc440be8b8acf7c96156320d965938f7e9ae1a35-pipe
# (Optional) Run the log collector in a separate command window
# note: log reader won't work if containerd is connected to the named pipe, this works when running manually to help debug locally
@ -183,8 +183,8 @@ $ cargo run --example windows-log-reader \\.\pipe\containerd-shim-default-1234-l
Reading logs from: \\.\pipe\containerd-shim-default-1234-log
<logs will appear after next command>
$ cargo run --example shim-proto-connect \\.\pipe\containerd-shim-17630016127144989388-pipe
Connecting to \\.\pipe\containerd-shim-17630016127144989388-pipe...
$ cargo run --example shim-proto-connect \\.\pipe\containerd-shim-bc764c65e177434fcefe8257dc440be8b8acf7c96156320d965938f7e9ae1a35-pipe
Connecting to \\.\pipe\containerd-shim-bc764c65e177434fcefe8257dc440be8b8acf7c96156320d965938f7e9ae1a35-pipe...
Sending `Connect` request...
Connect response: version: "example"
Sending `Shutdown` request...

View File

@ -41,6 +41,8 @@ pub struct Flags {
pub action: String,
/// Version of the shim.
pub version: bool,
/// get the option protobuf from stdin, print the shim info protobuf to stdout, and exit
pub info: bool,
}
/// Parses command line arguments passed to the shim.
@ -57,10 +59,11 @@ pub fn parse<S: AsRef<OsStr>>(args: &[S]) -> Result<Flags> {
f.add_flag("bundle", &mut flags.bundle);
f.add_flag("address", &mut flags.address);
f.add_flag("publish-binary", &mut flags.publish_binary);
f.add_flag("info", &mut flags.info);
})
.map_err(|e| Error::InvalidArgument(e.to_string()))?;
if let Some(action) = args.get(0) {
if let Some(action) = args.first() {
flags.action = action.into();
}

View File

@ -15,27 +15,28 @@
*/
use std::{
convert::TryFrom,
env,
io::Read,
os::unix::{fs::FileTypeExt, net::UnixListener},
path::Path,
process,
process::{Command, Stdio},
process::{self, Command as StdCommand, Stdio},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
task::{ready, Poll},
};
use async_trait::async_trait;
use command_fds::{CommandFdExt, FdMapping};
use containerd_shim_protos::{
api::DeleteResponse,
protobuf::Message,
protobuf::{well_known_types::any::Any, Message, MessageField},
shim::oci::Options,
shim_async::{create_task, Client, Task},
ttrpc::r#async::Server,
types::introspection::{self, RuntimeInfo},
};
use futures::StreamExt;
use futures::stream::{poll_fn, BoxStream, SelectAll, StreamExt};
use libc::{SIGCHLD, SIGINT, SIGPIPE, SIGTERM};
use log::{debug, error, info, warn};
use nix::{
@ -46,8 +47,11 @@ use nix::{
},
unistd::Pid,
};
use signal_hook_tokio::Signals;
use tokio::{io::AsyncWriteExt, sync::Notify};
use oci_spec::runtime::Features;
use tokio::{io::AsyncWriteExt, process::Command, sync::Notify};
use which::which;
const DEFAULT_BINARY_NAME: &str = "runc";
use crate::{
args,
@ -55,7 +59,7 @@ use crate::{
error::{Error, Result},
logger, parse_sockaddr, reap, socket_address,
util::{asyncify, read_file_to_str, write_str_to_file},
Config, Flags, StartOpts, SOCKET_FD, TTRPC_ADDRESS,
Config, Flags, StartOpts, TTRPC_ADDRESS,
};
pub mod monitor;
@ -109,6 +113,51 @@ where
process::exit(1);
}
}
/// get runtime info
pub fn run_info() -> Result<RuntimeInfo> {
let mut info = introspection::RuntimeInfo {
name: "containerd-shim-runc-v2-rs".to_string(),
version: MessageField::some(introspection::RuntimeVersion {
version: env!("CARGO_PKG_VERSION").to_string(),
revision: String::default(),
..Default::default()
}),
..Default::default()
};
let mut binary_name = DEFAULT_BINARY_NAME.to_string();
let mut data: Vec<u8> = Vec::new();
std::io::stdin()
.read_to_end(&mut data)
.map_err(io_error!(e, "read stdin"))?;
// get BinaryName from stdin
if !data.is_empty() {
let opts =
Any::parse_from_bytes(&data).and_then(|any| Options::parse_from_bytes(&any.value))?;
if !opts.binary_name().is_empty() {
binary_name = opts.binary_name().to_string();
}
}
let binary_path = which(binary_name).unwrap();
// get features
let output = StdCommand::new(binary_path)
.arg("features")
.output()
.unwrap();
let features: Features = serde_json::from_str(&String::from_utf8_lossy(&output.stdout))?;
// set features
let features_any = Any {
type_url: "types.containerd.io/opencontainers/runtime-spec/1/features/Features".to_string(),
// features to json
value: serde_json::to_vec(&features)?,
..Default::default()
};
info.features = MessageField::some(features_any);
Ok(info)
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "info"))]
async fn bootstrap<T>(runtime_id: &str, opts: Option<Config>) -> Result<()>
@ -167,6 +216,12 @@ where
Ok(())
}
_ => {
if flags.socket.is_empty() {
return Err(Error::InvalidArgument(String::from(
"Shim socket cannot be empty",
)));
}
if !config.no_setup_logger {
logger::init(
flags.debug,
@ -177,13 +232,18 @@ where
}
let publisher = RemotePublisher::new(&ttrpc_address).await?;
let task = shim.create_task_service(publisher).await;
let task_service = create_task(Arc::new(task));
let mut server = Server::new().register_service(task_service);
server = server.add_listener(SOCKET_FD)?;
server = server.set_domain_unix();
let task = Box::new(shim.create_task_service(publisher).await)
as Box<dyn containerd_shim_protos::shim_async::Task + Send + Sync>;
let task_service = create_task(Arc::from(task));
let Some(mut server) = create_server_with_retry(&flags).await? else {
signal_server_started();
return Ok(());
};
server = server.register_service(task_service);
server.start().await?;
signal_server_started();
info!("Shim successfully started, waiting for exit signal...");
tokio::spawn(async move {
handle_signals(signals).await;
@ -247,38 +307,18 @@ pub async fn spawn(opts: StartOpts, grouping: &str, vars: Vec<(&str, &str)>) ->
let cwd = env::current_dir().map_err(io_error!(e, ""))?;
let address = socket_address(&opts.address, &opts.namespace, grouping);
// Create socket and prepare listener.
// We'll use `add_listener` when creating TTRPC server.
let listener = match start_listener(&address).await {
Ok(l) => l,
Err(e) => {
if let Error::IoError {
err: ref io_err, ..
} = e
{
if io_err.kind() != std::io::ErrorKind::AddrInUse {
return Err(e);
};
}
if let Ok(()) = wait_socket_working(&address, 5, 200).await {
write_str_to_file("address", &address).await?;
return Ok(address);
}
remove_socket(&address).await?;
start_listener(&address).await?
}
};
// Activation pattern comes from the hcsshim: https://github.com/microsoft/hcsshim/blob/v0.10.0-rc.7/cmd/containerd-shim-runhcs-v1/serve.go#L57-L70
// another way to do it would to create named pipe and pass it to the child process through handle inheritence but that would require duplicating
// the logic in Rust's 'command' for process creation. There is an issue in Rust to make it simplier to specify handle inheritence and this could
// be revisited once https://github.com/rust-lang/rust/issues/54760 is implemented.
// tokio::process::Command do not have method `fd_mappings`,
// and the `spawn()` is also not an async method,
// so we use the std::process::Command here
let mut command = Command::new(cmd);
command
.current_dir(cwd)
.stdout(Stdio::null())
.stdout(Stdio::piped())
.stdin(Stdio::null())
.stderr(Stdio::null())
.envs(vars)
.args([
"-namespace",
&opts.namespace,
@ -286,31 +326,139 @@ pub async fn spawn(opts: StartOpts, grouping: &str, vars: Vec<(&str, &str)>) ->
&opts.id,
"-address",
&opts.address,
])
.fd_mappings(vec![FdMapping {
parent_fd: listener.into(),
child_fd: SOCKET_FD,
}])?;
"-socket",
&address,
]);
if opts.debug {
command.arg("-debug");
}
command.envs(vars);
let _child = command.spawn().map_err(io_error!(e, "spawn shim"))?;
let mut child = command.spawn().map_err(io_error!(e, "spawn shim"))?;
#[cfg(target_os = "linux")]
crate::cgroup::set_cgroup_and_oom_score(_child.id())?;
crate::cgroup::set_cgroup_and_oom_score(child.id().unwrap())?;
let mut reader = child.stdout.take().unwrap();
tokio::io::copy(&mut reader, &mut tokio::io::stderr())
.await
.unwrap();
Ok(address)
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "info"))]
fn setup_signals_tokio(config: &Config) -> Signals {
if config.no_reaper {
Signals::new([SIGTERM, SIGINT, SIGPIPE]).expect("new signal failed")
} else {
Signals::new([SIGTERM, SIGINT, SIGPIPE, SIGCHLD]).expect("new signal failed")
async fn create_server(flags: &args::Flags) -> Result<Server> {
use std::os::fd::IntoRawFd;
let listener = start_listener(&flags.socket).await?;
let mut server = Server::new();
server = server.add_listener(listener.into_raw_fd())?;
server = server.set_domain_unix();
Ok(server)
}
async fn create_server_with_retry(flags: &args::Flags) -> Result<Option<Server>> {
// Really try to create a server.
let server = match create_server(flags).await {
Ok(server) => server,
Err(Error::IoError { err, .. }) if err.kind() == std::io::ErrorKind::AddrInUse => {
// If the address is already in use then make sure it is up and running and return the address
// This allows for running a single shim per container scenarios
if let Ok(()) = wait_socket_working(&flags.socket, 5, 200).await {
write_str_to_file("address", &flags.socket).await?;
return Ok(None);
}
remove_socket(&flags.socket).await?;
create_server(flags).await?
}
Err(e) => return Err(e),
};
Ok(Some(server))
}
fn signal_server_started() {
use libc::{dup2, STDERR_FILENO, STDOUT_FILENO};
unsafe {
if dup2(STDERR_FILENO, STDOUT_FILENO) < 0 {
panic!("Error closing pipe: {}", std::io::Error::last_os_error())
}
}
}
#[cfg(unix)]
fn signal_stream(kind: i32) -> std::io::Result<BoxStream<'static, i32>> {
use tokio::signal::unix::{signal, SignalKind};
let kind = SignalKind::from_raw(kind);
signal(kind).map(|mut sig| {
// The object returned by `signal` is not a `Stream`.
// The `poll_fn` function constructs a `Stream` based on a polling function.
// We need to create a `Stream` so that we can use the `SelectAll` stream "merge"
// all the signal streams.
poll_fn(move |cx| {
ready!(sig.poll_recv(cx));
Poll::Ready(Some(kind.as_raw_value()))
})
.boxed()
})
}
#[cfg(windows)]
fn signal_stream(kind: i32) -> std::io::Result<BoxStream<'static, i32>> {
use tokio::signal::windows::ctrl_c;
// Windows doesn't have similar signal like SIGCHLD
// We could implement something if required but for now
// just implement support for SIGINT
if kind != SIGINT {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Invalid signal {kind}"),
));
}
ctrl_c().map(|mut sig| {
// The object returned by `signal` is not a `Stream`.
// The `poll_fn` function constructs a `Stream` based on a polling function.
// We need to create a `Stream` so that we can use the `SelectAll` stream "merge"
// all the signal streams.
poll_fn(move |cx| {
ready!(sig.poll_recv(cx));
Poll::Ready(Some(kind))
})
.boxed()
})
}
type Signals = SelectAll<BoxStream<'static, i32>>;
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "info"))]
fn setup_signals_tokio(config: &Config) -> Signals {
#[cfg(unix)]
let signals: &[i32] = if config.no_reaper {
&[SIGTERM, SIGINT, SIGPIPE]
} else {
&[SIGTERM, SIGINT, SIGPIPE, SIGCHLD]
};
// Windows doesn't have similar signal like SIGCHLD
// We could implement something if required but for now
// just listen for SIGINT
// Note: see comment at the counterpart in synchronous/mod.rs for details.
#[cfg(windows)]
let signals: &[i32] = &[SIGINT];
let signals: Vec<_> = signals
.iter()
.copied()
.map(signal_stream)
.collect::<std::io::Result<_>>()
.expect("signal setup failed");
SelectAll::from_iter(signals)
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "info"))]
async fn handle_signals(signals: Signals) {
let mut signals = signals.fuse();
@ -322,14 +470,7 @@ async fn handle_signals(signals: Signals) {
}
SIGCHLD => loop {
// Note: see comment at the counterpart in synchronous/mod.rs for details.
match asyncify(move || {
Ok(wait::waitpid(
Some(Pid::from_raw(-1)),
Some(WaitPidFlag::WNOHANG),
)?)
})
.await
{
match wait::waitpid(Some(Pid::from_raw(-1)), Some(WaitPidFlag::WNOHANG)) {
Ok(WaitStatus::Exited(pid, status)) => {
monitor_notify_by_pid(pid.as_raw(), status)
.await
@ -345,7 +486,7 @@ async fn handle_signals(signals: Signals) {
Ok(WaitStatus::StillAlive) => {
break;
}
Err(Error::Nix(Errno::ECHILD)) => {
Err(Errno::ECHILD) => {
break;
}
Err(e) => {

View File

@ -117,7 +117,7 @@ impl Monitor {
subject: subject.clone(),
exit_code,
})
.map_err(other_error!(e, "failed to send exit code"));
.map_err(other_error!("failed to send exit code"));
results.push(res);
}
}

View File

@ -16,24 +16,42 @@
use std::os::unix::io::RawFd;
use async_trait::async_trait;
use containerd_shim_protos::{
api::Empty,
api::Envelope,
protobuf::MessageDyn,
shim::events,
shim_async::{Client, Events, EventsClient},
shim_async::{Client, EventsClient},
ttrpc,
ttrpc::{context::Context, r#async::TtrpcContext},
ttrpc::context::Context,
};
use log::{debug, error, warn};
use tokio::sync::mpsc;
use crate::{
error::Result,
error::{self, Result},
util::{asyncify, connect, convert_to_any, timestamp},
};
/// The publisher reports events and uses a queue to retry the event reporting.
/// The maximum number of attempts to report is 5 times.
/// When the ttrpc client fails to report, it attempts to reconnect to the client and report.
/// Max queue size
const QUEUE_SIZE: i64 = 1024;
/// Max try five times
const MAX_REQUEUE: i64 = 5;
/// Async Remote publisher connects to containerd's TTRPC endpoint to publish events from shim.
pub struct RemotePublisher {
client: EventsClient,
pub address: String,
sender: mpsc::Sender<Item>,
}
#[derive(Clone, Debug)]
pub struct Item {
ev: Envelope,
ctx: Context,
count: i64,
}
impl RemotePublisher {
@ -41,11 +59,70 @@ impl RemotePublisher {
///
/// containerd uses `/run/containerd/containerd.sock.ttrpc` by default
pub async fn new(address: impl AsRef<str>) -> Result<RemotePublisher> {
let client = Self::connect(address).await?;
let client = Self::connect(address.as_ref()).await?;
// Init the queue channel
let (sender, receiver) = mpsc::channel::<Item>(QUEUE_SIZE as usize);
let rt = RemotePublisher {
address: address.as_ref().to_string(),
sender,
};
rt.process_queue(client, receiver).await;
Ok(rt)
}
Ok(RemotePublisher {
client: EventsClient::new(client),
})
/// Process_queue for push events
///
/// This is a loop task for dealing event tasks
pub async fn process_queue(&self, ttrpc_client: Client, mut receiver: mpsc::Receiver<Item>) {
let mut client = EventsClient::new(ttrpc_client);
let sender = self.sender.clone();
let address = self.address.clone();
tokio::spawn(async move {
// only this use receiver
while let Some(item) = receiver.recv().await {
// drop this event after MAX_REQUEUE try
if item.count > MAX_REQUEUE {
debug!("drop event {:?}", item);
continue;
}
let mut req = events::ForwardRequest::new();
req.set_envelope(item.ev.clone());
let new_item = Item {
ev: item.ev.clone(),
ctx: item.ctx.clone(),
count: item.count + 1,
};
if let Err(e) = client.forward(new_item.ctx.clone(), &req).await {
match e {
ttrpc::error::Error::RemoteClosed | ttrpc::error::Error::LocalClosed => {
warn!("publish fail because the server or client close {:?}", e);
// reconnect client
if let Ok(c) = Self::connect(address.as_str()).await.map_err(|e| {
debug!("reconnect the ttrpc client {:?} fail", e);
}) {
client = EventsClient::new(c);
}
}
_ => {
// TODO! if it is other error , May we should deal with socket file
error!("the client forward err is {:?}", e);
}
}
let sender_ref = sender.clone();
// Take a another task requeue , for no blocking the recv task
tokio::spawn(async move {
// wait for few time and send for imporving the success ratio
tokio::time::sleep(tokio::time::Duration::from_secs(new_item.count as u64))
.await;
// if channel is full and send fail ,release it after 3 seconds
let _ = sender_ref
.send_timeout(new_item, tokio::time::Duration::from_secs(3))
.await;
});
}
}
debug!("publisher 'process_queue' quit complete");
});
}
async fn connect(address: impl AsRef<str>) -> Result<Client> {
@ -76,26 +153,22 @@ impl RemotePublisher {
envelope.set_timestamp(timestamp()?);
envelope.set_event(convert_to_any(event)?);
let mut req = events::ForwardRequest::new();
req.set_envelope(envelope);
let item = Item {
ev: envelope.clone(),
ctx: ctx.clone(),
count: 0,
};
self.client.forward(ctx, &req).await?;
//if channel is full and send fail ,release it after 3 seconds
self.sender
.send_timeout(item, tokio::time::Duration::from_secs(3))
.await
.map_err(|e| error::Error::Ttrpc(ttrpc::error::Error::Others(e.to_string())))?;
Ok(())
}
}
#[async_trait]
impl Events for RemotePublisher {
async fn forward(
&self,
_ctx: &TtrpcContext,
req: events::ForwardRequest,
) -> ttrpc::Result<Empty> {
self.client.forward(Context::default(), &req).await
}
}
#[cfg(test)]
mod tests {
use std::{
@ -103,10 +176,11 @@ mod tests {
sync::Arc,
};
use async_trait::async_trait;
use containerd_shim_protos::{
api::{Empty, ForwardRequest},
events::task::TaskOOM,
shim_async::create_events,
shim_async::{create_events, Events},
ttrpc::asynchronous::Server,
};
use tokio::sync::{
@ -115,6 +189,7 @@ mod tests {
};
use super::*;
use crate::publisher::ttrpc::r#async::TtrpcContext;
struct FakeServer {
tx: Sender<i32>,

View File

@ -38,7 +38,7 @@ where
{
spawn_blocking(f)
.await
.map_err(other_error!(e, "failed to spawn blocking task"))?
.map_err(other_error!("failed to spawn blocking task"))?
}
pub async fn read_file_to_str(path: impl AsRef<Path>) -> Result<String> {
@ -96,14 +96,19 @@ pub async fn read_pid_from_file(pid_path: &Path) -> Result<i32> {
pub async fn read_spec(bundle: impl AsRef<Path>) -> Result<Spec> {
let path = bundle.as_ref().join(CONFIG_FILE_NAME);
let content = read_file_to_str(&path).await?;
serde_json::from_str::<Spec>(content.as_str()).map_err(other_error!(e, "read spec"))
serde_json::from_str::<Spec>(content.as_str()).map_err(other_error!("read spec"))
}
// read_options reads the option information from the path.
// When the file does not exist, read_options returns nil without an error.
pub async fn read_options(bundle: impl AsRef<Path>) -> Result<Options> {
let path = bundle.as_ref().join(OPTIONS_FILE_NAME);
if !path.exists() {
return Ok(Options::default());
}
let opts_str = read_file_to_str(path).await?;
let opts =
serde_json::from_str::<JsonOptions>(&opts_str).map_err(other_error!(e, "read options"))?;
serde_json::from_str::<JsonOptions>(&opts_str).map_err(other_error!("read options"))?;
Ok(opts.into())
}

Some files were not shown because too many files have changed in this diff Show More