diff --git a/.gitignore b/.gitignore index 3ca43ae..9fd00ce 100644 --- a/.gitignore +++ b/.gitignore @@ -4,13 +4,8 @@ debug/ target/ -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - # These are backup files generated by rustfmt **/*.rs.bk # MSVC Windows builds of rustc generate these, which store debugging information *.pdb - diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9ce76c5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1168 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +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 = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[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 = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.48.5", +] + +[[package]] +name = "clap" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys", +] + +[[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" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +dependencies = [ + "crossbeam-utils", +] + +[[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-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +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 = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dircpy" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8466f8d28ca6da4c9dfbbef6ad4bff6f2fdd5e412d821025b0d3f0a9d74a8c1e" +dependencies = [ + "jwalk", + "log", + "walkdir", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[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 = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[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 = "handlebars" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c73166c591e67fb4bf9bc04011b4e35f12e89fe8d676193aa263df065955a379" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "helmudi" +version = "0.1.0" +dependencies = [ + "clap", + "helmzoo_lib", +] + +[[package]] +name = "helmule" +version = "0.1.0" +dependencies = [ + "base64", + "clap", + "dircpy", + "helmzoo_lib", + "regex", + "serde", + "serde_json", + "serde_yaml", + "tempfile", +] + +[[package]] +name = "helmzoo_lib" +version = "0.1.0" +dependencies = [ + "base64", + "chrono", + "console", + "dialoguer", + "env_logger", + "handlebars", + "indicatif", + "log", + "serde", + "serde_json", + "serde_yaml", + "tempfile", + "which", +] + +[[package]] +name = "hermit-abi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +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 = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indicatif" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "js-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56" +dependencies = [ + "crossbeam", + "rayon", +] + +[[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.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "pest" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f200d8d83c44a45b21764d1916299752ca035d15ecd46faca3e9a2a2bf6ad06" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcd6ab1236bbdb3a49027e920e693192ebfe8913f6d60e294de57463a493cfde" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a31940305ffc96863a735bef7c7994a00b325a7138fdbc5bda0f1a0476d3275" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ff62f5259e53b78d1af898941cdcdccfae7385cf7d793a6e55de5d05bb4b7d" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustix" +version = "0.38.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[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.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1bf28c79a99f70ee1f1d83d10c875d2e70618417fda01ad1785e027579d9d38" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "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 = "unsafe-libyaml" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" + +[[package]] +name = "which" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", + "windows-sys", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[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 0.52.0", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3fdd4d6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] +resolver = "2" +members = [ + "helmule", + "helmudi", + "lib", +] + +[workspace.dependencies] +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.110" +serde_yaml = "0.9.29" +clap = { version = "4.4.18", features = ["derive"] } +tempfile = "3.9.0" +base64 = "0.21.7" diff --git a/helmudi/Cargo.toml b/helmudi/Cargo.toml new file mode 100644 index 0000000..2c51739 --- /dev/null +++ b/helmudi/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "helmudi" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.4.18", features = ["derive"] } +helmzoo_lib = { path = "../lib" } diff --git a/helmudi/src/main.rs b/helmudi/src/main.rs new file mode 100644 index 0000000..a7f560b --- /dev/null +++ b/helmudi/src/main.rs @@ -0,0 +1,46 @@ +use std::{error::Error, process::exit}; + +use helmzoo_lib::{ + self, + output::{message_empty, message_error}, +}; + +use clap::Parser; +/// Simple program to greet a person +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Name of the working dir + #[arg(short, long)] + workdir: Option, + /// Path to the configuration file + #[arg(short, long)] + config: String, + /// Dry run + #[arg(short, long, default_value = "false")] + dry_run: bool, + #[arg(long, default_value = "false")] + skip_prerequisites_check: bool, + /// Init git patch. Use it if you want to create git patch for a chart + /// It's going to pull a chart and init a git repo there, so you can + /// apply changes and create a patch file + /// It's not going to try mirroring changes, but will apply extensions + /// and patches that are already defined + #[arg(long)] + init_git_patch: Option>, +} + +fn exec(args: Args) -> Result<(), Box> { + let workdir_path = helmzoo_lib::workdir::setup_workdir(args.workdir)?; + Ok(()) +} + +fn main() { + match exec(Args::parse()) { + Ok(()) => message_empty("Thanks for using helmule"), + Err(err) => { + message_error(err); + exit(1) + } + } +} diff --git a/helmule/Cargo.toml b/helmule/Cargo.toml new file mode 100644 index 0000000..7bbd9db --- /dev/null +++ b/helmule/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "helmule" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { workspace = true } +dircpy = "0.3.15" +helmzoo_lib = { path = "../lib" } +regex = "1.10.3" +serde = { workspace = true } +serde_json ={ workspace = true } +serde_yaml = { workspace = true } +tempfile = { workspace = true } +base64 = { workspace = true } diff --git a/helmule/examples/extensions/flux2/crd-configmap.yaml b/helmule/examples/extensions/flux2/crd-configmap.yaml new file mode 100644 index 0000000..daf76b8 --- /dev/null +++ b/helmule/examples/extensions/flux2/crd-configmap.yaml @@ -0,0 +1,28 @@ +{{/* +We have to create individual configmaps for each CRD - they exceed the total +allowed length for a configmap if they are combined. +*/}} +{{ $currentScope := . }} +{{- if .Values.crds.install }} + {{- range $path, $_ := .Files.Glob "crd-base/**" }} + {{- with $currentScope }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "crdInstall" . }}-{{ $path | base | trimSuffix ".yaml" }} + namespace: {{ .Release.Namespace | quote }} + annotations: + # create hook dependencies in the right order + "helm.sh/hook-weight": "-5" + {{- include "crdInstallAnnotations" . | nindent 4 }} + labels: + app.kubernetes.io/component: {{ include "crdInstall" . | quote }} + {{- include "labels.selector" . | nindent 4 }} + role: {{ include "crdInstallSelector" . | quote }} +data: + content: | +{{ tpl (.Files.Get $path) . | indent 4 }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helmule/examples/extensions/flux2/crd-job.yaml b/helmule/examples/extensions/flux2/crd-job.yaml new file mode 100644 index 0000000..38287b2 --- /dev/null +++ b/helmule/examples/extensions/flux2/crd-job.yaml @@ -0,0 +1,80 @@ +{{- if .Values.crds.install }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "crdInstallJob" . }} + namespace: {{ .Release.Namespace | quote }} + annotations: + # create hook dependencies in the right order + "helm.sh/hook-weight": "-1" + {{- include "crdInstallAnnotations" . | nindent 4 }} + labels: + app.kubernetes.io/component: {{ include "crdInstall" . | quote }} + {{- include "labels.selector" . | nindent 4 }} + role: {{ include "crdInstallSelector" . | quote }} +spec: + ttlSecondsAfterFinished: 3600 + template: + metadata: + labels: + app.kubernetes.io/component: {{ include "crdInstall" . | quote }} + {{- include "labels.selector" . | nindent 8 }} + spec: + serviceAccountName: {{ include "crdInstall" . }} + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }} + {{- with .Values.crds.podSeccompProfile }} + seccompProfile: + {{- . | toYaml | nindent 10 }} + {{- end }} + {{- end }} + tolerations: + - key: node-role.kubernetes.io/master + effect: NoSchedule + - key: node-role.kubernetes.io/control-plane + effect: NoSchedule + containers: + - name: kubectl + image: "{{ .Values.images.registry }}/giantswarm/docker-kubectl:1.23.6" + command: + - sh + - -c + - | + set -o errexit ; set -o xtrace ; set -o nounset + + # piping stderr to stdout means kubectl's errors are surfaced + # in the pod's logs. + + kubectl apply -f /data/ 2>&1 + securityContext: + readOnlyRootFilesystem: true + {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }} + {{- with .Values.crds.seccompProfile }} + seccompProfile: + {{- . | toYaml | nindent 12 }} + {{- end }} + {{- end }} + volumeMounts: +{{- range $path, $_ := .Files.Glob "crd-base/**" }} + - name: {{ $path | base | trimSuffix ".yaml" }} + mountPath: /data/{{ $path | base }} + subPath: {{ $path | base }} +{{- end }} + resources: {{- toYaml .Values.crds.resources | nindent 10 }} + volumes: +{{ $currentScope := . }} +{{- range $path, $_ := .Files.Glob "crd-base/**" }} + {{- with $currentScope }} + - name: {{ $path | base | trimSuffix ".yaml" }} + configMap: + name: {{ include "crdInstall" . }}-{{ $path | base | trimSuffix ".yaml" }} + items: + - key: content + path: {{ $path | base }} +{{- end }} +{{- end }} + restartPolicy: Never + backoffLimit: 4 +{{- end }} diff --git a/helmule/examples/extensions/flux2/crd-np.yaml b/helmule/examples/extensions/flux2/crd-np.yaml new file mode 100644 index 0000000..ddf49f4 --- /dev/null +++ b/helmule/examples/extensions/flux2/crd-np.yaml @@ -0,0 +1,59 @@ +{{- if .Values.crds.install }} +{{- if .Capabilities.APIVersions.Has "cilium.io/v2/CiliumNetworkPolicy" }} +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: {{ include "crdInstall" . }} + namespace: {{ .Release.Namespace | quote }} + annotations: + # create hook dependencies in the right order + "helm.sh/hook-weight": "-7" + {{- include "crdInstallAnnotations" . | nindent 4 }} + labels: + app.kubernetes.io/component: {{ include "crdInstall" . | quote }} + {{- include "labels.selector" . | nindent 4 }} + role: {{ include "crdInstallSelector" . | quote }} +spec: + egress: + - toEntities: + - kube-apiserver + endpointSelector: {} +{{- else }} +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + name: {{ include "crdInstall" . }} + namespace: {{ .Release.Namespace | quote }} + annotations: + # create hook dependencies in the right order + "helm.sh/hook-weight": "-7" + {{- include "crdInstallAnnotations" . | nindent 4 }} + labels: + app.kubernetes.io/component: {{ include "crdInstall" . | quote }} + {{- include "labels.selector" . | nindent 4 }} + role: {{ include "crdInstallSelector" . | quote }} +spec: + podSelector: + matchLabels: + app.kubernetes.io/component: {{ include "crdInstall" . | quote }} + {{- include "labels.selector" . | nindent 6 }} + # allow egress traffic to the Kubernetes API + egress: + - ports: + - port: 443 + protocol: TCP + # legacy port kept for compatibility + - port: 6443 + protocol: TCP + to: + {{- range tuple "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/16" "100.64.0.0/10" }} + - ipBlock: + cidr: {{ . }} + {{- end }} + # deny ingress traffic + ingress: [] + policyTypes: + - Egress + - Ingress +{{- end }} +{{- end }} diff --git a/helmule/examples/extensions/flux2/crd-rbac.yaml b/helmule/examples/extensions/flux2/crd-rbac.yaml new file mode 100644 index 0000000..15d141d --- /dev/null +++ b/helmule/examples/extensions/flux2/crd-rbac.yaml @@ -0,0 +1,62 @@ +{{- if .Values.crds.install }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "crdInstall" . }} + namespace: {{ .Release.Namespace | quote }} + annotations: + # create hook dependencies in the right order + "helm.sh/hook-weight": "-3" + {{- include "crdInstallAnnotations" . | nindent 4 }} + labels: + app.kubernetes.io/component: {{ include "crdInstall" . | quote }} + {{- include "labels.selector" . | nindent 4 }} + role: {{ include "crdInstallSelector" . | quote }} +rules: +- apiGroups: + - "" + resources: + - jobs + verbs: + - create + - delete +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - create + - delete + - get + - patch +- apiGroups: + - policy + resources: + - podsecuritypolicies + resourceNames: + - {{ include "crdInstall" . }} + verbs: + - use +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "crdInstall" . }} + namespace: {{ .Release.Namespace | quote }} + annotations: + # create hook dependencies in the right order + "helm.sh/hook-weight": "-2" + {{- include "crdInstallAnnotations" . | nindent 4 }} + labels: + app.kubernetes.io/component: {{ include "crdInstall" . | quote }} + {{- include "labels.common" . | nindent 4 }} + role: {{ include "crdInstallSelector" . | quote }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "crdInstall" . }} +subjects: + - kind: ServiceAccount + name: {{ include "crdInstall" . }} + namespace: {{ .Release.Namespace | quote }} +{{- end }} diff --git a/helmule/examples/extensions/flux2/crd-serviceaccount.yaml b/helmule/examples/extensions/flux2/crd-serviceaccount.yaml new file mode 100644 index 0000000..41a259b --- /dev/null +++ b/helmule/examples/extensions/flux2/crd-serviceaccount.yaml @@ -0,0 +1,15 @@ +{{- if .Values.crds.install }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "crdInstall" . }} + namespace: {{ .Release.Namespace }} + annotations: + # create hook dependencies in the right order + "helm.sh/hook-weight": "-4" + {{- include "crdInstallAnnotations" . | nindent 4 }} + labels: + app.kubernetes.io/component: {{ include "crdInstall" . | quote }} + {{- include "labels.selector" . | nindent 4 }} + role: {{ include "crdInstallSelector" . | quote }} +{{- end }} diff --git a/helmule/examples/extensions/vaultwarden/virtual-service.yaml b/helmule/examples/extensions/vaultwarden/virtual-service.yaml new file mode 100644 index 0000000..e426a7b --- /dev/null +++ b/helmule/examples/extensions/vaultwarden/virtual-service.yaml @@ -0,0 +1,30 @@ +{{- if .Values.virtualservice.enabled -}} +{{- $fullName := include "vaultwarden.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if $.Capabilities.APIVersions.Has "networking.istio.io/v1beta1" }} +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: {{ $fullName }} + labels: + {{- include "vaultwarden.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + gateways: + - {{ .Values.virtaulservice.gatewayRef }} + hosts: + - ci.badhouseplants.ne + http: + - match: + - uri: + prefix: / + route: + - destination: + host: woodpecker-ci-server + port: + number: 80 +{{- end }} +{{- end }} diff --git a/helmule/examples/giantswarm/charts/external-secrets-operator.yaml b/helmule/examples/giantswarm/charts/external-secrets-operator.yaml new file mode 100644 index 0000000..5e6253a --- /dev/null +++ b/helmule/examples/giantswarm/charts/external-secrets-operator.yaml @@ -0,0 +1,7 @@ +name: external-secrets +repository: external-secrets +variables: + target_repo: app-external-secrets-operator +version: 0.8.3 +mirrors: + - apps-git diff --git a/helmule/examples/giantswarm/charts/gitops-server.yaml b/helmule/examples/giantswarm/charts/gitops-server.yaml new file mode 100644 index 0000000..f7e053a --- /dev/null +++ b/helmule/examples/giantswarm/charts/gitops-server.yaml @@ -0,0 +1,61 @@ +# ------------------------------------------------------------------- +# -- GitOps Server Application +# ------------------------------------------------------------------- +- name: weave-gitops + repository: weave + version: 4.0.15 + variables: + target_repo: app-gitops-server + mirrors: + - apps-git + extensions: + - name: Add VPA + source_dir: ../extensions/vpa-gitops-server + target_dir: templates/gs-vpa + patches: + - name: Git patch + git: + path: ../patches/git/gitops-server.patch + - name: Generate values.schema + custom_command: + commands: + - helm schema-gen values.yaml > values.schema.json + - name: Git patch for values schema + git: + path: ../patches/git/gitops-server-values-schema.patch + - name: Git patch for test-job security + git: + path: ../patches/git/gitops-server-test-job.patch + # -- Update Chart.ymal + #- name: Change the chart name + # yq: + # op: Replace + # file: Chart.yaml + # key: .name + # value: gitops-server + - name: Set the home URL + yq: + op: Add + file: Chart.yaml + key: .home + value: https://github.com/giantswarm/gitops-server-app + - name: set the icon url + yq: + op: Add + file: Chart.yaml + key: .icon + value: https://s.giantswarm.io/app-icons/weaveworks/1/icon_light.svg + - name: Add keywords + yq: + op: Add + file: Chart.yaml + key: .keywords + value: '["gitops", "flux"]' + - name: team annotation + - name: gs version + yq: + op: Add + key: .annotations."config.giantswarm.io/version" + value: 1.x.x + file: Chart.yaml + - name: yamlfmt diff --git a/helmule/examples/giantswarm/charts/zot.yaml b/helmule/examples/giantswarm/charts/zot.yaml new file mode 100644 index 0000000..3b17f9b --- /dev/null +++ b/helmule/examples/giantswarm/charts/zot.yaml @@ -0,0 +1,24 @@ +# ------------------------------------------------------------------- +# -- Zot Application +# ------------------------------------------------------------------- +name: zot +repository: zot-git +extensions: + - name: Add VPA + source_dir: ../extensions/vpa + target_dir: templates/gs-vpa + - name: Add values for CI + source_dir: ../extensions/ci-values + target_dir: ci +variables: + target_repo: zot-app +patches: + - name: team annotation + - name: set home + - name: set engine + - name: yamlfmt + - name: Git patch + git: + path: ../patches/git/zot.patch +mirrors: + - apps-git diff --git a/helmule/examples/giantswarm/extensions/ci-values/values-vpa.yaml b/helmule/examples/giantswarm/extensions/ci-values/values-vpa.yaml new file mode 100644 index 0000000..34c4089 --- /dev/null +++ b/helmule/examples/giantswarm/extensions/ci-values/values-vpa.yaml @@ -0,0 +1,9 @@ +resources: + requests: + memory: 100Mi + cpu: 70m + limits: + memory: 700Mi + cpu: 400m +vpa: + enabled: true diff --git a/helmule/examples/giantswarm/extensions/vpa-gitops-server/vpa.yaml b/helmule/examples/giantswarm/extensions/vpa-gitops-server/vpa.yaml new file mode 100644 index 0000000..e5ee491 --- /dev/null +++ b/helmule/examples/giantswarm/extensions/vpa-gitops-server/vpa.yaml @@ -0,0 +1,27 @@ +{{ if eq (include "resource.vpa.enabled" .) "true" }} +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: gitops-server + namespace: {{ .Release.Namespace }} + labels: + {{- include "chart.labels" . | nindent 4 }} +spec: + resourcePolicy: + containerPolicies: + - containerName: {{ .Chart.Name }} + controlledValues: RequestsAndLimits + minAllowed: + cpu: {{ .Values.giantswarm.resources.server.requests.cpu }} + memory: {{ .Values.giantswarm.resources.server.requests.memory }} + maxAllowed: + cpu: 1000m + memory: 1000Mi + mode: Auto + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "chart.fullname" . }} + updatePolicy: + updateMode: Auto +{{ end }} diff --git a/helmule/examples/giantswarm/extensions/vpa/_helpers.tpl b/helmule/examples/giantswarm/extensions/vpa/_helpers.tpl new file mode 100644 index 0000000..b4c3802 --- /dev/null +++ b/helmule/examples/giantswarm/extensions/vpa/_helpers.tpl @@ -0,0 +1,51 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "chart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "chart.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "chart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "chart.labels" -}} +helm.sh/chart: {{ include "chart.chart" . }} +{{ include "chart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "chart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "chart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/helmule/examples/giantswarm/extensions/vpa/vpa.yaml b/helmule/examples/giantswarm/extensions/vpa/vpa.yaml new file mode 100644 index 0000000..67a3584 --- /dev/null +++ b/helmule/examples/giantswarm/extensions/vpa/vpa.yaml @@ -0,0 +1,29 @@ +{{ if (.Values.vpa).enabled }} +{{ if .Values.resources }} +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: {{ include "chart.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "chart.labels" . | nindent 4 }} +spec: + resourcePolicy: + containerPolicies: + - containerName: manager + controlledValues: RequestsAndLimits + minAllowed: + cpu: {{ .Values.resources.requests.cpu }} + memory: {{ .Values.resources.requests.memory }} + maxAllowed: + cpu: {{ .Values.resources.limits.cpu }} + memory: {{ .Values.resources.limits.memory }} + mode: Auto + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "chart.fullname" . }} + updatePolicy: + updateMode: Auto +{{- end }} +{{- end }} diff --git a/helmule/examples/giantswarm/helmule.yaml b/helmule/examples/giantswarm/helmule.yaml new file mode 100644 index 0000000..3bd26c2 --- /dev/null +++ b/helmule/examples/giantswarm/helmule.yaml @@ -0,0 +1,58 @@ +variables: + global: example + +include: + - kind: Charts + path: ./charts/zot.yaml + - kind: Charts + path: ./charts/gitops-server.yaml + - kind: Charts + path: ./charts/external-secrets-operator.yaml +patches: + - name: yamlfmt + custom_command: + commands: + - "cat <> .yamlfmt\n formatter:\n pad_line_comments: 2\nEOT" + - yamlfmt values.yaml --conf ./yamlfmt.yaml + - rm -f yamlfmt.yaml + - name: team annotation + yq: + op: Add + key: .annotations."application.giantswarm.io/team" + value: team-honeybadger + file: Chart.yaml + - name: set home + yq: + op: Add + key: .home + value: https://github.com/giantswarm/flux-app + file: Chart.yaml + - name: set engine + yq: + op: Add + key: .engine + value: gtpl + file: Chart.yaml +repositories: + # -- Because their helm repo seems not to be accessible + - name: zot-git + git: + url: https://github.com/project-zot/helm-charts.git + git_ref: zot-0.1.42 + path: charts + - name: weave + helm: + url: https://helm.gitops.weave.works + - name: external-secrets + helm: + url: https://charts.external-secrets.io +mirrors: + - name: apps-git + git: + url: git@git.badhouseplants.net:allanger/{{ variables.target_repo }}.git + git_dir: app-{{ name }}-git + branch: upgrade-{{ name }}-to-{{ version }} + path: helm/{{ name }} + commit: |- + chore: mirror {{ name }}-{{ version }} + upstream_repo: {{ repo_url }} diff --git a/helmule/examples/giantswarm/patches/git/gitops-server-test-job.patch b/helmule/examples/giantswarm/patches/git/gitops-server-test-job.patch new file mode 100644 index 0000000..ce9ff68 --- /dev/null +++ b/helmule/examples/giantswarm/patches/git/gitops-server-test-job.patch @@ -0,0 +1,19 @@ +diff --git a/templates/tests/test-connection.yaml b/templates/tests/test-connection.yaml +index 8dfed87..b4b98bc 100644 +--- a/templates/tests/test-connection.yaml ++++ b/templates/tests/test-connection.yaml +@@ -9,7 +9,13 @@ metadata: + spec: + containers: + - name: wget +- image: busybox ++ image: "{{ .Values.image.registry }}/{{ .Values.giantswarm.images.test.image }}:{{ .Values.giantswarm.images.test.tag }}" ++ imagePullPolicy: {{ .Values.giantswarm.images.test.pullPolicy }} + command: ['wget'] + args: ['{{ include "chart.fullname" . }}:{{ .Values.service.port }}'] ++ securityContext: ++ readOnlyRootFilesystem: true ++ runAsUser: 1000 ++ resources: ++ {{- toYaml .Values.giantswarm.resources.test | nindent 8 }} + restartPolicy: Never diff --git a/helmule/examples/giantswarm/patches/git/gitops-server-values-schema.patch b/helmule/examples/giantswarm/patches/git/gitops-server-values-schema.patch new file mode 100644 index 0000000..aa6ad53 --- /dev/null +++ b/helmule/examples/giantswarm/patches/git/gitops-server-values-schema.patch @@ -0,0 +1,210 @@ +diff --git a/values.schema.json b/values.schema.json +index f759f82..c0762fa 100644 +--- a/values.schema.json ++++ b/values.schema.json +@@ -43,10 +43,51 @@ + } + }, + "extraVolumeMounts": { +- "type": "array" ++ "type": "array", ++ "items": { ++ "type": "object", ++ "properties": { ++ "mountPath": { ++ "type": "string" ++ }, ++ "name": { ++ "type": "string" ++ }, ++ "readOnly": { ++ "type": "boolean" ++ } ++ } ++ } + }, + "extraVolumes": { +- "type": "array" ++ "type": "array", ++ "items": { ++ "type": "object", ++ "properties": { ++ "csi": { ++ "type": "object", ++ "properties": { ++ "driver": { ++ "type": "string" ++ }, ++ "readOnly": { ++ "type": "boolean" ++ }, ++ "volumeAttributes": { ++ "type": "object", ++ "properties": { ++ "secretProviderClass": { ++ "type": "string" ++ } ++ } ++ } ++ } ++ }, ++ "name": { ++ "type": "string" ++ } ++ } ++ } + }, + "fullnameOverride": { + "type": "string" +@@ -91,7 +132,30 @@ + "type": "object", + "properties": { + "additionalRules": { +- "type": "array" ++ "type": "array", ++ "items": { ++ "type": "object", ++ "properties": { ++ "apiGroups": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "resources": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "verbs": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ } ++ } + }, + "create": { + "type": "boolean" +@@ -106,7 +170,10 @@ + "type": "boolean" + }, + "resourceNames": { +- "type": "array" ++ "type": "array", ++ "items": { ++ "type": "string" ++ } + } + } + }, +@@ -117,7 +184,10 @@ + "type": "boolean" + }, + "resourceNames": { +- "type": "array" ++ "type": "array", ++ "items": { ++ "type": "string" ++ } + } + } + } +@@ -134,6 +204,14 @@ + "resources": { + "type": "object", + "properties": { ++ "vpa":{ ++ "type": "object", ++ "properties": { ++ "enabled": { ++ "type": "boolean" ++ } ++ } ++ }, + "server": { + "type": "object", + "properties": { +@@ -187,14 +265,6 @@ + } + } + } +- }, +- "vpa": { +- "type": "object", +- "properties": { +- "enabled": { +- "type": "boolean" +- } +- } + } + } + } +@@ -209,7 +279,15 @@ + } + }, + "imagePullSecrets": { +- "type": "array" ++ "type": "array", ++ "items": { ++ "type": "object", ++ "properties": { ++ "name": { ++ "type": "string" ++ } ++ } ++ } + }, + "ingress": { + "type": "object", +@@ -224,10 +302,46 @@ + "type": "boolean" + }, + "hosts": { +- "type": "array" ++ "type": "array", ++ "items": { ++ "type": "object", ++ "properties": { ++ "host": { ++ "type": "string" ++ }, ++ "paths": { ++ "type": "array", ++ "items": { ++ "type": "object", ++ "properties": { ++ "path": { ++ "type": "string" ++ }, ++ "pathType": { ++ "type": "string" ++ } ++ } ++ } ++ } ++ } ++ } + }, + "tls": { +- "type": "array" ++ "type": "array", ++ "items": { ++ "type": "object", ++ "properties": { ++ "hosts": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "secretName": { ++ "type": "string" ++ } ++ } ++ } + } + } + }, diff --git a/helmule/examples/giantswarm/patches/git/gitops-server.patch b/helmule/examples/giantswarm/patches/git/gitops-server.patch new file mode 100644 index 0000000..5540cfc --- /dev/null +++ b/helmule/examples/giantswarm/patches/git/gitops-server.patch @@ -0,0 +1,307 @@ +diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl +index af32c5b..1fdf723 100644 +--- a/templates/_helpers.tpl ++++ b/templates/_helpers.tpl +@@ -39,6 +39,7 @@ helm.sh/chart: {{ include "chart.chart" . }} + {{- if .Chart.AppVersion }} + app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} + {{- end }} ++application.giantswarm.io/team: {{ index .Chart.Annotations "application.giantswarm.io/team" | quote }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- end }} + +@@ -75,3 +76,16 @@ Return the target Kubernetes version + {{- default .Capabilities.KubeVersion.Version .Values.kubeVersion -}} + {{- end -}} + {{- end -}} ++ ++{{- define "resource.vpa.enabled" -}} ++{{- if and (.Capabilities.APIVersions.Has "autoscaling.k8s.io/v1") (.Values.giantswarm.resources.vpa.enabled) }}true{{ else }}false{{ end }} ++{{- end -}} ++ ++{{- define "deployment.resources" -}} ++requests: ++{{ toYaml .Values.giantswarm.resources.server.requests | indent 2 -}} ++{{ if eq (include "resource.vpa.enabled" .) "false" }} ++limits: ++{{ toYaml .Values.giantswarm.resources.server.limits | indent 2 -}} ++{{- end -}} ++{{- end -}} +diff --git a/templates/admin-user-roles.yaml b/templates/admin-user-roles.yaml +index 74a1844..c0fa72c 100644 +--- a/templates/admin-user-roles.yaml ++++ b/templates/admin-user-roles.yaml +@@ -30,8 +30,8 @@ rules: + resources: ["terraforms"] + verbs: [ "get", "list", "watch", "patch" ] + +-{{- if gt (len $.Values.rbac.additionalRules) 0 -}} +-{{- toYaml $.Values.rbac.additionalRules | nindent 2 -}} ++{{- if gt (len $.Values.giantswarm.rbac.additionalRules) 0 -}} ++{{- toYaml $.Values.giantswarm.rbac.additionalRules | nindent 2 -}} + {{- end }} + {{- if .Values.adminUser.createClusterRole }} + --- +@@ -72,8 +72,8 @@ rules: + resources: [ "providers", "alerts" ] + verbs: [ "get", "list", "watch", "patch" ] + +-{{- if gt (len $.Values.rbac.additionalRules) 0 -}} +-{{- toYaml $.Values.rbac.additionalRules | nindent 2 -}} ++{{- if gt (len $.Values.giantswarm.rbac.additionalRules) 0 -}} ++{{- toYaml $.Values.giantswarm.rbac.additionalRules | nindent 2 -}} + {{- end -}} + {{- end }} + {{- end }} +diff --git a/templates/deployment.yaml b/templates/deployment.yaml +index a54c37c..a498259 100644 +--- a/templates/deployment.yaml ++++ b/templates/deployment.yaml +@@ -36,8 +36,8 @@ spec: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} +- image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" +- imagePullPolicy: {{ .Values.image.pullPolicy }} ++ image: "{{ .Values.image.registry }}/{{ .Values.giantswarm.images.server.image }}:{{ .Values.giantswarm.images.server.tag | default .Chart.AppVersion }}" ++ imagePullPolicy: {{ .Values.giantswarm.images.server.pullPolicy }} + args: + - "--log-level" + - "{{ .Values.logLevel }}" +@@ -88,7 +88,7 @@ spec: + {{- end }} + {{- end }} + resources: +- {{- toYaml .Values.resources | nindent 12 }} ++ {{- include "deployment.resources" . | nindent 12 }} + {{- if or .Values.serverTLS.enable .Values.extraVolumeMounts }} + volumeMounts: + {{- end }} +diff --git a/templates/role.yaml b/templates/role.yaml +index b292176..5a55339 100644 +--- a/templates/role.yaml ++++ b/templates/role.yaml +@@ -1,4 +1,4 @@ +-{{- if .Values.rbac.create -}} ++{{- if .Values.giantswarm.rbac.create -}} + {{- if semverCompare "<1.17-0" (include "common.capabilities.kubeVersion" .) -}} + apiVersion: rbac.authorization.k8s.io/v1beta1 + {{- else }} +@@ -6,32 +6,39 @@ apiVersion: rbac.authorization.k8s.io/v1 + {{- end }} + kind: ClusterRole + metadata: +- name: {{ include "chart.fullname" . }} ++ name: {{ include "chart.fullname" . }} + rules: + # impersonation rules for ui calls ++ {{- if .Values.giantswarm.rbac.impersonation.users.enabled }} + - apiGroups: [""] +- resources: {{ .Values.rbac.impersonationResources | toJson }} ++ resources: ["users"] + verbs: [ "impersonate" ] +- {{- with .Values.rbac.impersonationResourceNames }} ++ {{- with .Values.giantswarm.rbac.impersonation.users.resourceNames }} + resourceNames: {{ . | toJson }} + {{- end }} ++ {{- end }} ++ {{- if .Values.giantswarm.rbac.impersonation.groups.enabled }} ++ {{- if and .Values.giantswarm.rbac.impersonation.groups.enabled (not .Values.giantswarm.rbac.impersonation.users.enabled) }} ++ {{- fail "Enabling impersonation for groups requires users impersonation permissions, see https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation" }} ++ {{- end }} ++ - apiGroups: [""] ++ resources: ["groups"] ++ verbs: [ "impersonate" ] ++ {{- with .Values.giantswarm.rbac.impersonation.groups.resourceNames }} ++ resourceNames: {{ . | toJson }} ++ {{- end }} ++ {{- end }} + # Access to enterprise entitlement + - apiGroups: [""] + resources: [ "secrets" ] + verbs: [ "get", "list" ] +- {{- if and .Values.rbac.viewSecrets .Values.rbac.viewSecretsResourceNames }} +- {{- fail "You've supplied both rbac.viewSecrets and rbac.viewSecretsResourceNames. Please only use rbac.viewSecretsResourceNames" }} +- {{- end }} +- # or should return the first non-falsy result +- {{- with (or .Values.rbac.viewSecretsResourceNames .Values.rbac.viewSecrets) }} ++ {{- with .Values.giantswarm.rbac.viewSecretsResourceNames }} + resourceNames: {{ . | toJson }} + {{- end }} +- + # The service account needs to read namespaces to know where it can query + - apiGroups: [ "" ] + resources: [ "namespaces" ] + verbs: [ "get", "list", "watch" ] +- + # The service account needs to list custom resources to query if given feature + # is available or not. + - apiGroups: [ "apiextensions.k8s.io" ] +diff --git a/templates/rolebinding.yaml b/templates/rolebinding.yaml +index b8756fe..df718ff 100644 +--- a/templates/rolebinding.yaml ++++ b/templates/rolebinding.yaml +@@ -1,4 +1,4 @@ +-{{- if .Values.rbac.create -}} ++{{- if .Values.giantswarm.rbac.create -}} + {{- if semverCompare "<1.17-0" (include "common.capabilities.kubeVersion" .) -}} + apiVersion: rbac.authorization.k8s.io/v1beta1 + {{- else }} +@@ -9,7 +9,7 @@ metadata: + name: {{ include "chart.fullname" . }} + labels: + {{- include "chart.labels" . | nindent 4 }} +- {{- with .Values.rbac.annotations }} ++ {{- with .Values.giantswarm.rbac.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +diff --git a/values.yaml b/values.yaml +index 374ad32..7b3b35f 100644 +--- a/values.yaml ++++ b/values.yaml +@@ -1,16 +1,57 @@ +-# Default values for chart. +-# This is a YAML-formatted file. +-# Declare variables to be passed into your templates. ++giantswarm: ++ images: ++ server: ++ image: giantswarm/weaveworks-wego-app ++ pullPolicy: IfNotPresent ++ tag: v0.18.0 ++ test: ++ image: giantswarm/busybox ++ pullPolicy: IfNotPresent ++ tag: 1.36.0 ++ resources: ++ vpa: ++ enabled: true ++ server: ++ limits: ++ cpu: 200m ++ memory: 256Mi ++ requests: ++ cpu: 100m ++ memory: 128Mi ++ test: ++ requests: ++ cpu: 10m ++ memory: 2Mi ++ limits: ++ cpu: 10m ++ memory: 4Mi ++ rbac: ++ create: true ++ impersonation: ++ users: ++ enabled: true ++ # -- If non-empty, this limits the users names that the service account ++ # can impersonate, e.g. `['user1@corporation.com', 'user2@corporation.com']` ++ resourceNames: [] ++ groups: ++ enabled: true ++ # -- If non-empty, this limits the groups names that the service account ++ # can impersonate, e.g. `['admins', 'operations', 'devops']` ++ resourceNames: [] ++ # -- If non-empty, this limits the secrets that can be accessed by ++ # the service account to the specified ones, e.g. `['weave-gitops-enterprise-credentials']` ++ viewSecretsResourceNames: ["cluster-user-auth", "oidc-auth"] ++ # -- If non-empty, these additional rules will be appended to the RBAC role and the cluster role. ++ # for example, ++ # additionalRules: ++ # - apiGroups: ["infra.contrib.fluxcd.io"] ++ # resources: ["terraforms"] ++ # verbs: [ "get", "list", "patch" ] ++ additionalRules: [] + +-# Note: paragraphs starting with `# --` will end up in our manual - +-# see https://github.com/norwoodj/helm-docs + replicaCount: 1 + image: +- # FIXME check the app name +- repository: ghcr.io/weaveworks/wego-app +- pullPolicy: IfNotPresent +- # Overrides the image tag whose default is the chart appVersion. +- tag: "v0.18.0" ++ registry: gsoci.azurecr.io + imagePullSecrets: [] + nameOverride: "" + fullnameOverride: "" +@@ -43,28 +84,9 @@ serviceAccount: + # -- The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" +-rbac: +- # -- Specifies whether the clusterRole & binding to the service account should be created +- create: true +- # -- If non-empty, this limits the resources that the service +- # account can impersonate. This applies to both users and groups, e.g. +- # `['user1@corporation.com', 'user2@corporation.com', 'operations']` +- impersonationResourceNames: [] +- # -- Limit the type of principal that can be impersonated +- impersonationResources: ["users", "groups"] +- # -- If non-empty, this limits the secrets that can be accessed by +- # the service account to the specified ones, e.g. `['weave-gitops-enterprise-credentials']` +- viewSecretsResourceNames: ["cluster-user-auth", "oidc-auth"] +- # -- If non-empty, these additional rules will be appended to the RBAC role and the cluster role. +- # for example, +- # additionalRules: +- # - apiGroups: ["infra.contrib.fluxcd.io"] +- # resources: ["terraforms"] +- # verbs: [ "get", "list", "patch" ] +- additionalRules: [] + adminUser: + # -- Whether the local admin user should be created. +- # If you use this make sure you add it to `rbac.impersonationResourceNames`. ++ # If you use this make sure you add it to `giantswarm.rbac.impersonation.users.resourceNames`. + create: false + # -- Specifies whether the clusterRole & binding to the admin user should be created. + # Will be created only if `adminUser.create` is enabled. Without this, +@@ -82,7 +104,7 @@ adminUser: + # -- (string) Set the password for local admin user. Requires `adminUser.create` and `adminUser.createSecret` + # This needs to have been hashed using bcrypt. + # You can do this via our CLI with `gitops get bcrypt-hash`. +- passwordHash: ++ passwordHash: "" + podAnnotations: {} + podLabels: {} + # aadpodidbinding: identity +@@ -111,7 +133,7 @@ ingress: + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" +- hosts: ++ hosts: [] + # - host: chart-example.local + # paths: + # - path: / +@@ -123,8 +145,8 @@ ingress: + # - chart-example.local + extraVolumes: [] + extraVolumeMounts: [] +-# Example using extraVolumes and extraVolumeMounts to load 'oidc-auth' secret +-# with a secrets store CSI driver. Specify the secretName 'oidc-auth' in the ++# Example using extraVolumes and extraVolumeMounts to load 'oidc-auth' secret ++# with a secrets store CSI driver. Specify the secretName 'oidc-auth' in the + # secretProviderClass so this will be created by the secrets store CSI driver. + # See https://secrets-store-csi-driver.sigs.k8s.io/topics/sync-as-kubernetes-secret.html + # extraVolumeMounts: +@@ -138,17 +160,6 @@ extraVolumeMounts: [] + # readOnly: true + # volumeAttributes: + # secretProviderClass: ww-gitops-oauth-provider +-resources: {} +-# We usually recommend not to specify default resources and to leave this as a conscious +-# choice for the user. This also increases chances charts run on environments with little +-# resources, such as Minikube. If you do want to specify resources, uncomment the following +-# lines, adjust them as necessary, and remove the curly braces after 'resources:'. +-# limits: +-# cpu: 100m +-# memory: 128Mi +-# requests: +-# cpu: 100m +-# memory: 128Mi + + networkPolicy: + # -- Specifies whether default network policies should be created. diff --git a/helmule/examples/giantswarm/patches/git/zot.patch b/helmule/examples/giantswarm/patches/git/zot.patch new file mode 100644 index 0000000..59969bb --- /dev/null +++ b/helmule/examples/giantswarm/patches/git/zot.patch @@ -0,0 +1,121 @@ +diff --git a/templates/deployment.yaml b/templates/deployment.yaml +index c48dda1..b6de3af 100644 +--- a/templates/deployment.yaml ++++ b/templates/deployment.yaml +@@ -24,12 +24,28 @@ spec: + {{- end }} + serviceAccountName: {{ include "zot.serviceAccountName" . }} + securityContext: +- {{- toYaml .Values.podSecurityContext | nindent 8 }} ++ fsGroup: 1337 ++ {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }} ++ {{- with .Values.podSeccompProfile }} ++ seccompProfile: ++ {{- . | toYaml | nindent 10 }} ++ {{- end }} ++ {{- end }} + containers: + - name: {{ .Chart.Name }} + securityContext: +- {{- toYaml .Values.securityContext | nindent 12 }} +- image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" ++ {{- with .Values.containerSecurityContext }} ++ {{- . | toYaml | nindent 12 }} ++ {{- end }} ++ readOnlyRootFilesystem: true ++ runAsUser: 100 ++ {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }} ++ {{- with .Values.seccompProfile }} ++ seccompProfile: ++ {{- . | toYaml | nindent 14 }} ++ {{- end }} ++ {{- end }} ++ image: "{{ .Values.image.registry }}/{{ .Values.image.image }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + {{- toYaml .Values.env | nindent 12 }} +diff --git a/templates/tests/test-connection-fails.yaml b/templates/tests/test-connection-fails.yaml +index 0e7a059..6ec4916 100644 +--- a/templates/tests/test-connection-fails.yaml ++++ b/templates/tests/test-connection-fails.yaml +@@ -8,8 +8,28 @@ metadata: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded,hook-failed + spec: ++ securityContext: ++ fsGroup: 1337 ++ {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }} ++ {{- with .Values.podSeccompProfile }} ++ seccompProfile: ++ {{- . | toYaml | nindent 10 }} ++ {{- end }} ++ {{- end }} + containers: + - name: wget ++ securityContext: ++ {{- with .Values.containerSecurityContext }} ++ {{- . | toYaml | nindent 12 }} ++ {{- end }} ++ readOnlyRootFilesystem: true ++ runAsUser: 100 ++ {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }} ++ {{- with .Values.seccompProfile }} ++ seccompProfile: ++ {{- . | toYaml | nindent 14 }} ++ {{- end }} ++ {{- end }} + image: alpine:3.18 + command: + - sh +diff --git a/templates/tests/test-connection.yaml b/templates/tests/test-connection.yaml +index 59c64b4..2ded317 100644 +--- a/templates/tests/test-connection.yaml ++++ b/templates/tests/test-connection.yaml +@@ -8,8 +8,28 @@ metadata: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded,hook-failed + spec: ++ securityContext: ++ fsGroup: 1337 ++ {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }} ++ {{- with .Values.podSeccompProfile }} ++ seccompProfile: ++ {{- . | toYaml | nindent 10 }} ++ {{- end }} ++ {{- end }} + containers: + - name: wget ++ securityContext: ++ {{- with .Values.containerSecurityContext }} ++ {{- . | toYaml | nindent 12 }} ++ {{- end }} ++ readOnlyRootFilesystem: true ++ runAsUser: 100 ++ {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }} ++ {{- with .Values.seccompProfile }} ++ seccompProfile: ++ {{- . | toYaml | nindent 14 }} ++ {{- end }} ++ {{- end }} + image: alpine:3.18 + command: + - sh +diff --git a/values.yaml b/values.yaml +index ac7f0f0..9730e9c 100644 +--- a/values.yaml ++++ b/values.yaml +@@ -3,10 +3,10 @@ + # Declare variables to be passed into your templates. + replicaCount: 1 + image: +- repository: ghcr.io/project-zot/zot-linux-amd64 +- pullPolicy: IfNotPresent +- # Overrides the image tag whose default is the chart appVersion. +- tag: "v2.0.0" ++ registry: gsoci.azurecr.io ++ image: dummy/zot-linux ++ pullPolicy: Always ++ tag: "" + serviceAccount: + # Specifies whether a service account should be created + create: true diff --git a/helmule/examples/giantswarm/patches/regexp/values-patches.yaml b/helmule/examples/giantswarm/patches/regexp/values-patches.yaml new file mode 100644 index 0000000..ffd14e4 --- /dev/null +++ b/helmule/examples/giantswarm/patches/regexp/values-patches.yaml @@ -0,0 +1,89 @@ +--- +name: Replace image repository in values +targets: + - values.yaml +before: |- + image: + repository: ghcr.io/project-zot/zot-linux-amd64 + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "v2.0.0" +after: |- + image: + repository: gsoci/dummy/zot-linux + pullPolicy: Always + tag: "" +--- +name: Fix security policies in the deployment +targets: + - templates/deployment.yaml +before: |- + \{\{- toYaml .Values.podSecurityContext .* +after: |- + fsGroup: 1337 + {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }} + {{- with .Values.podSeccompProfile }} + seccompProfile: + {{- . | toYaml | nindent 10 }} + {{- end }} + {{- end }} +--- +name: Fix security policies in container +targets: + - templates/deployment.yaml +before: |- + \{\{- toYaml .Values.securityContext .* +after: |- + {{- with .Values.containerSecurityContext }} + {{- . | toYaml | nindent 12 }} + {{- end }} + readOnlyRootFilesystem: true + runAsUser: 100 + {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }} + {{- with .Values.seccompProfile }} + seccompProfile: + {{- . | toYaml | nindent 14 }} + {{- end }} + {{- end }} +--- +name: Fix security policies in test jobs +targets: + - templates/tests/test-connection-fails.yaml + - templates/tests/test-connection.yaml +before: |- + spec: + containers: +after: |- + spec: + securityContext: + fsGroup: 1337 + {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }} + {{- with .Values.podSeccompProfile }} + seccompProfile: + {{- . | toYaml | nindent 10 }} + {{- end }} + {{- end }} + containers: +--- +name: Fix security policies in test jobs containers +targets: + - templates/tests/test-connection-fails.yaml + - templates/tests/test-connection.yaml +before: |- + containers: + - name: wget +after: |- + containers: + - name: wget + securityContext: + {{- with .Values.containerSecurityContext }} + {{- . | toYaml | nindent 12 }} + {{- end }} + readOnlyRootFilesystem: true + runAsUser: 100 + {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }} + {{- with .Values.seccompProfile }} + seccompProfile: + {{- . | toYaml | nindent 14 }} + {{- end }} + {{- end }} diff --git a/helmule/examples/patches/flux-regexp/patch.yaml b/helmule/examples/patches/flux-regexp/patch.yaml new file mode 100644 index 0000000..6f3fdaa --- /dev/null +++ b/helmule/examples/patches/flux-regexp/patch.yaml @@ -0,0 +1,51 @@ +--- +name: Remove CRDs leftovers from values +targets: + - values.yaml +before: |- + installCRDs: true + crds: + # -- Add annotations to all CRD resources, e.g. "helm.sh/resource-policy": keep + annotations: \{\} +after: |- + crds: + install: true + + # Add seccomp to pod security context + podSeccompProfile: + type: RuntimeDefault + + # Add seccomp to container security context + seccompProfile: + type: RuntimeDefault + + resources: + requests: + memory: "128Mi" + cpu: "250m" + limits: + memory: "256Mi" + cpu: "500m" + +--- +name: Append crd install data to helpers +targets: + - templates/_helper.tpl +after: |- + {{- define "crdInstall" -}} + {{- printf "%s-%s" ( include "name" . ) "crd-install" | replace "+" "_" | trimSuffix "-" -}} + {{- end -}} + + {{- define "crdInstallJob" -}} + {{- printf "%s-%s-%s" ( include "name" . ) "crd-install" .Chart.AppVersion | replace "+" "_" | replace "." "-" | trimSuffix "-" | trunc 63 -}} + {{- end -}} + + {{- define "crdInstallAnnotations" -}} + "helm.sh/hook": "pre-install,pre-upgrade" + "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded,hook-failed" + {{- end -}} + + {{/* Create a label which can be used to select any orphaned crd-install hook resources */}} + {{- define "crdInstallSelector" -}} + {{- printf "%s" "crd-install-hook" -}} + {{- end -}} diff --git a/helmule/examples/patches/git/patch-2.diff b/helmule/examples/patches/git/patch-2.diff new file mode 100644 index 0000000..33b939c --- /dev/null +++ b/helmule/examples/patches/git/patch-2.diff @@ -0,0 +1,34 @@ +diff --git a/values.yaml b/values.yaml +index 7ed6839..2b144ad 100644 +--- a/values.yaml ++++ b/values.yaml +@@ -1,6 +1,6 @@ + image: + repository: registry.hub.docker.com/vaultwarden/server +- pullPolicy: IfNotPresent ++ pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + imagePullSecrets: [] +@@ -10,13 +10,14 @@ podAnnotations: {} + podSecurityContext: {} + # fsGroup: 2000 + +-securityContext: {} +-# capabilities: +-# drop: +-# - ALL +-# readOnlyRootFilesystem: true +-# runAsNonRoot: true +-# runAsUser: 1000 ++securityContext: ++capabilities: ++ drop: ++ - ALL ++ ++readOnlyRootFilesystem: true ++runAsNonRoot: true ++runAsUser: 1000 + + service: + type: ClusterIP diff --git a/helmule/examples/patches/git/patch.diff b/helmule/examples/patches/git/patch.diff new file mode 100644 index 0000000..d376b1c --- /dev/null +++ b/helmule/examples/patches/git/patch.diff @@ -0,0 +1,13 @@ +diff --git a/Chart.yaml b/Chart.yaml +index d8995d5..0e5f5a5 100644 +--- a/Chart.yaml ++++ b/Chart.yaml +@@ -8,7 +8,7 @@ keywords: + - bitwarden + - bitwarden_rs + maintainers: +-- email: allanger@badhouseplants.net ++- email: Somebody else + name: Nikolai Rodionov + url: https://badhouseplants.net + name: vaultwarden diff --git a/helmule/examples/patches/regexp/patch.yaml b/helmule/examples/patches/regexp/patch.yaml new file mode 100644 index 0000000..a6adb92 --- /dev/null +++ b/helmule/examples/patches/regexp/patch.yaml @@ -0,0 +1,7 @@ +--- +name: Add spaces before comments +targets: + - values.yaml +before: |- + ^.*[\S]+.*# +after: " #" diff --git a/helmule/examples/use/charts/vaultwarden.yaml b/helmule/examples/use/charts/vaultwarden.yaml new file mode 100644 index 0000000..59ee196 --- /dev/null +++ b/helmule/examples/use/charts/vaultwarden.yaml @@ -0,0 +1,27 @@ +name: vaultwarden +repository: badhouseplants +version: latest +extensions: + - name: Add virtual service to the chartc + target_dir: templates/extensions + source_dir: ../../extensions/vaultwarden +patches: + - name: Git patch 1 + git: + path: ../../patches/git/patch.diff + - name: Git patch 2 + git: + path: ../../patches/git/patch-2.diff + - name: yaml-fmt + custom_command: + commands: + - |- + cat <> .yamlfmt + formatter: + pad_line_comments: 2 + EOT + - yamlfmt values.yaml --conf ./yamlfmt.yaml + - rm -f yamlfmt.yaml +mirrors: + - badhouseplants-git + - custom-command diff --git a/helmule/examples/use/charts/vaultwardens.yaml b/helmule/examples/use/charts/vaultwardens.yaml new file mode 100644 index 0000000..7db7f41 --- /dev/null +++ b/helmule/examples/use/charts/vaultwardens.yaml @@ -0,0 +1,54 @@ +- name: vaultwarden + repository: badhouseplants + version: latest + extensions: + - name: Add virtual service to the chartc + target_dir: templates/extensions + source_dir: ./examples/extensions/vaultwarden + patches: + - name: Git patch 1 + git: + path: ./examples/patches/git/patch.diff + - name: Git patch 2 + git: + path: ./examples/patches/git/patch-2.diff + - name: yaml-fmt + custom_command: + commands: + - |- + cat <> .yamlfmt + formatter: + pad_line_comments: 2 + EOT + - yamlfmt values.yaml --conf ./yamlfmt.yaml + - rm -f yamlfmt.yaml + mirrors: + - badhouseplants-git + - custom-command +- name: vaultwarden + repository: badhouseplants + version: latest + extensions: + - name: Add virtual service to the chartc + target_dir: templates/extensions + source_dir: ./examples/extensions/vaultwarden + patches: + - name: Git patch 1 + git: + path: ./examples/patches/git/patch.diff + - name: Git patch 2 + git: + path: ./examples/patches/git/patch-2.diff + - name: yaml-fmt + custom_command: + commands: + - |- + cat <> .yamlfmt + formatter: + pad_line_comments: 2 + EOT + - yamlfmt values.yaml --conf ./yamlfmt.yaml + - rm -f yamlfmt.yaml + mirrors: + - badhouseplants-git + - custom-command diff --git a/helmule/examples/yamlfmt.yml b/helmule/examples/yamlfmt.yml new file mode 100644 index 0000000..e1e92dc --- /dev/null +++ b/helmule/examples/yamlfmt.yml @@ -0,0 +1 @@ +pad_line_comments: 2 diff --git a/helmule/src/config.rs b/helmule/src/config.rs new file mode 100644 index 0000000..8a21e65 --- /dev/null +++ b/helmule/src/config.rs @@ -0,0 +1,308 @@ +use std::{collections::HashMap, error::Error}; + +use helmzoo_lib::{ + cli::is_path_relative, + config::ConfigImpl, + helm::{chart::Chart, repository::Repository}, + include::Include, + output::message_info, +}; +use serde::{Deserialize, Serialize}; + +use crate::{extensions::Extension, mirror::Mirror, patches::Patch}; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, PartialOrd, Ord, Eq)] +pub(crate) enum SupportedIncludes { + Repositories, + Mirrors, + Charts, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)] +pub(crate) struct ChartExtended { + #[serde(flatten)] + pub(crate) chart: Chart, + pub(crate) extensions: Option>, + pub(crate) patches: Option>, + pub variables: Option>, +} + +impl ChartExtended { + pub(crate) fn populate_variables(&mut self, global_variables: Option>) { + if let Some(global_vars) = global_variables { + self.variables = match self.variables.clone() { + Some(mut vars) => { + vars.extend(global_vars); + Some(vars) + } + None => Some(global_vars), + } + }; + } +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)] +pub(crate) struct Config { + pub(crate) variables: Option>, + #[serde(default = "empty_vec")] + pub(crate) repositories: Vec, + pub(crate) include: Option>>, + #[serde(default = "empty_vec")] + pub(crate) charts: Vec, + pub(crate) patches: Option>, + #[serde(default = "empty_vec")] + pub(crate) mirrors: Vec, +} + +fn empty_vec() -> Vec { + vec![] +} + +impl ConfigImpl for Config { + fn apply_includes(&mut self, config_path: String) -> Result<(), Box> { + if let Some(mut include) = self.include.clone() { + include.sort_by_key(|f| f.kind.clone()); + include + .iter() + .try_for_each(|i| -> Result<(), Box> { + let include_path = match is_path_relative(i.path.clone()) { + true => format!("{}/{}", config_path, i.path), + false => i.path.clone(), + }; + match i.kind { + SupportedIncludes::Charts => { + Ok(self.append_to_charts(include_charts(include_path)?)) + } + SupportedIncludes::Repositories => { + Ok(self.append_to_repositories(include_repositories(include_path)?)) + } + SupportedIncludes::Mirrors => todo!(), + } + })? + } + Ok(()) + } +} + +impl Config { + // TODO: Maybe it can be a generic function + fn append_to_repositories(&mut self, mut repositories: Vec) { + self.repositories.append(&mut repositories); + } + + fn append_to_charts(&mut self, mut charts: Vec) { + self.charts.append(&mut charts); + } +} + +fn include_repositories(path: String) -> Result, Box> { + message_info(&format!( + "trying to include repositories from {}", + path.clone() + )); + let file = std::fs::File::open(path.clone())?; + let repositories: Vec = match serde_yaml::from_reader(file) { + Ok(res) => res, + Err(_) => { + let file = std::fs::File::open(path.clone())?; + let repo: Repository = serde_yaml::from_reader(file)?; + vec![repo] + } + }; + Ok(repositories) +} + +fn include_charts(path: String) -> Result, Box> { + message_info(&format!("trying to include chart from {}", path.clone())); + let file = std::fs::File::open(path.clone())?; + + let chart_dir = match std::path::Path::new(&path).parent() { + Some(dir) => match dir.to_str() { + Some(dir) => dir.to_string(), + None => { + return Err(Box::from(format!( + "chart parrent dir not found for {}", + path + ))); + } + }, + None => { + return Err(Box::from(format!( + "chart parrent dir not found for {}", + path + ))); + } + }; + + let mut charts: Vec = match serde_yaml::from_reader(file) { + Ok(res) => res, + Err(_) => { + let file = std::fs::File::open(path.clone())?; + let chart: ChartExtended = serde_yaml::from_reader(file)?; + vec![chart] + } + }; + + charts.iter_mut().for_each(|chart| { + match chart.extensions { + Some(ref mut extensions) => extensions.iter_mut().for_each(|extension| { + if is_path_relative(extension.source_dir.clone()) { + let clean_path = match extension.source_dir.clone().starts_with("./") { + true => extension.source_dir.clone().replacen("./", "", 1), + false => extension.source_dir.clone(), + }; + if is_path_relative(clean_path.clone()) { + let new_path = format!("{}/{}", chart_dir, clean_path); + extension.source_dir = new_path; + } + } + }), + None => message_info(&format!("no extensions set, nothing to update")), + }; + match chart.patches { + Some(ref mut patches) => patches.iter_mut().for_each(|patch| { + if is_path_relative(patch.get_path().clone()) { + let clean_path = match patch.get_path().clone().starts_with("./") { + true => patch.get_path().clone().replacen("./", "", 1), + false => patch.get_path().clone(), + }; + if is_path_relative(clean_path.clone()) { + let new_path = format!("{}/{}", chart_dir, clean_path); + patch.set_path(new_path); + } + } + }), + None => message_info(&format!("no patch set, nothing to update")), + }; + }); + Ok(charts) +} + +#[cfg(test)] +mod tests { + use std::{error::Error, fs::File, io::Write}; + + use helmzoo_lib::{ + config::ConfigImpl, + helm::{helm_repository::HelmRepo, repository::Repository}, + include::Include, + }; + use tempfile::tempdir; + + use crate::config::SupportedIncludes; + + use super::{include_repositories, Config}; + + fn prepare_test_file(name: &str, data: &str) -> Result> { + let dir = tempdir()?; + let file_path = dir.into_path().join(&name); + let mut file = File::create(file_path.clone())?; + file.write_all(data.as_bytes())?; + let path = file_path.into_os_string().to_str().unwrap().to_string(); + Ok(path) + } + + #[test] + fn test_cfg_apply_includes_repo() -> Result<(), Box> { + let data = "--- +- name: test + helm: + url: test.rocks + "; + let path = prepare_test_file("repositories.yaml", data)?; + let repo_1: Repository = Repository { + name: "test".to_string(), + helm: None, + git: None, + }; + let repos = vec![repo_1.clone()]; + let includes: Vec> = vec![Include { + path: path.clone(), + kind: SupportedIncludes::Repositories, + }]; + let mut config = Config { + repositories: repos.clone(), + include: Some(includes), + charts: vec![], + variables: None, + patches: None, + mirrors: vec![], + }; + let repo_2 = Repository { + name: "test".to_string(), + helm: Some(HelmRepo { + url: "test.rocks".to_string(), + }), + git: None, + }; + config.apply_includes(path)?; + assert_eq!(config.repositories, vec!(repo_1, repo_2)); + Ok(()) + } + #[test] + fn test_cfg_append_to_repos() -> Result<(), Box> { + let repo_1: Repository = Repository { + name: "test".to_string(), + helm: None, + git: None, + }; + let repos = vec![repo_1.clone()]; + let mut config = Config { + variables: None, + repositories: repos.clone(), + include: None, + mirrors: vec![], + charts: vec![], + patches: None, + }; + let repo_2 = Repository { + name: "test_2".to_string(), + helm: None, + git: None, + }; + let repo_2_vec = vec![repo_2.clone()]; + config.append_to_repositories(repo_2_vec); + assert_eq!(config.repositories, vec!(repo_1, repo_2)); + Ok(()) + } + + #[test] + fn test_include_repositories() -> Result<(), Box> { + let data = "--- +- name: test + helm: + url: test.rocks + "; + let path = prepare_test_file("repositories.yaml", data)?; + let include = include_repositories(path)?; + let expected: Vec = vec![Repository { + name: "test".to_string(), + git: None, + helm: Some(HelmRepo { + url: "test.rocks".to_string(), + }), + }]; + assert_eq!(expected, include); + Ok(()) + } + + #[test] + fn test_include_repository() -> Result<(), Box> { + let data = "--- +name: test +helm: + url: test.rocks + "; + let path = prepare_test_file("repositories.yaml", data)?; + let include = include_repositories(path)?; + let expected: Vec = vec![Repository { + name: "test".to_string(), + git: None, + helm: Some(HelmRepo { + url: "test.rocks".to_string(), + }), + }]; + assert_eq!(expected, include); + Ok(()) + } +} diff --git a/helmule/src/extensions.rs b/helmule/src/extensions.rs new file mode 100644 index 0000000..d77a486 --- /dev/null +++ b/helmule/src/extensions.rs @@ -0,0 +1,38 @@ +use std::fs::create_dir; + +use helmzoo_lib::{ + cli::{copy_recursively, is_path_relative}, + output::message_info, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct Extension { + name: Option, + target_dir: String, + pub(crate) source_dir: String, +} + +impl Extension { + pub(crate) fn apply( + &self, + chart_path: String, + config_path: String, + ) -> Result<(), Box> { + let extension_path = match is_path_relative(self.source_dir.clone()) { + true => format!("{}/{}", config_path, self.source_dir), + false => self.source_dir.clone(), + }; + let extension_name = match self.name.clone() { + Some(res) => res, + None => "Unnamed".to_string(), + }; + message_info(&format!("applying extension: '{}'", extension_name)); + let target_dir = format!("{}/{}", chart_path, self.target_dir); + message_info(&format!("trying to create a dir: {}", target_dir)); + create_dir(target_dir.clone())?; + message_info(&format!("copying {} to {}", extension_path, target_dir)); + copy_recursively(extension_path, target_dir)?; + Ok(()) + } +} diff --git a/helmule/src/main.rs b/helmule/src/main.rs new file mode 100644 index 0000000..0f9f147 --- /dev/null +++ b/helmule/src/main.rs @@ -0,0 +1,103 @@ +use clap::Parser; +use config::Config; +use helmzoo_lib::{ + self, + cli::{check_prerequisites, get_full_path_dir}, + config::{read_config, ConfigImpl}, + helm::repository::RepositoryImpl, + output::{message_empty, message_error}, +}; +use std::{error::Error, process::exit}; + +use crate::mirror::mirror_from_mirror_obj; + +mod config; +mod extensions; +mod mirror; +mod patches; + +/// Simple program to greet a person +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Name of the working dir + #[arg(short, long)] + workdir: Option, + /// Path to the configuration file + #[arg(short, long)] + config: String, + /// Dry run + #[arg(short, long, default_value = "false")] + dry_run: bool, + #[arg(long, default_value = "false")] + skip_prerequisites_check: bool, + /// Init git patch. Use it if you want to create git patch for a chart + /// It's going to pull a chart and init a git repo there, so you can + /// apply changes and create a patch file + /// It's not going to try mirroring changes, but will apply extensions + /// and patches that are already defined + #[arg(long)] + init_git_patch: Option>, + #[arg(long, default_value = "helm")] + helm_bin: String, + #[arg(long, default_value = "git")] + git_bin: String, + #[arg(long, default_value = "yq")] + yq_bin: String, +} + +fn exec(args: Args) -> Result<(), Box> { + let prerequisites = vec![args.helm_bin, args.git_bin, args.yq_bin]; + check_prerequisites(prerequisites)?; + let workdir_path = helmzoo_lib::workdir::setup_workdir(args.workdir)?; + let mut config: Config = read_config(args.config.clone())?; + let config_full_path = get_full_path_dir(args.config.clone())?; + config.apply_includes(config_full_path.clone())?; + config + .charts + .into_iter() + .try_for_each(|mut chart| -> Result<(), Box> { + chart.populate_variables(config.variables.clone()); + // First step is to pull the chart to the working dir + let current_repo = chart.chart.find_repo(config.repositories.clone())?; + let chart_path = current_repo.pull_chart(chart.chart.clone(), workdir_path.clone())?; + if let Some(extensions) = chart.extensions.clone() { + extensions + .into_iter() + .try_for_each(|extension| -> Result<(), Box> { + extension.apply(chart_path.clone(), config_full_path.clone()) + })? + } + if let Some(patches) = chart.patches.clone() { + patches + .into_iter() + .try_for_each(|patch| -> Result<(), Box> { + patch.apply(chart_path.clone(), config.patches.clone()) + })? + } + config + .mirrors + .iter() + .try_for_each(|mirror| -> Result<(), Box> { + mirror_from_mirror_obj(mirror.clone())?.push( + workdir_path.clone(), + chart_path.clone(), + chart.clone(), + args.dry_run, + ) + })?; + message_empty(&format!("{}", chart.chart.name)); + Ok(()) + })?; + todo!() +} + +fn main() { + match exec(Args::parse()) { + Ok(()) => message_empty("Thanks for using helmule"), + Err(err) => { + message_error(err); + exit(1) + } + } +} diff --git a/helmule/src/mirror/custom_command.rs b/helmule/src/mirror/custom_command.rs new file mode 100644 index 0000000..e01f426 --- /dev/null +++ b/helmule/src/mirror/custom_command.rs @@ -0,0 +1,126 @@ +use helmzoo_lib::{cli::cli_exec_from_dir, template}; + +use crate::config::ChartExtended; + +use super::Target; + +pub(crate) struct CustomCommands { + pub(crate) package: Vec, + pub(crate) upload: Vec, +} + +impl Target for CustomCommands { + fn push( + &self, + workdir_path: String, + chart_path: String, + chart_local: ChartExtended, + dry_run: bool, + ) -> Result<(), Box> { + for cmd_tmpl in self.package.clone() { + let mut reg = helmzoo_lib::template::register_handlebars(); + reg.register_template_string("cmd", cmd_tmpl)?; + let cmd = reg.render("cmd", &chart_local)?; + cli_exec_from_dir(cmd, workdir_path.clone())?; + } + if !dry_run { + for cmd_tmpl in self.upload.clone() { + let mut reg = template::register_handlebars(); + reg.register_template_string("cmd", cmd_tmpl)?; + let cmd = reg.render("cmd", &chart_local)?; + cli_exec_from_dir(cmd, workdir_path.clone())?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::CustomCommands; + use crate::{config::ChartExtended, mirror::Target}; + use std::{collections::HashMap, fs::create_dir_all, path::Path}; + use tempfile::TempDir; + + fn get_chart_local() -> ChartExtended { + let mut vars: HashMap = HashMap::new(); + vars.insert("key".to_string(), "value".to_string()); + ChartExtended { + name: "chart".to_string(), + version: "1.0.0".to_string(), + path: "chart-1.0.0".to_string(), + repo_url: "https:://helm.repo".to_string(), + vars, + } + } + + fn prepare_test_workdir(chart_path: String) -> String { + let workdir = TempDir::new().unwrap().path().to_str().unwrap().to_string(); + println!("test workdir is {}", workdir.clone()); + create_dir_all(format!("{}/{}", workdir, chart_path)).unwrap(); + workdir + } + + #[test] + fn test_package_basic() { + let chart_local = get_chart_local(); + let workdir = prepare_test_workdir(chart_local.path.clone()); + + let custom_commands = CustomCommands { + package: vec!["touch package".to_string()], + upload: vec!["touch upload".to_string()], + }; + + let cc_target: Box = Box::from(custom_commands); + cc_target.push(workdir.clone(), chart_local, true).unwrap(); + + assert!(Path::new(&format!("{}/package", workdir)).exists()); + assert!(!Path::new(&format!("{}/upload", workdir)).exists()); + } + + #[test] + fn test_upload_basic() { + let chart_local = get_chart_local(); + let workdir = prepare_test_workdir(chart_local.path.clone()); + + let custom_commands = CustomCommands { + package: vec!["touch package".to_string()], + upload: vec!["touch upload".to_string()], + }; + + let cc_target: Box = Box::from(custom_commands); + cc_target.push(workdir.clone(), chart_local, false).unwrap(); + + assert!(Path::new(&format!("{}/package", workdir)).exists()); + assert!(Path::new(&format!("{}/upload", workdir)).exists()); + } + + #[test] + fn test_templates() { + let chart_local = get_chart_local(); + let workdir = prepare_test_workdir(chart_local.path.clone()); + + let custom_commands = CustomCommands { + package: vec!["touch {{ name }}-{{ version }}".to_string()], + upload: vec!["touch {{ repo_url }}-{{ vars.key }}".to_string()], + }; + + let cc_target: Box = Box::from(custom_commands); + cc_target + .push(workdir.clone(), chart_local.clone(), true) + .unwrap(); + + assert!(Path::new(&format!( + "{}/{}-{}", + workdir, chart_local.name, chart_local.version + )) + .exists()); + assert!(!Path::new(&format!( + "{}/{}-{}", + workdir, + chart_local.repo_url, + chart_local.vars.get("key").unwrap() + )) + .exists()); + } +} diff --git a/helmule/src/mirror/git.rs b/helmule/src/mirror/git.rs new file mode 100644 index 0000000..a4b1433 --- /dev/null +++ b/helmule/src/mirror/git.rs @@ -0,0 +1,93 @@ +use base64::{engine::general_purpose, Engine}; +use dircpy::*; +use helmzoo_lib::git::{CheckoutOptions, CommitOptions, Git, GitOptions, PushOptions}; +use helmzoo_lib::template; +use serde::{Deserialize, Serialize}; + +use crate::config::ChartExtended; + +use super::Target; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct GitMirror { + pub(crate) git_dir: Option, + pub(crate) url: String, + pub(crate) path: String, + #[serde(default = "default_branch")] + pub(crate) branch: String, + #[serde(default = "default_commit")] + pub(crate) commit: String, + pub(crate) rebase_to: Option, + #[serde(default = "default_git_bin")] + pub(crate) git_bin: String, +} + +fn default_commit() -> String { + "helmuled {{ name }}-{{ version }}".to_string() +} + +fn default_branch() -> String { + "main".to_string() +} + +fn default_git_bin() -> String { + "git".to_string() +} + +impl Target for GitMirror { + fn push( + &self, + workdir_path: String, + chart_path: String, + chart_local: ChartExtended, + dry_run: bool, + ) -> Result<(), Box> { + let repo_path = match self.git_dir.clone() { + Some(dir) => template::render(dir.clone(), &chart_local)?, + None => general_purpose::STANDARD_NO_PAD.encode(self.url.clone()), + }; + let git_instance = Git { + url: template::render(self.url.clone(), &chart_local)?, + repo_path: repo_path.clone(), + }; + let git_opts = GitOptions::new(self.git_bin.clone(), Some(workdir_path.clone())); + git_instance.clone(git_opts.clone())?; + + // Prepare branch + let checkout_opts = CheckoutOptions { + create: true, + git_ref: template::render(self.branch.clone(), &chart_local)?, + }; + + git_instance.checkout(git_opts.clone(), checkout_opts)?; + + // Prepare path + let path = template::render(self.path.clone(), &chart_local)?; + let repo_local_full_path = format!("{}/{}/{}", workdir_path.clone(), repo_path, path); + CopyBuilder::new(chart_path.clone(), repo_local_full_path.clone()) + .overwrite_if_size_differs(true) + .run()?; + + // Prepare the commit message + let commit_opts = CommitOptions { + message: template::render(self.commit.clone(), &chart_local)?, + add: true, + }; + + git_instance.commit(git_opts.clone(), commit_opts)?; + + if !dry_run { + let force_push = match self.rebase_to { + Some(_) => true, + None => false, + }; + let push_opts = PushOptions { + rebase_to: self.rebase_to.clone(), + force: force_push, + brahcn: self.branch.clone(), + }; + git_instance.push(git_opts.clone(), push_opts)?; + } + Ok(()) + } +} diff --git a/helmule/src/mirror/mod.rs b/helmule/src/mirror/mod.rs new file mode 100644 index 0000000..a0284db --- /dev/null +++ b/helmule/src/mirror/mod.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; + +use crate::config::ChartExtended; + +pub(crate) mod custom_command; +pub(crate) mod git; + +pub(crate) trait Target { + fn push( + &self, + workdir_path: String, + chart_path: String, + chart_local: ChartExtended, + dry_run: bool, + ) -> Result<(), Box>; +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct Mirror { + pub(crate) name: String, + pub(crate) git: Option, + pub(crate) custom_command: Option, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct CustomCommandsMirror { + pub(crate) package: Vec, + pub(crate) upload: Vec, +} + +pub(crate) fn mirror_from_mirror_obj( + mirror: Mirror, +) -> Result, Box> { + if let Some(git) = mirror.git { + return Ok(Box::from(git)); + } else if let Some(command) = mirror.custom_command { + return Ok(Box::from(custom_command::CustomCommands { + package: command.package, + upload: command.upload, + })); + } + Err(Box::from(format!( + "a kind is unknown for the mirror {}", + mirror.name + ))) +} diff --git a/helmule/src/patches.rs b/helmule/src/patches.rs new file mode 100644 index 0000000..251fc55 --- /dev/null +++ b/helmule/src/patches.rs @@ -0,0 +1,313 @@ +use std::{ + fs::{self, read_dir, remove_dir_all, File, OpenOptions}, + io::Write, + path::{Path, PathBuf}, +}; + +use helmzoo_lib::{ + cli::{cli_exec, cli_exec_from_dir}, + output::message_info, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct RegexpPatchObj { + pub(crate) name: String, + pub(crate) targets: Vec, + pub(crate) before: Option, + pub(crate) after: Option, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct RegexpPatch { + pub(crate) path: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct GitPatch { + path: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) enum YqOperations { + Add, + Delete, + Replace, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct YqPatch { + file: String, + op: YqOperations, + key: String, + value: Option, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct CustomCommandPatch { + commands: Vec, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct Patch { + name: Option, + regexp: Option, + git: Option, + custom_command: Option, + yq: Option, +} + +impl Patch { + pub(crate) fn apply( + &self, + chart_local_path: String, + global_patches: Option>, + ) -> Result<(), Box> { + let patch_action: Box; + if self.is_ref() { + let patch_ref = self.get_ref(global_patches)?; + patch_action = Box::from(patch_action_from_definition(patch_ref)?); + } else { + patch_action = Box::from(patch_action_from_definition(self.clone())?); + } + patch_action.apply(chart_local_path) + } + pub(crate) fn get_path(&self) -> String { + match patch_action_from_definition(self.clone()) { + Ok(patch) => patch.get_path(), + Err(_) => "".to_string(), + } + } + pub(crate) fn set_path(&mut self, path: String) { + if let Some(ref mut regexp) = self.regexp { + regexp.path = path; + } else if let Some(ref mut git) = self.git { + git.path = path; + } + } + + fn is_ref(&self) -> bool { + self.regexp.is_none() + && self.git.is_none() + && self.custom_command.is_none() + && self.yq.is_none() + && self.name.is_some() + } + + pub(crate) fn get_ref( + &self, + global_patches: Option>, + ) -> Result> { + match global_patches { + Some(patches) => { + let patch = patches + .iter() + .find(|&patch| patch.clone().name.unwrap() == self.clone().name.unwrap()); + match patch { + Some(patch) => { + return Ok(patch.clone()); + } + None => { + return Err(Box::from(format!( + "global patch is not found: {}", + self.clone().name.unwrap() + ))) + } + } + } + None => { + return Err(Box::from(format!( + "patch {} is recognized as a reference, but global patches are not defined", + self.clone().name.unwrap() + ))) + } + } + } +} + +trait PatchInterface { + fn apply(&self, chart_local_path: String) -> Result<(), Box>; + fn get_path(&self) -> String; + fn set_path(&mut self, new_path: String); +} + +impl PatchInterface for YqPatch { + fn apply(&self, chart_local_path: String) -> Result<(), Box> { + let cmd = match self.op { + YqOperations::Add => { + let value = match self + .value + .clone() + .unwrap() + .starts_with(['{', '[', '\"', '\'']) + { + true => self.value.clone().unwrap(), + false => format!("\"{}\"", self.value.clone().unwrap()), + }; + format!("yq -i '{} += {}' {}", self.key, value, self.file) + } + YqOperations::Delete => format!("yq -i \'del({})\' {}", self.key, self.file), + YqOperations::Replace => { + let value = match self.value.clone().unwrap().starts_with(['{', '[']) { + true => self.value.clone().unwrap(), + false => format!("\"{}\"", self.value.clone().unwrap()), + }; + + format!("yq e -i '{} = {}' {}", self.key, value, self.file) + } + }; + cli_exec_from_dir(cmd, chart_local_path)?; + Ok(()) + } + + fn get_path(&self) -> String { + "".to_string() + } + + fn set_path(&mut self, _new_path: String) {} +} + +impl PatchInterface for RegexpPatch { + fn apply(&self, chart_local_path: String) -> Result<(), Box> { + for entry in read_dir(self.path.clone())? { + let entry = entry?; + let filetype = entry.file_type()?; + if filetype.is_dir() { + message_info(&format!( + "reading dirs is not supported yet, skipping {:?}", + entry.path() + )); + } else { + message_info(&format!("reading a patch file: {:?}", entry.path())); + let config_content = File::open(entry.path())?; + for patch_des in serde_yaml::Deserializer::from_reader(config_content) { + let patch: RegexpPatchObj = match RegexpPatchObj::deserialize(patch_des) { + Ok(patch) => patch, + Err(err) => return Err(Box::from(err)), + }; + message_info(&format!("applying patch: {}", patch.name)); + let after = match patch.after { + Some(after) => after, + None => "".to_string(), + }; + match patch.before { + Some(before) => { + let patch_regexp = regex::Regex::new(before.as_str())?; + for target in patch.targets { + let file_path = format!("{}/{}", chart_local_path, target); + let file_content = fs::read_to_string(file_path.clone())?; + let new_content = + patch_regexp.replace_all(file_content.as_str(), after.clone()); + let mut file = OpenOptions::new() + .write(true) + .truncate(true) + .open(file_path.clone())?; + file.write(new_content.as_bytes())?; + } + } + None => { + for target in patch.targets { + let file_path = format!("{}/{}", chart_local_path, target); + let file_content = fs::read_to_string(file_path.clone())?; + let new_content = format!("{}\n{}", file_content, after); + let mut file = OpenOptions::new() + .write(true) + .append(false) + .open(file_path.clone())?; + file.write(new_content.as_bytes())?; + } + } + }; + } + } + } + Ok(()) + } + + fn get_path(&self) -> String { + self.path.clone() + } + + fn set_path(&mut self, new_path: String) { + self.path = new_path + } +} + +impl PatchInterface for GitPatch { + fn apply(&self, chart_local_path: String) -> Result<(), Box> { + if !is_git_repo(chart_local_path.clone()) { + init_git_repo(chart_local_path.clone())?; + }; + let cmd = format!("git -C {} apply {}", chart_local_path, self.path); + cli_exec(cmd)?; + remove_dir_all(chart_local_path + "/.git")?; + Ok(()) + } + + fn get_path(&self) -> String { + self.path.clone() + } + + fn set_path(&mut self, new_path: String) { + self.path = new_path + } +} + +impl PatchInterface for CustomCommandPatch { + fn apply(&self, chart_local_path: String) -> Result<(), Box> { + for cmd in self.commands.clone() { + cli_exec_from_dir(cmd, chart_local_path.clone())?; + } + Ok(()) + } + + fn get_path(&self) -> String { + // Empty stings, cause cc patch doesn't have a path + "".to_string() + } + + fn set_path(&mut self, _new_path: String) { + () + } +} + +fn patch_action_from_definition( + patch: Patch, +) -> Result, Box> { + if let Some(regexp) = patch.regexp { + Ok(Box::new(RegexpPatch { path: regexp.path })) + } else if let Some(git) = patch.git { + return Ok(Box::new(GitPatch { + path: { + let path = PathBuf::from(git.path.clone()); + match fs::canonicalize(path).ok() { + Some(can_path) => can_path.into_os_string().into_string().ok().unwrap(), + None => git.path.clone(), + } + }, + })); + } else if let Some(custom_command) = patch.custom_command { + return Ok(Box::new(CustomCommandPatch { + commands: custom_command.commands, + })); + } else if let Some(yq) = patch.yq { + if yq.op != YqOperations::Delete && yq.value.is_none() { + return Err(Box::from("yq patch of non kind 'delete' requires a value")); + }; + return Ok(Box::from(yq)); + } else { + return Err(Box::from("unknown patch type")); + } +} + +fn is_git_repo(path: String) -> bool { + let dot_git_path = path + ".git"; + Path::new(dot_git_path.as_str()).exists() +} + +pub(crate) fn init_git_repo(path: String) -> Result<(), Box> { + cli_exec(format!("git -C {} init .", path))?; + cli_exec(format!("git -C {} add .", path))?; + cli_exec(format!("git -C {} commit -m 'Init commit'", path))?; + Ok(()) +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml new file mode 100644 index 0000000..b9782d6 --- /dev/null +++ b/lib/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "helmzoo_lib" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { workspace = true } +serde_json ={ workspace = true } +serde_yaml = { workspace = true } +tempfile = { workspace = true } +base64 = { workspace = true } +console = "0.15.8" +dialoguer = "0.11.0" +env_logger = "0.10.1" +indicatif = "0.17.7" +log = "0.4.20" +which = "6.0.0" +handlebars = "5.0.0" +chrono = "0.4.31" diff --git a/lib/src/cli.rs b/lib/src/cli.rs new file mode 100644 index 0000000..9d6aa84 --- /dev/null +++ b/lib/src/cli.rs @@ -0,0 +1,144 @@ +use std::{ + error::Error, + fs::{self, read_dir}, + path::{Path, PathBuf}, + process::Command, +}; + +use which::which; + +use crate::output::message_info; + +pub fn cli_exec(command: String) -> Result> { + message_info(&format!("executing: {}", command)); + let expect = format!("command has failed: {}", command); + let output = Command::new("sh") + .arg("-c") + .arg(command) + .output() + .expect(&expect); + let stderr = String::from_utf8_lossy(&output.stderr); + if !&output.status.success() { + return Err(Box::from(stderr)); + }; + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + +pub fn cli_exec_from_dir(command: String, dir: String) -> Result> { + message_info(&format!("executing: {} from {}", command, dir)); + let expect = format!("command has failed: {}", command); + let output = Command::new("sh") + .arg("-c") + .current_dir(dir) + .arg(command) + .output() + .expect(&expect); + let stderr = String::from_utf8_lossy(&output.stderr); + if !&output.status.success() { + return Err(Box::from(stderr)); + }; + let mut stdout = String::from_utf8_lossy(&output.stdout).to_string(); + stdout.pop(); + Ok(stdout) +} + +// A helper that checks wheter all the required binaries are installed +pub fn check_prerequisites(bins: Vec) -> Result<(), Box> { + message_info(&"checking prerequisites".to_string()); + for bin in bins { + message_info(&format!("checking {}", bin)); + which(bin)?; + } + Ok(()) +} + +pub fn get_full_path(rel_path: String) -> Result> { + match PathBuf::from(&rel_path) + .canonicalize()? + .into_os_string() + .into_string() + { + Ok(path) => Ok(path), + Err(_) => Err(Box::from(format!( + "{} can't be converted into a full path", + rel_path + ))), + } +} + +pub fn get_full_path_dir(rel_path: String) -> Result> { + let res = match PathBuf::from(&rel_path).parent() { + Some(res) => res.canonicalize()?.into_os_string().into_string(), + None => PathBuf::from(&rel_path) + .canonicalize()? + .into_os_string() + .into_string(), + }; + match res { + Ok(path) => Ok(path), + Err(_) => Err(Box::from(format!( + "{} can't be converted into a full path", + rel_path + ))), + } +} + +/// Copy files from source to destination recursively. +pub fn copy_recursively( + source: impl AsRef, + destination: impl AsRef, +) -> Result<(), Box> { + for entry in read_dir(source)? { + let entry = entry?; + let filetype = entry.file_type()?; + if filetype.is_dir() { + copy_recursively(entry.path(), destination.as_ref().join(entry.file_name()))?; + } else { + message_info(&format!("trying to copy {:?}", entry.path())); + fs::copy(entry.path(), destination.as_ref().join(entry.file_name()))?; + } + } + Ok(()) +} + +pub fn is_path_relative(path: String) -> bool { + !path.starts_with('/') +} + +#[cfg(test)] +mod tests { + use super::{cli_exec, cli_exec_from_dir}; + use tempfile::TempDir; + + #[test] + fn test_stderr() { + let command = ">&2 echo \"error\" && exit 1"; + let test = cli_exec(command.to_string()); + assert_eq!(test.err().unwrap().to_string(), "error\n".to_string()); + } + + #[test] + fn test_stdout() { + let command = "echo test"; + let test = cli_exec(command.to_string()); + assert_eq!(test.unwrap().to_string(), "test\n".to_string()); + } + + #[test] + fn test_stdout_current_dir() { + let dir = TempDir::new().unwrap(); + let dir_str = dir.path().to_str().unwrap().to_string(); + let command = "echo $PWD"; + let test = cli_exec_from_dir(command.to_string(), dir_str.clone()); + assert!(test.unwrap().to_string().contains(dir_str.as_str())); + } + + #[test] + fn test_stderr_current_dir() { + let dir = TempDir::new().unwrap(); + let dir_str = dir.path().to_str().unwrap().to_string(); + let command = ">&2 echo \"error\" && exit 1"; + let test = cli_exec_from_dir(command.to_string(), dir_str.clone()); + assert_eq!(test.err().unwrap().to_string(), "error\n".to_string()); + } +} diff --git a/lib/src/config.rs b/lib/src/config.rs new file mode 100644 index 0000000..ca247c1 --- /dev/null +++ b/lib/src/config.rs @@ -0,0 +1,86 @@ +use std::{error::Error, ffi::OsStr, fs::File, path::Path}; + +use serde::de::DeserializeOwned; + +pub trait ConfigImpl { + fn apply_includes(&mut self, config_path: String) -> Result<(), Box>; +} + +pub fn read_config(path: String) -> Result> { + let config_content = File::open(path.clone())?; + let config = match get_extension_from_filename(&path) { + Some(ext) => match ext { + "yaml" | "yml" => serde_yaml::from_reader(config_content)?, + _ => return Err(Box::from(format!("{} files are not supported", ext))), + }, + None => return Err(Box::from("can't read file without extension")), + }; + Ok(config) +} + +fn get_extension_from_filename(filename: &str) -> Option<&str> { + Path::new(filename).extension().and_then(OsStr::to_str) +} + +#[cfg(test)] +mod tests { + use super::{get_extension_from_filename, read_config}; + use serde::{Deserialize, Serialize}; + use std::{error::Error, fs::File, io::Write}; + use tempfile::tempdir; + + #[test] + fn test_extension_getter() { + let filepath = "/tmp/config.yaml"; + let extension = get_extension_from_filename(filepath); + assert_eq!(extension, Some("yaml")); + } + + #[test] + fn test_extension_getter_empty() { + let filepath = "/tmp/config"; + let extension = get_extension_from_filename(filepath); + assert_eq!(extension, None); + } + + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct DummyConfig { + string: String, + amounts: Vec, + } + + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct DummyProperty { + amount: i32, + } + + fn prepare_test_file(name: &str, data: &str) -> Result> { + let dir = tempdir()?; + let file_path = dir.into_path().join(&name); + let mut file = File::create(file_path.clone())?; + file.write_all(data.as_bytes())?; + let path = file_path.into_os_string().to_str().unwrap().to_string(); + Ok(path) + } + + #[test] + fn test_config_reader() -> Result<(), Box> { + let content = "--- + string: test + amounts: + - amount: 4 + - amount: 5 + "; + let file_path = prepare_test_file("config.yaml", content)?; + let config_data: DummyConfig; + config_data = read_config(file_path)?; + + let expected = DummyConfig { + string: "test".to_string(), + amounts: vec![DummyProperty { amount: 4 }, DummyProperty { amount: 5 }], + }; + + assert_eq!(expected, config_data); + Ok(()) + } +} diff --git a/lib/src/git.rs b/lib/src/git.rs new file mode 100644 index 0000000..598f9d2 --- /dev/null +++ b/lib/src/git.rs @@ -0,0 +1,327 @@ +use std::error::Error; + +use serde::{Deserialize, Serialize}; + +use crate::cli::{cli_exec, cli_exec_from_dir}; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct GitOptions { + bin: String, + workdir: Option, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct CheckoutOptions { + // Checkout with -b if branch doesn't exist + pub create: bool, + #[serde(alias = "ref")] + pub git_ref: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct PushOptions { + pub rebase_to: Option, + // If rebase, should be always set to true + pub force: bool, + pub brahcn: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct CommitOptions { + pub message: String, + pub add: bool, +} + +impl GitOptions { + pub fn new(bin: String, workdir: Option) -> Self { + Self { bin, workdir } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct Git { + pub url: String, + pub repo_path: String, +} + +impl Git { + pub fn new(url: String, repo_path: String) -> Self { + Self { url, repo_path } + } + + pub fn clone(&self, git_opts: GitOptions) -> Result<(), Box> { + let cmd = format!("{} clone {} {}", git_opts.bin, self.url, self.repo_path); + match self.exec(cmd, git_opts.workdir.clone()) { + Ok(_) => Ok(()), + Err(err) => Err(err), + } + } + + pub fn checkout( + &self, + git_opts: GitOptions, + opts: CheckoutOptions, + ) -> Result<(), Box> { + let cmd = format!( + "{} -C {} checkout {}", + git_opts.bin, self.repo_path, opts.git_ref + ); + match self.exec(cmd, git_opts.workdir.clone()) { + Ok(_) => Ok(()), + Err(err) => match opts.create { + true => { + let cmd = format!( + "{} -C {} checkout -b {}", + git_opts.bin, self.repo_path, opts.git_ref + ); + match self.exec(cmd, git_opts.workdir.clone()) { + Ok(_) => Ok(()), + Err(err) => Err(err), + } + } + false => Err(err), + }, + } + } + + pub fn commit(&self, git_opts: GitOptions, opts: CommitOptions) -> Result<(), Box> { + if opts.add { + let cmd = format!("{} -C {} add .", git_opts.bin, self.repo_path); + let _ = self.exec(cmd, git_opts.workdir.clone())?; + } + let cmd = format!( + "{} diff --staged --quiet || {} -C {} commit -m \"{}\"", + git_opts.bin, git_opts.bin, self.repo_path, opts.message + ); + match self.exec(cmd, git_opts.workdir.clone()) { + Ok(_) => Ok(()), + Err(err) => Err(err), + } + } + // TODO: Add tests for rebase and force + pub fn push(&self, git_opts: GitOptions, opts: PushOptions) -> Result<(), Box> { + if let Some(branch) = opts.rebase_to { + let cmd = format!("{} -C {} rebase {}", git_opts.bin, self.repo_path, branch); + let _ = self.exec(cmd, git_opts.workdir.clone())?; + } + let mut args = String::new(); + if opts.force { + args = format!("{} --force", args); + } + let cmd = format!( + "{} -C {} push --set-upstream origin {} {}", + git_opts.bin, self.repo_path, opts.brahcn, args + ); + let _ = self.exec(cmd, git_opts.workdir.clone()); + Ok(()) + } + // TODO: Add tests + pub fn init(&self, git_opts: GitOptions) -> Result<(), Box> { + let cmd = format!("{} -C {} init", git_opts.bin, self.repo_path); + let _ = self.exec(cmd, git_opts.workdir.clone())?; + let cmd = format!( + "{} -C {} remote add origin {}", + git_opts.bin, self.repo_path, self.url + ); + let _ = self.exec(cmd, git_opts.workdir.clone())?; + Ok(()) + } + + fn exec(&self, cmd: String, workdir: Option) -> Result> { + match workdir { + Some(workdir) => cli_exec_from_dir(cmd, workdir), + None => cli_exec(cmd), + } + } +} + +#[cfg(test)] +mod tests { + use crate::cli::cli_exec_from_dir; + use crate::git::{CheckoutOptions, CommitOptions, Git, PushOptions}; + use std::error::Error; + use std::path::Path; + + use tempfile::tempdir; + + use super::GitOptions; + + fn prepare_a_repo() -> Result> { + let tmp_dir = tempdir()? + .into_path() + .into_os_string() + .into_string() + .unwrap(); + cli_exec_from_dir("git init".to_string(), tmp_dir.clone())?; + Ok(tmp_dir) + } + + fn prepare_a_workdir() -> Result> { + let tmp_dir = tempdir()? + .into_path() + .into_os_string() + .into_string() + .unwrap(); + Ok(tmp_dir) + } + + #[test] + fn test_pull_no_wd() -> Result<(), Box> { + let tmp_dir = prepare_a_workdir()?; + let git_dir = format!("{}/test", tmp_dir); + let git = Git::new(prepare_a_repo()?, git_dir.clone()); + let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); + git.clone(git_opts)?; + let result = Path::new(&git_dir).exists(); + assert!(result); + Ok(()) + } + + #[test] + fn test_pull_wd() -> Result<(), Box> { + let tmp_dir = prepare_a_workdir()?; + let git_dir = "test".to_string(); + println!("{}", tmp_dir.clone()); + let git = Git::new(prepare_a_repo()?, git_dir.clone()); + let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); + git.clone(git_opts)?; + let result = Path::new(&format!("{}/{}", tmp_dir, git_dir)).exists(); + assert!(result); + Ok(()) + } + + #[test] + fn test_checkout_no_create() -> Result<(), Box> { + let tmp_dir = prepare_a_workdir()?; + let git_dir = "test".to_string(); + let git = Git::new(prepare_a_repo()?, git_dir.clone()); + let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); + cli_exec_from_dir("git checkout -b test".to_string(), git.url.clone())?; + cli_exec_from_dir( + "touch test.txt && git add . && git commit -m test".to_string(), + git.url.clone(), + )?; + cli_exec_from_dir("git checkout -b main".to_string(), git.url.clone())?; + let checkout_options = CheckoutOptions { + create: false, + git_ref: "test".to_string(), + }; + git.clone(git_opts.clone())?; + git.checkout(git_opts, checkout_options)?; + let result = cli_exec_from_dir( + "git rev-parse --abbrev-ref HEAD".to_string(), + format!("{}/{}", tmp_dir.clone(), git_dir.clone()), + )?; + assert_eq!(result, "test"); + Ok(()) + } + + #[test] + fn test_checkout_no_create_err() -> Result<(), Box> { + let tmp_dir = prepare_a_workdir()?; + let git_dir = "test".to_string(); + let git = Git::new(prepare_a_repo()?, git_dir.clone()); + let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); + cli_exec_from_dir("git checkout -b main".to_string(), git.url.clone())?; + cli_exec_from_dir( + "touch test.txt && git add . && git commit -m test".to_string(), + git.url.clone(), + )?; + git.clone(git_opts.clone())?; + let checkout_options = CheckoutOptions { + create: false, + git_ref: "test".to_string(), + }; + let res = git.checkout(git_opts, checkout_options); + assert!(res.is_err()); + Ok(()) + } + + #[test] + fn test_checkout_create() -> Result<(), Box> { + let tmp_dir = prepare_a_workdir()?; + let git_dir = "test".to_string(); + let git = Git::new(prepare_a_repo()?, git_dir.clone()); + let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); + cli_exec_from_dir("git checkout -b main".to_string(), git.url.clone())?; + cli_exec_from_dir( + "touch test.txt && git add . && git commit -m test".to_string(), + git.url.clone(), + )?; + git.clone(git_opts.clone())?; + let checkout_options = CheckoutOptions { + create: true, + git_ref: "test".to_string(), + }; + git.checkout(git_opts, checkout_options)?; + let result = cli_exec_from_dir( + "git rev-parse --abbrev-ref HEAD".to_string(), + format!("{}/{}", tmp_dir.clone(), git_dir.clone()), + )?; + assert_eq!(result, "test"); + Ok(()) + } + + #[test] + fn test_commit() -> Result<(), Box> { + let tmp_dir = prepare_a_workdir()?; + let git_dir = "test".to_string(); + let full_path = format!("{}/{}", tmp_dir.clone(), git_dir.clone()); + let git = Git::new(prepare_a_repo()?, git_dir.clone()); + let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); + git.clone(git_opts.clone())?; + let commit_options = CommitOptions { + message: "test commit".to_string(), + add: false, + }; + cli_exec_from_dir("touch test.txt && git add .".to_string(), full_path.clone())?; + git.commit(git_opts, commit_options)?; + let result = cli_exec_from_dir("git log --format=%B -n 1 HEAD".to_string(), full_path)?; + assert_eq!(result, "test commit\n"); + Ok(()) + } + + #[test] + fn test_commit_add() -> Result<(), Box> { + let tmp_dir = prepare_a_workdir()?; + let git_dir = "test".to_string(); + let full_path = format!("{}/{}", tmp_dir.clone(), git_dir.clone()); + let git = Git::new(prepare_a_repo()?, git_dir.clone()); + let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); + git.clone(git_opts.clone())?; + let commit_options = CommitOptions { + message: "test commit".to_string(), + add: true, + }; + cli_exec_from_dir("touch test.txt".to_string(), full_path.clone())?; + git.commit(git_opts, commit_options)?; + let result = cli_exec_from_dir("git log --format=%B -n 1 HEAD".to_string(), full_path)?; + assert_eq!(result, "test commit\n"); + Ok(()) + } + + #[test] + fn test_push_no_rebase() -> Result<(), Box> { + let tmp_dir = prepare_a_workdir()?; + let git_dir = "test".to_string(); + let full_path = format!("{}/{}", tmp_dir.clone(), git_dir.clone()); + let git = Git::new(prepare_a_repo()?, git_dir.clone()); + let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); + git.clone(git_opts.clone())?; + let push_options = PushOptions { + rebase_to: None, + force: false, + brahcn: "main".to_string(), + }; + cli_exec_from_dir("git checkout -b main".to_string(), full_path.clone())?; + cli_exec_from_dir( + "touch test.txt && git add . && git commit -m 'test commit'".to_string(), + full_path.clone(), + )?; + git.push(git_opts, push_options)?; + cli_exec_from_dir("git checkout main".to_string(), git.url.clone())?; + let result = cli_exec_from_dir("git log --format=%B -n 1 HEAD".to_string(), git.url)?; + assert_eq!(result, "test commit\n"); + Ok(()) + } +} diff --git a/lib/src/helm/chart.rs b/lib/src/helm/chart.rs new file mode 100644 index 0000000..24b258b --- /dev/null +++ b/lib/src/helm/chart.rs @@ -0,0 +1,75 @@ +use std::error::Error; + +use serde::{Deserialize, Serialize}; + +use super::repository::{Repository, RepositoryImpl}; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)] +pub struct Chart { + // A name of the helm chart + pub name: String, + // A reference to repository by name + pub repository: String, + pub mirrors: Vec, + // Versions to be mirrored + #[serde(default = "latest")] + pub version: String, +} + +pub(crate) fn latest() -> String { + "latest".to_string() +} + +impl Chart { + pub fn find_repo( + &self, + repositories: Vec, + ) -> Result, Box> { + for repository in repositories { + if repository.name == self.repository { + if let Some(helm) = repository.helm { + return Ok(Box::from(helm)); + } else if let Some(git) = repository.git { + return Ok(Box::from(git)); + } else { + return Err(Box::from("unsupported kind of repository")); + } + } + } + //let err = error!("repo {} is not found in the repo list", self.repository); + let error_msg = format!("repo {} is not found in the repo list", self.repository); + Err(Box::from(error_msg)) + } +} + +#[cfg(test)] +mod tests { + use std::error::Error; + + use crate::helm::{ + chart::latest, + helm_repository::HelmRepo, + repository::{Repository, RepositoryImpl}, + }; + + use super::Chart; + + #[test] + fn test_find_repo() -> Result<(), Box> { + let chart = Chart { + name: "test".to_string(), + repository: "test".to_string(), + mirrors: vec!["test".to_string()], + version: latest(), + }; + let repo = Repository { + name: "test".to_string(), + helm: Some(HelmRepo { + url: "test.rocks".to_string(), + }), + }; + let res = chart.find_repo(vec![repo])?; + assert_eq!(res.get_url(), "test.rocks".to_string()); + Ok(()) + } +} diff --git a/lib/src/helm/git_repository.rs b/lib/src/helm/git_repository.rs new file mode 100644 index 0000000..66637ad --- /dev/null +++ b/lib/src/helm/git_repository.rs @@ -0,0 +1,76 @@ +use crate::cli::cli_exec_from_dir; +use crate::git::CheckoutOptions; +use crate::git::GitOptions; +use crate::{cli::cli_exec, helm::repository::Version}; +use std::error::Error; +use std::fs::{self, rename}; + +use base64::{engine::general_purpose, Engine}; +use serde::Deserialize; +use serde::Serialize; + +use crate::git::Git; + +use super::{chart::Chart, repository::RepositoryImpl}; +// A struct that represents a git repo with a chart +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct GitRepo { + pub url: String, + #[serde(alias = "ref")] + pub git_ref: String, + pub path: String, + #[serde(default = "default_git_bin")] + pub(crate) git_bin: String, +} + +fn default_git_bin() -> String { + "git".to_string() +} + +impl RepositoryImpl for GitRepo { + fn pull_chart(&self, chart: Chart, workdir_path: String) -> Result> { + let repo_name = general_purpose::STANDARD_NO_PAD.encode(self.get_url().clone()); + let git_instance = Git::new(self.get_url(), repo_name.clone()); + + let git_opts = GitOptions::new(self.git_bin.clone(), Some(workdir_path.clone())); + git_instance.clone(git_opts.clone())?; + + let checkout_opts = CheckoutOptions { + create: true, + git_ref: self.git_ref.clone(), + }; + + git_instance.checkout(git_opts, checkout_opts)?; + + let old_dir_name = format!( + "{}/{}/{}/{}", + workdir_path, + repo_name, + self.path, + chart.name.clone() + ); + + let cmd = format!("helm show chart {}", old_dir_name); + let helm_stdout = cli_exec(cmd)?; + let new_dir_name: String; + match serde_yaml::from_str::(&helm_stdout) { + Ok(res) => { + new_dir_name = format!("{}/{}-{}", workdir_path, chart.name.clone(), res.version); + rename(old_dir_name, new_dir_name.clone())?; + } + Err(err) => return Err(Box::from(err)), + }; + + // Cleaning up + fs::remove_dir_all(format!("{}/{}", workdir_path, repo_name))?; + + // Get the version + let cmd = "helm show chart . | yq '.version'".to_string(); + let _version = cli_exec_from_dir(cmd, new_dir_name.clone())?; + Ok(new_dir_name) + } + + fn get_url(&self) -> String { + self.url.clone() + } +} diff --git a/lib/src/helm/helm_repository.rs b/lib/src/helm/helm_repository.rs new file mode 100644 index 0000000..764d67c --- /dev/null +++ b/lib/src/helm/helm_repository.rs @@ -0,0 +1,133 @@ +use std::{error::Error, fs::rename}; + +use base64::{engine::general_purpose, Engine}; +use serde::{Deserialize, Serialize}; + +use crate::cli::{cli_exec, cli_exec_from_dir}; + +use super::{ + chart::Chart, + repository::{RepositoryImpl, Version}, +}; + +// A struct that represents a regular helm repo +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct HelmRepo { + // A URL of the helm repository + pub url: String, +} + +const LATEST_VERSION: &str = "latest"; + +impl RepositoryImpl for HelmRepo { + fn pull_chart(&self, chart: Chart, workdir_path: String) -> Result> { + match self.repo_kind_from_url(self.clone())? { + RepoKind::Default => self.pull_default(chart, workdir_path), + RepoKind::Oci => self.pull_oci(chart, workdir_path), + } + } + + fn get_url(&self) -> String { + self.url.clone() + } +} + +pub(crate) enum RepoKind { + Default, + Oci, +} + +impl HelmRepo { + fn repo_kind_from_url(&self, repository: HelmRepo) -> Result> { + let prefix = repository + .url + .chars() + .take_while(|&ch| ch != ':') + .collect::(); + match prefix.as_str() { + "oci" => Ok(RepoKind::Oci), + "https" | "http" => Ok(RepoKind::Default), + _ => Err(Box::from(format!( + "repo kind is not defined by the prefix: {}", + prefix + ))), + } + } + + fn pull_oci(&self, chart: Chart, workdir_path: String) -> Result> { + let args = match chart.version.as_str() { + LATEST_VERSION => "".to_string(), + _ => format!("--version {}", chart.version.clone()), + }; + let repo = match self.get_url().ends_with('/') { + true => { + let mut repo = self.get_url().clone(); + repo.pop(); + repo + } + false => self.get_url().clone(), + }; + let cmd = format!( + "helm pull {}/{} {} --destination {} --untar", + repo, chart.name, args, workdir_path + ); + cli_exec(cmd)?; + // Get the version + let cmd = format!("helm show chart {}/{}", workdir_path, chart.name); + let helm_stdout = cli_exec(cmd)?; + let old_dir_name = format!("{}/{}", workdir_path, chart.name); + let new_dir_name: String; + match serde_yaml::from_str::(&helm_stdout) { + Ok(res) => { + new_dir_name = format!("{}-{}", old_dir_name, res.version); + rename(old_dir_name, new_dir_name.clone())?; + } + Err(err) => return Err(Box::from(err)), + }; + + // TODO: Do we really need it? + let cmd = "helm show chart . | yq '.version'".to_string(); + let _version = cli_exec_from_dir(cmd, new_dir_name.clone())?; + Ok(new_dir_name) + } + + fn pull_default(&self, chart: Chart, workdir_path: String) -> Result> { + // Add repo and update + let repo_local_name = general_purpose::STANDARD_NO_PAD.encode(self.get_url()); + let cmd = format!("helm repo add {} {}", repo_local_name, self.get_url()); + cli_exec(cmd)?; + cli_exec("helm repo update".to_string())?; + + let args = match chart.version.as_str() { + LATEST_VERSION => "".to_string(), + _ => format!("--version {}", chart.version.clone()), + }; + let cmd = format!( + "helm pull {}/{} {} --destination {} --untar", + repo_local_name, chart.name, args, workdir_path + ); + cli_exec(cmd)?; + + // Get the version + let cmd = format!("helm show chart {}/{}", workdir_path, chart.name); + let helm_stdout = cli_exec(cmd)?; + let old_dir_name = format!("{}/{}", workdir_path, chart.name); + let new_dir_name: String; + match serde_yaml::from_str::(&helm_stdout) { + Ok(res) => { + new_dir_name = format!("{}-{}", old_dir_name, res.version); + rename(old_dir_name, new_dir_name.clone())?; + } + Err(err) => return Err(Box::from(err)), + }; + + //cleaning up + let cmd = format!("helm repo remove {}", repo_local_name); + cli_exec(cmd)?; + + // TODO: Do we really need it? + let cmd = "helm show chart . | yq '.version'".to_string(); + let _version = cli_exec_from_dir(cmd, new_dir_name.clone())?; + Ok(new_dir_name) + } +} diff --git a/lib/src/helm/mod.rs b/lib/src/helm/mod.rs new file mode 100644 index 0000000..de4af12 --- /dev/null +++ b/lib/src/helm/mod.rs @@ -0,0 +1,4 @@ +pub mod chart; +pub mod git_repository; +pub mod helm_repository; +pub mod repository; diff --git a/lib/src/helm/repository.rs b/lib/src/helm/repository.rs new file mode 100644 index 0000000..ec0eebf --- /dev/null +++ b/lib/src/helm/repository.rs @@ -0,0 +1,36 @@ +use std::error::Error; + +use serde::{Deserialize, Serialize}; + +use super::{chart::Chart, git_repository::GitRepo, helm_repository::HelmRepo}; + +// A struct that represents a helm repository +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct Repository { + // A name of the repo. It's going to be used by tools + // to get a URL, so it can be any string + pub name: String, + pub helm: Option, + pub git: Option, +} + +// Supported kinds of helm repos +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) enum RepositoryKind { + // Regular helm repos and OCI + Helm, + // Git, it's not supposed to use helm-git plugin + // but instead it's just using git to get a repo + // and then look for charts in the repo + Git, +} + +pub trait RepositoryImpl { + fn pull_chart(&self, chart: Chart, workdir_path: String) -> Result>; + fn get_url(&self) -> String; +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct Version { + pub(crate) version: String, +} diff --git a/lib/src/include.rs b/lib/src/include.rs new file mode 100644 index 0000000..0c95047 --- /dev/null +++ b/lib/src/include.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct Include { + pub path: String, + pub kind: T, +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs new file mode 100644 index 0000000..b0c3446 --- /dev/null +++ b/lib/src/lib.rs @@ -0,0 +1,23 @@ +pub mod cli; +pub mod config; +pub mod git; +pub mod helm; +pub mod include; +pub mod output; +pub mod template; +pub mod workdir; + +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/lib/src/output.rs b/lib/src/output.rs new file mode 100644 index 0000000..899aea7 --- /dev/null +++ b/lib/src/output.rs @@ -0,0 +1,18 @@ +use console::style; +use std::error::Error; + +pub fn message_empty(msg: &str) { + println!(" {}", style(msg).blue()); +} + +pub fn message_info(msg: &str) { + let prefix = format!("{}", style("-->")); + let msg = format!("{} {}", prefix, msg,); + println!(" {}", style(msg).blue()); +} + +pub fn message_error(err: Box) { + let prefix = format!("{}", style("!->").red()); + let msg = format!("{} {}", prefix, err); + println!(" {}", style(msg).red()); +} diff --git a/lib/src/template.rs b/lib/src/template.rs new file mode 100644 index 0000000..ae4c284 --- /dev/null +++ b/lib/src/template.rs @@ -0,0 +1,55 @@ +use chrono::prelude::*; +use handlebars::{handlebars_helper, Handlebars}; +use serde::Serialize; + +handlebars_helper!(date_helper: | | Utc::now().format("%Y-%m-%d").to_string()); +handlebars_helper!(time_helper: | | Utc::now().format("%H-%M-%S").to_string()); + +pub fn register_handlebars() -> Handlebars<'static> { + let mut handlebars = Handlebars::new(); + handlebars.register_helper("date", Box::new(date_helper)); + handlebars.register_helper("time", Box::new(time_helper)); + handlebars +} + +pub fn render(string: String, data: &T) -> Result> +where + T: Serialize, +{ + let mut reg = register_handlebars(); + let tmpl_name = "template"; + reg.register_template_string(tmpl_name, string)?; + let result = reg.render(tmpl_name, data)?; + Ok(result) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use chrono::Utc; + + use crate::template::register_handlebars; + + #[test] + fn test_date_helper() { + let mut reg = register_handlebars(); + reg.register_template_string("test", "{{ date }}").unwrap(); + let result = reg + .render("test", &HashMap::::new()) + .unwrap(); + let expected = Utc::now().format("%Y-%m-%d").to_string(); + assert_eq!(result, expected); + } + + #[test] + fn test_time_helper() { + let mut reg = register_handlebars(); + reg.register_template_string("test", "{{ time }}").unwrap(); + let result = reg + .render("test", &HashMap::::new()) + .unwrap(); + let expected = Utc::now().format("%H-%M-%S").to_string(); + assert_eq!(result, expected); + } +} diff --git a/lib/src/workdir.rs b/lib/src/workdir.rs new file mode 100644 index 0000000..d3ec31a --- /dev/null +++ b/lib/src/workdir.rs @@ -0,0 +1,56 @@ +use tempfile::tempdir; + +use crate::output::message_info; +use std::{error::Error, fs::create_dir, path::PathBuf}; + +pub fn setup_workdir(path: Option) -> Result> { + let path = match path { + Some(res) => { + message_info(&format!("trying to create a dir: {}", res)); + match create_dir(res.clone()) { + Ok(_) => PathBuf::from(res), + Err(err) => { + let _msg = format!("couldn't create dir {}", res); + return Err(Box::from(err)); + } + } + } + + None => { + message_info("trying to create a temporary dir"); + // I'm using into_path to prevent the dir from being removed + tempdir()?.into_path() + } + }; + Ok(path.into_os_string().into_string().unwrap()) +} + +#[cfg(test)] +mod tests { + use std::{error::Error, fs::remove_dir_all, path::Path}; + + use tempfile::tempdir; + + use crate::workdir::setup_workdir; + + #[test] + fn test_temporary_dir() -> Result<(), Box> { + let wd = setup_workdir(None)?; + let result = Path::new(&wd).exists(); + assert!(result); + remove_dir_all(wd)?; + Ok(()) + } + + #[test] + fn test_specified_dir() -> Result<(), Box> { + let dir = tempdir()?; + let path = dir.path().join("test").to_str().unwrap().to_string(); + let wd = setup_workdir(Some(path.clone()))?; + let result = Path::new(&wd).exists(); + assert!(result); + assert!(setup_workdir(Some(path)).is_err()); + remove_dir_all(dir)?; + Ok(()) + } +}