├── .devcontainer ├── Dockerfile.alpine └── devcontainer.json ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── DCO ├── LICENSE ├── README.md ├── announce.sh ├── build.sh ├── changelog.sh ├── docs ├── archetypes │ └── default.md ├── config.toml ├── content │ ├── documentation │ │ └── getting-started.md │ └── intro │ │ ├── feature-overview.md │ │ └── navigation.md └── themes │ └── purehugo │ ├── .gitignore │ ├── LICENSE.md │ ├── README.md │ ├── archetypes │ └── default.md │ ├── assets │ ├── css │ │ ├── blog.css │ │ └── syntax-highlighter.css │ └── js │ │ ├── jquery.min.js │ │ ├── jquery.prettysocial.min.js │ │ └── scripts.js │ ├── gulpfile.js │ ├── images │ ├── screenshot.png │ └── tn.png │ ├── layouts │ ├── _default │ │ ├── list.html │ │ └── single.html │ ├── index.html │ ├── partials │ │ ├── analytics.html │ │ ├── footer.html │ │ ├── header.html │ │ ├── pagination.html │ │ └── sidebar.html │ └── shortcodes │ │ └── img-responsive.html │ ├── package.json │ ├── static │ ├── css │ │ └── all.min.css │ └── js │ │ └── all.min.js │ └── theme.toml ├── floki-hugo.yaml ├── floki.yaml ├── renovate.json ├── src ├── cli.rs ├── command.rs ├── config.rs ├── dind.rs ├── environment.rs ├── errors.rs ├── image.rs ├── interpret.rs ├── main.rs ├── spec.rs └── volumes.rs └── test_resources ├── values.json └── values.yaml /.devcontainer/Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | FROM rust:1-alpine3.18 2 | 3 | ENV CARGO_BUILD_TARGET=x86_64-unknown-linux-musl 4 | 5 | RUN apk update && \ 6 | apk add bash musl-dev shadow && \ 7 | # Add a user for floki using the shadow utils 8 | useradd -Um -s /bin/bash floki 9 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "floki", 3 | "build": { "dockerfile": "Dockerfile.alpine" }, 4 | "remoteUser": "floki" 5 | } 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Why this change? 2 | 3 | Describe this change, including why it's needed. 4 | 5 | ## Relevant testing 6 | 7 | What testing has been done? 8 | 9 | ## Contributor notes 10 | 11 | Anything you think will be useful for reviewers. 12 | 13 | ## Checks 14 | 15 | These aren't hard requirements, just guidelines 16 | 17 | - [ ] New/modified Rust code formatted with `cargo fmt` 18 | - [ ] Documentation and `README.md` updated for this change, if necessary 19 | - [ ] `CHANGELOG.md` updated for this change 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: floki-ci 2 | on: 3 | push: 4 | tags: 5 | # Full version 6 | - "[0-9]+.[0-9]+.[0-9]+" 7 | # Prerelease version 8 | - "[0-9]+.[0-9]+.[0-9]+-*" 9 | 10 | pull_request: 11 | branches: 12 | # Trigger on pull requests into main 13 | - main 14 | types: [ opened, synchronize ] 15 | 16 | jobs: 17 | lint: 18 | name: Linting and Formatting 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Install rust 23 | uses: dtolnay/rust-toolchain@master 24 | with: 25 | toolchain: stable 26 | - name: Run cargo clippy to pick up any errors 27 | run: cargo clippy --all-targets -- -Dwarnings 28 | - name: Check code is formatted 29 | run: cargo fmt -- --check 30 | 31 | build: 32 | name: Build static binary for publishing 33 | runs-on: ${{ matrix.os }} 34 | continue-on-error: ${{ matrix.experimental }} 35 | strategy: 36 | matrix: 37 | os: 38 | - ubuntu-24.04 39 | - macos-latest 40 | rust: 41 | - stable 42 | - beta 43 | experimental: [false] 44 | include: 45 | - os: ubuntu-24.04 46 | rust: nightly 47 | experimental: true 48 | steps: 49 | - uses: actions/checkout@v4 50 | - name: Install rust 51 | uses: dtolnay/rust-toolchain@master 52 | with: 53 | toolchain: ${{ matrix.rust }} 54 | - name: Install cargo-get 55 | run: cargo install cargo-get 56 | - name: Run tests 57 | run: cargo test --all-features 58 | - run: "./build.sh" 59 | env: 60 | OS_NAME: ${{ matrix.os }} 61 | - name: Archive artifacts 62 | uses: actions/upload-artifact@v4 63 | if: ${{ matrix.rust == 'stable' }} 64 | with: 65 | name: stableartifacts-${{ matrix.os }} 66 | path: | 67 | floki*.zip 68 | floki*.tar.gz 69 | 70 | publish: 71 | name: Publish release artifact 72 | runs-on: ubuntu-latest 73 | if: github.ref_type == 'tag' 74 | needs: 75 | - build 76 | # Required to publish a release 77 | permissions: 78 | contents: write 79 | steps: 80 | - uses: actions/checkout@v4 81 | - name: Install rust 82 | uses: dtolnay/rust-toolchain@master 83 | with: 84 | toolchain: stable 85 | - name: Install cargo-get 86 | run: cargo install cargo-get 87 | - name: Publish to crates.io 88 | run: cargo publish 89 | env: 90 | CARGO_REGISTRY_TOKEN: ${{ secrets.PUBLISH_SECRET }} 91 | # After publishing, create a release 92 | - name: Download ubuntu artifacts 93 | uses: actions/download-artifact@v4 94 | with: 95 | name: stableartifacts-ubuntu-24.04 96 | - name: Download macos artifacts 97 | uses: actions/download-artifact@v4 98 | with: 99 | name: stableartifacts-macos-latest 100 | - name: Generate release.txt 101 | run: "./changelog.sh" 102 | - name: Release 103 | uses: softprops/action-gh-release@v2 104 | with: 105 | body_path: release.txt 106 | files: | 107 | floki*.zip 108 | floki*.tar.gz 109 | # # Announce the release 110 | # - run: "./announce.sh" 111 | # env: 112 | # ANNOUNCE_HOOK: ${{ secrets.ANNOUNCE_HOOK }} 113 | 114 | publish-dry-run: 115 | name: Dry-run publish for non-release artifact 116 | runs-on: ubuntu-latest 117 | if: github.ref_type != 'tag' 118 | needs: 119 | - build 120 | steps: 121 | - uses: actions/checkout@v4 122 | - name: Install rust 123 | uses: dtolnay/rust-toolchain@master 124 | with: 125 | toolchain: stable 126 | - name: Install cargo-get 127 | run: cargo install cargo-get 128 | - name: Dry-run publish on non-tags 129 | run: cargo publish --dry-run 130 | # Test downloading the artifacts 131 | - name: Download ubuntu artifacts 132 | uses: actions/download-artifact@v4 133 | with: 134 | name: stableartifacts-ubuntu-24.04 135 | - name: Download macos artifacts 136 | uses: actions/download-artifact@v4 137 | with: 138 | name: stableartifacts-macos-latest 139 | # Test generating release.txt 140 | - name: Generate release.txt 141 | run: "./changelog.sh" 142 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | public 2 | /target/ 3 | release.txt -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metaswitch/floki/f524b736d41c6553b9f1d6c8f5eaa7b770aa356c/.gitmodules -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This file's format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). The 7 | version number is tracked in the file `Cargo.toml`. 8 | 9 | Contact: See Cargo.toml authors 10 | Status: Available for use 11 | 12 | ## [Unreleased] 13 | 14 | ### Breaking Changes 15 | 16 | ### Added 17 | 18 | ### Fixed 19 | 20 | ## [2.1.0] - 2024-09-26 21 | 22 | ### Added 23 | - Add `FLOKI_WORKING_DIR` environment variable that is accessible inside the 24 | container. Can be used when `mount:` has been specified in `floki.yaml`. 25 | 26 | ## [2.0.1] - 2024-05-15 27 | 28 | ### Fixed 29 | - Fix publishing releases 30 | 31 | ## [2.0.0] - 2024-05-15 32 | 33 | ### Breaking Changes 34 | - Change entrypoint suppression to allow entrypoints by default 35 | 36 | ### Fixed 37 | 38 | - Switch to yaml-rust2. 39 | 40 | ## [1.2.1] - 2023-07-20 41 | 42 | ### Fixed 43 | - #62: Use shell_words to split the outer shell so that more complex outer shells can be used. 44 | - Load values from files relative to the floki config file, not from the 45 | current working directory. 46 | - Optimise the release build for size 47 | 48 | ## [1.2.0] - 2023-07-07 49 | 50 | ### Added 51 | - Add `yaml` function to tera templating engine so that floki templates 52 | can use `yaml(file="")` in order to load values. 53 | - Add `json` function to tera templating engine so that floki templates 54 | can use `json(file="")` in order to load values. 55 | - Add `toml` function to tera templating engine so that floki templates 56 | can use `toml(file="")` in order to load values. 57 | - Add `floki render` to print out the rendered configuration template. 58 | - Switch rust image to `rust:1-alpine3.18` as it's better maintained. 59 | - Combine errors into a single FlokiError enum. 60 | 61 | ## [1.1.0] - 2023-07-05 62 | 63 | ### Added 64 | - Floki templates are parsed with `tera` before being deserialized. 65 | - Limit MSRV to 1.57 while using `ekidd/rust-musl-builder` - needs updating. 66 | 67 | ## [1.0.1] - 2023-04-18 68 | 69 | ### Fixed 70 | - Don't add "-i" to docker commands when stdin is not a tty. This allows commands `floki run -- ` to be run from inside other scripts even if stdout looks like a tty. 71 | 72 | ## [1.0.0] - 2022-09-12 73 | 74 | ### Breaking Changes 75 | 76 | - Move to version 1.0.0 - so long, ZeroVer! 77 | - No other functional changes. 78 | 79 | ## [0.9.1] - 2022-06-28 80 | 81 | ### Fixed 82 | - Don't add "-i" to docker command when invoked without a tty. This allows a one-off `floki run -- ` to be run from a non-interactive shell. 83 | 84 | ## [0.9.0] - 2022-05-11 85 | 86 | ### Added 87 | - set `tls=false` for dind 88 | 89 | ## [0.8.0] - 2022-01-25 90 | 91 | ### Added 92 | - Change docker in docker image tag from `stable-dind` to `dind` 93 | 94 | ### Fixed 95 | 96 | - Fix up clippy warnings and enforce clippy going forward 97 | - `image_exists_locally` now checks that the specified image exists 98 | 99 | ## [0.7.1] - 2021-12-08 100 | 101 | ### Fixed 102 | 103 | - Adjust SSH agent forwarding error message 104 | - Fixed Github releases 105 | 106 | ## [0.7.0] - 2021-11-16 107 | 108 | ### Changed 109 | 110 | - Automatically suppress the container entrypoint - MAJOR 111 | - Update simplelog from 0.9 to 0.11 - MINOR 112 | - Rework internals to have clear data structure for the runtime configuration - MINOR 113 | - Disable Bors as it's not gaining us much. 114 | 115 | ### Added 116 | 117 | - Allow entrypoint suppression to be set in the configuration file - MINOR 118 | - Add exec mode for generating images - MINOR 119 | 120 | ### Fixed 121 | - Fix up old dependencies; move to thiserror/anyhow instead of failure - MINOR 122 | - Publish tags to crates.io - PATCH 123 | 124 | ## [0.6.2] - 2021-03-03 125 | 126 | ### Changed 127 | - Some improvements to the getting started guide 128 | - Some improvements to the feature documentation (including adding how to specify the `dind` image). 129 | 130 | ### Added 131 | - Added Bors support 132 | - Added a navigation section to the documentation (temporary until documentation format is redesigned) 133 | - Added a floki config file for quickly running `hugo` for building and testing documentation 134 | 135 | ### Fixed 136 | - Don't require a floki config file to be present to run `floki completion` 137 | - Properly parse `docker_switches` in accordance with shell quoting rules 138 | 139 | ## [0.6.1] - 2020-07-16 140 | 141 | ### Fixed 142 | - Fixed bug in new `add_docker_switch` behaviour. 143 | 144 | ## [0.6.0] - 2020-07-15 145 | 146 | ### Changed 147 | - Cleanup of resolution of working directory to use proper path types - PATCH 148 | - Cleanup and improve handling of user uid and gid - PATCH 149 | 150 | ### Added 151 | 152 | - **New Subcommand**: `completion` :tada: 153 | + Generation of shell :shell: completion added for an assortment of shells: 154 | bash, fish, zsh, powershell, elvish. 155 | + See `floki completion --help` 156 | 157 | ## [0.5.0] - 2020-05-05 158 | 159 | ### Changed 160 | - Change `after_deploy.sh` into a separate stage as `after_deploy:` scripts run after each `deploy:` step. 161 | - Resolve Dockerfile path and Docker context correctly when running `floki` from a subdirectory of the directory containing `floki.yaml` - PATCH 162 | - Correct and refine path handling - PATCH 163 | - Correct and refine environment variable handling - PATCH 164 | - Update Rust dependencies (run `cargo update`) - PATCH 165 | - Bump serde_yaml from 0.7.5 to 0.8.4 - PATCH 166 | - Bump structopt from 0.2.18 to 0.3.13 - PATCH 167 | - Bump uuid from 0.6.5 to 0.8.1 - PATCH 168 | - Rework logging and verbosity setting to use simplelog (needed because of `structopt` upgrade) - PATCH 169 | - `--local` CLI flag is no longer required to use `docker_switches` - MINOR 170 | 171 | ### Added 172 | - Allow the docker-in-docker image to be specified in configuration - MINOR 173 | 174 | ## [0.4.3] - 2019-12-02 175 | 176 | ### Changed 177 | 178 | ### Added 179 | 180 | ## [0.4.2] - 2019-12-02 181 | 182 | ### Changed 183 | 184 | ### Added 185 | - Add support for specifying Dockerfile build target - MINOR 186 | - Add `announce` support for notifying Slack of new changes - MINOR 187 | - Fix up build.sh for Linux builds - PATCH 188 | 189 | ## [0.4.1] - 2019-11-12 190 | 191 | ### Changed 192 | - Attempt to fix up jobs and deployment to Cargo 193 | 194 | ### Added 195 | 196 | ## [0.4.0] - 2019-11-07 197 | 198 | ### Changed 199 | - Deploy tagged versions to crates.io - MINOR 200 | - Generalize `DockerCommandBuilder` and refactor docker-in-docker function to use it - PATCH 201 | 202 | ### Added 203 | - Add support for `floki` volumes. These can be used for caching build artifacts - MINOR 204 | 205 | ## [0.3.0] - 2019-10-01 206 | 207 | ### Changed 208 | - Rename `FLOKI_HOST_WORKDIR` to `FLOKI_HOST_MOUNTDIR` - BREAKING 209 | - Also search ancestors of the working directory for a `floki.yaml` - MINOR 210 | - Make parsing of `floki.yaml` strict - deny unknown fields - BREAKING 211 | - Parse `floki.yaml` using `Read` interface to file - PATCH 212 | ### Added 213 | 214 | ## [0.2.0] - 2019-08-10 215 | 216 | ### Changed 217 | - Small tidyups of environment collection module - PATCH 218 | - Disable TLS in `dind` to fix failing `dind` functionality on newer `dind:stable` images - PATCH 219 | ### Added 220 | - Forward host working directory as `FLOKI_HOST_WORKDIR` - MINOR 221 | 222 | ## [0.1.0] - 2019-05-26 223 | 224 | ### Changed 225 | - Remove `forward_tmux_socket` - BREAKING 226 | - Remove `--pull` switch - BREAKING 227 | - Remove pull specifications from configuration file - BREAKING 228 | - Refactor to collect environment at start of day - PATCH 229 | - Only mount the ssh_agent socket file - BREAKING 230 | - Start working in the mount_pwd path - BREAKING 231 | - Rename mount_pwd to mount - BREAKING 232 | - Enforce reproducibility (override with `--local`) - BREAKING 233 | - Move from `trim_right` to `trim_end` - PATCH 234 | - (Refactor) Simplify addition of environment variables to docker run - PATCH 235 | - Refactor - PATCH 236 | - Add Travis CI file - PATCH 237 | - Use 2018 edition of rust. - PATCH 238 | - Update quicli to 0.4 - PATCH 239 | - Deploy to GitHub - PATCH 240 | - Make `sh` the default shell - BREAKING 241 | 242 | ### Added 243 | - Make `pull` a subcommand of `floki` - MINOR 244 | 245 | ## [0.0.20] - 2019-02-12 246 | 247 | ### Changed 248 | 249 | ### Added 250 | - Expose host user id as FLOKI_HOST_UID - MINOR 251 | - Expose host user id as FLOKI_HOST_GID - MINOR 252 | - Allow inner and outer shells to be specified - MINOR 253 | 254 | ## [0.0.19] - 2018-10-23 255 | 256 | ### Changed 257 | 258 | - Exit if an `init` command fails (as opposed to carrying on) - BREAKING 259 | - Make sure `floki` detects docker errors properly - BUGFIX 260 | - Non-zero exit code on error - BUGFIX 261 | 262 | ## [0.0.18] - 2018-10-05 263 | 264 | ### Changed 265 | - Make `floki run` work properly with subcommand switches - BUGFIX 266 | - Make sure floki errors if `docker build` fails - BUGFIX 267 | 268 | ## [0.0.17] - 2018-10-02 269 | 270 | ### Added 271 | - Package floki in an RPM - PATCH 272 | - Add `floki run` subcommand - PATCH 273 | 274 | ## [0.0.16] - 2018-09-10 275 | 276 | ### Changed 277 | - Wrapped common docker errors to make them clearer - PATCH 278 | 279 | ## [0.0.15] - 2018-08-08 280 | 281 | ### Changed 282 | - Only kill `dind` container if we launched it - BUGFIX 283 | 284 | ## [0.0.14] - 2018-08-08 285 | 286 | ### Added 287 | - --pull switch to update images - PATCH 288 | ### Fixed 289 | - Fixup docker-in-docker to allow bind mounts - PATCH 290 | 291 | ## [0.0.13] - 2018-08-06 292 | 293 | ### Added 294 | - docker-in-docker support - PATCH 295 | - Add ability to forward current user - PATCH 296 | 297 | ## [0.0.12] - 2018-07-31 298 | 299 | ### Changed 300 | - Made tmux socket forwarding permissive (doesn't fail if not found) - PATCH 301 | 302 | ## [0.0.11] - 2018-07-31 303 | 304 | ### Changed 305 | - Build spec now requires the name as a subkey of build - BREAKING 306 | - forward_tmux_session -> forward_tmux_socket - BREAKING 307 | 308 | ### Added 309 | - Rewrite in Rust - PATCH 310 | - Sphinx docs - PATCH 311 | 312 | ## [0.0.10] - 2018-07-25 313 | 314 | ### Added 315 | - Allow custom docker switches - PATCH 316 | - Configurable pull policy - PATCH 317 | 318 | ## [0.0.9] - 2018-07-12 319 | 320 | ### Added 321 | - Add a version switch - PATCH 322 | 323 | ### Changed 324 | - Make docker not use sudo - PATCH 325 | 326 | ## [0.0.8] - 2018-07-11 327 | 328 | ### Changed 329 | - Empty init defaults to no commands - BUGFIX 330 | - Make image specification mandatory - PATCH 331 | 332 | ## [0.0.7] - 2018-07-10 333 | 334 | ### Changed 335 | - Change how we specify an image to build - PATCH 336 | 337 | ## [0.0.6] - 2018-07-10 338 | 339 | ### Added 340 | - Add option to forward tmux socket - PATCH 341 | - Add basic configuration validation - PATCH 342 | - Added ability to specify shell - PATCH 343 | - Add BSD style help switch - PATCH 344 | 345 | ## [0.0.5] - 2018-07-03 346 | 347 | ### Added 348 | - Config file now command line parameter. Default still `./floki.yaml` 349 | 350 | ## [0.0.4] - 2018-04-06 351 | 352 | ### Changed 353 | - Allow build container to originate from Dockerfile - PATCH 354 | 355 | ## [0.0.3] - 2018-04-06 356 | 357 | ### Changed 358 | - Rename to ssh-agent forwarding field - PATCH 359 | 360 | ## [0.0.2] - 2018-04-06 361 | 362 | ### Changed 363 | - Rename to floki to prevent conflicts on pypi - PATCH 364 | 365 | ## [0.0.1] - 2018-04-06 366 | 367 | ### Added 368 | - Initial primitive version 369 | ### Changed 370 | 371 | [unreleased]: https://github.com/Metaswitch/floki/compare/2.1.0...HEAD 372 | [2.1.0]: https://github.com/Metaswitch/floki/compare/2.0.1...2.1.0 373 | [2.0.1]: https://github.com/Metaswitch/floki/compare/2.0.0...2.0.1 374 | [2.0.0]: https://github.com/Metaswitch/floki/compare/1.2.1...2.0.0 375 | [1.2.1]: https://github.com/Metaswitch/floki/compare/1.2.0...1.2.1 376 | [1.2.0]: https://github.com/Metaswitch/floki/compare/1.1.0...1.2.0 377 | [1.1.0]: https://github.com/Metaswitch/floki/compare/1.0.1...1.1.0 378 | [1.0.1]: https://github.com/Metaswitch/floki/compare/1.0.0...1.0.1 379 | [1.0.0]: https://github.com/Metaswitch/floki/compare/0.9.1...1.0.0 380 | [0.9.1]: https://github.com/Metaswitch/floki/compare/0.9.0...0.9.1 381 | [0.9.0]: https://github.com/Metaswitch/floki/compare/0.8.0...0.9.0 382 | [0.8.0]: https://github.com/Metaswitch/floki/compare/0.7.1...0.8.0 383 | [0.7.1]: https://github.com/Metaswitch/floki/compare/0.7.0...0.7.1 384 | [0.7.0]: https://github.com/Metaswitch/floki/compare/0.6.2...0.7.0 385 | [0.6.2]: https://github.com/Metaswitch/floki/compare/0.6.1...0.6.2 386 | [0.6.1]: https://github.com/Metaswitch/floki/compare/0.6.0...0.6.1 387 | [0.6.0]: https://github.com/Metaswitch/floki/compare/0.5.0...0.6.0 388 | [0.5.0]: https://github.com/Metaswitch/floki/compare/0.4.3...0.5.0 389 | [0.4.3]: https://github.com/Metaswitch/floki/compare/0.4.2...0.4.3 390 | [0.4.2]: https://github.com/Metaswitch/floki/compare/0.4.1...0.4.2 391 | [0.4.1]: https://github.com/Metaswitch/floki/compare/0.4.0...0.4.1 392 | [0.4.0]: https://github.com/Metaswitch/floki/compare/0.3.0...0.4.0 393 | [0.3.0]: https://github.com/Metaswitch/floki/compare/0.2.0...0.3.0 394 | [0.2.0]: https://github.com/Metaswitch/floki/compare/0.1.0...0.2.0 395 | [0.1.0]: https://github.com/Metaswitch/floki/compare/0.0.20...0.1.0 396 | [0.0.20]: https://github.com/Metaswitch/floki/compare/0.0.19...0.0.20 397 | [0.0.19]: https://github.com/Metaswitch/floki/compare/0.0.18...0.0.19 398 | [0.0.18]: https://github.com/Metaswitch/floki/compare/0.0.17...0.0.18 399 | [0.0.17]: https://github.com/Metaswitch/floki/compare/0.0.16...0.0.17 400 | [0.0.16]: https://github.com/Metaswitch/floki/compare/0.0.15...0.0.16 401 | [0.0.15]: https://github.com/Metaswitch/floki/compare/0.0.14...0.0.15 402 | [0.0.14]: https://github.com/Metaswitch/floki/compare/0.0.13...0.0.14 403 | [0.0.13]: https://github.com/Metaswitch/floki/compare/0.0.12...0.0.13 404 | [0.0.12]: https://github.com/Metaswitch/floki/compare/0.0.11...0.0.12 405 | [0.0.11]: https://github.com/Metaswitch/floki/compare/0.0.10...0.0.11 406 | [0.0.10]: https://github.com/Metaswitch/floki/compare/0.0.9...0.0.10 407 | [0.0.9]: https://github.com/Metaswitch/floki/compare/0.0.8...0.0.9 408 | [0.0.8]: https://github.com/Metaswitch/floki/compare/0.0.7...0.0.8 409 | [0.0.7]: https://github.com/Metaswitch/floki/compare/0.0.6...0.0.7 410 | [0.0.6]: https://github.com/Metaswitch/floki/compare/0.0.5...0.0.6 411 | [0.0.5]: https://github.com/Metaswitch/floki/compare/0.0.4...0.0.5 412 | [0.0.4]: https://github.com/Metaswitch/floki/compare/0.0.3...0.0.4 413 | [0.0.3]: https://github.com/Metaswitch/floki/compare/0.0.2...0.0.3 414 | [0.0.2]: https://github.com/Metaswitch/floki/compare/0.0.1...0.0.2 415 | [0.0.1]: https://github.com/Metaswitch/floki/tree/0.0.1 416 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android-tzdata" 16 | version = "0.1.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 19 | 20 | [[package]] 21 | name = "android_system_properties" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 | dependencies = [ 26 | "libc", 27 | ] 28 | 29 | [[package]] 30 | name = "ansi_term" 31 | version = "0.12.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 34 | dependencies = [ 35 | "winapi", 36 | ] 37 | 38 | [[package]] 39 | name = "anyhow" 40 | version = "1.0.98" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 43 | 44 | [[package]] 45 | name = "arraydeque" 46 | version = "0.5.1" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" 49 | 50 | [[package]] 51 | name = "atty" 52 | version = "0.2.14" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 55 | dependencies = [ 56 | "hermit-abi", 57 | "libc", 58 | "winapi", 59 | ] 60 | 61 | [[package]] 62 | name = "autocfg" 63 | version = "1.1.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 66 | 67 | [[package]] 68 | name = "bitflags" 69 | version = "1.3.2" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 72 | 73 | [[package]] 74 | name = "bitflags" 75 | version = "2.4.1" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 78 | 79 | [[package]] 80 | name = "block-buffer" 81 | version = "0.10.4" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 84 | dependencies = [ 85 | "generic-array", 86 | ] 87 | 88 | [[package]] 89 | name = "bstr" 90 | version = "1.9.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" 93 | dependencies = [ 94 | "memchr", 95 | "serde", 96 | ] 97 | 98 | [[package]] 99 | name = "bumpalo" 100 | version = "3.14.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 103 | 104 | [[package]] 105 | name = "cc" 106 | version = "1.0.83" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 109 | dependencies = [ 110 | "libc", 111 | ] 112 | 113 | [[package]] 114 | name = "cfg-if" 115 | version = "1.0.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 118 | 119 | [[package]] 120 | name = "cfg_aliases" 121 | version = "0.2.1" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 124 | 125 | [[package]] 126 | name = "chrono" 127 | version = "0.4.31" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" 130 | dependencies = [ 131 | "android-tzdata", 132 | "iana-time-zone", 133 | "num-traits", 134 | "windows-targets 0.48.5", 135 | ] 136 | 137 | [[package]] 138 | name = "chrono-tz" 139 | version = "0.9.0" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" 142 | dependencies = [ 143 | "chrono", 144 | "chrono-tz-build", 145 | "phf", 146 | ] 147 | 148 | [[package]] 149 | name = "chrono-tz-build" 150 | version = "0.3.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" 153 | dependencies = [ 154 | "parse-zoneinfo", 155 | "phf", 156 | "phf_codegen", 157 | ] 158 | 159 | [[package]] 160 | name = "clap" 161 | version = "2.34.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 164 | dependencies = [ 165 | "ansi_term", 166 | "atty", 167 | "bitflags 1.3.2", 168 | "strsim", 169 | "textwrap", 170 | "unicode-width", 171 | "vec_map", 172 | ] 173 | 174 | [[package]] 175 | name = "core-foundation-sys" 176 | version = "0.8.6" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 179 | 180 | [[package]] 181 | name = "cpufeatures" 182 | version = "0.2.11" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" 185 | dependencies = [ 186 | "libc", 187 | ] 188 | 189 | [[package]] 190 | name = "crossbeam-deque" 191 | version = "0.8.4" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" 194 | dependencies = [ 195 | "cfg-if", 196 | "crossbeam-epoch", 197 | "crossbeam-utils", 198 | ] 199 | 200 | [[package]] 201 | name = "crossbeam-epoch" 202 | version = "0.9.17" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" 205 | dependencies = [ 206 | "autocfg", 207 | "cfg-if", 208 | "crossbeam-utils", 209 | ] 210 | 211 | [[package]] 212 | name = "crossbeam-utils" 213 | version = "0.8.18" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" 216 | dependencies = [ 217 | "cfg-if", 218 | ] 219 | 220 | [[package]] 221 | name = "crypto-common" 222 | version = "0.1.6" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 225 | dependencies = [ 226 | "generic-array", 227 | "typenum", 228 | ] 229 | 230 | [[package]] 231 | name = "deranged" 232 | version = "0.3.11" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 235 | dependencies = [ 236 | "powerfmt", 237 | ] 238 | 239 | [[package]] 240 | name = "deunicode" 241 | version = "1.4.2" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "3ae2a35373c5c74340b79ae6780b498b2b183915ec5dacf263aac5a099bf485a" 244 | 245 | [[package]] 246 | name = "digest" 247 | version = "0.10.7" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 250 | dependencies = [ 251 | "block-buffer", 252 | "crypto-common", 253 | ] 254 | 255 | [[package]] 256 | name = "encoding_rs" 257 | version = "0.8.34" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" 260 | dependencies = [ 261 | "cfg-if", 262 | ] 263 | 264 | [[package]] 265 | name = "equivalent" 266 | version = "1.0.1" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 269 | 270 | [[package]] 271 | name = "errno" 272 | version = "0.3.10" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 275 | dependencies = [ 276 | "libc", 277 | "windows-sys", 278 | ] 279 | 280 | [[package]] 281 | name = "fastrand" 282 | version = "2.1.1" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" 285 | 286 | [[package]] 287 | name = "floki" 288 | version = "2.1.0" 289 | dependencies = [ 290 | "anyhow", 291 | "atty", 292 | "log", 293 | "nix", 294 | "serde", 295 | "serde_json", 296 | "serde_yaml", 297 | "sha2", 298 | "shell-words", 299 | "shlex", 300 | "simplelog", 301 | "structopt", 302 | "tempfile", 303 | "tera", 304 | "thiserror 2.0.12", 305 | "toml", 306 | "uuid", 307 | "yaml-rust2", 308 | ] 309 | 310 | [[package]] 311 | name = "foldhash" 312 | version = "0.1.4" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" 315 | 316 | [[package]] 317 | name = "generic-array" 318 | version = "0.14.7" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 321 | dependencies = [ 322 | "typenum", 323 | "version_check", 324 | ] 325 | 326 | [[package]] 327 | name = "getrandom" 328 | version = "0.2.11" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" 331 | dependencies = [ 332 | "cfg-if", 333 | "libc", 334 | "wasi 0.11.0+wasi-snapshot-preview1", 335 | ] 336 | 337 | [[package]] 338 | name = "getrandom" 339 | version = "0.3.1" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 342 | dependencies = [ 343 | "cfg-if", 344 | "libc", 345 | "wasi 0.13.3+wasi-0.2.2", 346 | "windows-targets 0.52.6", 347 | ] 348 | 349 | [[package]] 350 | name = "globset" 351 | version = "0.4.14" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" 354 | dependencies = [ 355 | "aho-corasick", 356 | "bstr", 357 | "log", 358 | "regex-automata", 359 | "regex-syntax", 360 | ] 361 | 362 | [[package]] 363 | name = "globwalk" 364 | version = "0.9.1" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" 367 | dependencies = [ 368 | "bitflags 2.4.1", 369 | "ignore", 370 | "walkdir", 371 | ] 372 | 373 | [[package]] 374 | name = "hashbrown" 375 | version = "0.15.2" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 378 | dependencies = [ 379 | "foldhash", 380 | ] 381 | 382 | [[package]] 383 | name = "hashlink" 384 | version = "0.10.0" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 387 | dependencies = [ 388 | "hashbrown", 389 | ] 390 | 391 | [[package]] 392 | name = "heck" 393 | version = "0.3.3" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 396 | dependencies = [ 397 | "unicode-segmentation", 398 | ] 399 | 400 | [[package]] 401 | name = "hermit-abi" 402 | version = "0.1.19" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 405 | dependencies = [ 406 | "libc", 407 | ] 408 | 409 | [[package]] 410 | name = "humansize" 411 | version = "2.1.3" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" 414 | dependencies = [ 415 | "libm", 416 | ] 417 | 418 | [[package]] 419 | name = "iana-time-zone" 420 | version = "0.1.59" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" 423 | dependencies = [ 424 | "android_system_properties", 425 | "core-foundation-sys", 426 | "iana-time-zone-haiku", 427 | "js-sys", 428 | "wasm-bindgen", 429 | "windows-core", 430 | ] 431 | 432 | [[package]] 433 | name = "iana-time-zone-haiku" 434 | version = "0.1.2" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 437 | dependencies = [ 438 | "cc", 439 | ] 440 | 441 | [[package]] 442 | name = "ignore" 443 | version = "0.4.21" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060" 446 | dependencies = [ 447 | "crossbeam-deque", 448 | "globset", 449 | "log", 450 | "memchr", 451 | "regex-automata", 452 | "same-file", 453 | "walkdir", 454 | "winapi-util", 455 | ] 456 | 457 | [[package]] 458 | name = "indexmap" 459 | version = "2.7.1" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 462 | dependencies = [ 463 | "equivalent", 464 | "hashbrown", 465 | ] 466 | 467 | [[package]] 468 | name = "itoa" 469 | version = "1.0.10" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 472 | 473 | [[package]] 474 | name = "js-sys" 475 | version = "0.3.66" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" 478 | dependencies = [ 479 | "wasm-bindgen", 480 | ] 481 | 482 | [[package]] 483 | name = "lazy_static" 484 | version = "1.4.0" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 487 | 488 | [[package]] 489 | name = "libc" 490 | version = "0.2.172" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 493 | 494 | [[package]] 495 | name = "libm" 496 | version = "0.2.8" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" 499 | 500 | [[package]] 501 | name = "linux-raw-sys" 502 | version = "0.9.2" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" 505 | 506 | [[package]] 507 | name = "log" 508 | version = "0.4.27" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 511 | 512 | [[package]] 513 | name = "memchr" 514 | version = "2.7.1" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 517 | 518 | [[package]] 519 | name = "nix" 520 | version = "0.30.1" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 523 | dependencies = [ 524 | "bitflags 2.4.1", 525 | "cfg-if", 526 | "cfg_aliases", 527 | "libc", 528 | ] 529 | 530 | [[package]] 531 | name = "num-conv" 532 | version = "0.1.0" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 535 | 536 | [[package]] 537 | name = "num-traits" 538 | version = "0.2.17" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 541 | dependencies = [ 542 | "autocfg", 543 | ] 544 | 545 | [[package]] 546 | name = "num_threads" 547 | version = "0.1.6" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" 550 | dependencies = [ 551 | "libc", 552 | ] 553 | 554 | [[package]] 555 | name = "once_cell" 556 | version = "1.19.0" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 559 | 560 | [[package]] 561 | name = "parse-zoneinfo" 562 | version = "0.3.0" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" 565 | dependencies = [ 566 | "regex", 567 | ] 568 | 569 | [[package]] 570 | name = "percent-encoding" 571 | version = "2.3.1" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 574 | 575 | [[package]] 576 | name = "pest" 577 | version = "2.7.5" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" 580 | dependencies = [ 581 | "memchr", 582 | "thiserror 1.0.68", 583 | "ucd-trie", 584 | ] 585 | 586 | [[package]] 587 | name = "pest_derive" 588 | version = "2.7.5" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" 591 | dependencies = [ 592 | "pest", 593 | "pest_generator", 594 | ] 595 | 596 | [[package]] 597 | name = "pest_generator" 598 | version = "2.7.5" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" 601 | dependencies = [ 602 | "pest", 603 | "pest_meta", 604 | "proc-macro2", 605 | "quote", 606 | "syn 2.0.87", 607 | ] 608 | 609 | [[package]] 610 | name = "pest_meta" 611 | version = "2.7.5" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" 614 | dependencies = [ 615 | "once_cell", 616 | "pest", 617 | "sha2", 618 | ] 619 | 620 | [[package]] 621 | name = "phf" 622 | version = "0.11.2" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" 625 | dependencies = [ 626 | "phf_shared", 627 | ] 628 | 629 | [[package]] 630 | name = "phf_codegen" 631 | version = "0.11.2" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" 634 | dependencies = [ 635 | "phf_generator", 636 | "phf_shared", 637 | ] 638 | 639 | [[package]] 640 | name = "phf_generator" 641 | version = "0.11.2" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" 644 | dependencies = [ 645 | "phf_shared", 646 | "rand", 647 | ] 648 | 649 | [[package]] 650 | name = "phf_shared" 651 | version = "0.11.2" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" 654 | dependencies = [ 655 | "siphasher", 656 | ] 657 | 658 | [[package]] 659 | name = "powerfmt" 660 | version = "0.2.0" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 663 | 664 | [[package]] 665 | name = "ppv-lite86" 666 | version = "0.2.17" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 669 | 670 | [[package]] 671 | name = "proc-macro-error" 672 | version = "1.0.4" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 675 | dependencies = [ 676 | "proc-macro-error-attr", 677 | "proc-macro2", 678 | "quote", 679 | "syn 1.0.109", 680 | "version_check", 681 | ] 682 | 683 | [[package]] 684 | name = "proc-macro-error-attr" 685 | version = "1.0.4" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 688 | dependencies = [ 689 | "proc-macro2", 690 | "quote", 691 | "version_check", 692 | ] 693 | 694 | [[package]] 695 | name = "proc-macro2" 696 | version = "1.0.89" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 699 | dependencies = [ 700 | "unicode-ident", 701 | ] 702 | 703 | [[package]] 704 | name = "quote" 705 | version = "1.0.35" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 708 | dependencies = [ 709 | "proc-macro2", 710 | ] 711 | 712 | [[package]] 713 | name = "rand" 714 | version = "0.8.5" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 717 | dependencies = [ 718 | "libc", 719 | "rand_chacha", 720 | "rand_core", 721 | ] 722 | 723 | [[package]] 724 | name = "rand_chacha" 725 | version = "0.3.1" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 728 | dependencies = [ 729 | "ppv-lite86", 730 | "rand_core", 731 | ] 732 | 733 | [[package]] 734 | name = "rand_core" 735 | version = "0.6.4" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 738 | dependencies = [ 739 | "getrandom 0.2.11", 740 | ] 741 | 742 | [[package]] 743 | name = "regex" 744 | version = "1.10.2" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 747 | dependencies = [ 748 | "aho-corasick", 749 | "memchr", 750 | "regex-automata", 751 | "regex-syntax", 752 | ] 753 | 754 | [[package]] 755 | name = "regex-automata" 756 | version = "0.4.3" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 759 | dependencies = [ 760 | "aho-corasick", 761 | "memchr", 762 | "regex-syntax", 763 | ] 764 | 765 | [[package]] 766 | name = "regex-syntax" 767 | version = "0.8.2" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 770 | 771 | [[package]] 772 | name = "rustix" 773 | version = "1.0.2" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" 776 | dependencies = [ 777 | "bitflags 2.4.1", 778 | "errno", 779 | "libc", 780 | "linux-raw-sys", 781 | "windows-sys", 782 | ] 783 | 784 | [[package]] 785 | name = "ryu" 786 | version = "1.0.16" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 789 | 790 | [[package]] 791 | name = "same-file" 792 | version = "1.0.6" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 795 | dependencies = [ 796 | "winapi-util", 797 | ] 798 | 799 | [[package]] 800 | name = "serde" 801 | version = "1.0.219" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 804 | dependencies = [ 805 | "serde_derive", 806 | ] 807 | 808 | [[package]] 809 | name = "serde_derive" 810 | version = "1.0.219" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 813 | dependencies = [ 814 | "proc-macro2", 815 | "quote", 816 | "syn 2.0.87", 817 | ] 818 | 819 | [[package]] 820 | name = "serde_json" 821 | version = "1.0.140" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 824 | dependencies = [ 825 | "itoa", 826 | "memchr", 827 | "ryu", 828 | "serde", 829 | ] 830 | 831 | [[package]] 832 | name = "serde_spanned" 833 | version = "0.6.8" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 836 | dependencies = [ 837 | "serde", 838 | ] 839 | 840 | [[package]] 841 | name = "serde_yaml" 842 | version = "0.9.34+deprecated" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 845 | dependencies = [ 846 | "indexmap", 847 | "itoa", 848 | "ryu", 849 | "serde", 850 | "unsafe-libyaml", 851 | ] 852 | 853 | [[package]] 854 | name = "sha2" 855 | version = "0.10.9" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 858 | dependencies = [ 859 | "cfg-if", 860 | "cpufeatures", 861 | "digest", 862 | ] 863 | 864 | [[package]] 865 | name = "shell-words" 866 | version = "1.1.0" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 869 | 870 | [[package]] 871 | name = "shlex" 872 | version = "1.3.0" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 875 | 876 | [[package]] 877 | name = "simplelog" 878 | version = "0.12.2" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" 881 | dependencies = [ 882 | "log", 883 | "termcolor", 884 | "time", 885 | ] 886 | 887 | [[package]] 888 | name = "siphasher" 889 | version = "0.3.11" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 892 | 893 | [[package]] 894 | name = "slug" 895 | version = "0.1.5" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4" 898 | dependencies = [ 899 | "deunicode", 900 | "wasm-bindgen", 901 | ] 902 | 903 | [[package]] 904 | name = "strsim" 905 | version = "0.8.0" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 908 | 909 | [[package]] 910 | name = "structopt" 911 | version = "0.3.26" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 914 | dependencies = [ 915 | "clap", 916 | "lazy_static", 917 | "structopt-derive", 918 | ] 919 | 920 | [[package]] 921 | name = "structopt-derive" 922 | version = "0.4.18" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 925 | dependencies = [ 926 | "heck", 927 | "proc-macro-error", 928 | "proc-macro2", 929 | "quote", 930 | "syn 1.0.109", 931 | ] 932 | 933 | [[package]] 934 | name = "syn" 935 | version = "1.0.109" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 938 | dependencies = [ 939 | "proc-macro2", 940 | "quote", 941 | "unicode-ident", 942 | ] 943 | 944 | [[package]] 945 | name = "syn" 946 | version = "2.0.87" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" 949 | dependencies = [ 950 | "proc-macro2", 951 | "quote", 952 | "unicode-ident", 953 | ] 954 | 955 | [[package]] 956 | name = "tempfile" 957 | version = "3.20.0" 958 | source = "registry+https://github.com/rust-lang/crates.io-index" 959 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 960 | dependencies = [ 961 | "fastrand", 962 | "getrandom 0.3.1", 963 | "once_cell", 964 | "rustix", 965 | "windows-sys", 966 | ] 967 | 968 | [[package]] 969 | name = "tera" 970 | version = "1.20.0" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" 973 | dependencies = [ 974 | "chrono", 975 | "chrono-tz", 976 | "globwalk", 977 | "humansize", 978 | "lazy_static", 979 | "percent-encoding", 980 | "pest", 981 | "pest_derive", 982 | "rand", 983 | "regex", 984 | "serde", 985 | "serde_json", 986 | "slug", 987 | "unic-segment", 988 | ] 989 | 990 | [[package]] 991 | name = "termcolor" 992 | version = "1.1.3" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 995 | dependencies = [ 996 | "winapi-util", 997 | ] 998 | 999 | [[package]] 1000 | name = "textwrap" 1001 | version = "0.11.0" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 1004 | dependencies = [ 1005 | "unicode-width", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "thiserror" 1010 | version = "1.0.68" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" 1013 | dependencies = [ 1014 | "thiserror-impl 1.0.68", 1015 | ] 1016 | 1017 | [[package]] 1018 | name = "thiserror" 1019 | version = "2.0.12" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1022 | dependencies = [ 1023 | "thiserror-impl 2.0.12", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "thiserror-impl" 1028 | version = "1.0.68" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" 1031 | dependencies = [ 1032 | "proc-macro2", 1033 | "quote", 1034 | "syn 2.0.87", 1035 | ] 1036 | 1037 | [[package]] 1038 | name = "thiserror-impl" 1039 | version = "2.0.12" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1042 | dependencies = [ 1043 | "proc-macro2", 1044 | "quote", 1045 | "syn 2.0.87", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "time" 1050 | version = "0.3.36" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 1053 | dependencies = [ 1054 | "deranged", 1055 | "itoa", 1056 | "libc", 1057 | "num-conv", 1058 | "num_threads", 1059 | "powerfmt", 1060 | "serde", 1061 | "time-core", 1062 | "time-macros", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "time-core" 1067 | version = "0.1.2" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1070 | 1071 | [[package]] 1072 | name = "time-macros" 1073 | version = "0.2.18" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 1076 | dependencies = [ 1077 | "num-conv", 1078 | "time-core", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "toml" 1083 | version = "0.8.22" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" 1086 | dependencies = [ 1087 | "serde", 1088 | "serde_spanned", 1089 | "toml_datetime", 1090 | "toml_edit", 1091 | ] 1092 | 1093 | [[package]] 1094 | name = "toml_datetime" 1095 | version = "0.6.9" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" 1098 | dependencies = [ 1099 | "serde", 1100 | ] 1101 | 1102 | [[package]] 1103 | name = "toml_edit" 1104 | version = "0.22.26" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" 1107 | dependencies = [ 1108 | "indexmap", 1109 | "serde", 1110 | "serde_spanned", 1111 | "toml_datetime", 1112 | "toml_write", 1113 | "winnow", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "toml_write" 1118 | version = "0.1.1" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" 1121 | 1122 | [[package]] 1123 | name = "typenum" 1124 | version = "1.17.0" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1127 | 1128 | [[package]] 1129 | name = "ucd-trie" 1130 | version = "0.1.6" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" 1133 | 1134 | [[package]] 1135 | name = "unic-char-property" 1136 | version = "0.9.0" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" 1139 | dependencies = [ 1140 | "unic-char-range", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "unic-char-range" 1145 | version = "0.9.0" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" 1148 | 1149 | [[package]] 1150 | name = "unic-common" 1151 | version = "0.9.0" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" 1154 | 1155 | [[package]] 1156 | name = "unic-segment" 1157 | version = "0.9.0" 1158 | source = "registry+https://github.com/rust-lang/crates.io-index" 1159 | checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" 1160 | dependencies = [ 1161 | "unic-ucd-segment", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "unic-ucd-segment" 1166 | version = "0.9.0" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" 1169 | dependencies = [ 1170 | "unic-char-property", 1171 | "unic-char-range", 1172 | "unic-ucd-version", 1173 | ] 1174 | 1175 | [[package]] 1176 | name = "unic-ucd-version" 1177 | version = "0.9.0" 1178 | source = "registry+https://github.com/rust-lang/crates.io-index" 1179 | checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" 1180 | dependencies = [ 1181 | "unic-common", 1182 | ] 1183 | 1184 | [[package]] 1185 | name = "unicode-ident" 1186 | version = "1.0.12" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1189 | 1190 | [[package]] 1191 | name = "unicode-segmentation" 1192 | version = "1.10.1" 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" 1194 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 1195 | 1196 | [[package]] 1197 | name = "unicode-width" 1198 | version = "0.1.11" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 1201 | 1202 | [[package]] 1203 | name = "unsafe-libyaml" 1204 | version = "0.2.11" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 1207 | 1208 | [[package]] 1209 | name = "uuid" 1210 | version = "1.16.0" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" 1213 | dependencies = [ 1214 | "getrandom 0.3.1", 1215 | ] 1216 | 1217 | [[package]] 1218 | name = "vec_map" 1219 | version = "0.8.2" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 1222 | 1223 | [[package]] 1224 | name = "version_check" 1225 | version = "0.9.4" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1228 | 1229 | [[package]] 1230 | name = "walkdir" 1231 | version = "2.4.0" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 1234 | dependencies = [ 1235 | "same-file", 1236 | "winapi-util", 1237 | ] 1238 | 1239 | [[package]] 1240 | name = "wasi" 1241 | version = "0.11.0+wasi-snapshot-preview1" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1244 | 1245 | [[package]] 1246 | name = "wasi" 1247 | version = "0.13.3+wasi-0.2.2" 1248 | source = "registry+https://github.com/rust-lang/crates.io-index" 1249 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 1250 | dependencies = [ 1251 | "wit-bindgen-rt", 1252 | ] 1253 | 1254 | [[package]] 1255 | name = "wasm-bindgen" 1256 | version = "0.2.89" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" 1259 | dependencies = [ 1260 | "cfg-if", 1261 | "wasm-bindgen-macro", 1262 | ] 1263 | 1264 | [[package]] 1265 | name = "wasm-bindgen-backend" 1266 | version = "0.2.89" 1267 | source = "registry+https://github.com/rust-lang/crates.io-index" 1268 | checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" 1269 | dependencies = [ 1270 | "bumpalo", 1271 | "log", 1272 | "once_cell", 1273 | "proc-macro2", 1274 | "quote", 1275 | "syn 2.0.87", 1276 | "wasm-bindgen-shared", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "wasm-bindgen-macro" 1281 | version = "0.2.89" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" 1284 | dependencies = [ 1285 | "quote", 1286 | "wasm-bindgen-macro-support", 1287 | ] 1288 | 1289 | [[package]] 1290 | name = "wasm-bindgen-macro-support" 1291 | version = "0.2.89" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" 1294 | dependencies = [ 1295 | "proc-macro2", 1296 | "quote", 1297 | "syn 2.0.87", 1298 | "wasm-bindgen-backend", 1299 | "wasm-bindgen-shared", 1300 | ] 1301 | 1302 | [[package]] 1303 | name = "wasm-bindgen-shared" 1304 | version = "0.2.89" 1305 | source = "registry+https://github.com/rust-lang/crates.io-index" 1306 | checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" 1307 | 1308 | [[package]] 1309 | name = "winapi" 1310 | version = "0.3.9" 1311 | source = "registry+https://github.com/rust-lang/crates.io-index" 1312 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1313 | dependencies = [ 1314 | "winapi-i686-pc-windows-gnu", 1315 | "winapi-x86_64-pc-windows-gnu", 1316 | ] 1317 | 1318 | [[package]] 1319 | name = "winapi-i686-pc-windows-gnu" 1320 | version = "0.4.0" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1323 | 1324 | [[package]] 1325 | name = "winapi-util" 1326 | version = "0.1.6" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 1329 | dependencies = [ 1330 | "winapi", 1331 | ] 1332 | 1333 | [[package]] 1334 | name = "winapi-x86_64-pc-windows-gnu" 1335 | version = "0.4.0" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1338 | 1339 | [[package]] 1340 | name = "windows-core" 1341 | version = "0.52.0" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1344 | dependencies = [ 1345 | "windows-targets 0.52.6", 1346 | ] 1347 | 1348 | [[package]] 1349 | name = "windows-sys" 1350 | version = "0.59.0" 1351 | source = "registry+https://github.com/rust-lang/crates.io-index" 1352 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1353 | dependencies = [ 1354 | "windows-targets 0.52.6", 1355 | ] 1356 | 1357 | [[package]] 1358 | name = "windows-targets" 1359 | version = "0.48.5" 1360 | source = "registry+https://github.com/rust-lang/crates.io-index" 1361 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1362 | dependencies = [ 1363 | "windows_aarch64_gnullvm 0.48.5", 1364 | "windows_aarch64_msvc 0.48.5", 1365 | "windows_i686_gnu 0.48.5", 1366 | "windows_i686_msvc 0.48.5", 1367 | "windows_x86_64_gnu 0.48.5", 1368 | "windows_x86_64_gnullvm 0.48.5", 1369 | "windows_x86_64_msvc 0.48.5", 1370 | ] 1371 | 1372 | [[package]] 1373 | name = "windows-targets" 1374 | version = "0.52.6" 1375 | source = "registry+https://github.com/rust-lang/crates.io-index" 1376 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1377 | dependencies = [ 1378 | "windows_aarch64_gnullvm 0.52.6", 1379 | "windows_aarch64_msvc 0.52.6", 1380 | "windows_i686_gnu 0.52.6", 1381 | "windows_i686_gnullvm", 1382 | "windows_i686_msvc 0.52.6", 1383 | "windows_x86_64_gnu 0.52.6", 1384 | "windows_x86_64_gnullvm 0.52.6", 1385 | "windows_x86_64_msvc 0.52.6", 1386 | ] 1387 | 1388 | [[package]] 1389 | name = "windows_aarch64_gnullvm" 1390 | version = "0.48.5" 1391 | source = "registry+https://github.com/rust-lang/crates.io-index" 1392 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1393 | 1394 | [[package]] 1395 | name = "windows_aarch64_gnullvm" 1396 | version = "0.52.6" 1397 | source = "registry+https://github.com/rust-lang/crates.io-index" 1398 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1399 | 1400 | [[package]] 1401 | name = "windows_aarch64_msvc" 1402 | version = "0.48.5" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1405 | 1406 | [[package]] 1407 | name = "windows_aarch64_msvc" 1408 | version = "0.52.6" 1409 | source = "registry+https://github.com/rust-lang/crates.io-index" 1410 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1411 | 1412 | [[package]] 1413 | name = "windows_i686_gnu" 1414 | version = "0.48.5" 1415 | source = "registry+https://github.com/rust-lang/crates.io-index" 1416 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1417 | 1418 | [[package]] 1419 | name = "windows_i686_gnu" 1420 | version = "0.52.6" 1421 | source = "registry+https://github.com/rust-lang/crates.io-index" 1422 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1423 | 1424 | [[package]] 1425 | name = "windows_i686_gnullvm" 1426 | version = "0.52.6" 1427 | source = "registry+https://github.com/rust-lang/crates.io-index" 1428 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1429 | 1430 | [[package]] 1431 | name = "windows_i686_msvc" 1432 | version = "0.48.5" 1433 | source = "registry+https://github.com/rust-lang/crates.io-index" 1434 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1435 | 1436 | [[package]] 1437 | name = "windows_i686_msvc" 1438 | version = "0.52.6" 1439 | source = "registry+https://github.com/rust-lang/crates.io-index" 1440 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1441 | 1442 | [[package]] 1443 | name = "windows_x86_64_gnu" 1444 | version = "0.48.5" 1445 | source = "registry+https://github.com/rust-lang/crates.io-index" 1446 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1447 | 1448 | [[package]] 1449 | name = "windows_x86_64_gnu" 1450 | version = "0.52.6" 1451 | source = "registry+https://github.com/rust-lang/crates.io-index" 1452 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1453 | 1454 | [[package]] 1455 | name = "windows_x86_64_gnullvm" 1456 | version = "0.48.5" 1457 | source = "registry+https://github.com/rust-lang/crates.io-index" 1458 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1459 | 1460 | [[package]] 1461 | name = "windows_x86_64_gnullvm" 1462 | version = "0.52.6" 1463 | source = "registry+https://github.com/rust-lang/crates.io-index" 1464 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1465 | 1466 | [[package]] 1467 | name = "windows_x86_64_msvc" 1468 | version = "0.48.5" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1471 | 1472 | [[package]] 1473 | name = "windows_x86_64_msvc" 1474 | version = "0.52.6" 1475 | source = "registry+https://github.com/rust-lang/crates.io-index" 1476 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1477 | 1478 | [[package]] 1479 | name = "winnow" 1480 | version = "0.7.10" 1481 | source = "registry+https://github.com/rust-lang/crates.io-index" 1482 | checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" 1483 | dependencies = [ 1484 | "memchr", 1485 | ] 1486 | 1487 | [[package]] 1488 | name = "wit-bindgen-rt" 1489 | version = "0.33.0" 1490 | source = "registry+https://github.com/rust-lang/crates.io-index" 1491 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 1492 | dependencies = [ 1493 | "bitflags 2.4.1", 1494 | ] 1495 | 1496 | [[package]] 1497 | name = "yaml-rust2" 1498 | version = "0.10.1" 1499 | source = "registry+https://github.com/rust-lang/crates.io-index" 1500 | checksum = "818913695e83ece1f8d2a1c52d54484b7b46d0f9c06beeb2649b9da50d9b512d" 1501 | dependencies = [ 1502 | "arraydeque", 1503 | "encoding_rs", 1504 | "hashlink", 1505 | ] 1506 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floki" 3 | description = "floki aims to provide reproducible and shareable build tooling by helping you run docker containers interactively from a declarative yaml file." 4 | repository = "https://github.com/Metaswitch/floki" 5 | homepage = "https://metaswitch.github.io/floki/" 6 | readme = "README.md" 7 | keywords = ["docker"] 8 | categories = ["command-line-utilities"] 9 | license = "MIT" 10 | 11 | version = "2.1.0" 12 | authors = ["Richard Lupton ", 13 | "Max Dymond "] 14 | edition = '2018' 15 | rust-version = '1.57' 16 | 17 | [dependencies] 18 | atty = "0.2" 19 | log = "0.4" 20 | serde = { version = "1", features = ["derive"] } 21 | serde_yaml = "0.9" 22 | structopt = "0.3" 23 | uuid = { version = "1.16", features = ["v4"] } 24 | yaml-rust2 = "0.10.1" 25 | simplelog = "0.12" 26 | nix = { version = "0.30", default-features = false, features = ["user"] } 27 | shlex = "1.3" 28 | sha2 = "0.10.8" 29 | anyhow = "1.0.98" 30 | thiserror = "2.0.12" 31 | tera = "1" 32 | serde_json = "1.0.140" 33 | toml = "0.8.20" 34 | shell-words = "1.1.0" 35 | 36 | [dev-dependencies] 37 | tempfile = "3.19.1" 38 | 39 | [profile.release] 40 | strip = true # Automatically strip symbols from the binary. 41 | opt-level = "z" # Optimize for size. 42 | lto = true 43 | codegen-units = 1 44 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 1 Letterman Drive 6 | Suite D4700 7 | San Francisco, CA, 94129 8 | 9 | Everyone is permitted to copy and distribute verbatim copies of this 10 | license document, but changing it is not allowed. 11 | 12 | 13 | Developer's Certificate of Origin 1.1 14 | 15 | By making a contribution to this project, I certify that: 16 | 17 | (a) The contribution was created in whole or in part by me and I 18 | have the right to submit it under the open source license 19 | indicated in the file; or 20 | 21 | (b) The contribution is based upon previous work that, to the best 22 | of my knowledge, is covered under an appropriate open source 23 | license and I have the right under that license to submit that 24 | work with modifications, whether created in whole or in part 25 | by me, under the same open source license (unless I am 26 | permitted to submit under a different license), as indicated 27 | in the file; or 28 | 29 | (c) The contribution was provided directly to me by some other 30 | person who certified (a), (b) or (c) and I have not modified 31 | it. 32 | 33 | (d) I understand and agree that this project and the contribution 34 | are public and that a record of the contribution (including all 35 | personal information I submit with it, including my sign-off) is 36 | maintained indefinitely and may be redistributed consistent with 37 | this project or the open source license(s) involved. 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Metaswitch Networks Ltd 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # floki 2 | 3 | Floki was a boatbuilder. Floki now helps you manage interactive containers for building software. 4 | 5 | ## What is floki? 6 | 7 | Docker and kubernetes are great ways to run software, and it is often convenient to use the same containers interactively to get a repeatable and complete build environment. However, using these containers for development is not always straightforward. 8 | 9 | `floki` aims to improve the human interface for launching and using interactive docker containers. Instead of remembering or constructing complicated `docker run` commands, or writing custom scripts to launch docker containers, `floki` lets you specify what you want from your docker container in a configuration file. You can then get your environment just by running `floki`. It doesn't replace docker or kubernetes, its an addition to try and improve the human interface for working on a codebase. 10 | 11 | This has several advantages over the usual approaches (custom scripts, or, more commonly, leaving it to the user to figure out) 12 | 13 | - an immediate build environment 14 | - easier to share and on-board new developers 15 | - a consistent and uniform interface to get a working environment 16 | 17 | ## Documentation 18 | 19 | For installation, and basic usage, see [getting started](https://metaswitch.github.io/floki/documentation/getting-started/). 20 | 21 | Full documentation can be found [here](https://metaswitch.github.io/floki/). 22 | 23 | ### Quickstart 24 | 25 | This assumes you have already installed `floki` using the installation instructions below. 26 | 27 | Suppose we want a build environment based on `alpine:latest` with a C compiler, and `clang` tools. Suppose we want to also have SSH credentials available from the host, so we can, for example, authenticate with a private git server. 28 | 29 | First create your `Dockerfile`: 30 | 31 | ```dockerfile 32 | FROM alpine:latest 33 | 34 | RUN apk update && apk add alpine-sdk clang openssh 35 | ``` 36 | 37 | and then add a file called `floki.yaml` to the root of your codebase: 38 | 39 | 40 | ```yaml 41 | image: 42 | build: 43 | name: hello-floki 44 | 45 | forward_ssh_agent: true 46 | init: 47 | - echo "Welcome to the hello-floki build container" 48 | ``` 49 | 50 | Now run `floki`. You should see the docker container be built, and you will be dropped into a shell. If you had an `ssh-agent` running on the host before running `floki`, you can run `ssh-add -l` and you should see the same keys loaded as you had on the host. 51 | 52 | ## Install 53 | 54 | ### Prerequisites 55 | 56 | It's recommended you add your user to the `docker` group: 57 | 58 | ```shell 59 | $ sudo usermod -a -G docker USERNAME 60 | ``` 61 | 62 | and logout and in again to pick up the changes. 63 | 64 | Alternatively you can run `floki` (after installation) with `sudo -E floki`. 65 | 66 | ### Installation from pre-built binaries 67 | 68 | Precompiled binaries can be downloaded from the releases page (for linux and OSX). 69 | 70 | To obtain `curl` and extract the latest linux binary directly in your shell, run 71 | 72 | ```shell 73 | $ curl -L https://github.com/Metaswitch/floki/releases/download/2.1.0/floki-2.1.0-linux.tar.gz | tar xzvf - 74 | ``` 75 | 76 | You should be able to run `floki` from your working directory: 77 | 78 | ```shell 79 | $ ./floki --version 80 | floki 2.1.0 81 | ``` 82 | 83 | Move it onto your path to run it from anywhere. E.g. 84 | 85 | ```shell 86 | $ mv floki /usr/local/bin/ 87 | ``` 88 | 89 | Enjoy! 90 | 91 | ### Installation from cargo 92 | 93 | `floki` can also be installed directly from `cargo`. 94 | 95 | ```shell 96 | $ cargo install floki 97 | ``` 98 | 99 | ## Handy features 100 | 101 | - Forwarding of `ssh-agent` (useful for authenticating with remote private git servers to pull private dependencies) 102 | - Docker-in-docker support 103 | - Forwarding of host user information (allows non-root users to be added and used). 104 | - volumes (shared, or per-project) for e.g. build caching. 105 | 106 | ## Contributing 107 | 108 | Contributors will need to sign their commits to acknowledge the [DCO](DCO) 109 | 110 | ## TODO 111 | 112 | See issues. 113 | -------------------------------------------------------------------------------- /announce.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Announce changes to the world!" 4 | 5 | VERSION=$(cargo get package.version) 6 | echo "Version: $VERSION" 7 | 8 | docker run \ 9 | --rm \ 10 | -v $PWD:/floki \ 11 | metaswitch/announcer:3.0.2 \ 12 | announce \ 13 | --webhook $ANNOUNCE_HOOK \ 14 | --target teams \ 15 | --changelogversion $VERSION \ 16 | --changelogfile /floki/CHANGELOG.md \ 17 | --projectname floki \ 18 | --iconemoji ship 19 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | if [ $OS_NAME = "ubuntu-24.04" ] 6 | then 7 | OS_ID="linux" 8 | elif [ $OS_NAME = "macos-latest" ] 9 | then 10 | OS_ID="osx" 11 | fi 12 | 13 | TAG=$(cargo get package.version) 14 | 15 | LABEL=${TAG}-${OS_ID} 16 | 17 | echo "Starting release build for ${LABEL}" 18 | 19 | if [ ${OS_ID} = "linux" ] 20 | then 21 | echo "Building statically linked linux binary" 22 | docker build -f .devcontainer/Dockerfile.alpine -t flokirust . 23 | 24 | docker run --rm -v $(pwd):/src -w /src flokirust \ 25 | sh -c 'cargo build --release && cp target/x86_64-unknown-linux-musl/release/floki .' 26 | sudo chown -R $(id -u):$(id -g) . 27 | 28 | # Check that it's statically compiled! 29 | ldd floki 30 | 31 | tar -cvzf floki-${LABEL}.tar.gz floki 32 | else 33 | echo "Building release binary" 34 | cargo build --release 35 | zip -j floki-${LABEL}.zip target/release/floki 36 | fi 37 | 38 | echo "Release build complete" 39 | -------------------------------------------------------------------------------- /changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | VERSION=$(cargo get package.version) 6 | 7 | docker run -v $PWD:$PWD -w $PWD sean0x42/markdown-extract -r "${VERSION}" CHANGELOG.md | tee release.txt 8 | -------------------------------------------------------------------------------- /docs/archetypes/default.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "{{ replace .Name "-" " " | title }}" 3 | date: {{ .Date }} 4 | draft: true 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /docs/config.toml: -------------------------------------------------------------------------------- 1 | baseURL = "https://metaswitch.github.io/floki" 2 | languageCode = "en-us" 3 | title = "floki" 4 | theme = "purehugo" 5 | publishDir = "../public" 6 | 7 | [params] 8 | githubName = "Metaswitch/floki" 9 | -------------------------------------------------------------------------------- /docs/content/documentation/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting Started" 3 | date: 2019-04-03T23:01:31+01:00 4 | draft: false 5 | --- 6 | 7 | ## Installation 8 | 9 | ### Prerequisites 10 | 11 | It's recommended you add your user to the `docker` group: 12 | 13 | ```shell 14 | $ sudo usermod -a -G docker USERNAME 15 | ``` 16 | 17 | and logout and in again to pick up the changes. 18 | 19 | Alternatively you can run `floki` (after installation) with `sudo -E floki`. 20 | 21 | ### Installation from pre-built binaries 22 | 23 | Precompiled binaries for linux and OSX can be downloaded from the [releases](https://github.com/Metaswitch/floki/releases) page. 24 | 25 | For example, to obtain the latest binary with `curl` and extract it, run 26 | 27 | ```shell 28 | $ curl -L https://github.com/Metaswitch/floki/releases/download/2.1.0/floki-2.1.0-linux.tar.gz | tar xzvf - 29 | ``` 30 | 31 | in a shell. You should now be able to run `floki` from your working directory: 32 | 33 | ```shell 34 | $ ./floki --version 35 | floki 0.6.1 36 | ``` 37 | 38 | Copy this into your path to run it without needing to specify the path absolutely. E.g. 39 | 40 | ```shell 41 | $ mv floki /usr/local/bin/ 42 | ``` 43 | 44 | ### Installation from cargo 45 | 46 | `floki` can also be installed directly from `cargo`. 47 | 48 | ```shell 49 | $ cargo install floki 50 | ``` 51 | 52 | ### Shell completions 53 | 54 | Shell completions can be added to your existing shell session with 55 | 56 | ```shell 57 | source <(floki completion ) 58 | ``` 59 | 60 | See `floki completion --help` for a list of available ``s. Add this command to your shell's rc file to get completions in all new shell sessions. 61 | 62 | Enjoy! 63 | 64 | ## Getting started 65 | 66 | `floki` is configured using a configuration file typically placed in the root of your codebase. As a basic example, write a basic configuration file, and name it `floki.yaml`. 67 | 68 | ```yaml 69 | image: debian:latest 70 | init: 71 | - echo "Welcome to your first floki container!" 72 | ``` 73 | 74 | Now, in the same directory, run 75 | 76 | ```shell 77 | floki 78 | ``` 79 | 80 | A container will launch with the working directory mounted as your working directory. Verify this by running `ls`: 81 | 82 | ```shell 83 | $ ls 84 | ... floki.yaml ... 85 | ``` 86 | 87 | In general, invoking `floki` in any child directory of this root directory will launch a container with: 88 | - The directory containing `floki.yaml` mounted; 89 | - The container shell located in the guest directory corresponding to the child. 90 | 91 | ## Using a different configuration file 92 | 93 | You can use a different configuration file with `floki` by telling it to use a different file from the command line. For example, if you have another configuration in `config.yaml`, you can run `floki` with 94 | 95 | ```shell 96 | floki -c config.yaml 97 | ``` 98 | 99 | Note that, in contrast to invoking `floki` without the `-c` flag, this will always mount the current working directory. 100 | 101 | ### Features you may want to look at next 102 | 103 | - Forwarding of `ssh-agent` (useful for authenticating with remote private git servers to pull private dependencies) 104 | - Docker-in-docker support 105 | - Forwarding of host user information (allows non-root users to be added and used). 106 | - `floki` volumes for setting up cross-session build caches. 107 | -------------------------------------------------------------------------------- /docs/content/intro/feature-overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Feature Overview" 3 | date: 2019-04-06T20:22:44+01:00 4 | draft: false 5 | --- 6 | 7 | `floki` aims to provide reproducible and shareable build tooling. It does this by helping you run docker containers interactively from a declarative yaml file. 8 | 9 | The ideal workflow is 10 | 11 | - clone the source repository 12 | - run `floki` 13 | - get to work 14 | 15 | `floki` has a number of features to help achieve this. The following outlines these features. 16 | 17 | # Container images 18 | 19 | `floki` offers a couple of ways to configure the container image to use. 20 | 21 | ## Prebuilt images 22 | 23 | Using a prebuilt image (e.g. one from dockerhub or a docker registry) is as simple as providing its name as a top-level key in `floki.yaml`: 24 | 25 | ```yaml 26 | image: debian:sid 27 | ``` 28 | 29 | `floki` will use docker to pull this image if you need it. 30 | 31 | Custom registries can be used by configuring `docker` to use these registries. `floki` defers to `docker` to locate and pull images. 32 | 33 | ## Build an image 34 | 35 | `floki` can use an image built from a `Dockerfile` in source tree. It's easiest to see an example of `floki.yaml` to see how to configure this. 36 | 37 | ```yaml 38 | image: 39 | build: 40 | name: foo # Will build the image with name foo:floki 41 | dockerfile: Dockerfile.foo # Relative location in source tree; defaults to Dockerfile 42 | context: . # Defaults to . 43 | target: builder # Target to use, for multi-stage dockerfiles (optional) 44 | ``` 45 | 46 | ## Referencing a key in another yaml file 47 | `floki` can use an image by reference to another yaml file. This can help keep local development environments synced with a CI environment. 48 | 49 | ```yaml 50 | image: 51 | yaml: 52 | file: .gitlab-ci.yaml 53 | key: variables.RUST-IMAGE 54 | ``` 55 | 56 | ## Build an image using any tool 57 | 58 | `floki` can use an image built using any arbitrary tool. 59 | 60 | ```yaml 61 | image: 62 | exec: 63 | command: docker # Will execute the program "docker" 64 | args: # A set of arguments to be passed to the command 65 | - build 66 | - -t 67 | - devimage 68 | - . 69 | image: devimage # The name and tag of the image that is created by the command 70 | ``` 71 | 72 | ## Updating an image 73 | 74 | `floki pull` forces a pull of the container specified in `image`. While it is better to version images properly, this can be used when tracking a `latest` tag, or similar. 75 | 76 | # Setting the shell 77 | 78 | Different containers require different shells, so `floki` allows you to configure this. Sometimes you will want a different shell to run the `init` commands to the shell presented to the user, and so `floki` also allows you to set an outer (used for `init`) and inner (used by the user) shell. 79 | 80 | The default shell is `sh`. 81 | 82 | ## Single shell 83 | 84 | A shell can be set for a container using the top-level `shell` key: 85 | 86 | ```yaml 87 | image: alpine:latest 88 | shell: sh 89 | ``` 90 | 91 | ## Inner and outer shell 92 | 93 | A different shell can be used for initialization and the interactive shell provided to the user. 94 | 95 | ```yaml 96 | image: alpine:latest 97 | shell: 98 | inner: bash 99 | outer: sh 100 | init: 101 | - apk update && apk install bash 102 | ``` 103 | 104 | A useful use case here is if you want to run the container with the same user as on the host. `floki` exposes the user id and user group id in environment variables, so you can add a user to the running container and switch to the new user in the inner shell: 105 | 106 | ```yaml 107 | image: foo:latest 108 | shell: 109 | inner: bash 110 | outer: switch_user 111 | init: 112 | - add_new_user $FLOKI_HOST_UID $FLOKI_HOST_GID 113 | ``` 114 | 115 | The commands to make the above work depend on the container you are running. `floki` just provides the tools to allow you to make it happen. 116 | 117 | # Entrypoints 118 | 119 | Some images define entrypoints to start services or to configure run-time settings that might not be what's wanted when using the image as a development environment. To suppress any defined entrypoint in the image use: 120 | 121 | ```yaml 122 | entrypoint: 123 | suppress: true 124 | ``` 125 | 126 | # Docker-in-docker 127 | 128 | Docker-in-docker (`dind`) can be enabled by setting the top-level `dind` key to `true`. 129 | 130 | ```yaml 131 | image: foo:bar 132 | dind: true 133 | ``` 134 | 135 | Note that the docker CLI tools are still required in the container, and the docker host is a linked container, with the working directory mounted in the same place as the interactive container. 136 | 137 | The precise `dind` image can also be set 138 | 139 | ```yaml 140 | dind: 141 | image: docker:dind 142 | ``` 143 | 144 | This helps properly pin and version the docker-in-docker container. 145 | 146 | # Floki volumes 147 | 148 | `floki` has the ability to use volumes for caching build artifacts between runs of the container (amongst other things). Volumes can be configured in `floki.yaml`: 149 | 150 | ```yaml 151 | volumes: 152 | cargo-registry: 153 | mount: /home/rust/.cargo/registry 154 | ``` 155 | 156 | The key names the volume (it can be any valid yaml name), while the `mount` key specifies where the volume will be mounted inside the `floki` container. 157 | 158 | It's also possible to share volumes across different `floki.yaml`s. For example, you may want to share a `cargo` registry across all Rust build containers. These shared volumes are identified by the name given to the volume. 159 | 160 | ```yaml 161 | volumes: 162 | cargo-registry: 163 | shared: true 164 | mount: /home/rust/.cargo/registry 165 | ``` 166 | 167 | `floki` creates directories on the host to back these volumes in `~/.floki/volumes`. Non-shared volumes are given names unique to the source directory. 168 | 169 | # Environment forwarding 170 | 171 | ## User details 172 | 173 | `floki` captures the host user details in environment variables, and forwards these into the running container. 174 | 175 | * `FLOKI_HOST_UID` is set to the host user's user id (the output of `id -u`) 176 | * `FLOKI_HOST_GID` is set to the host user's group id (the output of `id -g`) 177 | 178 | These can be used to configure users in the container dynamically. This can be a little fiddly, especially if the container already uses a non-root user with the same id as the host user. 179 | 180 | ## Host working directory 181 | 182 | The host path to the mounted directory is forwarded into the `floki` container as an environment variable, `FLOKI_HOST_MOUNTDIR`. 183 | 184 | You can set where this directory is mounted in the container using the `mount` key in `floki.yaml`. The mount location is exposed in the `floki` container as an environment variable, `FLOKI_WORKING_DIR`. 185 | 186 | ## SSH agent 187 | 188 | Sometimes it is useful to be able to pull dependencies from source code management servers for builds. To make this easier to do in an automated fashion, `floki` can forward and `ssh-agent` socket into the container, and expose its path through `SSH_AUTH_SOCK`. 189 | 190 | ```yaml 191 | forward_ssh_agent: true 192 | ``` 193 | 194 | You will need to have an `ssh-agent` running on the host before launching `floki`. 195 | 196 | # Sandboxed commands with floki run 197 | 198 | `floki` also allows single commands to be run, rather than dropping into an interactive shell. 199 | 200 | ```shell 201 | $ floki run ls 202 | floki.yaml 203 | ``` 204 | 205 | Note that if you have configured an inner shell, the command will run within the inner shell. 206 | 207 | 208 | # Escaping with `docker_switches` 209 | 210 | `floki` also allows you to pass additional switches to the underlying docker command, for example to forward port `8080` to the host. 211 | 212 | ```yaml 213 | image: debian:sid 214 | docker_switches: 215 | - -p 216 | - 8080:8080 217 | init: 218 | - echo "Welcome to your server container!" 219 | ``` 220 | 221 | Note that use of `docker_switches` may reduce the reproducibility and shareability of your `floki.yaml` (for instance it could be used to mount a volume with a specific host path that works on no other machines). 222 | 223 | Nonetheless, it is useful to be able to add arbitrary switches in a pinch, just to be able to get something working. 224 | If there are things you can add with `docker_switches` which are reproducible and shareable, please raise a feature request, or go ahead and implement it yourself! 225 | 226 | # Templating 227 | 228 | `floki` supports templating using the [Tera engine](https://tera.netlify.app/). Your `floki.yaml` file is parsed once as a template. The `env` var is exposed to the template, which contains all environment variables. 229 | 230 | Example uses include referencing environmental variables in the `floki` config, e.g. to mount a subdirectory of the user's home directory: 231 | ```yaml 232 | docker_switches: 233 | - -v {{ env.HOME }}/.vim:/home/build/.vim 234 | ``` 235 | Note that extensive use may reduce the reproducibility and shareability of your `floki.yaml`. 236 | -------------------------------------------------------------------------------- /docs/content/intro/navigation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Navigation" 3 | date: 2020-12-21T20:26:44+00:00 4 | draft: false 5 | # Give heavy weight so that it appears at the top of the documentation homepage 6 | weight: 100 7 | --- 8 | 9 | For installation, and basic usage, see [getting started]({{< ref "/documentation/getting-started.md" >}} "floki: Getting Started"). 10 | 11 | For an overview of the features of `floki`, see the [feature overview]({{< ref "/intro/feature-overview.md" >}} "floki: Feature Overview"). 12 | 13 | Full documentation can be found [here]({{< ref "/" >}} "floki: Documentation"). 14 | -------------------------------------------------------------------------------- /docs/themes/purehugo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | assets/css/custom.css -------------------------------------------------------------------------------- /docs/themes/purehugo/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dragos Plesca 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /docs/themes/purehugo/README.md: -------------------------------------------------------------------------------- 1 | purehugo 2 | ======== 3 | 4 | Hugo theme based on [purecss](http://purecss.io/) from Yahoo. The theme is based on [the purecss blog layout example](http://purecss.io/layouts/blog/), is responsive and has a few more features: pagination (if enabled), responsive images (through a shortcode), google analytics, disqus comments and even a mini-asset-pipeline using gulp. If you end up using it, I'd love to see what you do with it so please give me a shout on [twitter](https://twitter.com/dragos_plesca). 5 | 6 | ### Installation 7 | 8 | Navigate to your Hugo site's theme folder 9 | ``` 10 | $ cd themes 11 | $ git clone https://github.com/dplesca/purehugo.git 12 | ``` 13 | 14 | ### Config file 15 | 16 | The config file for the demo site looks like this: 17 | 18 | ```toml 19 | baseurl = "http://dplesca.github.io/purehugo/" 20 | languageCode = "en-us" 21 | title = "purehugo" 22 | theme = "purehugo" 23 | Paginate = 10 24 | disqusShortname = "xxxx" 25 | 26 | [params] 27 | twitterName = "dragos_plesca" 28 | githubName = "dplesca" 29 | stackOverflowId = "#######" 30 | linkedinName = "dragos-plesca-52797444" 31 | description = "Demo site for a hugo theme" 32 | google_analytics = "UA-xxxxxx-xx" 33 | ``` 34 | 35 | Notice the configuration necessary for disqus comments (just setting the disqusShortname); the twitter, github, stack overflow and linkedin handlers (for the site sidebar); the site description and enabling Google Analytics reporting. 36 | 37 | ### Responsive Images 38 | 39 | For responsive images you could use the built-in responsive image shortcode (without the `/**/` characters): 40 | ``` 41 | {{%/* img-responsive "http://example.com/image.jpg" */%}} 42 | ``` 43 | 44 | ### Hide Share Options 45 | 46 | If you would like to hide the share options in the single post view, you can add this option in the `params` section of your config file. 47 | 48 | ```toml 49 | [params] 50 | # ... other options ... 51 | hideShareOptions = true 52 | ``` 53 | 54 | ### Hide Sidebar icons text Options 55 | 56 | 57 | If you would like to hide the text next to the icons on the sidebar, you can add this option in the `params` section of your config file. 58 | 59 | ```toml 60 | [params] 61 | # ... other options ... 62 | hideSidebarIconText = true 63 | ``` 64 | 65 | ### Screenshot 66 | ![Screenshot](https://i.imgur.com/Dsj41Rz.png) 67 | -------------------------------------------------------------------------------- /docs/themes/purehugo/archetypes/default.md: -------------------------------------------------------------------------------- 1 | +++ 2 | Description = "" 3 | Tags = [] 4 | Categories = [] 5 | +++ 6 | -------------------------------------------------------------------------------- /docs/themes/purehugo/assets/css/blog.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | /* 8 | When setting the primary font stack, apply it to the Pure grid units along 9 | with `html`, `button`, `input`, `select`, and `textarea`. Pure Grids use 10 | specific font stacks to ensure the greatest OS/browser compatibility. 11 | */ 12 | html, button, input, select, textarea, 13 | .pure-g [class *= "pure-u"] { 14 | /* Set your content font stack here: */ 15 | font-family: "Source Sans Pro", serif; 16 | } 17 | 18 | a { 19 | text-decoration: none; 20 | color: rgb(61, 146, 201); 21 | } 22 | a:hover, 23 | a:focus { 24 | text-decoration: underline; 25 | } 26 | 27 | h3 { 28 | font-weight: 100; 29 | } 30 | 31 | /* LAYOUT CSS */ 32 | .pure-img-responsive { 33 | max-width: 100%; 34 | height: auto; 35 | } 36 | 37 | #layout { 38 | padding: 0; 39 | } 40 | 41 | .header { 42 | text-align: center; 43 | top: auto; 44 | margin: 3em auto; 45 | } 46 | 47 | .sidebar { 48 | background: rgb(61, 79, 93); 49 | color: #fff; 50 | } 51 | 52 | .brand-title, 53 | .brand-tagline { 54 | margin: 0; 55 | } 56 | .brand-title { 57 | text-transform: uppercase; 58 | font-family: "Oxygen", sans-serif; 59 | } 60 | .brand-title a{ 61 | color:#fff; 62 | } 63 | .brand-title a:hover{ 64 | text-decoration: none; 65 | } 66 | .brand-tagline { 67 | font-weight: 300; 68 | color: rgb(176, 202, 219); 69 | } 70 | 71 | .nav-list { 72 | margin: 0; 73 | padding: 0; 74 | list-style: none; 75 | } 76 | .nav-item { 77 | display: inline-block; 78 | *display: inline; 79 | zoom: 1; 80 | } 81 | .nav-item a { 82 | background: transparent; 83 | border: 2px solid rgb(176, 202, 219); 84 | color: #fff; 85 | margin-top: 1em; 86 | /*letter-spacing: 0.05em; 87 | text-transform: uppercase; 88 | font-size: 85%; */ 89 | font-weight: bold; 90 | font-family: "Oxygen", sans-serif; 91 | } 92 | .nav-item a:hover, 93 | .nav-item a:focus { 94 | border: 2px solid rgb(61, 146, 201); 95 | text-decoration: none; 96 | } 97 | 98 | .content-subhead { 99 | text-transform: uppercase; 100 | color: #aaa; 101 | border-bottom: 1px solid #eee; 102 | padding: 0.4em 0; 103 | font-size: 80%; 104 | font-weight: 500; 105 | letter-spacing: 0.1em; 106 | } 107 | 108 | .content { 109 | padding: 2em 1em 0; 110 | } 111 | 112 | .post { 113 | padding-bottom: 2em; 114 | } 115 | .post-title { 116 | font-size: 2em; 117 | color: #222; 118 | margin: 0.4em 0; 119 | font-family: "Oxygen", sans-serif; 120 | font-weight: bold; 121 | } 122 | .post-title:hover{ 123 | text-decoration: none; 124 | } 125 | .post-avatar { 126 | border-radius: 50px; 127 | float: right; 128 | margin-left: 1em; 129 | } 130 | .post-description { 131 | font-family: "Source Sans Pro", serif; 132 | color: #333; 133 | line-height: 1.35em; 134 | } 135 | .post-meta { 136 | color: #999; 137 | font-size: 90%; 138 | margin: 5px 0; 139 | } 140 | 141 | .post-category { 142 | margin: 0 0.1em; 143 | padding: 0.3em 1em; 144 | color: #fff; 145 | background: #999; 146 | font-size: 80%; 147 | } 148 | .post-category-design { 149 | background: #5aba59; 150 | } 151 | .post-category-pure { 152 | background: #4d85d1; 153 | } 154 | .post-category-yui { 155 | background: #8156a7; 156 | } 157 | .post-category-javascript { 158 | background: #df2d4f; 159 | } 160 | 161 | .post-images { 162 | margin: 1em 0; 163 | } 164 | .post-image-meta { 165 | margin-top: -3.5em; 166 | margin-left: 1em; 167 | color: #fff; 168 | text-shadow: 0 1px 1px #333; 169 | } 170 | 171 | .footer { 172 | text-align: center; 173 | padding: 1em 0; 174 | color: #555; 175 | font-size: 80%; 176 | } 177 | .footer ul li a { 178 | display:inline; 179 | padding: 0; 180 | } 181 | .hugo{ 182 | color:#333; 183 | font-weight: bold; 184 | } 185 | .footer .pure-menu a:hover, 186 | .footer .pure-menu a:focus { 187 | background: none; 188 | } 189 | .footer li{ 190 | list-style-type: none; 191 | } 192 | 193 | .post-share-links{ 194 | margin:15px 15px 0 0; 195 | float:left; 196 | padding: 10px; 197 | border: 1px solid #ccc; 198 | text-align: center; 199 | } 200 | 201 | .post-share-links a{ 202 | color:#aaa; 203 | font-size: 28px; 204 | line-height: 36px; 205 | margin: 0 auto; 206 | display: block; 207 | transition: all .3s ease-in; 208 | } 209 | 210 | .post-share-links a:hover{ 211 | color: rgb(61, 146, 201); 212 | text-decoration: none; 213 | } 214 | 215 | .post-share-links h4{ 216 | font-family: "Oxygen", sans-serif; 217 | margin:0 0 5px; 218 | color:#aaa 219 | } 220 | 221 | @media (min-width: 48em) { 222 | .content { 223 | padding: 2em 3em 0; 224 | margin-left: 25%; 225 | } 226 | 227 | .header { 228 | margin: 80% 2em 0; 229 | text-align: right; 230 | } 231 | 232 | .sidebar { 233 | position: fixed; 234 | top: 0; 235 | bottom: 0; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /docs/themes/purehugo/assets/css/syntax-highlighter.css: -------------------------------------------------------------------------------- 1 | /** 2 | * GitHub theme 3 | * 4 | * @author Craig Campbell 5 | * @version 1.0.4 6 | */ 7 | pre { 8 | border: 1px solid #ccc; 9 | word-wrap: break-word; 10 | padding: 6px 10px; 11 | line-height: 19px; 12 | margin-bottom: 20px; 13 | } 14 | 15 | code { 16 | border: 1px solid #eaeaea; 17 | margin: 0px 2px; 18 | padding: 0px 5px; 19 | font-size: 12px; 20 | } 21 | 22 | pre code { 23 | border: 0px; 24 | padding: 0px; 25 | margin: 0px; 26 | -moz-border-radius: 0px; 27 | -webkit-border-radius: 0px; 28 | border-radius: 0px; 29 | } 30 | 31 | pre, code { 32 | font-family: Consolas, 'Liberation Mono', Courier, monospace; 33 | color: #333; 34 | background: #f8f8f8; 35 | -moz-border-radius: 3px; 36 | -webkit-border-radius: 3px; 37 | border-radius: 3px; 38 | } 39 | 40 | pre, pre code { 41 | font-size: 13px; 42 | } 43 | 44 | pre .comment { 45 | color: #998; 46 | } 47 | 48 | pre .support { 49 | color: #0086B3; 50 | } 51 | 52 | pre .tag, pre .tag-name { 53 | color: navy; 54 | } 55 | 56 | pre .keyword, pre .css-property, pre .vendor-prefix, pre .sass, pre .class, pre .id, pre .css-value, pre .entity.function, pre .storage.function { 57 | font-weight: bold; 58 | } 59 | 60 | pre .css-property, pre .css-value, pre .vendor-prefix, pre .support.namespace { 61 | color: #333; 62 | } 63 | 64 | pre .constant.numeric, pre .keyword.unit, pre .hex-color { 65 | font-weight: normal; 66 | color: #099; 67 | } 68 | 69 | pre .entity.class { 70 | color: #458; 71 | } 72 | 73 | pre .entity.id, pre .entity.function { 74 | color: #900; 75 | } 76 | 77 | pre .attribute, pre .variable { 78 | color: teal; 79 | } 80 | 81 | pre .string, pre .support.value { 82 | font-weight: normal; 83 | color: #d14; 84 | } 85 | 86 | pre .regexp { 87 | color: #009926; 88 | } 89 | -------------------------------------------------------------------------------- /docs/themes/purehugo/assets/js/jquery.prettysocial.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery prettySocial: Use custom social share buttons 3 | * Author: Sonny T. , sonnyt.com 4 | */(function(a){a.fn.prettySocial=function(){var b={pinterest:{url:"http://pinterest.com/pin/create/button/?url={{url}}&media={{media}}&description={{description}}",popup:{width:685,height:500}},facebook:{url:"https://www.facebook.com/sharer/sharer.php?s=100&p[title]={{title}}&p[summary]={{description}}&p[url]={{url}}&p[images][0]={{media}}",popup:{width:626,height:436}},twitter:{url:"https://twitter.com/share?url={{url}}&via={{via}}&text={{description}}",popup:{width:685,height:500}},googleplus:{url:"https://plus.google.com/share?url={{url}}",popup:{width:600,height:600}},linkedin:{url:"https://www.linkedin.com/shareArticle?mini=true&url={{url}}&title={{title}}&summary={{description}}+&source={{via}}",popup:{width:600,height:600}}},d=function(f,e){var h=(window.innerWidth/2)-(f.popup.width/2),g=(window.innerHeight/2)-(f.popup.height/2);return window.open(e,"","toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width="+f.popup.width+", height="+f.popup.height+", top="+g+", left="+h)},c=function(f,g){var e=f.url.replace(/{{url}}/g,encodeURIComponent(g.url)).replace(/{{title}}/g,encodeURIComponent(g.title)).replace(/{{description}}/g,encodeURIComponent(g.description)).replace(/{{media}}/g,encodeURIComponent(g.media)).replace(/{{via}}/g,encodeURIComponent(g.via));return e};return this.each(function(){var i=a(this);var g=i.data("type"),f=b[g]||null;if(!f){a.error("Social site is not set.")}var h={url:i.data("url")||"",title:i.data("title")||"",description:i.data("description")||"",media:i.data("media")||"",via:i.data("via")||""};var e=c(f,h);if(navigator.userAgent.match(/Android|IEMobile|BlackBerry|iPhone|iPad|iPod|Opera Mini/i)){i.bind("touchstart",function(j){if(j.originalEvent.touches.length>1){return}i.data("touchWithoutScroll",true)}).bind("touchmove",function(){i.data("touchWithoutScroll",false);return}).bind("touchend",function(k){k.preventDefault();var j=i.data("touchWithoutScroll");if(k.originalEvent.touches.length>1||!j){return}d(f,e)})}else{i.bind("click",function(j){j.preventDefault();d(f,e)})}})}})(jQuery); -------------------------------------------------------------------------------- /docs/themes/purehugo/assets/js/scripts.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $('.prettySocial').prettySocial(); 3 | }); -------------------------------------------------------------------------------- /docs/themes/purehugo/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp') 2 | uglify = require('gulp-uglify'), 3 | concat = require('gulp-concat'), 4 | minifyCSS = require('gulp-minify-css'); 5 | 6 | gulp.task('compress', function() { 7 | gulp.src(['assets/js/jquery.min.js', 'assets/js/jquery.prettysocial.min.js', 'assets/js/scripts.js']) 8 | .pipe(concat('all.min.js')) 9 | .pipe(uglify()) 10 | .pipe(gulp.dest('static/js/')); 11 | gulp.src(['assets/css/blog.css', 'assets/css/syntax-highlighter.css', 'assets/css/custom.css']) 12 | .pipe(concat('all.min.css')) 13 | .pipe(minifyCSS()) 14 | .pipe(gulp.dest('static/css/')); 15 | }); -------------------------------------------------------------------------------- /docs/themes/purehugo/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metaswitch/floki/f524b736d41c6553b9f1d6c8f5eaa7b770aa356c/docs/themes/purehugo/images/screenshot.png -------------------------------------------------------------------------------- /docs/themes/purehugo/images/tn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metaswitch/floki/f524b736d41c6553b9f1d6c8f5eaa7b770aa356c/docs/themes/purehugo/images/tn.png -------------------------------------------------------------------------------- /docs/themes/purehugo/layouts/_default/list.html: -------------------------------------------------------------------------------- 1 | {{ partial "header.html" . }} 2 | 3 |
4 | {{ partial "sidebar.html" . }} 5 | 6 |
7 |
8 | 9 |
10 | {{ range .Paginator.Pages }} 11 |

{{ .Date.Format "02 Jan 2006, 15:04" }}

12 |
13 |
14 | 15 | {{ .Title }} 16 | 17 | 28 |
29 | 30 |
31 | {{ .Content }} 32 |
33 |
34 | {{ end }} 35 |
36 | {{ partial "pagination.html" . }} 37 | {{ partial "footer.html" . }} 38 |
39 |
40 |
41 | {{ partial "analytics.html" . }} 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/themes/purehugo/layouts/_default/single.html: -------------------------------------------------------------------------------- 1 | {{ partial "header.html" . }} 2 | 3 |
4 | {{ partial "sidebar.html" . }} 5 | 6 |
7 |
8 | 9 |
10 |

{{ .Date.Format "02 Jan 2006, 15:04" }}

11 |
12 |
13 | 14 | {{ .Title }} 15 | 16 | 27 |
28 | {{ if not .Site.Params.hideShareOptions }} 29 |
30 |
31 |

Share

32 | {{ if isset .Site.Params "facebook" }} 33 | 34 | {{ end }} 35 | {{ if isset .Site.Params "googleplus" }} 36 | 37 | {{ end }} 38 | {{ if isset .Site.Params "twitter" }} 39 | 40 | {{ end }} 41 | 42 |
43 |
44 | {{ end }} 45 |
46 | {{ .Content }} 47 |
48 | {{ template "_internal/disqus.html" . }} 49 |
50 |
51 | {{ partial "footer.html" . }} 52 |
53 |
54 |
55 | 56 | {{ partial "analytics.html" . }} 57 | 58 | 59 | -------------------------------------------------------------------------------- /docs/themes/purehugo/layouts/index.html: -------------------------------------------------------------------------------- 1 | {{ partial "header.html" . }} 2 | 3 |
4 | {{ partial "sidebar.html" . }} 5 | 6 |
7 |
8 | 9 |
10 | {{ range .Paginator.Pages }} 11 |

{{ .Date.Format "02 Jan 2006, 15:04" }}

12 |
13 |
14 | 15 | {{ .Title }} 16 | 17 | 26 |
27 | 28 |
29 | {{ .Content }} 30 |
31 |
32 | {{ end }} 33 |
34 | {{ partial "pagination.html" . }} 35 | {{ partial "footer.html" . }} 36 |
37 |
38 |
39 | 40 | {{ partial "analytics.html" . }} 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/themes/purehugo/layouts/partials/analytics.html: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /docs/themes/purehugo/layouts/partials/footer.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /docs/themes/purehugo/layouts/partials/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ .Title }} · {{ .Site.Title }} 7 | 8 | 9 | 10 | {{hugo.Generator}} 11 | 12 | {{ with .Site.Params.twitter }}{{ end }} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/themes/purehugo/layouts/partials/pagination.html: -------------------------------------------------------------------------------- 1 | {{ if or (.Paginator.HasPrev) (.Paginator.HasNext) }} 2 | 17 | {{ end }} 18 | -------------------------------------------------------------------------------- /docs/themes/purehugo/layouts/partials/sidebar.html: -------------------------------------------------------------------------------- 1 | 47 | -------------------------------------------------------------------------------- /docs/themes/purehugo/layouts/shortcodes/img-responsive.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/themes/purehugo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "gulp": "^5.0.0", 4 | "gulp-concat": "^2.5.2", 5 | "gulp-minify-css": "^1.1.0", 6 | "gulp-uglify": "^3.0.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/themes/purehugo/static/css/all.min.css: -------------------------------------------------------------------------------- 1 | #layout,.nav-list{padding:0}.brand-title,.content-subhead{text-transform:uppercase}.footer,.header,.post-share-links{text-align:center}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-g [class*=pure-u],button,html,input,select,textarea{font-family:"Source Sans Pro",serif}.brand-title,.nav-item a,.post-title{font-family:Oxygen,sans-serif}a{text-decoration:none;color:#3d92c9}.brand-title a,.sidebar{color:#fff}a:focus,a:hover{text-decoration:underline}.brand-title a:hover,.post-share-links a:hover,.post-title:hover{text-decoration:none}h3{font-weight:100}.pure-img-responsive{max-width:100%;height:auto}.header{top:auto;margin:3em auto}.sidebar{background:#3d4f5d}.brand-tagline,.brand-title{margin:0}.brand-tagline{font-weight:300;color:#b0cadb}.nav-list{margin:0;list-style:none}.nav-item{display:inline-block;zoom:1}.nav-item a{background:0 0;border:2px solid #b0cadb;color:#fff;margin-top:1em;font-weight:700}.nav-item a:focus,.nav-item a:hover{border:2px solid #3d92c9;text-decoration:none}.content-subhead{color:#aaa;border-bottom:1px solid #eee;padding:.4em 0;font-size:80%;font-weight:500;letter-spacing:.1em}.hugo,.post-title,pre .class,pre .css-property,pre .css-value,pre .entity.function,pre .id,pre .keyword,pre .sass,pre .storage.function,pre .vendor-prefix{font-weight:700}.content{padding:2em 1em 0}.post{padding-bottom:2em}.post-title{font-size:2em;color:#222;margin:.4em 0}.post-avatar{border-radius:50px;float:right;margin-left:1em}.post-share-links,pre{border:1px solid #ccc}.post-description{font-family:"Source Sans Pro",serif;color:#333;line-height:1.35em}.post-meta{color:#999;font-size:90%;margin:5px 0}.post-category{margin:0 .1em;padding:.3em 1em;color:#fff;background:#999;font-size:80%}.post-category-design{background:#5aba59}.post-category-pure{background:#4d85d1}.post-category-yui{background:#8156a7}.post-category-javascript{background:#df2d4f}.post-images{margin:1em 0}.post-image-meta{margin-top:-3.5em;margin-left:1em;color:#fff;text-shadow:0 1px 1px #333}.footer{padding:1em 0;color:#555;font-size:80%}.footer ul li a{display:inline;padding:0}.hugo{color:#333}.footer .pure-menu a:focus,.footer .pure-menu a:hover{background:0 0}.footer li{list-style-type:none}.post-share-links{margin:15px 15px 0 0;float:left;padding:10px}.post-share-links a{color:#aaa;font-size:28px;line-height:36px;margin:0 auto;display:block;transition:all .3s ease-in}.post-share-links a:hover{color:#3d92c9}.post-share-links h4{font-family:Oxygen,sans-serif;margin:0 0 5px;color:#aaa}@media (min-width:48em){.content{padding:2em 3em 0;margin-left:25%}.header{margin:80% 2em 0;text-align:right}.sidebar{position:fixed;top:0;bottom:0}}pre{word-wrap:break-word;padding:6px 10px;line-height:19px;margin-bottom:20px}code{border:1px solid #eaeaea;margin:0 2px;padding:0 5px;font-size:12px}pre code{border:0;padding:0;margin:0;-moz-border-radius:0;-webkit-border-radius:0;border-radius:0}code,pre{font-family:Consolas,'Liberation Mono',Courier,monospace;color:#333;background:#f8f8f8;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px}pre,pre code{font-size:13px}pre .comment{color:#998}pre .support{color:#0086B3}pre .tag,pre .tag-name{color:navy}pre .css-property,pre .css-value,pre .support.namespace,pre .vendor-prefix{color:#333}pre .constant.numeric,pre .hex-color,pre .keyword.unit{font-weight:400;color:#099}pre .entity.class{color:#458}pre .entity.function,pre .entity.id{color:#900}pre .attribute,pre .variable{color:teal}pre .string,pre .support.value{font-weight:400;color:#d14}pre .regexp{color:#009926} -------------------------------------------------------------------------------- /docs/themes/purehugo/theme.toml: -------------------------------------------------------------------------------- 1 | name = "purehugo" 2 | description = "A theme based on the pure css blog layout" 3 | license = "MIT" 4 | source_repo = "https://github.com/dplesca/purehugo" 5 | tags = ["blog", "purecss"] 6 | min_version = 0.14 7 | 8 | [author] 9 | name = "dplesca" 10 | twitter = "http://twitter.com/dragos_plesca" 11 | url = "http://github.com/dplesca" 12 | 13 | -------------------------------------------------------------------------------- /floki-hugo.yaml: -------------------------------------------------------------------------------- 1 | # For shell to work, need Alpine, Debian or Ubuntu versions of this container. 2 | # See https://hub.docker.com/r/klakegg/hugo/. 3 | image: klakegg/hugo:alpine 4 | mount: /src 5 | docker_switches: 6 | # Expose port 1313, as this is the port the server exposes for locally built 7 | # documentation. 8 | - -p 9 | - 1313:1313 10 | # The entrypoint for this container is a direct call to the "hugo" script. To 11 | # get a shell, we need to pass "shell" to this script, hence the "hack" of 12 | # setting "shell" to "shell". 13 | shell: shell 14 | # To run a hugo server, run "hugo server -D" from the /src/docs directory. 15 | -------------------------------------------------------------------------------- /floki.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | build: 3 | name: flokirust 4 | dockerfile: .devcontainer/Dockerfile.alpine 5 | context: . 6 | 7 | volumes: 8 | alpine-cargo-registry: 9 | mount: /usr/local/cargo/registry 10 | 11 | forward_ssh_agent: true 12 | shell: 13 | outer: bash 14 | inner: su floki 15 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | "group:allNonMajor" 6 | ], 7 | "reviewers": [ 8 | "maxdymond" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | /// Description of the CLI interface to floki 2 | use std::path; 3 | use structopt::StructOpt; 4 | 5 | /// Subcommands of the main floki command 6 | #[derive(Debug, StructOpt)] 7 | pub(crate) enum Subcommand { 8 | /// Run a command within the container 9 | #[structopt(name = "run")] 10 | Run { command: Vec }, 11 | 12 | /// Pull the image in the configuration file 13 | #[structopt(name = "pull")] 14 | Pull {}, 15 | 16 | /// Generate shell completions to stdout. 17 | #[structopt(name = "completion")] 18 | Completion { 19 | /// The shell to generate completions for. Choose from: bash, fish, zsh, powershell, elvish 20 | #[structopt(name = "SHELL", parse(try_from_str))] 21 | shell: structopt::clap::Shell, 22 | }, 23 | 24 | /// Render the configuration file to stdout, performing any templating 25 | /// operations. 26 | #[structopt(name = "render")] 27 | Render {}, 28 | } 29 | 30 | /// Main CLI interface 31 | #[derive(Debug, StructOpt)] 32 | #[structopt(name = "floki", about = "The interactive container launcher.")] 33 | pub(crate) struct Cli { 34 | /// Use the specified config instead of searching the tree for a 35 | /// "floki.yaml" file. 36 | #[structopt(long = "config", short = "c")] 37 | pub(crate) config_file: Option, 38 | 39 | /// Deprecated, and no longer has any effect. 40 | #[structopt(long = "local", short = "l", hidden = true)] 41 | pub(crate) local: bool, 42 | 43 | /// Logging verbosity level 44 | #[structopt(short = "v", parse(from_occurrences))] 45 | pub(crate) verbosity: u8, 46 | 47 | #[structopt(subcommand)] 48 | pub(crate) subcommand: Option, 49 | } 50 | -------------------------------------------------------------------------------- /src/command.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{FlokiError, FlokiSubprocessExitStatus}; 2 | use anyhow::Error; 3 | use std::ffi::{OsStr, OsString}; 4 | use std::path; 5 | use std::process::{Command, Stdio}; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct DockerCommandBuilder { 9 | name: String, 10 | volumes: Vec, 11 | environment: Vec, 12 | switches: Vec, 13 | image: String, 14 | } 15 | 16 | #[derive(Debug)] 17 | pub struct DaemonHandle { 18 | name: String, 19 | } 20 | 21 | impl DaemonHandle { 22 | fn from_builder(builder: DockerCommandBuilder) -> Self { 23 | DaemonHandle { name: builder.name } 24 | } 25 | } 26 | 27 | impl Drop for DaemonHandle { 28 | fn drop(&mut self) { 29 | info!("Stopping daemon docker container '{}'", self.name); 30 | Command::new("docker") 31 | .args(["kill", &self.name]) 32 | .stdin(Stdio::null()) 33 | .stdout(Stdio::null()) 34 | .stderr(Stdio::null()) 35 | .spawn() 36 | .expect("Unable to kill docker container") 37 | .wait() 38 | .expect("Unable to wait for docker container to die"); 39 | } 40 | } 41 | 42 | impl DockerCommandBuilder { 43 | pub fn run(&self, command: I) -> Result<(), Error> 44 | where 45 | I: IntoIterator + std::fmt::Debug, 46 | S: AsRef, 47 | { 48 | debug!("Spawning docker command with configuration: {self:?}"); 49 | debug!("- and args: {command:?}"); 50 | 51 | let mut command = Command::new("docker") 52 | .args(self.base_args()) 53 | .args(self.build_volume_switches()) 54 | .args(self.build_environment_switches()) 55 | .args(self.build_docker_switches()) 56 | .arg(&self.image) 57 | .args(command) 58 | .stdout(Stdio::inherit()) 59 | .stderr(Stdio::inherit()) 60 | .stdin(Stdio::inherit()) 61 | .spawn() 62 | .map_err(|e| FlokiError::FailedToLaunchDocker { error: e })?; 63 | 64 | let exit_status = command 65 | .wait() 66 | .map_err(|e| FlokiError::FailedToCompleteDockerCommand { error: e })?; 67 | if exit_status.success() { 68 | Ok(()) 69 | } else { 70 | Err(FlokiError::RunContainerFailed { 71 | exit_status: FlokiSubprocessExitStatus { 72 | process_description: "docker run".into(), 73 | exit_status, 74 | }, 75 | } 76 | .into()) 77 | } 78 | } 79 | 80 | pub fn start_as_daemon(self, command: &[&str]) -> Result { 81 | debug!("Starting daemon container '{}'", self.name); 82 | let exit_status = Command::new("docker") 83 | .args(["run", "--rm"]) 84 | .args(["--name", &self.name]) 85 | .args(self.build_volume_switches()) 86 | .args(self.build_environment_switches()) 87 | .args(self.build_docker_switches()) 88 | .arg("-d") 89 | .arg(&self.image) 90 | .args(command) 91 | .stdin(Stdio::null()) 92 | .stdout(Stdio::null()) 93 | .stderr(Stdio::null()) 94 | .spawn() 95 | .map_err(|e| FlokiError::FailedToLaunchDocker { error: e })? 96 | .wait() 97 | .map_err(|e| FlokiError::FailedToCompleteDockerCommand { error: e })?; 98 | 99 | if exit_status.success() { 100 | Ok(DaemonHandle::from_builder(self)) 101 | } else { 102 | Err(FlokiError::RunContainerFailed { 103 | exit_status: FlokiSubprocessExitStatus { 104 | process_description: "docker run".into(), 105 | exit_status, 106 | }, 107 | } 108 | .into()) 109 | } 110 | } 111 | 112 | pub fn new(image: &str) -> Self { 113 | DockerCommandBuilder { 114 | name: uuid::Uuid::new_v4().to_string(), 115 | volumes: Vec::new(), 116 | environment: Vec::new(), 117 | switches: Vec::new(), 118 | image: image.into(), 119 | } 120 | } 121 | 122 | pub fn name(&self) -> &str { 123 | &self.name 124 | } 125 | 126 | pub fn add_volume(mut self, spec: (&path::PathBuf, &path::PathBuf)) -> Self { 127 | let (src, dst) = spec; 128 | self.volumes.push(Self::volume_mapping(src, dst)); 129 | self 130 | } 131 | 132 | pub fn add_environment, B: AsRef>(mut self, var: V, bind: B) -> Self { 133 | self.environment.push("-e".into()); 134 | self.environment.push(Self::environment_mapping(var, bind)); 135 | self 136 | } 137 | 138 | pub fn add_docker_switch>(mut self, switch: S) -> Self { 139 | self.switches.push(switch.as_ref().into()); 140 | self 141 | } 142 | 143 | pub fn set_working_directory>(self, directory: S) -> Self { 144 | let mut cmd = self; 145 | cmd = cmd.add_docker_switch("-w"); 146 | cmd = cmd.add_docker_switch(directory); 147 | cmd 148 | } 149 | 150 | fn build_volume_switches(&self) -> Vec<&OsStr> { 151 | let mut switches = Vec::new(); 152 | for mapping in self.volumes.iter() { 153 | switches.push("-v".as_ref()); 154 | switches.push(mapping.as_os_str()); 155 | } 156 | switches 157 | } 158 | 159 | fn volume_mapping(src: &path::Path, dst: &path::Path) -> OsString { 160 | let mut mapping = src.to_path_buf().into_os_string(); 161 | mapping.push(":"); 162 | mapping.push(dst); 163 | mapping 164 | } 165 | 166 | fn environment_mapping, B: AsRef>(var: V, bind: B) -> OsString { 167 | let mut binding: OsString = var.as_ref().into(); 168 | binding.push("="); 169 | binding.push(bind); 170 | binding 171 | } 172 | 173 | fn build_environment_switches(&self) -> &Vec { 174 | &self.environment 175 | } 176 | 177 | fn build_docker_switches(&self) -> &Vec { 178 | &self.switches 179 | } 180 | 181 | fn base_args(&self) -> Vec<&OsStr> { 182 | let mut base_args: Vec<&OsStr> = vec!["run".as_ref(), "--rm".as_ref(), "-t".as_ref()]; 183 | if atty::is(atty::Stream::Stdout) && atty::is(atty::Stream::Stdin) { 184 | base_args.push("-i".as_ref()); 185 | } 186 | base_args 187 | } 188 | } 189 | 190 | pub fn enable_forward_ssh_agent( 191 | command: DockerCommandBuilder, 192 | agent_socket: &OsStr, 193 | ) -> DockerCommandBuilder { 194 | debug!("Got SSH_AUTH_SOCK={:?}", agent_socket); 195 | let dir = path::Path::new(agent_socket).to_path_buf(); 196 | command 197 | .add_environment("SSH_AUTH_SOCK", agent_socket) 198 | .add_volume((&dir, &dir)) 199 | } 200 | 201 | pub fn enable_docker_in_docker( 202 | command: DockerCommandBuilder, 203 | dind: &crate::dind::Dind, 204 | ) -> Result { 205 | Ok(command 206 | .add_docker_switch("--link") 207 | .add_docker_switch(format!("{}:floki-docker", dind.name())) 208 | .add_environment("DOCKER_HOST", "tcp://floki-docker:2375")) 209 | } 210 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | /// Configuration file format for floki 2 | use crate::errors::FlokiError; 3 | use crate::image; 4 | use serde::{Deserialize, Serialize}; 5 | use tera::from_value; 6 | use tera::Context; 7 | use tera::Tera; 8 | 9 | use std::collections::BTreeMap; 10 | use std::collections::HashMap; 11 | use std::path::{Path, PathBuf}; 12 | 13 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 14 | #[serde(untagged)] 15 | pub(crate) enum Shell { 16 | Shell(String), 17 | TwoShell { inner: String, outer: String }, 18 | } 19 | 20 | impl Shell { 21 | pub(crate) fn inner_shell(&self) -> &str { 22 | match self { 23 | Shell::Shell(s) => s, 24 | Shell::TwoShell { inner: s, outer: _ } => s, 25 | } 26 | } 27 | 28 | pub(crate) fn outer_shell(&self) -> &str { 29 | match self { 30 | Shell::Shell(s) => s, 31 | Shell::TwoShell { inner: _, outer: s } => s, 32 | } 33 | } 34 | } 35 | 36 | impl Default for Shell { 37 | fn default() -> Self { 38 | Self::Shell("/bin/sh".into()) 39 | } 40 | } 41 | 42 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 43 | #[serde(untagged)] 44 | pub(crate) enum DindConfig { 45 | Toggle(bool), 46 | Image { image: String }, 47 | } 48 | 49 | impl Default for DindConfig { 50 | fn default() -> Self { 51 | DindConfig::Toggle(false) 52 | } 53 | } 54 | 55 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 56 | /// The Volume structure captures configuration for floki volumes 57 | pub(crate) struct Volume { 58 | #[serde(default)] 59 | /// A shared volume is reused by containers which also use a 60 | /// shared volume by the same name. Volumes which are not 61 | /// shared are localised to a particular floki configuration file. 62 | pub(crate) shared: bool, 63 | /// The mount path is the path at which the volume is mounted 64 | /// inside the floki container. 65 | pub(crate) mount: PathBuf, 66 | } 67 | 68 | #[derive(Debug, PartialEq, Serialize, Deserialize, Copy, Clone)] 69 | #[serde(untagged)] 70 | pub(crate) enum Entrypoint { 71 | Suppress { suppress: bool }, 72 | } 73 | 74 | impl Entrypoint { 75 | pub fn value(&self) -> Option<&str> { 76 | match self { 77 | &Entrypoint::Suppress { suppress } if suppress => Some(""), 78 | _ => None, 79 | } 80 | } 81 | } 82 | 83 | impl Default for Entrypoint { 84 | fn default() -> Self { 85 | Self::Suppress { suppress: false } 86 | } 87 | } 88 | 89 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 90 | #[serde(deny_unknown_fields)] 91 | pub(crate) struct FlokiConfig { 92 | pub(crate) image: image::Image, 93 | #[serde(default)] 94 | pub(crate) init: Vec, 95 | #[serde(default)] 96 | pub(crate) shell: Shell, 97 | #[serde(default = "default_mount")] 98 | pub(crate) mount: PathBuf, 99 | #[serde(default)] 100 | pub(crate) docker_switches: Vec, 101 | #[serde(default)] 102 | pub(crate) forward_ssh_agent: bool, 103 | #[serde(default)] 104 | pub(crate) dind: DindConfig, 105 | #[serde(default)] 106 | pub(crate) forward_user: bool, 107 | #[serde(default)] 108 | pub(crate) volumes: BTreeMap, 109 | #[serde(default)] 110 | pub(crate) entrypoint: Entrypoint, 111 | } 112 | 113 | fn default_mount() -> PathBuf { 114 | PathBuf::from("/src") 115 | } 116 | 117 | fn path_from_args(args: &HashMap) -> tera::Result { 118 | let file = match args.get("file") { 119 | Some(file) => file, 120 | None => return Err("file parameter is required".into()), 121 | }; 122 | Ok(from_value::(file.clone())?) 123 | } 124 | 125 | enum LoaderType { 126 | Yaml, 127 | Json, 128 | Toml, 129 | } 130 | 131 | fn makeloader(path: &Path, loader: LoaderType) -> impl tera::Function { 132 | // Get the dirname of the Path given (if a file), or just the directory. 133 | let directory = if path.is_file() { 134 | path.parent().expect("File should have a parent directory") 135 | } else { 136 | path 137 | } 138 | .to_path_buf(); 139 | 140 | Box::new(move |args: &HashMap| { 141 | path_from_args(args) 142 | // Calculate the full path using the parent directory 143 | .map(|path| directory.join(path)) 144 | // Read the file as a string 145 | .and_then(|full_path| std::fs::read_to_string(full_path).map_err(Into::into)) 146 | // Parse the file using the relevant parser 147 | .and_then(|contents| match loader { 148 | LoaderType::Yaml => serde_yaml::from_str(&contents) 149 | .map_err(|err| format!("Failed to parse file as YAML: {err}").into()), 150 | LoaderType::Json => serde_json::from_str(&contents) 151 | .map_err(|err| format!("Failed to parse file as JSON: {err}").into()), 152 | LoaderType::Toml => toml::from_str(&contents) 153 | .map_err(|err| format!("Failed to parse file as TOML: {err}").into()), 154 | }) 155 | }) 156 | } 157 | 158 | // Renders a template from a given string. 159 | pub fn render_template(template: &str, source_filename: &Path) -> Result { 160 | let template_path = source_filename.display().to_string(); 161 | debug!("Rendering template: {template_path}"); 162 | 163 | // Get the canonical path for the template. 164 | let canonical_path = std::fs::canonicalize(source_filename).map_err(|err| { 165 | FlokiError::ProblemNormalizingFilePath { 166 | name: template_path.clone(), 167 | error: err, 168 | } 169 | })?; 170 | debug!("Canonical path: {canonical_path:?}"); 171 | 172 | // Read the template using tera 173 | let mut tera = Tera::default(); 174 | 175 | // Allow templates to load variables files as Values. 176 | tera.register_function("yaml", makeloader(&canonical_path, LoaderType::Yaml)); 177 | tera.register_function("json", makeloader(&canonical_path, LoaderType::Json)); 178 | tera.register_function("toml", makeloader(&canonical_path, LoaderType::Toml)); 179 | 180 | tera.add_raw_template(&template_path, template) 181 | .map_err(|e| FlokiError::ProblemRenderingTemplate { 182 | name: template_path.clone(), 183 | error: e, 184 | })?; 185 | 186 | // Read the environment variables and store them in a tera context 187 | // under the `env` name. 188 | let vars: HashMap = std::env::vars().collect(); 189 | let mut context = Context::new(); 190 | context.insert("env", &vars); 191 | 192 | // Render the floki file to string using the context. 193 | tera.render(&template_path, &context) 194 | .map_err(|e| FlokiError::ProblemRenderingTemplate { 195 | name: template_path.clone(), 196 | error: e, 197 | }) 198 | } 199 | 200 | impl FlokiConfig { 201 | pub fn render(file: &Path) -> Result { 202 | let content = 203 | std::fs::read_to_string(file).map_err(|e| FlokiError::ProblemOpeningConfigYaml { 204 | name: file.display().to_string(), 205 | error: e, 206 | })?; 207 | 208 | // Render the template first before parsing it. 209 | render_template(&content, file) 210 | } 211 | 212 | pub fn from_file(file: &Path) -> Result { 213 | debug!("Reading configuration file: {:?}", file); 214 | 215 | // Render the output from the configuration file before parsing. 216 | let output = Self::render(file)?; 217 | 218 | // Parse the rendered floki file from the string. 219 | let mut config: FlokiConfig = 220 | serde_yaml::from_str(&output).map_err(|e| FlokiError::ProblemParsingConfigYaml { 221 | name: file.display().to_string(), 222 | error: e, 223 | })?; 224 | 225 | // Ensure the path to an external yaml file is correct. 226 | // If the image.yaml.path file is relative, then it should 227 | // be relative to the floki config file. At this point we 228 | // already have the path to the floki config file, so we 229 | // just prepend that to image.yaml.path. 230 | if let image::Image::Yaml { ref mut yaml } = config.image { 231 | if yaml.file.is_relative() { 232 | yaml.file = file 233 | .parent() 234 | .ok_or_else(|| FlokiError::InternalAssertionFailed { 235 | description: format!( 236 | "could not construct path to external yaml file '{:?}'", 237 | &yaml.file 238 | ), 239 | })? 240 | .join(yaml.file.clone()); 241 | } 242 | } 243 | 244 | debug!( 245 | "Parsed '{}' into configuration: {:?}", 246 | file.display(), 247 | &config 248 | ); 249 | 250 | Ok(config) 251 | } 252 | } 253 | 254 | #[cfg(test)] 255 | mod test { 256 | use super::*; 257 | 258 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 259 | struct TestShellConfig { 260 | shell: Shell, 261 | } 262 | 263 | #[test] 264 | fn test_single_shell_config() { 265 | let yaml = "shell: bash"; 266 | let expected = TestShellConfig { 267 | shell: Shell::Shell("bash".into()), 268 | }; 269 | let actual: TestShellConfig = serde_yaml::from_str(yaml).unwrap(); 270 | assert!(actual == expected); 271 | } 272 | 273 | #[test] 274 | fn test_two_shell_config() { 275 | let yaml = "shell:\n outer: sh\n inner: bash"; 276 | let expected_shell = Shell::TwoShell { 277 | inner: "bash".into(), 278 | outer: "sh".into(), 279 | }; 280 | let expected = TestShellConfig { 281 | shell: expected_shell, 282 | }; 283 | let actual: TestShellConfig = serde_yaml::from_str(yaml).unwrap(); 284 | assert!(actual == expected); 285 | } 286 | 287 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 288 | struct TestDindConfig { 289 | dind: DindConfig, 290 | } 291 | 292 | #[test] 293 | fn test_dind_enabled_config() { 294 | let yaml = "dind: true"; 295 | let expected = TestDindConfig { 296 | dind: DindConfig::Toggle(true), 297 | }; 298 | let actual: TestDindConfig = serde_yaml::from_str(yaml).unwrap(); 299 | assert_eq!(actual, expected); 300 | } 301 | 302 | #[test] 303 | fn test_dind_image_config() { 304 | let yaml = "dind:\n image: dind:custom"; 305 | let expected = TestDindConfig { 306 | dind: DindConfig::Image { 307 | image: "dind:custom".into(), 308 | }, 309 | }; 310 | let actual: TestDindConfig = serde_yaml::from_str(yaml).unwrap(); 311 | assert_eq!(actual, expected); 312 | } 313 | 314 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 315 | struct TestEntrypointConfig { 316 | entrypoint: Entrypoint, 317 | } 318 | 319 | #[test] 320 | fn test_entrypoint_suppress() { 321 | let yaml = "entrypoint:\n suppress: true"; 322 | let expected = TestEntrypointConfig { 323 | entrypoint: Entrypoint::Suppress { suppress: true }, 324 | }; 325 | let actual: TestEntrypointConfig = serde_yaml::from_str(yaml).unwrap(); 326 | assert_eq!(actual, expected); 327 | assert_eq!(actual.entrypoint.value(), Some("")); 328 | } 329 | 330 | #[test] 331 | fn test_entrypoint_no_suppress() { 332 | let yaml = "entrypoint:\n suppress: false"; 333 | let expected = TestEntrypointConfig { 334 | entrypoint: Entrypoint::Suppress { suppress: false }, 335 | }; 336 | let actual: TestEntrypointConfig = serde_yaml::from_str(yaml).unwrap(); 337 | assert_eq!(actual, expected); 338 | assert_eq!(actual.entrypoint.value(), None); 339 | } 340 | 341 | #[test] 342 | fn test_tera_render() -> Result<(), Box> { 343 | let template = r#"{% set var = "test" %}image: {{ var }}"#; 344 | let config = render_template(template, Path::new("floki.yaml"))?; 345 | assert_eq!(config, "image: test"); 346 | Ok(()) 347 | } 348 | 349 | #[test] 350 | fn test_tera_yamlload() -> Result<(), Box> { 351 | let template = 352 | r#"{% set values = yaml(file="test_resources/values.yaml") %}shell: {{ values.foo }}"#; 353 | let config = render_template(template, Path::new("floki.yaml"))?; 354 | assert_eq!(config, "shell: bar"); 355 | Ok(()) 356 | } 357 | 358 | #[test] 359 | fn test_tera_jsonload() -> Result<(), Box> { 360 | let template = 361 | r#"{% set values = json(file="test_resources/values.json") %}shell: {{ values.foo }}"#; 362 | let config = render_template(template, Path::new("floki.yaml"))?; 363 | assert_eq!(config, "shell: bar"); 364 | Ok(()) 365 | } 366 | 367 | #[test] 368 | fn test_tera_tomlload() -> Result<(), Box> { 369 | let template = 370 | r#"{% set values = toml(file="Cargo.toml") %}floki: {{ values.package.name }}"#; 371 | let config = render_template(template, Path::new("floki.yaml"))?; 372 | assert_eq!(config, "floki: floki"); 373 | Ok(()) 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /src/dind.rs: -------------------------------------------------------------------------------- 1 | /// Docker-in-docker structures 2 | use anyhow::Error; 3 | use std::path; 4 | 5 | use crate::command::{DaemonHandle, DockerCommandBuilder}; 6 | use crate::image::{image_exists_locally, pull_image}; 7 | 8 | pub const DEFAULT_DIND_IMAGE: &str = "docker:dind"; 9 | 10 | #[derive(Debug)] 11 | pub struct Dind { 12 | command: DockerCommandBuilder, 13 | } 14 | 15 | impl Dind { 16 | pub fn new(image: &str, mount: (&path::PathBuf, &path::PathBuf)) -> Self { 17 | Dind { 18 | command: DockerCommandBuilder::new(image) 19 | .add_docker_switch("--privileged") 20 | .add_volume(mount), 21 | } 22 | } 23 | 24 | pub fn name(&self) -> &str { 25 | self.command.name() 26 | } 27 | 28 | pub fn launch(self) -> Result { 29 | info!( 30 | "Starting docker:dind container with name {}", 31 | self.command.name() 32 | ); 33 | let handle = self.command.start_as_daemon(&[ 34 | "dockerd", 35 | "--tls=false", 36 | "--host=tcp://0.0.0.0:2375", 37 | ])?; 38 | info!("docker:dind launched"); 39 | Ok(handle) 40 | } 41 | } 42 | 43 | /// Check the docker dind image is available 44 | pub fn dind_preflight(image: &str) -> Result<(), Error> { 45 | if image_exists_locally(image)? { 46 | Ok(()) 47 | } else { 48 | pull_image(image) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/environment.rs: -------------------------------------------------------------------------------- 1 | /// Query the current user environment 2 | use crate::errors::FlokiError; 3 | use anyhow::Error; 4 | use std::env; 5 | use std::ffi::OsString; 6 | use std::path; 7 | 8 | #[derive(Debug, Clone, Copy)] 9 | pub struct User { 10 | /// The users uid 11 | pub uid: nix::unistd::Uid, 12 | /// The users gid 13 | pub gid: nix::unistd::Gid, 14 | } 15 | 16 | impl User { 17 | /// Get the user and group ids of the current user 18 | fn current() -> Self { 19 | let uid = nix::unistd::getuid(); 20 | let gid = nix::unistd::getgid(); 21 | debug!("Current user has uid {} and group {}", uid, gid); 22 | Self { uid, gid } 23 | } 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct Environment { 28 | /// User uid and gid 29 | pub user_details: User, 30 | /// The directory floki was launched in 31 | pub current_directory: path::PathBuf, 32 | /// The root directory for floki (may be different from 33 | /// the above if we had to search for floki.yaml 34 | pub floki_root: path::PathBuf, 35 | /// Absolute path to the configuration file 36 | pub config_file: path::PathBuf, 37 | /// Path to ssh socket if found 38 | pub ssh_agent_socket: Option, 39 | /// The host folder that floki uses to e.g. create directories 40 | /// to back volumes 41 | pub floki_workspace: path::PathBuf, 42 | } 43 | 44 | impl Environment { 45 | /// Gather information on the environment floki is running in 46 | pub fn gather(config_file: &Option) -> Result { 47 | let (floki_root, config_path) = resolve_floki_root_and_config(config_file)?; 48 | let user = User::current(); 49 | 50 | let env = Environment { 51 | user_details: user, 52 | current_directory: get_current_working_directory()?, 53 | floki_root, 54 | config_file: normalize_path(config_path)?, 55 | ssh_agent_socket: get_ssh_agent_socket_path(), 56 | floki_workspace: get_floki_work_path(user.uid), 57 | }; 58 | 59 | debug!("Got environment {:?}", &env); 60 | 61 | Ok(env) 62 | } 63 | } 64 | 65 | /// Get the current working directory as a String 66 | fn get_current_working_directory() -> Result { 67 | Ok(env::current_dir()?) 68 | } 69 | 70 | /// Get the path of the ssh agent socket from the SSH_AUTH_SOCK 71 | /// environment variable 72 | fn get_ssh_agent_socket_path() -> Option { 73 | env::var_os("SSH_AUTH_SOCK") 74 | } 75 | 76 | /// Search all ancestors of the current directory for a floki.yaml file name. 77 | fn find_floki_yaml(current_directory: &path::Path) -> Result { 78 | current_directory 79 | .ancestors() 80 | .map(|a| a.join("floki.yaml")) 81 | .find(|f| f.is_file()) 82 | .ok_or_else(|| FlokiError::ProblemFindingConfigYaml {}.into()) 83 | } 84 | 85 | /// Take a file path, and return a tuple consisting of its parent directory and the file path 86 | fn locate_file_in_parents(path: path::PathBuf) -> Result<(path::PathBuf, path::PathBuf), Error> { 87 | let dir = path 88 | .parent() 89 | .ok_or_else(|| FlokiError::InternalAssertionFailed { 90 | description: format!("config_file '{:?}' does not have a parent", &path), 91 | })? 92 | .to_path_buf(); 93 | Ok((dir, path)) 94 | } 95 | 96 | /// Resolve floki root directory and path to configuration file. The floki root directory 97 | /// here is the folder in which the floki.yaml was found when no configuration file 98 | /// is specified, and we have to search for it. 99 | fn resolve_floki_root_and_config( 100 | config_file: &Option, 101 | ) -> Result<(path::PathBuf, path::PathBuf), Error> { 102 | match config_file { 103 | Some(path) => Ok((get_current_working_directory()?, path.clone())), 104 | None => Ok(locate_file_in_parents(find_floki_yaml( 105 | &get_current_working_directory()?, 106 | )?)?), 107 | } 108 | } 109 | 110 | /// Resolve a directory for floki to use for user-global file (caches etc) 111 | fn get_floki_work_path(uid: nix::unistd::Uid) -> path::PathBuf { 112 | let root: path::PathBuf = env::var("HOME").unwrap_or(format!("/tmp/{}/", uid)).into(); 113 | root.join(".floki") 114 | } 115 | 116 | /// Normalize the filepath - this turns a relative path into an absolute one - to 117 | /// do this it must locate the file in the filesystem, and hence it may fail. 118 | fn normalize_path(path: path::PathBuf) -> Result { 119 | let res = std::fs::canonicalize(&path).map_err(|e| FlokiError::ProblemNormalizingFilePath { 120 | name: path.display().to_string(), 121 | error: e, 122 | })?; 123 | 124 | Ok(res) 125 | } 126 | 127 | #[cfg(test)] 128 | mod test { 129 | use super::*; 130 | use anyhow::anyhow; 131 | use std::fs; 132 | 133 | fn touch_file(path: &path::Path) -> Result<(), Error> { 134 | fs::create_dir_all( 135 | path.parent() 136 | .ok_or_else(|| anyhow!("Unable to take parent of path"))?, 137 | )?; 138 | fs::OpenOptions::new() 139 | .truncate(false) 140 | .create(true) 141 | .write(true) 142 | .open(path)?; 143 | Ok(()) 144 | } 145 | 146 | #[test] 147 | fn test_find_floki_yaml_current_dir() -> Result<(), Error> { 148 | let tmp_dir = tempfile::TempDir::new()?; 149 | let floki_yaml_path = tmp_dir.path().join("floki.yaml"); 150 | touch_file(&floki_yaml_path)?; 151 | assert_eq!(find_floki_yaml(tmp_dir.path())?, floki_yaml_path); 152 | Ok(()) 153 | } 154 | 155 | #[test] 156 | fn test_find_floki_yaml_ancestor() -> Result<(), Error> { 157 | let tmp_dir = tempfile::TempDir::new()?; 158 | let floki_yaml_path = tmp_dir.path().join("floki.yaml"); 159 | touch_file(&floki_yaml_path)?; 160 | assert_eq!( 161 | find_floki_yaml(&tmp_dir.path().join("dir/subdir"))?, 162 | floki_yaml_path 163 | ); 164 | Ok(()) 165 | } 166 | 167 | #[test] 168 | fn test_find_floki_yaml_sibling() -> Result<(), Error> { 169 | let tmp_dir = tempfile::TempDir::new()?; 170 | let floki_yaml_path = tmp_dir.path().join("src/floki.yaml"); 171 | touch_file(&floki_yaml_path)?; 172 | assert!(find_floki_yaml(&tmp_dir.path().join("include")).is_err()); 173 | Ok(()) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | /// Error type for floki 2 | use std::fmt; 3 | use std::io; 4 | use std::process::ExitStatus; 5 | 6 | /// FlokiSubprocessExitStatus is a structure which wraps an exit status 7 | /// with a process description so we can pretty-print it. 8 | pub struct FlokiSubprocessExitStatus { 9 | pub process_description: String, 10 | pub exit_status: ExitStatus, 11 | } 12 | 13 | /// Error types for Floki 14 | #[derive(Debug, thiserror::Error)] 15 | pub enum FlokiError { 16 | #[error("No floki.yaml found in tree")] 17 | ProblemFindingConfigYaml {}, 18 | 19 | #[error("Could not normalize the file path '{name}': {error:?}")] 20 | ProblemNormalizingFilePath { name: String, error: io::Error }, 21 | 22 | #[error("There was a problem rendering the template '{name}': {error:?}")] 23 | ProblemRenderingTemplate { name: String, error: tera::Error }, 24 | 25 | #[error("There was a problem opening the configuration file '{name}': {error:?}")] 26 | ProblemOpeningConfigYaml { name: String, error: std::io::Error }, 27 | 28 | #[error("There was a problem parsing the configuration file '{name}': {error:?}")] 29 | ProblemParsingConfigYaml { 30 | name: String, 31 | error: serde_yaml::Error, 32 | }, 33 | 34 | #[error("Running docker command failed with error: {error:?}")] 35 | FailedToLaunchDocker { error: io::Error }, 36 | 37 | #[error("Failed to complete docker command with error: {error:?}")] 38 | FailedToCompleteDockerCommand { error: io::Error }, 39 | 40 | #[error("Failed to pull docker image '{image}': {exit_status:?}")] 41 | FailedToPullImage { 42 | image: String, 43 | exit_status: FlokiSubprocessExitStatus, 44 | }, 45 | 46 | #[error("Failed to build docker image '{image}': {exit_status}")] 47 | FailedToBuildImage { 48 | image: String, 49 | exit_status: FlokiSubprocessExitStatus, 50 | }, 51 | 52 | #[error("Failed to check existence of image '{image}': {error:?}")] 53 | FailedToCheckForImage { image: String, error: io::Error }, 54 | 55 | #[error("Failed to find the key '{key}' in file '{file}'")] 56 | FailedToFindYamlKey { key: String, file: String }, 57 | 58 | #[error("Running container failed: {exit_status:?}")] 59 | RunContainerFailed { 60 | exit_status: FlokiSubprocessExitStatus, 61 | }, 62 | 63 | #[error("Unable to forward ssh socket - cannot find SSH_AUTH_SOCK in environment - do you have an ssh agent running?")] 64 | NoSshAuthSock {}, 65 | 66 | #[error("Malformed item in docker_switches: {item}")] 67 | MalformedDockerSwitch { item: String }, 68 | 69 | /// Internal error for floki - these represent failed assumptions of 70 | /// the developers, and shouldn't actually manifest. 71 | #[error("An internal assertion failed '{description}'. This is probably a bug!")] 72 | InternalAssertionFailed { description: String }, 73 | 74 | #[error("Invalid verbosity setting of {setting:?}. Use a setting between 0 and 3 (-vvv)")] 75 | InvalidVerbositySetting { setting: u8 }, 76 | } 77 | 78 | /// Generate a summary string for a process exiting 79 | fn exit_code_diagnosis(exit_status: &ExitStatus) -> String { 80 | match exit_status.code() { 81 | Some(rc) => format!("exited with return code {}", rc), 82 | None => "terminated by a signal".to_string(), 83 | } 84 | } 85 | 86 | /// Custom debug formatter for FlokiSubprocessExitStatus 87 | impl fmt::Debug for FlokiSubprocessExitStatus { 88 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 89 | write!( 90 | f, 91 | "{} {}", 92 | self.process_description, 93 | exit_code_diagnosis(&self.exit_status) 94 | ) 95 | } 96 | } 97 | 98 | /// Custom display formatter for FlokiSubprocessExitStatus 99 | impl fmt::Display for FlokiSubprocessExitStatus { 100 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 101 | write!( 102 | f, 103 | "{} {}", 104 | self.process_description, 105 | exit_code_diagnosis(&self.exit_status) 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/image.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fs; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::{Command, Stdio}; 6 | use yaml_rust2::YamlLoader; 7 | 8 | use crate::errors::{FlokiError, FlokiSubprocessExitStatus}; 9 | 10 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] 11 | pub struct BuildSpec { 12 | name: String, 13 | #[serde(default = "default_dockerfile")] 14 | dockerfile: PathBuf, 15 | #[serde(default = "default_context")] 16 | context: PathBuf, 17 | target: Option, 18 | } 19 | 20 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] 21 | pub struct YamlSpec { 22 | pub file: PathBuf, 23 | key: String, 24 | } 25 | 26 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] 27 | pub struct ExecSpec { 28 | command: String, 29 | args: Vec, 30 | image: String, 31 | } 32 | 33 | fn default_dockerfile() -> PathBuf { 34 | "Dockerfile".into() 35 | } 36 | 37 | fn default_context() -> PathBuf { 38 | ".".into() 39 | } 40 | 41 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] 42 | #[serde(untagged)] 43 | pub enum Image { 44 | Name(String), 45 | Build { build: BuildSpec }, 46 | Yaml { yaml: YamlSpec }, 47 | Exec { exec: ExecSpec }, 48 | } 49 | 50 | impl Image { 51 | /// Name of the image 52 | pub fn name(&self) -> Result { 53 | match *self { 54 | Image::Name(ref s) => Ok(s.clone()), 55 | Image::Build { ref build } => Ok(build.name.clone() + ":floki"), 56 | Image::Yaml { ref yaml } => { 57 | let contents = fs::read_to_string(&yaml.file)?; 58 | let raw = YamlLoader::load_from_str(&contents)?; 59 | let path = yaml.key.split('.').collect::>(); 60 | let mut val = &raw[0]; 61 | 62 | for key in &path { 63 | // Yaml arrays and maps with scalar keys can both be indexed by 64 | // usize, so heuristically prefer a usize index to a &str index. 65 | val = match key.parse::() { 66 | Ok(x) => &val[x], 67 | Err(_) => &val[*key], 68 | }; 69 | } 70 | val.as_str() 71 | .map(std::string::ToString::to_string) 72 | .ok_or_else(|| { 73 | FlokiError::FailedToFindYamlKey { 74 | key: yaml.key.to_string(), 75 | file: yaml.file.display().to_string(), 76 | } 77 | .into() 78 | }) 79 | } 80 | Image::Exec { ref exec } => Ok(exec.image.clone()), 81 | } 82 | } 83 | 84 | /// Do the required work to get the image, and then return 85 | /// it's name 86 | pub fn obtain_image(&self, floki_root: &Path) -> Result { 87 | match *self { 88 | // Deal with the case where want to build an image 89 | Image::Build { ref build } => { 90 | let mut command = Command::new("docker"); 91 | command 92 | .arg("build") 93 | .arg("-t") 94 | .arg(self.name()?) 95 | .arg("-f") 96 | .arg(floki_root.join(&build.dockerfile)); 97 | 98 | if let Some(target) = &build.target { 99 | command.arg("--target").arg(target); 100 | } 101 | 102 | let exit_status = command 103 | .arg(floki_root.join(&build.context)) 104 | .spawn()? 105 | .wait()?; 106 | if exit_status.success() { 107 | Ok(self.name()?) 108 | } else { 109 | Err(FlokiError::FailedToBuildImage { 110 | image: self.name()?, 111 | exit_status: FlokiSubprocessExitStatus { 112 | process_description: "docker build".into(), 113 | exit_status, 114 | }, 115 | } 116 | .into()) 117 | } 118 | } 119 | Image::Exec { ref exec } => { 120 | let exit_status = Command::new(&exec.command) 121 | .args(&exec.args) 122 | .spawn()? 123 | .wait()?; 124 | 125 | if exit_status.success() { 126 | Ok(self.name()?) 127 | } else { 128 | Err(FlokiError::FailedToBuildImage { 129 | image: self.name()?, 130 | exit_status: FlokiSubprocessExitStatus { 131 | process_description: exec.command.clone(), 132 | exit_status, 133 | }, 134 | } 135 | .into()) 136 | } 137 | } 138 | // All other cases we just return the name 139 | _ => Ok(self.name()?), 140 | } 141 | } 142 | } 143 | 144 | // Now we have some functions which are useful in general 145 | 146 | /// Wrapper to pull an image by it's name 147 | pub fn pull_image(name: &str) -> Result<(), Error> { 148 | debug!("Pulling image: {}", name); 149 | let exit_status = Command::new("docker") 150 | .arg("pull") 151 | .arg(name) 152 | .spawn()? 153 | .wait()?; 154 | 155 | if exit_status.success() { 156 | Ok(()) 157 | } else { 158 | Err(FlokiError::FailedToPullImage { 159 | image: name.into(), 160 | exit_status: FlokiSubprocessExitStatus { 161 | process_description: "docker pull".into(), 162 | exit_status, 163 | }, 164 | } 165 | .into()) 166 | } 167 | } 168 | 169 | /// Determine whether an image exists locally 170 | pub fn image_exists_locally(name: &str) -> Result { 171 | debug!("Checking for image: {}", name); 172 | let ret = Command::new("docker") 173 | .args(["history", name]) 174 | .stdin(Stdio::null()) 175 | .stdout(Stdio::null()) 176 | .stderr(Stdio::null()) 177 | .status() 178 | .map_err(|e| FlokiError::FailedToCheckForImage { 179 | image: name.to_string(), 180 | error: e, 181 | })?; 182 | Ok(ret.code() == Some(0)) 183 | } 184 | 185 | #[cfg(test)] 186 | mod test { 187 | use super::*; 188 | 189 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 190 | struct TestImage { 191 | image: Image, 192 | } 193 | 194 | #[test] 195 | fn test_image_spec_by_string() { 196 | let yaml = "image: foo"; 197 | let expected = TestImage { 198 | image: Image::Name("foo".into()), 199 | }; 200 | let actual: TestImage = serde_yaml::from_str(yaml).unwrap(); 201 | assert!(actual == expected); 202 | } 203 | 204 | #[test] 205 | fn test_image_spec_by_build_spec() { 206 | let yaml = "image:\n build:\n name: foo\n dockerfile: Dockerfile.test \n context: ./context\n target: builder"; 207 | let expected = TestImage { 208 | image: Image::Build { 209 | build: BuildSpec { 210 | name: "foo".into(), 211 | dockerfile: "Dockerfile.test".into(), 212 | context: "./context".into(), 213 | target: Some("builder".into()), 214 | }, 215 | }, 216 | }; 217 | let actual: TestImage = serde_yaml::from_str(yaml).unwrap(); 218 | assert!(actual == expected); 219 | } 220 | 221 | #[test] 222 | fn test_image_spec_by_exec_spec() { 223 | let yaml = r#" 224 | image: 225 | exec: 226 | command: foo 227 | args: 228 | - build 229 | image: "foobuild:1.0.0" 230 | "#; 231 | let expected = TestImage { 232 | image: Image::Exec { 233 | exec: ExecSpec { 234 | command: "foo".into(), 235 | args: vec!["build".into()], 236 | image: "foobuild:1.0.0".into(), 237 | }, 238 | }, 239 | }; 240 | let actual: TestImage = serde_yaml::from_str(yaml).unwrap(); 241 | assert!(actual == expected); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/interpret.rs: -------------------------------------------------------------------------------- 1 | use crate::command; 2 | use crate::command::DockerCommandBuilder; 3 | use crate::dind::Dind; 4 | use crate::spec; 5 | use crate::volumes::resolve_volume_mounts; 6 | 7 | use anyhow::Error; 8 | use std::path; 9 | 10 | pub(crate) fn run_floki_container( 11 | spec: &spec::FlokiSpec, 12 | inner_command: &str, 13 | ) -> Result<(), Error> { 14 | spec.image.obtain_image(&spec.paths.root)?; 15 | 16 | let mut cmd = command::DockerCommandBuilder::new(&spec.image.name()?) 17 | .add_volume((&spec.paths.root, &spec.mount)); 18 | 19 | let volumes = resolve_volume_mounts(&spec.paths.config, &spec.paths.workspace, &spec.volumes); 20 | instantiate_volumes(&volumes)?; 21 | 22 | cmd = configure_volumes(cmd, &volumes); 23 | cmd = cmd.add_environment("FLOKI_HOST_MOUNTDIR", &spec.paths.root); 24 | cmd = cmd.add_environment("FLOKI_HOST_UID", spec.user.uid.to_string()); 25 | cmd = cmd.add_environment("FLOKI_HOST_GID", spec.user.gid.to_string()); 26 | cmd = cmd.add_environment("FLOKI_WORKING_DIR", &spec.paths.internal_working_directory); 27 | cmd = cmd.set_working_directory(&spec.paths.internal_working_directory); 28 | 29 | if spec.user.forward { 30 | cmd = cmd 31 | .add_docker_switch("--user") 32 | .add_docker_switch(format!("{}:{}", spec.user.uid, spec.user.gid)); 33 | } 34 | 35 | if let Some(spec::SshAgent { path }) = &spec.ssh_agent { 36 | cmd = command::enable_forward_ssh_agent(cmd, path); 37 | } 38 | 39 | if let Some(entrypoint) = &spec.entrypoint { 40 | cmd = cmd.add_docker_switch(format!("--entrypoint={}", entrypoint)) 41 | } 42 | 43 | for switch in &spec.docker_switches { 44 | cmd = cmd.add_docker_switch(switch); 45 | } 46 | 47 | // Finally configure dind, taking care to hold a handle for the linked dind container 48 | let _handle = if let Some(spec::Dind { image }) = &spec.dind { 49 | let dind = Dind::new(image, (&spec.paths.root, &spec.mount)); 50 | cmd = command::enable_docker_in_docker(cmd, &dind)?; 51 | crate::dind::dind_preflight(image)?; 52 | Some(dind.launch()?) 53 | } else { 54 | None 55 | }; 56 | 57 | // Calculate the outer shell command. 58 | let subshell_command = subshell_command(&spec.init, inner_command); 59 | let mut outer_shell_cmd = shell_words::split(spec.shell.outer_shell())?; 60 | outer_shell_cmd.push("-c".to_string()); 61 | outer_shell_cmd.push(subshell_command); 62 | 63 | cmd.run(outer_shell_cmd) 64 | } 65 | 66 | pub(crate) fn command_in_shell(shell: &str, command: &[String]) -> String { 67 | // Make sure our command runs in a subshell (we might switch user) 68 | let inner_shell: String = shell.to_string(); 69 | inner_shell + " -c \"" + &command.join(" ") + "\"" 70 | } 71 | 72 | /// Add mounts for each of the passed in volumes 73 | fn configure_volumes( 74 | cmd: DockerCommandBuilder, 75 | volumes: &[(path::PathBuf, &path::PathBuf)], 76 | ) -> DockerCommandBuilder { 77 | let mut cmd = cmd; // Shadow as mutable 78 | for (src, dst) in volumes.iter() { 79 | cmd = cmd.add_volume((src, dst)); 80 | } 81 | cmd 82 | } 83 | 84 | /// Create the backing directories for floki volumes if needed 85 | fn instantiate_volumes(volumes: &[(path::PathBuf, &path::PathBuf)]) -> Result<(), Error> { 86 | for (src, _) in volumes.iter() { 87 | std::fs::create_dir_all(src)?; 88 | } 89 | Ok(()) 90 | } 91 | 92 | /// Turn the init section of a floki.yaml file into a command 93 | /// that can be given to a shell 94 | fn subshell_command(init: &[String], command: &str) -> String { 95 | let mut args: Vec<&str> = init.iter().map(|s| s as &str).collect::>(); 96 | args.push(command); 97 | args.join(" && ") 98 | } 99 | 100 | #[cfg(test)] 101 | mod test { 102 | use super::*; 103 | 104 | #[test] 105 | fn test_command_in_shell() { 106 | let subcommand = vec![String::from("foo"), String::from("bar")]; 107 | 108 | let result = command_in_shell("bash", &subcommand); 109 | let expected = String::from("bash -c \"foo bar\""); 110 | 111 | assert!(result == expected); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | /// floki - the development container launcher 2 | #[macro_use] 3 | extern crate log; 4 | 5 | mod cli; 6 | mod command; 7 | mod config; 8 | mod dind; 9 | mod environment; 10 | mod errors; 11 | mod image; 12 | mod interpret; 13 | mod spec; 14 | mod volumes; 15 | 16 | use anyhow::Error; 17 | use cli::{Cli, Subcommand}; 18 | use config::FlokiConfig; 19 | use environment::Environment; 20 | use errors::FlokiError; 21 | use structopt::StructOpt; 22 | 23 | fn main() -> Result<(), Error> { 24 | let args = Cli::from_args(); 25 | configure_logging(args.verbosity)?; 26 | 27 | match run_floki_from_args(&args) { 28 | Ok(()) => (), 29 | Err(e) => { 30 | error!("A problem occurred: {}", e); 31 | std::process::exit(1); 32 | } 33 | } 34 | Ok(()) 35 | } 36 | 37 | /// Decide which commands to run given the input from the shell 38 | fn run_floki_from_args(args: &Cli) -> Result<(), Error> { 39 | debug!("Got command line arguments: {:?}", &args); 40 | 41 | if args.local { 42 | warn!("-l/--local is deprecated and may be removed in a future release"); 43 | } 44 | 45 | // Dispatch appropriate subcommand 46 | match &args.subcommand { 47 | // Pull the image in the configuration file 48 | Some(Subcommand::Pull {}) => { 49 | let env = Environment::gather(&args.config_file)?; 50 | let config = FlokiConfig::from_file(&env.config_file)?; 51 | image::pull_image(&config.image.name()?) 52 | } 53 | 54 | // Run a command in the floki container 55 | Some(Subcommand::Run { command }) => { 56 | let env = Environment::gather(&args.config_file)?; 57 | let config = FlokiConfig::from_file(&env.config_file)?; 58 | let inner_command = interpret::command_in_shell(config.shell.inner_shell(), command); 59 | interpret::run_floki_container(&spec::FlokiSpec::from(config, env)?, &inner_command) 60 | } 61 | 62 | Some(Subcommand::Completion { shell }) => { 63 | Cli::clap().gen_completions_to("floki", *shell, &mut std::io::stdout()); 64 | Ok(()) 65 | } 66 | 67 | Some(Subcommand::Render {}) => { 68 | let env = Environment::gather(&args.config_file)?; 69 | let contents = FlokiConfig::render(&env.config_file)?; 70 | println!("{contents}"); 71 | Ok(()) 72 | } 73 | 74 | // Launch an interactive floki shell (the default) 75 | None => { 76 | let env = Environment::gather(&args.config_file)?; 77 | let config = FlokiConfig::from_file(&env.config_file)?; 78 | let inner_command = config.shell.inner_shell().to_string(); 79 | interpret::run_floki_container(&spec::FlokiSpec::from(config, env)?, &inner_command) 80 | } 81 | } 82 | } 83 | 84 | /// Configure the logger 85 | fn configure_logging(verbosity: u8) -> Result<(), Error> { 86 | let level = match verbosity { 87 | 0 => log::LevelFilter::Warn, 88 | 1 => log::LevelFilter::Info, 89 | 2 => log::LevelFilter::Debug, 90 | 3 => log::LevelFilter::Trace, 91 | _ => return Err(FlokiError::InvalidVerbositySetting { setting: verbosity }.into()), 92 | }; 93 | simplelog::TermLogger::init( 94 | level, 95 | simplelog::Config::default(), 96 | simplelog::TerminalMode::Stderr, 97 | simplelog::ColorChoice::Auto, 98 | )?; 99 | Ok(()) 100 | } 101 | -------------------------------------------------------------------------------- /src/spec.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{DindConfig, FlokiConfig}; 2 | use crate::dind::DEFAULT_DIND_IMAGE; 3 | use crate::environment::Environment; 4 | use crate::errors; 5 | 6 | use anyhow::Error; 7 | 8 | use std::collections::BTreeMap; 9 | use std::ffi::OsString; 10 | use std::path; 11 | 12 | /// Information for running docker-in-docker 13 | #[derive(Debug)] 14 | pub(crate) struct Dind { 15 | /// The image to use 16 | pub(crate) image: String, 17 | } 18 | 19 | /// Information about the user 20 | #[derive(Debug)] 21 | pub(crate) struct User { 22 | /// Should the user be forwarded? 23 | pub(crate) forward: bool, 24 | /// User host UID 25 | pub(crate) uid: nix::unistd::Uid, 26 | /// User host GID 27 | pub(crate) gid: nix::unistd::Gid, 28 | } 29 | 30 | /// Information about the host SSH agent 31 | #[derive(Debug)] 32 | pub(crate) struct SshAgent { 33 | /// Path to the agents socket 34 | pub(crate) path: OsString, 35 | } 36 | 37 | /// Paths used for running floki 38 | #[derive(Debug)] 39 | pub(crate) struct Paths { 40 | /// The internal working directory 41 | pub(crate) internal_working_directory: path::PathBuf, 42 | /// The root directory for the project (location of floki.yaml or 43 | /// configuration file) 44 | pub(crate) root: path::PathBuf, 45 | /// The path to the configuration file 46 | pub(crate) config: path::PathBuf, 47 | /// The base directory for storing volumes 48 | pub(crate) workspace: path::PathBuf, 49 | } 50 | 51 | /// FlokiSpec provides a fully resolved and preprocessed block of 52 | /// configuration data which is clearer to construct a command from. 53 | #[derive(Debug)] 54 | pub(crate) struct FlokiSpec { 55 | /// Details of the image to use 56 | pub(crate) image: crate::image::Image, 57 | /// Commands to run on initialization 58 | pub(crate) init: Vec, 59 | /// Shell to use in the environment 60 | pub(crate) shell: crate::config::Shell, 61 | /// Where to mount the working directory 62 | pub(crate) mount: path::PathBuf, 63 | /// Entrypoint 64 | pub(crate) entrypoint: Option, 65 | /// Volumes to mount into the container 66 | pub(crate) volumes: BTreeMap, 67 | /// User details and forwarding 68 | pub(crate) user: User, 69 | /// SSH agent forwarding 70 | pub(crate) ssh_agent: Option, 71 | /// Explicit docker switches to use 72 | pub(crate) docker_switches: Vec, 73 | /// Linked docker environments 74 | pub(crate) dind: Option, 75 | /// Paths on the host which are relevant to running 76 | pub(crate) paths: Paths, 77 | } 78 | 79 | impl FlokiSpec { 80 | pub(crate) fn from(config: FlokiConfig, environ: Environment) -> Result { 81 | let dind = match config.dind { 82 | DindConfig::Toggle(true) => Some(Dind { 83 | image: DEFAULT_DIND_IMAGE.to_string(), 84 | }), 85 | DindConfig::Toggle(false) => None, 86 | DindConfig::Image { image } => Some(Dind { image }), 87 | }; 88 | 89 | let user = User { 90 | forward: config.forward_user, 91 | uid: environ.user_details.uid, 92 | gid: environ.user_details.gid, 93 | }; 94 | 95 | let entrypoint = config.entrypoint.value().map(|v| v.to_string()); 96 | 97 | let ssh_agent = if config.forward_ssh_agent { 98 | if let Some(path) = environ.ssh_agent_socket { 99 | Ok(Some(SshAgent { path })) 100 | } else { 101 | Err(errors::FlokiError::NoSshAuthSock {}) 102 | }? 103 | } else { 104 | None 105 | }; 106 | 107 | let internal_working_directory = get_working_directory( 108 | &environ.current_directory, 109 | &environ.floki_root, 110 | &path::PathBuf::from(&config.mount), 111 | ); 112 | 113 | let paths = Paths { 114 | internal_working_directory, 115 | root: environ.floki_root, 116 | config: environ.config_file, 117 | workspace: environ.floki_workspace, 118 | }; 119 | 120 | let docker_switches = decompose_switches(&config.docker_switches)?; 121 | 122 | let spec = FlokiSpec { 123 | image: config.image, 124 | init: config.init, 125 | mount: config.mount, 126 | shell: config.shell, 127 | entrypoint, 128 | volumes: config.volumes, 129 | user, 130 | ssh_agent, 131 | docker_switches, 132 | dind, 133 | paths, 134 | }; 135 | 136 | debug!("built spec from config and environment: {:?}", spec); 137 | 138 | Ok(spec) 139 | } 140 | } 141 | 142 | fn decompose_switches(specs: &[String]) -> Result, Error> { 143 | let mut flattened = Vec::new(); 144 | 145 | for spec in specs { 146 | if let Some(switches) = shlex::split(spec) { 147 | for s in switches { 148 | flattened.push(s); 149 | } 150 | } else { 151 | return Err(errors::FlokiError::MalformedDockerSwitch { item: spec.into() }.into()); 152 | } 153 | } 154 | 155 | Ok(flattened) 156 | } 157 | 158 | /// Determine what directory we are currently in 159 | fn get_working_directory( 160 | current_directory: &path::Path, 161 | floki_root: &path::Path, 162 | mount: &path::Path, 163 | ) -> path::PathBuf { 164 | mount.join(current_directory.strip_prefix(floki_root).expect( 165 | "failed to deduce working directory - \ 166 | floki_root should always be an ancestor of current_directory", 167 | )) 168 | } 169 | 170 | #[cfg(test)] 171 | mod test { 172 | use super::*; 173 | #[test] 174 | fn test_decompose_switches() -> Result<(), Error> { 175 | let switches = vec!["-e FOO='bar baz'".to_string()]; 176 | 177 | let want: Vec = vec!["-e".to_string(), "FOO=bar baz".to_string()]; 178 | 179 | let got = decompose_switches(&switches)?; 180 | 181 | assert_eq!(want, got); 182 | 183 | Ok(()) 184 | } 185 | 186 | #[test] 187 | fn test_decompose_switches_error() { 188 | let switches = vec!["-e FOO='bar baz".to_string()]; 189 | let got = decompose_switches(&switches); 190 | assert!(got.is_err()); 191 | } 192 | 193 | #[test] 194 | fn test_get_working_directory() { 195 | let current_directory = path::PathBuf::from("/host/workingdir/"); 196 | let floki_root = path::PathBuf::from("/host"); 197 | let mount = path::PathBuf::from("/guest"); 198 | 199 | assert!( 200 | get_working_directory(¤t_directory, &floki_root, &mount) 201 | == path::PathBuf::from("/guest/workingdir/") 202 | ) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/volumes.rs: -------------------------------------------------------------------------------- 1 | use std::path; 2 | use std::{collections::BTreeMap, os::unix::prelude::OsStrExt}; 3 | 4 | use sha2::{Digest, Sha256}; 5 | 6 | use crate::config::Volume; 7 | 8 | static VOLUME_DIRECTORY: &str = "volumes/"; 9 | 10 | pub(crate) fn resolve_volume_mounts<'a>( 11 | config_filepath: &path::Path, 12 | work_path: &path::Path, 13 | volumes: &'a BTreeMap, 14 | ) -> Vec<(path::PathBuf, &'a path::PathBuf)> { 15 | volumes 16 | .iter() 17 | .map(|(name, volume)| { 18 | ( 19 | cache_path(work_path, config_filepath, name, volume), 20 | &volume.mount, 21 | ) 22 | }) 23 | .collect() 24 | } 25 | 26 | fn cache_path( 27 | work_path: &path::Path, 28 | config_filepath: &path::Path, 29 | name: &str, 30 | config: &Volume, 31 | ) -> path::PathBuf { 32 | let folder = prefix_cache(config.shared, config_filepath) + name; 33 | work_path.join(VOLUME_DIRECTORY).join::(folder) 34 | } 35 | 36 | fn prefix_cache(shared: bool, config_filepath: &path::Path) -> String { 37 | if shared { 38 | "".into() 39 | } else { 40 | hash_path(config_filepath) + "-" 41 | } 42 | } 43 | 44 | fn hash_path(path: &path::Path) -> String { 45 | let mut hasher = Sha256::new(); 46 | hasher.update(path.as_os_str().as_bytes()); 47 | format!("{:x}", hasher.finalize()) 48 | } 49 | 50 | #[cfg(test)] 51 | mod test { 52 | use super::*; 53 | use std::path::Path; 54 | 55 | #[test] 56 | fn test_shared_cache_path_is_shared_across_flokis() { 57 | let cache_1 = cache_path( 58 | Path::new("work_path"), 59 | Path::new("/floki/root/1/floki.yaml"), 60 | "cache", 61 | &Volume { 62 | shared: true, 63 | mount: "/".into(), 64 | }, 65 | ); 66 | let cache_2 = cache_path( 67 | Path::new("work_path"), 68 | Path::new("/floki/root/2/floki.yaml"), 69 | "cache", 70 | &Volume { 71 | shared: true, 72 | mount: "/".into(), 73 | }, 74 | ); 75 | 76 | assert_eq!(cache_1, cache_2); 77 | } 78 | 79 | #[test] 80 | fn test_local_cache_path_is_not_shared_across_flokis() { 81 | let cache_1 = cache_path( 82 | Path::new("work_path"), 83 | Path::new("/floki/root/1/floki.yaml"), 84 | "cache", 85 | &Volume { 86 | shared: false, 87 | mount: "/".into(), 88 | }, 89 | ); 90 | let cache_2 = cache_path( 91 | Path::new("work_path"), 92 | Path::new("/floki/root/2/floki.yaml"), 93 | "cache", 94 | &Volume { 95 | shared: false, 96 | mount: "/".into(), 97 | }, 98 | ); 99 | 100 | assert_ne!(cache_1, cache_2); 101 | } 102 | 103 | #[test] 104 | fn test_local_and_shared_caches_dont_collide() { 105 | let cache_shared = cache_path( 106 | Path::new("work_path"), 107 | Path::new("/floki/root/1/floki.yaml"), 108 | "cache", 109 | &Volume { 110 | shared: true, 111 | mount: "/".into(), 112 | }, 113 | ); 114 | let cache_local = cache_path( 115 | Path::new("work_path"), 116 | Path::new("/floki/root/1/floki.yaml"), 117 | "cache", 118 | &Volume { 119 | shared: false, 120 | mount: "/".into(), 121 | }, 122 | ); 123 | 124 | assert_ne!(cache_shared, cache_local); 125 | } 126 | 127 | #[test] 128 | fn test_local_volumes_from_different_configs_dont_collide() { 129 | let cache_shared = cache_path( 130 | Path::new("work_path"), 131 | Path::new("/floki/root/1/floki-alternate.yaml"), 132 | "cache", 133 | &Volume { 134 | shared: false, 135 | mount: "/".into(), 136 | }, 137 | ); 138 | let cache_local = cache_path( 139 | Path::new("work_path"), 140 | Path::new("/floki/root/1/floki.yaml"), 141 | "cache", 142 | &Volume { 143 | shared: false, 144 | mount: "/".into(), 145 | }, 146 | ); 147 | 148 | assert_ne!(cache_shared, cache_local); 149 | } 150 | 151 | #[test] 152 | fn test_path_sha() { 153 | let path = Path::new("/floki/root/1/floki.yaml"); 154 | let hash = hash_path(path); 155 | assert_eq!( 156 | hash, 157 | "04820cace8be1a2e8057c92231963c269cc0fd0fef01fd3fdf2deaffb62dc48d" 158 | ); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /test_resources/values.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar" 3 | } -------------------------------------------------------------------------------- /test_resources/values.yaml: -------------------------------------------------------------------------------- 1 | foo: bar --------------------------------------------------------------------------------