From 09f81a5899ec0a263094c1301785eb950d1edf84 Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Wed, 10 Jan 2024 12:33:05 +0100 Subject: [PATCH] Init commit --- .gitignore | 1 + .woodpecker/build.yaml | 28 + Cargo.lock | 1104 +++++++++++++++++ Cargo.toml | 23 + Containerfile | 30 + examples/extensions/flux2/crd-configmap.yaml | 28 + examples/extensions/flux2/crd-job.yaml | 80 ++ examples/extensions/flux2/crd-np.yaml | 59 + examples/extensions/flux2/crd-rbac.yaml | 62 + .../extensions/flux2/crd-serviceaccount.yaml | 15 + .../vaultwarden/virtual-service.yaml | 30 + examples/patches/flux-regexp/patch.yaml | 51 + examples/patches/git/patch-2.diff | 34 + examples/patches/git/patch.diff | 13 + examples/patches/regexp/patch.yaml | 7 + examples/yamlfmt.yml | 1 + helmule.yaml | 97 ++ src/config/extension.rs | 45 + src/config/mod.rs | 184 +++ src/config/patch.rs | 200 +++ src/helpers/cli.rs | 66 + src/helpers/copy.rs | 21 + src/helpers/mod.rs | 2 + src/main.rs | 180 +++ src/mirror/custom_command.rs | 33 + src/mirror/git.rs | 60 + src/mirror/mod.rs | 53 + src/patch/mod.rs | 1 + src/patch/regexp.rs | 9 + src/source/git.rs | 69 ++ src/source/helm.rs | 105 ++ src/source/mod.rs | 46 + 32 files changed, 2737 insertions(+) create mode 100644 .gitignore create mode 100644 .woodpecker/build.yaml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Containerfile create mode 100644 examples/extensions/flux2/crd-configmap.yaml create mode 100644 examples/extensions/flux2/crd-job.yaml create mode 100644 examples/extensions/flux2/crd-np.yaml create mode 100644 examples/extensions/flux2/crd-rbac.yaml create mode 100644 examples/extensions/flux2/crd-serviceaccount.yaml create mode 100644 examples/extensions/vaultwarden/virtual-service.yaml create mode 100644 examples/patches/flux-regexp/patch.yaml create mode 100644 examples/patches/git/patch-2.diff create mode 100644 examples/patches/git/patch.diff create mode 100644 examples/patches/regexp/patch.yaml create mode 100644 examples/yamlfmt.yml create mode 100644 helmule.yaml create mode 100644 src/config/extension.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/patch.rs create mode 100644 src/helpers/cli.rs create mode 100644 src/helpers/copy.rs create mode 100644 src/helpers/mod.rs create mode 100644 src/main.rs create mode 100644 src/mirror/custom_command.rs create mode 100644 src/mirror/git.rs create mode 100644 src/mirror/mod.rs create mode 100644 src/patch/mod.rs create mode 100644 src/patch/regexp.rs create mode 100644 src/source/git.rs create mode 100644 src/source/helm.rs create mode 100644 src/source/mod.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml new file mode 100644 index 0000000..48699f7 --- /dev/null +++ b/.woodpecker/build.yaml @@ -0,0 +1,28 @@ +# Build a container image +when: + event: + - push +steps: + build: + image: git.badhouseplants.net/badhouseplants/badhouseplants-builder:555262114ea81f6f286010474527f419b56d33a3 + name: Build helmule operator image + privileged: true + environment: + - PACKAGE_NAME=allanger/helmule + commands: + - | + if [[ "${CI_COMMIT_TAG}" ]]; then + export CUSTOM_TAG="${CI_COMMIT_TAG}"; + fi + - build-container + secrets: + - gitea_token + backend_options: + kubernetes: + resources: + requests: + memory: 800Mi + cpu: 500m + limits: + memory: 1000Mi + cpu: 1000m diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1902e45 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1104 @@ +# 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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +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 = "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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eb9105919ca8e40d437fc9cbb8f1975d916f1bd28afe795a48aae32a2cc8920" +dependencies = [ + "cfg-if", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc6598521bb5a83d491e8c1fe51db7296019d2ca3cb93cc6c2a20369a4d78a2" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" +dependencies = [ + "cfg-if", +] + +[[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 = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[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 = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +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.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94eae21d01d20dabef65d8eda734d83df6e2dea8166788804be9bd6bc92448fa" +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 = "helmule" +version = "0.1.0" +dependencies = [ + "base64", + "chrono", + "clap", + "dircpy", + "env_logger", + "handlebars", + "log", + "regex", + "semver", + "semver_sort", + "serde", + "serde_json", + "serde_yaml", + "tempfile", + "time", +] + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[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 = "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.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +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 = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + +[[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 = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "pest" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" +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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "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 = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + +[[package]] +name = "semver_sort" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd382d83fd0a8acafe3be2432f8f94abae2c26f94d2ff76fec99732cf30f233" +dependencies = [ + "regex", +] + +[[package]] +name = "serde" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fbd975230bada99c8bb618e0c365c2eefa219158d5c6c29610fd09ff1833257" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15e0ef66bf939a7c890a0bf6d5a733c70202225f9888a89ed5c62298b019129" +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 = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" +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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3de26b0965292219b4287ff031fcba86837900fe9cd2b34ea8ad893c0953d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "268026685b2be38d7103e9e507c938a1fcb3d7e6eb15e87870b617bf37b6d581" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +dependencies = [ + "time-core", +] + +[[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 = "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.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..dfa7d65 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[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] +base64 = "0.21.5" +chrono = "0.4.31" +clap = { version = "4.4.11", features = ["derive"] } +dircpy = "0.3.15" +env_logger = "0.10.1" +handlebars = "5.0.0" +log = "0.4.20" +regex = "1.10.2" +semver = "1.0.20" +semver_sort = "1.0.0" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.110" +serde_yaml = "0.9.29" +tempfile = "3.9.0" +time = { version = "0.3.31", features = ["serde", "formatting", "parsing"]} diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..2ad9337 --- /dev/null +++ b/Containerfile @@ -0,0 +1,30 @@ +FROM rust:1.70.0-alpine3.18 as builder +WORKDIR /src +RUN apk update && apk add --no-cache gcc musl-dev +COPY ./ . +RUN rustup default nightly && rustup update +RUN cargo build --release --jobs 2 -Z sparse-registry + +FROM ghcr.io/allanger/dumb-downloader as dudo +RUN apt-get update -y && apt-get install tar git -y +ARG HELM_VERSION=v3.13.3 +ARG YQ_VERSION=v4.40.5 +ENV RUST_LOG=info +RUN dudo -l "https://get.helm.sh/helm-{{ version }}-{{ os }}-{{ arch }}.tar.gz" -d /tmp/helm.tar.gz -p $HELM_VERSION +RUN dudo -l "https://github.com/mikefarah/yq/releases/download/{{ version }}/yq_{{ os }}_{{ arch }}.tar.gz" -d /tmp/yq.tar.gz -p $YQ_VERSION +RUN tar -xf /tmp/helm.tar.gz -C /tmp && rm -f /tmp/helm.tar.gz +RUN tar -xf /tmp/yq.tar.gz -C /tmp && rm -f /tmp/yq.tar.gz +RUN mkdir /out +RUN cp `find /tmp | grep helm` /out/ +RUN mv `find /tmp | grep yq_` /out/yq +RUN chmod +x /out/helm +RUN chmod +x /out/yq + +FROM alpine:3.18 +RUN apk update && apk add --no-cache git +COPY --from=builder /src/target/release/helmule /bin/helmule +COPY --from=dudo /out/ /usr/bin +WORKDIR /workdir +ENTRYPOINT ["/bin/helmule"] + + diff --git a/examples/extensions/flux2/crd-configmap.yaml b/examples/extensions/flux2/crd-configmap.yaml new file mode 100644 index 0000000..daf76b8 --- /dev/null +++ b/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/examples/extensions/flux2/crd-job.yaml b/examples/extensions/flux2/crd-job.yaml new file mode 100644 index 0000000..38287b2 --- /dev/null +++ b/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/examples/extensions/flux2/crd-np.yaml b/examples/extensions/flux2/crd-np.yaml new file mode 100644 index 0000000..ddf49f4 --- /dev/null +++ b/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/examples/extensions/flux2/crd-rbac.yaml b/examples/extensions/flux2/crd-rbac.yaml new file mode 100644 index 0000000..15d141d --- /dev/null +++ b/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/examples/extensions/flux2/crd-serviceaccount.yaml b/examples/extensions/flux2/crd-serviceaccount.yaml new file mode 100644 index 0000000..41a259b --- /dev/null +++ b/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/examples/extensions/vaultwarden/virtual-service.yaml b/examples/extensions/vaultwarden/virtual-service.yaml new file mode 100644 index 0000000..e426a7b --- /dev/null +++ b/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/examples/patches/flux-regexp/patch.yaml b/examples/patches/flux-regexp/patch.yaml new file mode 100644 index 0000000..6f3fdaa --- /dev/null +++ b/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/examples/patches/git/patch-2.diff b/examples/patches/git/patch-2.diff new file mode 100644 index 0000000..33b939c --- /dev/null +++ b/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/examples/patches/git/patch.diff b/examples/patches/git/patch.diff new file mode 100644 index 0000000..d376b1c --- /dev/null +++ b/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/examples/patches/regexp/patch.yaml b/examples/patches/regexp/patch.yaml new file mode 100644 index 0000000..a6adb92 --- /dev/null +++ b/examples/patches/regexp/patch.yaml @@ -0,0 +1,7 @@ +--- +name: Add spaces before comments +targets: + - values.yaml +before: |- + ^.*[\S]+.*# +after: " #" diff --git a/examples/yamlfmt.yml b/examples/yamlfmt.yml new file mode 100644 index 0000000..e1e92dc --- /dev/null +++ b/examples/yamlfmt.yml @@ -0,0 +1 @@ +pad_line_comments: 2 diff --git a/helmule.yaml b/helmule.yaml new file mode 100644 index 0000000..2b75afb --- /dev/null +++ b/helmule.yaml @@ -0,0 +1,97 @@ +# mirror charts +repositories: + - name: metrics-server + helm: + url: https://kubernetes-sigs.github.io/metrics-server/ + - name: db-operator + git: + url: https://github.com/db-operator/charts.git + ref: main + path: charts + - name: badhouseplants + helm: + url: https://badhouseplants.github.io/helm-charts/ + - name: flux-community + helm: + url: https://fluxcd-community.github.io/helm-charts +charts: + - 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: flux2 + repository: flux-community + extensions: + - name: Create a job that will apply crds + target_dir: templates/crd-install + source_dir: ./examples/extensions/flux2 + patches: + - name: Add crds to chart files + custom_command: + commands: + - mkdir crd-base + - |- + cd crd-base && helm template flux . \ + | yq '. | select(.kind == "CustomResourceDefinition")' \ + | yq -s '.kind + "-" + .metadata.name' + - name: Remove CRDs from the templates + custom_command: + commands: + - find . -name "*crds*" -type f -delete + - name: Remove installCRDs value from the default values + regexp: + path: ./examples/patches/flux-regexp + - 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: + - custom-command +mirrors: + - name: badhouseplants-git + git: + url: git@git.badhouseplants.net:allanger/helmuled-charts.git + branch: upgrade-{{ name }}-to-{{ version }} + path: charts/{{ name }} + commit: |- + chore: mirror {{ name }}-{{ version }} + + upstream_repo: {{ repo_url }} + - name: custom-command + custom_command: + package: + - zip -r {{ name }}-{{ version }}.zip {{ name }}-{{ version }} + upload: + - rm -f /tmp/{{ name }}-{{ version }}.zip + - rm -rf /tmp/{{ name }}-{{ version }} + - cp {{ name }}-{{ version }}.zip /tmp + - unzip /tmp/{{ name }}-{{ version }}.zip -d /tmp/{{ name }}-{{ version}} diff --git a/src/config/extension.rs b/src/config/extension.rs new file mode 100644 index 0000000..8d50f0e --- /dev/null +++ b/src/config/extension.rs @@ -0,0 +1,45 @@ +use std::{fs, path::Path}; + +use log::info; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct Extension { + name: Option, + target_dir: String, + source_dir: String, +} + +impl Extension { + pub(crate) fn apply(&self, chart_local_path: String) -> Result<(), Box> { + let extension_name = match self.name.clone() { + Some(res) => res, + None => "Unnamed".to_string(), + }; + info!("applying extension: '{}'", extension_name); + let target_dir = format!("{}/{}", chart_local_path, self.target_dir); + info!("trying to create a dir: {}", target_dir); + fs::create_dir(target_dir.clone())?; + info!("copying {} to {}", self.source_dir, target_dir); + copy_recursively(self.source_dir.clone(), target_dir)?; + Ok(()) + } +} + +/// Copy files from source to destination recursively. +pub fn copy_recursively( + source: impl AsRef, + destination: impl AsRef, +) -> Result<(), Box> { + for entry in fs::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 { + info!("trying to copy {:?}", entry.path()); + fs::copy(entry.path(), destination.as_ref().join(entry.file_name()))?; + } + } + Ok(()) +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..2d53d2d --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,184 @@ +use log::{info, warn}; +use serde::{Deserialize, Serialize}; +use std::fs::File; + +pub(crate) mod extension; +pub(crate) mod patch; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct Config { + pub(crate) repositories: Vec, + pub(crate) charts: Vec, + pub(crate) mirrors: Vec, +} + +impl Config { + pub(crate) fn new(config_path: String) -> Result> { + info!("reading the config file"); + let config_content = File::open(config_path)?; + let config: Config = serde_yaml::from_reader(config_content)?; + Ok(config) + } +} + +pub(crate) enum RepositoryKind { + Helm, + Git, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct Repository { + // A name of the repository to be references by charts + pub(crate) name: String, + // Helm repository data + pub(crate) helm: Option, + pub(crate) git: Option, +} + +#[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, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct GitMirror { + pub(crate) url: String, + pub(crate) path: Option, + pub(crate) branch: String, + pub(crate) commit: Option, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct Helm { + // A url of the helm repository + pub(crate) url: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct Git { + pub(crate) url: String, + #[serde(alias = "ref")] + pub(crate) git_ref: String, + pub(crate) path: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct Chart { + // A name of the helm chart + pub(crate) name: String, + // A reference to repository by name + pub(crate) repository: String, + pub(crate) mirrors: Vec, + // Versions to be mirrored + pub(crate) version: Option, + // A repository object + pub(crate) extensions: Option>, + pub(crate) patches: Option>, + #[serde(skip_serializing)] + pub(crate) repository_obj: Option, + #[serde(skip_serializing)] + pub(crate) mirror_objs: Option>, +} + +impl Chart { + pub(crate) fn populate_repository( + &mut self, + repositories: Vec, + ) -> Result<(), Box> { + for repository in repositories { + if repository.name == self.repository { + self.repository_obj = Some(repository); + return Ok(()); + } + } + //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); + return Err(Box::from(error_msg)); + } + // TODO: Handle the "mirror not found" error + pub(crate) fn populate_mirrors( + &mut self, + mirrors: Vec, + ) -> Result<(), Box> { + let mut mirror_objs: Vec = vec![]; + + for mirror_global in mirrors.clone() { + for mirror_name in self.mirrors.clone() { + if mirror_name == mirror_global.name.clone() { + mirror_objs.push(mirror_global.clone()); + } + } + } + if mirror_objs.len() > 0 { + self.mirror_objs = Some(mirror_objs); + } + Ok(()) + } + + pub(crate) fn get_helm_repository_url(&self) -> String { + match self.repository_obj.clone() { + Some(res) => res.helm.unwrap().url, + None => { + warn!("repository object is not filled for chart {}", self.name); + return "".to_string(); + } + } + } + + pub(crate) fn get_git_repository_url(&self) -> String { + match self.repository_obj.clone() { + Some(res) => res.git.unwrap().url, + None => { + warn!("repository object is not filled for chart {}", self.name); + return "".to_string(); + } + } + } + + pub(crate) fn get_git_repository_ref(&self) -> String { + match self.repository_obj.clone() { + Some(res) => res.git.unwrap().git_ref, + None => { + warn!("repository object is not filled for chart {}", self.name); + return "".to_string(); + } + } + } + + pub(crate) fn get_git_repository_path(&self) -> String { + match self.repository_obj.clone() { + Some(res) => res.git.unwrap().path, + None => { + warn!("repository object is not filled for chart {}", self.name); + return "".to_string(); + } + } + } + + pub(crate) fn get_repo_kind(&self) -> Result> { + match &self.repository_obj { + Some(res) => { + if res.helm.is_some() { + return Ok(RepositoryKind::Helm); + } else if res.git.is_some() { + return Ok(RepositoryKind::Git); + } else { + return Err(Box::from("unknown repository kind is found")); + } + } + None => { + return Err(Box::from( + "repository object is not filled up for the chart", + )); + } + } + } +} diff --git a/src/config/patch.rs b/src/config/patch.rs new file mode 100644 index 0000000..d298c51 --- /dev/null +++ b/src/config/patch.rs @@ -0,0 +1,200 @@ +use std::{ + fs::{self, read_dir, remove_dir_all, File, OpenOptions}, + io::Write, + path::{Path, PathBuf}, +}; + +use log::{error, info}; +use serde::{Deserialize, Serialize}; + +use crate::helpers::cli::{cli_exec, cli_exec_from_dir}; + +#[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 { + regexp: Option, + git: Option, + custom_command: Option, + yq: Option, +} + +impl Patch { + pub(crate) fn apply(&self, chart_local_path: String) -> Result<(), Box> { + let patch_action = patch_action_from_definition(self.clone())?; + patch_action.apply(chart_local_path) + } +} + +trait PatchInterface { + fn apply(&self, chart_local_path: String) -> Result<(), Box>; +} + +impl PatchInterface for YqPatch { + fn apply(&self, chart_local_path: String) -> Result<(), Box> { + let cmd = match self.op { + YqOperations::Add => format!( + "yq -i '{} += \"{}\"' {}", + self.key, + self.value.clone().unwrap(), + self.file + ), + YqOperations::Delete => format!("yq -i \'del({})\' {}", self.key, self.file), + YqOperations::Replace => format!( + "yq e -i \'{} = \"{}\"\' {}", + self.key, + self.value.clone().unwrap(), + self.file + ), + }; + cli_exec_from_dir(cmd, chart_local_path)?; + Ok(()) + } +} + +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() { + error!( + "reading dirs is not supported yet, skipping {:?}", + entry.path() + ); + } else { + info!("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: crate::patch::regexp::RegexpPatch = + match crate::patch::regexp::RegexpPatch::deserialize(patch_des) { + Ok(patch) => patch, + Err(err) => return Err(Box::from(err)), + }; + info!("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(()) + } +} + +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(()) + } +} + +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 patch_action_from_definition( + patch: Patch, +) -> Result, Box> { + if let Some(regexp) = patch.regexp { + return 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); + let can_path = fs::canonicalize(&path).ok().unwrap(); + can_path.into_os_string().into_string().ok().unwrap() + }, + })); + } 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/src/helpers/cli.rs b/src/helpers/cli.rs new file mode 100644 index 0000000..05e89f6 --- /dev/null +++ b/src/helpers/cli.rs @@ -0,0 +1,66 @@ +use std::process::{Command, ExitStatus}; + +use log::info; + +pub(crate) fn cli_exec(command: String) -> Result> { + info!("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(crate) fn cli_exec_from_dir( + command: String, + dir: String, +) -> Result> { + info!("executing: {}", command); + 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) +} +#[cfg(test)] +mod tests { + use crate::helpers::cli::{cli_exec, cli_exec_from_dir}; + + #[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 = tempfile::tempdir().unwrap(); + let command = "echo $PWD"; + let dir_str = dir.into_path().into_os_string().into_string().unwrap(); + let test = cli_exec_from_dir(command.to_string(), dir_str.clone()); + assert!(test.unwrap().to_string().contains(dir_str.as_str())); + } +} diff --git a/src/helpers/copy.rs b/src/helpers/copy.rs new file mode 100644 index 0000000..e7d51af --- /dev/null +++ b/src/helpers/copy.rs @@ -0,0 +1,21 @@ +use std::{fs, path::Path}; + +use log::info; + +/// Copy files from source to destination recursively. +pub(crate) fn copy_recursively( + source: impl AsRef, + destination: impl AsRef, +) -> Result<(), Box> { + for entry in fs::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 { + info!("trying to copy {:?}", entry.path()); + fs::copy(entry.path(), destination.as_ref().join(entry.file_name()))?; + } + } + Ok(()) +} diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs new file mode 100644 index 0000000..645c2d0 --- /dev/null +++ b/src/helpers/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod cli; +pub(crate) mod copy; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b5a0089 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,180 @@ +pub(crate) mod config; +pub(crate) mod helpers; +pub(crate) mod mirror; +pub(crate) mod patch; +pub(crate) mod source; + +use clap::Parser; +use log::{error, info}; +use std::fs; +use std::{fs::create_dir, path::PathBuf, process::exit}; +use tempfile::TempDir; + +use crate::config::patch::init_git_repo; + +/// 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, + /// 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 main() { + env_logger::init(); + let args = Args::parse(); + // Prepare the workdir + let workdir_path = match args.workdir { + Some(res) => match create_dir(res.clone()) { + Ok(_) => { + let path = PathBuf::from(res); + let can_path = fs::canonicalize(&path).ok().unwrap(); + can_path.into_os_string().into_string().ok().unwrap() + } + Err(err) => { + error!("{}", err); + exit(1); + } + }, + None => { + let tmp_dir = match TempDir::new() { + Ok(res) => res, + Err(err) => { + error!("{}", err); + exit(1); + } + }; + match tmp_dir.path().to_str() { + Some(res) => res.to_string(), + None => { + exit(1); + } + } + } + }; + + // Read the config + let config = match config::Config::new(args.config) { + Ok(res) => res, + Err(err) => { + error!("{}", err); + exit(1); + } + }; + + for mut chart in config.clone().charts { + match chart.populate_repository(config.repositories.clone()) { + Ok(_) => { + info!("repo is populated for chart {}", chart.name); + } + Err(err) => { + error!("{}", err); + exit(1); + } + } + match chart.populate_mirrors(config.mirrors.clone()) { + Ok(_) => { + info!("mirrors arepopulated for chart {}", chart.name) + } + Err(err) => { + error!("{}", err); + exit(1); + } + } + let chart_repo = match source::repo_from_chart(chart.clone()) { + Ok(res) => res, + Err(err) => { + error!("{}", err); + exit(1); + } + }; + match chart_repo.pull(workdir_path.clone()) { + Ok(res) => { + info!( + "succesfully pulled chart {} into {}", + chart.name.clone(), + res.path, + ); + if let Some(extensions) = chart.extensions.clone() { + for extension in extensions { + if let Err(err) = extension.apply(res.clone().path) { + error!("{}", err); + exit(1); + } + } + } + if let Some(patches) = chart.patches.clone() { + for patch in patches { + if let Err(err) = patch.apply(res.clone().path) { + error!("{}", err); + exit(1); + } + } + } + if let Some(init_git_patch) = args.init_git_patch.clone() { + if init_git_patch.contains(&chart.name) { + info!( + "init git patch mode is enabled, go to {} to make your changes", + res.path + ); + match init_git_repo(res.path) { + Ok(_) => { + info!("not mirroring, because of the init git patch mode"); + } + Err(err) => { + error!("{}", err); + exit(1); + } + }; + break; + } + } + if let Some(mirrors) = chart.mirror_objs.clone() { + for mirror_obj in mirrors { + match mirror::mirror_from_mirror_obj(mirror_obj.clone()) { + Ok(mirror) => { + match mirror.push(workdir_path.clone(), res.clone(), args.dry_run) { + Ok(_) => info!( + "mirrored {} to {}", + chart.name.clone(), + mirror_obj.name + ), + Err(err) => { + error!("{}", err); + exit(1); + } + }; + } + Err(err) => { + error!("{}", err); + exit(1); + } + } + } + } + } + Err(err) => { + error!("{}", err); + exit(1); + } + } + } + + // Populate charts + // Download helm charts from config + // If workdir is not provided, create a temporary di +} diff --git a/src/mirror/custom_command.rs b/src/mirror/custom_command.rs new file mode 100644 index 0000000..c4f3983 --- /dev/null +++ b/src/mirror/custom_command.rs @@ -0,0 +1,33 @@ +use crate::helpers::cli::cli_exec_from_dir; + +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_local: crate::source::ChartLocal, + dry_run: bool, + ) -> Result<(), Box> { + for cmd_tmpl in self.package.clone() { + let mut reg = super::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 = super::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(()) + } +} diff --git a/src/mirror/git.rs b/src/mirror/git.rs new file mode 100644 index 0000000..40f17c7 --- /dev/null +++ b/src/mirror/git.rs @@ -0,0 +1,60 @@ +use crate::{helpers::cli::cli_exec_from_dir, source::ChartLocal}; +use dircpy::*; + +use super::Target; + +pub(crate) struct Git { + pub(crate) git_dir: String, + pub(crate) url: String, + pub(crate) path: String, + pub(crate) branch: String, + pub(crate) commit: Option, +} + +impl Target for Git { + fn push( + &self, + workdir_path: String, + chart_local: ChartLocal, + dry_run: bool, + ) -> Result<(), Box> { + let cmd = format!("git clone {} {}", self.url, self.git_dir); + cli_exec_from_dir(cmd, workdir_path.clone())?; + let git_repo_path = format!("{}/{}", workdir_path, self.git_dir); + + // Prepare branch + let mut reg = super::register_handlebars(); + reg.register_template_string("branch", self.branch.clone())?; + let branch = reg.render("branch", &chart_local)?; + let cmd = format!("git checkout {}", branch); + if let Err(_) = cli_exec_from_dir(cmd, git_repo_path.clone()) { + let cmd = format!("git checkout -b {}", branch); + cli_exec_from_dir(cmd, git_repo_path.clone())?; + }; + // Prepare path + reg.register_template_string("path", self.path.clone())?; + let path = reg.render("path", &chart_local)?; + let repo_local_full_path = format!("{}/{}", git_repo_path, path); + CopyBuilder::new(chart_local.path.clone(), repo_local_full_path.clone()) + .overwrite_if_size_differs(true) + .run()?; + + // Prepare the commit message + let commit_message = match self.commit.clone() { + Some(commit) => commit, + None => "helmuled {{ name }}-{{ version }}".to_string(), + }; + reg.register_template_string("commit", commit_message.clone())?; + let commit = reg.render("commit", &chart_local)?; + let cmd = format!( + "git add . && git diff --staged --quiet || git commit -m '{}'", + commit + ); + cli_exec_from_dir(cmd, repo_local_full_path.clone())?; + if !dry_run { + let cmd = format!("git push --set-upstream origin {}", branch); + cli_exec_from_dir(cmd, repo_local_full_path)?; + } + Ok(()) + } +} diff --git a/src/mirror/mod.rs b/src/mirror/mod.rs new file mode 100644 index 0000000..55fd873 --- /dev/null +++ b/src/mirror/mod.rs @@ -0,0 +1,53 @@ +use chrono::prelude::*; +use handlebars::{handlebars_helper, Handlebars}; +use time::{format_description::parse, OffsetDateTime}; + +use crate::{config::Mirror, source::ChartLocal}; + +pub(crate) mod custom_command; +pub(crate) mod git; + +pub(crate) trait Target { + fn push( + &self, + workdir_path: String, + chart_local: ChartLocal, + dry_run: bool, + ) -> Result<(), Box>; +} + +pub(crate) fn mirror_from_mirror_obj( + mirror: Mirror, +) -> Result, Box> { + if let Some(git) = mirror.git { + return Ok(Box::from(git::Git { + git_dir: mirror.name.clone(), + url: git.url, + path: match git.path { + Some(path) => path, + None => "".to_string(), + }, + branch: git.branch, + commit: git.commit, + })); + } 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 + ))) +} + +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(crate) 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 +} diff --git a/src/patch/mod.rs b/src/patch/mod.rs new file mode 100644 index 0000000..0afccae --- /dev/null +++ b/src/patch/mod.rs @@ -0,0 +1 @@ +pub(crate) mod regexp; diff --git a/src/patch/regexp.rs b/src/patch/regexp.rs new file mode 100644 index 0000000..0110215 --- /dev/null +++ b/src/patch/regexp.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct RegexpPatch { + pub(crate) name: String, + pub(crate) targets: Vec, + pub(crate) before: Option, + pub(crate) after: Option, +} diff --git a/src/source/git.rs b/src/source/git.rs new file mode 100644 index 0000000..9b6da1c --- /dev/null +++ b/src/source/git.rs @@ -0,0 +1,69 @@ +use std::fs::{self, rename}; + +use crate::helpers::cli::{cli_exec, cli_exec_from_dir}; +use base64::{engine::general_purpose, Engine as _}; + +use super::{ChartLocal, Repo}; + +pub(crate) struct Git { + git_url: String, + git_ref: String, + path: String, + pub(crate) chart: String, +} + +impl From for Git { + fn from(value: crate::config::Chart) -> Self { + Git { + git_url: value.get_git_repository_url(), + git_ref: value.get_git_repository_ref(), + path: value.get_git_repository_path(), + chart: value.name, + } + } +} + +impl Repo for Git { + fn pull(&self, workdir_path: String) -> Result> { + let repo_local_name = general_purpose::STANDARD_NO_PAD.encode(self.git_url.clone()); + let cmd = format!( + "git clone {} {}/{}", + self.git_url, workdir_path, repo_local_name + ); + cli_exec(cmd)?; + + let cmd = format!( + "git -C {}/{} checkout {}", + workdir_path, repo_local_name, self.git_ref + ); + cli_exec(cmd)?; + + let old_dir_name = format!( + "{}/{}/{}/{}", + workdir_path, repo_local_name, self.path, self.chart + ); + 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, self.chart, 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_local_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(ChartLocal { + name: self.chart.clone(), + version, + path: new_dir_name, + repo_url: self.git_url.clone(), + }) + } +} diff --git a/src/source/helm.rs b/src/source/helm.rs new file mode 100644 index 0000000..fb1b71f --- /dev/null +++ b/src/source/helm.rs @@ -0,0 +1,105 @@ +use super::{ChartLocal, Repo}; +use crate::helpers::cli::{cli_exec, cli_exec_from_dir}; +use base64::{engine::general_purpose, Engine as _}; +use std::fs::rename; + +pub(crate) enum RepoKind { + Default, + Oci, +} + +const LATEST_VERSION: &str = "latest"; + +pub(crate) struct Helm { + pub(crate) chart: String, + pub(crate) repository_url: String, + pub(crate) version: String, +} + +impl From for Helm { + fn from(value: crate::config::Chart) -> Self { + Helm { + chart: value.name.clone(), + repository_url: value.get_helm_repository_url(), + version: match value.version { + Some(res) => res, + None => LATEST_VERSION.to_string(), + }, + } + } +} + +impl Helm { + fn repo_kind_from_url(&self) -> Result> { + let prefix = self + .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_default(&self, workdir_path: String) -> Result> { + // Add repo and update + let repo_local_name = general_purpose::STANDARD_NO_PAD.encode(self.repository_url.clone()); + let cmd = format!("helm repo add {} {}", repo_local_name, self.repository_url); + cli_exec(cmd)?; + cli_exec("helm repo update".to_string())?; + + let args = match self.version.as_str() { + LATEST_VERSION => "".to_string(), + _ => format!("--version {}", self.version.clone()), + }; + let cmd = format!( + "helm pull {}/{} {} --destination {} --untar", + repo_local_name, &self.chart, args, workdir_path + ); + cli_exec(cmd)?; + + // Get the version + let cmd = format!("helm show chart {}/{}", workdir_path, &self.chart); + let helm_stdout = cli_exec(cmd)?; + let old_dir_name = format!("{}/{}", workdir_path, &self.chart); + 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)?; + + let cmd = "helm show chart . | yq '.version'".to_string(); + let version = cli_exec_from_dir(cmd, new_dir_name.clone())?; + Ok(ChartLocal { + name: self.chart.clone(), + version, + path: new_dir_name, + repo_url: self.repository_url.clone(), + }) + } +} + +impl Repo for Helm { + fn pull(&self, workdir_path: String) -> Result> { + let repository_kind = self.repo_kind_from_url()?; + let path = match repository_kind { + RepoKind::Default => self.pull_default(workdir_path)?, + RepoKind::Oci => { + todo!() + } + }; + Ok(path) + } +} diff --git a/src/source/mod.rs b/src/source/mod.rs new file mode 100644 index 0000000..d675ef5 --- /dev/null +++ b/src/source/mod.rs @@ -0,0 +1,46 @@ +use crate::config::Chart; +use serde::{Deserialize, Serialize}; + +pub(crate) mod git; +pub(crate) mod helm; + +#[derive(Deserialize, Serialize, Clone)] +pub(crate) struct ChartLocal { + pub(crate) name: String, + pub(crate) version: String, + pub(crate) path: String, + pub(crate) repo_url: String, +} + +impl ChartLocal { + pub(crate) fn test(&self) -> String { + "test-me-if-you-can".to_string() + } +} + +pub(crate) trait Repo { + fn pull(&self, workdir_path: String) -> Result>; +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub(crate) struct Version { + pub(crate) version: String, +} + +pub(crate) fn repo_from_chart(chart: Chart) -> Result, Box> { + match chart.get_repo_kind() { + Ok(res) => { + return match res { + crate::config::RepositoryKind::Helm => { + let helm: helm::Helm = chart.into(); + Ok(Box::new(helm)) + } + crate::config::RepositoryKind::Git => { + let git: git::Git = chart.into(); + Ok(Box::new(git)) + } + } + } + Err(err) => return Err(Box::from(err)), + }; +}