├── .gitignore ├── config ├── README.md ├── Cargo.toml └── src │ ├── lib.rs │ ├── firecracker.rs │ └── manager.rs ├── manager ├── src │ ├── network │ │ ├── mod.rs │ │ ├── allocator.rs │ │ ├── forwarding.rs │ │ └── allocation.rs │ ├── disk.rs │ ├── lib.rs │ └── instance.rs ├── Cargo.toml └── README.md ├── util ├── README.md ├── Cargo.toml └── src │ ├── network.rs │ ├── mount.rs │ ├── lib.rs │ └── fs.rs ├── runner ├── README.md ├── Cargo.toml └── src │ └── lib.rs ├── github ├── README.md ├── Cargo.toml └── src │ └── lib.rs ├── bin ├── README.md ├── Cargo.toml └── src │ └── main.rs ├── test_fixtures └── config.toml ├── .github └── workflows │ ├── test.yml │ └── release.yml ├── initialiser ├── Cargo.toml ├── README.md └── src │ ├── cache.rs │ ├── service.rs │ ├── network.rs │ └── lib.rs ├── example ├── config.toml └── Dockerfile ├── Cargo.toml ├── LICENSE ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | .cargo 4 | Makefile 5 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # Config 2 | 3 | This crate contains the configuration for both the Manager, 4 | and Firecracker. 5 | -------------------------------------------------------------------------------- /manager/src/network/mod.rs: -------------------------------------------------------------------------------- 1 | pub use {allocation::NetworkAllocation, allocator::NetworkAllocator, forwarding::Forwarding}; 2 | 3 | pub mod allocation; 4 | pub mod allocator; 5 | pub mod forwarding; 6 | -------------------------------------------------------------------------------- /util/README.md: -------------------------------------------------------------------------------- 1 | # Util 2 | 3 | This crate contains utility functions that are used across the other crates. 4 | For example, the `exec` function is used to execute a command and return the output. It's setup in a way that we can stub out the responses in test mode. 5 | -------------------------------------------------------------------------------- /runner/README.md: -------------------------------------------------------------------------------- 1 | # Runner 2 | 3 | This crate is responsible for running the GitHub Actions Runner. 4 | 5 | It's copied by the initialiser into `/sbin/actions-run` and started as a systemd service. 6 | 7 | It relies on a number of environment variables, set by the initialiser, to authenticate with GitHub and start the runner. 8 | -------------------------------------------------------------------------------- /github/README.md: -------------------------------------------------------------------------------- 1 | # GitHub 2 | 3 | This crate is responsible for getting a Runner token from the provided 4 | GitHub Personal Access Token (PAT), it is used to authenticate the Runner 5 | with GitHub. 6 | 7 | We pass this token through the `kernel boot args` in the Manager, and pick it up 8 | inside the VM with our custom entrypoint. 9 | -------------------------------------------------------------------------------- /config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "config" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow.workspace = true 10 | serde.workspace = true 11 | toml.workspace = true 12 | thiserror.workspace = true 13 | camino.workspace = true 14 | -------------------------------------------------------------------------------- /bin/README.md: -------------------------------------------------------------------------------- 1 | # Runner 2 | 3 | Responsible for running the firecracker instances. 4 | 5 | ## Usage 6 | 7 | ```bash 8 | actions-runner run --config /path/to/config.toml 9 | ``` 10 | 11 | ## Debug a role 12 | 13 | `--debug_role` automatically sets the log level to `debug`. 14 | 15 | ```bash 16 | actions-runner run --config /path/to/config.toml --debug_role 17 | ``` 18 | -------------------------------------------------------------------------------- /runner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "runner" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | log.workspace = true 10 | anyhow.workspace = true 11 | thiserror.workspace = true 12 | 13 | [dependencies.github] 14 | path = "../github" 15 | 16 | [dependencies.util] 17 | path = "../util" 18 | -------------------------------------------------------------------------------- /test_fixtures/config.toml: -------------------------------------------------------------------------------- 1 | network_interface="eth0" 2 | run_path="/srv" 3 | github_pat="ghp_1234567890" 4 | github_org="matsimitsu" 5 | 6 | [[roles]] 7 | name="your-project" 8 | rootfs_image="/home/runner/containers/your-project-1.0.0/rootfs.img" 9 | kernel_image="/home/runner/containers/your-project-1.0.0/kernel.bin" 10 | cpus=4 11 | memory_size=1024 12 | cache_size=1024 13 | overlay_size=1024 14 | instance_count=4 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: ["main", "develop"] 6 | pull_request: 7 | types: [opened, reopened, synchronize] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: dtolnay/rust-toolchain@stable 18 | - run: cargo test --all-features 19 | -------------------------------------------------------------------------------- /github/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "github" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | reqwest.workspace = true 10 | anyhow.workspace = true 11 | nix.workspace = true 12 | serde.workspace = true 13 | serde_json.workspace = true 14 | thiserror.workspace = true 15 | log.workspace = true 16 | -------------------------------------------------------------------------------- /util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "util" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | thiserror.workspace = true 10 | log.workspace = true 11 | cfg-if.workspace = true 12 | camino.workspace = true 13 | lazy_static.workspace = true 14 | mockall.workspace = true 15 | 16 | [dependencies.config] 17 | path = "../config" 18 | -------------------------------------------------------------------------------- /initialiser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "initialiser" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow.workspace = true 8 | nix.workspace = true 9 | serde.workspace = true 10 | serde_json.workspace = true 11 | thiserror.workspace = true 12 | log.workspace = true 13 | camino.workspace = true 14 | 15 | [dependencies.util] 16 | path = "../util" 17 | 18 | [dependencies.github] 19 | path = "../github" 20 | 21 | [dependencies.config] 22 | path = "../config" 23 | -------------------------------------------------------------------------------- /example/config.toml: -------------------------------------------------------------------------------- 1 | network_interface="enp0s31f6" 2 | run_path="/srv" 3 | github_pat="ghp_1234567890" 4 | github_org="matsimitsu" 5 | 6 | [[roles]] 7 | name="your-project" 8 | rootfs_image="/home/runner/containers/your-project-2024-01-20/rootfs.img" 9 | kernel_image="/home/runner/containers/your-project-2024-01-20/vmlinux.bin" 10 | cpus=4 11 | memory_size=1 12 | cache_size=1 13 | overlay_size=1 14 | instance_count=4 15 | cache_paths=["docker:/var/lib/docker"] 16 | labels=["your-project", "your-project-2024-01-20"] 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | 5 | jobs: 6 | release: 7 | name: Build Release 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | - name: Compile and release 12 | uses: rust-build/rust-build.action@v1.4.4 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | SRC_DIR: "bin" 16 | TOOLCHAIN_VERSION: "1.74.0" 17 | with: 18 | RUSTTARGET: x86_64-unknown-linux-musl 19 | -------------------------------------------------------------------------------- /manager/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "manager" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow.workspace = true 8 | thiserror.workspace = true 9 | serde.workspace = true 10 | log.workspace = true 11 | serde_json.workspace = true 12 | rand.workspace = true 13 | camino.workspace = true 14 | signal-hook = "*" 15 | 16 | [dependencies.github] 17 | path = "../github" 18 | 19 | [dependencies.config] 20 | path = "../config" 21 | 22 | [dependencies.util] 23 | path = "../util" 24 | 25 | [dev-dependencies] 26 | mockall.workspace = true 27 | -------------------------------------------------------------------------------- /config/src/lib.rs: -------------------------------------------------------------------------------- 1 | use thiserror::*; 2 | 3 | 4 | pub mod firecracker; 5 | pub mod manager; 6 | 7 | pub const DEFAULT_BOOT_ARGS: &str = 8 | "random.trust_cpu=on reboot=k panic=1 pci=off overlay_root=vdb init=/sbin/actions-init"; 9 | pub const NETWORK_MAGIC_MAC_START: &str = "06:00"; 10 | pub const NETWORK_MASK_SHORT: u8 = 30; 11 | pub const NETWORK_MAX_ALLOCATIONS: u8 = 200; 12 | 13 | #[derive(Error, Debug)] 14 | pub enum ConfigError { 15 | #[error("IO error: {:?}", self)] 16 | Io(#[from] std::io::Error), 17 | #[error("Config TOML error: {:?}", self)] 18 | Toml(#[from] toml::de::Error), 19 | } 20 | -------------------------------------------------------------------------------- /bin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actions-runner" 3 | version = "0.1.1" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [dependencies] 8 | anyhow.workspace = true 9 | thiserror.workspace = true 10 | clap.workspace = true 11 | fern.workspace = true 12 | chrono.workspace = true 13 | log.workspace = true 14 | camino.workspace = true 15 | 16 | [dependencies.manager] 17 | path = "../manager" 18 | 19 | [dependencies.builder] 20 | path = "../builder" 21 | 22 | [dependencies.initialiser] 23 | path = "../initialiser" 24 | 25 | [dependencies.runner] 26 | path = "../runner" 27 | 28 | [dependencies.config] 29 | path = "../config" 30 | -------------------------------------------------------------------------------- /manager/README.md: -------------------------------------------------------------------------------- 1 | # Manager 2 | 3 | This crate is responsible for managing the VM lifecycle, and it's what gets called when you run `actions-runner run`. 4 | 5 | It sets up the required networking on the host machine, to allow internet 6 | connectivity inside the VM. It also sets up the Cache disk, so we can persist data between runs. 7 | 8 | Finally, it starts the Firecracker VM, and waits for it to finish. 9 | 10 | There's also a debug feature that starts a Firecracker VM with stdin/out/err connected to the host machine, so you can see the output of the VM, and maniulate it. 11 | 12 | We pass variables from outside the VM to inside the VM through the kernel boot args, and pick them up with our custom entrypoint. 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "bin", 5 | "initialiser", 6 | "manager", 7 | "builder", 8 | "runner", 9 | "github", 10 | "util", 11 | "config" 12 | ] 13 | 14 | [workspace.dependencies] 15 | anyhow = { version = "*", features = ["backtrace"] } 16 | thiserror = "*" 17 | clap = { version = "*", features = ["derive"] } 18 | serde = { version = "*", features = ["derive"] } 19 | serde_json = "*" 20 | toml = "*" 21 | log = "*" 22 | fern = "*" 23 | chrono = "*" 24 | nix = { version = "*", features = ["fs", "mount"] } 25 | reqwest = { version = "*", default-features = false, features = ["json", "blocking", "rustls-tls"] } 26 | rand = "*" 27 | mockall = "*" 28 | cfg-if = "*" 29 | camino = { version = "*", features = ["serde1"] } 30 | lazy_static = "*" 31 | 32 | [profile.dev] 33 | split-debuginfo = "unpacked" 34 | 35 | [profile.release] 36 | strip = true # Automatically strip symbols from the binary. 37 | opt-level = "z" # Optimize for size. 38 | lto = true 39 | -------------------------------------------------------------------------------- /manager/src/network/allocator.rs: -------------------------------------------------------------------------------- 1 | use super::NetworkAllocation; 2 | use config::NETWORK_MAX_ALLOCATIONS; 3 | use std::collections::BTreeMap; 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum AllocatorError { 8 | #[error("No free IPs")] 9 | NoFreeIps, 10 | } 11 | 12 | pub struct NetworkAllocator { 13 | pub interface: String, 14 | pub allocations: BTreeMap, 15 | } 16 | 17 | impl NetworkAllocator { 18 | pub fn new(interface: &str) -> Self { 19 | Self { 20 | interface: interface.to_string(), 21 | allocations: BTreeMap::new(), 22 | } 23 | } 24 | 25 | pub fn allocate(&self) -> Result { 26 | for idx in 0..NETWORK_MAX_ALLOCATIONS { 27 | if !self.allocations.contains_key(&idx) { 28 | return Ok(NetworkAllocation::new(&self.interface, idx)); 29 | } 30 | } 31 | Err(AllocatorError::NoFreeIps) 32 | } 33 | 34 | pub fn deallocate(&mut self, idx: &u8) -> Result<(), AllocatorError> { 35 | self.allocations.remove(idx); 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /runner/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::process::Command; 3 | use util::exec; 4 | 5 | pub struct Runner {} 6 | 7 | impl Default for Runner { 8 | fn default() -> Self { 9 | Self::new() 10 | } 11 | } 12 | 13 | impl Runner { 14 | pub fn new() -> Self { 15 | Runner {} 16 | } 17 | 18 | pub fn run(&self) -> Result<()> { 19 | exec( 20 | Command::new("/home/runner/config.sh") 21 | .arg("--url") 22 | .arg(format!( 23 | "https://github.com/{}", 24 | std::env::var("GITHUB_ORG").unwrap() 25 | )) 26 | .arg("--token") 27 | .arg(std::env::var("GITHUB_TOKEN").unwrap()) 28 | .arg("--unattended") 29 | .arg("--ephemeral") 30 | .arg("--name") 31 | .arg(std::env::var("GITHUB_RUNNER_NAME").unwrap()) 32 | .arg("--labels") 33 | .arg(std::env::var("GITHUB_RUNNER_LABELS").unwrap()), 34 | )?; 35 | 36 | exec(&mut Command::new("/home/runner/run.sh"))?; 37 | Ok(()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 the Probes developers. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/src/firecracker.rs: -------------------------------------------------------------------------------- 1 | use camino::Utf8PathBuf; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize, Debug)] 5 | #[serde(rename_all = "kebab-case")] 6 | pub struct FirecrackerConfig { 7 | pub boot_source: BootSource, 8 | pub drives: Vec, 9 | pub network_interfaces: Vec, 10 | pub machine_config: MachineConfig, 11 | } 12 | 13 | #[derive(Serialize, Deserialize, Debug)] 14 | pub struct BootSource { 15 | pub kernel_image_path: String, 16 | pub boot_args: String, 17 | } 18 | 19 | #[derive(Serialize, Deserialize, Debug)] 20 | pub struct Drive { 21 | pub drive_id: String, 22 | pub path_on_host: Utf8PathBuf, 23 | pub is_root_device: bool, 24 | pub is_read_only: bool, 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | pub cache_type: Option, 27 | } 28 | 29 | #[derive(Serialize, Deserialize, Debug)] 30 | pub struct NetworkInterface { 31 | pub iface_id: String, 32 | pub guest_mac: String, 33 | pub host_dev_name: String, 34 | } 35 | 36 | #[derive(Serialize, Deserialize, Debug)] 37 | pub struct MachineConfig { 38 | pub vcpu_count: u32, 39 | pub mem_size_mib: u32, 40 | } 41 | -------------------------------------------------------------------------------- /manager/src/network/forwarding.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | use util::{exec, CommandExecutionError}; 3 | 4 | pub struct Forwarding { 5 | pub interface: String, 6 | } 7 | 8 | impl Forwarding { 9 | pub fn new(interface: &str) -> Self { 10 | Self { 11 | interface: interface.to_string(), 12 | } 13 | } 14 | 15 | pub fn setup(&self) -> Result<(), CommandExecutionError> { 16 | // Enable IP forwarding 17 | let _ = exec(Command::new("sh").args(["-c", "echo 1 > /proc/sys/net/ipv4/ip_forward"])); 18 | 19 | // Set up nat 20 | let _ = exec(Command::new("iptables").args([ 21 | "-t", 22 | "nat", 23 | "-A", 24 | "POSTROUTING", 25 | "-o", 26 | &self.interface, 27 | "-j", 28 | "MASQUERADE", 29 | ])); 30 | 31 | // Set up forwarding 32 | let _ = exec(Command::new("iptables").args([ 33 | "-I", 34 | "FORWARD", 35 | "1", 36 | "-m", 37 | "conntrack", 38 | "--ctstate", 39 | "RELATED,ESTABLISHED", 40 | "-j", 41 | "ACCEPT", 42 | ])); 43 | Ok(()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /initialiser/README.md: -------------------------------------------------------------------------------- 1 | # Initialiser 2 | 3 | This crate is responsible for initialising a newly booted VM. 4 | 5 | It: 6 | 7 | - Sets up the network interface, so the VM can communicate with the outside world. 8 | - Sets up the persisted Cache disk, so we can persist packages/docker images between runs. 9 | - Sets up a runner systemd service, so we can run the GitHub Action runner after the boot process is complete. 10 | 11 | We don't start the runner from this init script, but we set it as a new (one-shot) service. This becase we want to only start the GitHub Action runner after other processes have finished booting (e.g. Docker). 12 | 13 | The service has a `ExecStopPost` command that will reboot the VM after the runner has finished running the GitHub Action. (this signals to Firecracker to stop the VM and boot a fresh one). 14 | 15 | You might notice we only have one binary file and it performs several jobs. 16 | The user-facing jobs are separated into different subcommands, (`actions-runner build`, `actions-runner run`), but the internal jobs rely on the name of the binary. 17 | 18 | To start this initialiser, make sure it's renamed (or copied) from `actions-runner` to `actions-init`. 19 | 20 | The same goes for the GitHub Actions runner process. This initialiser copies itself into `/sbin/actions-run`. 21 | -------------------------------------------------------------------------------- /initialiser/src/cache.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::fs::{set_permissions, Permissions}; 3 | use std::os::unix::fs::{symlink, PermissionsExt}; 4 | use thiserror::Error; 5 | use util::{fs, mount, CommandExecutionError}; 6 | 7 | const CACHE_PATH: &str = "/cache"; 8 | 9 | #[derive(Error, Debug)] 10 | pub enum CacheError { 11 | #[error("IO error: {:?}", self)] 12 | Io(#[from] std::io::Error), 13 | #[error("Could not mount: {:?}", self)] 14 | Mount(#[from] CommandExecutionError), 15 | } 16 | 17 | pub fn setup_cache(cache_str: &str) -> Result<(), CacheError> { 18 | fs::mkdir_p(CACHE_PATH)?; 19 | mount::mount_ext4("/dev/vdb", CACHE_PATH)?; 20 | set_permissions(CACHE_PATH, Permissions::from_mode(0o777))?; 21 | 22 | let cache_links = cache_str.split(','); 23 | for cache_link in cache_links { 24 | let cache_link = cache_link.trim(); 25 | let cache_parts: Vec<&str> = cache_link.split(':').collect(); 26 | if cache_parts.len() != 2 { 27 | return Err(CacheError::Io(std::io::Error::new( 28 | std::io::ErrorKind::InvalidInput, 29 | format!("Invalid cache link: {}", cache_link), 30 | ))); 31 | } 32 | let cache_root = format!("{}/{}", CACHE_PATH, cache_parts[0]); 33 | let cache_path = cache_parts[1]; 34 | 35 | fs::mkdir_p(&cache_root)?; 36 | symlink(&cache_root, cache_path)?; 37 | } 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /initialiser/src/service.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::fs::write; 3 | use std::os::unix::fs::symlink; 4 | 5 | pub const SERVICE_PATH: &str = "/etc/systemd/system/runner.service"; 6 | pub const SERVICE_TEMPLATE: &str = r#" 7 | [Unit] 8 | Description=Actions Runner 9 | After=network.target 10 | 11 | [Service] 12 | ExecStart=/sbin/actions-run 13 | KillMode=control-group 14 | KillSignal=SIGTERM 15 | TimeoutStopSec=5min 16 | WorkingDirectory=/home/runner 17 | User=runner 18 | Restart=never 19 | Environment="GITHUB_ORG={github_org}" 20 | Environment="GITHUB_TOKEN={github_token}" 21 | Environment="GITHUB_RUNNER_NAME={github_runner_name}" 22 | Environment="GITHUB_RUNNER_LABELS={github_runner_labels}" 23 | ExecStopPost=+/usr/sbin/reboot 24 | "#; 25 | 26 | pub fn setup_service( 27 | github_org: &str, 28 | github_token: &str, 29 | github_runner_name: &str, 30 | github_runner_labels: &str, 31 | ) -> Result<()> { 32 | let service = SERVICE_TEMPLATE 33 | .replace("{github_org}", github_org) 34 | .replace("{github_token}", github_token) 35 | .replace("{github_runner_name}", github_runner_name) 36 | .replace("{github_runner_labels}", github_runner_labels); 37 | 38 | write(SERVICE_PATH, service)?; 39 | 40 | Ok(()) 41 | } 42 | 43 | pub fn enable_service() -> Result<()> { 44 | symlink( 45 | SERVICE_PATH, 46 | "/etc/systemd/system/multi-user.target.wants/runner.service", 47 | )?; 48 | 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /util/src/network.rs: -------------------------------------------------------------------------------- 1 | use config::NETWORK_MAGIC_MAC_START; 2 | use std::net::Ipv4Addr; 3 | use std::num::ParseIntError; 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum MacToIpError { 8 | #[error("Could not parse octed from string: {}", self)] 9 | ParseIntError(#[from] ParseIntError), 10 | #[error("No valid IP adddress in mac address: {}", .0)] 11 | NoIpInMac(String), 12 | } 13 | 14 | // Convert an IP address to a MAC address 15 | pub fn ip_to_mac(ip: &Ipv4Addr) -> String { 16 | format!( 17 | "{}:{:02x}:{:02x}:{:02x}:{:02x}", 18 | NETWORK_MAGIC_MAC_START, 19 | ip.octets()[0], 20 | ip.octets()[1], 21 | ip.octets()[2], 22 | ip.octets()[3] 23 | ) 24 | } 25 | 26 | // Convert an IP address to a MAC address 27 | pub fn mac_to_ip(mac: &str) -> Result { 28 | let octets = mac 29 | .replace("06:00:", "") 30 | .split(':') 31 | .map(|octet| u8::from_str_radix(octet, 16)) 32 | .collect::, ParseIntError>>() 33 | .map_err(|_| MacToIpError::NoIpInMac(mac.to_string()))?; 34 | 35 | let address = Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]); 36 | Ok(address) 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use super::*; 42 | 43 | #[test] 44 | fn test_mac_to_ip() { 45 | let mac = "06:00:ac:10:c9:01"; 46 | let ip = super::mac_to_ip(mac).unwrap(); 47 | assert_eq!(ip, Ipv4Addr::new(172, 16, 201, 1)); 48 | } 49 | 50 | #[test] 51 | fn test_ip_to_mac() { 52 | assert_eq!( 53 | ip_to_mac(&Ipv4Addr::new(172, 16, 0, 1)), 54 | "06:00:ac:10:00:01" 55 | ); 56 | 57 | assert_eq!( 58 | ip_to_mac(&Ipv4Addr::new(172, 16, 10, 2)), 59 | "06:00:ac:10:0a:02" 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /manager/src/disk.rs: -------------------------------------------------------------------------------- 1 | use camino::Utf8PathBuf; 2 | use util::fs; 3 | 4 | #[derive(Debug)] 5 | pub struct Disk { 6 | pub size: u32, 7 | pub path: Utf8PathBuf, 8 | pub name: String, 9 | pub format: DiskFormat, 10 | } 11 | 12 | #[derive(Debug)] 13 | pub enum DiskFormat { 14 | Ext4, 15 | } 16 | 17 | impl Disk { 18 | pub fn new(path: &Utf8PathBuf, name: &str, size: u32, format: DiskFormat) -> Self { 19 | Self { 20 | path: path.clone(), 21 | name: name.to_string(), 22 | size, 23 | format, 24 | } 25 | } 26 | 27 | pub fn size_in_megabytes(&self) -> u64 { 28 | self.size as u64 * 1024 29 | } 30 | 31 | pub fn size_in_kilobytes(&self) -> u64 { 32 | self.size_in_megabytes() * 1024 33 | } 34 | 35 | pub fn filename(&self) -> Utf8PathBuf { 36 | match self.format { 37 | DiskFormat::Ext4 => Utf8PathBuf::from(format!("{}.ext4", &self.name)), 38 | } 39 | } 40 | 41 | pub fn path_with_filename(&self) -> Utf8PathBuf { 42 | self.path.join(self.filename()) 43 | } 44 | 45 | pub fn setup(&self) -> Result<(), std::io::Error> { 46 | match self.format { 47 | DiskFormat::Ext4 => self.setup_ext4(), 48 | } 49 | } 50 | 51 | pub fn usage_on_disk(&self) -> Result { 52 | fs::du(self.path_with_filename()) 53 | } 54 | 55 | pub fn setup_ext4(&self) -> Result<(), std::io::Error> { 56 | fs::dd(self.path_with_filename(), self.size_in_megabytes())?; 57 | fs::mkfs_ext4(self.path_with_filename())?; 58 | Ok(()) 59 | } 60 | 61 | pub fn destroy(&self) -> Result<(), std::io::Error> { 62 | fs::rm_rf(self.path_with_filename())?; 63 | Ok(()) 64 | } 65 | 66 | pub fn usage_pct(&self) -> Result { 67 | Ok((self.usage_on_disk()? * 100 / self.size_in_kilobytes()) as u8) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /manager/src/network/allocation.rs: -------------------------------------------------------------------------------- 1 | use config::NETWORK_MASK_SHORT; 2 | use std::net::Ipv4Addr; 3 | use std::process::Command; 4 | use util::{exec, network::ip_to_mac, CommandExecutionError}; 5 | 6 | #[derive(Debug)] 7 | pub struct NetworkAllocation { 8 | pub interface: String, 9 | pub host_ip: Ipv4Addr, 10 | pub guest_mac: String, 11 | pub client_ip: Ipv4Addr, 12 | pub tap_name: String, 13 | } 14 | 15 | impl NetworkAllocation { 16 | pub fn new(interface: &str, idx: u8) -> Self { 17 | let host_ip = Ipv4Addr::new(172, 16, idx, 1); 18 | let client_ip = Ipv4Addr::new(172, 16, idx, 2); 19 | Self { 20 | interface: interface.to_string(), 21 | guest_mac: ip_to_mac(&client_ip), 22 | tap_name: format!("tap{}", idx), 23 | host_ip, 24 | client_ip, 25 | } 26 | } 27 | 28 | pub fn setup(&self) -> Result<(), CommandExecutionError> { 29 | // Remove existing tap device 30 | let _ = exec(Command::new("ip").args(["link", "del", &self.tap_name])); 31 | 32 | // Create tap device 33 | let _ = 34 | exec(Command::new("ip").args(["tuntap", "add", "dev", &self.tap_name, "mode", "tap"])); 35 | 36 | // Add address to tap device 37 | let _ = exec(Command::new("ip").args([ 38 | "addr", 39 | "add", 40 | &format!("{}/{}", self.host_ip, NETWORK_MASK_SHORT), 41 | "dev", 42 | &self.tap_name, 43 | ])); 44 | 45 | // Bring up tap device 46 | let _ = exec(Command::new("ip").args(["link", "set", "dev", &self.tap_name, "up"])); 47 | 48 | // Set up internet access 49 | let _ = exec(Command::new("iptables").args([ 50 | "-I", 51 | "FORWARD", 52 | "1", 53 | "-i", 54 | &self.tap_name, 55 | "-o", 56 | &self.interface, 57 | "-j", 58 | "ACCEPT", 59 | ])); 60 | 61 | Ok(()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /github/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Serialize, Deserialize)] 6 | pub struct RegistrationTokenResult { 7 | pub token: String, 8 | pub expires_at: String, 9 | } 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct GitHub { 13 | pub org: String, 14 | pub pat: String, 15 | client: reqwest::blocking::Client, 16 | } 17 | 18 | impl GitHub { 19 | pub fn new(org: &str, pat: &str) -> Self { 20 | GitHub { 21 | org: org.to_string(), 22 | pat: pat.to_string(), 23 | client: reqwest::blocking::Client::new(), 24 | } 25 | } 26 | 27 | pub fn registration_token(&self) -> Result { 28 | let registration_token_result = self 29 | .client 30 | .post(format!( 31 | "https://api.github.com/orgs/{}/actions/runners/registration-token", 32 | self.org 33 | )) 34 | .header("Authorization", format!("Bearer {}", self.pat)) 35 | .header("Accept", "application/vnd.github+json") 36 | .header("X-GitHub-Api-Version", "2022-11-28") 37 | .header("User-Agent", "actions-runner") 38 | .send()? 39 | .json::()?; 40 | Ok(registration_token_result.token) 41 | } 42 | 43 | pub fn remove_runner(&self, runner_name: &str) -> Result<()> { 44 | self.client 45 | .post(format!( 46 | "https://api.github.com/orgs/{}/actions/runners/remove-token", 47 | self.org 48 | )) 49 | .header("Authorization", format!("Bearer {}", self.pat)) 50 | .header("Accept", "application/vnd.github+json") 51 | .header("X-GitHub-Api-Version", "2022-11-28") 52 | .header("User-Agent", "actions-runner") 53 | .json(&serde_json::json!({ 54 | "runner_name": runner_name 55 | })) 56 | .send()?; 57 | Ok(()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /config/src/manager.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use camino::Utf8PathBuf; 3 | use serde::Deserialize; 4 | use toml; 5 | 6 | #[derive(Deserialize, Debug, Clone)] 7 | pub struct ManagerConfig { 8 | pub network_interface: String, 9 | pub run_path: Utf8PathBuf, 10 | pub roles: Vec, 11 | pub github_org: String, 12 | pub github_pat: String, 13 | } 14 | 15 | impl ManagerConfig { 16 | pub fn from_file(path: &Utf8PathBuf) -> Result { 17 | let config_str = std::fs::read_to_string(path)?; 18 | let config = toml::from_str(&config_str)?; 19 | Ok(config) 20 | } 21 | } 22 | 23 | const fn _default_overlay_size() -> u32 { 24 | 10 // 10GB 25 | } 26 | 27 | const fn _default_max_cache_pct() -> u8 { 28 | 90 29 | } 30 | 31 | #[derive(Deserialize, Debug, Clone)] 32 | pub struct Role { 33 | pub name: String, 34 | pub rootfs_image: Utf8PathBuf, 35 | pub kernel_image: Utf8PathBuf, 36 | pub kernel_cmdline: Option, 37 | pub cpus: u32, 38 | pub memory_size: u32, 39 | pub cache_size: u32, 40 | #[serde(default = "_default_overlay_size")] 41 | pub overlay_size: u32, 42 | pub instance_count: u8, 43 | #[serde(default)] 44 | pub cache_paths: Vec, 45 | #[serde(default = "_default_max_cache_pct")] 46 | pub max_cache_pct: u8, 47 | #[serde(default)] 48 | pub labels: Vec, 49 | } 50 | 51 | impl Role { 52 | pub fn slug(&self) -> String { 53 | self.name.to_lowercase() 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use super::*; 60 | 61 | #[test] 62 | fn test_config_from_file() { 63 | let config = ManagerConfig::from_file(&helpers::test_fixtures_file("config.toml")) 64 | .expect("Could not load config"); 65 | 66 | assert_eq!(&config.network_interface, "eth0"); 67 | } 68 | 69 | mod helpers { 70 | use camino::Utf8PathBuf; 71 | use std::env::current_dir; 72 | 73 | pub fn test_fixtures_path() -> Utf8PathBuf { 74 | let current_dir: Utf8PathBuf = current_dir() 75 | .expect("Could not get current dir") 76 | .try_into() 77 | .expect("Invalid path"); 78 | 79 | // Check for two levels of nesting 80 | if current_dir.join("../test_fixtures").exists() { 81 | current_dir.join("../test_fixtures") 82 | } else { 83 | current_dir.join("../../test_fixtures") 84 | } 85 | } 86 | 87 | pub fn test_fixtures_file(file: &str) -> Utf8PathBuf { 88 | let path = test_fixtures_path().join(file); 89 | if !path.exists() { 90 | panic!("Test fixture file {:?} does not exist", path); 91 | } 92 | path 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /util/src/mount.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use camino::Utf8Path; 3 | use std::process::Command; 4 | 5 | pub fn mount_image( 6 | from: impl AsRef, 7 | to: impl AsRef, 8 | ) -> Result<(), CommandExecutionError> { 9 | let from = from.as_ref(); 10 | let to = to.as_ref(); 11 | 12 | let _ = exec(Command::new("mount").args([from.as_str(), to.as_str()]))?; 13 | Ok(()) 14 | } 15 | 16 | pub fn mount_ext4( 17 | from: impl AsRef, 18 | to: impl AsRef, 19 | ) -> Result<(), CommandExecutionError> { 20 | let from = from.as_ref(); 21 | let to = to.as_ref(); 22 | 23 | let _ = exec(Command::new("mount").args(["-t", "ext4", from.as_str(), to.as_str()]))?; 24 | Ok(()) 25 | } 26 | 27 | pub fn unmount(path: impl AsRef) -> Result<(), CommandExecutionError> { 28 | let path = path.as_ref(); 29 | let _ = exec(Command::new("umount").arg(path.as_str()))?; 30 | 31 | Ok(()) 32 | } 33 | 34 | #[cfg(test)] 35 | mod tests { 36 | use super::*; 37 | use std::os::unix::process::ExitStatusExt; 38 | 39 | #[test] 40 | fn test_mount_image() { 41 | let _m = MTX.lock(); 42 | 43 | let ctx = mock_inner::internal_exec_context(); 44 | ctx.expect() 45 | .withf(|c| inner::to_string(c) == "mount /dev/sda1 /mnt") 46 | .returning(|_| { 47 | Ok(std::process::Output { 48 | status: std::process::ExitStatus::from_raw(0), 49 | stdout: vec![], 50 | stderr: vec![], 51 | }) 52 | }); 53 | 54 | let result = mount_image("/dev/sda1", "/mnt"); 55 | assert!(result.is_ok()); 56 | ctx.checkpoint(); 57 | } 58 | 59 | #[test] 60 | fn test_mount_ext4() { 61 | let _m = MTX.lock(); 62 | 63 | let ctx = mock_inner::internal_exec_context(); 64 | ctx.expect() 65 | .withf(|c| inner::to_string(c) == "mount -t ext4 /dev/sda1 /mnt") 66 | .returning(|_| { 67 | Ok(std::process::Output { 68 | status: std::process::ExitStatus::from_raw(0), 69 | stdout: vec![], 70 | stderr: vec![], 71 | }) 72 | }); 73 | 74 | let result = mount_ext4("/dev/sda1", "/mnt"); 75 | assert!(result.is_ok()); 76 | ctx.checkpoint(); 77 | } 78 | 79 | #[test] 80 | fn test_unmount() { 81 | let _m = MTX.lock(); 82 | 83 | let ctx = mock_inner::internal_exec_context(); 84 | ctx.expect() 85 | .withf(|c| inner::to_string(c) == "umount /dev/sda1") 86 | .returning(|_| { 87 | Ok(std::process::Output { 88 | status: std::process::ExitStatus::from_raw(0), 89 | stdout: vec![], 90 | stderr: vec![], 91 | }) 92 | }); 93 | 94 | let result = unmount("/dev/sda1"); 95 | assert!(result.is_ok()); 96 | ctx.checkpoint(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /initialiser/src/network.rs: -------------------------------------------------------------------------------- 1 | use config::{NETWORK_MAGIC_MAC_START, NETWORK_MASK_SHORT}; 2 | use serde::Deserialize; 3 | 4 | use std::{fs::write, net::Ipv4Addr, process::Command}; 5 | use thiserror::Error; 6 | use util::{exec, network::mac_to_ip}; 7 | 8 | const RESOLV_CONF: &str = "nameserver 1.1.1.1\noptions use-vc\n"; 9 | const RESOLV_CONF_PATH: &str = "/etc/resolv.conf"; 10 | 11 | #[derive(Error, Debug)] 12 | pub enum NetworkError { 13 | #[error("IO error: {:?}", self)] 14 | Io(#[from] std::io::Error), 15 | #[error("UTF8 error: {:?}", self)] 16 | Utf8(#[from] std::string::FromUtf8Error), 17 | #[error("Command error: {:?}", self)] 18 | Command(#[from] util::CommandExecutionError), 19 | #[error("JSON error: {:?}", self)] 20 | Json(#[from] serde_json::Error), 21 | #[error("No interface found with mac address starting with 06:00")] 22 | NoInterfaceFound, 23 | #[error("No valid IP adddress in mac address: {}", .0)] 24 | MacToIpError(#[from] util::network::MacToIpError), 25 | } 26 | 27 | #[derive(Deserialize, Debug)] 28 | pub struct NetworkAddress { 29 | pub ifname: String, 30 | #[serde(rename = "address")] 31 | pub mac: String, 32 | } 33 | 34 | #[derive(Deserialize, Debug)] 35 | pub struct NetworkInterface { 36 | pub ifname: String, 37 | pub mac: String, 38 | pub own_address: Ipv4Addr, 39 | pub host_address: Ipv4Addr, 40 | } 41 | 42 | pub fn get_interfaces() -> Result, NetworkError> { 43 | let command_output = exec(Command::new("ip").arg("-j").arg("address"))?; 44 | let output_string = String::from_utf8_lossy(&command_output.stdout).to_string(); 45 | let interfaces: Vec = serde_json::from_str(&output_string)?; 46 | 47 | Ok(interfaces) 48 | } 49 | 50 | pub fn get_magic_address() -> Result { 51 | for interface in get_interfaces()? { 52 | if interface.mac.starts_with(NETWORK_MAGIC_MAC_START) { 53 | return Ok(interface); 54 | } 55 | } 56 | Err(NetworkError::NoInterfaceFound) 57 | } 58 | 59 | pub fn setup_network() -> Result, NetworkError> { 60 | let magic_address = match get_magic_address() { 61 | Ok(i) => i, 62 | Err(_) => return Ok(None), 63 | }; 64 | let own_ip = mac_to_ip(&magic_address.mac)?; 65 | let host_ip = Ipv4Addr::new( 66 | own_ip.octets()[0], 67 | own_ip.octets()[1], 68 | own_ip.octets()[2], 69 | 1, 70 | ); 71 | 72 | exec(Command::new("ip").args([ 73 | "addr", 74 | "add", 75 | &format!("{}/{}", own_ip, NETWORK_MASK_SHORT), 76 | "dev", 77 | &magic_address.ifname, 78 | ]))?; 79 | 80 | exec(Command::new("ip").args(["link", "set", &magic_address.ifname, "up"]))?; 81 | 82 | exec(Command::new("ip").args([ 83 | "route", 84 | "add", 85 | "default", 86 | "via", 87 | host_ip.to_string().as_str(), 88 | ]))?; 89 | 90 | Ok(Some(NetworkInterface { 91 | ifname: magic_address.ifname.to_string(), 92 | mac: magic_address.mac.to_string(), 93 | own_address: own_ip, 94 | host_address: host_ip, 95 | })) 96 | } 97 | 98 | pub fn setup_dns() -> Result<(), NetworkError> { 99 | write(RESOLV_CONF_PATH, RESOLV_CONF)?; 100 | Ok(()) 101 | } 102 | -------------------------------------------------------------------------------- /initialiser/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use camino::{Utf8Path, Utf8PathBuf}; 3 | use log::*; 4 | use std::env; 5 | use std::fs::copy; 6 | use std::os::unix::process::CommandExt; 7 | use std::process::Command; 8 | 9 | mod cache; 10 | mod network; 11 | mod service; 12 | 13 | pub struct Initialiser { 14 | pub own_path: Utf8PathBuf, 15 | } 16 | 17 | impl Initialiser { 18 | pub fn new(path: impl AsRef) -> Self { 19 | let path = path.as_ref(); 20 | 21 | Initialiser { 22 | own_path: Utf8PathBuf::from(path), 23 | } 24 | } 25 | 26 | pub fn run(&self) -> Result<()> { 27 | debug!("Setup network"); 28 | match network::setup_network() { 29 | Ok(Some(interface)) => info!( 30 | "Network setup complete: {} {} > {}", 31 | interface.ifname, interface.own_address, interface.host_address 32 | ), 33 | Ok(None) => info!("No magic address found, skipping network setup"), 34 | Err(e) => { 35 | error!("Network setup failed: {}\n\n", e); 36 | return Err(e.into()); 37 | } 38 | } 39 | 40 | debug!("Setup dns"); 41 | match network::setup_dns() { 42 | Ok(_) => info!("DNS setup complete"), 43 | Err(e) => { 44 | error!("DNS setup failed: {}", e); 45 | return Err(e.into()); 46 | } 47 | } 48 | 49 | debug!("Setup cache"); 50 | match env::var("cache_paths") { 51 | Ok(cache_paths) => { 52 | match cache::setup_cache(&cache_paths) { 53 | Ok(_) => info!("Cache setup complete"), 54 | Err(e) => { 55 | error!("Cache setup failed: {}", e); 56 | return Err(e.into()); 57 | } 58 | }; 59 | } 60 | Err(_) => { 61 | info!("No 'cache' kernel arg found, skipping cache setup"); 62 | } 63 | } 64 | 65 | debug!("Setup actions-runner"); 66 | match ( 67 | env::var("github_org"), 68 | env::var("github_token"), 69 | env::var("github_runner_name"), 70 | env::var("github_runner_labels"), 71 | ) { 72 | ( 73 | Ok(github_org), 74 | Ok(github_token), 75 | Ok(github_runner_name), 76 | Ok(github_runner_labels), 77 | ) => { 78 | debug!("Copy self to actions-runner"); 79 | copy(&self.own_path, Utf8PathBuf::from("/sbin/actions-run"))?; 80 | 81 | debug!("Set runner init script"); 82 | service::setup_service( 83 | &github_org, 84 | &github_token, 85 | &github_runner_name, 86 | &github_runner_labels, 87 | )?; 88 | 89 | debug!("Symlink init script to start at boot"); 90 | service::enable_service()?; 91 | } 92 | _ => { 93 | info!("No 'github_org', 'github_token' or 'github_runner_name' kernel arg found, skipping actions-runner setup"); 94 | } 95 | } 96 | 97 | Command::new("/sbin/init").exec(); 98 | Ok(()) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Actions Runner 2 | 3 | Actions Runner (`actions-runner`) is a tool that helps build and run a (group of) Firecracker VMs that 4 | are to be used with GitHub Actions. 5 | 6 | ## Requirements 7 | 8 | For the builder the following packages are required: 9 | 10 | * `qemu` 11 | * `docker` 12 | * A `Dockerfile` to build the rootfs image, this image needs a `runner` user with a home directory at `/home/runner` and a version of the GitHub actions runner installed in `/home/runner/`. If `Docker` is installed _within_ the container, make sure that `docker` is in the `runner` user's group and that the `runner` user has access to the docker socket. 13 | 14 | For the runner the following packages are required: 15 | 16 | * `firecracker` 17 | * A linux kernel (we use: [5.10]( https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/x86_64/kernels/vmlinux-5.10.bin)) 18 | * A rootfs image, created by the builder 19 | * A configuration file, see below for an example 20 | * A GitHub Personal Access Token with the `repo` scope, so we can add the runner to the organization. 21 | 22 | 23 | ## Building an image 24 | 25 | The builder takes a Dockerfile and creates a new rootfs image. It copies 26 | the `actions-runner` binary into the rootfs image and sets it as the entrypoint, so we can control how the server is setup (networking, caching, etc.). 27 | 28 | For example: 29 | 30 | ```bash 31 | ./actions-runner build Dockerfile result.img 32 | ``` 33 | 34 | This builds a new rootfs image from the Dockerfile and saves it as `result.img`. The `--debug` flag is optional and will print debug information. 35 | 36 | 37 | ## Running a VM 38 | 39 | The runner takes a configuration file and runs one or more groups of VMs. Each group of VMs is defined by a `group` in the configuration file. 40 | 41 | The sizes are always in Gigabytes. 42 | 43 | ```toml 44 | network_interface="enp0s31f6" 45 | run_path="/srv" 46 | github_pat="ghp_1234567890" 47 | github_org="matsimitsu" 48 | 49 | [[roles]] 50 | name="your-project" 51 | rootfs_image="/home/runner/containers/your-project-2024-01-20.img" 52 | kernel_image="/home/runner/kernels/vmlinux-5.10.bin" 53 | cpus=4 54 | memory_size=1 55 | cache_size=1 56 | instance_count=4 57 | cache_paths=["docker:/var/lib/docker"] 58 | labels=["your-project-2024-01-20"] 59 | ``` 60 | 61 | You can now run the VMs with the following command: 62 | 63 | ```bash 64 | ./actions-runner run --config config.toml 65 | ``` 66 | 67 | 68 | ### Debugging a VM 69 | 70 | You can run a `debug` instance of a role by setting the `--debug-role` flag. 71 | For example, to start a single vm in debug mode for the config file above, 72 | you can run: 73 | 74 | ```bash 75 | ./actions-runner runner --config config.toml --debug-role your-project --log-level debug 76 | ``` 77 | 78 | This will start a single VM with the `your-project` role, and binds 79 | stdin/stdout to the terminal, meaning you can see and manipulate the VM directly. 80 | 81 | You can run this command while the runner is running, and it will start a new, separate VM. 82 | 83 | 84 | ## Contributing 85 | 86 | This project was created to run our own GitHub actions, we're not necessarily 87 | looking into expanding this into a full-fledged standalione project. However, we're happy to accept contributions that fit our use case. 88 | 89 | We'll most likely not accept contributions that would make this project more 90 | generic, as we're not looking to maintain a generic actions runner. 91 | 92 | If you have doubts, please open an issue and we can discuss it! 93 | 94 | Please follow our [Contributing guide][contributing-guide] in our 95 | documentation and follow our [Code of Conduct][coc]. 96 | 97 | Running `cargo fmt` before contributing changes would reduce diffs for future 98 | contributions. 99 | 100 | Also, we would be very happy to send you Stroopwafles. Have look at everyone 101 | we send a package to so far on our [Stroopwafles page][waffles-page]. 102 | 103 | [contributing-guide]: http://docs.appsignal.com/appsignal/contributing.html 104 | [coc]: https://docs.appsignal.com/appsignal/code-of-conduct.html 105 | [waffles-page]: https://appsignal.com/waffles 106 | -------------------------------------------------------------------------------- /bin/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use builder::Builder; 3 | use camino::Utf8PathBuf; 4 | use chrono::Utc; 5 | use clap::{Parser, Subcommand}; 6 | use config::manager::ManagerConfig; 7 | 8 | use manager::Manager; 9 | use std::env; 10 | use std::process::ExitCode; 11 | 12 | #[derive(Parser)] 13 | #[command(author, version, about, long_about = None)] 14 | #[command(propagate_version = true)] 15 | struct Args { 16 | #[command(subcommand)] 17 | command: Commands, 18 | } 19 | 20 | #[derive(Subcommand)] 21 | enum Commands { 22 | /// Runs the manager, which will start the instances and manage them 23 | Run(ManageArgs), 24 | 25 | /// Build new image from a Dockerfile 26 | Build(BuildArgs), 27 | } 28 | 29 | #[derive(Parser, Debug)] 30 | #[command(version, about, long_about = None)] 31 | struct BuildArgs { 32 | dockerfile: Utf8PathBuf, 33 | 34 | output: Utf8PathBuf, 35 | 36 | #[arg(short, long)] 37 | size: Option, 38 | 39 | #[arg(short, long)] 40 | log_level: Option, 41 | } 42 | 43 | #[derive(Parser, Debug)] 44 | #[command(version, about, long_about = None)] 45 | struct ManageArgs { 46 | #[arg(short, long)] 47 | config: Utf8PathBuf, 48 | 49 | #[arg(short, long)] 50 | debug_role: Option, 51 | 52 | #[arg(short, long, default_value_t = 201)] 53 | instance_index: u8, 54 | 55 | #[arg(short, long)] 56 | log_level: Option, 57 | } 58 | 59 | fn main() -> Result { 60 | match env::args().next() { 61 | Some(path) if path.ends_with("actions-init") => { 62 | init(&path)?; 63 | } 64 | Some(path) if path.ends_with("actions-run") => { 65 | run()?; 66 | } 67 | _ => { 68 | let args = Args::parse(); 69 | match args.command { 70 | Commands::Build(args) => build(args)?, 71 | Commands::Run(args) => manage(args)?, 72 | } 73 | } 74 | } 75 | 76 | Ok(ExitCode::SUCCESS) 77 | } 78 | 79 | fn run() -> Result<()> { 80 | setup_logger(log::LevelFilter::Debug).expect("Could not setup logger"); 81 | 82 | let runner = runner::Runner::new(); 83 | runner.run()?; 84 | 85 | Ok(()) 86 | } 87 | 88 | fn init(path: &str) -> Result<()> { 89 | setup_logger(log::LevelFilter::Debug).expect("Could not setup logger"); 90 | 91 | let initialiser = initialiser::Initialiser::new(path); 92 | initialiser.run()?; 93 | 94 | Ok(()) 95 | } 96 | 97 | fn build(args: BuildArgs) -> Result<()> { 98 | setup_logger(args.log_level.unwrap_or(log::LevelFilter::Info)).expect("Could not setup logger"); 99 | 100 | let builder = Builder::new(&args.dockerfile, &args.output, args.size)?; 101 | builder.build()?; 102 | 103 | Ok(()) 104 | } 105 | 106 | fn manage(args: ManageArgs) -> Result<()> { 107 | setup_logger(args.log_level.unwrap_or(log::LevelFilter::Info)).expect("Could not setup logger"); 108 | 109 | let config = 110 | ManagerConfig::from_file(&args.config.clone()).expect("Could not load config"); 111 | let mut manager = Manager::new(config); 112 | 113 | match args.debug_role { 114 | Some(role) => { 115 | log::info!( 116 | "Debugging role: `{}` with index: '{}' from config: `{}`", 117 | role, 118 | args.instance_index, 119 | args.config 120 | ); 121 | manager 122 | .debug(&role, args.instance_index) 123 | .expect("Could not debug instance"); 124 | } 125 | None => { 126 | log::info!("Starting with config: {}", args.config); 127 | manager.setup()?; 128 | manager.run()?; 129 | } 130 | } 131 | 132 | Ok(()) 133 | } 134 | 135 | fn setup_logger(log_level: log::LevelFilter) -> Result<(), fern::InitError> { 136 | fern::Dispatch::new() 137 | .format(|out, message, record| { 138 | out.finish(format_args!( 139 | "[{} {} {}] {}", 140 | Utc::now().to_rfc3339(), 141 | record.level(), 142 | record.target(), 143 | message 144 | )) 145 | }) 146 | .level(log_level) 147 | .chain(std::io::stdout()) 148 | .apply()?; 149 | Ok(()) 150 | } 151 | -------------------------------------------------------------------------------- /example/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest as base 2 | 3 | # udev is needed for booting a "real" VM, setting up the ttyS0 console properly 4 | # kmod is needed for modprobing modules 5 | # systemd is needed for running as PID 1 as /sbin/init 6 | # ca-certificates, gnupg, lsb-release are needed for docker 7 | RUN apt update && apt install -y \ 8 | curl \ 9 | wget \ 10 | dbus \ 11 | kmod \ 12 | tar \ 13 | util-linux \ 14 | iproute2 \ 15 | iputils-ping \ 16 | net-tools \ 17 | openssh-server \ 18 | ca-certificates \ 19 | gnupg \ 20 | lsb-release \ 21 | systemd \ 22 | sudo \ 23 | bash \ 24 | udev \ 25 | parallel \ 26 | bridge-utils \ 27 | iputils-ping \ 28 | net-tools \ 29 | locales \ 30 | unzip xvfb libnss3-dev libgdk-pixbuf2.0-dev libgtk-3-dev libxss-dev libasound2 \ 31 | git \ 32 | jq \ 33 | nano \ 34 | libyaml-dev \ 35 | build-essential 36 | 37 | # Do tzdata separately to avoid interactive prompts 38 | RUN DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata 39 | 40 | # Update locale 41 | RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen 42 | ENV LANG en_US.UTF-8 43 | ENV LANGUAGE en_US:en 44 | ENV LC_ALL en_US.UTF-8 45 | 46 | # Install and enable docker 47 | RUN mkdir -m 0755 -p /etc/apt/keyrings && \ 48 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \ 49 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ 50 | $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null && \ 51 | apt update && \ 52 | apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin && \ 53 | apt clean && \ 54 | rm -rf /var/lib/apt/lists/* && \ 55 | systemctl enable docker && \ 56 | update-alternatives --set iptables /usr/sbin/iptables-legacy 57 | 58 | # Set the root password for logging in through the VM's ttyS0 console 59 | RUN echo "root:root" | chpasswd 60 | 61 | RUN groupadd user && \ 62 | useradd -m -d /home/runner -s /bin/bash -g user -G docker runner 63 | 64 | # Actions uses this for precompiled binaries 65 | RUN mkdir -p /opt/hostedtoolcache \ 66 | && chown -R runner:user /opt/hostedtoolcache \ 67 | && chmod g+rwx /opt/hostedtoolcache 68 | 69 | WORKDIR /home/runner 70 | 71 | RUN GITHUB_RUNNER_VERSION=$(curl --silent "https://api.github.com/repos/actions/runner/releases/latest" | jq -r '.tag_name[1:]') \ 72 | && curl -Ls https://github.com/actions/runner/releases/download/v${GITHUB_RUNNER_VERSION}/actions-runner-linux-x64-$GITHUB_RUNNER_VERSION.tar.gz | tar zx \ 73 | && chown -R runner:user /home/runner 74 | 75 | FROM base as runner 76 | 77 | RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor | tee /etc/apt/trusted.gpg.d/google.gpg >/dev/null \ 78 | && echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list 79 | 80 | RUN apt update && apt install -y \ 81 | libjemalloc-dev \ 82 | protobuf-compiler \ 83 | libyaml-dev \ 84 | libreadline-dev \ 85 | ${CHROME_VERSION:-google-chrome-stable} \ 86 | && rm /etc/apt/sources.list.d/google-chrome.list \ 87 | && rm -rf /var/lib/apt/lists/* /var/cache/apt/* 88 | 89 | ARG CHROME_DRIVER_VERSION 90 | RUN if [ ! -z "$CHROME_DRIVER_VERSION" ]; \ 91 | then CHROME_DRIVER_URL=https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/$CHROME_DRIVER_VERSION/linux64/chromedriver-linux64.zip ; \ 92 | else echo "Geting ChromeDriver latest version from https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_" \ 93 | && CHROME_MAJOR_VERSION=$(google-chrome --version | sed -E "s/.* ([0-9]+)(\.[0-9]+){3}.*/\1/") \ 94 | && CHROME_DRIVER_VERSION=$(wget -qO- https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_${CHROME_MAJOR_VERSION} | sed 's/\r$//') \ 95 | && CHROME_DRIVER_URL=https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/$CHROME_DRIVER_VERSION/linux64/chromedriver-linux64.zip ; \ 96 | fi \ 97 | && echo "Using ChromeDriver from: "$CHROME_DRIVER_URL \ 98 | && echo "Using ChromeDriver version: "$CHROME_DRIVER_VERSION \ 99 | && wget --no-verbose -O /tmp/chromedriver_linux64.zip $CHROME_DRIVER_URL \ 100 | && rm -rf /opt/selenium/chromedriver \ 101 | && unzip /tmp/chromedriver_linux64.zip -d /opt/selenium \ 102 | && rm /tmp/chromedriver_linux64.zip \ 103 | && mv /opt/selenium/chromedriver-linux64/chromedriver /opt/selenium/chromedriver-$CHROME_DRIVER_VERSION \ 104 | && chmod 755 /opt/selenium/chromedriver-$CHROME_DRIVER_VERSION \ 105 | && ln -fs /opt/selenium/chromedriver-$CHROME_DRIVER_VERSION /usr/bin/chromedriver 106 | 107 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain 1.74.0 -y 108 | -------------------------------------------------------------------------------- /util/src/lib.rs: -------------------------------------------------------------------------------- 1 | use cfg_if::cfg_if; 2 | #[allow(unused)] 3 | use lazy_static::lazy_static; 4 | 5 | #[allow(unused)] 6 | use std::sync::atomic::{AtomicBool, Ordering}; 7 | #[allow(unused)] 8 | use std::sync::Mutex; 9 | use std::time::Instant; 10 | 11 | pub mod fs; 12 | pub mod mount; 13 | pub mod network; 14 | 15 | #[derive(Debug)] 16 | pub struct CommandResult { 17 | pub command: String, 18 | pub stdout: String, 19 | pub stderr: String, 20 | pub status: std::process::ExitStatus, 21 | } 22 | 23 | impl std::fmt::Display for CommandResult { 24 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 25 | write!( 26 | f, 27 | "Command '{}' executed and failed with status: {}", 28 | self.command, self.status 29 | )?; 30 | write!(f, " stdout: {}", self.stdout)?; 31 | write!(f, " stderr: {}", self.stderr) 32 | } 33 | } 34 | 35 | #[derive(thiserror::Error, Debug)] 36 | pub enum CommandExecutionError { 37 | #[error("Failed to start execution of '{command}': {err}")] 38 | ExecutionStart { 39 | command: String, 40 | err: std::io::Error, 41 | }, 42 | 43 | #[error("{0}")] 44 | CommandFailure(Box), 45 | } 46 | 47 | #[cfg_attr( 48 | any(test, automock, feature = "testing"), 49 | mockall::automock, 50 | allow(dead_code) 51 | )] 52 | pub mod inner { 53 | use super::*; 54 | 55 | pub fn to_string(command: &std::process::Command) -> String { 56 | format!( 57 | "{} {}", 58 | command.get_program().to_string_lossy(), 59 | command 60 | .get_args() 61 | .map(|s| s.to_string_lossy().into()) 62 | .collect::>() 63 | .join(" ") 64 | ) 65 | } 66 | 67 | pub fn output_to_exec_error( 68 | command: &std::process::Command, 69 | output: &std::process::Output, 70 | ) -> CommandExecutionError { 71 | CommandExecutionError::CommandFailure(Box::new(CommandResult { 72 | command: to_string(command), 73 | status: output.status, 74 | stdout: String::from_utf8_lossy(&output.stdout).to_string(), 75 | stderr: String::from_utf8_lossy(&output.stderr).to_string(), 76 | })) 77 | } 78 | 79 | pub fn internal_exec( 80 | cmd: &mut std::process::Command, 81 | ) -> Result { 82 | let start = Instant::now(); 83 | 84 | let output = cmd 85 | .output() 86 | .map_err(|err| CommandExecutionError::ExecutionStart { 87 | command: to_string(cmd), 88 | err, 89 | })?; 90 | 91 | if !output.status.success() { 92 | return Err(output_to_exec_error(cmd, &output)); 93 | } 94 | 95 | let duration = start.elapsed(); 96 | log::trace!("Command {:?} executed in {}ms", cmd, duration.as_millis()); 97 | 98 | Ok(output) 99 | } 100 | 101 | pub fn internal_exec_spawn( 102 | cmd: &mut std::process::Command, 103 | ) -> Result { 104 | let output = cmd 105 | .spawn() 106 | .map_err(|err| CommandExecutionError::ExecutionStart { 107 | command: to_string(cmd), 108 | err, 109 | })?; 110 | 111 | Ok(output) 112 | } 113 | } 114 | 115 | #[cfg(any(test, feature = "testing"))] 116 | pub static USE_MOCKS: AtomicBool = AtomicBool::new(true); 117 | 118 | pub fn exec( 119 | cmd: &mut std::process::Command, 120 | ) -> Result { 121 | log::trace!( 122 | "Executing command {:?} with args {:?}", 123 | cmd.get_program(), 124 | cmd.get_args() 125 | ); 126 | cfg_if! { 127 | if #[cfg(any(test, feature = "testing"))] { 128 | if USE_MOCKS.load(Ordering::SeqCst) { 129 | mock_inner::internal_exec(cmd) 130 | } else { 131 | inner::internal_exec(cmd) 132 | } 133 | } else { 134 | inner::internal_exec(cmd) 135 | } 136 | } 137 | } 138 | 139 | pub fn exec_spawn( 140 | cmd: &mut std::process::Command, 141 | ) -> Result { 142 | log::trace!( 143 | "Executing command {:?} with args {:?}", 144 | cmd.get_program(), 145 | cmd.get_args() 146 | ); 147 | cfg_if! { 148 | if #[cfg(any(test, feature = "testing"))] { 149 | if USE_MOCKS.load(Ordering::SeqCst) { 150 | mock_inner::internal_exec_spawn(cmd) 151 | } else { 152 | inner::internal_exec_spawn(cmd) 153 | } 154 | } else { 155 | inner::internal_exec_spawn(cmd) 156 | } 157 | } 158 | } 159 | 160 | lazy_static! { 161 | pub static ref MTX: Mutex<()> = Mutex::new(()); 162 | } 163 | -------------------------------------------------------------------------------- /manager/src/lib.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | instance::{Instance, InstanceState}, 3 | network::{Forwarding, NetworkAllocation}, 4 | }; 5 | use anyhow::Result; 6 | use config::manager::ManagerConfig; 7 | use github::GitHub; 8 | use log::*; 9 | use signal_hook::{consts::SIGINT, iterator::Signals}; 10 | use std::sync::atomic::{AtomicBool, Ordering}; 11 | use std::sync::Arc; 12 | use std::thread; 13 | use std::time::Duration; 14 | 15 | pub mod disk; 16 | pub mod instance; 17 | pub mod network; 18 | 19 | pub struct Manager { 20 | pub config: ManagerConfig, 21 | pub instances: Vec, 22 | pub shutdown: Arc, 23 | } 24 | 25 | impl Manager { 26 | pub fn new(config: ManagerConfig) -> Self { 27 | let mut signals = Signals::new([SIGINT]).unwrap(); 28 | let shutdown = Arc::new(AtomicBool::new(false)); 29 | let cloned_shutdown = shutdown.clone(); 30 | 31 | thread::spawn(move || { 32 | for sig in signals.forever() { 33 | info!("Received signal {:?}", sig); 34 | shutdown.store(true, Ordering::Relaxed); 35 | } 36 | }); 37 | 38 | Self { 39 | config, 40 | instances: Vec::new(), 41 | shutdown: cloned_shutdown, 42 | } 43 | } 44 | 45 | pub fn setup(&mut self) -> Result<()> { 46 | let network_forwarding = Forwarding::new(&self.config.network_interface); 47 | network_forwarding.setup()?; 48 | 49 | let github = GitHub::new(&self.config.github_org, &self.config.github_pat); 50 | 51 | for role in &self.config.roles { 52 | for _ in 0..role.instance_count { 53 | let idx = self.instances.len() as u8 + 1; 54 | 55 | let network_allocation = 56 | NetworkAllocation::new(&self.config.network_interface, idx); 57 | 58 | let mut instance = Instance::new( 59 | network_allocation, 60 | github.clone(), 61 | &self.config.run_path, 62 | role, 63 | idx, 64 | ); 65 | instance.setup()?; 66 | self.instances.push(instance); 67 | } 68 | } 69 | Ok(()) 70 | } 71 | 72 | pub fn run(&mut self) -> Result<()> { 73 | loop { 74 | if self.shutdown.load(Ordering::Relaxed) { 75 | info!("Shutting down."); 76 | for instance in &mut self.instances { 77 | info!("{} Stopping instance", instance.log_prefix()); 78 | 79 | if let Err(e) = instance.stop() { 80 | error!("{} Failed to stop instance: {}", instance.log_prefix(), e); 81 | } 82 | let _ = instance.cleanup(); 83 | } 84 | break; 85 | } 86 | 87 | for instance in &mut self.instances { 88 | match instance.state() { 89 | InstanceState::Running => (), 90 | InstanceState::NotStarted | InstanceState::NotRunning => { 91 | info!("{} Starting instance", instance.log_prefix()); 92 | if let Err(e) = instance.start() { 93 | error!("{} Failed to start instance: {}", instance.log_prefix(), e); 94 | } 95 | } 96 | InstanceState::Errorred => { 97 | error!("{} Instance has errored.", instance.log_prefix()); 98 | thread::sleep(Duration::from_secs(20)); 99 | instance.reset(); 100 | } 101 | } 102 | } 103 | thread::sleep(Duration::from_secs(1)); 104 | } 105 | 106 | Ok(()) 107 | } 108 | 109 | pub fn debug(&mut self, role: &str, idx: u8) -> Result<()> { 110 | let network_forwarding = Forwarding::new(&self.config.network_interface); 111 | let network_allocation = NetworkAllocation::new(&self.config.network_interface, idx); 112 | let github = GitHub::new(&self.config.github_org, &self.config.github_pat); 113 | let mut role = self 114 | .config 115 | .roles 116 | .iter() 117 | .find(|r| r.name == role) 118 | .expect("Could not find role.") 119 | .clone(); 120 | 121 | // Set output to console 122 | role.kernel_cmdline = match role.kernel_cmdline { 123 | Some(ref cmdline) => Some(format!("console=ttyS0 {}", cmdline)), 124 | None => Some("console=ttyS0".to_string()), 125 | }; 126 | 127 | let mut instance = Instance::new( 128 | network_allocation, 129 | github, 130 | &self.config.run_path, 131 | &role, 132 | idx, 133 | ); 134 | network_forwarding.setup()?; 135 | instance.setup()?; 136 | 137 | instance.run_once()?; 138 | 139 | instance.cleanup()?; 140 | Ok(()) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /util/src/fs.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use camino::Utf8Path; 3 | use std::process::Command; 4 | 5 | pub fn copy_sparse(from: impl AsRef, to: impl AsRef) -> std::io::Result<()> { 6 | let from = from.as_ref(); 7 | let to = to.as_ref(); 8 | 9 | exec(Command::new("cp").args(["--sparse=always", from.as_str(), to.as_str()])) 10 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; 11 | 12 | Ok(()) 13 | } 14 | 15 | pub fn rm_rf(path: impl AsRef) -> std::io::Result<()> { 16 | let path = path.as_ref(); 17 | 18 | exec(Command::new("rm").args(["-rf", path.as_str()])) 19 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; 20 | 21 | Ok(()) 22 | } 23 | 24 | pub fn mkdir_p(path: impl AsRef) -> std::io::Result<()> { 25 | let path = path.as_ref(); 26 | 27 | exec(Command::new("mkdir").args(["-p", path.as_str()])) 28 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; 29 | 30 | Ok(()) 31 | } 32 | 33 | pub fn mkfs_ext4(path: impl AsRef) -> std::io::Result<()> { 34 | let path = path.as_ref(); 35 | 36 | exec(Command::new("mkfs.ext4").arg(path.as_str())) 37 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; 38 | 39 | Ok(()) 40 | } 41 | 42 | pub fn dd(path: impl AsRef, size_in_mb: u64) -> std::io::Result<()> { 43 | let path = path.as_ref(); 44 | 45 | exec(Command::new("dd").args([ 46 | "if=/dev/zero", 47 | &format!("of={}", &path), 48 | "conv=sparse", 49 | "bs=1M", 50 | &format!("count={}", size_in_mb), 51 | ])) 52 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; 53 | 54 | Ok(()) 55 | } 56 | 57 | pub fn du(path: impl AsRef) -> std::io::Result { 58 | let path = path.as_ref(); 59 | 60 | let du_output = exec(Command::new("du").args([&path.as_str()])) 61 | .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) 62 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; 63 | 64 | let size = du_output 65 | .split_whitespace() 66 | .next() 67 | .ok_or(std::io::Error::new( 68 | std::io::ErrorKind::Other, 69 | format!("Couldn not split '{:?}' into number and rest", du_output), 70 | ))?; 71 | 72 | size.parse().map_err(|e| { 73 | std::io::Error::new( 74 | std::io::ErrorKind::Other, 75 | format!("Could not parse '{:?}' to number: {}", size, e), 76 | ) 77 | }) 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | use super::*; 83 | use std::os::unix::process::ExitStatusExt; 84 | 85 | #[test] 86 | fn test_copy_sparse() { 87 | let _m = MTX.lock(); 88 | 89 | let ctx = mock_inner::internal_exec_context(); 90 | ctx.expect() 91 | .withf(|c| inner::to_string(c) == "cp --sparse=always /foo.txt /bar.txt") 92 | .returning(|_| { 93 | Ok(std::process::Output { 94 | status: std::process::ExitStatus::from_raw(0), 95 | stdout: vec![], 96 | stderr: vec![], 97 | }) 98 | }); 99 | 100 | let result = copy_sparse("/foo.txt", "/bar.txt"); 101 | assert!(result.is_ok()); 102 | ctx.checkpoint(); 103 | } 104 | 105 | #[test] 106 | fn test_rm_rf() { 107 | let _m = MTX.lock(); 108 | 109 | let ctx = mock_inner::internal_exec_context(); 110 | ctx.expect() 111 | .withf(|c| inner::to_string(c) == "rm -rf /foo.txt") 112 | .returning(|_| { 113 | Ok(std::process::Output { 114 | status: std::process::ExitStatus::from_raw(0), 115 | stdout: vec![], 116 | stderr: vec![], 117 | }) 118 | }); 119 | 120 | let result = rm_rf("/foo.txt"); 121 | assert!(result.is_ok()); 122 | ctx.checkpoint(); 123 | } 124 | 125 | #[test] 126 | fn test_mkdir_p() { 127 | let _m = MTX.lock(); 128 | 129 | let ctx = mock_inner::internal_exec_context(); 130 | ctx.expect() 131 | .withf(|c| inner::to_string(c) == "mkdir -p /foo") 132 | .returning(|_| { 133 | Ok(std::process::Output { 134 | status: std::process::ExitStatus::from_raw(0), 135 | stdout: vec![], 136 | stderr: vec![], 137 | }) 138 | }); 139 | 140 | let result = mkdir_p("/foo"); 141 | assert!(result.is_ok()); 142 | ctx.checkpoint(); 143 | } 144 | 145 | #[test] 146 | fn test_mkfs_ext4() { 147 | let _m = MTX.lock(); 148 | 149 | let ctx = mock_inner::internal_exec_context(); 150 | ctx.expect() 151 | .withf(|c| inner::to_string(c) == "mkfs.ext4 /dev/sda1") 152 | .returning(|_| { 153 | Ok(std::process::Output { 154 | status: std::process::ExitStatus::from_raw(0), 155 | stdout: vec![], 156 | stderr: vec![], 157 | }) 158 | }); 159 | 160 | let result = mkfs_ext4("/dev/sda1"); 161 | assert!(result.is_ok()); 162 | ctx.checkpoint(); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /manager/src/instance.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | disk::{Disk, DiskFormat}, 3 | network::NetworkAllocation, 4 | }; 5 | use anyhow::Result; 6 | use camino::Utf8PathBuf; 7 | use config::{ 8 | firecracker::{BootSource, Drive, FirecrackerConfig, MachineConfig, NetworkInterface}, 9 | manager::Role, 10 | DEFAULT_BOOT_ARGS, 11 | }; 12 | use github::GitHub; 13 | use log::*; 14 | use rand::distributions::{Alphanumeric, DistString}; 15 | use serde_json; 16 | use std::{fs, process::Command}; 17 | use util::fs::{copy_sparse, rm_rf}; 18 | 19 | pub enum InstanceState { 20 | NotStarted, 21 | Running, 22 | NotRunning, 23 | Errorred, 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct Instance { 28 | network_allocation: NetworkAllocation, 29 | work_dir: Utf8PathBuf, 30 | kernel_image: Utf8PathBuf, 31 | kernel_cmdline: Option, 32 | rootfs_image: Utf8PathBuf, 33 | cpus: u32, 34 | memory_size: u32, 35 | cache_paths: Vec, 36 | cache: Disk, 37 | max_cache_pct: u8, 38 | idx: u8, 39 | role: String, 40 | github: GitHub, 41 | labels: Vec, 42 | child: Option, 43 | } 44 | 45 | impl Instance { 46 | pub fn new( 47 | network_allocation: NetworkAllocation, 48 | github: GitHub, 49 | work_dir: &Utf8PathBuf, 50 | role: &Role, 51 | idx: u8, 52 | ) -> Self { 53 | let instance_dir: Utf8PathBuf = work_dir.join(role.slug()).join(format!("{}", idx)); 54 | let cache = Disk::new(&instance_dir, "cache", role.cache_size, DiskFormat::Ext4); 55 | 56 | Self { 57 | network_allocation, 58 | work_dir: instance_dir.clone(), 59 | kernel_image: role.kernel_image.clone(), 60 | kernel_cmdline: role.kernel_cmdline.clone(), 61 | rootfs_image: role.rootfs_image.clone(), 62 | cpus: role.cpus, 63 | memory_size: role.memory_size, 64 | cache_paths: role.cache_paths.clone(), 65 | role: role.slug(), 66 | max_cache_pct: role.max_cache_pct, 67 | labels: role.labels.clone(), 68 | github, 69 | cache, 70 | idx, 71 | child: None, 72 | } 73 | } 74 | 75 | pub fn log_prefix(&self) -> String { 76 | format!("[{} {}]", self.role, self.idx) 77 | } 78 | 79 | pub fn name(&self) -> String { 80 | format!( 81 | "{}-{}-{}", 82 | self.role, 83 | self.idx, 84 | Alphanumeric.sample_string(&mut rand::thread_rng(), 4), 85 | ) 86 | } 87 | 88 | pub fn setup(&mut self) -> Result<()> { 89 | info!("Running instance with: {:?}", self); 90 | 91 | debug!( 92 | "{} Creating work dir: '{}'", 93 | self.log_prefix(), 94 | self.work_dir 95 | ); 96 | fs::create_dir_all(&self.work_dir)?; 97 | 98 | debug!( 99 | "{} Setup network with tap: '{}', host address: '{}'", 100 | self.log_prefix(), 101 | self.network_allocation.tap_name, 102 | self.network_allocation.host_ip, 103 | ); 104 | self.network_allocation.setup()?; 105 | 106 | debug!( 107 | "{} Initialize shared cache on path: '{}' (size: {}GB)", 108 | self.log_prefix(), 109 | self.cache.path_with_filename(), 110 | self.cache.size, 111 | ); 112 | self.cache.setup()?; 113 | 114 | Ok(()) 115 | } 116 | 117 | pub fn boot_args(&self) -> Result { 118 | let mut boot_args = vec![DEFAULT_BOOT_ARGS.to_string()]; 119 | 120 | // Add GitHub token 121 | boot_args.push(format!( 122 | "github_token={}", 123 | &self.github.registration_token()? 124 | )); 125 | boot_args.push(format!("github_org={}", &self.github.org)); 126 | 127 | // Add cache paths 128 | if !self.cache_paths.is_empty() { 129 | boot_args.push(format!( 130 | "cache_paths=\"{}\"", 131 | self.cache_paths 132 | .iter() 133 | .map(|cp| cp.to_string()) 134 | .collect::>() 135 | .join(",") 136 | )); 137 | } 138 | 139 | // Add overridden boot args 140 | if let Some(ref cmdline) = &self.kernel_cmdline { 141 | boot_args.push(cmdline.to_string()); 142 | } 143 | 144 | boot_args.push(format!("github_runner_name={}", self.name())); 145 | boot_args.push(format!("github_runner_labels={}", self.labels())); 146 | 147 | Ok(boot_args.join(" ")) 148 | } 149 | 150 | pub fn labels(&self) -> String { 151 | let mut labels = self.labels.clone(); 152 | labels.push(self.role.to_string()); 153 | labels.join(",") 154 | } 155 | 156 | pub fn config(&self) -> Result { 157 | let boot_source = BootSource { 158 | kernel_image_path: self.kernel_image.to_string(), 159 | boot_args: self.boot_args()?, 160 | }; 161 | 162 | let drives = vec![ 163 | Drive { 164 | drive_id: "rootfs".to_string(), 165 | path_on_host: self.work_dir.join("rootfs.ext4"), 166 | is_root_device: true, 167 | is_read_only: false, 168 | cache_type: None, 169 | }, 170 | Drive { 171 | drive_id: "cache".to_string(), 172 | path_on_host: self.cache.path_with_filename(), 173 | is_root_device: false, 174 | is_read_only: false, 175 | cache_type: None, 176 | }, 177 | ]; 178 | 179 | let network_interfaces = vec![NetworkInterface { 180 | iface_id: "eth0".to_string(), 181 | guest_mac: self.network_allocation.guest_mac.clone(), 182 | host_dev_name: self.network_allocation.tap_name.clone(), 183 | }]; 184 | 185 | let machine_config = MachineConfig { 186 | vcpu_count: self.cpus, 187 | mem_size_mib: self.memory_size * 1024, 188 | }; 189 | 190 | Ok(FirecrackerConfig { 191 | boot_source, 192 | drives, 193 | network_interfaces, 194 | machine_config, 195 | }) 196 | } 197 | 198 | pub fn setup_run(&mut self) -> Result<()> { 199 | debug!( 200 | "{} Copy rootfs from: '{}'to '{}'", 201 | self.log_prefix(), 202 | self.rootfs_image, 203 | self.work_dir.join("rootfs.ext4"), 204 | ); 205 | let _ = rm_rf(self.work_dir.join("rootfs.ext4")); 206 | copy_sparse(&self.rootfs_image, self.work_dir.join("rootfs.ext4"))?; 207 | 208 | self.try_clear_cache()?; 209 | 210 | debug!( 211 | "{} Generate config: '{}'", 212 | self.log_prefix(), 213 | self.work_dir.join("config.json") 214 | ); 215 | 216 | fs::write( 217 | self.work_dir.join("config.json"), 218 | serde_json::to_string(&self.config()?)?, 219 | )?; 220 | Ok(()) 221 | } 222 | 223 | pub fn cleanup(&self) -> Result<()> { 224 | let _ = rm_rf(&self.work_dir); 225 | Ok(()) 226 | } 227 | 228 | pub fn reset(&mut self) { 229 | self.child = None; 230 | } 231 | 232 | pub fn try_clear_cache(&mut self) -> Result<()> { 233 | let usage_pct = self.cache.usage_pct()?; 234 | if usage_pct > self.max_cache_pct { 235 | info!( 236 | "Cache disk is over {}% ({}%), clearing cache", 237 | self.max_cache_pct, usage_pct 238 | ); 239 | 240 | self.cache.destroy()?; 241 | self.cache.setup()?; 242 | } 243 | Ok(()) 244 | } 245 | 246 | pub fn stop(&mut self) -> Result<()> { 247 | info!("{} Shutting down instance", self.log_prefix()); 248 | 249 | match self.child.as_mut() { 250 | Some(child) => { 251 | child.kill()?; 252 | child.wait()?; 253 | } 254 | None => { 255 | info!("{} No instance to shut down", self.log_prefix()); 256 | } 257 | } 258 | Ok(()) 259 | } 260 | 261 | pub fn start(&mut self) -> Result<()> { 262 | self.setup_run()?; 263 | 264 | debug!("{} Running firecracker", self.log_prefix()); 265 | let child = Command::new("firecracker") 266 | .args(["--config-file", "config.json", "--no-api"]) 267 | .stdin(std::process::Stdio::null()) 268 | .stdout(std::process::Stdio::null()) 269 | .stderr(std::process::Stdio::null()) 270 | .current_dir(&self.work_dir) 271 | .spawn()?; 272 | self.child = Some(child); 273 | Ok(()) 274 | } 275 | 276 | pub fn run_once(&mut self) -> Result<()> { 277 | self.setup_run()?; 278 | 279 | debug!("{} Running firecracker", self.log_prefix()); 280 | Command::new("firecracker") 281 | .args(["--config-file", "config.json", "--no-api"]) 282 | .current_dir(&self.work_dir) 283 | .status() 284 | .expect("Failed to start process"); 285 | Ok(()) 286 | } 287 | 288 | pub fn state(&mut self) -> InstanceState { 289 | match self.child.as_mut() { 290 | Some(child) => match child.try_wait() { 291 | Ok(Some(status)) => { 292 | if status.success() { 293 | InstanceState::NotRunning 294 | } else { 295 | InstanceState::Errorred 296 | } 297 | } 298 | Ok(None) => InstanceState::Running, 299 | Err(_) => InstanceState::Errorred, 300 | }, 301 | None => InstanceState::NotStarted, 302 | } 303 | } 304 | } 305 | 306 | #[cfg(test)] 307 | mod tests { 308 | use super::*; 309 | use camino::Utf8PathBuf; 310 | 311 | #[test] 312 | fn test_instance_setup() { 313 | let workdir: Utf8PathBuf = "/tmp/test_instance_setup".into(); 314 | let github = GitHub::new("test", "test"); 315 | let network_allocation = NetworkAllocation::new("eth0", 1); 316 | let role = Role { 317 | name: "test".to_string(), 318 | kernel_image: Utf8PathBuf::from("kernel"), 319 | kernel_cmdline: None, 320 | rootfs_image: Utf8PathBuf::from("rootfs"), 321 | cpus: 1, 322 | memory_size: 1, 323 | cache_size: 1, 324 | max_cache_pct: 90, 325 | overlay_size: 1, 326 | instance_count: 1, 327 | cache_paths: Vec::new(), 328 | labels: Vec::new(), 329 | }; 330 | 331 | let mut _instance = Instance::new(network_allocation, github.clone(), &workdir, &role, 1); 332 | //instance.setup().expect("Could not setup instance"); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "actions-runner" 7 | version = "0.1.1" 8 | dependencies = [ 9 | "anyhow", 10 | "builder", 11 | "camino", 12 | "chrono", 13 | "clap", 14 | "config", 15 | "fern", 16 | "initialiser", 17 | "log", 18 | "manager", 19 | "runner", 20 | "thiserror", 21 | ] 22 | 23 | [[package]] 24 | name = "addr2line" 25 | version = "0.22.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 28 | dependencies = [ 29 | "gimli", 30 | ] 31 | 32 | [[package]] 33 | name = "adler" 34 | version = "1.0.2" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 37 | 38 | [[package]] 39 | name = "android-tzdata" 40 | version = "0.1.1" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 43 | 44 | [[package]] 45 | name = "android_system_properties" 46 | version = "0.1.5" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 49 | dependencies = [ 50 | "libc", 51 | ] 52 | 53 | [[package]] 54 | name = "anstream" 55 | version = "0.6.14" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 58 | dependencies = [ 59 | "anstyle", 60 | "anstyle-parse", 61 | "anstyle-query", 62 | "anstyle-wincon", 63 | "colorchoice", 64 | "is_terminal_polyfill", 65 | "utf8parse", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle" 70 | version = "1.0.7" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 73 | 74 | [[package]] 75 | name = "anstyle-parse" 76 | version = "0.2.4" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 79 | dependencies = [ 80 | "utf8parse", 81 | ] 82 | 83 | [[package]] 84 | name = "anstyle-query" 85 | version = "1.1.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" 88 | dependencies = [ 89 | "windows-sys 0.52.0", 90 | ] 91 | 92 | [[package]] 93 | name = "anstyle-wincon" 94 | version = "3.0.3" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 97 | dependencies = [ 98 | "anstyle", 99 | "windows-sys 0.52.0", 100 | ] 101 | 102 | [[package]] 103 | name = "anyhow" 104 | version = "1.0.86" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 107 | dependencies = [ 108 | "backtrace", 109 | ] 110 | 111 | [[package]] 112 | name = "autocfg" 113 | version = "1.3.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 116 | 117 | [[package]] 118 | name = "backtrace" 119 | version = "0.3.73" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 122 | dependencies = [ 123 | "addr2line", 124 | "cc", 125 | "cfg-if", 126 | "libc", 127 | "miniz_oxide", 128 | "object", 129 | "rustc-demangle", 130 | ] 131 | 132 | [[package]] 133 | name = "base64" 134 | version = "0.22.1" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 137 | 138 | [[package]] 139 | name = "bitflags" 140 | version = "2.6.0" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 143 | 144 | [[package]] 145 | name = "builder" 146 | version = "0.1.0" 147 | dependencies = [ 148 | "anyhow", 149 | "camino", 150 | "log", 151 | "thiserror", 152 | "util", 153 | ] 154 | 155 | [[package]] 156 | name = "bumpalo" 157 | version = "3.16.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 160 | 161 | [[package]] 162 | name = "bytes" 163 | version = "1.6.1" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" 166 | 167 | [[package]] 168 | name = "camino" 169 | version = "1.1.7" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" 172 | dependencies = [ 173 | "serde", 174 | ] 175 | 176 | [[package]] 177 | name = "cc" 178 | version = "1.1.2" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "47de7e88bbbd467951ae7f5a6f34f70d1b4d9cfce53d5fd70f74ebe118b3db56" 181 | 182 | [[package]] 183 | name = "cfg-if" 184 | version = "1.0.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 187 | 188 | [[package]] 189 | name = "cfg_aliases" 190 | version = "0.2.1" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 193 | 194 | [[package]] 195 | name = "chrono" 196 | version = "0.4.38" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 199 | dependencies = [ 200 | "android-tzdata", 201 | "iana-time-zone", 202 | "js-sys", 203 | "num-traits", 204 | "wasm-bindgen", 205 | "windows-targets 0.52.6", 206 | ] 207 | 208 | [[package]] 209 | name = "clap" 210 | version = "4.5.9" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" 213 | dependencies = [ 214 | "clap_builder", 215 | "clap_derive", 216 | ] 217 | 218 | [[package]] 219 | name = "clap_builder" 220 | version = "4.5.9" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" 223 | dependencies = [ 224 | "anstream", 225 | "anstyle", 226 | "clap_lex", 227 | "strsim", 228 | ] 229 | 230 | [[package]] 231 | name = "clap_derive" 232 | version = "4.5.8" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" 235 | dependencies = [ 236 | "heck", 237 | "proc-macro2", 238 | "quote", 239 | "syn", 240 | ] 241 | 242 | [[package]] 243 | name = "clap_lex" 244 | version = "0.7.1" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" 247 | 248 | [[package]] 249 | name = "colorchoice" 250 | version = "1.0.1" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 253 | 254 | [[package]] 255 | name = "config" 256 | version = "0.1.0" 257 | dependencies = [ 258 | "anyhow", 259 | "camino", 260 | "serde", 261 | "thiserror", 262 | "toml", 263 | ] 264 | 265 | [[package]] 266 | name = "core-foundation-sys" 267 | version = "0.8.6" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 270 | 271 | [[package]] 272 | name = "downcast" 273 | version = "0.11.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" 276 | 277 | [[package]] 278 | name = "equivalent" 279 | version = "1.0.1" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 282 | 283 | [[package]] 284 | name = "fern" 285 | version = "0.6.2" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" 288 | dependencies = [ 289 | "log", 290 | ] 291 | 292 | [[package]] 293 | name = "fnv" 294 | version = "1.0.7" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 297 | 298 | [[package]] 299 | name = "form_urlencoded" 300 | version = "1.2.1" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 303 | dependencies = [ 304 | "percent-encoding", 305 | ] 306 | 307 | [[package]] 308 | name = "fragile" 309 | version = "2.0.0" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" 312 | 313 | [[package]] 314 | name = "futures-channel" 315 | version = "0.3.30" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 318 | dependencies = [ 319 | "futures-core", 320 | "futures-sink", 321 | ] 322 | 323 | [[package]] 324 | name = "futures-core" 325 | version = "0.3.30" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 328 | 329 | [[package]] 330 | name = "futures-io" 331 | version = "0.3.30" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 334 | 335 | [[package]] 336 | name = "futures-sink" 337 | version = "0.3.30" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 340 | 341 | [[package]] 342 | name = "futures-task" 343 | version = "0.3.30" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 346 | 347 | [[package]] 348 | name = "futures-util" 349 | version = "0.3.30" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 352 | dependencies = [ 353 | "futures-core", 354 | "futures-io", 355 | "futures-sink", 356 | "futures-task", 357 | "memchr", 358 | "pin-project-lite", 359 | "pin-utils", 360 | "slab", 361 | ] 362 | 363 | [[package]] 364 | name = "getrandom" 365 | version = "0.2.15" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 368 | dependencies = [ 369 | "cfg-if", 370 | "libc", 371 | "wasi", 372 | ] 373 | 374 | [[package]] 375 | name = "gimli" 376 | version = "0.29.0" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 379 | 380 | [[package]] 381 | name = "github" 382 | version = "0.1.0" 383 | dependencies = [ 384 | "anyhow", 385 | "log", 386 | "nix", 387 | "reqwest", 388 | "serde", 389 | "serde_json", 390 | "thiserror", 391 | ] 392 | 393 | [[package]] 394 | name = "hashbrown" 395 | version = "0.14.5" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 398 | 399 | [[package]] 400 | name = "heck" 401 | version = "0.5.0" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 404 | 405 | [[package]] 406 | name = "http" 407 | version = "1.1.0" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 410 | dependencies = [ 411 | "bytes", 412 | "fnv", 413 | "itoa", 414 | ] 415 | 416 | [[package]] 417 | name = "http-body" 418 | version = "1.0.1" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 421 | dependencies = [ 422 | "bytes", 423 | "http", 424 | ] 425 | 426 | [[package]] 427 | name = "http-body-util" 428 | version = "0.1.2" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 431 | dependencies = [ 432 | "bytes", 433 | "futures-util", 434 | "http", 435 | "http-body", 436 | "pin-project-lite", 437 | ] 438 | 439 | [[package]] 440 | name = "httparse" 441 | version = "1.9.4" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" 444 | 445 | [[package]] 446 | name = "hyper" 447 | version = "1.4.1" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" 450 | dependencies = [ 451 | "bytes", 452 | "futures-channel", 453 | "futures-util", 454 | "http", 455 | "http-body", 456 | "httparse", 457 | "itoa", 458 | "pin-project-lite", 459 | "smallvec", 460 | "tokio", 461 | "want", 462 | ] 463 | 464 | [[package]] 465 | name = "hyper-rustls" 466 | version = "0.27.2" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" 469 | dependencies = [ 470 | "futures-util", 471 | "http", 472 | "hyper", 473 | "hyper-util", 474 | "rustls", 475 | "rustls-pki-types", 476 | "tokio", 477 | "tokio-rustls", 478 | "tower-service", 479 | "webpki-roots", 480 | ] 481 | 482 | [[package]] 483 | name = "hyper-util" 484 | version = "0.1.6" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" 487 | dependencies = [ 488 | "bytes", 489 | "futures-channel", 490 | "futures-util", 491 | "http", 492 | "http-body", 493 | "hyper", 494 | "pin-project-lite", 495 | "socket2", 496 | "tokio", 497 | "tower", 498 | "tower-service", 499 | "tracing", 500 | ] 501 | 502 | [[package]] 503 | name = "iana-time-zone" 504 | version = "0.1.60" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 507 | dependencies = [ 508 | "android_system_properties", 509 | "core-foundation-sys", 510 | "iana-time-zone-haiku", 511 | "js-sys", 512 | "wasm-bindgen", 513 | "windows-core", 514 | ] 515 | 516 | [[package]] 517 | name = "iana-time-zone-haiku" 518 | version = "0.1.2" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 521 | dependencies = [ 522 | "cc", 523 | ] 524 | 525 | [[package]] 526 | name = "idna" 527 | version = "0.5.0" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 530 | dependencies = [ 531 | "unicode-bidi", 532 | "unicode-normalization", 533 | ] 534 | 535 | [[package]] 536 | name = "indexmap" 537 | version = "2.2.6" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 540 | dependencies = [ 541 | "equivalent", 542 | "hashbrown", 543 | ] 544 | 545 | [[package]] 546 | name = "initialiser" 547 | version = "0.1.0" 548 | dependencies = [ 549 | "anyhow", 550 | "camino", 551 | "config", 552 | "github", 553 | "log", 554 | "nix", 555 | "serde", 556 | "serde_json", 557 | "thiserror", 558 | "util", 559 | ] 560 | 561 | [[package]] 562 | name = "ipnet" 563 | version = "2.9.0" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" 566 | 567 | [[package]] 568 | name = "is_terminal_polyfill" 569 | version = "1.70.0" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 572 | 573 | [[package]] 574 | name = "itoa" 575 | version = "1.0.11" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 578 | 579 | [[package]] 580 | name = "js-sys" 581 | version = "0.3.69" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 584 | dependencies = [ 585 | "wasm-bindgen", 586 | ] 587 | 588 | [[package]] 589 | name = "lazy_static" 590 | version = "1.5.0" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 593 | 594 | [[package]] 595 | name = "libc" 596 | version = "0.2.155" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 599 | 600 | [[package]] 601 | name = "log" 602 | version = "0.4.22" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 605 | 606 | [[package]] 607 | name = "manager" 608 | version = "0.1.0" 609 | dependencies = [ 610 | "anyhow", 611 | "camino", 612 | "config", 613 | "github", 614 | "log", 615 | "mockall", 616 | "rand", 617 | "serde", 618 | "serde_json", 619 | "signal-hook", 620 | "thiserror", 621 | "util", 622 | ] 623 | 624 | [[package]] 625 | name = "memchr" 626 | version = "2.7.4" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 629 | 630 | [[package]] 631 | name = "mime" 632 | version = "0.3.17" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 635 | 636 | [[package]] 637 | name = "miniz_oxide" 638 | version = "0.7.4" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 641 | dependencies = [ 642 | "adler", 643 | ] 644 | 645 | [[package]] 646 | name = "mio" 647 | version = "0.8.11" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 650 | dependencies = [ 651 | "libc", 652 | "wasi", 653 | "windows-sys 0.48.0", 654 | ] 655 | 656 | [[package]] 657 | name = "mockall" 658 | version = "0.12.1" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" 661 | dependencies = [ 662 | "cfg-if", 663 | "downcast", 664 | "fragile", 665 | "lazy_static", 666 | "mockall_derive", 667 | "predicates", 668 | "predicates-tree", 669 | ] 670 | 671 | [[package]] 672 | name = "mockall_derive" 673 | version = "0.12.1" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" 676 | dependencies = [ 677 | "cfg-if", 678 | "proc-macro2", 679 | "quote", 680 | "syn", 681 | ] 682 | 683 | [[package]] 684 | name = "nix" 685 | version = "0.29.0" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 688 | dependencies = [ 689 | "bitflags", 690 | "cfg-if", 691 | "cfg_aliases", 692 | "libc", 693 | ] 694 | 695 | [[package]] 696 | name = "num-traits" 697 | version = "0.2.19" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 700 | dependencies = [ 701 | "autocfg", 702 | ] 703 | 704 | [[package]] 705 | name = "object" 706 | version = "0.36.1" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" 709 | dependencies = [ 710 | "memchr", 711 | ] 712 | 713 | [[package]] 714 | name = "once_cell" 715 | version = "1.19.0" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 718 | 719 | [[package]] 720 | name = "percent-encoding" 721 | version = "2.3.1" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 724 | 725 | [[package]] 726 | name = "pin-project" 727 | version = "1.1.5" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" 730 | dependencies = [ 731 | "pin-project-internal", 732 | ] 733 | 734 | [[package]] 735 | name = "pin-project-internal" 736 | version = "1.1.5" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" 739 | dependencies = [ 740 | "proc-macro2", 741 | "quote", 742 | "syn", 743 | ] 744 | 745 | [[package]] 746 | name = "pin-project-lite" 747 | version = "0.2.14" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 750 | 751 | [[package]] 752 | name = "pin-utils" 753 | version = "0.1.0" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 756 | 757 | [[package]] 758 | name = "ppv-lite86" 759 | version = "0.2.17" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 762 | 763 | [[package]] 764 | name = "predicates" 765 | version = "3.1.0" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" 768 | dependencies = [ 769 | "anstyle", 770 | "predicates-core", 771 | ] 772 | 773 | [[package]] 774 | name = "predicates-core" 775 | version = "1.0.6" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" 778 | 779 | [[package]] 780 | name = "predicates-tree" 781 | version = "1.0.9" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" 784 | dependencies = [ 785 | "predicates-core", 786 | "termtree", 787 | ] 788 | 789 | [[package]] 790 | name = "proc-macro2" 791 | version = "1.0.86" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 794 | dependencies = [ 795 | "unicode-ident", 796 | ] 797 | 798 | [[package]] 799 | name = "quinn" 800 | version = "0.11.2" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" 803 | dependencies = [ 804 | "bytes", 805 | "pin-project-lite", 806 | "quinn-proto", 807 | "quinn-udp", 808 | "rustc-hash", 809 | "rustls", 810 | "thiserror", 811 | "tokio", 812 | "tracing", 813 | ] 814 | 815 | [[package]] 816 | name = "quinn-proto" 817 | version = "0.11.3" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" 820 | dependencies = [ 821 | "bytes", 822 | "rand", 823 | "ring", 824 | "rustc-hash", 825 | "rustls", 826 | "slab", 827 | "thiserror", 828 | "tinyvec", 829 | "tracing", 830 | ] 831 | 832 | [[package]] 833 | name = "quinn-udp" 834 | version = "0.5.2" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" 837 | dependencies = [ 838 | "libc", 839 | "once_cell", 840 | "socket2", 841 | "tracing", 842 | "windows-sys 0.52.0", 843 | ] 844 | 845 | [[package]] 846 | name = "quote" 847 | version = "1.0.36" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 850 | dependencies = [ 851 | "proc-macro2", 852 | ] 853 | 854 | [[package]] 855 | name = "rand" 856 | version = "0.8.5" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 859 | dependencies = [ 860 | "libc", 861 | "rand_chacha", 862 | "rand_core", 863 | ] 864 | 865 | [[package]] 866 | name = "rand_chacha" 867 | version = "0.3.1" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 870 | dependencies = [ 871 | "ppv-lite86", 872 | "rand_core", 873 | ] 874 | 875 | [[package]] 876 | name = "rand_core" 877 | version = "0.6.4" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 880 | dependencies = [ 881 | "getrandom", 882 | ] 883 | 884 | [[package]] 885 | name = "reqwest" 886 | version = "0.12.5" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" 889 | dependencies = [ 890 | "base64", 891 | "bytes", 892 | "futures-channel", 893 | "futures-core", 894 | "futures-util", 895 | "http", 896 | "http-body", 897 | "http-body-util", 898 | "hyper", 899 | "hyper-rustls", 900 | "hyper-util", 901 | "ipnet", 902 | "js-sys", 903 | "log", 904 | "mime", 905 | "once_cell", 906 | "percent-encoding", 907 | "pin-project-lite", 908 | "quinn", 909 | "rustls", 910 | "rustls-pemfile", 911 | "rustls-pki-types", 912 | "serde", 913 | "serde_json", 914 | "serde_urlencoded", 915 | "sync_wrapper", 916 | "tokio", 917 | "tokio-rustls", 918 | "tower-service", 919 | "url", 920 | "wasm-bindgen", 921 | "wasm-bindgen-futures", 922 | "web-sys", 923 | "webpki-roots", 924 | "winreg", 925 | ] 926 | 927 | [[package]] 928 | name = "ring" 929 | version = "0.17.8" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" 932 | dependencies = [ 933 | "cc", 934 | "cfg-if", 935 | "getrandom", 936 | "libc", 937 | "spin", 938 | "untrusted", 939 | "windows-sys 0.52.0", 940 | ] 941 | 942 | [[package]] 943 | name = "runner" 944 | version = "0.1.0" 945 | dependencies = [ 946 | "anyhow", 947 | "github", 948 | "log", 949 | "thiserror", 950 | "util", 951 | ] 952 | 953 | [[package]] 954 | name = "rustc-demangle" 955 | version = "0.1.24" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 958 | 959 | [[package]] 960 | name = "rustc-hash" 961 | version = "1.1.0" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 964 | 965 | [[package]] 966 | name = "rustls" 967 | version = "0.23.11" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" 970 | dependencies = [ 971 | "once_cell", 972 | "ring", 973 | "rustls-pki-types", 974 | "rustls-webpki", 975 | "subtle", 976 | "zeroize", 977 | ] 978 | 979 | [[package]] 980 | name = "rustls-pemfile" 981 | version = "2.1.2" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" 984 | dependencies = [ 985 | "base64", 986 | "rustls-pki-types", 987 | ] 988 | 989 | [[package]] 990 | name = "rustls-pki-types" 991 | version = "1.7.0" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" 994 | 995 | [[package]] 996 | name = "rustls-webpki" 997 | version = "0.102.5" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" 1000 | dependencies = [ 1001 | "ring", 1002 | "rustls-pki-types", 1003 | "untrusted", 1004 | ] 1005 | 1006 | [[package]] 1007 | name = "ryu" 1008 | version = "1.0.18" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1011 | 1012 | [[package]] 1013 | name = "serde" 1014 | version = "1.0.204" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" 1017 | dependencies = [ 1018 | "serde_derive", 1019 | ] 1020 | 1021 | [[package]] 1022 | name = "serde_derive" 1023 | version = "1.0.204" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" 1026 | dependencies = [ 1027 | "proc-macro2", 1028 | "quote", 1029 | "syn", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "serde_json" 1034 | version = "1.0.120" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" 1037 | dependencies = [ 1038 | "itoa", 1039 | "ryu", 1040 | "serde", 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "serde_spanned" 1045 | version = "0.6.6" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" 1048 | dependencies = [ 1049 | "serde", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "serde_urlencoded" 1054 | version = "0.7.1" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1057 | dependencies = [ 1058 | "form_urlencoded", 1059 | "itoa", 1060 | "ryu", 1061 | "serde", 1062 | ] 1063 | 1064 | [[package]] 1065 | name = "signal-hook" 1066 | version = "0.3.17" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 1069 | dependencies = [ 1070 | "libc", 1071 | "signal-hook-registry", 1072 | ] 1073 | 1074 | [[package]] 1075 | name = "signal-hook-registry" 1076 | version = "1.4.2" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1079 | dependencies = [ 1080 | "libc", 1081 | ] 1082 | 1083 | [[package]] 1084 | name = "slab" 1085 | version = "0.4.9" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1088 | dependencies = [ 1089 | "autocfg", 1090 | ] 1091 | 1092 | [[package]] 1093 | name = "smallvec" 1094 | version = "1.13.2" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1097 | 1098 | [[package]] 1099 | name = "socket2" 1100 | version = "0.5.7" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 1103 | dependencies = [ 1104 | "libc", 1105 | "windows-sys 0.52.0", 1106 | ] 1107 | 1108 | [[package]] 1109 | name = "spin" 1110 | version = "0.9.8" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1113 | 1114 | [[package]] 1115 | name = "strsim" 1116 | version = "0.11.1" 1117 | source = "registry+https://github.com/rust-lang/crates.io-index" 1118 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1119 | 1120 | [[package]] 1121 | name = "subtle" 1122 | version = "2.6.1" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1125 | 1126 | [[package]] 1127 | name = "syn" 1128 | version = "2.0.71" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" 1131 | dependencies = [ 1132 | "proc-macro2", 1133 | "quote", 1134 | "unicode-ident", 1135 | ] 1136 | 1137 | [[package]] 1138 | name = "sync_wrapper" 1139 | version = "1.0.1" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" 1142 | 1143 | [[package]] 1144 | name = "termtree" 1145 | version = "0.4.1" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 1148 | 1149 | [[package]] 1150 | name = "thiserror" 1151 | version = "1.0.62" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" 1154 | dependencies = [ 1155 | "thiserror-impl", 1156 | ] 1157 | 1158 | [[package]] 1159 | name = "thiserror-impl" 1160 | version = "1.0.62" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" 1163 | dependencies = [ 1164 | "proc-macro2", 1165 | "quote", 1166 | "syn", 1167 | ] 1168 | 1169 | [[package]] 1170 | name = "tinyvec" 1171 | version = "1.8.0" 1172 | source = "registry+https://github.com/rust-lang/crates.io-index" 1173 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 1174 | dependencies = [ 1175 | "tinyvec_macros", 1176 | ] 1177 | 1178 | [[package]] 1179 | name = "tinyvec_macros" 1180 | version = "0.1.1" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1183 | 1184 | [[package]] 1185 | name = "tokio" 1186 | version = "1.38.0" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" 1189 | dependencies = [ 1190 | "backtrace", 1191 | "bytes", 1192 | "libc", 1193 | "mio", 1194 | "pin-project-lite", 1195 | "socket2", 1196 | "windows-sys 0.48.0", 1197 | ] 1198 | 1199 | [[package]] 1200 | name = "tokio-rustls" 1201 | version = "0.26.0" 1202 | source = "registry+https://github.com/rust-lang/crates.io-index" 1203 | checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" 1204 | dependencies = [ 1205 | "rustls", 1206 | "rustls-pki-types", 1207 | "tokio", 1208 | ] 1209 | 1210 | [[package]] 1211 | name = "toml" 1212 | version = "0.8.14" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" 1215 | dependencies = [ 1216 | "serde", 1217 | "serde_spanned", 1218 | "toml_datetime", 1219 | "toml_edit", 1220 | ] 1221 | 1222 | [[package]] 1223 | name = "toml_datetime" 1224 | version = "0.6.6" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" 1227 | dependencies = [ 1228 | "serde", 1229 | ] 1230 | 1231 | [[package]] 1232 | name = "toml_edit" 1233 | version = "0.22.15" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" 1236 | dependencies = [ 1237 | "indexmap", 1238 | "serde", 1239 | "serde_spanned", 1240 | "toml_datetime", 1241 | "winnow", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "tower" 1246 | version = "0.4.13" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 1249 | dependencies = [ 1250 | "futures-core", 1251 | "futures-util", 1252 | "pin-project", 1253 | "pin-project-lite", 1254 | "tokio", 1255 | "tower-layer", 1256 | "tower-service", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "tower-layer" 1261 | version = "0.3.2" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" 1264 | 1265 | [[package]] 1266 | name = "tower-service" 1267 | version = "0.3.2" 1268 | source = "registry+https://github.com/rust-lang/crates.io-index" 1269 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1270 | 1271 | [[package]] 1272 | name = "tracing" 1273 | version = "0.1.40" 1274 | source = "registry+https://github.com/rust-lang/crates.io-index" 1275 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1276 | dependencies = [ 1277 | "pin-project-lite", 1278 | "tracing-attributes", 1279 | "tracing-core", 1280 | ] 1281 | 1282 | [[package]] 1283 | name = "tracing-attributes" 1284 | version = "0.1.27" 1285 | source = "registry+https://github.com/rust-lang/crates.io-index" 1286 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 1287 | dependencies = [ 1288 | "proc-macro2", 1289 | "quote", 1290 | "syn", 1291 | ] 1292 | 1293 | [[package]] 1294 | name = "tracing-core" 1295 | version = "0.1.32" 1296 | source = "registry+https://github.com/rust-lang/crates.io-index" 1297 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1298 | dependencies = [ 1299 | "once_cell", 1300 | ] 1301 | 1302 | [[package]] 1303 | name = "try-lock" 1304 | version = "0.2.5" 1305 | source = "registry+https://github.com/rust-lang/crates.io-index" 1306 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1307 | 1308 | [[package]] 1309 | name = "unicode-bidi" 1310 | version = "0.3.15" 1311 | source = "registry+https://github.com/rust-lang/crates.io-index" 1312 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1313 | 1314 | [[package]] 1315 | name = "unicode-ident" 1316 | version = "1.0.12" 1317 | source = "registry+https://github.com/rust-lang/crates.io-index" 1318 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1319 | 1320 | [[package]] 1321 | name = "unicode-normalization" 1322 | version = "0.1.23" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 1325 | dependencies = [ 1326 | "tinyvec", 1327 | ] 1328 | 1329 | [[package]] 1330 | name = "untrusted" 1331 | version = "0.9.0" 1332 | source = "registry+https://github.com/rust-lang/crates.io-index" 1333 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1334 | 1335 | [[package]] 1336 | name = "url" 1337 | version = "2.5.2" 1338 | source = "registry+https://github.com/rust-lang/crates.io-index" 1339 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" 1340 | dependencies = [ 1341 | "form_urlencoded", 1342 | "idna", 1343 | "percent-encoding", 1344 | ] 1345 | 1346 | [[package]] 1347 | name = "utf8parse" 1348 | version = "0.2.2" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1351 | 1352 | [[package]] 1353 | name = "util" 1354 | version = "0.1.0" 1355 | dependencies = [ 1356 | "camino", 1357 | "cfg-if", 1358 | "config", 1359 | "lazy_static", 1360 | "log", 1361 | "mockall", 1362 | "thiserror", 1363 | ] 1364 | 1365 | [[package]] 1366 | name = "want" 1367 | version = "0.3.1" 1368 | source = "registry+https://github.com/rust-lang/crates.io-index" 1369 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1370 | dependencies = [ 1371 | "try-lock", 1372 | ] 1373 | 1374 | [[package]] 1375 | name = "wasi" 1376 | version = "0.11.0+wasi-snapshot-preview1" 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" 1378 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1379 | 1380 | [[package]] 1381 | name = "wasm-bindgen" 1382 | version = "0.2.92" 1383 | source = "registry+https://github.com/rust-lang/crates.io-index" 1384 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 1385 | dependencies = [ 1386 | "cfg-if", 1387 | "wasm-bindgen-macro", 1388 | ] 1389 | 1390 | [[package]] 1391 | name = "wasm-bindgen-backend" 1392 | version = "0.2.92" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 1395 | dependencies = [ 1396 | "bumpalo", 1397 | "log", 1398 | "once_cell", 1399 | "proc-macro2", 1400 | "quote", 1401 | "syn", 1402 | "wasm-bindgen-shared", 1403 | ] 1404 | 1405 | [[package]] 1406 | name = "wasm-bindgen-futures" 1407 | version = "0.4.42" 1408 | source = "registry+https://github.com/rust-lang/crates.io-index" 1409 | checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" 1410 | dependencies = [ 1411 | "cfg-if", 1412 | "js-sys", 1413 | "wasm-bindgen", 1414 | "web-sys", 1415 | ] 1416 | 1417 | [[package]] 1418 | name = "wasm-bindgen-macro" 1419 | version = "0.2.92" 1420 | source = "registry+https://github.com/rust-lang/crates.io-index" 1421 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 1422 | dependencies = [ 1423 | "quote", 1424 | "wasm-bindgen-macro-support", 1425 | ] 1426 | 1427 | [[package]] 1428 | name = "wasm-bindgen-macro-support" 1429 | version = "0.2.92" 1430 | source = "registry+https://github.com/rust-lang/crates.io-index" 1431 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 1432 | dependencies = [ 1433 | "proc-macro2", 1434 | "quote", 1435 | "syn", 1436 | "wasm-bindgen-backend", 1437 | "wasm-bindgen-shared", 1438 | ] 1439 | 1440 | [[package]] 1441 | name = "wasm-bindgen-shared" 1442 | version = "0.2.92" 1443 | source = "registry+https://github.com/rust-lang/crates.io-index" 1444 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 1445 | 1446 | [[package]] 1447 | name = "web-sys" 1448 | version = "0.3.69" 1449 | source = "registry+https://github.com/rust-lang/crates.io-index" 1450 | checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" 1451 | dependencies = [ 1452 | "js-sys", 1453 | "wasm-bindgen", 1454 | ] 1455 | 1456 | [[package]] 1457 | name = "webpki-roots" 1458 | version = "0.26.3" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" 1461 | dependencies = [ 1462 | "rustls-pki-types", 1463 | ] 1464 | 1465 | [[package]] 1466 | name = "windows-core" 1467 | version = "0.52.0" 1468 | source = "registry+https://github.com/rust-lang/crates.io-index" 1469 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1470 | dependencies = [ 1471 | "windows-targets 0.52.6", 1472 | ] 1473 | 1474 | [[package]] 1475 | name = "windows-sys" 1476 | version = "0.48.0" 1477 | source = "registry+https://github.com/rust-lang/crates.io-index" 1478 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1479 | dependencies = [ 1480 | "windows-targets 0.48.5", 1481 | ] 1482 | 1483 | [[package]] 1484 | name = "windows-sys" 1485 | version = "0.52.0" 1486 | source = "registry+https://github.com/rust-lang/crates.io-index" 1487 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1488 | dependencies = [ 1489 | "windows-targets 0.52.6", 1490 | ] 1491 | 1492 | [[package]] 1493 | name = "windows-targets" 1494 | version = "0.48.5" 1495 | source = "registry+https://github.com/rust-lang/crates.io-index" 1496 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1497 | dependencies = [ 1498 | "windows_aarch64_gnullvm 0.48.5", 1499 | "windows_aarch64_msvc 0.48.5", 1500 | "windows_i686_gnu 0.48.5", 1501 | "windows_i686_msvc 0.48.5", 1502 | "windows_x86_64_gnu 0.48.5", 1503 | "windows_x86_64_gnullvm 0.48.5", 1504 | "windows_x86_64_msvc 0.48.5", 1505 | ] 1506 | 1507 | [[package]] 1508 | name = "windows-targets" 1509 | version = "0.52.6" 1510 | source = "registry+https://github.com/rust-lang/crates.io-index" 1511 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1512 | dependencies = [ 1513 | "windows_aarch64_gnullvm 0.52.6", 1514 | "windows_aarch64_msvc 0.52.6", 1515 | "windows_i686_gnu 0.52.6", 1516 | "windows_i686_gnullvm", 1517 | "windows_i686_msvc 0.52.6", 1518 | "windows_x86_64_gnu 0.52.6", 1519 | "windows_x86_64_gnullvm 0.52.6", 1520 | "windows_x86_64_msvc 0.52.6", 1521 | ] 1522 | 1523 | [[package]] 1524 | name = "windows_aarch64_gnullvm" 1525 | version = "0.48.5" 1526 | source = "registry+https://github.com/rust-lang/crates.io-index" 1527 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1528 | 1529 | [[package]] 1530 | name = "windows_aarch64_gnullvm" 1531 | version = "0.52.6" 1532 | source = "registry+https://github.com/rust-lang/crates.io-index" 1533 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1534 | 1535 | [[package]] 1536 | name = "windows_aarch64_msvc" 1537 | version = "0.48.5" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1540 | 1541 | [[package]] 1542 | name = "windows_aarch64_msvc" 1543 | version = "0.52.6" 1544 | source = "registry+https://github.com/rust-lang/crates.io-index" 1545 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1546 | 1547 | [[package]] 1548 | name = "windows_i686_gnu" 1549 | version = "0.48.5" 1550 | source = "registry+https://github.com/rust-lang/crates.io-index" 1551 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1552 | 1553 | [[package]] 1554 | name = "windows_i686_gnu" 1555 | version = "0.52.6" 1556 | source = "registry+https://github.com/rust-lang/crates.io-index" 1557 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1558 | 1559 | [[package]] 1560 | name = "windows_i686_gnullvm" 1561 | version = "0.52.6" 1562 | source = "registry+https://github.com/rust-lang/crates.io-index" 1563 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1564 | 1565 | [[package]] 1566 | name = "windows_i686_msvc" 1567 | version = "0.48.5" 1568 | source = "registry+https://github.com/rust-lang/crates.io-index" 1569 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1570 | 1571 | [[package]] 1572 | name = "windows_i686_msvc" 1573 | version = "0.52.6" 1574 | source = "registry+https://github.com/rust-lang/crates.io-index" 1575 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1576 | 1577 | [[package]] 1578 | name = "windows_x86_64_gnu" 1579 | version = "0.48.5" 1580 | source = "registry+https://github.com/rust-lang/crates.io-index" 1581 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1582 | 1583 | [[package]] 1584 | name = "windows_x86_64_gnu" 1585 | version = "0.52.6" 1586 | source = "registry+https://github.com/rust-lang/crates.io-index" 1587 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1588 | 1589 | [[package]] 1590 | name = "windows_x86_64_gnullvm" 1591 | version = "0.48.5" 1592 | source = "registry+https://github.com/rust-lang/crates.io-index" 1593 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1594 | 1595 | [[package]] 1596 | name = "windows_x86_64_gnullvm" 1597 | version = "0.52.6" 1598 | source = "registry+https://github.com/rust-lang/crates.io-index" 1599 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1600 | 1601 | [[package]] 1602 | name = "windows_x86_64_msvc" 1603 | version = "0.48.5" 1604 | source = "registry+https://github.com/rust-lang/crates.io-index" 1605 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1606 | 1607 | [[package]] 1608 | name = "windows_x86_64_msvc" 1609 | version = "0.52.6" 1610 | source = "registry+https://github.com/rust-lang/crates.io-index" 1611 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1612 | 1613 | [[package]] 1614 | name = "winnow" 1615 | version = "0.6.13" 1616 | source = "registry+https://github.com/rust-lang/crates.io-index" 1617 | checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" 1618 | dependencies = [ 1619 | "memchr", 1620 | ] 1621 | 1622 | [[package]] 1623 | name = "winreg" 1624 | version = "0.52.0" 1625 | source = "registry+https://github.com/rust-lang/crates.io-index" 1626 | checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" 1627 | dependencies = [ 1628 | "cfg-if", 1629 | "windows-sys 0.48.0", 1630 | ] 1631 | 1632 | [[package]] 1633 | name = "zeroize" 1634 | version = "1.8.1" 1635 | source = "registry+https://github.com/rust-lang/crates.io-index" 1636 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 1637 | --------------------------------------------------------------------------------