├── .github ├── dependabot.yml └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── root.go ├── template.go └── version.go ├── examples └── workload-charts-with-kcl │ ├── kcl-run.yaml │ └── workload-charts │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── deployment.yaml │ └── service.yaml │ ├── values.yaml │ └── workload │ └── templates │ ├── deployment.yaml │ └── service.yaml ├── go.mod ├── go.sum ├── install-binary.sh ├── main.go ├── makefile ├── pkg ├── app │ ├── app.go │ ├── init.go │ └── logger.go ├── config │ ├── kcl.go │ ├── repo.go │ └── template.go └── helm │ ├── helm.go │ └── helm3.go ├── plugin.yaml └── scripts ├── dep-helm-version.sh ├── release.sh ├── setup-apimachinery.sh ├── update-gofmt.sh ├── verify-gofmt.sh ├── verify-golint.sh └── verify-govet.sh /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | commit-message: 13 | prefix: "Chore: " 14 | include: "scope" 15 | ignore: 16 | - dependency-name: k8s.io/* 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "weekly" 21 | commit-message: 22 | prefix: "chore: " 23 | include: "scope" 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | env: 5 | VERSION_GO: '1.23' 6 | VERSION_HELM: 'v3.15.2' 7 | 8 | on: 9 | pull_request: 10 | push: 11 | branches: 12 | - main 13 | 14 | jobs: 15 | build: 16 | name: "Build & Test" 17 | if: "!contains(github.event.head_commit.message, '[ci skip]')" 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-go@v5 22 | with: 23 | go-version: ${{ env.VERSION_GO }} 24 | 25 | - uses: actions/cache@v4 26 | with: 27 | path: ~/go/pkg/mod 28 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 29 | restore-keys: | 30 | ${{ runner.os }}-go- 31 | 32 | - name: Install dependencies 33 | run: make bootstrap 34 | 35 | - name: Run unit tests 36 | run: make test 37 | 38 | - name: Verify installation 39 | run: | 40 | mkdir -p helmhome 41 | make install HELM_HOME=helmhome 42 | helmhome/plugins/helm-kcl/bin/kcl version 43 | 44 | helm-install: 45 | name: helm install 46 | if: "!contains(github.event.head_commit.message, '[ci skip]')" 47 | needs: [build] 48 | runs-on: ${{ matrix.os }} 49 | container: ${{ matrix.container }} 50 | continue-on-error: ${{ matrix.experimental }} 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | os: [ubuntu-latest, macos-latest, windows-latest] 55 | shell: [ default ] 56 | experimental: [ false ] 57 | include: 58 | - os: windows-latest 59 | shell: wsl 60 | experimental: false 61 | - os: windows-latest 62 | shell: cygwin 63 | experimental: false 64 | - os: ubuntu-latest 65 | container: alpine 66 | shell: sh 67 | experimental: false 68 | 69 | steps: 70 | - name: Disable autocrlf 71 | if: "contains(matrix.os, 'windows-latest')" 72 | run: |- 73 | git config --global core.autocrlf false 74 | git config --global core.eol lf 75 | 76 | - uses: actions/checkout@v4 77 | 78 | - name: Setup Helm 79 | uses: azure/setup-helm@v4 80 | with: 81 | version: ${{ env.VERSION_HELM }} 82 | 83 | - name: Setup WSL 84 | if: "contains(matrix.shell, 'wsl')" 85 | uses: Vampire/setup-wsl@v5 86 | 87 | - name: Setup Cygwin 88 | if: "contains(matrix.shell, 'cygwin')" 89 | uses: egor-tensin/setup-cygwin@v4 90 | with: 91 | platform: x64 92 | 93 | - name: helm plugin install 94 | run: helm plugin install . 95 | 96 | integration-tests: 97 | name: Integration Tests 98 | if: "!contains(github.event.head_commit.message, '[ci skip]')" 99 | needs: [build] 100 | runs-on: ubuntu-latest 101 | steps: 102 | - uses: engineerd/setup-kind@v0.6.2 103 | with: 104 | version: "v0.11.1" 105 | 106 | - uses: actions/checkout@v4 107 | 108 | - name: Setup Helm 109 | uses: azure/setup-helm@v4 110 | with: 111 | version: ${{ env.VERSION_HELM }} 112 | 113 | - name: helm plugin install 114 | run: helm plugin install . 115 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | bin/ 17 | helmhome/ 18 | 19 | _build/ 20 | .DS_store 21 | build 22 | release 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Helm KCL Plugin 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/kcl-lang/helm-kcl)](https://goreportcard.com/report/github.com/kcl-lang/helm-kcl) 4 | [![GoDoc](https://godoc.org/github.com/kcl-lang/helm-kcl?status.svg)](https://godoc.org/github.com/kcl-lang/helm-kcl) 5 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/kcl-lang/helm-kcl/blob/main/LICENSE) 6 | 7 | [KCL](https://github.com/kcl-lang/kcl) is a constraint-based record & functional domain language. Full documents of KCL can be found [here](https://kcl-lang.io/). 8 | 9 | You can use the `Helm-KCL-Plugin` to 10 | 11 | + Edit the helm charts in a hook way to separate data and logic for the Kubernetes manifests management. 12 | + For multi-environment and multi-tenant scenarios, you can maintain these configurations gracefully rather than simply copy and paste. 13 | + Validate all KRM resources using the KCL schema. 14 | 15 | ## Install 16 | 17 | ### Using Helm plugin manager (> 2.3.x) 18 | 19 | ```shell 20 | helm plugin install https://github.com/kcl-lang/helm-kcl 21 | ``` 22 | 23 | ### Pre Helm 2.3.0 Installation 24 | 25 | Pick a release tarball from the [releases page](https://github.com/kcl-lang/helm-kcl/releases). 26 | 27 | Unpack the tarball in your helm plugins directory ($(helm home)/plugins). 28 | 29 | E.g. 30 | 31 | ```shell 32 | curl -L $TARBALL_URL | tar -C $(helm home)/plugins -xzv 33 | ``` 34 | 35 | ### Install From Source 36 | 37 | #### Prerequisites 38 | 39 | + GoLang 1.23+ 40 | 41 | Make sure you do not have a version of `helm-kcl` installed. You can remove it by running the command. 42 | 43 | ```shell 44 | helm plugin uninstall kcl 45 | ``` 46 | 47 | #### Installation Steps 48 | 49 | The first step is to download the repository and enter the directory. You can do this via git clone or downloading and extracting the release. If you clone via git, remember to check out the latest tag for the latest release. 50 | 51 | Next, depending on which helm version you have, install the plugin into helm. 52 | 53 | ##### Helm 2 54 | 55 | ```shell 56 | make install 57 | ``` 58 | 59 | ##### Helm 3 60 | 61 | ```shell 62 | make install/helm3 63 | ``` 64 | 65 | ## Quick Start 66 | 67 | ```shell 68 | helm kcl template --file ./examples/workload-charts-with-kcl/kcl-run.yaml 69 | ``` 70 | 71 | The content of `kcl-run.yaml` looks like this: 72 | 73 | ```yaml 74 | # kcl-config.yaml 75 | apiVersion: krm.kcl.dev/v1alpha1 76 | kind: KCLRun 77 | metadata: 78 | name: set-annotation 79 | spec: 80 | # EDIT THE SOURCE! 81 | # This should be your KCL code which preloads the `ResourceList` to `option("items") 82 | source: | 83 | [resource | {if resource.kind == "Deployment": metadata.annotations: {"managed-by" = "helm-kcl-plugin"}} for resource in option("items")] 84 | 85 | repositories: 86 | - name: workload 87 | path: ./workload-charts 88 | ``` 89 | 90 | The output is: 91 | 92 | ```yaml 93 | apiVersion: v1 94 | kind: Service 95 | metadata: 96 | labels: 97 | app.kubernetes.io/instance: workload 98 | app.kubernetes.io/managed-by: Helm 99 | app.kubernetes.io/name: workload 100 | app.kubernetes.io/version: 0.1.0 101 | helm.sh/chart: workload-0.1.0 102 | name: workload 103 | spec: 104 | ports: 105 | - name: www 106 | port: 80 107 | protocol: TCP 108 | targetPort: 80 109 | selector: 110 | app.kubernetes.io/instance: workload 111 | app.kubernetes.io/name: workload 112 | type: ClusterIP 113 | --- 114 | apiVersion: apps/v1 115 | kind: Deployment 116 | metadata: 117 | labels: 118 | app.kubernetes.io/instance: workload 119 | app.kubernetes.io/managed-by: Helm 120 | app.kubernetes.io/name: workload 121 | app.kubernetes.io/version: 0.1.0 122 | helm.sh/chart: workload-0.1.0 123 | name: workload 124 | annotations: 125 | managed-by: helm-kcl-plugin 126 | spec: 127 | selector: 128 | matchLabels: 129 | app.kubernetes.io/instance: workload 130 | app.kubernetes.io/name: workload 131 | template: 132 | metadata: 133 | labels: 134 | app.kubernetes.io/instance: workload 135 | app.kubernetes.io/name: workload 136 | spec: 137 | containers: 138 | - image: "nginx:alpine" 139 | name: frontend 140 | ``` 141 | 142 | ## Build 143 | 144 | ### Prerequisites 145 | 146 | + GoLang 1.23+ 147 | 148 | ```shell 149 | git clone https://github.com/kcl-lang/helm-kcl.git 150 | cd helm-kcl 151 | go run main.go 152 | ``` 153 | 154 | ## Test 155 | 156 | ### Unit Test 157 | 158 | ```shell 159 | go test -v ./... 160 | ``` 161 | 162 | ### Integration Test 163 | 164 | You need to put your KCL script source in the functionConfig of kind KCLRun and then the function will run the KCL script that you provide. 165 | 166 | ```bash 167 | # Verify that the annotation is added to the `Deployment` resource and the other resource `Service` 168 | # does not have this annotation. 169 | diff \ 170 | <(helm template ./examples/workload-charts-with-kcl/workload-charts) \ 171 | <(go run main.go template --file ./examples/workload-charts-with-kcl/kcl-run.yaml) |\ 172 | grep annotations -A1 173 | ``` 174 | 175 | The output is 176 | 177 | ```diff 178 | > annotations: 179 | > managed-by: helm-kcl-plugin 180 | ``` 181 | 182 | ## Release 183 | 184 | Bump version in `plugin.yaml`: 185 | 186 | ```shell 187 | code plugin.yaml 188 | git commit -m 'Bump helm-kcl version to 0.x.y' 189 | ``` 190 | 191 | Set `GITHUB_TOKEN` and run: 192 | 193 | ```shell 194 | make docker-run-release 195 | ``` 196 | 197 | ## Guides for Developing KCL 198 | 199 | Here's what you can do in the KCL script: 200 | 201 | + Read resources from `option("items")`. The `option("items")` complies with the [KRM Functions Specification](https://kpt.dev/book/05-developing-functions/01-functions-specification). 202 | + Return a KPM list for output resources. 203 | + Return an error using `assert {condition}, {error_message}`. 204 | + Read the environment variables. e.g. `option("PATH")` (Not yet implemented). 205 | + Read the OpenAPI schema. e.g. `option("open_api")["definitions"]["io.k8s.api.apps.v1.Deployment"]` (Not yet implemented). 206 | 207 | Full documents of KCL can be found [here](https://kcl-lang.io/). 208 | 209 | ## Examples 210 | 211 | See [here](https://kcl-lang.io/krm-kcl/tree/main/examples) for more examples. 212 | 213 | ## Thanks 214 | 215 | + [helmfile](https://github.com/helmfile/helmfile) 216 | + [helm-diff](https://github.com/databus23/helm-diff) 217 | + [helm-secrets](https://github.com/jkroepke/helm-secrets) 218 | + [helm-s3](https://github.com/hypnoglow/helm-s3) 219 | + [helm-git](https://github.com/aslafy-z/helm-git) 220 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | const rootCmdLongUsage = ` 8 | The Helm KCL Plugin. 9 | 10 | * Edit, transformer, validate Helm charts using the KCL programming language. 11 | ` 12 | 13 | // New creates a new cobra client 14 | func New() *cobra.Command { 15 | cmd := &cobra.Command{ 16 | Use: "kcl", 17 | Short: "Edit, transformer, validate Helm charts using the KCL programming language.", 18 | Long: rootCmdLongUsage, 19 | SilenceUsage: true, 20 | } 21 | 22 | cmd.AddCommand(NewVersionCmd()) 23 | cmd.AddCommand(NewTemplateCmd()) 24 | cmd.SetHelpCommand(&cobra.Command{}) // Disable the help command 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /cmd/template.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "kcl-lang.io/helm-kcl/pkg/app" 7 | "kcl-lang.io/helm-kcl/pkg/config" 8 | ) 9 | 10 | // NewTemplateCmd returns the template command. 11 | func NewTemplateCmd() *cobra.Command { 12 | templateOptions := config.NewTemplateOptions() 13 | 14 | cmd := &cobra.Command{ 15 | Use: "template", 16 | Short: "Template releases defined in the KCL state file", 17 | RunE: func(*cobra.Command, []string) error { 18 | err := app.New().Template(config.NewTemplateImpl(templateOptions)) 19 | if err != nil { 20 | return err 21 | } 22 | return nil 23 | }, 24 | SilenceUsage: true, 25 | } 26 | 27 | f := cmd.Flags() 28 | f.StringVar(&templateOptions.File, "file", "", "input kcl file to pass to helm kcl template") 29 | f.StringArrayVar(&templateOptions.Set, "set", nil, "additional values to be merged into the helm command --set flag") 30 | f.StringArrayVar(&templateOptions.Values, "values", nil, "additional value files to be merged into the helm command --values flag") 31 | f.StringVar(&templateOptions.OutputDir, "output-dir", "", "output directory to pass to helm template (helm template --output-dir)") 32 | f.StringVar(&templateOptions.OutputDirTemplate, "output-dir-template", "", "go text template for generating the output directory. Default: {{ .OutputDir }}/{{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}-{{ .Release.Name}}") 33 | f.IntVar(&templateOptions.Concurrency, "concurrency", 0, "maximum number of concurrent helm processes to run, 0 is unlimited") 34 | f.BoolVar(&templateOptions.Validate, "validate", false, "validate your manifests against the Kubernetes cluster you are currently pointing at. Note that this requires access to a Kubernetes cluster to obtain information necessary for validating, like the template of available API versions") 35 | f.BoolVar(&templateOptions.IncludeCRDs, "include-crds", false, "include CRDs in the templated output") 36 | f.BoolVar(&templateOptions.SkipTests, "skip-tests", false, "skip tests from templated output") 37 | f.BoolVar(&templateOptions.SkipNeeds, "skip-needs", true, `do not automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when --selector/-l flag is not provided. Defaults to true when --include-needs or --include-transitive-needs is not provided`) 38 | f.BoolVar(&templateOptions.IncludeNeeds, "include-needs", false, `automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when --selector/-l flag is not provided`) 39 | f.BoolVar(&templateOptions.IncludeTransitiveNeeds, "include-transitive-needs", false, `like --include-needs, but also includes transitive needs (needs of needs). Does nothing when --selector/-l flag is not provided. Overrides exclusions of other selectors and conditions.`) 40 | f.BoolVar(&templateOptions.SkipDeps, "skip-deps", false, `skip running "helm repo update" and "helm dependency build"`) 41 | f.StringVar(&templateOptions.PostRenderer, "post-renderer", "", `pass --post-renderer to "helm template" or "helm upgrade --install"`) 42 | 43 | return cmd 44 | } 45 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // Version identifier populated via the CI/CD process. 10 | var Version = "HEAD" 11 | 12 | // NewVersionCmd returns the version command. 13 | func NewVersionCmd() *cobra.Command { 14 | return &cobra.Command{ 15 | Use: "version", 16 | Short: "Show version of the helm kcl plugin", 17 | Run: func(*cobra.Command, []string) { 18 | fmt.Println(Version) 19 | }, 20 | SilenceUsage: true, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/workload-charts-with-kcl/kcl-run.yaml: -------------------------------------------------------------------------------- 1 | # kcl-config.yaml 2 | apiVersion: krm.kcl.dev/v1alpha1 3 | kind: KCLRun 4 | metadata: 5 | name: set-annotation 6 | spec: 7 | # EDIT THE SOURCE! 8 | # This should be your KCL code which preloads the `ResourceList` to `option("items") 9 | source: | 10 | [resource | {if resource.kind == "Deployment": metadata.annotations: {"managed-by" = "helm-kcl-plugin"}} for resource in option("items")] 11 | 12 | repositories: 13 | - name: workload 14 | path: ./workload-charts 15 | # - name: nginx-ingress 16 | # url: https://github.com/nginxinc/kubernetes-ingress/tree/v3.1.0/deployments/helm-chart 17 | -------------------------------------------------------------------------------- /examples/workload-charts-with-kcl/workload-charts/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ -------------------------------------------------------------------------------- /examples/workload-charts-with-kcl/workload-charts/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | appVersion: 0.1.0 3 | description: A helm chart to provision standard workloads. 4 | name: workload 5 | type: application 6 | version: 0.1.0 7 | -------------------------------------------------------------------------------- /examples/workload-charts-with-kcl/workload-charts/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "workload.name" -}} 5 | {{- default .Release.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | {{/* 8 | Create a default fully qualified app name. 9 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 10 | If release name contains chart name it will be used as a full name. 11 | */}} 12 | {{- define "workload.fullname" -}} 13 | {{- $name := default .Chart.Name .Values.nameOverride }} 14 | {{- if contains $name .Release.Name }} 15 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 18 | {{- end }} 19 | {{- end }} 20 | {{/* 21 | Create chart name and version as used by the chart label. 22 | */}} 23 | {{- define "workload.chart" -}} 24 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 25 | {{- end }} 26 | {{/* 27 | Common labels 28 | */}} 29 | {{- define "workload.labels" -}} 30 | helm.sh/chart: {{ include "workload.chart" . }} 31 | {{ include "workload.selectorLabels" . }} 32 | {{- if .Chart.AppVersion }} 33 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 34 | {{- end }} 35 | app.kubernetes.io/managed-by: {{ .Release.Service }} 36 | {{- end }} 37 | {{/* 38 | Selector labels 39 | */}} 40 | {{- define "workload.selectorLabels" -}} 41 | app.kubernetes.io/name: {{ include "workload.name" . }} 42 | app.kubernetes.io/instance: {{ .Release.Name }} 43 | {{- end }} 44 | -------------------------------------------------------------------------------- /examples/workload-charts-with-kcl/workload-charts/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "workload.name" . }} 5 | labels: 6 | {{- include "workload.labels" . | nindent 4 }} 7 | spec: 8 | selector: 9 | matchLabels: 10 | {{- include "workload.selectorLabels" . | nindent 6 }} 11 | template: 12 | metadata: 13 | labels: 14 | {{- include "workload.selectorLabels" . | nindent 8 }} 15 | spec: 16 | containers: 17 | {{- range $name, $container := .Values.containers }} 18 | - name: {{ $name }} 19 | image: "{{ $container.image.name }}" 20 | {{- with $container.command }} 21 | command: 22 | {{- toYaml $container.command | nindent 12 }} 23 | {{- end }} 24 | {{- with $container.args }} 25 | args: 26 | {{- toYaml $container.args | nindent 12 }} 27 | {{- end }} 28 | {{- with $container.env }} 29 | env: 30 | {{- toYaml $container.env | nindent 12 }} 31 | {{- end }} 32 | {{- with $container.volumeMounts }} 33 | volumeMounts: 34 | {{- toYaml $container.volumeMounts | nindent 12 }} 35 | {{- end }} 36 | {{- with $container.livenessProbe }} 37 | livenessProbe: 38 | {{- toYaml $container.livenessProbe | nindent 12 }} 39 | {{- end }} 40 | {{- with $container.readinessProbe }} 41 | readinessProbe: 42 | {{- toYaml $container.readinessProbe | nindent 12 }} 43 | {{- end }} 44 | {{- with $container.resources }} 45 | resources: 46 | {{- toYaml $container.resources | nindent 12 }} 47 | {{- end }} 48 | {{- end }} 49 | -------------------------------------------------------------------------------- /examples/workload-charts-with-kcl/workload-charts/templates/service.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Values.service }} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include "workload.name" . }} 6 | labels: 7 | {{- include "workload.labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .Values.service.type }} 10 | selector: 11 | {{- include "workload.selectorLabels" . | nindent 4 }} 12 | {{- with .Values.service.ports }} 13 | ports: 14 | {{- toYaml . | nindent 4 }} 15 | {{- end }} 16 | {{- end }} -------------------------------------------------------------------------------- /examples/workload-charts-with-kcl/workload-charts/values.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Default values for the chart (for reference only). 3 | # An actual values file is rendered from the source SCORE file by the CLI tool. 4 | 5 | containers: 6 | frontend: 7 | image: 8 | name: nginx:alpine 9 | 10 | service: 11 | type: ClusterIP 12 | ports: 13 | - name: www 14 | protocol: TCP 15 | port: 80 16 | targetPort: 80 17 | -------------------------------------------------------------------------------- /examples/workload-charts-with-kcl/workload-charts/workload/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: workload/templates/deployment.yaml 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: release-name 7 | labels: 8 | helm.sh/chart: workload-0.1.0 9 | app.kubernetes.io/name: release-name 10 | app.kubernetes.io/instance: release-name 11 | app.kubernetes.io/version: "0.1.0" 12 | app.kubernetes.io/managed-by: Helm 13 | spec: 14 | selector: 15 | matchLabels: 16 | app.kubernetes.io/name: release-name 17 | app.kubernetes.io/instance: release-name 18 | template: 19 | metadata: 20 | labels: 21 | app.kubernetes.io/name: release-name 22 | app.kubernetes.io/instance: release-name 23 | spec: 24 | containers: 25 | - name: frontend 26 | image: "nginx:alpine" 27 | -------------------------------------------------------------------------------- /examples/workload-charts-with-kcl/workload-charts/workload/templates/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: workload/templates/service.yaml 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: release-name 7 | labels: 8 | helm.sh/chart: workload-0.1.0 9 | app.kubernetes.io/name: release-name 10 | app.kubernetes.io/instance: release-name 11 | app.kubernetes.io/version: "0.1.0" 12 | app.kubernetes.io/managed-by: Helm 13 | spec: 14 | type: ClusterIP 15 | selector: 16 | app.kubernetes.io/name: release-name 17 | app.kubernetes.io/instance: release-name 18 | ports: 19 | - name: www 20 | port: 80 21 | protocol: TCP 22 | targetPort: 80 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module kcl-lang.io/helm-kcl 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/spf13/cobra v1.9.1 9 | go.uber.org/zap v1.27.0 10 | google.golang.org/grpc v1.72.2 11 | gopkg.in/yaml.v2 v2.4.0 12 | helm.sh/helm/v3 v3.18.0 13 | k8s.io/helm v2.17.0+incompatible 14 | kcl-lang.io/krm-kcl v0.11.2 15 | ) 16 | 17 | require ( 18 | cel.dev/expr v0.20.0 // indirect 19 | cloud.google.com/go v0.112.1 // indirect 20 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 21 | cloud.google.com/go/iam v1.1.6 // indirect 22 | cloud.google.com/go/storage v1.38.0 // indirect 23 | dario.cat/mergo v1.0.1 // indirect 24 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect 25 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect 26 | github.com/BurntSushi/toml v1.5.0 // indirect 27 | github.com/MakeNowJust/heredoc v1.0.0 // indirect 28 | github.com/Masterminds/goutils v1.1.1 // indirect 29 | github.com/Masterminds/semver v1.5.0 // indirect 30 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 31 | github.com/Masterminds/sprig/v3 v3.3.0 // indirect 32 | github.com/Masterminds/squirrel v1.5.4 // indirect 33 | github.com/Microsoft/go-winio v0.6.2 // indirect 34 | github.com/ProtonMail/go-crypto v1.1.5 // indirect 35 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 36 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 37 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 38 | github.com/aws/aws-sdk-go v1.48.6 // indirect 39 | github.com/bahlo/generic-list-go v0.2.0 // indirect 40 | github.com/beorn7/perks v1.0.1 // indirect 41 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect 42 | github.com/blang/semver/v4 v4.0.0 // indirect 43 | github.com/buger/jsonparser v1.1.1 // indirect 44 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 45 | github.com/chai2010/gettext-go v1.0.2 // indirect 46 | github.com/chai2010/jsonv v1.1.3 // indirect 47 | github.com/chai2010/protorpc v1.1.4 // indirect 48 | github.com/chainguard-dev/git-urls v1.0.2 // indirect 49 | github.com/cloudflare/circl v1.3.7 // indirect 50 | github.com/containerd/containerd v1.7.27 // indirect 51 | github.com/containerd/errdefs v0.3.0 // indirect 52 | github.com/containerd/log v0.1.0 // indirect 53 | github.com/containerd/platforms v0.2.1 // indirect 54 | github.com/containers/image/v5 v5.34.3 // indirect 55 | github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect 56 | github.com/containers/ocicrypt v1.2.1 // indirect 57 | github.com/containers/storage v1.57.2 // indirect 58 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 59 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 60 | github.com/dchest/siphash v1.2.3 // indirect 61 | github.com/distribution/reference v0.6.0 // indirect 62 | github.com/docker/cli v27.5.1+incompatible // indirect 63 | github.com/docker/distribution v2.8.3+incompatible // indirect 64 | github.com/docker/docker v27.5.1+incompatible // indirect 65 | github.com/docker/docker-credential-helpers v0.8.2 // indirect 66 | github.com/docker/go-connections v0.5.0 // indirect 67 | github.com/docker/go-metrics v0.0.1 // indirect 68 | github.com/docker/go-units v0.5.0 // indirect 69 | github.com/dominikbraun/graph v0.23.0 // indirect 70 | github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615 // indirect 71 | github.com/elliotchance/orderedmap/v2 v2.7.0 // indirect 72 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 73 | github.com/emicklei/proto v1.14.1 // indirect 74 | github.com/emirpasic/gods v1.18.1 // indirect 75 | github.com/evanphx/json-patch v5.9.11+incompatible // indirect 76 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect 77 | github.com/fatih/color v1.18.0 // indirect 78 | github.com/felixge/httpsnoop v1.0.4 // indirect 79 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 80 | github.com/getkin/kin-openapi v0.132.0 // indirect 81 | github.com/ghodss/yaml v1.0.0 // indirect 82 | github.com/go-errors/errors v1.5.1 // indirect 83 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 84 | github.com/go-git/go-billy/v5 v5.6.2 // indirect 85 | github.com/go-git/go-git/v5 v5.13.2 // indirect 86 | github.com/go-gorp/gorp/v3 v3.1.0 // indirect 87 | github.com/go-logr/logr v1.4.2 // indirect 88 | github.com/go-logr/stdr v1.2.2 // indirect 89 | github.com/go-openapi/analysis v0.23.0 // indirect 90 | github.com/go-openapi/errors v0.22.0 // indirect 91 | github.com/go-openapi/inflect v0.21.0 // indirect 92 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 93 | github.com/go-openapi/jsonreference v0.21.0 // indirect 94 | github.com/go-openapi/loads v0.22.0 // indirect 95 | github.com/go-openapi/spec v0.21.0 // indirect 96 | github.com/go-openapi/strfmt v0.23.0 // indirect 97 | github.com/go-openapi/swag v0.23.0 // indirect 98 | github.com/go-openapi/validate v0.24.0 // indirect 99 | github.com/gobwas/glob v0.2.3 // indirect 100 | github.com/goccy/go-yaml v1.17.1 // indirect 101 | github.com/gofrs/flock v0.12.1 // indirect 102 | github.com/gogo/protobuf v1.3.2 // indirect 103 | github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 // indirect 104 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 105 | github.com/golang/protobuf v1.5.4 // indirect 106 | github.com/golang/snappy v0.0.4 // indirect 107 | github.com/google/btree v1.1.3 // indirect 108 | github.com/google/cel-go v0.23.2 // indirect 109 | github.com/google/gnostic-models v0.6.9 // indirect 110 | github.com/google/go-cmp v0.7.0 // indirect 111 | github.com/google/s2a-go v0.1.7 // indirect 112 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 113 | github.com/google/uuid v1.6.0 // indirect 114 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 115 | github.com/googleapis/gax-go/v2 v2.12.2 // indirect 116 | github.com/gorilla/mux v1.8.1 // indirect 117 | github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect 118 | github.com/gosuri/uitable v0.0.4 // indirect 119 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 120 | github.com/hashicorp/errwrap v1.1.0 // indirect 121 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 122 | github.com/hashicorp/go-getter v1.7.8 // indirect 123 | github.com/hashicorp/go-multierror v1.1.1 // indirect 124 | github.com/hashicorp/go-safetemp v1.0.0 // indirect 125 | github.com/hashicorp/go-version v1.7.0 // indirect 126 | github.com/huandu/xstrings v1.5.0 // indirect 127 | github.com/iancoleman/strcase v0.3.0 // indirect 128 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 129 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 130 | github.com/jinzhu/copier v0.4.0 // indirect 131 | github.com/jmespath/go-jmespath v0.4.0 // indirect 132 | github.com/jmoiron/sqlx v1.4.0 // indirect 133 | github.com/josharian/intern v1.0.0 // indirect 134 | github.com/json-iterator/go v1.1.12 // indirect 135 | github.com/kevinburke/ssh_config v1.2.0 // indirect 136 | github.com/klauspost/compress v1.18.0 // indirect 137 | github.com/kr/pretty v0.3.1 // indirect 138 | github.com/kr/text v0.2.0 // indirect 139 | github.com/kubescape/go-git-url v0.0.30 // indirect 140 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 141 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 142 | github.com/lib/pq v1.10.9 // indirect 143 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 144 | github.com/mailru/easyjson v0.7.7 // indirect 145 | github.com/mattn/go-colorable v0.1.13 // indirect 146 | github.com/mattn/go-isatty v0.0.20 // indirect 147 | github.com/mattn/go-runewidth v0.0.16 // indirect 148 | github.com/mitchellh/copystructure v1.2.0 // indirect 149 | github.com/mitchellh/go-homedir v1.1.0 // indirect 150 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 151 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 152 | github.com/mitchellh/mapstructure v1.5.0 // indirect 153 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 154 | github.com/moby/locker v1.0.1 // indirect 155 | github.com/moby/spdystream v0.5.0 // indirect 156 | github.com/moby/sys/capability v0.4.0 // indirect 157 | github.com/moby/sys/mountinfo v0.7.2 // indirect 158 | github.com/moby/sys/user v0.3.0 // indirect 159 | github.com/moby/term v0.5.2 // indirect 160 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 161 | github.com/modern-go/reflect2 v1.0.2 // indirect 162 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 163 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 164 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 165 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 166 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 167 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 168 | github.com/oklog/ulid v1.3.1 // indirect 169 | github.com/opencontainers/go-digest v1.0.0 // indirect 170 | github.com/opencontainers/image-spec v1.1.1 // indirect 171 | github.com/opencontainers/runtime-spec v1.2.0 // indirect 172 | github.com/otiai10/copy v1.14.1 // indirect 173 | github.com/otiai10/mint v1.6.3 // indirect 174 | github.com/perimeterx/marshmallow v1.1.5 // indirect 175 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 176 | github.com/pjbgf/sha1cd v0.3.2 // indirect 177 | github.com/pkg/errors v0.9.1 // indirect 178 | github.com/prometheus/client_golang v1.22.0 // indirect 179 | github.com/prometheus/client_model v0.6.1 // indirect 180 | github.com/prometheus/common v0.62.0 // indirect 181 | github.com/prometheus/procfs v0.15.1 // indirect 182 | github.com/protocolbuffers/txtpbfmt v0.0.0-20240416193709-1e18ef0a7fdc // indirect 183 | github.com/qri-io/jsonpointer v0.1.1 // indirect 184 | github.com/rivo/uniseg v0.4.7 // indirect 185 | github.com/rogpeppe/go-internal v1.13.1 // indirect 186 | github.com/rubenv/sql-migrate v1.8.0 // indirect 187 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 188 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 189 | github.com/shopspring/decimal v1.4.0 // indirect 190 | github.com/sirupsen/logrus v1.9.3 // indirect 191 | github.com/skeema/knownhosts v1.3.0 // indirect 192 | github.com/spf13/cast v1.7.0 // indirect 193 | github.com/spf13/pflag v1.0.6 // indirect 194 | github.com/stoewer/go-strcase v1.3.0 // indirect 195 | github.com/thoas/go-funk v0.9.3 // indirect 196 | github.com/ulikunitz/xz v0.5.12 // indirect 197 | github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect 198 | github.com/x448/float16 v0.8.4 // indirect 199 | github.com/xanzy/ssh-agent v0.3.3 // indirect 200 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 201 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 202 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 203 | github.com/xlab/treeprint v1.2.0 // indirect 204 | github.com/yuin/goldmark v1.7.11 // indirect 205 | go.mongodb.org/mongo-driver v1.14.0 // indirect 206 | go.opencensus.io v0.24.0 // indirect 207 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 208 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect 209 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect 210 | go.opentelemetry.io/otel v1.34.0 // indirect 211 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 212 | go.opentelemetry.io/otel/trace v1.34.0 // indirect 213 | go.uber.org/multierr v1.11.0 // indirect 214 | golang.org/x/crypto v0.37.0 // indirect 215 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect 216 | golang.org/x/mod v0.24.0 // indirect 217 | golang.org/x/net v0.38.0 // indirect 218 | golang.org/x/oauth2 v0.28.0 // indirect 219 | golang.org/x/sync v0.14.0 // indirect 220 | golang.org/x/sys v0.33.0 // indirect 221 | golang.org/x/term v0.31.0 // indirect 222 | golang.org/x/text v0.24.0 // indirect 223 | golang.org/x/time v0.9.0 // indirect 224 | golang.org/x/tools v0.31.0 // indirect 225 | google.golang.org/api v0.169.0 // indirect 226 | google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect 227 | google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect 228 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect 229 | google.golang.org/protobuf v1.36.6 // indirect 230 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 231 | gopkg.in/inf.v0 v0.9.1 // indirect 232 | gopkg.in/warnings.v0 v0.1.2 // indirect 233 | gopkg.in/yaml.v3 v3.0.1 // indirect 234 | k8s.io/api v0.33.1 // indirect 235 | k8s.io/apiextensions-apiserver v0.33.0 // indirect 236 | k8s.io/apimachinery v0.33.1 // indirect 237 | k8s.io/apiserver v0.33.0 // indirect 238 | k8s.io/cli-runtime v0.33.1 // indirect 239 | k8s.io/client-go v0.33.1 // indirect 240 | k8s.io/component-base v0.33.0 // indirect 241 | k8s.io/klog/v2 v2.130.1 // indirect 242 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 243 | k8s.io/kubectl v0.33.0 // indirect 244 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 245 | kcl-lang.io/cli v0.11.2 // indirect 246 | kcl-lang.io/kcl-go v0.11.2 // indirect 247 | kcl-lang.io/kcl-openapi v0.10.0 // indirect 248 | kcl-lang.io/kpm v0.11.2 // indirect 249 | kcl-lang.io/lib v0.11.2 // indirect 250 | oras.land/oras-go v1.2.6 // indirect 251 | oras.land/oras-go/v2 v2.5.0 // indirect 252 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 253 | sigs.k8s.io/kustomize/api v0.19.0 // indirect 254 | sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect 255 | sigs.k8s.io/randfill v1.0.0 // indirect 256 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 257 | sigs.k8s.io/yaml v1.4.0 // indirect 258 | ) 259 | -------------------------------------------------------------------------------- /install-binary.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Shamelessly copied from https://github.com/technosophos/helm-template 4 | 5 | PROJECT_NAME="helm-kcl" 6 | PROJECT_GH="kcl-lang/$PROJECT_NAME" 7 | export GREP_COLOR="never" 8 | 9 | # Convert HELM_BIN and HELM_PLUGIN_DIR to unix if cygpath is 10 | # available. This is the case when using MSYS2 or Cygwin 11 | # on Windows where helm returns a Windows path but we 12 | # need a Unix path 13 | 14 | if command -v cygpath >/dev/null 2>&1; then 15 | HELM_BIN="$(cygpath -u "${HELM_BIN}")" 16 | HELM_PLUGIN_DIR="$(cygpath -u "${HELM_PLUGIN_DIR}")" 17 | fi 18 | 19 | [ -z "$HELM_BIN" ] && HELM_BIN=$(command -v helm) 20 | 21 | [ -z "$HELM_HOME" ] && HELM_HOME=$(helm env | grep 'HELM_DATA_HOME' | cut -d '=' -f2 | tr -d '"') 22 | 23 | mkdir -p "$HELM_HOME" 24 | 25 | : "${HELM_PLUGIN_DIR:="$HELM_HOME/plugins/helm-kcl"}" 26 | 27 | if [ "$SKIP_BIN_INSTALL" = "1" ]; then 28 | echo "Skipping binary install" 29 | exit 30 | fi 31 | 32 | # which mode is the common installer script running in 33 | SCRIPT_MODE="install" 34 | if [ "$1" = "-u" ]; then 35 | SCRIPT_MODE="update" 36 | fi 37 | 38 | # initArch discovers the architecture for this system. 39 | initArch() { 40 | ARCH=$(uname -m) 41 | case $ARCH in 42 | armv5*) ARCH="armv5" ;; 43 | armv6*) ARCH="armv6" ;; 44 | armv7*) ARCH="armv7" ;; 45 | aarch64) ARCH="arm64" ;; 46 | x86) ARCH="386" ;; 47 | x86_64) ARCH="amd64" ;; 48 | i686) ARCH="386" ;; 49 | i386) ARCH="386" ;; 50 | esac 51 | } 52 | 53 | # initOS discovers the operating system for this system. 54 | initOS() { 55 | OS=$(uname -s) 56 | 57 | case "$OS" in 58 | Windows_NT) OS='windows' ;; 59 | # Msys support 60 | MSYS*) OS='windows' ;; 61 | # Minimalist GNU for Windows 62 | MINGW*) OS='windows' ;; 63 | CYGWIN*) OS='windows' ;; 64 | Darwin) OS='macos' ;; 65 | Linux) OS='linux' ;; 66 | esac 67 | } 68 | 69 | # verifySupported checks that the os/arch combination is supported for 70 | # binary builds. 71 | verifySupported() { 72 | supported="linux-amd64\nlinux-arm64\nfreebsd-amd64\nmacos-amd64\nmacos-arm64\nwindows-amd64" 73 | if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then 74 | echo "No prebuild binary for ${OS}-${ARCH}." 75 | exit 1 76 | fi 77 | 78 | if 79 | ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1 80 | then 81 | echo "Either curl or wget is required" 82 | exit 1 83 | fi 84 | } 85 | 86 | # getDownloadURL checks the latest available version. 87 | getDownloadURL() { 88 | version=$(git -C "$HELM_PLUGIN_DIR" describe --tags --exact-match 2>/dev/null || :) 89 | if [ "$SCRIPT_MODE" = "install" ] && [ -n "$version" ]; then 90 | DOWNLOAD_URL="https://github.com/$PROJECT_GH/releases/download/$version/helm-kcl-$OS-$ARCH.tgz" 91 | else 92 | DOWNLOAD_URL="https://github.com/$PROJECT_GH/releases/latest/download/helm-kcl-$OS-$ARCH.tgz" 93 | fi 94 | } 95 | 96 | # Temporary dir 97 | mkTempDir() { 98 | HELM_TMP="$(mktemp -d -t "${PROJECT_NAME}-XXXXXX")" 99 | } 100 | rmTempDir() { 101 | if [ -d "${HELM_TMP:-/tmp/helm-kcl-tmp}" ]; then 102 | rm -rf "${HELM_TMP:-/tmp/helm-kcl-tmp}" 103 | fi 104 | } 105 | 106 | # downloadFile downloads the latest binary package and also the checksum 107 | # for that binary. 108 | downloadFile() { 109 | PLUGIN_TMP_FILE="${HELM_TMP}/${PROJECT_NAME}.tgz" 110 | echo "Downloading $DOWNLOAD_URL" 111 | if 112 | command -v curl >/dev/null 2>&1 113 | then 114 | curl -sSf -L "$DOWNLOAD_URL" >"$PLUGIN_TMP_FILE" 115 | elif 116 | command -v wget >/dev/null 2>&1 117 | then 118 | wget -q -O - "$DOWNLOAD_URL" >"$PLUGIN_TMP_FILE" 119 | fi 120 | } 121 | 122 | # installFile verifies the SHA256 for the file, then unpacks and 123 | # installs it. 124 | installFile() { 125 | tar xzf "$PLUGIN_TMP_FILE" -C "$HELM_TMP" 126 | HELM_TMP_BIN="$HELM_TMP/kcl/bin/kcl" 127 | if [ "${OS}" = "windows" ]; then 128 | HELM_TMP_BIN="$HELM_TMP_BIN.exe" 129 | fi 130 | echo "Preparing to install into ${HELM_PLUGIN_DIR}" 131 | mkdir -p "$HELM_PLUGIN_DIR/bin" 132 | cp "$HELM_TMP_BIN" "$HELM_PLUGIN_DIR/bin" 133 | } 134 | 135 | # exit_trap is executed if on exit (error or not). 136 | exit_trap() { 137 | result=$? 138 | rmTempDir 139 | if [ "$result" != "0" ]; then 140 | echo "Failed to install $PROJECT_NAME" 141 | printf '\tFor support, go to https://github.com/kcl-lang/helm-kcl.\n' 142 | fi 143 | exit $result 144 | } 145 | 146 | # testVersion tests the installed client to make sure it is working. 147 | testVersion() { 148 | set +e 149 | echo "$PROJECT_NAME installed into $HELM_PLUGIN_DIR/$PROJECT_NAME" 150 | "${HELM_PLUGIN_DIR}/bin/kcl" -h 151 | set -e 152 | } 153 | 154 | # Execution 155 | 156 | #Stop execution on any error 157 | trap "exit_trap" EXIT 158 | set -e 159 | initArch 160 | initOS 161 | verifySupported 162 | getDownloadURL 163 | mkTempDir 164 | downloadFile 165 | installFile 166 | testVersion 167 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "kcl-lang.io/helm-kcl/cmd" 7 | ) 8 | 9 | func main() { 10 | if err := cmd.New().Execute(); err != nil { 11 | os.Exit(1) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | HELM_HOME ?= $(shell helm home) 2 | VERSION := $(shell sed -n -e 's/version:[ "]*\([^"]*\).*/\1/p' plugin.yaml) 3 | 4 | HELM_3_PLUGINS := $(shell bash -c 'eval $$(helm env); echo $$HELM_PLUGINS') 5 | 6 | PKG:= kcl-lang.io/helm-kcl 7 | LDFLAGS := -X $(PKG)/cmd.Version=$(VERSION) 8 | 9 | # Clear the "unreleased" string in BuildMetadata 10 | LDFLAGS += -X k8s.io/helm/pkg/version.BuildMetadata= 11 | LDFLAGS += -X k8s.io/helm/pkg/version.Version=$(shell ./scripts/dep-helm-version.sh) 12 | 13 | GO ?= go 14 | 15 | .PHONY: run 16 | run: 17 | go run main.go template --file ./examples/workload-charts-with-kcl/kcl-run.yaml 18 | 19 | .PHONY: format 20 | format: 21 | test -z "$$(find . -type f -o -name '*.go' -exec gofmt -d {} + | tee /dev/stderr)" || \ 22 | test -z "$$(find . -type f -o -name '*.go' -exec gofmt -w {} + | tee /dev/stderr)" 23 | 24 | .PHONY: install 25 | install: build 26 | mkdir -p $(HELM_HOME)/plugins/helm-kcl/bin 27 | cp -f bin/kcl $(HELM_HOME)/plugins/helm-kcl/bin 28 | cp -f plugin.yaml $(HELM_HOME)/plugins/helm-kcl/ 29 | 30 | .PHONY: install/helm3 31 | install/helm3: build 32 | mkdir -p $(HELM_3_PLUGINS)/helm-kcl/bin 33 | cp -f bin/kcl $(HELM_3_PLUGINS)/helm-kcl/bin 34 | cp -f plugin.yaml $(HELM_3_PLUGINS)/helm-kcl/ 35 | 36 | .PHONY: lint 37 | lint: 38 | scripts/update-gofmt.sh 39 | scripts/verify-gofmt.sh 40 | # scripts/verify-golint.sh 41 | scripts/verify-govet.sh 42 | 43 | .PHONY: build 44 | build: lint 45 | mkdir -p bin/ 46 | go build -v -o bin/kcl -ldflags="$(LDFLAGS)" 47 | 48 | .PHONY: test 49 | test: 50 | go test -v ./... 51 | 52 | .PHONY: bootstrap 53 | bootstrap: 54 | go mod tidy 55 | 56 | .PHONY: docker-run-release 57 | docker-run-release: export pkg=/go/src/github.com/kcl-lang/helm-kcl 58 | docker-run-release: 59 | git checkout main 60 | git push 61 | docker run -it --rm -e GITHUB_TOKEN -v $(shell pwd):$(pkg) -w $(pkg) golang:1.23 make bootstrap release 62 | 63 | .PHONY: dist 64 | dist: export COPYFILE_DISABLE=1 #teach OSX tar to not put ._* files in tar archive 65 | dist: export CGO_ENABLED=0 66 | dist: 67 | rm -rf build/kcl/* release/* 68 | mkdir -p build/kcl/bin release/ 69 | cp -f README.md LICENSE plugin.yaml build/kcl 70 | GOOS=linux GOARCH=amd64 $(GO) build -o build/kcl/bin/kcl -trimpath -ldflags="$(LDFLAGS)" 71 | tar -C build/ -zcvf $(CURDIR)/release/helm-kcl-linux-amd64.tgz kcl/ 72 | GOOS=linux GOARCH=arm64 $(GO) build -o build/kcl/bin/kcl -trimpath -ldflags="$(LDFLAGS)" 73 | tar -C build/ -zcvf $(CURDIR)/release/helm-kcl-linux-arm64.tgz kcl/ 74 | GOOS=darwin GOARCH=amd64 $(GO) build -o build/kcl/bin/kcl -trimpath -ldflags="$(LDFLAGS)" 75 | tar -C build/ -zcvf $(CURDIR)/release/helm-kcl-macos-amd64.tgz kcl/ 76 | GOOS=darwin GOARCH=arm64 $(GO) build -o build/kcl/bin/kcl -trimpath -ldflags="$(LDFLAGS)" 77 | tar -C build/ -zcvf $(CURDIR)/release/helm-kcl-macos-arm64.tgz kcl/ 78 | rm build/kcl/bin/kcl 79 | GOOS=windows GOARCH=amd64 $(GO) build -o build/kcl/bin/kcl.exe -trimpath -ldflags="$(LDFLAGS)" 80 | tar -C build/ -zcvf $(CURDIR)/release/helm-kcl-windows-amd64.tgz kcl/ 81 | 82 | .PHONY: release 83 | release: lint dist 84 | scripts/release.sh v$(VERSION) 85 | 86 | # Test for the plugin installation with `helm plugin install -v THIS_BRANCH` works 87 | # Useful for verifying modified `install-binary.sh` still works against various environments 88 | .PHONY: test-plugin-installation 89 | test-plugin-installation: 90 | docker build -f testdata/Dockerfile.install . 91 | -------------------------------------------------------------------------------- /pkg/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "path/filepath" 9 | 10 | "go.uber.org/zap" 11 | "helm.sh/helm/v3/pkg/chart" 12 | "kcl-lang.io/helm-kcl/pkg/config" 13 | "kcl-lang.io/helm-kcl/pkg/helm" 14 | "kcl-lang.io/krm-kcl/pkg/kube" 15 | ) 16 | 17 | // App is the main application object. 18 | type App struct { 19 | helmBinary string 20 | logger *zap.SugaredLogger 21 | render helm.Render 22 | } 23 | 24 | // Template of App run the 25 | func (app *App) Template(templateImpl *config.TemplateImpl) error { 26 | kclRun, err := config.FromFile(templateImpl.File) 27 | if err != nil { 28 | return err 29 | } 30 | for _, repo := range kclRun.Repositories { 31 | path, err := app.chartPathFromRepo(templateImpl.File, repo) 32 | if err != nil { 33 | return err 34 | } 35 | if err := app.template(templateImpl.File, repo.Name, path); err != nil { 36 | return err 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | func (app *App) chartPathFromRepo(file string, repo config.RepositorySpec) (path string, err error) { 43 | if repo.URL != "" { 44 | path = repo.URL 45 | } else if repo.Path != "" { 46 | path = repo.Path 47 | if !filepath.IsAbs(repo.Path) { 48 | path = filepath.Join(filepath.Dir(file), repo.Path) 49 | } 50 | } else { 51 | return "", errors.New("no valid helm chart path, it should be from a local path or a url") 52 | } 53 | return path, nil 54 | } 55 | 56 | func (app *App) template(kclRunFile, release, chartPath string) error { 57 | // Generate Kubernetes manifests from helm charts. 58 | manifests, err := app.renderManifests(release, chartPath) 59 | if err != nil { 60 | return err 61 | } 62 | // KCL function config 63 | fnCfg, err := os.ReadFile(kclRunFile) 64 | if err != nil { 65 | return err 66 | } 67 | result, err := app.doMutate(manifests, fnCfg) 68 | if err != nil { 69 | return err 70 | } 71 | fmt.Println(result) 72 | return nil 73 | } 74 | 75 | // Generate Kubernetes manifests from helm charts. 76 | func (app *App) renderManifests(release, chartPath string) ([]byte, error) { 77 | var chart *chart.Chart 78 | _, err := url.Parse(chartPath) 79 | // Load from url 80 | if err != nil { 81 | // Load from url 82 | chart, err = app.render.LoadChartFromRemoteCharts(chartPath) 83 | if err != nil { 84 | return nil, err 85 | } 86 | } else { 87 | // Load from local path 88 | chart, err = app.render.LoadChartFromLocalDirectory(chartPath) 89 | if err != nil { 90 | return nil, err 91 | } 92 | } 93 | manifests, err := app.render.GenerateManifests(release, "default", chart, nil) 94 | if err != nil { 95 | return nil, err 96 | } 97 | return manifests, nil 98 | } 99 | 100 | func (app *App) doMutate(manifests, fnCfg []byte) (string, error) { 101 | items, err := kube.ParseKubeObjects(manifests) 102 | if err != nil { 103 | return "", err 104 | } 105 | functionConfig, err := kube.ParseKubeObject(fnCfg) 106 | if err != nil { 107 | return "", err 108 | } 109 | // Construct resource list. 110 | resourceList := &kube.ResourceList{ 111 | Items: items, 112 | FunctionConfig: functionConfig, 113 | } 114 | r := &config.KCLRun{} 115 | err = r.TransformResourceList(resourceList) 116 | if err != nil { 117 | return "", err 118 | } 119 | return resourceList.Items.MustString(), nil 120 | } 121 | -------------------------------------------------------------------------------- /pkg/app/init.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "kcl-lang.io/helm-kcl/pkg/helm" 9 | ) 10 | 11 | const ( 12 | DefaultHelmBinary = "helm" 13 | DefaultKubeContext = "" 14 | HelmRequiredVersion = "v3.10.3" 15 | HelmRecommendedVersion = "v3.11.2" 16 | HelmDiffRecommendedVersion = "v3.4.0" 17 | HelmSecretsRecommendedVersion = "v4.1.1" 18 | HelmGitRecommendedVersion = "v0.12.0" 19 | HelmS3RecommendedVersion = "v0.14.0" 20 | HelmInstallCommand = "https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3" 21 | ) 22 | 23 | var ( 24 | manuallyInstallCode = 1 25 | windowPackageManagers = map[string]string{ 26 | "scoop": fmt.Sprintf("scoop install helm@%s", strings.TrimLeft(HelmRecommendedVersion, "v")), 27 | "choco": fmt.Sprintf("choco install kubernetes-helm --version %s", strings.TrimLeft(HelmRecommendedVersion, "v")), 28 | } 29 | helmPlugins = []helmRecommendedPlugin{ 30 | { 31 | name: "diff", 32 | version: HelmDiffRecommendedVersion, 33 | repo: "https://github.com/databus23/helm-diff", 34 | }, 35 | { 36 | name: "secrets", 37 | version: HelmSecretsRecommendedVersion, 38 | repo: "https://github.com/jkroepke/helm-secrets", 39 | }, 40 | { 41 | name: "s3", 42 | version: HelmS3RecommendedVersion, 43 | repo: "https://github.com/hypnoglow/helm-s3.git", 44 | }, 45 | { 46 | name: "helm-git", 47 | version: HelmGitRecommendedVersion, 48 | repo: "https://github.com/aslafy-z/helm-git.git", 49 | }, 50 | } 51 | ) 52 | 53 | type helmRecommendedPlugin struct { 54 | name string 55 | version string 56 | repo string 57 | } 58 | 59 | func New() *App { 60 | return &App{helmBinary: DefaultHelmBinary, logger: NewLogger(os.Stdout, "debug"), render: helm.Render{}} 61 | } 62 | -------------------------------------------------------------------------------- /pkg/app/logger.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "io" 5 | 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | ) 9 | 10 | // NewLogger returns the application logger. 11 | func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger { 12 | var cfg zapcore.EncoderConfig 13 | cfg.MessageKey = "message" 14 | out := zapcore.AddSync(writer) 15 | var level zapcore.Level 16 | err := level.Set(logLevel) 17 | if err != nil { 18 | panic(err) 19 | } 20 | core := zapcore.NewCore( 21 | zapcore.NewConsoleEncoder(cfg), 22 | out, 23 | level, 24 | ) 25 | return zap.New(core).Sugar() 26 | } 27 | -------------------------------------------------------------------------------- /pkg/config/kcl.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "gopkg.in/yaml.v2" 7 | "kcl-lang.io/krm-kcl/pkg/config" 8 | ) 9 | 10 | // KCLRun is a custom resource to provider Helm kcl config including KCL source and params. 11 | type KCLRun struct { 12 | config.KCLRun `json:",inline" yaml:",inline"` 13 | Repositories []RepositorySpec `yaml:"repositories,omitempty"` 14 | } 15 | 16 | func FromFile(file string) (*KCLRun, error) { 17 | yamlFile, err := os.ReadFile(file) 18 | if err != nil { 19 | return nil, err 20 | } 21 | var config KCLRun 22 | err = yaml.Unmarshal(yamlFile, &config) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return &config, nil 27 | } 28 | -------------------------------------------------------------------------------- /pkg/config/repo.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // RepositorySpec that defines values for a helm repo 4 | type RepositorySpec struct { 5 | Name string `yaml:"name,omitempty"` 6 | Path string `yaml:"path,omitempty"` 7 | URL string `yaml:"url,omitempty"` 8 | CaFile string `yaml:"caFile,omitempty"` 9 | CertFile string `yaml:"certFile,omitempty"` 10 | KeyFile string `yaml:"keyFile,omitempty"` 11 | Username string `yaml:"username,omitempty"` 12 | Password string `yaml:"password,omitempty"` 13 | Managed string `yaml:"managed,omitempty"` 14 | OCI bool `yaml:"oci,omitempty"` 15 | PassCredentials string `yaml:"passCredentials,omitempty"` 16 | SkipTLSVerify string `yaml:"skipTLSVerify,omitempty"` 17 | } 18 | -------------------------------------------------------------------------------- /pkg/config/template.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | // TemplateOptions is the options for the build command 10 | type TemplateOptions struct { 11 | // File is the file flag 12 | File string 13 | // Set is the set flag 14 | Set []string 15 | // Values is the values flag 16 | Values []string 17 | // OutputDir is the output dir flag 18 | OutputDir string 19 | // OutputDirTemplate is the output dir template flag 20 | OutputDirTemplate string 21 | // Concurrency is the concurrency flag 22 | Concurrency int 23 | // Validate is the validate flag 24 | Validate bool 25 | // IncludeCRDs is the include crds flag 26 | IncludeCRDs bool 27 | // SkipTests is the skip tests flag 28 | SkipTests bool 29 | // SkipNeeds is the skip needs flag 30 | SkipNeeds bool 31 | // IncludeNeeds is the include needs flag 32 | IncludeNeeds bool 33 | // IncludeTransitiveNeeds is the include transitive needs flag 34 | IncludeTransitiveNeeds bool 35 | // SkipDeps is the skip deps flag 36 | SkipDeps bool 37 | // SkipCleanup is the skip cleanup flag 38 | SkipCleanup bool 39 | // Propagate '--post-renderer' to helmv3 template and helm install 40 | PostRenderer string 41 | } 42 | 43 | // NewTemplateOptions creates a new Apply 44 | func NewTemplateOptions() *TemplateOptions { 45 | return &TemplateOptions{} 46 | } 47 | 48 | // TemplateImpl is impl for applyOptions 49 | type TemplateImpl struct { 50 | *TemplateOptions 51 | } 52 | 53 | // NewTemplateImpl creates a new TemplateImpl 54 | func NewTemplateImpl(t *TemplateOptions) *TemplateImpl { 55 | return &TemplateImpl{ 56 | TemplateOptions: t, 57 | } 58 | } 59 | 60 | // Concurrency returns the concurrency 61 | func (t *TemplateImpl) Concurrency() int { 62 | return t.TemplateOptions.Concurrency 63 | } 64 | 65 | // IncludeCRDs returns the include crds 66 | func (t *TemplateImpl) IncludeCRDs() bool { 67 | return t.TemplateOptions.IncludeCRDs 68 | } 69 | 70 | // IncludeNeeds returns the include needs 71 | func (t *TemplateImpl) IncludeNeeds() bool { 72 | return t.TemplateOptions.IncludeNeeds || t.IncludeTransitiveNeeds() 73 | } 74 | 75 | // IncludeTransitiveNeeds returns the include transitive needs 76 | func (t *TemplateImpl) IncludeTransitiveNeeds() bool { 77 | return t.TemplateOptions.IncludeTransitiveNeeds 78 | } 79 | 80 | // OutputDir returns the output dir 81 | func (t *TemplateImpl) OutputDir() string { 82 | return strings.TrimRight(t.TemplateOptions.OutputDir, fmt.Sprintf("%c", os.PathSeparator)) 83 | } 84 | 85 | // OutputDirTemplate returns the output dir template 86 | func (t *TemplateImpl) OutputDirTemplate() string { 87 | return t.TemplateOptions.OutputDirTemplate 88 | } 89 | 90 | // Set returns the Set 91 | func (t *TemplateImpl) Set() []string { 92 | return t.TemplateOptions.Set 93 | } 94 | 95 | // SkipCleanup returns the skip cleanup 96 | func (t *TemplateImpl) SkipCleanup() bool { 97 | return t.TemplateOptions.SkipCleanup 98 | } 99 | 100 | // SkipDeps returns the skip deps 101 | func (t *TemplateImpl) SkipDeps() bool { 102 | return t.TemplateOptions.SkipDeps 103 | } 104 | 105 | // SkipNeeds returns the skip needs 106 | func (t *TemplateImpl) SkipNeeds() bool { 107 | if !t.IncludeNeeds() { 108 | return t.TemplateOptions.SkipNeeds 109 | } 110 | 111 | return false 112 | } 113 | 114 | // SkipTests returns the skip tests 115 | func (t *TemplateImpl) SkipTests() bool { 116 | return t.TemplateOptions.SkipTests 117 | } 118 | 119 | // Validate returns the validate 120 | func (t *TemplateImpl) Validate() bool { 121 | return t.TemplateOptions.Validate 122 | } 123 | 124 | // Values returns the values 125 | func (t *TemplateImpl) Values() []string { 126 | return t.TemplateOptions.Values 127 | } 128 | 129 | // PostRenderer returns the PostRenderer. 130 | func (t *TemplateImpl) PostRenderer() string { 131 | return t.TemplateOptions.PostRenderer 132 | } 133 | -------------------------------------------------------------------------------- /pkg/helm/helm.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | // This file contains functions that where blatantly copied from 4 | // https://github.wdf.sap.corp/kubernetes/helm 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "google.golang.org/grpc" 14 | "k8s.io/helm/pkg/downloader" 15 | "k8s.io/helm/pkg/getter" 16 | "k8s.io/helm/pkg/helm/environment" 17 | "k8s.io/helm/pkg/helm/helmpath" 18 | ) 19 | 20 | /////////////// Source: cmd/helm/install.go ///////////////////////// 21 | 22 | type valueFiles []string 23 | 24 | func (v *valueFiles) String() string { 25 | return fmt.Sprint(*v) 26 | } 27 | 28 | // Ensures all valuesFiles exist 29 | func (v *valueFiles) Valid() error { 30 | errStr := "" 31 | for _, valuesFile := range *v { 32 | if strings.TrimSpace(valuesFile) != "-" { 33 | if _, err := os.Stat(valuesFile); os.IsNotExist(err) { 34 | errStr += err.Error() 35 | } 36 | } 37 | } 38 | 39 | if errStr == "" { 40 | return nil 41 | } 42 | 43 | return errors.New(errStr) 44 | } 45 | 46 | func (v *valueFiles) Type() string { 47 | return "valueFiles" 48 | } 49 | 50 | func (v *valueFiles) Set(value string) error { 51 | for _, filePath := range strings.Split(value, ",") { 52 | *v = append(*v, filePath) 53 | } 54 | return nil 55 | } 56 | 57 | func IsHelm3() bool { 58 | return os.Getenv("TILLER_HOST") == "" 59 | } 60 | 61 | func IsDebug() bool { 62 | return os.Getenv("HELM_DEBUG") == "true" 63 | } 64 | 65 | func locateChartPath(name, version string, verify bool, keyring string) (string, error) { 66 | name = strings.TrimSpace(name) 67 | version = strings.TrimSpace(version) 68 | if fi, err := os.Stat(name); err == nil { 69 | abs, err := filepath.Abs(name) 70 | if err != nil { 71 | return abs, err 72 | } 73 | if verify { 74 | if fi.IsDir() { 75 | return "", errors.New("cannot verify a directory") 76 | } 77 | if _, err := downloader.VerifyChart(abs, keyring); err != nil { 78 | return "", err 79 | } 80 | } 81 | return abs, nil 82 | } 83 | if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { 84 | return name, fmt.Errorf("path %q not found", name) 85 | } 86 | 87 | crepo := filepath.Join(helmpath.Home(homePath()).Repository(), name) 88 | if _, err := os.Stat(crepo); err == nil { 89 | return filepath.Abs(crepo) 90 | } 91 | 92 | dl := downloader.ChartDownloader{ 93 | HelmHome: helmpath.Home(homePath()), 94 | Out: os.Stdout, 95 | Keyring: keyring, 96 | Getters: getter.All(environment.EnvSettings{}), 97 | } 98 | if verify { 99 | dl.Verify = downloader.VerifyAlways 100 | } 101 | 102 | filename, _, err := dl.DownloadTo(name, version, helmpath.Home(homePath()).Archive()) 103 | if err == nil { 104 | lname, err := filepath.Abs(filename) 105 | if err != nil { 106 | return filename, err 107 | } 108 | return lname, nil 109 | } 110 | 111 | return filename, err 112 | } 113 | 114 | /////////////// Source: cmd/helm/helm.go //////////////////////////// 115 | 116 | func checkArgsLength(argsReceived int, requiredArgs ...string) error { 117 | expectedNum := len(requiredArgs) 118 | if argsReceived != expectedNum { 119 | arg := "arguments" 120 | if expectedNum == 1 { 121 | arg = "argument" 122 | } 123 | return fmt.Errorf("This command needs %v %s: %s", expectedNum, arg, strings.Join(requiredArgs, ", ")) 124 | } 125 | return nil 126 | } 127 | 128 | func homePath() string { 129 | return os.Getenv("HELM_HOME") 130 | } 131 | 132 | func prettyError(err error) error { 133 | if err == nil { 134 | return nil 135 | } 136 | // This is ridiculous. Why is 'grpc.rpcError' not exported? The least they 137 | // could do is throw an interface on the lib that would let us get back 138 | // the desc. Instead, we have to pass ALL errors through this. 139 | return errors.New(grpc.ErrorDesc(err)) 140 | } 141 | -------------------------------------------------------------------------------- /pkg/helm/helm3.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | 8 | "io" 9 | "log" 10 | "net/http" 11 | "reflect" 12 | "strings" 13 | 14 | "helm.sh/helm/v3/pkg/action" 15 | "helm.sh/helm/v3/pkg/chart" 16 | "helm.sh/helm/v3/pkg/chart/loader" 17 | . "helm.sh/helm/v3/pkg/repo" 18 | "helm.sh/helm/v3/pkg/strvals" 19 | ) 20 | 21 | const ( 22 | helmFieldTag = "helm" 23 | ) 24 | 25 | type TemplateRender interface { 26 | GenerateManifests(releaseName, namespace string, chart *chart.Chart, values map[string]interface{}) ([]byte, error) 27 | } 28 | 29 | type Render struct { 30 | // indexFile is the index file of the remote charts. 31 | indexFile *IndexFile 32 | } 33 | 34 | var _ TemplateRender = &Render{} 35 | 36 | func (r *Render) LoadChartFromRemoteCharts(downloadURL string) (*chart.Chart, error) { 37 | rsp, err := http.Get(downloadURL) 38 | if err != nil { 39 | return nil, err 40 | } 41 | defer rsp.Body.Close() 42 | 43 | body, err := io.ReadAll(rsp.Body) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return loader.LoadArchive(bytes.NewReader(body)) 49 | } 50 | 51 | func (r *Render) LoadChartFromLocalDirectory(directory string) (*chart.Chart, error) { 52 | return loader.LoadDir(directory) 53 | } 54 | 55 | func (r *Render) GenerateManifests(releaseName, namespace string, chart *chart.Chart, values map[string]interface{}) ([]byte, error) { 56 | client, err := r.newHelmClient(releaseName, namespace) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | rel, err := client.RunWithContext(context.TODO(), chart, values) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | var manifests bytes.Buffer 67 | _, err = fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest)) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return manifests.Bytes(), nil 73 | } 74 | 75 | func (r *Render) GenerateHelmValues(input interface{}) (map[string]interface{}, error) { 76 | var rawArgs []string 77 | valueOf := reflect.ValueOf(input) 78 | 79 | // Make sure we are handling with a struct here. 80 | if valueOf.Kind() != reflect.Struct { 81 | return nil, fmt.Errorf("invalid input type, should be struct") 82 | } 83 | 84 | typeOf := reflect.TypeOf(input) 85 | for i := 0; i < valueOf.NumField(); i++ { 86 | helmValueKey := typeOf.Field(i).Tag.Get(helmFieldTag) 87 | if helmValueKey != "" && valueOf.Field(i).Len() > 0 { 88 | rawArgs = append(rawArgs, fmt.Sprintf("%s=%s\n", helmValueKey, valueOf.Field(i))) 89 | } 90 | } 91 | 92 | if len(rawArgs) > 0 { 93 | values := make(map[string]interface{}) 94 | if err := strvals.ParseInto(strings.Join(rawArgs, ","), values); err != nil { 95 | return nil, err 96 | } 97 | return values, nil 98 | } 99 | 100 | return nil, nil 101 | } 102 | 103 | func (r *Render) GetLatestChart(indexFile *IndexFile, chartName string) (*ChartVersion, error) { 104 | if versions, ok := indexFile.Entries[chartName]; ok { 105 | if versions.Len() > 0 { 106 | // The Entries are already sorted by version so the position 0 always point to the latest version. 107 | v := []*ChartVersion(versions) 108 | if len(v[0].URLs) == 0 { 109 | return nil, fmt.Errorf("no download URLs found for %s-%s", chartName, v[0].Version) 110 | } 111 | return v[0], nil 112 | } 113 | return nil, fmt.Errorf("chart %s has empty versions", chartName) 114 | } 115 | 116 | return nil, fmt.Errorf("chart %s not found", chartName) 117 | } 118 | 119 | func (r *Render) GetIndexFile(indexURL string) (*IndexFile, error) { 120 | if r.indexFile != nil { 121 | return r.indexFile, nil 122 | } 123 | 124 | rsp, err := http.Get(indexURL) 125 | if err != nil { 126 | return nil, err 127 | } 128 | defer rsp.Body.Close() 129 | 130 | body, err := io.ReadAll(rsp.Body) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | indexFile, err := loadIndex(body, indexURL) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | // Cache the index file, so we don't request the index every time. 141 | r.indexFile = indexFile 142 | 143 | return indexFile, nil 144 | } 145 | 146 | func (r *Render) newHelmClient(releaseName, namespace string) (*action.Install, error) { 147 | helmClient := action.NewInstall(new(action.Configuration)) 148 | helmClient.DryRun = true 149 | helmClient.ReleaseName = releaseName 150 | helmClient.Replace = true 151 | helmClient.ClientOnly = true 152 | helmClient.IncludeCRDs = true 153 | helmClient.Namespace = namespace 154 | 155 | return helmClient, nil 156 | } 157 | 158 | // loadIndex is from 'helm/pkg/index.go'. 159 | func loadIndex(data []byte, source string) (*IndexFile, error) { 160 | i := &IndexFile{} 161 | 162 | if len(data) == 0 { 163 | return i, ErrEmptyIndexYaml 164 | } 165 | 166 | for name, cvs := range i.Entries { 167 | for idx := len(cvs) - 1; idx >= 0; idx-- { 168 | if cvs[idx] == nil { 169 | log.Printf("skipping loading invalid entry for chart %q from %s: empty entry", name, source) 170 | continue 171 | } 172 | if cvs[idx].APIVersion == "" { 173 | cvs[idx].APIVersion = chart.APIVersionV1 174 | } 175 | if err := cvs[idx].Validate(); err != nil { 176 | log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err) 177 | cvs = append(cvs[:idx], cvs[idx+1:]...) 178 | } 179 | } 180 | } 181 | i.SortEntries() 182 | if i.APIVersion == "" { 183 | return i, ErrNoAPIVersion 184 | } 185 | return i, nil 186 | } 187 | -------------------------------------------------------------------------------- /plugin.yaml: -------------------------------------------------------------------------------- 1 | name: "kcl" 2 | # Version is the version of Helm plus the number of official builds for this 3 | # plugin 4 | version: "0.10.0" 5 | usage: "Helm KCL Plugin" 6 | description: "Helm KCL Plugin" 7 | useTunnel: true 8 | command: "$HELM_PLUGIN_DIR/bin/kcl" 9 | hooks: 10 | install: "$HELM_PLUGIN_DIR/install-binary.sh" 11 | update: "$HELM_PLUGIN_DIR/install-binary.sh -u" 12 | -------------------------------------------------------------------------------- /scripts/dep-helm-version.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | grep "k8s.io/helm" go.mod | sed -n -e "s/.*k8s.io\/helm \(v[.0-9]*\).*/\1/p" 4 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -e 3 | if [ "$1" == "" ]; then 4 | echo usage: "$0 VERSION" 5 | fi 6 | git tag $1 7 | git push origin $1 8 | gh release create $1 --draft --generate-notes --title "$1 Release" release/*.tgz 9 | -------------------------------------------------------------------------------- /scripts/setup-apimachinery.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2016 The Kubernetes Authors All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Copies the current versions of apimachinery and client-go out of the 18 | # main kubernetes repo. These repos are currently out of sync and not 19 | # versioned. 20 | set -euo pipefail 21 | 22 | 23 | rm -rf ./vendor/k8s.io/{kube-aggregator,apiserver,apimachinery,client-go} 24 | 25 | cp -r ./vendor/k8s.io/kubernetes/staging/src/k8s.io/{kube-aggregator,apiserver,apimachinery,client-go} ./vendor/k8s.io 26 | -------------------------------------------------------------------------------- /scripts/update-gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # script credits : https://github.com/infracloudio/botkube 3 | 4 | set -o errexit 5 | set -o nounset 6 | set -o pipefail 7 | 8 | find_files() { 9 | find . -not \( \ 10 | \( \ 11 | -wholename '*/vendor/*' \ 12 | \) -prune \ 13 | \) -name '*.go' 14 | } 15 | 16 | find_files | xargs gofmt -w -s 17 | -------------------------------------------------------------------------------- /scripts/verify-gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # script credits : https://github.com/infracloudio/botkube 3 | 4 | set -o errexit 5 | set -o nounset 6 | set -o pipefail 7 | 8 | find_files() { 9 | find . -not \( \ 10 | \( \ 11 | -wholename '*/vendor/*' \ 12 | \) -prune \ 13 | \) -name '*.go' 14 | } 15 | 16 | bad_files=$(find_files | xargs gofmt -d -s 2>&1) 17 | if [[ -n "${bad_files}" ]]; then 18 | echo "${bad_files}" >&2 19 | echo >&2 20 | echo "Run ./hack/update-gofmt.sh" >&2 21 | exit 1 22 | fi 23 | -------------------------------------------------------------------------------- /scripts/verify-golint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # script credits : https://github.com/infracloudio/botkube 3 | 4 | set -o errexit 5 | set -o nounset 6 | set -o pipefail 7 | 8 | find_files() { 9 | find . -not \( \ 10 | \( \ 11 | -wholename '*/vendor/*' \ 12 | \) -prune \ 13 | \) -name '*.go' 14 | } 15 | 16 | bad_files=$(find_files | xargs -I@ bash -c "golint @") 17 | if [[ -n "${bad_files}" ]]; then 18 | echo "${bad_files}" 19 | exit 1 20 | fi 21 | -------------------------------------------------------------------------------- /scripts/verify-govet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # script credits : https://github.com/infracloudio/botkube 3 | 4 | set -x 5 | 6 | go vet . ./cmd/... 7 | --------------------------------------------------------------------------------