feat: Add validation command

This commit is contained in:
Gerald Pinder 2024-10-11 19:37:28 -04:00
parent 1de71ab026
commit 9a3ad0ae17
35 changed files with 1666 additions and 508 deletions

2
.gitignore vendored
View file

@ -3,6 +3,8 @@
.vscode/
result*
.direnv/
.arg
.secret
cosign.key
!test-files/keys/cosign.key

605
Cargo.lock generated
View file

@ -58,6 +58,7 @@ dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"serde",
"version_check",
"zerocopy",
]
@ -264,6 +265,21 @@ dependencies = [
"serde",
]
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -327,7 +343,9 @@ dependencies = [
"clap_complete",
"colored",
"fuzzy-matcher",
"indexmap 2.6.0",
"indicatif",
"jsonschema",
"log",
"miette",
"oci-distribution",
@ -335,12 +353,14 @@ dependencies = [
"os_info",
"rayon",
"requestty",
"reqwest 0.12.8",
"rusty-hook",
"serde",
"serde_json",
"serde_yaml 0.9.34+deprecated",
"shadow-rs",
"tempdir",
"tokio",
"urlencoding",
"users",
]
@ -369,6 +389,7 @@ dependencies = [
"once_cell",
"os_pipe",
"rand 0.8.5",
"reqwest 0.12.8",
"rstest",
"semver",
"serde",
@ -458,6 +479,12 @@ dependencies = [
"syn 2.0.79",
]
[[package]]
name = "borrow-or-share"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32"
[[package]]
name = "bstr"
version = "1.10.0"
@ -474,6 +501,12 @@ version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytecount"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce"
[[package]]
name = "byteorder"
version = "1.5.0"
@ -537,8 +570,6 @@ version = "1.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58e804ac3194a48bb129643eb1d62fcc20d18c6b8c181704489353d13120bcd1"
dependencies = [
"jobserver",
"libc",
"shlex",
]
@ -701,12 +732,6 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const_fn"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373e9fafaa20882876db20562275ff58d50e0caa2590077fe7ce7bef90211d0d"
[[package]]
name = "const_format"
version = "0.2.33"
@ -1011,6 +1036,17 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
]
[[package]]
name = "docker_credential"
version = "1.3.1"
@ -1094,6 +1130,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "email_address"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
dependencies = [
"serde",
]
[[package]]
name = "encode_unicode"
version = "0.3.6"
@ -1135,6 +1180,17 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "fancy-regex"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "fastrand"
version = "2.1.1"
@ -1179,6 +1235,17 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "fluent-uri"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
dependencies = [
"borrow-or-share",
"ref-cast",
"serde",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -1207,6 +1274,16 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "fraction"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7"
dependencies = [
"lazy_static",
"num",
]
[[package]]
name = "fsio"
version = "0.1.3"
@ -1362,19 +1439,6 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "git2"
version = "0.18.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70"
dependencies = [
"bitflags 2.6.0",
"libc",
"libgit2-sys",
"log",
"url",
]
[[package]]
name = "glob"
version = "0.3.1"
@ -1663,7 +1727,6 @@ dependencies = [
"hyper 1.4.1",
"hyper-util",
"rustls 0.23.14",
"rustls-native-certs",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.0",
@ -1713,6 +1776,124 @@ dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
[[package]]
name = "icu_normalizer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
[[package]]
name = "icu_properties"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locid_transform",
"icu_properties_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
[[package]]
name = "icu_provider"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@ -1729,6 +1910,18 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "idna"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd"
dependencies = [
"icu_normalizer",
"icu_properties",
"smallvec",
"utf8_iter",
]
[[package]]
name = "indexmap"
version = "1.9.3"
@ -1884,15 +2077,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.72"
@ -1933,6 +2117,31 @@ dependencies = [
"utf8-decode",
]
[[package]]
name = "jsonschema"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ae663abb3bb9e77538ee88a0eb69cbd3f62a8bf2018f848fbc60c2cdec024d"
dependencies = [
"ahash 0.8.11",
"base64 0.22.1",
"bytecount",
"email_address",
"fancy-regex",
"fraction",
"idna 1.0.2",
"itoa",
"num-cmp",
"once_cell",
"percent-encoding",
"referencing",
"regex-syntax",
"reqwest 0.12.8",
"serde",
"serde_json",
"uuid-simd",
]
[[package]]
name = "jwt"
version = "0.16.0"
@ -2065,18 +2274,6 @@ version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "libgit2-sys"
version = "0.16.2+1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8"
dependencies = [
"cc",
"libc",
"libz-sys",
"pkg-config",
]
[[package]]
name = "libm"
version = "0.2.8"
@ -2093,18 +2290,6 @@ dependencies = [
"libc",
]
[[package]]
name = "libz-sys"
version = "1.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@ -2117,6 +2302,12 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "litemap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
[[package]]
name = "lock_api"
version = "0.4.12"
@ -2329,6 +2520,30 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
@ -2346,6 +2561,21 @@ dependencies = [
"zeroize",
]
[[package]]
name = "num-cmp"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa"
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-conv"
version = "0.1.0"
@ -2372,6 +2602,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -2591,12 +2832,6 @@ dependencies = [
"url",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "option-ext"
version = "0.2.0"
@ -2633,6 +2868,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "outref"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a"
[[package]]
name = "owo-colors"
version = "4.1.0"
@ -3209,6 +3450,39 @@ dependencies = [
"thiserror",
]
[[package]]
name = "ref-cast"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
]
[[package]]
name = "referencing"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c152a23ee0e5947ee31d9cfebc873a5aa3a249da9e59d2e76cd7416a13cc9a5d"
dependencies = [
"ahash 0.8.11",
"fluent-uri",
"once_cell",
"percent-encoding",
"serde_json",
]
[[package]]
name = "regex"
version = "1.11.0"
@ -3345,6 +3619,7 @@ checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http 1.1.0",
@ -3363,7 +3638,6 @@ dependencies = [
"pin-project-lite",
"quinn",
"rustls 0.23.14",
"rustls-native-certs",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
"serde",
@ -3562,19 +3836,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a"
dependencies = [
"openssl-probe",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
@ -3668,15 +3929,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -3719,29 +3971,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.6.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "1.0.23"
@ -3919,10 +4148,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5c5c8276991763b44ede03efaf966eaa0412fafbf299e6380704678ca3b997"
dependencies = [
"const_format",
"git2",
"is_debug",
"time",
"tzdb",
]
[[package]]
@ -4228,6 +4455,17 @@ dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
]
[[package]]
name = "syntect"
version = "5.2.0"
@ -4411,6 +4649,16 @@ dependencies = [
"time-core",
]
[[package]]
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.8.0"
@ -4617,35 +4865,6 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "tz-rs"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33851b15c848fad2cf4b105c6bb66eb9512b6f6c44a4b13f57c53c73c707e2b4"
dependencies = [
"const_fn",
]
[[package]]
name = "tzdb"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b580f6b365fa89f5767cdb619a55d534d04a4e14c2d7e5b9a31e94598687fb1"
dependencies = [
"iana-time-zone",
"tz-rs",
"tzdb_data",
]
[[package]]
name = "tzdb_data"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "654c1ec546942ce0594e8d220e6b8e3899e0a0a8fe70ddd54d32a376dfefe3f8"
dependencies = [
"tz-rs",
]
[[package]]
name = "unicase"
version = "2.7.0"
@ -4744,7 +4963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
"idna 0.5.0",
"percent-encoding",
"serde",
]
@ -4765,12 +4984,24 @@ dependencies = [
"log",
]
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8-decode"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca61eb27fa339aa08826a29f03e87b99b4d8f0fc2255306fd266bb1b6a9de498"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
@ -4787,10 +5018,15 @@ dependencies = [
]
[[package]]
name = "vcpkg"
version = "0.2.15"
name = "uuid-simd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
dependencies = [
"outref",
"uuid",
"vsimd",
]
[[package]]
name = "version_check"
@ -4798,6 +5034,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vsimd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
[[package]]
name = "walkdir"
version = "2.5.0"
@ -5274,6 +5516,18 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab703352da6a72f35c39a533526393725640575bb211f61987a2748323ad956"
[[package]]
name = "write16"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "x509-cert"
version = "0.2.5"
@ -5297,6 +5551,30 @@ dependencies = [
"linked-hash-map",
]
[[package]]
name = "yoke"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
@ -5318,6 +5596,27 @@ dependencies = [
"syn 2.0.79",
]
[[package]]
name = "zerofrom"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.1"
@ -5338,3 +5637,25 @@ dependencies = [
"quote",
"syn 2.0.79",
]
[[package]]
name = "zerovec"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
]

View file

@ -18,13 +18,15 @@ colored = "2"
indexmap = { version = "2", features = ["serde"] }
indicatif = { version = "0.17", features = ["improved_unicode"] }
log = "0.4"
oci-distribution = { version = "0.11.0", default-features = false, features = ["rustls-tls", "rustls-tls-native-roots"] }
oci-distribution = { version = "0.11", default-features = false }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
miette = "7"
rstest = "0.18"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
tempdir = "0.3"
tokio = { version = "1", features = ["rt", "rt-multi-thread"] }
users = "0.11"
uuid = { version = "1", features = ["v4"] }
@ -65,24 +67,28 @@ blue-build-process-management = { version = "=0.8.20", path = "./process" }
clap-verbosity-flag = "2"
clap_complete = "4"
fuzzy-matcher = "0.3"
jsonschema = { version = "0.26", optional = true }
open = "5"
os_info = "3"
rayon = { version = "1.10.0", optional = true }
requestty = { version = "0.5", features = ["macros", "termion"] }
shadow-rs = "0.26"
shadow-rs = { version = "0.26", default-features = false }
urlencoding = "2"
cached.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"] }
oci-distribution.workspace = true
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
tempdir.workspace = true
tokio = { workspace = true, optional = true }
bon.workspace = true
users.workspace = true
@ -90,17 +96,24 @@ users.workspace = true
default = []
stages = ["blue-build-recipe/stages"]
copy = ["blue-build-recipe/copy"]
multi-recipe = ["rayon", "indicatif/rayon"]
multi-recipe = ["dep:rayon", "indicatif/rayon"]
iso = []
switch = []
sigstore = ["blue-build-process-management/sigstore"]
login = []
validate = [
"dep:jsonschema",
"dep:rayon",
"dep:tokio",
"cached/async",
"blue-build-process-management/validate"
]
[dev-dependencies]
rusty-hook = "0.11"
[build-dependencies]
shadow-rs = "0.26"
shadow-rs = { version = "0.26", default-features = false }
[lints]
workspace = true

399
Earthfile
View file

@ -6,270 +6,281 @@ IMPORT github.com/earthly/lib/rust AS rust
ARG --global IMAGE=ghcr.io/blue-build/cli
all:
WAIT
BUILD --platform=linux/amd64 --platform=linux/arm64 +prebuild
END
BUILD +build
BUILD ./integration-tests+all
WAIT
BUILD --platform=linux/amd64 --platform=linux/arm64 +prebuild
END
BUILD +build
BUILD ./integration-tests+all
run-checks:
BUILD +lint
BUILD +test
BUILD +lint
BUILD +test
build-images:
BUILD +blue-build-cli
BUILD +blue-build-cli-alpine
BUILD +installer
BUILD +blue-build-cli
BUILD +blue-build-cli-alpine
BUILD +installer
prebuild:
BUILD +blue-build-cli-prebuild
BUILD +blue-build-cli-alpine-prebuild
BUILD +blue-build-cli-prebuild
BUILD +blue-build-cli-alpine-prebuild
lint:
FROM +common
RUN cargo fmt --check
DO rust+CARGO --args="clippy"
DO rust+CARGO --args="clippy --all-features"
DO rust+CARGO --args="clippy --no-default-features"
DO rust+CARGO --args="clippy --no-default-features --features stages"
DO rust+CARGO --args="clippy --no-default-features --features copy"
DO rust+CARGO --args="clippy --no-default-features --features multi-recipe"
DO rust+CARGO --args="clippy --no-default-features --features iso"
DO rust+CARGO --args="clippy --no-default-features --features switch"
DO rust+CARGO --args="clippy --no-default-features --features sigstore"
FROM +common
RUN cargo fmt --check
DO rust+CARGO --args="clippy"
DO rust+CARGO --args="clippy --all-features"
DO rust+CARGO --args="clippy --no-default-features"
FOR feat IN $( \
cargo metadata --format-version 1 \
| jq -cr '.packages[] | select(.name == "blue-build") | .features | keys | .[] | select(. != "default")' \
)
DO rust+CARGO --args="clippy --no-default-features --features $feat"
END
test:
FROM +common
COPY --dir test-files/ integration-tests/ /app
COPY +cosign/cosign /usr/bin/cosign
FROM +common
COPY --dir test-files/ integration-tests/ /app
COPY +cosign/cosign /usr/bin/cosign
DO rust+CARGO --args="test --workspace"
DO rust+CARGO --args="test --workspace --all-features"
DO rust+CARGO --args="test --workspace --no-default-features"
DO rust+CARGO --args="test --workspace --no-default-features --features stages"
DO rust+CARGO --args="test --workspace --no-default-features --features copy"
DO rust+CARGO --args="test --workspace --no-default-features --features multi-recipe"
DO rust+CARGO --args="test --workspace --no-default-features --features iso"
DO rust+CARGO --args="test --workspace --no-default-features --features switch"
DO rust+CARGO --args="test --workspace --no-default-features --features sigstore"
DO rust+CARGO --args="test --workspace"
DO rust+CARGO --args="test --workspace --all-features"
DO rust+CARGO --args="test --workspace --no-default-features"
FOR feat IN $( \
cargo metadata --format-version 1 \
| jq -cr '.packages[] | select(.name == "blue-build") | .features | keys | .[] | select(. != "default")' \
)
DO rust+CARGO --args="test --workspace --features $feat"
END
install:
FROM +common
ARG --required BUILD_TARGET
FROM +common
ARG --required BUILD_TARGET
ARG --required RELEASE
DO rust+CROSS --target="$BUILD_TARGET" --output="$BUILD_TARGET/release/[^\./]+"
SAVE ARTIFACT target/$BUILD_TARGET/release/bluebuild
IF [ "$RELEASE" = "true" ]
DO rust+CROSS --target="$BUILD_TARGET" --output="$BUILD_TARGET/release/[^\./]+"
SAVE ARTIFACT target/$BUILD_TARGET/release/bluebuild
ELSE
DO rust+CROSS --args="build" --target="$BUILD_TARGET" --output="$BUILD_TARGET/debug/[^\./]+"
SAVE ARTIFACT target/$BUILD_TARGET/debug/bluebuild
END
install-all-features:
FROM +common
ARG --required BUILD_TARGET
FROM +common
ARG --required BUILD_TARGET
ARG --required RELEASE
DO rust+CROSS --args="build --all-features --release" --target="$BUILD_TARGET" --output="$BUILD_TARGET/release/[^\./]+"
SAVE ARTIFACT target/$BUILD_TARGET/release/bluebuild
IF [ "$RELEASE" = "true" ]
DO rust+CROSS --args="build --all-features --release" --target="$BUILD_TARGET" --output="$BUILD_TARGET/release/[^\./]+"
SAVE ARTIFACT target/$BUILD_TARGET/release/bluebuild
ELSE
DO rust+CROSS --args="build --all-features" --target="$BUILD_TARGET" --output="$BUILD_TARGET/debug/[^\./]+"
SAVE ARTIFACT target/$BUILD_TARGET/debug/bluebuild
END
common:
FROM --platform=native ghcr.io/blue-build/earthly-lib/cargo-builder
FROM --platform=native ghcr.io/blue-build/earthly-lib/cargo-builder
WORKDIR /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
COPY --keep-ts build.rs /app
COPY --keep-ts --dir .git/ /app
RUN touch build.rs
WORKDIR /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
COPY --keep-ts build.rs /app
COPY --keep-ts --dir .git/ /app
RUN touch build.rs
DO rust+INIT --keep_fingerprints=true
DO rust+INIT --keep_fingerprints=true
build-scripts:
ARG BASE_IMAGE="alpine"
FROM $BASE_IMAGE
ARG BASE_IMAGE="alpine"
FROM $BASE_IMAGE
COPY --platform=native (+digest/base-image-digest --BASE_IMAGE=$BASE_IMAGE) /base-image-digest
LABEL org.opencontainers.image.base.name="$BASE_IMAGE"
LABEL org.opencontainers.image.base.digest="$(cat /base-image-digest)"
COPY --platform=native (+digest/base-image-digest --BASE_IMAGE=$BASE_IMAGE) /base-image-digest
LABEL org.opencontainers.image.base.name="$BASE_IMAGE"
LABEL org.opencontainers.image.base.digest="$(cat /base-image-digest)"
COPY --dir scripts/ /
FOR script IN "$(ls /scripts | grep -e '.*\.sh$')"
RUN echo "Making ${script} executable" && \
chmod +x "scripts/${script}"
END
COPY --dir scripts/ /
FOR script IN "$(ls /scripts | grep -e '.*\.sh$')"
RUN echo "Making ${script} executable" && \
chmod +x "scripts/${script}"
END
DO --pass-args +SAVE_IMAGE --IMAGE="$IMAGE/build-scripts"
DO --pass-args +SAVE_IMAGE --IMAGE="$IMAGE/build-scripts"
blue-build-cli-prebuild:
ARG BASE_IMAGE="registry.fedoraproject.org/fedora-toolbox"
FROM DOCKERFILE -f Dockerfile.fedora .
ARG BASE_IMAGE="registry.fedoraproject.org/fedora-toolbox"
FROM DOCKERFILE -f Dockerfile.fedora .
COPY --platform=native (+digest/base-image-digest --BASE_IMAGE=$BASE_IMAGE) /base-image-digest
LABEL org.opencontainers.image.base.name="$BASE_IMAGE"
LABEL org.opencontainers.image.base.digest="$(cat /base-image-digest)"
COPY --platform=native (+digest/base-image-digest --BASE_IMAGE=$BASE_IMAGE) /base-image-digest
LABEL org.opencontainers.image.base.name="$BASE_IMAGE"
LABEL org.opencontainers.image.base.digest="$(cat /base-image-digest)"
COPY +cosign/cosign /usr/bin/cosign
ARG EARTHLY_GIT_HASH
ARG TARGETARCH
SAVE IMAGE --push "$IMAGE:$EARTHLY_GIT_HASH-prebuild-$TARGETARCH"
COPY +cosign/cosign /usr/bin/cosign
ARG EARTHLY_GIT_HASH
ARG TARGETARCH
SAVE IMAGE --push "$IMAGE:$EARTHLY_GIT_HASH-prebuild-$TARGETARCH"
blue-build-cli:
ARG EARTHLY_GIT_HASH
ARG TARGETARCH
FROM "$IMAGE:$EARTHLY_GIT_HASH-prebuild-$TARGETARCH"
ARG EARTHLY_GIT_HASH
ARG TARGETARCH
FROM "$IMAGE:$EARTHLY_GIT_HASH-prebuild-$TARGETARCH"
IF [ "$TARGETARCH" = "arm64" ]
DO --pass-args +INSTALL --OUT_DIR="/usr/bin/" --BUILD_TARGET="aarch64-unknown-linux-gnu"
ELSE
DO --pass-args +INSTALL --OUT_DIR="/usr/bin/" --BUILD_TARGET="x86_64-unknown-linux-gnu"
END
IF [ "$TARGETARCH" = "arm64" ]
DO --pass-args +INSTALL --OUT_DIR="/usr/bin/" --BUILD_TARGET="aarch64-unknown-linux-gnu"
ELSE
DO --pass-args +INSTALL --OUT_DIR="/usr/bin/" --BUILD_TARGET="x86_64-unknown-linux-gnu"
END
RUN mkdir -p /bluebuild
WORKDIR /bluebuild
CMD ["bluebuild"]
RUN mkdir -p /bluebuild
WORKDIR /bluebuild
CMD ["bluebuild"]
DO --pass-args +SAVE_IMAGE
DO --pass-args +SAVE_IMAGE
blue-build-cli-alpine-prebuild:
ARG BASE_IMAGE="alpine"
FROM DOCKERFILE -f Dockerfile.alpine .
ARG BASE_IMAGE="alpine"
FROM DOCKERFILE -f Dockerfile.alpine .
COPY --platform=native (+digest/base-image-digest --BASE_IMAGE=$BASE_IMAGE) /base-image-digest
LABEL org.opencontainers.image.base.name="$BASE_IMAGE"
LABEL org.opencontainers.image.base.digest="$(cat /base-image-digest)"
COPY --platform=native (+digest/base-image-digest --BASE_IMAGE=$BASE_IMAGE) /base-image-digest
LABEL org.opencontainers.image.base.name="$BASE_IMAGE"
LABEL org.opencontainers.image.base.digest="$(cat /base-image-digest)"
COPY +cosign/cosign /usr/bin/cosign
COPY +cosign/cosign /usr/bin/cosign
ARG EARTHLY_GIT_HASH
ARG TARGETARCH
SAVE IMAGE --push "$IMAGE:$EARTHLY_GIT_HASH-alpine-prebuild-$TARGETARCH"
ARG EARTHLY_GIT_HASH
ARG TARGETARCH
SAVE IMAGE --push "$IMAGE:$EARTHLY_GIT_HASH-alpine-prebuild-$TARGETARCH"
blue-build-cli-alpine:
ARG EARTHLY_GIT_HASH
ARG TARGETARCH
FROM "$IMAGE:$EARTHLY_GIT_HASH-alpine-prebuild-$TARGETARCH"
ARG EARTHLY_GIT_HASH
ARG TARGETARCH
FROM "$IMAGE:$EARTHLY_GIT_HASH-alpine-prebuild-$TARGETARCH"
IF [ "$TARGETARCH" = "arm64" ]
DO --pass-args +INSTALL --OUT_DIR="/usr/bin/" --BUILD_TARGET="aarch64-unknown-linux-musl"
ELSE
DO --pass-args +INSTALL --OUT_DIR="/usr/bin/" --BUILD_TARGET="x86_64-unknown-linux-musl"
END
IF [ "$TARGETARCH" = "arm64" ]
DO --pass-args +INSTALL --OUT_DIR="/usr/bin/" --BUILD_TARGET="aarch64-unknown-linux-musl"
ELSE
DO --pass-args +INSTALL --OUT_DIR="/usr/bin/" --BUILD_TARGET="x86_64-unknown-linux-musl"
END
RUN mkdir -p /bluebuild
WORKDIR /bluebuild
CMD ["bluebuild"]
RUN mkdir -p /bluebuild
WORKDIR /bluebuild
CMD ["bluebuild"]
DO --pass-args +SAVE_IMAGE --SUFFIX="-alpine"
DO --pass-args +SAVE_IMAGE --SUFFIX="-alpine"
installer:
ARG BASE_IMAGE="alpine"
FROM $BASE_IMAGE
ARG BASE_IMAGE="alpine"
FROM $BASE_IMAGE
COPY --platform=native (+digest/base-image-digest --BASE_IMAGE=$BASE_IMAGE) /base-image-digest
LABEL org.opencontainers.image.base.name="$BASE_IMAGE"
LABEL org.opencontainers.image.base.digest="$(cat /base-image-digest)"
COPY --platform=native (+digest/base-image-digest --BASE_IMAGE=$BASE_IMAGE) /base-image-digest
LABEL org.opencontainers.image.base.name="$BASE_IMAGE"
LABEL org.opencontainers.image.base.digest="$(cat /base-image-digest)"
ARG TARGETARCH
IF [ "$TARGETARCH" = "arm64" ]
DO --pass-args +INSTALL --OUT_DIR="/out/" --BUILD_TARGET="aarch64-unknown-linux-musl"
ELSE
DO --pass-args +INSTALL --OUT_DIR="/out/" --BUILD_TARGET="x86_64-unknown-linux-musl"
END
ARG TARGETARCH
IF [ "$TARGETARCH" = "arm64" ]
DO --pass-args +INSTALL --OUT_DIR="/out/" --BUILD_TARGET="aarch64-unknown-linux-musl"
ELSE
DO --pass-args +INSTALL --OUT_DIR="/out/" --BUILD_TARGET="x86_64-unknown-linux-musl"
END
COPY install.sh /install.sh
COPY install.sh /install.sh
CMD ["cat", "/install.sh"]
CMD ["cat", "/install.sh"]
DO --pass-args +SAVE_IMAGE --SUFFIX="-installer"
SAVE ARTIFACT /out/bluebuild
DO --pass-args +SAVE_IMAGE --SUFFIX="-installer"
SAVE ARTIFACT /out/bluebuild
cosign:
FROM gcr.io/projectsigstore/cosign
SAVE ARTIFACT /ko-app/cosign
FROM gcr.io/projectsigstore/cosign
SAVE ARTIFACT /ko-app/cosign
digest:
FROM alpine
RUN apk update && apk add skopeo jq
FROM alpine
RUN apk update && apk add skopeo jq
ARG --required BASE_IMAGE
RUN skopeo inspect "docker://$BASE_IMAGE" | jq -r '.Digest' > /base-image-digest
SAVE ARTIFACT /base-image-digest
ARG --required BASE_IMAGE
RUN skopeo inspect "docker://$BASE_IMAGE" | jq -r '.Digest' > /base-image-digest
SAVE ARTIFACT /base-image-digest
version:
FROM rust
FROM rust
RUN apt-get update && apt-get install -y jq
RUN apt-get update && apt-get install -y jq
WORKDIR /app
COPY --keep-ts --dir src/ template/ recipe/ utils/ process/ /app
COPY --keep-ts Cargo.* /app
WORKDIR /app
COPY --keep-ts --dir src/ template/ recipe/ utils/ process/ /app
COPY --keep-ts Cargo.* /app
RUN /bin/bash -c 'set -eo pipefail; cargo metadata --no-deps --format-version 1 \
| jq -r ".packages[] | select(.name == \"blue-build\") .version" > /version'
RUN /bin/bash -c 'set -eo pipefail; cargo metadata --no-deps --format-version 1 \
| jq -r ".packages[] | select(.name == \"blue-build\") .version" > /version'
SAVE ARTIFACT /version
SAVE ARTIFACT /version
INSTALL:
FUNCTION
ARG TAGGED="false"
ARG --required BUILD_TARGET
ARG --required OUT_DIR
FUNCTION
ARG TAGGED="false"
ARG --required BUILD_TARGET
ARG --required OUT_DIR
ARG RELEASE="true"
IF [ "$TAGGED" = "true" ]
COPY --platform=native (+install/bluebuild --BUILD_TARGET="$BUILD_TARGET") $OUT_DIR
ELSE
COPY --platform=native (+install-all-features/bluebuild --BUILD_TARGET="$BUILD_TARGET") $OUT_DIR
END
IF [ "$TAGGED" = "true" ]
COPY --platform=native --pass-args +install/bluebuild $OUT_DIR
ELSE
COPY --platform=native --pass-args +install-all-features/bluebuild $OUT_DIR
END
SAVE_IMAGE:
FUNCTION
ARG SUFFIX=""
ARG IMAGE="$IMAGE"
ARG TAGGED="false"
FUNCTION
ARG SUFFIX=""
ARG IMAGE="$IMAGE"
ARG TAGGED="false"
COPY --platform=native +version/version /
ARG VERSION="$(cat /version)"
ARG MAJOR_VERSION="$(echo "$VERSION" | cut -d'.' -f1)"
ARG MINOR_VERSION="$(echo "$VERSION" | cut -d'.' -f2)"
ARG PATCH_VERSION="$(echo "$VERSION" | cut -d'.' -f3)"
ARG BUILD_TIME="$(date -Iseconds)"
DO --pass-args +LABELS
COPY --platform=native +version/version /
ARG VERSION="$(cat /version)"
ARG MAJOR_VERSION="$(echo "$VERSION" | cut -d'.' -f1)"
ARG MINOR_VERSION="$(echo "$VERSION" | cut -d'.' -f2)"
ARG PATCH_VERSION="$(echo "$VERSION" | cut -d'.' -f3)"
ARG BUILD_TIME="$(date -Iseconds)"
DO --pass-args +LABELS
IF [ "$TAGGED" = "true" ]
SAVE IMAGE --push "${IMAGE}:v${VERSION}${SUFFIX}"
IF [ "$TAGGED" = "true" ]
SAVE IMAGE --push "${IMAGE}:v${VERSION}${SUFFIX}"
ARG LATEST=false
IF [ "$LATEST" = "true" ]
SAVE IMAGE --push "${IMAGE}:latest${SUFFIX}"
SAVE IMAGE --push "${IMAGE}:v${MAJOR_VERSION}.${MINOR_VERSION}${SUFFIX}"
SAVE IMAGE --push "${IMAGE}:v${MAJOR_VERSION}${SUFFIX}"
END
ELSE
ARG EARTHLY_GIT_BRANCH
SAVE IMAGE --push "${IMAGE}:${EARTHLY_GIT_BRANCH}${SUFFIX}"
END
ARG EARTHLY_GIT_HASH
SAVE IMAGE --push "${IMAGE}:${EARTHLY_GIT_HASH}${SUFFIX}"
ARG LATEST=false
IF [ "$LATEST" = "true" ]
SAVE IMAGE --push "${IMAGE}:latest${SUFFIX}"
SAVE IMAGE --push "${IMAGE}:v${MAJOR_VERSION}.${MINOR_VERSION}${SUFFIX}"
SAVE IMAGE --push "${IMAGE}:v${MAJOR_VERSION}${SUFFIX}"
END
ELSE
ARG EARTHLY_GIT_BRANCH
SAVE IMAGE --push "${IMAGE}:${EARTHLY_GIT_BRANCH}${SUFFIX}"
END
ARG EARTHLY_GIT_HASH
SAVE IMAGE --push "${IMAGE}:${EARTHLY_GIT_HASH}${SUFFIX}"
LABELS:
FUNCTION
LABEL org.opencontainers.image.created="$BUILD_TIME"
LABEL org.opencontainers.image.url="https://github.com/blue-build/cli"
LABEL org.opencontainers.image.source="https://github.com/blue-build/cli"
LABEL org.opencontainers.image.version="$VERSION"
LABEL version="$VERSION"
LABEL org.opencontainers.image.vendor="BlueBuild"
LABEL vendor="BlueBuild"
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL license="Apache-2.0"
LABEL org.opencontainers.image.title="BlueBuild CLI tool"
LABEL name="blue-build/cli"
LABEL org.opencontainers.image.description="A CLI tool built for creating Containerfile templates for ostree based atomic distros"
LABEL org.opencontainers.image.documentation="https://raw.githubusercontent.com/blue-build/cli/main/README.md"
FUNCTION
LABEL org.opencontainers.image.created="$BUILD_TIME"
LABEL org.opencontainers.image.url="https://github.com/blue-build/cli"
LABEL org.opencontainers.image.source="https://github.com/blue-build/cli"
LABEL org.opencontainers.image.version="$VERSION"
LABEL version="$VERSION"
LABEL org.opencontainers.image.vendor="BlueBuild"
LABEL vendor="BlueBuild"
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL license="Apache-2.0"
LABEL org.opencontainers.image.title="BlueBuild CLI tool"
LABEL name="blue-build/cli"
LABEL org.opencontainers.image.description="A CLI tool built for creating Containerfile templates for ostree based atomic distros"
LABEL org.opencontainers.image.documentation="https://raw.githubusercontent.com/blue-build/cli/main/README.md"
ARG TAGGED="false"
IF [ "$TAGGED" = "true" ]
ARG EARTHLY_GIT_BRANCH
LABEL org.opencontainers.image.ref.name="$EARTHLY_GIT_BRANCH"
ELSE
LABEL org.opencontainers.image.ref.name="v$VERSION"
END
ARG TAGGED="false"
IF [ "$TAGGED" = "true" ]
ARG EARTHLY_GIT_BRANCH
LABEL org.opencontainers.image.ref.name="$EARTHLY_GIT_BRANCH"
ELSE
LABEL org.opencontainers.image.ref.name="v$VERSION"
END

View file

@ -89,6 +89,9 @@ watch = ["src", "process", "recipe", "template", "utils", "Cargo.toml", "build.r
# should go in your personal global prefs.toml file instead.
[keybindings]
# alt-m = "job:my-job"
c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target
i = "job:install-all"
t = "job:test-all"
c = "job:clippy"
shift-c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target
i = "job:install"
shift-i = "job:install-all"
t = "job:test"
shift-t = "job:test-all"

View file

@ -4,126 +4,137 @@ PROJECT blue-build/cli
IMPORT github.com/earthly/lib/utils/dind AS dind
all:
BUILD +test-image
BUILD +test-legacy-image
BUILD +build
BUILD +rebase
BUILD +upgrade
BUILD +switch
BUILD +test-image
BUILD +test-legacy-image
BUILD +build
BUILD +rebase
BUILD +upgrade
BUILD +switch
BUILD +validate
test-image:
FROM +build-template --src=template-containerfile
WORKDIR /tmp/test
COPY ./test-scripts/*.sh ./
FROM +build-template --src=template-containerfile
WORKDIR /tmp/test
COPY ./test-scripts/*.sh ./
DO +RUN_TESTS
DO +RUN_TESTS
test-legacy-image:
FROM +build-template --src=template-legacy-containerfile
WORKDIR /tmp/test
COPY ./test-scripts/*.sh ./
FROM +build-template --src=template-legacy-containerfile
WORKDIR /tmp/test
COPY ./test-scripts/*.sh ./
DO +RUN_TESTS
DO +RUN_TESTS
build-template:
ARG --required src
FROM DOCKERFILE \
-f +$src/test/Containerfile \
+$src/test/*
ARG --required src
FROM DOCKERFILE \
-f +$src/test/Containerfile \
+$src/test/*
template-containerfile:
FROM +test-base
RUN bluebuild -v generate recipes/recipe.yml | tee Containerfile
FROM +test-base
RUN --no-cache bluebuild -v generate recipes/recipe.yml | tee Containerfile
SAVE ARTIFACT /test
SAVE ARTIFACT /test
template-legacy-containerfile:
FROM +legacy-base
RUN bluebuild -v template config/recipe.yml | tee Containerfile
FROM +legacy-base
RUN --no-cache bluebuild -v template config/recipe.yml | tee Containerfile
SAVE ARTIFACT /test
SAVE ARTIFACT /test
build:
FROM +test-base
FROM +test-base
RUN bluebuild -v build recipes/recipe.yml
RUN bluebuild -v build recipes/recipe.yml
build-full:
FROM +test-base --MOCK="false"
FROM +test-base --MOCK="false"
DO dind+INSTALL
DO dind+INSTALL
ENV BB_USERNAME=gmpinder
ENV BB_REGISTRY=ghcr.io
ENV BB_REGISTRY_NAMESPACE=blue-build
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
WITH DOCKER
RUN --secret BB_PASSWORD=github/registry bluebuild build --push -S sigstore -vv recipes/recipe.yml
END
rebase:
FROM +legacy-base
FROM +legacy-base
RUN bluebuild -v rebase config/recipe.yml
RUN --no-cache bluebuild -v rebase config/recipe.yml
upgrade:
FROM +legacy-base
FROM +legacy-base
RUN mkdir -p /etc/bluebuild && touch $BB_TEST_LOCAL_IMAGE
RUN bluebuild -v upgrade config/recipe.yml
RUN mkdir -p /etc/bluebuild && touch $BB_TEST_LOCAL_IMAGE
RUN --no-cache bluebuild -v upgrade config/recipe.yml
switch:
FROM +test-base
FROM +test-base
RUN mkdir -p /etc/bluebuild && touch $BB_TEST_LOCAL_IMAGE
RUN bluebuild -v switch recipes/recipe.yml
RUN mkdir -p /etc/bluebuild && touch $BB_TEST_LOCAL_IMAGE
RUN --no-cache bluebuild -v switch recipes/recipe.yml
validate:
FROM +test-base
RUN --no-cache bluebuild -v validate recipes/recipe.yml
RUN --no-cache bluebuild -v validate recipes/recipe-39.yml
RUN --no-cache bluebuild -v validate recipes/recipe-arm64.yml
RUN --no-cache bluebuild -v validate recipes/recipe-invalid.yml && exit 1 || exit 0
RUN --no-cache bluebuild -v validate recipes/recipe-invalid-module.yml && exit 1 || exit 0
RUN --no-cache bluebuild -v validate recipes/recipe-invalid-stage.yml && exit 1 || exit 0
legacy-base:
FROM ../+blue-build-cli-alpine
RUN apk update --no-cache && apk add bash grep jq sudo coreutils
ENV BB_TEST_LOCAL_IMAGE=/etc/bluebuild/cli_test-legacy.tar.gz
ENV CLICOLOR_FORCE=1
FROM ../+blue-build-cli-alpine --RELEASE=false
RUN apk update --no-cache && apk add bash grep jq sudo coreutils
ENV BB_TEST_LOCAL_IMAGE=/etc/bluebuild/cli_test-legacy.tar.gz
ENV CLICOLOR_FORCE=1
COPY ./mock-scripts/ /usr/bin/
COPY ./mock-scripts/ /usr/bin/
WORKDIR /test
COPY ./legacy-test-repo /test
WORKDIR /test
COPY ./legacy-test-repo /test
DO ../+INSTALL --OUT_DIR="/usr/bin/" --BUILD_TARGET="x86_64-unknown-linux-musl" --TAGGED="true"
DO ../+INSTALL --OUT_DIR="/usr/bin/" --BUILD_TARGET="x86_64-unknown-linux-musl" --TAGGED="true"
DO +GEN_KEYPAIR
DO +GEN_KEYPAIR
test-base:
FROM ../+blue-build-cli-alpine
RUN apk update --no-cache && apk add bash grep jq sudo coreutils
ENV BB_TEST_LOCAL_IMAGE=/etc/bluebuild/cli_test.tar.gz
ENV CLICOLOR_FORCE=1
FROM ../+blue-build-cli-alpine --RELEASE=false
RUN apk update --no-cache && apk add bash grep jq sudo coreutils
ENV BB_TEST_LOCAL_IMAGE=/etc/bluebuild/cli_test.tar.gz
ENV CLICOLOR_FORCE=1
ARG MOCK="true"
IF [ "$MOCK" = "true" ]
COPY ./mock-scripts/ /usr/bin/
END
ARG MOCK="true"
IF [ "$MOCK" = "true" ]
COPY ./mock-scripts/ /usr/bin/
END
WORKDIR /test
COPY ./test-repo /test
WORKDIR /test
COPY ./test-repo /test
DO +GEN_KEYPAIR
DO +GEN_KEYPAIR
GEN_KEYPAIR:
FUNCTION
# Setup a cosign 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
FUNCTION
# Setup a cosign 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
RUN_TESTS:
FUNCTION
FOR script IN $(ls *.sh)
RUN --no-cache chmod +x $script \
&& echo "Running test $script" \
&& ./$script
END
FUNCTION
FOR script IN $(ls *.sh)
RUN --no-cache chmod +x $script \
&& echo "Running test $script" \
&& ./$script
END

View file

@ -35,6 +35,7 @@ modules:
- type: signing
- type: test-module
source: local
- type: containerfile
containerfiles:

View file

@ -1,3 +1,5 @@
---
# yaml-language-server: $schema=https://schema.blue-build.org/module-list-v1.json
modules:
# Tests installing rpms from a combo image stage
- type: akmods

View file

@ -1,6 +1,8 @@
---
# yaml-language-server: $schema=https://schema.blue-build.org/module-stage-list-v1.json
stages:
- name: blue-build
image: rust
from: rust
modules:
- type: script
scripts:

View file

@ -1,3 +1,5 @@
---
# yaml-language-server: $schema=https://schema.blue-build.org/module-v1.json
type: default-flatpaks
notify: true
system:

View file

@ -33,6 +33,7 @@ modules:
- type: signing
- type: test-module
source: local
- type: containerfile
containerfiles:

View file

@ -1,10 +1,12 @@
---
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test
description: This is my personal OS image.
base-image: quay.io/fedora/fedora-silverblue
image-version: 40
alt_tags:
alt-tags:
- arm64
stages:
stages: []
modules:
- from-file: flatpaks.yml
@ -29,6 +31,7 @@ modules:
- type: signing
- type: test-module
source: local
- type: containerfile
containerfiles:

View file

@ -0,0 +1,57 @@
---
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-invalid-module
description: This is my personal OS image.
base-image: ghcr.io/ublue-os/silverblue-main
image-version: 40
stages:
- from-file: stages.yml
modules:
- from-file: akmods.yml
- from-file: flatpaks.yml
- type: files
files:
- source: usr
destination: /usr
- type: script
scripts:
- example.sh
- type: rpm-ostree
repos:
- https://copr.fedorainfracloud.org/coprs/atim/starship/repo/fedora-%OS_VERSION%/atim-starship-fedora-%OS_VERSION%.repo
install: micro
installer: test
remove:
- firefox
- firefox-langpacks
- type: signing
- type: test-module
source: local
- type: containerfile
containerfiles:
labels: labels
snippets:
- RUN echo "This is a snippet" && ostree container commit
- type: copy
from: alpine-test
src: /test.txt
dest: /
- type: copy
from: ubuntu-test
src: /test.txt
dest: /
- type: copy
from: debian-test
src: /test.txt
dest: /
- type: copy
from: fedora-test
src: /test.txt
dest: /

View file

@ -0,0 +1,61 @@
---
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-invalid-stage
description: This is my personal OS image.
base-image: ghcr.io/ublue-os/silverblue-main
image-version: 40
stages:
- name: ubuntu-test
from:
- ubuntu
modules: {}
modules:
- from-file: akmods.yml
- from-file: flatpaks.yml
- type: files
files:
- source: usr
destination: /usr
- type: script
scripts:
- example.sh
- type: rpm-ostree
repos:
- https://copr.fedorainfracloud.org/coprs/atim/starship/repo/fedora-%OS_VERSION%/atim-starship-fedora-%OS_VERSION%.repo
install:
- micro
- starship
remove:
- firefox
- firefox-langpacks
- type: signing
- type: test-module
source: local
- type: containerfile
containerfiles:
- labels
snippets:
- RUN echo "This is a snippet" && ostree container commit
- type: copy
from: alpine-test
src: /test.txt
dest: /
- type: copy
from: ubuntu-test
src: /test.txt
dest: /
- type: copy
from: debian-test
src: /test.txt
dest: /
- type: copy
from: fedora-test
src: /test.txt
dest: /

View file

@ -0,0 +1,59 @@
---
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-invalid
description: 10
base-image: ghcr.io/ublue-os/silverblue-main
image-version:
- 40
- 39
stages: {}
modules:
- from-file: akmods.yml
- from-file: flatpaks.yml
- type: files
files:
- source: usr
destination: /usr
- type: script
scripts:
- example.sh
- type: rpm-ostree
repos:
- https://copr.fedorainfracloud.org/coprs/atim/starship/repo/fedora-%OS_VERSION%/atim-starship-fedora-%OS_VERSION%.repo
install:
- micro
- starship
remove:
- firefox
- firefox-langpacks
- type: signing
- type: test-module
source: local
- type: containerfile
containerfiles:
- labels
snippets:
- RUN echo "This is a snippet" && ostree container commit
- type: copy
from: alpine-test
src: /test.txt
dest: /
- type: copy
from: ubuntu-test
src: /test.txt
dest: /
- type: copy
from: debian-test
src: /test.txt
dest: /
- type: copy
from: fedora-test
src: /test.txt
dest: /

View file

@ -32,6 +32,7 @@ modules:
- type: signing
- type: test-module
source: local
- type: containerfile
containerfiles:

View file

@ -1,3 +1,5 @@
---
# yaml-language-server: $schema=https://schema.blue-build.org/module-stage-list-v1.json
stages:
- name: ubuntu-test
from: ubuntu
@ -25,6 +27,7 @@ modules:
snippets:
- echo "test" > /test.txt
- type: test-module
source: local
- type: containerfile
containerfiles:
- labels

View file

@ -24,7 +24,6 @@ rand = "0.8"
semver = { version = "1", features = ["serde"] }
signal-hook = { version = "0.3", features = ["extended-siginfo"] }
sigstore = { version = "0.10", features = ["full-rustls-tls", "cached-client", "sigstore-trust-root", "sign"], default-features = false, optional = true }
tokio = { version = "1.39.2", features = ["rt", "rt-multi-thread"], optional = true }
zeroize = { version = "1", features = ["aarch64", "derive", "serde"] }
cached.workspace = true
@ -36,9 +35,11 @@ indexmap.workspace = true
log.workspace = true
miette.workspace = true
oci-distribution.workspace = true
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
tempdir.workspace = true
tokio = { workspace = true, optional = true }
bon.workspace = true
users.workspace = true
uuid.workspace = true
@ -52,3 +53,4 @@ workspace = true
[features]
sigstore = ["dep:tokio", "dep:sigstore"]
validate = ["dep:tokio"]

View file

@ -1,5 +1,7 @@
//! This module is responsible for managing various strategies
//! to perform actions throughout the program. This hides all
//! 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.

View file

@ -2,7 +2,7 @@ use std::{fs, path::Path};
use crate::{
drivers::opts::{PrivateKeyContents, VerifyType},
RT,
ASYNC_RUNTIME,
};
use super::{
@ -135,7 +135,8 @@ impl SigningDriver for SigstoreDriver {
debug!("Credentials retrieved");
let (cosign_signature_image, source_image_digest) = retry(2, 5, || {
RT.block_on(client.triangulate(&image_digest, &auth))
ASYNC_RUNTIME
.block_on(client.triangulate(&image_digest, &auth))
.into_diagnostic()
.with_context(|| format!("Failed to triangulate image {image_digest}"))
})?;
@ -151,18 +152,19 @@ impl SigningDriver for SigstoreDriver {
debug!("Pushing signature");
retry(2, 5, || {
RT.block_on(client.push_signature(
None,
&auth,
&cosign_signature_image,
vec![signature_layer.clone()],
))
.into_diagnostic()
.with_context(|| {
format!(
ASYNC_RUNTIME
.block_on(client.push_signature(
None,
&auth,
&cosign_signature_image,
vec![signature_layer.clone()],
))
.into_diagnostic()
.with_context(|| {
format!(
"Failed to push signature {cosign_signature_image} for image {image_digest}"
)
})
})
})?;
debug!("Successfully pushed signature");
@ -196,19 +198,21 @@ impl SigningDriver for SigstoreDriver {
debug!("Triangulating image");
let auth = Auth::Anonymous;
let (cosign_signature_image, source_image_digest) = retry(2, 5, || {
RT.block_on(client.triangulate(&image_digest, &auth))
ASYNC_RUNTIME
.block_on(client.triangulate(&image_digest, &auth))
.into_diagnostic()
.with_context(|| format!("Failed to triangulate image {image_digest}"))
})?;
trace!("{cosign_signature_image}, {source_image_digest}");
let trusted_layers = retry(2, 5, || {
RT.block_on(client.trusted_signature_layers(
&auth,
&source_image_digest,
&cosign_signature_image,
))
.into_diagnostic()
ASYNC_RUNTIME
.block_on(client.trusted_signature_layers(
&auth,
&source_image_digest,
&cosign_signature_image,
))
.into_diagnostic()
})?;
sigstore::cosign::verify_constraints(&trusted_layers, verification_constraints.iter())

View file

@ -2,17 +2,17 @@
//! by this tool. It contains drivers for running, building, inspecting, and signing
//! images that interface with tools like docker or podman.
#[cfg(feature = "sigstore")]
#[cfg(any(feature = "sigstore", feature = "validate"))]
use once_cell::sync::Lazy;
#[cfg(feature = "sigstore")]
#[cfg(any(feature = "sigstore", feature = "validate"))]
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(|| {
#[cfg(any(feature = "sigstore", feature = "validate"))]
pub static ASYNC_RUNTIME: Lazy<Runtime> = Lazy::new(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()

View file

@ -5,9 +5,32 @@ pub mod recipe;
pub mod stage;
pub mod stages_ext;
use std::path::{Path, PathBuf};
use blue_build_utils::constants::{CONFIG_PATH, RECIPE_PATH};
use log::warn;
pub use akmods_info::*;
pub use module::*;
pub use module_ext::*;
pub use recipe::*;
pub use stage::*;
pub use stages_ext::*;
pub trait FromFileList {
const LIST_KEY: &str;
fn get_from_file_paths(&self) -> Vec<PathBuf>;
}
pub(crate) fn base_recipe_path() -> &'static Path {
let legacy_path = Path::new(CONFIG_PATH);
let recipe_path = Path::new(RECIPE_PATH);
if recipe_path.exists() && recipe_path.is_dir() {
recipe_path
} else {
warn!("Use of {CONFIG_PATH} for recipes is deprecated, please move your recipe files into {RECIPE_PATH}");
legacy_path
}
}

View file

@ -9,7 +9,7 @@ use miette::{bail, Result};
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use crate::{AkmodsInfo, ModuleExt};
use crate::{base_recipe_path, AkmodsInfo, ModuleExt};
#[derive(Serialize, Deserialize, Debug, Clone, Builder, Default)]
pub struct ModuleRequiredFields<'a> {
@ -80,6 +80,17 @@ impl<'a> ModuleRequiredFields<'a> {
}
}
#[must_use]
pub fn get_non_local_source(&'a self) -> Option<&'a str> {
let source = self.source.as_deref()?;
if source == "local" {
None
} else {
Some(source)
}
}
#[must_use]
pub fn generate_akmods_info(&'a self, os_version: &u64) -> AkmodsInfo {
#[derive(Debug, Copy, Clone)]
@ -164,7 +175,7 @@ pub struct Module<'a> {
pub from_file: Option<Cow<'a, str>>,
}
impl<'a> Module<'a> {
impl Module<'_> {
/// Get's any child modules.
///
/// # Errors
@ -202,7 +213,7 @@ impl<'a> Module<'a> {
traversed_files.push(file_name.clone());
Self::get_modules(
&ModuleExt::parse(&file_name)?.modules,
&ModuleExt::try_from(&file_name)?.modules,
Some(traversed_files),
)?
}
@ -224,6 +235,13 @@ impl<'a> Module<'a> {
Ok(found_modules)
}
#[must_use]
pub fn get_from_file_path(&self) -> Option<PathBuf> {
self.from_file
.as_ref()
.map(|path| base_recipe_path().join(&**path))
}
#[must_use]
pub fn example() -> Self {
Self::builder()

View file

@ -1,12 +1,15 @@
use std::{collections::HashSet, fs, path::Path};
use std::{
collections::HashSet,
fs,
path::{Path, PathBuf},
};
use blue_build_utils::constants::{CONFIG_PATH, RECIPE_PATH};
use bon::Builder;
use log::{trace, warn};
use miette::{Context, IntoDiagnostic, Result};
use log::trace;
use miette::{Context, IntoDiagnostic, Report, Result};
use serde::{Deserialize, Serialize};
use crate::{AkmodsInfo, Module};
use crate::{base_recipe_path, AkmodsInfo, FromFileList, Module};
#[derive(Default, Serialize, Clone, Deserialize, Debug, Builder)]
pub struct ModuleExt<'a> {
@ -14,22 +17,31 @@ pub struct ModuleExt<'a> {
pub modules: Vec<Module<'a>>,
}
impl ModuleExt<'_> {
/// Parse a module file returning a [`ModuleExt`]
///
/// # Errors
/// Can return an `anyhow` Error if the file cannot be read or deserialized
/// into a [`ModuleExt`]
pub fn parse(file_name: &Path) -> Result<Self> {
let legacy_path = Path::new(CONFIG_PATH);
let recipe_path = Path::new(RECIPE_PATH);
impl FromFileList for ModuleExt<'_> {
const LIST_KEY: &'static str = "modules";
let file_path = if recipe_path.exists() && recipe_path.is_dir() {
recipe_path.join(file_name)
} else {
warn!("Use of {CONFIG_PATH} for recipes is deprecated, please move your recipe files into {RECIPE_PATH}");
legacy_path.join(file_name)
};
#[must_use]
fn get_from_file_paths(&self) -> Vec<PathBuf> {
self.modules
.iter()
.filter_map(Module::get_from_file_path)
.collect()
}
}
impl TryFrom<&PathBuf> for ModuleExt<'_> {
type Error = Report;
fn try_from(value: &PathBuf) -> std::result::Result<Self, Self::Error> {
Self::try_from(value.as_path())
}
}
impl TryFrom<&Path> for ModuleExt<'_> {
type Error = Report;
fn try_from(file_name: &Path) -> Result<Self> {
let file_path = base_recipe_path().join(file_name);
let file = fs::read_to_string(&file_path)
.into_diagnostic()
@ -45,7 +57,9 @@ impl ModuleExt<'_> {
Ok,
)
}
}
impl ModuleExt<'_> {
#[must_use]
pub fn get_akmods_info_list(&self, os_version: &u64) -> Vec<AkmodsInfo> {
trace!("get_akmods_image_list({self:#?}, {os_version})");

View file

@ -1,12 +1,10 @@
use std::{borrow::Cow, fs, path::Path};
use bon::Builder;
use indexmap::IndexMap;
use log::{debug, trace};
use miette::{Context, IntoDiagnostic, Result};
use oci_distribution::Reference;
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use crate::{Module, ModuleExt, StagesExt};
@ -69,13 +67,6 @@ pub struct Recipe<'a> {
/// This holds the list of modules to be run on the image.
#[serde(flatten)]
pub modules_ext: ModuleExt<'a>,
/// Extra data that the user might have added. This is
/// done in case we serialize the data to a yaml file
/// so that we retain any unused information.
#[serde(flatten)]
#[builder(into)]
pub extra: IndexMap<String, Value>,
}
impl<'a> Recipe<'a> {

View file

@ -6,7 +6,7 @@ use colored::Colorize;
use miette::{bail, Result};
use serde::{Deserialize, Serialize};
use crate::{Module, ModuleExt, StagesExt};
use crate::{base_recipe_path, Module, ModuleExt, StagesExt};
/// Contains the required fields for a stage.
#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
@ -86,7 +86,7 @@ pub struct Stage<'a> {
pub from_file: Option<Cow<'a, str>>,
}
impl<'a> Stage<'a> {
impl Stage<'_> {
/// Get's any child stages.
///
/// # Errors
@ -119,7 +119,7 @@ impl<'a> Stage<'a> {
let mut tf = traversed_files.clone();
tf.push(file_name.clone());
Self::get_stages(&StagesExt::parse(&file_name)?.stages, Some(tf))?
Self::get_stages(&StagesExt::try_from(&file_name)?.stages, Some(tf))?
}
_ => {
let from_example = Stage::builder().from_file("path/to/stage.yml").build();
@ -139,6 +139,13 @@ impl<'a> Stage<'a> {
Ok(found_stages)
}
#[must_use]
pub fn get_from_file_path(&self) -> Option<PathBuf> {
self.from_file
.as_ref()
.map(|path| base_recipe_path().join(&**path))
}
#[must_use]
pub fn example() -> Self {
Stage::builder()

View file

@ -1,12 +1,13 @@
use std::{fs, path::Path};
use std::{
fs,
path::{Path, PathBuf},
};
use blue_build_utils::constants::{CONFIG_PATH, RECIPE_PATH};
use bon::Builder;
use log::warn;
use miette::{Context, IntoDiagnostic, Result};
use miette::{Context, IntoDiagnostic, Report, Result};
use serde::{Deserialize, Serialize};
use crate::{Module, Stage};
use crate::{base_recipe_path, FromFileList, Module, Stage};
#[derive(Default, Serialize, Clone, Deserialize, Debug, Builder)]
pub struct StagesExt<'a> {
@ -14,22 +15,31 @@ pub struct StagesExt<'a> {
pub stages: Vec<Stage<'a>>,
}
impl<'a> StagesExt<'a> {
/// Parse a module file returning a [`StagesExt`]
///
/// # Errors
/// Can return an `anyhow` Error if the file cannot be read or deserialized
/// into a [`StagesExt`]
pub fn parse(file_name: &Path) -> Result<Self> {
let legacy_path = Path::new(CONFIG_PATH);
let recipe_path = Path::new(RECIPE_PATH);
impl FromFileList for StagesExt<'_> {
const LIST_KEY: &'static str = "stages";
let file_path = if recipe_path.exists() && recipe_path.is_dir() {
recipe_path.join(file_name)
} else {
warn!("Use of {CONFIG_PATH} for recipes is deprecated, please move your recipe files into {RECIPE_PATH}");
legacy_path.join(file_name)
};
#[must_use]
fn get_from_file_paths(&self) -> Vec<PathBuf> {
self.stages
.iter()
.filter_map(Stage::get_from_file_path)
.collect()
}
}
impl TryFrom<&PathBuf> for StagesExt<'_> {
type Error = Report;
fn try_from(value: &PathBuf) -> Result<Self> {
Self::try_from(value.as_path())
}
}
impl TryFrom<&Path> for StagesExt<'_> {
type Error = Report;
fn try_from(file_name: &Path) -> Result<Self> {
let file_path = base_recipe_path().join(file_name);
let file = fs::read_to_string(&file_path)
.into_diagnostic()

View file

@ -12,6 +12,7 @@ fn main() {
("hyper::proto", LevelFilter::Off),
("hyper_util", LevelFilter::Off),
("oci_distribution", LevelFilter::Off),
("reqwest", LevelFilter::Off),
])
.log_out_dir(args.log_out.clone())
.init();
@ -42,6 +43,9 @@ fn main() {
#[cfg(feature = "iso")]
CommandArgs::GenerateIso(mut command) => command.run(),
#[cfg(feature = "validate")]
CommandArgs::Validate(mut command) => command.run(),
CommandArgs::BugReport(mut command) => command.run(),
CommandArgs::Completions(mut command) => command.run(),

View file

@ -5,7 +5,7 @@ use log::error;
use clap::{command, crate_authors, Parser, Subcommand};
use clap_verbosity_flag::{InfoLevel, Verbosity};
use crate::shadow;
use crate::info::shadow;
pub mod bug_report;
pub mod build;
@ -15,6 +15,8 @@ pub mod generate;
pub mod generate_iso;
#[cfg(feature = "login")]
pub mod login;
#[cfg(feature = "validate")]
pub mod validate;
// #[cfg(feature = "init")]
// pub mod init;
#[cfg(not(feature = "switch"))]
@ -115,6 +117,11 @@ pub enum CommandArgs {
#[cfg(feature = "login")]
Login(login::LoginCommand),
/// Validate your recipe file and display
/// errors to help fix problems.
#[cfg(feature = "validate")]
Validate(Box<validate::ValidateCommand>),
// /// Initialize a new Ublue Starting Point repo
// #[cfg(feature = "init")]
// Init(init::InitCommand),

View file

@ -16,7 +16,7 @@ use std::time::Duration;
use super::BlueBuildCommand;
use crate::shadow;
use crate::info::shadow;
#[derive(Default, Debug, Clone, Builder, Args)]
pub struct BugReportRecipe {

View file

@ -18,7 +18,9 @@ use clap::{crate_version, Args};
use log::{debug, info, trace, warn};
use miette::{IntoDiagnostic, Result};
use crate::shadow;
#[cfg(feature = "validate")]
use crate::commands::validate::ValidateCommand;
use crate::info::shadow;
use super::BlueBuildCommand;
@ -99,6 +101,13 @@ impl GenerateCommand {
legacy_path.join(RECIPE_FILE)
}
});
#[cfg(feature = "validate")]
ValidateCommand::builder()
.recipe(recipe_path.clone())
.build()
.try_run()?;
let registry = if let (Some(registry), Some(registry_namespace)) =
(&self.registry, &self.registry_namespace)
{

405
src/commands/validate.rs Normal file
View file

@ -0,0 +1,405 @@
use std::{
fs::OpenOptions,
io::{BufReader, Read},
path::{Path, PathBuf},
};
use blue_build_process_management::ASYNC_RUNTIME;
use blue_build_recipe::{FromFileList, ModuleExt, Recipe, StagesExt};
use blue_build_utils::{
string,
syntax_highlighting::{self},
};
use bon::Builder;
use clap::Args;
use colored::Colorize;
use indexmap::IndexMap;
use jsonschema::{BasicOutput, ValidationError};
use log::{debug, info, trace};
use miette::{bail, miette, Context, IntoDiagnostic, Report};
use rayon::prelude::*;
use schema_validator::{
build_validator, SchemaValidator, MODULE_LIST_V1_SCHEMA_URL, MODULE_V1_SCHEMA_URL,
RECIPE_V1_SCHEMA_URL, STAGE_LIST_V1_SCHEMA_URL, STAGE_V1_SCHEMA_URL,
};
use serde::de::DeserializeOwned;
use serde_json::Value;
use super::BlueBuildCommand;
mod schema_validator;
#[derive(Debug, Args, Builder)]
pub struct ValidateCommand {
/// The path to the recipe.
///
/// NOTE: In order for this to work,
/// you must be in the root of your
/// bluebuild repository.
pub recipe: PathBuf,
/// Display all errors that failed
/// validation of the recipe.
#[arg(short, long)]
#[builder(default)]
pub all_errors: bool,
#[clap(skip)]
recipe_validator: Option<SchemaValidator>,
#[clap(skip)]
stage_validator: Option<SchemaValidator>,
#[clap(skip)]
stage_list_validator: Option<SchemaValidator>,
#[clap(skip)]
module_validator: Option<SchemaValidator>,
#[clap(skip)]
module_list_validator: Option<SchemaValidator>,
}
impl BlueBuildCommand for ValidateCommand {
fn try_run(&mut self) -> miette::Result<()> {
let recipe_path_display = self.recipe.display().to_string().bold().italic();
if !self.recipe.is_file() {
bail!("File {recipe_path_display} must exist");
}
ASYNC_RUNTIME.block_on(self.setup_validators())?;
if let Err(errors) = self.validate_recipe() {
let errors = errors.into_iter().fold(String::new(), |mut full, err| {
full.push_str(&format!("{err:?}"));
full
});
if self.all_errors {
bail!("Recipe {recipe_path_display} failed to validate:\n{errors}");
} else {
bail!(
help = format!(
"Use `{}` to view more information",
format!("bluebuild validate --all-errors {}", self.recipe.display()).bold(),
),
"Recipe {recipe_path_display} failed to validate:\n{errors}",
);
}
}
info!("Recipe {recipe_path_display} is valid");
Ok(())
}
}
impl ValidateCommand {
async fn setup_validators(&mut self) -> Result<(), Report> {
let (rv, sv, slv, mv, mlv) = tokio::try_join!(
build_validator(RECIPE_V1_SCHEMA_URL),
build_validator(STAGE_V1_SCHEMA_URL),
build_validator(STAGE_LIST_V1_SCHEMA_URL),
build_validator(MODULE_V1_SCHEMA_URL),
build_validator(MODULE_LIST_V1_SCHEMA_URL),
)?;
self.recipe_validator = Some(rv);
self.stage_validator = Some(sv);
self.stage_list_validator = Some(slv);
self.module_validator = Some(mv);
self.module_list_validator = Some(mlv);
Ok(())
}
fn validate_file<DF>(
&self,
path: &Path,
traversed_files: &[&Path],
single_validator: &SchemaValidator,
list_validator: &SchemaValidator,
) -> Vec<Report>
where
DF: DeserializeOwned + FromFileList,
{
let path_display = path.display().to_string().bold().italic();
if traversed_files.contains(&path) {
return vec![miette!(
"{} File {path_display} has already been parsed:\n{traversed_files:?}",
"Circular dependency detected!".bright_red(),
)];
}
let traversed_files = {
let mut files: Vec<&Path> = Vec::with_capacity(traversed_files.len() + 1);
files.extend_from_slice(traversed_files);
files.push(path);
files
};
let file_str = match read_file(path) {
Err(e) => return vec![e],
Ok(f) => f,
};
match serde_yaml::from_str::<Value>(&file_str)
.into_diagnostic()
.with_context(|| format!("Failed to deserialize file {path_display}"))
{
Ok(instance) => {
trace!("{path_display}:\n{instance}");
if instance.get(DF::LIST_KEY).is_some() {
debug!("{path_display} is a multi file file");
let errors = if self.all_errors {
process_basic_output(
list_validator.validator().apply(&instance).basic(),
&instance,
path,
)
} else {
list_validator
.validator()
.iter_errors(&instance)
.map(process_err(&self.recipe))
.collect()
};
if errors.is_empty() {
match serde_yaml::from_str::<DF>(&file_str).into_diagnostic() {
Err(e) => vec![e],
Ok(file) => file
.get_from_file_paths()
.par_iter()
.map(|file_path| {
self.validate_file::<DF>(
file_path,
&traversed_files,
single_validator,
list_validator,
)
})
.flatten()
.collect(),
}
} else {
errors
}
} else {
debug!("{path_display} is a single file file");
if self.all_errors {
process_basic_output(
single_validator.validator().apply(&instance).basic(),
&instance,
path,
)
} else {
single_validator
.validator()
.iter_errors(&instance)
.map(|err| miette!("{err}"))
.collect()
}
}
}
Err(e) => vec![e],
}
}
fn validate_recipe(&self) -> Result<(), Vec<Report>> {
let recipe_path_display = self.recipe.display().to_string().bold().italic();
debug!("Validating recipe {recipe_path_display}");
let recipe_str = read_file(&self.recipe).map_err(err_vec)?;
let recipe: Value = serde_yaml::from_str(&recipe_str)
.into_diagnostic()
.with_context(|| format!("Failed to deserialize recipe {recipe_path_display}"))
.map_err(err_vec)?;
trace!("{recipe_path_display}:\n{recipe}");
let schema_validator = self.recipe_validator.as_ref().unwrap();
let errors = if self.all_errors {
process_basic_output(
schema_validator.validator().apply(&recipe).basic(),
&recipe,
&self.recipe,
)
} else {
schema_validator
.validator()
.iter_errors(&recipe)
.map(process_err(&self.recipe))
.collect()
};
if errors.is_empty() {
let recipe: Recipe = serde_yaml::from_str(&recipe_str)
.into_diagnostic()
.with_context(|| {
format!("Unable to convert Value to Recipe for {recipe_path_display}")
})
.map_err(err_vec)?;
let mut errors: Vec<Report> = Vec::new();
if let Some(stages) = &recipe.stages_ext {
debug!("Validating stages for recipe {recipe_path_display}");
errors.extend(
stages
.get_from_file_paths()
.par_iter()
.map(|stage_path| {
debug!(
"Found 'from-file' reference in {recipe_path_display} going to {}",
stage_path.display().to_string().italic().bold()
);
self.validate_file::<StagesExt>(
stage_path,
&[],
self.stage_validator.as_ref().unwrap(),
self.stage_list_validator.as_ref().unwrap(),
)
})
.flatten()
.collect::<Vec<_>>(),
);
}
debug!("Validating modules for recipe {recipe_path_display}");
errors.extend(
recipe
.modules_ext
.get_from_file_paths()
.par_iter()
.map(|module_path| {
debug!(
"Found 'from-file' reference in {recipe_path_display} going to {}",
module_path.display().to_string().italic().bold()
);
self.validate_file::<ModuleExt>(
module_path,
&[],
self.module_validator.as_ref().unwrap(),
self.module_list_validator.as_ref().unwrap(),
)
})
.flatten()
.collect::<Vec<_>>(),
);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
} else {
Err(errors)
}
}
}
fn err_vec(err: Report) -> Vec<Report> {
vec![err]
}
fn read_file(path: &Path) -> Result<String, Report> {
let mut recipe = String::new();
BufReader::new(
OpenOptions::new()
.read(true)
.open(path)
.into_diagnostic()
.with_context(|| {
format!(
"Unable to open {}",
path.display().to_string().italic().bold()
)
})?,
)
.read_to_string(&mut recipe)
.into_diagnostic()?;
Ok(recipe)
}
fn process_basic_output(out: BasicOutput<'_>, instance: &Value, path: &Path) -> Vec<Report> {
match out {
BasicOutput::Valid(_) => vec![],
BasicOutput::Invalid(errors) => {
let mut collection: IndexMap<String, Vec<String>> = IndexMap::new();
let errors = {
let mut e = errors.into_iter().collect::<Vec<_>>();
e.sort_by(|e1, e2| {
e1.instance_location()
.as_str()
.cmp(e2.instance_location().as_str())
});
e
};
for err in errors {
let schema_path = err.keyword_location();
let instance_path = err.instance_location().to_string();
let build_err = || {
format!(
"{:?}",
miette!(
"schema_path:'{}'",
schema_path.to_string().italic().dimmed(),
)
.context(err.error_description().to_string().bold().bright_red())
)
};
collection
.entry(instance_path)
.and_modify(|errs| {
errs.push(build_err());
// errs.sort_by(|(path1, _), (path2, _)| path1.cmp(path2));
})
.or_insert_with(|| vec![build_err()]);
}
collection
.into_iter()
.map(|(key, value)| {
let instance = instance.pointer(&key).unwrap();
miette!(
"In file {} at '{}':\n\n{}\n{}",
path.display().to_string().bold().italic(),
key.bold().bright_yellow(),
serde_yaml::to_string(instance)
.into_diagnostic()
.and_then(|file| syntax_highlighting::highlight(&file, "yml", None))
.unwrap_or_else(|_| instance.to_string()),
value.into_iter().collect::<String>()
)
})
.collect()
}
}
}
fn process_err<'a, 'b>(path: &'b Path) -> impl Fn(ValidationError<'a>) -> Report + use<'a, 'b> {
move |ValidationError {
instance,
instance_path,
kind: _,
schema_path: _,
}| {
miette!(
"- Invalid value {} file '{}':\n{}",
if instance_path.as_str().is_empty() {
string!("in root of")
} else {
format!(
"at path '{}' in",
instance_path.as_str().bold().bright_yellow()
)
},
path.display().to_string().italic().bold(),
&serde_yaml::to_string(&*instance)
.into_diagnostic()
.and_then(|file| syntax_highlighting::highlight(&file, "yml", None))
.unwrap_or_else(|_| instance.to_string())
)
}
}

View file

@ -0,0 +1,105 @@
use std::sync::Arc;
use blue_build_process_management::ASYNC_RUNTIME;
use cached::proc_macro::cached;
use colored::Colorize;
use jsonschema::{Retrieve, Uri, Validator};
use log::{debug, trace};
use miette::{bail, Context, IntoDiagnostic, Report};
use serde_json::Value;
pub const BASE_SCHEMA_URL: &str = "https://schema.blue-build.org";
pub const RECIPE_V1_SCHEMA_URL: &str = "https://schema.blue-build.org/recipe-v1.json";
pub const STAGE_V1_SCHEMA_URL: &str = "https://schema.blue-build.org/stage-v1.json";
pub const STAGE_LIST_V1_SCHEMA_URL: &str = "https://schema.blue-build.org/stage-list-v1.json";
pub const MODULE_V1_SCHEMA_URL: &str = "https://schema.blue-build.org/module-v1.json";
pub const MODULE_LIST_V1_SCHEMA_URL: &str = "https://schema.blue-build.org/module-list-v1.json";
#[derive(Debug, Clone)]
pub struct SchemaValidator {
schema: Arc<Value>,
validator: Arc<Validator>,
}
impl SchemaValidator {
pub fn validator(&self) -> Arc<Validator> {
self.validator.clone()
}
pub fn schema(&self) -> Arc<Value> {
self.schema.clone()
}
}
pub async fn build_validator(url: &'static str) -> Result<SchemaValidator, Report> {
tokio::spawn(async move {
let schema: Arc<Value> = Arc::new(
reqwest::get(url)
.await
.into_diagnostic()
.with_context(|| format!("Failed to get schema at {url}"))?
.json()
.await
.into_diagnostic()
.with_context(|| format!("Failed to get json for schema {url}"))?,
);
let validator = Arc::new(
tokio::task::spawn_blocking({
let schema = schema.clone();
move || {
jsonschema::options()
.with_retriever(ModuleSchemaRetriever)
.build(&schema)
.into_diagnostic()
.with_context(|| format!("Failed to build validator for schema {url}"))
}
})
.await
.expect("Should join blocking thread")?,
);
Ok(SchemaValidator { schema, validator })
})
.await
.expect("Should join task")
}
struct ModuleSchemaRetriever;
impl Retrieve for ModuleSchemaRetriever {
fn retrieve(
&self,
uri: &Uri<&str>,
) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
Ok(ASYNC_RUNTIME.block_on(cache_retrieve(uri))?)
}
}
#[cached(result = true, key = "String", convert = r#"{ format!("{uri}") }"#)]
async fn cache_retrieve(uri: &Uri<&str>) -> miette::Result<Value> {
let scheme = uri.scheme();
let path = uri.path();
let uri = match scheme.as_str() {
"json-schema" => {
format!("{BASE_SCHEMA_URL}{path}")
}
"https" => uri.to_string(),
scheme => bail!("Unknown scheme {scheme}"),
};
debug!("Retrieving schema from {}", uri.bold().italic());
tokio::spawn(async move {
reqwest::get(&uri)
.await
.into_diagnostic()
.with_context(|| format!("Failed to retrieve schema from {uri}"))?
.json()
.await
.into_diagnostic()
.with_context(|| format!("Failed to parse json from {uri}"))
.inspect(|value| trace!("{}:\n{value}", uri.bold().italic()))
})
.await
.expect("Should join task")
}

View file

@ -2,7 +2,10 @@
#![doc = include_str!("../README.md")]
#![allow(clippy::needless_raw_string_hashes)]
shadow_rs::shadow!(shadow);
pub(crate) mod info {
#![allow(clippy::too_long_first_doc_paragraph)]
shadow_rs::shadow!(shadow);
}
pub mod commands;
pub mod rpm_ostree_status;

View file

@ -17,7 +17,7 @@ RUN \
{%- else if self::config_dir_exists() %}
--mount=type=bind,from=stage-config,src=/config,dst=/tmp/config,rw \
{%- endif %}
{%- if let Some(source) = module.source %}
{%- if let Some(source) = module.get_non_local_source() %}
--mount=type=bind,from={{ source }},src=/modules,dst=/tmp/modules,rw \
{%- else %}
--mount=type=bind,from=stage-modules,src=/modules,dst=/tmp/modules,rw \
@ -33,6 +33,7 @@ RUN \
{%- endif %}
{%- endfor %}
{% endmacro %}
{% macro stage_modules_run(modules_ext, os_version) %}
# Module RUNs
{%- for module in modules_ext.modules %}
@ -53,7 +54,7 @@ RUN \
{%- else if self::config_dir_exists() %}
--mount=type=bind,from=stage-config,src=/config,dst=/tmp/config,rw \
{%- endif %}
{%- if let Some(source) = module.source %}
{%- if let Some(source) = module.get_non_local_source() %}
--mount=type=bind,from={{ source }},src=/modules,dst=/tmp/modules,rw \
{%- else %}
--mount=type=bind,from=stage-modules,src=/modules,dst=/tmp/modules,rw \