Compare commits

...

189 Commits

Author SHA1 Message Date
07cceed0c8 chore: release (#202)
Co-authored-by: edera-cultivation[bot] <165992271+edera-cultivation[bot]@users.noreply.github.com>
2024-07-12 23:28:49 +00:00
4ef466ceb6 chore(workflow): implement oci releases (#248) 2024-07-12 21:38:17 +00:00
8c9b3a6ceb fix(dependabot): separate production and development dependency updates (#247) 2024-07-12 20:36:19 +00:00
a970cddacf fix(dependabot): enable docker version update checks (#244) 2024-07-12 20:00:00 +00:00
a878d16c3c build(deps): bump thiserror from 1.0.61 to 1.0.62 (#246)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.61 to 1.0.62.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.61...1.0.62)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-12 06:56:50 +00:00
1126f1ffc9 fix(install): use /usr/sbin as install path and fix systemd dependency (#245) 2024-07-12 06:49:02 +00:00
d1b2cb3683 build(deps): bump serde from 1.0.203 to 1.0.204 (#234)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.203 to 1.0.204.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.203...v1.0.204)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-10 22:53:48 +00:00
8e1e197113 build(deps): bump uuid from 1.9.1 to 1.10.0 (#239)
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.9.1 to 1.10.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.9.1...1.10.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-10 22:53:33 +00:00
ffb7de7d68 build(deps): bump sysinfo from 0.30.12 to 0.30.13 (#238)
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.30.12 to 0.30.13.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/v0.30.13/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits/v0.30.13)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-10 22:53:09 +00:00
bd464d9f03 build(deps): bump clap from 4.5.8 to 4.5.9 (#237)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.8 to 4.5.9.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.5.8...v4.5.9)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-10 22:53:06 +00:00
31d04c2f43 build(deps): bump async-trait from 0.1.80 to 0.1.81 (#235)
Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.80 to 0.1.81.
- [Release notes](https://github.com/dtolnay/async-trait/releases)
- [Commits](https://github.com/dtolnay/async-trait/compare/0.1.80...0.1.81)

---
updated-dependencies:
- dependency-name: async-trait
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-10 22:53:02 +00:00
04401c1d07 fix(runtime): use iommu only if devices are needed (#243) 2024-07-10 04:02:53 +00:00
b2dd4af09b chore(powermgmt): disable for now as a hackfix (#242)
Signed-off-by: Ariadne Conill <ariadne@dereferenced.org>
2024-07-10 03:47:02 +00:00
783dd51f05 chore(systemd): align systemd unit definitions with OCI asset paths (#241)
Signed-off-by: Ariadne Conill <ariadne@dereferenced.org>
2024-07-10 00:37:12 +00:00
2f866ad47b feature(oci-distribution): distribute guestinit via OCI (#240)
Signed-off-by: Ariadne Conill <ariadne@dereferenced.org>
2024-07-10 00:34:05 +00:00
94e45c1c8c build(deps): bump actions/upload-artifact from 4.3.3 to 4.3.4 (#236)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.3 to 4.3.4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](65462800fd...0b2256b8c0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-08 12:56:34 +00:00
3a398810b6 Minor readability fixes to the lovely FAQ (#229)
* Minor readability fixes to the lovely FAQ

Signed-off-by: Jed Salazar <jed@edera.dev>

* Remove comma

Signed-off-by: Jed Salazar <jed@edera.dev>

---------

Signed-off-by: Jed Salazar <jed@edera.dev>
2024-07-07 18:54:31 +00:00
5da214fa48 build(deps): bump serde_json from 1.0.119 to 1.0.120 (#226)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.119 to 1.0.120.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.119...v1.0.120)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-05 20:21:14 +00:00
8840bf34a4 build(deps): bump actions/create-github-app-token from 1.10.2 to 1.10.3 (#227)
Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.10.2 to 1.10.3.
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](ad38cffc07...31c86eb3b3)

---
updated-dependencies:
- dependency-name: actions/create-github-app-token
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-05 20:21:11 +00:00
f953c87b90 build(deps): bump oci-spec from 0.6.5 to 0.6.7 (#232)
Bumps [oci-spec](https://github.com/containers/oci-spec-rs) from 0.6.5 to 0.6.7.
- [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.5...v0.6.7)

---
updated-dependencies:
- dependency-name: oci-spec
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-05 16:03:04 +00:00
ff571630b9 build(deps): bump docker/build-push-action from 6.2.0 to 6.3.0 (#231)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.2.0 to 6.3.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](15560696de...1a162644f9)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-05 16:03:00 +00:00
e45a9d82d2 build(deps): bump docker/setup-buildx-action from 3.3.0 to 3.4.0 (#233)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.3.0 to 3.4.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](d70bba72b1...4fd812986e)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-05 16:02:40 +00:00
98ca623828 fix(oci-distribution): use scratch images for OCI distributed-artefacts (#230)
Signed-off-by: Ariadne Conill <ariadne@ariadne.space>
2024-07-05 16:02:26 +00:00
deeaa20a4a fix(workflow): format check should print output but not error (#225) 2024-07-01 20:11:25 +00:00
fe8e1d5521 feature(oci): add configuration value for oci seed file (#220) 2024-07-01 19:36:21 +00:00
367d31b11f fix(workflow): remove reference to unused platform matrix key (#223) 2024-07-01 09:10:09 +00:00
71301ee689 fix(daemon): decrease rate of runtime reconcile (#224) 2024-07-01 09:09:50 +00:00
350e02c553 build(deps): bump clap from 4.5.7 to 4.5.8 (#222)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.7 to 4.5.8.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.7...v4.5.8)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 05:49:32 +00:00
f0914fb39f build(deps): bump serde_json from 1.0.118 to 1.0.119 (#221)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.118 to 1.0.119.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.118...v1.0.119)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 05:49:25 +00:00
0e64d4ea79 feature(power-management-defaults): set an initial power management policy (#219)
The default policy enables performance mode and SMT.

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>
2024-07-01 03:37:17 +00:00
35d585e3b1 fix(power): ensure that xeon cpus with cpu gaps are not detected as p/e compatible (#218) 2024-06-30 05:25:15 +00:00
a79320b4fc Power management core functionality (#217)
* feat(power-management-core): add core power management control messages for kratad

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(power-management-core): expose xen hypercall client publicly

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(power-management-core): add indexmap to kratart crate dependencies

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(power-management-core): implement power management core in kratart

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(power-management-core): bubble up runtime context in daemon/control service

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(power-management-core): expose performance/efficiency core data in protobuf

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(power-management-core): fix up some protobuf message names

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(power-management-core): fix up performance core heuristic

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(power-management-core): implement GetHostCpuTopology RPC

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(power-management-core): hackfix to get sysctls working with tokio

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(power-management-core): borrow the PowerManagementContext when calling functions belonging to it

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(power-management-core): remove GetHostPowerManagementPolicy RPC for now

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(power-management-core): implement SetHostPowerManagementPolicy RPC

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(power-management-core): add cpu-topology command

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(power-management-core): appease format checking

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* fix(runtime): cpu topology corrections

---------

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>
Co-authored-by: Alex Zenla <alex@edera.dev>
2024-06-29 15:43:08 -07:00
39ded9c7f4 Ensure list item ends with period for consistency (#216)
Signed-off-by: Andrés Vega <av@messier42.com>
2024-06-29 00:17:41 +00:00
b42b730b77 feature(xen): implement power management operations (#215) 2024-06-28 22:13:57 +00:00
0f49d0cec4 build(deps): bump log from 0.4.21 to 0.4.22 (#214)
Bumps [log](https://github.com/rust-lang/log) from 0.4.21 to 0.4.22.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.21...0.4.22)

---
updated-dependencies:
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-28 19:35:03 +00:00
dc4b14b5d1 chore: temporarily disable format checks (#207)
As per https://github.com/edera-dev/krata/issues/206, we are disabling
format checks until we have migrated to the new formatting rules, which
are commited in a later change.
2024-06-28 17:01:03 +00:00
f5b4c66ec7 build(deps): bump docker/build-push-action from 6.1.0 to 6.2.0 (#211)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](31159d49c0...15560696de)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-27 12:34:18 +00:00
9062d78e51 build(deps): bump actions/checkout from 4.1.6 to 4.1.7 (#212)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.6 to 4.1.7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.1.6...692973e3d937129bcbf40652eb9f2f61becf3332)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-27 12:33:13 +00:00
6161bea7bf build(deps): bump step-security/harden-runner from 2.8.0 to 2.8.1 (#213)
Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.8.0 to 2.8.1.
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/v2.8.0...17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-27 12:32:31 +00:00
8363ed0085 OCI distribution (#210)
* feat(images): add dockerfiles for the OCI distributions of krata components

Signed-off-by: Ariadne Conill <ariadne@dereferenced.org>

* feat(images): add oci distribution workflow

Signed-off-by: Ariadne Conill <ariadne@dereferenced.org>

---------

Signed-off-by: Ariadne Conill <ariadne@dereferenced.org>
2024-06-26 21:31:30 +00:00
8ddc190018 build(deps): bump actions/create-github-app-token from 1.10.1 to 1.10.2 (#208)
Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.10.1 to 1.10.2.
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](c8f55efbd4...ad38cffc07)

---
updated-dependencies:
- dependency-name: actions/create-github-app-token
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-26 17:42:29 +00:00
c687561541 build(deps): bump serde_json from 1.0.117 to 1.0.118 (#204)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.117 to 1.0.118.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.117...v1.0.118)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-26 17:42:13 +00:00
4c83902729 build(deps): bump uuid from 1.9.0 to 1.9.1 (#203)
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.9.0 to 1.9.1.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.9.0...1.9.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-26 17:41:22 +00:00
6f50167798 Use native loopdev implementation instead of loopdev-3 (#209)
* feature(loopdev): add native loop device implementation

The previous loop device implementation required bindgen for no reason,
making cross-compilation difficult.

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* feat(runtime): use native krata-loopdev instead of loopdev-3

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* chore: update cargo workspace lock file

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* chore: appease formatting linter

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

---------

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>
2024-06-26 10:29:58 -07:00
88a62441b1 Initial fluentd support (#205)
* fix(hack): use sudo -E when running Rust binaries

This makes it possible to pass envvars to the Krata runtime

* feat(o11y): add fluent sink to logs

This change adds fluent logging as an opt-in feature. Setting
`KRATA_LOG_FLUENT` with an address:port will start a TCP connection,
sending logs.

A later changes will respect a URI scheme and use structured logging.
2024-06-25 19:10:57 +00:00
93aae83b3f build(deps): bump nix from 0.28.0 to 0.29.0 (#198)
Bumps [nix](https://github.com/nix-rust/nix) from 0.28.0 to 0.29.0.
- [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nix-rust/nix/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: nix
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 05:30:30 +00:00
6e1e4e3806 build(deps): bump reqwest from 0.12.4 to 0.12.5 (#199)
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.4 to 0.12.5.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.4...v0.12.5)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 05:30:22 +00:00
9e532345f0 build(deps): bump uuid from 1.8.0 to 1.9.0 (#200)
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.8.0...1.9.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 05:30:14 +00:00
89b7f40520 build(deps): bump memchr from 2.7.2 to 2.7.4 (#201)
Bumps [memchr](https://github.com/BurntSushi/memchr) from 2.7.2 to 2.7.4.
- [Commits](https://github.com/BurntSushi/memchr/compare/2.7.2...2.7.4)

---
updated-dependencies:
- dependency-name: memchr
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 05:30:06 +00:00
4175e1e3fe chore: release (#181)
Co-authored-by: edera-cultivation[bot] <165992271+edera-cultivation[bot]@users.noreply.github.com>
2024-06-24 05:01:32 +00:00
1bdf3bda87 build(deps): bump regex from 1.10.4 to 1.10.5 (#187)
Bumps [regex](https://github.com/rust-lang/regex) from 1.10.4 to 1.10.5.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.10.4...1.10.5)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-23 12:58:15 +00:00
9a45d754bf chore(xenplatform): elf loader should async load the file (#197)
* fix(build): remove unused environment variables

* chore(xenplatform): elf loader should async load the file
2024-06-23 12:57:01 +00:00
6c3fc54688 build(deps): bump url from 2.5.0 to 2.5.2 (#193)
Bumps [url](https://github.com/servo/rust-url) from 2.5.0 to 2.5.2.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.5.0...v2.5.2)

---
updated-dependencies:
- dependency-name: url
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-23 12:15:20 +00:00
af6a1a3ad2 build(deps): bump MarcoIeni/release-plz-action from 0.5.61 to 0.5.62 (#192)
Bumps [MarcoIeni/release-plz-action](https://github.com/marcoieni/release-plz-action) from 0.5.61 to 0.5.62.
- [Release notes](https://github.com/marcoieni/release-plz-action/releases)
- [Commits](7566221bba...86afd21a7b)

---
updated-dependencies:
- dependency-name: MarcoIeni/release-plz-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-23 12:15:17 +00:00
7bef74fadf build(deps): bump actions/checkout from 4.1.6 to 4.1.7 (#190)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.6 to 4.1.7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](a5ac7e51b4...692973e3d9)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-23 12:15:15 +00:00
ec1b6d4370 build(deps): bump clap from 4.5.4 to 4.5.7 (#188)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.4 to 4.5.7.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.4...v4.5.7)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-23 12:15:10 +00:00
b2d146713b build(deps): bump redb from 2.1.0 to 2.1.1 (#186)
Bumps [redb](https://github.com/cberner/redb) from 2.1.0 to 2.1.1.
- [Release notes](https://github.com/cberner/redb/releases)
- [Changelog](https://github.com/cberner/redb/blob/master/CHANGELOG.md)
- [Commits](https://github.com/cberner/redb/compare/v2.1.0...v2.1.1)

---
updated-dependencies:
- dependency-name: redb
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-23 12:15:04 +00:00
b730b08d6e build(deps): bump step-security/harden-runner from 2.8.0 to 2.8.1 (#185)
Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.8.0 to 2.8.1.
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](f086349bfa...17d0e2bd7d)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-23 12:15:02 +00:00
87758d7ae9 build(deps): bump tokio-tun from 0.11.4 to 0.11.5 (#183)
Bumps [tokio-tun](https://github.com/yaa110/tokio-tun) from 0.11.4 to 0.11.5.
- [Commits](https://github.com/yaa110/tokio-tun/compare/0.11.4...0.11.5)

---
updated-dependencies:
- dependency-name: tokio-tun
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-23 12:14:59 +00:00
349664abf1 chore(git): delete old gitmodules 2024-06-23 01:50:35 -07:00
ef068e790c chore(xen): move device creation into transaction interface (#196)
* chore(xen): move domain creation to xenplatform

* chore(xen): move device transactions into separate interface
2024-06-21 17:38:19 +00:00
6f39f115b7 chore(xen): split platform support into separate crate (#195) 2024-06-21 08:10:45 +00:00
23c7302c04 docs: first pass of krata as an isolation engine 2024-06-20 19:57:18 -07:00
e219f3adf1 feature(xen): dynamic platform architecture (#194)
* wip hvm

* feat: move platform stuff all into it's own thing

* hvm work

* more hvm work

* more hvm work

* feat: rework to support multiple platforms

* hvm nonredist

* more hvm work

* more hvm work

* pvh work

* work on loading cmdline

* implement initrd loading for pvh

* partially working pvh support

* fix merge issues

* pvh works!

* swap over to pv support

* remove old kernel stuff

* fix support for pv

* pvh is gone for now

* fix(runtime): debug should be respected

* fix(xen): arm64 is currently unsupported, treat it as such at runtime

* fix(examples): use architecture cfg for boot example

* fix(x86): use IOMMU only when needed for passthrough

* chore(build): print kernel architecture during fetch
2024-06-21 02:42:45 +00:00
2c7210d85e build(deps): bump prost-build from 0.12.4 to 0.12.6 (#170)
Bumps [prost-build](https://github.com/tokio-rs/prost) from 0.12.4 to 0.12.6.
- [Release notes](https://github.com/tokio-rs/prost/releases)
- [Commits](https://github.com/tokio-rs/prost/compare/v0.12.4...v0.12.6)

---
updated-dependencies:
- dependency-name: prost-build
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 19:24:00 +00:00
ade37e92f3 build(deps): bump serde from 1.0.202 to 1.0.203 (#172)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.202 to 1.0.203.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.202...v1.0.203)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 19:23:52 +00:00
ef3bc83069 build(deps): bump async-compression from 0.4.10 to 0.4.11 (#175)
Bumps [async-compression](https://github.com/Nullus157/async-compression) from 0.4.10 to 0.4.11.
- [Release notes](https://github.com/Nullus157/async-compression/releases)
- [Changelog](https://github.com/Nullus157/async-compression/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Nullus157/async-compression/compare/v0.4.10...v0.4.11)

---
updated-dependencies:
- dependency-name: async-compression
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 19:23:43 +00:00
14084f13d8 build(deps): bump tokio from 1.37.0 to 1.38.0 (#176)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.37.0 to 1.38.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.37.0...tokio-1.38.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 19:23:35 +00:00
fbc953cf46 build(deps): bump actions/create-github-app-token from 1.10.0 to 1.10.1 (#177)
Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.10.0 to 1.10.1.
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](a0de6af839...c8f55efbd4)

---
updated-dependencies:
- dependency-name: actions/create-github-app-token
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 19:23:26 +00:00
fd7974fc98 build(deps): bump MarcoIeni/release-plz-action from 0.5.58 to 0.5.61 (#178)
Bumps [MarcoIeni/release-plz-action](https://github.com/marcoieni/release-plz-action) from 0.5.58 to 0.5.61.
- [Release notes](https://github.com/marcoieni/release-plz-action/releases)
- [Commits](7fe60ae5d7...7566221bba)

---
updated-dependencies:
- dependency-name: MarcoIeni/release-plz-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 19:23:18 +00:00
d17769d69f build(deps): bump toml from 0.8.13 to 0.8.14 (#179)
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.13 to 0.8.14.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.13...toml-v0.8.14)

---
updated-dependencies:
- dependency-name: toml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 19:23:11 +00:00
7ba04f26a3 fix(os): alpine v3.20 requires copying kernel config before grub-mkconfig (#180)
There is currently a bug in the Xen support for Alpine where /boot/config-lts
is expected to exist but in Alpine /boot/config-VERSION-lts is the only file
available. This change copies the config to /boot/config-lts to fix the build.
2024-06-04 17:00:49 +00:00
11235b6837 --- (#168)
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 06:29:06 +00:00
e8849048db --- (#167)
updated-dependencies:
- dependency-name: prost-types
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 06:29:03 +00:00
cd15337ad8 build(deps): bump libc from 0.2.154 to 0.2.155 (#163)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.154 to 0.2.155.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.154...0.2.155)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 04:44:36 +00:00
037261991a build(deps): bump anyhow from 1.0.83 to 1.0.86 (#164)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.83 to 1.0.86.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.83...1.0.86)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 04:44:33 +00:00
67fb5891e4 build(deps): bump prost from 0.12.4 to 0.12.6 (#165)
Bumps [prost](https://github.com/tokio-rs/prost) from 0.12.4 to 0.12.6.
- [Release notes](https://github.com/tokio-rs/prost/releases)
- [Commits](https://github.com/tokio-rs/prost/compare/v0.12.4...v0.12.6)

---
updated-dependencies:
- dependency-name: prost
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 04:44:30 +00:00
d1f6d1e742 --- (#166)
updated-dependencies:
- dependency-name: ratatui
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 04:44:27 +00:00
18fc2c3a7e build(deps): bump thiserror from 1.0.60 to 1.0.61 (#162)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.60 to 1.0.61.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.60...1.0.61)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 04:44:24 +00:00
54486b119b build(deps): bump actions/checkout from 4.1.5 to 4.1.6 (#161)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.5 to 4.1.6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](44c2b7a8a4...a5ac7e51b4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-17 17:48:51 +00:00
04a633d501 build(deps): bump MarcoIeni/release-plz-action from 0.5.57 to 0.5.58 (#152)
Bumps [MarcoIeni/release-plz-action](https://github.com/marcoieni/release-plz-action) from 0.5.57 to 0.5.58.
- [Release notes](https://github.com/marcoieni/release-plz-action/releases)
- [Commits](a290444218...7fe60ae5d7)

---
updated-dependencies:
- dependency-name: MarcoIeni/release-plz-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-16 19:11:46 +00:00
612203f014 build(deps): bump serde from 1.0.201 to 1.0.202 (#154)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.201 to 1.0.202.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.201...v1.0.202)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-16 19:09:33 +00:00
e9ba336f68 build(deps): bump toml from 0.8.12 to 0.8.13 (#155)
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.12 to 0.8.13.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.12...toml-v0.8.13)

---
updated-dependencies:
- dependency-name: toml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-16 19:09:14 +00:00
94790ce7dc fix(build): kernel fetch should use host target (#159) 2024-05-16 18:13:04 +00:00
023063327f fix(build): use host resolv.conf in os build chroot (#153)
The resolv.conf that the stage1 os script generates is fine for actual use,
but our GitHub workflows now uses the Step Security hardened runner action.
This action replaces the nameserver so that all lookups go through that,
but because the chroot calls apk add, it needs to contact the internet.
On the GitHub workflows, the OS build currently fails since the hardened
runner cannot access other nameservers.
2024-05-16 08:41:42 +00:00
d46aa878af feat(build): fetch kernels from image registry instead of building the kernel (#156)
Now that we have the kernel build infrastructure at https://github.com/edera-dev/kernels
it makes sense to drop building the kernel and download the kernel images directly.

This change introduces a ./hack/kernel/fetch.sh script which is backed by crates/build
We utilize the OCI infrastructure itself to download the kernel image. The DEV guide
has been updated to include calling the fetch script, and the OS builder now uses this
method instead. Due to the lack of need for the kernel build infra to exist here now,
it has also been removed. This should significantly speed up full builds.

This change will also enable us to turn on os build workflows for all PRs. We should
likely make the OS status checks required once this is merged.
2024-05-16 08:40:58 +00:00
2462a99fdc chore(dependabot): group some dependency updates (#157)
We have a need to ensure great security while also ensuring that dependabot
does not constantly provide multiple PRs. After all, when something becomes
too time consuming it risks not being handled with care. With grouped updates,
version bumps will get grouped together, but security updates will still be
indvidualized. This makes it safer for us to enable grouped dependency updates.
2024-05-16 08:39:50 +00:00
fc18bc6a18 feat(runtime): concurrent ip allocation (#151)
Previously, krata runtime allowed a single permit when performing operations.
This was necessary because the only IP allocation storage was xenstore, and
the commit of xenstore data happens after allocation. This commit introduces
IpVendor, a service which vends IPv4 and IPv6 addresses to guests using a
linear address strategy within an IP network space. The IpVendor table is
initialized from xenstore, and from there on out, the in-memory table
is the source of truth. This implementation is not perfect, but it will allow
us to lift the single permit limit, allowing guests to start concurrently.
2024-05-14 18:29:12 +00:00
b0f0934fa4 build(deps): bump async-compression from 0.4.9 to 0.4.10 (#145)
Bumps [async-compression](https://github.com/Nullus157/async-compression) from 0.4.9 to 0.4.10.
- [Release notes](https://github.com/Nullus157/async-compression/releases)
- [Changelog](https://github.com/Nullus157/async-compression/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Nullus157/async-compression/commits/v0.4.10)

---
updated-dependencies:
- dependency-name: async-compression
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 19:23:40 +00:00
f6721d5e2c build(deps): bump actions/checkout from 4.1.4 to 4.1.5 (#149)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.4 to 4.1.5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](0ad4b8fada...44c2b7a8a4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 19:23:24 +00:00
0d43a8be54 build(deps): bump MarcoIeni/release-plz-action from 0.5.55 to 0.5.57 (#150)
Bumps [MarcoIeni/release-plz-action](https://github.com/marcoieni/release-plz-action) from 0.5.55 to 0.5.57.
- [Release notes](https://github.com/marcoieni/release-plz-action/releases)
- [Commits](76e66a600f...a290444218)

---
updated-dependencies:
- dependency-name: MarcoIeni/release-plz-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 19:23:08 +00:00
0193921053 Pin actions to digests and introduce Step Security Harden Runners (#137)
Signed-off-by: Jed Salazar <jedsalazar@gmail.com>
2024-05-11 00:00:56 +00:00
485f6e8319 chore(kernel): upgrade to kernel 6.8.9 (#143) 2024-05-10 17:30:06 +00:00
09ee251c9e Fix typo and nit (#144)
Signed-off-by: Jed Salazar <jed@edera.dev>
2024-05-10 01:44:42 +00:00
75011ef8cb fix(oci): use mirror.gcr.io as a mirror to docker hub (#141) 2024-05-09 17:30:27 +00:00
69c7af5220 build(deps): bump thiserror from 1.0.59 to 1.0.60 (#135)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.59 to 1.0.60.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.59...1.0.60)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-08 19:14:17 +00:00
a364abe887 build(deps): bump anyhow from 1.0.82 to 1.0.83 (#136)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.82 to 1.0.83.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.82...1.0.83)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-08 19:14:00 +00:00
95accc6d3f build(deps): bump serde from 1.0.200 to 1.0.201 (#139)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.200 to 1.0.201.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.200...v1.0.201)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-08 19:13:44 +00:00
04fb6cce8e build(deps): bump serde_json from 1.0.116 to 1.0.117 (#140)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.116 to 1.0.117.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.116...v1.0.117)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-08 19:13:27 +00:00
5420214bdd build(deps): bump sysinfo from 0.30.11 to 0.30.12 (#131)
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.30.11 to 0.30.12.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-08 19:13:11 +00:00
b4f26787d4 fix(oci): remove file size limit (#142)
the addons.squashfs file often is fairly large due to the GPU modules containing a lot of code
2024-05-08 19:09:33 +00:00
51dff0361d fix(xenclient): use a single transaction for device setup (#130) 2024-05-05 20:39:53 +00:00
3187830ff5 build(deps): bump serde from 1.0.199 to 1.0.200 (#129)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.199 to 1.0.200.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.199...v1.0.200)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-02 17:19:20 +00:00
338322619c build(deps): bump base64 from 0.22.0 to 0.22.1 (#128)
Bumps [base64](https://github.com/marshallpierce/rust-base64) from 0.22.0 to 0.22.1.
- [Changelog](https://github.com/marshallpierce/rust-base64/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/marshallpierce/rust-base64/compare/v0.22.0...v0.22.1)

---
updated-dependencies:
- dependency-name: base64
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-01 16:27:53 +00:00
511f83bfd9 fix: dev guide should mention copying kernel addons (fixes #125) (#126) 2024-04-30 21:46:45 +00:00
b0f5c38fb0 build(deps): bump async-compression from 0.4.8 to 0.4.9 (#120)
Bumps [async-compression](https://github.com/Nullus157/async-compression) from 0.4.8 to 0.4.9.
- [Release notes](https://github.com/Nullus157/async-compression/releases)
- [Changelog](https://github.com/Nullus157/async-compression/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Nullus157/async-compression/commits)

---
updated-dependencies:
- dependency-name: async-compression
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-30 16:31:41 +00:00
520018a86d build(deps): bump serde from 1.0.198 to 1.0.199 (#122)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.198 to 1.0.199.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.198...v1.0.199)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-30 16:31:20 +00:00
39c2e58fbc build(deps): bump flate2 from 1.0.28 to 1.0.30 (#123)
Bumps [flate2](https://github.com/rust-lang/flate2-rs) from 1.0.28 to 1.0.30.
- [Release notes](https://github.com/rust-lang/flate2-rs/releases)
- [Commits](https://github.com/rust-lang/flate2-rs/compare/1.0.28...1.0.30)

---
updated-dependencies:
- dependency-name: flate2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-30 16:31:00 +00:00
d1d6eb5c8b build(deps): bump libc from 0.2.153 to 0.2.154 (#124)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.153 to 0.2.154.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.153...0.2.154)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-30 16:30:44 +00:00
84920a88ab feat: pci passthrough (#114)
* feat: pci passthrough

* feat: guest device management

* feat: addons mounting and kernel modules support

* feat: more pci work

* fix: kernel build squashfs fixes

* fix: e820entry should be available on all platforms
2024-04-29 17:02:20 +00:00
bece7f33c7 feat: CONTRIBUTING.md and Bug Report template (#117)
This change introduces an initial CONTRIBUTING.md doc and a template for
bug reports.

Signed-off-by: Khionu Sybiern <khionu@edera.dev>
2024-04-24 21:01:52 +00:00
95fbc62486 chore: release (#87)
Signed-off-by: edera-cultivation[bot] <165992271+edera-cultivation[bot]@users.noreply.github.com>
Co-authored-by: edera-cultivation[bot] <165992271+edera-cultivation[bot]@users.noreply.github.com>
2024-04-23 09:41:56 +00:00
284ed8f17b feat: implement guest exec (#107) 2024-04-22 20:13:43 +00:00
82576df7b7 feat: implement kernel / initrd oci image support (#103)
* feat: implement kernel / initrd oci image support

* fix: implement image urls more faithfully
2024-04-22 19:48:45 +00:00
1b90eedbcd build(deps): bump thiserror from 1.0.58 to 1.0.59 (#104)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.58 to 1.0.59.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.58...1.0.59)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-22 05:36:20 +00:00
aa941c6e87 build(deps): bump redb from 2.0.0 to 2.1.0 (#106)
Bumps [redb](https://github.com/cberner/redb) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/cberner/redb/releases)
- [Changelog](https://github.com/cberner/redb/blob/master/CHANGELOG.md)
- [Commits](https://github.com/cberner/redb/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: redb
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-22 05:36:13 +00:00
d0bf3c4c77 build(deps): bump reqwest from 0.12.3 to 0.12.4 (#105)
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.3 to 0.12.4.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.3...v0.12.4)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-22 05:36:06 +00:00
38e892e249 feat: idm v2 (#102)
* feat: rebuild idm to separate transport from content

* feat: fast guest lookup table and host identification
2024-04-22 04:00:32 +00:00
1a90372037 build(deps): bump sysinfo from 0.30.10 to 0.30.11 (#99)
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.30.10 to 0.30.11.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/compare/v0.30.10...v0.30.11)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-19 20:35:57 +00:00
4754cdd128 build(deps): bump rustls from 0.22.3 to 0.22.4 (#101)
Bumps [rustls](https://github.com/rustls/rustls) from 0.22.3 to 0.22.4.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.22.3...v/0.22.4)

---
updated-dependencies:
- dependency-name: rustls
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-19 20:35:44 +00:00
f843abcabf docs(dev): update order of setup instructions (#98)
This change corrects the steps order to have the krata daemon started
before starting the krata networking service

Co-authored-by: Khionu Sybiern <khionu@edera.dev>
2024-04-18 18:00:48 +00:00
e8d89d4d5b build(deps): bump serde from 1.0.197 to 1.0.198 (#97)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.197 to 1.0.198.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.197...v1.0.198)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-17 05:17:00 +00:00
4e9738b959 fix: oci cache store should fallback to copy when rename won't work (#96) 2024-04-16 17:05:24 +00:00
8135307283 feat: oci concurrency improvements (#95)
* feat: implement improved and detailed oci progress indication

* feat: implement on-disk indexes of images

* oci: utilize rw-lock for increased cache performance
2024-04-16 16:29:54 +00:00
e450ebd2a2 feat: oci tar format, bit-perfect disk storage for config and manifest, concurrent image pulls (#88)
* oci: retain bit-perfect copies of manifest and config on disk

* feat: oci tar format support

* feat: concurrent image pulls
2024-04-16 08:53:44 +00:00
79f7742caa build(deps): bump serde_json from 1.0.115 to 1.0.116 (#90)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.115 to 1.0.116.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.115...v1.0.116)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 07:17:11 +00:00
c3c18271b4 build(deps): bump ratatui from 0.26.1 to 0.26.2 (#89)
Bumps [ratatui](https://github.com/ratatui-org/ratatui) from 0.26.1 to 0.26.2.
- [Release notes](https://github.com/ratatui-org/ratatui/releases)
- [Changelog](https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ratatui-org/ratatui/compare/v0.26.1...v0.26.2)

---
updated-dependencies:
- dependency-name: ratatui
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 07:16:53 +00:00
218f848170 chore: release (#41)
Signed-off-by: edera-cultivation[bot] <165992271+edera-cultivation[bot]@users.noreply.github.com>
Co-authored-by: edera-cultivation[bot] <165992271+edera-cultivation[bot]@users.noreply.github.com>
2024-04-15 19:15:00 +00:00
9d8c516a29 build(deps): bump sysinfo from 0.30.9 to 0.30.10 (#86)
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.30.9 to 0.30.10.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/v0.30.10/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/compare/v0.30.9...v0.30.10)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-15 17:24:41 +00:00
89055ef77c feat: oci compliance work (#85)
* chore: rework oci crate to be more composable

* feat: image pull is now internally explicit

* feat: utilize vfs for assembling oci images

* feat: rework oci to preserve permissions via a vfs
2024-04-15 17:24:14 +00:00
24c71e9725 feat: oci packer can now use mksquashfs if available (#70)
* feat: oci packer can now use mksquashfs if available

* fix: use nproc in kernel build script for default jobs, and fix DEV.md guide

* feat: working erofs backend
2024-04-15 00:19:38 +00:00
0a6a112133 feat: basic kratactl top command (#72)
* feat: basic kratactl top command

* fix: use magic bytes 0xff 0xff in idm to improve reliability
2024-04-14 22:32:34 +00:00
1627cbcdd7 feat: idm snooping (#71)
Implement IDM snooping, a new feature that lets you snoop on messages between guests and the host. The feature exposes the IDM packets send and receives
to the API, allowing kratactl to now listen for messages and feed them to a user for debugging purposes.
2024-04-14 11:54:21 +00:00
f8247f13e4 build: use LTO for release builds and strip guestinit (#68)
* initrd: strip guestinit binary before adding it to initramfs

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* build: use LTO for release profile artifacts

this allows us to save ~25-30% on binary sizes, at least in guestinit

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* revert strip command usage, breaks arm

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

* build: use strip=symbols

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>

---------

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>
2024-04-13 09:20:24 +00:00
6d07112e3d feat: implement oci image progress (#64)
* feat: oci progress events

* feat: oci progress bars on launch
2024-04-12 18:09:26 +00:00
6cef03bffa debug: common: run programs in a way that is compatible with alpine doas-sudo-shim (#53)
doas sudo shim (as used by Alpine) does not support passing through environment variables
in the same way that sudo does, therefore use `sh -c` instead.

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>
2024-04-12 09:20:34 +00:00
73fd95dbe2 guest: init: default to xterm if TERM is not set (#52)
Most terminal emulators support the xterm control codes more faithfully than the
vt100 ones.

Fixes #51.

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>
2024-04-12 08:52:18 +00:00
f41a1e2168 build: target: use alpine rust triplets when building on alpine (#49)
Signed-off-by: Ariadne Conill <ariadne@ariadne.space>
2024-04-12 08:40:44 +00:00
346cf4a7fa build(deps): bump async-trait from 0.1.79 to 0.1.80 (#48)
Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.79 to 0.1.80.
- [Release notes](https://github.com/dtolnay/async-trait/releases)
- [Commits](https://github.com/dtolnay/async-trait/compare/0.1.79...0.1.80)

---
updated-dependencies:
- dependency-name: async-trait
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-12 07:35:12 +00:00
5e16f3149f feat: guest metrics support (#46)
* feat: initial support for idm send in daemon

* feat: implement IdmClient backend support

* feat: daemon idm now uses IdmClient

* fix: implement channel destruction propagation

* feat: implement request response idm system

* feat: implement metrics support

* proto: move metrics into GuestMetrics for reusability

* fix: log level of guest agent was trace

* feat: metrics tree with process information
2024-04-12 07:34:46 +00:00
ec9060d872 build(deps): bump anyhow from 1.0.81 to 1.0.82 (#42)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.81 to 1.0.82.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.81...1.0.82)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 06:19:10 +00:00
6050e99aa7 chore: release (#39)
Co-authored-by: edera-cultivation[bot] <165992271+edera-cultivation[bot]@users.noreply.github.com>
2024-04-09 11:47:58 +00:00
7cfdb27d23 chore(workflows): fix release asset assembly for alpine/debian packages (#40) 2024-04-09 04:45:36 -07:00
87c4d7b0c3 chore: release (#37)
Co-authored-by: edera-cultivation[bot] <165992271+edera-cultivation[bot]@users.noreply.github.com>
2024-04-09 11:06:22 +00:00
4f84dfa3c7 chore(workflows): fix release binary upload (#38) 2024-04-09 04:02:07 -07:00
dedc514944 chore: release (#33)
Signed-off-by: edera-cultivation[bot] <165992271+edera-cultivation[bot]@users.noreply.github.com>
Co-authored-by: edera-cultivation[bot] <165992271+edera-cultivation[bot]@users.noreply.github.com>
2024-04-09 02:50:52 -07:00
9c0597157b fix: increase channel acquisition timeout to support lower performance hosts (#36) 2024-04-09 02:36:22 -07:00
3f8c9e7a7c chore(workflows): prefix all jobs with workflow name to help distinguish (#35) 2024-04-09 00:05:06 -07:00
6ed31bc436 build(deps): bump prost-build from 0.12.3 to 0.12.4 (#34)
Bumps [prost-build](https://github.com/tokio-rs/prost) from 0.12.3 to 0.12.4.
- [Release notes](https://github.com/tokio-rs/prost/releases)
- [Commits](https://github.com/tokio-rs/prost/compare/v0.12.3...v0.12.4)

---
updated-dependencies:
- dependency-name: prost-build
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-09 05:52:14 +00:00
2526065d74 chore: release (#32)
Co-authored-by: edera-cultivation[bot] <165992271+edera-cultivation[bot]@users.noreply.github.com>
2024-04-08 22:18:29 -07:00
a509f69398 chore: release (#24)
Signed-off-by: edera-cultivation[bot] <165992271+edera-cultivation[bot]@users.noreply.github.com>
Co-authored-by: edera-cultivation[bot] <165992271+edera-cultivation[bot]@users.noreply.github.com>
2024-04-08 22:12:38 -07:00
d021fb2147 build(deps): bump tokio-tun from 0.11.2 to 0.11.4 (#27)
Bumps [tokio-tun](https://github.com/yaa110/tokio-tun) from 0.11.2 to 0.11.4.
- [Commits](https://github.com/yaa110/tokio-tun/compare/0.11.2...0.11.4)

---
updated-dependencies:
- dependency-name: tokio-tun
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-09 04:30:24 +00:00
d33079c7c5 build(deps): bump comfy-table from 7.1.0 to 7.1.1 (#28)
Bumps [comfy-table](https://github.com/nukesor/comfy-table) from 7.1.0 to 7.1.1.
- [Release notes](https://github.com/nukesor/comfy-table/releases)
- [Changelog](https://github.com/Nukesor/comfy-table/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nukesor/comfy-table/compare/v7.1.0...v7.1.1)

---
updated-dependencies:
- dependency-name: comfy-table
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-09 04:30:14 +00:00
bea6d1e6fe build(deps): bump reqwest from 0.12.2 to 0.12.3 (#29)
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.2 to 0.12.3.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.2...v0.12.3)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-09 04:30:06 +00:00
bc27eeb286 build(deps): bump prost from 0.12.3 to 0.12.4 (#30)
Bumps [prost](https://github.com/tokio-rs/prost) from 0.12.3 to 0.12.4.
- [Release notes](https://github.com/tokio-rs/prost/releases)
- [Commits](https://github.com/tokio-rs/prost/compare/v0.12.3...v0.12.4)

---
updated-dependencies:
- dependency-name: prost
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-09 04:30:01 +00:00
42bb289421 build(deps): bump async-compression from 0.4.7 to 0.4.8 (#31)
Bumps [async-compression](https://github.com/Nullus157/async-compression) from 0.4.7 to 0.4.8.
- [Release notes](https://github.com/Nullus157/async-compression/releases)
- [Changelog](https://github.com/Nullus157/async-compression/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Nullus157/async-compression/compare/async-compression-v0.4.7...async-compression-v0.4.8)

---
updated-dependencies:
- dependency-name: async-compression
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-09 04:29:55 +00:00
f2ab03711e feat(ctl): add help and about to commands and arguments (#25) 2024-04-06 00:00:02 +00:00
2f3daad80a build(deps): bump h2 from 0.3.25 to 0.3.26 (#26)
Bumps [h2](https://github.com/hyperium/h2) from 0.3.25 to 0.3.26.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.25...v0.3.26)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-05 22:15:12 +00:00
8f7e47a218 chore: release workflow fixes to improve utilization and fix checks (#23)
* chore: use edera-cultivation bot to push release changes

* chore: workflows now largely only run on pull requests or merge queues
2024-04-04 23:05:59 -07:00
19683b80c1 fix: service files should have kratanet depend on kratad (#18) 2024-04-04 23:44:57 +00:00
ae486e347f fix: release pr trigger problem (#21) 2024-04-03 23:59:07 -07:00
9f4db08e07 build(deps): bump prost-reflect from 0.13.0 to 0.13.1 (#19)
Bumps [prost-reflect](https://github.com/andrewhickman/prost-reflect) from 0.13.0 to 0.13.1.
- [Changelog](https://github.com/andrewhickman/prost-reflect/blob/main/CHANGELOG.md)
- [Commits](https://github.com/andrewhickman/prost-reflect/compare/0.13.0...0.13.1)

---
updated-dependencies:
- dependency-name: prost-reflect
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 06:05:45 +00:00
d674a69c17 ci: fix tag names and disable semver checks for low-level crates (#17) 2024-04-03 17:16:30 +00:00
f59976eb80 chore: release (#16)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-04-03 00:27:16 +00:00
3641bc55f4 workflows: publish on release 2024-04-03 00:08:28 +00:00
4b6272f49d chore: release-plz on main branch 2024-04-02 23:52:26 +00:00
7c55e63f24 chore: implement automatic releases 2024-04-02 23:50:45 +00:00
2083ec0604 docs: update README 2024-04-02 20:17:34 +00:00
5ad2e40a7b krata: reimplement console to utilize channels, and provide logs support 2024-04-02 08:57:34 +00:00
0fd6318c5f xenstore: use read thread to avoid need for non-blocking I/O 2024-04-02 03:02:00 +00:00
7940eea588 build(deps): bump async-compression from 0.4.6 to 0.4.7 (#14)
Bumps [async-compression](https://github.com/Nullus157/async-compression) from 0.4.6 to 0.4.7.
- [Release notes](https://github.com/Nullus157/async-compression/releases)
- [Changelog](https://github.com/Nullus157/async-compression/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Nullus157/async-compression/compare/async-compression-v0.4.6...async-compression-v0.4.7)

---
updated-dependencies:
- dependency-name: async-compression
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-01 18:44:13 -07:00
45aa4914bb build(deps): bump etherparse from 0.14.2 to 0.14.3 (#15)
Bumps [etherparse](https://github.com/JulianSchmid/etherparse) from 0.14.2 to 0.14.3.
- [Release notes](https://github.com/JulianSchmid/etherparse/releases)
- [Changelog](https://github.com/JulianSchmid/etherparse/blob/master/changelog.md)
- [Commits](https://github.com/JulianSchmid/etherparse/compare/v0.14.2...v0.14.3)

---
updated-dependencies:
- dependency-name: etherparse
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-01 18:43:37 -07:00
8dd3cc7692 krata: work on parallel reconciliation 2024-04-02 00:56:18 +00:00
6a2f1e6517 dependabot: fix config file 2024-04-01 15:46:39 -07:00
d433cd49e2 krata: unvendor dependencies 2024-04-01 15:45:43 -07:00
0fd32e84cd workflows: fix client workflow spec 2024-03-31 16:30:22 -07:00
ab8941326a workflows: client build on windows should use lf checkout 2024-03-31 16:29:00 -07:00
8be75a722e workflows: use submodules checkout everywhere 2024-03-31 16:21:25 -07:00
e6f35eb3d0 cargo: fix vendor path 2024-03-31 16:09:32 -07:00
58c6413ca2 workflows: checkout submodules 2024-03-31 16:07:57 -07:00
e1f1f8579c vendor dependencies using krata-vendor repository 2024-03-31 15:53:10 -07:00
6bf1d3f88c krata: implement parallel guest reconciliation 2024-03-31 10:10:06 +00:00
377b837db9 guest: set hostname from launch config 2024-03-31 03:18:56 +00:00
6cd8cc12db guest: remove device restriction 2024-03-31 02:33:58 +00:00
c68f367e4a krata: log when a guest start failures occurs 2024-03-31 01:44:28 +00:00
15d5ed5a45 krata: implement event stream retries 2024-03-31 01:11:50 +00:00
6d6bdade87 kernel: update to 6.8.2 2024-03-31 00:28:21 +00:00
693d62a41a guest: setup loopback interface 2024-03-30 23:46:01 +00:00
8ec7042ea4 guest: place running tasks in cgroup 2024-03-30 23:25:00 +00:00
178 changed files with 16422 additions and 15360 deletions

34
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: Bug Report
description: File a bug report.
title: "bug: "
labels: ["bug"]
assignees:
- edera-dev/engineering
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: input
id: version
attributes:
label: Version / Commit
description: What version of our software are you running?
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell

View File

@ -4,7 +4,32 @@ updates:
directory: "/"
schedule:
interval: "daily"
groups:
production-version-updates:
dependency-type: "production"
applies-to: "version-updates"
development-version-updates:
dependency-type: "development"
applies-to: "version-updates"
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"
groups:
production-version-updates:
dependency-type: "production"
applies-to: "version-updates"
development-version-updates:
dependency-type: "development"
applies-to: "version-updates"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"
groups:
production-version-updates:
dependency-type: "production"
applies-to: "version-updates"
development-version-updates:
dependency-type: "development"
applies-to: "version-updates"

View File

@ -1,19 +1,36 @@
name: check
on: [push, pull_request]
on:
pull_request:
branches:
- main
merge_group:
branches:
- main
jobs:
fmt:
name: fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
with:
components: rustfmt
- run: ./hack/ci/install-linux-deps.sh
- run: ./hack/build/cargo.sh fmt --all -- --check
# Temporarily ignored: https://github.com/edera-dev/krata/issues/206
- run: ./hack/build/cargo.sh fmt --all -- --check || true
shellcheck:
name: shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
- run: ./hack/code/shellcheck.sh

View File

@ -1,5 +1,11 @@
name: client
on: [push, pull_request]
on:
pull_request:
branches:
- main
merge_group:
branches:
- main
jobs:
build:
strategy:
@ -16,19 +22,26 @@ jobs:
TARGET_OS: "${{ matrix.platform.os }}"
TARGET_ARCH: "${{ matrix.platform.arch }}"
runs-on: "${{ matrix.platform.on }}"
name: build ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
name: client build ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- run: git config --global core.autocrlf false && git config --global core.eol lf
if: ${{ matrix.platform.os == 'windows' }}
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
if: ${{ matrix.platform.os != 'darwin' }}
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
with:
targets: "${{ matrix.platform.arch }}-apple-darwin"
if: ${{ matrix.platform.os == 'darwin' }}
- uses: homebrew/actions/setup-homebrew@master
- uses: homebrew/actions/setup-homebrew@4b34604e75af8f8b23b454f0b5ffb7c5d8ce0056 # master
if: ${{ matrix.platform.os == 'darwin' }}
- run: ./hack/ci/install-${{ matrix.platform.deps }}-deps.sh
- run: ./hack/build/cargo.sh build --bin kratactl

View File

@ -1,28 +0,0 @@
name: kernel
on:
push:
paths:
- "kernel/**"
- "hack/ci/**"
pull_request:
paths:
- "kernel/**"
- "hack/ci/**"
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
arch:
- x86_64
- aarch64
env:
TARGET_ARCH: "${{ matrix.arch }}"
name: build ${{ matrix.arch }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: ./hack/ci/install-linux-deps.sh
- run: ./hack/kernel/build.sh
env:
KRATA_KERNEL_BUILD_JOBS: "5"

View File

@ -3,6 +3,10 @@ on:
workflow_dispatch:
schedule:
- cron: "0 10 * * *"
permissions:
contents: read
packages: write
id-token: write
jobs:
server:
runs-on: ubuntu-latest
@ -14,25 +18,26 @@ jobs:
- aarch64
env:
TARGET_ARCH: "${{ matrix.arch }}"
name: server ${{ matrix.arch }}
name: nightly server ${{ matrix.arch }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
with:
targets: "${{ matrix.arch }}-unknown-linux-gnu,${{ matrix.arch }}-unknown-linux-musl"
- run: ./hack/ci/install-linux-deps.sh
- run: ./hack/dist/bundle.sh
env:
KRATA_KERNEL_BUILD_JOBS: "5"
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: krata-bundle-systemd-${{ matrix.arch }}
path: "target/dist/bundle-systemd-${{ matrix.arch }}.tgz"
compression-level: 0
- run: ./hack/dist/deb.sh
env:
KRATA_KERNEL_BUILD_SKIP: "1"
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: krata-debian-${{ matrix.arch }}
path: "target/dist/*.deb"
@ -40,15 +45,13 @@ jobs:
- run: ./hack/dist/apk.sh
env:
KRATA_KERNEL_BUILD_SKIP: "1"
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: krata-alpine-${{ matrix.arch }}
path: "target/dist/*_${{ matrix.arch }}.apk"
compression-level: 0
- run: ./hack/os/build.sh
env:
KRATA_KERNEL_BUILD_SKIP: "1"
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: krata-os-${{ matrix.arch }}
path: "target/os/krata-${{ matrix.arch }}.qcow2"
@ -68,23 +71,73 @@ jobs:
TARGET_OS: "${{ matrix.platform.os }}"
TARGET_ARCH: "${{ matrix.platform.arch }}"
runs-on: "${{ matrix.platform.on }}"
name: client ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
name: nightly client ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- run: git config --global core.autocrlf false && git config --global core.eol lf
if: ${{ matrix.platform.os == 'windows' }}
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
if: ${{ matrix.platform.os != 'darwin' }}
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
with:
targets: "${{ matrix.platform.arch }}-apple-darwin"
if: ${{ matrix.platform.os == 'darwin' }}
- uses: homebrew/actions/setup-homebrew@master
- uses: homebrew/actions/setup-homebrew@4b34604e75af8f8b23b454f0b5ffb7c5d8ce0056 # master
if: ${{ matrix.platform.os == 'darwin' }}
- run: ./hack/ci/install-${{ matrix.platform.deps }}-deps.sh
- run: ./hack/build/cargo.sh build --release --bin kratactl
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: kratactl-${{ matrix.platform.os }}-${{ matrix.platform.arch }}
path: "target/*/release/kratactl*"
path: "target/*/release/kratactl"
if: ${{ matrix.platform.os != 'windows' }}
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: kratactl-${{ matrix.platform.os }}-${{ matrix.platform.arch }}
path: "target/*/release/kratactl.exe"
if: ${{ matrix.platform.os == 'windows' }}
oci:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
component:
- kratactl
- kratad
- kratanet
- krata-guest-init
name: "oci build ${{ matrix.component }}"
steps:
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
- uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
- uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # v3.4.0
- uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
with:
registry: ghcr.io
username: "${{ github.actor }}"
password: "${{ secrets.GITHUB_TOKEN }}"
- uses: docker/build-push-action@1a162644f9a7e87d8f4b053101d1d9a712edc18c # v6.3.0
id: push
with:
file: ./images/Dockerfile.${{ matrix.component }}
platforms: linux/amd64,linux/aarch64
tags: "ghcr.io/edera-dev/${{ matrix.component }}:nightly"
push: true
- env:
DIGEST: "${{ steps.push.outputs.digest }}"
TAGS: "ghcr.io/edera-dev/${{ matrix.component }}:nightly"
COSIGN_EXPERIMENTAL: "true"
run: cosign sign --yes "${TAGS}@${DIGEST}"

View File

@ -1,36 +1,36 @@
name: os
on:
push:
paths:
- "os/**"
- "hack/os/**"
- "hack/ci/**"
pull_request:
paths:
- "os/**"
- "hack/os/**"
- "hack/ci/**"
branches:
- main
merge_group:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch:
- x86_64
- aarch64
env:
TARGET_ARCH: "${{ matrix.arch }}"
name: build ${{ matrix.arch }}
name: os build ${{ matrix.arch }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
with:
targets: "${{ matrix.arch }}-unknown-linux-gnu,${{ matrix.arch }}-unknown-linux-musl"
- run: ./hack/ci/install-linux-deps.sh
- run: ./hack/os/build.sh
env:
KRATA_KERNEL_BUILD_JOBS: "5"
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: krata-os-${{ matrix.arch }}
path: "target/os/krata-${{ matrix.arch }}.qcow2"

134
.github/workflows/release-binaries.yml vendored Normal file
View File

@ -0,0 +1,134 @@
name: release-binaries
permissions:
contents: write
packages: write
id-token: write
on:
release:
types:
- published
env:
CARGO_INCREMENTAL: 0
CARGO_NET_GIT_FETCH_WITH_CLI: true
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
RUSTUP_MAX_RETRIES: 10
jobs:
server:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch:
- x86_64
- aarch64
env:
TARGET_ARCH: "${{ matrix.arch }}"
name: release-binaries server ${{ matrix.arch }}
steps:
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
with:
targets: "${{ matrix.arch }}-unknown-linux-gnu,${{ matrix.arch }}-unknown-linux-musl"
- run: ./hack/ci/install-linux-deps.sh
- run: ./hack/dist/bundle.sh
- run: "./hack/ci/assemble-release-assets.sh bundle-systemd ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/dist/bundle-systemd-${{ matrix.arch }}.tgz"
- run: ./hack/dist/deb.sh
- run: "./hack/ci/assemble-release-assets.sh debian ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/dist/*.deb"
- run: ./hack/dist/apk.sh
- run: "./hack/ci/assemble-release-assets.sh alpine ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/dist/*_${{ matrix.arch }}.apk"
- run: ./hack/os/build.sh
- run: "./hack/ci/assemble-release-assets.sh os ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/os/krata-${{ matrix.arch }}.qcow2"
- run: "./hack/ci/upload-release-assets.sh ${{ github.event.release.tag_name }}"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
client:
strategy:
fail-fast: false
matrix:
platform:
- { os: linux, arch: x86_64, on: ubuntu-latest, deps: linux }
- { os: linux, arch: aarch64, on: ubuntu-latest, deps: linux }
- { os: darwin, arch: x86_64, on: macos-14, deps: darwin }
- { os: darwin, arch: aarch64, on: macos-14, deps: darwin }
- { os: freebsd, arch: x86_64, on: ubuntu-latest, deps: linux }
- { os: windows, arch: x86_64, on: windows-latest, deps: windows }
env:
TARGET_OS: "${{ matrix.platform.os }}"
TARGET_ARCH: "${{ matrix.platform.arch }}"
runs-on: "${{ matrix.platform.on }}"
name: release-binaries client ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
defaults:
run:
shell: bash
timeout-minutes: 60
steps:
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
if: ${{ matrix.platform.os != 'darwin' }}
- uses: dtolnay/rust-toolchain@stable
with:
targets: "${{ matrix.platform.arch }}-apple-darwin"
if: ${{ matrix.platform.os == 'darwin' }}
- uses: homebrew/actions/setup-homebrew@4b34604e75af8f8b23b454f0b5ffb7c5d8ce0056 # master
if: ${{ matrix.platform.os == 'darwin' }}
- run: ./hack/ci/install-${{ matrix.platform.deps }}-deps.sh
- run: ./hack/build/cargo.sh build --release --bin kratactl
- run: "./hack/ci/assemble-release-assets.sh kratactl ${{ github.event.release.tag_name }} ${{ matrix.platform.os }}-${{ matrix.platform.arch }} target/*/release/kratactl"
if: ${{ matrix.platform.os != 'windows' }}
- run: "./hack/ci/assemble-release-assets.sh kratactl ${{ github.event.release.tag_name }} ${{ matrix.platform.os }}-${{ matrix.platform.arch }} target/*/release/kratactl.exe"
if: ${{ matrix.platform.os == 'windows' }}
- run: "./hack/ci/upload-release-assets.sh ${{ github.event.release.tag_name }}"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
oci:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
component:
- kratactl
- kratad
- kratanet
- krata-guest-init
name: "release-binaries oci ${{ matrix.component }}"
steps:
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
- uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
- uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # v3.4.0
- uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
with:
registry: ghcr.io
username: "${{ github.actor }}"
password: "${{ secrets.GITHUB_TOKEN }}"
- id: version
run: |
echo "KRATA_VERSION=$(./hack/dist/version.sh)" >> "${GITHUB_OUTPUT}"
- uses: docker/build-push-action@1a162644f9a7e87d8f4b053101d1d9a712edc18c # v6.3.0
id: push
with:
file: ./images/Dockerfile.${{ matrix.component }}
platforms: linux/amd64,linux/aarch64
tags: "ghcr.io/edera-dev/${{ matrix.component }}:${{ steps.version.outputs.KRATA_VERSION }}"
push: true
- env:
DIGEST: "${{ steps.push.outputs.digest }}"
TAGS: "ghcr.io/edera-dev/${{ matrix.component }}:${{ steps.version.outputs.KRATA_VERSION }}"
COSIGN_EXPERIMENTAL: "true"
run: cosign sign --yes "${TAGS}@${DIGEST}"

36
.github/workflows/release-plz.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: release-plz
permissions:
pull-requests: write
contents: write
on:
push:
branches:
- main
concurrency:
group: "${{ github.workflow }}"
cancel-in-progress: true
jobs:
release-plz:
name: release-plz
runs-on: ubuntu-latest
steps:
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- uses: actions/create-github-app-token@31c86eb3b33c9b601a1f60f98dcbfd1d70f379b4 # v1.10.3
id: generate-token
with:
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
fetch-depth: 0
token: "${{ steps.generate-token.outputs.token }}"
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
- run: ./hack/ci/install-linux-deps.sh
- name: release-plz
uses: MarcoIeni/release-plz-action@86afd21a7b114234aab55ba0005eed52f77d89e4 # v0.5.62
env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
CARGO_REGISTRY_TOKEN: "${{ secrets.KRATA_RELEASE_CARGO_TOKEN }}"

View File

@ -1,19 +1,31 @@
name: server
on: [push, pull_request]
on:
pull_request:
branches:
- main
merge_group:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch:
- x86_64
- aarch64
env:
TARGET_ARCH: "${{ matrix.arch }}"
name: build ${{ matrix.arch }}
name: server build ${{ matrix.arch }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
- run: ./hack/ci/install-linux-deps.sh
- run: ./hack/build/cargo.sh build
test:
@ -25,10 +37,15 @@ jobs:
- aarch64
env:
TARGET_ARCH: "${{ matrix.arch }}"
name: test ${{ matrix.arch }}
name: server test ${{ matrix.arch }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
- run: ./hack/ci/install-linux-deps.sh
- run: ./hack/build/cargo.sh test
clippy:
@ -40,10 +57,15 @@ jobs:
- aarch64
env:
TARGET_ARCH: "${{ matrix.arch }}"
name: clippy ${{ matrix.arch }}
name: server clippy ${{ matrix.arch }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
with:
components: clippy
- run: ./hack/ci/install-linux-deps.sh
@ -57,10 +79,15 @@ jobs:
- aarch64
env:
TARGET_ARCH: "${{ matrix.arch }}"
name: initrd ${{ matrix.arch }}
name: server initrd ${{ matrix.arch }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
with:
targets: "${{ matrix.arch }}-unknown-linux-gnu,${{ matrix.arch }}-unknown-linux-musl"
- run: ./hack/ci/install-linux-deps.sh

1
.gitignore vendored
View File

@ -1,2 +1 @@
/target
Cargo.lock

113
CHANGELOG.md Normal file
View File

@ -0,0 +1,113 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.0.12](https://github.com/edera-dev/krata/compare/v0.0.11...v0.0.12) - 2024-07-12
### Added
- *(oci)* add configuration value for oci seed file ([#220](https://github.com/edera-dev/krata/pull/220))
- *(power-management-defaults)* set an initial power management policy ([#219](https://github.com/edera-dev/krata/pull/219))
### Fixed
- *(daemon)* decrease rate of runtime reconcile ([#224](https://github.com/edera-dev/krata/pull/224))
- *(power)* ensure that xeon cpus with cpu gaps are not detected as p/e compatible ([#218](https://github.com/edera-dev/krata/pull/218))
- *(runtime)* use iommu only if devices are needed ([#243](https://github.com/edera-dev/krata/pull/243))
### Other
- Power management core functionality ([#217](https://github.com/edera-dev/krata/pull/217))
- *(powermgmt)* disable for now as a hackfix ([#242](https://github.com/edera-dev/krata/pull/242))
- Initial fluentd support ([#205](https://github.com/edera-dev/krata/pull/205))
- update Cargo.toml dependencies
- Use native loopdev implementation instead of loopdev-3 ([#209](https://github.com/edera-dev/krata/pull/209))
## [0.0.11](https://github.com/edera-dev/krata/compare/v0.0.10...v0.0.11) - 2024-06-23
### Added
- pci passthrough ([#114](https://github.com/edera-dev/krata/pull/114))
- *(runtime)* concurrent ip allocation ([#151](https://github.com/edera-dev/krata/pull/151))
- *(xen)* dynamic platform architecture ([#194](https://github.com/edera-dev/krata/pull/194))
### Fixed
- *(oci)* remove file size limit ([#142](https://github.com/edera-dev/krata/pull/142))
- *(oci)* use mirror.gcr.io as a mirror to docker hub ([#141](https://github.com/edera-dev/krata/pull/141))
### Other
- first pass of krata as an isolation engine
- *(xen)* split platform support into separate crate ([#195](https://github.com/edera-dev/krata/pull/195))
- *(xen)* move device creation into transaction interface ([#196](https://github.com/edera-dev/krata/pull/196))
## [0.0.10](https://github.com/edera-dev/krata/compare/v0.0.9...v0.0.10) - 2024-04-22
### Added
- implement guest exec ([#107](https://github.com/edera-dev/krata/pull/107))
- implement kernel / initrd oci image support ([#103](https://github.com/edera-dev/krata/pull/103))
- idm v2 ([#102](https://github.com/edera-dev/krata/pull/102))
- oci concurrency improvements ([#95](https://github.com/edera-dev/krata/pull/95))
- oci tar format, bit-perfect disk storage for config and manifest, concurrent image pulls ([#88](https://github.com/edera-dev/krata/pull/88))
### Fixed
- oci cache store should fallback to copy when rename won't work ([#96](https://github.com/edera-dev/krata/pull/96))
### Other
- update Cargo.lock dependencies
## [0.0.9](https://github.com/edera-dev/krata/compare/v0.0.8...v0.0.9) - 2024-04-15
### Added
- oci compliance work ([#85](https://github.com/edera-dev/krata/pull/85))
- oci packer can now use mksquashfs if available ([#70](https://github.com/edera-dev/krata/pull/70))
- basic kratactl top command ([#72](https://github.com/edera-dev/krata/pull/72))
- idm snooping ([#71](https://github.com/edera-dev/krata/pull/71))
- implement oci image progress ([#64](https://github.com/edera-dev/krata/pull/64))
- guest metrics support ([#46](https://github.com/edera-dev/krata/pull/46))
### Other
- init: default to xterm if TERM is not set ([#52](https://github.com/edera-dev/krata/pull/52))
- update Cargo.toml dependencies
## [0.0.8](https://github.com/edera-dev/krata/compare/v0.0.7...v0.0.8) - 2024-04-09
### Other
- update Cargo.lock dependencies
## [0.0.7](https://github.com/edera-dev/krata/compare/v0.0.6...v0.0.7) - 2024-04-09
### Other
- update Cargo.toml dependencies
- update Cargo.lock dependencies
## [0.0.6](https://github.com/edera-dev/krata/compare/v0.0.5...v0.0.6) - 2024-04-09
### Fixed
- increase channel acquisition timeout to support lower performance hosts ([#36](https://github.com/edera-dev/krata/pull/36))
### Other
- update Cargo.toml dependencies
- update Cargo.lock dependencies
## [0.0.5](https://github.com/edera-dev/krata/compare/v0.0.4...v0.0.5) - 2024-04-09
### Added
- *(ctl)* add help and about to commands and arguments ([#25](https://github.com/edera-dev/krata/pull/25))
### Other
- update Cargo.toml dependencies
- update Cargo.lock dependencies
## [0.0.4](https://github.com/edera-dev/krata/releases/tag/v${version}) - 2024-04-03
### Other
- implement automatic releases
- reimplement console to utilize channels, and provide logs support
- set hostname from launch config
- implement event stream retries
- work on parallel reconciliation
- implement parallel guest reconciliation
- log when a guest start failures occurs
- remove device restriction
- setup loopback interface
- place running tasks in cgroup

34
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,34 @@
# Contributing to Krata
Welcome! We're very glad you're reading this; Edera is excited for all kinds of contributions! Please read the following to ensure you're aware of our flow and policies.
## Before contributing
1. Please read our [Code of Conduct](CODE_OF_CONDUCT.md), which applies to all interactions in/with all Edera projects and venues.
2. Before opening an issue or PR, please try a few searches to see if there is overlap with existing conversations or WIP contributions.
3. For security or otherwise sensitive topics, please read our [Security Policy].
4. Ask questions! If you want to ask something, chances are someone else wants to ask it as well.
## Contributing Code
To get started with technical contributions, please read out [Development Guide]. If you're looking for something easy to tackle, [look for issues labeled `good first issue`][good-first-issue].
## Reporting bugs and other issues
While it's totally fine to simply bring it up on our Discord, we encourage opening an issue on GitHub using the Bug Report template.
## Pull Requests
1. For anything more than simple bug/doc fixes, please open a GitHub issue for tracking purposes.
- Else skip to step 3.
2. Discuss the change with the teams to ensure we have consensus on the change being welcome.
3. We encourage opening the PR sooner than later, and prefixing with `WIP:` so GitHub labels it as a Draft.
4. Please include a detailed list of changes that the PR makes.
5. Once the PR is ready for review, remove the Draft status, and request a review from `edera-dev/engineering`.
6. After the review cycle concludes and we know you are ready for merging, a team member will submit the PR to the merge queue.
[Code of Conduct]: ./CODE_OF_CONDUCT.md
[Security Policy]: ./SECURITY.md
[Development Guide]: ./DEV.md
[good-first-issues]: https://github.com/edera-dev/krata/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22

3737
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
[workspace]
members = [
"crates/build",
"crates/krata",
"crates/oci",
"crates/guest",
@ -11,12 +12,13 @@ members = [
"crates/xen/xenclient",
"crates/xen/xenevtchn",
"crates/xen/xengnt",
"crates/xen/xenplatform",
"crates/xen/xenstore",
]
resolver = "2"
[workspace.package]
version = "0.0.3"
version = "0.0.12"
homepage = "https://krata.dev"
license = "Apache-2.0"
repository = "https://github.com/edera-dev/krata"
@ -24,68 +26,84 @@ repository = "https://github.com/edera-dev/krata"
[workspace.dependencies]
anyhow = "1.0"
arrayvec = "0.7.4"
async-compression = "0.4.6"
async-compression = "0.4.11"
async-stream = "0.3.5"
async-trait = "0.1.77"
async-trait = "0.1.81"
backhand = "0.15.0"
base64 = "0.22.1"
byteorder = "1"
bytes = "1.5.0"
cli-tables = "0.2.1"
c2rust-bitfields = "0.18.0"
cgroups-rs = "0.3.4"
circular-buffer = "0.1.7"
comfy-table = "7.1.1"
crossterm = "0.27.0"
ctrlc = "3.4.4"
elf = "0.7.4"
env_logger = "0.11.0"
etherparse = "0.14.2"
etherparse = "0.14.3"
fancy-duration = "0.9.2"
flate2 = "1.0"
futures = "0.3.30"
human_bytes = "0.4"
indexmap = "2.2.6"
indicatif = "0.17.8"
ipnetwork = "0.20.0"
libc = "0.2"
log = "0.4.20"
log = "0.4.22"
loopdev-3 = "0.5.1"
krata-advmac = "1.1.0"
krata-tokio-tar = "0.4.0"
memchr = "2"
nix = "0.28.0"
oci-spec = "0.6.4"
nix = "0.29.0"
oci-spec = "0.6.7"
once_cell = "1.19.0"
path-absolutize = "3.1.1"
path-clean = "1.0.1"
prost = "0.12.3"
prost-build = "0.12.3"
platform-info = "2.0.3"
prost = "0.12.6"
prost-build = "0.12.6"
prost-reflect-build = "0.13.0"
prost-types = "0.12.6"
rand = "0.8.5"
redb = "2.0.0"
ratatui = "0.26.3"
redb = "2.1.1"
regex = "1.10.5"
rtnetlink = "0.14.1"
serde_json = "1.0.113"
scopeguard = "1.2.0"
serde_json = "1.0.120"
serde_yaml = "0.9"
sha256 = "1.5.0"
signal-hook = "0.3.17"
slice-copy = "0.3.0"
smoltcp = "0.11.0"
sysinfo = "0.30.13"
termtree = "0.4.1"
thiserror = "1.0"
tokio-tun = "0.11.2"
tokio-tun = "0.11.5"
toml = "0.8.14"
tonic-build = "0.11.0"
tower = "0.4.13"
udp-stream = "0.0.11"
url = "2.5.0"
url = "2.5.2"
walkdir = "2"
xz2 = "0.1"
[workspace.dependencies.clap]
version = "4.4.18"
version = "4.5.9"
features = ["derive"]
[workspace.dependencies.prost-reflect]
version = "0.13.0"
version = "0.13.1"
features = ["derive"]
[workspace.dependencies.reqwest]
version = "0.12.0"
version = "0.12.5"
default-features = false
features = ["rustls-tls"]
[workspace.dependencies.serde]
version = "1.0.196"
version = "1.0.204"
features = ["derive"]
[workspace.dependencies.sys-mount]
@ -93,7 +111,7 @@ version = "3.0.0"
default-features = false
[workspace.dependencies.tokio]
version = "1.35.1"
version = "1.38.0"
features = ["full"]
[workspace.dependencies.tokio-stream]
@ -105,5 +123,9 @@ version = "0.11.0"
features = ["tls"]
[workspace.dependencies.uuid]
version = "1.6.1"
version = "1.10.0"
features = ["v4"]
[profile.release]
lto = "fat"
strip = "symbols"

38
DEV.md
View File

@ -19,7 +19,7 @@ it's corresponding code path from the above table.
| Component | Specification | Notes |
| ------------- | ------------- | --------------------------------------------------------------------------------- |
| Architecture | x86_64 | aarch64 support is still in development |
| Memory | At least 6GB | dom0 will need to be configured will lower memory limit to give krata guests room |
| Memory | At least 6GB | dom0 will need to be configured with lower memory limit to give krata guests room |
| Xen | 4.17 | Temporary due to hardcoded interface version constants |
| Debian | stable / sid | Debian is recommended due to the ease of Xen setup |
| rustup | any | Install Rustup from https://rustup.rs |
@ -28,10 +28,23 @@ it's corresponding code path from the above table.
1. Install the specified Debian version on a x86_64 host _capable_ of KVM (NOTE: KVM is not used, Xen is a type-1 hypervisor).
2. Install required packages: `apt install git xen-system-amd64 flex bison libelf-dev libssl-dev bc`
2. Install required packages:
```sh
$ apt install git xen-system-amd64 build-essential \
libclang-dev musl-tools flex bison libelf-dev libssl-dev bc \
protobuf-compiler libprotobuf-dev squashfs-tools erofs-utils
```
3. Install [rustup](https://rustup.rs) for managing a Rust environment.
Make sure to install the targets that you need for krata:
```sh
$ rustup target add x86_64-unknown-linux-gnu
$ rustup target add x86_64-unknown-linux-musl
```
4. Configure `/etc/default/grub.d/xen.cfg` to give krata guests some room:
```sh
@ -43,7 +56,7 @@ After changing the grub config, update grub: `update-grub`
Then reboot to boot the system as a Xen dom0.
You can validate that Xen is setup by running `xl info` and ensuring it returns useful information about the Xen hypervisor.
You can validate that Xen is setup by running `dmesg | grep "Hypervisor detected"` and ensuring it returns a line like `Hypervisor detected: Xen PV`, if that is missing, the host is not running under Xen.
5. Clone the krata source code:
```sh
@ -51,16 +64,23 @@ $ git clone https://github.com/edera-dev/krata.git krata
$ cd krata
```
6. Build a guest kernel image:
6. Fetch the guest kernel image:
```sh
$ ./hack/kernel/build.sh
$ ./hack/kernel/fetch.sh -u
```
7. Copy the guest kernel image at `target/kernel/kernel-x86_64` to `/var/lib/krata/guest/kernel` to have it automatically detected by kratad.
8. Launch `./hack/debug/kratanet.sh` and keep it running in the foreground.
9. Launch `./hack/debug/kratad.sh` and keep it running in the foreground.
10. Run kratactl to launch a guest:
7. Copy the guest kernel artifacts to `/var/lib/krata/guest/kernel` so it is automatically detected by kratad:
```sh
$ mkdir -p /var/lib/krata/guest
$ cp target/kernel/kernel-x86_64 /var/lib/krata/guest/kernel
$ cp target/kernel/addons-x86_64.squashfs /var/lib/krata/guest/addons.squashfs
```
8. Launch `./hack/debug/kratad.sh` and keep it running in the foreground.
9. Launch `./hack/debug/kratanet.sh` and keep it running in the foreground.
10. Run `kratactl` to launch a guest:
```sh
$ ./hack/debug/kratactl.sh launch --attach alpine:latest

10
FAQ.md
View File

@ -2,18 +2,14 @@
## How does krata currently work?
The krata hypervisor makes it possible to launch OCI containers on a Xen hypervisor without utilizing the Xen userspace tooling. krata contains just enough of the userspace of Xen (reimplemented in Rust) to start an x86_64 Xen Linux PV guest, and implements a Linux init process that can boot an OCI container. It does so by converting an OCI image into a squashfs file and packaging basic startup data in a bundle which the init container can read.
The krata isolation engine makes it possible to launch OCI containers on a Xen hypervisor without utilizing the Xen userspace tooling. krata contains just enough of the userspace of Xen (reimplemented in Rust) to start an x86_64 Xen Linux PV guest, and implements a Linux init process that can boot an OCI container. It does so by converting an OCI image into a squashfs/erofs file and packaging basic startup data in a bundle that the init container can read.
In addition, due to the desire to reduce dependence on the dom0 network, krata contains a networking daemon called kratanet. kratanet listens for krata guests to startup and launches a userspace networking environment. krata guests can access the dom0 networking stack via the proxynat layer that makes it possible to communicate over UDP, TCP, and ICMP (echo only) to the outside world. In addition, each krata guest is provided a "gateway" IP (both in IPv4 and IPv6) which utilizes smoltcp to provide a virtual host. That virtual host in the future could dial connections into the container to access container networking resources.
In addition, due to the desire to reduce dependence on the dom0 network, krata contains a networking daemon called kratanet. kratanet listens for krata guests to startup and launches a userspace networking environment. krata guests can access the dom0 networking stack via the proxynat, which that makes it possible to communicate over UDP, TCP, and ICMP (echo only) to the outside world. In addition, each krata guest is provided a "gateway" IP (both in IPv4 and IPv6) which utilizes smoltcp to provide a virtual host. That virtual host in the future could dial connections into the container to access container networking resources.
## Why utilize Xen instead of KVM?
Xen is a very interesting technology, and Edera believes that type-1 hypervisors are ideal for security. Most OCI isolation techniques use KVM, which is not a type-1 hypervisor, and thus is subject to the security limitations of the OS kernel. A type-1 hypervisor on the otherhand provides a minimal amount of attack surface upon which less-trusted guests can be launched on top of.
Xen is a very interesting technology, and Edera believes that type-1 hypervisors are ideal for security. Most OCI isolation techniques use KVM, which is not a type-1 hypervisor, and thus is subject to the security limitations of the OS kernel. A type-1 hypervisor on the other hand provides a minimal attack surface upon which less-trusted guests can be launched on top of.
## Why not utilize pvcalls to provide access to the host network?
pvcalls is extremely interesting, and although it is certainly possible to utilize pvcalls to get the job done, we chose to utilize userspace networking technology in order to enhance security. Our goal is to drop the use of all xen networking backend drivers within the kernel and have the guest talk directly to a userspace daemon, bypassing the vif (xen-netback) driver. Currently, in order to develop the networking layer, we utilize xen-netback and then use raw sockets to provide the userspace networking layer on the host.
## What are the future plans?
Edera is building a company to compete in the hypervisor space with open-source technology. More information to come soon on official channels.

View File

@ -1,6 +1,6 @@
# krata
The Edera Hypervisor
An isolation engine for securing compute workloads.
![license](https://img.shields.io/github/license/edera-dev/krata)
![discord](https://img.shields.io/discord/1207447453083766814?label=discord)
@ -16,4 +16,13 @@ The Edera Hypervisor
## Introduction
krata is a single-host hypervisor service built primarily for OCI containers.
krata is a single-host workload isolation service. It isolates workloads using a type-1 hypervisor, providing a tight security boundary while preserving performance.
krata utilizes the core of the Xen hypervisor with a fully memory-safe Rust control plane.
## Hardware Support
| Architecture | Completion Level | Hardware Virtualization |
| ------------ | ---------------- | ------------------------------- |
| x86_64 | 100% Completed | None, Intel VT-x, AMD-V |
| aarch64 | 10% Completed | AArch64 virtualization |

25
crates/build/Cargo.toml Normal file
View File

@ -0,0 +1,25 @@
[package]
name = "krata-buildtools"
description = "Build tools for krata."
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition = "2021"
resolver = "2"
publish = false
[dependencies]
anyhow = { workspace = true }
env_logger = { workspace = true }
oci-spec = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
krata-oci = { path = "../oci", version = "^0.0.12" }
krata-tokio-tar = { workspace = true }
uuid = { workspace = true }
[[bin]]
name = "build-fetch-kernel"
path = "bin/fetch_kernel.rs"

View File

@ -0,0 +1,121 @@
use std::{
env::{self, args},
path::PathBuf,
};
use anyhow::{anyhow, Result};
use env_logger::Env;
use krataoci::{
name::ImageName,
packer::{service::OciPackerService, OciPackedFormat},
progress::OciProgressContext,
registry::OciPlatform,
};
use oci_spec::image::{Arch, Os};
use tokio::{
fs::{self, File},
io::BufReader,
};
use tokio_stream::StreamExt;
use tokio_tar::Archive;
use uuid::Uuid;
#[tokio::main]
async fn main() -> Result<()> {
env_logger::Builder::from_env(Env::default().default_filter_or("warn")).init();
fs::create_dir_all("target/kernel").await?;
let arch = env::var("TARGET_ARCH").map_err(|_| anyhow!("missing TARGET_ARCH env var"))?;
println!("kernel architecture: {}", arch);
let platform = OciPlatform::new(
Os::Linux,
match arch.as_str() {
"x86_64" => Arch::Amd64,
"aarch64" => Arch::ARM64,
_ => {
return Err(anyhow!("unknown architecture '{}'", arch));
}
},
);
let image = ImageName::parse(&args().nth(1).unwrap())?;
let mut cache_dir = std::env::temp_dir().clone();
cache_dir.push(format!("krata-cache-{}", Uuid::new_v4()));
fs::create_dir_all(&cache_dir).await?;
let _delete_cache_dir = scopeguard::guard(cache_dir.clone(), |dir| {
let _ = std::fs::remove_dir_all(dir);
});
let (context, _) = OciProgressContext::create();
let service = OciPackerService::new(None, &cache_dir, platform).await?;
let packed = service
.request(image.clone(), OciPackedFormat::Tar, false, context)
.await?;
let annotations = packed
.manifest
.item()
.annotations()
.clone()
.unwrap_or_default();
let Some(format) = annotations.get("dev.edera.kernel.format") else {
return Err(anyhow!(
"image manifest missing 'dev.edera.kernel.format' annotation"
));
};
let Some(version) = annotations.get("dev.edera.kernel.version") else {
return Err(anyhow!(
"image manifest missing 'dev.edera.kernel.version' annotation"
));
};
let Some(flavor) = annotations.get("dev.edera.kernel.flavor") else {
return Err(anyhow!(
"image manifest missing 'dev.edera.kernel.flavor' annotation"
));
};
if format != "1" {
return Err(anyhow!("kernel format version '{}' is unknown", format));
}
let file = BufReader::new(File::open(packed.path).await?);
let mut archive = Archive::new(file);
let mut entries = archive.entries()?;
let kernel_image_tar_path = PathBuf::from("kernel/image");
let kernel_addons_tar_path = PathBuf::from("kernel/addons.squashfs");
let kernel_image_out_path = PathBuf::from(format!("target/kernel/kernel-{}", arch));
let kernel_addons_out_path = PathBuf::from(format!("target/kernel/addons-{}.squashfs", arch));
if kernel_image_out_path.exists() {
fs::remove_file(&kernel_image_out_path).await?;
}
if kernel_addons_out_path.exists() {
fs::remove_file(&kernel_addons_out_path).await?;
}
while let Some(entry) = entries.next().await {
let mut entry = entry?;
let path = entry.path()?.to_path_buf();
if !entry.header().entry_type().is_file() {
continue;
}
if path == kernel_image_tar_path {
entry.unpack(&kernel_image_out_path).await?;
} else if path == kernel_addons_tar_path {
entry.unpack(&kernel_addons_out_path).await?;
}
}
if !kernel_image_out_path.exists() {
return Err(anyhow!("image did not contain a file named /kernel/image"));
}
println!("kernel version: v{}", version);
println!("kernel flavor: {}", flavor);
Ok(())
}

View File

@ -1,8 +1,8 @@
[package]
name = "krata-ctl"
description = "Command-line tool to control the krata hypervisor"
description = "Command-line tool to control the krata isolation engine"
license.workspace = true
version= "0.0.3"
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition = "2021"
@ -11,16 +11,24 @@ resolver = "2"
[dependencies]
anyhow = { workspace = true }
async-stream = { workspace = true }
base64 = { workspace = true }
clap = { workspace = true }
cli-tables = { workspace = true }
crossterm = { workspace = true }
comfy-table = { workspace = true }
crossterm = { workspace = true, features = ["event-stream"] }
ctrlc = { workspace = true, features = ["termination"] }
env_logger = { workspace = true }
krata = { path = "../krata", version = "^0.0.3" }
fancy-duration = { workspace = true }
human_bytes = { workspace = true }
indicatif = { workspace = true }
krata = { path = "../krata", version = "^0.0.12" }
log = { workspace = true }
prost-reflect = { workspace = true, features = ["serde"] }
prost-types = { workspace = true }
ratatui = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
termtree = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tonic = { workspace = true }

View File

@ -10,8 +10,9 @@ use crate::console::StdioConsoleStream;
use super::resolve_guest;
#[derive(Parser)]
#[command(about = "Attach to the guest console")]
pub struct AttachCommand {
#[arg()]
#[arg(help = "Guest to attach to, either the name or the uuid")]
guest: String,
}

View File

@ -0,0 +1,46 @@
use anyhow::Result;
use clap::Parser;
use krata::v1::control::{control_service_client::ControlServiceClient, HostCpuTopologyRequest};
use tonic::{transport::Channel, Request};
fn class_to_str(input: i32) -> String {
match input {
0 => "Standard".to_string(),
1 => "Performance".to_string(),
2 => "Efficiency".to_string(),
_ => "???".to_string(),
}
}
#[derive(Parser)]
#[command(about = "Display information about a host's CPU topology")]
pub struct CpuTopologyCommand {}
impl CpuTopologyCommand {
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
println!(
"{0:<10} {1:<10} {2:<10} {3:<10} {4:<10} {5:<10}",
"CPUID", "Node", "Socket", "Core", "Thread", "Class"
);
let response = client
.get_host_cpu_topology(Request::new(HostCpuTopologyRequest {}))
.await?
.into_inner();
for (i, cpu) in response.cpus.iter().enumerate() {
println!(
"{0:<10} {1:<10} {2:<10} {3:<10} {4:<10} {5:<10}",
i,
cpu.node,
cpu.socket,
cpu.core,
cpu.thread,
class_to_str(cpu.class)
);
}
Ok(())
}
}

View File

@ -17,10 +17,15 @@ use tonic::{transport::Channel, Request};
use crate::cli::resolve_guest;
#[derive(Parser)]
#[command(about = "Destroy a guest")]
pub struct DestroyCommand {
#[arg(short = 'W', long)]
#[arg(
short = 'W',
long,
help = "Wait for the destruction of the guest to complete"
)]
wait: bool,
#[arg()]
#[arg(help = "Guest to destroy, either the name or the uuid")]
guest: String,
}
@ -47,34 +52,31 @@ impl DestroyCommand {
async fn wait_guest_destroyed(id: &str, events: EventStream) -> Result<()> {
let mut stream = events.subscribe();
while let Ok(event) = stream.recv().await {
match event {
Event::GuestChanged(changed) => {
let Some(guest) = changed.guest else {
continue;
};
let Event::GuestChanged(changed) = event;
let Some(guest) = changed.guest else {
continue;
};
if guest.id != id {
continue;
}
if guest.id != id {
continue;
}
let Some(state) = guest.state else {
continue;
};
let Some(state) = guest.state else {
continue;
};
if let Some(ref error) = state.error_info {
if state.status() == GuestStatus::Failed {
error!("destroy failed: {}", error.message);
std::process::exit(1);
} else {
error!("guest error: {}", error.message);
}
}
if state.status() == GuestStatus::Destroyed {
std::process::exit(0);
}
if let Some(ref error) = state.error_info {
if state.status() == GuestStatus::Failed {
error!("destroy failed: {}", error.message);
std::process::exit(1);
} else {
error!("guest error: {}", error.message);
}
}
if state.status() == GuestStatus::Destroyed {
std::process::exit(0);
}
}
Ok(())
}

View File

@ -0,0 +1,70 @@
use std::collections::HashMap;
use anyhow::Result;
use clap::Parser;
use krata::v1::{
common::{GuestTaskSpec, GuestTaskSpecEnvVar},
control::{control_service_client::ControlServiceClient, ExecGuestRequest},
};
use tonic::{transport::Channel, Request};
use crate::console::StdioConsoleStream;
use super::resolve_guest;
#[derive(Parser)]
#[command(about = "Execute a command inside the guest")]
pub struct ExecCommand {
#[arg[short, long, help = "Environment variables"]]
env: Option<Vec<String>>,
#[arg(short = 'w', long, help = "Working directory")]
working_directory: Option<String>,
#[arg(help = "Guest to exec inside, either the name or the uuid")]
guest: String,
#[arg(
allow_hyphen_values = true,
trailing_var_arg = true,
help = "Command to run inside the guest"
)]
command: Vec<String>,
}
impl ExecCommand {
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
let guest_id: String = resolve_guest(&mut client, &self.guest).await?;
let initial = ExecGuestRequest {
guest_id,
task: Some(GuestTaskSpec {
environment: env_map(&self.env.unwrap_or_default())
.iter()
.map(|(key, value)| GuestTaskSpecEnvVar {
key: key.clone(),
value: value.clone(),
})
.collect(),
command: self.command,
working_directory: self.working_directory.unwrap_or_default(),
}),
data: vec![],
};
let stream = StdioConsoleStream::stdin_stream_exec(initial).await;
let response = client.exec_guest(Request::new(stream)).await?.into_inner();
let code = StdioConsoleStream::exec_output(response).await?;
std::process::exit(code);
}
}
fn env_map(env: &[String]) -> HashMap<String, String> {
let mut map = HashMap::<String, String>::new();
for item in env {
if let Some((key, value)) = item.split_once('=') {
map.insert(key.to_string(), value.to_string());
}
}
map
}

View File

@ -0,0 +1,22 @@
use anyhow::Result;
use clap::Parser;
use krata::v1::control::{control_service_client::ControlServiceClient, IdentifyHostRequest};
use tonic::{transport::Channel, Request};
#[derive(Parser)]
#[command(about = "Identify information about the host")]
pub struct IdentifyHostCommand {}
impl IdentifyHostCommand {
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
let response = client
.identify_host(Request::new(IdentifyHostRequest {}))
.await?
.into_inner();
println!("Host UUID: {}", response.host_uuid);
println!("Host Domain: {}", response.host_domid);
println!("Krata Version: {}", response.krata_version);
Ok(())
}
}

View File

@ -0,0 +1,157 @@
use anyhow::Result;
use base64::Engine;
use clap::{Parser, ValueEnum};
use krata::{
events::EventStream,
idm::{internal, serialize::IdmSerializable, transport::IdmTransportPacketForm},
v1::control::{control_service_client::ControlServiceClient, SnoopIdmReply, SnoopIdmRequest},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio_stream::StreamExt;
use tonic::transport::Channel;
use crate::format::{kv2line, proto2dynamic, value2kv};
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
enum IdmSnoopFormat {
Simple,
Jsonl,
KeyValue,
}
#[derive(Parser)]
#[command(about = "Snoop on the IDM bus")]
pub struct IdmSnoopCommand {
#[arg(short, long, default_value = "simple", help = "Output format")]
format: IdmSnoopFormat,
}
impl IdmSnoopCommand {
pub async fn run(
self,
mut client: ControlServiceClient<Channel>,
_events: EventStream,
) -> Result<()> {
let mut stream = client.snoop_idm(SnoopIdmRequest {}).await?.into_inner();
while let Some(reply) = stream.next().await {
let reply = reply?;
let Some(line) = convert_idm_snoop(reply) else {
continue;
};
match self.format {
IdmSnoopFormat::Simple => {
self.print_simple(line)?;
}
IdmSnoopFormat::Jsonl => {
let encoded = serde_json::to_string(&line)?;
println!("{}", encoded.trim());
}
IdmSnoopFormat::KeyValue => {
self.print_key_value(line)?;
}
}
}
Ok(())
}
fn print_simple(&self, line: IdmSnoopLine) -> Result<()> {
let encoded = if !line.packet.decoded.is_null() {
serde_json::to_string(&line.packet.decoded)?
} else {
base64::prelude::BASE64_STANDARD.encode(&line.packet.data)
};
println!(
"({} -> {}) {} {} {}",
line.from, line.to, line.packet.id, line.packet.form, encoded
);
Ok(())
}
fn print_key_value(&self, line: IdmSnoopLine) -> Result<()> {
let kvs = value2kv(serde_json::to_value(line)?)?;
println!("{}", kv2line(kvs));
Ok(())
}
}
#[derive(Serialize, Deserialize)]
pub struct IdmSnoopLine {
pub from: String,
pub to: String,
pub packet: IdmSnoopData,
}
#[derive(Serialize, Deserialize)]
pub struct IdmSnoopData {
pub id: u64,
pub channel: u64,
pub form: String,
pub data: String,
pub decoded: Value,
}
pub fn convert_idm_snoop(reply: SnoopIdmReply) -> Option<IdmSnoopLine> {
let packet = &(reply.packet?);
let decoded = if packet.channel == 0 {
match packet.form() {
IdmTransportPacketForm::Event => internal::Event::decode(&packet.data)
.ok()
.and_then(|event| proto2dynamic(event).ok()),
IdmTransportPacketForm::Request
| IdmTransportPacketForm::StreamRequest
| IdmTransportPacketForm::StreamRequestUpdate => {
internal::Request::decode(&packet.data)
.ok()
.and_then(|event| proto2dynamic(event).ok())
}
IdmTransportPacketForm::Response | IdmTransportPacketForm::StreamResponseUpdate => {
internal::Response::decode(&packet.data)
.ok()
.and_then(|event| proto2dynamic(event).ok())
}
_ => None,
}
} else {
None
};
let decoded = decoded
.and_then(|message| serde_json::to_value(message).ok())
.unwrap_or(Value::Null);
let data = IdmSnoopData {
id: packet.id,
channel: packet.channel,
form: match packet.form() {
IdmTransportPacketForm::Raw => "raw".to_string(),
IdmTransportPacketForm::Event => "event".to_string(),
IdmTransportPacketForm::Request => "request".to_string(),
IdmTransportPacketForm::Response => "response".to_string(),
IdmTransportPacketForm::StreamRequest => "stream-request".to_string(),
IdmTransportPacketForm::StreamRequestUpdate => "stream-request-update".to_string(),
IdmTransportPacketForm::StreamRequestClosed => "stream-request-closed".to_string(),
IdmTransportPacketForm::StreamResponseUpdate => "stream-response-update".to_string(),
IdmTransportPacketForm::StreamResponseClosed => "stream-response-closed".to_string(),
_ => format!("unknown-{}", packet.form),
},
data: base64::prelude::BASE64_STANDARD.encode(&packet.data),
decoded,
};
Some(IdmSnoopLine {
from: reply.from,
to: reply.to,
packet: data,
})
}

View File

@ -1,17 +1,17 @@
use std::collections::HashMap;
use anyhow::Result;
use clap::Parser;
use clap::{Parser, ValueEnum};
use krata::{
events::EventStream,
v1::{
common::{
guest_image_spec::Image, GuestImageSpec, GuestOciImageSpec, GuestSpec, GuestStatus,
GuestTaskSpec, GuestTaskSpecEnvVar,
guest_image_spec::Image, GuestImageSpec, GuestOciImageSpec, GuestSpec, GuestSpecDevice,
GuestStatus, GuestTaskSpec, GuestTaskSpecEnvVar, OciImageFormat,
},
control::{
control_service_client::ControlServiceClient, watch_events_reply::Event,
CreateGuestRequest,
CreateGuestRequest, PullImageRequest,
},
},
};
@ -19,40 +19,110 @@ use log::error;
use tokio::select;
use tonic::{transport::Channel, Request};
use crate::console::StdioConsoleStream;
use crate::{console::StdioConsoleStream, pull::pull_interactive_progress};
#[derive(Parser)]
pub struct LauchCommand {
#[arg(short, long)]
name: Option<String>,
#[arg(short, long, default_value_t = 1)]
cpus: u32,
#[arg(short, long, default_value_t = 512)]
mem: u64,
#[arg[short, long]]
env: Option<Vec<String>>,
#[arg(short, long)]
attach: bool,
#[arg(short = 'W', long)]
wait: bool,
#[arg()]
oci: String,
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
run: Vec<String>,
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
pub enum LaunchImageFormat {
Squashfs,
Erofs,
}
impl LauchCommand {
#[derive(Parser)]
#[command(about = "Launch a new guest")]
pub struct LaunchCommand {
#[arg(long, default_value = "squashfs", help = "Image format")]
image_format: LaunchImageFormat,
#[arg(long, help = "Overwrite image cache on pull")]
pull_overwrite_cache: bool,
#[arg(short, long, help = "Name of the guest")]
name: Option<String>,
#[arg(
short,
long,
default_value_t = 1,
help = "vCPUs available to the guest"
)]
cpus: u32,
#[arg(
short,
long,
default_value_t = 512,
help = "Memory available to the guest, in megabytes"
)]
mem: u64,
#[arg[short = 'D', long = "device", help = "Devices to request for the guest"]]
device: Vec<String>,
#[arg[short, long, help = "Environment variables set in the guest"]]
env: Option<Vec<String>>,
#[arg(
short,
long,
help = "Attach to the guest after guest starts, implies --wait"
)]
attach: bool,
#[arg(
short = 'W',
long,
help = "Wait for the guest to start, implied by --attach"
)]
wait: bool,
#[arg(short = 'k', long, help = "OCI kernel image for guest to use")]
kernel: Option<String>,
#[arg(short = 'I', long, help = "OCI initrd image for guest to use")]
initrd: Option<String>,
#[arg(short = 'w', long, help = "Working directory")]
working_directory: Option<String>,
#[arg(help = "Container image for guest to use")]
oci: String,
#[arg(
allow_hyphen_values = true,
trailing_var_arg = true,
help = "Command to run inside the guest"
)]
command: Vec<String>,
}
impl LaunchCommand {
pub async fn run(
self,
mut client: ControlServiceClient<Channel>,
events: EventStream,
) -> Result<()> {
let image = self
.pull_image(
&mut client,
&self.oci,
match self.image_format {
LaunchImageFormat::Squashfs => OciImageFormat::Squashfs,
LaunchImageFormat::Erofs => OciImageFormat::Erofs,
},
)
.await?;
let kernel = if let Some(ref kernel) = self.kernel {
let kernel_image = self
.pull_image(&mut client, kernel, OciImageFormat::Tar)
.await?;
Some(kernel_image)
} else {
None
};
let initrd = if let Some(ref initrd) = self.initrd {
let kernel_image = self
.pull_image(&mut client, initrd, OciImageFormat::Tar)
.await?;
Some(kernel_image)
} else {
None
};
let request = CreateGuestRequest {
spec: Some(GuestSpec {
name: self.name.unwrap_or_default(),
image: Some(GuestImageSpec {
image: Some(Image::Oci(GuestOciImageSpec { image: self.oci })),
}),
image: Some(image),
kernel,
initrd,
vcpus: self.cpus,
mem: self.mem,
task: Some(GuestTaskSpec {
@ -63,9 +133,15 @@ impl LauchCommand {
value: value.clone(),
})
.collect(),
command: self.run,
command: self.command,
working_directory: self.working_directory.unwrap_or_default(),
}),
annotations: vec![],
devices: self
.device
.iter()
.map(|name| GuestSpecDevice { name: name.clone() })
.collect(),
}),
};
let response = client
@ -98,6 +174,28 @@ impl LauchCommand {
StdioConsoleStream::restore_terminal_mode();
std::process::exit(code.unwrap_or(0));
}
async fn pull_image(
&self,
client: &mut ControlServiceClient<Channel>,
image: &str,
format: OciImageFormat,
) -> Result<GuestImageSpec> {
let response = client
.pull_image(PullImageRequest {
image: image.to_string(),
format: format.into(),
overwrite_cache: self.pull_overwrite_cache,
})
.await?;
let reply = pull_interactive_progress(response.into_inner()).await?;
Ok(GuestImageSpec {
image: Some(Image::Oci(GuestOciImageSpec {
digest: reply.digest,
format: reply.format,
})),
})
}
}
async fn wait_guest_started(id: &str, events: EventStream) -> Result<()> {

View File

@ -1,10 +1,10 @@
use anyhow::{anyhow, Result};
use clap::{Parser, ValueEnum};
use cli_tables::Table;
use comfy_table::{presets::UTF8_FULL_CONDENSED, Cell, Color, Table};
use krata::{
events::EventStream,
v1::{
common::{guest_image_spec::Image, Guest, GuestStatus},
common::{Guest, GuestStatus},
control::{
control_service_client::ControlServiceClient, ListGuestsRequest, ResolveGuestRequest,
},
@ -14,11 +14,11 @@ use krata::{
use serde_json::Value;
use tonic::{transport::Channel, Request};
use crate::format::{guest_state_text, guest_status_text, kv2line, proto2dynamic, proto2kv};
use crate::format::{guest_simple_line, guest_status_text, kv2line, proto2dynamic, proto2kv};
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
enum ListFormat {
CliTable,
Table,
Json,
JsonPretty,
Jsonl,
@ -28,10 +28,11 @@ enum ListFormat {
}
#[derive(Parser)]
#[command(about = "List the guests on the isolation engine")]
pub struct ListCommand {
#[arg(short, long, default_value = "cli-table")]
#[arg(short, long, default_value = "table", help = "Output format")]
format: ListFormat,
#[arg()]
#[arg(help = "Limit to a single guest, either the name or the uuid")]
guest: Option<String>,
}
@ -70,24 +71,13 @@ impl ListCommand {
});
match self.format {
ListFormat::CliTable => {
ListFormat::Table => {
self.print_guest_table(guests)?;
}
ListFormat::Simple => {
for guest in guests {
let state = guest_status_text(
guest
.state
.as_ref()
.map(|x| x.status())
.unwrap_or(GuestStatus::Unknown),
);
let name = guest.spec.as_ref().map(|x| x.name.as_str()).unwrap_or("");
let network = guest.state.as_ref().and_then(|x| x.network.as_ref());
let ipv4 = network.map(|x| x.guest_ipv4.as_str()).unwrap_or("");
let ipv6 = network.map(|x| x.guest_ipv6.as_str()).unwrap_or("");
println!("{}\t{}\t{}\t{}\t{}", guest.id, state, name, ipv4, ipv6);
println!("{}", guest_simple_line(&guest));
}
}
@ -125,49 +115,51 @@ impl ListCommand {
fn print_guest_table(&self, guests: Vec<Guest>) -> Result<()> {
let mut table = Table::new();
let header = vec!["name", "uuid", "state", "ipv4", "ipv6", "image"];
table.push_row(&header)?;
table.load_preset(UTF8_FULL_CONDENSED);
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
table.set_header(vec!["name", "uuid", "status", "ipv4", "ipv6"]);
for guest in guests {
let ipv4 = guest
.state
.as_ref()
.and_then(|x| x.network.as_ref())
.map(|x| x.guest_ipv4.as_str())
.unwrap_or("unknown");
.unwrap_or("n/a");
let ipv6 = guest
.state
.as_ref()
.and_then(|x| x.network.as_ref())
.map(|x| x.guest_ipv6.as_str())
.unwrap_or("unknown");
.unwrap_or("n/a");
let Some(spec) = guest.spec else {
continue;
};
let image = spec
.image
.map(|x| {
x.image
.map(|y| match y {
Image::Oci(oci) => oci.image,
})
.unwrap_or("unknown".to_string())
})
.unwrap_or("unknown".to_string());
table.push_row_string(&vec![
spec.name,
guest.id,
format!("{}", guest_state_text(guest.state.as_ref())),
ipv4.to_string(),
ipv6.to_string(),
image,
])?;
let status = guest.state.as_ref().cloned().unwrap_or_default().status();
let status_text = guest_status_text(status);
let status_color = match status {
GuestStatus::Destroyed | GuestStatus::Failed => Color::Red,
GuestStatus::Destroying | GuestStatus::Exited | GuestStatus::Starting => {
Color::Yellow
}
GuestStatus::Started => Color::Green,
_ => Color::Reset,
};
table.add_row(vec![
Cell::new(spec.name),
Cell::new(guest.id),
Cell::new(status_text).fg(status_color),
Cell::new(ipv4.to_string()),
Cell::new(ipv6.to_string()),
]);
}
if table.num_records() == 1 {
if table.is_empty() {
if self.guest.is_none() {
println!("no guests have been launched");
}
} else {
println!("{}", table.to_string());
println!("{}", table);
}
Ok(())
}

View File

@ -0,0 +1,128 @@
use anyhow::Result;
use clap::{Parser, ValueEnum};
use comfy_table::{presets::UTF8_FULL_CONDENSED, Cell, Color, Table};
use krata::{
events::EventStream,
v1::control::{control_service_client::ControlServiceClient, DeviceInfo, ListDevicesRequest},
};
use serde_json::Value;
use tonic::transport::Channel;
use crate::format::{kv2line, proto2dynamic, proto2kv};
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
enum ListDevicesFormat {
Table,
Json,
JsonPretty,
Jsonl,
Yaml,
KeyValue,
Simple,
}
#[derive(Parser)]
#[command(about = "List the devices on the isolation engine")]
pub struct ListDevicesCommand {
#[arg(short, long, default_value = "table", help = "Output format")]
format: ListDevicesFormat,
}
impl ListDevicesCommand {
pub async fn run(
self,
mut client: ControlServiceClient<Channel>,
_events: EventStream,
) -> Result<()> {
let reply = client
.list_devices(ListDevicesRequest {})
.await?
.into_inner();
let mut devices = reply.devices;
devices.sort_by(|a, b| a.name.cmp(&b.name));
match self.format {
ListDevicesFormat::Table => {
self.print_devices_table(devices)?;
}
ListDevicesFormat::Simple => {
for device in devices {
println!("{}\t{}\t{}", device.name, device.claimed, device.owner);
}
}
ListDevicesFormat::Json | ListDevicesFormat::JsonPretty | ListDevicesFormat::Yaml => {
let mut values = Vec::new();
for device in devices {
let message = proto2dynamic(device)?;
values.push(serde_json::to_value(message)?);
}
let value = Value::Array(values);
let encoded = if self.format == ListDevicesFormat::JsonPretty {
serde_json::to_string_pretty(&value)?
} else if self.format == ListDevicesFormat::Yaml {
serde_yaml::to_string(&value)?
} else {
serde_json::to_string(&value)?
};
println!("{}", encoded.trim());
}
ListDevicesFormat::Jsonl => {
for device in devices {
let message = proto2dynamic(device)?;
println!("{}", serde_json::to_string(&message)?);
}
}
ListDevicesFormat::KeyValue => {
self.print_key_value(devices)?;
}
}
Ok(())
}
fn print_devices_table(&self, devices: Vec<DeviceInfo>) -> Result<()> {
let mut table = Table::new();
table.load_preset(UTF8_FULL_CONDENSED);
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
table.set_header(vec!["name", "status", "owner"]);
for device in devices {
let status_text = if device.claimed {
"claimed"
} else {
"available"
};
let status_color = if device.claimed {
Color::Blue
} else {
Color::Green
};
table.add_row(vec![
Cell::new(device.name),
Cell::new(status_text).fg(status_color),
Cell::new(device.owner),
]);
}
if table.is_empty() {
println!("no devices configured");
} else {
println!("{}", table);
}
Ok(())
}
fn print_key_value(&self, devices: Vec<DeviceInfo>) -> Result<()> {
for device in devices {
let kvs = proto2kv(device)?;
println!("{}", kv2line(kvs));
}
Ok(())
}
}

View File

@ -0,0 +1,58 @@
use anyhow::Result;
use async_stream::stream;
use clap::Parser;
use krata::{
events::EventStream,
v1::control::{control_service_client::ControlServiceClient, ConsoleDataRequest},
};
use tokio::select;
use tokio_stream::{pending, StreamExt};
use tonic::transport::Channel;
use crate::console::StdioConsoleStream;
use super::resolve_guest;
#[derive(Parser)]
#[command(about = "View the logs of a guest")]
pub struct LogsCommand {
#[arg(short, long, help = "Follow output from the guest")]
follow: bool,
#[arg(help = "Guest to show logs for, either the name or the uuid")]
guest: String,
}
impl LogsCommand {
pub async fn run(
self,
mut client: ControlServiceClient<Channel>,
events: EventStream,
) -> Result<()> {
let guest_id: String = resolve_guest(&mut client, &self.guest).await?;
let guest_id_stream = guest_id.clone();
let follow = self.follow;
let input = stream! {
yield ConsoleDataRequest { guest_id: guest_id_stream, data: Vec::new() };
if follow {
let mut pending = pending::<ConsoleDataRequest>();
while let Some(x) = pending.next().await {
yield x;
}
}
};
let output = client.console_data(input).await?.into_inner();
let stdout_handle =
tokio::task::spawn(async move { StdioConsoleStream::stdout(output).await });
let exit_hook_task = StdioConsoleStream::guest_exit_hook(guest_id.clone(), events).await?;
let code = select! {
x = stdout_handle => {
x??;
None
},
x = exit_hook_task => x?
};
StdioConsoleStream::restore_terminal_mode();
std::process::exit(code.unwrap_or(0));
}
}

View File

@ -0,0 +1,83 @@
use anyhow::Result;
use clap::{Parser, ValueEnum};
use krata::{
events::EventStream,
v1::{
common::GuestMetricNode,
control::{control_service_client::ControlServiceClient, ReadGuestMetricsRequest},
},
};
use tonic::transport::Channel;
use crate::format::{kv2line, metrics_flat, metrics_tree, proto2dynamic};
use super::resolve_guest;
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
enum MetricsFormat {
Tree,
Json,
JsonPretty,
Yaml,
KeyValue,
}
#[derive(Parser)]
#[command(about = "Read metrics from the guest")]
pub struct MetricsCommand {
#[arg(short, long, default_value = "tree", help = "Output format")]
format: MetricsFormat,
#[arg(help = "Guest to read metrics for, either the name or the uuid")]
guest: String,
}
impl MetricsCommand {
pub async fn run(
self,
mut client: ControlServiceClient<Channel>,
_events: EventStream,
) -> Result<()> {
let guest_id: String = resolve_guest(&mut client, &self.guest).await?;
let root = client
.read_guest_metrics(ReadGuestMetricsRequest { guest_id })
.await?
.into_inner()
.root
.unwrap_or_default();
match self.format {
MetricsFormat::Tree => {
self.print_metrics_tree(root)?;
}
MetricsFormat::Json | MetricsFormat::JsonPretty | MetricsFormat::Yaml => {
let value = serde_json::to_value(proto2dynamic(root)?)?;
let encoded = if self.format == MetricsFormat::JsonPretty {
serde_json::to_string_pretty(&value)?
} else if self.format == MetricsFormat::Yaml {
serde_yaml::to_string(&value)?
} else {
serde_json::to_string(&value)?
};
println!("{}", encoded.trim());
}
MetricsFormat::KeyValue => {
self.print_key_value(root)?;
}
}
Ok(())
}
fn print_metrics_tree(&self, root: GuestMetricNode) -> Result<()> {
print!("{}", metrics_tree(root));
Ok(())
}
fn print_key_value(&self, metrics: GuestMetricNode) -> Result<()> {
let kvs = metrics_flat(metrics);
println!("{}", kv2line(kvs));
Ok(())
}
}

View File

@ -1,8 +1,17 @@
pub mod attach;
pub mod cpu_topology;
pub mod destroy;
pub mod exec;
pub mod identify_host;
pub mod idm_snoop;
pub mod launch;
pub mod list;
pub mod list_devices;
pub mod logs;
pub mod metrics;
pub mod pull;
pub mod resolve;
pub mod top;
pub mod watch;
use anyhow::{anyhow, Result};
@ -10,21 +19,27 @@ use clap::{Parser, Subcommand};
use krata::{
client::ControlClientProvider,
events::EventStream,
v1::control::{
control_service_client::ControlServiceClient, ResolveGuestRequest, WatchEventsRequest,
},
v1::control::{control_service_client::ControlServiceClient, ResolveGuestRequest},
};
use tonic::{transport::Channel, Request};
use self::{
attach::AttachCommand, destroy::DestroyCommand, launch::LauchCommand, list::ListCommand,
resolve::ResolveCommand, watch::WatchCommand,
attach::AttachCommand, cpu_topology::CpuTopologyCommand, destroy::DestroyCommand,
exec::ExecCommand, identify_host::IdentifyHostCommand, idm_snoop::IdmSnoopCommand,
launch::LaunchCommand, list::ListCommand, list_devices::ListDevicesCommand, logs::LogsCommand,
metrics::MetricsCommand, pull::PullCommand, resolve::ResolveCommand, top::TopCommand,
watch::WatchCommand,
};
#[derive(Parser)]
#[command(version, about)]
#[command(version, about = "Control the krata isolation engine")]
pub struct ControlCommand {
#[arg(short, long, default_value = "unix:///var/lib/krata/daemon.socket")]
#[arg(
short,
long,
help = "The connection URL to the krata isolation engine",
default_value = "unix:///var/lib/krata/daemon.socket"
)]
connection: String,
#[command(subcommand)]
@ -33,24 +48,27 @@ pub struct ControlCommand {
#[derive(Subcommand)]
pub enum Commands {
Launch(LauchCommand),
Launch(LaunchCommand),
Destroy(DestroyCommand),
List(ListCommand),
ListDevices(ListDevicesCommand),
Attach(AttachCommand),
Pull(PullCommand),
Logs(LogsCommand),
Watch(WatchCommand),
Resolve(ResolveCommand),
Metrics(MetricsCommand),
IdmSnoop(IdmSnoopCommand),
Top(TopCommand),
IdentifyHost(IdentifyHostCommand),
Exec(ExecCommand),
CpuTopology(CpuTopologyCommand),
}
impl ControlCommand {
pub async fn run(self) -> Result<()> {
let mut client = ControlClientProvider::dial(self.connection.parse()?).await?;
let events = EventStream::open(
client
.watch_events(WatchEventsRequest {})
.await?
.into_inner(),
)
.await?;
let client = ControlClientProvider::dial(self.connection.parse()?).await?;
let events = EventStream::open(client.clone()).await?;
match self.command {
Commands::Launch(launch) => {
@ -65,6 +83,10 @@ impl ControlCommand {
attach.run(client, events).await?;
}
Commands::Logs(logs) => {
logs.run(client, events).await?;
}
Commands::List(list) => {
list.run(client, events).await?;
}
@ -76,6 +98,38 @@ impl ControlCommand {
Commands::Resolve(resolve) => {
resolve.run(client).await?;
}
Commands::Metrics(metrics) => {
metrics.run(client, events).await?;
}
Commands::IdmSnoop(snoop) => {
snoop.run(client, events).await?;
}
Commands::Top(top) => {
top.run(client, events).await?;
}
Commands::Pull(pull) => {
pull.run(client).await?;
}
Commands::IdentifyHost(identify) => {
identify.run(client).await?;
}
Commands::Exec(exec) => {
exec.run(client).await?;
}
Commands::ListDevices(list) => {
list.run(client, events).await?;
}
Commands::CpuTopology(cpu_topology) => {
cpu_topology.run(client).await?;
}
}
Ok(())
}

View File

@ -0,0 +1,47 @@
use anyhow::Result;
use clap::{Parser, ValueEnum};
use krata::v1::{
common::OciImageFormat,
control::{control_service_client::ControlServiceClient, PullImageRequest},
};
use tonic::transport::Channel;
use crate::pull::pull_interactive_progress;
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
pub enum PullImageFormat {
Squashfs,
Erofs,
Tar,
}
#[derive(Parser)]
#[command(about = "Pull an image into the cache")]
pub struct PullCommand {
#[arg(help = "Image name")]
image: String,
#[arg(short = 's', long, default_value = "squashfs", help = "Image format")]
image_format: PullImageFormat,
#[arg(short = 'o', long, help = "Overwrite image cache")]
overwrite_cache: bool,
}
impl PullCommand {
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
let response = client
.pull_image(PullImageRequest {
image: self.image.clone(),
format: match self.image_format {
PullImageFormat::Squashfs => OciImageFormat::Squashfs.into(),
PullImageFormat::Erofs => OciImageFormat::Erofs.into(),
PullImageFormat::Tar => OciImageFormat::Tar.into(),
},
overwrite_cache: self.overwrite_cache,
})
.await?;
let reply = pull_interactive_progress(response.into_inner()).await?;
println!("{}", reply.digest);
Ok(())
}
}

View File

@ -5,8 +5,9 @@ use krata::v1::control::{control_service_client::ControlServiceClient, ResolveGu
use tonic::{transport::Channel, Request};
#[derive(Parser)]
#[command(about = "Resolve a guest name to a uuid")]
pub struct ResolveCommand {
#[arg()]
#[arg(help = "Guest name")]
guest: String,
}

215
crates/ctl/src/cli/top.rs Normal file
View File

@ -0,0 +1,215 @@
use anyhow::Result;
use clap::Parser;
use krata::{events::EventStream, v1::control::control_service_client::ControlServiceClient};
use std::{
io::{self, stdout, Stdout},
time::Duration,
};
use tokio::select;
use tokio_stream::StreamExt;
use tonic::transport::Channel;
use crossterm::{
event::{Event, KeyCode, KeyEvent, KeyEventKind},
execute,
terminal::*,
};
use ratatui::{
prelude::*,
symbols::border,
widgets::{
block::{Position, Title},
Block, Borders, Row, Table, TableState,
},
};
use crate::{
format::guest_status_text,
metrics::{
lookup_metric_value, MultiMetricCollector, MultiMetricCollectorHandle, MultiMetricState,
},
};
#[derive(Parser)]
#[command(about = "Dashboard for running guests")]
pub struct TopCommand {}
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
impl TopCommand {
pub async fn run(
self,
client: ControlServiceClient<Channel>,
events: EventStream,
) -> Result<()> {
let collector = MultiMetricCollector::new(client, events, Duration::from_millis(200))?;
let collector = collector.launch().await?;
let mut tui = TopCommand::init()?;
let mut app = TopApp {
metrics: MultiMetricState { guests: vec![] },
exit: false,
table: TableState::new(),
};
app.run(collector, &mut tui).await?;
TopCommand::restore()?;
Ok(())
}
pub fn init() -> io::Result<Tui> {
execute!(stdout(), EnterAlternateScreen)?;
enable_raw_mode()?;
Terminal::new(CrosstermBackend::new(stdout()))
}
pub fn restore() -> io::Result<()> {
execute!(stdout(), LeaveAlternateScreen)?;
disable_raw_mode()?;
Ok(())
}
}
pub struct TopApp {
table: TableState,
metrics: MultiMetricState,
exit: bool,
}
impl TopApp {
pub async fn run(
&mut self,
mut collector: MultiMetricCollectorHandle,
terminal: &mut Tui,
) -> Result<()> {
let mut events = crossterm::event::EventStream::new();
while !self.exit {
terminal.draw(|frame| self.render_frame(frame))?;
select! {
x = collector.receiver.recv() => match x {
Some(state) => {
self.metrics = state;
},
None => {
break;
}
},
x = events.next() => match x {
Some(event) => {
let event = event?;
self.handle_event(event)?;
},
None => {
break;
}
}
};
}
Ok(())
}
fn render_frame(&mut self, frame: &mut Frame) {
frame.render_widget(self, frame.size());
}
fn handle_event(&mut self, event: Event) -> io::Result<()> {
match event {
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
self.handle_key_event(key_event)
}
_ => {}
};
Ok(())
}
fn exit(&mut self) {
self.exit = true;
}
fn handle_key_event(&mut self, key_event: KeyEvent) {
if let KeyCode::Char('q') = key_event.code {
self.exit()
}
}
}
impl Widget for &mut TopApp {
fn render(self, area: Rect, buf: &mut Buffer) {
let title = Title::from(" krata isolation engine ".bold());
let instructions = Title::from(vec![" Quit ".into(), "<Q> ".blue().bold()]);
let block = Block::default()
.title(title.alignment(Alignment::Center))
.title(
instructions
.alignment(Alignment::Center)
.position(Position::Bottom),
)
.borders(Borders::ALL)
.border_set(border::THICK);
let mut rows = vec![];
for ms in &self.metrics.guests {
let Some(ref spec) = ms.guest.spec else {
continue;
};
let Some(ref state) = ms.guest.state else {
continue;
};
let memory_total = ms
.root
.as_ref()
.and_then(|root| lookup_metric_value(root, "system/memory/total"));
let memory_used = ms
.root
.as_ref()
.and_then(|root| lookup_metric_value(root, "system/memory/used"));
let memory_free = ms
.root
.as_ref()
.and_then(|root| lookup_metric_value(root, "system/memory/free"));
let row = Row::new(vec![
spec.name.clone(),
ms.guest.id.clone(),
guest_status_text(state.status()),
memory_total.unwrap_or_default(),
memory_used.unwrap_or_default(),
memory_free.unwrap_or_default(),
]);
rows.push(row);
}
let widths = [
Constraint::Min(8),
Constraint::Min(8),
Constraint::Min(8),
Constraint::Min(8),
Constraint::Min(8),
Constraint::Min(8),
];
let table = Table::new(rows, widths)
.header(
Row::new(vec![
"name",
"id",
"status",
"total memory",
"used memory",
"free memory",
])
.style(Style::new().bold())
.bottom_margin(1),
)
.column_spacing(1)
.block(block);
StatefulWidget::render(table, area, buf, &mut self.table);
}
}

View File

@ -7,7 +7,7 @@ use krata::{
use prost_reflect::ReflectMessage;
use serde_json::Value;
use crate::format::{guest_state_text, kv2line, proto2dynamic, proto2kv};
use crate::format::{guest_simple_line, kv2line, proto2dynamic, proto2kv};
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
enum WatchFormat {
@ -17,8 +17,9 @@ enum WatchFormat {
}
#[derive(Parser)]
#[command(about = "Watch for guest changes")]
pub struct WatchCommand {
#[arg(short, long, default_value = "simple")]
#[arg(short, long, default_value = "simple", help = "Output format")]
format: WatchFormat,
}
@ -27,12 +28,10 @@ impl WatchCommand {
let mut stream = events.subscribe();
loop {
let event = stream.recv().await?;
match event {
Event::GuestChanged(changed) => {
let guest = changed.guest.clone();
self.print_event("guest.changed", changed, guest)?;
}
}
let Event::GuestChanged(changed) = event;
let guest = changed.guest.clone();
self.print_event("guest.changed", changed, guest)?;
}
}
@ -45,12 +44,7 @@ impl WatchCommand {
match self.format {
WatchFormat::Simple => {
if let Some(guest) = guest {
println!(
"{} guest={} status=\"{}\"",
typ,
guest.id,
guest_state_text(guest.state.as_ref()).replace('"', "\\\"")
);
println!("{}", guest_simple_line(&guest));
}
}

View File

@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{anyhow, Result};
use async_stream::stream;
use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, is_raw_mode_enabled},
@ -8,12 +8,15 @@ use krata::{
events::EventStream,
v1::{
common::GuestStatus,
control::{watch_events_reply::Event, ConsoleDataReply, ConsoleDataRequest},
control::{
watch_events_reply::Event, ConsoleDataReply, ConsoleDataRequest, ExecGuestReply,
ExecGuestRequest,
},
},
};
use log::debug;
use tokio::{
io::{stdin, stdout, AsyncReadExt, AsyncWriteExt},
io::{stderr, stdin, stdout, AsyncReadExt, AsyncWriteExt},
task::JoinHandle,
};
use tokio_stream::{Stream, StreamExt};
@ -45,6 +48,31 @@ impl StdioConsoleStream {
}
}
pub async fn stdin_stream_exec(
initial: ExecGuestRequest,
) -> impl Stream<Item = ExecGuestRequest> {
let mut stdin = stdin();
stream! {
yield initial;
let mut buffer = vec![0u8; 60];
loop {
let size = match stdin.read(&mut buffer).await {
Ok(size) => size,
Err(error) => {
debug!("failed to read stdin: {}", error);
break;
}
};
let data = buffer[0..size].to_vec();
if size == 1 && buffer[0] == 0x1d {
break;
}
yield ExecGuestRequest { guest_id: String::default(), task: None, data };
}
}
}
pub async fn stdout(mut stream: Streaming<ConsoleDataReply>) -> Result<()> {
if stdin().is_tty() {
enable_raw_mode()?;
@ -62,6 +90,32 @@ impl StdioConsoleStream {
Ok(())
}
pub async fn exec_output(mut stream: Streaming<ExecGuestReply>) -> Result<i32> {
let mut stdout = stdout();
let mut stderr = stderr();
while let Some(reply) = stream.next().await {
let reply = reply?;
if !reply.stdout.is_empty() {
stdout.write_all(&reply.stdout).await?;
stdout.flush().await?;
}
if !reply.stderr.is_empty() {
stderr.write_all(&reply.stderr).await?;
stderr.flush().await?;
}
if reply.exited {
if reply.error.is_empty() {
return Ok(reply.exit_code);
} else {
return Err(anyhow!("exec failed: {}", reply.error));
}
}
}
Ok(-1)
}
pub async fn guest_exit_hook(
id: String,
events: EventStream,
@ -69,29 +123,26 @@ impl StdioConsoleStream {
Ok(tokio::task::spawn(async move {
let mut stream = events.subscribe();
while let Ok(event) = stream.recv().await {
match event {
Event::GuestChanged(changed) => {
let Some(guest) = changed.guest else {
continue;
};
let Event::GuestChanged(changed) = event;
let Some(guest) = changed.guest else {
continue;
};
let Some(state) = guest.state else {
continue;
};
let Some(state) = guest.state else {
continue;
};
if guest.id != id {
continue;
}
if guest.id != id {
continue;
}
if let Some(exit_info) = state.exit_info {
return Some(exit_info.code);
}
if let Some(exit_info) = state.exit_info {
return Some(exit_info.code);
}
let status = state.status();
if status == GuestStatus::Destroying || status == GuestStatus::Destroyed {
return Some(10);
}
}
let status = state.status();
if status == GuestStatus::Destroying || status == GuestStatus::Destroyed {
return Some(10);
}
}
None

View File

@ -1,8 +1,12 @@
use std::collections::HashMap;
use std::{collections::HashMap, time::Duration};
use anyhow::Result;
use krata::v1::common::{GuestState, GuestStatus};
use prost_reflect::{DynamicMessage, ReflectMessage, Value};
use fancy_duration::FancyDuration;
use human_bytes::human_bytes;
use krata::v1::common::{Guest, GuestMetricFormat, GuestMetricNode, GuestStatus};
use prost_reflect::{DynamicMessage, ReflectMessage};
use prost_types::Value;
use termtree::Tree;
pub fn proto2dynamic(proto: impl ReflectMessage) -> Result<DynamicMessage> {
Ok(DynamicMessage::decode(
@ -11,46 +15,59 @@ pub fn proto2dynamic(proto: impl ReflectMessage) -> Result<DynamicMessage> {
)?)
}
pub fn proto2kv(proto: impl ReflectMessage) -> Result<HashMap<String, String>> {
let message = proto2dynamic(proto)?;
pub fn value2kv(value: serde_json::Value) -> Result<HashMap<String, String>> {
let mut map = HashMap::new();
fn crawl(prefix: &str, map: &mut HashMap<String, String>, message: &DynamicMessage) {
for (field, value) in message.fields() {
let path = if prefix.is_empty() {
field.name().to_string()
fn crawl(prefix: String, map: &mut HashMap<String, String>, value: serde_json::Value) {
fn dot(prefix: &str, next: String) -> String {
if prefix.is_empty() {
next.to_string()
} else {
format!("{}.{}", prefix, field.name())
};
match value {
Value::Message(child) => {
crawl(&path, map, child);
}
format!("{}.{}", prefix, next)
}
}
Value::EnumNumber(number) => {
if let Some(e) = field.kind().as_enum() {
if let Some(value) = e.get_value(*number) {
map.insert(path, value.name().to_string());
}
}
}
match value {
serde_json::Value::Null => {
map.insert(prefix, "null".to_string());
}
Value::String(value) => {
map.insert(path, value.clone());
}
serde_json::Value::String(value) => {
map.insert(prefix, value);
}
_ => {
map.insert(path, value.to_string());
serde_json::Value::Bool(value) => {
map.insert(prefix, value.to_string());
}
serde_json::Value::Number(value) => {
map.insert(prefix, value.to_string());
}
serde_json::Value::Array(value) => {
for (i, item) in value.into_iter().enumerate() {
let next = dot(&prefix, i.to_string());
crawl(next, map, item);
}
}
serde_json::Value::Object(value) => {
for (key, item) in value {
let next = dot(&prefix, key);
crawl(next, map, item);
}
}
}
}
crawl("", &mut map, &message);
crawl("".to_string(), &mut map, value);
Ok(map)
}
pub fn proto2kv(proto: impl ReflectMessage) -> Result<HashMap<String, String>> {
let message = proto2dynamic(proto)?;
let value = serde_json::to_value(message)?;
value2kv(value)
}
pub fn kv2line(map: HashMap<String, String>) -> String {
map.iter()
.map(|(k, v)| format!("{}=\"{}\"", k, v.replace('"', "\\\"")))
@ -71,16 +88,77 @@ pub fn guest_status_text(status: GuestStatus) -> String {
.to_string()
}
pub fn guest_state_text(state: Option<&GuestState>) -> String {
let state = state.cloned().unwrap_or_default();
let mut text = guest_status_text(state.status());
if let Some(exit) = state.exit_info {
text.push_str(&format!(" (exit code: {})", exit.code));
}
if let Some(error) = state.error_info {
text.push_str(&format!(" (error: {})", error.message));
}
text
pub fn guest_simple_line(guest: &Guest) -> String {
let state = guest_status_text(
guest
.state
.as_ref()
.map(|x| x.status())
.unwrap_or(GuestStatus::Unknown),
);
let name = guest.spec.as_ref().map(|x| x.name.as_str()).unwrap_or("");
let network = guest.state.as_ref().and_then(|x| x.network.as_ref());
let ipv4 = network.map(|x| x.guest_ipv4.as_str()).unwrap_or("");
let ipv6 = network.map(|x| x.guest_ipv6.as_str()).unwrap_or("");
format!("{}\t{}\t{}\t{}\t{}", guest.id, state, name, ipv4, ipv6)
}
fn metrics_value_string(value: Value) -> String {
proto2dynamic(value)
.map(|x| serde_json::to_string(&x).ok())
.ok()
.flatten()
.unwrap_or_default()
}
fn metrics_value_numeric(value: Value) -> f64 {
let string = metrics_value_string(value);
string.parse::<f64>().ok().unwrap_or(f64::NAN)
}
pub fn metrics_value_pretty(value: Value, format: GuestMetricFormat) -> String {
match format {
GuestMetricFormat::Bytes => human_bytes(metrics_value_numeric(value)),
GuestMetricFormat::Integer => (metrics_value_numeric(value) as u64).to_string(),
GuestMetricFormat::DurationSeconds => {
FancyDuration(Duration::from_secs_f64(metrics_value_numeric(value))).to_string()
}
_ => metrics_value_string(value),
}
}
fn metrics_flat_internal(prefix: &str, node: GuestMetricNode, map: &mut HashMap<String, String>) {
if let Some(value) = node.value {
map.insert(prefix.to_string(), metrics_value_string(value));
}
for child in node.children {
let path = if prefix.is_empty() {
child.name.to_string()
} else {
format!("{}.{}", prefix, child.name)
};
metrics_flat_internal(&path, child, map);
}
}
pub fn metrics_flat(root: GuestMetricNode) -> HashMap<String, String> {
let mut map = HashMap::new();
metrics_flat_internal("", root, &mut map);
map
}
pub fn metrics_tree(node: GuestMetricNode) -> Tree<String> {
let mut name = node.name.to_string();
let format = node.format();
if let Some(value) = node.value {
let value_string = metrics_value_pretty(value, format);
name.push_str(&format!(": {}", value_string));
}
let mut tree = Tree::new(name);
for child in node.children {
tree.push(metrics_tree(child));
}
tree
}

View File

@ -1,3 +1,5 @@
pub mod cli;
pub mod console;
pub mod format;
pub mod metrics;
pub mod pull;

158
crates/ctl/src/metrics.rs Normal file
View File

@ -0,0 +1,158 @@
use anyhow::Result;
use krata::{
events::EventStream,
v1::{
common::{Guest, GuestMetricNode, GuestStatus},
control::{
control_service_client::ControlServiceClient, watch_events_reply::Event,
ListGuestsRequest, ReadGuestMetricsRequest,
},
},
};
use log::error;
use std::time::Duration;
use tokio::{
select,
sync::mpsc::{channel, Receiver, Sender},
task::JoinHandle,
time::{sleep, timeout},
};
use tonic::transport::Channel;
use crate::format::metrics_value_pretty;
pub struct MetricState {
pub guest: Guest,
pub root: Option<GuestMetricNode>,
}
pub struct MultiMetricState {
pub guests: Vec<MetricState>,
}
pub struct MultiMetricCollector {
client: ControlServiceClient<Channel>,
events: EventStream,
period: Duration,
}
pub struct MultiMetricCollectorHandle {
pub receiver: Receiver<MultiMetricState>,
task: JoinHandle<()>,
}
impl Drop for MultiMetricCollectorHandle {
fn drop(&mut self) {
self.task.abort();
}
}
impl MultiMetricCollector {
pub fn new(
client: ControlServiceClient<Channel>,
events: EventStream,
period: Duration,
) -> Result<MultiMetricCollector> {
Ok(MultiMetricCollector {
client,
events,
period,
})
}
pub async fn launch(mut self) -> Result<MultiMetricCollectorHandle> {
let (sender, receiver) = channel::<MultiMetricState>(100);
let task = tokio::task::spawn(async move {
if let Err(error) = self.process(sender).await {
error!("failed to process multi metric collector: {}", error);
}
});
Ok(MultiMetricCollectorHandle { receiver, task })
}
pub async fn process(&mut self, sender: Sender<MultiMetricState>) -> Result<()> {
let mut events = self.events.subscribe();
let mut guests: Vec<Guest> = self
.client
.list_guests(ListGuestsRequest {})
.await?
.into_inner()
.guests;
loop {
let collect = select! {
x = events.recv() => match x {
Ok(event) => {
let Event::GuestChanged(changed) = event;
let Some(guest) = changed.guest else {
continue;
};
let Some(ref state) = guest.state else {
continue;
};
guests.retain(|x| x.id != guest.id);
if state.status() != GuestStatus::Destroying {
guests.push(guest);
}
false
},
Err(error) => {
return Err(error.into());
}
},
_ = sleep(self.period) => {
true
}
};
if !collect {
continue;
}
let mut metrics = Vec::new();
for guest in &guests {
let Some(ref state) = guest.state else {
continue;
};
if state.status() != GuestStatus::Started {
continue;
}
let root = timeout(
Duration::from_secs(5),
self.client.read_guest_metrics(ReadGuestMetricsRequest {
guest_id: guest.id.clone(),
}),
)
.await
.ok()
.and_then(|x| x.ok())
.map(|x| x.into_inner())
.and_then(|x| x.root);
metrics.push(MetricState {
guest: guest.clone(),
root,
});
}
sender.send(MultiMetricState { guests: metrics }).await?;
}
}
}
pub fn lookup<'a>(node: &'a GuestMetricNode, path: &str) -> Option<&'a GuestMetricNode> {
let Some((what, b)) = path.split_once('/') else {
return node.children.iter().find(|x| x.name == path);
};
let next = node.children.iter().find(|x| x.name == what)?;
return lookup(next, b);
}
pub fn lookup_metric_value(node: &GuestMetricNode, path: &str) -> Option<String> {
lookup(node, path).and_then(|x| {
x.value
.as_ref()
.map(|v| metrics_value_pretty(v.clone(), x.format()))
})
}

268
crates/ctl/src/pull.rs Normal file
View File

@ -0,0 +1,268 @@
use std::{
collections::{hash_map::Entry, HashMap},
time::Duration,
};
use anyhow::{anyhow, Result};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use krata::v1::control::{
image_progress_indication::Indication, ImageProgressIndication, ImageProgressLayerPhase,
ImageProgressPhase, PullImageReply,
};
use tokio_stream::StreamExt;
use tonic::Streaming;
const SPINNER_STRINGS: &[&str] = &[
"[= ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ = ]",
"[ =]",
"[====================]",
];
fn progress_bar_for_indication(indication: &ImageProgressIndication) -> Option<ProgressBar> {
match indication.indication.as_ref() {
Some(Indication::Hidden(_)) | None => None,
Some(Indication::Bar(indic)) => {
let bar = ProgressBar::new(indic.total);
bar.enable_steady_tick(Duration::from_millis(100));
Some(bar)
}
Some(Indication::Spinner(_)) => {
let bar = ProgressBar::new_spinner();
bar.enable_steady_tick(Duration::from_millis(100));
Some(bar)
}
Some(Indication::Completed(indic)) => {
let bar = ProgressBar::new_spinner();
bar.enable_steady_tick(Duration::from_millis(100));
if !indic.message.is_empty() {
bar.finish_with_message(indic.message.clone());
} else {
bar.finish()
}
Some(bar)
}
}
}
fn configure_for_indication(
bar: &mut ProgressBar,
multi_progress: &mut MultiProgress,
indication: &ImageProgressIndication,
top_phase: Option<ImageProgressPhase>,
layer_phase: Option<ImageProgressLayerPhase>,
layer_id: Option<&str>,
) {
let prefix = if let Some(phase) = top_phase {
match phase {
ImageProgressPhase::Unknown => "unknown",
ImageProgressPhase::Started => "started",
ImageProgressPhase::Resolving => "resolving",
ImageProgressPhase::Resolved => "resolved",
ImageProgressPhase::ConfigDownload => "downloading",
ImageProgressPhase::LayerDownload => "downloading",
ImageProgressPhase::Assemble => "assembling",
ImageProgressPhase::Pack => "packing",
ImageProgressPhase::Complete => "complete",
}
} else if let Some(phase) = layer_phase {
match phase {
ImageProgressLayerPhase::Unknown => "unknown",
ImageProgressLayerPhase::Waiting => "waiting",
ImageProgressLayerPhase::Downloading => "downloading",
ImageProgressLayerPhase::Downloaded => "downloaded",
ImageProgressLayerPhase::Extracting => "extracting",
ImageProgressLayerPhase::Extracted => "extracted",
}
} else {
""
};
let prefix = prefix.to_string();
let id = if let Some(layer_id) = layer_id {
let hash = if let Some((_, hash)) = layer_id.split_once(':') {
hash
} else {
"unknown"
};
let small_hash = if hash.len() > 10 { &hash[0..10] } else { hash };
Some(format!("{:width$}", small_hash, width = 10))
} else {
None
};
let prefix = if let Some(id) = id {
format!("{} {:width$}", id, prefix, width = 11)
} else {
format!(" {:width$}", prefix, width = 11)
};
match indication.indication.as_ref() {
Some(Indication::Hidden(_)) | None => {
multi_progress.remove(bar);
return;
}
Some(Indication::Bar(indic)) => {
if indic.is_bytes {
bar.set_style(ProgressStyle::with_template("{prefix} [{bar:20}] {msg} {binary_bytes}/{binary_total_bytes} ({binary_bytes_per_sec}) eta: {eta}").unwrap().progress_chars("=>-"));
} else {
bar.set_style(
ProgressStyle::with_template(
"{prefix} [{bar:20} {msg} {human_pos}/{human_len} ({per_sec}/sec)",
)
.unwrap()
.progress_chars("=>-"),
);
}
bar.set_message(indic.message.clone());
bar.set_position(indic.current);
bar.set_length(indic.total);
}
Some(Indication::Spinner(indic)) => {
bar.set_style(
ProgressStyle::with_template("{prefix} {spinner} {msg}")
.unwrap()
.tick_strings(SPINNER_STRINGS),
);
bar.set_message(indic.message.clone());
}
Some(Indication::Completed(indic)) => {
if bar.is_finished() {
return;
}
bar.disable_steady_tick();
bar.set_message(indic.message.clone());
if indic.total != 0 {
bar.set_position(indic.total);
bar.set_length(indic.total);
}
if bar.style().get_tick_str(0).contains('=') {
bar.set_style(
ProgressStyle::with_template("{prefix} {spinner} {msg}")
.unwrap()
.tick_strings(SPINNER_STRINGS),
);
bar.finish_with_message(indic.message.clone());
} else if indic.is_bytes {
bar.set_style(
ProgressStyle::with_template("{prefix} [{bar:20}] {msg} {binary_total_bytes}")
.unwrap()
.progress_chars("=>-"),
);
} else {
bar.set_style(
ProgressStyle::with_template("{prefix} [{bar:20}] {msg}")
.unwrap()
.progress_chars("=>-"),
);
}
bar.tick();
bar.enable_steady_tick(Duration::from_millis(100));
}
};
bar.set_prefix(prefix);
bar.tick();
}
pub async fn pull_interactive_progress(
mut stream: Streaming<PullImageReply>,
) -> Result<PullImageReply> {
let mut multi_progress = MultiProgress::new();
multi_progress.set_move_cursor(false);
let mut progresses = HashMap::new();
while let Some(reply) = stream.next().await {
let reply = match reply {
Ok(reply) => reply,
Err(error) => {
multi_progress.clear()?;
return Err(error.into());
}
};
if reply.progress.is_none() && !reply.digest.is_empty() {
multi_progress.clear()?;
return Ok(reply);
}
let Some(oci) = reply.progress else {
continue;
};
for layer in &oci.layers {
let Some(ref indication) = layer.indication else {
continue;
};
let bar = match progresses.entry(layer.id.clone()) {
Entry::Occupied(entry) => Some(entry.into_mut()),
Entry::Vacant(entry) => {
if let Some(bar) = progress_bar_for_indication(indication) {
multi_progress.add(bar.clone());
Some(entry.insert(bar))
} else {
None
}
}
};
if let Some(bar) = bar {
configure_for_indication(
bar,
&mut multi_progress,
indication,
None,
Some(layer.phase()),
Some(&layer.id),
);
}
}
if let Some(ref indication) = oci.indication {
let bar = match progresses.entry("root".to_string()) {
Entry::Occupied(entry) => Some(entry.into_mut()),
Entry::Vacant(entry) => {
if let Some(bar) = progress_bar_for_indication(indication) {
multi_progress.add(bar.clone());
Some(entry.insert(bar))
} else {
None
}
}
};
if let Some(bar) = bar {
configure_for_indication(
bar,
&mut multi_progress,
indication,
Some(oci.phase()),
None,
None,
);
}
}
}
multi_progress.clear()?;
Err(anyhow!("never received final reply for image pull"))
}

View File

@ -1,8 +1,8 @@
[package]
name = "krata-daemon"
description = "Daemon for the krata hypervisor."
description = "Daemon for the krata isolation engine"
license.workspace = true
version= "0.0.3"
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition = "2021"
@ -13,17 +13,23 @@ anyhow = { workspace = true }
async-stream = { workspace = true }
async-trait = { workspace = true }
bytes = { workspace = true }
circular-buffer = { workspace = true }
clap = { workspace = true }
env_logger = { workspace = true }
futures = { workspace = true }
krata = { path = "../krata", version = "^0.0.3" }
krata-runtime = { path = "../runtime", version = "^0.0.3" }
krata = { path = "../krata", version = "^0.0.12" }
krata-oci = { path = "../oci", version = "^0.0.12" }
krata-runtime = { path = "../runtime", version = "^0.0.12" }
log = { workspace = true }
prost = { workspace = true }
redb = { workspace = true }
scopeguard = { workspace = true }
serde = { workspace = true }
signal-hook = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
toml = { workspace = true }
krata-tokio-tar = { workspace = true }
tonic = { workspace = true, features = ["tls"] }
uuid = { workspace = true }

View File

@ -1,36 +1,35 @@
use anyhow::Result;
use clap::Parser;
use env_logger::Env;
use krata::dial::ControlDialAddress;
use kratad::Daemon;
use kratart::Runtime;
use log::LevelFilter;
use std::{
net::{SocketAddr, TcpStream},
str::FromStr,
sync::{atomic::AtomicBool, Arc},
};
#[derive(Parser)]
struct DaemonCommand {
#[arg(short, long, default_value = "unix:///var/lib/krata/daemon.socket")]
listen: String,
#[arg(short, long, default_value = "/var/lib/krata")]
store: String,
}
use anyhow::Result;
use clap::Parser;
use env_logger::fmt::Target;
use log::LevelFilter;
use kratad::command::DaemonCommand;
#[tokio::main(flavor = "multi_thread", worker_threads = 10)]
async fn main() -> Result<()> {
env_logger::Builder::from_env(Env::default().default_filter_or("info"))
.filter(Some("backhand::filesystem::writer"), LevelFilter::Warn)
.init();
let mut builder = env_logger::Builder::new();
builder
.filter_level(LevelFilter::Trace)
.parse_default_env()
.filter(Some("backhand::filesystem::writer"), LevelFilter::Warn);
if let Ok(f_addr) = std::env::var("KRATA_FLUENT_ADDR") {
let target = SocketAddr::from_str(f_addr.as_str())?;
builder.target(Target::Pipe(Box::new(TcpStream::connect(target)?)));
}
builder.init();
mask_sighup()?;
let args = DaemonCommand::parse();
let addr = ControlDialAddress::from_str(&args.listen)?;
let runtime = Runtime::new(args.store.clone()).await?;
let mut daemon = Daemon::new(args.store.clone(), runtime).await?;
daemon.listen(addr).await?;
Ok(())
let command = DaemonCommand::parse();
command.run().await
}
fn mask_sighup() -> Result<()> {

View File

@ -0,0 +1,36 @@
use anyhow::Result;
use clap::{CommandFactory, Parser};
use krata::dial::ControlDialAddress;
use std::str::FromStr;
use crate::Daemon;
#[derive(Parser)]
#[command(version, about = "krata isolation engine daemon")]
pub struct DaemonCommand {
#[arg(
short,
long,
default_value = "unix:///var/lib/krata/daemon.socket",
help = "Listen address"
)]
listen: String,
#[arg(short, long, default_value = "/var/lib/krata", help = "Storage path")]
store: String,
}
impl DaemonCommand {
pub async fn run(self) -> Result<()> {
let addr = ControlDialAddress::from_str(&self.listen)?;
let mut daemon = Daemon::new(self.store.clone()).await?;
daemon.listen(addr).await?;
Ok(())
}
pub fn version() -> String {
DaemonCommand::command()
.get_version()
.unwrap_or("unknown")
.to_string()
}
}

View File

@ -0,0 +1,63 @@
use std::{collections::HashMap, path::Path};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use tokio::fs;
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct DaemonConfig {
#[serde(default)]
pub oci: OciConfig,
#[serde(default)]
pub pci: DaemonPciConfig,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct OciConfig {
#[serde(default)]
pub seed: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct DaemonPciConfig {
#[serde(default)]
pub devices: HashMap<String, DaemonPciDeviceConfig>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DaemonPciDeviceConfig {
pub locations: Vec<String>,
#[serde(default)]
pub permissive: bool,
#[serde(default)]
#[serde(rename = "msi-translate")]
pub msi_translate: bool,
#[serde(default)]
#[serde(rename = "power-management")]
pub power_management: bool,
#[serde(default)]
#[serde(rename = "rdm-reserve-policy")]
pub rdm_reserve_policy: DaemonPciDeviceRdmReservePolicy,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub enum DaemonPciDeviceRdmReservePolicy {
#[default]
#[serde(rename = "strict")]
Strict,
#[serde(rename = "relaxed")]
Relaxed,
}
impl DaemonConfig {
pub async fn load(path: &Path) -> Result<DaemonConfig> {
if path.exists() {
let content = fs::read_to_string(path).await?;
let config: DaemonConfig = toml::from_str(&content)?;
Ok(config)
} else {
fs::write(&path, "").await?;
Ok(DaemonConfig::default())
}
}
}

View File

@ -0,0 +1,164 @@
use std::{collections::HashMap, sync::Arc};
use anyhow::{anyhow, Result};
use circular_buffer::CircularBuffer;
use kratart::channel::ChannelService;
use log::error;
use tokio::{
sync::{
mpsc::{error::TrySendError, Receiver, Sender},
Mutex,
},
task::JoinHandle,
};
use uuid::Uuid;
use crate::glt::GuestLookupTable;
const CONSOLE_BUFFER_SIZE: usize = 1024 * 1024;
type RawConsoleBuffer = CircularBuffer<CONSOLE_BUFFER_SIZE, u8>;
type ConsoleBuffer = Box<RawConsoleBuffer>;
type ListenerMap = Arc<Mutex<HashMap<u32, Vec<Sender<Vec<u8>>>>>>;
type BufferMap = Arc<Mutex<HashMap<u32, ConsoleBuffer>>>;
#[derive(Clone)]
pub struct DaemonConsoleHandle {
glt: GuestLookupTable,
listeners: ListenerMap,
buffers: BufferMap,
sender: Sender<(u32, Vec<u8>)>,
task: Arc<JoinHandle<()>>,
}
#[derive(Clone)]
pub struct DaemonConsoleAttachHandle {
pub initial: Vec<u8>,
listeners: ListenerMap,
sender: Sender<(u32, Vec<u8>)>,
domid: u32,
}
impl DaemonConsoleAttachHandle {
pub async fn unsubscribe(&self) -> Result<()> {
let mut guard = self.listeners.lock().await;
let _ = guard.remove(&self.domid);
Ok(())
}
pub async fn send(&self, data: Vec<u8>) -> Result<()> {
Ok(self.sender.send((self.domid, data)).await?)
}
}
impl DaemonConsoleHandle {
pub async fn attach(
&self,
uuid: Uuid,
sender: Sender<Vec<u8>>,
) -> Result<DaemonConsoleAttachHandle> {
let Some(domid) = self.glt.lookup_domid_by_uuid(&uuid).await else {
return Err(anyhow!("unable to find domain {}", uuid));
};
let buffers = self.buffers.lock().await;
let buffer = buffers.get(&domid).map(|x| x.to_vec()).unwrap_or_default();
drop(buffers);
let mut listeners = self.listeners.lock().await;
let senders = listeners.entry(domid).or_default();
senders.push(sender);
Ok(DaemonConsoleAttachHandle {
initial: buffer,
sender: self.sender.clone(),
listeners: self.listeners.clone(),
domid,
})
}
}
impl Drop for DaemonConsoleHandle {
fn drop(&mut self) {
if Arc::strong_count(&self.task) <= 1 {
self.task.abort();
}
}
}
pub struct DaemonConsole {
glt: GuestLookupTable,
listeners: ListenerMap,
buffers: BufferMap,
receiver: Receiver<(u32, Option<Vec<u8>>)>,
sender: Sender<(u32, Vec<u8>)>,
task: JoinHandle<()>,
}
impl DaemonConsole {
pub async fn new(glt: GuestLookupTable) -> Result<DaemonConsole> {
let (service, sender, receiver) =
ChannelService::new("krata-console".to_string(), Some(0)).await?;
let task = service.launch().await?;
let listeners = Arc::new(Mutex::new(HashMap::new()));
let buffers = Arc::new(Mutex::new(HashMap::new()));
Ok(DaemonConsole {
glt,
listeners,
buffers,
receiver,
sender,
task,
})
}
pub async fn launch(mut self) -> Result<DaemonConsoleHandle> {
let glt = self.glt.clone();
let listeners = self.listeners.clone();
let buffers = self.buffers.clone();
let sender = self.sender.clone();
let task = tokio::task::spawn(async move {
if let Err(error) = self.process().await {
error!("failed to process console: {}", error);
}
});
Ok(DaemonConsoleHandle {
glt,
listeners,
buffers,
sender,
task: Arc::new(task),
})
}
async fn process(&mut self) -> Result<()> {
loop {
let Some((domid, data)) = self.receiver.recv().await else {
break;
};
let mut buffers = self.buffers.lock().await;
if let Some(data) = data {
let buffer = buffers
.entry(domid)
.or_insert_with_key(|_| RawConsoleBuffer::boxed());
buffer.extend_from_slice(&data);
drop(buffers);
let mut listeners = self.listeners.lock().await;
if let Some(senders) = listeners.get_mut(&domid) {
senders.retain(|sender| {
!matches!(sender.try_send(data.to_vec()), Err(TrySendError::Closed(_)))
});
}
} else {
buffers.remove(&domid);
let mut listeners = self.listeners.lock().await;
listeners.remove(&domid);
}
}
Ok(())
}
}
impl Drop for DaemonConsole {
fn drop(&mut self) {
self.task.abort();
}
}

View File

@ -1,27 +1,46 @@
use std::{io, pin::Pin, str::FromStr};
use async_stream::try_stream;
use futures::Stream;
use krata::v1::{
common::{Guest, GuestState, GuestStatus},
control::{
control_service_server::ControlService, ConsoleDataReply, ConsoleDataRequest,
CreateGuestReply, CreateGuestRequest, DestroyGuestReply, DestroyGuestRequest,
ListGuestsReply, ListGuestsRequest, ResolveGuestReply, ResolveGuestRequest,
WatchEventsReply, WatchEventsRequest,
use krata::{
idm::internal::{
exec_stream_request_update::Update, request::Request as IdmRequestType,
response::Response as IdmResponseType, ExecEnvVar, ExecStreamRequestStart,
ExecStreamRequestStdin, ExecStreamRequestUpdate, MetricsRequest, Request as IdmRequest,
},
v1::{
common::{Guest, GuestState, GuestStatus, OciImageFormat},
control::{
control_service_server::ControlService, ConsoleDataReply, ConsoleDataRequest,
CreateGuestReply, CreateGuestRequest, DestroyGuestReply, DestroyGuestRequest,
DeviceInfo, ExecGuestReply, ExecGuestRequest, HostCpuTopologyInfo,
HostCpuTopologyReply, HostCpuTopologyRequest, HostPowerManagementPolicy,
IdentifyHostReply, IdentifyHostRequest, ListDevicesReply, ListDevicesRequest,
ListGuestsReply, ListGuestsRequest, PullImageReply, PullImageRequest,
ReadGuestMetricsReply, ReadGuestMetricsRequest, ResolveGuestReply, ResolveGuestRequest,
SnoopIdmReply, SnoopIdmRequest, WatchEventsReply, WatchEventsRequest,
},
},
};
use krataoci::{
name::ImageName,
packer::{service::OciPackerService, OciPackedFormat, OciPackedImage},
progress::{OciProgress, OciProgressContext},
};
use kratart::Runtime;
use std::{pin::Pin, str::FromStr};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
select,
sync::mpsc::Sender,
sync::mpsc::{channel, Sender},
task::JoinError,
};
use tokio_stream::StreamExt;
use tonic::{Request, Response, Status, Streaming};
use uuid::Uuid;
use crate::{db::GuestStore, event::DaemonEventContext};
use crate::{
command::DaemonCommand, console::DaemonConsoleHandle, db::GuestStore,
devices::DaemonDeviceManager, event::DaemonEventContext, glt::GuestLookupTable,
idm::DaemonIdmHandle, metrics::idm_metric_to_api, oci::convert_oci_progress,
};
pub struct ApiError {
message: String,
@ -42,42 +61,84 @@ impl From<ApiError> for Status {
}
#[derive(Clone)]
pub struct RuntimeControlService {
pub struct DaemonControlService {
glt: GuestLookupTable,
devices: DaemonDeviceManager,
events: DaemonEventContext,
runtime: Runtime,
console: DaemonConsoleHandle,
idm: DaemonIdmHandle,
guests: GuestStore,
guest_reconciler_notify: Sender<Uuid>,
packer: OciPackerService,
runtime: Runtime,
}
impl RuntimeControlService {
impl DaemonControlService {
#[allow(clippy::too_many_arguments)]
pub fn new(
glt: GuestLookupTable,
devices: DaemonDeviceManager,
events: DaemonEventContext,
runtime: Runtime,
console: DaemonConsoleHandle,
idm: DaemonIdmHandle,
guests: GuestStore,
guest_reconciler_notify: Sender<Uuid>,
packer: OciPackerService,
runtime: Runtime,
) -> Self {
Self {
glt,
devices,
events,
runtime,
console,
idm,
guests,
guest_reconciler_notify,
packer,
runtime,
}
}
}
enum ConsoleDataSelect {
Read(io::Result<usize>),
Read(Option<Vec<u8>>),
Write(Option<Result<ConsoleDataRequest, tonic::Status>>),
}
enum PullImageSelect {
Progress(Option<OciProgress>),
Completed(Result<Result<OciPackedImage, anyhow::Error>, JoinError>),
}
#[tonic::async_trait]
impl ControlService for RuntimeControlService {
impl ControlService for DaemonControlService {
type ExecGuestStream =
Pin<Box<dyn Stream<Item = Result<ExecGuestReply, Status>> + Send + 'static>>;
type ConsoleDataStream =
Pin<Box<dyn Stream<Item = Result<ConsoleDataReply, Status>> + Send + 'static>>;
type PullImageStream =
Pin<Box<dyn Stream<Item = Result<PullImageReply, Status>> + Send + 'static>>;
type WatchEventsStream =
Pin<Box<dyn Stream<Item = Result<WatchEventsReply, Status>> + Send + 'static>>;
type SnoopIdmStream =
Pin<Box<dyn Stream<Item = Result<SnoopIdmReply, Status>> + Send + 'static>>;
async fn identify_host(
&self,
request: Request<IdentifyHostRequest>,
) -> Result<Response<IdentifyHostReply>, Status> {
let _ = request.into_inner();
Ok(Response::new(IdentifyHostReply {
host_domid: self.glt.host_domid(),
host_uuid: self.glt.host_uuid().to_string(),
krata_version: DaemonCommand::version(),
}))
}
async fn create_guest(
&self,
request: Request<CreateGuestRequest>,
@ -100,6 +161,7 @@ impl ControlService for RuntimeControlService {
network: None,
exit_info: None,
error_info: None,
host: self.glt.host_uuid().to_string(),
domid: u32::MAX,
}),
spec: Some(spec),
@ -118,6 +180,98 @@ impl ControlService for RuntimeControlService {
}))
}
async fn exec_guest(
&self,
request: Request<Streaming<ExecGuestRequest>>,
) -> Result<Response<Self::ExecGuestStream>, Status> {
let mut input = request.into_inner();
let Some(request) = input.next().await else {
return Err(ApiError {
message: "expected to have at least one request".to_string(),
}
.into());
};
let request = request?;
let Some(task) = request.task else {
return Err(ApiError {
message: "task is missing".to_string(),
}
.into());
};
let uuid = Uuid::from_str(&request.guest_id).map_err(|error| ApiError {
message: error.to_string(),
})?;
let idm = self.idm.client(uuid).await.map_err(|error| ApiError {
message: error.to_string(),
})?;
let idm_request = IdmRequest {
request: Some(IdmRequestType::ExecStream(ExecStreamRequestUpdate {
update: Some(Update::Start(ExecStreamRequestStart {
environment: task
.environment
.into_iter()
.map(|x| ExecEnvVar {
key: x.key,
value: x.value,
})
.collect(),
command: task.command,
working_directory: task.working_directory,
})),
})),
};
let output = try_stream! {
let mut handle = idm.send_stream(idm_request).await.map_err(|x| ApiError {
message: x.to_string(),
})?;
loop {
select! {
x = input.next() => if let Some(update) = x {
let update: Result<ExecGuestRequest, Status> = update.map_err(|error| ApiError {
message: error.to_string()
}.into());
if let Ok(update) = update {
if !update.data.is_empty() {
let _ = handle.update(IdmRequest {
request: Some(IdmRequestType::ExecStream(ExecStreamRequestUpdate {
update: Some(Update::Stdin(ExecStreamRequestStdin {
data: update.data,
})),
}))}).await;
}
}
},
x = handle.receiver.recv() => match x {
Some(response) => {
let Some(IdmResponseType::ExecStream(update)) = response.response else {
break;
};
let reply = ExecGuestReply {
exited: update.exited,
error: update.error,
exit_code: update.exit_code,
stdout: update.stdout,
stderr: update.stderr
};
yield reply;
},
None => {
break;
}
}
};
}
};
Ok(Response::new(Box::pin(output) as Self::ExecGuestStream))
}
async fn destroy_guest(
&self,
request: Request<DestroyGuestRequest>,
@ -200,27 +354,38 @@ impl ControlService for RuntimeControlService {
let uuid = Uuid::from_str(&request.guest_id).map_err(|error| ApiError {
message: error.to_string(),
})?;
let mut console = self.runtime.console(uuid).await.map_err(ApiError::from)?;
let (sender, mut receiver) = channel(100);
let console = self
.console
.attach(uuid, sender)
.await
.map_err(|error| ApiError {
message: format!("failed to attach to console: {}", error),
})?;
let output = try_stream! {
let mut buffer: Vec<u8> = vec![0u8; 256];
yield ConsoleDataReply { data: console.initial.clone(), };
loop {
let what = select! {
x = console.read_handle.read(&mut buffer) => ConsoleDataSelect::Read(x),
x = receiver.recv() => ConsoleDataSelect::Read(x),
x = input.next() => ConsoleDataSelect::Write(x),
};
match what {
ConsoleDataSelect::Read(result) => {
let size = result?;
let data = buffer[0..size].to_vec();
ConsoleDataSelect::Read(Some(data)) => {
yield ConsoleDataReply { data, };
},
ConsoleDataSelect::Read(None) => {
break;
}
ConsoleDataSelect::Write(Some(request)) => {
let request = request?;
if !request.data.is_empty() {
console.write_handle.write_all(&request.data).await?;
console.send(request.data).await.map_err(|error| ApiError {
message: error.to_string(),
})?;
}
},
@ -234,6 +399,107 @@ impl ControlService for RuntimeControlService {
Ok(Response::new(Box::pin(output) as Self::ConsoleDataStream))
}
async fn read_guest_metrics(
&self,
request: Request<ReadGuestMetricsRequest>,
) -> Result<Response<ReadGuestMetricsReply>, Status> {
let request = request.into_inner();
let uuid = Uuid::from_str(&request.guest_id).map_err(|error| ApiError {
message: error.to_string(),
})?;
let client = self.idm.client(uuid).await.map_err(|error| ApiError {
message: error.to_string(),
})?;
let response = client
.send(IdmRequest {
request: Some(IdmRequestType::Metrics(MetricsRequest {})),
})
.await
.map_err(|error| ApiError {
message: error.to_string(),
})?;
let mut reply = ReadGuestMetricsReply::default();
if let Some(IdmResponseType::Metrics(metrics)) = response.response {
reply.root = metrics.root.map(idm_metric_to_api);
}
Ok(Response::new(reply))
}
async fn pull_image(
&self,
request: Request<PullImageRequest>,
) -> Result<Response<Self::PullImageStream>, Status> {
let request = request.into_inner();
let name = ImageName::parse(&request.image).map_err(|err| ApiError {
message: err.to_string(),
})?;
let format = match request.format() {
OciImageFormat::Unknown => OciPackedFormat::Squashfs,
OciImageFormat::Squashfs => OciPackedFormat::Squashfs,
OciImageFormat::Erofs => OciPackedFormat::Erofs,
OciImageFormat::Tar => OciPackedFormat::Tar,
};
let (context, mut receiver) = OciProgressContext::create();
let our_packer = self.packer.clone();
let output = try_stream! {
let mut task = tokio::task::spawn(async move {
our_packer.request(name, format, request.overwrite_cache, context).await
});
let abort_handle = task.abort_handle();
let _task_cancel_guard = scopeguard::guard(abort_handle, |handle| {
handle.abort();
});
loop {
let what = select! {
x = receiver.changed() => match x {
Ok(_) => PullImageSelect::Progress(Some(receiver.borrow_and_update().clone())),
Err(_) => PullImageSelect::Progress(None),
},
x = &mut task => PullImageSelect::Completed(x),
};
match what {
PullImageSelect::Progress(Some(progress)) => {
let reply = PullImageReply {
progress: Some(convert_oci_progress(progress)),
digest: String::new(),
format: OciImageFormat::Unknown.into(),
};
yield reply;
},
PullImageSelect::Completed(result) => {
let result = result.map_err(|err| ApiError {
message: err.to_string(),
})?;
let packed = result.map_err(|err| ApiError {
message: err.to_string(),
})?;
let reply = PullImageReply {
progress: None,
digest: packed.digest,
format: match packed.format {
OciPackedFormat::Squashfs => OciImageFormat::Squashfs.into(),
OciPackedFormat::Erofs => OciImageFormat::Erofs.into(),
OciPackedFormat::Tar => OciImageFormat::Tar.into(),
},
};
yield reply;
break;
},
_ => {
continue;
}
}
}
};
Ok(Response::new(Box::pin(output) as Self::PullImageStream))
}
async fn watch_events(
&self,
request: Request<WatchEventsRequest>,
@ -247,4 +513,97 @@ impl ControlService for RuntimeControlService {
};
Ok(Response::new(Box::pin(output) as Self::WatchEventsStream))
}
async fn snoop_idm(
&self,
request: Request<SnoopIdmRequest>,
) -> Result<Response<Self::SnoopIdmStream>, Status> {
let _ = request.into_inner();
let mut messages = self.idm.snoop();
let glt = self.glt.clone();
let output = try_stream! {
while let Ok(event) = messages.recv().await {
let Some(from_uuid) = glt.lookup_uuid_by_domid(event.from).await else {
continue;
};
let Some(to_uuid) = glt.lookup_uuid_by_domid(event.to).await else {
continue;
};
yield SnoopIdmReply { from: from_uuid.to_string(), to: to_uuid.to_string(), packet: Some(event.packet) };
}
};
Ok(Response::new(Box::pin(output) as Self::SnoopIdmStream))
}
async fn list_devices(
&self,
request: Request<ListDevicesRequest>,
) -> Result<Response<ListDevicesReply>, Status> {
let _ = request.into_inner();
let mut devices = Vec::new();
let state = self.devices.copy().await.map_err(|error| ApiError {
message: error.to_string(),
})?;
for (name, state) in state {
devices.push(DeviceInfo {
name,
claimed: state.owner.is_some(),
owner: state.owner.map(|x| x.to_string()).unwrap_or_default(),
});
}
Ok(Response::new(ListDevicesReply { devices }))
}
async fn get_host_cpu_topology(
&self,
request: Request<HostCpuTopologyRequest>,
) -> Result<Response<HostCpuTopologyReply>, Status> {
let _ = request.into_inner();
let power = self
.runtime
.power_management_context()
.await
.map_err(ApiError::from)?;
let cputopo = power.cpu_topology().await.map_err(ApiError::from)?;
let mut cpus = vec![];
for cpu in cputopo {
cpus.push(HostCpuTopologyInfo {
core: cpu.core,
socket: cpu.socket,
node: cpu.node,
thread: cpu.thread,
class: cpu.class as i32,
})
}
Ok(Response::new(HostCpuTopologyReply { cpus }))
}
async fn set_host_power_management_policy(
&self,
request: Request<HostPowerManagementPolicy>,
) -> Result<Response<HostPowerManagementPolicy>, Status> {
let policy = request.into_inner();
let power = self
.runtime
.power_management_context()
.await
.map_err(ApiError::from)?;
let scheduler = &policy.scheduler;
power
.set_smt_policy(policy.smt_awareness)
.await
.map_err(ApiError::from)?;
power
.set_scheduler_policy(scheduler)
.await
.map_err(ApiError::from)?;
Ok(Response::new(HostPowerManagementPolicy {
scheduler: scheduler.to_string(),
smt_awareness: policy.smt_awareness,
}))
}
}

View File

@ -0,0 +1,106 @@
use std::{collections::HashMap, sync::Arc};
use anyhow::{anyhow, Result};
use log::warn;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::config::{DaemonConfig, DaemonPciDeviceConfig};
#[derive(Clone)]
pub struct DaemonDeviceState {
pub pci: Option<DaemonPciDeviceConfig>,
pub owner: Option<Uuid>,
}
#[derive(Clone)]
pub struct DaemonDeviceManager {
config: Arc<DaemonConfig>,
devices: Arc<RwLock<HashMap<String, DaemonDeviceState>>>,
}
impl DaemonDeviceManager {
pub fn new(config: Arc<DaemonConfig>) -> Self {
Self {
config,
devices: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn claim(&self, device: &str, uuid: Uuid) -> Result<DaemonDeviceState> {
let mut devices = self.devices.write().await;
let Some(state) = devices.get_mut(device) else {
return Err(anyhow!(
"unable to claim unknown device '{}' for guest {}",
device,
uuid
));
};
if let Some(owner) = state.owner {
return Err(anyhow!(
"unable to claim device '{}' for guest {}: already claimed by {}",
device,
uuid,
owner
));
}
state.owner = Some(uuid);
Ok(state.clone())
}
pub async fn release_all(&self, uuid: Uuid) -> Result<()> {
let mut devices = self.devices.write().await;
for state in (*devices).values_mut() {
if state.owner == Some(uuid) {
state.owner = None;
}
}
Ok(())
}
pub async fn release(&self, device: &str, uuid: Uuid) -> Result<()> {
let mut devices = self.devices.write().await;
let Some(state) = devices.get_mut(device) else {
return Ok(());
};
if let Some(owner) = state.owner {
if owner != uuid {
return Ok(());
}
}
state.owner = None;
Ok(())
}
pub async fn update_claims(&self, claims: HashMap<String, Uuid>) -> Result<()> {
let mut devices = self.devices.write().await;
devices.clear();
for (name, pci) in &self.config.pci.devices {
let owner = claims.get(name).cloned();
devices.insert(
name.clone(),
DaemonDeviceState {
owner,
pci: Some(pci.clone()),
},
);
}
for (name, uuid) in &claims {
if !devices.contains_key(name) {
warn!("unknown device '{}' assigned to guest {}", name, uuid);
}
}
Ok(())
}
pub async fn copy(&self) -> Result<HashMap<String, DaemonDeviceState>> {
let devices = self.devices.read().await;
Ok(devices.clone())
}
}

View File

@ -6,10 +6,10 @@ use std::{
use anyhow::Result;
use krata::{
idm::protocol::{idm_event::Event, IdmPacket},
idm::{internal::event::Event as EventType, internal::Event},
v1::common::{GuestExitInfo, GuestState, GuestStatus},
};
use log::error;
use log::{error, warn};
use tokio::{
select,
sync::{
@ -21,15 +21,12 @@ use tokio::{
};
use uuid::Uuid;
use crate::{
db::GuestStore,
idm::{DaemonIdmHandle, DaemonIdmSubscribeHandle},
};
use crate::{db::GuestStore, idm::DaemonIdmHandle};
pub type DaemonEvent = krata::v1::control::watch_events_reply::Event;
const EVENT_CHANNEL_QUEUE_LEN: usize = 1000;
const IDM_CHANNEL_QUEUE_LEN: usize = 1000;
const IDM_EVENT_CHANNEL_QUEUE_LEN: usize = 1000;
#[derive(Clone)]
pub struct DaemonEventContext {
@ -52,9 +49,9 @@ pub struct DaemonEventGenerator {
guest_reconciler_notify: Sender<Uuid>,
feed: broadcast::Receiver<DaemonEvent>,
idm: DaemonIdmHandle,
idms: HashMap<u32, (Uuid, DaemonIdmSubscribeHandle)>,
idm_sender: Sender<(u32, IdmPacket)>,
idm_receiver: Receiver<(u32, IdmPacket)>,
idms: HashMap<u32, (Uuid, JoinHandle<()>)>,
idm_sender: Sender<(u32, Event)>,
idm_receiver: Receiver<(u32, Event)>,
_event_sender: broadcast::Sender<DaemonEvent>,
}
@ -65,7 +62,7 @@ impl DaemonEventGenerator {
idm: DaemonIdmHandle,
) -> Result<(DaemonEventContext, DaemonEventGenerator)> {
let (sender, _) = broadcast::channel(EVENT_CHANNEL_QUEUE_LEN);
let (idm_sender, idm_receiver) = channel(IDM_CHANNEL_QUEUE_LEN);
let (idm_sender, idm_receiver) = channel(IDM_EVENT_CHANNEL_QUEUE_LEN);
let generator = DaemonEventGenerator {
guests,
guest_reconciler_notify,
@ -81,46 +78,55 @@ impl DaemonEventGenerator {
}
async fn handle_feed_event(&mut self, event: &DaemonEvent) -> Result<()> {
match event {
DaemonEvent::GuestChanged(changed) => {
let Some(ref guest) = changed.guest else {
return Ok(());
};
let DaemonEvent::GuestChanged(changed) = event;
let Some(ref guest) = changed.guest else {
return Ok(());
};
let Some(ref state) = guest.state else {
return Ok(());
};
let Some(ref state) = guest.state else {
return Ok(());
};
let status = state.status();
let id = Uuid::from_str(&guest.id)?;
let domid = state.domid;
match status {
GuestStatus::Started => {
if let Entry::Vacant(e) = self.idms.entry(domid) {
let subscribe =
self.idm.subscribe(domid, self.idm_sender.clone()).await?;
e.insert((id, subscribe));
let status = state.status();
let id = Uuid::from_str(&guest.id)?;
let domid = state.domid;
match status {
GuestStatus::Started => {
if let Entry::Vacant(e) = self.idms.entry(domid) {
let client = self.idm.client_by_domid(domid).await?;
let mut receiver = client.subscribe().await?;
let sender = self.idm_sender.clone();
let task = tokio::task::spawn(async move {
loop {
let Ok(event) = receiver.recv().await else {
break;
};
if let Err(error) = sender.send((domid, event)).await {
warn!("unable to deliver idm event: {}", error);
}
}
}
GuestStatus::Destroyed => {
if let Some((_, handle)) = self.idms.remove(&domid) {
handle.unsubscribe().await?;
}
}
_ => {}
});
e.insert((id, task));
}
}
GuestStatus::Destroyed => {
if let Some((_, handle)) = self.idms.remove(&domid) {
handle.abort();
}
}
_ => {}
}
Ok(())
}
async fn handle_idm_packet(&mut self, id: Uuid, packet: IdmPacket) -> Result<()> {
if let Some(Event::Exit(exit)) = packet.event.and_then(|x| x.event) {
self.handle_exit_code(id, exit.code).await?;
async fn handle_idm_event(&mut self, id: Uuid, event: Event) -> Result<()> {
match event.event {
Some(EventType::Exit(exit)) => self.handle_exit_code(id, exit.code).await,
None => Ok(()),
}
Ok(())
}
async fn handle_exit_code(&mut self, id: Uuid, code: i32) -> Result<()> {
@ -130,6 +136,7 @@ impl DaemonEventGenerator {
network: guest.state.clone().unwrap_or_default().network,
exit_info: Some(GuestExitInfo { code }),
error_info: None,
host: guest.state.clone().map(|x| x.host).unwrap_or_default(),
domid: guest.state.clone().map(|x| x.domid).unwrap_or(u32::MAX),
});
@ -142,9 +149,9 @@ impl DaemonEventGenerator {
async fn evaluate(&mut self) -> Result<()> {
select! {
x = self.idm_receiver.recv() => match x {
Some((domid, packet)) => {
Some((domid, event)) => {
if let Some((id, _)) = self.idms.get(&domid) {
self.handle_idm_packet(*id, packet).await?;
self.handle_idm_event(*id, event).await?;
}
Ok(())
},
@ -159,7 +166,7 @@ impl DaemonEventGenerator {
Err(error) => {
Err(error.into())
}
}
},
}
}

69
crates/daemon/src/glt.rs Normal file
View File

@ -0,0 +1,69 @@
use std::{collections::HashMap, sync::Arc};
use tokio::sync::RwLock;
use uuid::Uuid;
struct GuestLookupTableState {
domid_to_uuid: HashMap<u32, Uuid>,
uuid_to_domid: HashMap<Uuid, u32>,
}
impl GuestLookupTableState {
pub fn new(host_uuid: Uuid) -> Self {
let mut domid_to_uuid = HashMap::new();
let mut uuid_to_domid = HashMap::new();
domid_to_uuid.insert(0, host_uuid);
uuid_to_domid.insert(host_uuid, 0);
GuestLookupTableState {
domid_to_uuid,
uuid_to_domid,
}
}
}
#[derive(Clone)]
pub struct GuestLookupTable {
host_domid: u32,
host_uuid: Uuid,
state: Arc<RwLock<GuestLookupTableState>>,
}
impl GuestLookupTable {
pub fn new(host_domid: u32, host_uuid: Uuid) -> Self {
GuestLookupTable {
host_domid,
host_uuid,
state: Arc::new(RwLock::new(GuestLookupTableState::new(host_uuid))),
}
}
pub fn host_uuid(&self) -> Uuid {
self.host_uuid
}
pub fn host_domid(&self) -> u32 {
self.host_domid
}
pub async fn lookup_uuid_by_domid(&self, domid: u32) -> Option<Uuid> {
let state = self.state.read().await;
state.domid_to_uuid.get(&domid).cloned()
}
pub async fn lookup_domid_by_uuid(&self, uuid: &Uuid) -> Option<u32> {
let state = self.state.read().await;
state.uuid_to_domid.get(uuid).cloned()
}
pub async fn associate(&self, uuid: Uuid, domid: u32) {
let mut state = self.state.write().await;
state.uuid_to_domid.insert(uuid, domid);
state.domid_to_uuid.insert(domid, uuid);
}
pub async fn remove(&self, uuid: Uuid, domid: u32) {
let mut state = self.state.write().await;
state.uuid_to_domid.remove(&uuid);
state.domid_to_uuid.remove(&domid);
}
}

View File

@ -1,53 +1,58 @@
use std::{collections::HashMap, sync::Arc};
use std::{
collections::{hash_map::Entry, HashMap},
sync::Arc,
};
use anyhow::Result;
use anyhow::{anyhow, Result};
use bytes::{Buf, BytesMut};
use krata::idm::protocol::IdmPacket;
use krata::idm::{
client::{IdmBackend, IdmInternalClient},
internal::INTERNAL_IDM_CHANNEL,
transport::IdmTransportPacket,
};
use kratart::channel::ChannelService;
use log::{error, warn};
use prost::Message;
use tokio::{
select,
sync::{
mpsc::{Receiver, Sender},
broadcast,
mpsc::{channel, Receiver, Sender},
Mutex,
},
task::JoinHandle,
};
use uuid::Uuid;
type ListenerMap = Arc<Mutex<HashMap<u32, Sender<(u32, IdmPacket)>>>>;
use crate::glt::GuestLookupTable;
type BackendFeedMap = Arc<Mutex<HashMap<u32, Sender<IdmTransportPacket>>>>;
type ClientMap = Arc<Mutex<HashMap<u32, IdmInternalClient>>>;
#[derive(Clone)]
pub struct DaemonIdmHandle {
listeners: ListenerMap,
glt: GuestLookupTable,
clients: ClientMap,
feeds: BackendFeedMap,
tx_sender: Sender<(u32, IdmTransportPacket)>,
task: Arc<JoinHandle<()>>,
}
#[derive(Clone)]
pub struct DaemonIdmSubscribeHandle {
domid: u32,
listeners: ListenerMap,
}
impl DaemonIdmSubscribeHandle {
pub async fn unsubscribe(&self) -> Result<()> {
let mut guard = self.listeners.lock().await;
let _ = guard.remove(&self.domid);
Ok(())
}
snoop_sender: broadcast::Sender<DaemonIdmSnoopPacket>,
}
impl DaemonIdmHandle {
pub async fn subscribe(
&self,
domid: u32,
sender: Sender<(u32, IdmPacket)>,
) -> Result<DaemonIdmSubscribeHandle> {
let mut guard = self.listeners.lock().await;
guard.insert(domid, sender);
Ok(DaemonIdmSubscribeHandle {
domid,
listeners: self.listeners.clone(),
})
pub fn snoop(&self) -> broadcast::Receiver<DaemonIdmSnoopPacket> {
self.snoop_sender.subscribe()
}
pub async fn client(&self, uuid: Uuid) -> Result<IdmInternalClient> {
let Some(domid) = self.glt.lookup_domid_by_uuid(&uuid).await else {
return Err(anyhow!("unable to find domain {}", uuid));
};
self.client_by_domid(domid).await
}
pub async fn client_by_domid(&self, domid: u32) -> Result<IdmInternalClient> {
client_or_create(domid, &self.tx_sender, &self.clients, &self.feeds).await
}
}
@ -59,70 +64,141 @@ impl Drop for DaemonIdmHandle {
}
}
#[derive(Clone)]
pub struct DaemonIdmSnoopPacket {
pub from: u32,
pub to: u32,
pub packet: IdmTransportPacket,
}
pub struct DaemonIdm {
listeners: ListenerMap,
receiver: Receiver<(u32, Vec<u8>)>,
glt: GuestLookupTable,
clients: ClientMap,
feeds: BackendFeedMap,
tx_sender: Sender<(u32, IdmTransportPacket)>,
tx_raw_sender: Sender<(u32, Vec<u8>)>,
tx_receiver: Receiver<(u32, IdmTransportPacket)>,
rx_receiver: Receiver<(u32, Option<Vec<u8>>)>,
snoop_sender: broadcast::Sender<DaemonIdmSnoopPacket>,
task: JoinHandle<()>,
}
impl DaemonIdm {
pub async fn new() -> Result<DaemonIdm> {
let (service, receiver) = ChannelService::new("krata-channel".to_string()).await?;
pub async fn new(glt: GuestLookupTable) -> Result<DaemonIdm> {
let (service, tx_raw_sender, rx_receiver) =
ChannelService::new("krata-channel".to_string(), None).await?;
let (tx_sender, tx_receiver) = channel(100);
let (snoop_sender, _) = broadcast::channel(100);
let task = service.launch().await?;
let listeners = Arc::new(Mutex::new(HashMap::new()));
let clients = Arc::new(Mutex::new(HashMap::new()));
let feeds = Arc::new(Mutex::new(HashMap::new()));
Ok(DaemonIdm {
receiver,
glt,
rx_receiver,
tx_receiver,
tx_sender,
tx_raw_sender,
snoop_sender,
task,
listeners,
clients,
feeds,
})
}
pub async fn launch(mut self) -> Result<DaemonIdmHandle> {
let listeners = self.listeners.clone();
let glt = self.glt.clone();
let clients = self.clients.clone();
let feeds = self.feeds.clone();
let tx_sender = self.tx_sender.clone();
let snoop_sender = self.snoop_sender.clone();
let task = tokio::task::spawn(async move {
let mut buffers: HashMap<u32, BytesMut> = HashMap::new();
if let Err(error) = self.process(&mut buffers).await {
while let Err(error) = self.process(&mut buffers).await {
error!("failed to process idm: {}", error);
}
});
Ok(DaemonIdmHandle {
listeners,
glt,
clients,
feeds,
tx_sender,
snoop_sender,
task: Arc::new(task),
})
}
async fn process(&mut self, buffers: &mut HashMap<u32, BytesMut>) -> Result<()> {
loop {
let Some((domid, data)) = self.receiver.recv().await else {
break;
};
select! {
x = self.rx_receiver.recv() => match x {
Some((domid, data)) => {
if let Some(data) = data {
let buffer = buffers.entry(domid).or_insert_with_key(|_| BytesMut::new());
buffer.extend_from_slice(&data);
if buffer.len() < 6 {
continue;
}
let buffer = buffers.entry(domid).or_insert_with_key(|_| BytesMut::new());
buffer.extend_from_slice(&data);
if buffer.len() < 2 {
continue;
}
let size = (buffer[0] as u16 | (buffer[1] as u16) << 8) as usize;
let needed = size + 2;
if buffer.len() < needed {
continue;
}
let mut packet = buffer.split_to(needed);
packet.advance(2);
match IdmPacket::decode(packet) {
Ok(packet) => {
let guard = self.listeners.lock().await;
if let Some(sender) = guard.get(&domid) {
if let Err(error) = sender.try_send((domid, packet)) {
warn!("dropped idm packet from domain {}: {}", domid, error);
if buffer[0] != 0xff || buffer[1] != 0xff {
buffer.clear();
continue;
}
let size = (buffer[2] as u32 | (buffer[3] as u32) << 8 | (buffer[4] as u32) << 16 | (buffer[5] as u32) << 24) as usize;
let needed = size + 6;
if buffer.len() < needed {
continue;
}
let mut packet = buffer.split_to(needed);
packet.advance(6);
match IdmTransportPacket::decode(packet) {
Ok(packet) => {
let _ = client_or_create(domid, &self.tx_sender, &self.clients, &self.feeds).await?;
let guard = self.feeds.lock().await;
if let Some(feed) = guard.get(&domid) {
let _ = feed.try_send(packet.clone());
}
let _ = self.snoop_sender.send(DaemonIdmSnoopPacket { from: domid, to: 0, packet });
}
Err(packet) => {
warn!("received invalid packet from domain {}: {}", domid, packet);
}
}
} else {
let mut clients = self.clients.lock().await;
let mut feeds = self.feeds.lock().await;
clients.remove(&domid);
feeds.remove(&domid);
}
},
None => {
break;
}
},
x = self.tx_receiver.recv() => match x {
Some((domid, packet)) => {
let data = packet.encode_to_vec();
let mut buffer = vec![0u8; 6];
let length = data.len() as u32;
buffer[0] = 0xff;
buffer[1] = 0xff;
buffer[2] = length as u8;
buffer[3] = (length << 8) as u8;
buffer[4] = (length << 16) as u8;
buffer[5] = (length << 24) as u8;
buffer.extend_from_slice(&data);
self.tx_raw_sender.send((domid, buffer)).await?;
let _ = self.snoop_sender.send(DaemonIdmSnoopPacket { from: 0, to: domid, packet });
},
None => {
break;
}
}
Err(packet) => {
warn!("received invalid packet from domain {}: {}", domid, packet);
}
}
};
}
Ok(())
}
@ -133,3 +209,54 @@ impl Drop for DaemonIdm {
self.task.abort();
}
}
async fn client_or_create(
domid: u32,
tx_sender: &Sender<(u32, IdmTransportPacket)>,
clients: &ClientMap,
feeds: &BackendFeedMap,
) -> Result<IdmInternalClient> {
let mut clients = clients.lock().await;
let mut feeds = feeds.lock().await;
match clients.entry(domid) {
Entry::Occupied(entry) => Ok(entry.get().clone()),
Entry::Vacant(entry) => {
let (rx_sender, rx_receiver) = channel(100);
feeds.insert(domid, rx_sender);
let backend = IdmDaemonBackend {
domid,
rx_receiver,
tx_sender: tx_sender.clone(),
};
let client = IdmInternalClient::new(
INTERNAL_IDM_CHANNEL,
Box::new(backend) as Box<dyn IdmBackend>,
)
.await?;
entry.insert(client.clone());
Ok(client)
}
}
}
pub struct IdmDaemonBackend {
domid: u32,
rx_receiver: Receiver<IdmTransportPacket>,
tx_sender: Sender<(u32, IdmTransportPacket)>,
}
#[async_trait::async_trait]
impl IdmBackend for IdmDaemonBackend {
async fn recv(&mut self) -> Result<IdmTransportPacket> {
if let Some(packet) = self.rx_receiver.recv().await {
Ok(packet)
} else {
Err(anyhow!("idm receive channel closed"))
}
}
async fn send(&mut self, packet: IdmTransportPacket) -> Result<()> {
self.tx_sender.send((self.domid, packet)).await?;
Ok(())
}
}

View File

@ -1,15 +1,21 @@
use std::{net::SocketAddr, path::PathBuf, str::FromStr};
use std::{net::SocketAddr, path::PathBuf, str::FromStr, sync::Arc};
use anyhow::Result;
use control::RuntimeControlService;
use anyhow::{anyhow, Result};
use config::DaemonConfig;
use console::{DaemonConsole, DaemonConsoleHandle};
use control::DaemonControlService;
use db::GuestStore;
use devices::DaemonDeviceManager;
use event::{DaemonEventContext, DaemonEventGenerator};
use glt::GuestLookupTable;
use idm::{DaemonIdm, DaemonIdmHandle};
use krata::{dial::ControlDialAddress, v1::control::control_service_server::ControlServiceServer};
use krataoci::{packer::service::OciPackerService, registry::OciPlatform};
use kratart::Runtime;
use log::info;
use reconcile::guest::GuestReconciler;
use tokio::{
fs,
net::UnixListener,
sync::mpsc::{channel, Sender},
task::JoinHandle,
@ -18,60 +24,144 @@ use tokio_stream::wrappers::UnixListenerStream;
use tonic::transport::{Identity, Server, ServerTlsConfig};
use uuid::Uuid;
pub mod command;
pub mod config;
pub mod console;
pub mod control;
pub mod db;
pub mod devices;
pub mod event;
pub mod glt;
pub mod idm;
pub mod metrics;
pub mod oci;
pub mod reconcile;
pub struct Daemon {
store: String,
runtime: Runtime,
_config: Arc<DaemonConfig>,
glt: GuestLookupTable,
devices: DaemonDeviceManager,
guests: GuestStore,
events: DaemonEventContext,
guest_reconciler_task: JoinHandle<()>,
guest_reconciler_notify: Sender<Uuid>,
generator_task: JoinHandle<()>,
_idm: DaemonIdmHandle,
idm: DaemonIdmHandle,
console: DaemonConsoleHandle,
packer: OciPackerService,
runtime: Runtime,
}
const GUEST_RECONCILER_QUEUE_LEN: usize = 1000;
impl Daemon {
pub async fn new(store: String, runtime: Runtime) -> Result<Self> {
pub async fn new(store: String) -> Result<Self> {
let store_dir = PathBuf::from(store.clone());
let mut config_path = store_dir.clone();
config_path.push("config.toml");
let config = DaemonConfig::load(&config_path).await?;
let config = Arc::new(config);
let devices = DaemonDeviceManager::new(config.clone());
let mut image_cache_dir = store_dir.clone();
image_cache_dir.push("cache");
image_cache_dir.push("image");
fs::create_dir_all(&image_cache_dir).await?;
let mut host_uuid_path = store_dir.clone();
host_uuid_path.push("host.uuid");
let host_uuid = if host_uuid_path.is_file() {
let content = fs::read_to_string(&host_uuid_path).await?;
Uuid::from_str(content.trim()).ok()
} else {
None
};
let host_uuid = if let Some(host_uuid) = host_uuid {
host_uuid
} else {
let generated = Uuid::new_v4();
let mut string = generated.to_string();
string.push('\n');
fs::write(&host_uuid_path, string).await?;
generated
};
let initrd_path = detect_guest_path(&store, "initrd")?;
let kernel_path = detect_guest_path(&store, "kernel")?;
let addons_path = detect_guest_path(&store, "addons.squashfs")?;
let seed = config.oci.seed.clone().map(PathBuf::from);
let packer = OciPackerService::new(seed, &image_cache_dir, OciPlatform::current()).await?;
let runtime = Runtime::new(host_uuid).await?;
let glt = GuestLookupTable::new(0, host_uuid);
let guests_db_path = format!("{}/guests.db", store);
let guests = GuestStore::open(&PathBuf::from(guests_db_path))?;
let (guest_reconciler_notify, guest_reconciler_receiver) =
channel::<Uuid>(GUEST_RECONCILER_QUEUE_LEN);
let idm = DaemonIdm::new().await?;
let idm = DaemonIdm::new(glt.clone()).await?;
let idm = idm.launch().await?;
let console = DaemonConsole::new(glt.clone()).await?;
let console = console.launch().await?;
let (events, generator) =
DaemonEventGenerator::new(guests.clone(), guest_reconciler_notify.clone(), idm.clone())
.await?;
let runtime_for_reconciler = runtime.dupe().await?;
let guest_reconciler =
GuestReconciler::new(guests.clone(), events.clone(), runtime_for_reconciler)?;
let guest_reconciler = GuestReconciler::new(
devices.clone(),
glt.clone(),
guests.clone(),
events.clone(),
runtime_for_reconciler,
packer.clone(),
guest_reconciler_notify.clone(),
kernel_path,
initrd_path,
addons_path,
)?;
let guest_reconciler_task = guest_reconciler.launch(guest_reconciler_receiver).await?;
let generator_task = generator.launch().await?;
// TODO: Create a way of abstracting early init tasks in kratad.
// TODO: Make initial power management policy configurable.
// FIXME: Power management hypercalls fail when running as an L1 hypervisor.
// let power = runtime.power_management_context().await?;
// power.set_smt_policy(true).await?;
// power
// .set_scheduler_policy("performance".to_string())
// .await?;
Ok(Self {
store,
runtime,
_config: config,
glt,
devices,
guests,
events,
guest_reconciler_task,
guest_reconciler_notify,
generator_task,
_idm: idm,
idm,
console,
packer,
runtime,
})
}
pub async fn listen(&mut self, addr: ControlDialAddress) -> Result<()> {
let control_service = RuntimeControlService::new(
let control_service = DaemonControlService::new(
self.glt.clone(),
self.devices.clone(),
self.events.clone(),
self.runtime.clone(),
self.console.clone(),
self.idm.clone(),
self.guests.clone(),
self.guest_reconciler_notify.clone(),
self.packer.clone(),
self.runtime.clone(),
);
let mut server = Server::builder();
@ -97,7 +187,7 @@ impl Daemon {
ControlDialAddress::UnixSocket { path } => {
let path = PathBuf::from(path);
if path.exists() {
tokio::fs::remove_file(&path).await?;
fs::remove_file(&path).await?;
}
let listener = UnixListener::bind(path)?;
let stream = UnixListenerStream::new(listener);
@ -128,3 +218,16 @@ impl Drop for Daemon {
self.generator_task.abort();
}
}
fn detect_guest_path(store: &str, name: &str) -> Result<PathBuf> {
let mut path = PathBuf::from(format!("{}/guest/{}", store, name));
if path.is_file() {
return Ok(path);
}
path = PathBuf::from(format!("/usr/share/krata/guest/{}", name));
if path.is_file() {
return Ok(path);
}
Err(anyhow!("unable to find required guest file: {}", name))
}

View File

@ -0,0 +1,27 @@
use krata::{
idm::internal::{MetricFormat, MetricNode},
v1::common::{GuestMetricFormat, GuestMetricNode},
};
fn idm_metric_format_to_api(format: MetricFormat) -> GuestMetricFormat {
match format {
MetricFormat::Unknown => GuestMetricFormat::Unknown,
MetricFormat::Bytes => GuestMetricFormat::Bytes,
MetricFormat::Integer => GuestMetricFormat::Integer,
MetricFormat::DurationSeconds => GuestMetricFormat::DurationSeconds,
}
}
pub fn idm_metric_to_api(node: MetricNode) -> GuestMetricNode {
let format = node.format();
GuestMetricNode {
name: node.name,
value: node.value,
format: idm_metric_format_to_api(format).into(),
children: node
.children
.into_iter()
.map(idm_metric_to_api)
.collect::<Vec<_>>(),
}
}

79
crates/daemon/src/oci.rs Normal file
View File

@ -0,0 +1,79 @@
use krata::v1::control::{
image_progress_indication::Indication, ImageProgress, ImageProgressIndication,
ImageProgressIndicationBar, ImageProgressIndicationCompleted, ImageProgressIndicationHidden,
ImageProgressIndicationSpinner, ImageProgressLayer, ImageProgressLayerPhase,
ImageProgressPhase,
};
use krataoci::progress::{
OciProgress, OciProgressIndication, OciProgressLayer, OciProgressLayerPhase, OciProgressPhase,
};
fn convert_oci_progress_indication(indication: OciProgressIndication) -> ImageProgressIndication {
ImageProgressIndication {
indication: Some(match indication {
OciProgressIndication::Hidden => Indication::Hidden(ImageProgressIndicationHidden {}),
OciProgressIndication::ProgressBar {
message,
current,
total,
bytes,
} => Indication::Bar(ImageProgressIndicationBar {
message: message.unwrap_or_default(),
current,
total,
is_bytes: bytes,
}),
OciProgressIndication::Spinner { message } => {
Indication::Spinner(ImageProgressIndicationSpinner {
message: message.unwrap_or_default(),
})
}
OciProgressIndication::Completed {
message,
total,
bytes,
} => Indication::Completed(ImageProgressIndicationCompleted {
message: message.unwrap_or_default(),
total: total.unwrap_or(0),
is_bytes: bytes,
}),
}),
}
}
fn convert_oci_layer_progress(layer: OciProgressLayer) -> ImageProgressLayer {
ImageProgressLayer {
id: layer.id,
phase: match layer.phase {
OciProgressLayerPhase::Waiting => ImageProgressLayerPhase::Waiting,
OciProgressLayerPhase::Downloading => ImageProgressLayerPhase::Downloading,
OciProgressLayerPhase::Downloaded => ImageProgressLayerPhase::Downloaded,
OciProgressLayerPhase::Extracting => ImageProgressLayerPhase::Extracting,
OciProgressLayerPhase::Extracted => ImageProgressLayerPhase::Extracted,
}
.into(),
indication: Some(convert_oci_progress_indication(layer.indication)),
}
}
pub fn convert_oci_progress(oci: OciProgress) -> ImageProgress {
ImageProgress {
phase: match oci.phase {
OciProgressPhase::Started => ImageProgressPhase::Started,
OciProgressPhase::Resolving => ImageProgressPhase::Resolving,
OciProgressPhase::Resolved => ImageProgressPhase::Resolved,
OciProgressPhase::ConfigDownload => ImageProgressPhase::ConfigDownload,
OciProgressPhase::LayerDownload => ImageProgressPhase::LayerDownload,
OciProgressPhase::Assemble => ImageProgressPhase::Assemble,
OciProgressPhase::Pack => ImageProgressPhase::Pack,
OciProgressPhase::Complete => ImageProgressPhase::Complete,
}
.into(),
layers: oci
.layers
.into_values()
.map(convert_oci_layer_progress)
.collect::<Vec<_>>(),
indication: Some(convert_oci_progress_indication(oci.indication)),
}
}

View File

@ -1,246 +0,0 @@
use std::{collections::HashMap, time::Duration};
use anyhow::{anyhow, Result};
use krata::v1::{
common::{
guest_image_spec::Image, Guest, GuestErrorInfo, GuestExitInfo, GuestNetworkState,
GuestState, GuestStatus,
},
control::GuestChangedEvent,
};
use kratart::{launch::GuestLaunchRequest, GuestInfo, Runtime};
use log::{error, info, trace, warn};
use tokio::{select, sync::mpsc::Receiver, task::JoinHandle, time::sleep};
use uuid::Uuid;
use crate::{
db::GuestStore,
event::{DaemonEvent, DaemonEventContext},
};
pub struct GuestReconciler {
guests: GuestStore,
events: DaemonEventContext,
runtime: Runtime,
}
impl GuestReconciler {
pub fn new(guests: GuestStore, events: DaemonEventContext, runtime: Runtime) -> Result<Self> {
Ok(Self {
guests,
events,
runtime,
})
}
pub async fn launch(self, mut notify: Receiver<Uuid>) -> Result<JoinHandle<()>> {
Ok(tokio::task::spawn(async move {
if let Err(error) = self.reconcile_runtime(true).await {
error!("runtime reconciler failed: {}", error);
}
loop {
select! {
x = notify.recv() => match x {
None => {
break;
},
Some(uuid) => {
if let Err(error) = self.reconcile(uuid).await {
error!("failed to reconcile guest {}: {}", uuid, error);
}
}
},
_ = sleep(Duration::from_secs(5)) => {
if let Err(error) = self.reconcile_runtime(false).await {
error!("runtime reconciler failed: {}", error);
}
}
};
}
}))
}
pub async fn reconcile_runtime(&self, initial: bool) -> Result<()> {
trace!("reconciling runtime");
let runtime_guests = self.runtime.list().await?;
let stored_guests = self.guests.list().await?;
for (uuid, mut stored_guest) in stored_guests {
let previous_guest = stored_guest.clone();
let runtime_guest = runtime_guests.iter().find(|x| x.uuid == uuid);
match runtime_guest {
None => {
let mut state = stored_guest.state.as_mut().cloned().unwrap_or_default();
if state.status() == GuestStatus::Started {
state.status = GuestStatus::Starting.into();
}
stored_guest.state = Some(state);
}
Some(runtime) => {
let mut state = stored_guest.state.as_mut().cloned().unwrap_or_default();
if let Some(code) = runtime.state.exit_code {
state.status = GuestStatus::Exited.into();
state.exit_info = Some(GuestExitInfo { code });
} else {
state.status = GuestStatus::Started.into();
}
state.network = Some(guestinfo_to_networkstate(runtime));
stored_guest.state = Some(state);
}
}
let changed = stored_guest != previous_guest;
if changed || initial {
self.guests.update(uuid, stored_guest).await?;
if let Err(error) = self.reconcile(uuid).await {
error!("failed to reconcile guest {}: {}", uuid, error);
}
}
}
Ok(())
}
pub async fn reconcile(&self, uuid: Uuid) -> Result<()> {
let Some(mut guest) = self.guests.read(uuid).await? else {
warn!(
"notified of reconcile for guest {} but it didn't exist",
uuid
);
return Ok(());
};
info!("reconciling guest {}", uuid);
self.events
.send(DaemonEvent::GuestChanged(GuestChangedEvent {
guest: Some(guest.clone()),
}))?;
let result = match guest.state.as_ref().map(|x| x.status()).unwrap_or_default() {
GuestStatus::Starting => self.start(uuid, &mut guest).await,
GuestStatus::Destroying | GuestStatus::Exited => self.destroy(uuid, &mut guest).await,
_ => Ok(false),
};
let changed = match result {
Ok(changed) => changed,
Err(error) => {
guest.state = Some(guest.state.as_mut().cloned().unwrap_or_default());
guest.state.as_mut().unwrap().status = GuestStatus::Failed.into();
guest.state.as_mut().unwrap().error_info = Some(GuestErrorInfo {
message: error.to_string(),
});
true
}
};
info!("reconciled guest {}", uuid);
let status = guest.state.as_ref().map(|x| x.status()).unwrap_or_default();
let destroyed = status == GuestStatus::Destroyed || status == GuestStatus::Failed;
if changed {
let event = DaemonEvent::GuestChanged(GuestChangedEvent {
guest: Some(guest.clone()),
});
if destroyed {
self.guests.remove(uuid).await?;
} else {
self.guests.update(uuid, guest.clone()).await?;
}
self.events.send(event)?;
}
Ok(())
}
async fn start(&self, uuid: Uuid, guest: &mut Guest) -> Result<bool> {
let Some(ref spec) = guest.spec else {
return Err(anyhow!("guest spec not specified"));
};
let Some(ref image) = spec.image else {
return Err(anyhow!("image spec not provided"));
};
let oci = match image.image {
Some(Image::Oci(ref oci)) => oci,
None => {
return Err(anyhow!("oci spec not specified"));
}
};
let task = spec.task.as_ref().cloned().unwrap_or_default();
let info = self
.runtime
.launch(GuestLaunchRequest {
uuid: Some(uuid),
name: if spec.name.is_empty() {
None
} else {
Some(&spec.name)
},
image: &oci.image,
vcpus: spec.vcpus,
mem: spec.mem,
env: task
.environment
.iter()
.map(|x| (x.key.clone(), x.value.clone()))
.collect::<HashMap<_, _>>(),
run: empty_vec_optional(task.command.clone()),
debug: false,
})
.await?;
info!("started guest {}", uuid);
guest.state = Some(GuestState {
status: GuestStatus::Started.into(),
network: Some(guestinfo_to_networkstate(&info)),
exit_info: None,
error_info: None,
domid: info.domid,
});
Ok(true)
}
async fn destroy(&self, uuid: Uuid, guest: &mut Guest) -> Result<bool> {
if let Err(error) = self.runtime.destroy(uuid).await {
trace!("failed to destroy runtime guest {}: {}", uuid, error);
}
info!("destroyed guest {}", uuid);
guest.state = Some(GuestState {
status: GuestStatus::Destroyed.into(),
network: None,
exit_info: None,
error_info: None,
domid: guest.state.as_ref().map(|x| x.domid).unwrap_or(u32::MAX),
});
Ok(true)
}
}
fn empty_vec_optional<T>(value: Vec<T>) -> Option<Vec<T>> {
if value.is_empty() {
None
} else {
Some(value)
}
}
fn guestinfo_to_networkstate(info: &GuestInfo) -> GuestNetworkState {
GuestNetworkState {
guest_ipv4: info.guest_ipv4.map(|x| x.to_string()).unwrap_or_default(),
guest_ipv6: info.guest_ipv6.map(|x| x.to_string()).unwrap_or_default(),
guest_mac: info.guest_mac.as_ref().cloned().unwrap_or_default(),
gateway_ipv4: info.gateway_ipv4.map(|x| x.to_string()).unwrap_or_default(),
gateway_ipv6: info.gateway_ipv6.map(|x| x.to_string()).unwrap_or_default(),
gateway_mac: info.gateway_mac.as_ref().cloned().unwrap_or_default(),
}
}

View File

@ -0,0 +1,374 @@
use std::{
collections::{hash_map::Entry, HashMap},
path::PathBuf,
sync::Arc,
time::Duration,
};
use anyhow::Result;
use krata::v1::{
common::{Guest, GuestErrorInfo, GuestExitInfo, GuestNetworkState, GuestState, GuestStatus},
control::GuestChangedEvent,
};
use krataoci::packer::service::OciPackerService;
use kratart::{GuestInfo, Runtime};
use log::{error, info, trace, warn};
use tokio::{
select,
sync::{
mpsc::{channel, Receiver, Sender},
Mutex, RwLock,
},
task::JoinHandle,
time::sleep,
};
use uuid::Uuid;
use crate::{
db::GuestStore,
devices::DaemonDeviceManager,
event::{DaemonEvent, DaemonEventContext},
glt::GuestLookupTable,
};
use self::start::GuestStarter;
mod start;
const PARALLEL_LIMIT: u32 = 5;
#[derive(Debug)]
enum GuestReconcilerResult {
Unchanged,
Changed { rerun: bool },
}
struct GuestReconcilerEntry {
task: JoinHandle<()>,
sender: Sender<()>,
}
impl Drop for GuestReconcilerEntry {
fn drop(&mut self) {
self.task.abort();
}
}
#[derive(Clone)]
pub struct GuestReconciler {
devices: DaemonDeviceManager,
glt: GuestLookupTable,
guests: GuestStore,
events: DaemonEventContext,
runtime: Runtime,
packer: OciPackerService,
kernel_path: PathBuf,
initrd_path: PathBuf,
addons_path: PathBuf,
tasks: Arc<Mutex<HashMap<Uuid, GuestReconcilerEntry>>>,
guest_reconciler_notify: Sender<Uuid>,
reconcile_lock: Arc<RwLock<()>>,
}
impl GuestReconciler {
#[allow(clippy::too_many_arguments)]
pub fn new(
devices: DaemonDeviceManager,
glt: GuestLookupTable,
guests: GuestStore,
events: DaemonEventContext,
runtime: Runtime,
packer: OciPackerService,
guest_reconciler_notify: Sender<Uuid>,
kernel_path: PathBuf,
initrd_path: PathBuf,
modules_path: PathBuf,
) -> Result<Self> {
Ok(Self {
devices,
glt,
guests,
events,
runtime,
packer,
kernel_path,
initrd_path,
addons_path: modules_path,
tasks: Arc::new(Mutex::new(HashMap::new())),
guest_reconciler_notify,
reconcile_lock: Arc::new(RwLock::with_max_readers((), PARALLEL_LIMIT)),
})
}
pub async fn launch(self, mut notify: Receiver<Uuid>) -> Result<JoinHandle<()>> {
Ok(tokio::task::spawn(async move {
if let Err(error) = self.reconcile_runtime(true).await {
error!("runtime reconciler failed: {}", error);
}
loop {
select! {
x = notify.recv() => match x {
None => {
break;
},
Some(uuid) => {
if let Err(error) = self.launch_task_if_needed(uuid).await {
error!("failed to start guest reconciler task {}: {}", uuid, error);
}
let map = self.tasks.lock().await;
if let Some(entry) = map.get(&uuid) {
if let Err(error) = entry.sender.send(()).await {
error!("failed to notify guest reconciler task {}: {}", uuid, error);
}
}
}
},
_ = sleep(Duration::from_secs(15)) => {
if let Err(error) = self.reconcile_runtime(false).await {
error!("runtime reconciler failed: {}", error);
}
}
};
}
}))
}
pub async fn reconcile_runtime(&self, initial: bool) -> Result<()> {
let _permit = self.reconcile_lock.write().await;
trace!("reconciling runtime");
let runtime_guests = self.runtime.list().await?;
let stored_guests = self.guests.list().await?;
let non_existent_guests = runtime_guests
.iter()
.filter(|x| !stored_guests.iter().any(|g| *g.0 == x.uuid))
.collect::<Vec<_>>();
for guest in non_existent_guests {
warn!("destroying unknown runtime guest {}", guest.uuid);
if let Err(error) = self.runtime.destroy(guest.uuid).await {
error!(
"failed to destroy unknown runtime guest {}: {}",
guest.uuid, error
);
}
self.guests.remove(guest.uuid).await?;
}
let mut device_claims = HashMap::new();
for (uuid, mut stored_guest) in stored_guests {
let previous_guest = stored_guest.clone();
let runtime_guest = runtime_guests.iter().find(|x| x.uuid == uuid);
match runtime_guest {
None => {
let mut state = stored_guest.state.as_mut().cloned().unwrap_or_default();
if state.status() == GuestStatus::Started {
state.status = GuestStatus::Starting.into();
}
stored_guest.state = Some(state);
}
Some(runtime) => {
self.glt.associate(uuid, runtime.domid).await;
let mut state = stored_guest.state.as_mut().cloned().unwrap_or_default();
if let Some(code) = runtime.state.exit_code {
state.status = GuestStatus::Exited.into();
state.exit_info = Some(GuestExitInfo { code });
} else {
state.status = GuestStatus::Started.into();
}
for device in &stored_guest
.spec
.as_ref()
.cloned()
.unwrap_or_default()
.devices
{
device_claims.insert(device.name.clone(), uuid);
}
state.network = Some(guestinfo_to_networkstate(runtime));
stored_guest.state = Some(state);
}
}
let changed = stored_guest != previous_guest;
if changed || initial {
self.guests.update(uuid, stored_guest).await?;
let _ = self.guest_reconciler_notify.try_send(uuid);
}
}
self.devices.update_claims(device_claims).await?;
Ok(())
}
pub async fn reconcile(&self, uuid: Uuid) -> Result<bool> {
let _runtime_reconcile_permit = self.reconcile_lock.read().await;
let Some(mut guest) = self.guests.read(uuid).await? else {
warn!(
"notified of reconcile for guest {} but it didn't exist",
uuid
);
return Ok(false);
};
info!("reconciling guest {}", uuid);
self.events
.send(DaemonEvent::GuestChanged(GuestChangedEvent {
guest: Some(guest.clone()),
}))?;
let start_status = guest.state.as_ref().map(|x| x.status()).unwrap_or_default();
let result = match start_status {
GuestStatus::Starting => self.start(uuid, &mut guest).await,
GuestStatus::Exited => self.exited(&mut guest).await,
GuestStatus::Destroying => self.destroy(uuid, &mut guest).await,
_ => Ok(GuestReconcilerResult::Unchanged),
};
let result = match result {
Ok(result) => result,
Err(error) => {
guest.state = Some(guest.state.as_mut().cloned().unwrap_or_default());
guest.state.as_mut().unwrap().status = GuestStatus::Failed.into();
guest.state.as_mut().unwrap().error_info = Some(GuestErrorInfo {
message: error.to_string(),
});
warn!("failed to start guest {}: {}", guest.id, error);
GuestReconcilerResult::Changed { rerun: false }
}
};
info!("reconciled guest {}", uuid);
let status = guest.state.as_ref().map(|x| x.status()).unwrap_or_default();
let destroyed = status == GuestStatus::Destroyed;
let rerun = if let GuestReconcilerResult::Changed { rerun } = result {
let event = DaemonEvent::GuestChanged(GuestChangedEvent {
guest: Some(guest.clone()),
});
if destroyed {
self.guests.remove(uuid).await?;
let mut map = self.tasks.lock().await;
map.remove(&uuid);
} else {
self.guests.update(uuid, guest.clone()).await?;
}
self.events.send(event)?;
rerun
} else {
false
};
Ok(rerun)
}
async fn start(&self, uuid: Uuid, guest: &mut Guest) -> Result<GuestReconcilerResult> {
let starter = GuestStarter {
devices: &self.devices,
kernel_path: &self.kernel_path,
initrd_path: &self.initrd_path,
addons_path: &self.addons_path,
packer: &self.packer,
glt: &self.glt,
runtime: &self.runtime,
};
starter.start(uuid, guest).await
}
async fn exited(&self, guest: &mut Guest) -> Result<GuestReconcilerResult> {
if let Some(ref mut state) = guest.state {
state.set_status(GuestStatus::Destroying);
Ok(GuestReconcilerResult::Changed { rerun: true })
} else {
Ok(GuestReconcilerResult::Unchanged)
}
}
async fn destroy(&self, uuid: Uuid, guest: &mut Guest) -> Result<GuestReconcilerResult> {
if let Err(error) = self.runtime.destroy(uuid).await {
trace!("failed to destroy runtime guest {}: {}", uuid, error);
}
let domid = guest.state.as_ref().map(|x| x.domid);
if let Some(domid) = domid {
self.glt.remove(uuid, domid).await;
}
info!("destroyed guest {}", uuid);
guest.state = Some(GuestState {
status: GuestStatus::Destroyed.into(),
network: None,
exit_info: None,
error_info: None,
host: self.glt.host_uuid().to_string(),
domid: domid.unwrap_or(u32::MAX),
});
self.devices.release_all(uuid).await?;
Ok(GuestReconcilerResult::Changed { rerun: false })
}
async fn launch_task_if_needed(&self, uuid: Uuid) -> Result<()> {
let mut map = self.tasks.lock().await;
match map.entry(uuid) {
Entry::Occupied(_) => {}
Entry::Vacant(entry) => {
entry.insert(self.launch_task(uuid).await?);
}
}
Ok(())
}
async fn launch_task(&self, uuid: Uuid) -> Result<GuestReconcilerEntry> {
let this = self.clone();
let (sender, mut receiver) = channel(10);
let task = tokio::task::spawn(async move {
'notify_loop: loop {
if receiver.recv().await.is_none() {
break 'notify_loop;
}
'rerun_loop: loop {
let rerun = match this.reconcile(uuid).await {
Ok(rerun) => rerun,
Err(error) => {
error!("failed to reconcile guest {}: {}", uuid, error);
false
}
};
if rerun {
continue 'rerun_loop;
}
break 'rerun_loop;
}
}
});
Ok(GuestReconcilerEntry { task, sender })
}
}
pub fn guestinfo_to_networkstate(info: &GuestInfo) -> GuestNetworkState {
GuestNetworkState {
guest_ipv4: info.guest_ipv4.map(|x| x.to_string()).unwrap_or_default(),
guest_ipv6: info.guest_ipv6.map(|x| x.to_string()).unwrap_or_default(),
guest_mac: info.guest_mac.as_ref().cloned().unwrap_or_default(),
gateway_ipv4: info.gateway_ipv4.map(|x| x.to_string()).unwrap_or_default(),
gateway_ipv6: info.gateway_ipv6.map(|x| x.to_string()).unwrap_or_default(),
gateway_mac: info.gateway_mac.as_ref().cloned().unwrap_or_default(),
}
}

View File

@ -0,0 +1,224 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use anyhow::{anyhow, Result};
use futures::StreamExt;
use krata::launchcfg::LaunchPackedFormat;
use krata::v1::common::GuestOciImageSpec;
use krata::v1::common::{guest_image_spec::Image, Guest, GuestState, GuestStatus, OciImageFormat};
use krataoci::packer::{service::OciPackerService, OciPackedFormat};
use kratart::launch::{PciBdf, PciDevice, PciRdmReservePolicy};
use kratart::{launch::GuestLaunchRequest, Runtime};
use log::info;
use tokio::fs::{self, File};
use tokio::io::AsyncReadExt;
use tokio_tar::Archive;
use uuid::Uuid;
use crate::config::DaemonPciDeviceRdmReservePolicy;
use crate::devices::DaemonDeviceManager;
use crate::{
glt::GuestLookupTable,
reconcile::guest::{guestinfo_to_networkstate, GuestReconcilerResult},
};
pub struct GuestStarter<'a> {
pub devices: &'a DaemonDeviceManager,
pub kernel_path: &'a Path,
pub initrd_path: &'a Path,
pub addons_path: &'a Path,
pub packer: &'a OciPackerService,
pub glt: &'a GuestLookupTable,
pub runtime: &'a Runtime,
}
impl GuestStarter<'_> {
pub async fn oci_spec_tar_read_file(
&self,
file: &Path,
oci: &GuestOciImageSpec,
) -> Result<Vec<u8>> {
if oci.format() != OciImageFormat::Tar {
return Err(anyhow!(
"oci image spec for {} is required to be in tar format",
oci.digest
));
}
let image = self
.packer
.recall(&oci.digest, OciPackedFormat::Tar)
.await?;
let Some(image) = image else {
return Err(anyhow!("image {} was not found in tar format", oci.digest));
};
let mut archive = Archive::new(File::open(&image.path).await?);
let mut entries = archive.entries()?;
while let Some(entry) = entries.next().await {
let mut entry = entry?;
let path = entry.path()?;
if path == file {
let mut buffer = Vec::new();
entry.read_to_end(&mut buffer).await?;
return Ok(buffer);
}
}
Err(anyhow!(
"unable to find file {} in image {}",
file.to_string_lossy(),
oci.digest
))
}
pub async fn start(&self, uuid: Uuid, guest: &mut Guest) -> Result<GuestReconcilerResult> {
let Some(ref spec) = guest.spec else {
return Err(anyhow!("guest spec not specified"));
};
let Some(ref image) = spec.image else {
return Err(anyhow!("image spec not provided"));
};
let oci = match image.image {
Some(Image::Oci(ref oci)) => oci,
None => {
return Err(anyhow!("oci spec not specified"));
}
};
let task = spec.task.as_ref().cloned().unwrap_or_default();
let image = self
.packer
.recall(
&oci.digest,
match oci.format() {
OciImageFormat::Unknown => OciPackedFormat::Squashfs,
OciImageFormat::Squashfs => OciPackedFormat::Squashfs,
OciImageFormat::Erofs => OciPackedFormat::Erofs,
OciImageFormat::Tar => {
return Err(anyhow!("tar image format is not supported for guests"));
}
},
)
.await?;
let Some(image) = image else {
return Err(anyhow!(
"image {} in the requested format did not exist",
oci.digest
));
};
let kernel = if let Some(ref spec) = spec.kernel {
let Some(Image::Oci(ref oci)) = spec.image else {
return Err(anyhow!("kernel image spec must be an oci image"));
};
self.oci_spec_tar_read_file(&PathBuf::from("kernel/image"), oci)
.await?
} else {
fs::read(&self.kernel_path).await?
};
let initrd = if let Some(ref spec) = spec.initrd {
let Some(Image::Oci(ref oci)) = spec.image else {
return Err(anyhow!("initrd image spec must be an oci image"));
};
self.oci_spec_tar_read_file(&PathBuf::from("krata/initrd"), oci)
.await?
} else {
fs::read(&self.initrd_path).await?
};
let success = AtomicBool::new(false);
let _device_release_guard = scopeguard::guard(
(spec.devices.clone(), self.devices.clone()),
|(devices, manager)| {
if !success.load(Ordering::Acquire) {
tokio::task::spawn(async move {
for device in devices {
let _ = manager.release(&device.name, uuid).await;
}
});
}
},
);
let mut pcis = Vec::new();
for device in &spec.devices {
let state = self.devices.claim(&device.name, uuid).await?;
if let Some(cfg) = state.pci {
for location in cfg.locations {
let pci = PciDevice {
bdf: PciBdf::from_str(&location)?.with_domain(0),
permissive: cfg.permissive,
msi_translate: cfg.msi_translate,
power_management: cfg.power_management,
rdm_reserve_policy: match cfg.rdm_reserve_policy {
DaemonPciDeviceRdmReservePolicy::Strict => PciRdmReservePolicy::Strict,
DaemonPciDeviceRdmReservePolicy::Relaxed => {
PciRdmReservePolicy::Relaxed
}
},
};
pcis.push(pci);
}
} else {
return Err(anyhow!(
"device '{}' isn't a known device type",
device.name
));
}
}
let info = self
.runtime
.launch(GuestLaunchRequest {
format: LaunchPackedFormat::Squashfs,
uuid: Some(uuid),
name: if spec.name.is_empty() {
None
} else {
Some(spec.name.clone())
},
image,
kernel,
initrd,
vcpus: spec.vcpus,
mem: spec.mem,
pcis,
env: task
.environment
.iter()
.map(|x| (x.key.clone(), x.value.clone()))
.collect::<HashMap<_, _>>(),
run: empty_vec_optional(task.command.clone()),
debug: false,
addons_image: Some(self.addons_path.to_path_buf()),
})
.await?;
self.glt.associate(uuid, info.domid).await;
info!("started guest {}", uuid);
guest.state = Some(GuestState {
status: GuestStatus::Started.into(),
network: Some(guestinfo_to_networkstate(&info)),
exit_info: None,
error_info: None,
host: self.glt.host_uuid().to_string(),
domid: info.domid,
});
success.store(true, Ordering::Release);
Ok(GuestReconcilerResult::Changed { rerun: false })
}
}
fn empty_vec_optional<T>(value: Vec<T>) -> Option<Vec<T>> {
if value.is_empty() {
None
} else {
Some(value)
}
}

View File

@ -1,8 +1,8 @@
[package]
name = "krata-guest"
description = "Guest services for the krata hypervisor."
description = "Guest services for the krata isolation engine"
license.workspace = true
version= "0.0.3"
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition = "2021"
@ -10,22 +10,24 @@ resolver = "2"
[dependencies]
anyhow = { workspace = true }
cgroups-rs = { workspace = true }
env_logger = { workspace = true }
futures = { workspace = true }
ipnetwork = { workspace = true }
krata = { path = "../krata", version = "^0.0.3" }
krata-xenstore = { path = "../xen/xenstore", version = "^0.0.3" }
krata = { path = "../krata", version = "^0.0.12" }
krata-xenstore = { path = "../xen/xenstore", version = "^0.0.12" }
libc = { workspace = true }
log = { workspace = true }
nix = { workspace = true, features = ["ioctl", "process", "fs"] }
oci-spec = { workspace = true }
path-absolutize = { workspace = true }
platform-info = { workspace = true }
rtnetlink = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sys-mount = { workspace = true }
sysinfo = { workspace = true }
tokio = { workspace = true }
walkdir = { workspace = true }
[lib]
name = "krataguest"

View File

@ -23,6 +23,8 @@ async fn main() -> Result<()> {
if let Err(error) = guest.init().await {
error!("failed to initialize guest: {}", error);
death(127).await?;
return Ok(());
}
death(1).await?;
Ok(())
}

View File

@ -1,42 +1,91 @@
use crate::{
childwait::{ChildEvent, ChildWait},
death,
exec::GuestExecTask,
metrics::MetricsCollector,
};
use anyhow::Result;
use cgroups_rs::Cgroup;
use krata::idm::{
client::IdmClient,
protocol::{idm_event::Event, IdmEvent, IdmExitEvent, IdmPacket},
client::{IdmClientStreamResponseHandle, IdmInternalClient},
internal::{
event::Event as EventType, request::Request as RequestType,
response::Response as ResponseType, Event, ExecStreamResponseUpdate, ExitEvent,
MetricsResponse, PingResponse, Request, Response,
},
};
use log::error;
use log::debug;
use nix::unistd::Pid;
use tokio::select;
use tokio::{select, sync::broadcast};
pub struct GuestBackground {
idm: IdmClient,
idm: IdmInternalClient,
child: Pid,
_cgroup: Cgroup,
wait: ChildWait,
}
impl GuestBackground {
pub async fn new(idm: IdmClient, child: Pid) -> Result<GuestBackground> {
pub async fn new(
idm: IdmInternalClient,
cgroup: Cgroup,
child: Pid,
) -> Result<GuestBackground> {
Ok(GuestBackground {
idm,
child,
_cgroup: cgroup,
wait: ChildWait::new()?,
})
}
pub async fn run(&mut self) -> Result<()> {
let mut event_subscription = self.idm.subscribe().await?;
let mut requests_subscription = self.idm.requests().await?;
let mut request_streams_subscription = self.idm.request_streams().await?;
loop {
select! {
x = self.idm.receiver.recv() => match x {
Some(_packet) => {
x = event_subscription.recv() => match x {
Ok(_event) => {
},
None => {
error!("idm packet channel closed");
Err(broadcast::error::RecvError::Closed) => {
debug!("idm packet channel closed");
break;
},
_ => {
continue;
}
},
x = requests_subscription.recv() => match x {
Ok((id, request)) => {
self.handle_idm_request(id, request).await?;
},
Err(broadcast::error::RecvError::Closed) => {
debug!("idm packet channel closed");
break;
},
_ => {
continue;
}
},
x = request_streams_subscription.recv() => match x {
Ok(handle) => {
self.handle_idm_stream_request(handle).await?;
},
Err(broadcast::error::RecvError::Closed) => {
debug!("idm packet channel closed");
break;
},
_ => {
continue;
}
},
@ -51,14 +100,65 @@ impl GuestBackground {
Ok(())
}
async fn handle_idm_request(&mut self, id: u64, packet: Request) -> Result<()> {
match packet.request {
Some(RequestType::Ping(_)) => {
self.idm
.respond(
id,
Response {
response: Some(ResponseType::Ping(PingResponse {})),
},
)
.await?;
}
Some(RequestType::Metrics(_)) => {
let metrics = MetricsCollector::new()?;
let root = metrics.collect()?;
let response = Response {
response: Some(ResponseType::Metrics(MetricsResponse { root: Some(root) })),
};
self.idm.respond(id, response).await?;
}
_ => {}
}
Ok(())
}
async fn handle_idm_stream_request(
&mut self,
handle: IdmClientStreamResponseHandle<Request>,
) -> Result<()> {
if let Some(RequestType::ExecStream(_)) = &handle.initial.request {
tokio::task::spawn(async move {
let exec = GuestExecTask { handle };
if let Err(error) = exec.run().await {
let _ = exec
.handle
.respond(Response {
response: Some(ResponseType::ExecStream(ExecStreamResponseUpdate {
exited: true,
error: error.to_string(),
exit_code: -1,
stdout: vec![],
stderr: vec![],
})),
})
.await;
}
});
}
Ok(())
}
async fn child_event(&mut self, event: ChildEvent) -> Result<()> {
if event.pid == self.child {
self.idm
.sender
.send(IdmPacket {
event: Some(IdmEvent {
event: Some(Event::Exit(IdmExitEvent { code: event.status })),
}),
.emit(Event {
event: Some(EventType::Exit(ExitEvent { code: event.status })),
})
.await?;
death(event.status).await?;

172
crates/guest/src/exec.rs Normal file
View File

@ -0,0 +1,172 @@
use std::{collections::HashMap, process::Stdio};
use anyhow::{anyhow, Result};
use krata::idm::{
client::IdmClientStreamResponseHandle,
internal::{
exec_stream_request_update::Update, request::Request as RequestType,
ExecStreamResponseUpdate,
},
internal::{response::Response as ResponseType, Request, Response},
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
join,
process::Command,
};
pub struct GuestExecTask {
pub handle: IdmClientStreamResponseHandle<Request>,
}
impl GuestExecTask {
pub async fn run(&self) -> Result<()> {
let mut receiver = self.handle.take().await?;
let Some(ref request) = self.handle.initial.request else {
return Err(anyhow!("request was empty"));
};
let RequestType::ExecStream(update) = request else {
return Err(anyhow!("request was not an exec update"));
};
let Some(Update::Start(ref start)) = update.update else {
return Err(anyhow!("first request did not contain a start update"));
};
let mut cmd = start.command.clone();
if cmd.is_empty() {
return Err(anyhow!("command line was empty"));
}
let exe = cmd.remove(0);
let mut env = HashMap::new();
for entry in &start.environment {
env.insert(entry.key.clone(), entry.value.clone());
}
if !env.contains_key("PATH") {
env.insert(
"PATH".to_string(),
"/bin:/usr/bin:/usr/local/bin".to_string(),
);
}
let dir = if start.working_directory.is_empty() {
"/".to_string()
} else {
start.working_directory.clone()
};
let mut child = Command::new(exe)
.args(cmd)
.envs(env)
.current_dir(dir)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.map_err(|error| anyhow!("failed to spawn: {}", error))?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| anyhow!("stdin was missing"))?;
let mut stdout = child
.stdout
.take()
.ok_or_else(|| anyhow!("stdout was missing"))?;
let mut stderr = child
.stderr
.take()
.ok_or_else(|| anyhow!("stderr was missing"))?;
let stdout_handle = self.handle.clone();
let stdout_task = tokio::task::spawn(async move {
let mut stdout_buffer = vec![0u8; 8 * 1024];
loop {
let Ok(size) = stdout.read(&mut stdout_buffer).await else {
break;
};
if size > 0 {
let response = Response {
response: Some(ResponseType::ExecStream(ExecStreamResponseUpdate {
exited: false,
exit_code: 0,
error: String::new(),
stdout: stdout_buffer[0..size].to_vec(),
stderr: vec![],
})),
};
let _ = stdout_handle.respond(response).await;
} else {
break;
}
}
});
let stderr_handle = self.handle.clone();
let stderr_task = tokio::task::spawn(async move {
let mut stderr_buffer = vec![0u8; 8 * 1024];
loop {
let Ok(size) = stderr.read(&mut stderr_buffer).await else {
break;
};
if size > 0 {
let response = Response {
response: Some(ResponseType::ExecStream(ExecStreamResponseUpdate {
exited: false,
exit_code: 0,
error: String::new(),
stdout: vec![],
stderr: stderr_buffer[0..size].to_vec(),
})),
};
let _ = stderr_handle.respond(response).await;
} else {
break;
}
}
});
let stdin_task = tokio::task::spawn(async move {
loop {
let Some(request) = receiver.recv().await else {
break;
};
let Some(RequestType::ExecStream(update)) = request.request else {
continue;
};
let Some(Update::Stdin(update)) = update.update else {
continue;
};
if stdin.write_all(&update.data).await.is_err() {
break;
}
}
});
let exit = child.wait().await?;
let code = exit.code().unwrap_or(-1);
let _ = join!(stdout_task, stderr_task);
stdin_task.abort();
let response = Response {
response: Some(ResponseType::ExecStream(ExecStreamResponseUpdate {
exited: true,
exit_code: code,
error: String::new(),
stdout: vec![],
stderr: vec![],
})),
};
self.handle.respond(response).await?;
Ok(())
}
}

View File

@ -1,29 +1,30 @@
use anyhow::{anyhow, Result};
use cgroups_rs::{Cgroup, CgroupPid};
use futures::stream::TryStreamExt;
use ipnetwork::IpNetwork;
use krata::ethtool::EthtoolHandle;
use krata::idm::client::IdmClient;
use krata::launchcfg::{LaunchInfo, LaunchNetwork};
use libc::{setsid, TIOCSCTTY};
use krata::idm::client::IdmInternalClient;
use krata::idm::internal::INTERNAL_IDM_CHANNEL;
use krata::launchcfg::{LaunchInfo, LaunchNetwork, LaunchPackedFormat};
use libc::{sethostname, setsid, TIOCSCTTY};
use log::{trace, warn};
use nix::ioctl_write_int_bad;
use nix::unistd::{dup2, execve, fork, ForkResult, Pid};
use oci_spec::image::{Config, ImageConfiguration};
use path_absolutize::Absolutize;
use platform_info::{PlatformInfo, PlatformInfoAPI, UNameAPI};
use std::collections::HashMap;
use std::ffi::CString;
use std::fs::{File, OpenOptions, Permissions};
use std::io;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::os::fd::AsRawFd;
use std::os::linux::fs::MetadataExt;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{chroot, symlink, PermissionsExt};
use std::os::unix::fs::{chroot, PermissionsExt};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use sys_mount::{FilesystemType, Mount, MountFlags};
use tokio::fs;
use walkdir::WalkDir;
use crate::background::GuestBackground;
@ -50,6 +51,10 @@ const NEW_ROOT_DEV_PATH: &str = "/newroot/dev";
const IMAGE_CONFIG_JSON_PATH: &str = "/config/image/config.json";
const LAUNCH_CONFIG_JSON_PATH: &str = "/config/launch.json";
const ADDONS_DEVICE_PATH: &str = "/dev/xvdc";
const ADDONS_MOUNT_PATH: &str = "/addons";
const ADDONS_MODULES_PATH: &str = "/addons/modules";
ioctl_write_int_bad!(set_controlling_terminal, TIOCSCTTY);
pub struct GuestInit {}
@ -78,21 +83,45 @@ impl GuestInit {
Err(error) => warn!("failed to open console: {}", error),
};
let idm = IdmClient::open("/dev/hvc1")
let idm = IdmInternalClient::open(INTERNAL_IDM_CHANNEL, "/dev/hvc1")
.await
.map_err(|x| anyhow!("failed to open idm client: {}", x))?;
self.mount_squashfs_images().await?;
self.mount_config_image().await?;
let config = self.parse_image_config().await?;
let launch = self.parse_launch_config().await?;
self.mount_root_image(launch.root.format.clone()).await?;
self.mount_addons().await?;
self.mount_new_root().await?;
self.nuke_initrd().await?;
self.mount_kernel_modules().await?;
self.bind_new_root().await?;
if let Some(hostname) = launch.hostname.clone() {
let result = unsafe {
sethostname(
hostname.as_bytes().as_ptr() as *mut libc::c_char,
hostname.len(),
)
};
if result != 0 {
warn!("failed to set hostname: {}", result);
}
let etc = PathBuf::from_str("/etc")?;
if !etc.exists() {
fs::create_dir(&etc).await?;
}
let mut etc_hostname = etc;
etc_hostname.push("hostname");
fs::write(&etc_hostname, hostname + "\n").await?;
}
if let Some(network) = &launch.network {
trace!("initializing network");
if let Err(error) = self.network_setup(network).await {
if let Err(error) = self.network_setup(&launch, network).await {
warn!("failed to initialize network: {}", error);
}
}
@ -112,14 +141,64 @@ impl GuestInit {
trace!("early init");
self.create_dir("/dev", Some(0o0755)).await?;
self.create_dir("/proc", None).await?;
self.create_dir("/sys", None).await?;
self.create_dir("/sys", Some(0o0555)).await?;
self.create_dir("/root", Some(0o0700)).await?;
self.create_dir("/tmp", None).await?;
self.mount_kernel_fs("devtmpfs", "/dev", "mode=0755")
self.create_dir("/run", Some(0o0755)).await?;
self.mount_kernel_fs("devtmpfs", "/dev", "mode=0755", None, None)
.await?;
self.mount_kernel_fs("proc", "/proc", "").await?;
self.mount_kernel_fs("sysfs", "/sys", "").await?;
symlink("/proc/self/fd", "/dev/fd")?;
self.mount_kernel_fs("proc", "/proc", "", None, None)
.await?;
self.mount_kernel_fs("sysfs", "/sys", "", None, None)
.await?;
self.create_dir("/dev/pts", Some(0o0755)).await?;
self.mount_kernel_fs("devpts", "/dev/pts", "", None, Some("/dev/ptmx"))
.await?;
fs::symlink("/proc/self/fd", "/dev/fd").await?;
fs::symlink("/proc/self/fd/0", "/dev/stdin").await?;
fs::symlink("/proc/self/fd/1", "/dev/stdout").await?;
fs::symlink("/proc/self/fd/2", "/dev/stderr").await?;
self.mount_kernel_fs(
"cgroup2",
"/sys/fs/cgroup",
"",
Some(MountFlags::RELATIME),
None,
)
.await?;
Ok(())
}
async fn mount_addons(&mut self) -> Result<()> {
if !fs::try_exists(ADDONS_DEVICE_PATH).await? {
return Ok(());
}
self.mount_image(
&PathBuf::from(ADDONS_DEVICE_PATH),
&PathBuf::from(ADDONS_MOUNT_PATH),
LaunchPackedFormat::Squashfs,
)
.await?;
Ok(())
}
async fn mount_kernel_modules(&mut self) -> Result<()> {
if !fs::try_exists(ADDONS_MODULES_PATH).await? {
return Ok(());
}
let Some(platform_info) = PlatformInfo::new().ok() else {
return Ok(());
};
let kernel_release = platform_info.release().to_string_lossy().to_string();
let modules_path = format!("/newroot/lib/modules/{}", kernel_release);
fs::create_dir_all(&modules_path).await?;
Mount::builder()
.fstype(FilesystemType::Manual("none"))
.flags(MountFlags::BIND | MountFlags::RDONLY)
.mount(ADDONS_MODULES_PATH, modules_path)?;
Ok(())
}
@ -137,16 +216,20 @@ impl GuestInit {
Ok(())
}
async fn mount_kernel_fs(&mut self, fstype: &str, path: &str, data: &str) -> Result<()> {
let metadata = fs::metadata(path).await?;
if metadata.st_dev() == fs::metadata("/").await?.st_dev() {
trace!("mounting kernel fs {} to {}", fstype, path);
Mount::builder()
.fstype(FilesystemType::Manual(fstype))
.flags(MountFlags::NOEXEC | MountFlags::NOSUID)
.data(data)
.mount(fstype, path)?;
}
async fn mount_kernel_fs(
&mut self,
fstype: &str,
path: &str,
data: &str,
flags: Option<MountFlags>,
source: Option<&str>,
) -> Result<()> {
trace!("mounting kernel fs {} to {}", fstype, path);
Mount::builder()
.fstype(FilesystemType::Manual(fstype))
.flags(MountFlags::NOEXEC | MountFlags::NOSUID | flags.unwrap_or(MountFlags::empty()))
.data(data)
.mount(source.unwrap_or(fstype), path)?;
Ok(())
}
@ -158,24 +241,41 @@ impl GuestInit {
Ok(())
}
async fn mount_squashfs_images(&mut self) -> Result<()> {
trace!("mounting squashfs images");
let image_mount_path = Path::new(IMAGE_MOUNT_PATH);
async fn mount_config_image(&mut self) -> Result<()> {
trace!("mounting config image");
let config_mount_path = Path::new(CONFIG_MOUNT_PATH);
self.mount_squashfs(Path::new(IMAGE_BLOCK_DEVICE_PATH), image_mount_path)
.await?;
self.mount_squashfs(Path::new(CONFIG_BLOCK_DEVICE_PATH), config_mount_path)
self.mount_image(
Path::new(CONFIG_BLOCK_DEVICE_PATH),
config_mount_path,
LaunchPackedFormat::Squashfs,
)
.await?;
Ok(())
}
async fn mount_root_image(&mut self, format: LaunchPackedFormat) -> Result<()> {
trace!("mounting root image");
let image_mount_path = Path::new(IMAGE_MOUNT_PATH);
self.mount_image(Path::new(IMAGE_BLOCK_DEVICE_PATH), image_mount_path, format)
.await?;
Ok(())
}
async fn mount_squashfs(&mut self, from: &Path, to: &Path) -> Result<()> {
trace!("mounting squashfs image {:?} to {:?}", from, to);
async fn mount_image(
&mut self,
from: &Path,
to: &Path,
format: LaunchPackedFormat,
) -> Result<()> {
trace!("mounting {:?} image {:?} to {:?}", format, from, to);
if !to.is_dir() {
fs::create_dir(to).await?;
}
Mount::builder()
.fstype(FilesystemType::Manual("squashfs"))
.fstype(FilesystemType::Manual(match format {
LaunchPackedFormat::Squashfs => "squashfs",
LaunchPackedFormat::Erofs => "erofs",
}))
.flags(MountFlags::RDONLY)
.mount(from, to)?;
Ok(())
@ -249,40 +349,6 @@ impl GuestInit {
Ok(serde_json::from_str(&content)?)
}
async fn nuke_initrd(&mut self) -> Result<()> {
trace!("nuking initrd");
let initrd_dev = fs::metadata("/").await?.st_dev();
for item in WalkDir::new("/")
.same_file_system(true)
.follow_links(false)
.contents_first(true)
{
if item.is_err() {
continue;
}
let item = item?;
let metadata = match item.metadata() {
Ok(value) => value,
Err(_) => continue,
};
if metadata.st_dev() != initrd_dev {
continue;
}
if metadata.is_symlink() || metadata.is_file() {
let _ = fs::remove_file(item.path()).await;
trace!("deleting file {:?}", item.path());
} else if metadata.is_dir() {
let _ = fs::remove_dir(item.path()).await;
trace!("deleting directory {:?}", item.path());
}
}
trace!("nuked initrd");
Ok(())
}
async fn bind_new_root(&mut self) -> Result<()> {
self.mount_move_subtree(Path::new(SYS_PATH), Path::new(NEW_ROOT_SYS_PATH))
.await?;
@ -302,7 +368,7 @@ impl GuestInit {
Ok(())
}
async fn network_setup(&mut self, network: &LaunchNetwork) -> Result<()> {
async fn network_setup(&mut self, cfg: &LaunchInfo, network: &LaunchNetwork) -> Result<()> {
trace!("setting up network for link");
let etc = PathBuf::from_str("/etc")?;
@ -310,14 +376,33 @@ impl GuestInit {
fs::create_dir(etc).await?;
}
let resolv = PathBuf::from_str("/etc/resolv.conf")?;
let mut lines = vec!["# krata resolver configuration".to_string()];
for nameserver in &network.resolver.nameservers {
lines.push(format!("nameserver {}", nameserver));
{
let mut lines = vec!["# krata resolver configuration".to_string()];
for nameserver in &network.resolver.nameservers {
lines.push(format!("nameserver {}", nameserver));
}
let mut conf = lines.join("\n");
conf.push('\n');
fs::write(resolv, conf).await?;
}
let hosts = PathBuf::from_str("/etc/hosts")?;
if let Some(ref hostname) = cfg.hostname {
let mut lines = if hosts.exists() {
fs::read_to_string(&hosts)
.await?
.lines()
.map(|x| x.to_string())
.collect::<Vec<_>>()
} else {
vec!["127.0.0.1 localhost".to_string()]
};
lines.push(format!("127.0.1.1 {}", hostname));
fs::write(&hosts, lines.join("\n") + "\n").await?;
}
let mut conf = lines.join("\n");
conf.push('\n');
fs::write(resolv, conf).await?;
self.network_configure_ethtool(network).await?;
self.network_configure_link(network).await?;
Ok(())
@ -327,6 +412,14 @@ impl GuestInit {
let (connection, handle, _) = rtnetlink::new_connection()?;
tokio::spawn(connection);
let mut links = handle.link().get().match_name("lo".to_string()).execute();
let Some(link) = links.try_next().await? else {
warn!("unable to find link named lo");
return Ok(());
};
handle.link().set(link.header.index).up().execute().await?;
let ipv4_network: IpNetwork = network.ipv4.address.parse()?;
let ipv4_gateway: Ipv4Addr = network.ipv4.gateway.parse()?;
let ipv6_network: IpNetwork = network.ipv6.address.parse()?;
@ -399,14 +492,19 @@ impl GuestInit {
Ok(())
}
async fn run(&mut self, config: &Config, launch: &LaunchInfo, idm: IdmClient) -> Result<()> {
async fn run(
&mut self,
config: &Config,
launch: &LaunchInfo,
idm: IdmInternalClient,
) -> Result<()> {
let mut cmd = match config.cmd() {
None => vec![],
Some(value) => value.clone(),
};
if launch.run.is_some() {
cmd = launch.run.as_ref().unwrap().clone();
cmd.clone_from(launch.run.as_ref().unwrap());
}
if let Some(entrypoint) = config.entrypoint() {
@ -427,9 +525,14 @@ impl GuestInit {
}
env.extend(launch.env.clone());
env.insert("KRATA_CONTAINER".to_string(), "1".to_string());
env.insert("TERM".to_string(), "vt100".to_string());
let path = GuestInit::resolve_executable(&env, path.into())?;
// If we were not provided a terminal definition in our launch manifest, we
// default to xterm as most terminal emulators support the xterm control codes.
if !env.contains_key("TERM") {
env.insert("TERM".to_string(), "xterm".to_string());
}
let path = resolve_executable(&env, path.into())?;
let Some(file_name) = path.file_name() else {
return Err(anyhow!("cannot get file name of command path"));
};
@ -454,10 +557,21 @@ impl GuestInit {
working_dir = "/".to_string();
}
self.fork_and_exec(idm, working_dir, path, cmd, env).await?;
let cgroup = self.init_cgroup().await?;
self.fork_and_exec(idm, cgroup, working_dir, path, cmd, env)
.await?;
Ok(())
}
async fn init_cgroup(&self) -> Result<Cgroup> {
trace!("initializing cgroup");
let hierarchy = cgroups_rs::hierarchies::auto();
let cgroup = Cgroup::new(hierarchy, "krata-guest-task")?;
cgroup.set_cgroup_type("threaded")?;
trace!("initialized cgroup");
Ok(cgroup)
}
fn strings_as_cstrings(values: Vec<String>) -> Result<Vec<CString>> {
let mut results: Vec<CString> = vec![];
for value in values {
@ -476,27 +590,6 @@ impl GuestInit {
map
}
fn resolve_executable(env: &HashMap<String, String>, path: PathBuf) -> Result<PathBuf> {
if path.is_absolute() {
return Ok(path);
}
if path.is_file() {
return Ok(path.absolutize()?.to_path_buf());
}
if let Some(path_var) = env.get("PATH") {
for item in path_var.split(':') {
let mut exe_path: PathBuf = item.into();
exe_path.push(&path);
if exe_path.is_file() {
return Ok(exe_path);
}
}
}
Ok(path)
}
fn env_list(env: HashMap<String, String>) -> Vec<String> {
env.iter()
.map(|(key, value)| format!("{}={}", key, value))
@ -505,20 +598,22 @@ impl GuestInit {
async fn fork_and_exec(
&mut self,
idm: IdmClient,
idm: IdmInternalClient,
cgroup: Cgroup,
working_dir: String,
path: CString,
cmd: Vec<CString>,
env: Vec<CString>,
) -> Result<()> {
match unsafe { fork()? } {
ForkResult::Parent { child } => self.background(idm, child).await,
ForkResult::Child => self.foreground(working_dir, path, cmd, env).await,
ForkResult::Parent { child } => self.background(idm, cgroup, child).await,
ForkResult::Child => self.foreground(cgroup, working_dir, path, cmd, env).await,
}
}
async fn foreground(
&mut self,
cgroup: Cgroup,
working_dir: String,
path: CString,
cmd: Vec<CString>,
@ -526,6 +621,7 @@ impl GuestInit {
) -> Result<()> {
GuestInit::set_controlling_terminal()?;
std::env::set_current_dir(working_dir)?;
cgroup.add_task(CgroupPid::from(std::process::id() as u64))?;
execve(&path, &cmd, &env)?;
Ok(())
}
@ -538,9 +634,35 @@ impl GuestInit {
Ok(())
}
async fn background(&mut self, idm: IdmClient, executed: Pid) -> Result<()> {
let mut background = GuestBackground::new(idm, executed).await?;
async fn background(
&mut self,
idm: IdmInternalClient,
cgroup: Cgroup,
executed: Pid,
) -> Result<()> {
let mut background = GuestBackground::new(idm, cgroup, executed).await?;
background.run().await?;
Ok(())
}
}
pub fn resolve_executable(env: &HashMap<String, String>, path: PathBuf) -> Result<PathBuf> {
if path.is_absolute() {
return Ok(path);
}
if path.is_file() {
return Ok(path.absolutize()?.to_path_buf());
}
if let Some(path_var) = env.get("PATH") {
for item in path_var.split(':') {
let mut exe_path: PathBuf = item.into();
exe_path.push(&path);
if exe_path.is_file() {
return Ok(exe_path);
}
}
}
Ok(path)
}

View File

@ -6,7 +6,9 @@ use xenstore::{XsdClient, XsdInterface};
pub mod background;
pub mod childwait;
pub mod exec;
pub mod init;
pub mod metrics;
pub async fn death(code: c_int) -> Result<()> {
let store = XsdClient::open().await?;

118
crates/guest/src/metrics.rs Normal file
View File

@ -0,0 +1,118 @@
use std::{ops::Add, path::Path};
use anyhow::Result;
use krata::idm::internal::{MetricFormat, MetricNode};
use sysinfo::Process;
pub struct MetricsCollector {}
impl MetricsCollector {
pub fn new() -> Result<Self> {
Ok(MetricsCollector {})
}
pub fn collect(&self) -> Result<MetricNode> {
let mut sysinfo = sysinfo::System::new();
Ok(MetricNode::structural(
"guest",
vec![
self.collect_system(&mut sysinfo)?,
self.collect_processes(&mut sysinfo)?,
],
))
}
fn collect_system(&self, sysinfo: &mut sysinfo::System) -> Result<MetricNode> {
sysinfo.refresh_memory();
Ok(MetricNode::structural(
"system",
vec![MetricNode::structural(
"memory",
vec![
MetricNode::value("total", sysinfo.total_memory(), MetricFormat::Bytes),
MetricNode::value("used", sysinfo.used_memory(), MetricFormat::Bytes),
MetricNode::value("free", sysinfo.free_memory(), MetricFormat::Bytes),
],
)],
))
}
fn collect_processes(&self, sysinfo: &mut sysinfo::System) -> Result<MetricNode> {
sysinfo.refresh_processes();
let mut processes = Vec::new();
let mut sysinfo_processes = sysinfo.processes().values().collect::<Vec<_>>();
sysinfo_processes.sort_by_key(|x| x.pid());
for process in sysinfo_processes {
if process.thread_kind().is_some() {
continue;
}
processes.push(MetricsCollector::process_node(process)?);
}
Ok(MetricNode::structural("process", processes))
}
fn process_node(process: &Process) -> Result<MetricNode> {
let mut metrics = vec![];
if let Some(parent) = process.parent() {
metrics.push(MetricNode::value(
"parent",
parent.as_u32() as u64,
MetricFormat::Integer,
));
}
if let Some(exe) = process.exe().and_then(path_as_str) {
metrics.push(MetricNode::raw_value("executable", exe));
}
if let Some(working_directory) = process.cwd().and_then(path_as_str) {
metrics.push(MetricNode::raw_value("cwd", working_directory));
}
let cmdline = process.cmd().to_vec();
metrics.push(MetricNode::raw_value("cmdline", cmdline));
metrics.push(MetricNode::structural(
"memory",
vec![
MetricNode::value("resident", process.memory(), MetricFormat::Bytes),
MetricNode::value("virtual", process.virtual_memory(), MetricFormat::Bytes),
],
));
metrics.push(MetricNode::value(
"lifetime",
process.run_time(),
MetricFormat::DurationSeconds,
));
metrics.push(MetricNode::value(
"uid",
process.user_id().map(|x| (*x).add(0)).unwrap_or(0) as f64,
MetricFormat::Integer,
));
metrics.push(MetricNode::value(
"gid",
process.group_id().map(|x| (*x).add(0)).unwrap_or(0) as f64,
MetricFormat::Integer,
));
metrics.push(MetricNode::value(
"euid",
process
.effective_user_id()
.map(|x| (*x).add(0))
.unwrap_or(0) as f64,
MetricFormat::Integer,
));
metrics.push(MetricNode::value(
"egid",
process.effective_group_id().map(|x| x.add(0)).unwrap_or(0) as f64,
MetricFormat::Integer,
));
Ok(MetricNode::structural(process.pid().to_string(), metrics))
}
}
fn path_as_str(path: &Path) -> Option<String> {
String::from_utf8(path.as_os_str().as_encoded_bytes().to_vec()).ok()
}

View File

@ -1,8 +1,8 @@
[package]
name = "krata"
description = "Client library and common services for the krata hypervisor."
description = "Client library and common services for the krata isolation engine"
license.workspace = true
version= "0.0.3"
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition = "2021"
@ -10,12 +10,15 @@ resolver = "2"
[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
bytes = { workspace = true }
libc = { workspace = true }
log = { workspace = true }
once_cell = { workspace = true }
prost = { workspace = true }
prost-reflect = { workspace = true }
prost-types = { workspace = true }
scopeguard = { workspace = true }
serde = { workspace = true }
tonic = { workspace = true }
tokio = { workspace = true }

View File

@ -8,7 +8,8 @@ fn main() -> Result<()> {
&mut config,
&[
"proto/krata/v1/control.proto",
"proto/krata/internal/idm.proto",
"proto/krata/idm/transport.proto",
"proto/krata/idm/internal.proto",
],
&["proto/"],
)?;
@ -16,7 +17,8 @@ fn main() -> Result<()> {
config,
&[
"proto/krata/v1/control.proto",
"proto/krata/internal/idm.proto",
"proto/krata/idm/transport.proto",
"proto/krata/idm/internal.proto",
],
&["proto/"],
)?;

View File

@ -0,0 +1,89 @@
syntax = "proto3";
package krata.idm.internal;
option java_multiple_files = true;
option java_package = "dev.krata.proto.idm.internal";
option java_outer_classname = "IdmInternalProto";
import "google/protobuf/struct.proto";
message ExitEvent {
int32 code = 1;
}
message PingRequest {}
message PingResponse {}
message MetricsRequest {}
message MetricsResponse {
MetricNode root = 1;
}
message MetricNode {
string name = 1;
google.protobuf.Value value = 2;
MetricFormat format = 3;
repeated MetricNode children = 4;
}
enum MetricFormat {
METRIC_FORMAT_UNKNOWN = 0;
METRIC_FORMAT_BYTES = 1;
METRIC_FORMAT_INTEGER = 2;
METRIC_FORMAT_DURATION_SECONDS = 3;
}
message ExecEnvVar {
string key = 1;
string value = 2;
}
message ExecStreamRequestStart {
repeated ExecEnvVar environment = 1;
repeated string command = 2;
string working_directory = 3;
}
message ExecStreamRequestStdin {
bytes data = 1;
}
message ExecStreamRequestUpdate {
oneof update {
ExecStreamRequestStart start = 1;
ExecStreamRequestStdin stdin = 2;
}
}
message ExecStreamResponseUpdate {
bool exited = 1;
string error = 2;
int32 exit_code = 3;
bytes stdout = 4;
bytes stderr = 5;
}
message Event {
oneof event {
ExitEvent exit = 1;
}
}
message Request {
oneof request {
PingRequest ping = 1;
MetricsRequest metrics = 2;
ExecStreamRequestUpdate exec_stream = 3;
}
}
message Response {
oneof response {
PingResponse ping = 1;
MetricsResponse metrics = 2;
ExecStreamResponseUpdate exec_stream = 3;
}
}

View File

@ -0,0 +1,27 @@
syntax = "proto3";
package krata.idm.transport;
option java_multiple_files = true;
option java_package = "dev.krata.proto.idm.transport";
option java_outer_classname = "IdmTransportProto";
message IdmTransportPacket {
uint64 id = 1;
uint64 channel = 2;
IdmTransportPacketForm form = 3;
bytes data = 4;
}
enum IdmTransportPacketForm {
IDM_TRANSPORT_PACKET_FORM_UNKNOWN = 0;
IDM_TRANSPORT_PACKET_FORM_RAW = 1;
IDM_TRANSPORT_PACKET_FORM_EVENT = 2;
IDM_TRANSPORT_PACKET_FORM_REQUEST = 3;
IDM_TRANSPORT_PACKET_FORM_RESPONSE = 4;
IDM_TRANSPORT_PACKET_FORM_STREAM_REQUEST = 5;
IDM_TRANSPORT_PACKET_FORM_STREAM_REQUEST_UPDATE = 6;
IDM_TRANSPORT_PACKET_FORM_STREAM_RESPONSE_UPDATE = 7;
IDM_TRANSPORT_PACKET_FORM_STREAM_REQUEST_CLOSED = 8;
IDM_TRANSPORT_PACKET_FORM_STREAM_RESPONSE_CLOSED = 9;
}

View File

@ -1,21 +0,0 @@
syntax = "proto3";
package krata.internal.idm;
option java_multiple_files = true;
option java_package = "dev.krata.proto.internal.idm";
option java_outer_classname = "IdmProto";
message IdmExitEvent {
int32 code = 1;
}
message IdmEvent {
oneof event {
IdmExitEvent exit = 1;
}
}
message IdmPacket {
IdmEvent event = 1;
}

View File

@ -6,6 +6,8 @@ option java_multiple_files = true;
option java_package = "dev.krata.proto.v1.common";
option java_outer_classname = "CommonProto";
import "google/protobuf/struct.proto";
message Guest {
string id = 1;
GuestSpec spec = 2;
@ -15,10 +17,15 @@ message Guest {
message GuestSpec {
string name = 1;
GuestImageSpec image = 2;
uint32 vcpus = 3;
uint64 mem = 4;
GuestTaskSpec task = 5;
repeated GuestSpecAnnotation annotations = 6;
// If not specified, defaults to the daemon default kernel.
GuestImageSpec kernel = 3;
// If not specified, defaults to the daemon default initrd.
GuestImageSpec initrd = 4;
uint32 vcpus = 5;
uint64 mem = 6;
GuestTaskSpec task = 7;
repeated GuestSpecAnnotation annotations = 8;
repeated GuestSpecDevice devices = 9;
}
message GuestImageSpec {
@ -27,13 +34,23 @@ message GuestImageSpec {
}
}
enum OciImageFormat {
OCI_IMAGE_FORMAT_UNKNOWN = 0;
OCI_IMAGE_FORMAT_SQUASHFS = 1;
OCI_IMAGE_FORMAT_EROFS = 2;
// Tar format is not launchable, and is intended for kernel images.
OCI_IMAGE_FORMAT_TAR = 3;
}
message GuestOciImageSpec {
string image = 1;
string digest = 1;
OciImageFormat format = 2;
}
message GuestTaskSpec {
repeated GuestTaskSpecEnvVar environment = 1;
repeated string command = 2;
string working_directory = 3;
}
message GuestTaskSpecEnvVar {
@ -46,12 +63,17 @@ message GuestSpecAnnotation {
string value = 2;
}
message GuestSpecDevice {
string name = 1;
}
message GuestState {
GuestStatus status = 1;
GuestNetworkState network = 2;
GuestExitInfo exit_info = 3;
GuestErrorInfo error_info = 4;
uint32 domid = 5;
string host = 5;
uint32 domid = 6;
}
enum GuestStatus {
@ -80,3 +102,17 @@ message GuestExitInfo {
message GuestErrorInfo {
string message = 1;
}
message GuestMetricNode {
string name = 1;
google.protobuf.Value value = 2;
GuestMetricFormat format = 3;
repeated GuestMetricNode children = 4;
}
enum GuestMetricFormat {
GUEST_METRIC_FORMAT_UNKNOWN = 0;
GUEST_METRIC_FORMAT_BYTES = 1;
GUEST_METRIC_FORMAT_INTEGER = 2;
GUEST_METRIC_FORMAT_DURATION_SECONDS = 3;
}

View File

@ -6,15 +6,38 @@ option java_multiple_files = true;
option java_package = "dev.krata.proto.v1.control";
option java_outer_classname = "ControlProto";
import "krata/idm/transport.proto";
import "krata/v1/common.proto";
service ControlService {
rpc IdentifyHost(IdentifyHostRequest) returns (IdentifyHostReply);
rpc CreateGuest(CreateGuestRequest) returns (CreateGuestReply);
rpc DestroyGuest(DestroyGuestRequest) returns (DestroyGuestReply);
rpc ResolveGuest(ResolveGuestRequest) returns (ResolveGuestReply);
rpc ListGuests(ListGuestsRequest) returns (ListGuestsReply);
rpc ListDevices(ListDevicesRequest) returns (ListDevicesReply);
rpc ExecGuest(stream ExecGuestRequest) returns (stream ExecGuestReply);
rpc ConsoleData(stream ConsoleDataRequest) returns (stream ConsoleDataReply);
rpc ReadGuestMetrics(ReadGuestMetricsRequest) returns (ReadGuestMetricsReply);
rpc SnoopIdm(SnoopIdmRequest) returns (stream SnoopIdmReply);
rpc WatchEvents(WatchEventsRequest) returns (stream WatchEventsReply);
rpc PullImage(PullImageRequest) returns (stream PullImageReply);
rpc GetHostCpuTopology(HostCpuTopologyRequest) returns (HostCpuTopologyReply);
rpc SetHostPowerManagementPolicy(HostPowerManagementPolicy) returns (HostPowerManagementPolicy);
}
message IdentifyHostRequest {}
message IdentifyHostReply {
string host_uuid = 1;
uint32 host_domid = 2;
string krata_version = 3;
}
message CreateGuestRequest {
@ -45,6 +68,20 @@ message ListGuestsReply {
repeated krata.v1.common.Guest guests = 1;
}
message ExecGuestRequest {
string guest_id = 1;
krata.v1.common.GuestTaskSpec task = 2;
bytes data = 3;
}
message ExecGuestReply {
bool exited = 1;
string error = 2;
int32 exit_code = 3;
bytes stdout = 4;
bytes stderr = 5;
}
message ConsoleDataRequest {
string guest_id = 1;
bytes data = 2;
@ -65,3 +102,131 @@ message WatchEventsReply {
message GuestChangedEvent {
krata.v1.common.Guest guest = 1;
}
message ReadGuestMetricsRequest {
string guest_id = 1;
}
message ReadGuestMetricsReply {
krata.v1.common.GuestMetricNode root = 1;
}
message SnoopIdmRequest {}
message SnoopIdmReply {
string from = 1;
string to = 2;
krata.idm.transport.IdmTransportPacket packet = 3;
}
message ImageProgress {
ImageProgressPhase phase = 1;
repeated ImageProgressLayer layers = 2;
ImageProgressIndication indication = 3;
}
enum ImageProgressPhase {
IMAGE_PROGRESS_PHASE_UNKNOWN = 0;
IMAGE_PROGRESS_PHASE_STARTED = 1;
IMAGE_PROGRESS_PHASE_RESOLVING = 2;
IMAGE_PROGRESS_PHASE_RESOLVED = 3;
IMAGE_PROGRESS_PHASE_CONFIG_DOWNLOAD = 4;
IMAGE_PROGRESS_PHASE_LAYER_DOWNLOAD = 5;
IMAGE_PROGRESS_PHASE_ASSEMBLE = 6;
IMAGE_PROGRESS_PHASE_PACK = 7;
IMAGE_PROGRESS_PHASE_COMPLETE = 8;
}
message ImageProgressLayer {
string id = 1;
ImageProgressLayerPhase phase = 2;
ImageProgressIndication indication = 3;
}
enum ImageProgressLayerPhase {
IMAGE_PROGRESS_LAYER_PHASE_UNKNOWN = 0;
IMAGE_PROGRESS_LAYER_PHASE_WAITING = 1;
IMAGE_PROGRESS_LAYER_PHASE_DOWNLOADING = 2;
IMAGE_PROGRESS_LAYER_PHASE_DOWNLOADED = 3;
IMAGE_PROGRESS_LAYER_PHASE_EXTRACTING = 4;
IMAGE_PROGRESS_LAYER_PHASE_EXTRACTED = 5;
}
message ImageProgressIndication {
oneof indication {
ImageProgressIndicationBar bar = 1;
ImageProgressIndicationSpinner spinner = 2;
ImageProgressIndicationHidden hidden = 3;
ImageProgressIndicationCompleted completed = 4;
}
}
message ImageProgressIndicationBar {
string message = 1;
uint64 current = 2;
uint64 total = 3;
bool is_bytes = 4;
}
message ImageProgressIndicationSpinner {
string message = 1;
}
message ImageProgressIndicationHidden {}
message ImageProgressIndicationCompleted {
string message = 1;
uint64 total = 2;
bool is_bytes = 3;
}
message PullImageRequest {
string image = 1;
krata.v1.common.OciImageFormat format = 2;
bool overwrite_cache = 3;
}
message PullImageReply {
ImageProgress progress = 1;
string digest = 2;
krata.v1.common.OciImageFormat format = 3;
}
message DeviceInfo {
string name = 1;
bool claimed = 2;
string owner = 3;
}
message ListDevicesRequest {}
message ListDevicesReply {
repeated DeviceInfo devices = 1;
}
enum HostCpuTopologyClass {
CPU_CLASS_STANDARD = 0;
CPU_CLASS_PERFORMANCE = 1;
CPU_CLASS_EFFICIENCY = 2;
}
message HostCpuTopologyInfo {
uint32 core = 1;
uint32 socket = 2;
uint32 node = 3;
uint32 thread = 4;
HostCpuTopologyClass class = 5;
}
message HostCpuTopologyRequest {}
message HostCpuTopologyReply {
repeated HostCpuTopologyInfo cpus = 1;
}
message HostPowerManagementPolicyRequest {}
message HostPowerManagementPolicy {
string scheduler = 1;
bool smt_awareness = 2;
}

View File

@ -1,11 +1,14 @@
use std::sync::Arc;
use std::{sync::Arc, time::Duration};
use crate::v1::control::{watch_events_reply::Event, WatchEventsReply};
use crate::v1::control::{
control_service_client::ControlServiceClient, watch_events_reply::Event, WatchEventsReply,
WatchEventsRequest,
};
use anyhow::Result;
use log::trace;
use tokio::{sync::broadcast, task::JoinHandle};
use log::{error, trace, warn};
use tokio::{sync::broadcast, task::JoinHandle, time::sleep};
use tokio_stream::StreamExt;
use tonic::Streaming;
use tonic::{transport::Channel, Streaming};
#[derive(Clone)]
pub struct EventStream {
@ -14,27 +17,12 @@ pub struct EventStream {
}
impl EventStream {
pub async fn open(mut events: Streaming<WatchEventsReply>) -> Result<Self> {
pub async fn open(client: ControlServiceClient<Channel>) -> Result<Self> {
let (sender, _) = broadcast::channel(1000);
let emit = sender.clone();
let task = tokio::task::spawn(async move {
loop {
let Some(result) = events.next().await else {
break;
};
let reply = match result {
Ok(reply) => reply,
Err(error) => {
trace!("event stream processing failed: {}", error);
break;
}
};
let Some(event) = reply.event else {
continue;
};
let _ = emit.send(event);
if let Err(error) = EventStream::process(client, emit).await {
error!("failed to process event stream: {}", error);
}
});
Ok(Self {
@ -43,6 +31,48 @@ impl EventStream {
})
}
async fn process(
mut client: ControlServiceClient<Channel>,
emit: broadcast::Sender<Event>,
) -> Result<()> {
let mut events: Option<Streaming<WatchEventsReply>> = None;
loop {
let mut stream = match events {
Some(stream) => stream,
None => {
let result = client.watch_events(WatchEventsRequest {}).await;
if let Err(error) = result {
warn!("failed to watch events: {}", error);
sleep(Duration::from_secs(1)).await;
continue;
}
result.unwrap().into_inner()
}
};
let Some(result) = stream.next().await else {
events = None;
continue;
};
let reply = match result {
Ok(reply) => reply,
Err(error) => {
trace!("event stream processing failed: {}", error);
events = None;
continue;
}
};
let Some(event) = reply.event else {
events = Some(stream);
continue;
};
let _ = emit.send(event);
events = Some(stream);
}
}
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
self.sender.subscribe()
}

View File

@ -1,53 +1,63 @@
use std::path::Path;
use std::{
collections::HashMap,
path::Path,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
};
use super::protocol::IdmPacket;
use anyhow::{anyhow, Result};
use bytes::BytesMut;
use log::error;
use log::{debug, error};
use nix::sys::termios::{cfmakeraw, tcgetattr, tcsetattr, SetArg};
use prost::Message;
use tokio::{
fs::File,
io::{unix::AsyncFd, AsyncReadExt, AsyncWriteExt},
select,
sync::mpsc::{channel, Receiver, Sender},
sync::{
broadcast,
mpsc::{self, Receiver, Sender},
oneshot, Mutex,
},
task::JoinHandle,
time::timeout,
};
use super::{
internal,
serialize::{IdmRequest, IdmSerializable},
transport::{IdmTransportPacket, IdmTransportPacketForm},
};
type OneshotRequestMap<R> = Arc<Mutex<HashMap<u64, oneshot::Sender<<R as IdmRequest>::Response>>>>;
type StreamRequestMap<R> = Arc<Mutex<HashMap<u64, Sender<<R as IdmRequest>::Response>>>>;
type StreamRequestUpdateMap<R> = Arc<Mutex<HashMap<u64, mpsc::Sender<R>>>>;
pub type IdmInternalClient = IdmClient<internal::Request, internal::Event>;
const IDM_PACKET_QUEUE_LEN: usize = 100;
const IDM_REQUEST_TIMEOUT_SECS: u64 = 10;
const IDM_PACKET_MAX_SIZE: usize = 20 * 1024 * 1024;
pub struct IdmClient {
pub receiver: Receiver<IdmPacket>,
pub sender: Sender<IdmPacket>,
task: JoinHandle<()>,
#[async_trait::async_trait]
pub trait IdmBackend: Send {
async fn recv(&mut self) -> Result<IdmTransportPacket>;
async fn send(&mut self, packet: IdmTransportPacket) -> Result<()>;
}
impl Drop for IdmClient {
fn drop(&mut self) {
self.task.abort();
}
pub struct IdmFileBackend {
read_fd: Arc<Mutex<AsyncFd<File>>>,
write: Arc<Mutex<File>>,
}
impl IdmClient {
pub async fn open<P: AsRef<Path>>(path: P) -> Result<IdmClient> {
let file = File::options()
.read(true)
.write(true)
.create(false)
.open(path)
.await?;
IdmClient::set_raw_port(&file)?;
let (rx_sender, rx_receiver) = channel(IDM_PACKET_QUEUE_LEN);
let (tx_sender, tx_receiver) = channel(IDM_PACKET_QUEUE_LEN);
let task = tokio::task::spawn(async move {
if let Err(error) = IdmClient::process(file, rx_sender, tx_receiver).await {
error!("failed to handle idm client processing: {}", error);
}
});
Ok(IdmClient {
receiver: rx_receiver,
sender: tx_sender,
task,
impl IdmFileBackend {
pub async fn new(read_file: File, write_file: File) -> Result<IdmFileBackend> {
IdmFileBackend::set_raw_port(&read_file)?;
IdmFileBackend::set_raw_port(&write_file)?;
Ok(IdmFileBackend {
read_fd: Arc::new(Mutex::new(AsyncFd::new(read_file)?)),
write: Arc::new(Mutex::new(write_file)),
})
}
@ -57,31 +67,413 @@ impl IdmClient {
tcsetattr(file, SetArg::TCSANOW, &termios)?;
Ok(())
}
}
#[async_trait::async_trait]
impl IdmBackend for IdmFileBackend {
async fn recv(&mut self) -> Result<IdmTransportPacket> {
let mut fd = self.read_fd.lock().await;
let mut guard = fd.readable_mut().await?;
let b1 = guard.get_inner_mut().read_u8().await?;
if b1 != 0xff {
return Ok(IdmTransportPacket::default());
}
let b2 = guard.get_inner_mut().read_u8().await?;
if b2 != 0xff {
return Ok(IdmTransportPacket::default());
}
let size = guard.get_inner_mut().read_u32_le().await?;
if size == 0 {
return Ok(IdmTransportPacket::default());
}
let mut buffer = vec![0u8; size as usize];
guard.get_inner_mut().read_exact(&mut buffer).await?;
match IdmTransportPacket::decode(buffer.as_slice()) {
Ok(packet) => Ok(packet),
Err(error) => Err(anyhow!("received invalid idm packet: {}", error)),
}
}
async fn send(&mut self, packet: IdmTransportPacket) -> Result<()> {
let mut file = self.write.lock().await;
let data = packet.encode_to_vec();
file.write_all(&[0xff, 0xff]).await?;
file.write_u32_le(data.len() as u32).await?;
file.write_all(&data).await?;
Ok(())
}
}
#[derive(Clone)]
pub struct IdmClient<R: IdmRequest, E: IdmSerializable> {
channel: u64,
request_backend_sender: broadcast::Sender<(u64, R)>,
request_stream_backend_sender: broadcast::Sender<IdmClientStreamResponseHandle<R>>,
next_request_id: Arc<Mutex<u64>>,
event_receiver_sender: broadcast::Sender<E>,
tx_sender: Sender<IdmTransportPacket>,
requests: OneshotRequestMap<R>,
request_streams: StreamRequestMap<R>,
task: Arc<JoinHandle<()>>,
}
impl<R: IdmRequest, E: IdmSerializable> Drop for IdmClient<R, E> {
fn drop(&mut self) {
if Arc::strong_count(&self.task) <= 1 {
self.task.abort();
}
}
}
pub struct IdmClientStreamRequestHandle<R: IdmRequest, E: IdmSerializable> {
pub id: u64,
pub receiver: Receiver<R::Response>,
pub client: IdmClient<R, E>,
}
impl<R: IdmRequest, E: IdmSerializable> IdmClientStreamRequestHandle<R, E> {
pub async fn update(&self, request: R) -> Result<()> {
self.client
.tx_sender
.send(IdmTransportPacket {
id: self.id,
channel: self.client.channel,
form: IdmTransportPacketForm::StreamRequestUpdate.into(),
data: request.encode()?,
})
.await?;
Ok(())
}
}
impl<R: IdmRequest, E: IdmSerializable> Drop for IdmClientStreamRequestHandle<R, E> {
fn drop(&mut self) {
let id = self.id;
let client = self.client.clone();
tokio::task::spawn(async move {
let _ = client
.tx_sender
.send(IdmTransportPacket {
id,
channel: client.channel,
form: IdmTransportPacketForm::StreamRequestClosed.into(),
data: vec![],
})
.await;
});
}
}
#[derive(Clone)]
pub struct IdmClientStreamResponseHandle<R: IdmRequest> {
pub initial: R,
pub id: u64,
channel: u64,
tx_sender: Sender<IdmTransportPacket>,
receiver: Arc<Mutex<Option<Receiver<R>>>>,
}
impl<R: IdmRequest> IdmClientStreamResponseHandle<R> {
pub async fn respond(&self, response: R::Response) -> Result<()> {
self.tx_sender
.send(IdmTransportPacket {
id: self.id,
channel: self.channel,
form: IdmTransportPacketForm::StreamResponseUpdate.into(),
data: response.encode()?,
})
.await?;
Ok(())
}
pub async fn take(&self) -> Result<Receiver<R>> {
let mut guard = self.receiver.lock().await;
let Some(receiver) = (*guard).take() else {
return Err(anyhow!("request has already been claimed!"));
};
Ok(receiver)
}
}
impl<R: IdmRequest> Drop for IdmClientStreamResponseHandle<R> {
fn drop(&mut self) {
if Arc::strong_count(&self.receiver) <= 1 {
let id = self.id;
let channel = self.channel;
let tx_sender = self.tx_sender.clone();
tokio::task::spawn(async move {
let _ = tx_sender
.send(IdmTransportPacket {
id,
channel,
form: IdmTransportPacketForm::StreamResponseClosed.into(),
data: vec![],
})
.await;
});
}
}
}
impl<R: IdmRequest, E: IdmSerializable> IdmClient<R, E> {
pub async fn new(channel: u64, backend: Box<dyn IdmBackend>) -> Result<Self> {
let requests = Arc::new(Mutex::new(HashMap::new()));
let request_streams = Arc::new(Mutex::new(HashMap::new()));
let request_update_streams = Arc::new(Mutex::new(HashMap::new()));
let (event_sender, event_receiver) = broadcast::channel(IDM_PACKET_QUEUE_LEN);
let (internal_request_backend_sender, _) = broadcast::channel(IDM_PACKET_QUEUE_LEN);
let (internal_request_stream_backend_sender, _) = broadcast::channel(IDM_PACKET_QUEUE_LEN);
let (tx_sender, tx_receiver) = mpsc::channel(IDM_PACKET_QUEUE_LEN);
let backend_event_sender = event_sender.clone();
let request_backend_sender = internal_request_backend_sender.clone();
let request_stream_backend_sender = internal_request_stream_backend_sender.clone();
let requests_for_client = requests.clone();
let request_streams_for_client = request_streams.clone();
let tx_sender_for_client = tx_sender.clone();
let task = tokio::task::spawn(async move {
if let Err(error) = IdmClient::process(
backend,
channel,
tx_sender,
backend_event_sender,
requests,
request_streams,
request_update_streams,
internal_request_backend_sender,
internal_request_stream_backend_sender,
event_receiver,
tx_receiver,
)
.await
{
debug!("failed to handle idm client processing: {}", error);
}
});
Ok(IdmClient {
channel,
next_request_id: Arc::new(Mutex::new(0)),
event_receiver_sender: event_sender.clone(),
request_backend_sender,
request_stream_backend_sender,
requests: requests_for_client,
request_streams: request_streams_for_client,
tx_sender: tx_sender_for_client,
task: Arc::new(task),
})
}
pub async fn open<P: AsRef<Path>>(channel: u64, path: P) -> Result<Self> {
let read_file = File::options()
.read(true)
.write(false)
.create(false)
.open(&path)
.await?;
let write_file = File::options()
.read(false)
.write(true)
.create(false)
.open(path)
.await?;
let backend = IdmFileBackend::new(read_file, write_file).await?;
IdmClient::new(channel, Box::new(backend) as Box<dyn IdmBackend>).await
}
pub async fn emit<T: IdmSerializable>(&self, event: T) -> Result<()> {
let id = {
let mut guard = self.next_request_id.lock().await;
let req = *guard;
*guard = req.wrapping_add(1);
req
};
self.tx_sender
.send(IdmTransportPacket {
id,
form: IdmTransportPacketForm::Event.into(),
channel: self.channel,
data: event.encode()?,
})
.await?;
Ok(())
}
pub async fn requests(&self) -> Result<broadcast::Receiver<(u64, R)>> {
Ok(self.request_backend_sender.subscribe())
}
pub async fn request_streams(
&self,
) -> Result<broadcast::Receiver<IdmClientStreamResponseHandle<R>>> {
Ok(self.request_stream_backend_sender.subscribe())
}
pub async fn respond<T: IdmSerializable>(&self, id: u64, response: T) -> Result<()> {
let packet = IdmTransportPacket {
id,
form: IdmTransportPacketForm::Response.into(),
channel: self.channel,
data: response.encode()?,
};
self.tx_sender.send(packet).await?;
Ok(())
}
pub async fn subscribe(&self) -> Result<broadcast::Receiver<E>> {
Ok(self.event_receiver_sender.subscribe())
}
pub async fn send(&self, request: R) -> Result<R::Response> {
let (sender, receiver) = oneshot::channel::<R::Response>();
let req = {
let mut guard = self.next_request_id.lock().await;
let req = *guard;
*guard = req.wrapping_add(1);
req
};
let mut requests = self.requests.lock().await;
requests.insert(req, sender);
drop(requests);
let success = AtomicBool::new(false);
let _guard = scopeguard::guard(self.requests.clone(), |requests| {
if success.load(Ordering::Acquire) {
return;
}
tokio::task::spawn(async move {
let mut requests = requests.lock().await;
requests.remove(&req);
});
});
self.tx_sender
.send(IdmTransportPacket {
id: req,
channel: self.channel,
form: IdmTransportPacketForm::Request.into(),
data: request.encode()?,
})
.await?;
let response = timeout(Duration::from_secs(IDM_REQUEST_TIMEOUT_SECS), receiver).await??;
success.store(true, Ordering::Release);
Ok(response)
}
pub async fn send_stream(&self, request: R) -> Result<IdmClientStreamRequestHandle<R, E>> {
let (sender, receiver) = mpsc::channel::<R::Response>(100);
let req = {
let mut guard = self.next_request_id.lock().await;
let req = *guard;
*guard = req.wrapping_add(1);
req
};
let mut requests = self.request_streams.lock().await;
requests.insert(req, sender);
drop(requests);
self.tx_sender
.send(IdmTransportPacket {
id: req,
channel: self.channel,
form: IdmTransportPacketForm::StreamRequest.into(),
data: request.encode()?,
})
.await?;
Ok(IdmClientStreamRequestHandle {
id: req,
receiver,
client: self.clone(),
})
}
#[allow(clippy::too_many_arguments)]
async fn process(
file: File,
sender: Sender<IdmPacket>,
mut receiver: Receiver<IdmPacket>,
mut backend: Box<dyn IdmBackend>,
channel: u64,
tx_sender: Sender<IdmTransportPacket>,
event_sender: broadcast::Sender<E>,
requests: OneshotRequestMap<R>,
request_streams: StreamRequestMap<R>,
request_update_streams: StreamRequestUpdateMap<R>,
request_backend_sender: broadcast::Sender<(u64, R)>,
request_stream_backend_sender: broadcast::Sender<IdmClientStreamResponseHandle<R>>,
_event_receiver: broadcast::Receiver<E>,
mut receiver: Receiver<IdmTransportPacket>,
) -> Result<()> {
let mut file = AsyncFd::new(file)?;
loop {
select! {
x = file.readable_mut() => match x {
Ok(mut guard) => {
let size = guard.get_inner_mut().read_u16_le().await?;
if size == 0 {
x = backend.recv() => match x {
Ok(packet) => {
if packet.channel != channel {
continue;
}
let mut buffer = BytesMut::with_capacity(size as usize);
guard.get_inner_mut().read_exact(&mut buffer).await?;
match IdmPacket::decode(buffer) {
Ok(packet) => {
sender.send(packet).await?;
match packet.form() {
IdmTransportPacketForm::Event => {
if let Ok(event) = E::decode(&packet.data) {
let _ = event_sender.send(event);
}
},
Err(error) => {
error!("received invalid idm packet: {}", error);
IdmTransportPacketForm::Request => {
if let Ok(request) = R::decode(&packet.data) {
let _ = request_backend_sender.send((packet.id, request));
}
},
IdmTransportPacketForm::Response => {
let mut requests = requests.lock().await;
if let Some(sender) = requests.remove(&packet.id) {
drop(requests);
if let Ok(response) = R::Response::decode(&packet.data) {
let _ = sender.send(response);
}
}
},
IdmTransportPacketForm::StreamRequest => {
if let Ok(request) = R::decode(&packet.data) {
let mut update_streams = request_update_streams.lock().await;
let (sender, receiver) = mpsc::channel(100);
update_streams.insert(packet.id, sender.clone());
let handle = IdmClientStreamResponseHandle {
initial: request,
id: packet.id,
channel,
tx_sender: tx_sender.clone(),
receiver: Arc::new(Mutex::new(Some(receiver))),
};
let _ = request_stream_backend_sender.send(handle);
}
}
IdmTransportPacketForm::StreamRequestUpdate => {
if let Ok(request) = R::decode(&packet.data) {
let mut update_streams = request_update_streams.lock().await;
if let Some(stream) = update_streams.get_mut(&packet.id) {
let _ = stream.try_send(request);
}
}
}
IdmTransportPacketForm::StreamRequestClosed => {
let mut update_streams = request_update_streams.lock().await;
update_streams.remove(&packet.id);
}
IdmTransportPacketForm::StreamResponseUpdate => {
let requests = request_streams.lock().await;
if let Some(sender) = requests.get(&packet.id) {
if let Ok(response) = R::Response::decode(&packet.data) {
let _ = sender.try_send(response);
}
}
}
IdmTransportPacketForm::StreamResponseClosed => {
let mut requests = request_streams.lock().await;
requests.remove(&packet.id);
}
_ => {},
}
},
@ -91,13 +483,12 @@ impl IdmClient {
},
x = receiver.recv() => match x {
Some(packet) => {
let data = packet.encode_to_vec();
if data.len() > u16::MAX as usize {
error!("unable to send idm packet, packet size exceeded (tried to send {} bytes)", data.len());
let length = packet.encoded_len();
if length > IDM_PACKET_MAX_SIZE {
error!("unable to send idm packet, packet size exceeded (tried to send {} bytes)", length);
continue;
}
file.get_mut().write_u16_le(data.len() as u16).await?;
file.get_mut().write_all(&data).await?;
backend.send(packet).await?;
},
None => {

View File

@ -0,0 +1,129 @@
use anyhow::Result;
use prost::Message;
use prost_types::{ListValue, Value};
use super::serialize::{IdmRequest, IdmSerializable};
include!(concat!(env!("OUT_DIR"), "/krata.idm.internal.rs"));
pub const INTERNAL_IDM_CHANNEL: u64 = 0;
impl IdmSerializable for Event {
fn encode(&self) -> Result<Vec<u8>> {
Ok(self.encode_to_vec())
}
fn decode(bytes: &[u8]) -> Result<Self> {
Ok(<Self as prost::Message>::decode(bytes)?)
}
}
impl IdmSerializable for Request {
fn encode(&self) -> Result<Vec<u8>> {
Ok(self.encode_to_vec())
}
fn decode(bytes: &[u8]) -> Result<Self> {
Ok(<Self as prost::Message>::decode(bytes)?)
}
}
impl IdmRequest for Request {
type Response = Response;
}
impl IdmSerializable for Response {
fn encode(&self) -> Result<Vec<u8>> {
Ok(self.encode_to_vec())
}
fn decode(bytes: &[u8]) -> Result<Self> {
Ok(<Self as prost::Message>::decode(bytes)?)
}
}
pub trait AsIdmMetricValue {
fn as_metric_value(&self) -> Value;
}
impl MetricNode {
pub fn structural<N: AsRef<str>>(name: N, children: Vec<MetricNode>) -> MetricNode {
MetricNode {
name: name.as_ref().to_string(),
value: None,
format: MetricFormat::Unknown.into(),
children,
}
}
pub fn raw_value<N: AsRef<str>, V: AsIdmMetricValue>(name: N, value: V) -> MetricNode {
MetricNode {
name: name.as_ref().to_string(),
value: Some(value.as_metric_value()),
format: MetricFormat::Unknown.into(),
children: vec![],
}
}
pub fn value<N: AsRef<str>, V: AsIdmMetricValue>(
name: N,
value: V,
format: MetricFormat,
) -> MetricNode {
MetricNode {
name: name.as_ref().to_string(),
value: Some(value.as_metric_value()),
format: format.into(),
children: vec![],
}
}
}
impl AsIdmMetricValue for String {
fn as_metric_value(&self) -> Value {
Value {
kind: Some(prost_types::value::Kind::StringValue(self.to_string())),
}
}
}
impl AsIdmMetricValue for &str {
fn as_metric_value(&self) -> Value {
Value {
kind: Some(prost_types::value::Kind::StringValue(self.to_string())),
}
}
}
impl AsIdmMetricValue for u64 {
fn as_metric_value(&self) -> Value {
numeric(*self as f64)
}
}
impl AsIdmMetricValue for i64 {
fn as_metric_value(&self) -> Value {
numeric(*self as f64)
}
}
impl AsIdmMetricValue for f64 {
fn as_metric_value(&self) -> Value {
numeric(*self)
}
}
impl<T: AsIdmMetricValue> AsIdmMetricValue for Vec<T> {
fn as_metric_value(&self) -> Value {
let values = self.iter().map(|x| x.as_metric_value()).collect::<_>();
Value {
kind: Some(prost_types::value::Kind::ListValue(ListValue { values })),
}
}
}
fn numeric(value: f64) -> Value {
Value {
kind: Some(prost_types::value::Kind::NumberValue(value)),
}
}

View File

@ -1,3 +1,5 @@
#[cfg(unix)]
pub mod client;
pub mod protocol;
pub mod internal;
pub mod serialize;
pub mod transport;

View File

@ -1 +0,0 @@
include!(concat!(env!("OUT_DIR"), "/krata.internal.idm.rs"));

View File

@ -0,0 +1,10 @@
use anyhow::Result;
pub trait IdmSerializable: Sized + Clone + Send + Sync + 'static {
fn decode(bytes: &[u8]) -> Result<Self>;
fn encode(&self) -> Result<Vec<u8>>;
}
pub trait IdmRequest: IdmSerializable {
type Response: IdmSerializable;
}

View File

@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/krata.idm.transport.rs"));

View File

@ -2,24 +2,30 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum LaunchPackedFormat {
Squashfs,
Erofs,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LaunchNetworkIpv4 {
pub address: String,
pub gateway: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LaunchNetworkIpv6 {
pub address: String,
pub gateway: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LaunchNetworkResolver {
pub nameservers: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LaunchNetwork {
pub link: String,
pub ipv4: LaunchNetworkIpv4,
@ -27,8 +33,15 @@ pub struct LaunchNetwork {
pub resolver: LaunchNetworkResolver,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LaunchRoot {
pub format: LaunchPackedFormat,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LaunchInfo {
pub root: LaunchRoot,
pub hostname: Option<String>,
pub network: Option<LaunchNetwork>,
pub env: HashMap<String, String>,
pub run: Option<Vec<String>>,

View File

@ -1 +1,2 @@
#![allow(clippy::all)]
tonic::include_proto!("krata.v1.common");

View File

@ -1 +1,2 @@
#![allow(clippy::all)]
tonic::include_proto!("krata.v1.control");

15
crates/loopdev/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "krata-loopdev"
description = "Loop device handling library for krata"
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition = "2021"
resolver = "2"
[lib]
name = "krataloopdev"
[dependencies]
libc.workspace = true

348
crates/loopdev/src/lib.rs Normal file
View File

@ -0,0 +1,348 @@
use libc::{c_int, ioctl};
use std::{
fs::{File, OpenOptions},
io,
os::fd::{AsRawFd, IntoRawFd, RawFd},
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
};
#[cfg(all(not(target_os = "android"), not(target_env = "musl")))]
type IoctlRequest = libc::c_ulong;
#[cfg(any(target_os = "android", target_env = "musl"))]
type IoctlRequest = libc::c_int;
const LOOP_CONTROL: &str = "/dev/loop-control";
const LOOP_PREFIX: &str = "/dev/loop";
/// Loop control interface IOCTLs.
const LOOP_CTL_GET_FREE: IoctlRequest = 0x4C82;
/// Loop device flags.
const LO_FLAGS_READ_ONLY: u32 = 1;
const LO_FLAGS_AUTOCLEAR: u32 = 4;
const LO_FLAGS_PARTSCAN: u32 = 8;
const LO_FLAGS_DIRECT_IO: u32 = 16;
/// Loop device IOCTLs.
const LOOP_SET_FD: IoctlRequest = 0x4C00;
const LOOP_CLR_FD: IoctlRequest = 0x4C01;
const LOOP_SET_STATUS64: IoctlRequest = 0x4C04;
const LOOP_SET_CAPACITY: IoctlRequest = 0x4C07;
const LOOP_SET_DIRECT_IO: IoctlRequest = 0x4C08;
/// Interface which wraps a handle to the loop control device.
#[derive(Debug)]
pub struct LoopControl {
dev_file: File,
}
/// Translate ioctl results to errors if appropriate.
fn translate_error(ret: i32) -> io::Result<i32> {
if ret < 0 {
Err(io::Error::last_os_error())
} else {
Ok(ret)
}
}
impl LoopControl {
/// Open the loop control device.
///
/// # Errors
///
/// Any errors from physically opening the loop control device are
/// bubbled up.
pub fn open() -> io::Result<Self> {
Ok(Self {
dev_file: OpenOptions::new()
.read(true)
.write(true)
.open(LOOP_CONTROL)?,
})
}
/// Requests the next available loop device from the kernel and opens it.
///
/// # Examples
///
/// ```no_run
/// use krataloopdev::LoopControl;
/// let lc = LoopControl::open().unwrap();
/// let ld = lc.next_free().unwrap();
/// println!("{}", ld.path().unwrap().display());
/// ```
///
/// # Errors
///
/// Any errors from opening the loop device are bubbled up.
pub fn next_free(&self) -> io::Result<LoopDevice> {
let dev_num = translate_error(unsafe {
ioctl(
self.dev_file.as_raw_fd() as c_int,
LOOP_CTL_GET_FREE as IoctlRequest,
)
})?;
LoopDevice::open(format!("{}{}", LOOP_PREFIX, dev_num))
}
}
/// Interface to a loop device itself, e.g. `/dev/loop0`.
#[derive(Debug)]
pub struct LoopDevice {
device: File,
}
impl AsRawFd for LoopDevice {
fn as_raw_fd(&self) -> RawFd {
self.device.as_raw_fd()
}
}
impl IntoRawFd for LoopDevice {
fn into_raw_fd(self) -> RawFd {
self.device.into_raw_fd()
}
}
impl LoopDevice {
/// Opens a loop device.
///
/// # Errors
///
/// Any errors from opening the underlying physical loop device are bubbled up.
pub fn open<P: AsRef<Path>>(dev: P) -> io::Result<Self> {
Ok(Self {
device: OpenOptions::new().read(true).write(true).open(dev)?,
})
}
/// Attach a loop device to a file with the given options.
pub fn with(&self) -> AttachOptions<'_> {
AttachOptions {
device: self,
info: LoopInfo64::default(),
}
}
/// Enables or disables Direct I/O mode.
pub fn set_direct_io(&self, direct_io: bool) -> io::Result<()> {
translate_error(unsafe {
ioctl(
self.device.as_raw_fd() as c_int,
LOOP_SET_DIRECT_IO as IoctlRequest,
if direct_io { 1 } else { 0 },
)
})?;
Ok(())
}
/// Attach the loop device to a fully-mapped file.
pub fn attach_file<P: AsRef<Path>>(&self, backing_file: P) -> io::Result<()> {
let info = LoopInfo64 {
..Default::default()
};
Self::attach_with_loop_info(self, backing_file, info)
}
/// Attach the loop device to a file with `LoopInfo64`.
fn attach_with_loop_info(
&self,
backing_file: impl AsRef<Path>,
info: LoopInfo64,
) -> io::Result<()> {
let write_access = (info.lo_flags & LO_FLAGS_READ_ONLY) == 0;
let bf = OpenOptions::new()
.read(true)
.write(write_access)
.open(backing_file)?;
self.attach_fd_with_loop_info(bf, info)
}
/// Attach the loop device to a file descriptor with `LoopInfo64`.
fn attach_fd_with_loop_info(&self, bf: impl AsRawFd, info: LoopInfo64) -> io::Result<()> {
translate_error(unsafe {
ioctl(
self.device.as_raw_fd() as c_int,
LOOP_SET_FD as IoctlRequest,
bf.as_raw_fd() as c_int,
)
})?;
let result = unsafe {
ioctl(
self.device.as_raw_fd() as c_int,
LOOP_SET_STATUS64 as IoctlRequest,
&info,
)
};
match translate_error(result) {
Err(err) => {
let _detach_err = self.detach();
Err(err)
}
Ok(_) => Ok(()),
}
}
/// Get the path for the loop device.
pub fn path(&self) -> Option<PathBuf> {
let mut p = PathBuf::from("/proc/self/fd");
p.push(self.device.as_raw_fd().to_string());
std::fs::read_link(&p).ok()
}
/// Detach a loop device.
pub fn detach(&self) -> io::Result<()> {
translate_error(unsafe {
ioctl(
self.device.as_raw_fd() as c_int,
LOOP_CLR_FD as IoctlRequest,
0,
)
})?;
Ok(())
}
/// Update a loop device's capacity.
pub fn set_capacity(&self) -> io::Result<()> {
translate_error(unsafe {
ioctl(
self.device.as_raw_fd() as c_int,
LOOP_SET_CAPACITY as IoctlRequest,
0,
)
})?;
Ok(())
}
/// Return the major device node number.
pub fn major(&self) -> io::Result<u32> {
self.device
.metadata()
.map(|m| unsafe { libc::major(m.rdev()) })
}
/// Return the minor device node number.
pub fn minor(&self) -> io::Result<u32> {
self.device
.metadata()
.map(|m| unsafe { libc::minor(m.rdev()) })
}
}
#[allow(dead_code)]
#[derive(Clone)]
pub struct LoopInfo64 {
lo_device: u64,
lo_inode: u64,
lo_rdevice: u64,
lo_offset: u64,
lo_sizelimit: u64,
lo_number: u32,
lo_encrypt_type: u32,
lo_encrypt_key_size: u32,
lo_flags: u32,
lo_file_name: [u8; 64],
lo_crypt_name: [u8; 64],
lo_encrypt_key: [u8; 32],
lo_init: [u64; 2],
}
impl Default for LoopInfo64 {
fn default() -> Self {
Self {
lo_device: 0,
lo_inode: 0,
lo_rdevice: 0,
lo_offset: 0,
lo_sizelimit: 0,
lo_number: 0,
lo_encrypt_type: 0,
lo_encrypt_key_size: 0,
lo_flags: 0,
lo_file_name: [0; 64],
lo_crypt_name: [0; 64],
lo_encrypt_key: [0; 32],
lo_init: [0, 2],
}
}
}
#[must_use]
pub struct AttachOptions<'d> {
device: &'d LoopDevice,
info: LoopInfo64,
}
impl AttachOptions<'_> {
pub fn offset(mut self, offset: u64) -> Self {
self.info.lo_offset = offset;
self
}
pub fn size_limit(mut self, size_limit: u64) -> Self {
self.info.lo_sizelimit = size_limit;
self
}
pub fn read_only(mut self, read_only: bool) -> Self {
if read_only {
self.info.lo_flags |= LO_FLAGS_READ_ONLY;
} else {
self.info.lo_flags &= !LO_FLAGS_READ_ONLY;
}
self
}
pub fn autoclear(mut self, autoclear: bool) -> Self {
if autoclear {
self.info.lo_flags |= LO_FLAGS_AUTOCLEAR;
} else {
self.info.lo_flags &= !LO_FLAGS_AUTOCLEAR;
}
self
}
pub fn part_scan(mut self, part_scan: bool) -> Self {
if part_scan {
self.info.lo_flags |= LO_FLAGS_PARTSCAN;
} else {
self.info.lo_flags &= !LO_FLAGS_PARTSCAN;
}
self
}
pub fn set_direct_io(mut self, direct_io: bool) -> Self {
if direct_io {
self.info.lo_flags |= LO_FLAGS_DIRECT_IO;
} else {
self.info.lo_flags &= !LO_FLAGS_DIRECT_IO;
}
self
}
pub fn direct_io(&self) -> bool {
(self.info.lo_flags & LO_FLAGS_DIRECT_IO) == LO_FLAGS_DIRECT_IO
}
pub fn attach(&self, backing_file: impl AsRef<Path>) -> io::Result<()> {
self.device
.attach_with_loop_info(backing_file, self.info.clone())?;
if self.direct_io() {
self.device.set_direct_io(self.direct_io())?;
}
Ok(())
}
pub fn attach_fd(&self, backing_file_fd: impl AsRawFd) -> io::Result<()> {
self.device
.attach_fd_with_loop_info(backing_file_fd, self.info.clone())?;
if self.direct_io() {
self.device.set_direct_io(self.direct_io())?;
}
Ok(())
}
}

View File

@ -1,8 +1,8 @@
[package]
name = "krata-network"
description = "Networking services for the krata hypervisor."
description = "Networking services for the krata isolation engine"
license.workspace = true
version= "0.0.3"
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition = "2021"
@ -16,7 +16,7 @@ clap = { workspace = true }
env_logger = { workspace = true }
etherparse = { workspace = true }
futures = { workspace = true }
krata = { path = "../krata", version = "^0.0.3" }
krata = { path = "../krata", version = "^0.0.12" }
krata-advmac = { workspace = true }
libc = { workspace = true }
log = { workspace = true }

View File

@ -5,7 +5,7 @@ use krata::{
common::Guest,
control::{
control_service_client::ControlServiceClient, watch_events_reply::Event,
ListGuestsRequest, WatchEventsRequest,
ListGuestsRequest,
},
},
};
@ -50,12 +50,11 @@ pub struct AutoNetworkChangeset {
}
impl AutoNetworkWatcher {
pub async fn new(mut control: ControlServiceClient<Channel>) -> Result<AutoNetworkWatcher> {
let watch_events_response = control.watch_events(WatchEventsRequest {}).await?;
pub async fn new(control: ControlServiceClient<Channel>) -> Result<AutoNetworkWatcher> {
let client = control.clone();
Ok(AutoNetworkWatcher {
control,
events: EventStream::open(watch_events_response.into_inner()).await?,
events: EventStream::open(client).await?,
known: HashMap::new(),
})
}
@ -136,7 +135,15 @@ impl AutoNetworkWatcher {
let mut added: Vec<NetworkMetadata> = Vec::new();
let mut removed: Vec<NetworkMetadata> = Vec::new();
for network in self.read().await? {
let networks = match self.read().await {
Ok(networks) => networks,
Err(error) => {
warn!("failed to read network changes: {}", error);
return Ok(AutoNetworkChangeset { added, removed });
}
};
for network in networks {
seen.push(network.uuid);
if self.known.contains_key(&network.uuid) {
continue;

View File

@ -1,8 +1,8 @@
[package]
name = "krata-oci"
description = "OCI services for the krata hypervisor."
description = "OCI services for the krata isolation engine"
license.workspace = true
version= "0.0.3"
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition = "2021"
@ -14,11 +14,13 @@ async-compression = { workspace = true, features = ["tokio", "gzip", "zstd"] }
async-trait = { workspace = true }
backhand = { workspace = true }
bytes = { workspace = true }
indexmap = { workspace = true }
krata-tokio-tar = { workspace = true }
log = { workspace = true }
oci-spec = { workspace = true }
path-clean = { workspace = true }
reqwest = { workspace = true }
scopeguard = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha256 = { workspace = true }

View File

@ -2,7 +2,12 @@ use std::{env::args, path::PathBuf};
use anyhow::Result;
use env_logger::Env;
use krataoci::{cache::ImageCache, compiler::ImageCompiler, name::ImageName};
use krataoci::{
name::ImageName,
packer::{service::OciPackerService, OciPackedFormat},
progress::OciProgressContext,
registry::OciPlatform,
};
use tokio::fs;
#[tokio::main]
@ -17,13 +22,27 @@ async fn main() -> Result<()> {
fs::create_dir(&cache_dir).await?;
}
let cache = ImageCache::new(&cache_dir)?;
let compiler = ImageCompiler::new(&cache, seed)?;
let info = compiler.compile(&image).await?;
let (context, mut receiver) = OciProgressContext::create();
tokio::task::spawn(async move {
loop {
if (receiver.changed().await).is_err() {
break;
}
let progress = receiver.borrow_and_update();
println!("phase {:?}", progress.phase);
for (id, layer) in &progress.layers {
println!("{} {:?} {:?}", id, layer.phase, layer.indication,)
}
}
});
let service = OciPackerService::new(seed, &cache_dir, OciPlatform::current()).await?;
let packed = service
.request(image.clone(), OciPackedFormat::Squashfs, false, context)
.await?;
println!(
"generated squashfs of {} to {}",
image,
info.image_squashfs.to_string_lossy()
packed.path.to_string_lossy()
);
Ok(())
}

273
crates/oci/src/assemble.rs Normal file
View File

@ -0,0 +1,273 @@
use crate::fetch::{OciImageFetcher, OciImageLayer, OciImageLayerReader, OciResolvedImage};
use crate::progress::OciBoundProgress;
use crate::schema::OciSchema;
use crate::vfs::{VfsNode, VfsTree};
use anyhow::{anyhow, Result};
use log::{debug, trace, warn};
use oci_spec::image::{Descriptor, ImageConfiguration, ImageManifest};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::fs;
use tokio_stream::StreamExt;
use tokio_tar::{Archive, Entry};
use uuid::Uuid;
pub struct OciImageAssembled {
pub digest: String,
pub descriptor: Descriptor,
pub manifest: OciSchema<ImageManifest>,
pub config: OciSchema<ImageConfiguration>,
pub vfs: Arc<VfsTree>,
pub tmp_dir: Option<PathBuf>,
}
impl Drop for OciImageAssembled {
fn drop(&mut self) {
if let Some(tmp) = self.tmp_dir.clone() {
tokio::task::spawn(async move {
let _ = fs::remove_dir_all(&tmp).await;
});
}
}
}
pub struct OciImageAssembler {
downloader: OciImageFetcher,
resolved: Option<OciResolvedImage>,
progress: OciBoundProgress,
work_dir: PathBuf,
disk_dir: PathBuf,
tmp_dir: Option<PathBuf>,
success: AtomicBool,
}
impl OciImageAssembler {
pub async fn new(
downloader: OciImageFetcher,
resolved: OciResolvedImage,
progress: OciBoundProgress,
work_dir: Option<PathBuf>,
disk_dir: Option<PathBuf>,
) -> Result<OciImageAssembler> {
let tmp_dir = if work_dir.is_none() || disk_dir.is_none() {
let mut tmp_dir = std::env::temp_dir().clone();
tmp_dir.push(format!("oci-assemble-{}", Uuid::new_v4()));
Some(tmp_dir)
} else {
None
};
let work_dir = if let Some(work_dir) = work_dir {
work_dir
} else {
let mut tmp_dir = tmp_dir
.clone()
.ok_or(anyhow!("tmp_dir was not created when expected"))?;
tmp_dir.push("work");
tmp_dir
};
let target_dir = if let Some(target_dir) = disk_dir {
target_dir
} else {
let mut tmp_dir = tmp_dir
.clone()
.ok_or(anyhow!("tmp_dir was not created when expected"))?;
tmp_dir.push("image");
tmp_dir
};
fs::create_dir_all(&work_dir).await?;
fs::create_dir_all(&target_dir).await?;
Ok(OciImageAssembler {
downloader,
resolved: Some(resolved),
progress,
work_dir,
disk_dir: target_dir,
tmp_dir,
success: AtomicBool::new(false),
})
}
pub async fn assemble(self) -> Result<OciImageAssembled> {
debug!("assemble");
let mut layer_dir = self.work_dir.clone();
layer_dir.push("layer");
fs::create_dir_all(&layer_dir).await?;
self.assemble_with(&layer_dir).await
}
async fn assemble_with(mut self, layer_dir: &Path) -> Result<OciImageAssembled> {
let Some(ref resolved) = self.resolved else {
return Err(anyhow!("resolved image was not available when expected"));
};
let local = self.downloader.download(resolved, layer_dir).await?;
let mut vfs = VfsTree::new();
for layer in &local.layers {
debug!(
"process layer digest={} compression={:?}",
&layer.digest, layer.compression,
);
self.progress
.update(|progress| {
progress.start_extracting_layer(&layer.digest);
})
.await;
debug!("process layer digest={}", &layer.digest,);
let mut archive = layer.archive().await?;
let mut entries = archive.entries()?;
let mut count = 0u64;
let mut size = 0u64;
while let Some(entry) = entries.next().await {
let mut entry = entry?;
let path = entry.path()?;
let Some(name) = path.file_name() else {
continue;
};
let Some(name) = name.to_str() else {
continue;
};
if name.starts_with(".wh.") {
self.process_whiteout_entry(&mut vfs, &entry, name, layer)
.await?;
} else {
let reference = vfs.insert_tar_entry(&entry)?;
self.progress
.update(|progress| {
progress.extracting_layer(&layer.digest, &reference.name);
})
.await;
size += self
.process_write_entry(&mut vfs, &mut entry, layer)
.await?;
count += 1;
}
}
self.progress
.update(|progress| {
progress.extracted_layer(&layer.digest, count, size);
})
.await;
}
for layer in &local.layers {
if layer.path.exists() {
fs::remove_file(&layer.path).await?;
}
}
let Some(resolved) = self.resolved.take() else {
return Err(anyhow!("resolved image was not available when expected"));
};
let assembled = OciImageAssembled {
vfs: Arc::new(vfs),
descriptor: resolved.descriptor,
digest: resolved.digest,
manifest: resolved.manifest,
config: local.config,
tmp_dir: self.tmp_dir.clone(),
};
self.success.store(true, Ordering::Release);
Ok(assembled)
}
async fn process_whiteout_entry(
&self,
vfs: &mut VfsTree,
entry: &Entry<Archive<Pin<Box<dyn OciImageLayerReader + Send>>>>,
name: &str,
layer: &OciImageLayer,
) -> Result<()> {
let path = entry.path()?;
let mut path = path.to_path_buf();
path.pop();
let opaque = name == ".wh..wh..opq";
if !opaque {
let file = &name[4..];
path.push(file);
}
trace!(
"whiteout entry {:?} layer={} path={:?}",
entry.path()?,
&layer.digest,
path
);
let result = vfs.root.remove(&path);
if let Some((parent, mut removed)) = result {
delete_disk_paths(&removed).await?;
if opaque {
removed.children.clear();
parent.children.push(removed);
}
} else {
warn!(
"whiteout entry layer={} path={:?} did not exist",
&layer.digest, path
);
}
Ok(())
}
async fn process_write_entry(
&self,
vfs: &mut VfsTree,
entry: &mut Entry<Archive<Pin<Box<dyn OciImageLayerReader + Send>>>>,
layer: &OciImageLayer,
) -> Result<u64> {
if !entry.header().entry_type().is_file() {
return Ok(0);
}
trace!(
"unpack entry layer={} path={:?} type={:?}",
&layer.digest,
entry.path()?,
entry.header().entry_type(),
);
entry.set_preserve_permissions(false);
entry.set_unpack_xattrs(false);
entry.set_preserve_mtime(false);
let path = entry
.unpack_in(&self.disk_dir)
.await?
.ok_or(anyhow!("unpack did not return a path"))?;
vfs.set_disk_path(&entry.path()?, &path)?;
Ok(entry.header().size()?)
}
}
impl Drop for OciImageAssembler {
fn drop(&mut self) {
if !self.success.load(Ordering::Acquire) {
if let Some(tmp_dir) = self.tmp_dir.clone() {
tokio::task::spawn(async move {
let _ = fs::remove_dir_all(tmp_dir).await;
});
}
}
}
}
async fn delete_disk_paths(node: &VfsNode) -> Result<()> {
let mut queue = vec![node];
while !queue.is_empty() {
let node = queue.remove(0);
if let Some(ref disk_path) = node.disk_path {
if !disk_path.exists() {
warn!("disk path {:?} does not exist", disk_path);
}
fs::remove_file(disk_path).await?;
}
let children = node.children.iter().collect::<Vec<_>>();
queue.extend_from_slice(&children);
}
Ok(())
}

View File

@ -1,70 +0,0 @@
use super::compiler::ImageInfo;
use anyhow::Result;
use log::debug;
use oci_spec::image::{ImageConfiguration, ImageManifest};
use std::path::{Path, PathBuf};
use tokio::fs;
pub struct ImageCache {
cache_dir: PathBuf,
}
impl ImageCache {
pub fn new(cache_dir: &Path) -> Result<ImageCache> {
Ok(ImageCache {
cache_dir: cache_dir.to_path_buf(),
})
}
pub async fn recall(&self, digest: &str) -> Result<Option<ImageInfo>> {
let mut squashfs_path = self.cache_dir.clone();
let mut config_path = self.cache_dir.clone();
let mut manifest_path = self.cache_dir.clone();
squashfs_path.push(format!("{}.squashfs", digest));
manifest_path.push(format!("{}.manifest.json", digest));
config_path.push(format!("{}.config.json", digest));
Ok(
if squashfs_path.exists() && manifest_path.exists() && config_path.exists() {
let squashfs_metadata = fs::metadata(&squashfs_path).await?;
let manifest_metadata = fs::metadata(&manifest_path).await?;
let config_metadata = fs::metadata(&config_path).await?;
if squashfs_metadata.is_file()
&& manifest_metadata.is_file()
&& config_metadata.is_file()
{
let manifest_text = fs::read_to_string(&manifest_path).await?;
let manifest: ImageManifest = serde_json::from_str(&manifest_text)?;
let config_text = fs::read_to_string(&config_path).await?;
let config: ImageConfiguration = serde_json::from_str(&config_text)?;
debug!("cache hit digest={}", digest);
Some(ImageInfo::new(squashfs_path.clone(), manifest, config)?)
} else {
None
}
} else {
debug!("cache miss digest={}", digest);
None
},
)
}
pub async fn store(&self, digest: &str, info: &ImageInfo) -> Result<ImageInfo> {
debug!("cache store digest={}", digest);
let mut squashfs_path = self.cache_dir.clone();
let mut manifest_path = self.cache_dir.clone();
let mut config_path = self.cache_dir.clone();
squashfs_path.push(format!("{}.squashfs", digest));
manifest_path.push(format!("{}.manifest.json", digest));
config_path.push(format!("{}.config.json", digest));
fs::copy(&info.image_squashfs, &squashfs_path).await?;
let manifest_text = serde_json::to_string_pretty(&info.manifest)?;
fs::write(&manifest_path, manifest_text).await?;
let config_text = serde_json::to_string_pretty(&info.config)?;
fs::write(&config_path, config_text).await?;
ImageInfo::new(
squashfs_path.clone(),
info.manifest.clone(),
info.config.clone(),
)
}
}

View File

@ -1,411 +0,0 @@
use crate::cache::ImageCache;
use crate::fetch::{OciImageDownloader, OciImageLayer};
use crate::name::ImageName;
use crate::registry::OciRegistryPlatform;
use anyhow::{anyhow, Result};
use backhand::compression::Compressor;
use backhand::{FilesystemCompressor, FilesystemWriter, NodeHeader};
use log::{debug, trace, warn};
use oci_spec::image::{ImageConfiguration, ImageManifest};
use std::borrow::Cow;
use std::fs::File;
use std::io::{BufWriter, ErrorKind, Read};
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use tokio::fs;
use tokio::io::AsyncRead;
use tokio_stream::StreamExt;
use tokio_tar::{Archive, Entry};
use uuid::Uuid;
use walkdir::WalkDir;
pub const IMAGE_SQUASHFS_VERSION: u64 = 2;
pub struct ImageInfo {
pub image_squashfs: PathBuf,
pub manifest: ImageManifest,
pub config: ImageConfiguration,
}
impl ImageInfo {
pub fn new(
squashfs: PathBuf,
manifest: ImageManifest,
config: ImageConfiguration,
) -> Result<ImageInfo> {
Ok(ImageInfo {
image_squashfs: squashfs,
manifest,
config,
})
}
}
pub struct ImageCompiler<'a> {
cache: &'a ImageCache,
seed: Option<PathBuf>,
}
impl ImageCompiler<'_> {
pub fn new(cache: &ImageCache, seed: Option<PathBuf>) -> Result<ImageCompiler> {
Ok(ImageCompiler { cache, seed })
}
pub async fn compile(&self, image: &ImageName) -> Result<ImageInfo> {
debug!("compile image={image}");
let mut tmp_dir = std::env::temp_dir().clone();
tmp_dir.push(format!("krata-compile-{}", Uuid::new_v4()));
let mut image_dir = tmp_dir.clone();
image_dir.push("image");
fs::create_dir_all(&image_dir).await?;
let mut layer_dir = tmp_dir.clone();
layer_dir.push("layer");
fs::create_dir_all(&layer_dir).await?;
let mut squash_file = tmp_dir.clone();
squash_file.push("image.squashfs");
let info = self
.download_and_compile(image, &layer_dir, &image_dir, &squash_file)
.await?;
fs::remove_dir_all(&tmp_dir).await?;
Ok(info)
}
async fn download_and_compile(
&self,
image: &ImageName,
layer_dir: &Path,
image_dir: &Path,
squash_file: &Path,
) -> Result<ImageInfo> {
let downloader = OciImageDownloader::new(
self.seed.clone(),
layer_dir.to_path_buf(),
OciRegistryPlatform::current(),
);
let resolved = downloader.resolve(image.clone()).await?;
let cache_key = format!(
"manifest={}:squashfs-version={}\n",
resolved.digest, IMAGE_SQUASHFS_VERSION
);
let cache_digest = sha256::digest(cache_key);
if let Some(cached) = self.cache.recall(&cache_digest).await? {
return Ok(cached);
}
let local = downloader.download(resolved).await?;
for layer in &local.layers {
debug!(
"process layer digest={} compression={:?}",
&layer.digest, layer.compression,
);
let whiteouts = self.process_layer_whiteout(layer, image_dir).await?;
debug!(
"process layer digest={} whiteouts={:?}",
&layer.digest, whiteouts
);
let mut archive = layer.archive().await?;
let mut entries = archive.entries()?;
while let Some(entry) = entries.next().await {
let mut entry = entry?;
let path = entry.path()?;
let mut maybe_whiteout_path_str =
path.to_str().map(|x| x.to_string()).unwrap_or_default();
if whiteouts.contains(&maybe_whiteout_path_str) {
continue;
}
maybe_whiteout_path_str.push('/');
if whiteouts.contains(&maybe_whiteout_path_str) {
continue;
}
let Some(name) = path.file_name() else {
return Err(anyhow!("unable to get file name"));
};
let Some(name) = name.to_str() else {
return Err(anyhow!("unable to get file name as string"));
};
if name.starts_with(".wh.") {
continue;
} else {
self.process_write_entry(&mut entry, layer, image_dir)
.await?;
}
}
}
for layer in &local.layers {
if layer.path.exists() {
fs::remove_file(&layer.path).await?;
}
}
self.squash(image_dir, squash_file)?;
let info = ImageInfo::new(
squash_file.to_path_buf(),
local.image.manifest,
local.config,
)?;
self.cache.store(&cache_digest, &info).await
}
async fn process_layer_whiteout(
&self,
layer: &OciImageLayer,
image_dir: &Path,
) -> Result<Vec<String>> {
let mut whiteouts = Vec::new();
let mut archive = layer.archive().await?;
let mut entries = archive.entries()?;
while let Some(entry) = entries.next().await {
let entry = entry?;
let path = entry.path()?;
let Some(name) = path.file_name() else {
return Err(anyhow!("unable to get file name"));
};
let Some(name) = name.to_str() else {
return Err(anyhow!("unable to get file name as string"));
};
if name.starts_with(".wh.") {
let path = self
.process_whiteout_entry(&entry, name, layer, image_dir)
.await?;
if let Some(path) = path {
whiteouts.push(path);
}
}
}
Ok(whiteouts)
}
async fn process_whiteout_entry(
&self,
entry: &Entry<Archive<Pin<Box<dyn AsyncRead + Send>>>>,
name: &str,
layer: &OciImageLayer,
image_dir: &Path,
) -> Result<Option<String>> {
let path = entry.path()?;
let mut dst = self.check_safe_entry(path.clone(), image_dir)?;
dst.pop();
let mut path = path.to_path_buf();
path.pop();
let opaque = name == ".wh..wh..opq";
if !opaque {
let file = &name[4..];
dst.push(file);
path.push(file);
self.check_safe_path(&dst, image_dir)?;
}
trace!("whiteout entry layer={} path={:?}", &layer.digest, path,);
let whiteout = path
.to_str()
.ok_or(anyhow!("unable to convert path to string"))?
.to_string();
if opaque {
if dst.is_dir() {
let mut reader = fs::read_dir(dst).await?;
while let Some(entry) = reader.next_entry().await? {
let path = entry.path();
if path.is_symlink() || path.is_file() {
fs::remove_file(&path).await?;
} else if path.is_dir() {
fs::remove_dir_all(&path).await?;
} else {
return Err(anyhow!("opaque whiteout entry did not exist"));
}
}
} else {
debug!(
"whiteout opaque entry missing locally layer={} path={:?} local={:?}",
&layer.digest,
entry.path()?,
dst,
);
}
} else if dst.is_file() || dst.is_symlink() {
fs::remove_file(&dst).await?;
} else if dst.is_dir() {
fs::remove_dir_all(&dst).await?;
} else {
debug!(
"whiteout entry missing locally layer={} path={:?} local={:?}",
&layer.digest,
entry.path()?,
dst,
);
}
Ok(if opaque { None } else { Some(whiteout) })
}
async fn process_write_entry(
&self,
entry: &mut Entry<Archive<Pin<Box<dyn AsyncRead + Send>>>>,
layer: &OciImageLayer,
image_dir: &Path,
) -> Result<()> {
let uid = entry.header().uid()?;
let gid = entry.header().gid()?;
trace!(
"unpack entry layer={} path={:?} type={:?} uid={} gid={}",
&layer.digest,
entry.path()?,
entry.header().entry_type(),
uid,
gid,
);
entry.set_preserve_mtime(true);
entry.set_preserve_permissions(true);
entry.set_unpack_xattrs(true);
if let Some(path) = entry.unpack_in(image_dir).await? {
if !path.is_symlink() {
std::os::unix::fs::chown(path, Some(uid as u32), Some(gid as u32))?;
}
}
Ok(())
}
fn check_safe_entry(&self, path: Cow<Path>, image_dir: &Path) -> Result<PathBuf> {
let mut dst = image_dir.to_path_buf();
dst.push(path);
if let Some(name) = dst.file_name() {
if let Some(name) = name.to_str() {
if name.starts_with(".wh.") {
let copy = dst.clone();
dst.pop();
self.check_safe_path(&dst, image_dir)?;
return Ok(copy);
}
}
}
self.check_safe_path(&dst, image_dir)?;
Ok(dst)
}
fn check_safe_path(&self, dst: &Path, image_dir: &Path) -> Result<()> {
let resolved = path_clean::clean(dst);
if !resolved.starts_with(image_dir) {
return Err(anyhow!("layer attempts to work outside image dir"));
}
Ok(())
}
fn squash(&self, image_dir: &Path, squash_file: &Path) -> Result<()> {
let mut writer = FilesystemWriter::default();
writer.set_compressor(FilesystemCompressor::new(Compressor::Gzip, None)?);
let walk = WalkDir::new(image_dir).follow_links(false);
for entry in walk {
let entry = entry?;
let rel = entry
.path()
.strip_prefix(image_dir)?
.to_str()
.ok_or_else(|| anyhow!("failed to strip prefix of tmpdir"))?;
let rel = format!("/{}", rel);
trace!("squash write {}", rel);
let typ = entry.file_type();
let metadata = std::fs::symlink_metadata(entry.path())?;
let uid = metadata.uid();
let gid = metadata.gid();
let mode = metadata.permissions().mode();
let mtime = metadata.mtime();
if rel == "/" {
writer.set_root_uid(uid);
writer.set_root_gid(gid);
writer.set_root_mode(mode as u16);
continue;
}
let header = NodeHeader {
permissions: mode as u16,
uid,
gid,
mtime: mtime as u32,
};
if typ.is_symlink() {
let symlink = std::fs::read_link(entry.path())?;
let symlink = symlink
.to_str()
.ok_or_else(|| anyhow!("failed to read symlink"))?;
writer.push_symlink(symlink, rel, header)?;
} else if typ.is_dir() {
writer.push_dir(rel, header)?;
} else if typ.is_file() {
writer.push_file(ConsumingFileReader::new(entry.path()), rel, header)?;
} else if typ.is_block_device() {
let device = metadata.dev();
writer.push_block_device(device as u32, rel, header)?;
} else if typ.is_char_device() {
let device = metadata.dev();
writer.push_char_device(device as u32, rel, header)?;
} else if typ.is_fifo() {
writer.push_fifo(rel, header)?;
} else if typ.is_socket() {
writer.push_socket(rel, header)?;
} else {
return Err(anyhow!("invalid file type"));
}
}
let squash_file_path = squash_file
.to_str()
.ok_or_else(|| anyhow!("failed to convert squashfs string"))?;
let file = File::create(squash_file)?;
let mut bufwrite = BufWriter::new(file);
trace!("squash generate: {}", squash_file_path);
writer.write(&mut bufwrite)?;
std::fs::remove_dir_all(image_dir)?;
Ok(())
}
}
struct ConsumingFileReader {
path: PathBuf,
file: Option<File>,
}
impl ConsumingFileReader {
fn new(path: &Path) -> ConsumingFileReader {
ConsumingFileReader {
path: path.to_path_buf(),
file: None,
}
}
}
impl Read for ConsumingFileReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.file.is_none() {
self.file = Some(File::open(&self.path)?);
}
let Some(ref mut file) = self.file else {
return Err(std::io::Error::new(
ErrorKind::NotFound,
"file was not opened",
));
};
file.read(buf)
}
}
impl Drop for ConsumingFileReader {
fn drop(&mut self) {
let file = self.file.take();
drop(file);
if let Err(error) = std::fs::remove_file(&self.path) {
warn!("failed to delete consuming file {:?}: {}", self.path, error);
}
}
}

View File

@ -1,9 +1,17 @@
use crate::{
progress::{OciBoundProgress, OciProgressPhase},
schema::OciSchema,
};
use super::{
name::ImageName,
registry::{OciRegistryClient, OciRegistryPlatform},
registry::{OciPlatform, OciRegistryClient},
};
use std::{
fmt::Debug,
io::SeekFrom,
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
pin::Pin,
};
@ -12,20 +20,21 @@ use anyhow::{anyhow, Result};
use async_compression::tokio::bufread::{GzipDecoder, ZstdDecoder};
use log::debug;
use oci_spec::image::{
Descriptor, ImageConfiguration, ImageIndex, ImageManifest, MediaType, ToDockerV2S2,
Descriptor, DescriptorBuilder, ImageConfiguration, ImageIndex, ImageManifest, MediaType,
ToDockerV2S2,
};
use serde::de::DeserializeOwned;
use tokio::{
fs::File,
io::{AsyncRead, AsyncReadExt, BufReader, BufWriter},
fs::{self, File},
io::{AsyncRead, AsyncReadExt, AsyncSeekExt, BufReader, BufWriter},
};
use tokio_stream::StreamExt;
use tokio_tar::Archive;
pub struct OciImageDownloader {
pub struct OciImageFetcher {
seed: Option<PathBuf>,
storage: PathBuf,
platform: OciRegistryPlatform,
platform: OciPlatform,
progress: OciBoundProgress,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -37,16 +46,43 @@ pub enum OciImageLayerCompression {
#[derive(Clone, Debug)]
pub struct OciImageLayer {
pub metadata: Descriptor,
pub path: PathBuf,
pub digest: String,
pub compression: OciImageLayerCompression,
}
#[async_trait::async_trait]
pub trait OciImageLayerReader: AsyncRead + Sync {
async fn position(&mut self) -> Result<u64>;
}
#[async_trait::async_trait]
impl OciImageLayerReader for BufReader<File> {
async fn position(&mut self) -> Result<u64> {
Ok(self.seek(SeekFrom::Current(0)).await?)
}
}
#[async_trait::async_trait]
impl OciImageLayerReader for GzipDecoder<BufReader<File>> {
async fn position(&mut self) -> Result<u64> {
self.get_mut().position().await
}
}
#[async_trait::async_trait]
impl OciImageLayerReader for ZstdDecoder<BufReader<File>> {
async fn position(&mut self) -> Result<u64> {
self.get_mut().position().await
}
}
impl OciImageLayer {
pub async fn decompress(&self) -> Result<Pin<Box<dyn AsyncRead + Send>>> {
pub async fn decompress(&self) -> Result<Pin<Box<dyn OciImageLayerReader + Send>>> {
let file = File::open(&self.path).await?;
let reader = BufReader::new(file);
let reader: Pin<Box<dyn AsyncRead + Send>> = match self.compression {
let reader: Pin<Box<dyn OciImageLayerReader + Send>> = match self.compression {
OciImageLayerCompression::None => Box::pin(reader),
OciImageLayerCompression::Gzip => Box::pin(GzipDecoder::new(reader)),
OciImageLayerCompression::Zstd => Box::pin(ZstdDecoder::new(reader)),
@ -54,7 +90,7 @@ impl OciImageLayer {
Ok(reader)
}
pub async fn archive(&self) -> Result<Archive<Pin<Box<dyn AsyncRead + Send>>>> {
pub async fn archive(&self) -> Result<Archive<Pin<Box<dyn OciImageLayerReader + Send>>>> {
let decompress = self.decompress().await?;
Ok(Archive::new(decompress))
}
@ -64,33 +100,34 @@ impl OciImageLayer {
pub struct OciResolvedImage {
pub name: ImageName,
pub digest: String,
pub manifest: ImageManifest,
pub descriptor: Descriptor,
pub manifest: OciSchema<ImageManifest>,
}
#[derive(Clone, Debug)]
pub struct OciLocalImage {
pub image: OciResolvedImage,
pub config: ImageConfiguration,
pub config: OciSchema<ImageConfiguration>,
pub layers: Vec<OciImageLayer>,
}
impl OciImageDownloader {
impl OciImageFetcher {
pub fn new(
seed: Option<PathBuf>,
storage: PathBuf,
platform: OciRegistryPlatform,
) -> OciImageDownloader {
OciImageDownloader {
platform: OciPlatform,
progress: OciBoundProgress,
) -> OciImageFetcher {
OciImageFetcher {
seed,
storage,
platform,
progress,
}
}
async fn load_seed_json_blob<T: DeserializeOwned>(
async fn load_seed_json_blob<T: Clone + Debug + DeserializeOwned>(
&self,
descriptor: &Descriptor,
) -> Result<Option<T>> {
) -> Result<Option<OciSchema<T>>> {
let digest = descriptor.digest();
let Some((digest_type, digest_content)) = digest.split_once(':') else {
return Err(anyhow!("digest content was not properly formatted"));
@ -99,7 +136,10 @@ impl OciImageDownloader {
self.load_seed_json(&want).await
}
async fn load_seed_json<T: DeserializeOwned>(&self, want: &str) -> Result<Option<T>> {
async fn load_seed_json<T: Clone + Debug + DeserializeOwned>(
&self,
want: &str,
) -> Result<Option<OciSchema<T>>> {
let Some(ref seed) = self.seed else {
return Ok(None);
};
@ -111,10 +151,10 @@ impl OciImageDownloader {
let mut entry = entry?;
let path = String::from_utf8(entry.path_bytes().to_vec())?;
if path == want {
let mut content = String::new();
entry.read_to_string(&mut content).await?;
let data = serde_json::from_str::<T>(&content)?;
return Ok(Some(data));
let mut content = Vec::new();
entry.read_to_end(&mut content).await?;
let item = serde_json::from_slice::<T>(&content)?;
return Ok(Some(OciSchema::new(content, item)));
}
}
Ok(None)
@ -152,7 +192,7 @@ impl OciImageDownloader {
if let Some(index) = self.load_seed_json::<ImageIndex>("index.json").await? {
let mut found: Option<&Descriptor> = None;
for manifest in index.manifests() {
for manifest in index.item().manifests() {
let Some(annotations) = manifest.annotations() else {
continue;
};
@ -177,6 +217,13 @@ impl OciImageDownloader {
continue;
}
}
if let Some(ref digest) = image.digest {
if digest != manifest.digest() {
continue;
}
}
found = Some(manifest);
break;
}
@ -190,6 +237,7 @@ impl OciImageDownloader {
);
return Ok(OciResolvedImage {
name: image,
descriptor: found.clone(),
digest: found.digest().clone(),
manifest,
});
@ -198,37 +246,79 @@ impl OciImageDownloader {
}
let mut client = OciRegistryClient::new(image.registry_url()?, self.platform.clone())?;
let (manifest, digest) = client
.get_manifest_with_digest(&image.name, &image.reference)
let (manifest, descriptor, digest) = client
.get_manifest_with_digest(&image.name, image.reference.as_ref(), image.digest.as_ref())
.await?;
let descriptor = descriptor.unwrap_or_else(|| {
DescriptorBuilder::default()
.media_type(MediaType::ImageManifest)
.size(manifest.raw().len() as i64)
.digest(digest.clone())
.build()
.unwrap()
});
Ok(OciResolvedImage {
name: image,
descriptor,
digest,
manifest,
})
}
pub async fn download(&self, image: OciResolvedImage) -> Result<OciLocalImage> {
let config: ImageConfiguration;
pub async fn download(
&self,
image: &OciResolvedImage,
layer_dir: &Path,
) -> Result<OciLocalImage> {
let config: OciSchema<ImageConfiguration>;
self.progress
.update(|progress| {
progress.phase = OciProgressPhase::ConfigDownload;
})
.await;
let mut client = OciRegistryClient::new(image.name.registry_url()?, self.platform.clone())?;
if let Some(seeded) = self
.load_seed_json_blob::<ImageConfiguration>(image.manifest.config())
.load_seed_json_blob::<ImageConfiguration>(image.manifest.item().config())
.await?
{
config = seeded;
} else {
let config_bytes = client
.get_blob(&image.name.name, image.manifest.config())
.get_blob(&image.name.name, image.manifest.item().config())
.await?;
config = serde_json::from_slice(&config_bytes)?;
config = OciSchema::new(
config_bytes.to_vec(),
serde_json::from_slice(&config_bytes)?,
);
}
self.progress
.update(|progress| {
progress.phase = OciProgressPhase::LayerDownload;
for layer in image.manifest.item().layers() {
progress.add_layer(layer.digest());
}
})
.await;
let mut layers = Vec::new();
for layer in image.manifest.layers() {
layers.push(self.acquire_layer(&image.name, layer, &mut client).await?);
for layer in image.manifest.item().layers() {
self.progress
.update(|progress| {
progress.downloading_layer(layer.digest(), 0, layer.size() as u64);
})
.await;
layers.push(
self.acquire_layer(&image.name, layer, layer_dir, &mut client)
.await?,
);
self.progress
.update(|progress| {
progress.downloaded_layer(layer.digest(), layer.size() as u64);
})
.await;
}
Ok(OciLocalImage {
image,
image: image.clone(),
config,
layers,
})
@ -238,6 +328,7 @@ impl OciImageDownloader {
&self,
image: &ImageName,
layer: &Descriptor,
layer_dir: &Path,
client: &mut OciRegistryClient,
) -> Result<OciImageLayer> {
debug!(
@ -245,13 +336,15 @@ impl OciImageDownloader {
layer.digest(),
layer.size()
);
let mut layer_path = self.storage.clone();
let mut layer_path = layer_dir.to_path_buf();
layer_path.push(format!("{}.layer", layer.digest()));
let seeded = self.extract_seed_blob(layer, &layer_path).await?;
if !seeded {
let file = File::create(&layer_path).await?;
let size = client.write_blob_to_file(&image.name, layer, file).await?;
let size = client
.write_blob_to_file(&image.name, layer, file, Some(self.progress.clone()))
.await?;
if layer.size() as u64 != size {
return Err(anyhow!(
"downloaded layer size differs from size in manifest",
@ -259,6 +352,12 @@ impl OciImageDownloader {
}
}
let metadata = fs::metadata(&layer_path).await?;
if layer.size() as u64 != metadata.size() {
return Err(anyhow!("layer size differs from size in manifest",));
}
let mut media_type = layer.media_type().clone();
// docker layer compatibility
@ -273,6 +372,7 @@ impl OciImageDownloader {
other => return Err(anyhow!("found layer with unknown media type: {}", other)),
};
Ok(OciImageLayer {
metadata: layer.clone(),
path: layer_path,
digest: layer.digest().clone(),
compression,

View File

@ -1,5 +1,8 @@
pub mod cache;
pub mod compiler;
pub mod assemble;
pub mod fetch;
pub mod name;
pub mod packer;
pub mod progress;
pub mod registry;
pub mod schema;
pub mod vfs;

View File

@ -2,27 +2,39 @@ use anyhow::Result;
use std::fmt;
use url::Url;
const DOCKER_HUB_MIRROR: &str = "mirror.gcr.io";
const DEFAULT_IMAGE_TAG: &str = "latest";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ImageName {
pub hostname: String,
pub port: Option<u16>,
pub name: String,
pub reference: String,
pub reference: Option<String>,
pub digest: Option<String>,
}
impl fmt::Display for ImageName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(port) = self.port {
write!(
f,
"{}:{}/{}:{}",
self.hostname, port, self.name, self.reference
)
let mut suffix = String::new();
if let Some(ref reference) = self.reference {
suffix.push(':');
suffix.push_str(reference);
}
if let Some(ref digest) = self.digest {
suffix.push('@');
suffix.push_str(digest);
}
if ImageName::DOCKER_HUB_MIRROR == self.hostname && self.port.is_none() {
if self.name.starts_with("library/") {
write!(f, "{}{}", &self.name[8..], suffix)
} else {
write!(f, "{}{}", self.name, suffix)
}
} else if let Some(port) = self.port {
write!(f, "{}:{}/{}{}", self.hostname, port, self.name, suffix)
} else {
write!(f, "{}/{}:{}", self.hostname, self.name, self.reference)
write!(f, "{}/{}{}", self.hostname, self.name, suffix)
}
}
}
@ -35,13 +47,21 @@ impl Default for ImageName {
}
impl ImageName {
pub const DOCKER_HUB_MIRROR: &'static str = "mirror.gcr.io";
pub const DEFAULT_IMAGE_TAG: &'static str = "latest";
pub fn parse(name: &str) -> Result<Self> {
let full_name = name.to_string();
let name = full_name.clone();
let (mut hostname, mut name) = name
.split_once('/')
.map(|x| (x.0.to_string(), x.1.to_string()))
.unwrap_or_else(|| (DOCKER_HUB_MIRROR.to_string(), format!("library/{}", name)));
.unwrap_or_else(|| {
(
ImageName::DOCKER_HUB_MIRROR.to_string(),
format!("library/{}", name),
)
});
// heuristic to find any docker hub image formats
// that may be in the hostname format. for example:
@ -49,7 +69,7 @@ impl ImageName {
// and neither will abc/hello/xyz:latest
if !hostname.contains('.') && full_name.chars().filter(|x| *x == '/').count() == 1 {
name = format!("{}/{}", hostname, name);
hostname = DOCKER_HUB_MIRROR.to_string();
hostname = ImageName::DOCKER_HUB_MIRROR.to_string();
}
let (hostname, port) = if let Some((hostname, port)) = hostname
@ -60,15 +80,54 @@ impl ImageName {
} else {
(hostname, None)
};
let (name, reference) = name
.split_once(':')
.map(|x| (x.0.to_string(), x.1.to_string()))
.unwrap_or((name.to_string(), DEFAULT_IMAGE_TAG.to_string()));
let name_has_digest = if name.contains('@') {
let digest_start = name.chars().position(|c| c == '@');
let ref_start = name.chars().position(|c| c == ':');
if let (Some(digest_start), Some(ref_start)) = (digest_start, ref_start) {
digest_start < ref_start
} else {
true
}
} else {
false
};
let (name, digest) = if name_has_digest {
name.split_once('@')
.map(|(name, digest)| (name.to_string(), Some(digest.to_string())))
.unwrap_or_else(|| (name, None))
} else {
(name, None)
};
let (name, reference) = if name.contains(':') {
name.split_once(':')
.map(|(name, reference)| (name.to_string(), Some(reference.to_string())))
.unwrap_or((name, None))
} else {
(name, None)
};
let (reference, digest) = if let Some(reference) = reference {
if let Some(digest) = digest {
(Some(reference), Some(digest))
} else {
reference
.split_once('@')
.map(|(reff, digest)| (Some(reff.to_string()), Some(digest.to_string())))
.unwrap_or_else(|| (Some(reference), None))
}
} else {
(None, digest)
};
Ok(ImageName {
hostname,
port,
name,
reference,
digest,
})
}

View File

@ -0,0 +1,236 @@
use std::{os::unix::fs::MetadataExt, path::Path, process::Stdio, sync::Arc};
use super::OciPackedFormat;
use crate::{progress::OciBoundProgress, vfs::VfsTree};
use anyhow::{anyhow, Result};
use log::warn;
use tokio::{
fs::{self, File},
io::BufWriter,
pin,
process::{Child, Command},
select,
};
#[derive(Debug, Clone, Copy)]
pub enum OciPackerBackendType {
MkSquashfs,
MkfsErofs,
Tar,
}
impl OciPackerBackendType {
pub fn format(&self) -> OciPackedFormat {
match self {
OciPackerBackendType::MkSquashfs => OciPackedFormat::Squashfs,
OciPackerBackendType::MkfsErofs => OciPackedFormat::Erofs,
OciPackerBackendType::Tar => OciPackedFormat::Tar,
}
}
pub fn create(&self) -> Box<dyn OciPackerBackend> {
match self {
OciPackerBackendType::MkSquashfs => {
Box::new(OciPackerMkSquashfs {}) as Box<dyn OciPackerBackend>
}
OciPackerBackendType::MkfsErofs => {
Box::new(OciPackerMkfsErofs {}) as Box<dyn OciPackerBackend>
}
OciPackerBackendType::Tar => Box::new(OciPackerTar {}) as Box<dyn OciPackerBackend>,
}
}
}
#[async_trait::async_trait]
pub trait OciPackerBackend: Send + Sync {
async fn pack(&self, progress: OciBoundProgress, vfs: Arc<VfsTree>, file: &Path) -> Result<()>;
}
pub struct OciPackerMkSquashfs {}
#[async_trait::async_trait]
impl OciPackerBackend for OciPackerMkSquashfs {
async fn pack(&self, progress: OciBoundProgress, vfs: Arc<VfsTree>, file: &Path) -> Result<()> {
progress
.update(|progress| {
progress.start_packing();
})
.await;
let child = Command::new("mksquashfs")
.arg("-")
.arg(file)
.arg("-comp")
.arg("gzip")
.arg("-tar")
.stdin(Stdio::piped())
.stderr(Stdio::null())
.stdout(Stdio::null())
.spawn()?;
let mut child = ChildProcessKillGuard(child);
let stdin = child
.0
.stdin
.take()
.ok_or(anyhow!("unable to acquire stdin stream"))?;
let mut writer = Some(tokio::task::spawn(async move {
if let Err(error) = vfs.write_to_tar(stdin).await {
warn!("failed to write tar: {}", error);
return Err(error);
}
Ok(())
}));
let wait = child.0.wait();
pin!(wait);
let status_result = loop {
if let Some(inner) = writer.as_mut() {
select! {
x = inner => {
writer = None;
match x {
Ok(_) => {},
Err(error) => {
return Err(error.into());
}
}
},
status = &mut wait => {
break status;
}
};
} else {
select! {
status = &mut wait => {
break status;
}
};
}
};
if let Some(writer) = writer {
writer.await??;
}
let status = status_result?;
if !status.success() {
Err(anyhow!(
"mksquashfs failed with exit code: {}",
status.code().unwrap()
))
} else {
let metadata = fs::metadata(&file).await?;
progress
.update(|progress| progress.complete(metadata.size()))
.await;
Ok(())
}
}
}
pub struct OciPackerMkfsErofs {}
#[async_trait::async_trait]
impl OciPackerBackend for OciPackerMkfsErofs {
async fn pack(&self, progress: OciBoundProgress, vfs: Arc<VfsTree>, file: &Path) -> Result<()> {
progress
.update(|progress| {
progress.start_packing();
})
.await;
let child = Command::new("mkfs.erofs")
.arg("-L")
.arg("root")
.arg("--tar=-")
.arg(file)
.stdin(Stdio::piped())
.stderr(Stdio::null())
.stdout(Stdio::null())
.spawn()?;
let mut child = ChildProcessKillGuard(child);
let stdin = child
.0
.stdin
.take()
.ok_or(anyhow!("unable to acquire stdin stream"))?;
let mut writer = Some(tokio::task::spawn(
async move { vfs.write_to_tar(stdin).await },
));
let wait = child.0.wait();
pin!(wait);
let status_result = loop {
if let Some(inner) = writer.as_mut() {
select! {
x = inner => {
match x {
Ok(_) => {
writer = None;
},
Err(error) => {
return Err(error.into());
}
}
},
status = &mut wait => {
break status;
}
};
} else {
select! {
status = &mut wait => {
break status;
}
};
}
};
if let Some(writer) = writer {
writer.await??;
}
let status = status_result?;
if !status.success() {
Err(anyhow!(
"mkfs.erofs failed with exit code: {}",
status.code().unwrap()
))
} else {
let metadata = fs::metadata(&file).await?;
progress
.update(|progress| {
progress.complete(metadata.size());
})
.await;
Ok(())
}
}
}
pub struct OciPackerTar {}
#[async_trait::async_trait]
impl OciPackerBackend for OciPackerTar {
async fn pack(&self, progress: OciBoundProgress, vfs: Arc<VfsTree>, file: &Path) -> Result<()> {
progress
.update(|progress| {
progress.start_packing();
})
.await;
let output = File::create(file).await?;
let output = BufWriter::new(output);
vfs.write_to_tar(output).await?;
let metadata = fs::metadata(file).await?;
progress
.update(|progress| {
progress.complete(metadata.size());
})
.await;
Ok(())
}
}
struct ChildProcessKillGuard(Child);
impl Drop for ChildProcessKillGuard {
fn drop(&mut self) {
let _ = self.0.start_kill();
}
}

View File

@ -0,0 +1,220 @@
use crate::{
name::ImageName,
packer::{OciPackedFormat, OciPackedImage},
schema::OciSchema,
};
use anyhow::Result;
use log::{debug, error};
use oci_spec::image::{
Descriptor, ImageConfiguration, ImageIndex, ImageIndexBuilder, ImageManifest, MediaType,
ANNOTATION_REF_NAME,
};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use tokio::{fs, sync::RwLock};
#[derive(Clone)]
pub struct OciPackerCache {
cache_dir: PathBuf,
index: Arc<RwLock<ImageIndex>>,
}
const ANNOTATION_IMAGE_NAME: &str = "io.containerd.image.name";
const ANNOTATION_OCI_PACKER_FORMAT: &str = "dev.krata.oci.packer.format";
impl OciPackerCache {
pub async fn new(cache_dir: &Path) -> Result<OciPackerCache> {
let index = ImageIndexBuilder::default()
.schema_version(2u32)
.media_type(MediaType::ImageIndex)
.manifests(Vec::new())
.build()?;
let cache = OciPackerCache {
cache_dir: cache_dir.to_path_buf(),
index: Arc::new(RwLock::new(index)),
};
{
let mut mutex = cache.index.write().await;
*mutex = cache.load_index().await?;
}
Ok(cache)
}
pub async fn list(&self) -> Result<Vec<Descriptor>> {
let index = self.index.read().await;
Ok(index.manifests().clone())
}
pub async fn recall(
&self,
name: ImageName,
digest: &str,
format: OciPackedFormat,
) -> Result<Option<OciPackedImage>> {
let index = self.index.read().await;
let mut descriptor: Option<Descriptor> = None;
for manifest in index.manifests() {
if manifest.digest() == digest
&& manifest
.annotations()
.as_ref()
.and_then(|x| x.get(ANNOTATION_OCI_PACKER_FORMAT))
.map(|x| x.as_str())
== Some(format.extension())
{
descriptor = Some(manifest.clone());
break;
}
}
let Some(descriptor) = descriptor else {
return Ok(None);
};
let mut fs_path = self.cache_dir.clone();
let mut config_path = self.cache_dir.clone();
let mut manifest_path = self.cache_dir.clone();
fs_path.push(format!("{}.{}", digest, format.extension()));
manifest_path.push(format!("{}.manifest.json", digest));
config_path.push(format!("{}.config.json", digest));
if fs_path.exists() && manifest_path.exists() && config_path.exists() {
let image_metadata = fs::metadata(&fs_path).await?;
let manifest_metadata = fs::metadata(&manifest_path).await?;
let config_metadata = fs::metadata(&config_path).await?;
if image_metadata.is_file() && manifest_metadata.is_file() && config_metadata.is_file()
{
let manifest_bytes = fs::read(&manifest_path).await?;
let manifest: ImageManifest = serde_json::from_slice(&manifest_bytes)?;
let config_bytes = fs::read(&config_path).await?;
let config: ImageConfiguration = serde_json::from_slice(&config_bytes)?;
debug!("cache hit digest={}", digest);
Ok(Some(OciPackedImage::new(
name,
digest.to_string(),
fs_path.clone(),
format,
descriptor,
OciSchema::new(config_bytes, config),
OciSchema::new(manifest_bytes, manifest),
)))
} else {
Ok(None)
}
} else {
debug!("cache miss digest={}", digest);
Ok(None)
}
}
pub async fn store(&self, packed: OciPackedImage) -> Result<OciPackedImage> {
let mut index = self.index.write().await;
let mut manifests = index.manifests().clone();
debug!("cache store digest={}", packed.digest);
let mut fs_path = self.cache_dir.clone();
let mut manifest_path = self.cache_dir.clone();
let mut config_path = self.cache_dir.clone();
fs_path.push(format!("{}.{}", packed.digest, packed.format.extension()));
manifest_path.push(format!("{}.manifest.json", packed.digest));
config_path.push(format!("{}.config.json", packed.digest));
if fs::rename(&packed.path, &fs_path).await.is_err() {
fs::copy(&packed.path, &fs_path).await?;
fs::remove_file(&packed.path).await?;
}
fs::write(&config_path, packed.config.raw()).await?;
fs::write(&manifest_path, packed.manifest.raw()).await?;
manifests.retain(|item| {
if item.digest() != &packed.digest {
return true;
}
let Some(format) = item
.annotations()
.as_ref()
.and_then(|x| x.get(ANNOTATION_OCI_PACKER_FORMAT))
.map(|x| x.as_str())
else {
return true;
};
if format != packed.format.extension() {
return true;
}
false
});
let mut descriptor = packed.descriptor.clone();
let mut annotations = descriptor.annotations().clone().unwrap_or_default();
annotations.insert(
ANNOTATION_OCI_PACKER_FORMAT.to_string(),
packed.format.extension().to_string(),
);
let image_name = packed.name.to_string();
annotations.insert(ANNOTATION_IMAGE_NAME.to_string(), image_name);
let image_ref = packed.name.reference.clone();
if let Some(image_ref) = image_ref {
annotations.insert(ANNOTATION_REF_NAME.to_string(), image_ref);
}
descriptor.set_annotations(Some(annotations));
manifests.push(descriptor.clone());
index.set_manifests(manifests);
self.save_index(&index).await?;
let packed = OciPackedImage::new(
packed.name,
packed.digest,
fs_path.clone(),
packed.format,
descriptor,
packed.config,
packed.manifest,
);
Ok(packed)
}
async fn save_empty_index(&self) -> Result<ImageIndex> {
let index = ImageIndexBuilder::default()
.schema_version(2u32)
.media_type(MediaType::ImageIndex)
.manifests(Vec::new())
.build()?;
self.save_index(&index).await?;
Ok(index)
}
async fn load_index(&self) -> Result<ImageIndex> {
let mut index_path = self.cache_dir.clone();
index_path.push("index.json");
if !index_path.exists() {
self.save_empty_index().await?;
}
let content = fs::read_to_string(&index_path).await?;
let index = match serde_json::from_str::<ImageIndex>(&content) {
Ok(index) => index,
Err(error) => {
error!("image index was corrupted, creating a new one: {}", error);
self.save_empty_index().await?
}
};
Ok(index)
}
async fn save_index(&self, index: &ImageIndex) -> Result<()> {
let mut encoded = serde_json::to_string_pretty(index)?;
encoded.push('\n');
let mut index_path = self.cache_dir.clone();
index_path.push("index.json");
fs::write(&index_path, encoded).await?;
Ok(())
}
}

View File

@ -0,0 +1,69 @@
use std::path::PathBuf;
use crate::{name::ImageName, schema::OciSchema};
use self::backend::OciPackerBackendType;
use oci_spec::image::{Descriptor, ImageConfiguration, ImageManifest};
pub mod backend;
pub mod cache;
pub mod service;
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub enum OciPackedFormat {
#[default]
Squashfs,
Erofs,
Tar,
}
impl OciPackedFormat {
pub fn extension(&self) -> &str {
match self {
OciPackedFormat::Squashfs => "squashfs",
OciPackedFormat::Erofs => "erofs",
OciPackedFormat::Tar => "tar",
}
}
pub fn backend(&self) -> OciPackerBackendType {
match self {
OciPackedFormat::Squashfs => OciPackerBackendType::MkSquashfs,
OciPackedFormat::Erofs => OciPackerBackendType::MkfsErofs,
OciPackedFormat::Tar => OciPackerBackendType::Tar,
}
}
}
#[derive(Clone)]
pub struct OciPackedImage {
pub name: ImageName,
pub digest: String,
pub path: PathBuf,
pub format: OciPackedFormat,
pub descriptor: Descriptor,
pub config: OciSchema<ImageConfiguration>,
pub manifest: OciSchema<ImageManifest>,
}
impl OciPackedImage {
pub fn new(
name: ImageName,
digest: String,
path: PathBuf,
format: OciPackedFormat,
descriptor: Descriptor,
config: OciSchema<ImageConfiguration>,
manifest: OciSchema<ImageManifest>,
) -> OciPackedImage {
OciPackedImage {
name,
digest,
path,
format,
descriptor,
config,
manifest,
}
}
}

View File

@ -0,0 +1,278 @@
use std::{
collections::{hash_map::Entry, HashMap},
fmt::Display,
path::{Path, PathBuf},
sync::Arc,
};
use anyhow::{anyhow, Result};
use oci_spec::image::Descriptor;
use tokio::{
sync::{watch, Mutex},
task::JoinHandle,
};
use crate::{
assemble::OciImageAssembler,
fetch::{OciImageFetcher, OciResolvedImage},
name::ImageName,
progress::{OciBoundProgress, OciProgress, OciProgressContext},
registry::OciPlatform,
};
use log::{error, info, warn};
use super::{cache::OciPackerCache, OciPackedFormat, OciPackedImage};
pub struct OciPackerTask {
progress: OciBoundProgress,
watch: watch::Sender<Option<Result<OciPackedImage>>>,
task: JoinHandle<()>,
}
#[derive(Clone)]
pub struct OciPackerService {
seed: Option<PathBuf>,
platform: OciPlatform,
cache: OciPackerCache,
tasks: Arc<Mutex<HashMap<OciPackerTaskKey, OciPackerTask>>>,
}
impl OciPackerService {
pub async fn new(
seed: Option<PathBuf>,
cache_dir: &Path,
platform: OciPlatform,
) -> Result<OciPackerService> {
Ok(OciPackerService {
seed,
cache: OciPackerCache::new(cache_dir).await?,
platform,
tasks: Arc::new(Mutex::new(HashMap::new())),
})
}
pub async fn list(&self) -> Result<Vec<Descriptor>> {
self.cache.list().await
}
pub async fn recall(
&self,
digest: &str,
format: OciPackedFormat,
) -> Result<Option<OciPackedImage>> {
if digest.contains('/') || digest.contains('\\') || digest.contains("..") {
return Ok(None);
}
self.cache
.recall(ImageName::parse("cached:latest")?, digest, format)
.await
}
pub async fn request(
&self,
name: ImageName,
format: OciPackedFormat,
overwrite: bool,
progress_context: OciProgressContext,
) -> Result<OciPackedImage> {
let progress = OciProgress::new();
let progress = OciBoundProgress::new(progress_context.clone(), progress);
let fetcher =
OciImageFetcher::new(self.seed.clone(), self.platform.clone(), progress.clone());
let resolved = fetcher.resolve(name.clone()).await?;
let key = OciPackerTaskKey {
digest: resolved.digest.clone(),
format,
};
let (progress_copy_task, mut receiver) = match self.tasks.lock().await.entry(key.clone()) {
Entry::Occupied(entry) => {
let entry = entry.get();
(
Some(entry.progress.also_update(progress_context).await),
entry.watch.subscribe(),
)
}
Entry::Vacant(entry) => {
let task = self
.clone()
.launch(
name,
key.clone(),
format,
overwrite,
resolved,
fetcher,
progress.clone(),
)
.await;
let (watch, receiver) = watch::channel(None);
let task = OciPackerTask {
progress: progress.clone(),
task,
watch,
};
entry.insert(task);
(None, receiver)
}
};
let _progress_task_guard = scopeguard::guard(progress_copy_task, |task| {
if let Some(task) = task {
task.abort();
}
});
let _task_cancel_guard = scopeguard::guard(self.clone(), |service| {
service.maybe_cancel_task(key);
});
loop {
receiver.changed().await?;
let current = receiver.borrow_and_update();
if current.is_some() {
return current
.as_ref()
.map(|x| x.as_ref().map_err(|err| anyhow!("{}", err)).cloned())
.unwrap();
}
}
}
#[allow(clippy::too_many_arguments)]
async fn launch(
self,
name: ImageName,
key: OciPackerTaskKey,
format: OciPackedFormat,
overwrite: bool,
resolved: OciResolvedImage,
fetcher: OciImageFetcher,
progress: OciBoundProgress,
) -> JoinHandle<()> {
info!("started packer task {}", key);
tokio::task::spawn(async move {
let _task_drop_guard =
scopeguard::guard((key.clone(), self.clone()), |(key, service)| {
service.ensure_task_gone(key);
});
if let Err(error) = self
.task(
name,
key.clone(),
format,
overwrite,
resolved,
fetcher,
progress,
)
.await
{
self.finish(&key, Err(error)).await;
}
})
}
#[allow(clippy::too_many_arguments)]
async fn task(
&self,
name: ImageName,
key: OciPackerTaskKey,
format: OciPackedFormat,
overwrite: bool,
resolved: OciResolvedImage,
fetcher: OciImageFetcher,
progress: OciBoundProgress,
) -> Result<()> {
if !overwrite {
if let Some(cached) = self
.cache
.recall(name.clone(), &resolved.digest, format)
.await?
{
self.finish(&key, Ok(cached)).await;
return Ok(());
}
}
let assembler =
OciImageAssembler::new(fetcher, resolved, progress.clone(), None, None).await?;
let assembled = assembler.assemble().await?;
let mut file = assembled
.tmp_dir
.clone()
.ok_or(anyhow!("tmp_dir was missing when packing image"))?;
file.push("image.pack");
let target = file.clone();
let packer = format.backend().create();
packer
.pack(progress, assembled.vfs.clone(), &target)
.await?;
let packed = OciPackedImage::new(
name,
assembled.digest.clone(),
file,
format,
assembled.descriptor.clone(),
assembled.config.clone(),
assembled.manifest.clone(),
);
let packed = self.cache.store(packed).await?;
self.finish(&key, Ok(packed)).await;
Ok(())
}
async fn finish(&self, key: &OciPackerTaskKey, result: Result<OciPackedImage>) {
let Some(task) = self.tasks.lock().await.remove(key) else {
error!("packer task {} was not found when task completed", key);
return;
};
match result.as_ref() {
Ok(_) => {
info!("completed packer task {}", key);
}
Err(err) => {
warn!("packer task {} failed: {}", key, err);
}
}
task.watch.send_replace(Some(result));
}
fn maybe_cancel_task(self, key: OciPackerTaskKey) {
tokio::task::spawn(async move {
let tasks = self.tasks.lock().await;
if let Some(task) = tasks.get(&key) {
if task.watch.is_closed() {
task.task.abort();
}
}
});
}
fn ensure_task_gone(self, key: OciPackerTaskKey) {
tokio::task::spawn(async move {
let mut tasks = self.tasks.lock().await;
if let Some(task) = tasks.remove(&key) {
warn!("aborted packer task {}", key);
task.watch.send_replace(Some(Err(anyhow!("task aborted"))));
}
});
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
struct OciPackerTaskKey {
digest: String,
format: OciPackedFormat,
}
impl Display for OciPackerTaskKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}:{}", self.digest, self.format.extension()))
}
}

238
crates/oci/src/progress.rs Normal file
View File

@ -0,0 +1,238 @@
use indexmap::IndexMap;
use std::sync::Arc;
use tokio::{
sync::{watch, Mutex},
task::JoinHandle,
};
#[derive(Clone, Debug)]
pub struct OciProgress {
pub phase: OciProgressPhase,
pub digest: Option<String>,
pub layers: IndexMap<String, OciProgressLayer>,
pub indication: OciProgressIndication,
}
impl Default for OciProgress {
fn default() -> Self {
Self::new()
}
}
impl OciProgress {
pub fn new() -> Self {
OciProgress {
phase: OciProgressPhase::Started,
digest: None,
layers: IndexMap::new(),
indication: OciProgressIndication::Hidden,
}
}
pub fn start_resolving(&mut self) {
self.phase = OciProgressPhase::Resolving;
self.indication = OciProgressIndication::Spinner { message: None };
}
pub fn resolved(&mut self, digest: &str) {
self.digest = Some(digest.to_string());
self.indication = OciProgressIndication::Hidden;
}
pub fn add_layer(&mut self, id: &str) {
self.layers.insert(
id.to_string(),
OciProgressLayer {
id: id.to_string(),
phase: OciProgressLayerPhase::Waiting,
indication: OciProgressIndication::Spinner { message: None },
},
);
}
pub fn downloading_layer(&mut self, id: &str, downloaded: u64, total: u64) {
if let Some(entry) = self.layers.get_mut(id) {
entry.phase = OciProgressLayerPhase::Downloading;
entry.indication = OciProgressIndication::ProgressBar {
message: None,
current: downloaded,
total,
bytes: true,
};
}
}
pub fn downloaded_layer(&mut self, id: &str, total: u64) {
if let Some(entry) = self.layers.get_mut(id) {
entry.phase = OciProgressLayerPhase::Downloaded;
entry.indication = OciProgressIndication::Completed {
message: None,
total: Some(total),
bytes: true,
};
}
}
pub fn start_assemble(&mut self) {
self.phase = OciProgressPhase::Assemble;
self.indication = OciProgressIndication::Hidden;
}
pub fn start_extracting_layer(&mut self, id: &str) {
if let Some(entry) = self.layers.get_mut(id) {
entry.phase = OciProgressLayerPhase::Extracting;
entry.indication = OciProgressIndication::Spinner { message: None };
}
}
pub fn extracting_layer(&mut self, id: &str, file: &str) {
if let Some(entry) = self.layers.get_mut(id) {
entry.phase = OciProgressLayerPhase::Extracting;
entry.indication = OciProgressIndication::Spinner {
message: Some(file.to_string()),
};
}
}
pub fn extracted_layer(&mut self, id: &str, count: u64, total_size: u64) {
if let Some(entry) = self.layers.get_mut(id) {
entry.phase = OciProgressLayerPhase::Extracted;
entry.indication = OciProgressIndication::Completed {
message: Some(format!("{} files", count)),
total: Some(total_size),
bytes: true,
};
}
}
pub fn start_packing(&mut self) {
self.phase = OciProgressPhase::Pack;
for layer in self.layers.values_mut() {
layer.indication = OciProgressIndication::Hidden;
}
self.indication = OciProgressIndication::Spinner { message: None };
}
pub fn complete(&mut self, size: u64) {
self.phase = OciProgressPhase::Complete;
self.indication = OciProgressIndication::Completed {
message: None,
total: Some(size),
bytes: true,
}
}
}
#[derive(Clone, Debug)]
pub enum OciProgressPhase {
Started,
Resolving,
Resolved,
ConfigDownload,
LayerDownload,
Assemble,
Pack,
Complete,
}
#[derive(Clone, Debug)]
pub enum OciProgressIndication {
Hidden,
ProgressBar {
message: Option<String>,
current: u64,
total: u64,
bytes: bool,
},
Spinner {
message: Option<String>,
},
Completed {
message: Option<String>,
total: Option<u64>,
bytes: bool,
},
}
#[derive(Clone, Debug)]
pub struct OciProgressLayer {
pub id: String,
pub phase: OciProgressLayerPhase,
pub indication: OciProgressIndication,
}
#[derive(Clone, Debug)]
pub enum OciProgressLayerPhase {
Waiting,
Downloading,
Downloaded,
Extracting,
Extracted,
}
#[derive(Clone)]
pub struct OciProgressContext {
sender: watch::Sender<OciProgress>,
}
impl OciProgressContext {
pub fn create() -> (OciProgressContext, watch::Receiver<OciProgress>) {
let (sender, receiver) = watch::channel(OciProgress::new());
(OciProgressContext::new(sender), receiver)
}
pub fn new(sender: watch::Sender<OciProgress>) -> OciProgressContext {
OciProgressContext { sender }
}
pub fn update(&self, progress: &OciProgress) {
let _ = self.sender.send(progress.clone());
}
pub fn subscribe(&self) -> watch::Receiver<OciProgress> {
self.sender.subscribe()
}
}
#[derive(Clone)]
pub struct OciBoundProgress {
context: OciProgressContext,
instance: Arc<Mutex<OciProgress>>,
}
impl OciBoundProgress {
pub fn new(context: OciProgressContext, progress: OciProgress) -> OciBoundProgress {
OciBoundProgress {
context,
instance: Arc::new(Mutex::new(progress)),
}
}
pub async fn update(&self, function: impl FnOnce(&mut OciProgress)) {
let mut progress = self.instance.lock().await;
function(&mut progress);
self.context.update(&progress);
}
pub fn update_blocking(&self, function: impl FnOnce(&mut OciProgress)) {
let mut progress = self.instance.blocking_lock();
function(&mut progress);
self.context.update(&progress);
}
pub async fn also_update(&self, context: OciProgressContext) -> JoinHandle<()> {
let progress = self.instance.lock().await.clone();
context.update(&progress);
let mut receiver = self.context.subscribe();
tokio::task::spawn(async move {
while (receiver.changed().await).is_ok() {
context
.sender
.send_replace(receiver.borrow_and_update().clone());
}
})
}
}

View File

@ -7,26 +7,28 @@ use reqwest::{Client, RequestBuilder, Response, StatusCode};
use tokio::{fs::File, io::AsyncWriteExt};
use url::Url;
use crate::{name::ImageName, progress::OciBoundProgress, schema::OciSchema};
#[derive(Clone, Debug)]
pub struct OciRegistryPlatform {
pub struct OciPlatform {
pub os: Os,
pub arch: Arch,
}
impl OciRegistryPlatform {
impl OciPlatform {
#[cfg(target_arch = "x86_64")]
const CURRENT_ARCH: Arch = Arch::Amd64;
#[cfg(target_arch = "aarch64")]
const CURRENT_ARCH: Arch = Arch::ARM64;
pub fn new(os: Os, arch: Arch) -> OciRegistryPlatform {
OciRegistryPlatform { os, arch }
pub fn new(os: Os, arch: Arch) -> OciPlatform {
OciPlatform { os, arch }
}
pub fn current() -> OciRegistryPlatform {
OciRegistryPlatform {
pub fn current() -> OciPlatform {
OciPlatform {
os: Os::Linux,
arch: OciRegistryPlatform::CURRENT_ARCH,
arch: OciPlatform::CURRENT_ARCH,
}
}
}
@ -34,12 +36,12 @@ impl OciRegistryPlatform {
pub struct OciRegistryClient {
agent: Client,
url: Url,
platform: OciRegistryPlatform,
platform: OciPlatform,
token: Option<String>,
}
impl OciRegistryClient {
pub fn new(url: Url, platform: OciRegistryPlatform) -> Result<OciRegistryClient> {
pub fn new(url: Url, platform: OciPlatform) -> Result<OciRegistryClient> {
Ok(OciRegistryClient {
agent: Client::new(),
url,
@ -138,6 +140,7 @@ impl OciRegistryClient {
name: N,
descriptor: &Descriptor,
mut dest: File,
progress: Option<OciBoundProgress>,
) -> Result<u64> {
let url = self.url.join(&format!(
"/v2/{}/blobs/{}",
@ -149,6 +152,18 @@ impl OciRegistryClient {
while let Some(chunk) = response.chunk().await? {
dest.write_all(&chunk).await?;
size += chunk.len() as u64;
if let Some(ref progress) = progress {
progress
.update(|progress| {
progress.downloading_layer(
descriptor.digest(),
size,
descriptor.size() as u64,
);
})
.await;
}
}
Ok(size)
}
@ -157,11 +172,11 @@ impl OciRegistryClient {
&mut self,
name: N,
reference: R,
) -> Result<(ImageManifest, String)> {
) -> Result<(OciSchema<ImageManifest>, String)> {
let url = self.url.join(&format!(
"/v2/{}/manifests/{}",
name.as_ref(),
reference.as_ref()
reference.as_ref(),
))?;
let accept = format!(
"{}, {}, {}, {}",
@ -179,20 +194,28 @@ impl OciRegistryClient {
.ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))?
.to_str()?
.to_string();
let manifest = serde_json::from_str(&response.text().await?)?;
Ok((manifest, digest))
let bytes = response.bytes().await?;
let manifest = serde_json::from_slice(&bytes)?;
Ok((OciSchema::new(bytes.to_vec(), manifest), digest))
}
pub async fn get_manifest_with_digest<N: AsRef<str>, R: AsRef<str>>(
&mut self,
name: N,
reference: R,
) -> Result<(ImageManifest, String)> {
let url = self.url.join(&format!(
"/v2/{}/manifests/{}",
name.as_ref(),
reference.as_ref()
))?;
reference: Option<R>,
digest: Option<N>,
) -> Result<(OciSchema<ImageManifest>, Option<Descriptor>, String)> {
let what = digest
.as_ref()
.map(|x| x.as_ref().to_string())
.unwrap_or_else(|| {
reference
.map(|x| x.as_ref().to_string())
.unwrap_or_else(|| ImageName::DEFAULT_IMAGE_TAG.to_string())
});
let url = self
.url
.join(&format!("/v2/{}/manifests/{}", name.as_ref(), what,))?;
let accept = format!(
"{}, {}, {}, {}",
MediaType::ImageManifest.to_docker_v2s2()?,
@ -215,18 +238,21 @@ impl OciRegistryClient {
let descriptor = self
.pick_manifest(index)
.ok_or_else(|| anyhow!("unable to pick manifest from index"))?;
return self
let (manifest, digest) = self
.get_raw_manifest_with_digest(name, descriptor.digest())
.await;
.await?;
return Ok((manifest, Some(descriptor), digest));
}
let digest = response
.headers()
.get("Docker-Content-Digest")
.ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))?
.to_str()?
.to_string();
let manifest = serde_json::from_str(&response.text().await?)?;
Ok((manifest, digest))
.and_then(|x| x.to_str().ok())
.map(|x| x.to_string())
.or_else(|| digest.map(|x: N| x.as_ref().to_string()))
.ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))?;
let bytes = response.bytes().await?;
let manifest = serde_json::from_slice(&bytes)?;
Ok((OciSchema::new(bytes.to_vec(), manifest), None, digest))
}
fn pick_manifest(&mut self, index: ImageIndex) -> Option<Descriptor> {

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