diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d787b70 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +/target +/result diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..fcdb020 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,45 @@ +name: Create and push a Docker image + +on: + push: + branches: ['main'] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + VERSION: $GITHUB_SHA + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create version tag + id: version + run: echo "tag=$(git show -s --format="%ct-%h" $GITHUB_SHA)" >> $GITHUB_OUTPUT + + - name: Log in to the container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: docker/app.Dockerfile + pull: true + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.tag }} + build-args: VERSION=${{ steps.version.outputs.tag }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d787b70 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/result diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1af2597 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1503 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aes-ctr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7729c3cde54d67063be556aeac75a81330d802f0259500ca40cb52967f975763" +dependencies = [ + "aes-soft", + "aesni", + "cipher", + "ctr", +] + +[[package]] +name = "aes-soft" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +dependencies = [ + "cipher", + "opaque-debug", +] + +[[package]] +name = "aesni" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +dependencies = [ + "cipher", + "opaque-debug", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "ansible-vault" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8e42972d7396345d7695c54bc6d4cc1054f21729627d503ff091c962dd51f0" +dependencies = [ + "aes-ctr", + "block-padding", + "hex", + "hmac", + "pbkdf2", + "rand", + "sha2 0.9.9", +] + +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", + "humansize", + "num-traits", + "percent-encoding", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn 2.0.60", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "basic-toml" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "bstr" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "cc" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets", +] + +[[package]] +name = "chrono-tz" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" +dependencies = [ + "cipher", +] + +[[package]] +name = "deunicode" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322ef0094744e63628e6f0eb2295517f79276a5b342a4c2ff3042566ca181d4e" + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "fastrand" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kuberwave" +version = "0.1.0" +dependencies = [ + "ansible-vault", + "askama", + "base64", + "clap", + "failure", + "itertools", + "serde", + "serde_derive", + "serde_json", + "serde_yaml", + "slugify", + "tempfile", + "tera", + "yaml-rust", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + +[[package]] +name = "password-hash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54986aa4bfc9b98c6a5f40184223658d187159d7b3c6af33f2b2aa25ae1db0fa" +dependencies = [ + "base64ct", + "rand_core", +] + +[[package]] +name = "pbkdf2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf916dd32dd26297907890d99dc2740e33f6bd9073965af4ccff2967962f5508" +dependencies = [ + "base64ct", + "crypto-mac", + "hmac", + "password-hash", + "sha2 0.9.9", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "pest_meta" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca" +dependencies = [ + "once_cell", + "pest", + "sha2 0.10.8", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "serde_json" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slug" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "slugify" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b8cf203d2088b831d7558f8e5151bfa420c57a34240b28cee29d0ae5f2ac8b" +dependencies = [ + "unidecode", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys", +] + +[[package]] +name = "tera" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "unidecode" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.60", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..021d361 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "kuberwave" +description = "generate Kubernetes configurations from the commandline" +version = "0.1.0" +authors = ["Wouter Geraedts ", "Marlon Baeten "] +edition = "2018" + +[dependencies] +failure = "0.1" +clap = "2.34" +serde = "1.0" +serde_derive = "1.0" +serde_yaml = "0.9" +serde_json = "1.0" +yaml-rust = "0.4" +askama = "0.12" +tera = "1.19" +base64 = "0.13" +ansible-vault = "0.2" +slugify = "0.1.0" +itertools = "0.12" +tempfile = "3.2" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c51bf7b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM ghcr.io/tweedegolf/rust-dev:stable AS builder +WORKDIR /app +COPY . /app +RUN cargo build --release + +FROM ghcr.io/tweedegolf/debian:bookworm +# install kubectl +RUN curl -s -L https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - \ + && echo "deb http://packages.cloud.google.com/apt cloud-sdk-stretch main" > /etc/apt/sources.list.d/google-cloud-sdk.list \ + && apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + google-cloud-sdk \ + kubectl \ + && rm -rf /var/lib/apt/lists/* +# install age +ENV AGE_VERSION=1.1.1 +RUN curl -s -L https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/age-v${AGE_VERSION}-linux-amd64.tar.gz -o /tmp/age.tar.gz \ + && tar xvf /tmp/age.tar.gz -C /tmp \ + && mv /tmp/age/age /usr/local/bin/age \ + && mv /tmp/age/age-keygen /usr/local/bin/age-keygen \ + && rm -rf /tmp/{age,age.tar.gz} +# install sops +ENV SOPS_VERSION=3.8.1 +RUN curl -s -L https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux -o /usr/local/bin/sops \ + && chmod 0755 /usr/local/bin/sops +# copy executable +COPY --from=builder /app/target/release/kuberwave /app/kuberwave +# run kuberwave +CMD ["/app/kuberwave"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..f2c0d67 --- /dev/null +++ b/README.md @@ -0,0 +1,181 @@ +# Kuberwave + +Conveniently generate and deploy Kubernetes projects for real production applications. + +Kuberwave provides: +* A relatively compact form to write deployments in. +* Frequently occurring patterns such as ingress and certificate definitions. +* Multi-environment inventories similar to Ansible. +* A convenient method of storing secrets in your repositories with ansible-vault. +* Checks whether you have provided all required environment variables. +* Locally deploy your projects in a reproducible manner. +* Conveniently deploy on your CI servers. + +``` +# ./target/debug/kuberwave -h +kuberwave 0.1.0 +generate Kubernetes configurations from the commandline + +USAGE: + kuberwave [SUBCOMMAND] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +SUBCOMMANDS: + cluster-generate Generates a cluster configuration and writes to a directory + deploy Deploys a configuration to the current cluster + generate Generates a configuration and writes to a directory + help Prints this message or the help of the given subcommand(s) +``` + +## Projects + +### Generate + +``` +# ./target/debug/kuberwave generate -h +kuberwave-generate +Generates a configuration and writes to a directory + +USAGE: + kuberwave generate [FLAGS] [OPTIONS] + +FLAGS: + -d, --dry-run Do not actually write the configuration + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + -i, --inventory Path to inventory file + -o, --output Path to output directory [default: ./result] + +ARGS: + Path to manifest file +``` + +### Deploy +Directly deploy to a Kubernetes cluster. +Deploys to `kubectl` default cluster or to the default cluster specified with `--kubeconfig`. +Optionally you can authenticate to the cluster with a separate `--token`. + +This command is primarily designed to be used by Continuous Integration (CI) environments. + +``` +# ./target/debug/kuberwave deploy -h +kuberwave-deploy +Deploys a configuration to the current cluster + +USAGE: + kuberwave deploy [FLAGS] [OPTIONS] + +FLAGS: + -d, --dry-run Do not actually write the configuration + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + -i, --inventory Path to inventory file + -c, --kubeconfig Path to kubeconfig file + -t, --token Path to token file, encrypted with SECRET + +ARGS: + Path to manifest file +``` + +### Getting deploy tokens +Use the `bin/extract-token.sh` script to quickly get the deploy token for a +basic service account generated after running kubectl on the files generated by +`cluster-generate`. If this doesn't work, follow these steps to get the deploy +token manually: You can fetch the token by executing the following with +`kubectl`: + +``` +# kubectl describe serviceaccount example-ci +Name: example-ci +Namespace: default +Labels: +Annotations: kubectl.kubernetes.io/last-applied-configuration: + {"apiVersion":"v1","kind":"ServiceAccount","metadata":{"annotations":{},"name":"example-ci","namespace":"default"}} +Image pull secrets: +Mountable secrets: example-ci-token-rpg9x +Tokens: example-ci-token-rpg9x +Events: + +# kubectl get secret example-ci-token-rpg9x -o=jsonpath='{.data.token}'| base64 --decode + +``` + +Service accounts are populated using `cluster-generate`. Only kubernetes +cluster admins can deploy service accounts, so ask your local admin to get it +for you. Deploy tokens need to be encrypted using `ansible-vault` and your +`SECRET`. + +## Inspect serviceaccount privileges +An admin can inspect the privileges handed out to all service account *per namespace* using the following invocation or similar: + +```kubectl auth can-i --as system:serviceaccount:default:woz-viewer-ci --list -n woz-viewer-production``` + +Currently we provide two clusterroles via clustergenerate: +* *role-all*: a role that gives all permissions. When bound as a RoleBinding for a specific namespace, will only grant all permissions for that namespace, except for changing more RoleBindings and Roles. When handed out as an ClusterRoleBinding, will grant all permissions. +* *role-view-unprivileged*: a role that gives read-only (read, watch, list) privileges on all objects, except for Secrets. + +Generally all service accounts related to users get cluster-wide view-unprivileged access, and global access for concrete namespaces. + +## Cluster (generate) + +Generates specific cluster definition files such as serviceaccounts and rolebindings. + +``` +# ./target/debug/kuberwave cluster-generate -h +kuberwave-cluster-generate +Generates a cluster configuration and writes to a directory + +USAGE: + kuberwave cluster-generate [OPTIONS] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + -o, --output Path to output directory [default: ./result] + +ARGS: + Path to manifest file +``` + +As a Kubernetes cluster admin you can deploy the resulting files by running: + +``` +# kubectl apply -Rf ./result/apply +# kubectl auth reconcile -f ./result/auth +``` + +## Running in docker +You can run `kuberwave` in docker such that it is reproducible, both locally and on a CI-server. +Here is an example script for a typical deployment with an inventory. + +You need to set: +* `SECRET`: your ansible vault password +* `ENV`: to the name of your inventory + +``` +#!/bin/bash +set -e + +echo "Deploying $TAG to $ENV" + +docker pull ghcr.io/tweedegolf/kuberwave:latest +docker run \ + -e SECRET="$SECRET" \ + -v `pwd`:/app \ + -w /app/deployment \ + ghcr.io/tweedegolf/kuberwave:latest \ + kuberwave deploy \ + --token=./deploy.token \ + --kubeconfig=./kubeconfig.yml \ + --inventory="./inventory/$ENV.yml" \ + ./manifest.yml +``` diff --git a/src/cluster/mod.rs b/src/cluster/mod.rs new file mode 100644 index 0000000..df48381 --- /dev/null +++ b/src/cluster/mod.rs @@ -0,0 +1,2 @@ +pub mod templates; +pub mod types; diff --git a/src/cluster/templates.rs b/src/cluster/templates.rs new file mode 100644 index 0000000..f6595de --- /dev/null +++ b/src/cluster/templates.rs @@ -0,0 +1,180 @@ +use askama::Template; + +use crate::cluster::types::*; +use crate::resourcefile::{Resourcefile, Resourceproto}; + +#[derive(Template)] +#[template(path = "cluster/namespace.yml")] +pub struct NamespaceTemplate<'a> { + name: &'a str, +} + +impl<'a> NamespaceTemplate<'a> { + pub fn instantiate(ns: &'a Namespace) -> Resourcefile { + let mut rsrc = (Resourceproto { + name: &format!("namespace-{}.yml", &ns.name), + prototype: Self { name: &ns.name }, + }) + .render(); + + rsrc.append(ResourcequotaTemplate::instantiate(&ns.limits, &ns.name)); + + rsrc + } +} + +#[derive(Template)] +#[template(path = "cluster/serviceaccount.yml")] +pub struct ServiceAccountTemplate<'a> { + name: &'a str, +} + +impl<'a> ServiceAccountTemplate<'a> { + pub fn instantiate(u: &'a User) -> Resourcefile { + (Resourceproto { + name: &format!("sa-{}.yml", &u.name.single()), + prototype: Self { + name: u.name.single(), + }, + }) + .render() + } +} + +#[derive(Template)] +#[template(path = "cluster/clusterrolebinding.yml")] +pub struct ClusterRolebindingTemplate<'a> { + name: &'a str, + rolename: &'a str, + usernames: Vec<&'a str>, + kind: UserKind, +} + +impl<'a> ClusterRolebindingTemplate<'a> { + pub fn instantiate( + permission: &ClusterRole, + username: &'a UserNames, + kind: UserKind, + ) -> Resourcefile { + let rolename: &str = match permission { + ClusterRole::All => "kuberwave-all", + ClusterRole::View => "kuberwave-view", + }; + + let userfilename = match kind { + UserKind::ServiceAccount => username.single().clone(), + UserKind::User => username.get_file_name(), + }; + + let name = &format!("{}-{}", rolename, userfilename); + + (Resourceproto { + name: &format!("clusterrolebinding-{}.yml", name), + prototype: ClusterRolebindingTemplate { + name, + rolename, + usernames: username.vec(), + kind, + }, + }) + .render() + } +} + +#[derive(Template)] +#[template(path = "cluster/rolebinding.yml")] +pub struct RolebindingTemplate<'a> { + name: &'a str, + namespace: &'a str, + bindingkind: &'a str, + rolename: &'a str, + usernames: Vec<&'a str>, + subjectkind: UserKind, +} + +impl<'a> RolebindingTemplate<'a> { + pub fn instantiate( + permission: &ClusterRole, + namespace: &'a str, + username: &'a UserNames, + kind: UserKind, + ) -> Resourcefile { + let rolename: &str = match permission { + ClusterRole::All => "kuberwave-all", + ClusterRole::View => "kuberwave-view", + }; + + let userfilename = match kind { + UserKind::ServiceAccount => username.single().clone(), + UserKind::User => username.get_file_name(), + }; + + let name = &format!("{}-{}-{}", namespace, rolename, userfilename); + + (Resourceproto { + name: &format!("rolebinding-{}.yml", name), + prototype: RolebindingTemplate { + name, + namespace, + bindingkind: "ClusterRole", + rolename, + usernames: username.vec(), + subjectkind: kind, + }, + }) + .render() + } +} + +#[derive(Template)] +#[template(path = "cluster/resourcequota.yml")] +pub struct ResourcequotaTemplate<'a> { + name: &'a str, + namespace: &'a str, + ingresses: &'a str, + jobs: &'a str, + pods: &'a str, + services: &'a str, + cpu: &'a str, + memory: &'a str, +} + +fn flatten(x: &Option>) -> &Option { + match x { + Some(x) => x, + None => &None, + } +} + +fn unwrap_or_else_opt_string<'a, 'b: 'a>( + opt: &'a Option>, + default: &'b str, +) -> &'a str { + flatten(opt) + .as_ref() + .map(|str| str.as_str()) + .unwrap_or(default) +} + +impl<'a> ResourcequotaTemplate<'a> { + pub fn instantiate(limits: &'a Option, namespace: &'a str) -> Resourcefile { + let f = unwrap_or_else_opt_string; + + let limits = limits.as_ref(); + + (Resourceproto { + name: &format!("resourcequota-{}.yml", namespace), + prototype: ResourcequotaTemplate { + name: "kuberwave-resource-quotas", + namespace, + ingresses: f(&limits.map(|l| l.ingresses.as_ref()), "20"), + jobs: f(&limits.map(|l| l.jobs.as_ref()), "100"), + pods: f(&limits.map(|l| l.pods.as_ref()), "25"), + services: f(&limits.map(|l| l.services.as_ref()), "25"), + cpu: f(&limits.map(|l| l.cpu.as_ref()), "2000m"), + memory: f(&limits.map(|l| l.memory.as_ref()), "2Gi"), + }, + }) + .render() + } +} diff --git a/src/cluster/types.rs b/src/cluster/types.rs new file mode 100644 index 0000000..40e0fe6 --- /dev/null +++ b/src/cluster/types.rs @@ -0,0 +1,111 @@ +use serde_derive::{Deserialize, Serialize}; +use slugify::slugify; +use std::{ + collections::HashMap, + fmt::{Display, Formatter}, +}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Limits { + pub ingresses: Option, + pub jobs: Option, + pub pods: Option, + pub services: Option, + pub cpu: Option, + pub memory: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Namespace { + pub name: String, + pub limits: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub enum Permission { + Write, + Read, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub enum ClusterRole { + /// You can do anything. (within that namespace and not the cluster iff normal rolebinding) + All, + /// Get, list and watch all resources, with the notable exception of secrets. + View, +} + +#[derive(Eq, PartialEq, Serialize, Deserialize, Debug, Clone, Copy)] +#[derive(Default)] +pub enum UserKind { + /// A Google Cloud user account or Google IAM service account + User, + /// A kubernetes service account + #[default] + ServiceAccount, +} + + + +impl Display for UserKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + UserKind::User => "User", + UserKind::ServiceAccount => "ServiceAccount", + } + ) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum UserNames { + Single(String), + Multiple(Vec), +} + +impl UserNames { + pub fn get_file_name(&self) -> String { + match self { + UserNames::Single(s) => slugify!(s), + UserNames::Multiple(v) => { + use itertools::Itertools; + v.iter().map(|s| slugify!(s)).join("-") + } + } + } + + pub fn single(&self) -> &String { + match self { + UserNames::Single(s) => s, + _ => panic!("Invalid: cannot have array of usernames in this case"), + } + } + + pub fn vec(&self) -> Vec<&str> { + match self { + UserNames::Single(s) => vec![s], + UserNames::Multiple(v) => v.iter().map(|s| s as &str).collect(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct User { + pub name: UserNames, + #[serde(default)] + pub kind: UserKind, + pub cluster_permissions: Option>, + pub permissions: Option>>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ClusterManifest { + pub namespaces: Vec, + pub users: Vec, +} diff --git a/src/commands/cluster_generate.rs b/src/commands/cluster_generate.rs new file mode 100644 index 0000000..19dd68c --- /dev/null +++ b/src/commands/cluster_generate.rs @@ -0,0 +1,60 @@ +use crate::cluster::templates::*; +use crate::cluster::types::*; + +fn parse_quick(path: &std::path::Path) -> T { + let f = std::fs::File::open(path).unwrap(); + serde_yaml::from_reader(f).unwrap() +} + +pub fn exec(path: &std::path::Path, destination: &std::path::Path) { + let manifest: ClusterManifest = parse_quick(path); + let destination_base = destination.to_owned(); + + let mut apply_files = vec![]; + for namespace in manifest.namespaces { + apply_files.push(NamespaceTemplate::instantiate(&namespace)); + } + + let mut auth_files = vec![]; + for user in manifest.users { + if user.kind == UserKind::ServiceAccount { + apply_files.push(ServiceAccountTemplate::instantiate(&user)); + } + + if let Some(cluster_permissions) = &user.cluster_permissions { + for cluster_permission in cluster_permissions { + auth_files.push(ClusterRolebindingTemplate::instantiate( + cluster_permission, + &user.name, + user.kind, + )); + } + } + + if let Some(ns_perms_binding) = &user.permissions { + for (namespace, permissions) in ns_perms_binding { + for permission in permissions { + auth_files.push(RolebindingTemplate::instantiate( + permission, namespace, &user.name, user.kind, + )); + } + } + } + } + + let destination_apply = destination_base.join("apply"); + for file in &apply_files { + file.write(destination_apply.clone()).unwrap(); + } + + let destination_auth = destination_base.join("auth"); + for file in &auth_files { + file.write(destination_auth.clone()).unwrap(); + } + + println!( + "{} files generated in {}.", + apply_files.len() + auth_files.len(), + destination.to_string_lossy() + ); +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..d5b794a --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,80 @@ +pub mod cluster_generate; +pub mod project_deploy; +pub mod project_generate; + +use crate::error::{ErrorKind, Result}; +use crate::kubectl::Kubectl; +use crate::project::types::Manifest; +use crate::resourcefile::Resourcefile; +use failure::ResultExt; + +#[derive(Debug)] +pub struct ProjectOpts { + manifest: std::path::PathBuf, + inventory: Option, + dry_run: bool, +} + +impl ProjectOpts { + pub fn parse(opts: &clap::ArgMatches) -> ProjectOpts { + ProjectOpts { + manifest: clap::value_t_or_exit!(opts.value_of("manifest"), std::path::PathBuf), + inventory: clap::value_t!(opts.value_of("inventory"), std::path::PathBuf).ok(), + dry_run: opts.is_present("dry-run"), + } + } +} + +fn load_inventory(path: &std::path::Path) -> Result> { + let content = std::fs::read_to_string(path).context(ErrorKind::FileReadError { + name: path.to_owned(), + })?; + let yaml = yaml_rust::YamlLoader::load_from_str(&content).context(ErrorKind::ParseError { + name: path.to_owned(), + })?; + + Ok(yaml) +} + +fn load_manifest_with_context(path: &std::path::Path, context: &tera::Context) -> Result { + let content: std::string::String = crate::util::process_template(path, context)?; + + let manifest: Manifest = serde_yaml::from_str(&content).context(ErrorKind::ParseError { + name: path.to_owned(), + })?; + + Ok(manifest) +} + +pub type LoadedManifest<'a> = (Manifest, tera::Context); + +pub fn load_manifest<'a>(opts: &ProjectOpts) -> Result> { + let context = match &opts.inventory { + Some(inventory) => { + let inventory = load_inventory(inventory).context(ErrorKind::InventoryError)?; + crate::util::map_yaml_to_context(inventory).context(ErrorKind::InventoryError)? + } + None => tera::Context::new(), + }; + + let manifest = + load_manifest_with_context(&opts.manifest, &context).context(ErrorKind::ManifestError)?; + + Ok((manifest, context)) +} + +pub fn compute_project_files( + opts: &ProjectOpts, + (manifest, context): &LoadedManifest<'_>, + kubectl: Option<&Kubectl>, +) -> Result> { + let base = opts.manifest.parent().unwrap().to_owned(); + let secret = crate::util::get_secret(); + let secret = secret.as_deref(); + + let files = manifest + .to_sourcefiles(&base, context.clone(), secret, kubectl) + .context(ErrorKind::ManifestError)?; + + Ok(files) +} diff --git a/src/commands/project_deploy.rs b/src/commands/project_deploy.rs new file mode 100644 index 0000000..0b3a90e --- /dev/null +++ b/src/commands/project_deploy.rs @@ -0,0 +1,45 @@ +use crate::commands::{compute_project_files, load_manifest, ProjectOpts}; +use crate::error::{ErrorKind, Result}; +use crate::project::types::EncryptionType; +use crate::util; + +pub fn exec( + opts: ProjectOpts, + kubeconfig: Option, + token_source: Option<(EncryptionType, std::path::PathBuf)>, +) -> Result<()> { + use failure::ResultExt; + + let secret = util::get_secret(); + let secret = secret.as_deref(); + + let token: Option = match token_source { + Some((encryption, path)) => Some( + String::from_utf8( + crate::secrets::read_secret_from_file(encryption, &path, secret) + .context(ErrorKind::TokenError)?, + ) + .context(ErrorKind::TokenError)?, + ), + None => None, + }; + + let loaded_manifest = load_manifest(&opts)?; + let (manifest, _) = &loaded_manifest; + + let kubectl = crate::kubectl::Kubectl::new(kubeconfig, token, Some(manifest.namespace.clone())); + + let files = compute_project_files(&opts, &loaded_manifest, Some(&kubectl))?; + + if opts.dry_run { + println!("{} files generated (dry-run).", files.len()); + } else { + for file in &files { + kubectl.apply(file)?; + } + + println!("{} files deployed.", files.len()); + } + + Ok(()) +} diff --git a/src/commands/project_generate.rs b/src/commands/project_generate.rs new file mode 100644 index 0000000..33c2f46 --- /dev/null +++ b/src/commands/project_generate.rs @@ -0,0 +1,26 @@ +use crate::commands::{compute_project_files, load_manifest, ProjectOpts}; +use crate::error::Result; + +pub fn exec(opts: &ProjectOpts, output: &std::path::Path) -> Result<()> { + let loaded_manifest = load_manifest(opts)?; + + // Assume we do not have access to kubectl in the generate setting; potentially breaking the + // generate and kubectl apply -f scenario. + let files = compute_project_files(opts, &loaded_manifest, None)?; + + if opts.dry_run { + println!("{} files generated (dry-run).", files.len()); + } else { + for file in &files { + file.write(output.to_path_buf()).unwrap(); + } + + println!( + "{} files generated in {}.", + files.len(), + output.to_string_lossy() + ); + } + + Ok(()) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..6123f94 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,99 @@ +use failure::{Backtrace, Context, Fail}; +use std::fmt::{Debug, Display, Formatter}; + +#[derive(Debug)] +pub struct Error { + inner: Context, +} + +impl Error { + pub fn create(message: D, kind: ErrorKind) -> Self { + ::failure::err_msg(message).context(kind).into() + } +} + +impl Fail for Error { + fn cause(&self) -> Option<&dyn Fail> { + self.inner.cause() + } + + fn backtrace(&self) -> Option<&Backtrace> { + self.inner.backtrace() + } +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> ::std::fmt::Result { + Display::fmt(&self.inner, f) + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Error { + Error { + inner: Context::new(kind), + } + } +} + +impl From> for Error { + fn from(inner: Context) -> Error { + Error { inner } + } +} + +#[derive(Clone, Eq, PartialEq, Debug, Fail)] +pub enum ErrorKind { + /// Could not load deploy token. + #[fail(display = "Could not load deploy token.")] + TokenError, + + /// Environment is incomplete. + #[fail(display = "Environment is incomplete, missing '{}'.", name)] + EnvError { name: std::string::String }, + + /// Failed to process the manifest file. + #[fail(display = "Failed to process the manifest file.")] + ManifestError, + + /// Failed to process the inventory file. + #[fail(display = "Failed to process the inventory file.")] + InventoryError, + + /// Failed to populate a context. + #[fail(display = "Failed to populate a context.")] + ContextError, + + /// Failed to parse file. + #[fail(display = "Failed parse file {:?}.", name)] + ParseError { name: std::path::PathBuf }, + + /// Failed to process template. + #[fail(display = "Failed to process template {:?}.", name)] + TemplateError { name: std::path::PathBuf }, + + /// Failed to read file. + #[fail(display = "Failed to read file {:?}.", name)] + FileReadError { name: std::path::PathBuf }, + + /// Failed to open Ansible Vault. + #[fail( + display = "Failed to open Ansible vault: {:?}. Maybe the SECRET is missing?", + file + )] + AnsibleVaultError { file: std::path::PathBuf }, + + /// Failed to read SOPS file. + #[fail(display = "Failed to read SOPS file: {:?}", file)] + SOPSError { file: std::path::PathBuf }, + + /// Kubectl failed to run. + #[fail(display = "Kubectl failed to run.")] + KubectlError, + + /// Something unexpected happened. + #[fail(display = "An unexpected error occured.")] + Error, +} + +pub type Result = std::result::Result; diff --git a/src/kubectl.rs b/src/kubectl.rs new file mode 100644 index 0000000..07f8cd9 --- /dev/null +++ b/src/kubectl.rs @@ -0,0 +1,128 @@ +use crate::error::{Error, ErrorKind}; +use crate::resourcefile::Resourcefile; +use std::process::Command; + +pub struct Kubectl { + kubeconfig: Option, + token: Option, + namespace: Option, +} + +impl Kubectl { + pub fn new( + kubeconfig: Option, + token: Option, + namespace: Option, + ) -> Kubectl { + Kubectl { + kubeconfig, + token, + namespace, + } + } + + fn load_default_args(&self, cmd: &mut Command) { + match &self.namespace { + Some(n) => { + cmd.arg("-n").arg(n.trim()); + } + None => (), + } + + match &self.kubeconfig { + Some(kc) => { + cmd.arg("--kubeconfig").arg(kc); + } + None => (), + } + + match &self.token { + Some(t) => { + cmd.arg("--token").arg(t.trim()); + } + None => (), + } + } + + pub fn get_resource_version( + &self, + resource_type: &str, + name: &str, + ) -> crate::error::Result> { + use failure::ResultExt; + let mut cmd = Command::new("kubectl"); + + cmd.arg("get").arg("-ojsonpath={.metadata.resourceVersion}"); + + self.load_default_args(&mut cmd); + + cmd.arg(resource_type); + cmd.arg(name); + + cmd.stdout(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + + let output = cmd.output().context(ErrorKind::KubectlError)?; + let status = output.status; + + if !status.success() { + let error = String::from_utf8(output.stderr).context(ErrorKind::KubectlError)?; + if error.trim().ends_with("not found") { + return Ok(None); + } else { + return Err(Error::create( + format!( + "Unexpected kubectl exec code {} for {}\n{}", + status.code().unwrap(), + name, + error, + ), + ErrorKind::KubectlError, + )); + } + } + + let resource_version: String = + String::from_utf8(output.stdout).context(ErrorKind::KubectlError)?; + + Ok(Some(resource_version)) + } + + pub fn apply(&self, file: &Resourcefile) -> crate::error::Result<()> { + use failure::ResultExt; + + let mut cmd = Command::new("kubectl"); + + cmd.arg("apply").arg("-f").arg("-"); + + self.load_default_args(&mut cmd); + + cmd.stdin(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + + let mut proc = cmd.spawn().context(ErrorKind::KubectlError)?; + + use std::io::Write; + proc.stdin + .as_mut() + .unwrap() + .write_all(file.buffer.as_bytes()) + .context(ErrorKind::KubectlError)?; + + let output = proc.wait_with_output().context(ErrorKind::KubectlError)?; + + if !output.status.success() { + return Err(Error::create( + format!( + "Unexpected kubectl exec code {} for {}\n{}", + output.status.code().unwrap(), + file.name, + String::from_utf8(output.stderr).unwrap(), + ), + ErrorKind::KubectlError, + )); + } + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6fd54af --- /dev/null +++ b/src/main.rs @@ -0,0 +1,137 @@ +pub mod cluster; +pub mod commands; +pub mod error; +pub mod project; +pub mod resourcefile; + +mod kubectl; +mod secrets; +mod util; + +use crate::commands::*; +use crate::error::Result; + +fn run() -> Result<()> { + let manifest_arg = clap::Arg::with_name("manifest") + .required(true) + .value_name("manifest-path") + .takes_value(true) + .help("Path to manifest file"); + + let output_dir_arg = clap::Arg::with_name("output") + .short("o") + .long("output") + .default_value("./result") + .value_name("output-path") + .takes_value(true) + .help("Path to output directory"); + + let inventory_arg = clap::Arg::with_name("inventory") + .short("i") + .long("inventory") + .value_name("inventory-path") + .takes_value(true) + .help("Path to inventory file"); + + let token_arg = clap::Arg::with_name("token") + .short("t") + .long("token") + .value_name("token-path") + .takes_value(true) + .help("Path to token file, encrypted with ansible-vault"); + + let token_type_arg = clap::Arg::with_name("token-type") + .long("token-type") + .value_name("type") + .takes_value(true) + .default_value("ansible-vault") + .help("Encryption type (either sops or ansible-vault)"); + + let kubeconfig_arg = clap::Arg::with_name("kubeconfig") + .short("c") + .long("kubeconfig") + .value_name("kubeconfig-path") + .takes_value(true) + .help("Path to kubeconfig file"); + + let dry_run_arg = clap::Arg::with_name("dry-run") + .short("d") + .long("dry-run") + .help("Do not actually write the configuration"); + + let generate_args = &[dry_run_arg, inventory_arg, manifest_arg.clone()]; + + let matches = clap::App::new(clap::crate_name!()) + .about(clap::crate_description!()) + .version(clap::crate_version!()) + .subcommand( + clap::SubCommand::with_name("cluster-generate") + .about("Generates a cluster configuration and writes to a directory") + .arg(&output_dir_arg) + .arg(&manifest_arg), + ) + .subcommand( + clap::SubCommand::with_name("generate") + .about("Generates a configuration and writes to a directory") + .arg(&output_dir_arg) + .args(generate_args), + ) + .subcommand( + clap::SubCommand::with_name("deploy") + .about("Deploys a configuration to the current cluster") + .arg(token_type_arg) + .arg(token_arg) + .arg(kubeconfig_arg) + .args(generate_args), + ) + .get_matches(); + + match matches.subcommand() { + ("cluster-generate", Some(opts)) => { + let manifest = clap::value_t_or_exit!(opts.value_of("manifest"), std::path::PathBuf); + let output = clap::value_t_or_exit!(opts.value_of("output"), std::path::PathBuf); + + cluster_generate::exec(&manifest, &output) + } + ("generate", Some(opts)) => { + let project_opts = ProjectOpts::parse(opts); + let output = clap::value_t_or_exit!(opts.value_of("output"), std::path::PathBuf); + + project_generate::exec(&project_opts, &output)? + } + ("deploy", Some(opts)) => { + let project_opts = ProjectOpts::parse(opts); + let kubeconfig = clap::value_t!(opts.value_of("kubeconfig"), std::path::PathBuf).ok(); + let token = clap::value_t!(opts.value_of("token"), std::path::PathBuf); + let token_type = clap::value_t!( + opts.value_of("token-type"), + crate::project::types::EncryptionType + ); + let token_source = match (token_type, token) { + (Ok(e), Ok(token)) => Some((e, token)), + _ => None, + }; + + project_deploy::exec(project_opts, kubeconfig, token_source)? + } + _ => (), + } + + Ok(()) +} + +fn main() { + ::std::process::exit(match run() { + Ok(_) => 0, + Err(err) => { + use failure::AsFail; + eprintln!("{}", err); + + for cause in err.as_fail().iter_causes() { + eprintln!("cause: {}", cause); + } + + 1 + } + }); +} diff --git a/src/project/mod.rs b/src/project/mod.rs new file mode 100644 index 0000000..df48381 --- /dev/null +++ b/src/project/mod.rs @@ -0,0 +1,2 @@ +pub mod templates; +pub mod types; diff --git a/src/project/templates.rs b/src/project/templates.rs new file mode 100644 index 0000000..b114b75 --- /dev/null +++ b/src/project/templates.rs @@ -0,0 +1,251 @@ +use askama::Template; + +use crate::project::types::*; +use crate::resourcefile::{Resourcefile, Resourceproto}; +use std::collections::HashMap; + +#[derive(Clone, Copy)] +pub struct OptRevisionVersion<'a>(pub Option<&'a str>); + +impl<'a> OptRevisionVersion<'a> { + pub fn is_some(&self) -> bool { + self.0.is_some() + } + pub fn unwrap(self) -> &'a str { + self.0.unwrap() + } +} + +pub enum IssuerMode { + Production, + Staging, +} + +#[derive(Template)] +#[template(path = "project/certificate.yml")] +pub struct CertificateTemplate<'a> { + resource_version: OptRevisionVersion<'a>, + name: &'a str, + issuer: &'a str, + hostnames: &'a Vec, +} + +impl<'a> CertificateTemplate<'a> { + /// Issue the certificate for an ingress. + /// + /// Currently we also support the `dns-01` ACME solver, hence the tight coupling between Ingress and Certificates is + /// no longer conceptually required. For practical reasons we however still require it in Kuberwave. + /// + /// If you require a Certificate without necessarilly having an Ingress, please consider decoupling Ingresses and Certificates. + pub fn instantiate( + resource_version: OptRevisionVersion<'a>, + ingress: &'a Ingress, + mode: IssuerMode, + ) -> Resourcefile { + (Resourceproto { + name: &format!("certificate-{}.yml", &ingress.certificate), + prototype: CertificateTemplate { + resource_version, + name: &ingress.certificate, + issuer: match (mode, &ingress.certificate_solver) { + (IssuerMode::Production, CertificateSolver::HTTP) => "kikundi-production-http", + (IssuerMode::Production, CertificateSolver::DNS) => "kikundi-production-dns", + (IssuerMode::Staging, CertificateSolver::HTTP) => "kikundi-staging-http", + (IssuerMode::Staging, CertificateSolver::DNS) => "kikundi-staging-dns", + (IssuerMode::Production, CertificateSolver::NONE) => { + panic!("NONE certificate resolver should not be created") + } + (IssuerMode::Staging, CertificateSolver::NONE) => { + panic!("NONE certificate resolver should not be created") + } + }, + hostnames: &ingress.hosts, + }, + }) + .render() + } +} + +pub struct IngressService<'a> { + name: &'a str, + port: u16, +} + +pub struct IngressHost<'a> { + name: &'a str, + service: IngressService<'a>, +} + +pub struct Annotation<'a> { + key: &'a str, + value: &'a str, +} + +#[derive(Template)] +#[template(path = "project/ingress.yml")] +pub struct IngressTemplate<'a> { + name: &'a str, + certificate: &'a str, + hosts: Vec>, + annotations: Vec>, +} + +impl<'a> IngressTemplate<'a> { + pub fn instantiate(ingress: &'a Ingress) -> Resourcefile { + (Resourceproto { + name: &format!("ingress-{}.yml", ingress.name), + prototype: IngressTemplate { + name: &ingress.name, + certificate: &ingress.certificate, + hosts: ingress + .hosts + .iter() + .map(|host| IngressHost { + name: host, + service: IngressService { + name: &ingress.service, + port: ingress.port, + }, + }) + .collect(), + annotations: ingress + .annotations + .as_ref() + .map(|map| { + map.iter() + .map(|(key, value)| Annotation { key, value }) + .collect() + }) + .unwrap_or_default(), + }, + }) + .render() + } +} + +fn b64encode(input: &[u8]) -> String { + base64::encode(input) +} + +#[derive(Template)] +#[template(path = "project/secrets-regcred.yml")] +pub struct SecretRegcredTemplate<'a> { + name: &'a str, + content: &'a str, +} + +impl<'a> SecretRegcredTemplate<'a> { + pub fn instantiate(name: &str, content: &[u8]) -> Resourcefile { + (Resourceproto { + name: &format!("secret-{}.yml", name), + prototype: SecretRegcredTemplate { + name, + content: &b64encode(content), + }, + }) + .render() + } +} + +pub struct Field<'a> { + pub name: &'a str, + pub value: &'a str, +} + +#[derive(Template)] +#[template(path = "project/secrets-opaque.yml")] +pub struct SecretOpaqueTemplate<'a> { + name: &'a str, + secrets: Vec>, +} + +impl<'a> SecretOpaqueTemplate<'a> { + pub fn instantiate(name: &str, secrets: HashMap>) -> Resourcefile { + let secrets: Vec<(String, String)> = secrets + .into_iter() + .map(|(k, v)| (k, b64encode(&v))) + .collect(); + + let secrets: Vec = secrets + .iter() + .map(|(name, value)| Field { name, value }) + .collect(); + + (Resourceproto { + name: &format!("secret-{}.yml", name), + prototype: SecretOpaqueTemplate { name, secrets }, + }) + .render() + } +} + +pub struct BinaryField<'a> { + pub name: &'a str, + pub value: &'a [u8], +} + +#[derive(Template)] +#[template(path = "project/configmap.yml")] +pub struct ConfigMapTemplate<'a> { + name: &'a str, + data: &'a [Field<'a>], + binary_data: &'a [Field<'a>], +} + +impl<'a> ConfigMapTemplate<'a> { + pub fn instantiate( + name: &str, + data: Vec<(String, String)>, + binary_data: Vec<(String, Vec)>, + ) -> Resourcefile { + let data: Vec = data + .iter() + .map(|(name, value)| Field { name, value }) + .collect(); + + let binary_data: Vec<(String, String)> = binary_data + .into_iter() + .map(|(name, value)| (name, base64::encode(value))) + .collect(); + + let binary_data: Vec = binary_data + .iter() + .map(|(name, value)| Field { name, value }) + .collect(); + + (Resourceproto { + name: &format!("configmap-{}.yml", name), + prototype: ConfigMapTemplate { + name, + data: &data, + binary_data: &binary_data, + }, + }) + .render() + } +} + +#[derive(Template)] +#[template(path = "project/network-policy.yml")] +pub struct NetworkPolicyTemplate<'a> { + namespace: &'a str, + ingresses: &'a Vec, +} + +impl<'a> NetworkPolicyTemplate<'a> { + pub fn instantiate(namespace: &str, ingress: &Option>) -> Resourcefile { + let empty: Vec = vec![]; + + (Resourceproto { + name: "network-policy.yml", + prototype: NetworkPolicyTemplate { + namespace, + ingresses: match ingress { + Some(ingresses) => ingresses, + _ => &empty, + }, + }, + }) + .render() + } +} diff --git a/src/project/types.rs b/src/project/types.rs new file mode 100644 index 0000000..8f5dfcc --- /dev/null +++ b/src/project/types.rs @@ -0,0 +1,395 @@ +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use crate::error::{ErrorKind, Result}; +use crate::kubectl::Kubectl; +use crate::project::templates::*; +use crate::resourcefile::Resourcefile; +use crate::secrets::read_secret_from_file; +use failure::ResultExt; +use std::collections::HashMap; +use tera::Context; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone)] +#[serde(rename_all = "camelCase")] +pub enum ManifestOptions { + #[serde(rename = "pd-ssd")] + PdSsd, + #[serde(rename = "pd-hdd")] + PdHdd, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[derive(Default)] +pub enum EncryptionType { + #[serde(rename = "sops")] + SOPS, + #[serde(rename = "ansible-vault")] + #[default] + AnsibleVault, +} + +pub struct UnknownEncryptionType; + +impl FromStr for EncryptionType { + type Err = UnknownEncryptionType; + + fn from_str(s: &str) -> std::result::Result { + match s { + "sops" => Ok(EncryptionType::SOPS), + "ansible-vault" => Ok(EncryptionType::AnsibleVault), + _ => Err(UnknownEncryptionType), + } + } +} + + + +/// A docker registry secret JSON file generated by `docker login`. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RegcredSecret { + name: String, + file: String, + #[serde(default)] + encryption: EncryptionType, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct OpaqueSecret { + name: String, + file: String, + #[serde(default)] + encryption: EncryptionType, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct FileSecret { + name: String, + dest: String, + file: String, + #[serde(default)] + template: bool, + #[serde(default)] + encryption: EncryptionType, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ContextSecret { + file: String, + #[serde(default)] + encryption: EncryptionType, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "type")] +pub enum Secret { + Regcred(RegcredSecret), + Opaque(OpaqueSecret), + File(FileSecret), + Context(ContextSecret), +} + +/// A hardcopy of a file or folder as a ConfigMap or Secret. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Hardmount { + name: String, + dest: String, + src: String, + /// Emit as a Secret instead of a ConfigMap. + #[serde(default)] + secret: bool, + /// Parse as templates. + #[serde(default)] + template: bool, + /// Optionally remap (copy, not move) file entries in the ConfigMap from something else. + /// Keys are the destinations, values are the sources. + #[serde(default)] + remap: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct Manifest { + pub version: u8, + pub namespace: String, + pub templates: Option>, + pub options: Option>, + #[serde(alias = "defaultNetworkPolicy")] + pub default_network_policy: Option, + pub ingress: Option>, + pub env: Option>, + pub secrets: Option>, + pub hardmounts: Option>, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[derive(Default)] +pub enum CertificateSolver { + #[default] + HTTP, + DNS, + NONE, +} + + + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Ingress { + pub name: String, + pub hosts: Vec, + pub service: String, + pub port: u16, + pub certificate: String, + #[serde(default)] + pub certificate_solver: CertificateSolver, + pub annotations: Option>, +} + +impl Manifest { + pub fn to_sourcefiles( + &self, + base: &Path, + mut context: Context, + vaultkey: Option<&str>, + kubectl: Option<&Kubectl>, + ) -> Result> { + let mut files: Vec = vec![]; + + match &self.env { + Some(es) => { + for env in es { + let value = std::env::var(env).context(ErrorKind::EnvError { + name: env.to_string(), + })?; + context.insert(env, &value); + } + } + None => (), + } + + match &self.secrets { + Some(ss) => { + for secret in ss { + secret.apply_context(base, &mut context, vaultkey)?; + } + + for secret in ss { + let sf = secret.to_sourcefile(base, &context, vaultkey); + if let Some(sf) = sf { + files.push(sf) + } + } + } + None => (), + } + + if let Some(true) = &self.default_network_policy { + files.push(NetworkPolicyTemplate::instantiate(&self.namespace, &self.ingress)); + } + + match &self.ingress { + Some(is) => { + for i in is { + if i.certificate_solver != CertificateSolver::NONE { + let resource_version = kubectl + .map(|kubectl| { + kubectl.get_resource_version("certificate", &i.certificate) + }) + .transpose()? + .flatten(); + files.push(CertificateTemplate::instantiate( + crate::project::templates::OptRevisionVersion( + resource_version.as_deref(), + ), + i, + IssuerMode::Production, + )); + } + files.push(IngressTemplate::instantiate(i)); + } + } + None => (), + } + + match &self.templates { + Some(ts) => { + let template_base = base.join("templates"); + for template in ts { + let parsed = + crate::util::process_template(&template_base.join(template), &context)?; + files.push(Resourcefile { + name: template.to_string_lossy().to_string(), + buffer: parsed, + }) + } + } + None => (), + } + + match &self.hardmounts { + Some(hms) => { + for hardmount in hms { + files.push(hardmount.to_sourcefile(base, &context)?) + } + } + None => (), + } + + Ok(files) + } +} + +impl RegcredSecret { + pub fn to_sourcefile(&self, base: &Path, key: Option<&str>) -> Resourcefile { + let content = read_secret_from_file(self.encryption, &base.join(&self.file), key).unwrap(); + SecretRegcredTemplate::instantiate(&self.name, &content) + } +} + +impl OpaqueSecret { + pub fn to_sourcefile(&self, base: &Path, key: Option<&str>) -> Resourcefile { + let content = read_secret_from_file(self.encryption, &base.join(&self.file), key).unwrap(); + let secrets: HashMap = serde_yaml::from_slice(&content).unwrap(); + SecretOpaqueTemplate::instantiate( + &self.name, + secrets + .into_iter() + .map(|(k, v)| (k, v.into_bytes())) + .collect(), + ) + } +} + +impl FileSecret { + pub fn to_sourcefile(&self, base: &Path, context: &Context, key: Option<&str>) -> Resourcefile { + let file = base.join(&self.file); + let mut content = read_secret_from_file(self.encryption, &file, key).unwrap(); + + if self.template { + content = tera::Tera::one_off(&String::from_utf8(content).unwrap(), context, false) + .unwrap() + .into_bytes() + } + + SecretOpaqueTemplate::instantiate( + &self.name, + [(self.dest.to_string(), content)].iter().cloned().collect(), + ) + } +} + +impl ContextSecret { + pub fn apply_context( + &self, + base: &Path, + context: &mut Context, + key: Option<&str>, + ) -> Result<()> { + let content = read_secret_from_file(self.encryption, &base.join(&self.file), key).unwrap(); + let content = String::from_utf8(content).unwrap(); + let content = yaml_rust::YamlLoader::load_from_str(&content).unwrap(); + + crate::util::append_yaml_to_context(content, context) + } +} + +impl Secret { + pub fn to_sourcefile( + &self, + base: &Path, + context: &Context, + key: Option<&str>, + ) -> Option { + match self { + Secret::Opaque(s) => Some(s.to_sourcefile(base, key)), + Secret::Regcred(s) => Some(s.to_sourcefile(base, key)), + Secret::File(s) => Some(s.to_sourcefile(base, context, key)), + Secret::Context(_) => None, + } + } + + pub fn apply_context( + &self, + base: &Path, + context: &mut Context, + key: Option<&str>, + ) -> Result<()> { + match self { + Secret::Opaque(_) => (), + Secret::Regcred(_) => (), + Secret::File(_) => (), + Secret::Context(s) => s.apply_context(base, context, key)?, + } + + Ok(()) + } +} + +impl Hardmount { + pub fn to_sourcefile(&self, base: &Path, context: &Context) -> Result { + let path = base.join(&self.src); + + let files = if path.is_dir() { + // We only support direct paths. + // TODO recursively iterate over directory. + path.read_dir() + .map_err(|_| ErrorKind::FileReadError { name: path })? + .filter_map(|entry| { + let entry = entry.ok()?; + if entry.metadata().ok()?.is_file() { + Some(entry.path()) + } else { + None + } + }) + .collect() + } else { + vec![path] + }; + + let read_f = |path: &Path| { + if self.template { + crate::util::process_template(path, context).map(|b| b.into_bytes()) + } else { + std::fs::read(path).map_err(|_| { + ErrorKind::FileReadError { + name: path.to_path_buf(), + } + .into() + }) + } + }; + + let file_content: Result)>> = files + .into_iter() + .map(|path| { + Ok(( + path.file_name().unwrap().to_str().unwrap().to_string(), + read_f(&path)?, + )) + }) + .collect(); + + let mut file_content: HashMap> = file_content?.into_iter().collect(); + + if let Some(remap) = &self.remap { + for (dst, src) in remap { + let new_v = file_content + .get(src) + .ok_or(ErrorKind::ManifestError)? + .clone(); + file_content.insert(dst.clone(), new_v); + } + } + + let result = if self.secret { + SecretOpaqueTemplate::instantiate(&self.name, file_content) + } else { + ConfigMapTemplate::instantiate(&self.name, vec![], file_content.into_iter().collect()) + }; + + Ok(result) + } +} diff --git a/src/resourcefile.rs b/src/resourcefile.rs new file mode 100644 index 0000000..a448f39 --- /dev/null +++ b/src/resourcefile.rs @@ -0,0 +1,40 @@ +use std::path::{Path, PathBuf}; + +pub struct Resourceproto<'a, T: 'a + askama::Template> { + pub name: &'a str, + pub prototype: T, +} + +pub struct Resourcefile { + pub name: String, + pub buffer: String, +} + +impl<'a, T: 'a + askama::Template> Resourceproto<'a, T> { + pub fn render(self) -> Resourcefile { + Resourcefile { + name: self.name.to_owned(), + buffer: self.prototype.render().unwrap(), + } + } +} + +impl Resourcefile { + fn ensure_base(path: &Path) -> Result<(), std::io::Error> { + std::fs::create_dir_all(path) + } + + pub fn write(&self, mut path: PathBuf) -> Result<(), std::io::Error> { + path.push(&self.name); + Resourcefile::ensure_base(path.parent().unwrap())?; + println!("Writing to {}", &path.to_string_lossy()); + std::fs::write(path, &self.buffer)?; + Ok(()) + } + + pub fn append(&mut self, other: Resourcefile) { + self.buffer + .push_str(&format!("\n\n# {}\n---\n", other.name)); + self.buffer.push_str(&other.buffer); + } +} diff --git a/src/secrets/mod.rs b/src/secrets/mod.rs new file mode 100644 index 0000000..521f5d2 --- /dev/null +++ b/src/secrets/mod.rs @@ -0,0 +1,60 @@ +use crate::error::Result; +use crate::project::types::EncryptionType; +use failure::ResultExt; +use std::path::Path; + +mod sops { + use crate::error::Result; + use failure::ResultExt; + use std::ffi::OsStr; + use std::path::Path; + use std::process::Command; + + pub fn read_from_file(file: &Path, age_keys_env: Option<&str>) -> Result> { + let args = [OsStr::new("--decrypt"), file.as_os_str()]; + let mut command = Command::new("sops"); + command.args(args); + if let Some(k) = age_keys_env { + command.env("SOPS_AGE_KEY", k); + } + let output = command + .output() + .context(crate::error::ErrorKind::SOPSError { + file: file.to_owned(), + })?; + + if !output.status.success() { + let message = std::str::from_utf8(&output.stderr) + .unwrap_or_default() + .to_owned(); + return Err(crate::error::Error::create( + message, + crate::error::ErrorKind::SOPSError { + file: file.to_owned(), + }, + )); + } + Ok(output.stdout) + } +} + +pub fn read_secret_from_file( + encryption: EncryptionType, + file: &Path, + vault_key: Option<&str>, +) -> Result> { + match (encryption, vault_key) { + (EncryptionType::AnsibleVault, None) => Err(crate::error::ErrorKind::AnsibleVaultError { + file: file.to_owned(), + } + .into()), + (EncryptionType::AnsibleVault, Some(vault_key)) => Ok( + ansible_vault::decrypt_vault_from_file(file, vault_key).context( + crate::error::ErrorKind::AnsibleVaultError { + file: file.to_owned(), + }, + )?, + ), + (EncryptionType::SOPS, k) => sops::read_from_file(file, k), + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..16f254c --- /dev/null +++ b/src/util.rs @@ -0,0 +1,70 @@ +use crate::error::{Error, ErrorKind, Result}; + +pub fn map_value(x: &yaml_rust::Yaml) -> serde_json::value::Value { + use serde_json::value::Value; + use yaml_rust::Yaml; + + match x { + Yaml::Real(str) => { + Value::Number(serde_json::Number::from_f64(str.parse::().unwrap()).unwrap()) + } + Yaml::Integer(i) => Value::Number(serde_json::Number::from(*i)), + Yaml::String(str) => Value::String(str.to_owned()), + Yaml::Boolean(b) => Value::Bool(*b), + Yaml::Array(a) => Value::Array(a.iter().map(map_value).collect()), + Yaml::Hash(h) => Value::Object( + h.into_iter() + .map(|(k, v)| (k.as_str().unwrap().to_owned(), map_value(v))) + .collect(), + ), + Yaml::Alias(_) => unimplemented!(), + Yaml::Null => Value::Null, + Yaml::BadValue => unimplemented!(), + } +} + +pub fn append_yaml_to_context(xs: Vec, context: &mut tera::Context) -> Result<()> { + for range in xs { + for (k, v) in map_value(&range) + .as_object() + .ok_or(ErrorKind::ContextError)? + { + context.insert(k, v); + } + } + + Ok(()) +} + +pub fn map_yaml_to_context(xs: Vec) -> Result { + let mut context = tera::Context::new(); + append_yaml_to_context(xs, &mut context)?; + Ok(context) +} + +pub fn process_template( + path: &std::path::Path, + context: &tera::Context, +) -> Result { + use failure::ResultExt; + + tera::Tera::one_off( + &std::fs::read_to_string(path).context(ErrorKind::FileReadError { + name: path.to_owned(), + })?, + context, + false, + ) + .map_err(|e| { + Error::create( + e.to_string(), + ErrorKind::TemplateError { + name: path.to_owned(), + }, + ) + }) +} + +pub fn get_secret() -> Option { + std::env::var("SECRET").ok() +} diff --git a/templates/cluster/clusterrolebinding.yml b/templates/cluster/clusterrolebinding.yml new file mode 100644 index 0000000..ab18458 --- /dev/null +++ b/templates/cluster/clusterrolebinding.yml @@ -0,0 +1,21 @@ +kind: "ClusterRoleBinding" +apiVersion: "rbac.authorization.k8s.io/v1" +metadata: + name: "{{ name }}" + labels: + kuberwave: "true" +subjects: +{%- for username in usernames %} + - kind: {{kind}} + name: "{{ username }}" +{%- if kind == UserKind::ServiceAccount %} + namespace: default + apiGroup: "" +{%- else %} + apiGroup: rbac.authorization.k8s.io +{% endif -%} +{%- endfor -%} +roleRef: + kind: ClusterRole + name: "{{ rolename }}" + apiGroup: "" diff --git a/templates/cluster/namespace.yml b/templates/cluster/namespace.yml new file mode 100644 index 0000000..01b6fe5 --- /dev/null +++ b/templates/cluster/namespace.yml @@ -0,0 +1,6 @@ +kind: "Namespace" +apiVersion: "v1" +metadata: + name: "{{name}}" + labels: + name: "{{name}}" diff --git a/templates/cluster/resourcequota.yml b/templates/cluster/resourcequota.yml new file mode 100644 index 0000000..29c71fb --- /dev/null +++ b/templates/cluster/resourcequota.yml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ResourceQuota +metadata: + namespace: "{{ namespace }}" + name: "{{ name }}" + labels: + kuberwave: "true" +spec: + hard: + count/ingresses.extensions: "{{ ingresses }}" + count/jobs.batch: "{{ jobs }}" + pods: "{{ pods }}" + services: "{{ services }}" + limits.cpu: "{{ cpu }}" + limits.memory: "{{ memory }}" diff --git a/templates/cluster/role-all.yml b/templates/cluster/role-all.yml new file mode 100644 index 0000000..3910f9a --- /dev/null +++ b/templates/cluster/role-all.yml @@ -0,0 +1,8 @@ +kind: "ClusterRole" +apiVersion: "rbac.authorization.k8s.io/v1" +metadata: + name: "kuberwave-all" +rules: + - apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] diff --git a/templates/cluster/role-view.yml b/templates/cluster/role-view.yml new file mode 100644 index 0000000..6623afa --- /dev/null +++ b/templates/cluster/role-view.yml @@ -0,0 +1,72 @@ +kind: "ClusterRole" +apiVersion: "rbac.authorization.k8s.io/v1" +metadata: + name: "kuberwave-view" +rules: + - apiGroups: ["cert-manager.io"] + resources: + - certificates + - certificaterequests + - issuers + verbs: ["get", "watch", "list"] + - apiGroups: [""] + resources: + - configmaps + - endpoints + - persistentvolumeclaims + - pods + - replicationcontrollers + - replicationcontrollers/scale + - serviceaccounts + - services + - bindings + - events + - limitranges + - namespaces/status + - pods/log + - pods/status + - replicationcontrollers/status + - resourcequotas + - resourcequotas/status + - namespaces + verbs: ["get", "watch", "list"] + - apiGroups: ["apps"] + resources: + - daemonsets + - deployments + - deployments/scale + - replicasets + - replicasets/scale + - statefulsets + - statefulsets/scale + - controllerrevisions + verbs: ["get", "watch", "list"] + - apiGroups: ["autoscaling"] + resources: + - horizontalpodautoscalers + verbs: ["get", "watch", "list"] + - apiGroups: ["batch"] + resources: + - cronjobs + - jobs + verbs: ["get", "watch", "list"] + - apiGroups: ["extensions"] + resources: + - daemonsets + - deployments + - deployments/scale + - ingresses + - networkpolicies + - replicasets + - replicasets/scale + - replicationcontrollers/scale + verbs: ["get", "watch", "list"] + - apiGroups: ["policy"] + resources: + - poddisruptionbudgets + verbs: ["get", "watch", "list"] + - apiGroups: ["networking.k8s.io"] + resources: + - networkpolicies + - ingresses + verbs: ["get", "watch", "list"] diff --git a/templates/cluster/rolebinding.yml b/templates/cluster/rolebinding.yml new file mode 100644 index 0000000..38a2b90 --- /dev/null +++ b/templates/cluster/rolebinding.yml @@ -0,0 +1,22 @@ +kind: "RoleBinding" +apiVersion: "rbac.authorization.k8s.io/v1" +metadata: + namespace: "{{ namespace }}" + name: "{{ name }}" + labels: + kuberwave: "true" +subjects: +{%- for username in usernames %} + - kind: {{subjectkind}} + name: "{{ username }}" +{%- if subjectkind == UserKind::ServiceAccount %} + namespace: default + apiGroup: "" +{%- else %} + apiGroup: rbac.authorization.k8s.io +{% endif -%} +{%- endfor -%} +roleRef: + kind: "{{ bindingkind }}" + name: "{{ rolename }}" + apiGroup: "" diff --git a/templates/cluster/serviceaccount.yml b/templates/cluster/serviceaccount.yml new file mode 100644 index 0000000..45f60a5 --- /dev/null +++ b/templates/cluster/serviceaccount.yml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: "{{ name }}" +secrets: + - name: "{{ name }}-token" +--- +apiVersion: v1 +kind: Secret +type: kubernetes.io/service-account-token +metadata: + name: "{{ name }}-token" + annotations: + kubernetes.io/service-account.name: "{{ name }}" \ No newline at end of file diff --git a/templates/project/certificate.yml b/templates/project/certificate.yml new file mode 100644 index 0000000..4ef1402 --- /dev/null +++ b/templates/project/certificate.yml @@ -0,0 +1,15 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ name }} + {% if resource_version.is_some() %}resourceVersion: "{{ resource_version.unwrap() }}"{% endif %} +spec: + secretName: {{ name }} + issuerRef: + name: {{ issuer }} + kind: ClusterIssuer + commonName: {{ hostnames[0] }} + dnsNames: + {% for hostname in hostnames %} + - {{ hostname }} + {% endfor %} diff --git a/templates/project/configmap.yml b/templates/project/configmap.yml new file mode 100644 index 0000000..bbe24ac --- /dev/null +++ b/templates/project/configmap.yml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ name }} + +{% if !binary_data.is_empty() %} +binaryData: +{% for field in binary_data %} + {{ field.name }}: {{ field.value }} +{% endfor %} +{% endif %} + +{% if !data.is_empty() %} +data: +{% for field in data %} + {{ field.name }}: {{ field.value }} +{% endfor %} +{% endif %} \ No newline at end of file diff --git a/templates/project/ingress.yml b/templates/project/ingress.yml new file mode 100644 index 0000000..c3a0174 --- /dev/null +++ b/templates/project/ingress.yml @@ -0,0 +1,29 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ name }} + annotations: + kubernetes.io/ingress.class: "nginx" + {% for annotation in annotations %} + {{ annotation.key }}: {{ annotation.value }} + {% endfor %} +spec: + rules: + {% for host in hosts %} + - host: {{ host.name }} + http: + paths: + - path: "/" + pathType: Prefix + backend: + service: + name: {{ host.service.name }} + port: + number: {{ host.service.port }} + {% endfor %} + tls: + - secretName: {{ certificate }} + hosts: + {% for host in hosts %} + - {{ host.name }} + {% endfor %} diff --git a/templates/project/network-policy.yml b/templates/project/network-policy.yml new file mode 100644 index 0000000..c799f4d --- /dev/null +++ b/templates/project/network-policy.yml @@ -0,0 +1,50 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-within-namespace + namespace: "{{ namespace }}" +spec: + podSelector: {} + ingress: + - from: + - namespaceSelector: + matchLabels: + name: "{{ namespace }}" + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-cert-manager + namespace: "{{ namespace }}" +spec: + podSelector: + matchLabels: + acme.cert-manager.io/http01-solver: "true" + ingress: + - from: [] + policyTypes: + - Ingress +{% for ingress in ingresses -%} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-ingress-http-{{ ingress.service }} + namespace: "{{ namespace }}" +spec: + podSelector: + matchLabels: + app: "{{ ingress.service }}" + ingress: + - from: + - namespaceSelector: + matchLabels: + name: "ingress" + ports: + - protocol: TCP + port: {{ ingress.port }} + policyTypes: + - Ingress +{% endfor %} diff --git a/templates/project/secrets-opaque.yml b/templates/project/secrets-opaque.yml new file mode 100644 index 0000000..3cce17f --- /dev/null +++ b/templates/project/secrets-opaque.yml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ name }} +type: Opaque +data: +{% for secret in secrets %} + {{ secret.name }}: {{ secret.value }} +{% endfor %} \ No newline at end of file diff --git a/templates/project/secrets-regcred.yml b/templates/project/secrets-regcred.yml new file mode 100644 index 0000000..7c2cb3d --- /dev/null +++ b/templates/project/secrets-regcred.yml @@ -0,0 +1,7 @@ +apiVersion: v1 +data: + .dockerconfigjson: {{ content }} +kind: Secret +metadata: + name: {{ name }} +type: kubernetes.io/dockerconfigjson \ No newline at end of file