refactor: Create SigningDriver and CiDriver (#197)
This also includes a new `login` command. The signing and CI logic is now using the Driver trait system along with a new experimental sigstore signing driver. New static macros have also been created to make implementation management easier for `Command` usage and `Driver` trait implementation calls. --------- Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>
This commit is contained in:
parent
3ecb0d3d93
commit
8ce83ba7ff
63 changed files with 6468 additions and 2083 deletions
|
|
@ -1,34 +0,0 @@
|
|||
# Add the contents of this file to `config.toml` to enable "fast build" configuration. Please read the notes below.
|
||||
|
||||
# NOTE: For maximum performance, build using a nightly compiler
|
||||
# If you are using rust stable, remove the "-Zshare-generics=y" below.
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = [
|
||||
"-Clink-arg=-fuse-ld=lld", # Use LLD Linker
|
||||
# "-Zshare-generics=y", # (Nightly) Make the current crate share its generic instantiations
|
||||
]
|
||||
|
||||
# NOTE: you must install [Mach-O LLD Port](https://lld.llvm.org/MachO/index.html) on mac. you can easily do this by installing llvm which includes lld with the "brew" package manager:
|
||||
# `brew install llvm`
|
||||
[target.x86_64-apple-darwin]
|
||||
rustflags = [
|
||||
"-Clink-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld", # Use LLD Linker
|
||||
# "-Zshare-generics=y", # (Nightly) Make the current crate share its generic instantiations
|
||||
]
|
||||
|
||||
[target.aarch64-apple-darwin]
|
||||
rustflags = [
|
||||
"-Clink-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld", # Use LLD Linker
|
||||
# "-Zshare-generics=y", # (Nightly) Make the current crate share its generic instantiations
|
||||
]
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
linker = "rust-lld.exe" # Use LLD Linker
|
||||
# rustflags = ["-Zshare-generics=n"]
|
||||
|
||||
# Optional: Uncommenting the following improves compile times, but reduces the amount of debug info to 'line number tables only'
|
||||
# In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains.
|
||||
#[profile.dev]
|
||||
#debug = 1
|
||||
46
.github/workflows/build-pr.yml
vendored
46
.github/workflows/build-pr.yml
vendored
|
|
@ -13,6 +13,42 @@ env:
|
|||
RUST_LOG_STYLE: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: earthly/actions-setup@v1
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Run test
|
||||
id: build
|
||||
run: |
|
||||
earthly --ci +test
|
||||
|
||||
lint:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: earthly/actions-setup@v1
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Run lint
|
||||
id: build
|
||||
run: |
|
||||
earthly --ci +lint
|
||||
|
||||
arm64-prebuild:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -220,7 +256,7 @@ jobs:
|
|||
cd integration-tests/test-repo
|
||||
bluebuild template -vv | tee Containerfile
|
||||
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
|
||||
bluebuild build --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
bluebuild build --retry-push -B docker -I docker -S sigstore --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
|
||||
docker-build-external-login:
|
||||
timeout-minutes: 60
|
||||
|
|
@ -275,7 +311,7 @@ jobs:
|
|||
cd integration-tests/test-repo
|
||||
bluebuild template -vv | tee Containerfile
|
||||
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
|
||||
bluebuild build --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
bluebuild build --retry-push -S sigstore --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
|
||||
podman-build:
|
||||
timeout-minutes: 60
|
||||
|
|
@ -327,10 +363,10 @@ jobs:
|
|||
cd integration-tests/test-repo
|
||||
bluebuild template -vv | tee Containerfile
|
||||
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
|
||||
bluebuild build -B podman --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
bluebuild build --retry-push -B podman -I podman -S sigstore --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
|
||||
buildah-build:
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -379,4 +415,4 @@ jobs:
|
|||
cd integration-tests/test-repo
|
||||
bluebuild template -vv | tee Containerfile
|
||||
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
|
||||
bluebuild build -B buildah --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
bluebuild build --retry-push -B buildah -I podman -S sigstore --squash --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
|
|
|
|||
44
.github/workflows/build.yml
vendored
44
.github/workflows/build.yml
vendored
|
|
@ -16,6 +16,42 @@ env:
|
|||
RUST_LOG_STYLE: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: earthly/actions-setup@v1
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Run build
|
||||
id: build
|
||||
run: |
|
||||
earthly --ci +test
|
||||
|
||||
lint:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: earthly/actions-setup@v1
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Run build
|
||||
id: build
|
||||
run: |
|
||||
earthly --ci +test
|
||||
|
||||
arm64-prebuild:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -218,7 +254,7 @@ jobs:
|
|||
cd integration-tests/test-repo
|
||||
bluebuild template -vv | tee Containerfile
|
||||
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
|
||||
bluebuild build --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
bluebuild build --retry-push -B docker -I docker -S sigstore --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
|
||||
docker-build-external-login:
|
||||
timeout-minutes: 60
|
||||
|
|
@ -273,7 +309,7 @@ jobs:
|
|||
cd integration-tests/test-repo
|
||||
bluebuild template -vv | tee Containerfile
|
||||
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
|
||||
bluebuild build --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
bluebuild build --retry-push -S sigstore --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
|
||||
podman-build:
|
||||
timeout-minutes: 60
|
||||
|
|
@ -325,7 +361,7 @@ jobs:
|
|||
cd integration-tests/test-repo
|
||||
bluebuild template -vv | tee Containerfile
|
||||
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
|
||||
bluebuild build -B podman --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
bluebuild build --retry-push -B podman -I podman -S sigstore --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
|
||||
buildah-build:
|
||||
timeout-minutes: 60
|
||||
|
|
@ -377,4 +413,4 @@ jobs:
|
|||
cd integration-tests/test-repo
|
||||
bluebuild template -vv | tee Containerfile
|
||||
grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1
|
||||
bluebuild build -B buildah --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
bluebuild build --retry-push -B buildah -I podman -S sigstore --squash --push -vv recipes/recipe.yml recipes/recipe-39.yml
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,6 +5,7 @@ result*
|
|||
.direnv/
|
||||
|
||||
cosign.key
|
||||
!test-files/keys/cosign.key
|
||||
|
||||
# Local testing for bluebuild recipe files
|
||||
/config/*
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[hooks]
|
||||
pre-commit = "cargo fmt --check && cargo test && cargo test --all-features && cargo clippy -- -D warnings && cargo clippy --all-features -- -D warnings"
|
||||
pre-push = "cargo fmt --check && cargo test --workspace && cargo test --workspace --all-features && cargo clippy -- -D warnings && cargo clippy --all-features -- -D warnings"
|
||||
|
||||
[logging]
|
||||
verbose = true
|
||||
|
|
|
|||
3036
Cargo.lock
generated
3036
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
38
Cargo.toml
38
Cargo.toml
|
|
@ -1,5 +1,5 @@
|
|||
[workspace]
|
||||
members = ["utils", "recipe", "template"]
|
||||
members = ["utils", "recipe", "template", "process"]
|
||||
|
||||
[workspace.package]
|
||||
description = "A CLI tool built for creating Containerfile templates for ostree based atomic distros"
|
||||
|
|
@ -13,30 +13,29 @@ version = "0.8.12"
|
|||
chrono = "0.4"
|
||||
clap = "4"
|
||||
colored = "2"
|
||||
format_serde_error = "0.3"
|
||||
indexmap = { version = "2", features = ["serde"] }
|
||||
indicatif = { version = "0.17", features = ["improved_unicode"] }
|
||||
indicatif-log-bridge = "0.2"
|
||||
log = "0.4"
|
||||
miette = "7"
|
||||
once_cell = "1"
|
||||
rstest = "0.18"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
tempdir = "0.3"
|
||||
typed-builder = "0.18"
|
||||
users = "0.11"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
correctness = "warn"
|
||||
suspicious = "warn"
|
||||
perf = "warn"
|
||||
style = "warn"
|
||||
nursery = "warn"
|
||||
pedantic = "warn"
|
||||
correctness = "deny"
|
||||
suspicious = "deny"
|
||||
perf = "deny"
|
||||
style = "deny"
|
||||
nursery = "deny"
|
||||
pedantic = "deny"
|
||||
module_name_repetitions = { level = "allow", priority = 1 }
|
||||
|
||||
[package]
|
||||
|
|
@ -59,34 +58,28 @@ pre-release-replacements = [
|
|||
blue-build-recipe = { version = "=0.8.12", path = "./recipe" }
|
||||
blue-build-template = { version = "=0.8.12", path = "./template" }
|
||||
blue-build-utils = { version = "=0.8.12", path = "./utils" }
|
||||
blue-build-process-management = { version = "=0.8.12", path = "./process" }
|
||||
clap-verbosity-flag = "2"
|
||||
clap_complete = "4"
|
||||
clap_complete_nushell = "4"
|
||||
fuzzy-matcher = "0.3"
|
||||
lenient_semver = "0.4"
|
||||
open = "5"
|
||||
os_info = "3"
|
||||
rayon = { version = "1.10.0", optional = true }
|
||||
requestty = { version = "0.5", features = ["macros", "termion"] }
|
||||
semver = { version = "1", features = ["serde"] }
|
||||
shadow-rs = "0.26"
|
||||
urlencoding = "2"
|
||||
users = "0.11"
|
||||
|
||||
chrono.workspace = true
|
||||
clap = { workspace = true, features = ["derive", "cargo", "unicode", "env"] }
|
||||
colored.workspace = true
|
||||
indexmap.workspace = true
|
||||
indicatif.workspace = true
|
||||
log.workspace = true
|
||||
miette = { workspace = true, features = ["fancy"] }
|
||||
once_cell.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
tempdir.workspace = true
|
||||
typed-builder.workspace = true
|
||||
uuid.workspace = true
|
||||
users.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
@ -94,6 +87,8 @@ stages = ["blue-build-recipe/stages"]
|
|||
copy = ["blue-build-recipe/copy"]
|
||||
multi-recipe = ["rayon", "indicatif/rayon"]
|
||||
switch = []
|
||||
sigstore = ["blue-build-process-management/sigstore"]
|
||||
login = []
|
||||
|
||||
[dev-dependencies]
|
||||
rusty-hook = "0.11"
|
||||
|
|
@ -107,5 +102,10 @@ workspace = true
|
|||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
strip = "none"
|
||||
debug = false
|
||||
panic = "abort"
|
||||
|
||||
[patch.crates-io]
|
||||
# sigstore = { path = "../../sigstore-rs/" }
|
||||
sigstore = { git = "https://github.com/gmpinder/sigstore-rs.git", rev = "3a804bff" }
|
||||
|
|
|
|||
19
Earthfile
19
Earthfile
|
|
@ -16,7 +16,6 @@ build:
|
|||
WAIT
|
||||
BUILD --platform=linux/amd64 --platform=linux/arm64 +build-scripts
|
||||
END
|
||||
BUILD +run-checks
|
||||
BUILD --platform=linux/amd64 --platform=linux/arm64 +build-images
|
||||
|
||||
run-checks:
|
||||
|
|
@ -34,15 +33,19 @@ prebuild:
|
|||
|
||||
lint:
|
||||
FROM +common
|
||||
DO rust+CARGO --args="clippy -- -D warnings"
|
||||
DO rust+CARGO --args="clippy --all-features -- -D warnings"
|
||||
DO rust+CARGO --args="clippy --no-default-features -- -D warnings"
|
||||
RUN cargo fmt --check
|
||||
DO rust+CARGO --args="clippy"
|
||||
DO rust+CARGO --args="clippy --all-features"
|
||||
DO rust+CARGO --args="clippy --no-default-features"
|
||||
|
||||
test:
|
||||
FROM +common
|
||||
DO rust+CARGO --args="test -- --show-output"
|
||||
DO rust+CARGO --args="test --all-features -- --show-output"
|
||||
DO rust+CARGO --args="test --no-default-features -- --show-output"
|
||||
COPY --dir test-files/ integration-tests/ /app
|
||||
COPY +cosign/cosign /usr/bin/cosign
|
||||
|
||||
DO rust+CARGO --args="test --workspace -- --show-output"
|
||||
DO rust+CARGO --args="test --workspace --all-features -- --show-output"
|
||||
DO rust+CARGO --args="test --workspace --no-default-features -- --show-output"
|
||||
|
||||
install:
|
||||
FROM +common
|
||||
|
|
@ -64,7 +67,7 @@ common:
|
|||
FROM --platform=native ghcr.io/blue-build/earthly-lib/cargo-builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY --keep-ts --dir src/ template/ recipe/ utils/ /app
|
||||
COPY --keep-ts --dir src/ template/ recipe/ utils/ process/ /app
|
||||
COPY --keep-ts Cargo.* /app
|
||||
COPY --keep-ts *.md /app
|
||||
COPY --keep-ts LICENSE /app
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
VERSION 0.8
|
||||
PROJECT blue-build/cli
|
||||
|
||||
IMPORT github.com/earthly/lib/utils/dind AS dind
|
||||
|
||||
all:
|
||||
BUILD +test-image
|
||||
BUILD +test-legacy-image
|
||||
|
|
@ -31,37 +33,51 @@ build-template:
|
|||
|
||||
template-containerfile:
|
||||
FROM +test-base
|
||||
RUN bluebuild -vv generate recipes/recipe.yml | tee Containerfile
|
||||
RUN bluebuild -v generate recipes/recipe.yml | tee Containerfile
|
||||
|
||||
SAVE ARTIFACT /test
|
||||
|
||||
template-legacy-containerfile:
|
||||
FROM +legacy-base
|
||||
RUN bluebuild -vv template config/recipe.yml | tee Containerfile
|
||||
RUN bluebuild -v template config/recipe.yml | tee Containerfile
|
||||
|
||||
SAVE ARTIFACT /test
|
||||
|
||||
build:
|
||||
FROM +test-base
|
||||
|
||||
RUN bluebuild -vv build recipes/recipe.yml
|
||||
RUN bluebuild -v build recipes/recipe.yml
|
||||
|
||||
build-full:
|
||||
FROM +test-base --MOCK="false"
|
||||
|
||||
DO dind+INSTALL
|
||||
|
||||
ENV BB_USERNAME=gmpinder
|
||||
ENV BB_REGISTRY=ghcr.io
|
||||
ENV BB_REGISTRY_NAMESPACE=blue-build
|
||||
|
||||
WITH DOCKER
|
||||
RUN --secret BB_PASSWORD=github/registry bluebuild build --push -S sigstore -vv recipes/recipe.yml
|
||||
END
|
||||
|
||||
|
||||
rebase:
|
||||
FROM +legacy-base
|
||||
|
||||
RUN bluebuild -vv rebase config/recipe.yml
|
||||
RUN bluebuild -v rebase config/recipe.yml
|
||||
|
||||
upgrade:
|
||||
FROM +legacy-base
|
||||
|
||||
RUN mkdir -p /etc/bluebuild && touch $BB_TEST_LOCAL_IMAGE
|
||||
RUN bluebuild -vv upgrade config/recipe.yml
|
||||
RUN bluebuild -v upgrade config/recipe.yml
|
||||
|
||||
switch:
|
||||
FROM +test-base
|
||||
|
||||
RUN mkdir -p /etc/bluebuild && touch $BB_TEST_LOCAL_IMAGE
|
||||
RUN bluebuild -vv switch recipes/recipe.yml
|
||||
RUN bluebuild -v switch recipes/recipe.yml
|
||||
|
||||
legacy-base:
|
||||
FROM ../+blue-build-cli-alpine
|
||||
|
|
@ -84,7 +100,10 @@ test-base:
|
|||
ENV BB_TEST_LOCAL_IMAGE=/etc/bluebuild/cli_test.tar.gz
|
||||
ENV CLICOLOR_FORCE=1
|
||||
|
||||
COPY ./mock-scripts/ /usr/bin/
|
||||
ARG MOCK="true"
|
||||
IF [ "$MOCK" = "true" ]
|
||||
COPY ./mock-scripts/ /usr/bin/
|
||||
END
|
||||
|
||||
WORKDIR /test
|
||||
COPY ./test-repo /test
|
||||
|
|
@ -94,7 +113,9 @@ test-base:
|
|||
GEN_KEYPAIR:
|
||||
FUNCTION
|
||||
# Setup a cosign key pair
|
||||
RUN echo -n "\n\n" | cosign generate-key-pair
|
||||
ENV COSIGN_PASSWORD=""
|
||||
ENV COSIGN_YES="true"
|
||||
RUN cosign generate-key-pair
|
||||
ENV COSIGN_PRIVATE_KEY=$(cat cosign.key)
|
||||
RUN rm cosign.key
|
||||
|
||||
|
|
|
|||
11
justfile
11
justfile
|
|
@ -35,11 +35,11 @@ test-all-features:
|
|||
|
||||
# Run clippy
|
||||
lint:
|
||||
cargo clippy -- -D warnings
|
||||
cargo clippy
|
||||
|
||||
# Run clippy for all features
|
||||
lint-all-features:
|
||||
cargo clippy --all-features -- -D warnings
|
||||
cargo clippy --all-features
|
||||
|
||||
# Watch the files and run cargo check on changes
|
||||
watch:
|
||||
|
|
@ -63,14 +63,17 @@ watch-test-all-features:
|
|||
|
||||
# Run lint anytime a file is changed
|
||||
watch-lint:
|
||||
cargo watch -c -x 'clippy -- -D warnings'
|
||||
cargo watch -c -x 'clippy'
|
||||
|
||||
# Run all feature lint anytime a file is changed
|
||||
watch-lint-all-features:
|
||||
cargo watch -c -x 'clippy --all-features -- -D warnings'
|
||||
cargo watch -c -x 'clippy --all-features'
|
||||
|
||||
# Installs cargo tools that help with development
|
||||
tools:
|
||||
rustup toolchain install stable
|
||||
rustup override set stable
|
||||
rustup component add --toolchain stable rust-analyzer clippy rustfmt
|
||||
cargo install cargo-watch
|
||||
|
||||
# Run cargo release and push the tag separately
|
||||
|
|
|
|||
50
process/Cargo.toml
Normal file
50
process/Cargo.toml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
[package]
|
||||
name = "blue-build-process-management"
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
categories.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
path = "process.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
blue-build-recipe = { version = "=0.8.12", path = "../recipe" }
|
||||
blue-build-utils = { version = "=0.8.12", path = "../utils" }
|
||||
expect-exit = "0.5"
|
||||
indicatif-log-bridge = "0.2"
|
||||
lenient_semver = "0.4"
|
||||
log4rs = { version = "1", features = ["background_rotation"] }
|
||||
nu-ansi-term = { version = "0.50", features = ["gnu_legacy"] }
|
||||
nix = { version = "0.29", features = ["signal"] }
|
||||
once_cell = "1"
|
||||
os_pipe = { version = "1", features = ["io_safety"] }
|
||||
rand = "0.8"
|
||||
semver = { version = "1", features = ["serde"] }
|
||||
signal-hook = { version = "0.3", features = ["extended-siginfo"] }
|
||||
sigstore = { version = "0.9", features = ["full-rustls-tls", "cached-client", "sigstore-trust-root", "sign"], default-features = false }
|
||||
tokio = { version = "1.39.2", features = ["rt", "rt-multi-thread"], optional = true }
|
||||
zeroize = { version = "1", features = ["aarch64", "derive", "serde"] }
|
||||
|
||||
chrono.workspace = true
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
colored.workspace = true
|
||||
indicatif.workspace = true
|
||||
indexmap.workspace = true
|
||||
log.workspace = true
|
||||
miette.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempdir.workspace = true
|
||||
typed-builder.workspace = true
|
||||
users.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
sigstore = ["dep:tokio"]
|
||||
408
process/drivers.rs
Normal file
408
process/drivers.rs
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
//! This module is responsible for managing various strategies
|
||||
//! to perform actions throughout the program. This hides all
|
||||
//! the implementation details from the command logic and allows
|
||||
//! for caching certain long execution tasks like inspecting the
|
||||
//! labels for an image.
|
||||
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
fmt::Debug,
|
||||
process::{ExitStatus, Output},
|
||||
sync::{Mutex, RwLock},
|
||||
};
|
||||
|
||||
use blue_build_recipe::Recipe;
|
||||
use blue_build_utils::constants::IMAGE_VERSION_LABEL;
|
||||
use clap::Args;
|
||||
use log::{debug, info, trace};
|
||||
use miette::{miette, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
#[cfg(feature = "sigstore")]
|
||||
use sigstore_driver::SigstoreDriver;
|
||||
use typed_builder::TypedBuilder;
|
||||
use uuid::Uuid;
|
||||
|
||||
use self::{
|
||||
buildah_driver::BuildahDriver,
|
||||
cosign_driver::CosignDriver,
|
||||
docker_driver::DockerDriver,
|
||||
github_driver::GithubDriver,
|
||||
gitlab_driver::GitlabDriver,
|
||||
image_metadata::ImageMetadata,
|
||||
local_driver::LocalDriver,
|
||||
opts::{
|
||||
BuildOpts, BuildTagPushOpts, CheckKeyPairOpts, GenerateKeyPairOpts, GetMetadataOpts,
|
||||
PushOpts, RunOpts, SignOpts, TagOpts, VerifyOpts,
|
||||
},
|
||||
podman_driver::PodmanDriver,
|
||||
skopeo_driver::SkopeoDriver,
|
||||
types::{
|
||||
BuildDriverType, CiDriverType, DetermineDriver, InspectDriverType, RunDriverType,
|
||||
SigningDriverType,
|
||||
},
|
||||
};
|
||||
|
||||
pub use traits::*;
|
||||
|
||||
mod buildah_driver;
|
||||
mod cosign_driver;
|
||||
mod docker_driver;
|
||||
mod functions;
|
||||
mod github_driver;
|
||||
mod gitlab_driver;
|
||||
pub mod image_metadata;
|
||||
mod local_driver;
|
||||
pub mod opts;
|
||||
mod podman_driver;
|
||||
#[cfg(feature = "sigstore")]
|
||||
mod sigstore_driver;
|
||||
mod skopeo_driver;
|
||||
mod traits;
|
||||
pub mod types;
|
||||
|
||||
static INIT: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
|
||||
static SELECTED_BUILD_DRIVER: Lazy<RwLock<Option<BuildDriverType>>> =
|
||||
Lazy::new(|| RwLock::new(None));
|
||||
static SELECTED_INSPECT_DRIVER: Lazy<RwLock<Option<InspectDriverType>>> =
|
||||
Lazy::new(|| RwLock::new(None));
|
||||
static SELECTED_RUN_DRIVER: Lazy<RwLock<Option<RunDriverType>>> = Lazy::new(|| RwLock::new(None));
|
||||
static SELECTED_SIGNING_DRIVER: Lazy<RwLock<Option<SigningDriverType>>> =
|
||||
Lazy::new(|| RwLock::new(None));
|
||||
static SELECTED_CI_DRIVER: Lazy<RwLock<Option<CiDriverType>>> = Lazy::new(|| RwLock::new(None));
|
||||
|
||||
/// UUID used to mark the current builds
|
||||
static BUILD_ID: Lazy<Uuid> = Lazy::new(Uuid::new_v4);
|
||||
|
||||
/// The cached os versions
|
||||
static OS_VERSION: Lazy<Mutex<HashMap<String, u64>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// Args for selecting the various drivers to use for runtime.
|
||||
///
|
||||
/// If the args are left uninitialized, the program will determine
|
||||
/// the best one available.
|
||||
#[derive(Default, Clone, Copy, Debug, TypedBuilder, Args)]
|
||||
pub struct DriverArgs {
|
||||
/// Select which driver to use to build
|
||||
/// your image.
|
||||
#[builder(default)]
|
||||
#[arg(short = 'B', long)]
|
||||
build_driver: Option<BuildDriverType>,
|
||||
|
||||
/// Select which driver to use to inspect
|
||||
/// images.
|
||||
#[builder(default)]
|
||||
#[arg(short = 'I', long)]
|
||||
inspect_driver: Option<InspectDriverType>,
|
||||
|
||||
/// Select which driver to use to sign
|
||||
/// images.
|
||||
#[builder(default)]
|
||||
#[arg(short = 'S', long)]
|
||||
signing_driver: Option<SigningDriverType>,
|
||||
|
||||
/// Select which driver to use to run
|
||||
/// containers.
|
||||
#[builder(default)]
|
||||
#[arg(short = 'R', long)]
|
||||
run_driver: Option<RunDriverType>,
|
||||
}
|
||||
|
||||
macro_rules! impl_driver_type {
|
||||
($cache:ident) => {{
|
||||
let lock = $cache.read().expect("Should read");
|
||||
lock.expect("Driver should have initialized build driver")
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! impl_driver_init {
|
||||
(@) => { };
|
||||
($init:ident; $($tail:tt)*) => {
|
||||
{
|
||||
let mut initialized = $init.lock().expect("Must lock INIT");
|
||||
|
||||
if !*initialized {
|
||||
impl_driver_init!(@ $($tail)*);
|
||||
|
||||
*initialized = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
(@ default => $cache:ident; $($tail:tt)*) => {
|
||||
{
|
||||
let mut driver = $cache.write().expect("Should lock");
|
||||
|
||||
impl_driver_init!(@ $($tail)*);
|
||||
|
||||
*driver = Some(driver.determine_driver());
|
||||
::log::trace!("Driver set {driver:?}");
|
||||
drop(driver);
|
||||
}
|
||||
};
|
||||
(@ $driver:expr => $cache:ident; $($tail:tt)*) => {
|
||||
{
|
||||
let mut driver = $cache.write().expect("Should lock");
|
||||
|
||||
impl_driver_init!(@ $($tail)*);
|
||||
|
||||
*driver = Some($driver.determine_driver());
|
||||
::log::trace!("Driver set {driver:?}");
|
||||
drop(driver);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub struct Driver;
|
||||
|
||||
impl Driver {
|
||||
/// Initializes the Strategy with user provided credentials.
|
||||
///
|
||||
/// If you want to take advantage of a user's credentials,
|
||||
/// you will want to run init before trying to use any of
|
||||
/// the strategies.
|
||||
///
|
||||
/// # Panics
|
||||
/// Will panic if it is unable to initialize drivers.
|
||||
pub fn init(mut args: DriverArgs) {
|
||||
trace!("Driver::init()");
|
||||
|
||||
impl_driver_init! {
|
||||
INIT;
|
||||
args.build_driver => SELECTED_BUILD_DRIVER;
|
||||
args.inspect_driver => SELECTED_INSPECT_DRIVER;
|
||||
args.run_driver => SELECTED_RUN_DRIVER;
|
||||
args.signing_driver => SELECTED_SIGNING_DRIVER;
|
||||
default => SELECTED_CI_DRIVER;
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current build's UUID
|
||||
#[must_use]
|
||||
pub fn get_build_id() -> Uuid {
|
||||
trace!("Driver::get_build_id()");
|
||||
*BUILD_ID
|
||||
}
|
||||
|
||||
/// Retrieve the `os_version` for an image.
|
||||
///
|
||||
/// This gets cached for faster resolution if it's required
|
||||
/// in another part of the program.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the image doesn't have OS version info
|
||||
/// or we are unable to lock a mutex.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the mutex fails to lock.
|
||||
pub fn get_os_version(recipe: &Recipe) -> Result<u64> {
|
||||
#[cfg(test)]
|
||||
{
|
||||
use miette::IntoDiagnostic;
|
||||
|
||||
if std::env::var(crate::test::BB_UNIT_TEST_MOCK_GET_OS_VERSION).is_ok() {
|
||||
return crate::test::create_test_recipe()
|
||||
.image_version
|
||||
.parse()
|
||||
.into_diagnostic();
|
||||
}
|
||||
}
|
||||
|
||||
trace!("Driver::get_os_version({recipe:#?})");
|
||||
let image = format!("{}:{}", &recipe.base_image, &recipe.image_version);
|
||||
|
||||
let mut os_version_lock = OS_VERSION.lock().expect("Should lock");
|
||||
|
||||
let entry = os_version_lock.get(&image);
|
||||
|
||||
let os_version = match entry {
|
||||
None => {
|
||||
info!("Retrieving OS version from {image}. This might take a bit");
|
||||
let inspect_opts = GetMetadataOpts::builder()
|
||||
.image(&*recipe.base_image)
|
||||
.tag(&*recipe.image_version)
|
||||
.build();
|
||||
let inspection = Self::get_metadata(&inspect_opts)?;
|
||||
|
||||
let os_version = inspection.get_version().ok_or_else(|| {
|
||||
miette!(
|
||||
help = format!("Please check with the image author about using '{IMAGE_VERSION_LABEL}' to report the os version."),
|
||||
"Unable to get the OS version from the labels"
|
||||
)
|
||||
})?;
|
||||
trace!("os_version: {os_version}");
|
||||
|
||||
os_version
|
||||
}
|
||||
Some(os_version) => {
|
||||
debug!("Found cached {os_version} for {image}");
|
||||
*os_version
|
||||
}
|
||||
};
|
||||
|
||||
if let Entry::Vacant(entry) = os_version_lock.entry(image.clone()) {
|
||||
trace!("Caching version {os_version} for {image}");
|
||||
entry.insert(os_version);
|
||||
}
|
||||
drop(os_version_lock);
|
||||
Ok(os_version)
|
||||
}
|
||||
|
||||
fn get_build_driver() -> BuildDriverType {
|
||||
impl_driver_type!(SELECTED_BUILD_DRIVER)
|
||||
}
|
||||
|
||||
fn get_inspect_driver() -> InspectDriverType {
|
||||
impl_driver_type!(SELECTED_INSPECT_DRIVER)
|
||||
}
|
||||
|
||||
fn get_signing_driver() -> SigningDriverType {
|
||||
impl_driver_type!(SELECTED_SIGNING_DRIVER)
|
||||
}
|
||||
|
||||
fn get_run_driver() -> RunDriverType {
|
||||
impl_driver_type!(SELECTED_RUN_DRIVER)
|
||||
}
|
||||
|
||||
fn get_ci_driver() -> CiDriverType {
|
||||
impl_driver_type!(SELECTED_CI_DRIVER)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_build_driver {
|
||||
($func:ident($($args:expr),*)) => {
|
||||
match Self::get_build_driver() {
|
||||
BuildDriverType::Buildah => BuildahDriver::$func($($args,)*),
|
||||
BuildDriverType::Podman => PodmanDriver::$func($($args,)*),
|
||||
BuildDriverType::Docker => DockerDriver::$func($($args,)*),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl BuildDriver for Driver {
|
||||
fn build(opts: &BuildOpts) -> Result<()> {
|
||||
impl_build_driver!(build(opts))
|
||||
}
|
||||
|
||||
fn tag(opts: &TagOpts) -> Result<()> {
|
||||
impl_build_driver!(tag(opts))
|
||||
}
|
||||
|
||||
fn push(opts: &PushOpts) -> Result<()> {
|
||||
impl_build_driver!(push(opts))
|
||||
}
|
||||
|
||||
fn login() -> Result<()> {
|
||||
impl_build_driver!(login())
|
||||
}
|
||||
|
||||
fn build_tag_push(opts: &BuildTagPushOpts) -> Result<()> {
|
||||
impl_build_driver!(build_tag_push(opts))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_signing_driver {
|
||||
($func:ident($($args:expr),*)) => {
|
||||
match Self::get_signing_driver() {
|
||||
SigningDriverType::Cosign => CosignDriver::$func($($args,)*),
|
||||
|
||||
#[cfg(feature = "sigstore")]
|
||||
SigningDriverType::Sigstore => SigstoreDriver::$func($($args,)*),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl SigningDriver for Driver {
|
||||
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> Result<()> {
|
||||
impl_signing_driver!(generate_key_pair(opts))
|
||||
}
|
||||
|
||||
fn check_signing_files(opts: &CheckKeyPairOpts) -> Result<()> {
|
||||
impl_signing_driver!(check_signing_files(opts))
|
||||
}
|
||||
|
||||
fn sign(opts: &SignOpts) -> Result<()> {
|
||||
impl_signing_driver!(sign(opts))
|
||||
}
|
||||
|
||||
fn verify(opts: &VerifyOpts) -> Result<()> {
|
||||
impl_signing_driver!(verify(opts))
|
||||
}
|
||||
|
||||
fn signing_login() -> Result<()> {
|
||||
impl_signing_driver!(signing_login())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_inspect_driver {
|
||||
($func:ident($($args:expr),*)) => {
|
||||
match Self::get_inspect_driver() {
|
||||
InspectDriverType::Skopeo => SkopeoDriver::$func($($args,)*),
|
||||
InspectDriverType::Podman => PodmanDriver::$func($($args,)*),
|
||||
InspectDriverType::Docker => DockerDriver::$func($($args,)*),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl InspectDriver for Driver {
|
||||
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
|
||||
impl_inspect_driver!(get_metadata(opts))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_run_driver {
|
||||
($func:ident($($args:expr),*)) => {
|
||||
match Self::get_run_driver() {
|
||||
RunDriverType::Docker => DockerDriver::$func($($args,)*),
|
||||
RunDriverType::Podman => PodmanDriver::$func($($args,)*),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl RunDriver for Driver {
|
||||
fn run(opts: &RunOpts) -> std::io::Result<ExitStatus> {
|
||||
impl_run_driver!(run(opts))
|
||||
}
|
||||
|
||||
fn run_output(opts: &RunOpts) -> std::io::Result<Output> {
|
||||
impl_run_driver!(run_output(opts))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_ci_driver {
|
||||
($func:ident($($args:expr),*)) => {
|
||||
match Self::get_ci_driver() {
|
||||
CiDriverType::Local => LocalDriver::$func($($args)*),
|
||||
CiDriverType::Gitlab => GitlabDriver::$func($($args)*),
|
||||
CiDriverType::Github => GithubDriver::$func($($args)*),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl CiDriver for Driver {
|
||||
fn on_default_branch() -> bool {
|
||||
impl_ci_driver!(on_default_branch())
|
||||
}
|
||||
|
||||
fn keyless_cert_identity() -> Result<String> {
|
||||
impl_ci_driver!(keyless_cert_identity())
|
||||
}
|
||||
|
||||
fn oidc_provider() -> Result<String> {
|
||||
impl_ci_driver!(oidc_provider())
|
||||
}
|
||||
|
||||
fn generate_tags(recipe: &Recipe) -> Result<Vec<String>> {
|
||||
impl_ci_driver!(generate_tags(recipe))
|
||||
}
|
||||
|
||||
fn get_repo_url() -> Result<String> {
|
||||
impl_ci_driver!(get_repo_url())
|
||||
}
|
||||
|
||||
fn get_registry() -> Result<String> {
|
||||
impl_ci_driver!(get_registry())
|
||||
}
|
||||
|
||||
fn generate_image_name(recipe: &Recipe) -> Result<String> {
|
||||
impl_ci_driver!(generate_image_name(recipe))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
use std::process::Command;
|
||||
use std::{io::Write, process::Stdio};
|
||||
|
||||
use blue_build_utils::logging::CommandLogging;
|
||||
use log::{error, info, trace};
|
||||
use miette::{bail, IntoDiagnostic, Result};
|
||||
use blue_build_utils::{cmd, credentials::Credentials};
|
||||
use log::{debug, error, info, trace};
|
||||
use miette::{bail, miette, IntoDiagnostic, Result};
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::credentials::{self, Credentials};
|
||||
use crate::logging::CommandLogging;
|
||||
|
||||
use super::{
|
||||
opts::{BuildOpts, PushOpts, TagOpts},
|
||||
|
|
@ -30,9 +30,7 @@ impl DriverVersion for BuildahDriver {
|
|||
trace!("BuildahDriver::version()");
|
||||
|
||||
trace!("buildah version --json");
|
||||
let output = Command::new("buildah")
|
||||
.arg("version")
|
||||
.arg("--json")
|
||||
let output = cmd!("buildah", "version", "--json")
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
|
||||
|
|
@ -46,24 +44,21 @@ impl DriverVersion for BuildahDriver {
|
|||
}
|
||||
|
||||
impl BuildDriver for BuildahDriver {
|
||||
fn build(&self, opts: &BuildOpts) -> Result<()> {
|
||||
fn build(opts: &BuildOpts) -> Result<()> {
|
||||
trace!("BuildahDriver::build({opts:#?})");
|
||||
|
||||
trace!(
|
||||
"buildah build --pull=true --layers={} -f {} -t {}",
|
||||
!opts.squash,
|
||||
opts.containerfile.display(),
|
||||
opts.image,
|
||||
let command = cmd!(
|
||||
"buildah",
|
||||
"build",
|
||||
"--pull=true",
|
||||
format!("--layers={}", !opts.squash),
|
||||
"-f",
|
||||
&*opts.containerfile,
|
||||
"-t",
|
||||
&*opts.image,
|
||||
);
|
||||
let mut command = Command::new("buildah");
|
||||
command
|
||||
.arg("build")
|
||||
.arg("--pull=true")
|
||||
.arg(format!("--layers={}", !opts.squash))
|
||||
.arg("-f")
|
||||
.arg(opts.containerfile.as_ref())
|
||||
.arg("-t")
|
||||
.arg(opts.image.as_ref());
|
||||
|
||||
trace!("{command:?}");
|
||||
let status = command
|
||||
.status_image_ref_progress(&opts.image, "Building Image")
|
||||
.into_diagnostic()?;
|
||||
|
|
@ -76,18 +71,13 @@ impl BuildDriver for BuildahDriver {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn tag(&self, opts: &TagOpts) -> Result<()> {
|
||||
fn tag(opts: &TagOpts) -> Result<()> {
|
||||
trace!("BuildahDriver::tag({opts:#?})");
|
||||
|
||||
trace!("buildah tag {} {}", opts.src_image, opts.dest_image);
|
||||
let status = Command::new("buildah")
|
||||
.arg("tag")
|
||||
.arg(opts.src_image.as_ref())
|
||||
.arg(opts.dest_image.as_ref())
|
||||
.status()
|
||||
.into_diagnostic()?;
|
||||
let mut command = cmd!("buildah", "tag", &*opts.src_image, &*opts.dest_image,);
|
||||
|
||||
if status.success() {
|
||||
trace!("{command:?}");
|
||||
if command.status().into_diagnostic()?.success() {
|
||||
info!("Successfully tagged {}!", opts.dest_image);
|
||||
} else {
|
||||
bail!("Failed to tag image {}", opts.dest_image);
|
||||
|
|
@ -95,18 +85,20 @@ impl BuildDriver for BuildahDriver {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn push(&self, opts: &PushOpts) -> Result<()> {
|
||||
fn push(opts: &PushOpts) -> Result<()> {
|
||||
trace!("BuildahDriver::push({opts:#?})");
|
||||
|
||||
trace!("buildah push {}", opts.image);
|
||||
let mut command = Command::new("buildah");
|
||||
command
|
||||
.arg("push")
|
||||
.arg(format!(
|
||||
let command = cmd!(
|
||||
"buildah",
|
||||
"push",
|
||||
format!(
|
||||
"--compression-format={}",
|
||||
opts.compression_type.unwrap_or_default()
|
||||
))
|
||||
.arg(opts.image.as_ref());
|
||||
),
|
||||
&*opts.image,
|
||||
);
|
||||
|
||||
trace!("{command:?}");
|
||||
let status = command
|
||||
.status_image_ref_progress(&opts.image, "Pushing Image")
|
||||
.into_diagnostic()?;
|
||||
|
|
@ -119,30 +111,47 @@ impl BuildDriver for BuildahDriver {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn login(&self) -> Result<()> {
|
||||
fn login() -> Result<()> {
|
||||
trace!("BuildahDriver::login()");
|
||||
|
||||
if let Some(Credentials {
|
||||
registry,
|
||||
username,
|
||||
password,
|
||||
}) = credentials::get()
|
||||
}) = Credentials::get()
|
||||
{
|
||||
trace!("buildah login -u {username} -p [MASKED] {registry}");
|
||||
let output = Command::new("buildah")
|
||||
.arg("login")
|
||||
.arg("-u")
|
||||
.arg(username)
|
||||
.arg("-p")
|
||||
.arg(password)
|
||||
.arg(registry)
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
let mut command = cmd!(
|
||||
"buildah",
|
||||
"login",
|
||||
"-u",
|
||||
username,
|
||||
"--password-stdin",
|
||||
registry
|
||||
);
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
trace!("{command:?}");
|
||||
let mut child = command.spawn().into_diagnostic()?;
|
||||
|
||||
write!(
|
||||
child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.ok_or_else(|| miette!("Unable to open pipe to stdin"))?,
|
||||
"{password}"
|
||||
)
|
||||
.into_diagnostic()?;
|
||||
|
||||
let output = child.wait_with_output().into_diagnostic()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let err_out = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("Failed to login for buildah: {err_out}");
|
||||
bail!("Failed to login for buildah:\n{}", err_out.trim());
|
||||
}
|
||||
debug!("Logged into {registry}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
241
process/drivers/cosign_driver.rs
Normal file
241
process/drivers/cosign_driver.rs
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
use std::{fmt::Debug, fs, io::Write, path::Path, process::Stdio};
|
||||
|
||||
use blue_build_utils::{
|
||||
cmd,
|
||||
constants::{COSIGN_PASSWORD, COSIGN_PUB_PATH, COSIGN_YES},
|
||||
credentials::Credentials,
|
||||
};
|
||||
use log::{debug, trace};
|
||||
use miette::{bail, miette, Context, IntoDiagnostic, Result};
|
||||
|
||||
use crate::drivers::opts::VerifyType;
|
||||
|
||||
use super::{
|
||||
functions::get_private_key,
|
||||
opts::{CheckKeyPairOpts, GenerateKeyPairOpts, SignOpts, VerifyOpts},
|
||||
SigningDriver,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CosignDriver;
|
||||
|
||||
impl SigningDriver for CosignDriver {
|
||||
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> Result<()> {
|
||||
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
|
||||
|
||||
let mut command = cmd!(
|
||||
"cosign",
|
||||
"generate-key-pair",
|
||||
COSIGN_PASSWORD => "",
|
||||
COSIGN_YES => "true",
|
||||
);
|
||||
command.current_dir(path);
|
||||
|
||||
let status = command.status().into_diagnostic()?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("Failed to generate cosign key-pair!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_signing_files(opts: &CheckKeyPairOpts) -> Result<()> {
|
||||
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
|
||||
let priv_key = get_private_key(path)?;
|
||||
|
||||
let mut command = cmd!(
|
||||
"cosign",
|
||||
"public-key",
|
||||
format!("--key={priv_key}"),
|
||||
COSIGN_PASSWORD => "",
|
||||
COSIGN_YES => "true",
|
||||
);
|
||||
|
||||
trace!("{command:?}");
|
||||
let output = command.output().into_diagnostic()?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"Failed to run cosign public-key: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
let calculated_pub_key = String::from_utf8(output.stdout).into_diagnostic()?;
|
||||
let found_pub_key = fs::read_to_string(path.join(COSIGN_PUB_PATH))
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to read {COSIGN_PUB_PATH}"))?;
|
||||
trace!("calculated_pub_key={calculated_pub_key},found_pub_key={found_pub_key}");
|
||||
|
||||
if calculated_pub_key.trim() == found_pub_key.trim() {
|
||||
debug!("Cosign files match, continuing build");
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Public key '{COSIGN_PUB_PATH}' does not match private key")
|
||||
}
|
||||
}
|
||||
|
||||
fn signing_login() -> Result<()> {
|
||||
trace!("CosignDriver::signing_login()");
|
||||
|
||||
if let Some(Credentials {
|
||||
registry,
|
||||
username,
|
||||
password,
|
||||
}) = Credentials::get()
|
||||
{
|
||||
let mut command = cmd!(
|
||||
"cosign",
|
||||
"login",
|
||||
"-u",
|
||||
username,
|
||||
"--password-stdin",
|
||||
registry,
|
||||
stdin = Stdio::piped(),
|
||||
stdout = Stdio::piped(),
|
||||
stderr = Stdio::piped(),
|
||||
);
|
||||
|
||||
trace!("{command:?}");
|
||||
let mut child = command.spawn().into_diagnostic()?;
|
||||
|
||||
write!(
|
||||
child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.ok_or_else(|| miette!("Unable to open pipe to stdin"))?,
|
||||
"{password}"
|
||||
)
|
||||
.into_diagnostic()?;
|
||||
|
||||
let output = child.wait_with_output().into_diagnostic()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let err_out = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("Failed to login for cosign:\n{}", err_out.trim());
|
||||
}
|
||||
debug!("Logged into {registry}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sign(opts: &SignOpts) -> Result<()> {
|
||||
let image_digest: &str = opts.image.as_ref();
|
||||
let mut command = cmd!(
|
||||
"cosign",
|
||||
"sign",
|
||||
if let Some(ref key) = opts.key => format!("--key={key}"),
|
||||
"--recursive",
|
||||
image_digest,
|
||||
COSIGN_PASSWORD => "",
|
||||
COSIGN_YES => "true",
|
||||
);
|
||||
|
||||
trace!("{command:?}");
|
||||
if !command.status().into_diagnostic()?.success() {
|
||||
bail!("Failed to sign {image_digest}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify(opts: &VerifyOpts) -> Result<()> {
|
||||
let image_name_tag: &str = opts.image.as_ref();
|
||||
let mut command = cmd!(
|
||||
"cosign",
|
||||
"verify",
|
||||
|c| {
|
||||
match &opts.verify_type {
|
||||
VerifyType::File(path) => cmd!(c, format!("--key={}", path.display())),
|
||||
VerifyType::Keyless { issuer, identity } => cmd!(
|
||||
c,
|
||||
"--certificate-identity-regexp",
|
||||
identity as &str,
|
||||
"--certificate-oidc-issuer",
|
||||
issuer as &str,
|
||||
),
|
||||
};
|
||||
},
|
||||
image_name_tag
|
||||
);
|
||||
|
||||
trace!("{command:?}");
|
||||
if !command.status().into_diagnostic()?.success() {
|
||||
bail!("Failed to verify {image_name_tag}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use blue_build_utils::constants::{COSIGN_PRIV_PATH, COSIGN_PUB_PATH};
|
||||
use tempdir::TempDir;
|
||||
|
||||
use crate::drivers::{
|
||||
opts::{CheckKeyPairOpts, GenerateKeyPairOpts},
|
||||
SigningDriver,
|
||||
};
|
||||
|
||||
use super::CosignDriver;
|
||||
|
||||
#[test]
|
||||
fn generate_key_pair() {
|
||||
let tempdir = TempDir::new("keypair").unwrap();
|
||||
|
||||
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
CosignDriver::generate_key_pair(&gen_opts).unwrap();
|
||||
|
||||
eprintln!(
|
||||
"Private key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
|
||||
);
|
||||
eprintln!(
|
||||
"Public key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
|
||||
);
|
||||
|
||||
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
CosignDriver::check_signing_files(&check_opts).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_key_pairs() {
|
||||
let path = Path::new("../test-files/keys");
|
||||
|
||||
let opts = CheckKeyPairOpts::builder().dir(path).build();
|
||||
|
||||
CosignDriver::check_signing_files(&opts).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "sigstore")]
|
||||
fn compatibility() {
|
||||
use crate::drivers::sigstore_driver::SigstoreDriver;
|
||||
|
||||
let tempdir = TempDir::new("keypair").unwrap();
|
||||
|
||||
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
CosignDriver::generate_key_pair(&gen_opts).unwrap();
|
||||
|
||||
eprintln!(
|
||||
"Private key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
|
||||
);
|
||||
eprintln!(
|
||||
"Public key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
|
||||
);
|
||||
|
||||
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
SigstoreDriver::check_signing_files(&check_opts).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,20 @@
|
|||
use std::{
|
||||
env,
|
||||
io::Write,
|
||||
path::Path,
|
||||
process::{Command, ExitStatus},
|
||||
process::{Command, ExitStatus, Stdio},
|
||||
sync::Mutex,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use blue_build_utils::{
|
||||
cmd,
|
||||
constants::{BB_BUILDKIT_CACHE_GHA, CONTAINER_FILE, DOCKER_HOST, SKOPEO_IMAGE},
|
||||
logging::{CommandLogging, Logger},
|
||||
signal_handler::{add_cid, remove_cid, ContainerId},
|
||||
credentials::Credentials,
|
||||
string_vec,
|
||||
};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use log::{info, trace, warn};
|
||||
use log::{debug, info, trace, warn};
|
||||
use miette::{bail, IntoDiagnostic, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
use semver::Version;
|
||||
|
|
@ -20,11 +22,12 @@ use serde::Deserialize;
|
|||
use tempdir::TempDir;
|
||||
|
||||
use crate::{
|
||||
credentials::Credentials, drivers::types::RunDriverType, image_metadata::ImageMetadata,
|
||||
drivers::image_metadata::ImageMetadata,
|
||||
logging::{CommandLogging, Logger},
|
||||
signal_handler::{add_cid, remove_cid, ContainerId, ContainerRuntime},
|
||||
};
|
||||
|
||||
use super::{
|
||||
credentials,
|
||||
opts::{BuildOpts, BuildTagPushOpts, GetMetadataOpts, PushOpts, RunOpts, TagOpts},
|
||||
BuildDriver, DriverVersion, InspectDriver, RunDriver,
|
||||
};
|
||||
|
|
@ -58,10 +61,7 @@ impl DockerDriver {
|
|||
}
|
||||
|
||||
trace!("docker buildx ls --format={}", "{{.Name}}");
|
||||
let ls_out = Command::new("docker")
|
||||
.arg("buildx")
|
||||
.arg("ls")
|
||||
.arg("--format={{.Name}}")
|
||||
let ls_out = cmd!("docker", "buildx", "ls", "--format={{.Name}}")
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
|
||||
|
|
@ -75,14 +75,16 @@ impl DockerDriver {
|
|||
|
||||
if !ls_out.lines().any(|line| line == "bluebuild") {
|
||||
trace!("docker buildx create --bootstrap --driver=docker-container --name=bluebuild");
|
||||
let create_out = Command::new("docker")
|
||||
.arg("buildx")
|
||||
.arg("create")
|
||||
.arg("--bootstrap")
|
||||
.arg("--driver=docker-container")
|
||||
.arg("--name=bluebuild")
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
let create_out = cmd!(
|
||||
"docker",
|
||||
"buildx",
|
||||
"create",
|
||||
"--bootstrap",
|
||||
"--driver=docker-container",
|
||||
"--name=bluebuild",
|
||||
)
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
|
||||
if !create_out.status.success() {
|
||||
bail!("{}", String::from_utf8_lossy(&create_out.stderr));
|
||||
|
|
@ -101,10 +103,7 @@ impl DriverVersion for DockerDriver {
|
|||
const VERSION_REQ: &'static str = ">=23";
|
||||
|
||||
fn version() -> Result<Version> {
|
||||
let output = Command::new("docker")
|
||||
.arg("version")
|
||||
.arg("-f")
|
||||
.arg("json")
|
||||
let output = cmd!("docker", "version", "-f", "json")
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
|
||||
|
|
@ -116,7 +115,7 @@ impl DriverVersion for DockerDriver {
|
|||
}
|
||||
|
||||
impl BuildDriver for DockerDriver {
|
||||
fn build(&self, opts: &BuildOpts) -> Result<()> {
|
||||
fn build(opts: &BuildOpts) -> Result<()> {
|
||||
trace!("DockerDriver::build({opts:#?})");
|
||||
|
||||
if opts.squash {
|
||||
|
|
@ -124,15 +123,17 @@ impl BuildDriver for DockerDriver {
|
|||
}
|
||||
|
||||
trace!("docker build -t {} -f {CONTAINER_FILE} .", opts.image);
|
||||
let status = Command::new("docker")
|
||||
.arg("build")
|
||||
.arg("-t")
|
||||
.arg(opts.image.as_ref())
|
||||
.arg("-f")
|
||||
.arg(opts.containerfile.as_ref())
|
||||
.arg(".")
|
||||
.status()
|
||||
.into_diagnostic()?;
|
||||
let status = cmd!(
|
||||
"docker",
|
||||
"build",
|
||||
"-t",
|
||||
&*opts.image,
|
||||
"-f",
|
||||
&*opts.containerfile,
|
||||
".",
|
||||
)
|
||||
.status()
|
||||
.into_diagnostic()?;
|
||||
|
||||
if status.success() {
|
||||
info!("Successfully built {}", opts.image);
|
||||
|
|
@ -142,14 +143,11 @@ impl BuildDriver for DockerDriver {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn tag(&self, opts: &TagOpts) -> Result<()> {
|
||||
fn tag(opts: &TagOpts) -> Result<()> {
|
||||
trace!("DockerDriver::tag({opts:#?})");
|
||||
|
||||
trace!("docker tag {} {}", opts.src_image, opts.dest_image);
|
||||
let status = Command::new("docker")
|
||||
.arg("tag")
|
||||
.arg(opts.src_image.as_ref())
|
||||
.arg(opts.dest_image.as_ref())
|
||||
let status = cmd!("docker", "tag", &*opts.src_image, &*opts.dest_image,)
|
||||
.status()
|
||||
.into_diagnostic()?;
|
||||
|
||||
|
|
@ -161,13 +159,11 @@ impl BuildDriver for DockerDriver {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn push(&self, opts: &PushOpts) -> Result<()> {
|
||||
fn push(opts: &PushOpts) -> Result<()> {
|
||||
trace!("DockerDriver::push({opts:#?})");
|
||||
|
||||
trace!("docker push {}", opts.image);
|
||||
let status = Command::new("docker")
|
||||
.arg("push")
|
||||
.arg(opts.image.as_ref())
|
||||
let status = cmd!("docker", "push", &*opts.image)
|
||||
.status()
|
||||
.into_diagnostic()?;
|
||||
|
||||
|
|
@ -179,119 +175,117 @@ impl BuildDriver for DockerDriver {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn login(&self) -> Result<()> {
|
||||
fn login() -> Result<()> {
|
||||
trace!("DockerDriver::login()");
|
||||
|
||||
if let Some(Credentials {
|
||||
registry,
|
||||
username,
|
||||
password,
|
||||
}) = credentials::get()
|
||||
}) = Credentials::get()
|
||||
{
|
||||
trace!("docker login -u {username} -p [MASKED] {registry}");
|
||||
let output = Command::new("docker")
|
||||
.arg("login")
|
||||
.arg("-u")
|
||||
.arg(username)
|
||||
.arg("-p")
|
||||
.arg(password)
|
||||
.arg(registry)
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
let mut command = cmd!(
|
||||
"docker",
|
||||
"login",
|
||||
"-u",
|
||||
username,
|
||||
"--password-stdin",
|
||||
registry,
|
||||
stdin = Stdio::piped(),
|
||||
stdout = Stdio::piped(),
|
||||
stderr = Stdio::piped(),
|
||||
);
|
||||
|
||||
trace!("{command:?}");
|
||||
let mut child = command.spawn().into_diagnostic()?;
|
||||
|
||||
write!(child.stdin.as_mut().unwrap(), "{password}").into_diagnostic()?;
|
||||
|
||||
let output = child.wait_with_output().into_diagnostic()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let err_out = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("Failed to login for docker: {err_out}");
|
||||
bail!("Failed to login for docker:\n{}", err_out.trim());
|
||||
}
|
||||
debug!("Logged into {registry}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_tag_push(&self, opts: &BuildTagPushOpts) -> Result<()> {
|
||||
fn build_tag_push(opts: &BuildTagPushOpts) -> Result<()> {
|
||||
trace!("DockerDriver::build_tag_push({opts:#?})");
|
||||
|
||||
if opts.squash {
|
||||
warn!("Squash is deprecated for docker so this build will not squash");
|
||||
}
|
||||
|
||||
trace!("docker buildx");
|
||||
let mut command = Command::new("docker");
|
||||
command.arg("buildx");
|
||||
|
||||
if !env::var(DOCKER_HOST).is_ok_and(|dh| !dh.is_empty()) {
|
||||
Self::setup()?;
|
||||
|
||||
trace!("--builder=bluebuild");
|
||||
command.arg("--builder=bluebuild");
|
||||
}
|
||||
|
||||
trace!(
|
||||
"build --progress=plain --pull -f {}",
|
||||
opts.containerfile.display()
|
||||
let mut command = cmd!(
|
||||
"docker",
|
||||
"buildx",
|
||||
|command|? {
|
||||
if !env::var(DOCKER_HOST).is_ok_and(|dh| !dh.is_empty()) {
|
||||
Self::setup()?;
|
||||
cmd!(command, "--builder=bluebuild");
|
||||
}
|
||||
},
|
||||
"build",
|
||||
"--pull",
|
||||
"-f",
|
||||
&*opts.containerfile,
|
||||
// https://github.com/moby/buildkit?tab=readme-ov-file#github-actions-cache-experimental
|
||||
if env::var(BB_BUILDKIT_CACHE_GHA)
|
||||
.map_or_else(|_| false, |e| e == "true") => [
|
||||
"--cache-from",
|
||||
"type=gha",
|
||||
"--cache-to",
|
||||
"type=gha",
|
||||
],
|
||||
);
|
||||
command
|
||||
.arg("build")
|
||||
.arg("--pull")
|
||||
.arg("-f")
|
||||
.arg(opts.containerfile.as_ref());
|
||||
|
||||
// https://github.com/moby/buildkit?tab=readme-ov-file#github-actions-cache-experimental
|
||||
if env::var(BB_BUILDKIT_CACHE_GHA).map_or_else(|_| false, |e| e == "true") {
|
||||
trace!("--cache-from type=gha --cache-to type=gha");
|
||||
command
|
||||
.arg("--cache-from")
|
||||
.arg("type=gha")
|
||||
.arg("--cache-to")
|
||||
.arg("type=gha");
|
||||
}
|
||||
|
||||
let mut final_image = String::new();
|
||||
|
||||
match (opts.image.as_ref(), opts.archive_path.as_ref()) {
|
||||
match (opts.image.as_deref(), opts.archive_path.as_deref()) {
|
||||
(Some(image), None) => {
|
||||
if opts.tags.is_empty() {
|
||||
final_image.push_str(image);
|
||||
|
||||
trace!("-t {image}");
|
||||
command.arg("-t").arg(image.as_ref());
|
||||
cmd!(command, "-t", image);
|
||||
} else {
|
||||
final_image
|
||||
.push_str(format!("{image}:{}", opts.tags.first().unwrap_or(&"")).as_str());
|
||||
final_image.push_str(
|
||||
format!("{image}:{}", opts.tags.first().map_or("", String::as_str))
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
opts.tags.iter().for_each(|tag| {
|
||||
let full_image = format!("{image}:{tag}");
|
||||
|
||||
trace!("-t {full_image}");
|
||||
command.arg("-t").arg(full_image);
|
||||
cmd!(command, "-t", format!("{image}:{tag}"));
|
||||
});
|
||||
}
|
||||
|
||||
if opts.push {
|
||||
trace!("--output type=image,name={image},push=true,compression={},oci-mediatypes=true", opts.compression);
|
||||
command.arg("--output").arg(format!(
|
||||
"type=image,name={image},push=true,compression={},oci-mediatypes=true",
|
||||
opts.compression
|
||||
));
|
||||
cmd!(
|
||||
command,
|
||||
"--output",
|
||||
format!(
|
||||
"type=image,name={image},push=true,compression={},oci-mediatypes=true",
|
||||
opts.compression
|
||||
)
|
||||
);
|
||||
} else {
|
||||
trace!("--load");
|
||||
command.arg("--load");
|
||||
cmd!(command, "--load");
|
||||
}
|
||||
}
|
||||
(None, Some(archive_path)) => {
|
||||
final_image.push_str(archive_path);
|
||||
|
||||
trace!("--output type=oci,dest={archive_path}");
|
||||
command
|
||||
.arg("--output")
|
||||
.arg(format!("type=oci,dest={archive_path}"));
|
||||
cmd!(command, "--output", format!("type=oci,dest={archive_path}"));
|
||||
}
|
||||
(Some(_), Some(_)) => bail!("Cannot use both image and archive path"),
|
||||
(None, None) => bail!("Need either the image or archive path set"),
|
||||
}
|
||||
|
||||
trace!(".");
|
||||
command.arg(".");
|
||||
cmd!(command, ".");
|
||||
|
||||
trace!("{command:?}");
|
||||
if command
|
||||
.status_image_ref_progress(&final_image, "Building Image")
|
||||
.into_diagnostic()?
|
||||
|
|
@ -310,7 +304,7 @@ impl BuildDriver for DockerDriver {
|
|||
}
|
||||
|
||||
impl InspectDriver for DockerDriver {
|
||||
fn get_metadata(&self, opts: &GetMetadataOpts) -> Result<ImageMetadata> {
|
||||
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
|
||||
trace!("DockerDriver::get_labels({opts:#?})");
|
||||
|
||||
let url = opts.tag.as_ref().map_or_else(
|
||||
|
|
@ -325,14 +319,14 @@ impl InspectDriver for DockerDriver {
|
|||
);
|
||||
progress.enable_steady_tick(Duration::from_millis(100));
|
||||
|
||||
let output = self
|
||||
.run_output(
|
||||
&RunOpts::builder()
|
||||
.image(SKOPEO_IMAGE)
|
||||
.args(&["inspect".to_string(), url.clone()])
|
||||
.build(),
|
||||
)
|
||||
.into_diagnostic()?;
|
||||
let output = Self::run_output(
|
||||
&RunOpts::builder()
|
||||
.image(SKOPEO_IMAGE)
|
||||
.args(string_vec!["inspect", url.clone()])
|
||||
.remove(true)
|
||||
.build(),
|
||||
)
|
||||
.into_diagnostic()?;
|
||||
|
||||
progress.finish();
|
||||
Logger::multi_progress().remove(&progress);
|
||||
|
|
@ -348,29 +342,25 @@ impl InspectDriver for DockerDriver {
|
|||
}
|
||||
|
||||
impl RunDriver for DockerDriver {
|
||||
fn run(&self, opts: &RunOpts) -> std::io::Result<ExitStatus> {
|
||||
trace!("DockerDriver::run({opts:#?})");
|
||||
|
||||
fn run(opts: &RunOpts) -> std::io::Result<ExitStatus> {
|
||||
let cid_path = TempDir::new("docker")?;
|
||||
let cid_file = cid_path.path().join("cid");
|
||||
let cid = ContainerId::new(&cid_file, RunDriverType::Docker, false);
|
||||
let cid = ContainerId::new(&cid_file, ContainerRuntime::Docker, false);
|
||||
|
||||
add_cid(&cid);
|
||||
|
||||
let status = docker_run(opts, &cid_file)
|
||||
.status_image_ref_progress(opts.image.as_ref(), "Running container")?;
|
||||
.status_image_ref_progress(&*opts.image, "Running container")?;
|
||||
|
||||
remove_cid(&cid);
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
fn run_output(&self, opts: &RunOpts) -> std::io::Result<std::process::Output> {
|
||||
trace!("DockerDriver::run({opts:#?})");
|
||||
|
||||
fn run_output(opts: &RunOpts) -> std::io::Result<std::process::Output> {
|
||||
let cid_path = TempDir::new("docker")?;
|
||||
let cid_file = cid_path.path().join("cid");
|
||||
let cid = ContainerId::new(&cid_file, RunDriverType::Docker, false);
|
||||
let cid = ContainerId::new(&cid_file, ContainerRuntime::Docker, false);
|
||||
|
||||
add_cid(&cid);
|
||||
|
||||
|
|
@ -383,41 +373,29 @@ impl RunDriver for DockerDriver {
|
|||
}
|
||||
|
||||
fn docker_run(opts: &RunOpts, cid_file: &Path) -> Command {
|
||||
let mut command = Command::new("docker");
|
||||
|
||||
command
|
||||
.arg("run")
|
||||
.arg(format!("--cidfile={}", cid_file.display()));
|
||||
|
||||
if opts.privileged {
|
||||
command.arg("--privileged");
|
||||
}
|
||||
|
||||
if opts.remove {
|
||||
command.arg("--rm");
|
||||
}
|
||||
|
||||
if opts.pull {
|
||||
command.arg("--pull=always");
|
||||
}
|
||||
|
||||
opts.volumes.iter().for_each(|volume| {
|
||||
command.arg("--volume");
|
||||
command.arg(format!(
|
||||
"{}:{}",
|
||||
volume.path_or_vol_name, volume.container_path,
|
||||
));
|
||||
});
|
||||
|
||||
opts.env_vars.iter().for_each(|env| {
|
||||
command.arg("--env");
|
||||
command.arg(format!("{}={}", env.key, env.value));
|
||||
});
|
||||
|
||||
command.arg(opts.image.as_ref());
|
||||
|
||||
command.args(opts.args.iter());
|
||||
|
||||
trace!("{command:?}");
|
||||
command
|
||||
cmd!(
|
||||
"docker",
|
||||
"run",
|
||||
format!("--cidfile={}", cid_file.display()),
|
||||
if opts.privileged => "--privileged",
|
||||
if opts.remove => "--rm",
|
||||
if opts.pull => "--pull=always",
|
||||
for volume in opts.volumes => [
|
||||
"--volume",
|
||||
format!("{}:{}", volume.path_or_vol_name, volume.container_path),
|
||||
],
|
||||
for env in opts.env_vars => [
|
||||
"--env",
|
||||
format!("{}={}", env.key, env.value),
|
||||
],
|
||||
|command| {
|
||||
match (opts.uid, opts.gid) {
|
||||
(Some(uid), None) => cmd!(command, "-u", format!("{uid}")),
|
||||
(Some(uid), Some(gid)) => cmd!(command, "-u", format!("{}:{}", uid, gid)),
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
&*opts.image,
|
||||
for opts.args,
|
||||
)
|
||||
}
|
||||
52
process/drivers/functions.rs
Normal file
52
process/drivers/functions.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
use std::{env, path::Path};
|
||||
|
||||
use blue_build_utils::{
|
||||
constants::{BB_PRIVATE_KEY, COSIGN_PRIVATE_KEY, COSIGN_PRIV_PATH, COSIGN_PUB_PATH},
|
||||
string,
|
||||
};
|
||||
use miette::{bail, Result};
|
||||
|
||||
use super::opts::PrivateKey;
|
||||
|
||||
pub(super) fn get_private_key<P>(path: P) -> Result<PrivateKey>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
|
||||
Ok(
|
||||
match (
|
||||
path.join(COSIGN_PUB_PATH).exists(),
|
||||
env::var(BB_PRIVATE_KEY).ok(),
|
||||
env::var(COSIGN_PRIVATE_KEY).ok(),
|
||||
path.join(COSIGN_PRIV_PATH),
|
||||
) {
|
||||
(true, Some(private_key), _, _) if !private_key.is_empty() => {
|
||||
PrivateKey::Env(string!(BB_PRIVATE_KEY))
|
||||
}
|
||||
(true, _, Some(cosign_priv_key), _) if !cosign_priv_key.is_empty() => {
|
||||
PrivateKey::Env(string!(COSIGN_PRIVATE_KEY))
|
||||
}
|
||||
(true, _, _, cosign_priv_key_path) if cosign_priv_key_path.exists() => {
|
||||
PrivateKey::Path(cosign_priv_key_path)
|
||||
}
|
||||
_ => {
|
||||
bail!(
|
||||
help = format!(
|
||||
"{}{}{}{}{}{}",
|
||||
format_args!("Make sure you have a `{COSIGN_PUB_PATH}`\n"),
|
||||
format_args!(
|
||||
"in the root of your repo and have either {COSIGN_PRIVATE_KEY}\n"
|
||||
),
|
||||
format_args!("set in your env variables or a `{COSIGN_PRIV_PATH}`\n"),
|
||||
"file in the root of your repo.\n\n",
|
||||
"See https://blue-build.org/how-to/cosign/ for more information.\n\n",
|
||||
"If you don't want to sign your image, use the `--no-sign` flag.",
|
||||
),
|
||||
"{}",
|
||||
"Unable to find private/public key pair",
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
272
process/drivers/github_driver.rs
Normal file
272
process/drivers/github_driver.rs
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
use blue_build_utils::{
|
||||
constants::{
|
||||
GITHUB_EVENT_NAME, GITHUB_REF_NAME, GITHUB_SHA, GITHUB_TOKEN_ISSUER_URL,
|
||||
GITHUB_WORKFLOW_REF, PR_EVENT_NUMBER,
|
||||
},
|
||||
get_env_var,
|
||||
};
|
||||
use event::Event;
|
||||
use log::trace;
|
||||
|
||||
use super::{CiDriver, Driver};
|
||||
|
||||
mod event;
|
||||
|
||||
pub struct GithubDriver;
|
||||
|
||||
impl CiDriver for GithubDriver {
|
||||
fn on_default_branch() -> bool {
|
||||
Event::try_new().map_or_else(
|
||||
|_| false,
|
||||
|event| match (event.commit_ref, event.head) {
|
||||
(Some(commit_ref), _) => {
|
||||
commit_ref.trim_start_matches("refs/heads/") == event.repository.default_branch
|
||||
}
|
||||
(_, Some(head)) => event.repository.default_branch == head.commit_ref,
|
||||
_ => false,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn keyless_cert_identity() -> miette::Result<String> {
|
||||
get_env_var(GITHUB_WORKFLOW_REF)
|
||||
}
|
||||
|
||||
fn oidc_provider() -> miette::Result<String> {
|
||||
Ok(GITHUB_TOKEN_ISSUER_URL.to_string())
|
||||
}
|
||||
|
||||
fn generate_tags(recipe: &blue_build_recipe::Recipe) -> miette::Result<Vec<String>> {
|
||||
let mut tags: Vec<String> = Vec::new();
|
||||
let os_version = Driver::get_os_version(recipe)?;
|
||||
let github_event_name = get_env_var(GITHUB_EVENT_NAME)?;
|
||||
|
||||
if github_event_name == "pull_request" {
|
||||
trace!("Running in a PR");
|
||||
|
||||
let github_event_number = get_env_var(PR_EVENT_NUMBER)?;
|
||||
|
||||
tags.push(format!("pr-{github_event_number}-{os_version}"));
|
||||
} else if Self::on_default_branch() {
|
||||
tags.push(os_version.to_string());
|
||||
|
||||
let timestamp = blue_build_utils::get_tag_timestamp();
|
||||
tags.push(format!("{timestamp}-{os_version}"));
|
||||
|
||||
if let Some(ref alt_tags) = recipe.alt_tags {
|
||||
tags.extend(alt_tags.iter().map(ToString::to_string));
|
||||
} else {
|
||||
tags.push("latest".into());
|
||||
tags.push(timestamp);
|
||||
}
|
||||
} else {
|
||||
let github_ref_name = get_env_var(GITHUB_REF_NAME)?;
|
||||
|
||||
tags.push(format!("br-{github_ref_name}-{os_version}"));
|
||||
}
|
||||
|
||||
let mut short_sha = get_env_var(GITHUB_SHA)?;
|
||||
short_sha.truncate(7);
|
||||
|
||||
tags.push(format!("{short_sha}-{os_version}"));
|
||||
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
fn get_repo_url() -> miette::Result<String> {
|
||||
Ok(Event::try_new()?.repository.html_url)
|
||||
}
|
||||
|
||||
fn get_registry() -> miette::Result<String> {
|
||||
Ok(format!(
|
||||
"ghcr.io/{}",
|
||||
Event::try_new()?.repository.owner.login
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::env;
|
||||
|
||||
use blue_build_utils::constants::{
|
||||
GITHUB_EVENT_NAME, GITHUB_EVENT_PATH, GITHUB_REF_NAME, GITHUB_SHA, PR_EVENT_NUMBER,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
drivers::CiDriver,
|
||||
test::{create_test_recipe, BB_UNIT_TEST_MOCK_GET_OS_VERSION, ENV_LOCK},
|
||||
};
|
||||
|
||||
use super::GithubDriver;
|
||||
|
||||
fn setup_default_branch() {
|
||||
setup();
|
||||
env::set_var(
|
||||
GITHUB_EVENT_PATH,
|
||||
"../test-files/github-events/default-branch.json",
|
||||
);
|
||||
env::set_var(GITHUB_REF_NAME, "main");
|
||||
}
|
||||
|
||||
fn setup_pr_branch() {
|
||||
setup();
|
||||
env::set_var(
|
||||
GITHUB_EVENT_PATH,
|
||||
"../test-files/github-events/pr-branch.json",
|
||||
);
|
||||
env::set_var(GITHUB_EVENT_NAME, "pull_request");
|
||||
env::set_var(GITHUB_REF_NAME, "test");
|
||||
env::set_var(PR_EVENT_NUMBER, "12");
|
||||
}
|
||||
|
||||
fn setup_branch() {
|
||||
setup();
|
||||
env::set_var(GITHUB_EVENT_PATH, "../test-files/github-events/branch.json");
|
||||
env::set_var(GITHUB_REF_NAME, "test");
|
||||
}
|
||||
|
||||
fn setup() {
|
||||
env::set_var(GITHUB_EVENT_NAME, "push");
|
||||
env::set_var(GITHUB_SHA, "1234567890");
|
||||
env::set_var(BB_UNIT_TEST_MOCK_GET_OS_VERSION, "");
|
||||
}
|
||||
|
||||
fn teardown() {
|
||||
env::remove_var(GITHUB_EVENT_NAME);
|
||||
env::remove_var(GITHUB_EVENT_PATH);
|
||||
env::remove_var(GITHUB_REF_NAME);
|
||||
env::remove_var(PR_EVENT_NUMBER);
|
||||
env::remove_var(GITHUB_SHA);
|
||||
env::remove_var(BB_UNIT_TEST_MOCK_GET_OS_VERSION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_registry() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_default_branch();
|
||||
|
||||
let registry = GithubDriver::get_registry().unwrap();
|
||||
|
||||
assert_eq!(registry, "ghcr.io/test-owner");
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_default_branch_true() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_default_branch();
|
||||
|
||||
assert!(GithubDriver::on_default_branch());
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_default_branch_false() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_pr_branch();
|
||||
|
||||
assert!(!GithubDriver::on_default_branch());
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_repo_url() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_branch();
|
||||
|
||||
let url = GithubDriver::get_repo_url().unwrap();
|
||||
|
||||
assert_eq!(url, "https://example.com/");
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_default_branch() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
let timestamp = blue_build_utils::get_tag_timestamp();
|
||||
|
||||
setup_default_branch();
|
||||
|
||||
let mut tags = GithubDriver::generate_tags(&create_test_recipe()).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec![
|
||||
format!("{timestamp}-40"),
|
||||
"latest".to_string(),
|
||||
timestamp,
|
||||
"1234567-40".to_string(),
|
||||
"40".to_string(),
|
||||
];
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_default_branch_alt_tags() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
let timestamp = blue_build_utils::get_tag_timestamp();
|
||||
|
||||
setup_default_branch();
|
||||
|
||||
let mut recipe = create_test_recipe();
|
||||
|
||||
recipe.alt_tags = Some(vec!["test-tag1".into(), "test-tag2".into()]);
|
||||
|
||||
let mut tags = GithubDriver::generate_tags(&recipe).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec![
|
||||
format!("{timestamp}-40"),
|
||||
"1234567-40".to_string(),
|
||||
"40".to_string(),
|
||||
];
|
||||
expected_tags.extend(recipe.alt_tags.unwrap().iter().map(ToString::to_string));
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_pr_branch() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_pr_branch();
|
||||
|
||||
let mut tags = GithubDriver::generate_tags(&create_test_recipe()).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec!["pr-12-40".to_string(), "1234567-40".to_string()];
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_branch() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_branch();
|
||||
|
||||
let mut tags = GithubDriver::generate_tags(&create_test_recipe()).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec!["1234567-40".to_string(), "br-test-40".to_string()];
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
}
|
||||
44
process/drivers/github_driver/event.rs
Normal file
44
process/drivers/github_driver/event.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use std::{fs, path::PathBuf};
|
||||
|
||||
use blue_build_utils::{constants::GITHUB_EVENT_PATH, get_env_var};
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub(super) struct Event {
|
||||
pub repository: EventRepository,
|
||||
// pub base: Option<EventRefInfo>,
|
||||
pub head: Option<EventRefInfo>,
|
||||
|
||||
#[serde(alias = "ref")]
|
||||
pub commit_ref: Option<String>,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn try_new() -> Result<Self> {
|
||||
get_env_var(GITHUB_EVENT_PATH)
|
||||
.map(PathBuf::from)
|
||||
.and_then(|event_path| {
|
||||
serde_json::from_str::<Self>(&fs::read_to_string(event_path).into_diagnostic()?)
|
||||
.into_diagnostic()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub(super) struct EventRepository {
|
||||
pub default_branch: String,
|
||||
pub owner: EventRepositoryOwner,
|
||||
pub html_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub(super) struct EventRepositoryOwner {
|
||||
pub login: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub(super) struct EventRefInfo {
|
||||
#[serde(alias = "ref")]
|
||||
pub commit_ref: String,
|
||||
}
|
||||
293
process/drivers/gitlab_driver.rs
Normal file
293
process/drivers/gitlab_driver.rs
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
use std::env;
|
||||
|
||||
use blue_build_utils::{
|
||||
constants::{
|
||||
CI_COMMIT_REF_NAME, CI_COMMIT_SHORT_SHA, CI_DEFAULT_BRANCH, CI_MERGE_REQUEST_IID,
|
||||
CI_PIPELINE_SOURCE, CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_PROJECT_URL, CI_REGISTRY,
|
||||
CI_SERVER_HOST, CI_SERVER_PROTOCOL,
|
||||
},
|
||||
get_env_var,
|
||||
};
|
||||
use log::{debug, trace};
|
||||
|
||||
use crate::drivers::Driver;
|
||||
|
||||
use super::CiDriver;
|
||||
|
||||
pub struct GitlabDriver;
|
||||
|
||||
impl CiDriver for GitlabDriver {
|
||||
fn on_default_branch() -> bool {
|
||||
env::var(CI_DEFAULT_BRANCH).is_ok_and(|default_branch| {
|
||||
env::var(CI_COMMIT_REF_NAME).is_ok_and(|branch| default_branch == branch)
|
||||
})
|
||||
}
|
||||
|
||||
fn keyless_cert_identity() -> miette::Result<String> {
|
||||
Ok(format!(
|
||||
"{}//.gitlab-ci.yml@refs/heads/{}",
|
||||
get_env_var(CI_PROJECT_URL)?,
|
||||
get_env_var(CI_DEFAULT_BRANCH)?,
|
||||
))
|
||||
}
|
||||
|
||||
fn oidc_provider() -> miette::Result<String> {
|
||||
Ok(format!(
|
||||
"{}://{}",
|
||||
get_env_var(CI_SERVER_PROTOCOL)?,
|
||||
get_env_var(CI_SERVER_HOST)?,
|
||||
))
|
||||
}
|
||||
|
||||
fn generate_tags(recipe: &blue_build_recipe::Recipe) -> miette::Result<Vec<String>> {
|
||||
let mut tags: Vec<String> = Vec::new();
|
||||
let os_version = Driver::get_os_version(recipe)?;
|
||||
|
||||
if Self::on_default_branch() {
|
||||
debug!("Running on the default branch");
|
||||
|
||||
tags.push(os_version.to_string());
|
||||
|
||||
let timestamp = blue_build_utils::get_tag_timestamp();
|
||||
tags.push(format!("{timestamp}-{os_version}"));
|
||||
|
||||
if let Some(ref alt_tags) = recipe.alt_tags {
|
||||
tags.extend(alt_tags.iter().map(ToString::to_string));
|
||||
} else {
|
||||
tags.push("latest".into());
|
||||
tags.push(timestamp);
|
||||
}
|
||||
} else if let Ok(mr_iid) = env::var(CI_MERGE_REQUEST_IID) {
|
||||
trace!("{CI_MERGE_REQUEST_IID}={mr_iid}");
|
||||
|
||||
let pipeline_source = get_env_var(CI_PIPELINE_SOURCE)?;
|
||||
trace!("{CI_PIPELINE_SOURCE}={pipeline_source}");
|
||||
|
||||
if pipeline_source == "merge_request_event" {
|
||||
debug!("Running in a MR");
|
||||
tags.push(format!("mr-{mr_iid}-{os_version}"));
|
||||
}
|
||||
} else {
|
||||
let commit_branch = get_env_var(CI_COMMIT_REF_NAME)?;
|
||||
trace!("{CI_COMMIT_REF_NAME}={commit_branch}");
|
||||
|
||||
debug!("Running on branch {commit_branch}");
|
||||
tags.push(format!("br-{commit_branch}-{os_version}"));
|
||||
}
|
||||
|
||||
let commit_sha = get_env_var(CI_COMMIT_SHORT_SHA)?;
|
||||
trace!("{CI_COMMIT_SHORT_SHA}={commit_sha}");
|
||||
|
||||
tags.push(format!("{commit_sha}-{os_version}"));
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
fn get_repo_url() -> miette::Result<String> {
|
||||
Ok(format!(
|
||||
"{}://{}/{}/{}",
|
||||
get_env_var(CI_SERVER_PROTOCOL)?,
|
||||
get_env_var(CI_SERVER_HOST)?,
|
||||
get_env_var(CI_PROJECT_NAMESPACE)?,
|
||||
get_env_var(CI_PROJECT_NAME)?,
|
||||
))
|
||||
}
|
||||
|
||||
fn get_registry() -> miette::Result<String> {
|
||||
Ok(format!(
|
||||
"{}/{}/{}",
|
||||
get_env_var(CI_REGISTRY)?,
|
||||
get_env_var(CI_PROJECT_NAMESPACE)?,
|
||||
get_env_var(CI_PROJECT_NAME)?,
|
||||
)
|
||||
.to_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::env;
|
||||
|
||||
use blue_build_utils::constants::{
|
||||
CI_COMMIT_REF_NAME, CI_COMMIT_SHORT_SHA, CI_DEFAULT_BRANCH, CI_MERGE_REQUEST_IID,
|
||||
CI_PIPELINE_SOURCE, CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_REGISTRY, CI_SERVER_HOST,
|
||||
CI_SERVER_PROTOCOL,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
drivers::CiDriver,
|
||||
test::{create_test_recipe, BB_UNIT_TEST_MOCK_GET_OS_VERSION, ENV_LOCK},
|
||||
};
|
||||
|
||||
use super::GitlabDriver;
|
||||
|
||||
fn setup_default_branch() {
|
||||
setup();
|
||||
env::set_var(CI_COMMIT_REF_NAME, "main");
|
||||
}
|
||||
|
||||
fn setup_mr_branch() {
|
||||
setup();
|
||||
env::set_var(CI_MERGE_REQUEST_IID, "12");
|
||||
env::set_var(CI_PIPELINE_SOURCE, "merge_request_event");
|
||||
env::set_var(CI_COMMIT_REF_NAME, "test");
|
||||
}
|
||||
|
||||
fn setup_branch() {
|
||||
setup();
|
||||
env::set_var(CI_COMMIT_REF_NAME, "test");
|
||||
}
|
||||
|
||||
fn setup() {
|
||||
env::set_var(CI_DEFAULT_BRANCH, "main");
|
||||
env::set_var(CI_COMMIT_SHORT_SHA, "1234567");
|
||||
env::set_var(CI_REGISTRY, "registry.example.com");
|
||||
env::set_var(CI_PROJECT_NAMESPACE, "test-project");
|
||||
env::set_var(CI_PROJECT_NAME, "test");
|
||||
env::set_var(CI_SERVER_PROTOCOL, "https");
|
||||
env::set_var(CI_SERVER_HOST, "gitlab.example.com");
|
||||
env::set_var(BB_UNIT_TEST_MOCK_GET_OS_VERSION, "");
|
||||
}
|
||||
|
||||
fn teardown() {
|
||||
env::remove_var(CI_COMMIT_REF_NAME);
|
||||
env::remove_var(CI_MERGE_REQUEST_IID);
|
||||
env::remove_var(CI_PIPELINE_SOURCE);
|
||||
env::remove_var(CI_DEFAULT_BRANCH);
|
||||
env::remove_var(CI_COMMIT_SHORT_SHA);
|
||||
env::remove_var(CI_REGISTRY);
|
||||
env::remove_var(CI_PROJECT_NAMESPACE);
|
||||
env::remove_var(CI_PROJECT_NAME);
|
||||
env::remove_var(CI_SERVER_PROTOCOL);
|
||||
env::remove_var(CI_SERVER_HOST);
|
||||
env::remove_var(BB_UNIT_TEST_MOCK_GET_OS_VERSION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_registry() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup();
|
||||
|
||||
let registry = GitlabDriver::get_registry().unwrap();
|
||||
|
||||
assert_eq!(registry, "registry.example.com/test-project/test");
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_default_branch_true() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_default_branch();
|
||||
|
||||
assert!(GitlabDriver::on_default_branch());
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_default_branch_false() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_branch();
|
||||
|
||||
assert!(!GitlabDriver::on_default_branch());
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_repo_url() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup();
|
||||
|
||||
let url = GitlabDriver::get_repo_url().unwrap();
|
||||
|
||||
assert_eq!(url, "https://gitlab.example.com/test-project/test");
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_default_branch() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
let timestamp = blue_build_utils::get_tag_timestamp();
|
||||
|
||||
setup_default_branch();
|
||||
|
||||
let mut tags = GitlabDriver::generate_tags(&create_test_recipe()).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec![
|
||||
format!("{timestamp}-40"),
|
||||
"latest".to_string(),
|
||||
timestamp,
|
||||
"1234567-40".to_string(),
|
||||
"40".to_string(),
|
||||
];
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_default_branch_alt_tags() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
let timestamp = blue_build_utils::get_tag_timestamp();
|
||||
|
||||
setup_default_branch();
|
||||
|
||||
let mut recipe = create_test_recipe();
|
||||
|
||||
recipe.alt_tags = Some(vec!["test-tag1".into(), "test-tag2".into()]);
|
||||
|
||||
let mut tags = GitlabDriver::generate_tags(&recipe).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec![
|
||||
format!("{timestamp}-40"),
|
||||
"1234567-40".to_string(),
|
||||
"40".to_string(),
|
||||
];
|
||||
expected_tags.extend(recipe.alt_tags.unwrap().iter().map(ToString::to_string));
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_mr_branch() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_mr_branch();
|
||||
|
||||
let mut tags = GitlabDriver::generate_tags(&create_test_recipe()).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec!["mr-12-40".to_string(), "1234567-40".to_string()];
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_branch() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_branch();
|
||||
|
||||
let mut tags = GitlabDriver::generate_tags(&create_test_recipe()).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec!["1234567-40".to_string(), "br-test-40".to_string()];
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
}
|
||||
43
process/drivers/local_driver.rs
Normal file
43
process/drivers/local_driver.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use log::trace;
|
||||
use miette::bail;
|
||||
|
||||
use super::{CiDriver, Driver};
|
||||
|
||||
pub struct LocalDriver;
|
||||
|
||||
impl CiDriver for LocalDriver {
|
||||
fn on_default_branch() -> bool {
|
||||
trace!("LocalDriver::on_default_branch()");
|
||||
false
|
||||
}
|
||||
|
||||
fn keyless_cert_identity() -> miette::Result<String> {
|
||||
trace!("LocalDriver::keyless_cert_identity()");
|
||||
bail!("Keyless not supported");
|
||||
}
|
||||
|
||||
fn oidc_provider() -> miette::Result<String> {
|
||||
trace!("LocalDriver::oidc_provider()");
|
||||
bail!("Keyless not supported");
|
||||
}
|
||||
|
||||
fn generate_tags(recipe: &blue_build_recipe::Recipe) -> miette::Result<Vec<String>> {
|
||||
trace!("LocalDriver::generate_tags({recipe:?})");
|
||||
Ok(vec![format!("local-{}", Driver::get_os_version(recipe)?)])
|
||||
}
|
||||
|
||||
fn generate_image_name(recipe: &blue_build_recipe::Recipe) -> miette::Result<String> {
|
||||
trace!("LocalDriver::generate_image_name({recipe:?})");
|
||||
Ok(recipe.name.trim().to_lowercase())
|
||||
}
|
||||
|
||||
fn get_repo_url() -> miette::Result<String> {
|
||||
trace!("LocalDriver::get_repo_url()");
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
fn get_registry() -> miette::Result<String> {
|
||||
trace!("LocalDriver::get_registry()");
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,12 @@ use clap::ValueEnum;
|
|||
pub use build::*;
|
||||
pub use inspect::*;
|
||||
pub use run::*;
|
||||
pub use signing::*;
|
||||
|
||||
mod build;
|
||||
mod inspect;
|
||||
mod run;
|
||||
mod signing;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default, ValueEnum)]
|
||||
pub enum CompressionType {
|
||||
|
|
@ -57,7 +57,7 @@ pub struct BuildTagPushOpts<'a> {
|
|||
|
||||
/// The list of tags for the image being built.
|
||||
#[builder(default, setter(into))]
|
||||
pub tags: Cow<'a, [&'a str]>,
|
||||
pub tags: Cow<'a, [String]>,
|
||||
|
||||
/// Enable pushing the image.
|
||||
#[builder(default)]
|
||||
82
process/drivers/opts/run.rs
Normal file
82
process/drivers/opts/run.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct RunOpts<'scope> {
|
||||
#[builder(setter(into))]
|
||||
pub image: Cow<'scope, str>,
|
||||
|
||||
#[builder(default, setter(into))]
|
||||
pub args: Cow<'scope, [String]>,
|
||||
|
||||
#[builder(default, setter(into))]
|
||||
pub env_vars: Cow<'scope, [RunOptsEnv<'scope>]>,
|
||||
|
||||
#[builder(default, setter(into))]
|
||||
pub volumes: Cow<'scope, [RunOptsVolume<'scope>]>,
|
||||
|
||||
#[builder(default, setter(strip_option))]
|
||||
pub uid: Option<u32>,
|
||||
|
||||
#[builder(default, setter(strip_option))]
|
||||
pub gid: Option<u32>,
|
||||
|
||||
#[builder(default, setter(into))]
|
||||
pub workdir: Cow<'scope, str>,
|
||||
|
||||
#[builder(default)]
|
||||
pub privileged: bool,
|
||||
|
||||
#[builder(default)]
|
||||
pub pull: bool,
|
||||
|
||||
#[builder(default)]
|
||||
pub remove: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct RunOptsVolume<'scope> {
|
||||
#[builder(setter(into))]
|
||||
pub path_or_vol_name: Cow<'scope, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
pub container_path: Cow<'scope, str>,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! run_volumes {
|
||||
($($host:expr => $container:expr),+ $(,)?) => {
|
||||
{
|
||||
[
|
||||
$($crate::drivers::opts::RunOptsVolume::builder()
|
||||
.path_or_vol_name($host)
|
||||
.container_path($container)
|
||||
.build(),)*
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct RunOptsEnv<'scope> {
|
||||
#[builder(setter(into))]
|
||||
pub key: Cow<'scope, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
pub value: Cow<'scope, str>,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! run_envs {
|
||||
($($key:expr => $value:expr),+ $(,)?) => {
|
||||
{
|
||||
[
|
||||
$($crate::drivers::opts::RunOptsEnv::builder()
|
||||
.key($key)
|
||||
.value($value)
|
||||
.build(),)*
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
115
process/drivers/opts/signing.rs
Normal file
115
process/drivers/opts/signing.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use typed_builder::TypedBuilder;
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
pub enum PrivateKey {
|
||||
Env(String),
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
pub trait PrivateKeyContents<T>
|
||||
where
|
||||
T: Zeroize,
|
||||
{
|
||||
/// Gets's the contents of the `PrivateKey`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the file or the environment couldn't be read.
|
||||
fn contents(&self) -> Result<Zeroizing<T>>;
|
||||
}
|
||||
|
||||
impl PrivateKeyContents<Vec<u8>> for PrivateKey {
|
||||
fn contents(&self) -> Result<Zeroizing<Vec<u8>>> {
|
||||
let key: Zeroizing<String> = self.contents()?;
|
||||
Ok(Zeroizing::new(key.as_bytes().to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivateKeyContents<String> for PrivateKey {
|
||||
fn contents(&self) -> Result<Zeroizing<String>> {
|
||||
Ok(Zeroizing::new(match *self {
|
||||
Self::Env(ref env) => env::var(env).into_diagnostic()?,
|
||||
Self::Path(ref path) => fs::read_to_string(path).into_diagnostic()?,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PrivateKey {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(
|
||||
match *self {
|
||||
Self::Env(ref env) => format!("env://{env}"),
|
||||
Self::Path(ref path) => format!("{}", path.display()),
|
||||
}
|
||||
.as_str(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct GenerateKeyPairOpts<'scope> {
|
||||
#[builder(setter(into, strip_option))]
|
||||
pub dir: Option<Cow<'scope, Path>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct CheckKeyPairOpts<'scope> {
|
||||
#[builder(setter(into, strip_option))]
|
||||
pub dir: Option<Cow<'scope, Path>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct SignOpts<'scope> {
|
||||
#[builder(setter(into))]
|
||||
pub image: Cow<'scope, str>,
|
||||
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub key: Option<Cow<'scope, str>>,
|
||||
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub dir: Option<Cow<'scope, Path>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum VerifyType<'scope> {
|
||||
File(Cow<'scope, Path>),
|
||||
Keyless {
|
||||
issuer: Cow<'scope, str>,
|
||||
identity: Cow<'scope, str>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct VerifyOpts<'scope> {
|
||||
#[builder(setter(into))]
|
||||
pub image: Cow<'scope, str>,
|
||||
pub verify_type: VerifyType<'scope>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct SignVerifyOpts<'scope> {
|
||||
#[builder(setter(into))]
|
||||
pub image: Cow<'scope, str>,
|
||||
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub tag: Option<Cow<'scope, str>>,
|
||||
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub dir: Option<Cow<'scope, Path>>,
|
||||
|
||||
/// Enable retry logic for pushing.
|
||||
#[builder(default)]
|
||||
pub retry_push: bool,
|
||||
|
||||
/// Number of times to retry pushing.
|
||||
///
|
||||
/// Defaults to 1.
|
||||
#[builder(default = 1)]
|
||||
pub retry_count: u8,
|
||||
}
|
||||
|
|
@ -1,28 +1,26 @@
|
|||
use std::{
|
||||
io::Write,
|
||||
path::Path,
|
||||
process::{Command, ExitStatus},
|
||||
process::{Command, ExitStatus, Stdio},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use blue_build_utils::{
|
||||
constants::SKOPEO_IMAGE,
|
||||
logging::{CommandLogging, Logger},
|
||||
signal_handler::{add_cid, remove_cid, ContainerId},
|
||||
};
|
||||
use blue_build_utils::{cmd, constants::SKOPEO_IMAGE, credentials::Credentials, string_vec};
|
||||
use colored::Colorize;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use miette::{bail, IntoDiagnostic, Result};
|
||||
use miette::{bail, miette, IntoDiagnostic, Result};
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
use tempdir::TempDir;
|
||||
|
||||
use crate::{
|
||||
credentials::Credentials, drivers::types::RunDriverType, image_metadata::ImageMetadata,
|
||||
drivers::image_metadata::ImageMetadata,
|
||||
logging::{CommandLogging, Logger},
|
||||
signal_handler::{add_cid, remove_cid, ContainerId, ContainerRuntime},
|
||||
};
|
||||
|
||||
use super::{
|
||||
credentials,
|
||||
opts::{BuildOpts, GetMetadataOpts, PushOpts, RunOpts, TagOpts},
|
||||
BuildDriver, DriverVersion, InspectDriver, RunDriver,
|
||||
};
|
||||
|
|
@ -51,10 +49,7 @@ impl DriverVersion for PodmanDriver {
|
|||
trace!("PodmanDriver::version()");
|
||||
|
||||
trace!("podman version -f json");
|
||||
let output = Command::new("podman")
|
||||
.arg("version")
|
||||
.arg("-f")
|
||||
.arg("json")
|
||||
let output = cmd!("podman", "version", "-f", "json")
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
|
||||
|
|
@ -68,25 +63,22 @@ impl DriverVersion for PodmanDriver {
|
|||
}
|
||||
|
||||
impl BuildDriver for PodmanDriver {
|
||||
fn build(&self, opts: &BuildOpts) -> Result<()> {
|
||||
fn build(opts: &BuildOpts) -> Result<()> {
|
||||
trace!("PodmanDriver::build({opts:#?})");
|
||||
|
||||
trace!(
|
||||
"podman build --pull=true --layers={} -f {} -t {} .",
|
||||
!opts.squash,
|
||||
opts.containerfile.display(),
|
||||
opts.image,
|
||||
let command = cmd!(
|
||||
"podman",
|
||||
"build",
|
||||
"--pull=true",
|
||||
format!("--layers={}", !opts.squash),
|
||||
"-f",
|
||||
&*opts.containerfile,
|
||||
"-t",
|
||||
&*opts.image,
|
||||
".",
|
||||
);
|
||||
let mut command = Command::new("podman");
|
||||
command
|
||||
.arg("build")
|
||||
.arg("--pull=true")
|
||||
.arg(format!("--layers={}", !opts.squash))
|
||||
.arg("-f")
|
||||
.arg(opts.containerfile.as_ref())
|
||||
.arg("-t")
|
||||
.arg(opts.image.as_ref())
|
||||
.arg(".");
|
||||
|
||||
trace!("{command:?}");
|
||||
let status = command
|
||||
.status_image_ref_progress(&opts.image, "Building Image")
|
||||
.into_diagnostic()?;
|
||||
|
|
@ -99,16 +91,13 @@ impl BuildDriver for PodmanDriver {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn tag(&self, opts: &TagOpts) -> Result<()> {
|
||||
fn tag(opts: &TagOpts) -> Result<()> {
|
||||
trace!("PodmanDriver::tag({opts:#?})");
|
||||
|
||||
trace!("podman tag {} {}", opts.src_image, opts.dest_image);
|
||||
let status = Command::new("podman")
|
||||
.arg("tag")
|
||||
.arg(opts.src_image.as_ref())
|
||||
.arg(opts.dest_image.as_ref())
|
||||
.status()
|
||||
.into_diagnostic()?;
|
||||
let mut command = cmd!("podman", "tag", &*opts.src_image, &*opts.dest_image,);
|
||||
|
||||
trace!("{command:?}");
|
||||
let status = command.status().into_diagnostic()?;
|
||||
|
||||
if status.success() {
|
||||
info!("Successfully tagged {}!", opts.dest_image);
|
||||
|
|
@ -118,18 +107,20 @@ impl BuildDriver for PodmanDriver {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn push(&self, opts: &PushOpts) -> Result<()> {
|
||||
fn push(opts: &PushOpts) -> Result<()> {
|
||||
trace!("PodmanDriver::push({opts:#?})");
|
||||
|
||||
trace!("podman push {}", opts.image);
|
||||
let mut command = Command::new("podman");
|
||||
command
|
||||
.arg("push")
|
||||
.arg(format!(
|
||||
let command = cmd!(
|
||||
"podman",
|
||||
"push",
|
||||
format!(
|
||||
"--compression-format={}",
|
||||
opts.compression_type.unwrap_or_default()
|
||||
))
|
||||
.arg(opts.image.as_ref());
|
||||
),
|
||||
&*opts.image,
|
||||
);
|
||||
|
||||
trace!("{command:?}");
|
||||
let status = command
|
||||
.status_image_ref_progress(&opts.image, "Pushing Image")
|
||||
.into_diagnostic()?;
|
||||
|
|
@ -142,40 +133,57 @@ impl BuildDriver for PodmanDriver {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn login(&self) -> Result<()> {
|
||||
fn login() -> Result<()> {
|
||||
trace!("PodmanDriver::login()");
|
||||
|
||||
if let Some(Credentials {
|
||||
registry,
|
||||
username,
|
||||
password,
|
||||
}) = credentials::get()
|
||||
}) = Credentials::get()
|
||||
{
|
||||
trace!("podman login -u {username} -p [MASKED] {registry}");
|
||||
let output = Command::new("podman")
|
||||
.arg("login")
|
||||
.arg("-u")
|
||||
.arg(username)
|
||||
.arg("-p")
|
||||
.arg(password)
|
||||
.arg(registry)
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
let mut command = cmd!(
|
||||
"podman",
|
||||
"login",
|
||||
"-u",
|
||||
username,
|
||||
"--password-stdin",
|
||||
registry
|
||||
);
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
trace!("{command:?}");
|
||||
let mut child = command.spawn().into_diagnostic()?;
|
||||
|
||||
write!(
|
||||
child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.ok_or_else(|| miette!("Unable to open pipe to stdin"))?,
|
||||
"{password}"
|
||||
)
|
||||
.into_diagnostic()?;
|
||||
|
||||
let output = child.wait_with_output().into_diagnostic()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let err_out = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("Failed to login for podman: {err_out}");
|
||||
bail!("Failed to login for podman:\n{}", err_out.trim());
|
||||
}
|
||||
debug!("Logged into {registry}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl InspectDriver for PodmanDriver {
|
||||
fn get_metadata(&self, opts: &GetMetadataOpts) -> Result<ImageMetadata> {
|
||||
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
|
||||
trace!("PodmanDriver::get_metadata({opts:#?})");
|
||||
|
||||
let url = opts.tag.as_ref().map_or_else(
|
||||
let url = opts.tag.as_deref().map_or_else(
|
||||
|| format!("docker://{}", opts.image),
|
||||
|tag| format!("docker://{}:{tag}", opts.image),
|
||||
);
|
||||
|
|
@ -187,14 +195,14 @@ impl InspectDriver for PodmanDriver {
|
|||
);
|
||||
progress.enable_steady_tick(Duration::from_millis(100));
|
||||
|
||||
let output = self
|
||||
.run_output(
|
||||
&RunOpts::builder()
|
||||
.image(SKOPEO_IMAGE)
|
||||
.args(&["inspect".to_string(), url.clone()])
|
||||
.build(),
|
||||
)
|
||||
.into_diagnostic()?;
|
||||
let output = Self::run_output(
|
||||
&RunOpts::builder()
|
||||
.image(SKOPEO_IMAGE)
|
||||
.args(string_vec!["inspect", url.clone()])
|
||||
.remove(true)
|
||||
.build(),
|
||||
)
|
||||
.into_diagnostic()?;
|
||||
|
||||
progress.finish();
|
||||
Logger::multi_progress().remove(&progress);
|
||||
|
|
@ -209,31 +217,31 @@ impl InspectDriver for PodmanDriver {
|
|||
}
|
||||
|
||||
impl RunDriver for PodmanDriver {
|
||||
fn run(&self, opts: &RunOpts) -> std::io::Result<ExitStatus> {
|
||||
fn run(opts: &RunOpts) -> std::io::Result<ExitStatus> {
|
||||
trace!("PodmanDriver::run({opts:#?})");
|
||||
|
||||
let cid_path = TempDir::new("podman")?;
|
||||
let cid_file = cid_path.path().join("cid");
|
||||
|
||||
let cid = ContainerId::new(&cid_file, RunDriverType::Podman, opts.privileged);
|
||||
let cid = ContainerId::new(&cid_file, ContainerRuntime::Podman, opts.privileged);
|
||||
|
||||
add_cid(&cid);
|
||||
|
||||
let status = podman_run(opts, &cid_file)
|
||||
.status_image_ref_progress(opts.image.as_ref(), "Running container")?;
|
||||
.status_image_ref_progress(&*opts.image, "Running container")?;
|
||||
|
||||
remove_cid(&cid);
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
fn run_output(&self, opts: &RunOpts) -> std::io::Result<std::process::Output> {
|
||||
fn run_output(opts: &RunOpts) -> std::io::Result<std::process::Output> {
|
||||
trace!("PodmanDriver::run_output({opts:#?})");
|
||||
|
||||
let cid_path = TempDir::new("podman")?;
|
||||
let cid_file = cid_path.path().join("cid");
|
||||
|
||||
let cid = ContainerId::new(&cid_file, RunDriverType::Podman, opts.privileged);
|
||||
let cid = ContainerId::new(&cid_file, ContainerRuntime::Podman, opts.privileged);
|
||||
|
||||
add_cid(&cid);
|
||||
|
||||
|
|
@ -246,52 +254,31 @@ impl RunDriver for PodmanDriver {
|
|||
}
|
||||
|
||||
fn podman_run(opts: &RunOpts, cid_file: &Path) -> Command {
|
||||
let mut command = if opts.privileged {
|
||||
warn!(
|
||||
"Running 'podman' in privileged mode requires '{}'",
|
||||
"sudo".bold().red()
|
||||
);
|
||||
Command::new("sudo")
|
||||
} else {
|
||||
Command::new("podman")
|
||||
};
|
||||
|
||||
if opts.privileged {
|
||||
command.arg("podman");
|
||||
}
|
||||
|
||||
command
|
||||
.arg("run")
|
||||
.arg(format!("--cidfile={}", cid_file.display()));
|
||||
|
||||
if opts.privileged {
|
||||
command.arg("--privileged");
|
||||
}
|
||||
|
||||
if opts.remove {
|
||||
command.arg("--rm");
|
||||
}
|
||||
|
||||
if opts.pull {
|
||||
command.arg("--pull=always");
|
||||
}
|
||||
|
||||
opts.volumes.iter().for_each(|volume| {
|
||||
command.arg("--volume");
|
||||
command.arg(format!(
|
||||
"{}:{}",
|
||||
volume.path_or_vol_name, volume.container_path,
|
||||
));
|
||||
});
|
||||
|
||||
opts.env_vars.iter().for_each(|env| {
|
||||
command.arg("--env");
|
||||
command.arg(format!("{}={}", env.key, env.value));
|
||||
});
|
||||
|
||||
command.arg(opts.image.as_ref());
|
||||
command.args(opts.args.iter());
|
||||
|
||||
trace!("{command:?}");
|
||||
command
|
||||
cmd!(
|
||||
if opts.privileged {
|
||||
warn!(
|
||||
"Running 'podman' in privileged mode requires '{}'",
|
||||
"sudo".bold().red()
|
||||
);
|
||||
"sudo"
|
||||
} else {
|
||||
"podman"
|
||||
},
|
||||
if opts.privileged => "podman",
|
||||
"run",
|
||||
format!("--cidfile={}", cid_file.display()),
|
||||
if opts.privileged => "--privileged",
|
||||
if opts.remove => "--rm",
|
||||
if opts.pull => "--pull=always",
|
||||
for volume in opts.volumes => [
|
||||
"--volume",
|
||||
format!("{}:{}", volume.path_or_vol_name, volume.container_path),
|
||||
],
|
||||
for env in opts.env_vars => [
|
||||
"--env",
|
||||
format!("{}={}", env.key, env.value),
|
||||
],
|
||||
&*opts.image,
|
||||
for opts.args,
|
||||
)
|
||||
}
|
||||
288
process/drivers/sigstore_driver.rs
Normal file
288
process/drivers/sigstore_driver.rs
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
use std::{fs, path::Path};
|
||||
|
||||
use crate::{
|
||||
drivers::opts::{PrivateKeyContents, VerifyType},
|
||||
RT,
|
||||
};
|
||||
|
||||
use super::{
|
||||
functions::get_private_key,
|
||||
opts::{CheckKeyPairOpts, GenerateKeyPairOpts, SignOpts, VerifyOpts},
|
||||
SigningDriver,
|
||||
};
|
||||
use blue_build_utils::{
|
||||
constants::{COSIGN_PRIV_PATH, COSIGN_PUB_PATH},
|
||||
credentials::Credentials,
|
||||
};
|
||||
use log::{debug, trace};
|
||||
use miette::{bail, miette, Context, IntoDiagnostic};
|
||||
use sigstore::{
|
||||
cosign::{
|
||||
constraint::PrivateKeySigner,
|
||||
verification_constraint::{PublicKeyVerifier, VerificationConstraintVec},
|
||||
ClientBuilder, Constraint, CosignCapabilities, SignatureLayer,
|
||||
},
|
||||
crypto::{signing_key::SigStoreKeyPair, SigningScheme},
|
||||
errors::SigstoreVerifyConstraintsError,
|
||||
registry::{Auth, OciReference},
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub struct SigstoreDriver;
|
||||
|
||||
impl SigningDriver for SigstoreDriver {
|
||||
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> miette::Result<()> {
|
||||
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
|
||||
let priv_key_path = path.join(COSIGN_PRIV_PATH);
|
||||
let pub_key_path = path.join(COSIGN_PUB_PATH);
|
||||
|
||||
if priv_key_path.exists() {
|
||||
bail!("Private key file already exists at {COSIGN_PRIV_PATH}");
|
||||
} else if pub_key_path.exists() {
|
||||
bail!("Public key file already exists at {COSIGN_PUB_PATH}");
|
||||
}
|
||||
|
||||
let signer = SigningScheme::default()
|
||||
.create_signer()
|
||||
.into_diagnostic()
|
||||
.context("Failed to create signer")?;
|
||||
|
||||
let keypair = signer
|
||||
.to_sigstore_keypair()
|
||||
.into_diagnostic()
|
||||
.context("Failed to create key pair")?;
|
||||
|
||||
let priv_key = keypair
|
||||
.private_key_to_encrypted_pem(b"")
|
||||
.into_diagnostic()
|
||||
.context("Failed to create encrypted private key")?;
|
||||
let pub_key = keypair.public_key_to_pem().into_diagnostic()?;
|
||||
|
||||
fs::write(priv_key_path, priv_key)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to write {COSIGN_PRIV_PATH}"))?;
|
||||
fs::write(pub_key_path, pub_key)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to write {COSIGN_PUB_PATH}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_signing_files(opts: &CheckKeyPairOpts) -> miette::Result<()> {
|
||||
trace!("SigstoreDriver::check_signing_files({opts:?})");
|
||||
|
||||
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
|
||||
let pub_path = path.join(COSIGN_PUB_PATH);
|
||||
|
||||
let pub_key = fs::read_to_string(&pub_path)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to open public key file {}", pub_path.display()))?;
|
||||
debug!("Retrieved public key from {COSIGN_PUB_PATH}");
|
||||
trace!("{pub_key}");
|
||||
|
||||
let key: Zeroizing<String> = get_private_key(path)
|
||||
.context("Failed to get private key")?
|
||||
.contents()?;
|
||||
debug!("Retrieved private key");
|
||||
|
||||
let keypair = SigStoreKeyPair::from_encrypted_pem(key.as_bytes(), b"")
|
||||
.into_diagnostic()
|
||||
.context("Failed to generate key pair from private key")?;
|
||||
let gen_pub = keypair
|
||||
.public_key_to_pem()
|
||||
.into_diagnostic()
|
||||
.context("Failed to generate public key from private key")?;
|
||||
debug!("Generated public key from private key");
|
||||
trace!("{gen_pub}");
|
||||
|
||||
if pub_key.trim() == gen_pub.trim() {
|
||||
debug!("Public and private key matches");
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Private and public keys do not match.")
|
||||
}
|
||||
}
|
||||
|
||||
fn sign(opts: &SignOpts) -> miette::Result<()> {
|
||||
trace!("SigstoreDriver::sign({opts:?})");
|
||||
|
||||
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
|
||||
let mut client = ClientBuilder::default().build().into_diagnostic()?;
|
||||
|
||||
let image_digest: &str = opts.image.as_ref();
|
||||
let image_digest: OciReference = image_digest.parse().into_diagnostic()?;
|
||||
trace!("{image_digest:?}");
|
||||
|
||||
let signing_scheme = SigningScheme::default();
|
||||
let key: Zeroizing<Vec<u8>> = get_private_key(path)?.contents()?;
|
||||
debug!("Retrieved private key");
|
||||
|
||||
let signer = PrivateKeySigner::new_with_signer(
|
||||
SigStoreKeyPair::from_encrypted_pem(&key, b"")
|
||||
.into_diagnostic()?
|
||||
.to_sigstore_signer(&signing_scheme)
|
||||
.into_diagnostic()?,
|
||||
);
|
||||
debug!("Created signer");
|
||||
|
||||
let Credentials {
|
||||
registry: _,
|
||||
username,
|
||||
password,
|
||||
} = Credentials::get().ok_or_else(|| miette!("Credentials are required for signing"))?;
|
||||
let auth = Auth::Basic(username.clone(), password.clone());
|
||||
debug!("Credentials retrieved");
|
||||
|
||||
let (cosign_signature_image, source_image_digest) = RT
|
||||
.block_on(client.triangulate(&image_digest, &auth))
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to triangulate image {image_digest}"))?;
|
||||
debug!("Triangulating image");
|
||||
trace!("{cosign_signature_image}, {source_image_digest}");
|
||||
|
||||
let mut signature_layer =
|
||||
SignatureLayer::new_unsigned(&image_digest, &source_image_digest).into_diagnostic()?;
|
||||
signer
|
||||
.add_constraint(&mut signature_layer)
|
||||
.into_diagnostic()?;
|
||||
debug!("Created signing layer");
|
||||
|
||||
debug!("Pushing signature");
|
||||
RT.block_on(client.push_signature(
|
||||
None,
|
||||
&auth,
|
||||
&cosign_signature_image,
|
||||
vec![signature_layer],
|
||||
))
|
||||
.into_diagnostic()
|
||||
.with_context(|| {
|
||||
format!("Failed to push signature {cosign_signature_image} for image {image_digest}")
|
||||
})?;
|
||||
debug!("Successfully pushed signature");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify(opts: &VerifyOpts) -> miette::Result<()> {
|
||||
let mut client = ClientBuilder::default().build().into_diagnostic()?;
|
||||
|
||||
let image_digest: &str = opts.image.as_ref();
|
||||
let image_digest: OciReference = image_digest.parse().into_diagnostic()?;
|
||||
trace!("{image_digest:?}");
|
||||
|
||||
let signing_scheme = SigningScheme::default();
|
||||
|
||||
let pub_key = fs::read_to_string(match &opts.verify_type {
|
||||
VerifyType::File(path) => path,
|
||||
VerifyType::Keyless { .. } => {
|
||||
todo!("Keyless currently not supported for sigstore driver")
|
||||
}
|
||||
})
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to open public key file {COSIGN_PUB_PATH}"))?;
|
||||
debug!("Retrieved public key from {COSIGN_PUB_PATH}");
|
||||
trace!("{pub_key}");
|
||||
|
||||
let verifier =
|
||||
PublicKeyVerifier::new(pub_key.as_bytes(), &signing_scheme).into_diagnostic()?;
|
||||
let verification_constraints: VerificationConstraintVec = vec![Box::new(verifier)];
|
||||
|
||||
let auth = Auth::Anonymous;
|
||||
let (cosign_signature_image, source_image_digest) = RT
|
||||
.block_on(client.triangulate(&image_digest, &auth))
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to triangulate image {image_digest}"))?;
|
||||
debug!("Triangulating image");
|
||||
trace!("{cosign_signature_image}, {source_image_digest}");
|
||||
|
||||
let trusted_layers = RT
|
||||
.block_on(client.trusted_signature_layers(
|
||||
&auth,
|
||||
&source_image_digest,
|
||||
&cosign_signature_image,
|
||||
))
|
||||
.into_diagnostic()?;
|
||||
|
||||
sigstore::cosign::verify_constraints(&trusted_layers, verification_constraints.iter())
|
||||
.map_err(
|
||||
|SigstoreVerifyConstraintsError {
|
||||
unsatisfied_constraints,
|
||||
}| {
|
||||
miette!("Failed to verify for constraints: {unsatisfied_constraints:?}")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn signing_login() -> miette::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use blue_build_utils::constants::{COSIGN_PRIV_PATH, COSIGN_PUB_PATH};
|
||||
use tempdir::TempDir;
|
||||
|
||||
use crate::drivers::{
|
||||
cosign_driver::CosignDriver,
|
||||
opts::{CheckKeyPairOpts, GenerateKeyPairOpts},
|
||||
SigningDriver,
|
||||
};
|
||||
|
||||
use super::SigstoreDriver;
|
||||
|
||||
#[test]
|
||||
fn generate_key_pair() {
|
||||
let tempdir = TempDir::new("keypair").unwrap();
|
||||
|
||||
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
SigstoreDriver::generate_key_pair(&gen_opts).unwrap();
|
||||
|
||||
eprintln!(
|
||||
"Private key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
|
||||
);
|
||||
eprintln!(
|
||||
"Public key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
|
||||
);
|
||||
|
||||
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
SigstoreDriver::check_signing_files(&check_opts).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_key_pairs() {
|
||||
let path = Path::new("../test-files/keys");
|
||||
|
||||
let opts = CheckKeyPairOpts::builder().dir(path).build();
|
||||
|
||||
SigstoreDriver::check_signing_files(&opts).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compatibility() {
|
||||
let tempdir = TempDir::new("keypair").unwrap();
|
||||
|
||||
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
SigstoreDriver::generate_key_pair(&gen_opts).unwrap();
|
||||
|
||||
eprintln!(
|
||||
"Private key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
|
||||
);
|
||||
eprintln!(
|
||||
"Public key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
|
||||
);
|
||||
|
||||
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
CosignDriver::check_signing_files(&check_opts).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,19 @@
|
|||
use std::{
|
||||
process::{Command, Stdio},
|
||||
time::Duration,
|
||||
};
|
||||
use std::{process::Stdio, time::Duration};
|
||||
|
||||
use blue_build_utils::logging::Logger;
|
||||
use blue_build_utils::cmd;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use log::{debug, trace};
|
||||
use miette::{bail, IntoDiagnostic, Result};
|
||||
|
||||
use crate::image_metadata::ImageMetadata;
|
||||
use crate::logging::Logger;
|
||||
|
||||
use super::{opts::GetMetadataOpts, InspectDriver};
|
||||
use super::{image_metadata::ImageMetadata, opts::GetMetadataOpts, InspectDriver};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SkopeoDriver;
|
||||
|
||||
impl InspectDriver for SkopeoDriver {
|
||||
fn get_metadata(&self, opts: &GetMetadataOpts) -> Result<ImageMetadata> {
|
||||
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
|
||||
trace!("SkopeoDriver::get_metadata({opts:#?})");
|
||||
|
||||
let url = opts.tag.as_ref().map_or_else(
|
||||
|
|
@ -32,9 +29,7 @@ impl InspectDriver for SkopeoDriver {
|
|||
progress.enable_steady_tick(Duration::from_millis(100));
|
||||
|
||||
trace!("skopeo inspect {url}");
|
||||
let output = Command::new("skopeo")
|
||||
.arg("inspect")
|
||||
.arg(&url)
|
||||
let output = cmd!("skopeo", "inspect", &url)
|
||||
.stderr(Stdio::inherit())
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
335
process/drivers/traits.rs
Normal file
335
process/drivers/traits.rs
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
use std::{
|
||||
path::PathBuf,
|
||||
process::{ExitStatus, Output},
|
||||
};
|
||||
|
||||
use blue_build_recipe::Recipe;
|
||||
use blue_build_utils::{constants::COSIGN_PUB_PATH, retry};
|
||||
use log::{debug, info, trace};
|
||||
use miette::{bail, miette, Result};
|
||||
use semver::{Version, VersionReq};
|
||||
|
||||
use crate::drivers::{functions::get_private_key, types::CiDriverType, Driver};
|
||||
|
||||
use super::{
|
||||
image_metadata::ImageMetadata,
|
||||
opts::{
|
||||
BuildOpts, BuildTagPushOpts, CheckKeyPairOpts, GenerateKeyPairOpts, GetMetadataOpts,
|
||||
PushOpts, RunOpts, SignOpts, SignVerifyOpts, TagOpts, VerifyOpts, VerifyType,
|
||||
},
|
||||
};
|
||||
|
||||
/// Trait for retrieving version of a driver.
|
||||
pub trait DriverVersion {
|
||||
/// The version req string slice that follows
|
||||
/// the semver standard <https://semver.org/>.
|
||||
const VERSION_REQ: &'static str;
|
||||
|
||||
/// Returns the version of the driver.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if it can't retrieve the version.
|
||||
fn version() -> Result<Version>;
|
||||
|
||||
#[must_use]
|
||||
fn is_supported_version() -> bool {
|
||||
Self::version().is_ok_and(|version| {
|
||||
VersionReq::parse(Self::VERSION_REQ).is_ok_and(|req| req.matches(&version))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows agnostic building, tagging
|
||||
/// pushing, and login.
|
||||
pub trait BuildDriver {
|
||||
/// Runs the build logic for the driver.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the build fails.
|
||||
fn build(opts: &BuildOpts) -> Result<()>;
|
||||
|
||||
/// Runs the tag logic for the driver.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the tagging fails.
|
||||
fn tag(opts: &TagOpts) -> Result<()>;
|
||||
|
||||
/// Runs the push logic for the driver
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the push fails.
|
||||
fn push(opts: &PushOpts) -> Result<()>;
|
||||
|
||||
/// Runs the login logic for the driver.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if login fails.
|
||||
fn login() -> Result<()>;
|
||||
|
||||
/// Runs the logic for building, tagging, and pushing an image.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if building, tagging, or pusing fails.
|
||||
fn build_tag_push(opts: &BuildTagPushOpts) -> Result<()> {
|
||||
trace!("BuildDriver::build_tag_push({opts:#?})");
|
||||
|
||||
let full_image = match (opts.archive_path.as_ref(), opts.image.as_ref()) {
|
||||
(Some(archive_path), None) => {
|
||||
format!("oci-archive:{archive_path}")
|
||||
}
|
||||
(None, Some(image)) => opts
|
||||
.tags
|
||||
.first()
|
||||
.map_or_else(|| image.to_string(), |tag| format!("{image}:{tag}")),
|
||||
(Some(_), Some(_)) => bail!("Cannot use both image and archive path"),
|
||||
(None, None) => bail!("Need either the image or archive path set"),
|
||||
};
|
||||
|
||||
let build_opts = BuildOpts::builder()
|
||||
.image(&full_image)
|
||||
.containerfile(opts.containerfile.as_ref())
|
||||
.squash(opts.squash)
|
||||
.build();
|
||||
|
||||
info!("Building image {full_image}");
|
||||
Self::build(&build_opts)?;
|
||||
|
||||
if !opts.tags.is_empty() && opts.archive_path.is_none() {
|
||||
let image = opts
|
||||
.image
|
||||
.as_ref()
|
||||
.ok_or_else(|| miette!("Image is required in order to tag"))?;
|
||||
debug!("Tagging all images");
|
||||
|
||||
for tag in opts.tags.as_ref() {
|
||||
debug!("Tagging {} with {tag}", &full_image);
|
||||
|
||||
let tag_opts = TagOpts::builder()
|
||||
.src_image(&full_image)
|
||||
.dest_image(format!("{image}:{tag}"))
|
||||
.build();
|
||||
|
||||
Self::tag(&tag_opts)?;
|
||||
|
||||
if opts.push {
|
||||
let retry_count = if opts.retry_push { opts.retry_count } else { 0 };
|
||||
|
||||
debug!("Pushing all images");
|
||||
// Push images with retries (1s delay between retries)
|
||||
blue_build_utils::retry(retry_count, 5, || {
|
||||
let tag_image = format!("{image}:{tag}");
|
||||
|
||||
debug!("Pushing image {tag_image}");
|
||||
|
||||
let push_opts = PushOpts::builder()
|
||||
.image(&tag_image)
|
||||
.compression_type(opts.compression)
|
||||
.build();
|
||||
|
||||
Self::push(&push_opts)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows agnostic inspection of images.
|
||||
pub trait InspectDriver {
|
||||
/// Gets the metadata on an image tag.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if it is unable to get the labels.
|
||||
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata>;
|
||||
}
|
||||
|
||||
pub trait RunDriver: Sync + Send {
|
||||
/// Run a container to perform an action.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if there is an issue running the container.
|
||||
fn run(opts: &RunOpts) -> std::io::Result<ExitStatus>;
|
||||
|
||||
/// Run a container to perform an action and capturing output.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if there is an issue running the container.
|
||||
fn run_output(opts: &RunOpts) -> std::io::Result<Output>;
|
||||
}
|
||||
|
||||
pub trait SigningDriver {
|
||||
/// Generate a new private/public key pair.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if a key-pair couldn't be generated.
|
||||
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> Result<()>;
|
||||
|
||||
/// Checks the signing key files to ensure
|
||||
/// they match.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the files cannot be verified.
|
||||
fn check_signing_files(opts: &CheckKeyPairOpts) -> Result<()>;
|
||||
|
||||
/// Signs the image digest.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if signing fails.
|
||||
fn sign(opts: &SignOpts) -> Result<()>;
|
||||
|
||||
/// Verifies the image.
|
||||
///
|
||||
/// The image can be verified either with `VerifyType::File` containing
|
||||
/// the public key contents, or with `VerifyType::Keyless` containing
|
||||
/// information about the `issuer` and `identity`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the image fails to be verified.
|
||||
fn verify(opts: &VerifyOpts) -> Result<()>;
|
||||
|
||||
/// Sign an image given the image name and tag.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the image fails to be signed.
|
||||
fn sign_and_verify(opts: &SignVerifyOpts) -> Result<()> {
|
||||
trace!("sign_and_verify({opts:?})");
|
||||
|
||||
let path = opts
|
||||
.dir
|
||||
.as_ref()
|
||||
.map_or_else(|| PathBuf::from("."), |d| d.to_path_buf());
|
||||
|
||||
let image_name: &str = opts.image.as_ref();
|
||||
let inspect_opts = GetMetadataOpts::builder().image(image_name);
|
||||
|
||||
let inspect_opts = if let Some(ref tag) = opts.tag {
|
||||
inspect_opts.tag(tag.as_ref() as &str).build()
|
||||
} else {
|
||||
inspect_opts.build()
|
||||
};
|
||||
|
||||
let image_digest = Driver::get_metadata(&inspect_opts)?.digest;
|
||||
let image_name_tag = opts
|
||||
.tag
|
||||
.as_ref()
|
||||
.map_or_else(|| image_name.to_owned(), |t| format!("{image_name}:{t}"));
|
||||
let image_digest = format!("{image_name}@{image_digest}");
|
||||
|
||||
let (sign_opts, verify_opts) = match (Driver::get_ci_driver(), get_private_key(&path)) {
|
||||
// Cosign public/private key pair
|
||||
(_, Ok(priv_key)) => (
|
||||
SignOpts::builder()
|
||||
.image(&image_digest)
|
||||
.dir(&path)
|
||||
.key(priv_key.to_string())
|
||||
.build(),
|
||||
VerifyOpts::builder()
|
||||
.image(&image_name_tag)
|
||||
.verify_type(VerifyType::File(path.join(COSIGN_PUB_PATH).into()))
|
||||
.build(),
|
||||
),
|
||||
// Gitlab keyless
|
||||
(CiDriverType::Github | CiDriverType::Gitlab, _) => (
|
||||
SignOpts::builder().dir(&path).image(&image_digest).build(),
|
||||
VerifyOpts::builder()
|
||||
.image(&image_name_tag)
|
||||
.verify_type(VerifyType::Keyless {
|
||||
issuer: Driver::oidc_provider()?.into(),
|
||||
identity: Driver::keyless_cert_identity()?.into(),
|
||||
})
|
||||
.build(),
|
||||
),
|
||||
_ => bail!("Failed to get information for signing the image"),
|
||||
};
|
||||
|
||||
let retry_count = if opts.retry_push { opts.retry_count } else { 0 };
|
||||
|
||||
retry(retry_count, 5, || {
|
||||
Self::sign(&sign_opts)?;
|
||||
Self::verify(&verify_opts)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs the login logic for the signing driver.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if login fails.
|
||||
fn signing_login() -> Result<()>;
|
||||
}
|
||||
|
||||
/// Allows agnostic retrieval of CI-based information.
|
||||
pub trait CiDriver {
|
||||
/// Determines if we're on the main branch of
|
||||
/// a repository.
|
||||
fn on_default_branch() -> bool;
|
||||
|
||||
/// Retrieve the certificate identity for
|
||||
/// keyless signing.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the environment variables aren't set.
|
||||
fn keyless_cert_identity() -> Result<String>;
|
||||
|
||||
/// Retrieve the OIDC Provider for keyless signing.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the environment variables aren't set.
|
||||
fn oidc_provider() -> Result<String>;
|
||||
|
||||
/// Generate a list of tags based on the OS version.
|
||||
///
|
||||
/// ## CI
|
||||
/// The tags are generated based on the CI system that
|
||||
/// is detected. The general format for the default branch is:
|
||||
/// - `${os_version}`
|
||||
/// - `${timestamp}-${os_version}`
|
||||
///
|
||||
/// On a branch:
|
||||
/// - `br-${branch_name}-${os_version}`
|
||||
///
|
||||
/// In a PR(GitHub)/MR(GitLab)
|
||||
/// - `pr-${pr_event_number}-${os_version}`/`mr-${mr_iid}-${os_version}`
|
||||
///
|
||||
/// In all above cases the short git sha is also added:
|
||||
/// - `${commit_sha}-${os_version}`
|
||||
///
|
||||
/// When `alt_tags` are not present, the following tags are added:
|
||||
/// - `latest`
|
||||
/// - `${timestamp}`
|
||||
///
|
||||
/// ## Locally
|
||||
/// When ran locally, only a local tag is created:
|
||||
/// - `local-${os_version}`
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the environment variables aren't set.
|
||||
fn generate_tags(recipe: &Recipe) -> Result<Vec<String>>;
|
||||
|
||||
/// Generates the image name based on CI.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the environment variables aren't set.
|
||||
fn generate_image_name(recipe: &Recipe) -> Result<String> {
|
||||
Ok(format!(
|
||||
"{}/{}",
|
||||
Self::get_registry()?,
|
||||
recipe.name.trim().to_lowercase()
|
||||
))
|
||||
}
|
||||
|
||||
/// Get the URL for the repository.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the environment variables aren't set.
|
||||
fn get_repo_url() -> Result<String>;
|
||||
|
||||
/// Get the registry ref for the image.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the environment variables aren't set.
|
||||
fn get_registry() -> Result<String>;
|
||||
}
|
||||
169
process/drivers/types.rs
Normal file
169
process/drivers/types.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
use std::env;
|
||||
|
||||
use blue_build_utils::constants::{GITHUB_ACTIONS, GITLAB_CI};
|
||||
use clap::ValueEnum;
|
||||
use log::trace;
|
||||
|
||||
use crate::drivers::{
|
||||
buildah_driver::BuildahDriver, docker_driver::DockerDriver, podman_driver::PodmanDriver,
|
||||
DriverVersion,
|
||||
};
|
||||
|
||||
pub(super) trait DetermineDriver<T> {
|
||||
fn determine_driver(&mut self) -> T;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum InspectDriverType {
|
||||
Skopeo,
|
||||
Podman,
|
||||
Docker,
|
||||
}
|
||||
|
||||
impl DetermineDriver<InspectDriverType> for Option<InspectDriverType> {
|
||||
fn determine_driver(&mut self) -> InspectDriverType {
|
||||
*self.get_or_insert(
|
||||
match (
|
||||
blue_build_utils::check_command_exists("skopeo"),
|
||||
blue_build_utils::check_command_exists("docker"),
|
||||
blue_build_utils::check_command_exists("podman"),
|
||||
) {
|
||||
(Ok(_skopeo), _, _) => InspectDriverType::Skopeo,
|
||||
(_, Ok(_docker), _) => InspectDriverType::Docker,
|
||||
(_, _, Ok(_podman)) => InspectDriverType::Podman,
|
||||
_ => panic!(
|
||||
"{}{}",
|
||||
"Could not determine inspection strategy. ",
|
||||
"You need either skopeo, docker, or podman",
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum BuildDriverType {
|
||||
Buildah,
|
||||
Podman,
|
||||
Docker,
|
||||
}
|
||||
|
||||
impl DetermineDriver<BuildDriverType> for Option<BuildDriverType> {
|
||||
fn determine_driver(&mut self) -> BuildDriverType {
|
||||
*self.get_or_insert(
|
||||
match (
|
||||
blue_build_utils::check_command_exists("docker"),
|
||||
blue_build_utils::check_command_exists("podman"),
|
||||
blue_build_utils::check_command_exists("buildah"),
|
||||
) {
|
||||
(Ok(_docker), _, _) if DockerDriver::is_supported_version() => {
|
||||
BuildDriverType::Docker
|
||||
}
|
||||
(_, Ok(_podman), _) if PodmanDriver::is_supported_version() => {
|
||||
BuildDriverType::Podman
|
||||
}
|
||||
(_, _, Ok(_buildah)) if BuildahDriver::is_supported_version() => {
|
||||
BuildDriverType::Buildah
|
||||
}
|
||||
_ => panic!(
|
||||
"{}{}{}{}",
|
||||
"Could not determine strategy, ",
|
||||
format_args!("need either docker version {}, ", DockerDriver::VERSION_REQ,),
|
||||
format_args!("podman version {}, ", PodmanDriver::VERSION_REQ,),
|
||||
format_args!(
|
||||
"or buildah version {} to continue",
|
||||
BuildahDriver::VERSION_REQ,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum SigningDriverType {
|
||||
Cosign,
|
||||
#[cfg(feature = "sigstore")]
|
||||
Sigstore,
|
||||
}
|
||||
|
||||
impl DetermineDriver<SigningDriverType> for Option<SigningDriverType> {
|
||||
fn determine_driver(&mut self) -> SigningDriverType {
|
||||
trace!("SigningDriverType::determine_signing_driver()");
|
||||
|
||||
#[cfg(feature = "sigstore")]
|
||||
{
|
||||
*self.get_or_insert(
|
||||
blue_build_utils::check_command_exists("cosign")
|
||||
.map_or(SigningDriverType::Sigstore, |()| SigningDriverType::Cosign),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "sigstore"))]
|
||||
{
|
||||
SigningDriverType::Cosign
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum RunDriverType {
|
||||
Podman,
|
||||
Docker,
|
||||
}
|
||||
|
||||
impl From<RunDriverType> for String {
|
||||
fn from(value: RunDriverType) -> Self {
|
||||
match value {
|
||||
RunDriverType::Podman => "podman".to_string(),
|
||||
RunDriverType::Docker => "docker".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DetermineDriver<RunDriverType> for Option<RunDriverType> {
|
||||
fn determine_driver(&mut self) -> RunDriverType {
|
||||
trace!("RunDriver::determine_driver()");
|
||||
|
||||
*self.get_or_insert(
|
||||
match (
|
||||
blue_build_utils::check_command_exists("docker"),
|
||||
blue_build_utils::check_command_exists("podman"),
|
||||
) {
|
||||
(Ok(_docker), _) if DockerDriver::is_supported_version() => RunDriverType::Docker,
|
||||
(_, Ok(_podman)) if PodmanDriver::is_supported_version() => RunDriverType::Podman,
|
||||
_ => panic!(
|
||||
"{}{}{}{}",
|
||||
"Could not determine strategy, ",
|
||||
format_args!("need either docker version {}, ", DockerDriver::VERSION_REQ),
|
||||
format_args!("podman version {}, ", PodmanDriver::VERSION_REQ),
|
||||
format_args!(
|
||||
"or buildah version {} to continue",
|
||||
BuildahDriver::VERSION_REQ
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum CiDriverType {
|
||||
Local,
|
||||
Gitlab,
|
||||
Github,
|
||||
}
|
||||
|
||||
impl DetermineDriver<CiDriverType> for Option<CiDriverType> {
|
||||
fn determine_driver(&mut self) -> CiDriverType {
|
||||
trace!("CiDriverType::determine_driver()");
|
||||
|
||||
*self.get_or_insert(
|
||||
match (env::var(GITLAB_CI).ok(), env::var(GITHUB_ACTIONS).ok()) {
|
||||
(Some(_gitlab_ci), None) => CiDriverType::Gitlab,
|
||||
(None, Some(_github_actions)) => CiDriverType::Github,
|
||||
_ => CiDriverType::Local,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
57
process/process.rs
Normal file
57
process/process.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
//! This module is responsible for managing processes spawned
|
||||
//! by this tool. It contains drivers for running, building, inspecting, and signing
|
||||
//! images that interface with tools like docker or podman.
|
||||
|
||||
#[cfg(feature = "sigstore")]
|
||||
use once_cell::sync::Lazy;
|
||||
#[cfg(feature = "sigstore")]
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
pub mod drivers;
|
||||
pub mod logging;
|
||||
pub mod signal_handler;
|
||||
|
||||
#[cfg(feature = "sigstore")]
|
||||
pub(crate) static RT: Lazy<Runtime> = Lazy::new(|| {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use std::sync::Mutex;
|
||||
|
||||
use blue_build_recipe::{Module, ModuleExt, Recipe};
|
||||
use indexmap::IndexMap;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
pub const BB_UNIT_TEST_MOCK_GET_OS_VERSION: &str = "BB_UNIT_TEST_MOCK_GET_OS_VERSION";
|
||||
|
||||
/// This mutex is used for tests that require the reading of
|
||||
/// environment variables. Env vars are an inheritly unsafe
|
||||
/// as they can be changed and affect other threads functionality.
|
||||
///
|
||||
/// For tests that require setting env vars, they need to lock this
|
||||
/// mutex before making changes to the env. Any changes made to the env
|
||||
/// MUST be undone in the same test before dropping the lock. Failure to
|
||||
/// do so will cause unpredictable behavior with other tests.
|
||||
pub static ENV_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
|
||||
|
||||
pub fn create_test_recipe() -> Recipe<'static> {
|
||||
Recipe::builder()
|
||||
.name("test")
|
||||
.description("This is a test")
|
||||
.base_image("base-image")
|
||||
.image_version("40")
|
||||
.modules_ext(
|
||||
ModuleExt::builder()
|
||||
.modules(vec![Module::builder().build()])
|
||||
.build(),
|
||||
)
|
||||
.stages_ext(None)
|
||||
.extra(IndexMap::new())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
use std::{
|
||||
fs,
|
||||
path::PathBuf,
|
||||
process::{self, Command},
|
||||
sync::{atomic::AtomicBool, Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
|
||||
use blue_build_utils::cmd;
|
||||
use log::{debug, error, trace, warn};
|
||||
use nix::{
|
||||
libc::{SIGABRT, SIGCONT, SIGHUP, SIGTSTP},
|
||||
|
|
@ -26,21 +26,34 @@ use crate::logging::Logger;
|
|||
pub struct ContainerId {
|
||||
cid_path: PathBuf,
|
||||
requires_sudo: bool,
|
||||
crt: String,
|
||||
container_runtime: ContainerRuntime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ContainerRuntime {
|
||||
Podman,
|
||||
Docker,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ContainerRuntime {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match *self {
|
||||
Self::Podman => "podman",
|
||||
Self::Docker => "docker",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ContainerId {
|
||||
pub fn new<P, S>(cid_path: P, container_runtime: S, requires_sudo: bool) -> Self
|
||||
pub fn new<P>(cid_path: P, container_runtime: ContainerRuntime, requires_sudo: bool) -> Self
|
||||
where
|
||||
P: Into<PathBuf>,
|
||||
S: Into<String>,
|
||||
{
|
||||
let cid_path = cid_path.into();
|
||||
let crt = container_runtime.into();
|
||||
Self {
|
||||
cid_path,
|
||||
requires_sudo,
|
||||
crt,
|
||||
container_runtime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -97,13 +110,9 @@ where
|
|||
debug!("Killing container {id}");
|
||||
|
||||
let status = if cid.requires_sudo {
|
||||
Command::new("sudo")
|
||||
.arg(&cid.crt)
|
||||
.arg("stop")
|
||||
.arg(id)
|
||||
.status()
|
||||
cmd!("sudo", cid.container_runtime.to_string(), "stop", id).status()
|
||||
} else {
|
||||
Command::new(&cid.crt).arg("stop").arg(id).status()
|
||||
cmd!(cid.container_runtime.to_string(), "stop", id).status()
|
||||
};
|
||||
|
||||
if let Err(e) = status {
|
||||
|
|
@ -113,7 +122,7 @@ where
|
|||
});
|
||||
drop(cid_list);
|
||||
|
||||
process::exit(1);
|
||||
expect_exit::exit_unwind(1);
|
||||
}
|
||||
SIGTSTP => {
|
||||
if has_terminal {
|
||||
|
|
@ -11,7 +11,6 @@ license.workspace = true
|
|||
[dependencies]
|
||||
blue-build-utils = { version = "=0.8.12", path = "../utils" }
|
||||
|
||||
chrono.workspace = true
|
||||
colored.workspace = true
|
||||
log.workspace = true
|
||||
miette.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
use std::{borrow::Cow, env, fs, path::Path};
|
||||
use std::{borrow::Cow, fs, path::Path};
|
||||
|
||||
use blue_build_utils::constants::{
|
||||
CI_COMMIT_REF_NAME, CI_COMMIT_SHORT_SHA, CI_DEFAULT_BRANCH, CI_MERGE_REQUEST_IID,
|
||||
CI_PIPELINE_SOURCE, GITHUB_EVENT_NAME, GITHUB_REF_NAME, GITHUB_SHA, PR_EVENT_NUMBER,
|
||||
};
|
||||
use chrono::Local;
|
||||
use indexmap::IndexMap;
|
||||
use log::{debug, trace, warn};
|
||||
use log::{debug, trace};
|
||||
use miette::{Context, IntoDiagnostic, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Value;
|
||||
|
|
@ -83,116 +78,6 @@ pub struct Recipe<'a> {
|
|||
}
|
||||
|
||||
impl<'a> Recipe<'a> {
|
||||
/// Generate a list of tags based on the OS version.
|
||||
///
|
||||
/// ## CI
|
||||
/// The tags are generated based on the CI system that
|
||||
/// is detected. The general format for the default branch is:
|
||||
/// - `${os_version}`
|
||||
/// - `${timestamp}-${os_version}`
|
||||
///
|
||||
/// On a branch:
|
||||
/// - `br-${branch_name}-${os_version}`
|
||||
///
|
||||
/// In a PR(GitHub)/MR(GitLab)
|
||||
/// - `pr-${pr_event_number}-${os_version}`/`mr-${mr_iid}-${os_version}`
|
||||
///
|
||||
/// In all above cases the short git sha is also added:
|
||||
/// - `${commit_sha}-${os_version}`
|
||||
///
|
||||
/// When `alt_tags` are not present, the following tags are added:
|
||||
/// - `latest`
|
||||
/// - `${timestamp}`
|
||||
///
|
||||
/// ## Locally
|
||||
/// When ran locally, only a local tag is created:
|
||||
/// - `local-${os_version}`
|
||||
#[must_use]
|
||||
pub fn generate_tags(&self, os_version: u64) -> Vec<String> {
|
||||
trace!("Recipe::generate_tags()");
|
||||
trace!("Generating image tags for {}", &self.name);
|
||||
|
||||
let mut tags: Vec<String> = Vec::new();
|
||||
let timestamp = Local::now().format("%Y%m%d").to_string();
|
||||
|
||||
if let (Ok(commit_branch), Ok(default_branch), Ok(commit_sha), Ok(pipeline_source)) = (
|
||||
env::var(CI_COMMIT_REF_NAME),
|
||||
env::var(CI_DEFAULT_BRANCH),
|
||||
env::var(CI_COMMIT_SHORT_SHA),
|
||||
env::var(CI_PIPELINE_SOURCE),
|
||||
) {
|
||||
trace!("CI_COMMIT_REF_NAME={commit_branch}, CI_DEFAULT_BRANCH={default_branch},CI_COMMIT_SHORT_SHA={commit_sha}, CI_PIPELINE_SOURCE={pipeline_source}");
|
||||
warn!("Detected running in Gitlab, pulling information from CI variables");
|
||||
|
||||
if let Ok(mr_iid) = env::var(CI_MERGE_REQUEST_IID) {
|
||||
trace!("CI_MERGE_REQUEST_IID={mr_iid}");
|
||||
if pipeline_source == "merge_request_event" {
|
||||
debug!("Running in a MR");
|
||||
tags.push(format!("mr-{mr_iid}-{os_version}"));
|
||||
}
|
||||
}
|
||||
|
||||
if default_branch == commit_branch {
|
||||
debug!("Running on the default branch");
|
||||
tags.push(os_version.to_string());
|
||||
tags.push(format!("{timestamp}-{os_version}"));
|
||||
|
||||
if let Some(alt_tags) = self.alt_tags.as_ref() {
|
||||
tags.extend(alt_tags.iter().map(ToString::to_string));
|
||||
} else {
|
||||
tags.push("latest".into());
|
||||
tags.push(timestamp);
|
||||
}
|
||||
} else {
|
||||
debug!("Running on branch {commit_branch}");
|
||||
tags.push(format!("br-{commit_branch}-{os_version}"));
|
||||
}
|
||||
|
||||
tags.push(format!("{commit_sha}-{os_version}"));
|
||||
} else if let (
|
||||
Ok(github_event_name),
|
||||
Ok(github_event_number),
|
||||
Ok(github_sha),
|
||||
Ok(github_ref_name),
|
||||
) = (
|
||||
env::var(GITHUB_EVENT_NAME),
|
||||
env::var(PR_EVENT_NUMBER),
|
||||
env::var(GITHUB_SHA),
|
||||
env::var(GITHUB_REF_NAME),
|
||||
) {
|
||||
trace!("GITHUB_EVENT_NAME={github_event_name},PR_EVENT_NUMBER={github_event_number},GITHUB_SHA={github_sha},GITHUB_REF_NAME={github_ref_name}");
|
||||
warn!("Detected running in Github, pulling information from GITHUB variables");
|
||||
|
||||
let mut short_sha = github_sha;
|
||||
short_sha.truncate(7);
|
||||
|
||||
if github_event_name == "pull_request" {
|
||||
debug!("Running in a PR");
|
||||
tags.push(format!("pr-{github_event_number}-{os_version}"));
|
||||
} else if github_ref_name == "live" || github_ref_name == "main" {
|
||||
tags.push(os_version.to_string());
|
||||
tags.push(format!("{timestamp}-{os_version}"));
|
||||
|
||||
if let Some(alt_tags) = self.alt_tags.as_ref() {
|
||||
tags.extend(alt_tags.iter().map(ToString::to_string));
|
||||
} else {
|
||||
tags.push("latest".into());
|
||||
tags.push(timestamp);
|
||||
}
|
||||
} else {
|
||||
tags.push(format!("br-{github_ref_name}-{os_version}"));
|
||||
}
|
||||
tags.push(format!("{short_sha}-{os_version}"));
|
||||
} else {
|
||||
warn!("Running locally");
|
||||
tags.push(format!("local-{os_version}"));
|
||||
}
|
||||
debug!("Finished generating tags!");
|
||||
debug!("Tags: {tags:#?}");
|
||||
|
||||
tags.into_iter().map(|t| t.replace('/', "_")).collect()
|
||||
}
|
||||
|
||||
/// Parse a recipe file
|
||||
///
|
||||
/// # Errors
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use blue_build::commands::{BlueBuildArgs, BlueBuildCommand, CommandArgs};
|
||||
use blue_build_utils::{logging::Logger, signal_handler};
|
||||
use blue_build_process_management::{logging::Logger, signal_handler};
|
||||
use clap::Parser;
|
||||
use log::LevelFilter;
|
||||
|
||||
|
|
@ -8,7 +8,11 @@ fn main() {
|
|||
|
||||
Logger::new()
|
||||
.filter_level(args.verbosity.log_level_filter())
|
||||
.filter_modules([("hyper::proto", LevelFilter::Info)])
|
||||
.filter_modules([
|
||||
("hyper::proto", LevelFilter::Off),
|
||||
("hyper_util", LevelFilter::Off),
|
||||
("oci_distribution", LevelFilter::Off),
|
||||
])
|
||||
.log_out_dir(args.log_out.clone())
|
||||
.init();
|
||||
log::trace!("Parsed arguments: {args:#?}");
|
||||
|
|
@ -32,6 +36,9 @@ fn main() {
|
|||
#[cfg(not(feature = "switch"))]
|
||||
CommandArgs::Upgrade(mut command) => command.run(),
|
||||
|
||||
#[cfg(feature = "login")]
|
||||
CommandArgs::Login(mut command) => command.run(),
|
||||
|
||||
CommandArgs::BugReport(mut command) => command.run(),
|
||||
|
||||
CommandArgs::Completions(mut command) => command.run(),
|
||||
|
|
|
|||
|
|
@ -2,19 +2,17 @@ use std::path::PathBuf;
|
|||
|
||||
use log::error;
|
||||
|
||||
use clap::{command, crate_authors, Args, Parser, Subcommand};
|
||||
use clap::{command, crate_authors, Parser, Subcommand};
|
||||
use clap_verbosity_flag::{InfoLevel, Verbosity};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::{
|
||||
drivers::types::{BuildDriverType, InspectDriverType},
|
||||
shadow,
|
||||
};
|
||||
use crate::shadow;
|
||||
|
||||
pub mod bug_report;
|
||||
pub mod build;
|
||||
pub mod completions;
|
||||
pub mod generate;
|
||||
#[cfg(feature = "login")]
|
||||
pub mod login;
|
||||
// #[cfg(feature = "init")]
|
||||
// pub mod init;
|
||||
#[cfg(not(feature = "switch"))]
|
||||
|
|
@ -107,6 +105,10 @@ pub enum CommandArgs {
|
|||
#[cfg(feature = "switch")]
|
||||
Switch(switch::SwitchCommand),
|
||||
|
||||
/// Login to all services used for building.
|
||||
#[cfg(feature = "login")]
|
||||
Login(login::LoginCommand),
|
||||
|
||||
// /// Initialize a new Ublue Starting Point repo
|
||||
// #[cfg(feature = "init")]
|
||||
// Init(init::InitCommand),
|
||||
|
|
@ -120,21 +122,6 @@ pub enum CommandArgs {
|
|||
Completions(completions::CompletionsCommand),
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug, TypedBuilder, Args)]
|
||||
pub struct DriverArgs {
|
||||
/// Select which driver to use to build
|
||||
/// your image.
|
||||
#[builder(default)]
|
||||
#[arg(short = 'B', long)]
|
||||
build_driver: Option<BuildDriverType>,
|
||||
|
||||
/// Select which driver to use to inspect
|
||||
/// images.
|
||||
#[builder(default)]
|
||||
#[arg(short = 'I', long)]
|
||||
inspect_driver: Option<InspectDriverType>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use clap::CommandFactory;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
use std::{
|
||||
env, fs,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use blue_build_process_management::drivers::{
|
||||
opts::{BuildTagPushOpts, CheckKeyPairOpts, CompressionType},
|
||||
BuildDriver, CiDriver, Driver, DriverArgs, SigningDriver,
|
||||
};
|
||||
use blue_build_recipe::Recipe;
|
||||
use blue_build_utils::{
|
||||
constants::{
|
||||
ARCHIVE_SUFFIX, BB_PASSWORD, BB_REGISTRY, BB_REGISTRY_NAMESPACE, BB_USERNAME,
|
||||
BUILD_ID_LABEL, CI_DEFAULT_BRANCH, CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_PROJECT_URL,
|
||||
CI_REGISTRY, CI_SERVER_HOST, CI_SERVER_PROTOCOL, CONFIG_PATH, CONTAINER_FILE,
|
||||
COSIGN_PRIVATE_KEY, COSIGN_PRIV_PATH, COSIGN_PUB_PATH, GITHUB_REPOSITORY_OWNER,
|
||||
GITHUB_TOKEN, GITHUB_TOKEN_ISSUER_URL, GITHUB_WORKFLOW_REF, GITIGNORE_PATH,
|
||||
LABELED_ERROR_MESSAGE, NO_LABEL_ERROR_MESSAGE, RECIPE_FILE, RECIPE_PATH, SIGSTORE_ID_TOKEN,
|
||||
ARCHIVE_SUFFIX, BB_REGISTRY_NAMESPACE, BUILD_ID_LABEL, CONFIG_PATH, CONTAINER_FILE,
|
||||
GITIGNORE_PATH, LABELED_ERROR_MESSAGE, NO_LABEL_ERROR_MESSAGE, RECIPE_FILE, RECIPE_PATH,
|
||||
},
|
||||
generate_containerfile_path,
|
||||
credentials::{Credentials, CredentialsArgs},
|
||||
};
|
||||
use clap::Args;
|
||||
use colored::Colorize;
|
||||
|
|
@ -22,16 +21,9 @@ use log::{debug, info, trace, warn};
|
|||
use miette::{bail, Context, IntoDiagnostic, Result};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::{
|
||||
commands::generate::GenerateCommand,
|
||||
credentials::{self, Credentials},
|
||||
drivers::{
|
||||
opts::{BuildTagPushOpts, CompressionType, GetMetadataOpts},
|
||||
Driver,
|
||||
},
|
||||
};
|
||||
use crate::commands::generate::GenerateCommand;
|
||||
|
||||
use super::{BlueBuildCommand, DriverArgs};
|
||||
use super::BlueBuildCommand;
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone, Args, TypedBuilder)]
|
||||
|
|
@ -88,11 +80,6 @@ pub struct BuildCommand {
|
|||
#[builder(default, setter(into, strip_option))]
|
||||
archive: Option<PathBuf>,
|
||||
|
||||
/// The registry's domain name.
|
||||
#[arg(long, env = BB_REGISTRY)]
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
registry: Option<String>,
|
||||
|
||||
/// The url path to your base
|
||||
/// project images.
|
||||
#[arg(long, env = BB_REGISTRY_NAMESPACE)]
|
||||
|
|
@ -100,18 +87,6 @@ pub struct BuildCommand {
|
|||
#[arg(visible_alias("registry-path"))]
|
||||
registry_namespace: Option<String>,
|
||||
|
||||
/// The username to login to the
|
||||
/// container registry.
|
||||
#[arg(short = 'U', long, env = BB_USERNAME, hide_env_values = true)]
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
username: Option<String>,
|
||||
|
||||
/// The password to login to the
|
||||
/// container registry.
|
||||
#[arg(short = 'P', long, env = BB_PASSWORD, hide_env_values = true)]
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
password: Option<String>,
|
||||
|
||||
/// Do not sign the image on push.
|
||||
#[arg(long)]
|
||||
#[builder(default)]
|
||||
|
|
@ -128,6 +103,10 @@ pub struct BuildCommand {
|
|||
#[builder(default)]
|
||||
squash: bool,
|
||||
|
||||
#[clap(flatten)]
|
||||
#[builder(default)]
|
||||
credentials: CredentialsArgs,
|
||||
|
||||
#[clap(flatten)]
|
||||
#[builder(default)]
|
||||
drivers: DriverArgs,
|
||||
|
|
@ -138,14 +117,9 @@ impl BlueBuildCommand for BuildCommand {
|
|||
fn try_run(&mut self) -> Result<()> {
|
||||
trace!("BuildCommand::try_run()");
|
||||
|
||||
Driver::builder()
|
||||
.username(self.username.as_ref())
|
||||
.password(self.password.as_ref())
|
||||
.registry(self.registry.as_ref())
|
||||
.build_driver(self.drivers.build_driver)
|
||||
.inspect_driver(self.drivers.inspect_driver)
|
||||
.build()
|
||||
.init();
|
||||
Driver::init(self.drivers);
|
||||
|
||||
Credentials::init(self.credentials.clone());
|
||||
|
||||
self.update_gitignore()?;
|
||||
|
||||
|
|
@ -155,8 +129,9 @@ impl BlueBuildCommand for BuildCommand {
|
|||
|
||||
if self.push {
|
||||
blue_build_utils::check_command_exists("cosign")?;
|
||||
self.check_cosign_files()?;
|
||||
Self::login()?;
|
||||
Driver::check_signing_files(&CheckKeyPairOpts::builder().dir(Path::new(".")).build())?;
|
||||
Driver::login()?;
|
||||
Driver::signing_login()?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "multi-recipe")]
|
||||
|
|
@ -180,7 +155,11 @@ impl BlueBuildCommand for BuildCommand {
|
|||
|
||||
recipe_paths.par_iter().try_for_each(|recipe| {
|
||||
GenerateCommand::builder()
|
||||
.output(generate_containerfile_path(recipe)?)
|
||||
.output(if recipe_paths.len() > 1 {
|
||||
blue_build_utils::generate_containerfile_path(recipe)?
|
||||
} else {
|
||||
PathBuf::from(CONTAINER_FILE)
|
||||
})
|
||||
.recipe(recipe)
|
||||
.drivers(self.drivers)
|
||||
.build()
|
||||
|
|
@ -204,7 +183,7 @@ impl BlueBuildCommand for BuildCommand {
|
|||
});
|
||||
|
||||
GenerateCommand::builder()
|
||||
.output(generate_containerfile_path(&recipe_path)?)
|
||||
.output(CONTAINER_FILE)
|
||||
.recipe(&recipe_path)
|
||||
.drivers(self.drivers)
|
||||
.build()
|
||||
|
|
@ -218,16 +197,21 @@ impl BlueBuildCommand for BuildCommand {
|
|||
impl BuildCommand {
|
||||
#[cfg(feature = "multi-recipe")]
|
||||
fn start(&self, recipe_paths: &[PathBuf]) -> Result<()> {
|
||||
use blue_build_process_management::drivers::opts::SignVerifyOpts;
|
||||
use rayon::prelude::*;
|
||||
|
||||
trace!("BuildCommand::build_image()");
|
||||
|
||||
recipe_paths
|
||||
.par_iter()
|
||||
.try_for_each(|recipe_path| -> Result<()> {
|
||||
let recipe = Recipe::parse(recipe_path)?;
|
||||
let os_version = Driver::get_os_version(&recipe)?;
|
||||
let containerfile = generate_containerfile_path(recipe_path)?;
|
||||
let tags = recipe.generate_tags(os_version);
|
||||
let containerfile = if recipe_paths.len() > 1 {
|
||||
blue_build_utils::generate_containerfile_path(recipe_path)?
|
||||
} else {
|
||||
PathBuf::from(CONTAINER_FILE)
|
||||
};
|
||||
let tags = Driver::generate_tags(&recipe)?;
|
||||
let image_name = self.generate_full_image_name(&recipe)?;
|
||||
|
||||
let opts = if let Some(archive_dir) = self.archive.as_ref() {
|
||||
|
|
@ -244,7 +228,7 @@ impl BuildCommand {
|
|||
BuildTagPushOpts::builder()
|
||||
.image(&image_name)
|
||||
.containerfile(&containerfile)
|
||||
.tags(tags.iter().map(String::as_str).collect::<Vec<_>>())
|
||||
.tags(&tags)
|
||||
.push(self.push)
|
||||
.retry_push(self.retry_push)
|
||||
.retry_count(self.retry_count)
|
||||
|
|
@ -253,10 +237,19 @@ impl BuildCommand {
|
|||
.build()
|
||||
};
|
||||
|
||||
Driver::get_build_driver().build_tag_push(&opts)?;
|
||||
Driver::build_tag_push(&opts)?;
|
||||
|
||||
if self.push && !self.no_sign {
|
||||
sign_images(&image_name, tags.first().map(String::as_str))?;
|
||||
let opts = SignVerifyOpts::builder()
|
||||
.image(&image_name)
|
||||
.retry_push(self.retry_push)
|
||||
.retry_count(self.retry_count);
|
||||
let opts = if let Some(tag) = tags.first() {
|
||||
opts.tag(tag).build()
|
||||
} else {
|
||||
opts.build()
|
||||
};
|
||||
Driver::sign_and_verify(&opts)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -268,12 +261,13 @@ impl BuildCommand {
|
|||
|
||||
#[cfg(not(feature = "multi-recipe"))]
|
||||
fn start(&self, recipe_path: &Path) -> Result<()> {
|
||||
use blue_build_process_management::drivers::opts::SignVerifyOpts;
|
||||
|
||||
trace!("BuildCommand::start()");
|
||||
|
||||
let recipe = Recipe::parse(recipe_path)?;
|
||||
let os_version = Driver::get_os_version(&recipe)?;
|
||||
let containerfile = generate_containerfile_path(recipe_path)?;
|
||||
let tags = recipe.generate_tags(os_version);
|
||||
let containerfile = PathBuf::from(CONTAINER_FILE);
|
||||
let tags = Driver::generate_tags(&recipe)?;
|
||||
let image_name = self.generate_full_image_name(&recipe)?;
|
||||
|
||||
let opts = if let Some(archive_dir) = self.archive.as_ref() {
|
||||
|
|
@ -290,7 +284,7 @@ impl BuildCommand {
|
|||
BuildTagPushOpts::builder()
|
||||
.image(&image_name)
|
||||
.containerfile(&containerfile)
|
||||
.tags(tags.iter().map(String::as_str).collect::<Vec<_>>())
|
||||
.tags(&tags)
|
||||
.push(self.push)
|
||||
.retry_push(self.retry_push)
|
||||
.retry_count(self.retry_count)
|
||||
|
|
@ -299,105 +293,45 @@ impl BuildCommand {
|
|||
.build()
|
||||
};
|
||||
|
||||
Driver::get_build_driver().build_tag_push(&opts)?;
|
||||
Driver::build_tag_push(&opts)?;
|
||||
|
||||
if self.push && !self.no_sign {
|
||||
sign_images(&image_name, tags.first().map(String::as_str))?;
|
||||
let opts = SignVerifyOpts::builder()
|
||||
.image(&image_name)
|
||||
.retry_push(self.retry_push)
|
||||
.retry_count(self.retry_count);
|
||||
let opts = if let Some(tag) = tags.first() {
|
||||
opts.tag(tag).build()
|
||||
} else {
|
||||
opts.build()
|
||||
};
|
||||
Driver::sign_and_verify(&opts)?;
|
||||
}
|
||||
|
||||
info!("Build complete!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn login() -> Result<()> {
|
||||
trace!("BuildCommand::login()");
|
||||
info!("Attempting to login to the registry");
|
||||
|
||||
if let Some(Credentials {
|
||||
registry,
|
||||
username,
|
||||
password,
|
||||
}) = credentials::get()
|
||||
{
|
||||
info!("Logging into the registry, {registry}");
|
||||
Driver::get_build_driver().login()?;
|
||||
|
||||
trace!("cosign login -u {username} -p [MASKED] {registry}");
|
||||
let login_output = Command::new("cosign")
|
||||
.arg("login")
|
||||
.arg("-u")
|
||||
.arg(username)
|
||||
.arg("-p")
|
||||
.arg(password)
|
||||
.arg(registry)
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
|
||||
if !login_output.status.success() {
|
||||
let err_output = String::from_utf8_lossy(&login_output.stderr);
|
||||
bail!("Failed to login for cosign: {err_output}");
|
||||
}
|
||||
info!("Login success at {registry}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if the image name cannot be generated.
|
||||
pub fn generate_full_image_name(&self, recipe: &Recipe) -> Result<String> {
|
||||
fn generate_full_image_name(&self, recipe: &Recipe) -> Result<String> {
|
||||
trace!("BuildCommand::generate_full_image_name({recipe:#?})");
|
||||
info!("Generating full image name");
|
||||
|
||||
let image_name = match (
|
||||
env::var(CI_REGISTRY).ok().map(|s| s.to_lowercase()),
|
||||
env::var(CI_PROJECT_NAMESPACE)
|
||||
.ok()
|
||||
.map(|s| s.to_lowercase()),
|
||||
env::var(CI_PROJECT_NAME).ok().map(|s| s.to_lowercase()),
|
||||
env::var(GITHUB_REPOSITORY_OWNER)
|
||||
.ok()
|
||||
.map(|s| s.to_lowercase()),
|
||||
self.registry.as_ref().map(|s| s.to_lowercase()),
|
||||
let image_name = if let (Some(registry), Some(registry_path)) = (
|
||||
self.credentials.registry.as_ref().map(|r| r.to_lowercase()),
|
||||
self.registry_namespace.as_ref().map(|s| s.to_lowercase()),
|
||||
) {
|
||||
(_, _, _, _, Some(registry), Some(registry_path)) => {
|
||||
trace!("registry={registry}, registry_path={registry_path}");
|
||||
format!(
|
||||
"{}/{}/{}",
|
||||
registry.trim().trim_matches('/'),
|
||||
registry_path.trim().trim_matches('/'),
|
||||
recipe.name.trim(),
|
||||
)
|
||||
}
|
||||
(
|
||||
Some(ci_registry),
|
||||
Some(ci_project_namespace),
|
||||
Some(ci_project_name),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
) => {
|
||||
trace!("CI_REGISTRY={ci_registry}, CI_PROJECT_NAMESPACE={ci_project_namespace}, CI_PROJECT_NAME={ci_project_name}");
|
||||
warn!("Generating Gitlab Registry image");
|
||||
format!(
|
||||
"{ci_registry}/{ci_project_namespace}/{ci_project_name}/{}",
|
||||
recipe.name.trim().to_lowercase()
|
||||
)
|
||||
}
|
||||
(None, None, None, Some(github_repository_owner), None, None) => {
|
||||
trace!("GITHUB_REPOSITORY_OWNER={github_repository_owner}");
|
||||
warn!("Generating Github Registry image");
|
||||
format!("ghcr.io/{github_repository_owner}/{}", &recipe.name)
|
||||
}
|
||||
_ => {
|
||||
trace!("Nothing to indicate an image name with a registry");
|
||||
if self.push {
|
||||
bail!("Need '--registry' and '--registry-namespace' in order to push image");
|
||||
}
|
||||
recipe.name.trim().to_lowercase()
|
||||
}
|
||||
trace!("registry={registry}, registry_path={registry_path}");
|
||||
format!(
|
||||
"{}/{}/{}",
|
||||
registry.trim().trim_matches('/'),
|
||||
registry_path.trim().trim_matches('/'),
|
||||
recipe.name.trim(),
|
||||
)
|
||||
} else {
|
||||
Driver::generate_image_name(recipe)?
|
||||
};
|
||||
|
||||
debug!("Using image name '{image_name}'");
|
||||
|
|
@ -478,296 +412,4 @@ impl BuildCommand {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
/// Checks the cosign private/public key pair to ensure they match.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if it's unable to verify key pairs.
|
||||
fn check_cosign_files(&self) -> Result<()> {
|
||||
trace!("check_for_cosign_files()");
|
||||
|
||||
if self.no_sign {
|
||||
Ok(())
|
||||
} else {
|
||||
env::set_var("COSIGN_PASSWORD", "");
|
||||
env::set_var("COSIGN_YES", "true");
|
||||
|
||||
match (
|
||||
env::var(COSIGN_PRIVATE_KEY).ok(),
|
||||
Path::new(COSIGN_PRIV_PATH),
|
||||
) {
|
||||
(Some(cosign_priv_key), _)
|
||||
if !cosign_priv_key.is_empty() && Path::new(COSIGN_PUB_PATH).exists() =>
|
||||
{
|
||||
trace!("cosign public-key --key env://COSIGN_PRIVATE_KEY");
|
||||
let output = Command::new("cosign")
|
||||
.arg("public-key")
|
||||
.arg("--key=env://COSIGN_PRIVATE_KEY")
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"Failed to run cosign public-key: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
let calculated_pub_key = String::from_utf8(output.stdout).into_diagnostic()?;
|
||||
let found_pub_key = fs::read_to_string(COSIGN_PUB_PATH)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to read {COSIGN_PUB_PATH}"))?;
|
||||
trace!("calculated_pub_key={calculated_pub_key},found_pub_key={found_pub_key}");
|
||||
|
||||
if calculated_pub_key.trim() == found_pub_key.trim() {
|
||||
debug!("Cosign files match, continuing build");
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Public key '{COSIGN_PUB_PATH}' does not match private key")
|
||||
}
|
||||
}
|
||||
(None, cosign_priv_key_path) if cosign_priv_key_path.exists() => {
|
||||
trace!("cosign public-key --key {COSIGN_PRIV_PATH}");
|
||||
let output = Command::new("cosign")
|
||||
.arg("public-key")
|
||||
.arg(format!("--key={COSIGN_PRIV_PATH}"))
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"Failed to run cosign public-key: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
let calculated_pub_key = String::from_utf8(output.stdout).into_diagnostic()?;
|
||||
let found_pub_key = fs::read_to_string(COSIGN_PUB_PATH)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to read {COSIGN_PUB_PATH}"))?;
|
||||
trace!("calculated_pub_key={calculated_pub_key},found_pub_key={found_pub_key}");
|
||||
|
||||
if calculated_pub_key.trim() == found_pub_key.trim() {
|
||||
debug!("Cosign files match, continuing build");
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Public key '{COSIGN_PUB_PATH}' does not match private key")
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
bail!("Unable to find private/public key pair.\n\nMake sure you have a `{COSIGN_PUB_PATH}` in the root of your repo and have either {COSIGN_PRIVATE_KEY} set in your env variables or a `{COSIGN_PRIV_PATH}` file in the root of your repo.\n\nSee https://blue-build.org/how-to/cosign/ for more information.\n\nIf you don't want to sign your image, use the `--no-sign` flag.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================================== //
|
||||
// ========================= Helpers ====================== //
|
||||
// ======================================================== //
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn sign_images(image_name: &str, tag: Option<&str>) -> Result<()> {
|
||||
trace!("BuildCommand::sign_images({image_name}, {tag:?})");
|
||||
|
||||
env::set_var("COSIGN_PASSWORD", "");
|
||||
env::set_var("COSIGN_YES", "true");
|
||||
|
||||
let inspect_opts = GetMetadataOpts::builder().image(image_name);
|
||||
|
||||
let inspect_opts = if let Some(tag) = tag {
|
||||
inspect_opts.tag(tag).build()
|
||||
} else {
|
||||
inspect_opts.build()
|
||||
};
|
||||
|
||||
let image_digest = Driver::get_inspection_driver()
|
||||
.get_metadata(&inspect_opts)?
|
||||
.digest;
|
||||
let image_name_digest = format!("{image_name}@{image_digest}");
|
||||
let image_name_tag = tag.map_or_else(|| image_name.to_owned(), |t| format!("{image_name}:{t}"));
|
||||
|
||||
match (
|
||||
// GitLab specific vars
|
||||
env::var(CI_DEFAULT_BRANCH),
|
||||
env::var(CI_PROJECT_URL),
|
||||
env::var(CI_SERVER_PROTOCOL),
|
||||
env::var(CI_SERVER_HOST),
|
||||
env::var(SIGSTORE_ID_TOKEN),
|
||||
// GitHub specific vars
|
||||
env::var(GITHUB_TOKEN),
|
||||
env::var(GITHUB_WORKFLOW_REF),
|
||||
// Cosign public/private key pair
|
||||
env::var(COSIGN_PRIVATE_KEY),
|
||||
Path::new(COSIGN_PRIV_PATH),
|
||||
) {
|
||||
// Cosign public/private key pair
|
||||
(_, _, _, _, _, _, _, Ok(cosign_private_key), _)
|
||||
if !cosign_private_key.is_empty() && Path::new(COSIGN_PUB_PATH).exists() =>
|
||||
{
|
||||
sign_priv_public_pair_env(&image_name_digest, &image_name_tag)?;
|
||||
}
|
||||
(_, _, _, _, _, _, _, _, cosign_priv_key_path) if cosign_priv_key_path.exists() => {
|
||||
sign_priv_public_pair_file(&image_name_digest, &image_name_tag)?;
|
||||
}
|
||||
// Gitlab keyless
|
||||
(
|
||||
Ok(ci_default_branch),
|
||||
Ok(ci_project_url),
|
||||
Ok(ci_server_protocol),
|
||||
Ok(ci_server_host),
|
||||
Ok(_),
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
) => {
|
||||
trace!("CI_PROJECT_URL={ci_project_url}, CI_DEFAULT_BRANCH={ci_default_branch}, CI_SERVER_PROTOCOL={ci_server_protocol}, CI_SERVER_HOST={ci_server_host}");
|
||||
|
||||
info!("Signing image: {image_name_digest}");
|
||||
|
||||
trace!("cosign sign {image_name_digest}");
|
||||
|
||||
if Command::new("cosign")
|
||||
.arg("sign")
|
||||
.arg("--recursive")
|
||||
.arg(&image_name_digest)
|
||||
.status()
|
||||
.into_diagnostic()?
|
||||
.success()
|
||||
{
|
||||
info!("Successfully signed image!");
|
||||
} else {
|
||||
bail!("Failed to sign image: {image_name_digest}");
|
||||
}
|
||||
|
||||
let cert_ident =
|
||||
format!("{ci_project_url}//.gitlab-ci.yml@refs/heads/{ci_default_branch}");
|
||||
|
||||
let cert_oidc = format!("{ci_server_protocol}://{ci_server_host}");
|
||||
|
||||
trace!("cosign verify --certificate-identity {cert_ident} --certificate-oidc-issuer {cert_oidc} {image_name_tag}");
|
||||
|
||||
if !Command::new("cosign")
|
||||
.arg("verify")
|
||||
.arg("--certificate-identity")
|
||||
.arg(&cert_ident)
|
||||
.arg("--certificate-oidc-issuer")
|
||||
.arg(&cert_oidc)
|
||||
.arg(&image_name_tag)
|
||||
.status()
|
||||
.into_diagnostic()?
|
||||
.success()
|
||||
{
|
||||
bail!("Failed to verify image!");
|
||||
}
|
||||
}
|
||||
// GitHub keyless
|
||||
(_, _, _, _, _, Ok(_), Ok(github_worflow_ref), _, _) => {
|
||||
trace!("GITHUB_WORKFLOW_REF={github_worflow_ref}");
|
||||
|
||||
info!("Signing image {image_name_digest}");
|
||||
|
||||
trace!("cosign sign {image_name_digest}");
|
||||
if Command::new("cosign")
|
||||
.arg("sign")
|
||||
.arg("--recursive")
|
||||
.arg(&image_name_digest)
|
||||
.status()
|
||||
.into_diagnostic()?
|
||||
.success()
|
||||
{
|
||||
info!("Successfully signed image!");
|
||||
} else {
|
||||
bail!("Failed to sign image: {image_name_digest}");
|
||||
}
|
||||
|
||||
trace!("cosign verify --certificate-identity-regexp {github_worflow_ref} --certificate-oidc-issuer {GITHUB_TOKEN_ISSUER_URL} {image_name_tag}");
|
||||
if !Command::new("cosign")
|
||||
.arg("verify")
|
||||
.arg("--certificate-identity-regexp")
|
||||
.arg(&github_worflow_ref)
|
||||
.arg("--certificate-oidc-issuer")
|
||||
.arg(GITHUB_TOKEN_ISSUER_URL)
|
||||
.arg(&image_name_tag)
|
||||
.status()
|
||||
.into_diagnostic()?
|
||||
.success()
|
||||
{
|
||||
bail!("Failed to verify image!");
|
||||
}
|
||||
}
|
||||
_ => warn!("Not running in CI with cosign variables, not signing"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sign_priv_public_pair_env(image_digest: &str, image_name_tag: &str) -> Result<()> {
|
||||
info!("Signing image: {image_digest}");
|
||||
|
||||
trace!("cosign sign --key=env://{COSIGN_PRIVATE_KEY} {image_digest}");
|
||||
|
||||
if Command::new("cosign")
|
||||
.arg("sign")
|
||||
.arg("--key=env://COSIGN_PRIVATE_KEY")
|
||||
.arg("--recursive")
|
||||
.arg(image_digest)
|
||||
.status()
|
||||
.into_diagnostic()?
|
||||
.success()
|
||||
{
|
||||
info!("Successfully signed image!");
|
||||
} else {
|
||||
bail!("Failed to sign image: {image_digest}");
|
||||
}
|
||||
|
||||
trace!("cosign verify --key {COSIGN_PUB_PATH} {image_name_tag}");
|
||||
|
||||
if !Command::new("cosign")
|
||||
.arg("verify")
|
||||
.arg(format!("--key={COSIGN_PUB_PATH}"))
|
||||
.arg(image_name_tag)
|
||||
.status()
|
||||
.into_diagnostic()?
|
||||
.success()
|
||||
{
|
||||
bail!("Failed to verify image!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sign_priv_public_pair_file(image_digest: &str, image_name_tag: &str) -> Result<()> {
|
||||
info!("Signing image: {image_digest}");
|
||||
|
||||
trace!("cosign sign --key={COSIGN_PRIV_PATH} {image_digest}");
|
||||
|
||||
if Command::new("cosign")
|
||||
.arg("sign")
|
||||
.arg(format!("--key={COSIGN_PRIV_PATH}"))
|
||||
.arg("--recursive")
|
||||
.arg(image_digest)
|
||||
.status()
|
||||
.into_diagnostic()?
|
||||
.success()
|
||||
{
|
||||
info!("Successfully signed image!");
|
||||
} else {
|
||||
bail!("Failed to sign image: {image_digest}");
|
||||
}
|
||||
|
||||
trace!("cosign verify --key {COSIGN_PUB_PATH} {image_name_tag}");
|
||||
|
||||
if !Command::new("cosign")
|
||||
.arg("verify")
|
||||
.arg(format!("--key={COSIGN_PUB_PATH}"))
|
||||
.arg(image_name_tag)
|
||||
.status()
|
||||
.into_diagnostic()?
|
||||
.success()
|
||||
{
|
||||
bail!("Failed to verify image!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,11 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use blue_build_process_management::drivers::{CiDriver, Driver, DriverArgs};
|
||||
use blue_build_recipe::Recipe;
|
||||
use blue_build_template::{ContainerFileTemplate, Template};
|
||||
use blue_build_utils::{
|
||||
constants::{
|
||||
CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_REGISTRY, CONFIG_PATH, GITHUB_REPOSITORY_OWNER,
|
||||
RECIPE_FILE, RECIPE_PATH,
|
||||
},
|
||||
constants::{CONFIG_PATH, RECIPE_FILE, RECIPE_PATH},
|
||||
syntax_highlighting::{self, DefaultThemes},
|
||||
};
|
||||
use clap::{crate_version, Args};
|
||||
|
|
@ -17,9 +15,9 @@ use log::{debug, info, trace, warn};
|
|||
use miette::{IntoDiagnostic, Result};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::{drivers::Driver, shadow};
|
||||
use crate::shadow;
|
||||
|
||||
use super::{BlueBuildCommand, DriverArgs};
|
||||
use super::BlueBuildCommand;
|
||||
|
||||
#[derive(Debug, Clone, Args, TypedBuilder)]
|
||||
pub struct GenerateCommand {
|
||||
|
|
@ -73,11 +71,7 @@ pub struct GenerateCommand {
|
|||
|
||||
impl BlueBuildCommand for GenerateCommand {
|
||||
fn try_run(&mut self) -> Result<()> {
|
||||
Driver::builder()
|
||||
.build_driver(self.drivers.build_driver)
|
||||
.inspect_driver(self.drivers.inspect_driver)
|
||||
.build()
|
||||
.init();
|
||||
Driver::init(self.drivers);
|
||||
|
||||
self.template_file()
|
||||
}
|
||||
|
|
@ -119,7 +113,8 @@ impl GenerateCommand {
|
|||
.build_id(Driver::get_build_id())
|
||||
.recipe(&recipe_de)
|
||||
.recipe_path(recipe_path.as_path())
|
||||
.registry(self.get_registry())
|
||||
.registry(Driver::get_registry()?)
|
||||
.repo(Driver::get_repo_url()?)
|
||||
.exports_tag({
|
||||
#[allow(clippy::const_is_empty)]
|
||||
if shadow::COMMIT_HASH.is_empty() {
|
||||
|
|
@ -146,37 +141,6 @@ impl GenerateCommand {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_registry(&self) -> String {
|
||||
match (
|
||||
self.registry.as_ref(),
|
||||
self.registry_namespace.as_ref(),
|
||||
Self::get_github_repo_owner(),
|
||||
Self::get_gitlab_registry_path(),
|
||||
) {
|
||||
(Some(r), Some(rn), _, _) => format!("{r}/{rn}"),
|
||||
(Some(r), None, _, _) => r.to_string(),
|
||||
(None, None, Some(gh_repo_owner), None) => format!("ghcr.io/{gh_repo_owner}"),
|
||||
(None, None, None, Some(gl_reg_path)) => gl_reg_path,
|
||||
_ => "localhost".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_github_repo_owner() -> Option<String> {
|
||||
Some(env::var(GITHUB_REPOSITORY_OWNER).ok()?.to_lowercase())
|
||||
}
|
||||
|
||||
fn get_gitlab_registry_path() -> Option<String> {
|
||||
Some(
|
||||
format!(
|
||||
"{}/{}/{}",
|
||||
env::var(CI_REGISTRY).ok()?,
|
||||
env::var(CI_PROJECT_NAMESPACE).ok()?,
|
||||
env::var(CI_PROJECT_NAME).ok()?,
|
||||
)
|
||||
.to_lowercase(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================================== //
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use blue_build_process_management::drivers::DriverArgs;
|
||||
use blue_build_recipe::Recipe;
|
||||
use blue_build_utils::constants::{ARCHIVE_SUFFIX, LOCAL_BUILD};
|
||||
use blue_build_utils::{
|
||||
cmd,
|
||||
constants::{ARCHIVE_SUFFIX, LOCAL_BUILD},
|
||||
};
|
||||
use clap::Args;
|
||||
use log::{debug, info, trace};
|
||||
use miette::{bail, IntoDiagnostic, Result};
|
||||
|
|
@ -14,7 +17,7 @@ use users::{Users, UsersCache};
|
|||
|
||||
use crate::commands::build::BuildCommand;
|
||||
|
||||
use super::{BlueBuildCommand, DriverArgs};
|
||||
use super::BlueBuildCommand;
|
||||
|
||||
#[derive(Default, Clone, Debug, TypedBuilder, Args)]
|
||||
pub struct LocalCommonArgs {
|
||||
|
|
@ -81,19 +84,14 @@ impl BlueBuildCommand for UpgradeCommand {
|
|||
info!("Upgrading image {image_name} and rebooting");
|
||||
|
||||
trace!("rpm-ostree upgrade --reboot");
|
||||
Command::new("rpm-ostree")
|
||||
.arg("upgrade")
|
||||
.arg("--reboot")
|
||||
cmd!("rpm-ostree", "upgrade", "--reboot")
|
||||
.status()
|
||||
.into_diagnostic()?
|
||||
} else {
|
||||
info!("Upgrading image {image_name}");
|
||||
|
||||
trace!("rpm-ostree upgrade");
|
||||
Command::new("rpm-ostree")
|
||||
.arg("upgrade")
|
||||
.status()
|
||||
.into_diagnostic()?
|
||||
cmd!("rpm-ostree", "upgrade").status().into_diagnostic()?
|
||||
};
|
||||
|
||||
if status.success() {
|
||||
|
|
@ -146,19 +144,14 @@ impl BlueBuildCommand for RebaseCommand {
|
|||
info!("Rebasing image {image_name} and rebooting");
|
||||
|
||||
trace!("rpm-ostree rebase --reboot {rebase_url}");
|
||||
Command::new("rpm-ostree")
|
||||
.arg("rebase")
|
||||
.arg("--reboot")
|
||||
.arg(rebase_url)
|
||||
cmd!("rpm-ostree", "rebase", "--reboot", rebase_url)
|
||||
.status()
|
||||
.into_diagnostic()?
|
||||
} else {
|
||||
info!("Rebasing image {image_name}");
|
||||
|
||||
trace!("rpm-ostree rebase {rebase_url}");
|
||||
Command::new("rpm-ostree")
|
||||
.arg("rebase")
|
||||
.arg(rebase_url)
|
||||
cmd!("rpm-ostree", "rebase", rebase_url)
|
||||
.status()
|
||||
.into_diagnostic()?
|
||||
};
|
||||
|
|
|
|||
105
src/commands/login.rs
Normal file
105
src/commands/login.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
use std::io::{self, Read};
|
||||
|
||||
use blue_build_process_management::drivers::{BuildDriver, Driver, DriverArgs, SigningDriver};
|
||||
use blue_build_utils::credentials::{Credentials, CredentialsArgs};
|
||||
use clap::Args;
|
||||
use miette::{bail, IntoDiagnostic, Result};
|
||||
use requestty::questions;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use super::BlueBuildCommand;
|
||||
|
||||
#[derive(Debug, Clone, Args, TypedBuilder)]
|
||||
pub struct LoginCommand {
|
||||
/// The server to login to.
|
||||
server: String,
|
||||
|
||||
/// The password to login with.
|
||||
///
|
||||
/// Cannont be used with `--password-stdin`.
|
||||
#[arg(group = "pass", long, short)]
|
||||
password: Option<String>,
|
||||
|
||||
/// Read password from stdin,
|
||||
///
|
||||
/// Cannot be used with `--password/-p`
|
||||
#[arg(group = "pass", long)]
|
||||
password_stdin: bool,
|
||||
|
||||
/// The username to login with
|
||||
#[arg(long, short)]
|
||||
username: Option<String>,
|
||||
|
||||
#[clap(flatten)]
|
||||
#[builder(default)]
|
||||
drivers: DriverArgs,
|
||||
}
|
||||
|
||||
impl BlueBuildCommand for LoginCommand {
|
||||
fn try_run(&mut self) -> miette::Result<()> {
|
||||
Driver::init(self.drivers);
|
||||
|
||||
Credentials::init(
|
||||
CredentialsArgs::builder()
|
||||
.registry(&self.server)
|
||||
.username(self.get_username()?)
|
||||
.password(self.get_password()?)
|
||||
.build(),
|
||||
);
|
||||
|
||||
Driver::login()?;
|
||||
Driver::signing_login()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl LoginCommand {
|
||||
fn get_username(&self) -> Result<String> {
|
||||
Ok(if let Some(ref username) = self.username {
|
||||
username.clone()
|
||||
} else if !self.password_stdin {
|
||||
let questions = questions! [ inline
|
||||
Input {
|
||||
name: "username",
|
||||
},
|
||||
];
|
||||
|
||||
requestty::prompt(questions)
|
||||
.into_diagnostic()?
|
||||
.get("username")
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
} else {
|
||||
bail!("Cannot prompt for username when using `--password-stdin`");
|
||||
})
|
||||
}
|
||||
|
||||
fn get_password(&self) -> Result<String> {
|
||||
Ok(if let Some(ref password) = self.password {
|
||||
password.clone()
|
||||
} else if self.password_stdin {
|
||||
let mut password = String::new();
|
||||
io::stdin()
|
||||
.read_to_string(&mut password)
|
||||
.into_diagnostic()?;
|
||||
password
|
||||
} else {
|
||||
let questions = questions! [ inline
|
||||
Password {
|
||||
name: "password",
|
||||
}
|
||||
];
|
||||
|
||||
requestty::prompt(questions)
|
||||
.into_diagnostic()?
|
||||
.get("password")
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,16 @@
|
|||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use blue_build_process_management::{
|
||||
drivers::{Driver, DriverArgs},
|
||||
logging::CommandLogging,
|
||||
};
|
||||
use blue_build_recipe::Recipe;
|
||||
use blue_build_utils::{
|
||||
cmd,
|
||||
constants::{ARCHIVE_SUFFIX, LOCAL_BUILD, OCI_ARCHIVE, OSTREE_UNVERIFIED_IMAGE},
|
||||
logging::CommandLogging,
|
||||
};
|
||||
use clap::Args;
|
||||
use colored::Colorize;
|
||||
|
|
@ -17,9 +20,9 @@ use miette::{bail, IntoDiagnostic, Result};
|
|||
use tempdir::TempDir;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::{commands::build::BuildCommand, drivers::Driver, rpm_ostree_status::RpmOstreeStatus};
|
||||
use crate::{commands::build::BuildCommand, rpm_ostree_status::RpmOstreeStatus};
|
||||
|
||||
use super::{BlueBuildCommand, DriverArgs};
|
||||
use super::BlueBuildCommand;
|
||||
|
||||
#[derive(Default, Clone, Debug, TypedBuilder, Args)]
|
||||
pub struct SwitchCommand {
|
||||
|
|
@ -51,11 +54,7 @@ impl BlueBuildCommand for SwitchCommand {
|
|||
fn try_run(&mut self) -> Result<()> {
|
||||
trace!("SwitchCommand::try_run()");
|
||||
|
||||
Driver::builder()
|
||||
.build_driver(self.drivers.build_driver)
|
||||
.inspect_driver(self.drivers.inspect_driver)
|
||||
.build()
|
||||
.init();
|
||||
Driver::init(self.drivers);
|
||||
|
||||
let status = RpmOstreeStatus::try_new()?;
|
||||
trace!("{status:?}");
|
||||
|
|
@ -110,17 +109,13 @@ impl SwitchCommand {
|
|||
let status = if status.is_booted_on_archive(archive_path)
|
||||
|| status.is_staged_on_archive(archive_path)
|
||||
{
|
||||
let mut command = Command::new("rpm-ostree");
|
||||
command.arg("upgrade");
|
||||
let mut command = cmd!("rpm-ostree", "upgrade");
|
||||
|
||||
if self.reboot {
|
||||
command.arg("--reboot");
|
||||
cmd!(command, "--reboot");
|
||||
}
|
||||
|
||||
trace!(
|
||||
"rpm-ostree upgrade {}",
|
||||
self.reboot.then_some("--reboot").unwrap_or_default()
|
||||
);
|
||||
trace!("{command:?}");
|
||||
command
|
||||
} else {
|
||||
let image_ref = format!(
|
||||
|
|
@ -128,17 +123,13 @@ impl SwitchCommand {
|
|||
path = archive_path.display()
|
||||
);
|
||||
|
||||
let mut command = Command::new("rpm-ostree");
|
||||
command.arg("rebase").arg(&image_ref);
|
||||
let mut command = cmd!("rpm-ostree", "rebase", &image_ref);
|
||||
|
||||
if self.reboot {
|
||||
command.arg("--reboot");
|
||||
cmd!(command, "--reboot");
|
||||
}
|
||||
|
||||
trace!(
|
||||
"rpm-ostree rebase{} {image_ref}",
|
||||
self.reboot.then_some(" --reboot").unwrap_or_default()
|
||||
);
|
||||
trace!("{command:?}");
|
||||
command
|
||||
}
|
||||
.status_image_ref_progress(
|
||||
|
|
@ -165,11 +156,7 @@ impl SwitchCommand {
|
|||
progress.set_message(format!("Moving image archive to {}...", to.display()));
|
||||
|
||||
trace!("sudo mv {} {}", from.display(), to.display());
|
||||
let status = Command::new("sudo")
|
||||
.arg("mv")
|
||||
.args([from, to])
|
||||
.status()
|
||||
.into_diagnostic()?;
|
||||
let status = cmd!("sudo", "mv", from, to).status().into_diagnostic()?;
|
||||
|
||||
progress.finish_and_clear();
|
||||
|
||||
|
|
@ -194,8 +181,7 @@ impl SwitchCommand {
|
|||
|
||||
trace!("sudo ls {LOCAL_BUILD}");
|
||||
let output = String::from_utf8(
|
||||
Command::new("sudo")
|
||||
.args(["ls", LOCAL_BUILD])
|
||||
cmd!("sudo", "ls", LOCAL_BUILD)
|
||||
.output()
|
||||
.into_diagnostic()?
|
||||
.stdout,
|
||||
|
|
@ -218,11 +204,7 @@ impl SwitchCommand {
|
|||
progress.set_message("Removing old image archive files...");
|
||||
|
||||
trace!("sudo rm -f {files}");
|
||||
let status = Command::new("sudo")
|
||||
.args(["rm", "-f"])
|
||||
.arg(files)
|
||||
.status()
|
||||
.into_diagnostic()?;
|
||||
let status = cmd!("sudo", "rm", "-f", files).status().into_diagnostic()?;
|
||||
|
||||
progress.finish_and_clear();
|
||||
|
||||
|
|
@ -236,8 +218,7 @@ impl SwitchCommand {
|
|||
local_build_path.display()
|
||||
);
|
||||
|
||||
let status = Command::new("sudo")
|
||||
.args(["mkdir", "-p", LOCAL_BUILD])
|
||||
let status = cmd!("sudo", "mkdir", "-p", LOCAL_BUILD)
|
||||
.status()
|
||||
.into_diagnostic()?;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,129 +0,0 @@
|
|||
use std::{env, sync::Mutex};
|
||||
|
||||
use blue_build_utils::constants::{
|
||||
CI_REGISTRY, CI_REGISTRY_PASSWORD, CI_REGISTRY_USER, GITHUB_ACTIONS, GITHUB_ACTOR, GITHUB_TOKEN,
|
||||
};
|
||||
use log::trace;
|
||||
use once_cell::sync::Lazy;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// Stored user creds.
|
||||
///
|
||||
/// This is a special handoff static ref that is consumed
|
||||
/// by the `ENV_CREDENTIALS` static ref. This can be set
|
||||
/// at the beginning of a command for future calls for
|
||||
/// creds to source from.
|
||||
static USER_CREDS: Mutex<UserCreds> = Mutex::new(UserCreds {
|
||||
username: None,
|
||||
password: None,
|
||||
registry: None,
|
||||
});
|
||||
|
||||
/// The credentials for logging into image registries.
|
||||
#[derive(Debug, Default, Clone, TypedBuilder)]
|
||||
pub struct Credentials {
|
||||
pub registry: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
struct UserCreds {
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub registry: Option<String>,
|
||||
}
|
||||
|
||||
/// Stores the global env credentials.
|
||||
///
|
||||
/// This on load will determine the credentials based off of
|
||||
/// `USER_CREDS` and env vars from CI systems. Once this is called
|
||||
/// the value is stored and cannot change.
|
||||
///
|
||||
/// If you have user
|
||||
/// provided credentials, make sure you update `USER_CREDS`
|
||||
/// before trying to access this reference.
|
||||
static ENV_CREDENTIALS: Lazy<Option<Credentials>> = Lazy::new(|| {
|
||||
let (username, password, registry) = {
|
||||
USER_CREDS.lock().map_or((None, None, None), |creds| {
|
||||
(
|
||||
creds.username.as_ref().map(std::borrow::ToOwned::to_owned),
|
||||
creds.password.as_ref().map(std::borrow::ToOwned::to_owned),
|
||||
creds.registry.as_ref().map(std::borrow::ToOwned::to_owned),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
let registry = match (
|
||||
registry,
|
||||
env::var(CI_REGISTRY).ok(),
|
||||
env::var(GITHUB_ACTIONS).ok(),
|
||||
) {
|
||||
(Some(registry), _, _) if !registry.is_empty() => registry,
|
||||
(None, Some(ci_registry), None) if !ci_registry.is_empty() => ci_registry,
|
||||
(None, None, Some(_)) => "ghcr.io".to_string(),
|
||||
_ => return None,
|
||||
};
|
||||
trace!("Registry: {registry}");
|
||||
|
||||
let username = match (
|
||||
username,
|
||||
env::var(CI_REGISTRY_USER).ok(),
|
||||
env::var(GITHUB_ACTOR).ok(),
|
||||
) {
|
||||
(Some(username), _, _) if !username.is_empty() => username,
|
||||
(None, Some(ci_registry_user), None) if !ci_registry_user.is_empty() => ci_registry_user,
|
||||
(None, None, Some(github_actor)) if !github_actor.is_empty() => github_actor,
|
||||
_ => return None,
|
||||
};
|
||||
trace!("Username: {username}");
|
||||
|
||||
let password = match (
|
||||
password,
|
||||
env::var(CI_REGISTRY_PASSWORD).ok(),
|
||||
env::var(GITHUB_TOKEN).ok(),
|
||||
) {
|
||||
(Some(password), _, _) if !password.is_empty() => password,
|
||||
(None, Some(ci_registry_password), None) if !ci_registry_password.is_empty() => {
|
||||
ci_registry_password
|
||||
}
|
||||
(None, None, Some(registry_token)) if !registry_token.is_empty() => registry_token,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(
|
||||
Credentials::builder()
|
||||
.registry(registry)
|
||||
.username(username)
|
||||
.password(password)
|
||||
.build(),
|
||||
)
|
||||
});
|
||||
|
||||
/// Set the users credentials for
|
||||
/// the current set of actions.
|
||||
///
|
||||
/// Be sure to call this before trying to use
|
||||
/// any strategy that requires credentials as
|
||||
/// the environment credentials are lazy allocated.
|
||||
///
|
||||
/// # Panics
|
||||
/// Will panic if it can't lock the mutex.
|
||||
pub fn set_user_creds(
|
||||
username: Option<&String>,
|
||||
password: Option<&String>,
|
||||
registry: Option<&String>,
|
||||
) {
|
||||
trace!("credentials::set({username:?}, password, {registry:?})");
|
||||
let mut creds_lock = USER_CREDS.lock().expect("Must lock USER_CREDS");
|
||||
creds_lock.username = username.map(ToOwned::to_owned);
|
||||
creds_lock.password = password.map(ToOwned::to_owned);
|
||||
creds_lock.registry = registry.map(ToOwned::to_owned);
|
||||
drop(creds_lock);
|
||||
let _ = ENV_CREDENTIALS.as_ref();
|
||||
}
|
||||
|
||||
/// Get the credentials for the current set of actions.
|
||||
pub fn get() -> Option<&'static Credentials> {
|
||||
trace!("credentials::get()");
|
||||
ENV_CREDENTIALS.as_ref()
|
||||
}
|
||||
474
src/drivers.rs
474
src/drivers.rs
|
|
@ -1,474 +0,0 @@
|
|||
//! This module is responsible for managing various strategies
|
||||
//! to perform actions throughout the program. This hides all
|
||||
//! the implementation details from the command logic and allows
|
||||
//! for caching certain long execution tasks like inspecting the
|
||||
//! labels for an image.
|
||||
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
process::{ExitStatus, Output},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use blue_build_recipe::Recipe;
|
||||
use blue_build_utils::constants::IMAGE_VERSION_LABEL;
|
||||
use log::{debug, info, trace};
|
||||
use miette::{bail, miette, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
use semver::{Version, VersionReq};
|
||||
use typed_builder::TypedBuilder;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{credentials, image_metadata::ImageMetadata};
|
||||
|
||||
use self::{
|
||||
buildah_driver::BuildahDriver,
|
||||
docker_driver::DockerDriver,
|
||||
opts::{BuildOpts, BuildTagPushOpts, GetMetadataOpts, PushOpts, RunOpts, TagOpts},
|
||||
podman_driver::PodmanDriver,
|
||||
skopeo_driver::SkopeoDriver,
|
||||
types::{BuildDriverType, InspectDriverType, RunDriverType},
|
||||
};
|
||||
|
||||
mod buildah_driver;
|
||||
mod docker_driver;
|
||||
pub mod opts;
|
||||
mod podman_driver;
|
||||
mod skopeo_driver;
|
||||
pub mod types;
|
||||
|
||||
static INIT: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
|
||||
static SELECTED_BUILD_DRIVER: Lazy<Mutex<Option<BuildDriverType>>> = Lazy::new(|| Mutex::new(None));
|
||||
static SELECTED_INSPECT_DRIVER: Lazy<Mutex<Option<InspectDriverType>>> =
|
||||
Lazy::new(|| Mutex::new(None));
|
||||
static SELECTED_RUN_DRIVER: Lazy<Mutex<Option<RunDriverType>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
/// Stores the build driver.
|
||||
///
|
||||
/// This will, on load, find the best way to build in the
|
||||
/// current environment. Once that strategy is determined,
|
||||
/// it will be available for any part of the program to call
|
||||
/// on to perform builds.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This will cause a panic if a build strategy could
|
||||
/// not be determined.
|
||||
static BUILD_DRIVER: Lazy<Arc<dyn BuildDriver>> = Lazy::new(|| {
|
||||
let driver = SELECTED_BUILD_DRIVER.lock().unwrap();
|
||||
driver.map_or_else(
|
||||
|| panic!("Driver needs to be initialized"),
|
||||
|driver| -> Arc<dyn BuildDriver> {
|
||||
match driver {
|
||||
BuildDriverType::Buildah => Arc::new(BuildahDriver),
|
||||
BuildDriverType::Podman => Arc::new(PodmanDriver),
|
||||
BuildDriverType::Docker => Arc::new(DockerDriver),
|
||||
}
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
/// Stores the inspection driver.
|
||||
///
|
||||
/// This will, on load, find the best way to inspect images in the
|
||||
/// current environment. Once that strategy is determined,
|
||||
/// it will be available for any part of the program to call
|
||||
/// on to perform inspections.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This will cause a panic if a inspect strategy could
|
||||
/// not be determined.
|
||||
static INSPECT_DRIVER: Lazy<Arc<dyn InspectDriver>> = Lazy::new(|| {
|
||||
let driver = SELECTED_INSPECT_DRIVER.lock().unwrap();
|
||||
driver.map_or_else(
|
||||
|| panic!("Driver needs to be initialized"),
|
||||
|driver| -> Arc<dyn InspectDriver> {
|
||||
match driver {
|
||||
InspectDriverType::Skopeo => Arc::new(SkopeoDriver),
|
||||
InspectDriverType::Podman => Arc::new(PodmanDriver),
|
||||
InspectDriverType::Docker => Arc::new(DockerDriver),
|
||||
}
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
/// Stores the run driver.
|
||||
///
|
||||
/// This will, on load, find the best way to run containers in the
|
||||
/// current environment. Once that strategy is determined,
|
||||
/// it will be available for any part of the program to call
|
||||
/// on to perform inspections.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This will cause a panic if a run strategy could
|
||||
/// not be determined.
|
||||
static RUN_DRIVER: Lazy<Arc<dyn RunDriver>> = Lazy::new(|| {
|
||||
let driver = SELECTED_RUN_DRIVER.lock().unwrap();
|
||||
driver.map_or_else(
|
||||
|| panic!("Driver needs to be initialized"),
|
||||
|driver| -> Arc<dyn RunDriver> {
|
||||
match driver {
|
||||
RunDriverType::Podman => Arc::new(PodmanDriver),
|
||||
RunDriverType::Docker => Arc::new(DockerDriver),
|
||||
}
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
/// UUID used to mark the current builds
|
||||
static BUILD_ID: Lazy<Uuid> = Lazy::new(Uuid::new_v4);
|
||||
|
||||
/// The cached os versions
|
||||
static OS_VERSION: Lazy<Mutex<HashMap<String, u64>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// Trait for retrieving version of a driver.
|
||||
pub trait DriverVersion {
|
||||
/// The version req string slice that follows
|
||||
/// the semver standard <https://semver.org/>.
|
||||
const VERSION_REQ: &'static str;
|
||||
|
||||
/// Returns the version of the driver.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if it can't retrieve the version.
|
||||
fn version() -> Result<Version>;
|
||||
|
||||
#[must_use]
|
||||
fn is_supported_version() -> bool {
|
||||
Self::version().is_ok_and(|version| {
|
||||
VersionReq::parse(Self::VERSION_REQ).is_ok_and(|req| req.matches(&version))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows agnostic building, tagging
|
||||
/// pushing, and login.
|
||||
pub trait BuildDriver: Sync + Send {
|
||||
/// Runs the build logic for the strategy.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the build fails.
|
||||
fn build(&self, opts: &BuildOpts) -> Result<()>;
|
||||
|
||||
/// Runs the tag logic for the strategy.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the tagging fails.
|
||||
fn tag(&self, opts: &TagOpts) -> Result<()>;
|
||||
|
||||
/// Runs the push logic for the strategy
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the push fails.
|
||||
fn push(&self, opts: &PushOpts) -> Result<()>;
|
||||
|
||||
/// Runs the login logic for the strategy.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if login fails.
|
||||
fn login(&self) -> Result<()>;
|
||||
|
||||
/// Runs the logic for building, tagging, and pushing an image.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if building, tagging, or pusing fails.
|
||||
fn build_tag_push(&self, opts: &BuildTagPushOpts) -> Result<()> {
|
||||
trace!("BuildDriver::build_tag_push({opts:#?})");
|
||||
|
||||
let full_image = match (opts.archive_path.as_ref(), opts.image.as_ref()) {
|
||||
(Some(archive_path), None) => {
|
||||
format!("oci-archive:{archive_path}")
|
||||
}
|
||||
(None, Some(image)) => opts
|
||||
.tags
|
||||
.first()
|
||||
.map_or_else(|| image.to_string(), |tag| format!("{image}:{tag}")),
|
||||
(Some(_), Some(_)) => bail!("Cannot use both image and archive path"),
|
||||
(None, None) => bail!("Need either the image or archive path set"),
|
||||
};
|
||||
|
||||
let build_opts = BuildOpts::builder()
|
||||
.image(&full_image)
|
||||
.containerfile(opts.containerfile.as_ref())
|
||||
.squash(opts.squash)
|
||||
.build();
|
||||
|
||||
info!("Building image {full_image}");
|
||||
self.build(&build_opts)?;
|
||||
|
||||
if !opts.tags.is_empty() && opts.archive_path.is_none() {
|
||||
let image = opts
|
||||
.image
|
||||
.as_ref()
|
||||
.ok_or_else(|| miette!("Image is required in order to tag"))?;
|
||||
debug!("Tagging all images");
|
||||
|
||||
for tag in opts.tags.as_ref() {
|
||||
debug!("Tagging {} with {tag}", &full_image);
|
||||
|
||||
let tag_opts = TagOpts::builder()
|
||||
.src_image(&full_image)
|
||||
.dest_image(format!("{image}:{tag}"))
|
||||
.build();
|
||||
|
||||
self.tag(&tag_opts)?;
|
||||
|
||||
if opts.push {
|
||||
let retry_count = if opts.retry_push { opts.retry_count } else { 0 };
|
||||
|
||||
debug!("Pushing all images");
|
||||
// Push images with retries (1s delay between retries)
|
||||
blue_build_utils::retry(retry_count, 10, || {
|
||||
let tag_image = format!("{image}:{tag}");
|
||||
|
||||
debug!("Pushing image {tag_image}");
|
||||
|
||||
let push_opts = PushOpts::builder()
|
||||
.image(&tag_image)
|
||||
.compression_type(opts.compression)
|
||||
.build();
|
||||
|
||||
self.push(&push_opts)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RunDriver: Sync + Send {
|
||||
/// Run a container to perform an action.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if there is an issue running the container.
|
||||
fn run(&self, opts: &RunOpts) -> std::io::Result<ExitStatus>;
|
||||
|
||||
/// Run a container to perform an action and capturing output.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if there is an issue running the container.
|
||||
fn run_output(&self, opts: &RunOpts) -> std::io::Result<Output>;
|
||||
}
|
||||
|
||||
/// Allows agnostic inspection of images.
|
||||
pub trait InspectDriver: Sync + Send {
|
||||
/// Gets the metadata on an image tag.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if it is unable to get the labels.
|
||||
fn get_metadata(&self, opts: &GetMetadataOpts) -> Result<ImageMetadata>;
|
||||
}
|
||||
|
||||
#[derive(Debug, TypedBuilder)]
|
||||
pub struct Driver<'a> {
|
||||
#[builder(default)]
|
||||
username: Option<&'a String>,
|
||||
|
||||
#[builder(default)]
|
||||
password: Option<&'a String>,
|
||||
|
||||
#[builder(default)]
|
||||
registry: Option<&'a String>,
|
||||
|
||||
#[builder(default)]
|
||||
build_driver: Option<BuildDriverType>,
|
||||
|
||||
#[builder(default)]
|
||||
inspect_driver: Option<InspectDriverType>,
|
||||
|
||||
#[builder(default)]
|
||||
run_driver: Option<RunDriverType>,
|
||||
}
|
||||
|
||||
impl Driver<'_> {
|
||||
/// Initializes the Strategy with user provided credentials.
|
||||
///
|
||||
/// If you want to take advantage of a user's credentials,
|
||||
/// you will want to run init before trying to use any of
|
||||
/// the strategies.
|
||||
///
|
||||
/// # Panics
|
||||
/// Will panic if it is unable to initialize drivers.
|
||||
pub fn init(self) {
|
||||
trace!("Driver::init()");
|
||||
let mut initialized = INIT.lock().expect("Must lock INIT");
|
||||
|
||||
if !*initialized {
|
||||
credentials::set_user_creds(self.username, self.password, self.registry);
|
||||
|
||||
let mut build_driver = SELECTED_BUILD_DRIVER.lock().expect("Must lock BuildDriver");
|
||||
let mut inspect_driver = SELECTED_INSPECT_DRIVER
|
||||
.lock()
|
||||
.expect("Must lock InspectDriver");
|
||||
let mut run_driver = SELECTED_RUN_DRIVER.lock().expect("Must lock RunDriver");
|
||||
|
||||
*build_driver = Some(
|
||||
self.build_driver
|
||||
.map_or_else(Self::determine_build_driver, |driver| driver),
|
||||
);
|
||||
trace!("Build driver set to {:?}", *build_driver);
|
||||
drop(build_driver);
|
||||
let _ = Self::get_build_driver();
|
||||
|
||||
*inspect_driver = Some(
|
||||
self.inspect_driver
|
||||
.map_or_else(Self::determine_inspect_driver, |driver| driver),
|
||||
);
|
||||
trace!("Inspect driver set to {:?}", *inspect_driver);
|
||||
drop(inspect_driver);
|
||||
let _ = Self::get_inspection_driver();
|
||||
|
||||
*run_driver = Some(
|
||||
self.run_driver
|
||||
.map_or_else(Self::determine_run_driver, |driver| driver),
|
||||
);
|
||||
drop(run_driver);
|
||||
let _ = Self::get_run_driver();
|
||||
|
||||
*initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current build's UUID
|
||||
#[must_use]
|
||||
pub fn get_build_id() -> Uuid {
|
||||
trace!("Driver::get_build_id()");
|
||||
*BUILD_ID
|
||||
}
|
||||
|
||||
/// Gets the current run's build strategy
|
||||
pub fn get_build_driver() -> Arc<dyn BuildDriver> {
|
||||
trace!("Driver::get_build_driver()");
|
||||
BUILD_DRIVER.clone()
|
||||
}
|
||||
|
||||
/// Gets the current run's inspectioin strategy
|
||||
pub fn get_inspection_driver() -> Arc<dyn InspectDriver> {
|
||||
trace!("Driver::get_inspection_driver()");
|
||||
INSPECT_DRIVER.clone()
|
||||
}
|
||||
|
||||
pub fn get_run_driver() -> Arc<dyn RunDriver> {
|
||||
trace!("Driver::get_run_driver()");
|
||||
RUN_DRIVER.clone()
|
||||
}
|
||||
|
||||
/// Retrieve the `os_version` for an image.
|
||||
///
|
||||
/// This gets cached for faster resolution if it's required
|
||||
/// in another part of the program.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the image doesn't have OS version info
|
||||
/// or we are unable to lock a mutex.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the mutex fails to lock.
|
||||
pub fn get_os_version(recipe: &Recipe) -> Result<u64> {
|
||||
trace!("Driver::get_os_version({recipe:#?})");
|
||||
let image = format!("{}:{}", &recipe.base_image, &recipe.image_version);
|
||||
|
||||
let mut os_version_lock = OS_VERSION.lock().expect("Should lock");
|
||||
|
||||
let entry = os_version_lock.get(&image);
|
||||
|
||||
let os_version = match entry {
|
||||
None => {
|
||||
info!("Retrieving OS version from {image}. This might take a bit");
|
||||
let inspect_opts = GetMetadataOpts::builder()
|
||||
.image(recipe.base_image.as_ref())
|
||||
.tag(recipe.image_version.as_ref())
|
||||
.build();
|
||||
let inspection = INSPECT_DRIVER.get_metadata(&inspect_opts)?;
|
||||
|
||||
let os_version = inspection.get_version().ok_or_else(|| {
|
||||
miette!(
|
||||
help = format!("Please check with the image author about using '{IMAGE_VERSION_LABEL}' to report the os version."),
|
||||
"Unable to get the OS version from the labels"
|
||||
)
|
||||
})?;
|
||||
trace!("os_version: {os_version}");
|
||||
|
||||
os_version
|
||||
}
|
||||
Some(os_version) => {
|
||||
debug!("Found cached {os_version} for {image}");
|
||||
*os_version
|
||||
}
|
||||
};
|
||||
|
||||
if let Entry::Vacant(entry) = os_version_lock.entry(image.clone()) {
|
||||
trace!("Caching version {os_version} for {image}");
|
||||
entry.insert(os_version);
|
||||
}
|
||||
drop(os_version_lock);
|
||||
Ok(os_version)
|
||||
}
|
||||
|
||||
fn determine_inspect_driver() -> InspectDriverType {
|
||||
trace!("Driver::determine_inspect_driver()");
|
||||
|
||||
match (
|
||||
blue_build_utils::check_command_exists("skopeo"),
|
||||
blue_build_utils::check_command_exists("docker"),
|
||||
blue_build_utils::check_command_exists("podman"),
|
||||
) {
|
||||
(Ok(_skopeo), _, _) => InspectDriverType::Skopeo,
|
||||
(_, Ok(_docker), _) => InspectDriverType::Docker,
|
||||
(_, _, Ok(_podman)) => InspectDriverType::Podman,
|
||||
_ => panic!("Could not determine inspection strategy. You need either skopeo, docker, or podman"),
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_build_driver() -> BuildDriverType {
|
||||
trace!("Driver::determine_build_driver()");
|
||||
|
||||
match (
|
||||
blue_build_utils::check_command_exists("docker"),
|
||||
blue_build_utils::check_command_exists("podman"),
|
||||
blue_build_utils::check_command_exists("buildah"),
|
||||
) {
|
||||
(Ok(_docker), _, _) if DockerDriver::is_supported_version() => {
|
||||
BuildDriverType::Docker
|
||||
}
|
||||
(_, Ok(_podman), _) if PodmanDriver::is_supported_version() => {
|
||||
BuildDriverType::Podman
|
||||
}
|
||||
(_, _, Ok(_buildah)) if BuildahDriver::is_supported_version() => {
|
||||
BuildDriverType::Buildah
|
||||
}
|
||||
_ => panic!(
|
||||
"Could not determine strategy, need either docker version {}, podman version {}, or buildah version {} to continue",
|
||||
DockerDriver::VERSION_REQ,
|
||||
PodmanDriver::VERSION_REQ,
|
||||
BuildahDriver::VERSION_REQ,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_run_driver() -> RunDriverType {
|
||||
trace!("Driver::determine_run_driver()");
|
||||
|
||||
match (
|
||||
blue_build_utils::check_command_exists("docker"),
|
||||
blue_build_utils::check_command_exists("podman"),
|
||||
) {
|
||||
(Ok(_docker), _) if DockerDriver::is_supported_version() => RunDriverType::Docker,
|
||||
(_, Ok(_podman)) if PodmanDriver::is_supported_version() => RunDriverType::Podman,
|
||||
_ => panic!(
|
||||
"{}{}{}{}",
|
||||
"Could not determine strategy, ",
|
||||
format_args!("need either docker version {}, ", DockerDriver::VERSION_REQ),
|
||||
format_args!("podman version {}, ", PodmanDriver::VERSION_REQ),
|
||||
format_args!(
|
||||
"or buildah version {} to continue",
|
||||
BuildahDriver::VERSION_REQ
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct RunOpts<'a> {
|
||||
#[builder(default, setter(into))]
|
||||
pub image: Cow<'a, str>,
|
||||
|
||||
#[builder(default, setter(into))]
|
||||
pub args: Cow<'a, [String]>,
|
||||
|
||||
#[builder(default, setter(into))]
|
||||
pub env_vars: Cow<'a, [RunOptsEnv<'a>]>,
|
||||
|
||||
#[builder(default, setter(into))]
|
||||
pub volumes: Cow<'a, [RunOptsVolume<'a>]>,
|
||||
|
||||
#[builder(default)]
|
||||
pub privileged: bool,
|
||||
|
||||
#[builder(default)]
|
||||
pub pull: bool,
|
||||
|
||||
#[builder(default)]
|
||||
pub remove: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct RunOptsVolume<'a> {
|
||||
#[builder(setter(into))]
|
||||
pub path_or_vol_name: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
pub container_path: Cow<'a, str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct RunOptsEnv<'a> {
|
||||
#[builder(setter(into))]
|
||||
pub key: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
pub value: Cow<'a, str>,
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
use clap::ValueEnum;
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum InspectDriverType {
|
||||
Skopeo,
|
||||
Podman,
|
||||
Docker,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum BuildDriverType {
|
||||
Buildah,
|
||||
Podman,
|
||||
Docker,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum RunDriverType {
|
||||
Podman,
|
||||
Docker,
|
||||
}
|
||||
|
||||
impl From<RunDriverType> for String {
|
||||
fn from(value: RunDriverType) -> Self {
|
||||
match value {
|
||||
RunDriverType::Podman => "podman".to_string(),
|
||||
RunDriverType::Docker => "docker".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,4 @@
|
|||
shadow_rs::shadow!(shadow);
|
||||
|
||||
pub mod commands;
|
||||
pub mod credentials;
|
||||
pub mod drivers;
|
||||
pub mod image_metadata;
|
||||
pub mod rpm_ostree_status;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use std::{borrow::Cow, path::Path, process::Command};
|
||||
use std::{borrow::Cow, path::Path};
|
||||
|
||||
use blue_build_utils::cmd;
|
||||
use log::trace;
|
||||
use miette::{bail, IntoDiagnostic, Result};
|
||||
use serde::Deserialize;
|
||||
|
|
@ -27,8 +28,7 @@ impl<'a> RpmOstreeStatus<'a> {
|
|||
blue_build_utils::check_command_exists("rpm-ostree")?;
|
||||
|
||||
trace!("rpm-ostree status --json");
|
||||
let output = Command::new("rpm-ostree")
|
||||
.args(["status", "--json"])
|
||||
let output = cmd!("rpm-ostree", "status", "--json")
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ blue-build-recipe = { version = "=0.8.12", path = "../recipe" }
|
|||
blue-build-utils = { version = "=0.8.12", path = "../utils" }
|
||||
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
serde_json.workspace = true
|
||||
typed-builder.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
use std::{borrow::Cow, env, fs, path::Path, process};
|
||||
use std::{borrow::Cow, fs, path::Path, process};
|
||||
|
||||
use blue_build_recipe::Recipe;
|
||||
use blue_build_utils::constants::{
|
||||
CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_SERVER_HOST, CI_SERVER_PROTOCOL, CONFIG_PATH,
|
||||
CONTAINERFILES_PATH, CONTAINER_FILE, COSIGN_PUB_PATH, FILES_PATH, GITHUB_RESPOSITORY,
|
||||
GITHUB_SERVER_URL,
|
||||
CONFIG_PATH, CONTAINERFILES_PATH, CONTAINER_FILE, COSIGN_PUB_PATH, FILES_PATH,
|
||||
};
|
||||
use log::{debug, error, trace, warn};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
|
@ -30,6 +28,9 @@ pub struct ContainerFileTemplate<'a> {
|
|||
|
||||
#[builder(setter(into))]
|
||||
exports_tag: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
repo: Cow<'a, str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Template, TypedBuilder)]
|
||||
|
|
@ -78,6 +79,19 @@ pub struct GithubIssueTemplate<'a> {
|
|||
terminal_version: Cow<'a, str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Template, TypedBuilder)]
|
||||
#[template(path = "init/README.j2", escape = "md")]
|
||||
pub struct InitReadmeTemplate<'a> {
|
||||
#[builder(setter(into))]
|
||||
repo_name: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
registry: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
image_name: Cow<'a, str>,
|
||||
}
|
||||
|
||||
fn has_cosign_file() -> bool {
|
||||
trace!("has_cosign_file()");
|
||||
std::env::current_dir()
|
||||
|
|
@ -110,38 +124,6 @@ fn print_containerfile(containerfile: &str) -> String {
|
|||
file
|
||||
}
|
||||
|
||||
fn get_repo_url() -> Option<String> {
|
||||
Some(
|
||||
match (
|
||||
// GitHub vars
|
||||
env::var(GITHUB_SERVER_URL),
|
||||
env::var(GITHUB_RESPOSITORY),
|
||||
// GitLab vars
|
||||
env::var(CI_SERVER_PROTOCOL),
|
||||
env::var(CI_SERVER_HOST),
|
||||
env::var(CI_PROJECT_NAMESPACE),
|
||||
env::var(CI_PROJECT_NAME),
|
||||
) {
|
||||
(Ok(github_server), Ok(github_repo), _, _, _, _) => {
|
||||
format!("{github_server}/{github_repo}")
|
||||
}
|
||||
(
|
||||
_,
|
||||
_,
|
||||
Ok(ci_server_protocol),
|
||||
Ok(ci_server_host),
|
||||
Ok(ci_project_namespace),
|
||||
Ok(ci_project_name),
|
||||
) => {
|
||||
format!(
|
||||
"{ci_server_protocol}://{ci_server_host}/{ci_project_namespace}/{ci_project_name}"
|
||||
)
|
||||
}
|
||||
_ => return None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn modules_exists() -> bool {
|
||||
let mod_path = Path::new("modules");
|
||||
mod_path.exists() && mod_path.is_dir()
|
||||
|
|
|
|||
|
|
@ -38,7 +38,5 @@ RUN rm -fr /tmp/* /var/* && ostree container commit
|
|||
LABEL {{ blue_build_utils::constants::BUILD_ID_LABEL }}="{{ build_id }}"
|
||||
LABEL org.opencontainers.image.title="{{ recipe.name }}"
|
||||
LABEL org.opencontainers.image.description="{{ recipe.description }}"
|
||||
{%- if let Some(repo) = self::get_repo_url() %}
|
||||
LABEL org.opencontainers.image.source="{{ repo }}"
|
||||
{%- endif %}
|
||||
LABEL io.artifacthub.package.readme-url=https://raw.githubusercontent.com/blue-build/cli/main/README.md
|
||||
|
|
|
|||
45
template/templates/init/README.j2
Normal file
45
template/templates/init/README.j2
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# {{ repo_name }} Image Repo
|
||||
|
||||
See the [BlueBuild docs](https://blue-build.org/how-to/setup/) for quick setup instructions for setting up your own repository based on this template.
|
||||
|
||||
After setup, it is recommended you update this README to describe your custom image.
|
||||
|
||||
## Installation
|
||||
|
||||
> **Warning**
|
||||
> [This is an experimental feature](https://www.fedoraproject.org/wiki/Changes/OstreeNativeContainerStable), try at your own discretion.
|
||||
|
||||
To rebase an existing atomic Fedora installation to the latest build:
|
||||
|
||||
- First rebase to the unsigned image, to get the proper signing keys and policies installed:
|
||||
```
|
||||
rpm-ostree rebase ostree-unverified-registry:{{ registry }}/{{ repo_name }}/{{ image_name }}:latest
|
||||
```
|
||||
- Reboot to complete the rebase:
|
||||
```
|
||||
systemctl reboot
|
||||
```
|
||||
- Then rebase to the signed image, like so:
|
||||
```
|
||||
rpm-ostree rebase ostree-image-signed:docker://{{ registry }}/{{ repo_name }}/{{ image_name }}:latest
|
||||
```
|
||||
- Reboot again to complete the installation
|
||||
```
|
||||
systemctl reboot
|
||||
```
|
||||
|
||||
The `latest` tag will automatically point to the latest build. That build will still always use the Fedora version specified in `recipe.yml`, so you won't get accidentally updated to the next major version.
|
||||
|
||||
## ISO
|
||||
|
||||
If build on Fedora Atomic, you can generate an offline ISO with the instructions available [here](https://blue-build.org/learn/universal-blue/#fresh-install-from-an-iso). These ISOs cannot unfortunately be distributed on GitHub for free due to large sizes, so for public projects something else has to be used for hosting.
|
||||
|
||||
## Verification
|
||||
|
||||
These images are signed with [Sigstore](https://www.sigstore.dev/)'s [cosign](https://github.com/sigstore/cosign). You can verify the signature by downloading the `cosign.pub` file from this repo and running the following command:
|
||||
|
||||
```bash
|
||||
cosign verify --key cosign.pub {{ registry }}/{{ repo_name }}/{{ image_name }}
|
||||
```
|
||||
|
||||
Cloned from https://github.com/blue-build/template
|
||||
10
test-files/github-events/branch.json
Normal file
10
test-files/github-events/branch.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"ref": "refs/heads/test-branch",
|
||||
"repository": {
|
||||
"default_branch": "main",
|
||||
"owner": {
|
||||
"login": "test-owner"
|
||||
},
|
||||
"html_url": "https://example.com/"
|
||||
}
|
||||
}
|
||||
10
test-files/github-events/default-branch.json
Normal file
10
test-files/github-events/default-branch.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"ref": "refs/heads/main",
|
||||
"repository": {
|
||||
"default_branch": "main",
|
||||
"owner": {
|
||||
"login": "test-owner"
|
||||
},
|
||||
"html_url": "https://example.com/"
|
||||
}
|
||||
}
|
||||
15
test-files/github-events/pr-branch.json
Normal file
15
test-files/github-events/pr-branch.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"head": {
|
||||
"ref": "test-branch"
|
||||
},
|
||||
"base": {
|
||||
"ref": "main"
|
||||
},
|
||||
"repository": {
|
||||
"default_branch": "main",
|
||||
"owner": {
|
||||
"login": "test-owner"
|
||||
},
|
||||
"html_url": "https://example.com/"
|
||||
}
|
||||
}
|
||||
11
test-files/keys/cosign.key
Normal file
11
test-files/keys/cosign.key
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
-----BEGIN ENCRYPTED SIGSTORE PRIVATE KEY-----
|
||||
eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjo2NTUzNiwiciI6
|
||||
OCwicCI6MX0sInNhbHQiOiIvNjdKOVZ3WThhNnJhdk9DQUxmTzFQM05HRDRYc2s2
|
||||
L005aE5iYVhDNytBPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94
|
||||
Iiwibm9uY2UiOiIvaHQ1MjlSNlhnbkFGbVV6L3U0anlRVE1lb200VDZNVCJ9LCJj
|
||||
aXBoZXJ0ZXh0IjoiWkpZWWsyR1FhWmdKdEh6UzBKdFVuTWhTblFXc25HcEQzYTVC
|
||||
MjN3ZVlLb2REbzJkeFVOZXhFSURwODhGUkMzalVSTTRiNTZFSEVjblZVWmFETDNj
|
||||
Z2ZrTjdNZWVvMThWWVN2Wm13STdYaFJaczExOUc2eWlmaThIcVpGYmdJM21Rd052
|
||||
MEVEcDFEekw0d2ZJWjBweVAreEEvM2xOeTlteWZSZDZSM1JoR0h5SWt6NVF4eHJ2
|
||||
WjB1VHZHVExOcmdLSHVzL3NTbis1WktsL1E9PSJ9
|
||||
-----END ENCRYPTED SIGSTORE PRIVATE KEY-----
|
||||
4
test-files/keys/cosign.pub
Normal file
4
test-files/keys/cosign.pub
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq8TdgrRtcWVq6MXuB2uznS14EOQ9
|
||||
Ol41BztsDr0Qd8BGfYM6lOkZ+/NLteBFZ9gQsgVhVrjrSifcHmMAUOZYwg==
|
||||
-----END PUBLIC KEY-----
|
||||
|
|
@ -9,38 +9,30 @@ license.workspace = true
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
atty = "0.2"
|
||||
base64 = "0.22.1"
|
||||
blake2 = "0.10.6"
|
||||
base64 = "0.22"
|
||||
blake2 = "0.10"
|
||||
directories = "5"
|
||||
rand = "0.8.5"
|
||||
log4rs = { version = "1.3.0", features = ["background_rotation"] }
|
||||
nix = { version = "0.29.0", features = ["signal"] }
|
||||
nu-ansi-term = { version = "0.50.0", features = ["gnu_legacy"] }
|
||||
os_pipe = { version = "1", features = ["io_safety"] }
|
||||
docker_credential = "1"
|
||||
format_serde_error = "0.3"
|
||||
process_control = { version = "4", features = ["crossbeam-channel"] }
|
||||
signal-hook = { version = "0.3.17", features = ["extended-siginfo"] }
|
||||
syntect = "5"
|
||||
which = "6"
|
||||
|
||||
chrono.workspace = true
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
colored.workspace = true
|
||||
format_serde_error.workspace = true
|
||||
indicatif.workspace = true
|
||||
indicatif-log-bridge.workspace = true
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
log.workspace = true
|
||||
miette.workspace = true
|
||||
once_cell.workspace = true
|
||||
tempdir.workspace = true
|
||||
serde.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
typed-builder.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
syntect = "5.2.0"
|
||||
syntect = "5"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub const IMAGE_VERSION_LABEL: &str = "org.opencontainers.image.version";
|
|||
// BlueBuild vars
|
||||
pub const BB_BUILDKIT_CACHE_GHA: &str = "BB_BUILDKIT_CACHE_GHA";
|
||||
pub const BB_PASSWORD: &str = "BB_PASSWORD";
|
||||
pub const BB_PRIVATE_KEY: &str = "BB_PRIVATE_KEY";
|
||||
pub const BB_REGISTRY: &str = "BB_REGISTRY";
|
||||
pub const BB_REGISTRY_NAMESPACE: &str = "BB_REGISTRY_NAMESPACE";
|
||||
pub const BB_USERNAME: &str = "BB_USERNAME";
|
||||
|
|
@ -27,7 +28,9 @@ pub const BB_USERNAME: &str = "BB_USERNAME";
|
|||
pub const DOCKER_HOST: &str = "DOCKER_HOST";
|
||||
|
||||
// Cosign vars
|
||||
pub const COSIGN_PASSWORD: &str = "COSIGN_PASSWORD";
|
||||
pub const COSIGN_PRIVATE_KEY: &str = "COSIGN_PRIVATE_KEY";
|
||||
pub const COSIGN_YES: &str = "COSIGN_YES";
|
||||
pub const GITHUB_TOKEN_ISSUER_URL: &str = "https://token.actions.githubusercontent.com";
|
||||
pub const SIGSTORE_ID_TOKEN: &str = "SIGSTORE_ID_TOKEN";
|
||||
|
||||
|
|
@ -35,6 +38,7 @@ pub const SIGSTORE_ID_TOKEN: &str = "SIGSTORE_ID_TOKEN";
|
|||
pub const GITHUB_ACTIONS: &str = "GITHUB_ACTIONS";
|
||||
pub const GITHUB_ACTOR: &str = "GITHUB_ACTOR";
|
||||
pub const GITHUB_EVENT_NAME: &str = "GITHUB_EVENT_NAME";
|
||||
pub const GITHUB_EVENT_PATH: &str = "GITHUB_EVENT_PATH";
|
||||
pub const GITHUB_REF_NAME: &str = "GITHUB_REF_NAME";
|
||||
pub const GITHUB_RESPOSITORY: &str = "GITHUB_REPOSITORY";
|
||||
pub const GITHUB_REPOSITORY_OWNER: &str = "GITHUB_REPOSITORY_OWNER";
|
||||
|
|
@ -58,6 +62,7 @@ pub const CI_SERVER_PROTOCOL: &str = "CI_SERVER_PROTOCOL";
|
|||
pub const CI_REGISTRY: &str = "CI_REGISTRY";
|
||||
pub const CI_REGISTRY_PASSWORD: &str = "CI_REGISTRY_PASSWORD";
|
||||
pub const CI_REGISTRY_USER: &str = "CI_REGISTRY_USER";
|
||||
pub const GITLAB_CI: &str = "GITLAB_CI";
|
||||
|
||||
// Terminal vars
|
||||
pub const TERM_PROGRAM: &str = "TERM_PROGRAM";
|
||||
|
|
@ -67,10 +72,12 @@ pub const LC_TERMINAL_VERSION: &str = "LC_TERMINAL_VERSION";
|
|||
pub const XDG_RUNTIME_DIR: &str = "XDG_RUNTIME_DIR";
|
||||
|
||||
// Misc
|
||||
pub const COSIGN_IMAGE: &str = "gcr.io/projectsigstore/cosign:latest";
|
||||
pub const OCI_ARCHIVE: &str = "oci-archive";
|
||||
pub const OSTREE_IMAGE_SIGNED: &str = "ostree-image-signed";
|
||||
pub const OSTREE_UNVERIFIED_IMAGE: &str = "ostree-unverified-image";
|
||||
pub const SKOPEO_IMAGE: &str = "quay.io/skopeo/stable:latest";
|
||||
pub const TEMPLATE_REPO_URL: &str = "https://github.com/blue-build/template.git";
|
||||
pub const UNKNOWN_SHELL: &str = "<unknown shell>";
|
||||
pub const UNKNOWN_VERSION: &str = "<unknown version>";
|
||||
pub const UNKNOWN_TERMINAL: &str = "<unknown terminal>";
|
||||
|
|
|
|||
177
utils/src/credentials.rs
Normal file
177
utils/src/credentials.rs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
use std::{
|
||||
env,
|
||||
sync::{LazyLock, Mutex},
|
||||
};
|
||||
|
||||
use clap::Args;
|
||||
use docker_credential::DockerCredential;
|
||||
use log::trace;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::{
|
||||
constants::{
|
||||
BB_PASSWORD, BB_REGISTRY, BB_USERNAME, CI_REGISTRY, CI_REGISTRY_PASSWORD, CI_REGISTRY_USER,
|
||||
GITHUB_ACTIONS, GITHUB_ACTOR, GITHUB_TOKEN,
|
||||
},
|
||||
string,
|
||||
};
|
||||
|
||||
static INIT: LazyLock<Mutex<bool>> = LazyLock::new(|| Mutex::new(false));
|
||||
|
||||
/// Stored user creds.
|
||||
///
|
||||
/// This is a special handoff static ref that is consumed
|
||||
/// by the `ENV_CREDENTIALS` static ref. This can be set
|
||||
/// at the beginning of a command for future calls for
|
||||
/// creds to source from.
|
||||
static INIT_CREDS: Mutex<CredentialsArgs> = Mutex::new(CredentialsArgs {
|
||||
username: None,
|
||||
password: None,
|
||||
registry: None,
|
||||
});
|
||||
|
||||
/// Stores the global env credentials.
|
||||
///
|
||||
/// This on load will determine the credentials based off of
|
||||
/// `USER_CREDS` and env vars from CI systems. Once this is called
|
||||
/// the value is stored and cannot change.
|
||||
///
|
||||
/// If you have user
|
||||
/// provided credentials, make sure you update `USER_CREDS`
|
||||
/// before trying to access this reference.
|
||||
static ENV_CREDENTIALS: LazyLock<Option<Credentials>> = LazyLock::new(|| {
|
||||
let (username, password, registry) = {
|
||||
INIT_CREDS.lock().map_or((None, None, None), |mut creds| {
|
||||
(
|
||||
creds.username.take(),
|
||||
creds.password.take(),
|
||||
creds.registry.take(),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
let registry = match (
|
||||
registry,
|
||||
env::var(CI_REGISTRY).ok(),
|
||||
env::var(GITHUB_ACTIONS).ok(),
|
||||
) {
|
||||
(Some(registry), _, _) if !registry.is_empty() => registry,
|
||||
(None, Some(ci_registry), None) if !ci_registry.is_empty() => ci_registry,
|
||||
(None, None, Some(_)) => string!("ghcr.io"),
|
||||
_ => return None,
|
||||
};
|
||||
trace!("Registry: {registry:?}");
|
||||
|
||||
let docker_creds = docker_credential::get_credential(®istry).ok();
|
||||
let podman_creds = docker_credential::get_podman_credential(®istry).ok();
|
||||
|
||||
let username = match (
|
||||
username,
|
||||
env::var(CI_REGISTRY_USER).ok(),
|
||||
env::var(GITHUB_ACTOR).ok(),
|
||||
&docker_creds,
|
||||
&podman_creds,
|
||||
) {
|
||||
(Some(username), _, _, _, _) if !username.is_empty() => username,
|
||||
(_, _, _, Some(DockerCredential::UsernamePassword(username, _)), _)
|
||||
| (_, _, _, _, Some(DockerCredential::UsernamePassword(username, _)))
|
||||
if !username.is_empty() =>
|
||||
{
|
||||
username.clone()
|
||||
}
|
||||
(None, Some(ci_registry_user), None, _, _) if !ci_registry_user.is_empty() => {
|
||||
ci_registry_user
|
||||
}
|
||||
(None, None, Some(github_actor), _, _) if !github_actor.is_empty() => github_actor,
|
||||
_ => return None,
|
||||
};
|
||||
trace!("Username: {username:?}");
|
||||
|
||||
let password = match (
|
||||
password,
|
||||
env::var(CI_REGISTRY_PASSWORD).ok(),
|
||||
env::var(GITHUB_TOKEN).ok(),
|
||||
&docker_creds,
|
||||
&podman_creds,
|
||||
) {
|
||||
(Some(password), _, _, _, _) if !password.is_empty() => password,
|
||||
(_, _, _, Some(DockerCredential::UsernamePassword(_, password)), _)
|
||||
| (_, _, _, _, Some(DockerCredential::UsernamePassword(_, password)))
|
||||
if !password.is_empty() =>
|
||||
{
|
||||
password.clone()
|
||||
}
|
||||
(None, Some(ci_registry_password), None, _, _) if !ci_registry_password.is_empty() => {
|
||||
ci_registry_password
|
||||
}
|
||||
(None, None, Some(registry_token), _, _) if !registry_token.is_empty() => registry_token,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(
|
||||
Credentials::builder()
|
||||
.registry(registry)
|
||||
.username(username)
|
||||
.password(password)
|
||||
.build(),
|
||||
)
|
||||
});
|
||||
|
||||
/// The credentials for logging into image registries.
|
||||
#[derive(Debug, Default, Clone, TypedBuilder)]
|
||||
pub struct Credentials {
|
||||
pub registry: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl Credentials {
|
||||
/// Set the users credentials for
|
||||
/// the current set of actions.
|
||||
///
|
||||
/// Be sure to call this before trying to use
|
||||
/// any strategy that requires credentials as
|
||||
/// the environment credentials are lazy allocated.
|
||||
///
|
||||
/// # Panics
|
||||
/// Will panic if it can't lock the mutex.
|
||||
pub fn init(args: CredentialsArgs) {
|
||||
trace!("Credentials::init()");
|
||||
let mut initialized = INIT.lock().expect("Must lock INIT");
|
||||
|
||||
if !*initialized {
|
||||
let mut creds_lock = INIT_CREDS.lock().expect("Must lock USER_CREDS");
|
||||
*creds_lock = args;
|
||||
drop(creds_lock);
|
||||
let _ = ENV_CREDENTIALS.as_ref();
|
||||
|
||||
*initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the credentials for the current set of actions.
|
||||
pub fn get() -> Option<&'static Self> {
|
||||
trace!("credentials::get()");
|
||||
ENV_CREDENTIALS.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, TypedBuilder, Args)]
|
||||
pub struct CredentialsArgs {
|
||||
/// The registry's domain name.
|
||||
#[arg(long, env = BB_REGISTRY)]
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub registry: Option<String>,
|
||||
|
||||
/// The username to login to the
|
||||
/// container registry.
|
||||
#[arg(short = 'U', long, env = BB_USERNAME, hide_env_values = true)]
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub username: Option<String>,
|
||||
|
||||
/// The password to login to the
|
||||
/// container registry.
|
||||
#[arg(short = 'P', long, env = BB_PASSWORD, hide_env_values = true)]
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
pub mod command_output;
|
||||
pub mod constants;
|
||||
pub mod logging;
|
||||
pub mod signal_handler;
|
||||
pub mod credentials;
|
||||
mod macros;
|
||||
pub mod syntax_highlighting;
|
||||
|
||||
use std::{
|
||||
os::unix::ffi::OsStrExt,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
|
@ -17,9 +16,10 @@ use blake2::{
|
|||
digest::{Update, VariableOutput},
|
||||
Blake2bVar,
|
||||
};
|
||||
use chrono::Local;
|
||||
use format_serde_error::SerdeError;
|
||||
use log::trace;
|
||||
use miette::{miette, IntoDiagnostic, Result};
|
||||
use miette::{miette, Context, IntoDiagnostic, Result};
|
||||
|
||||
use crate::constants::CONTAINER_FILE;
|
||||
|
||||
|
|
@ -33,8 +33,7 @@ pub fn check_command_exists(command: &str) -> Result<()> {
|
|||
trace!("check_command_exists({command})");
|
||||
|
||||
trace!("which {command}");
|
||||
if Command::new("which")
|
||||
.arg(command)
|
||||
if cmd!("which", command)
|
||||
.output()
|
||||
.into_diagnostic()?
|
||||
.status
|
||||
|
|
@ -109,3 +108,18 @@ pub fn generate_containerfile_path<T: AsRef<Path>>(path: T) -> Result<PathBuf> {
|
|||
BASE64_URL_SAFE_NO_PAD.encode(buf)
|
||||
)))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_tag_timestamp() -> String {
|
||||
Local::now().format("%Y%m%d").to_string()
|
||||
}
|
||||
|
||||
/// Get's the env var wrapping it with a miette error
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the env var doesn't exist.
|
||||
pub fn get_env_var(key: &str) -> Result<String> {
|
||||
std::env::var(key)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to get {key}'"))
|
||||
}
|
||||
|
|
|
|||
150
utils/src/macros.rs
Normal file
150
utils/src/macros.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
/// Creates or modifies a `std::process::Command` adding args.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use blue_build_utils::cmd;
|
||||
///
|
||||
/// const NAME: &str = "Bob";
|
||||
/// let mut command = cmd!("echo", "Hello world!");
|
||||
/// cmd!(command, "This is Joe.", format!("And this is {NAME}"));
|
||||
/// command.status().unwrap();
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! cmd {
|
||||
($command:expr) => {
|
||||
::std::process::Command::new($command)
|
||||
};
|
||||
($command:ident, $($tail:tt)*) => {
|
||||
cmd!(@ $command, $($tail)*)
|
||||
};
|
||||
($command:expr, $($tail:tt)*) => {
|
||||
{
|
||||
let mut c = cmd!($command);
|
||||
cmd!(@ c, $($tail)*);
|
||||
c
|
||||
}
|
||||
};
|
||||
(@ $command:ident $(,)?) => { };
|
||||
(@ $command:ident, for $for_expr:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
for arg in $for_expr.iter() {
|
||||
cmd!($command, arg);
|
||||
}
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, for $iter:ident in $for_expr:expr => [ $($arg:expr),* $(,)? ] $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
for $iter in $for_expr.iter() {
|
||||
$(cmd!(@ $command, $arg);)*
|
||||
}
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, for $iter:ident in $for_expr:expr => $arg:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
for $iter in $for_expr.iter() {
|
||||
cmd!(@ $command, $arg);
|
||||
}
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, if let $let_pat:pat = $if_expr:expr => [ $($arg:expr),* $(,)? ] $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
if let $let_pat = $if_expr {
|
||||
$(cmd!(@ $command, $arg);)*
|
||||
}
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, if let $let_pat:pat = $if_expr:expr => $arg:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
if let $let_pat = $if_expr {
|
||||
cmd!(@ $command, $arg);
|
||||
}
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, if $if_expr:expr => [ $($arg:expr),* $(,)?] $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
if $if_expr {
|
||||
$(cmd!(@ $command, $arg);)*
|
||||
}
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, if $if_expr:expr => $arg:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
if $if_expr {
|
||||
cmd!(@ $command, $arg);
|
||||
}
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, |$cmd_ref:ident|? $op:block $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
let op_fn = |$cmd_ref: &mut ::std::process::Command| -> Result<()> {
|
||||
$op
|
||||
Ok(())
|
||||
};
|
||||
op_fn(&mut $command)?;
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, |$cmd_ref:ident| $op:block $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
let op_fn = |$cmd_ref: &mut ::std::process::Command| $op;
|
||||
op_fn(&mut $command);
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, $key:expr => $value:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
$command.env($key, $value);
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, stdin = $pipe:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
$command.stdin($pipe);
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, stdout = $pipe:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
$command.stdout($pipe);
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, stderr = $pipe:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
$command.stderr($pipe);
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, $arg:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
$command.arg($arg);
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! string {
|
||||
($str:expr) => {
|
||||
String::from($str)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! string_vec {
|
||||
($($string:expr),+ $(,)?) => {
|
||||
{
|
||||
use $crate::string;
|
||||
vec![
|
||||
$(string!($string),)*
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue