├── .github └── workflows │ ├── push.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── PKGBUILD ├── README.md ├── cfg ├── filter.go ├── type.go └── validate.go ├── cmd ├── init.go ├── root.go ├── sync.go ├── validate.go └── version.go ├── exec └── helm.go ├── go.mod ├── go.sum ├── helm_freeze_logo.png ├── main.go └── util └── io.go /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: Push 3 | jobs: 4 | tests: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v2 9 | with: 10 | fetch-depth: 0 11 | - name: Fetch tags 12 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 13 | - name: Set up Go 14 | uses: actions/setup-go@main 15 | with: 16 | go-version: 1.19.x 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v2 19 | with: 20 | version: latest 21 | - name: Run GoReleaser 22 | uses: goreleaser/goreleaser-action@v1 23 | with: 24 | version: latest 25 | args: release --rm-dist --skip-publish --skip-validate 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | create: 5 | tags: 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - name: Fetch tags 16 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 17 | - name: Ensure tag match the current version 18 | run: | 19 | if [ "v$(grep '// ci-version-check' cmd/version.go | sed -r 's/.+return\s"(.+)".+/\1/')" != "$(git tag | sort --version-sort | tail -1)" ] ; then 20 | echo "Tag version do not match application version" 21 | exit 1 22 | fi 23 | - name: Set up Go 24 | uses: actions/setup-go@main 25 | with: 26 | go-version: 1.22.x 27 | - name: golangci-lint 28 | uses: golangci/golangci-lint-action@v2 29 | with: 30 | version: latest 31 | - name: Run GoReleaser 32 | uses: goreleaser/goreleaser-action@v1 33 | with: 34 | version: latest 35 | args: release --clean 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GORELEASER_QOVERY_TOKEN }} 38 | - name: Prepare AUR package 39 | run: | 40 | version=$(awk -F'"' '/ci-version-check/{print $2}' cmd/version.go) 41 | md5version=$(curl -sL https://github.com/Qovery/helm-freeze/archive/v${version}.tar.gz --output - | md5sum | awk '{ print $1 }') 42 | sed -i "s/pkgver=tbd/pkgver=$version/" PKGBUILD 43 | echo "md5sums=('${md5version}')" >> PKGBUILD 44 | - name: Publish AUR package 45 | uses: KSXGitHub/github-actions-deploy-aur@v2.2.4 46 | with: 47 | pkgname: helm-freeze 48 | pkgbuild: ./PKGBUILD 49 | commit_username: ${{ secrets.AUR_USERNAME }} 50 | commit_email: ${{ secrets.AUR_EMAIL }} 51 | ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} 52 | commit_message: Update AUR package 53 | ssh_keyscan_types: rsa,dsa,ecdsa,ed25519 54 | force_push: "true" 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .dist/ 3 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - main: main.go 3 | binary: helm-freeze 4 | goos: 5 | - darwin 6 | - linux 7 | - windows 8 | goarch: 9 | - amd64 10 | - arm64 11 | archives: 12 | - format_overrides: 13 | - goos: windows 14 | format: zip 15 | checksum: 16 | name_template: 'checksums.txt' 17 | changelog: 18 | sort: asc 19 | filters: 20 | exclude: 21 | - '^docs:' 22 | - '^test:' 23 | brews: 24 | - 25 | name: helm-freeze 26 | goarm: 6 27 | repository: 28 | owner: qovery 29 | name: homebrew-helm-freeze 30 | url_template: "https://github.com/Qovery/helm-freeze/releases/download/{{ .Tag }}/{{ .ArtifactName }}" 31 | commit_author: 32 | name: Pierre Mavro 33 | email: pmavro@qovery.com 34 | directory: Formula 35 | homepage: "" 36 | description: "Freeze your charts in the wished versions" 37 | skip_upload: false 38 | scoops: 39 | - 40 | url_template: "https://github.com/Qovery/helm-freeze/releases/download/{{ .Tag }}/{{ .ArtifactName }}" 41 | repository: 42 | owner: qovery 43 | name: scoop-helm-freeze 44 | commit_author: 45 | name: qovery 46 | email: contact@qovery.com 47 | homepage: "https://docs.qovery.com" 48 | description: "Freeze your charts in the wished versions" 49 | license: GPL3 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Pierre Mavro 2 | pkgname=helm-freeze 3 | pkgver=tbd 4 | pkgrel=1 5 | pkgdesc="Freeze your charts in the wished versions" 6 | arch=(x86_64) 7 | url="https://github.com/Qovery/helm-freeze" 8 | license=('GPL') 9 | makedepends=(git go) 10 | source=("https://github.com/Qovery/helm-freeze/archive/v$pkgver.tar.gz") 11 | 12 | build() { 13 | cd "$pkgname-$pkgver" 14 | export CGO_LDFLAGS="${LDFLAGS}" 15 | export CGO_CFLAGS="${CFLAGS}" 16 | export CGO_CPPFLAGS="${CPPFLAGS}" 17 | export CGO_CXXFLAGS="${CXXFLAGS}" 18 | export GOFLAGS="-buildmode=pie -trimpath -mod=readonly -modcacherw" 19 | go build -o $pkgname main.go 20 | } 21 | 22 | package() { 23 | cd "$pkgname-$pkgver" 24 | install -Dm755 "$pkgname" "$pkgdir/usr/bin/helm-freeze" 25 | } 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # helm-freeze 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/Qovery/helm-freeze)](https://goreportcard.com/report/github.com/Qovery/helm-freeze) 3 | [![Powered](https://img.shields.io/badge/Powered%20by-Qovery-blueviolet)](https://www.qovery.com) 4 | 5 |

6 | 7 |

8 | 9 | Freeze your charts in the wished versions. Helm freeze helps you to declare the charts 10 | you want to use in a desired version and download them locally. This to freeze/lock them 11 | directly in your Git repository. 12 | 13 | The advantages are: 14 | * Follow GitOps philosophy 15 | * Know exactly what has changed between 2 charts version with a `git diff` 16 | * One place to list them all 17 | * Works well with monorepo 18 | * Declarative configuration (YAML file) 19 | * Supports git repositories in addition to charts repositories 20 | 21 | ## Installation 22 | 23 | ### Mac OS 24 | On Mac, you need to have [brew](https://brew.sh/) installed, then you can run those commands: 25 | ```bash 26 | brew tap Qovery/helm-freeze 27 | brew install helm-freeze 28 | ``` 29 | 30 | ### Arch Linux 31 | An AUR package exists called `helm-freeze`, you can install it with `yay`: 32 | ```bash 33 | yay helm-freeze 34 | ``` 35 | 36 | ### Others 37 | You can download binaries from the [release section](https://github.com/Qovery/helm-freeze/releases). 38 | 39 | ## Usage 40 | To use helm-freeze, you need a configuration file. You can generate a default file like this: 41 | 42 | ```shell script 43 | helm-freeze init 44 | ``` 45 | A minimal file named `helm-freeze.yaml` will be generated. Here is an example of a more complex one: 46 | 47 | ```yaml 48 | charts: 49 | # Chart name 50 | - name: cert-manager 51 | # Chart version 52 | version: v1.7.0 53 | # The repo to use (declared below in the repos section) 54 | repo_name: jetstack 55 | # No destinations is declared, the default one will be used 56 | comment: "You can add comments" 57 | - name: cert-manager 58 | # Chart version 59 | version: v1.8.0 60 | # The repo to use (declared below in the repos section) 61 | repo_name: jetstack 62 | # Override the folder path 63 | dest_folder_override: cert-manager-1.8 64 | - name: fluent-bit 65 | repo_name: lifen 66 | version: 2.8.0 67 | # If you temporary want to stop syncing a specific chart 68 | no_sync: true 69 | - name: nginx-ingress 70 | # No repo_name is specified, stable will be used 71 | version: 1.35.0 72 | # Change the destination to another one (declared in destinations section) 73 | dest: custom 74 | - name: pleco 75 | repo_name: git-repo 76 | # When using a git repo, chart_path is mandatory, you need to specify the chart folder path 77 | chart_path: /charts/pleco 78 | dest: custom 79 | # Set git reference 80 | version: 5e05faddb0fde1f5ddd822c3d3ba72925f094e67 81 | 82 | repos: 83 | # Stable is the default one 84 | - name: stable 85 | url: https://charts.helm.sh/stable 86 | - name: jetstack 87 | url: https://charts.jetstack.io 88 | - name: lifen 89 | url: https://honestica.github.io/lifen-charts 90 | - name: git-repo 91 | url: https://github.com/Qovery/pleco.git 92 | # If you want to directly use a chart folder in a git repo, set type to git 93 | type: git 94 | 95 | destinations: 96 | - name: default 97 | path: /my/absolute/path 98 | - name: custom 99 | path: ./my/relative/path 100 | ``` 101 | 102 | Then use `sync` arg to locally download the declared versions, here is an example: 103 | ```bash 104 | $ helm-freeze sync 105 | 106 | [+] Adding helm repos 107 | -> stable 108 | -> aws 109 | -> git-repo 110 | 111 | [+] Updating helm repos 112 | 113 | [+] Downloading charts 114 | -> stable/nginx-ingress 1.35.0 115 | -> stable/prometheus-operator 8.15.12 116 | -> stable/elasticsearch-curator 2.1.5 117 | -> aws/aws-node-termination-handler 0.8.0 118 | -> aws/aws-vpc-cni 1.0.9 119 | -> git/pleco 5e05faddb0fde1f5ddd822c3d3ba72925f094e67 120 | 121 | Sync succeed! 122 | ``` 123 | 124 | If you update a chart, launch `sync` and you'll be able to see the differences with `git diff`. 125 | -------------------------------------------------------------------------------- /cfg/filter.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | func FilterCharts(config Config, charts []string) Config { 4 | chartsToFilter := make(map[string]bool) 5 | reposToFilter := make(map[string]bool) 6 | 7 | for _, name := range charts { 8 | chartsToFilter[name] = true 9 | } 10 | 11 | var filteredCharts []map[string]string 12 | for _, chart := range config.Charts { 13 | if chartsToFilter[chart["name"]] { 14 | filteredCharts = append(filteredCharts, chart) 15 | reposToFilter[chart["repo_name"]] = true 16 | } 17 | } 18 | 19 | var filteredRepos []map[string]string 20 | for _, repo := range config.Repos { 21 | if reposToFilter[repo["name"]] { 22 | filteredRepos = append(filteredRepos, repo) 23 | } 24 | } 25 | 26 | config.Charts = filteredCharts 27 | config.Repos = filteredRepos 28 | 29 | return config 30 | } 31 | -------------------------------------------------------------------------------- /cfg/type.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | type Config struct { 4 | Charts []map[string]string 5 | Repos []map[string]string 6 | Destinations []map[string]string 7 | } 8 | -------------------------------------------------------------------------------- /cfg/validate.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | func ValidateConfig(configFile string) (Config, error) { 12 | cfg := Config{} 13 | allReposName := make(map[string]bool) 14 | allReposUrl := make(map[string]bool) 15 | allDestinationsName := make(map[string]bool) 16 | allDestinationsPath := make(map[string]bool) 17 | 18 | data, err := os.ReadFile(configFile) 19 | if err != nil { 20 | fmt.Printf("Error while reading config file %s: #%v ", configFile, err) 21 | return Config{}, err 22 | } 23 | 24 | err = yaml.Unmarshal([]byte(data), &cfg) 25 | if err != nil { 26 | fmt.Printf("Configuration file error: %v", err) 27 | return Config{}, err 28 | } 29 | 30 | // Ensure content is coherent 31 | for _, chart := range cfg.Charts { 32 | if _, ok := chart["name"]; !ok { 33 | return cfg, fmt.Errorf("name is missing in charts config for this element: %v\n", chart) 34 | } 35 | if _, ok := chart["version"]; !ok { 36 | return cfg, fmt.Errorf("version is missing in %s charts config\n", chart["name"]) 37 | } 38 | } 39 | 40 | for _, repo := range cfg.Repos { 41 | if _, ok := repo["name"]; !ok { 42 | return cfg, fmt.Errorf("name is missing in repos config for this element: %v\n", repo) 43 | } 44 | if _, ok := repo["url"]; !ok { 45 | return cfg, fmt.Errorf("url is missing in %s repos config\n", repo["name"]) 46 | } 47 | } 48 | 49 | for _, dest := range cfg.Destinations { 50 | if _, ok := dest["name"]; !ok { 51 | return cfg, fmt.Errorf("name is missing in destinations config for this element: %v\n", dest) 52 | } 53 | if _, ok := dest["path"]; !ok { 54 | return cfg, fmt.Errorf("path is missing in %s destinations config\n", dest["name"]) 55 | } 56 | } 57 | 58 | // Find duplicates 59 | for _, repo := range cfg.Repos { 60 | if _, ok := allReposName[repo["name"]]; ok { 61 | return cfg, fmt.Errorf("Duplicate repo name found: %s\n", repo["name"]) 62 | } else { 63 | allReposName[repo["name"]] = true 64 | } 65 | 66 | if _, ok := allReposUrl[repo["url"]]; ok { 67 | return cfg, fmt.Errorf("Duplicate url found: %s\n", repo["url"]) 68 | } else { 69 | allReposUrl[repo["url"]] = true 70 | } 71 | } 72 | 73 | for _, dest := range cfg.Destinations { 74 | if _, ok := allDestinationsName[dest["name"]]; ok { 75 | return cfg, fmt.Errorf("Duplicate destination name found: %s\n", dest["name"]) 76 | } else { 77 | allDestinationsName[dest["name"]] = true 78 | } 79 | 80 | if _, ok := allDestinationsPath[dest["path"]]; ok { 81 | return cfg, fmt.Errorf("Duplicate destination path found: %s\n", dest["path"]) 82 | } else { 83 | allDestinationsPath[dest["path"]] = true 84 | } 85 | } 86 | 87 | // check if defaults elements are present 88 | if _, ok := allDestinationsName["default"]; !ok { 89 | return cfg, errors.New("missing default destination") 90 | } 91 | 92 | if _, ok := allReposName["stable"]; !ok { 93 | return cfg, errors.New("missing stable repository") 94 | } 95 | 96 | return cfg, nil 97 | } 98 | -------------------------------------------------------------------------------- /cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // initCmd represents the init command 11 | var initCmd = &cobra.Command{ 12 | Use: "init", 13 | Short: "create a simple helm-freeze.yaml file", 14 | Long: `Create a configuration file with minimal content`, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | var minimalConfig = ` 17 | charts: 18 | - name: prometheus-operator 19 | version: 9.3.1 20 | 21 | repos: 22 | - name: stable 23 | url: https://charts.helm.sh/stable 24 | 25 | destinations: 26 | - name: default 27 | path: ./ 28 | ` 29 | configFile := "helm-freeze.yaml" 30 | 31 | _, err := os.Stat(configFile) 32 | if err == nil { 33 | fmt.Printf("Configuration file %s already exists\n", configFile) 34 | os.Exit(1) 35 | } 36 | 37 | f, err := os.Create(configFile) 38 | if err != nil { 39 | fmt.Println(err) 40 | os.Exit(1) 41 | } 42 | 43 | _, err = f.WriteString(minimalConfig) 44 | if err != nil { 45 | fmt.Println(err) 46 | f.Close() 47 | os.Exit(1) 48 | } 49 | 50 | err = f.Close() 51 | if err != nil { 52 | fmt.Println(err) 53 | os.Exit(1) 54 | } 55 | 56 | fmt.Printf("Configuration file %s created\n", configFile) 57 | }, 58 | } 59 | 60 | func init() { 61 | rootCmd.AddCommand(initCmd) 62 | } 63 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // rootCmd represents the base command when called without any subcommands 11 | var rootCmd = &cobra.Command{ 12 | Use: "helm-freeze", 13 | Short: "Helm chart manager to freeze wished versions of your charts", 14 | Long: `Helm freeze brings flexibility to freeze some versions of charts you would like to commit into your 15 | repository easily.`, 16 | } 17 | 18 | // Execute adds all child commands to the root command and sets flags appropriately. 19 | // This is called by main.main(). It only needs to happen once to the rootCmd. 20 | func Execute() { 21 | if err := rootCmd.Execute(); err != nil { 22 | fmt.Println(err) 23 | os.Exit(1) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cmd/sync.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/Qovery/helm-freeze/cfg" 8 | "github.com/Qovery/helm-freeze/exec" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // syncCmd represents the sync command 13 | var syncCmd = &cobra.Command{ 14 | Use: "sync", 15 | Short: "Sync locally from the helm-freeze.yaml file", 16 | Long: `Download charts and un compress in the desired folders from the given configuration file information. 17 | Running a git diff then will help to see any differences`, 18 | Run: func(cmd *cobra.Command, args []string) { 19 | errorsDuringSync := false 20 | configFile, _ := cmd.Flags().GetString("config-file") 21 | config, err := cfg.ValidateConfig(configFile) 22 | if err != nil { 23 | fmt.Println(err) 24 | os.Exit(1) 25 | } 26 | 27 | onlyCharts, _ := cmd.Flags().GetStringSlice("only-charts") 28 | if len(onlyCharts) > 0 { 29 | config = cfg.FilterCharts(config, onlyCharts) 30 | } 31 | 32 | err = exec.AddAllRepos(config) 33 | if err != nil { 34 | fmt.Printf("Error message: %s", err) 35 | os.Exit(1) 36 | } 37 | 38 | err = exec.HelmRepoUpdate() 39 | if err != nil { 40 | fmt.Printf("Error message: %s", err) 41 | errorsDuringSync = true 42 | } 43 | 44 | err = exec.GetAllCharts(config, configFile) 45 | if err != nil { 46 | fmt.Printf("Error message: %s", err) 47 | os.Exit(1) 48 | } 49 | 50 | if errorsDuringSync { 51 | fmt.Println("\nSync ended with errors") 52 | os.Exit(1) 53 | } 54 | fmt.Println("\nSync succeed!") 55 | }, 56 | } 57 | 58 | func init() { 59 | rootCmd.AddCommand(syncCmd) 60 | 61 | syncCmd.Flags().StringP("config-file", "f", "./helm-freeze.yaml", "Configuration file") 62 | syncCmd.Flags().StringSlice("only-charts", []string{}, "Sync only specified charts, comma separated") 63 | } 64 | -------------------------------------------------------------------------------- /cmd/validate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/Qovery/helm-freeze/cfg" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var validateCmd = &cobra.Command{ 13 | Use: "validate", 14 | Short: "Validate configuration", 15 | Long: `Ensure the configuration is valid`, 16 | Run: func(cmd *cobra.Command, args []string) { 17 | configFile, _ := cmd.Flags().GetString("config-file") 18 | _, err := cfg.ValidateConfig(configFile) 19 | if err != nil { 20 | fmt.Println(err) 21 | os.Exit(1) 22 | } 23 | fmt.Println("Configuration is valid") 24 | }, 25 | } 26 | 27 | func init() { 28 | rootCmd.AddCommand(validateCmd) 29 | validateCmd.Flags().StringP("config-file", "f", "./helm-freeze.yaml", "Configuration file") 30 | } 31 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // initCmd represents the init command 10 | var versionCmd = &cobra.Command{ 11 | Use: "version", 12 | Short: "Print version", 13 | Run: func(cmd *cobra.Command, args []string) { 14 | fmt.Printf("v%s\n", GetCurrentVersion()) 15 | }, 16 | } 17 | 18 | func GetCurrentVersion() string { 19 | return "1.1.0" // ci-version-check 20 | } 21 | 22 | func init() { 23 | rootCmd.AddCommand(versionCmd) 24 | } 25 | -------------------------------------------------------------------------------- /exec/helm.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | 10 | "github.com/Qovery/helm-freeze/cfg" 11 | "github.com/Qovery/helm-freeze/util" 12 | "github.com/go-git/go-git/v5" 13 | "github.com/go-git/go-git/v5/plumbing" 14 | "github.com/otiai10/copy" 15 | ) 16 | 17 | func AddAllRepos(config cfg.Config) error { 18 | fmt.Println("\n[+] Adding helm repos") 19 | for _, repo := range config.Repos { 20 | fmt.Printf(" -> %s\n", repo["name"]) 21 | 22 | // ignore if git repo 23 | if repo["type"] == "git" { 24 | continue 25 | } 26 | 27 | err := helmRepoAdd(repo["name"], repo["url"]) 28 | if err != nil { 29 | return err 30 | } 31 | } 32 | return nil 33 | } 34 | 35 | type repo struct { 36 | name string 37 | url string 38 | kind string 39 | } 40 | 41 | func GetAllCharts(config cfg.Config, configPath string) error { 42 | // Move to folder path to correctly manage non absolute paths 43 | filenamePath, _ := filepath.Abs(configPath) 44 | absolutePath, _ := filepath.Split(filenamePath) 45 | err := os.Chdir(absolutePath) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | // convert destinations to map for easier usage 51 | destinations := make(map[string]string) 52 | for _, dest := range config.Destinations { 53 | destinations[dest["name"]] = dest["path"] 54 | } 55 | 56 | // convert repos to map for easier usage 57 | var repos []repo 58 | for _, dest := range config.Repos { 59 | repoType := "chart" 60 | if val, ok := dest["type"]; ok { 61 | repoType = val 62 | } 63 | 64 | repos = append(repos, repo{ 65 | name: dest["name"], 66 | url: dest["url"], 67 | kind: repoType, 68 | }) 69 | } 70 | 71 | // download and extract all charts 72 | fmt.Println("\n[+] Downloading charts") 73 | for _, chart := range config.Charts { 74 | if noSync, ok := chart["no_sync"]; ok { 75 | if noSync == "true" { 76 | continue 77 | } 78 | } 79 | 80 | chartType := "chart" 81 | if _, ok := chart["chart_path"]; ok { 82 | chartType = "git" 83 | } 84 | 85 | if chartType == "chart" { 86 | err = getHelmChart(chart, destinations) 87 | if err != nil { 88 | return err 89 | } 90 | } else if chartType == "git" { 91 | err = getGitChart(chart, destinations, repos) 92 | if err != nil { 93 | return err 94 | } 95 | } else { 96 | return errors.New("unknown chart type") 97 | } 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func getHelmChart(chart map[string]string, destinations map[string]string) error { 104 | // set default values 105 | chartUrl := "stable/" + chart["name"] 106 | destinationFolder := destinations["default"] 107 | 108 | // use user defined repo if specified 109 | if repoNameDefined, ok := chart["repo_name"]; ok { 110 | chartUrl = repoNameDefined + "/" + chart["name"] 111 | } 112 | 113 | // use user defined destinationFolder if specified 114 | if destinationIsDefined, ok := chart["dest"]; ok { 115 | destinationFolder = destinations[destinationIsDefined] 116 | } 117 | 118 | fmt.Printf(" -> %s %s\n", chartUrl, chart["version"]) 119 | 120 | // move the current folder to avoid helm failure 121 | chartFolderDestName := destinationFolder + "/" + chart["name"] 122 | destOverride := "" 123 | if _, ok := chart["dest_folder_override"]; ok { 124 | chartFolderDestName = destinationFolder + "/" + chart["dest_folder_override"] 125 | destOverride = chart["dest_folder_override"] 126 | } 127 | oldChartFolderDestName := chartFolderDestName + ".old" 128 | 129 | chartExists := false 130 | if _, err := os.Stat(chartFolderDestName); !os.IsNotExist(err) { 131 | chartExists = true 132 | } 133 | 134 | _, err := os.Stat(oldChartFolderDestName) 135 | if err == nil { 136 | return fmt.Errorf("%s already exists, please fix manually then retry", oldChartFolderDestName) 137 | } 138 | 139 | if chartExists { 140 | err := os.Rename(chartFolderDestName, oldChartFolderDestName) 141 | if err != nil { 142 | return err 143 | } 144 | } 145 | 146 | err = helmDownload(chart["name"], chartUrl, chart["version"], destinationFolder, destOverride) 147 | if err != nil { 148 | if chartExists { 149 | // restore old chart on failure 150 | _ = os.Rename(oldChartFolderDestName, chartFolderDestName) 151 | } 152 | return err 153 | } 154 | 155 | // finally remove the old chart 156 | if chartExists { 157 | err = os.RemoveAll(oldChartFolderDestName) 158 | if err != nil { 159 | return err 160 | } 161 | } 162 | 163 | return nil 164 | } 165 | 166 | func getGitChart(chart map[string]string, destinations map[string]string, repos []repo) error { 167 | // set default values 168 | chartName := chart["name"] 169 | chartUrl := "" 170 | destinationFolder := destinations["default"] 171 | repoPath := "/" 172 | 173 | fmt.Printf(" -> git/%s %s\n", chartName, chart["version"]) 174 | 175 | if rPath, ok := chart["chart_path"]; ok { 176 | repoPath = rPath 177 | } 178 | 179 | // find chart Url 180 | for _, repo := range repos { 181 | if repo.kind == "git" { 182 | if repo.name == chart["repo_name"] { 183 | chartUrl = repo.url 184 | break 185 | } 186 | } 187 | } 188 | if chartUrl == "" { 189 | return errors.New("no url defined for this git repo") 190 | } 191 | 192 | // use user defined destinationFolder if specified 193 | if destinationIsDefined, ok := chart["dest"]; ok { 194 | destinationFolder = destinations[destinationIsDefined] 195 | } 196 | 197 | // move the current folder to avoid helm failure 198 | chartFolderDestName := destinationFolder + "/" + chart["name"] 199 | if _, ok := chart["dest_folder_override"]; ok { 200 | chartFolderDestName = destinationFolder + "/" + chart["dest_folder_override"] 201 | } 202 | oldChartFolderDestName := chartFolderDestName + ".helm_freeze_old" 203 | 204 | chartExists := false 205 | if _, err := os.Stat(chartFolderDestName); !os.IsNotExist(err) { 206 | chartExists = true 207 | } 208 | 209 | _, err := os.Stat(oldChartFolderDestName) 210 | if err == nil { 211 | return fmt.Errorf("%s already exists, please fix manually then retry", oldChartFolderDestName) 212 | } 213 | 214 | if chartExists { 215 | err := os.Rename(chartFolderDestName, oldChartFolderDestName) 216 | if err != nil { 217 | return err 218 | } 219 | } 220 | 221 | clonedRepoDir, err := gitClone(chartUrl, chart["version"]) 222 | if err != nil { 223 | if chartExists { 224 | // restore old chart on failure 225 | _ = os.Rename(oldChartFolderDestName, chartFolderDestName) 226 | } 227 | return err 228 | } 229 | 230 | err = copy.Copy(clonedRepoDir+"/"+repoPath, chartFolderDestName) 231 | if err != nil { 232 | if chartExists { 233 | // restore old chart on failure 234 | _ = os.Rename(oldChartFolderDestName, chartFolderDestName) 235 | } 236 | return err 237 | } 238 | 239 | // finally remove the old chart 240 | if chartExists { 241 | err = os.RemoveAll(oldChartFolderDestName) 242 | if err != nil { 243 | return err 244 | } 245 | } 246 | _ = os.RemoveAll(clonedRepoDir) 247 | 248 | return nil 249 | } 250 | 251 | func gitClone(gitUrl string, commitSha string) (string, error) { 252 | // create tmp dir to clone to this dir 253 | tmpDir, err := util.MkdirTemp() 254 | if err != nil { 255 | return "", err 256 | } 257 | r, err := git.PlainClone(tmpDir, false, &git.CloneOptions{ 258 | URL: gitUrl, 259 | }) 260 | if err != nil { 261 | return "", err 262 | } 263 | 264 | w, err := r.Worktree() 265 | if err != nil { 266 | return "", err 267 | } 268 | 269 | // git checkout 270 | err = w.Checkout(&git.CheckoutOptions{ 271 | Hash: plumbing.NewHash(commitSha), 272 | }) 273 | if err != nil { 274 | return "", err 275 | } 276 | 277 | return tmpDir, nil 278 | } 279 | 280 | func helmDownload(chartName string, chartUrl string, version string, dest string, destOverride string) error { 281 | destination := dest 282 | if destOverride != "" { 283 | destination = dest + "/" + destOverride + ".tmp" 284 | if _, err := os.Stat(destination); !os.IsNotExist(err) { 285 | os.RemoveAll(destination) 286 | } 287 | } 288 | 289 | cmd := exec.Command("helm", "pull", "--untar", "-d", destination, chartUrl, "--version", version) 290 | out, err := cmd.CombinedOutput() 291 | if err != nil { 292 | return errors.New(string(out)) 293 | } 294 | 295 | if destOverride != "" { 296 | err := os.Rename(destination+"/"+chartName, dest+"/"+destOverride) 297 | if err != nil { 298 | return err 299 | } 300 | os.RemoveAll(destination) 301 | } 302 | return nil 303 | } 304 | 305 | func helmRepoAdd(name string, url string) error { 306 | cmd := exec.Command("helm", "repo", "add", "--force-update", name, url) 307 | out, err := cmd.CombinedOutput() 308 | if err != nil { 309 | return errors.New(string(out)) 310 | } 311 | return nil 312 | } 313 | 314 | func HelmRepoUpdate() error { 315 | fmt.Println("\n[+] Updating helm repos") 316 | cmd := exec.Command("helm", "repo", "update") 317 | out, err := cmd.CombinedOutput() 318 | if err != nil { 319 | return errors.New(string(out)) 320 | } 321 | return nil 322 | } 323 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Qovery/helm-freeze 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/go-git/go-git/v5 v5.4.2 7 | github.com/otiai10/copy v1.7.0 8 | github.com/spf13/cobra v1.5.0 9 | gopkg.in/yaml.v2 v2.4.0 10 | ) 11 | 12 | require ( 13 | github.com/Microsoft/go-winio v0.4.16 // indirect 14 | github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect 15 | github.com/acomagu/bufpipe v1.0.3 // indirect 16 | github.com/emirpasic/gods v1.12.0 // indirect 17 | github.com/go-git/gcfg v1.5.0 // indirect 18 | github.com/go-git/go-billy/v5 v5.3.1 // indirect 19 | github.com/imdario/mergo v0.3.12 // indirect 20 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 21 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 22 | github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect 23 | github.com/mitchellh/go-homedir v1.1.0 // indirect 24 | github.com/sergi/go-diff v1.1.0 // indirect 25 | github.com/spf13/pflag v1.0.5 // indirect 26 | github.com/xanzy/ssh-agent v0.3.0 // indirect 27 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect 28 | golang.org/x/net v0.0.0-20210326060303-6b1517762897 // indirect 29 | golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79 // indirect 30 | gopkg.in/warnings.v0 v0.1.2 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= 2 | github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= 3 | github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= 4 | github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= 5 | github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= 6 | github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= 7 | github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= 8 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= 9 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 10 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 11 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 13 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 18 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 19 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 20 | github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= 21 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 22 | github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= 23 | github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= 24 | github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 25 | github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= 26 | github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 27 | github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= 28 | github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= 29 | github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= 30 | github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= 31 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 32 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 33 | github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= 34 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 35 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 36 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 37 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 38 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 39 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 40 | github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= 41 | github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 42 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 43 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 44 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 45 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 46 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 47 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 48 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 49 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 50 | github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= 51 | github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 52 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 53 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 54 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 55 | github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE= 56 | github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= 57 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= 58 | github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= 59 | github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= 60 | github.com/otiai10/mint v1.3.3 h1:7JgpsBaN0uMkyju4tbYHu0mnM55hNKVYLsXmwr15NQI= 61 | github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= 62 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 63 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 64 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 65 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 66 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 67 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 68 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 69 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 70 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 71 | github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= 72 | github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= 73 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 74 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 75 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 76 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 77 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 78 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 79 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 80 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 81 | github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= 82 | github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= 83 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 84 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 85 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= 86 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 87 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 88 | golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs= 89 | golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= 90 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 91 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 94 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 95 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 96 | golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 97 | golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79 h1:RX8C8PRZc2hTIod4ds8ij+/4RQX3AqhYj3uOHmyaz4E= 98 | golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 100 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 101 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 102 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 103 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 104 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 105 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 106 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 107 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 108 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 109 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 110 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 111 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 112 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 113 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 114 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 115 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 116 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 117 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 118 | -------------------------------------------------------------------------------- /helm_freeze_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Qovery/helm-freeze/1c17754e81a5b9e97d7bac4016c4a549db92521e/helm_freeze_logo.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/Qovery/helm-freeze/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /util/io.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "os" 4 | 5 | func MkdirTemp() (string, error) { 6 | dir, err := os.MkdirTemp("", "hf_") 7 | if err != nil { 8 | return "", err 9 | } 10 | return dir, nil 11 | } 12 | --------------------------------------------------------------------------------