├── .cargo └── config.toml ├── .gitattributes ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.org ├── baustelle ├── Cargo.toml ├── examples │ └── fetch_centos.rs ├── src │ ├── archive.rs │ ├── archive │ │ ├── entry.rs │ │ └── resource.rs │ ├── containerfile.rs │ ├── fetcher.rs │ ├── lib.rs │ ├── runtime_config.rs │ ├── runtime_config │ │ ├── user.rs │ │ └── user │ │ │ └── unix_user.rs │ ├── storage.rs │ └── unpacker.rs └── test │ └── resources │ ├── config.json │ ├── containerfile │ ├── foo.tar.gz │ ├── foo_archive_entries │ ├── runtime_config.json │ ├── server_mocks │ ├── basic.yml │ ├── basic │ │ ├── auth.json │ │ ├── config.json │ │ ├── layer1 │ │ ├── manifest.json │ │ └── manifest_index.json │ ├── unix.yml │ ├── unix │ │ ├── config.json │ │ ├── layer.tar.gz │ │ ├── layer │ │ │ └── etc │ │ │ │ ├── group │ │ │ │ └── passwd │ │ ├── manifest.json │ │ └── manifest_index.json │ ├── whiteouts.yml │ └── whiteouts │ │ ├── config.json │ │ ├── layer1.tar.gz │ │ ├── layer2.tar.gz │ │ ├── layer3.tar.gz │ │ ├── manifest.json │ │ └── manifest_index.json │ ├── unix │ ├── happy_path │ │ └── etc │ │ │ ├── group │ │ │ └── passwd │ └── malformed │ │ └── etc │ │ ├── malformed_error_msg │ │ └── passwd │ └── unpacked_layers ├── common_lib ├── Cargo.toml └── src │ └── lib.rs ├── containerd-shim ├── Cargo.toml ├── build.rs ├── proto │ ├── github.com │ │ └── containerd │ │ │ └── containerd │ │ │ └── api │ │ │ └── types │ │ │ ├── mount.proto │ │ │ └── task │ │ │ └── task.proto │ ├── gogoproto │ │ └── gogo.proto │ ├── google │ │ └── protobuf │ │ │ ├── any.proto │ │ │ ├── descriptor.proto │ │ │ ├── empty.proto │ │ │ └── timestamp.proto │ └── shim.proto └── src │ ├── filesystem.rs │ ├── main.rs │ ├── oci_extensions.rs │ ├── protocols.rs │ └── task_service.rs ├── docs └── quicklinks.org ├── libknast ├── Cargo.toml ├── src │ ├── filesystem.rs │ ├── filesystem │ │ ├── devfs.rs │ │ └── mount.rs │ ├── lib.rs │ ├── operations.rs │ └── operations │ │ ├── command_ext.rs │ │ ├── network.rs │ │ └── utils.rs └── test │ └── resources │ ├── commands_output │ ├── env │ ├── id │ ├── mount │ └── trapster_sigbus │ └── container │ ├── config.json │ └── rootfs │ ├── COPYING │ ├── bin │ ├── env │ ├── id │ ├── ls │ ├── mount │ ├── quitely_stop.sh │ ├── sh │ ├── sleep │ └── trapster.sh │ ├── etc │ ├── group │ ├── master.passwd │ ├── nsswitch.conf │ ├── passwd │ ├── pwd.db │ └── spwd.db │ ├── lib │ ├── libc.so.7 │ ├── libedit.so.8 │ ├── libgcc_s.so.1 │ ├── libm.so.5 │ ├── libncursesw.so.9 │ ├── libthr.so.3 │ └── libutil.so.9 │ └── libexec │ ├── COPYING │ └── ld-elf.so.1 ├── netzwerk ├── Cargo.toml ├── build.rs ├── examples │ └── bridge_jail.rs ├── ffi │ └── ffi.h ├── src │ ├── common_bindings.rs │ ├── interface.rs │ ├── interface │ │ └── operations.rs │ ├── lib.rs │ ├── nat.rs │ ├── pf.rs │ ├── pf │ │ └── .gitkeep │ ├── range.rs │ ├── route.rs │ └── route │ │ └── bindings.rs └── test │ └── resources │ └── ifconfig ├── registratur ├── Cargo.toml ├── src │ ├── lib.rs │ ├── reqwest_ext.rs │ ├── v2.rs │ └── v2 │ │ ├── client.rs │ │ ├── client │ │ └── www_authenticate.rs │ │ ├── domain.rs │ │ └── domain │ │ ├── config.rs │ │ ├── descriptor.rs │ │ ├── layer.rs │ │ ├── manifest.rs │ │ └── manifest_index.rs └── test │ └── resources │ ├── config.json │ ├── manifest.json │ ├── manifest_index.json │ ├── server_mocks │ ├── basic.yml │ └── basic │ │ ├── auth.json │ │ ├── config.json │ │ ├── layer1 │ │ ├── manifest.json │ │ └── manifest_index.json │ └── www_authenticate ├── runc ├── Cargo.toml └── src │ └── bin │ ├── fetch_image.rs │ ├── runc.rs │ └── runc.yaml ├── rustfmt.toml ├── storage ├── Cargo.toml └── src │ ├── lib.rs │ ├── sled_engine.rs │ ├── sqlite_engine.rs │ └── sqlite_engine │ ├── cas.sql │ ├── exists.sql │ ├── get.sql │ ├── migration.sql │ ├── put.sql │ ├── remove.sql │ └── try_insert.sql ├── test └── resources │ ├── commands_output │ ├── env │ ├── id │ ├── mount │ └── trapster_sigbus │ └── container │ ├── config.json │ └── rootfs │ ├── COPYING │ ├── bin │ ├── env │ ├── id │ ├── ls │ ├── mount │ ├── quitely_stop.sh │ ├── sh │ ├── sleep │ └── trapster.sh │ ├── etc │ ├── group │ ├── master.passwd │ ├── nsswitch.conf │ ├── passwd │ ├── pwd.db │ └── spwd.db │ ├── lib │ ├── libc.so.7 │ ├── libedit.so.8 │ ├── libgcc_s.so.1 │ ├── libm.so.5 │ ├── libncursesw.so.9 │ ├── libthr.so.3 │ └── libutil.so.9 │ └── libexec │ ├── COPYING │ └── ld-elf.so.1 └── test_helpers ├── Cargo.toml ├── procedural_macros ├── Cargo.toml └── src │ └── lib.rs └── src └── lib.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [net] 2 | # asks cargo to use git from path (assuming it has credentials setup) 3 | git-fetch-with-cli = true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | coreutils filter=lfs diff=lfs merge=lfs -text 2 | *.gz filter=lfs diff=lfs merge=lfs -text 3 | *.so* filter=lfs diff=lfs merge=lfs -text 4 | **/rootfs/**/* filter=lfs diff=lfs merge=lfs -text 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */target 2 | target 3 | *~ 4 | netzwerk/src/pf/bindings.rs 5 | containerd-shim/src/protocols -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "baustelle", 4 | "common_lib", 5 | "libknast", 6 | "netzwerk", 7 | "registratur", 8 | "storage", 9 | "test_helpers", 10 | "runc", 11 | "containerd-shim", 12 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2021 Artem Khramov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * KNAST 2 | 3 | Knast is FreeBSD experimental toolkit for building a modern 4 | container infrastructure. 5 | This is a research project project not intended to be used in 6 | production. See [[#Goals][Goals]]. 7 | 8 | 9 | ** Status 10 | Currently, knast provides an OCI-compatible container runtime, which 11 | diverges from the [[https://github.com/opencontainers/runc][etalon realization]] in some places. Namely, 12 | 13 | - ~runc start~ doesn't detach process. 14 | - ~runc kill~ cannot be applied to ~created~ process. 15 | - Non-standard extensions, such as ~runc run~ are not implemented. 16 | 17 | ** Kernel requirements 18 | Knast runs on amd64 FreeBSD 13.0-STABLE Kernel and newer. Support for 19 | aarch64 is planned. 20 | 21 | Knast runs on GENERIC kernel, but requires several kernel modules to 22 | be loaded (or compiled in kernel). These are 23 | 24 | - ~if_bridge~ is required for setting up VNET networking for jail. 25 | - ~if_epair~ is required for setting up VNET networking for jail 26 | - ~linux64~ (optional) is required for linux jails 27 | - ~pf~ firewall is required for networking, use ~pf~ service to load 28 | it. 29 | 30 | Following sysctl variables need to be set: 31 | 32 | - ~net.inet.ip.forwarding -> 1~ for jail networking. 33 | 34 | ** Usage 35 | 36 | *** Building 37 | 38 | #+BEGIN_SRC 39 | cargo build release 40 | #+END_SRC 41 | 42 | 43 | *** Creating a bundle 44 | To run a container OCI runtimes need a [[https://github.com/opencontainers/runtime-spec/blob/1c3f411f041711bbeecf35ff7e93461ea6789220/bundle.md][runtime bundle]]. Runtime bundles 45 | are built from OCI images. Knast is a mere runtime and not responsible 46 | for creating these files for you. You can use third party tools to 47 | create a bundle, or create it manually. 48 | 49 | For convenience, we provide a util to fetch and unpack OCI images from 50 | docker registry. For instance, following command will create a runtime 51 | bundle from oldoldstable debian: 52 | 53 | #+BEGIN_SRC sh 54 | fetch_image debian:oldoldstable-20201209-slim 55 | #+END_SRC 56 | 57 | ~fetch_image~ will create a bundle somewhere in a $HOME directory 58 | the exact location will be printed. 59 | 60 | In this example we fetched the oldoldstable debian, whose binaries 61 | still rely on older kernel ABI which is likely will be covered by 62 | Linuxulator. 63 | 64 | *** OCI lifecycle 65 | 66 | Once the bundle is created, you can create a container. 67 | 68 | Navigate to ~runc~ folder and build the project using ~cargo build~. 69 | 70 | Following will create a VNET jail, configure network for it, mount all 71 | necessary devices and so on. 72 | 73 | #+BEGIN_SRC sh 74 | runc create debian /home/akhramov/containers/d19a2ab9-af67-4d04-8aef-9c364686c4fb 75 | #+END_SRC 76 | 77 | Then you will be able to start the container 78 | 79 | #+BEGIN_SRC sh 80 | runc start debian 81 | #+END_SRC 82 | 83 | Finally, you can delete the stopped container 84 | 85 | #+BEGIN_SRC sh 86 | runc delete debian 87 | #+END_SRC 88 | 89 | Apart from that, you can query containers state using ~state~ command 90 | and send signals to the container using ~kill~. Please note that 91 | *kill diverges* from the etalone runc realization in sense that it 92 | only support signal numbers, not names. 93 | 94 | ** Project structure 95 | This project consists of several libraries, namely 96 | 97 | - netzwerk contains network-related routines. Setting up interfaces, 98 | NATs, etc 99 | - registratur is a client library for docker registry. It's just a 100 | convenience library containing types & HTTP client and does not 101 | directly serve project goals. This functionality is to be handled by 102 | other tools. 103 | - baustelle builds containers from images. It even supports a limited 104 | subsets of dockerfiles, though, just like with registratur this 105 | functionality is to be handled by other tools. 106 | - storage provides storage-agnostic embedded db. Is used by runc to 107 | store containers state and other metadata. 108 | - runc provides an OCI compatible runc binary. 109 | 110 | ** Goals 111 | 112 | Knast is a research project conducted (in order of priority) to 113 | 114 | - examine capabilities of Rust in niche of OS-level virtualization, in 115 | vein of Oracle's [[https://github.com/oracle/railcar][railcar]]. Long term goal is to implement modules for 116 | the popular orchestration / containerization solutions, like a [[https://www.redhat.com/sysadmin/cni-kubernetes][CNI 117 | plugin]]. 118 | 119 | - Examine feasibility to implement a container runtime without 120 | userspace dependencies: a self-sufficient binary which doesn't rely 121 | on presence of any binaries in system. 122 | 123 | That implies that we will need to rewrite bindings to ~mount~, 124 | ~devfs~, ~route~, partially implement functionality provided by 125 | ~ifconfig~. Again, such a good test for Rust :) 126 | 127 | - Build a runtime for containerd, a CRI, etc. Which has lower 128 | priority. 129 | 130 | ** State 131 | Not ready, please don't use it anywhere. 132 | 133 | ** Other efforts. 134 | 135 | - https://github.com/samuelkarp/runj this project will likely achieve 136 | production-readiness sooner and reuses stable, reliable tooling 137 | which is already there. Please check it out. 138 | 139 | ** Contributing 140 | 141 | If you would like to contribute -- please do. Check the list of open 142 | issues and tackle any task you want in regards to project priorities. 143 | 144 | Testers, bug reporters, ocasional users -- I love you all. 145 | -------------------------------------------------------------------------------- /baustelle/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "baustelle" 3 | version = "0.1.0" 4 | authors = ["akhramov"] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | anyhow = "1.0" 9 | csv = "1.1" 10 | dockerfile-parser = "0.7.1" 11 | fehler = "1.0" 12 | futures = { git = "https://github.com/akhramov/futures-rs", branch = "fix/add-derive-clone-to-with-combinator" } 13 | registratur = { path = "../registratur" } 14 | itertools = "0.9.0" 15 | libc = "0.2.69" 16 | log = "0.4" 17 | nom = "5" 18 | once_cell = "1.5.2" 19 | serde = "1.0" 20 | serde_json = "1.0" 21 | storage = { path = "../storage" } 22 | uuid = { version = "0.8.1", features = ["v4"] } 23 | 24 | [dev-dependencies] 25 | env_logger = "0.8" 26 | tempfile = "3.1.0" 27 | tokio = { version = "1.1.1", features = ["macros", "rt", "rt-multi-thread"] } 28 | test_helpers = { "path" = "../test_helpers" } 29 | 30 | [features] 31 | integration_testing = [] 32 | 33 | [lib] 34 | # Documenting private items is currently not possible. 35 | doctest = false 36 | -------------------------------------------------------------------------------- /baustelle/examples/fetch_centos.rs: -------------------------------------------------------------------------------- 1 | // Fetch & unpack a centos image. 2 | extern crate baustelle; 3 | extern crate futures; 4 | #[macro_use] 5 | extern crate log; 6 | extern crate registratur; 7 | extern crate tokio; 8 | 9 | use baustelle::{Builder, EvaluationUpdate, LayerDownloadStatus}; 10 | use storage::TestStorage; 11 | 12 | const CONTAINERFILE: &[u8] = r#" 13 | FROM centos:latest 14 | 15 | ENV FOO=/bar 16 | WORKDIR ${FOO} 17 | 18 | CMD /bin/sleep 42 19 | "# 20 | .as_bytes(); 21 | 22 | #[tokio::main] 23 | async fn main() { 24 | env_logger::init(); 25 | 26 | info!("Fetching a centos image"); 27 | let current_dir = std::env::current_dir().unwrap(); 28 | let storage = TestStorage::new(current_dir).unwrap(); 29 | let builder = Builder::new("amd64".into(), vec!["linux".into()], storage) 30 | .expect("Failed to build the image builder"); 31 | 32 | builder 33 | .build("https://registry-1.docker.io", CONTAINERFILE, |x| { 34 | if let EvaluationUpdate::From(LayerDownloadStatus::InProgress( 35 | name, 36 | count, 37 | total, 38 | )) = x 39 | { 40 | info!("{} downloaded {} of {}", name, count, total); 41 | } 42 | }) 43 | .await 44 | .expect("Failed to build the image"); 45 | 46 | info!("Fetched an image"); 47 | } 48 | -------------------------------------------------------------------------------- /baustelle/src/archive.rs: -------------------------------------------------------------------------------- 1 | pub mod entry; 2 | pub mod resource; 3 | 4 | use std::ffi::OsString; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use anyhow::{Error, Result}; 8 | 9 | use resource::ArchiveResource; 10 | 11 | pub struct Archive<'a> { 12 | content: &'a [u8], 13 | } 14 | 15 | impl<'a> Archive<'a> { 16 | pub fn new(content: &'a [u8]) -> Self { 17 | Self { content } 18 | } 19 | 20 | #[fehler::throws] 21 | pub fn entries(&self) -> impl Iterator> { 22 | self.resource()?.map_entries(|entry, _| { 23 | let os_string: OsString = entry.pathname().into(); 24 | 25 | os_string.into() 26 | })? 27 | } 28 | 29 | #[fehler::throws] 30 | pub fn extract( 31 | &self, 32 | path: impl AsRef, 33 | ignore: impl Fn(String) -> bool, 34 | ) { 35 | self.resource()?.extract(path, ignore)?; 36 | } 37 | 38 | #[fehler::throws] 39 | fn resource(&self) -> ArchiveResource { 40 | ArchiveResource::new(&self.content)? 41 | } 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use super::*; 47 | 48 | #[test] 49 | fn test_list_content() { 50 | let content = test_helpers::bytes_fixture!("foo.tar.gz"); 51 | 52 | let archive = Archive::new(content); 53 | let expected = test_helpers::code_fixture!("foo_archive_entries"); 54 | 55 | let actual = archive 56 | .entries() 57 | .unwrap() 58 | .collect::>>() 59 | .expect("One or more entry failed to report its pathname"); 60 | 61 | assert_eq!(expected, actual); 62 | } 63 | 64 | #[test] 65 | fn test_extract() { 66 | let content = test_helpers::bytes_fixture!("foo.tar.gz"); 67 | 68 | let archive = Archive::new(content); 69 | let dir = 70 | tempfile::tempdir().expect("failed to create a tmp directory"); 71 | 72 | archive 73 | .extract(dir.path(), |_| false) 74 | .expect("failed to extract archive"); 75 | 76 | let link = std::fs::read_link(dir.path().join("foo/bis")) 77 | .expect("symlink does not exist"); 78 | 79 | assert_eq!("bad/bad", link.to_string_lossy()); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /baustelle/src/archive/entry.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{CStr, CString}; 2 | use std::path::Path; 3 | 4 | use anyhow::{anyhow, Error}; 5 | use libc::{c_char, c_void}; 6 | 7 | #[link(name = "archive")] 8 | extern "C" { 9 | fn archive_entry_pathname(entry: *const c_void) -> *const c_char; 10 | fn archive_entry_set_pathname( 11 | entry: *const c_void, 12 | pathname: *const c_char, 13 | ); 14 | } 15 | 16 | pub struct ArchiveEntry; 17 | 18 | impl ArchiveEntry { 19 | pub fn pathname(&self) -> String { 20 | unsafe { 21 | let string = archive_entry_pathname(self as *const _ as _); 22 | 23 | CStr::from_ptr(string).to_string_lossy().into_owned() 24 | } 25 | } 26 | 27 | #[fehler::throws] 28 | pub fn set_pathname(&self, path: impl AsRef) { 29 | let pathname = path 30 | .as_ref() 31 | .join(self.pathname()) 32 | .into_os_string() 33 | .into_string() 34 | .map_err(|err| anyhow!("Couldn't convert {:?} to string", err))?; 35 | 36 | unsafe { 37 | let pathname_raw = CString::from_vec_unchecked(pathname.into()); 38 | 39 | archive_entry_set_pathname( 40 | self as *const _ as _, 41 | pathname_raw.into_raw(), 42 | ); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /baustelle/src/containerfile.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::TryFrom, fs, io::Read, path::PathBuf}; 2 | 3 | use anyhow::{Context, Error}; 4 | use dockerfile_parser::{ 5 | Dockerfile as Containerfile, FromInstruction, 6 | Instruction::{self, *}, 7 | }; 8 | 9 | use futures::{ 10 | channel::mpsc::{unbounded, SendError, UnboundedSender}, 11 | future::{self, Future}, 12 | stream::Stream, 13 | SinkExt, TryFutureExt, 14 | }; 15 | 16 | use uuid::Uuid; 17 | 18 | use registratur::v2::{ 19 | client::Client, 20 | domain::{config::Config, manifest::Manifest}, 21 | }; 22 | 23 | use crate::{ 24 | fetcher::{Fetcher, LayerDownloadStatus}, 25 | runtime_config::RuntimeConfig, 26 | storage::{Storage, StorageEngine, BLOBS_STORAGE_KEY}, 27 | unpacker::Unpacker, 28 | }; 29 | 30 | #[derive(Clone, Debug)] 31 | pub enum EvaluationUpdate { 32 | From(LayerDownloadStatus), 33 | } 34 | 35 | pub struct Builder<'a, T: StorageEngine> { 36 | fetcher: Fetcher<'a, T>, 37 | storage: &'a Storage, 38 | container_folder: PathBuf, 39 | } 40 | 41 | impl<'a, T: StorageEngine> Builder<'a, T> { 42 | #[fehler::throws] 43 | pub fn new( 44 | registry_url: &'a str, 45 | architecture: String, 46 | os: Vec, 47 | storage: &'a Storage, 48 | ) -> Self { 49 | let client = Client::build(registry_url)?; 50 | let fetcher = Fetcher::new(storage, client, architecture, os); 51 | let container_uuid = format!("{}", Uuid::new_v4()); 52 | let container_folder = 53 | storage.folder().join("containers").join(&container_uuid); 54 | fs::create_dir_all(&container_folder)?; 55 | 56 | Self { 57 | fetcher, 58 | container_folder, 59 | storage, 60 | } 61 | } 62 | 63 | #[fehler::throws] 64 | pub fn interpret( 65 | &self, 66 | file: impl Read, 67 | ) -> ( 68 | impl Stream, 69 | impl Future> + '_, 70 | ) { 71 | let (sender, receiver) = unbounded(); 72 | 73 | let containerfile = Containerfile::from_reader(file)?; 74 | 75 | let result = containerfile.iter_stages().flat_map(|stage| { 76 | stage.instructions.into_iter().map(|instruction| { 77 | self.execute_instruction(instruction.clone(), sender.clone()) 78 | }) 79 | }); 80 | 81 | let folder = self.container_folder.clone(); 82 | 83 | let completion_future = 84 | future::try_join_all(result).and_then(|_| future::ok(folder)); 85 | 86 | (receiver, completion_future) 87 | } 88 | 89 | #[fehler::throws] 90 | async fn execute_instruction( 91 | &self, 92 | instruction: Instruction, 93 | sender: UnboundedSender, 94 | ) { 95 | match instruction { 96 | From(instruction) => { 97 | self.execute_from_instruction(instruction, sender).await?; 98 | } 99 | _ => { 100 | log::warn!( 101 | "Unhandled containerfile instruction {:?}", 102 | instruction 103 | ) 104 | } 105 | } 106 | } 107 | 108 | #[fehler::throws] 109 | async fn execute_from_instruction( 110 | &self, 111 | instruction: FromInstruction, 112 | sender: UnboundedSender, 113 | ) { 114 | let image = &instruction.image_parsed; 115 | 116 | let sender = sender.with(|val| { 117 | future::ok::<_, SendError>(EvaluationUpdate::From(val)) 118 | }); 119 | 120 | let default_tag = String::from("latest"); 121 | let tag = image.tag.as_ref().unwrap_or(&default_tag); 122 | 123 | let digest = self.fetcher.fetch(&image.image, &tag, sender).await?; 124 | 125 | let manifest: Manifest = 126 | self.storage.get(BLOBS_STORAGE_KEY, &digest)?.context( 127 | "Fetched manifest was not found. Possible storage corruption", 128 | )?; 129 | 130 | let config: Config = self 131 | .storage 132 | .get(BLOBS_STORAGE_KEY, manifest.config.digest)? 133 | .context( 134 | "Fetched config was not found. Possible storage corruption", 135 | )?; 136 | 137 | let destination = self.container_folder.join("rootfs"); 138 | 139 | let unpacker = Unpacker::new(&self.storage, &destination); 140 | 141 | unpacker.unpack(digest)?; 142 | 143 | let runtime_config = 144 | RuntimeConfig::try_from((config, destination.as_path()))?; 145 | 146 | serde_json::to_writer( 147 | fs::File::create(&self.container_folder.join("config.json"))?, 148 | &runtime_config, 149 | )?; 150 | } 151 | } 152 | 153 | #[cfg(test)] 154 | mod tests { 155 | use futures::StreamExt; 156 | 157 | use super::*; 158 | use crate::storage::TestStorage as Storage; 159 | 160 | #[tokio::test] 161 | async fn test_interpretation() { 162 | #[cfg(feature = "integration_testing")] 163 | let (url, _mocks) = ("https://registry-1.docker.io", ()); 164 | #[cfg(not(feature = "integration_testing"))] 165 | let (url, _mocks) = test_helpers::mock_server!("unix.yml"); 166 | 167 | let tempdir = tempfile::tempdir().expect("Failed to create a tempdir"); 168 | 169 | let storage = 170 | Storage::new(tempdir.path()).expect("Unable to initialize cache"); 171 | 172 | let builder = 173 | Builder::new(&url, "amd64".into(), vec!["linux".into()], &storage) 174 | .expect("failed to initialize the builder"); 175 | 176 | let containerfile = test_helpers::fixture!("containerfile"); 177 | 178 | let (updates, complete_future) = 179 | builder.interpret(containerfile.as_bytes()).unwrap(); 180 | 181 | let (_, result) = 182 | future::join(updates.collect::>(), complete_future).await; 183 | 184 | let container_folder = 185 | result.expect("Unable to enterpret containerfile"); 186 | 187 | assert!(container_folder.join("rootfs/etc/passwd").exists()); 188 | 189 | let file = fs::File::open(container_folder.join("config.json")) 190 | .expect("Failed to open OCI runtime config file"); 191 | 192 | let config: RuntimeConfig = serde_json::from_reader(file) 193 | .expect("Failed to parse OCI runtime config file"); 194 | 195 | let command = config.process.unwrap().args.unwrap().join(" "); 196 | 197 | assert_eq!(command, "nginx -g daemon off;"); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /baustelle/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod fetcher; 2 | pub mod runtime_config; 3 | mod storage; 4 | mod unpacker; 5 | 6 | mod containerfile; 7 | 8 | mod archive; 9 | 10 | use std::{io::Read, path::PathBuf}; 11 | 12 | use anyhow::Error; 13 | use futures::{future, StreamExt}; 14 | 15 | use crate::storage::{Storage, StorageEngine}; 16 | use containerfile::Builder as ContainerfileBuilder; 17 | pub use containerfile::EvaluationUpdate; 18 | pub use fetcher::LayerDownloadStatus; 19 | 20 | pub struct Builder { 21 | architecture: String, 22 | os: Vec, 23 | storage: Storage, 24 | } 25 | 26 | impl Builder { 27 | #[fehler::throws] 28 | pub fn new( 29 | architecture: String, 30 | os: Vec, 31 | storage: Storage, 32 | ) -> Self { 33 | Self { 34 | architecture, 35 | os, 36 | storage, 37 | } 38 | } 39 | 40 | #[fehler::throws] 41 | pub async fn build( 42 | &self, 43 | registry: &str, 44 | containerfile: impl Read, 45 | callback: impl Fn(EvaluationUpdate), 46 | ) -> PathBuf { 47 | let Self { 48 | architecture, 49 | os, 50 | storage, 51 | } = self; 52 | 53 | let builder = ContainerfileBuilder::new( 54 | registry, 55 | architecture.into(), 56 | os.to_vec(), 57 | &storage, 58 | )?; 59 | 60 | let (updates, future) = builder.interpret(containerfile)?; 61 | 62 | let updates = updates.for_each(|item| { 63 | callback(item); 64 | 65 | future::ready(()) 66 | }); 67 | 68 | let (result, _) = future::join(future, updates).await; 69 | 70 | result? 71 | } 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use tempfile::TempDir; 77 | 78 | use super::*; 79 | use crate::storage::{StorageEngine, TestStorage as Storage}; 80 | 81 | #[test] 82 | fn test_image_build_initializer() { 83 | let builder = construct_builder(); 84 | 85 | assert!(builder.is_ok(), "Failed to create builder") 86 | } 87 | 88 | #[tokio::test] 89 | async fn test_image_building_api() { 90 | #[cfg(feature = "integration_testing")] 91 | let (url, _mocks) = ("https://registry-1.docker.io", ()); 92 | #[cfg(not(feature = "integration_testing"))] 93 | let (url, _mocks) = test_helpers::mock_server!("unix.yml"); 94 | 95 | let (builder, _path) = 96 | construct_builder().expect("failed to create builder"); 97 | 98 | let containerfile = test_helpers::fixture!("containerfile"); 99 | let container_folder = builder 100 | .build(&url, containerfile.as_bytes(), |_| {}) 101 | .await 102 | .unwrap(); 103 | 104 | assert!(container_folder.join("rootfs/etc/passwd").exists()); 105 | } 106 | 107 | #[fehler::throws] 108 | fn construct_builder() -> (Builder, TempDir) { 109 | let tmpdir = tempfile::tempdir().unwrap(); 110 | let storage = Storage::new(tmpdir.path()).unwrap(); 111 | 112 | ( 113 | Builder::new("amd64".into(), vec!["linux".into()], storage)?, 114 | tmpdir, 115 | ) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /baustelle/src/runtime_config/user.rs: -------------------------------------------------------------------------------- 1 | mod unix_user; 2 | 3 | use std::convert::AsRef; 4 | use std::path::Path; 5 | use std::str::FromStr; 6 | 7 | use anyhow::{Context, Error, Result}; 8 | 9 | use nom::{ 10 | branch::alt, bytes::complete::tag, character::complete::alphanumeric1, 11 | combinator::map_res, sequence::separated_pair, IResult, 12 | }; 13 | 14 | use serde::de::DeserializeOwned; 15 | 16 | use unix_user::{EtcConf, EtcGroupEntry, EtcPasswdEntry}; 17 | 18 | fn identifier(input: &str) -> IResult<&str, T> 19 | where 20 | T: FromStr, 21 | { 22 | map_res(alphanumeric1, FromStr::from_str)(input) 23 | } 24 | 25 | fn pair(input: &str) -> IResult<&str, (S, T)> 26 | where 27 | S: FromStr, 28 | T: FromStr, 29 | { 30 | separated_pair(identifier, tag(":"), identifier)(input) 31 | } 32 | 33 | fn find_entry(rootfs: impl AsRef, predicate: F) -> Result 34 | where 35 | T: DeserializeOwned + 'static, 36 | F: Fn(&T) -> bool, 37 | { 38 | let items = EtcConf::::new(&rootfs)?; 39 | 40 | for maybe_item in items { 41 | if let Ok(item) = maybe_item { 42 | if predicate(&item) { 43 | return Ok(item); 44 | } 45 | } 46 | } 47 | 48 | anyhow::bail!("Entry was not found") 49 | } 50 | 51 | fn find_group_by_name( 52 | rootfs: &Path, 53 | ) -> impl Fn(String) -> Result { 54 | let path = Path::new(rootfs).join("etc/group"); 55 | 56 | move |groupname| { 57 | find_entry(&path, |group: &EtcGroupEntry| group.groupname == groupname) 58 | .context(format!( 59 | "Group {} was not found in {:?}", 60 | groupname, path 61 | )) 62 | } 63 | } 64 | 65 | fn find_user_by_name( 66 | rootfs: &Path, 67 | ) -> impl Fn(String) -> Result { 68 | let path = Path::new(rootfs).join("etc/passwd"); 69 | 70 | move |username| { 71 | find_entry(&path, |user: &EtcPasswdEntry| user.username == username) 72 | .context(format!("User {} was not found in {:?}", username, path)) 73 | } 74 | } 75 | 76 | fn find_user_by_uid(rootfs: &Path) -> impl Fn(u32) -> Result { 77 | let path = Path::new(rootfs).join("etc/passwd"); 78 | 79 | move |uid| { 80 | find_entry(&path, |user: &EtcPasswdEntry| user.uid == uid).context( 81 | format!("User with uid {} was not found in {:?}", uid, path), 82 | ) 83 | } 84 | } 85 | 86 | /// Parses user string to retrieve uid / gid pair 87 | /// 88 | /// If user string doesn't contain all required information, 89 | /// then the info is looked up in the container's root 90 | /// filesystem. Namely, in `/etc/passwd` and `/etc/group` 91 | /// files. 92 | /// 93 | /// Adhering to Linux specification, these types of user 94 | /// strings are valid: `user`, `uid`, `user:group`, 95 | /// `uid:gid`, `uid:group`, `user:gid`. In practice, docker 96 | /// registry may serve the config with the empty (`""`) user 97 | /// string. This case is to be handled outside the scope of 98 | /// this function. 99 | /// 100 | /// ``` 101 | #[fehler::throws] 102 | pub fn parse(user: String, rootfs: &Path) -> (u32, u32) { 103 | let uid_gid = pair::; 104 | 105 | let uid_group = map_res(pair, |(uid, group)| -> Result<(u32, u32)> { 106 | Ok((uid, find_group_by_name(rootfs)(group)?.gid)) 107 | }); 108 | 109 | let username = 110 | map_res(identifier, |username: String| -> Result<(u32, u32)> { 111 | let user = find_user_by_name(rootfs)(username)?; 112 | 113 | Ok((user.uid, user.gid)) 114 | }); 115 | 116 | let uid = map_res(identifier, |uid: u32| -> Result<(u32, u32)> { 117 | let user = find_user_by_uid(rootfs)(uid)?; 118 | 119 | Ok((user.uid, user.gid)) 120 | }); 121 | 122 | let user_group = 123 | map_res(pair, |(username, group)| -> Result<(u32, u32)> { 124 | let user = find_user_by_name(rootfs)(username)?; 125 | let group = find_group_by_name(rootfs)(group)?; 126 | 127 | Ok((user.uid, group.gid)) 128 | }); 129 | 130 | let user_gid = map_res(pair, |(username, gid)| -> Result<(u32, u32)> { 131 | let user = find_user_by_name(rootfs)(username)?; 132 | 133 | Ok((user.uid, gid)) 134 | }); 135 | 136 | match alt((uid_gid, uid_group, user_group, user_gid, username, uid))(&user) 137 | { 138 | Ok((_, res)) => res, 139 | Err(err) => { 140 | anyhow::bail!("Failed to parse user config string: {}", err) 141 | } 142 | } 143 | } 144 | 145 | #[cfg(test)] 146 | mod test { 147 | use super::*; 148 | 149 | macro_rules! do_parse { 150 | ($str:expr) => {{ 151 | let user = $str.into(); 152 | let path = test_helpers::fixture_path!("unix/happy_path"); 153 | 154 | parse(user, path).unwrap() 155 | }}; 156 | } 157 | 158 | #[test] 159 | fn test_uid_gid_parsing() { 160 | assert_eq!(do_parse!("1001:1002"), (1001, 1002)); 161 | } 162 | 163 | #[test] 164 | fn test_resolve_gid_from_name() { 165 | assert_eq!(do_parse!("1337:tests"), (1337, 977)); 166 | } 167 | 168 | #[test] 169 | fn test_only_username_supplied() { 170 | assert_eq!(do_parse!("akhramov"), (1001, 1001)); 171 | } 172 | 173 | #[test] 174 | fn test_only_uid_supplied() { 175 | assert_eq!(do_parse!("977"), (977, 977)); 176 | } 177 | 178 | #[test] 179 | fn test_username_groupname_supplied() { 180 | assert_eq!(do_parse!("tests:games"), (977, 13)); 181 | } 182 | 183 | #[test] 184 | fn test_username_gid_supplied() { 185 | assert_eq!(do_parse!("tests:13"), (977, 13)); 186 | } 187 | 188 | #[test] 189 | fn test_invalid_username() { 190 | let user = "testsa:13".into(); 191 | let path = test_helpers::fixture_path!("unix/happy_path"); 192 | 193 | let error = r#"Parsing Error: ("testsa:13", MapRes)"#; 194 | 195 | assert!( 196 | format!("{:?}", parse(user, path).unwrap_err()).contains(error) 197 | ); 198 | } 199 | 200 | #[test] 201 | fn test_malformed_file() { 202 | let user = "tests:13".into(); 203 | let path = test_helpers::fixture_path!("unix/malformed"); 204 | 205 | assert!(parse(user, path).is_err()); 206 | } 207 | 208 | #[test] 209 | fn test_nonexistent_file() { 210 | let user = "tests:13".into(); 211 | let path = test_helpers::fixture_path!("unix/this_is_sad"); 212 | 213 | assert!(parse(user, path).is_err()); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /baustelle/src/runtime_config/user/unix_user.rs: -------------------------------------------------------------------------------- 1 | use std::convert::AsRef; 2 | use std::fs::File; 3 | use std::path::Path; 4 | 5 | use anyhow::Error; 6 | use csv::{Error as CsvError, ReaderBuilder}; 7 | use serde::{de::DeserializeOwned, Deserialize, Deserializer}; 8 | 9 | /// Abstraction over /etc/passwd & /etc/group 10 | pub struct EtcConf( 11 | Box>>, 12 | ); 13 | 14 | #[derive(Deserialize, Debug)] 15 | pub struct EtcPasswdEntry { 16 | pub username: String, 17 | _password: String, 18 | pub uid: u32, 19 | pub gid: u32, 20 | } 21 | 22 | #[derive(Deserialize, Debug)] 23 | pub struct EtcGroupEntry { 24 | pub groupname: String, 25 | _password: String, 26 | pub gid: u32, 27 | #[serde(deserialize_with = "comma_delimited_string")] 28 | pub users: Vec, 29 | } 30 | 31 | fn comma_delimited_string<'de, D>( 32 | deserializer: D, 33 | ) -> Result, D::Error> 34 | where 35 | D: Deserializer<'de>, 36 | { 37 | String::deserialize(deserializer) 38 | .map(|string| string.split(',').map(String::from).collect()) 39 | } 40 | 41 | impl EtcConf { 42 | #[fehler::throws] 43 | pub fn new(file: impl AsRef) -> Self { 44 | let file = File::open(file)?; 45 | let csv_reader = ReaderBuilder::new() 46 | .has_headers(false) 47 | .delimiter(b':') 48 | .comment(Some(b'#')) 49 | .from_reader(file); 50 | 51 | let result = csv_reader.into_deserialize(); 52 | 53 | Self(Box::new(result)) 54 | } 55 | } 56 | 57 | impl Iterator for EtcConf { 58 | type Item = Result; 59 | 60 | fn next(&mut self) -> Option { 61 | self.0.next() 62 | } 63 | } 64 | 65 | #[cfg(test)] 66 | mod test { 67 | use super::*; 68 | 69 | #[test] 70 | fn test_etc_passwd_enumeration() { 71 | let path = test_helpers::fixture_path!("unix/happy_path/etc/passwd"); 72 | let passwd: EtcConf = EtcConf::new(path).unwrap(); 73 | 74 | let result: Vec<_> = passwd.collect(); 75 | let second_user = &result[1].as_ref().unwrap(); 76 | 77 | assert_eq!(second_user.username, "door"); 78 | } 79 | 80 | #[test] 81 | fn test_etc_group_enumeration() { 82 | let path = test_helpers::fixture_path!("unix/happy_path/etc/group"); 83 | let groups: EtcConf = EtcConf::new(path).unwrap(); 84 | 85 | let result: Vec<_> = groups.collect(); 86 | let first_group = result[0].as_ref().unwrap(); 87 | 88 | assert_eq!(first_group.users, &["root", "akhramov", "donald_watson"]); 89 | } 90 | 91 | #[test] 92 | fn test_malformed_file() { 93 | let path = test_helpers::fixture_path!("unix/malformed/etc/passwd"); 94 | let groups: EtcConf = EtcConf::new(path).unwrap(); 95 | let result: Vec<_> = groups.collect(); 96 | let first_err = result[0].as_ref().unwrap_err(); 97 | 98 | assert_eq!( 99 | test_helpers::fixture!("unix/malformed/etc/malformed_error_msg"), 100 | format!("{}", first_err) 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /baustelle/src/storage.rs: -------------------------------------------------------------------------------- 1 | pub const BLOBS_STORAGE_KEY: &[u8] = b"blobs"; 2 | pub const IMAGES_INDEX_STORAGE_KEY: &[u8] = b"images"; 3 | 4 | pub use storage::Storage; 5 | pub use storage::StorageEngine; 6 | 7 | #[cfg(test)] 8 | pub use storage::TestStorage; 9 | -------------------------------------------------------------------------------- /baustelle/test/resources/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": "2020-04-23T13:03:01.355887897+00:00", 3 | "author": null, 4 | "architecture": "amd64", 5 | "os": "linux", 6 | "config": { 7 | "User": "977:13", 8 | "ExposedPorts": { 9 | "80/tcp": { 10 | } 11 | }, 12 | "Env": [ 13 | "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 14 | "NGINX_VERSION=1.17.10", 15 | "NJS_VERSION=0.3.9", 16 | "PKG_RELEASE=1~buster" 17 | ], 18 | "Entrypoint": null, 19 | "Cmd": [ 20 | "nginx", 21 | "-g", 22 | "daemon off;" 23 | ], 24 | "Volumes": null, 25 | "WorkingDir": "", 26 | "labels": null, 27 | "StopSignal": "SIGTERM" 28 | }, 29 | "rootfs": { 30 | "type": "layers", 31 | "diff_ids": [ 32 | "sha256:c2adabaecedbda0af72b153c6499a0555f3a769d52370469d8f6bd6328af9b13", 33 | "sha256:216cf33c0a2877e88bd687ced2d05331f442b8490962469220a3a63bf2aad3b0", 34 | "sha256:b3003aac411c1d650bc4e3757ad96afe8f98a99b81c4e760e09c6542ee674289" 35 | ] 36 | }, 37 | "history": [ 38 | { 39 | "created": "2020-04-23T00:20:32.126556976+00:00", 40 | "author": null, 41 | "created_by": "/bin/sh -c #(nop) ADD file:9b8be2b52ee0fa31da1b6256099030b73546253a57e94cccb24605cd888bb74d in / ", 42 | "comment": null, 43 | "empty_layer": null 44 | }, 45 | { 46 | "created": "2020-04-23T00:20:32.391326355+00:00", 47 | "author": null, 48 | "created_by": "/bin/sh -c #(nop) CMD [\"bash\"]", 49 | "comment": null, 50 | "empty_layer": true 51 | }, 52 | { 53 | "created": "2020-04-23T13:02:24.647346893+00:00", 54 | "author": null, 55 | "created_by": "/bin/sh -c #(nop) LABEL maintainer=NGINX Docker Maintainers ", 56 | "comment": null, 57 | "empty_layer": true 58 | }, 59 | { 60 | "created": "2020-04-23T13:02:24.951828955+00:00", 61 | "author": null, 62 | "created_by": "/bin/sh -c #(nop) ENV NGINX_VERSION=1.17.10", 63 | "comment": null, 64 | "empty_layer": true 65 | }, 66 | { 67 | "created": "2020-04-23T13:02:25.259326754+00:00", 68 | "author": null, 69 | "created_by": "/bin/sh -c #(nop) ENV NJS_VERSION=0.3.9", 70 | "comment": null, 71 | "empty_layer": true 72 | }, 73 | { 74 | "created": "2020-04-23T13:02:25.591421520+00:00", 75 | "author": null, 76 | "created_by": "/bin/sh -c #(nop) ENV PKG_RELEASE=1~buster", 77 | "comment": null, 78 | "empty_layer": true 79 | }, 80 | { 81 | "created": "2020-04-23T13:02:59.072951853+00:00", 82 | "author": null, 83 | "created_by": "/bin/sh -c set -x && addgroup --system --gid 101 nginx && adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos \"nginx user\" --shell /bin/false --uid 101 nginx && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 ca-certificates && NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; found=''; for server in ha.pool.sks-keyservers.net hkp://keyserver.ubuntu.com:80 hkp://p80.pool.sks-keyservers.net:80 pgp.mit.edu ; do echo \"Fetching GPG key $NGINX_GPGKEY from $server\"; apt-key adv --keyserver \"$server\" --keyserver-options timeout=10 --recv-keys \"$NGINX_GPGKEY\" && found=yes && break; done; test -z \"$found\" && echo >&2 \"error: failed to fetch GPG key $NGINX_GPGKEY\" && exit 1; apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* && dpkgArch=\"$(dpkg --print-architecture)\" && nginxPackages=\" nginx=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-njs=${NGINX_VERSION}.${NJS_VERSION}-${PKG_RELEASE} \" && case \"$dpkgArch\" in amd64|i386) echo \"deb https://nginx.org/packages/mainline/debian/ buster nginx\" >> /etc/apt/sources.list.d/nginx.list && apt-get update ;; *) echo \"deb-src https://nginx.org/packages/mainline/debian/ buster nginx\" >> /etc/apt/sources.list.d/nginx.list && tempDir=\"$(mktemp -d)\" && chmod 777 \"$tempDir\" && savedAptMark=\"$(apt-mark showmanual)\" && apt-get update && apt-get build-dep -y $nginxPackages && ( cd \"$tempDir\" && DEB_BUILD_OPTIONS=\"nocheck parallel=$(nproc)\" apt-get source --compile $nginxPackages ) && apt-mark showmanual | xargs apt-mark auto > /dev/null && { [ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark; } && ls -lAFh \"$tempDir\" && ( cd \"$tempDir\" && dpkg-scanpackages . > Packages ) && grep '^Package: ' \"$tempDir/Packages\" && echo \"deb [ trusted=yes ] file://$tempDir ./\" > /etc/apt/sources.list.d/temp.list && apt-get -o Acquire::GzipIndexes=false update ;; esac && apt-get install --no-install-recommends --no-install-suggests -y $nginxPackages gettext-base && apt-get remove --purge --auto-remove -y ca-certificates && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list && if [ -n \"$tempDir\" ]; then apt-get purge -y --auto-remove && rm -rf \"$tempDir\" /etc/apt/sources.list.d/temp.list; fi", 84 | "comment": null, 85 | "empty_layer": null 86 | }, 87 | { 88 | "created": "2020-04-23T13:03:00.368933408+00:00", 89 | "author": null, 90 | "created_by": "/bin/sh -c ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log", 91 | "comment": null, 92 | "empty_layer": null 93 | }, 94 | { 95 | "created": "2020-04-23T13:03:00.732751286+00:00", 96 | "author": null, 97 | "created_by": "/bin/sh -c #(nop) EXPOSE 80", 98 | "comment": null, 99 | "empty_layer": true 100 | }, 101 | { 102 | "created": "2020-04-23T13:03:01.053575170+00:00", 103 | "author": null, 104 | "created_by": "/bin/sh -c #(nop) STOPSIGNAL SIGTERM", 105 | "comment": null, 106 | "empty_layer": true 107 | }, 108 | { 109 | "created": "2020-04-23T13:03:01.355887897+00:00", 110 | "author": null, 111 | "created_by": "/bin/sh -c #(nop) CMD [\"nginx\" \"-g\" \"daemon off;\"]", 112 | "comment": null, 113 | "empty_layer": true 114 | } 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /baustelle/test/resources/containerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.17.10 2 | 3 | ENV FOO=/bar 4 | WORKDIR ${FOO} 5 | 6 | CMD /bin/sleep 42 -------------------------------------------------------------------------------- /baustelle/test/resources/foo.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhramov/knast/8c9f5f481a467a22c7cc47f11a28fe52536f9950/baustelle/test/resources/foo.tar.gz -------------------------------------------------------------------------------- /baustelle/test/resources/foo_archive_entries: -------------------------------------------------------------------------------- 1 | vec![ 2 | "foo/", 3 | "foo/bar", 4 | "foo/baz", 5 | "foo/bad/", 6 | "foo/bis", 7 | "foo/bad/baz/", 8 | "foo/bad/bad", 9 | "foo/bad/baz/baz/", 10 | "foo/bad/baz/baz/baz", 11 | ].into_iter() 12 | .map(std::path::PathBuf::from) 13 | .collect::>() 14 | -------------------------------------------------------------------------------- /baustelle/test/resources/runtime_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ociVersion": "1.0.1-dev", 3 | "process": { 4 | "terminal": true, 5 | "user": { 6 | "uid": 0, 7 | "gid": 0 8 | }, 9 | "args": [ 10 | "/docker-entrypoint.sh", 11 | "nginx", 12 | "-g", 13 | "daemon off;" 14 | ], 15 | "env": [ 16 | "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 17 | "TERM=xterm", 18 | "HOSTNAME=1285a7fec103", 19 | "NGINX_VERSION=1.19.0", 20 | "container=podman", 21 | "NJS_VERSION=0.4.1", 22 | "PKG_RELEASE=1~buster", 23 | "HOME=/root" 24 | ], 25 | "cwd": "/", 26 | "rlimits": [ 27 | { 28 | "type": "RLIMIT_NOFILE", 29 | "hard": 524288, 30 | "soft": 524288 31 | }, 32 | { 33 | "type": "RLIMIT_NPROC", 34 | "hard": 32768, 35 | "soft": 32768 36 | } 37 | ] 38 | }, 39 | "root": { 40 | "path": "/home/user/.local/share/containers/storage/overlay/97dc6f4e9dd4fba06f9580bec0542d12da25b2a1bd527a28ccd6b82d6d065edf/merged" 41 | }, 42 | "hostname": "1285a7fec103", 43 | "mounts": [ 44 | { 45 | "destination": "/proc", 46 | "type": "proc", 47 | "source": "proc", 48 | "options": [ 49 | "nosuid", 50 | "noexec", 51 | "nodev" 52 | ] 53 | }, 54 | { 55 | "destination": "/dev", 56 | "type": "tmpfs", 57 | "source": "tmpfs", 58 | "options": [ 59 | "nosuid", 60 | "strictatime", 61 | "mode=755", 62 | "size=65536k" 63 | ] 64 | }, 65 | { 66 | "destination": "/sys", 67 | "type": "sysfs", 68 | "source": "sysfs", 69 | "options": [ 70 | "nosuid", 71 | "noexec", 72 | "nodev", 73 | "ro" 74 | ] 75 | }, 76 | { 77 | "destination": "/dev/pts", 78 | "type": "devpts", 79 | "source": "devpts", 80 | "options": [ 81 | "nosuid", 82 | "noexec", 83 | "newinstance", 84 | "ptmxmode=0666", 85 | "mode=0620", 86 | "gid=5" 87 | ] 88 | }, 89 | { 90 | "destination": "/dev/mqueue", 91 | "type": "mqueue", 92 | "source": "mqueue", 93 | "options": [ 94 | "nosuid", 95 | "noexec", 96 | "nodev" 97 | ] 98 | }, 99 | { 100 | "destination": "/etc/resolv.conf", 101 | "type": "bind", 102 | "source": "/run/user/1000/containers/overlay-containers/1285a7fec1035b6a8ee748bd23f4b38d079c030d7ba318b33776fa968897b69b/userdata/resolv.conf", 103 | "options": [ 104 | "bind", 105 | "private" 106 | ] 107 | }, 108 | { 109 | "destination": "/etc/hosts", 110 | "type": "bind", 111 | "source": "/run/user/1000/containers/overlay-containers/1285a7fec1035b6a8ee748bd23f4b38d079c030d7ba318b33776fa968897b69b/userdata/hosts", 112 | "options": [ 113 | "bind", 114 | "private" 115 | ] 116 | }, 117 | { 118 | "destination": "/dev/shm", 119 | "type": "bind", 120 | "source": "/home/user/.local/share/containers/storage/overlay-containers/1285a7fec1035b6a8ee748bd23f4b38d079c030d7ba318b33776fa968897b69b/userdata/shm", 121 | "options": [ 122 | "bind", 123 | "private" 124 | ] 125 | }, 126 | { 127 | "destination": "/etc/hostname", 128 | "type": "bind", 129 | "source": "/run/user/1000/containers/overlay-containers/1285a7fec1035b6a8ee748bd23f4b38d079c030d7ba318b33776fa968897b69b/userdata/hostname", 130 | "options": [ 131 | "bind", 132 | "private" 133 | ] 134 | }, 135 | { 136 | "destination": "/run/.containerenv", 137 | "type": "bind", 138 | "source": "/run/user/1000/containers/overlay-containers/1285a7fec1035b6a8ee748bd23f4b38d079c030d7ba318b33776fa968897b69b/userdata/.containerenv", 139 | "options": [ 140 | "bind", 141 | "private" 142 | ] 143 | }, 144 | { 145 | "destination": "/sys/fs/cgroup", 146 | "type": "cgroup", 147 | "source": "cgroup", 148 | "options": [ 149 | "rprivate", 150 | "nosuid", 151 | "noexec", 152 | "nodev", 153 | "relatime", 154 | "ro" 155 | ] 156 | } 157 | ], 158 | "annotations": { 159 | "io.container.manager": "libpod", 160 | "io.kubernetes.cri-o.Created": "2020-07-03T15:11:40.591482768+06:00", 161 | "io.kubernetes.cri-o.TTY": "true", 162 | "io.podman.annotations.autoremove": "FALSE", 163 | "io.podman.annotations.init": "FALSE", 164 | "io.podman.annotations.privileged": "FALSE", 165 | "io.podman.annotations.publish-all": "FALSE", 166 | "org.opencontainers.image.stopSignal": "15" 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/basic.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - request: 3 | method: head 4 | response: 5 | headers: 6 | - header: WWW-Authenticate 7 | value: Bearer realm="SERVER_URL/auth",service="registry.docker.io",scope="repository:library/nginx:pull" 8 | 9 | - request: 10 | method: GET 11 | path: /auth 12 | response: 13 | body: ./basic/auth.json 14 | 15 | - request: 16 | method: GET 17 | path: /v2/(.*)/manifests/(.*) 18 | headers: 19 | - header: Accept 20 | value: application/vnd.docker.distribution.manifest.v2+json 21 | response: 22 | body: ./basic/manifest.json 23 | 24 | - request: 25 | method: GET 26 | path: /v2/(.*)/manifests/(.*) 27 | headers: 28 | - header: Accept 29 | value: application/vnd.docker.distribution.manifest.list.v2+json 30 | response: 31 | body: ./basic/manifest_index.json 32 | 33 | - request: 34 | method: GET 35 | path: /v2/(.*)/blobs/(.*) 36 | headers: 37 | - header: Accept 38 | value: application/vnd.oci.image.config.v1+json 39 | response: 40 | body: ./basic/config.json 41 | 42 | - request: 43 | method: GET 44 | path: /v2/(.*)/blobs/(.*) 45 | headers: 46 | - header: Accept 47 | value: application/vnd.oci.image.layer.v1.tar+gzip 48 | response: 49 | body: ./basic/layer1 50 | -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/basic/auth.json: -------------------------------------------------------------------------------- 1 | {"access_token": "well, that might work"} 2 | -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/basic/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": "2020-04-23T13:03:01.355887897+00:00", 3 | "author": null, 4 | "architecture": "amd64", 5 | "os": "linux", 6 | "config": { 7 | "User": "", 8 | "ExposedPorts": { 9 | "80/tcp": { 10 | } 11 | }, 12 | "Env": [ 13 | "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 14 | "NGINX_VERSION=1.17.10", 15 | "NJS_VERSION=0.3.9", 16 | "PKG_RELEASE=1~buster" 17 | ], 18 | "Entrypoint": null, 19 | "Cmd": [ 20 | "nginx", 21 | "-g", 22 | "daemon off;" 23 | ], 24 | "Volumes": null, 25 | "WorkingDir": "", 26 | "labels": null, 27 | "StopSignal": "SIGTERM" 28 | }, 29 | "rootfs": { 30 | "type": "layers", 31 | "diff_ids": [ 32 | "sha256:c2adabaecedbda0af72b153c6499a0555f3a769d52370469d8f6bd6328af9b13", 33 | "sha256:216cf33c0a2877e88bd687ced2d05331f442b8490962469220a3a63bf2aad3b0", 34 | "sha256:b3003aac411c1d650bc4e3757ad96afe8f98a99b81c4e760e09c6542ee674289" 35 | ] 36 | }, 37 | "history": [ 38 | { 39 | "created": "2020-04-23T00:20:32.126556976+00:00", 40 | "author": null, 41 | "created_by": "/bin/sh -c #(nop) ADD file:9b8be2b52ee0fa31da1b6256099030b73546253a57e94cccb24605cd888bb74d in / ", 42 | "comment": null, 43 | "empty_layer": null 44 | }, 45 | { 46 | "created": "2020-04-23T00:20:32.391326355+00:00", 47 | "author": null, 48 | "created_by": "/bin/sh -c #(nop) CMD [\"bash\"]", 49 | "comment": null, 50 | "empty_layer": true 51 | }, 52 | { 53 | "created": "2020-04-23T13:02:24.647346893+00:00", 54 | "author": null, 55 | "created_by": "/bin/sh -c #(nop) LABEL maintainer=NGINX Docker Maintainers ", 56 | "comment": null, 57 | "empty_layer": true 58 | }, 59 | { 60 | "created": "2020-04-23T13:02:24.951828955+00:00", 61 | "author": null, 62 | "created_by": "/bin/sh -c #(nop) ENV NGINX_VERSION=1.17.10", 63 | "comment": null, 64 | "empty_layer": true 65 | }, 66 | { 67 | "created": "2020-04-23T13:02:25.259326754+00:00", 68 | "author": null, 69 | "created_by": "/bin/sh -c #(nop) ENV NJS_VERSION=0.3.9", 70 | "comment": null, 71 | "empty_layer": true 72 | }, 73 | { 74 | "created": "2020-04-23T13:02:25.591421520+00:00", 75 | "author": null, 76 | "created_by": "/bin/sh -c #(nop) ENV PKG_RELEASE=1~buster", 77 | "comment": null, 78 | "empty_layer": true 79 | }, 80 | { 81 | "created": "2020-04-23T13:02:59.072951853+00:00", 82 | "author": null, 83 | "created_by": "/bin/sh -c set -x && addgroup --system --gid 101 nginx && adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos \"nginx user\" --shell /bin/false --uid 101 nginx && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 ca-certificates && NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; found=''; for server in ha.pool.sks-keyservers.net hkp://keyserver.ubuntu.com:80 hkp://p80.pool.sks-keyservers.net:80 pgp.mit.edu ; do echo \"Fetching GPG key $NGINX_GPGKEY from $server\"; apt-key adv --keyserver \"$server\" --keyserver-options timeout=10 --recv-keys \"$NGINX_GPGKEY\" && found=yes && break; done; test -z \"$found\" && echo >&2 \"error: failed to fetch GPG key $NGINX_GPGKEY\" && exit 1; apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* && dpkgArch=\"$(dpkg --print-architecture)\" && nginxPackages=\" nginx=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-njs=${NGINX_VERSION}.${NJS_VERSION}-${PKG_RELEASE} \" && case \"$dpkgArch\" in amd64|i386) echo \"deb https://nginx.org/packages/mainline/debian/ buster nginx\" >> /etc/apt/sources.list.d/nginx.list && apt-get update ;; *) echo \"deb-src https://nginx.org/packages/mainline/debian/ buster nginx\" >> /etc/apt/sources.list.d/nginx.list && tempDir=\"$(mktemp -d)\" && chmod 777 \"$tempDir\" && savedAptMark=\"$(apt-mark showmanual)\" && apt-get update && apt-get build-dep -y $nginxPackages && ( cd \"$tempDir\" && DEB_BUILD_OPTIONS=\"nocheck parallel=$(nproc)\" apt-get source --compile $nginxPackages ) && apt-mark showmanual | xargs apt-mark auto > /dev/null && { [ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark; } && ls -lAFh \"$tempDir\" && ( cd \"$tempDir\" && dpkg-scanpackages . > Packages ) && grep '^Package: ' \"$tempDir/Packages\" && echo \"deb [ trusted=yes ] file://$tempDir ./\" > /etc/apt/sources.list.d/temp.list && apt-get -o Acquire::GzipIndexes=false update ;; esac && apt-get install --no-install-recommends --no-install-suggests -y $nginxPackages gettext-base && apt-get remove --purge --auto-remove -y ca-certificates && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list && if [ -n \"$tempDir\" ]; then apt-get purge -y --auto-remove && rm -rf \"$tempDir\" /etc/apt/sources.list.d/temp.list; fi", 84 | "comment": null, 85 | "empty_layer": null 86 | }, 87 | { 88 | "created": "2020-04-23T13:03:00.368933408+00:00", 89 | "author": null, 90 | "created_by": "/bin/sh -c ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log", 91 | "comment": null, 92 | "empty_layer": null 93 | }, 94 | { 95 | "created": "2020-04-23T13:03:00.732751286+00:00", 96 | "author": null, 97 | "created_by": "/bin/sh -c #(nop) EXPOSE 80", 98 | "comment": null, 99 | "empty_layer": true 100 | }, 101 | { 102 | "created": "2020-04-23T13:03:01.053575170+00:00", 103 | "author": null, 104 | "created_by": "/bin/sh -c #(nop) STOPSIGNAL SIGTERM", 105 | "comment": null, 106 | "empty_layer": true 107 | }, 108 | { 109 | "created": "2020-04-23T13:03:01.355887897+00:00", 110 | "author": null, 111 | "created_by": "/bin/sh -c #(nop) CMD [\"nginx\" \"-g\" \"daemon off;\"]", 112 | "comment": null, 113 | "empty_layer": true 114 | } 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/basic/layer1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhramov/knast/8c9f5f481a467a22c7cc47f11a28fe52536f9950/baustelle/test/resources/server_mocks/basic/layer1 -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/basic/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 2, 3 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 4 | "config": { 5 | "mediaType": "application/vnd.docker.container.image.v1+json", 6 | "size": 6668, 7 | "digest": "sha256:f31930d5b3c26e15a6d72d85b89f55a727c9290cb9f126ed3daa9aa36f3db8b2" 8 | }, 9 | "layers": [ 10 | { 11 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", 12 | "size": 274, 13 | "digest": "sha256:c28d7487cd39cf086cb8ab040798f46176a8c2414c23114969b0ac85c4c61a23" 14 | }, 15 | { 16 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", 17 | "size": 274, 18 | "digest": "sha256:c28d7487cd39cf086cb8ab040798f46176a8c2414c23114969b0ac85c4c61a23" 19 | }, 20 | { 21 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", 22 | "size": 274, 23 | "digest": "sha256:c28d7487cd39cf086cb8ab040798f46176a8c2414c23114969b0ac85c4c61a23" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/basic/manifest_index.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 2, 3 | "manifests": [ 4 | { 5 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 6 | "digest": "sha256:dfd6fcd3b075530b95f53b46f9f0e665f0fdb3ee2debe349ed3f146600e0060f", 7 | "size": 948, 8 | "urls": null, 9 | "platform": { 10 | "architecture": "amd64", 11 | "os": "linux", 12 | "os.version": null, 13 | "os.features": null, 14 | "variant": null 15 | } 16 | }, 17 | { 18 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 19 | "digest": "sha256:3cf37a5b5826ca2d2daa8c026d81b7f7b002dc11b5b9e5f7b34514ed8d19663c", 20 | "size": 948, 21 | "urls": null, 22 | "platform": { 23 | "architecture": "arm", 24 | "os": "linux", 25 | "os.version": null, 26 | "os.features": null, 27 | "variant": "v7" 28 | } 29 | }, 30 | { 31 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 32 | "digest": "sha256:0eb6da8a5474a960e9008a73c4802332d8c09654d56a630a736bdd8502a2b8ed", 33 | "size": 948, 34 | "urls": null, 35 | "platform": { 36 | "architecture": "arm64", 37 | "os": "linux", 38 | "os.version": null, 39 | "os.features": null, 40 | "variant": "v8" 41 | } 42 | }, 43 | { 44 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 45 | "digest": "sha256:d930948e1f4cc8844d1ebd47fae24fa10d2ffca7fe68bced431354c1b03399c7", 46 | "size": 948, 47 | "urls": null, 48 | "platform": { 49 | "architecture": "386", 50 | "os": "linux", 51 | "os.version": null, 52 | "os.features": null, 53 | "variant": null 54 | } 55 | }, 56 | { 57 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 58 | "digest": "sha256:06e2a1fa4c8be156dc2c05d2123d7876f504f73d64f88cb3f79410d7c5ccb193", 59 | "size": 948, 60 | "urls": null, 61 | "platform": { 62 | "architecture": "ppc64le", 63 | "os": "linux", 64 | "os.version": null, 65 | "os.features": null, 66 | "variant": null 67 | } 68 | }, 69 | { 70 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 71 | "digest": "sha256:a04b9f7d9d37cab1dc499e9f77fa0c2238552bea20afa0722780e92aa7f02634", 72 | "size": 948, 73 | "urls": null, 74 | "platform": { 75 | "architecture": "s390x", 76 | "os": "linux", 77 | "os.version": null, 78 | "os.features": null, 79 | "variant": null 80 | } 81 | } 82 | ], 83 | "annotations": null 84 | } 85 | -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/unix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - request: 3 | method: head 4 | response: 5 | headers: 6 | - header: WWW-Authenticate 7 | value: Bearer realm="SERVER_URL/auth",service="registry.docker.io",scope="repository:library/nginx:pull" 8 | 9 | - request: 10 | method: GET 11 | path: /auth 12 | response: 13 | body: ./basic/auth.json 14 | 15 | - request: 16 | method: GET 17 | path: /v2/(.*)/manifests/(.*) 18 | headers: 19 | - header: Accept 20 | value: application/vnd.docker.distribution.manifest.v2+json 21 | response: 22 | body: ./unix/manifest.json 23 | 24 | - request: 25 | method: GET 26 | path: /v2/(.*)/manifests/(.*) 27 | headers: 28 | - header: Accept 29 | value: application/vnd.docker.distribution.manifest.list.v2+json 30 | response: 31 | body: ./unix/manifest_index.json 32 | 33 | - request: 34 | method: GET 35 | path: /v2/(.*)/blobs/(.*) 36 | headers: 37 | - header: Accept 38 | value: application/vnd.oci.image.config.v1+json 39 | response: 40 | body: ./unix/config.json 41 | 42 | - request: 43 | method: GET 44 | path: /v2/(.*)/blobs/(.*) 45 | headers: 46 | - header: Accept 47 | value: application/vnd.oci.image.layer.v1.tar+gzip 48 | response: 49 | body: ./unix/layer.tar.gz 50 | -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/unix/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": "2020-04-23T13:03:01.355887897+00:00", 3 | "author": null, 4 | "architecture": "amd64", 5 | "os": "linux", 6 | "config": { 7 | "User": "", 8 | "ExposedPorts": { 9 | "80/tcp": { 10 | } 11 | }, 12 | "Env": [ 13 | "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 14 | "NGINX_VERSION=1.17.10", 15 | "NJS_VERSION=0.3.9", 16 | "PKG_RELEASE=1~buster" 17 | ], 18 | "Entrypoint": null, 19 | "Cmd": [ 20 | "nginx", 21 | "-g", 22 | "daemon off;" 23 | ], 24 | "Volumes": null, 25 | "WorkingDir": "", 26 | "labels": null, 27 | "StopSignal": "SIGTERM" 28 | }, 29 | "rootfs": { 30 | "type": "layers", 31 | "diff_ids": [ 32 | "sha256:c2adabaecedbda0af72b153c6499a0555f3a769d52370469d8f6bd6328af9b13", 33 | "sha256:216cf33c0a2877e88bd687ced2d05331f442b8490962469220a3a63bf2aad3b0", 34 | "sha256:b3003aac411c1d650bc4e3757ad96afe8f98a99b81c4e760e09c6542ee674289" 35 | ] 36 | }, 37 | "history": [ 38 | { 39 | "created": "2020-04-23T00:20:32.126556976+00:00", 40 | "author": null, 41 | "created_by": "/bin/sh -c #(nop) ADD file:9b8be2b52ee0fa31da1b6256099030b73546253a57e94cccb24605cd888bb74d in / ", 42 | "comment": null, 43 | "empty_layer": null 44 | }, 45 | { 46 | "created": "2020-04-23T00:20:32.391326355+00:00", 47 | "author": null, 48 | "created_by": "/bin/sh -c #(nop) CMD [\"bash\"]", 49 | "comment": null, 50 | "empty_layer": true 51 | }, 52 | { 53 | "created": "2020-04-23T13:02:24.647346893+00:00", 54 | "author": null, 55 | "created_by": "/bin/sh -c #(nop) LABEL maintainer=NGINX Docker Maintainers ", 56 | "comment": null, 57 | "empty_layer": true 58 | }, 59 | { 60 | "created": "2020-04-23T13:02:24.951828955+00:00", 61 | "author": null, 62 | "created_by": "/bin/sh -c #(nop) ENV NGINX_VERSION=1.17.10", 63 | "comment": null, 64 | "empty_layer": true 65 | }, 66 | { 67 | "created": "2020-04-23T13:02:25.259326754+00:00", 68 | "author": null, 69 | "created_by": "/bin/sh -c #(nop) ENV NJS_VERSION=0.3.9", 70 | "comment": null, 71 | "empty_layer": true 72 | }, 73 | { 74 | "created": "2020-04-23T13:02:25.591421520+00:00", 75 | "author": null, 76 | "created_by": "/bin/sh -c #(nop) ENV PKG_RELEASE=1~buster", 77 | "comment": null, 78 | "empty_layer": true 79 | }, 80 | { 81 | "created": "2020-04-23T13:02:59.072951853+00:00", 82 | "author": null, 83 | "created_by": "/bin/sh -c set -x && addgroup --system --gid 101 nginx && adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos \"nginx user\" --shell /bin/false --uid 101 nginx && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 ca-certificates && NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; found=''; for server in ha.pool.sks-keyservers.net hkp://keyserver.ubuntu.com:80 hkp://p80.pool.sks-keyservers.net:80 pgp.mit.edu ; do echo \"Fetching GPG key $NGINX_GPGKEY from $server\"; apt-key adv --keyserver \"$server\" --keyserver-options timeout=10 --recv-keys \"$NGINX_GPGKEY\" && found=yes && break; done; test -z \"$found\" && echo >&2 \"error: failed to fetch GPG key $NGINX_GPGKEY\" && exit 1; apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* && dpkgArch=\"$(dpkg --print-architecture)\" && nginxPackages=\" nginx=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-njs=${NGINX_VERSION}.${NJS_VERSION}-${PKG_RELEASE} \" && case \"$dpkgArch\" in amd64|i386) echo \"deb https://nginx.org/packages/mainline/debian/ buster nginx\" >> /etc/apt/sources.list.d/nginx.list && apt-get update ;; *) echo \"deb-src https://nginx.org/packages/mainline/debian/ buster nginx\" >> /etc/apt/sources.list.d/nginx.list && tempDir=\"$(mktemp -d)\" && chmod 777 \"$tempDir\" && savedAptMark=\"$(apt-mark showmanual)\" && apt-get update && apt-get build-dep -y $nginxPackages && ( cd \"$tempDir\" && DEB_BUILD_OPTIONS=\"nocheck parallel=$(nproc)\" apt-get source --compile $nginxPackages ) && apt-mark showmanual | xargs apt-mark auto > /dev/null && { [ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark; } && ls -lAFh \"$tempDir\" && ( cd \"$tempDir\" && dpkg-scanpackages . > Packages ) && grep '^Package: ' \"$tempDir/Packages\" && echo \"deb [ trusted=yes ] file://$tempDir ./\" > /etc/apt/sources.list.d/temp.list && apt-get -o Acquire::GzipIndexes=false update ;; esac && apt-get install --no-install-recommends --no-install-suggests -y $nginxPackages gettext-base && apt-get remove --purge --auto-remove -y ca-certificates && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list && if [ -n \"$tempDir\" ]; then apt-get purge -y --auto-remove && rm -rf \"$tempDir\" /etc/apt/sources.list.d/temp.list; fi", 84 | "comment": null, 85 | "empty_layer": null 86 | }, 87 | { 88 | "created": "2020-04-23T13:03:00.368933408+00:00", 89 | "author": null, 90 | "created_by": "/bin/sh -c ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log", 91 | "comment": null, 92 | "empty_layer": null 93 | }, 94 | { 95 | "created": "2020-04-23T13:03:00.732751286+00:00", 96 | "author": null, 97 | "created_by": "/bin/sh -c #(nop) EXPOSE 80", 98 | "comment": null, 99 | "empty_layer": true 100 | }, 101 | { 102 | "created": "2020-04-23T13:03:01.053575170+00:00", 103 | "author": null, 104 | "created_by": "/bin/sh -c #(nop) STOPSIGNAL SIGTERM", 105 | "comment": null, 106 | "empty_layer": true 107 | }, 108 | { 109 | "created": "2020-04-23T13:03:01.355887897+00:00", 110 | "author": null, 111 | "created_by": "/bin/sh -c #(nop) CMD [\"nginx\" \"-g\" \"daemon off;\"]", 112 | "comment": null, 113 | "empty_layer": true 114 | } 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/unix/layer.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhramov/knast/8c9f5f481a467a22c7cc47f11a28fe52536f9950/baustelle/test/resources/server_mocks/unix/layer.tar.gz -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/unix/layer/etc/group: -------------------------------------------------------------------------------- 1 | root:x:0: 2 | bin:x:1: 3 | daemon:x:2: 4 | sys:x:3: 5 | adm:x:4: 6 | tty:x:5: 7 | disk:x:6: 8 | lp:x:7: 9 | mem:x:8: 10 | kmem:x:9: 11 | wheel:x:10: 12 | cdrom:x:11: 13 | mail:x:12: 14 | man:x:15: 15 | dialout:x:18: 16 | floppy:x:19: 17 | games:x:20: 18 | tape:x:33: 19 | video:x:39: 20 | ftp:x:50: 21 | lock:x:54: 22 | audio:x:63: 23 | users:x:100: 24 | nobody:x:65534: 25 | dbus:x:81: 26 | utmp:x:22: 27 | utempter:x:35: 28 | input:x:999: 29 | kvm:x:36: 30 | render:x:998: 31 | systemd-journal:x:190: 32 | systemd-coredump:x:997: 33 | systemd-resolve:x:193: 34 | -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/unix/layer/etc/passwd: -------------------------------------------------------------------------------- 1 | root:x:0:0:root:/root:/bin/bash 2 | bin:x:1:1:bin:/bin:/sbin/nologin 3 | daemon:x:2:2:daemon:/sbin:/sbin/nologin 4 | adm:x:3:4:adm:/var/adm:/sbin/nologin 5 | lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin 6 | sync:x:5:0:sync:/sbin:/bin/sync 7 | shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown 8 | halt:x:7:0:halt:/sbin:/sbin/halt 9 | mail:x:8:12:mail:/var/spool/mail:/sbin/nologin 10 | operator:x:11:0:operator:/root:/sbin/nologin 11 | games:x:12:100:games:/usr/games:/sbin/nologin 12 | ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin 13 | nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin 14 | dbus:x:81:81:System message bus:/:/sbin/nologin 15 | systemd-coredump:x:999:997:systemd Core Dumper:/:/sbin/nologin 16 | systemd-resolve:x:193:193:systemd Resolver:/:/sbin/nologin 17 | -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/unix/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 2, 3 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 4 | "config": { 5 | "mediaType": "application/vnd.docker.container.image.v1+json", 6 | "size": 6668, 7 | "digest": "sha256:f31930d5b3c26e15a6d72d85b89f55a727c9290cb9f126ed3daa9aa36f3db8b2" 8 | }, 9 | "layers": [ 10 | { 11 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", 12 | "size": 659, 13 | "digest": "sha256:15d23ad7722e97ee7ae26c59b21504b875f98b5cb9b53497159d98d03c9474f3" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/unix/manifest_index.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 2, 3 | "manifests": [ 4 | { 5 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 6 | "digest": "sha256:bbc0ab1519cc42b97337f34629702ac6c6b4257f23a3ec35a82cd74fbe903bbb", 7 | "size": 525, 8 | "urls": null, 9 | "platform": { 10 | "architecture": "amd64", 11 | "os": "linux", 12 | "os.version": null, 13 | "os.features": null, 14 | "variant": null 15 | } 16 | } 17 | ], 18 | "annotations": null 19 | } 20 | -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/whiteouts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - request: 3 | method: head 4 | response: 5 | headers: 6 | - header: WWW-Authenticate 7 | value: Bearer realm="SERVER_URL/auth",service="registry.docker.io",scope="repository:library/nginx:pull" 8 | 9 | - request: 10 | method: GET 11 | path: /auth 12 | response: 13 | body: ./basic/auth.json 14 | 15 | - request: 16 | method: GET 17 | path: /v2/(.*)/manifests/(.*) 18 | headers: 19 | - header: Accept 20 | value: application/vnd.docker.distribution.manifest.v2+json 21 | response: 22 | body: ./whiteouts/manifest.json 23 | 24 | - request: 25 | method: GET 26 | path: /v2/(.*)/manifests/(.*) 27 | headers: 28 | - header: Accept 29 | value: application/vnd.docker.distribution.manifest.list.v2+json 30 | response: 31 | body: ./whiteouts/manifest_index.json 32 | 33 | - request: 34 | method: GET 35 | path: /v2/(.*)/blobs/(.*) 36 | headers: 37 | - header: Accept 38 | value: application/vnd.oci.image.config.v1+json 39 | response: 40 | body: ./whiteouts/config.json 41 | 42 | - request: 43 | method: GET 44 | path: /v2/(.*)/blobs/sha256:526471adf41794e411a43ac336431e7ef287d7bd203559cfa73fb3c4dbdf70d8 45 | headers: 46 | - header: Accept 47 | value: application/vnd.oci.image.layer.v1.tar+gzip 48 | response: 49 | body: ./whiteouts/layer1.tar.gz 50 | 51 | - request: 52 | method: GET 53 | path: /v2/(.*)/blobs/sha256:0c65bd1b64c07b2b884d3c5fa2164bb332a59532dc10754e987ff60a05507571 54 | headers: 55 | - header: Accept 56 | value: application/vnd.oci.image.layer.v1.tar+gzip 57 | response: 58 | body: ./whiteouts/layer2.tar.gz 59 | 60 | - request: 61 | method: GET 62 | path: /v2/(.*)/blobs/sha256:a05366bf184818015998c59328d934f36d912c6e06699543c879adca845f8f4a 63 | headers: 64 | - header: Accept 65 | value: application/vnd.oci.image.layer.v1.tar+gzip 66 | response: 67 | body: ./whiteouts/layer3.tar.gz 68 | -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/whiteouts/layer1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhramov/knast/8c9f5f481a467a22c7cc47f11a28fe52536f9950/baustelle/test/resources/server_mocks/whiteouts/layer1.tar.gz -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/whiteouts/layer2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhramov/knast/8c9f5f481a467a22c7cc47f11a28fe52536f9950/baustelle/test/resources/server_mocks/whiteouts/layer2.tar.gz -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/whiteouts/layer3.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhramov/knast/8c9f5f481a467a22c7cc47f11a28fe52536f9950/baustelle/test/resources/server_mocks/whiteouts/layer3.tar.gz -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/whiteouts/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 2, 3 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 4 | "config": { 5 | "mediaType": "application/vnd.docker.container.image.v1+json", 6 | "size": 6668, 7 | "digest": "sha256:f31930d5b3c26e15a6d72d85b89f55a727c9290cb9f126ed3daa9aa36f3db8b2" 8 | }, 9 | "layers": [ 10 | { 11 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", 12 | "size": 232, 13 | "digest": "sha256:526471adf41794e411a43ac336431e7ef287d7bd203559cfa73fb3c4dbdf70d8" 14 | }, 15 | { 16 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", 17 | "size": 323, 18 | "digest": "sha256:0c65bd1b64c07b2b884d3c5fa2164bb332a59532dc10754e987ff60a05507571" 19 | }, 20 | { 21 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", 22 | "size": 151, 23 | "digest": "sha256:a05366bf184818015998c59328d934f36d912c6e06699543c879adca845f8f4a" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /baustelle/test/resources/server_mocks/whiteouts/manifest_index.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 2, 3 | "manifests": [ 4 | { 5 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 6 | "digest": "sha256:5c894b651e2afac18038e3c3b58e52a2edb74cbeba02a5cccbb7a67f07ba3268", 7 | "size": 939, 8 | "urls": null, 9 | "platform": { 10 | "architecture": "amd64", 11 | "os": "linux", 12 | "os.version": null, 13 | "os.features": null, 14 | "variant": null 15 | } 16 | }, 17 | { 18 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 19 | "digest": "sha256:3cf37a5b5826ca2d2daa8c026d81b7f7b002dc11b5b9e5f7b34514ed8d19663c", 20 | "size": 948, 21 | "urls": null, 22 | "platform": { 23 | "architecture": "arm", 24 | "os": "linux", 25 | "os.version": null, 26 | "os.features": null, 27 | "variant": "v7" 28 | } 29 | }, 30 | { 31 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 32 | "digest": "sha256:0eb6da8a5474a960e9008a73c4802332d8c09654d56a630a736bdd8502a2b8ed", 33 | "size": 948, 34 | "urls": null, 35 | "platform": { 36 | "architecture": "arm64", 37 | "os": "linux", 38 | "os.version": null, 39 | "os.features": null, 40 | "variant": "v8" 41 | } 42 | }, 43 | { 44 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 45 | "digest": "sha256:d930948e1f4cc8844d1ebd47fae24fa10d2ffca7fe68bced431354c1b03399c7", 46 | "size": 948, 47 | "urls": null, 48 | "platform": { 49 | "architecture": "386", 50 | "os": "linux", 51 | "os.version": null, 52 | "os.features": null, 53 | "variant": null 54 | } 55 | }, 56 | { 57 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 58 | "digest": "sha256:06e2a1fa4c8be156dc2c05d2123d7876f504f73d64f88cb3f79410d7c5ccb193", 59 | "size": 948, 60 | "urls": null, 61 | "platform": { 62 | "architecture": "ppc64le", 63 | "os": "linux", 64 | "os.version": null, 65 | "os.features": null, 66 | "variant": null 67 | } 68 | }, 69 | { 70 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 71 | "digest": "sha256:a04b9f7d9d37cab1dc499e9f77fa0c2238552bea20afa0722780e92aa7f02634", 72 | "size": 948, 73 | "urls": null, 74 | "platform": { 75 | "architecture": "s390x", 76 | "os": "linux", 77 | "os.version": null, 78 | "os.features": null, 79 | "variant": null 80 | } 81 | } 82 | ], 83 | "annotations": null 84 | } 85 | -------------------------------------------------------------------------------- /baustelle/test/resources/unix/happy_path/etc/group: -------------------------------------------------------------------------------- 1 | # $FreeBSD: head/etc/group 359368 2020-03-27 16:05:37Z brooks $ 2 | # 3 | wheel:*:0:root,akhramov,donald_watson 4 | daemon:*:1: 5 | kmem:*:2: 6 | sys:*:3: 7 | tty:*:4: 8 | operator:*:5:root 9 | mail:*:6: 10 | bin:*:7: 11 | news:*:8: 12 | man:*:9: 13 | games:*:13: 14 | ftp:*:14: 15 | staff:*:20: 16 | sshd:*:22: 17 | smmsp:*:25: 18 | mailnull:*:26: 19 | guest:*:31: 20 | video:*:44: 21 | bind:*:53: 22 | unbound:*:59: 23 | proxy:*:62: 24 | authpf:*:63: 25 | _pflogd:*:64: 26 | _dhcp:*:65: 27 | uucp:*:66: 28 | dialer:*:68: 29 | network:*:69: 30 | audit:*:77: 31 | www:*:80: 32 | ntpd:*:123: 33 | _ypldap:*:160: 34 | hast:*:845: 35 | tests:*:977: 36 | nogroup:*:65533: 37 | nobody:*:65534: 38 | akhramov:*:1001: 39 | _tss:*:601: 40 | messagebus:*:556: 41 | avahi:*:558: 42 | cups:*:193: 43 | polkitd:*:565: 44 | colord:*:970: 45 | git_daemon:*:964: 46 | -------------------------------------------------------------------------------- /baustelle/test/resources/unix/happy_path/etc/passwd: -------------------------------------------------------------------------------- 1 | # $FreeBSD: head/etc/master.passwd 359368 2020-03-27 16:05:37Z brooks $ 2 | # 3 | root:*:0:0:Charlie &:/root:/bin/csh 4 | door:*:0:0:Bourne-again Superuser:/root: 5 | daemon:*:1:1:Owner of many system processes:/root:/usr/sbin/nologin 6 | operator:*:2:5:System &:/:/usr/sbin/nologin 7 | bin:*:3:7:Binaries Commands and Source:/:/usr/sbin/nologin 8 | tty:*:4:65533:Tty Sandbox:/:/usr/sbin/nologin 9 | kmem:*:5:65533:KMem Sandbox:/:/usr/sbin/nologin 10 | games:*:7:13:Games pseudo-user:/:/usr/sbin/nologin 11 | news:*:8:8:News Subsystem:/:/usr/sbin/nologin 12 | man:*:9:9:Mister Man Pages:/usr/share/man:/usr/sbin/nologin 13 | sshd:*:22:22:Secure Shell Daemon:/var/empty:/usr/sbin/nologin 14 | smmsp:*:25:25:Sendmail Submission User:/var/spool/clientmqueue:/usr/sbin/nologin 15 | mailnull:*:26:26:Sendmail Default User:/var/spool/mqueue:/usr/sbin/nologin 16 | bind:*:53:53:Bind Sandbox:/:/usr/sbin/nologin 17 | unbound:*:59:59:Unbound DNS Resolver:/var/unbound:/usr/sbin/nologin 18 | proxy:*:62:62:Packet Filter pseudo-user:/nonexistent:/usr/sbin/nologin 19 | _pflogd:*:64:64:pflogd privsep user:/var/empty:/usr/sbin/nologin 20 | _dhcp:*:65:65:dhcp programs:/var/empty:/usr/sbin/nologin 21 | uucp:*:66:66:UUCP pseudo-user:/var/spool/uucppublic:/usr/local/libexec/uucp/uucico 22 | pop:*:68:6:Post Office Owner:/nonexistent:/usr/sbin/nologin 23 | auditdistd:*:78:77:Auditdistd unprivileged user:/var/empty:/usr/sbin/nologin 24 | www:*:80:80:World Wide Web Owner:/nonexistent:/usr/sbin/nologin 25 | ntpd:*:123:123:NTP Daemon:/var/db/ntp:/usr/sbin/nologin 26 | _ypldap:*:160:160:YP LDAP unprivileged user:/var/empty:/usr/sbin/nologin 27 | hast:*:845:845:HAST unprivileged user:/var/empty:/usr/sbin/nologin 28 | tests:*:977:977:Unprivileged user for tests:/nonexistent:/usr/sbin/nologin 29 | nobody:*:65534:65534:Unprivileged user:/nonexistent:/usr/sbin/nologin 30 | akhramov:*:1001:1001:Artem Khramov:/home/akhramov:/usr/local/bin/bash 31 | _tss:*:601:601:TCG Software Stack user:/var/empty:/usr/sbin/nologin 32 | messagebus:*:556:556:D-BUS Daemon User:/nonexistent:/usr/sbin/nologin 33 | avahi:*:558:558:Avahi Daemon User:/nonexistent:/usr/sbin/nologin 34 | cups:*:193:193:Cups Owner:/nonexistent:/usr/sbin/nologin 35 | polkitd:*:565:565:Polkit Daemon User:/var/empty:/usr/sbin/nologin 36 | colord:*:970:970:colord color management daemon:/nonexistent:/usr/sbin/nologin 37 | git_daemon:*:964:964:git daemon:/nonexistent:/usr/sbin/nologin 38 | -------------------------------------------------------------------------------- /baustelle/test/resources/unix/malformed/etc/malformed_error_msg: -------------------------------------------------------------------------------- 1 | CSV deserialize error: record 0 (line: 1, byte: 0): invalid length 2, expected struct EtcGroupEntry with 4 elements -------------------------------------------------------------------------------- /baustelle/test/resources/unix/malformed/etc/passwd: -------------------------------------------------------------------------------- 1 | spook:42 -------------------------------------------------------------------------------- /baustelle/test/resources/unpacked_layers: -------------------------------------------------------------------------------- 1 | vec![ 2 | "directory/bsd/getting/oci/containers", 3 | "directory/foo/bad", 4 | "directory/foo/baz", 5 | ].into_iter() 6 | .map(std::path::PathBuf::from) 7 | .collect::>() 8 | -------------------------------------------------------------------------------- /common_lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "common_lib" 3 | version = "0.1.0" 4 | authors = ["Artem Khramov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /common_lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub trait AsSignedBytes { 2 | fn as_signed_bytes(&self) -> &[i8] { 3 | let bytes = unsafe { self.bytes().align_to() }; 4 | 5 | bytes.1 6 | } 7 | 8 | fn bytes(&self) -> &[u8]; 9 | } 10 | 11 | impl AsSignedBytes for &str { 12 | fn bytes(&self) -> &[u8] { 13 | self.as_bytes() 14 | } 15 | } 16 | 17 | impl AsSignedBytes for Vec { 18 | fn bytes(&self) -> &[u8] { 19 | self.as_slice() 20 | } 21 | } 22 | 23 | impl AsSignedBytes for &[u8] { 24 | fn bytes(&self) -> &[u8] { 25 | self 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /containerd-shim/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "containerd-shim" 3 | version = "0.1.0" 4 | authors = ["Artem Khramov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | anyhow = "1" 9 | async-trait = "0.1.42" 10 | futures = "0.3" 11 | libknast = { path = "../libknast" } 12 | libc = "0.2.71" 13 | nix = "0.20.0" 14 | protobuf = "2.8.0" 15 | serde = { version = "1.0", features = ["derive"] } 16 | serde_json = "1.0" 17 | storage = { path = "../storage" } 18 | ttrpc = { git = "ssh://git@github.com/akhramov/ttrpc-rust", features = ["async"] } 19 | tokio = { version = "1.1.1", features = ["macros", "rt", "rt-multi-thread"] } 20 | tracing = { version = "0.1.25", features = ["attributes"] } 21 | tracing-appender = "0.1.2" 22 | tracing-core = "0.1.19" 23 | tracing-subscriber = "0.2.18" 24 | url = "2.2.2" 25 | 26 | [build-dependencies] 27 | ttrpc-codegen = { git = "ssh://git@github.com/akhramov/ttrpc-rust" } -------------------------------------------------------------------------------- /containerd-shim/build.rs: -------------------------------------------------------------------------------- 1 | use ttrpc_codegen::Codegen; 2 | 3 | fn main() { 4 | let shim_inputs = vec![ 5 | "proto/shim.proto", 6 | "proto/google/protobuf/any.proto", 7 | "proto/google/protobuf/empty.proto", 8 | "proto/google/protobuf/timestamp.proto", 9 | "proto/github.com/containerd/containerd/api/types/mount.proto", 10 | "proto/github.com/containerd/containerd/api/types/task/task.proto", 11 | ]; 12 | Codegen::new() 13 | .out_dir("src/protocols") 14 | .inputs(&shim_inputs) 15 | .include("proto") 16 | .rust_protobuf() 17 | .run() 18 | .expect("Failed to generate ttrpc server code"); 19 | } 20 | -------------------------------------------------------------------------------- /containerd-shim/proto/github.com/containerd/containerd/api/types/mount.proto: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | syntax = "proto3"; 18 | 19 | package containerd.types; 20 | 21 | import weak "gogoproto/gogo.proto"; 22 | 23 | option go_package = "github.com/containerd/containerd/api/types;types"; 24 | 25 | // Mount describes mounts for a container. 26 | // 27 | // This type is the lingua franca of ContainerD. All services provide mounts 28 | // to be used with the container at creation time. 29 | // 30 | // The Mount type follows the structure of the mount syscall, including a type, 31 | // source, target and options. 32 | message Mount { 33 | // Type defines the nature of the mount. 34 | string type = 1; 35 | 36 | // Source specifies the name of the mount. Depending on mount type, this 37 | // may be a volume name or a host path, or even ignored. 38 | string source = 2; 39 | 40 | // Target path in container 41 | string target = 3; 42 | 43 | // Options specifies zero or more fstab style mount options. 44 | repeated string options = 4; 45 | } 46 | -------------------------------------------------------------------------------- /containerd-shim/proto/github.com/containerd/containerd/api/types/task/task.proto: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | syntax = "proto3"; 18 | 19 | package containerd.v1.types; 20 | 21 | import weak "gogoproto/gogo.proto"; 22 | import "google/protobuf/timestamp.proto"; 23 | import "google/protobuf/any.proto"; 24 | 25 | enum Status { 26 | option (gogoproto.goproto_enum_prefix) = false; 27 | option (gogoproto.enum_customname) = "Status"; 28 | 29 | UNKNOWN = 0 [(gogoproto.enumvalue_customname) = "StatusUnknown"]; 30 | CREATED = 1 [(gogoproto.enumvalue_customname) = "StatusCreated"]; 31 | RUNNING = 2 [(gogoproto.enumvalue_customname) = "StatusRunning"]; 32 | STOPPED = 3 [(gogoproto.enumvalue_customname) = "StatusStopped"]; 33 | PAUSED = 4 [(gogoproto.enumvalue_customname) = "StatusPaused"]; 34 | PAUSING = 5 [(gogoproto.enumvalue_customname) = "StatusPausing"]; 35 | } 36 | 37 | message Process { 38 | string container_id = 1; 39 | string id = 2; 40 | uint32 pid = 3; 41 | Status status = 4; 42 | string stdin = 5; 43 | string stdout = 6; 44 | string stderr = 7; 45 | bool terminal = 8; 46 | uint32 exit_status = 9; 47 | google.protobuf.Timestamp exited_at = 10 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; 48 | } 49 | 50 | message ProcessInfo { 51 | // PID is the process ID. 52 | uint32 pid = 1; 53 | // Info contains additional process information. 54 | // 55 | // Info varies by platform. 56 | google.protobuf.Any info = 2; 57 | } 58 | -------------------------------------------------------------------------------- /containerd-shim/proto/gogoproto/gogo.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers for Go with Gadgets 2 | // 3 | // Copyright (c) 2013, The GoGo Authors. All rights reserved. 4 | // http://github.com/gogo/protobuf 5 | // 6 | // Redistribution and use in source and binary forms, with or without 7 | // modification, are permitted provided that the following conditions are 8 | // met: 9 | // 10 | // * Redistributions of source code must retain the above copyright 11 | // notice, this list of conditions and the following disclaimer. 12 | // * Redistributions in binary form must reproduce the above 13 | // copyright notice, this list of conditions and the following disclaimer 14 | // in the documentation and/or other materials provided with the 15 | // distribution. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | syntax = "proto2"; 30 | package gogoproto; 31 | 32 | import "google/protobuf/descriptor.proto"; 33 | 34 | option java_package = "com.google.protobuf"; 35 | option java_outer_classname = "GoGoProtos"; 36 | option go_package = "github.com/gogo/protobuf/gogoproto"; 37 | 38 | extend google.protobuf.EnumOptions { 39 | optional bool goproto_enum_prefix = 62001; 40 | optional bool goproto_enum_stringer = 62021; 41 | optional bool enum_stringer = 62022; 42 | optional string enum_customname = 62023; 43 | optional bool enumdecl = 62024; 44 | } 45 | 46 | extend google.protobuf.EnumValueOptions { 47 | optional string enumvalue_customname = 66001; 48 | } 49 | 50 | extend google.protobuf.FileOptions { 51 | optional bool goproto_getters_all = 63001; 52 | optional bool goproto_enum_prefix_all = 63002; 53 | optional bool goproto_stringer_all = 63003; 54 | optional bool verbose_equal_all = 63004; 55 | optional bool face_all = 63005; 56 | optional bool gostring_all = 63006; 57 | optional bool populate_all = 63007; 58 | optional bool stringer_all = 63008; 59 | optional bool onlyone_all = 63009; 60 | 61 | optional bool equal_all = 63013; 62 | optional bool description_all = 63014; 63 | optional bool testgen_all = 63015; 64 | optional bool benchgen_all = 63016; 65 | optional bool marshaler_all = 63017; 66 | optional bool unmarshaler_all = 63018; 67 | optional bool stable_marshaler_all = 63019; 68 | 69 | optional bool sizer_all = 63020; 70 | 71 | optional bool goproto_enum_stringer_all = 63021; 72 | optional bool enum_stringer_all = 63022; 73 | 74 | optional bool unsafe_marshaler_all = 63023; 75 | optional bool unsafe_unmarshaler_all = 63024; 76 | 77 | optional bool goproto_extensions_map_all = 63025; 78 | optional bool goproto_unrecognized_all = 63026; 79 | optional bool gogoproto_import = 63027; 80 | optional bool protosizer_all = 63028; 81 | optional bool compare_all = 63029; 82 | optional bool typedecl_all = 63030; 83 | optional bool enumdecl_all = 63031; 84 | 85 | optional bool goproto_registration = 63032; 86 | optional bool messagename_all = 63033; 87 | 88 | optional bool goproto_sizecache_all = 63034; 89 | optional bool goproto_unkeyed_all = 63035; 90 | } 91 | 92 | extend google.protobuf.MessageOptions { 93 | optional bool goproto_getters = 64001; 94 | optional bool goproto_stringer = 64003; 95 | optional bool verbose_equal = 64004; 96 | optional bool face = 64005; 97 | optional bool gostring = 64006; 98 | optional bool populate = 64007; 99 | optional bool stringer = 67008; 100 | optional bool onlyone = 64009; 101 | 102 | optional bool equal = 64013; 103 | optional bool description = 64014; 104 | optional bool testgen = 64015; 105 | optional bool benchgen = 64016; 106 | optional bool marshaler = 64017; 107 | optional bool unmarshaler = 64018; 108 | optional bool stable_marshaler = 64019; 109 | 110 | optional bool sizer = 64020; 111 | 112 | optional bool unsafe_marshaler = 64023; 113 | optional bool unsafe_unmarshaler = 64024; 114 | 115 | optional bool goproto_extensions_map = 64025; 116 | optional bool goproto_unrecognized = 64026; 117 | 118 | optional bool protosizer = 64028; 119 | optional bool compare = 64029; 120 | 121 | optional bool typedecl = 64030; 122 | 123 | optional bool messagename = 64033; 124 | 125 | optional bool goproto_sizecache = 64034; 126 | optional bool goproto_unkeyed = 64035; 127 | } 128 | 129 | extend google.protobuf.FieldOptions { 130 | optional bool nullable = 65001; 131 | optional bool embed = 65002; 132 | optional string customtype = 65003; 133 | optional string customname = 65004; 134 | optional string jsontag = 65005; 135 | optional string moretags = 65006; 136 | optional string casttype = 65007; 137 | optional string castkey = 65008; 138 | optional string castvalue = 65009; 139 | 140 | optional bool stdtime = 65010; 141 | optional bool stdduration = 65011; 142 | optional bool wktpointer = 65012; 143 | 144 | } 145 | -------------------------------------------------------------------------------- /containerd-shim/proto/google/protobuf/any.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option go_package = "google.golang.org/protobuf/types/known/anypb"; 37 | option java_package = "com.google.protobuf"; 38 | option java_outer_classname = "AnyProto"; 39 | option java_multiple_files = true; 40 | option objc_class_prefix = "GPB"; 41 | 42 | // `Any` contains an arbitrary serialized protocol buffer message along with a 43 | // URL that describes the type of the serialized message. 44 | // 45 | // Protobuf library provides support to pack/unpack Any values in the form 46 | // of utility functions or additional generated methods of the Any type. 47 | // 48 | // Example 1: Pack and unpack a message in C++. 49 | // 50 | // Foo foo = ...; 51 | // Any any; 52 | // any.PackFrom(foo); 53 | // ... 54 | // if (any.UnpackTo(&foo)) { 55 | // ... 56 | // } 57 | // 58 | // Example 2: Pack and unpack a message in Java. 59 | // 60 | // Foo foo = ...; 61 | // Any any = Any.pack(foo); 62 | // ... 63 | // if (any.is(Foo.class)) { 64 | // foo = any.unpack(Foo.class); 65 | // } 66 | // 67 | // Example 3: Pack and unpack a message in Python. 68 | // 69 | // foo = Foo(...) 70 | // any = Any() 71 | // any.Pack(foo) 72 | // ... 73 | // if any.Is(Foo.DESCRIPTOR): 74 | // any.Unpack(foo) 75 | // ... 76 | // 77 | // Example 4: Pack and unpack a message in Go 78 | // 79 | // foo := &pb.Foo{...} 80 | // any, err := anypb.New(foo) 81 | // if err != nil { 82 | // ... 83 | // } 84 | // ... 85 | // foo := &pb.Foo{} 86 | // if err := any.UnmarshalTo(foo); err != nil { 87 | // ... 88 | // } 89 | // 90 | // The pack methods provided by protobuf library will by default use 91 | // 'type.googleapis.com/full.type.name' as the type URL and the unpack 92 | // methods only use the fully qualified type name after the last '/' 93 | // in the type URL, for example "foo.bar.com/x/y.z" will yield type 94 | // name "y.z". 95 | // 96 | // 97 | // JSON 98 | // ==== 99 | // The JSON representation of an `Any` value uses the regular 100 | // representation of the deserialized, embedded message, with an 101 | // additional field `@type` which contains the type URL. Example: 102 | // 103 | // package google.profile; 104 | // message Person { 105 | // string first_name = 1; 106 | // string last_name = 2; 107 | // } 108 | // 109 | // { 110 | // "@type": "type.googleapis.com/google.profile.Person", 111 | // "firstName": , 112 | // "lastName": 113 | // } 114 | // 115 | // If the embedded message type is well-known and has a custom JSON 116 | // representation, that representation will be embedded adding a field 117 | // `value` which holds the custom JSON in addition to the `@type` 118 | // field. Example (for message [google.protobuf.Duration][]): 119 | // 120 | // { 121 | // "@type": "type.googleapis.com/google.protobuf.Duration", 122 | // "value": "1.212s" 123 | // } 124 | // 125 | message Any { 126 | // A URL/resource name that uniquely identifies the type of the serialized 127 | // protocol buffer message. This string must contain at least 128 | // one "/" character. The last segment of the URL's path must represent 129 | // the fully qualified name of the type (as in 130 | // `path/google.protobuf.Duration`). The name should be in a canonical form 131 | // (e.g., leading "." is not accepted). 132 | // 133 | // In practice, teams usually precompile into the binary all types that they 134 | // expect it to use in the context of Any. However, for URLs which use the 135 | // scheme `http`, `https`, or no scheme, one can optionally set up a type 136 | // server that maps type URLs to message definitions as follows: 137 | // 138 | // * If no scheme is provided, `https` is assumed. 139 | // * An HTTP GET on the URL must yield a [google.protobuf.Type][] 140 | // value in binary format, or produce an error. 141 | // * Applications are allowed to cache lookup results based on the 142 | // URL, or have them precompiled into a binary to avoid any 143 | // lookup. Therefore, binary compatibility needs to be preserved 144 | // on changes to types. (Use versioned type names to manage 145 | // breaking changes.) 146 | // 147 | // Note: this functionality is not currently available in the official 148 | // protobuf release, and it is not used for type URLs beginning with 149 | // type.googleapis.com. 150 | // 151 | // Schemes other than `http`, `https` (or the empty scheme) might be 152 | // used with implementation specific semantics. 153 | // 154 | string type_url = 1; 155 | 156 | // Must be a valid serialized protocol buffer of the above specified type. 157 | bytes value = 2; 158 | } 159 | -------------------------------------------------------------------------------- /containerd-shim/proto/google/protobuf/empty.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option go_package = "google.golang.org/protobuf/types/known/emptypb"; 37 | option java_package = "com.google.protobuf"; 38 | option java_outer_classname = "EmptyProto"; 39 | option java_multiple_files = true; 40 | option objc_class_prefix = "GPB"; 41 | option cc_enable_arenas = true; 42 | 43 | // A generic empty message that you can re-use to avoid defining duplicated 44 | // empty messages in your APIs. A typical example is to use it as the request 45 | // or the response type of an API method. For instance: 46 | // 47 | // service Foo { 48 | // rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); 49 | // } 50 | // 51 | // The JSON representation for `Empty` is empty JSON object `{}`. 52 | message Empty {} 53 | -------------------------------------------------------------------------------- /containerd-shim/proto/shim.proto: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | syntax = "proto3"; 18 | 19 | package containerd.task.v2; 20 | 21 | import "google/protobuf/any.proto"; 22 | import "google/protobuf/empty.proto"; 23 | import weak "gogoproto/gogo.proto"; 24 | import "google/protobuf/timestamp.proto"; 25 | import "github.com/containerd/containerd/api/types/mount.proto"; 26 | import "github.com/containerd/containerd/api/types/task/task.proto"; 27 | 28 | option go_package = "github.com/containerd/containerd/runtime/v2/task;task"; 29 | 30 | // Shim service is launched for each container and is responsible for owning the IO 31 | // for the container and its additional processes. The shim is also the parent of 32 | // each container and allows reattaching to the IO and receiving the exit status 33 | // for the container processes. 34 | service Task { 35 | rpc State(StateRequest) returns (StateResponse); 36 | rpc Create(CreateTaskRequest) returns (CreateTaskResponse); 37 | rpc Start(StartRequest) returns (StartResponse); 38 | rpc Delete(DeleteRequest) returns (DeleteResponse); 39 | rpc Pids(PidsRequest) returns (PidsResponse); 40 | rpc Pause(PauseRequest) returns (google.protobuf.Empty); 41 | rpc Resume(ResumeRequest) returns (google.protobuf.Empty); 42 | rpc Checkpoint(CheckpointTaskRequest) returns (google.protobuf.Empty); 43 | rpc Kill(KillRequest) returns (google.protobuf.Empty); 44 | rpc Exec(ExecProcessRequest) returns (google.protobuf.Empty); 45 | rpc ResizePty(ResizePtyRequest) returns (google.protobuf.Empty); 46 | rpc CloseIO(CloseIORequest) returns (google.protobuf.Empty); 47 | rpc Update(UpdateTaskRequest) returns (google.protobuf.Empty); 48 | rpc Wait(WaitRequest) returns (WaitResponse); 49 | rpc Stats(StatsRequest) returns (StatsResponse); 50 | rpc Connect(ConnectRequest) returns (ConnectResponse); 51 | rpc Shutdown(ShutdownRequest) returns (google.protobuf.Empty); 52 | } 53 | 54 | message CreateTaskRequest { 55 | string id = 1; 56 | string bundle = 2; 57 | repeated containerd.types.Mount rootfs = 3; 58 | bool terminal = 4; 59 | string stdin = 5; 60 | string stdout = 6; 61 | string stderr = 7; 62 | string checkpoint = 8; 63 | string parent_checkpoint = 9; 64 | google.protobuf.Any options = 10; 65 | } 66 | 67 | message CreateTaskResponse { 68 | uint32 pid = 1; 69 | } 70 | 71 | message DeleteRequest { 72 | string id = 1; 73 | string exec_id = 2; 74 | } 75 | 76 | message DeleteResponse { 77 | uint32 pid = 1; 78 | uint32 exit_status = 2; 79 | google.protobuf.Timestamp exited_at = 3 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; 80 | } 81 | 82 | message ExecProcessRequest { 83 | string id = 1; 84 | string exec_id = 2; 85 | bool terminal = 3; 86 | string stdin = 4; 87 | string stdout = 5; 88 | string stderr = 6; 89 | google.protobuf.Any spec = 7; 90 | } 91 | 92 | message ExecProcessResponse { 93 | } 94 | 95 | message ResizePtyRequest { 96 | string id = 1; 97 | string exec_id = 2; 98 | uint32 width = 3; 99 | uint32 height = 4; 100 | } 101 | 102 | message StateRequest { 103 | string id = 1; 104 | string exec_id = 2; 105 | } 106 | 107 | message StateResponse { 108 | string id = 1; 109 | string bundle = 2; 110 | uint32 pid = 3; 111 | containerd.v1.types.Status status = 4; 112 | string stdin = 5; 113 | string stdout = 6; 114 | string stderr = 7; 115 | bool terminal = 8; 116 | uint32 exit_status = 9; 117 | google.protobuf.Timestamp exited_at = 10 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; 118 | string exec_id = 11; 119 | } 120 | 121 | message KillRequest { 122 | string id = 1; 123 | string exec_id = 2; 124 | uint32 signal = 3; 125 | bool all = 4; 126 | } 127 | 128 | message CloseIORequest { 129 | string id = 1; 130 | string exec_id = 2; 131 | bool stdin = 3; 132 | } 133 | 134 | message PidsRequest { 135 | string id = 1; 136 | } 137 | 138 | message PidsResponse { 139 | repeated containerd.v1.types.ProcessInfo processes = 1; 140 | } 141 | 142 | message CheckpointTaskRequest { 143 | string id = 1; 144 | string path = 2; 145 | google.protobuf.Any options = 3; 146 | } 147 | 148 | message UpdateTaskRequest { 149 | string id = 1; 150 | google.protobuf.Any resources = 2; 151 | map annotations = 3; 152 | } 153 | 154 | message StartRequest { 155 | string id = 1; 156 | string exec_id = 2; 157 | } 158 | 159 | message StartResponse { 160 | uint32 pid = 1; 161 | } 162 | 163 | message WaitRequest { 164 | string id = 1; 165 | string exec_id = 2; 166 | } 167 | 168 | message WaitResponse { 169 | uint32 exit_status = 1; 170 | google.protobuf.Timestamp exited_at = 2 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; 171 | } 172 | 173 | message StatsRequest { 174 | string id = 1; 175 | } 176 | 177 | message StatsResponse { 178 | google.protobuf.Any stats = 1; 179 | } 180 | 181 | message ConnectRequest { 182 | string id = 1; 183 | } 184 | 185 | message ConnectResponse { 186 | uint32 shim_pid = 1; 187 | uint32 task_pid = 2; 188 | string version = 3; 189 | } 190 | 191 | message ShutdownRequest { 192 | string id = 1; 193 | bool now = 2; 194 | } 195 | 196 | message PauseRequest { 197 | string id = 1; 198 | } 199 | 200 | message ResumeRequest { 201 | string id = 1; 202 | } 203 | -------------------------------------------------------------------------------- /containerd-shim/src/filesystem.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::Error; 4 | use libknast::filesystem::Mountable; 5 | 6 | use super::protocols::mount::Mount; 7 | 8 | impl Mountable for Mount { 9 | fn kind(&self) -> &String { 10 | &self.field_type 11 | } 12 | 13 | fn source(&self) -> &String { 14 | &self.source 15 | } 16 | 17 | fn destination(&self) -> &String { 18 | &self.target 19 | } 20 | 21 | fn options(&self) -> Vec { 22 | self.options.as_ref().to_vec() 23 | } 24 | 25 | fn post_mount_hooks( 26 | &self, 27 | _rootfs: impl AsRef, 28 | ) -> Result<(), Error> { 29 | Ok(()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /containerd-shim/src/main.rs: -------------------------------------------------------------------------------- 1 | mod filesystem; 2 | mod oci_extensions; 3 | mod protocols; 4 | mod task_service; 5 | 6 | use std::{ 7 | fs::remove_file, 8 | io::Error as StdError, 9 | sync::mpsc::{self, Receiver}, 10 | thread, time, 11 | }; 12 | 13 | use anyhow::Error; 14 | use libc::{rfork, RFCFDG, RFPROC}; 15 | use libknast::operations::OciOperations; 16 | use storage::TestStorage; 17 | use ttrpc::{client::Client, context, server::Server}; 18 | 19 | use protocols::{shim::ConnectRequest, shim_ttrpc::TaskClient}; 20 | use task_service::TaskService; 21 | 22 | const CONNECTION_RETRY_ATTEMPTS: u32 = 3; 23 | const CONNECTION_TIMEOUT_NANOS: i64 = 1_000_000_000; 24 | 25 | fn main() { 26 | let (command, id) = parse_opts(); 27 | match &command[..] { 28 | "start" => start_command(), 29 | "delete" => delete_command(id), 30 | _ => panic!("Unknown command {:?}", command), 31 | } 32 | } 33 | 34 | fn start_command() { 35 | if parent_process().is_ok() { 36 | return; 37 | } 38 | 39 | match unsafe { rfork(RFPROC | RFCFDG) } { 40 | 0 => { 41 | child_process(); 42 | } 43 | -1 => { 44 | eprintln!("rfork failed {:?}", StdError::last_os_error()); 45 | } 46 | _pid => parent_process().expect("Server is not running"), 47 | } 48 | } 49 | 50 | fn delete_command(id: String) { 51 | let _guard = setup_logging(); 52 | let storage = storage(); 53 | let ops = OciOperations::new(&storage, id) 54 | .expect("Failed to initialize runtime"); 55 | 56 | ops.delete() 57 | } 58 | 59 | fn parent_process() -> Result<(), Error> { 60 | client().and_then(|client| { 61 | let request = ConnectRequest::new(); 62 | Ok(client.connect( 63 | context::with_timeout(CONNECTION_TIMEOUT_NANOS), 64 | &request, 65 | )?) 66 | })?; 67 | 68 | let server_address = server_address()?; 69 | 70 | println!("{}", server_address.as_str()); 71 | 72 | Ok(()) 73 | } 74 | 75 | fn child_process() { 76 | let _guard = setup_logging(); 77 | 78 | match server() { 79 | Ok((mut server, shutdown_notification)) => { 80 | server.start().expect("failed to start server"); 81 | 82 | tracing::info!( 83 | "Server is listening at {}", 84 | server_address().unwrap().as_str() 85 | ); 86 | 87 | if let Err(_) = shutdown_notification.recv() { 88 | tracing::error!( 89 | "Sender dropped. Attempting to shutdown server" 90 | ); 91 | } 92 | 93 | server.shutdown(); 94 | } 95 | Err(err) => { 96 | tracing::error!("Server failed to start due to error: {:?}", err); 97 | } 98 | } 99 | } 100 | 101 | fn server() -> Result<(Server, Receiver<()>), Error> { 102 | let (sender, shutdown_notification) = mpsc::sync_channel(1); 103 | let nat_interface = 104 | std::env::var("NAT_INTERFACE").unwrap_or_else(|_| "lagg0".into()); 105 | let service = protocols::shim_ttrpc::create_task(TaskService::new( 106 | storage(), 107 | sender, 108 | nat_interface, 109 | )); 110 | tracing::info!("Initializing server"); 111 | let address = server_address()?; 112 | if let Err(error) = remove_file(address.path()) { 113 | tracing::info!("Previous socket wasn't deleted due to {}", error) 114 | }; 115 | let server = Server::new() 116 | .bind(address.as_str())? 117 | .register_service(service); 118 | 119 | Ok((server, shutdown_notification)) 120 | } 121 | 122 | fn client() -> Result { 123 | use nix::sys::socket::*; 124 | 125 | let socket = socket( 126 | AddressFamily::Unix, 127 | SockType::Stream, 128 | SockFlag::empty(), 129 | None, 130 | )?; 131 | 132 | let sockaddr = UnixAddr::new(server_address()?.path().as_bytes())?; 133 | let sockaddr = SockAddr::Unix(sockaddr); 134 | 135 | let base: u32 = 2; 136 | let mut attempt: u32 = 1; 137 | let mut result; 138 | loop { 139 | result = connect(socket, &sockaddr); 140 | 141 | if result.is_err() && attempt < CONNECTION_RETRY_ATTEMPTS { 142 | let interval = time::Duration::from_secs(base.pow(attempt) as _); 143 | thread::sleep(interval); 144 | attempt = attempt + 1; 145 | } else { 146 | break; 147 | } 148 | } 149 | 150 | result?; 151 | 152 | let client = Client::new(socket); 153 | 154 | Ok(TaskClient::new(client)) 155 | } 156 | 157 | fn storage() -> TestStorage { 158 | let home = std::env::var("HOME").unwrap(); 159 | TestStorage::new(home).unwrap() 160 | } 161 | 162 | fn setup_logging() -> tracing_appender::non_blocking::WorkerGuard { 163 | let file_appender = 164 | tracing_appender::rolling::never("/var/log", "knast.log"); 165 | let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); 166 | tracing_subscriber::fmt().with_writer(non_blocking).init(); 167 | 168 | guard 169 | } 170 | 171 | /// Returns command and container id 172 | fn parse_opts() -> (String, String) { 173 | // Spike: this relies on arguments order. 174 | let mut args = std::env::args().rev(); 175 | let command = args.next().expect("COMMAND is required"); 176 | 177 | let id = if command == "start" { 178 | args.next() 179 | } else { 180 | args.next(); 181 | args.next(); 182 | args.next() 183 | } 184 | .expect("ID is required"); 185 | 186 | (command, id) 187 | } 188 | 189 | // TODO: this should be static variable. 190 | fn server_address() -> Result { 191 | let address = url::Url::parse("unix:///tmp/knast.sock")?; 192 | 193 | Ok(address) 194 | } 195 | -------------------------------------------------------------------------------- /containerd-shim/src/protocols.rs: -------------------------------------------------------------------------------- 1 | pub mod empty; 2 | pub mod mount; 3 | pub mod shim; 4 | pub mod shim_ttrpc; 5 | pub mod task; 6 | -------------------------------------------------------------------------------- /docs/quicklinks.org: -------------------------------------------------------------------------------- 1 | * Restricting mounted devfs nodes using rulesets: 2 | 3 | #+BEGIN_SRC sh 4 | mount -o ruleset=5 -t devfs devfs /path/to/jail 5 | #+END_SRC 6 | 7 | 5 is default ruleset for vnet jails. See /etc/defaults/devfs.rules 8 | 9 | * VNET jails 10 | 11 | - [[https://issue.freebsdfoundation.org/publication/?m=33057&i=651491&p=23&ver=html5][FreeBSD Journal Entry]] 12 | 13 | Firewall configuration 14 | 15 | #+BEGIN_SRC pf 16 | ext_if="lagg0" 17 | 18 | table persist 19 | nat on $ext_if from to any -> ($ext_if:0) 20 | #+END_SRC 21 | 22 | Add jails network to table. 23 | 24 | #+BEGIN_SRC sh 25 | pfctl -t jails -T add 10.0.0.0/24 26 | #+END_SRC 27 | 28 | And please never ever forget following 29 | 30 | #+BEGIN_SRC sh 31 | sysctl net.inet.ip.forwarding=1 32 | #+END_SRC 33 | 34 | * PF firewall 35 | 36 | pfctl -N -f /etc/pf.conf 37 | -------------------------------------------------------------------------------- /libknast/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libknast" 3 | version = "0.1.0" 4 | authors = ["Artem Khramov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | anyhow = "1" 9 | baustelle = { path = "../baustelle" } 10 | common_lib = { path = "../common_lib" } 11 | fehler = "1" 12 | jail = { git = "https://github.com/fubarnetes/libjail-rs", branch = "dev" } 13 | libc = "0.2.71" 14 | netzwerk = { path = "../netzwerk" } 15 | nix = "0.20.0" 16 | serde = "1" 17 | serde_json = "1" 18 | storage = { path = "../storage" } 19 | strum_macros = "0.20.1" 20 | tracing = "0.1.25" 21 | 22 | [dev-dependencies] 23 | gag = "0.1.10" 24 | test_helpers = { path = "../test_helpers" } 25 | tempfile = "3.1.0" -------------------------------------------------------------------------------- /libknast/src/filesystem.rs: -------------------------------------------------------------------------------- 1 | /// Handles mounting and unmounting of filesystems. 2 | /// Some filesystems require additional actions on mount, 3 | /// i.e. devfs nodes need to be hidden using the rule 4 | /// subsystem, and so on. 5 | mod devfs; 6 | mod mount; 7 | 8 | use std::{ 9 | convert::AsRef, 10 | path::{Component, Path, PathBuf}, 11 | }; 12 | 13 | use anyhow::Error; 14 | 15 | use baustelle::runtime_config::Mount; 16 | 17 | pub trait Mountable { 18 | #[fehler::throws] 19 | fn mount(&self, rootfs: impl AsRef) { 20 | let kind = self.kind(); 21 | let source = self.source(); 22 | let destination = prefixed_destination(&rootfs, self.destination()); 23 | 24 | tracing::info!( 25 | "Mounting {} fs {:?} -> {:?}", 26 | kind, 27 | source, 28 | destination 29 | ); 30 | mount::mount( 31 | kind, 32 | source, 33 | &destination, 34 | self.options().iter().map(|x| x as &dyn AsRef), 35 | )?; 36 | 37 | self.post_mount_hooks(rootfs)?; 38 | } 39 | 40 | #[fehler::throws] 41 | fn unmount(&self, rootfs: impl AsRef) { 42 | mount::unmount(&prefixed_destination(rootfs, self.destination()))?; 43 | } 44 | 45 | fn post_mount_hooks(&self, rootfs: impl AsRef) -> Result<(), Error>; 46 | 47 | fn kind(&self) -> &String; 48 | fn source(&self) -> &String; 49 | fn destination(&self) -> &String; 50 | fn options(&self) -> Vec; 51 | } 52 | 53 | impl Mountable for Mount { 54 | fn kind(&self) -> &String { 55 | &self.r#type 56 | } 57 | 58 | fn source(&self) -> &String { 59 | if let Some(source) = &self.source { 60 | return &source; 61 | } 62 | 63 | self.kind() 64 | } 65 | 66 | fn destination(&self) -> &String { 67 | &self.destination 68 | } 69 | 70 | fn options(&self) -> Vec { 71 | self.options.clone().unwrap_or_else(|| vec![]) 72 | } 73 | 74 | #[fehler::throws] 75 | fn post_mount_hooks(&self, rootfs: impl AsRef) { 76 | if self.r#type == "devfs" { 77 | prepare_devfs(&prefixed_destination(rootfs, self.destination()))?; 78 | } 79 | } 80 | } 81 | 82 | /// There's no FreeBSD spec yet, so follow Linux config as 83 | /// possible https://git.io/JOQal 84 | #[fehler::throws] 85 | fn prepare_devfs(path: impl AsRef) { 86 | use devfs::{apply, Operation}; 87 | 88 | const DEFAULT_DEVICES: [&str; 10] = [ 89 | "null", "zero", "full", "random", "urandom", "tty", "console", "pts", 90 | "pts/*", "fd", 91 | ]; 92 | 93 | apply(&path, Operation::HideAll)?; 94 | 95 | for device in &DEFAULT_DEVICES { 96 | apply(&path, Operation::Unhide(device))? 97 | } 98 | } 99 | 100 | /// For args, cwd, and mountpoints runtime config specifies 101 | /// paths inside containers Therefore, we need to prefix 102 | /// these paths with the rootfs of the container. 103 | pub fn prefixed_destination( 104 | rootfs: impl AsRef, 105 | destination: impl AsRef, 106 | ) -> PathBuf { 107 | let mut result = rootfs.as_ref().to_owned(); 108 | 109 | for component in destination.as_ref().components() { 110 | // Sanitization: we don't want "..", "." or "/" here 111 | if let Component::Normal(component) = component { 112 | result.push(component); 113 | } 114 | } 115 | 116 | result 117 | } 118 | 119 | #[cfg(test)] 120 | mod tests { 121 | use std::process::Command; 122 | 123 | use super::*; 124 | 125 | #[test] 126 | fn test_mounting_nullfs() { 127 | let source = tempfile::tempdir().unwrap(); 128 | let destination = tempfile::tempdir().unwrap(); 129 | 130 | let rootfs = destination.path(); 131 | let src = source.path().to_str().unwrap().into(); 132 | 133 | let mount = Mount { 134 | destination: "/".into(), 135 | source: Some(src), 136 | options: None, 137 | r#type: "nullfs".into(), 138 | }; 139 | 140 | mount.mount(rootfs).expect("failed to mount nullfs"); 141 | 142 | let mount_output = Command::new("/sbin/mount") 143 | .output() 144 | .expect("Failed to execute mount"); 145 | 146 | let output_string = String::from_utf8(mount_output.stdout).unwrap(); 147 | 148 | assert!(output_string.contains(&format!( 149 | "{} on {} (nullfs", 150 | source.path().display(), 151 | rootfs.display() 152 | ))); 153 | 154 | mount.unmount(rootfs).expect("failed to unmount nullfs"); 155 | } 156 | 157 | #[test] 158 | fn test_mounting_devfs() { 159 | let destination = tempfile::tempdir().unwrap(); 160 | 161 | let rootfs = destination.path(); 162 | 163 | let mount = Mount { 164 | destination: "/".into(), 165 | source: None, 166 | options: None, 167 | r#type: "devfs".into(), 168 | }; 169 | 170 | mount.mount(rootfs).expect("failed to mount devfs"); 171 | 172 | let entries = std::fs::read_dir(rootfs) 173 | .expect("failed to read the mounted directory") 174 | .map(|res| res.map(|e| e.file_name())) 175 | .collect::, std::io::Error>>() 176 | .expect("failed to read a filename in the mounted directory"); 177 | 178 | mount.unmount(rootfs).expect("failed to unmount devfs"); 179 | assert_eq!( 180 | entries, 181 | vec![ 182 | "random", "urandom", "console", "full", "null", "zero", "fd", 183 | "pts" 184 | ] 185 | ); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /libknast/src/filesystem/devfs.rs: -------------------------------------------------------------------------------- 1 | /// During OCI runtime creation, we need to mount 2 | /// directories, in particular /dev. However, giving the 3 | /// container access to all device nodes is dangerous. The 4 | /// idiomatic tool for this is devfs control facilities, 5 | /// which allows us to dynamically hide & unhide device 6 | /// nodes from the mounted subsystem. 7 | /// This module replicates devfs(8) behavior to hide devfs 8 | /// nodes from the jail. 9 | use std::{ 10 | convert::AsRef, fs::File, io::Error as StdError, mem, 11 | os::unix::io::AsRawFd, path::Path, 12 | }; 13 | 14 | use anyhow::{anyhow, Error}; 15 | use common_lib::AsSignedBytes; 16 | use libc::{c_char, c_int, gid_t, ioctl, mode_t, uid_t}; 17 | 18 | const MAGIC: u32 = 0xdb0a087a; 19 | const DRA_BACTS: c_int = 0x1; 20 | const DRB_HIDE: c_int = 0x1; 21 | const DRB_UNHIDE: c_int = 0x2; 22 | const DRC_PATHPTRN: c_int = 0x2; 23 | const DEVFSIO_RAPPLY: u64 = 0x80ec4402; 24 | 25 | #[repr(C)] 26 | struct DevfsRule { 27 | magic: u32, 28 | id: u32, 29 | icond: c_int, 30 | dswflags: c_int, 31 | pathptrn: [c_char; 200], 32 | iacts: c_int, 33 | bacts: c_int, 34 | uid: uid_t, 35 | gid: gid_t, 36 | mode: mode_t, 37 | incset: u32, 38 | } 39 | 40 | pub enum Operation<'a> { 41 | HideAll, 42 | Unhide(&'a str), 43 | } 44 | 45 | #[fehler::throws] 46 | pub fn apply(path: impl AsRef, operation: Operation) { 47 | let file = File::open(path.as_ref())?; 48 | let mut rule: DevfsRule = unsafe { mem::zeroed() }; 49 | rule.magic = MAGIC; 50 | rule.iacts = DRA_BACTS; 51 | 52 | match operation { 53 | Operation::HideAll => { 54 | rule.bacts = DRB_HIDE; 55 | } 56 | Operation::Unhide(node) => { 57 | rule.bacts = DRB_UNHIDE; 58 | rule.icond = DRC_PATHPTRN; 59 | rule.pathptrn[0..node.len()] 60 | .copy_from_slice(node.as_signed_bytes()); 61 | } 62 | } 63 | 64 | if unsafe { ioctl(file.as_raw_fd(), DEVFSIO_RAPPLY, &rule) } < 0 { 65 | fehler::throw!(anyhow!( 66 | "devfs rule: ioctl(DEVFSIO_RAPPLY) failed: {}", 67 | StdError::last_os_error() 68 | )) 69 | }; 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use super::*; 75 | 76 | use crate::filesystem::mount::{mount, unmount}; 77 | 78 | struct MountedDirectory<'a> { 79 | path: &'a Path, 80 | } 81 | 82 | impl<'a> MountedDirectory<'a> { 83 | fn new(path: &'a Path) -> Self { 84 | mount(&"devfs", &"devfs", &path, std::iter::empty()) 85 | .expect("failed to mount directory"); 86 | 87 | Self { path } 88 | } 89 | } 90 | 91 | impl<'a> Drop for MountedDirectory<'a> { 92 | fn drop(&mut self) { 93 | unmount(&self.path).expect("failed to unmount directory"); 94 | } 95 | } 96 | 97 | #[test] 98 | fn test_device_unhide() { 99 | let tmpdir = tempfile::tempdir().unwrap(); 100 | 101 | let _directory = MountedDirectory::new(tmpdir.path()); 102 | 103 | assert!( 104 | tmpdir.path().join("null").exists(), 105 | "/dev/null must be present" 106 | ); 107 | 108 | apply(tmpdir.path(), Operation::HideAll) 109 | .expect("Failed to hide all nodes"); 110 | 111 | assert!( 112 | !tmpdir.path().join("null").exists(), 113 | "hide all hides /dev/null" 114 | ); 115 | 116 | apply(tmpdir.path(), Operation::Unhide("null")) 117 | .expect("Failed to unhide /dev/null"); 118 | 119 | assert!( 120 | tmpdir.path().join("null").exists(), 121 | "unhide(null) unhides /dev/null" 122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /libknast/src/filesystem/mount.rs: -------------------------------------------------------------------------------- 1 | /// Bindings around mount and umount(2) syscalls. 2 | use std::{convert::AsRef, io::Error as StdError, io::IoSlice, path::Path}; 3 | 4 | use anyhow::{anyhow, Error}; 5 | 6 | #[fehler::throws] 7 | pub fn mount<'a>( 8 | kind: &dyn AsRef, 9 | source: &dyn AsRef, 10 | destination: &dyn AsRef, 11 | options: impl Iterator>, 12 | ) { 13 | let kind = kind.as_bytes()?; 14 | let source = source.as_bytes()?; 15 | let destination = destination.as_bytes()?; 16 | let options: Vec<_> = options 17 | .flat_map(|option| { 18 | let mut split = option.as_ref().split("="); 19 | let key = [split.next().unwrap_or("").as_bytes(), b"\0"].concat(); 20 | let value = split 21 | .next() 22 | .map(|item| [item.as_bytes(), b"\0"].concat()) 23 | .unwrap_or(vec![]); 24 | 25 | vec![key, value] 26 | }) 27 | .collect(); 28 | 29 | let iovecs: Vec<_> = options 30 | .iter() 31 | .map(|x| IoSlice::new(x)) 32 | .chain( 33 | vec![ 34 | IoSlice::new(b"fstype\0"), 35 | IoSlice::new(kind.as_slice()), 36 | IoSlice::new(b"fspath\0"), 37 | IoSlice::new(destination.as_slice()), 38 | IoSlice::new(b"from\0"), 39 | IoSlice::new(source.as_slice()), 40 | IoSlice::new(b"errmsg\0"), 41 | IoSlice::new(&[0; 255]), 42 | ] 43 | .into_iter(), 44 | ) 45 | .collect(); 46 | 47 | let slice = iovecs.as_slice(); 48 | 49 | if unsafe { libc::nmount(slice as *const _ as _, iovecs.len() as _, 0) } 50 | < 0 51 | { 52 | fehler::throw!(anyhow!( 53 | "mount: nmount failed: {}", 54 | StdError::last_os_error() 55 | )) 56 | }; 57 | } 58 | 59 | #[fehler::throws] 60 | pub fn unmount(destination: &dyn AsRef) { 61 | if unsafe { 62 | libc::unmount( 63 | destination.as_bytes()?.as_slice() as *const _ as _, 64 | libc::MNT_FORCE, 65 | ) 66 | } < 0 67 | { 68 | fehler::throw!(anyhow!( 69 | "mount: unmount failed: {}", 70 | StdError::last_os_error(), 71 | )) 72 | } 73 | } 74 | 75 | trait AsBytes { 76 | #[fehler::throws] 77 | fn as_bytes(&self) -> Vec; 78 | } 79 | 80 | impl AsBytes for &dyn AsRef { 81 | // TODO: too complex. Is there a better way? 82 | #[fehler::throws] 83 | fn as_bytes(&self) -> Vec { 84 | use std::{ffi::CString, ffi::OsStr, os::unix::ffi::OsStrExt}; 85 | 86 | let path: &Path = self.as_ref(); 87 | let os_str: &OsStr = path.as_ref(); 88 | CString::new(os_str.as_bytes())?.into_bytes_with_nul() 89 | } 90 | } 91 | 92 | #[cfg(test)] 93 | mod tests { 94 | use std::process::Command; 95 | 96 | use super::*; 97 | 98 | #[test] 99 | fn test_mounting_nullfs() { 100 | let source = tempfile::tempdir().unwrap(); 101 | let dest = tempfile::tempdir().unwrap(); 102 | 103 | mount(&"nullfs", &source.path(), &dest.path(), std::iter::empty()) 104 | .expect("failed to mount nullfs"); 105 | 106 | let mount_output = Command::new("mount") 107 | .output() 108 | .expect("Failed to execute mount"); 109 | 110 | let output_string = String::from_utf8(mount_output.stdout).unwrap(); 111 | 112 | assert!(output_string.contains(&format!( 113 | "{} on {} (nullfs", 114 | source.path().display(), 115 | dest.path().display() 116 | ))); 117 | 118 | unmount(&dest.path()).expect("failed to unmount nullfs"); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /libknast/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod filesystem; 2 | pub mod operations; 3 | -------------------------------------------------------------------------------- /libknast/src/operations/command_ext.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::Error, os::unix::process::CommandExt as StdCommandExt, 3 | process::Command, 4 | }; 5 | 6 | use libc::{setuid, uid_t}; 7 | 8 | // A workaround for https://github.com/fubarnetes/libjail-rs/issues/103 9 | pub trait CommandExt { 10 | fn uid(&mut self, uid: u32) -> &mut Command; 11 | fn gid(&mut self, gid: u32) -> &mut Command; 12 | } 13 | 14 | impl CommandExt for Command { 15 | fn uid(&mut self, uid: u32) -> &mut Command { 16 | unsafe { 17 | self.pre_exec(move || { 18 | if setuid(uid as uid_t) < 0 { 19 | return Err(Error::last_os_error()); 20 | } 21 | 22 | Ok(()) 23 | }); 24 | } 25 | 26 | self 27 | } 28 | 29 | fn gid(&mut self, gid: u32) -> &mut Command { 30 | StdCommandExt::gid(self, gid) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libknast/src/operations/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{BufRead, BufReader, Write}, 3 | os::unix::net::UnixStream, 4 | }; 5 | 6 | use anyhow::{anyhow, Error}; 7 | use nix::{ 8 | sys::wait::{waitpid, WaitStatus}, 9 | unistd::{fork, ForkResult}, 10 | }; 11 | use serde::{de::DeserializeOwned, ser::Serialize}; 12 | 13 | /// Executes closure in a forked process 14 | pub fn run_in_fork( 15 | f: impl FnOnce() -> Result, 16 | ) -> Result { 17 | let (read, mut write) = UnixStream::pair()?; 18 | 19 | match unsafe { fork() } { 20 | Ok(ForkResult::Child) => { 21 | let result = f().map_err(|err| err.to_string()); 22 | let result = serde_json::to_string(&result) 23 | .map_err(Error::from) 24 | .and_then(|string| { 25 | write.write_all(string.as_bytes())?; 26 | write.write(b"\n")?; 27 | Ok(()) 28 | }); 29 | 30 | let status = match result { 31 | Ok(_) => 0, 32 | Err(err) => { 33 | tracing::error!("run_in_fork failed: {:?}", err); 34 | 35 | 15 36 | } 37 | }; 38 | 39 | std::process::exit(status); 40 | } 41 | Ok(ForkResult::Parent { child }) => { 42 | let status = waitpid(child, None)?; 43 | 44 | match status { 45 | WaitStatus::Exited(_, 0) => { 46 | let mut string = String::new(); 47 | 48 | BufReader::new(read).read_line(&mut string)?; 49 | 50 | let result: Result = 51 | serde_json::from_str(&string)?; 52 | 53 | return result.map_err(|err| anyhow!(err)); 54 | } 55 | WaitStatus::Exited(_, 15) => { 56 | anyhow::bail!( 57 | "Forked process failed unexpectedly. Check logs" 58 | ); 59 | } 60 | status => { 61 | anyhow::bail!("unexpected status {:?}", status); 62 | } 63 | } 64 | } 65 | Err(err) => { 66 | anyhow::bail!(err) 67 | } 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /libknast/test/resources/commands_output/env: -------------------------------------------------------------------------------- 1 | SETTING=env_var 2 | WORKED=well 3 | -------------------------------------------------------------------------------- /libknast/test/resources/commands_output/id: -------------------------------------------------------------------------------- 1 | uid=1000 gid=1000(akhramov) groups=1000(akhramov),44(video) 2 | -------------------------------------------------------------------------------- /libknast/test/resources/commands_output/mount: -------------------------------------------------------------------------------- 1 | zroot/tmp on / (zfs, local, noatime, nosuid, nfsv4acls) 2 | devfs on /dev (devfs) 3 | fdescfs on /dev/fd (fdescfs) 4 | linprocfs on /proc (linprocfs, local, noexec, nosuid, read-only) 5 | linsysfs on /sys (linsysfs, local, noexec, nosuid, read-only) 6 | -------------------------------------------------------------------------------- /libknast/test/resources/commands_output/trapster_sigbus: -------------------------------------------------------------------------------- 1 | BUS received. 2 | -------------------------------------------------------------------------------- /libknast/test/resources/container/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ociVersion": "1.0", 3 | "root": { 4 | "path": "./rootfs", 5 | "readonly": false 6 | }, 7 | "mounts": [ 8 | { 9 | "destination": "/dev", 10 | "source": "devfs", 11 | "options": null, 12 | "type": "devfs" 13 | }, 14 | { 15 | "destination": "/sys", 16 | "source": "linsysfs", 17 | "options": [ 18 | "nosuid", 19 | "noexec", 20 | "ro" 21 | ], 22 | "type": "linsysfs" 23 | }, 24 | { 25 | "destination": "/proc", 26 | "source": "linprocfs", 27 | "options": [ 28 | "nosuid", 29 | "noexec", 30 | "ro" 31 | ], 32 | "type": "linprocfs" 33 | }, 34 | { 35 | "destination": "/dev/fd", 36 | "source": "fdescfs", 37 | "options": [ 38 | "linrdlnk" 39 | ], 40 | "type": "fdescfs" 41 | } 42 | ], 43 | "process": { 44 | "terminal": null, 45 | "consoleSize": null, 46 | "cwd": "/", 47 | "env": [ 48 | "SETTING=env_var", 49 | "WORKED=well" 50 | ], 51 | "args": [ 52 | "sleep", 53 | "1000" 54 | ], 55 | "rlimits": null, 56 | "user": { 57 | "uid": 1000, 58 | "gid": 1000, 59 | "umask": null, 60 | "additionalGids": null 61 | }, 62 | "hostname": null 63 | }, 64 | "hooks": null, 65 | "annotations": { 66 | "io.container.manager": "werft", 67 | "org.opencontainers.image.stopSignal": "15" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/COPYING: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8b5f7851c28f42604349e2f301f033877b5a3fbea1318af6cd32dba0eaced03e 3 | size 6253 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/bin/env: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ff80f1cf9c5d7fe99f1d5e47b06ca71466c7f7f153d16b7c6aa2bf8d81db0056 3 | size 14976 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/bin/id: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f3e661a1e8e45a567c6e1603d5938852f9fef3150b3d8cabec12486ce96afa55 3 | size 13536 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/bin/ls: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:51978eebba6d9dc4dff96872a13e69b7b4282aa392e000f686371c9725fa3a25 3 | size 33560 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/bin/mount: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9be9e4ed35cfdfbbf0f06af9b7b15b21d6d11b8de7adabeb4324568bf88bf71e 3 | size 26304 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/bin/quitely_stop.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhramov/knast/8c9f5f481a467a22c7cc47f11a28fe52536f9950/libknast/test/resources/container/rootfs/bin/quitely_stop.sh -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/bin/sh: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e59ee2e313541b8fe869b8e8f8d79aeab103caffdf2a6c54669dd53b5b1dff99 3 | size 162616 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/bin/sleep: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e66256257ac757b16dd723376351ecbebe21bb92c103fd74986e5fb0fd65c63c 3 | size 7696 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/bin/trapster.sh: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f860536b8cc1caef4b4461ce640eadc5fd0cf54101c1d19deeb99cd2bae6f70d 3 | size 146 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/etc/group: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d9ea7b7d00cfd30b5af9c6016e3607aa94d8c63c56f4b05e0699a110458e93f9 3 | size 77 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/etc/master.passwd: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a705f7e04b6fcb5dc83bb0bee338baaead4ee73893f8fba758fa78565914da48 3 | size 101 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/etc/nsswitch.conf: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f83fe8650e3632c87e9136d91b864c678cd3a966ff2ccd6e4a336f55e85f98b1 3 | size 272 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/etc/passwd: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a34c0b9b1d10a93b5394253ac1d453767af43640c7064a9a0bbab9272bed7c12 3 | size 91 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/etc/pwd.db: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:bf04fd4210d64c2a9b130961a94764c57b742e66e0b6f05583003ea180dcb1ef 3 | size 40960 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/etc/spwd.db: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:bf04fd4210d64c2a9b130961a94764c57b742e66e0b6f05583003ea180dcb1ef 3 | size 40960 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/lib/libc.so.7: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:0e04dd44e7eb18631ea5b3180bd6e9c1e79c3edea885e599a737df341cd7b7c2 3 | size 1982072 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/lib/libedit.so.8: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4a8917f8b7c16cb1229af34595717cd3aa0e49096e47698925e8eaabdad00914 3 | size 215928 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/lib/libgcc_s.so.1: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1e790e473b4ef3cf23461699982e8341ba0926cc403a0c553e6775326d94f729 3 | size 93864 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/lib/libm.so.5: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:562d7ee2b6f56e67a07211ad10cd593afc77054cc90fe95ce4b7d64be2688f0e 3 | size 195696 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/lib/libncursesw.so.9: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d7f3c5d8bf7f0471b56bd60cbcf87224f50c59e3f708bb21fa468e91ce117dc4 3 | size 459464 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/lib/libthr.so.3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4b4d58d75329fcff90b338542d83c99207fb6e63813edcfd6d373ad8c5627e90 3 | size 125712 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/lib/libutil.so.9: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:330d8064849caf268b60908400a1a147e81524597401f093a65d1cd29c673cf4 3 | size 79384 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/libexec/COPYING: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2a7ff63cc010c0b9811780638b556084e71a07c7bead4e2520bb46d4efab9df7 3 | size 6232 4 | -------------------------------------------------------------------------------- /libknast/test/resources/container/rootfs/libexec/ld-elf.so.1: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1140fc3f9e89bb23328fe08aa0c1709e82223c52915e9db75e2a73d11867081c 3 | size 114832 4 | -------------------------------------------------------------------------------- /netzwerk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "netzwerk" 3 | version = "0.1.0" 4 | authors = ["akhramov"] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | anyhow = "1.0" 9 | common_lib = { path = "../common_lib" } 10 | fehler = "1.0" 11 | libc = "0.2.71" 12 | ipnetwork = "0.18.0" 13 | 14 | [build-dependencies] 15 | bindgen = "0.57" 16 | 17 | [dev-dependencies] 18 | test_helpers = { path = "../test_helpers" } 19 | jail = { git = "https://github.com/fubarnetes/libjail-rs", branch = "dev" } 20 | nix = "0.20.0" 21 | -------------------------------------------------------------------------------- /netzwerk/build.rs: -------------------------------------------------------------------------------- 1 | use bindgen::builder; 2 | 3 | fn main() { 4 | let bindings = builder() 5 | .header("ffi/ffi.h") 6 | .generate() 7 | .expect("failed to generate bindings"); 8 | 9 | bindings 10 | .write_to_file("src/bindings.rs") 11 | .expect("failed to write bindings on disk"); 12 | } 13 | -------------------------------------------------------------------------------- /netzwerk/examples/bridge_jail.rs: -------------------------------------------------------------------------------- 1 | // Set up network inside a jail: 2 | // 3 | // * Create a bridge and an epair 4 | // * Assign an ip address to the jail 5 | // * Move one edge of the epair into the jail 6 | // * Give that edge an ip address 7 | // * Configure default route inside the jail 8 | extern crate anyhow; 9 | extern crate netzwerk; 10 | extern crate nix; 11 | 12 | use anyhow::Result; 13 | use netzwerk::interface::Interface; 14 | use netzwerk::route; 15 | use nix::unistd::{fork, ForkResult}; 16 | 17 | extern "C" { 18 | fn jail_attach(jid: i32) -> i32; 19 | } 20 | 21 | fn create_interfaces(jail_number: i32) -> Result { 22 | let bridge = Interface::new("bridge") 23 | .expect("Failed to create iface socket") 24 | .create()? 25 | .name("knast0")?; 26 | 27 | let pair_a = Interface::new("epair")?.create()?.address( 28 | "172.24.0.1", 29 | "172.24.0.255", 30 | "255.255.255.0", 31 | )?; 32 | 33 | let name = pair_a.get_name()?; 34 | let len = name.len(); 35 | let name_b = &[&name[..len - 1], "b"].join(""); 36 | 37 | let pair_b = 38 | Interface::new(name_b).expect("Failed to create iface socket"); 39 | 40 | pair_b 41 | .vnet(jail_number) /* Transfer interface to the jail */ 42 | .expect("Failed to move interface to the jail"); 43 | 44 | bridge.bridge_addm(&[name])?; 45 | 46 | Ok(String::from(name_b)) 47 | } 48 | 49 | fn main() { 50 | let jid = std::env::args() 51 | .nth(1) 52 | .expect("USAGE: bridge_jail JID") 53 | .parse() 54 | .expect("Failed to parse jail id"); 55 | 56 | let name = create_interfaces(jid).expect("Failed to create interfaces"); 57 | 58 | match unsafe { fork() } { 59 | Ok(ForkResult::Child) => { 60 | if unsafe { jail_attach(jid) } < 0 { 61 | panic!("Failed to attach to jail {}", jid); 62 | }; 63 | 64 | let pair_b = 65 | Interface::new(&name).expect("Failed to create iface socket"); 66 | 67 | pair_b 68 | .address("172.24.0.2", "172.24.0.255", "255.255.255.0") 69 | .unwrap(); 70 | route::add_default("172.24.0.1").unwrap(); 71 | } 72 | _ => (), 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /netzwerk/ffi/ffi.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | -------------------------------------------------------------------------------- /netzwerk/src/common_bindings.rs: -------------------------------------------------------------------------------- 1 | use std::io::Error as StdError; 2 | use std::mem; 3 | 4 | use anyhow::{anyhow, Error}; 5 | use libc::{c_int, c_void, close, sockaddr_in, socket, AF_INET}; 6 | 7 | extern "C" { 8 | fn inet_pton(af: i32, src: *const u8, dst: *mut c_void) -> i32; 9 | } 10 | 11 | pub struct Socket(pub i32); 12 | 13 | impl Socket { 14 | #[fehler::throws] 15 | pub fn new(domain: c_int, r#type: c_int) -> Self { 16 | let sock = unsafe { socket(domain, r#type, 0) }; 17 | 18 | if sock < 0 { 19 | fehler::throw!(anyhow!( 20 | "cannot open socket: {}", 21 | StdError::last_os_error() 22 | )) 23 | } 24 | 25 | Self(sock) 26 | } 27 | } 28 | 29 | impl Drop for Socket { 30 | fn drop(&mut self) { 31 | unsafe { close(self.0) }; 32 | } 33 | } 34 | 35 | #[fehler::throws] 36 | pub fn get_address(address: Option<&str>) -> sockaddr_in { 37 | let mut result: sockaddr_in = unsafe { mem::zeroed() }; 38 | 39 | result.sin_len = mem::size_of::() as u8; 40 | result.sin_family = AF_INET as u8; 41 | 42 | let address = match address { 43 | Some(add) => add, 44 | None => return result, 45 | }; 46 | 47 | match unsafe { 48 | inet_pton( 49 | AF_INET, 50 | [address, "\0"].concat().as_ptr(), 51 | &mut result.sin_addr as *mut _ as *mut c_void, 52 | ) 53 | } { 54 | 0 => { 55 | fehler::throw!(anyhow!( 56 | "inet_pton failed: could not parse inet address" 57 | )) 58 | } 59 | -1 => { 60 | fehler::throw!(anyhow!( 61 | "inet_pton failed: {}", 62 | StdError::last_os_error() 63 | )) 64 | } 65 | _ => result, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /netzwerk/src/interface/operations.rs: -------------------------------------------------------------------------------- 1 | use std::io::Error as StdError; 2 | use std::mem; 3 | 4 | use anyhow::{anyhow, Error}; 5 | use common_lib::AsSignedBytes; 6 | use libc::ioctl; 7 | 8 | use crate::{ 9 | bindings::{ifaliasreq, ifbreq, ifdrv, ifreq}, 10 | common_bindings::{get_address, Socket}, 11 | }; 12 | 13 | // FreeBSD 13.0-CURRENT r361779 14 | const SIOCAIFADDR: u64 = 0x8044692b; 15 | const SIOCIFCREATE: u64 = 0xc020697a; 16 | const SIOCSIFNAME: u64 = 0x80206928; 17 | const SIOCIFDESTROY: u64 = 0x80206979; 18 | const SIOCSDRVSPEC: u64 = 0x8028697b; 19 | const SIOCSIFVNET: u64 = 0xc020695a; 20 | const SIOCGIFCAP: u64 = 0xc020691f; 21 | 22 | const BRDGADD: u64 = 0x0; 23 | const BRDGDEL: u64 = 0x1; 24 | 25 | #[fehler::throws] 26 | pub fn destroy_interface(socket: &Socket, request: &ifreq) { 27 | if unsafe { ioctl(socket.0, SIOCIFDESTROY, request) } < 0 { 28 | fehler::throw!(anyhow!( 29 | "destroy interface: ioctl(SIOCIFDESTROY) failed: {}", 30 | StdError::last_os_error() 31 | )) 32 | }; 33 | } 34 | 35 | #[fehler::throws] 36 | pub fn create_interface(socket: &Socket, request: &ifreq) { 37 | if unsafe { ioctl(socket.0, SIOCIFCREATE, request) } < 0 { 38 | fehler::throw!(anyhow!( 39 | "create interface: ioctl(SIOCIFCREATE) failed: {}", 40 | StdError::last_os_error() 41 | )) 42 | }; 43 | } 44 | 45 | #[fehler::throws] 46 | pub fn rename_interface(socket: &Socket, request: &mut ifreq, name: &str) { 47 | let new_name = [name, "\0"].concat(); 48 | request.ifr_ifru.ifru_data = new_name.as_ptr() as *mut _; 49 | 50 | { 51 | if unsafe { ioctl(socket.0, SIOCSIFNAME, request as *mut _) } < 0 { 52 | fehler::throw!(anyhow!( 53 | "rename interface: ioctl(SIOCSIFNAME) failed: {}", 54 | StdError::last_os_error() 55 | )) 56 | }; 57 | } 58 | 59 | request.ifr_name[0..new_name.len()] 60 | .copy_from_slice(new_name.as_str().as_signed_bytes()); 61 | } 62 | 63 | #[fehler::throws] 64 | pub fn jail_interface(socket: &Socket, request: &mut ifreq, jid: i32) { 65 | request.ifr_ifru.ifru_jid = jid; 66 | 67 | if unsafe { ioctl(socket.0, SIOCSIFVNET, request as *mut _) } < 0 { 68 | fehler::throw!(anyhow!( 69 | "jail interface: ioctl(SIOCSIFVNET) failed: {}", 70 | StdError::last_os_error() 71 | )) 72 | }; 73 | } 74 | 75 | #[fehler::throws] 76 | pub fn set_interface_address( 77 | socket: &Socket, 78 | name: &[i8], 79 | address: &str, 80 | broadcast: &str, 81 | mask: &str, 82 | ) { 83 | let mut request: ifaliasreq = unsafe { mem::zeroed() }; 84 | 85 | request.ifra_name[0..name.len()].copy_from_slice(name); 86 | 87 | // Safety: ifra_addr receives `sockaddr`, which is a generalization of `sockaddr_in`. 88 | unsafe { 89 | request.ifra_addr = std::mem::transmute(get_address(Some(&address))?); 90 | request.ifra_broadaddr = 91 | std::mem::transmute(get_address(Some(&broadcast))?); 92 | request.ifra_mask = std::mem::transmute(get_address(Some(&mask))?); 93 | } 94 | 95 | if unsafe { ioctl(socket.0, SIOCAIFADDR, &request) } < 0 { 96 | fehler::throw!(anyhow!( 97 | "set interface address: ioctl(SIOCAIFADDR) failed: {}", 98 | StdError::last_os_error() 99 | )) 100 | }; 101 | } 102 | 103 | #[fehler::throws] 104 | pub fn check_interface_existence(socket: &Socket, request: &ifreq) -> bool { 105 | unsafe { ioctl(socket.0, SIOCGIFCAP, request) >= 0 } 106 | } 107 | 108 | macro_rules! bridge_request { 109 | ($func:ident, $cmd:expr) => { 110 | #[fehler::throws] 111 | pub fn $func(socket: &Socket, name: &[i8], member: &str) { 112 | let mut bridge_request: ifbreq = unsafe { mem::zeroed() }; 113 | bridge_request.ifbr_ifsname[0..member.len()] 114 | .copy_from_slice(member.as_signed_bytes()); 115 | 116 | let mut request: ifdrv = unsafe { mem::zeroed() }; 117 | request.ifd_name[0..name.len()].copy_from_slice(name); 118 | request.ifd_cmd = $cmd; 119 | request.ifd_len = mem::size_of::() as _; 120 | request.ifd_data = &bridge_request as *const _ as _; 121 | 122 | if unsafe { ioctl(socket.0, SIOCSDRVSPEC, &request) } < 0 { 123 | fehler::throw!(anyhow!( 124 | "bridge request: ioctl(SIOCSDRVSPEC) failed: {}", 125 | StdError::last_os_error() 126 | )) 127 | } 128 | } 129 | }; 130 | } 131 | 132 | bridge_request!(bridge_addm, BRDGADD); 133 | bridge_request!(bridge_delm, BRDGDEL); 134 | -------------------------------------------------------------------------------- /netzwerk/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod interface; 2 | pub mod nat; 3 | pub mod pf; 4 | pub mod range; 5 | pub mod route; 6 | 7 | #[allow( 8 | non_camel_case_types, 9 | non_snake_case, 10 | dead_code, 11 | non_upper_case_globals 12 | )] 13 | mod bindings; 14 | mod common_bindings; 15 | -------------------------------------------------------------------------------- /netzwerk/src/nat.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | pub trait Nat { 4 | fn add(&self, subnet: &str) -> Result<(), Error>; 5 | } 6 | -------------------------------------------------------------------------------- /netzwerk/src/pf/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhramov/knast/8c9f5f481a467a22c7cc47f11a28fe52536f9950/netzwerk/src/pf/.gitkeep -------------------------------------------------------------------------------- /netzwerk/src/range.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::BinaryHeap, convert::AsRef, convert::TryFrom, 3 | iter::FromIterator, net::Ipv4Addr, 4 | }; 5 | 6 | use anyhow::Error; 7 | use ipnetwork::Ipv4Network; 8 | 9 | #[fehler::throws] 10 | pub fn range(range: impl AsRef) -> BinaryHeap { 11 | BinaryHeap::from_iter(&Ipv4Network::try_from(range.as_ref())?) 12 | } 13 | 14 | #[fehler::throws] 15 | pub fn broadcast(range: impl AsRef) -> Ipv4Addr { 16 | Ipv4Network::try_from(range.as_ref())?.broadcast() 17 | } 18 | 19 | #[fehler::throws] 20 | pub fn mask(range: impl AsRef) -> Ipv4Addr { 21 | Ipv4Network::try_from(range.as_ref())?.mask() 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use super::*; 27 | 28 | #[test] 29 | fn test_range() { 30 | let mut result = range("172.24.0.2/16").unwrap(); 31 | 32 | assert_eq!(result.len(), 256 * 256); 33 | assert_eq!("172.24.255.255", result.pop().unwrap().to_string()); 34 | assert_eq!("172.24.255.254", result.pop().unwrap().to_string()); 35 | } 36 | 37 | #[test] 38 | fn test_broadcast() { 39 | let result = broadcast("172.24.0.2/16").unwrap(); 40 | 41 | assert_eq!("172.24.255.255", result.to_string()); 42 | } 43 | 44 | #[test] 45 | fn test_mask() { 46 | let result = mask("172.24.0.2/16").unwrap(); 47 | 48 | assert_eq!("255.255.0.0", result.to_string()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /netzwerk/src/route.rs: -------------------------------------------------------------------------------- 1 | mod bindings; 2 | 3 | use anyhow::Error; 4 | 5 | use bindings::{rtmsg, Operation}; 6 | 7 | /// Add default route 8 | /// 9 | /// This operation may fail for several reasons, such as 10 | /// unreacheable network, default route already exists and 11 | /// so on. 12 | /// 13 | /// # Examples 14 | /// add net default 172.23.0.1 15 | /// 16 | /// ```rust,no_run 17 | /// use netzwerk::route; 18 | /// 19 | /// route::add_default("172.23.0.1") 20 | /// .expect("Add net default failed."); 21 | /// ``` 22 | #[fehler::throws] 23 | pub fn add_default(address: &str) { 24 | rtmsg(Operation::Add, Some(address))?; 25 | } 26 | 27 | /// Delete default route 28 | /// 29 | /// # Examples 30 | /// delete net default 31 | /// 32 | /// ```rust,no_run 33 | /// use netzwerk::route; 34 | /// 35 | /// route::delete_default() 36 | /// .expect("Delete net default failed"); 37 | /// ``` 38 | #[fehler::throws] 39 | pub fn delete_default() { 40 | rtmsg(Operation::Delete, None)?; 41 | } 42 | 43 | #[cfg(test)] 44 | mod test { 45 | use super::*; 46 | use std::process::Command; 47 | 48 | #[test_helpers::jailed_test] 49 | fn test_add_default() { 50 | setup_lo(); 51 | add_default("127.0.0.1").expect("failed to add default route"); 52 | 53 | let content = routing_tables_content() 54 | .expect("(netstat) failed to get routing tables content"); 55 | 56 | assert!(content.contains("default 127.0.0.1")); 57 | } 58 | 59 | #[test_helpers::jailed_test] 60 | fn test_delete_default() { 61 | setup_lo(); 62 | add_default("127.0.0.1").expect("failed to add default route"); 63 | delete_default().expect("failed to delete default route"); 64 | 65 | let content = routing_tables_content() 66 | .expect("(netstat) failed to get routing tables content"); 67 | 68 | assert!(!content.contains("default 127.0.0.1")); 69 | } 70 | 71 | #[fehler::throws] 72 | fn routing_tables_content() -> String { 73 | String::from_utf8(Command::new("netstat").arg("-rn").output()?.stdout)? 74 | } 75 | 76 | fn setup_lo() { 77 | use crate::interface::Interface; 78 | 79 | Interface::new("lo0") 80 | .expect("failed to get iface socket") 81 | .address("127.0.0.1", "127.255.255.255", "255.0.0.0") 82 | .expect("failed to assign expected address"); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /netzwerk/src/route/bindings.rs: -------------------------------------------------------------------------------- 1 | use std::io::Error as StdError; 2 | use std::mem; 3 | 4 | use anyhow::{anyhow, Error}; 5 | use libc::{sockaddr_in, write, PF_ROUTE, SOCK_RAW}; 6 | 7 | /* net/route.h */ 8 | const RTM_ADD: u8 = 0x1; 9 | const RTM_DELETE: u8 = 0x2; 10 | 11 | const RTM_VERSION: u8 = 5; 12 | 13 | const RTF_UP: u32 = 0x1; 14 | const RTF_GATEWAY: u32 = 0x2; 15 | const RTF_STATIC: u32 = 0x800; 16 | const RTF_PINNED: u32 = 0x100000; 17 | 18 | const RTA_DST: u32 = 0x1; 19 | const RTA_GATEWAY: u32 = 0x2; 20 | const RTA_NETMASK: u32 = 0x4; 21 | 22 | use crate::common_bindings::{get_address, Socket}; 23 | 24 | #[derive(Copy, Clone)] 25 | pub enum Operation { 26 | Add = RTM_ADD as _, 27 | Delete = RTM_DELETE as _, 28 | } 29 | 30 | #[fehler::throws] 31 | pub fn rtmsg(operation: Operation, address: Option<&str>) { 32 | let socket = Socket::new(PF_ROUTE, SOCK_RAW)?; 33 | 34 | let header: rt_msghdr = unsafe { mem::zeroed() }; 35 | 36 | let payload = [ 37 | get_address(None)?, 38 | get_address(address)?, 39 | get_address(None)?, 40 | ]; 41 | 42 | let mut message = rtmsg { header, payload }; 43 | 44 | message.header.rtm_type = operation as _; 45 | message.header.rtm_flags = RTF_UP | RTF_GATEWAY | RTF_STATIC | RTF_PINNED; 46 | message.header.rtm_version = RTM_VERSION; 47 | message.header.rtm_addrs = match operation { 48 | Operation::Add => RTA_DST | RTA_GATEWAY | RTA_NETMASK, 49 | Operation::Delete => RTA_DST | RTA_NETMASK, 50 | }; 51 | message.header.rtm_seq = 1; 52 | let len = mem::size_of::>(); 53 | 54 | message.header.rtm_msglen = len as _; 55 | 56 | if unsafe { write(socket.0, &message as *const _ as _, len) } < 0 { 57 | fehler::throw!(anyhow!( 58 | "add net default: write failed: {}", 59 | StdError::last_os_error() 60 | )) 61 | }; 62 | } 63 | 64 | #[repr(C)] 65 | struct rtmsg { 66 | pub header: rt_msghdr, 67 | pub payload: T, 68 | } 69 | 70 | // This makes us 64-bit only, right? 71 | #[repr(C)] 72 | struct rt_msghdr { 73 | pub rtm_msglen: u16, 74 | pub rtm_version: u8, 75 | pub rtm_type: u8, 76 | pub rtm_index: u16, 77 | _rtm_spare1: u16, 78 | pub rtm_flags: u32, 79 | pub rtm_addrs: u32, 80 | pub rtm_pid: u32, 81 | pub rtm_seq: u32, 82 | pub rtm_errno: u32, 83 | pub rtm_fmask: u32, 84 | pub rtm_inits: u64, 85 | _rt_metrics: [u64; 14usize], 86 | } 87 | -------------------------------------------------------------------------------- /netzwerk/test/resources/ifconfig: -------------------------------------------------------------------------------- 1 | knast0: flags=8843 metric 0 mtu 1500 2 | inet 172.23.0.1 netmask 0xffff0000 broadcast 172.23.255.255 3 | -------------------------------------------------------------------------------- /registratur/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | license = "BSD-4-Clause" 3 | description = "Docker Registry API client" 4 | name = "registratur" 5 | version = "0.1.0" 6 | authors = ["akhramov"] 7 | edition = "2018" 8 | 9 | [dependencies] 10 | anyhow = "1.0" 11 | async-trait = "0.1.30" 12 | chrono = { version = "0.4", features = ["serde"] } 13 | # https://github.com/withoutboats/fehler/pull/51 14 | fehler = { git = "https://github.com/withoutboats/fehler" } 15 | futures = "0.3" 16 | reqwest = { version = "0.11", features = ["json", "stream"] } 17 | hex = "0.4.2" 18 | log = "0.4" 19 | nom = "5" 20 | ring = "0.16.13" 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde_json = "1.0" 23 | url = "2.1" 24 | 25 | [dev-dependencies] 26 | serde = "1.0" 27 | test_helpers = { path = "../test_helpers" } 28 | tokio = { version = "1.1.1", features = ["macros", "rt"] } 29 | 30 | [features] 31 | integration_testing = [] -------------------------------------------------------------------------------- /registratur/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all, clippy::pedantic)] 2 | 3 | mod reqwest_ext; 4 | pub mod v2; 5 | -------------------------------------------------------------------------------- /registratur/src/reqwest_ext.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use futures::stream::TryStreamExt; 3 | use reqwest::Response; 4 | use ring::digest::{self, SHA256}; 5 | 6 | #[async_trait::async_trait] 7 | pub trait ReqwestResponseExt { 8 | /// Provides a facility to report the download progress 9 | /// and validate that the downloaded content matches 10 | /// it's hash. 11 | async fn read( 12 | self, 13 | mut f: Option, 14 | digest: Option<&str>, 15 | ) -> Result>; 16 | } 17 | 18 | #[async_trait::async_trait] 19 | impl ReqwestResponseExt for Response { 20 | async fn read( 21 | self, 22 | mut f: Option, 23 | digest: Option<&str>, 24 | ) -> Result> { 25 | let result = self 26 | .bytes_stream() 27 | .try_fold(vec![], move |mut agg, bytes| { 28 | /* https://github.com/tokio-rs/bytes/issues/ */ 29 | agg.extend(bytes); 30 | f.as_mut().map(|x| x(agg.len())); 31 | futures::future::ok(agg) 32 | }) 33 | .await?; 34 | 35 | let res = digest::digest(&SHA256, &result); 36 | 37 | if &digest.unwrap()[7..] != hex::encode(&res) { 38 | Err(anyhow!("Content hash mismatch.")) 39 | } else { 40 | Ok(result) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /registratur/src/v2.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod domain; 3 | -------------------------------------------------------------------------------- /registratur/src/v2/client/www_authenticate.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Error}; 2 | 3 | use nom::{ 4 | bytes::complete::take_while, 5 | character::complete::char, 6 | sequence::{delimited, preceded, tuple}, 7 | IResult, 8 | }; 9 | 10 | const QUOTE: char = '"'; 11 | 12 | /// Represents WWW-Authenticate header 13 | /// Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/nginx:pull" 14 | #[derive(Debug)] 15 | pub struct WwwAuthenticate<'a> { 16 | pub realm: &'a str, 17 | pub service: &'a str, 18 | pub scope: &'a str, 19 | } 20 | 21 | impl<'a> WwwAuthenticate<'a> { 22 | pub fn parse(input: &'a str) -> Result { 23 | tuple((term, term, term))(input) 24 | .map(|(_, (realm, service, scope))| Self { 25 | realm, 26 | service, 27 | scope, 28 | }) 29 | .map_err(|err| { 30 | anyhow!("Failed to parse WWW-Authenticate header: {:?}", err) 31 | }) 32 | } 33 | } 34 | 35 | pub fn term(input: &str) -> IResult<&str, &str> { 36 | delimited(preceded(string, char(QUOTE)), string, char(QUOTE))(input) 37 | } 38 | 39 | fn string(input: &str) -> IResult<&str, &str> { 40 | take_while(|c| c != QUOTE)(input) 41 | } 42 | 43 | #[cfg(test)] 44 | mod test { 45 | #[test] 46 | fn test_parsing() { 47 | let header = test_helpers::fixture!("www_authenticate"); 48 | let parsed_header = super::WwwAuthenticate::parse(header) 49 | .expect("Failed to parse WwwAuthenticate header"); 50 | 51 | assert_eq!(parsed_header.realm, "https://auth.docker.io/token"); 52 | assert_eq!(parsed_header.service, "registry.docker.io"); 53 | assert_eq!(parsed_header.scope, "repository:library/nginx:pull"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /registratur/src/v2/domain.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod descriptor; 3 | pub mod layer; 4 | pub mod manifest; 5 | pub mod manifest_index; 6 | -------------------------------------------------------------------------------- /registratur/src/v2/domain/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use chrono::{DateTime, Local}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use std::collections::HashMap; 6 | 7 | use crate::reqwest_ext::ReqwestResponseExt; 8 | use crate::v2::client::Client; 9 | 10 | type Empty = HashMap<(), ()>; 11 | 12 | /// Diverges from OCI spec. 13 | /// OCI media type is 14 | /// "application/vnd.oci.image.config.v1+json" 15 | const MEDIA_TYPE: &str = "application/vnd.oci.image.config.v1+json"; 16 | 17 | /// Represents [OCI Image Configuration](https://git.io/Jfv42) 18 | #[derive(Serialize, Deserialize, Debug)] 19 | pub struct Config { 20 | pub created: Option>, 21 | pub author: Option, 22 | pub architecture: String, 23 | pub os: String, 24 | pub config: Option, 25 | pub rootfs: RootFs, 26 | pub history: Vec, 27 | } 28 | 29 | #[derive(Serialize, Deserialize, Debug)] 30 | pub struct Container { 31 | #[serde(rename = "User")] 32 | pub user: Option, 33 | #[serde(rename = "ExposedPorts")] 34 | pub exposed_ports: Option>, 35 | #[serde(rename = "Env")] 36 | pub env: Option>, 37 | #[serde(rename = "Entrypoint")] 38 | pub entrypoint: Option>, 39 | #[serde(rename = "Cmd")] 40 | pub cmd: Option>, 41 | #[serde(rename = "Volumes")] 42 | pub volumes: Option>, 43 | #[serde(rename = "WorkingDir")] 44 | pub working_dir: String, 45 | #[serde(rename = "labels")] 46 | pub labels: Option>, 47 | #[serde(rename = "StopSignal")] 48 | pub stop_signal: Option, 49 | } 50 | 51 | #[derive(Serialize, Deserialize, Debug)] 52 | pub struct RootFs { 53 | pub r#type: String, 54 | pub diff_ids: Vec, 55 | } 56 | 57 | #[derive(Serialize, Deserialize, Debug)] 58 | pub struct HistoryItem { 59 | pub created: Option>, 60 | pub author: Option, 61 | pub created_by: Option, 62 | pub comment: Option, 63 | pub empty_layer: Option, 64 | } 65 | 66 | impl Config { 67 | /// Pull an OCI Image config from a registry 68 | /// 69 | /// # Example 70 | /// 71 | /// ```rust,no_run 72 | /// use registratur::v2::client::Client; 73 | /// use registratur::v2::domain::config::Config; 74 | /// 75 | /// let ref client = Client::build("registry-1.docker.io").unwrap(); 76 | /// 77 | /// async { 78 | /// let config = Config::pull( 79 | /// client, 80 | /// "library/nginx", 81 | /// "sha256:abde" 82 | /// ).await; 83 | /// println!("Got Config: {:?}", config.unwrap()); 84 | /// }; 85 | /// ``` 86 | #[fehler::throws] 87 | pub async fn pull(client: &Client<'_>, name: &str, digest: &str) -> Self { 88 | use reqwest::{header, Method}; 89 | 90 | let path = format!("/v2/{}/blobs/{}", name, digest); 91 | 92 | let result = client 93 | .request(Method::GET, &path, |request| { 94 | request.header(header::ACCEPT, MEDIA_TYPE) 95 | }) 96 | .await? 97 | .read(None::, Some(digest)) 98 | .await?; 99 | 100 | serde_json::from_slice(&result)? 101 | } 102 | } 103 | 104 | #[cfg(test)] 105 | mod tests { 106 | use chrono::prelude::*; 107 | use serde_json; 108 | 109 | use super::Config; 110 | 111 | #[test] 112 | fn test_deserialization() { 113 | let fixture = test_helpers::fixture!("config.json"); 114 | 115 | let config: Config = serde_json::from_str(fixture) 116 | .expect("failed to deserialize config"); 117 | 118 | assert_eq!(config.created.unwrap().weekday(), Weekday::Sat); 119 | 120 | let volumes_map = config.config.unwrap().volumes.unwrap(); 121 | let mut volumes = volumes_map.keys().collect::>(); 122 | 123 | volumes.sort(); 124 | 125 | assert_eq!(volumes, ["/var/job-result-data", "/var/log/my-app-logs"]); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /registratur/src/v2/domain/descriptor.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Represents [OCI Content Descriptor](https://git.io/JvpqR) 4 | #[derive(Serialize, Deserialize, Debug)] 5 | pub struct Descriptor { 6 | #[serde(rename = "mediaType")] 7 | pub media_type: String, 8 | pub digest: String, 9 | pub size: usize, 10 | pub urls: Option>, 11 | } 12 | -------------------------------------------------------------------------------- /registratur/src/v2/domain/layer.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use crate::reqwest_ext::ReqwestResponseExt; 4 | use crate::v2::client::Client; 5 | 6 | const MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar+gzip"; 7 | 8 | /// Represents [Image Layer Filesystem Changeset](https://git.io/JfkAk) 9 | pub struct Layer; 10 | 11 | impl Layer { 12 | /// Pull an OCI Layer FS Changesetfrom a registry 13 | /// 14 | /// # Example 15 | /// 16 | /// ```rust,no_run 17 | /// use registratur::v2::client::Client; 18 | /// use registratur::v2::domain::layer::Layer; 19 | /// 20 | /// let ref client = Client::build("registry-1.docker.io").unwrap(); 21 | /// 22 | /// async { 23 | /// let config = Layer::pull( 24 | /// client, 25 | /// "library/nginx", 26 | /// "sha256:abde", 27 | /// |_| {}, 28 | /// ).await; 29 | /// println!("Got Layer: {:?}", config.unwrap()); 30 | /// }; 31 | /// ``` 32 | #[fehler::throws] 33 | pub async fn pull( 34 | client: &Client<'_>, 35 | name: &str, 36 | digest: &str, 37 | progress_callback: F, 38 | ) -> Vec 39 | where 40 | F: FnMut(usize) + Send, 41 | { 42 | use reqwest::{header, Method}; 43 | 44 | let path = format!("/v2/{}/blobs/{}", name, digest); 45 | 46 | let result = &*client 47 | .request(Method::GET, &path, |request| { 48 | request.header(header::ACCEPT, MEDIA_TYPE) 49 | }) 50 | .await? 51 | .read(Some(progress_callback), Some(&digest)) 52 | .await?; 53 | 54 | result.into() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /registratur/src/v2/domain/manifest.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use std::collections::HashMap; 5 | 6 | use super::descriptor::Descriptor; 7 | use crate::reqwest_ext::ReqwestResponseExt; 8 | use crate::v2::client::Client; 9 | 10 | /// Diverges from OCI spec. 11 | /// OCI media type is 12 | /// application/vnd.oci.image.manifest.v1+json 13 | const MEDIA_TYPE: &str = 14 | "application/vnd.docker.distribution.manifest.v2+json"; 15 | 16 | /// Represents [OCI Image Manifest](https://git.io/JvptH) 17 | #[derive(Serialize, Deserialize, Debug)] 18 | pub struct Manifest { 19 | #[serde(rename = "schemaVersion")] 20 | schema_version: u32, 21 | media_type: Option, 22 | pub config: Descriptor, 23 | pub layers: Vec, 24 | pub annotations: Option>, 25 | } 26 | 27 | impl Manifest { 28 | /// Pull an OCI manifest from a registry 29 | /// This function operates +only+ on digests. 30 | /// 31 | /// # Example 32 | /// 33 | /// ```rust,no_run 34 | /// use registratur::v2::client::Client; 35 | /// use registratur::v2::domain::manifest::Manifest; 36 | /// 37 | /// let ref client = Client::build("registry-1.docker.io").unwrap(); 38 | /// 39 | /// async { 40 | /// let manifest = 41 | /// Manifest::pull(client, "library/nginx", "sha256:6036ab").await; 42 | /// println!("Got Manifest: {:?}", manifest.unwrap()); 43 | /// }; 44 | /// ``` 45 | #[fehler::throws] 46 | pub async fn pull(client: &Client<'_>, name: &str, digest: &str) -> Self { 47 | use reqwest::{header, Method}; 48 | 49 | let path = format!("/v2/{}/manifests/{}", name, digest); 50 | 51 | let result = client 52 | .request(Method::GET, &path, |request| { 53 | request.header(header::ACCEPT, MEDIA_TYPE) 54 | }) 55 | .await? 56 | .read(None::, Some(digest)) 57 | .await?; 58 | 59 | serde_json::from_slice(&result)? 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use serde_json; 66 | 67 | use super::Manifest; 68 | 69 | #[test] 70 | fn test_deserialization() { 71 | let fixture = test_helpers::fixture!("manifest.json"); 72 | 73 | let manifest: Manifest = serde_json::from_str(fixture) 74 | .expect("failed to deserialize manifest"); 75 | 76 | assert_eq!(manifest.layers[2].size, 73109); 77 | assert_eq!( 78 | manifest 79 | .annotations 80 | .and_then(|mut x| x.remove("com.example.key1")), 81 | Some(String::from("value1")) 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /registratur/src/v2/domain/manifest_index.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use std::collections::HashMap; 5 | 6 | use super::descriptor::Descriptor; 7 | use crate::v2::client::Client; 8 | 9 | /// Diverges from OCI spec. 10 | /// OCI media type is 11 | /// application/vnd.oci.image.index.v1+json 12 | const MEDIA_TYPE: &str = 13 | "application/vnd.docker.distribution.manifest.list.v2+json"; 14 | 15 | /// Represents [OCI Image Manifest Index](https://git.io/JfLGL) 16 | #[derive(Serialize, Deserialize, Debug)] 17 | pub struct ManifestIndex { 18 | #[serde(rename = "schemaVersion")] 19 | schema_version: u32, 20 | pub manifests: Vec, 21 | pub annotations: Option>, 22 | } 23 | 24 | #[derive(Serialize, Deserialize, Debug)] 25 | pub struct Manifest { 26 | #[serde(flatten)] 27 | pub descriptor: Descriptor, 28 | pub platform: Option, 29 | } 30 | 31 | #[derive(Serialize, Deserialize, Debug)] 32 | pub struct Platform { 33 | pub architecture: String, 34 | pub os: String, 35 | #[serde(rename = "os.version")] 36 | pub os_version: Option, 37 | #[serde(rename = "os.features")] 38 | pub os_features: Option>, 39 | pub variant: Option, 40 | } 41 | 42 | impl ManifestIndex { 43 | /// Pull an OCI manifest from a registry 44 | /// 45 | /// # Example 46 | /// 47 | /// ```rust,no_run 48 | /// use registratur::v2::client::Client; 49 | /// use registratur::v2::domain::manifest_index::ManifestIndex; 50 | /// 51 | /// let ref client = Client::build("registry-1.docker.io").unwrap(); 52 | /// 53 | /// async { 54 | /// let index = ManifestIndex::pull(client, "library/nginx", "latest"); 55 | /// println!("Got Manifest Index: {:?}", index.await.unwrap()); 56 | /// }; 57 | /// ``` 58 | #[fehler::throws] 59 | pub async fn pull(client: &Client<'_>, name: &str, tag: &str) -> Self { 60 | use reqwest::{header, Method}; 61 | 62 | log::debug!("Pulling Manifest Index for {}:{}", name, tag); 63 | 64 | let path = format!("/v2/{}/manifests/{}", name, tag); 65 | 66 | client 67 | .request(Method::GET, &path, |request| { 68 | request.header(header::ACCEPT, MEDIA_TYPE) 69 | }) 70 | .await? 71 | .json() 72 | .await? 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use serde_json; 79 | 80 | use super::ManifestIndex; 81 | 82 | #[test] 83 | fn test_deserialization() { 84 | let fixture = test_helpers::fixture!("manifest_index.json"); 85 | 86 | let index: ManifestIndex = serde_json::from_str(fixture) 87 | .expect("failed to deserialize index"); 88 | 89 | let platform = index.manifests[1].platform.as_ref(); 90 | 91 | assert_eq!(platform.unwrap().architecture, "amd64"); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /registratur/test/resources/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": "2015-10-31T18:22:56.015925234Z", 3 | "author": "Alyssa P. Hacker ", 4 | "architecture": "amd64", 5 | "os": "linux", 6 | "config": { 7 | "User": "alice", 8 | "ExposedPorts": { 9 | "8080/tcp": {} 10 | }, 11 | "Env": [ 12 | "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 13 | "FOO=oci_is_a", 14 | "BAR=well_written_spec" 15 | ], 16 | "Entrypoint": [ 17 | "/bin/my-app-binary" 18 | ], 19 | "Cmd": [ 20 | "--foreground", 21 | "--config", 22 | "/etc/my-app.d/default.cfg" 23 | ], 24 | "Volumes": { 25 | "/var/job-result-data": {}, 26 | "/var/log/my-app-logs": {} 27 | }, 28 | "WorkingDir": "/home/alice", 29 | "Labels": { 30 | "com.example.project.git.url": "https://example.com/project.git", 31 | "com.example.project.git.commit": "45a939b2999782a3f005621a8d0f29aa387e1d6b" 32 | } 33 | }, 34 | "rootfs": { 35 | "diff_ids": [ 36 | "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1", 37 | "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" 38 | ], 39 | "type": "layers" 40 | }, 41 | "history": [ 42 | { 43 | "created": "2015-10-31T22:22:54.690851953Z", 44 | "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /" 45 | }, 46 | { 47 | "created": "2015-10-31T22:22:55.613815829Z", 48 | "created_by": "/bin/sh -c #(nop) CMD [\"sh\"]", 49 | "empty_layer": true 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /registratur/test/resources/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 2, 3 | "config": { 4 | "mediaType": "application/vnd.oci.image.config.v1+json", 5 | "size": 7023, 6 | "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" 7 | }, 8 | "layers": [ 9 | { 10 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", 11 | "size": 32654, 12 | "digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0" 13 | }, 14 | { 15 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", 16 | "size": 16724, 17 | "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" 18 | }, 19 | { 20 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", 21 | "size": 73109, 22 | "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" 23 | } 24 | ], 25 | "annotations": { 26 | "com.example.key1": "value1", 27 | "com.example.key2": "value2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /registratur/test/resources/manifest_index.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 2, 3 | "manifests": [ 4 | { 5 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 6 | "size": 7143, 7 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", 8 | "platform": { 9 | "architecture": "ppc64le", 10 | "os": "linux" 11 | } 12 | }, 13 | { 14 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 15 | "size": 7682, 16 | "digest": "sha256:f40d81dff6f411968c8276b3c481a95e7f6ca23b49490009472f61fd580a0984", 17 | "platform": { 18 | "architecture": "amd64", 19 | "os": "linux" 20 | } 21 | } 22 | ], 23 | "annotations": { 24 | "com.example.key1": "value1", 25 | "com.example.key2": "value2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /registratur/test/resources/server_mocks/basic.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - request: 3 | method: head 4 | response: 5 | headers: 6 | - header: WWW-Authenticate 7 | value: Bearer realm="SERVER_URL/auth",service="registry.docker.io",scope="repository:library/nginx:pull" 8 | 9 | - request: 10 | method: GET 11 | path: /auth 12 | response: 13 | body: ./basic/auth.json 14 | 15 | - request: 16 | method: GET 17 | path: /v2/(.*)/manifests/(.*) 18 | headers: 19 | - header: Accept 20 | value: application/vnd.docker.distribution.manifest.v2+json 21 | response: 22 | body: ./basic/manifest.json 23 | 24 | - request: 25 | method: GET 26 | path: /v2/(.*)/manifests/(.*) 27 | headers: 28 | - header: Accept 29 | value: application/vnd.docker.distribution.manifest.list.v2+json 30 | response: 31 | body: ./basic/manifest_index.json 32 | 33 | - request: 34 | method: GET 35 | path: /v2/(.*)/blobs/(.*) 36 | headers: 37 | - header: Accept 38 | value: application/vnd.oci.image.config.v1+json 39 | response: 40 | body: ./basic/config.json 41 | 42 | - request: 43 | method: GET 44 | path: /v2/(.*)/blobs/(.*) 45 | headers: 46 | - header: Accept 47 | value: application/vnd.oci.image.layer.v1.tar+gzip 48 | response: 49 | body: ./basic/layer1 50 | -------------------------------------------------------------------------------- /registratur/test/resources/server_mocks/basic/auth.json: -------------------------------------------------------------------------------- 1 | {"access_token": "well, that might work"} 2 | -------------------------------------------------------------------------------- /registratur/test/resources/server_mocks/basic/layer1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhramov/knast/8c9f5f481a467a22c7cc47f11a28fe52536f9950/registratur/test/resources/server_mocks/basic/layer1 -------------------------------------------------------------------------------- /registratur/test/resources/server_mocks/basic/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 2, 3 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 4 | "config": { 5 | "mediaType": "application/vnd.docker.container.image.v1+json", 6 | "size": 6668, 7 | "digest": "sha256:f31930d5b3c26e15a6d72d85b89f55a727c9290cb9f126ed3daa9aa36f3db8b2" 8 | }, 9 | "layers": [ 10 | { 11 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", 12 | "size": 274, 13 | "digest": "sha256:c28d7487cd39cf086cb8ab040798f46176a8c2414c23114969b0ac85c4c61a23" 14 | }, 15 | { 16 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", 17 | "size": 274, 18 | "digest": "sha256:c28d7487cd39cf086cb8ab040798f46176a8c2414c23114969b0ac85c4c61a23" 19 | }, 20 | { 21 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", 22 | "size": 274, 23 | "digest": "sha256:c28d7487cd39cf086cb8ab040798f46176a8c2414c23114969b0ac85c4c61a23" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /registratur/test/resources/server_mocks/basic/manifest_index.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 2, 3 | "manifests": [ 4 | { 5 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 6 | "digest": "sha256:dfd6fcd3b075530b95f53b46f9f0e665f0fdb3ee2debe349ed3f146600e0060f", 7 | "size": 948, 8 | "urls": null, 9 | "platform": { 10 | "architecture": "amd64", 11 | "os": "linux", 12 | "os.version": null, 13 | "os.features": null, 14 | "variant": null 15 | } 16 | }, 17 | { 18 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 19 | "digest": "sha256:3cf37a5b5826ca2d2daa8c026d81b7f7b002dc11b5b9e5f7b34514ed8d19663c", 20 | "size": 948, 21 | "urls": null, 22 | "platform": { 23 | "architecture": "arm", 24 | "os": "linux", 25 | "os.version": null, 26 | "os.features": null, 27 | "variant": "v7" 28 | } 29 | }, 30 | { 31 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 32 | "digest": "sha256:0eb6da8a5474a960e9008a73c4802332d8c09654d56a630a736bdd8502a2b8ed", 33 | "size": 948, 34 | "urls": null, 35 | "platform": { 36 | "architecture": "arm64", 37 | "os": "linux", 38 | "os.version": null, 39 | "os.features": null, 40 | "variant": "v8" 41 | } 42 | }, 43 | { 44 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 45 | "digest": "sha256:d930948e1f4cc8844d1ebd47fae24fa10d2ffca7fe68bced431354c1b03399c7", 46 | "size": 948, 47 | "urls": null, 48 | "platform": { 49 | "architecture": "386", 50 | "os": "linux", 51 | "os.version": null, 52 | "os.features": null, 53 | "variant": null 54 | } 55 | }, 56 | { 57 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 58 | "digest": "sha256:06e2a1fa4c8be156dc2c05d2123d7876f504f73d64f88cb3f79410d7c5ccb193", 59 | "size": 948, 60 | "urls": null, 61 | "platform": { 62 | "architecture": "ppc64le", 63 | "os": "linux", 64 | "os.version": null, 65 | "os.features": null, 66 | "variant": null 67 | } 68 | }, 69 | { 70 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 71 | "digest": "sha256:a04b9f7d9d37cab1dc499e9f77fa0c2238552bea20afa0722780e92aa7f02634", 72 | "size": 948, 73 | "urls": null, 74 | "platform": { 75 | "architecture": "s390x", 76 | "os": "linux", 77 | "os.version": null, 78 | "os.features": null, 79 | "variant": null 80 | } 81 | } 82 | ], 83 | "annotations": null 84 | } 85 | -------------------------------------------------------------------------------- /registratur/test/resources/www_authenticate: -------------------------------------------------------------------------------- 1 | Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/nginx:pull" -------------------------------------------------------------------------------- /runc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "runc" 3 | version = "0.1.0" 4 | authors = ["Artem Khramov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | baustelle = { path = "../baustelle" } 9 | clap = { version = "3.0.0-beta.2", features = ["yaml"] } 10 | libknast = { path = "../libknast" } 11 | serde_json = "1" 12 | storage = { path = "../storage" } 13 | tokio = { version = "1.1.1", features = ["macros", "rt", "rt-multi-thread"] } 14 | tracing = "0.1.25" 15 | tracing-subscriber = "0.2.18" -------------------------------------------------------------------------------- /runc/src/bin/fetch_image.rs: -------------------------------------------------------------------------------- 1 | // Fetch & unpack a centos image. 2 | use baustelle::{Builder, EvaluationUpdate, LayerDownloadStatus}; 3 | use storage::TestStorage; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | let home = std::env::var("HOME").unwrap(); 8 | let storage = TestStorage::new(home).unwrap(); 9 | let builder = Builder::new("amd64".into(), vec!["linux".into()], storage) 10 | .expect("Failed to build the image builder"); 11 | tracing_subscriber::fmt().init(); 12 | 13 | let image = std::env::args().nth(1).expect("USAGE: fetch_image IMAGE"); 14 | let containerfile = format!("FROM {}", image); 15 | 16 | let rootfs = builder 17 | .build( 18 | "https://registry-1.docker.io", 19 | containerfile.as_bytes(), 20 | |x| { 21 | if let EvaluationUpdate::From( 22 | LayerDownloadStatus::InProgress(name, count, total), 23 | ) = x 24 | { 25 | tracing::info!( 26 | "{} downloaded {} of {}", 27 | name, 28 | count, 29 | total 30 | ); 31 | } 32 | }, 33 | ) 34 | .await 35 | .expect("Failed to build the image"); 36 | 37 | tracing::info!("Build a container"); 38 | tracing::info!("Bundle located in {:#?}", rootfs); 39 | } 40 | -------------------------------------------------------------------------------- /runc/src/bin/runc.rs: -------------------------------------------------------------------------------- 1 | use std::process::exit; 2 | 3 | use clap::{load_yaml, App, ArgMatches}; 4 | use libknast::operations::OciOperations; 5 | use storage::{StorageEngine, TestStorage}; 6 | 7 | fn main() { 8 | let yaml = load_yaml!("runc.yaml"); 9 | let matches = App::from(yaml).get_matches(); 10 | let home = std::env::var("HOME").unwrap(); 11 | let storage = TestStorage::new(home).unwrap(); 12 | let container_id = 13 | |matches: &ArgMatches| matches.value_of("ID").unwrap().to_owned(); 14 | 15 | if let Some(matches) = matches.subcommand_matches("state") { 16 | let ops = OciOperations::new(&storage, container_id(matches)).unwrap(); 17 | 18 | return state(ops); 19 | } 20 | if let Some(matches) = matches.subcommand_matches("create") { 21 | let ops = OciOperations::new(&storage, container_id(matches)).unwrap(); 22 | let bundle = matches.value_of("BUNDLE").unwrap(); 23 | let interface = matches.value_of("nat-interface").unwrap(); 24 | 25 | return create(ops, bundle, interface); 26 | } 27 | if let Some(matches) = matches.subcommand_matches("start") { 28 | let ops = OciOperations::new(&storage, container_id(matches)).unwrap(); 29 | 30 | return start(ops); 31 | } 32 | if let Some(matches) = matches.subcommand_matches("kill") { 33 | let ops = OciOperations::new(&storage, container_id(matches)).unwrap(); 34 | let signal = matches.value_of("SIGNAL").unwrap().parse().unwrap(); 35 | 36 | return kill(ops, signal); 37 | } 38 | if let Some(matches) = matches.subcommand_matches("delete") { 39 | let ops = OciOperations::new(&storage, container_id(matches)).unwrap(); 40 | 41 | return delete(ops); 42 | } 43 | } 44 | 45 | fn state(ops: OciOperations) { 46 | match ops.state() { 47 | Ok(result) => { 48 | println!("{}", serde_json::to_string_pretty(&result).unwrap()) 49 | } 50 | Err(error) => { 51 | println!("{}", error); 52 | exit(1); 53 | } 54 | } 55 | } 56 | 57 | fn create( 58 | ops: OciOperations, 59 | bundle: &str, 60 | nat_interface: &str, 61 | ) { 62 | match ops.create(bundle, Some(nat_interface)) { 63 | Ok(_) => (), 64 | Err(error) => { 65 | println!("{}", error); 66 | exit(1); 67 | } 68 | } 69 | } 70 | 71 | fn start(ops: OciOperations) { 72 | match ops.start() { 73 | Ok(_) => (), 74 | Err(error) => { 75 | println!("{}", error); 76 | exit(1); 77 | } 78 | } 79 | } 80 | 81 | fn kill(ops: OciOperations, signal: i32) { 82 | match ops.kill(signal) { 83 | Ok(_) => (), 84 | Err(error) => { 85 | println!("{}", error); 86 | exit(1); 87 | } 88 | } 89 | } 90 | 91 | fn delete(ops: OciOperations) { 92 | ops.delete(); 93 | } 94 | -------------------------------------------------------------------------------- /runc/src/bin/runc.yaml: -------------------------------------------------------------------------------- 1 | name: knast-runc 2 | version: "0.0.1" 3 | author: Artem K. 4 | about: OCI-compatible runc implementation atop of FreeBSD jails 5 | args: [] 6 | default: help 7 | subcommands: 8 | - state: 9 | about: Query container state 10 | version: "0.0.1" 11 | args: 12 | - ID: 13 | about: Container identifier 14 | required: true 15 | - create: 16 | about: Create container ID from OCI runtime BUNDLE 17 | version: "0.0.1" 18 | args: 19 | - ID: 20 | about: Container identifier 21 | required: true 22 | - BUNDLE: 23 | about: OCI runtime bundle 24 | required: true 25 | - nat-interface: 26 | short: n 27 | default_value: lagg0 28 | help: interface for NAT 29 | - start: 30 | about: Start container ID 31 | version: "0.0.1" 32 | args: 33 | - ID: 34 | about: Container identifier 35 | required: true 36 | - kill: 37 | about: Send the specified SIGNAL to container ID 38 | version: "0.0.1" 39 | args: 40 | - ID: 41 | about: Container identifier 42 | required: true 43 | - SIGNAL: 44 | about: Signal to send to container 45 | required: true 46 | - delete: 47 | about: Delete container ID 48 | version: "0.0.1" 49 | args: 50 | - ID: 51 | about: Container identifier 52 | required: true 53 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 79 2 | force_multiline_blocks = true 3 | wrap_comments = true 4 | comment_width = 60 -------------------------------------------------------------------------------- /storage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "storage" 3 | version = "0.1.0" 4 | authors = ["Artem Khramov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | anyhow = "1.0" 9 | bincode = "1.2.1" 10 | fehler = "1.0" 11 | r2d2 = "0.8.9" 12 | r2d2_sqlite="0.18.0" 13 | rusqlite = { version = "0.25.3", optional = true } 14 | serde = "1.0" 15 | sled = { version = "0.34", optional = true } 16 | 17 | [dev-dependencies] 18 | tempfile = "3.1.0" 19 | 20 | [features] 21 | default = ["sqlite_engine"] 22 | sled_engine = ["sled"] 23 | sqlite_engine = ["rusqlite"] 24 | -------------------------------------------------------------------------------- /storage/src/sled_engine.rs: -------------------------------------------------------------------------------- 1 | use std::{future::Future, path::Path}; 2 | 3 | use anyhow::Error; 4 | 5 | use super::StorageEngine; 6 | 7 | const STORAGE_FILE: &str = "storage.db"; 8 | 9 | impl StorageEngine for sled::Db { 10 | #[fehler::throws] 11 | fn initialize(cache_dir: impl AsRef) -> Box { 12 | Box::new(sled::open(cache_dir.as_ref().join(STORAGE_FILE))?) 13 | } 14 | 15 | #[fehler::throws] 16 | fn get( 17 | &self, 18 | collection: impl AsRef<[u8]>, 19 | key: impl AsRef<[u8]>, 20 | ) -> Option> { 21 | let tree = self.open_tree(collection)?; 22 | 23 | tree.get(key)?.map(|x| (*x).to_vec()) 24 | } 25 | 26 | #[fehler::throws] 27 | fn put( 28 | &self, 29 | collection: impl AsRef<[u8]>, 30 | key: impl AsRef<[u8]>, 31 | value: impl AsRef<[u8]>, 32 | ) { 33 | let tree = self.open_tree(collection)?; 34 | 35 | tree.insert(key.as_ref(), value.as_ref())?; 36 | } 37 | 38 | #[fehler::throws] 39 | fn compare_and_swap( 40 | &self, 41 | collection: impl AsRef<[u8]>, 42 | key: impl AsRef<[u8]>, 43 | old_value: Option>, 44 | new_value: Option>, 45 | ) { 46 | let tree = self.open_tree(collection)?; 47 | let old_value = if let Some(old_value) = &old_value { 48 | Some(old_value.as_ref()) 49 | } else { 50 | None 51 | }; 52 | let new_value = if let Some(new_value) = &new_value { 53 | Some(new_value.as_ref()) 54 | } else { 55 | None 56 | }; 57 | tree.compare_and_swap(key.as_ref(), old_value, new_value)??; 58 | } 59 | 60 | #[fehler::throws] 61 | fn remove(&self, collection: impl AsRef<[u8]>, key: impl AsRef<[u8]>) { 62 | let tree = self.open_tree(collection)?; 63 | 64 | tree.remove(key.as_ref())?; 65 | } 66 | 67 | #[fehler::throws] 68 | fn exists( 69 | &self, 70 | collection: impl AsRef<[u8]>, 71 | key: impl AsRef<[u8]>, 72 | ) -> bool { 73 | let tree = self.open_tree(collection)?; 74 | 75 | tree.contains_key(key)? 76 | } 77 | 78 | fn flush(&self) -> Box> + Unpin> { 79 | self.flush_async() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /storage/src/sqlite_engine.rs: -------------------------------------------------------------------------------- 1 | use std::{future::Future, path::Path}; 2 | 3 | use anyhow::Error; 4 | use r2d2::Pool; 5 | use r2d2_sqlite::SqliteConnectionManager; 6 | use rusqlite::named_params; 7 | 8 | use super::StorageEngine; 9 | 10 | const STORAGE_FILE: &str = "storage.db"; 11 | 12 | pub type Connection = Pool; 13 | 14 | impl StorageEngine for Connection { 15 | #[fehler::throws] 16 | fn initialize(cache_dir: impl AsRef) -> Box { 17 | let file = cache_dir.as_ref().join(STORAGE_FILE); 18 | let manager = SqliteConnectionManager::file(file); 19 | let pool = r2d2::Pool::new(manager)?; 20 | let connection = pool.get()?; 21 | connection.execute(include_str!("sqlite_engine/migration.sql"), [])?; 22 | 23 | Box::new(pool) 24 | } 25 | 26 | fn get( 27 | &self, 28 | collection: impl AsRef<[u8]>, 29 | key: impl AsRef<[u8]>, 30 | ) -> Result>, Error> { 31 | let connection = self.get()?; 32 | let mut get_statement = connection 33 | .prepare_cached(include_str!("sqlite_engine/get.sql"))?; 34 | 35 | let params = named_params! { 36 | ":key": key.as_ref(), 37 | ":tree": collection.as_ref() 38 | }; 39 | let mut results = get_statement.query_map(params, |row| { 40 | let result: Vec = row.get(0)?; 41 | 42 | Ok(result) 43 | })?; 44 | 45 | results.next().transpose().map_err(From::from) 46 | } 47 | 48 | #[fehler::throws] 49 | fn put( 50 | &self, 51 | collection: impl AsRef<[u8]>, 52 | key: impl AsRef<[u8]>, 53 | value: impl AsRef<[u8]>, 54 | ) { 55 | let connection = self.get()?; 56 | let mut put_statement = connection 57 | .prepare_cached(include_str!("sqlite_engine/put.sql"))?; 58 | 59 | let params = named_params! { 60 | ":key": key.as_ref(), 61 | ":tree": collection.as_ref(), 62 | ":value": value.as_ref(), 63 | }; 64 | put_statement.execute(params)?; 65 | } 66 | 67 | #[fehler::throws] 68 | fn compare_and_swap( 69 | &self, 70 | collection: impl AsRef<[u8]>, 71 | key: impl AsRef<[u8]>, 72 | old_value: Option>, 73 | new_value: Option>, 74 | ) { 75 | let mut connection = self.get()?; 76 | let old_value = if let Some(old_value) = &old_value { 77 | Some(old_value.as_ref()) 78 | } else { 79 | None 80 | }; 81 | let new_value = if let Some(new_value) = &new_value { 82 | Some(new_value.as_ref()) 83 | } else { 84 | None 85 | }; 86 | 87 | let tx = connection.transaction()?; 88 | { 89 | let insert_params = named_params! { 90 | ":key": key.as_ref(), 91 | ":tree": collection.as_ref(), 92 | ":old_value": old_value, 93 | }; 94 | let mut try_insert_statement = tx.prepare_cached(include_str!( 95 | "sqlite_engine/try_insert.sql" 96 | ))?; 97 | 98 | try_insert_statement.execute(insert_params)?; 99 | } 100 | 101 | { 102 | let cas_params = named_params! { 103 | ":key": key.as_ref(), 104 | ":tree": collection.as_ref(), 105 | ":old_value": old_value, 106 | ":new_value": new_value, 107 | }; 108 | 109 | let mut cas_statement = 110 | tx.prepare_cached(include_str!("sqlite_engine/cas.sql"))?; 111 | let mut rows = cas_statement.query(cas_params)?; 112 | 113 | if rows.next()?.is_none() { 114 | anyhow::bail!("Compare and swap conflict"); 115 | } 116 | } 117 | 118 | tx.commit()?; 119 | } 120 | 121 | #[fehler::throws] 122 | fn remove(&self, collection: impl AsRef<[u8]>, key: impl AsRef<[u8]>) { 123 | let connection = self.get()?; 124 | let mut remove_statement = connection 125 | .prepare_cached(include_str!("sqlite_engine/remove.sql"))?; 126 | let params = named_params! { 127 | ":key": key.as_ref(), 128 | ":tree": collection.as_ref(), 129 | }; 130 | 131 | remove_statement.execute(params)?; 132 | } 133 | 134 | #[fehler::throws] 135 | fn exists( 136 | &self, 137 | collection: impl AsRef<[u8]>, 138 | key: impl AsRef<[u8]>, 139 | ) -> bool { 140 | let connection = self.get()?; 141 | let mut exists_statement = connection 142 | .prepare_cached(include_str!("sqlite_engine/exists.sql"))?; 143 | let params = named_params! { 144 | ":key": key.as_ref(), 145 | ":tree": collection.as_ref(), 146 | }; 147 | 148 | let mut results = exists_statement.query_map(params, |row| { 149 | let result: bool = row.get(0)?; 150 | 151 | Ok(result) 152 | })?; 153 | 154 | results.next().transpose()?.unwrap_or_default() 155 | } 156 | 157 | fn flush(&self) -> Box> + Unpin> { 158 | Box::new(std::future::ready(Ok(0))) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /storage/src/sqlite_engine/cas.sql: -------------------------------------------------------------------------------- 1 | UPDATE storage SET value = :new_value WHERE tree = :tree AND key = :key AND value IS :old_value 2 | RETURNING id; 3 | -------------------------------------------------------------------------------- /storage/src/sqlite_engine/exists.sql: -------------------------------------------------------------------------------- 1 | SELECT EXISTS(SELECT 1 FROM storage WHERE key = :key AND tree = :tree); 2 | -------------------------------------------------------------------------------- /storage/src/sqlite_engine/get.sql: -------------------------------------------------------------------------------- 1 | SELECT value FROM storage WHERE tree = :tree AND key = :key LIMIT 1; 2 | -------------------------------------------------------------------------------- /storage/src/sqlite_engine/migration.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS storage( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | tree BLOB NOT NULL, 4 | key BLOB NOT NULL, 5 | value BLOB, 6 | UNIQUE (tree, key) 7 | ); 8 | -------------------------------------------------------------------------------- /storage/src/sqlite_engine/put.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO storage (tree, key, value) VALUES (:tree, :key, :value) ON CONFLICT 2 | DO UPDATE 3 | SET value = EXCLUDED.value; 4 | -------------------------------------------------------------------------------- /storage/src/sqlite_engine/remove.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM storage WHERE key = :key AND tree = :tree; 2 | -------------------------------------------------------------------------------- /storage/src/sqlite_engine/try_insert.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO storage (tree, key, value) VALUES (:tree, :key, :old_value) ON CONFLICT DO NOTHING; 2 | -------------------------------------------------------------------------------- /test/resources/commands_output/env: -------------------------------------------------------------------------------- 1 | SETTING=env_var 2 | WORKED=well 3 | -------------------------------------------------------------------------------- /test/resources/commands_output/id: -------------------------------------------------------------------------------- 1 | uid=1000 gid=1000(akhramov) groups=1000(akhramov),44(video) 2 | -------------------------------------------------------------------------------- /test/resources/commands_output/mount: -------------------------------------------------------------------------------- 1 | zroot/tmp on / (zfs, local, noatime, nosuid, nfsv4acls) 2 | devfs on /dev (devfs) 3 | linsysfs on /sys (linsysfs, local, noexec, nosuid, read-only) 4 | linprocfs on /proc (linprocfs, local, noexec, nosuid, read-only) 5 | fdescfs on /dev/fd (fdescfs) 6 | -------------------------------------------------------------------------------- /test/resources/commands_output/trapster_sigbus: -------------------------------------------------------------------------------- 1 | BUS received. 2 | -------------------------------------------------------------------------------- /test/resources/container/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ociVersion": "1.0", 3 | "root": { 4 | "path": "./rootfs", 5 | "readonly": false 6 | }, 7 | "mounts": [ 8 | { 9 | "destination": "/dev", 10 | "source": "devfs", 11 | "options": null, 12 | "type": "devfs" 13 | }, 14 | { 15 | "destination": "/sys", 16 | "source": "linsysfs", 17 | "options": [ 18 | "nosuid", 19 | "noexec", 20 | "ro" 21 | ], 22 | "type": "linsysfs" 23 | }, 24 | { 25 | "destination": "/proc", 26 | "source": "linprocfs", 27 | "options": [ 28 | "nosuid", 29 | "noexec", 30 | "ro" 31 | ], 32 | "type": "linprocfs" 33 | }, 34 | { 35 | "destination": "/dev/fd", 36 | "source": "fdescfs", 37 | "options": [ 38 | "linrdlnk" 39 | ], 40 | "type": "fdescfs" 41 | } 42 | ], 43 | "process": { 44 | "terminal": null, 45 | "consoleSize": null, 46 | "cwd": "/", 47 | "env": [ 48 | "SETTING=env_var", 49 | "WORKED=well" 50 | ], 51 | "args": [ 52 | "sleep", 53 | "1000" 54 | ], 55 | "rlimits": null, 56 | "user": { 57 | "uid": 1000, 58 | "gid": 1000, 59 | "umask": null, 60 | "additionalGids": null 61 | }, 62 | "hostname": null 63 | }, 64 | "hooks": null, 65 | "annotations": { 66 | "io.container.manager": "werft", 67 | "org.opencontainers.image.stopSignal": "15" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/COPYING: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8b5f7851c28f42604349e2f301f033877b5a3fbea1318af6cd32dba0eaced03e 3 | size 6253 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/bin/env: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ff80f1cf9c5d7fe99f1d5e47b06ca71466c7f7f153d16b7c6aa2bf8d81db0056 3 | size 14976 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/bin/id: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f3e661a1e8e45a567c6e1603d5938852f9fef3150b3d8cabec12486ce96afa55 3 | size 13536 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/bin/ls: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:51978eebba6d9dc4dff96872a13e69b7b4282aa392e000f686371c9725fa3a25 3 | size 33560 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/bin/mount: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9be9e4ed35cfdfbbf0f06af9b7b15b21d6d11b8de7adabeb4324568bf88bf71e 3 | size 26304 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/bin/quitely_stop.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhramov/knast/8c9f5f481a467a22c7cc47f11a28fe52536f9950/test/resources/container/rootfs/bin/quitely_stop.sh -------------------------------------------------------------------------------- /test/resources/container/rootfs/bin/sh: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e59ee2e313541b8fe869b8e8f8d79aeab103caffdf2a6c54669dd53b5b1dff99 3 | size 162616 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/bin/sleep: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e66256257ac757b16dd723376351ecbebe21bb92c103fd74986e5fb0fd65c63c 3 | size 7696 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/bin/trapster.sh: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f860536b8cc1caef4b4461ce640eadc5fd0cf54101c1d19deeb99cd2bae6f70d 3 | size 146 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/etc/group: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d9ea7b7d00cfd30b5af9c6016e3607aa94d8c63c56f4b05e0699a110458e93f9 3 | size 77 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/etc/master.passwd: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a705f7e04b6fcb5dc83bb0bee338baaead4ee73893f8fba758fa78565914da48 3 | size 101 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/etc/nsswitch.conf: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f83fe8650e3632c87e9136d91b864c678cd3a966ff2ccd6e4a336f55e85f98b1 3 | size 272 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/etc/passwd: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a34c0b9b1d10a93b5394253ac1d453767af43640c7064a9a0bbab9272bed7c12 3 | size 91 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/etc/pwd.db: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:bf04fd4210d64c2a9b130961a94764c57b742e66e0b6f05583003ea180dcb1ef 3 | size 40960 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/etc/spwd.db: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:bf04fd4210d64c2a9b130961a94764c57b742e66e0b6f05583003ea180dcb1ef 3 | size 40960 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/lib/libc.so.7: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:0e04dd44e7eb18631ea5b3180bd6e9c1e79c3edea885e599a737df341cd7b7c2 3 | size 1982072 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/lib/libedit.so.8: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4a8917f8b7c16cb1229af34595717cd3aa0e49096e47698925e8eaabdad00914 3 | size 215928 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/lib/libgcc_s.so.1: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1e790e473b4ef3cf23461699982e8341ba0926cc403a0c553e6775326d94f729 3 | size 93864 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/lib/libm.so.5: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:562d7ee2b6f56e67a07211ad10cd593afc77054cc90fe95ce4b7d64be2688f0e 3 | size 195696 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/lib/libncursesw.so.9: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d7f3c5d8bf7f0471b56bd60cbcf87224f50c59e3f708bb21fa468e91ce117dc4 3 | size 459464 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/lib/libthr.so.3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4b4d58d75329fcff90b338542d83c99207fb6e63813edcfd6d373ad8c5627e90 3 | size 125712 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/lib/libutil.so.9: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:330d8064849caf268b60908400a1a147e81524597401f093a65d1cd29c673cf4 3 | size 79384 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/libexec/COPYING: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2a7ff63cc010c0b9811780638b556084e71a07c7bead4e2520bb46d4efab9df7 3 | size 6232 4 | -------------------------------------------------------------------------------- /test/resources/container/rootfs/libexec/ld-elf.so.1: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1140fc3f9e89bb23328fe08aa0c1709e82223c52915e9db75e2a73d11867081c 3 | size 114832 4 | -------------------------------------------------------------------------------- /test_helpers/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | license = "BSD-4-Clause" 3 | name = "test_helpers" 4 | version = "0.1.0" 5 | authors = ["akhramov"] 6 | edition = "2018" 7 | 8 | [dependencies] 9 | bincode = "1.2.1" 10 | jail = { git = "https://github.com/fubarnetes/libjail-rs", branch = "dev" } 11 | memmap = "0.7.0" 12 | nix = "0.17.0" 13 | mockito = "0.25.1" 14 | serde = { version = "1.0", features = ["derive"] } 15 | serde_yaml = "0.8" 16 | procedural_macros = { path = "./procedural_macros" } -------------------------------------------------------------------------------- /test_helpers/procedural_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "procedural_macros" 3 | version = "0.1.0" 4 | authors = ["Artem Khramov "] 5 | edition = "2018" 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | proc-macro2 = "^1" 12 | syn = { version = "^1", features = ["full"] } 13 | quote = "^1" -------------------------------------------------------------------------------- /test_helpers/procedural_macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use syn::ItemFn; 3 | 4 | #[proc_macro_attribute] 5 | pub fn jailed_test(_attrs: TokenStream, item: TokenStream) -> TokenStream { 6 | let input = syn::parse_macro_input!(item as ItemFn); 7 | let fn_name = input.sig.ident; 8 | let block = input.block; 9 | 10 | let body = quote::quote! { 11 | use std::io::Write; 12 | use test_helpers::nix::{ 13 | sys::{ 14 | signal::Signal, 15 | wait::{waitpid, WaitStatus}, 16 | }, 17 | unistd::{fork, ForkResult}, 18 | }; 19 | use test_helpers::jail::StoppedJail; 20 | use test_helpers::memmap::MmapMut; 21 | use test_helpers::bincode; 22 | 23 | let mut mmap = MmapMut::map_anon(1024) 24 | .expect("failed to create a mmap"); 25 | 26 | let jail = StoppedJail::new("/") 27 | .param("vnet", jail::param::Value::Int(1)) 28 | .param("children.max", jail::param::Value::Int(100)) 29 | .start() 30 | .expect("Couldn't start jail"); 31 | 32 | match unsafe { fork() } { 33 | Ok(ForkResult::Child) => { 34 | jail.attach().unwrap(); 35 | let result = std::panic::catch_unwind(|| { 36 | #block 37 | }); 38 | 39 | if let Err(err) = result { 40 | err.downcast_ref::() 41 | .and_then(|string| { 42 | bincode::serialize(&format!("{:?}", string)) 43 | .and_then(|serialized| { 44 | Ok((&mut mmap[..]).write_all(&serialized[..])?) 45 | }).ok() 46 | }).unwrap_or(()); 47 | std::process::abort(); 48 | }; 49 | }, 50 | Ok(ForkResult::Parent { child: child }) => { 51 | let status = waitpid(child, None) 52 | .expect("failed to wait the child process"); 53 | jail.defer_cleanup() 54 | .expect("failed to defer jail clean up"); 55 | 56 | match status { 57 | WaitStatus::Exited(_, 0) => (), 58 | WaitStatus::Signaled(_, Signal::SIGABRT, _) => { 59 | let error: String = bincode::deserialize(&mmap).expect( 60 | "Test failed, but result couldn't be deserialized" 61 | ); 62 | 63 | panic!("{}", error); 64 | }, 65 | status => { 66 | panic!("Unexpected jailed process status {:?}", status); 67 | } 68 | } 69 | }, 70 | _ => panic!("Failed to fork"), 71 | } 72 | }; 73 | 74 | quote::quote!( 75 | #[test] 76 | fn #fn_name() { 77 | #body 78 | } 79 | ) 80 | .into() 81 | } 82 | -------------------------------------------------------------------------------- /test_helpers/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use bincode; 2 | pub use jail; 3 | pub use memmap; 4 | pub use mockito; 5 | pub use nix; 6 | pub use procedural_macros::*; 7 | use serde::Deserialize; 8 | pub use serde_yaml; 9 | 10 | #[derive(Deserialize)] 11 | pub struct MockDefinition { 12 | pub request: MockRequest, 13 | pub response: MockResponse, 14 | } 15 | 16 | #[derive(Deserialize)] 17 | pub struct MockRequest { 18 | pub method: String, 19 | pub path: Option, 20 | pub headers: Option>, 21 | } 22 | 23 | #[derive(Deserialize)] 24 | pub struct MockHeader { 25 | pub header: String, 26 | pub value: String, 27 | } 28 | 29 | #[derive(Deserialize)] 30 | pub struct MockResponse { 31 | pub headers: Option>, 32 | pub body: Option, 33 | } 34 | 35 | #[macro_export] 36 | macro_rules! fixture { 37 | ($file:expr) => { 38 | include_str!(concat!( 39 | env!("CARGO_MANIFEST_DIR"), 40 | "/test/resources/", 41 | $file 42 | )) 43 | }; 44 | } 45 | 46 | #[macro_export] 47 | macro_rules! bytes_fixture { 48 | ($file:expr) => { 49 | include_bytes!(concat!( 50 | env!("CARGO_MANIFEST_DIR"), 51 | "/test/resources/", 52 | $file 53 | )) 54 | }; 55 | } 56 | 57 | #[macro_export] 58 | macro_rules! code_fixture { 59 | ($file:expr) => { 60 | include!(concat!( 61 | env!("CARGO_MANIFEST_DIR"), 62 | "/test/resources/", 63 | $file 64 | )) 65 | }; 66 | } 67 | 68 | #[macro_export] 69 | macro_rules! fixture_path { 70 | ($file:expr) => { 71 | std::path::Path::new(concat!( 72 | env!("CARGO_MANIFEST_DIR"), 73 | "/test/resources/", 74 | $file 75 | )) 76 | }; 77 | } 78 | 79 | /// Generate mockito mocks using declarative (yml) definition. 80 | #[macro_export] 81 | macro_rules! mock_server { 82 | ($file:expr) => {{ 83 | use $crate::mockito::{mock, Matcher}; 84 | use $crate::serde_yaml; 85 | use $crate::*; 86 | 87 | let mocks_path = concat!( 88 | env!("CARGO_MANIFEST_DIR"), 89 | "/test/resources/server_mocks", 90 | ); 91 | 92 | let file = std::fs::read(format!("{}/{}", mocks_path, $file)) 93 | .expect("definition file's not found!"); 94 | 95 | let definitions: Vec = serde_yaml::from_slice(&file) 96 | .expect("failed to parse definition file"); 97 | 98 | let mocks = definitions 99 | .into_iter() 100 | .map(|MockDefinition { request, response }| { 101 | let path = 102 | request.path.map(Matcher::Regex).unwrap_or(Matcher::Any); 103 | 104 | let mut mock = mock(&request.method, path); 105 | 106 | if let Some(headers) = request.headers { 107 | for MockHeader { header, value } in headers { 108 | mock = mock.match_header(&header, &value[..]); 109 | } 110 | } 111 | 112 | if let Some(body) = response.body { 113 | mock = mock.with_body_from_file(format!( 114 | "{}/{}", 115 | mocks_path, body 116 | )); 117 | } 118 | 119 | if let Some(headers) = response.headers { 120 | for MockHeader { header, value } in headers { 121 | let value = value 122 | .replace("SERVER_URL", &mockito::server_url()); 123 | 124 | mock = mock.with_header(&header, &value[..]); 125 | } 126 | } 127 | 128 | mock.create() 129 | }) 130 | .collect::>(); 131 | 132 | (mockito::server_url(), mocks) 133 | }}; 134 | } 135 | --------------------------------------------------------------------------------