├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── chart └── parser.go ├── chartsconfig └── parser.go ├── config ├── cli.go └── options.go ├── go.mod ├── go.sum ├── helm ├── helm.go ├── init.go ├── repo_add.go └── version.go ├── main.go └── script └── bootstrap /.gitignore: -------------------------------------------------------------------------------- 1 | /_dist 2 | /bin 3 | /kubecrt 4 | /vendor 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2017, Blendle 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY=kubecrt 2 | 3 | TAG=$(shell git for-each-ref refs/tags --sort=-creatordate --format='%(refname:short)' --count=1) 4 | MAJOR=`echo $(TAG) | awk -F[v.] '{print $$2}'` 5 | MINOR=`echo $(TAG) | awk -F[v.] '{print $$3}'` 6 | PATCH=`echo $(TAG) | awk -F[v.] '{print $$4}'` 7 | GIT_COMMIT=`git rev-parse --short @` 8 | LDFLAGS=-X github.com/blendle/kubecrt/config.version=$(TAG) -X github.com/blendle/kubecrt/config.gitrev=$(GIT_COMMIT) 9 | 10 | build: 11 | mkdir -p bin 12 | go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY) 13 | 14 | prep: 15 | @mkdir -p _dist 16 | 17 | dist: prep 18 | GOOS=linux GOARCH=amd64 go build -ldflags "-s -w $(LDFLAGS)" -o _dist/$(BINARY)_$(TAG)_linux_amd64 19 | GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w $(LDFLAGS)" -o _dist/$(BINARY)_$(TAG)_darwin_amd64 20 | 21 | patch: prep 22 | @version=v$(MAJOR).$(MINOR).$$(expr $(PATCH) + 1); \ 23 | git tag $$version 24 | 25 | minor: prep 26 | @version=v$(MAJOR).$$(expr $(MINOR) + 1).0; \ 27 | git tag $$version 28 | 29 | major: prep 30 | @version=v$$(expr $(MAJOR) + 1).0.0; \ 31 | git tag $$version 32 | 33 | push: 34 | git push --tags 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubecrt 2 | 3 | Convert Helm charts to Kubernetes resources. 4 | 5 | ## Description 6 | 7 | kubecrt allows you to define your application's Kubernetes infrastructure based 8 | on a single configuration file. 9 | 10 | The configuration file contains your application name, and namespace (both can 11 | also be set using CLI arguments), and you provide a list of charts that you want 12 | to install, optionally providing override values for those charts. 13 | 14 | When running `kubecrt`, you provide it your project's configuration file, and in 15 | turn, it returns you the parsed Kubernetes resource files generated by the 16 | charts. 17 | 18 | This allows you to use Helm Charts without actually using Helm, and instead 19 | using regular `kubectl` to deploy and manage your resources. 20 | 21 | The configuration file you feed into kubecrt will be processed using the epp 22 | templating tool, allowing you to inject variables at runtime, based on your own 23 | conditional logic (production vs staging, etc...). 24 | 25 | ## Installation 26 | 27 | ``` 28 | brew tap blendle/blendle 29 | brew install kubecrt 30 | ``` 31 | 32 | ## Usage 33 | 34 | See `kubecrt --help` 35 | 36 | ``` 37 | kubecrt - convert Helm charts to Kubernetes resources 38 | 39 | Given a charts.yml file, compile all the charts with 40 | the provided template variables and return the 41 | resulting Kubernetes resource files to STDOUT, or 42 | write them to a provided file. 43 | 44 | Doing this, you can use Kubernetes charts, without 45 | having to use Helm locally, or Tiller on the server. 46 | 47 | Usage: 48 | kubecrt [options] CHARTS_CONFIG 49 | kubecrt -h | --help 50 | kubecrt --version 51 | kubecrt --example-config 52 | 53 | Where CHARTS_CONFIG is the location of the YAML file 54 | containing the Kubernetes Charts configuration. 55 | 56 | Arguments: 57 | CHARTS_CONFIG Charts configuration file 58 | 59 | Options: 60 | -h, --help Show this screen 61 | --version Show version 62 | -n NS, --namespace=NS Set the .Release.Namespace chart variable, 63 | used by charts during compilation 64 | -a NAME, --name=NAME Set the .Release.Name chart variable, used by 65 | charts during compilation 66 | -o PATH, --output=PATH Write output to a file, instead of STDOUT 67 | -r NAME=URL, --repo=NAME=URL,... List of NAME=URL pairs of repositories to add 68 | to the index before compiling charts config 69 | -p DIR, --partials-dir=DIR Path from which to load partial templates 70 | [default: config/deploy/partials] 71 | -j, --json Print resources formatted as JSON instead of 72 | YAML. Each resource is printed on a single 73 | line. 74 | --example-config Print an example charts.yaml, including 75 | extended documentation on the tunables 76 | ``` 77 | 78 | ## Charts Configuration File 79 | 80 | See `kubecrt --example-config` 81 | 82 | ```yaml 83 | # apiVersion defines the version of the charts.yaml structure. Currently, 84 | # only "v1" is supported. 85 | apiVersion: v1 86 | 87 | # name is the .Release.Name template value that charts can use in their 88 | # templates, which can be overridden by the "--name" CLI flag. If omitted, 89 | # "--name" is required. 90 | name: my-bundled-apps 91 | 92 | # namespace is the .Release.Namespace template value that charts can use in 93 | # their templates. Note that since kubecrt does not communicate with 94 | # Kubernetes in any way, it is up to you to also use this namespace when 95 | # doing kubectl apply [...]. Can be overridden using "--namespace". If omitted, 96 | # "--namespace" is required. 97 | namespace: apps 98 | 99 | # charts is an array of charts you want to compile into Kubernetes resource 100 | # files. 101 | # 102 | # A single chart might be used to deploy something simple, like a memcached pod, 103 | # or something complex, like a full web app stack with HTTP servers, databases, 104 | # caches, and so on. 105 | charts: 106 | 107 | # A Chart can either be in the format REPO/NAME, or a PATH to a local chart. 108 | # 109 | # If using REPO/NAME, kubecrt knows by-default where to locate the "stable" 110 | # repository, all other repositories require the "repo" configuration (see 111 | # below). 112 | - stable/factorio: 113 | # values is a map of key/value pairs used when compiling the chart. This 114 | # uses the same format as in regular chart "values.yaml" files. 115 | # 116 | # see: https://git.io/v9Tyr 117 | values: 118 | resources: 119 | requests: 120 | memory: 1024Mi 121 | cpu: 750m 122 | factorioServer: 123 | # charts.yaml supports the same templating as chart templates do, 124 | # using the "sprig" library. 125 | # 126 | # see: https://masterminds.github.io/sprig/ 127 | name: > 128 | {{ env "MY_SERVER_NAME" | default "hello world!" }} 129 | 130 | - stable/minecraft: 131 | # version is a semantic version constraint. 132 | # 133 | # see: https://github.com/Masterminds/semver#basic-comparisons 134 | version: ~> 0.1.0 135 | values: 136 | minecraftServer: 137 | difficulty: hard 138 | 139 | - opsgoodness/prometheus-operator: 140 | # repo is the location of a repositry, if other than "stable". This is 141 | # the URL you would normally add using "helm repo add NAME URL". 142 | repo: http://charts.opsgoodness.com 143 | values: 144 | sendAnalytics: false 145 | 146 | # For the above charts, see here for the default configurations: 147 | # 148 | # * stable/factorio: https://git.io/v9Tyr 149 | # * stable/minecraft: https://git.io/v9Tya 150 | # * opsgoodness/prometheus-operator: https://git.io/v9SAY 151 | ``` 152 | 153 | ## Partial Templates 154 | 155 | You can optionally split your `charts.yml` file into multiple chunks, by using 156 | _partial templates_. This works almost the same way as Helm's support for these 157 | in charts. See the [Helm documentation][docs] for more details. 158 | 159 | To use these partials, you have to set the `--partials-dir` flag when calling 160 | `kubecrt`, pass it the path to your partials directory, and then use those 161 | partials in your `charts.yml`. 162 | 163 | Example: 164 | 165 | **charts.yml**: 166 | 167 | ```yaml 168 | apiVersion: v1 169 | name: my-bundled-apps 170 | namespace: apps 171 | charts: 172 | - stable/factorio: 173 | values: 174 | resources: 175 | {{ include "factorio/resources" . | indent 8 }} 176 | ``` 177 | 178 | **partials/factorio/resources.yml** 179 | 180 | ```yaml 181 | {{- define "factorio/resources" -}} 182 | requests: 183 | memory: 1024Mi 184 | cpu: 750m 185 | {{- end -}} 186 | ``` 187 | 188 | You can then run this as follows: 189 | 190 | ``` 191 | kubecrt --partials-dir ./partials charts.yml 192 | ``` 193 | 194 | And the result is a fully-parsed charts printed to stdout. 195 | 196 | Some notes: 197 | 198 | * you can use subfolders to organise your partials 199 | * each named `define` has to be uniquely named, or risk being overwritten 200 | * you can define multiple `define` blocks in a single file 201 | * the files don't need to be yaml files, you can use any content you need 202 | 203 | [docs]: https://github.com/kubernetes/helm/blob/master/docs/chart_template_guide/named_templates.md 204 | 205 | ## Releasing new version 206 | 207 | ``` 208 | make (major|minor|patch) 209 | git push --tags 210 | make dist 211 | open _dist/ 212 | ``` 213 | 214 | Next, go to the [GitHub releases](https://github.com/blendle/kubecrt/releases) 215 | page, and edit the tag you just push: 216 | 217 | * Release title: vx.x.x 218 | * Describe this release: short description of important changes 219 | * Attach binaries: drop the files created in `_dist` here 220 | 221 | Click "Update release". 222 | 223 | Don't forget to update the Homebrew formula, located at 224 | [blendle/homebrew-blendle][tap]. 225 | 226 | [tap]: https://github.com/blendle/homebrew-blendle 227 | -------------------------------------------------------------------------------- /chart/parser.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/blendle/kubecrt/helm" 11 | 12 | yaml "gopkg.in/yaml.v2" 13 | "k8s.io/helm/pkg/chartutil" 14 | "k8s.io/helm/pkg/downloader" 15 | "k8s.io/helm/pkg/engine" 16 | "k8s.io/helm/pkg/getter" 17 | "k8s.io/helm/pkg/helm/environment" 18 | "k8s.io/helm/pkg/helm/helmpath" 19 | "k8s.io/helm/pkg/proto/hapi/chart" 20 | "k8s.io/helm/pkg/timeconv" 21 | ) 22 | 23 | // Chart ... 24 | type Chart struct { 25 | Version string `yaml:"version"` 26 | Repo string `yaml:"repo"` 27 | Values interface{} `yaml:"values"` 28 | Location string 29 | } 30 | 31 | // ParseChart ... 32 | func (c *Chart) ParseChart(name, namespace string) ([]byte, error) { 33 | s := strings.Split(c.Location, "/") 34 | 35 | if len(s) == 2 && c.Repo != "" { 36 | err := helm.AddRepository(s[0], c.Repo) 37 | if err != nil { 38 | return nil, err 39 | } 40 | } 41 | 42 | d, err := yaml.Marshal(c.Values) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | tmpfile, err := ioutil.TempFile("", "") 48 | if err != nil { 49 | return nil, err 50 | } 51 | defer os.Remove(tmpfile.Name()) 52 | 53 | if _, err = tmpfile.Write(d); err != nil { 54 | return nil, err 55 | } 56 | 57 | if err = tmpfile.Close(); err != nil { 58 | return nil, err 59 | } 60 | 61 | resources, err := c.compile(name, namespace, tmpfile.Name()) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | return resources, nil 67 | } 68 | 69 | func (c *Chart) compile(releaseName, namespace, values string) ([]byte, error) { 70 | var output string 71 | 72 | location, err := locateChartPath(c.Location, c.Version) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | cr, err := chartutil.Load(location) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | vv, err := vals(values) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | config := &chart.Config{Raw: string(vv), Values: map[string]*chart.Value{}} 88 | 89 | options := chartutil.ReleaseOptions{ 90 | Name: releaseName, 91 | Time: timeconv.Now(), 92 | Namespace: namespace, 93 | } 94 | 95 | renderer := engine.New() 96 | 97 | vals, err := chartutil.ToRenderValues(cr, config, options) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | out, err := renderer.Render(cr, vals) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | for name, data := range out { 108 | b := filepath.Base(name) 109 | if b == "NOTES.txt" { 110 | continue 111 | } 112 | if strings.HasPrefix(b, "_") { 113 | continue 114 | } 115 | if strings.TrimSpace(data) == "" { 116 | continue 117 | } 118 | 119 | data = strings.TrimSpace(data) 120 | 121 | if !strings.HasPrefix(data, "---\n") { 122 | data = "---\n" + data 123 | } 124 | 125 | if output != "" { 126 | data = "\n\n" + data 127 | } 128 | 129 | output = output + data 130 | } 131 | 132 | return []byte(strings.Trim(output, "\n") + "\n"), nil 133 | } 134 | 135 | func vals(valuesPath string) ([]byte, error) { 136 | base := map[string]interface{}{} 137 | 138 | bytes, err := ioutil.ReadFile(valuesPath) 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | if err := yaml.Unmarshal(bytes, &base); err != nil { 144 | return nil, fmt.Errorf("failed to parse %s: %s", valuesPath, err) 145 | } 146 | 147 | return yaml.Marshal(base) 148 | } 149 | 150 | func locateChartPath(name, version string) (string, error) { 151 | homepath := filepath.Join(os.Getenv("HOME"), ".helm") 152 | 153 | name = strings.TrimSpace(name) 154 | version = strings.TrimSpace(version) 155 | if _, err := os.Stat(name); err == nil { 156 | abs, err := filepath.Abs(name) 157 | if err != nil { 158 | return abs, err 159 | } 160 | 161 | return abs, nil 162 | } 163 | 164 | if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { 165 | return name, fmt.Errorf("path %q not found", name) 166 | } 167 | 168 | crepo := filepath.Join(helmpath.Home(homepath).Repository(), name) 169 | if _, err := os.Stat(crepo); err == nil { 170 | return filepath.Abs(crepo) 171 | } 172 | 173 | settings := environment.EnvSettings{ 174 | Home: helmpath.Home(environment.DefaultHelmHome), 175 | } 176 | 177 | dl := downloader.ChartDownloader{ 178 | HelmHome: helmpath.Home(homepath), 179 | Out: os.Stdout, 180 | Getters: getter.All(settings), 181 | } 182 | 183 | err := os.MkdirAll(filepath.Dir(crepo), 0755) 184 | if err != nil { 185 | return "", fmt.Errorf("Failed to untar (mkdir): %s", err) 186 | } 187 | 188 | version, err = helm.GetAcceptableVersion(name, version) 189 | if err != nil { 190 | return "", err 191 | } 192 | 193 | filename, _, err := dl.DownloadTo(name, version, filepath.Dir(crepo)) 194 | if err != nil { 195 | return "", err 196 | } 197 | 198 | return filename, nil 199 | } 200 | -------------------------------------------------------------------------------- /chartsconfig/parser.go: -------------------------------------------------------------------------------- 1 | package chartsconfig 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "html/template" 9 | "io/ioutil" 10 | "os" 11 | "path/filepath" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/Masterminds/semver" 16 | "github.com/blendle/kubecrt/chart" 17 | "github.com/blendle/kubecrt/config" 18 | yaml "gopkg.in/yaml.v2" 19 | "k8s.io/helm/pkg/engine" 20 | hchart "k8s.io/helm/pkg/proto/hapi/chart" 21 | ) 22 | 23 | // ChartsConfiguration ... 24 | type ChartsConfiguration struct { 25 | APIVersion string `yaml:"apiVersion"` 26 | Name string `yaml:"name"` 27 | Namespace string `yaml:"namespace"` 28 | ChartsMap []map[string]*chart.Chart `yaml:"charts"` 29 | ChartsList []*chart.Chart 30 | } 31 | 32 | // NewChartsConfiguration initializes a new ChartsConfiguration. 33 | func NewChartsConfiguration(input []byte, tpath string) (*ChartsConfiguration, error) { 34 | m := &ChartsConfiguration{} 35 | 36 | renderer := engine.New() 37 | 38 | funcs := template.FuncMap{ 39 | "env": func(s string) string { return os.Getenv(s) }, 40 | "expandenv": func(s string) string { return os.ExpandEnv(s) }, 41 | } 42 | 43 | for k, v := range funcs { 44 | renderer.FuncMap[k] = v 45 | } 46 | 47 | t, err := stubChart(input, tpath) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | tpls, err := renderer.Render(t, map[string]interface{}{}) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | out := []byte(tpls["kubecrt/charts.yml"]) 58 | 59 | if err = yaml.Unmarshal(out, m); err != nil { 60 | return nil, wrapError(out, err) 61 | } 62 | 63 | for _, a := range m.ChartsMap { 64 | for loc, c := range a { 65 | c.Location = loc 66 | m.ChartsList = append(m.ChartsList, c) 67 | } 68 | } 69 | 70 | return m, nil 71 | } 72 | 73 | // ParseCharts loops through all charts, and returns the parsed resources. 74 | func (cc *ChartsConfiguration) ParseCharts() ([]byte, error) { 75 | var out []byte 76 | 77 | for _, c := range cc.ChartsList { 78 | resources, err := c.ParseChart(cc.Name, cc.Namespace) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | out = append(out, resources...) 84 | } 85 | 86 | return out, nil 87 | } 88 | 89 | // Validate makes sure the charts configuration is configured as expected. 90 | func (cc *ChartsConfiguration) Validate() error { 91 | if cc.APIVersion == "" { 92 | return errors.New("Missing API version, please add \"apiVersion: v1\"") 93 | } 94 | 95 | if cc.APIVersion != "v1" { 96 | return errors.New("Unknown API version, please set apiVersion to \"v1\"") 97 | } 98 | 99 | if cc.Name == "" { 100 | return errors.New("Missing name, please add \"name: my-app-name\" or pass \"--name=my-app-name\"") 101 | } 102 | 103 | if cc.Namespace == "" { 104 | return errors.New("Missing namespace, please add \"namespace: my-namespace\" or pass \"--namespace=my-namespace\"") 105 | } 106 | 107 | if len(cc.ChartsList) == 0 { 108 | return errors.New("Missing charts, you need to define at least one chart") 109 | } 110 | 111 | for _, c := range cc.ChartsList { 112 | if c.Location == "" { 113 | return errors.New("Invalid or missing chart name") 114 | } 115 | 116 | if c.Version != "" { 117 | if _, err := semver.NewConstraint(c.Version); err != nil { 118 | return errors.New(c.Version + ": " + err.Error()) 119 | } 120 | } 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func stubChart(b []byte, partialPath string) (*hchart.Chart, error) { 127 | tpls, err := loadTemplates(b, partialPath) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | chart := &hchart.Chart{ 133 | Metadata: &hchart.Metadata{ 134 | Name: "kubecrt", 135 | }, 136 | Templates: tpls, 137 | } 138 | 139 | return chart, nil 140 | } 141 | 142 | func loadTemplates(b []byte, partialPath string) ([]*hchart.Template, error) { 143 | tpls := []*hchart.Template{{Data: b, Name: "charts.yml"}} 144 | 145 | if partialPath == config.DefaultPartialTemplatesPath { 146 | if _, err := os.Stat(partialPath); os.IsNotExist(err) { 147 | return tpls, nil 148 | } 149 | } 150 | 151 | if partialPath == "" { 152 | return tpls, nil 153 | } 154 | 155 | err := filepath.Walk(partialPath, func(path string, f os.FileInfo, err error) error { 156 | if info, e := os.Stat(path); e == nil && info.IsDir() { 157 | return nil 158 | } 159 | 160 | content, err := ioutil.ReadFile(path) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | tpls = append(tpls, &hchart.Template{Data: content, Name: strings.Replace(path, partialPath, "", 1)}) 166 | return nil 167 | }) 168 | 169 | return tpls, err 170 | } 171 | 172 | func wrapError(b []byte, err error) error { 173 | var lines []string 174 | shortLines := []string{"\n"} 175 | 176 | el, str := errorLine(err) 177 | 178 | scanner := bufio.NewScanner(bytes.NewReader(b)) 179 | l := 0 180 | for scanner.Scan() { 181 | l++ 182 | } 183 | ll := len(strconv.Itoa(l)) 184 | 185 | i := 1 186 | scanner = bufio.NewScanner(bytes.NewReader(b)) 187 | for scanner.Scan() { 188 | line := fmt.Sprintf("%*d: %s", ll, i, scanner.Text()) 189 | 190 | // if we know the error line, we create an extra summary of the context 191 | // surrounding the error itself, starting 3 lines before, ending 3 after. 192 | if el != 0 { 193 | if i == el { 194 | line = "\x1b[31;1m" + line + "\x1b[0m" 195 | } 196 | 197 | if (i >= el-3) && (i <= el+3) { 198 | shortLines = append(shortLines, line) 199 | } 200 | } 201 | 202 | lines = append(lines, line) 203 | i++ 204 | } 205 | 206 | lines = append(lines, shortLines...) 207 | lines = append(lines, "\n"+str) 208 | 209 | return errors.New(strings.Join(lines, "\n")) 210 | } 211 | 212 | func errorLine(err error) (int, string) { 213 | var i int 214 | var p []string 215 | str := err.Error() 216 | 217 | println(str) 218 | 219 | if strings.HasPrefix(str, "yaml: ") { 220 | p = strings.SplitN(str, ":", 3) 221 | i, _ = strconv.Atoi(strings.Replace(p[1], " line ", "", -1)) 222 | str = strings.TrimSpace(p[2]) 223 | } 224 | 225 | if strings.HasPrefix(str, "template: test:") { 226 | p = strings.SplitN(str, ":", 4) 227 | i, _ = strconv.Atoi(p[2]) 228 | str = strings.TrimSpace(p[3]) 229 | } 230 | 231 | return i, "Templating error: " + str 232 | } 233 | -------------------------------------------------------------------------------- /config/cli.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | docopt "github.com/docopt/docopt-go" 8 | ) 9 | 10 | var version = "unknown" 11 | var gitrev = "unknown" 12 | 13 | const usage = `kubecrt - convert Helm charts to Kubernetes resources 14 | 15 | Given a charts.yml file, compile all the charts with 16 | the provided template variables and return the 17 | resulting Kubernetes resource files to STDOUT, or 18 | write them to a provided file. 19 | 20 | Doing this, you can use Kubernetes charts, without 21 | having to use Helm locally, or Tiller on the server. 22 | 23 | Usage: 24 | kubecrt [options] CHARTS_CONFIG 25 | kubecrt -h | --help 26 | kubecrt --version 27 | kubecrt --example-config 28 | 29 | Where CHARTS_CONFIG is the location of the YAML file 30 | containing the Kubernetes Charts configuration. 31 | 32 | Arguments: 33 | CHARTS_CONFIG Charts configuration file 34 | 35 | Options: 36 | -h, --help Show this screen 37 | --version Show version 38 | -n NS, --namespace=NS Set the .Release.Namespace chart variable, 39 | used by charts during compilation 40 | -a NAME, --name=NAME Set the .Release.Name chart variable, used by 41 | charts during compilation 42 | -o PATH, --output=PATH Write output to a file, instead of STDOUT 43 | -r NAME=URL, --repo=NAME=URL,... List of NAME=URL pairs of repositories to add 44 | to the index before compiling charts config 45 | -p DIR, --partials-dir=DIR Path from which to load partial templates 46 | [default: config/deploy/partials] 47 | -j, --json Print resources formatted as JSON instead of 48 | YAML. Each resource is printed on a single 49 | line. 50 | --example-config Print an example charts.yaml, including 51 | extended documentation on the tunables 52 | ` 53 | 54 | // CLI returns the parsed command-line arguments 55 | func CLI() map[string]interface{} { 56 | arguments, err := docopt.Parse(usage, nil, true, "kubecrt "+version+" ("+gitrev+")", true) 57 | if err != nil { 58 | panic(err) 59 | } 60 | 61 | if arguments["--example-config"].(bool) { 62 | generateExampleConfig() 63 | os.Exit(0) 64 | } 65 | 66 | return arguments 67 | } 68 | 69 | func generateExampleConfig() { 70 | fmt.Println(docs) 71 | } 72 | 73 | const docs = ` 74 | # apiVersion defines the version of the charts.yaml structure. Currently, 75 | # only "v1" is supported. 76 | apiVersion: v1 77 | 78 | # name is the .Release.Name template value that charts can use in their 79 | # templates, which can be overridden by the "--name" CLI flag. If omitted, 80 | # "--name" is required. 81 | name: my-bundled-apps 82 | 83 | # namespace is the .Release.Namespace template value that charts can use in 84 | # their templates. Note that since kubecrt does not communicate with 85 | # Kubernetes in any way, it is up to you to also use this namespace when 86 | # doing kubectl apply [...]. Can be overridden using "--namespace". If omitted, 87 | # "--namespace" is required. 88 | namespace: apps 89 | 90 | # charts is an array of charts you want to compile into Kubernetes resource 91 | # files. 92 | # 93 | # A single chart might be used to deploy something simple, like a memcached pod, 94 | # or something complex, like a full web app stack with HTTP servers, databases, 95 | # caches, and so on. 96 | charts: 97 | 98 | # A Chart can either be in the format REPO/NAME, or a PATH to a local chart. 99 | # 100 | # If using REPO/NAME, kubecrt knows by-default where to locate the "stable" 101 | # repository, all other repositories require the "repo" configuration (see 102 | # below). 103 | - stable/factorio: 104 | # values is a map of key/value pairs used when compiling the chart. This 105 | # uses the same format as in regular chart "values.yaml" files. 106 | # 107 | # see: https://git.io/v9Tyr 108 | values: 109 | resources: 110 | requests: 111 | memory: 1024Mi 112 | cpu: 750m 113 | factorioServer: 114 | # charts.yaml supports the same templating as chart templates do, 115 | # using the "sprig" library. 116 | # 117 | # see: https://masterminds.github.io/sprig/ 118 | name: {{ env "MY_SERVER_NAME" | default "hello world!" }} 119 | 120 | - stable/minecraft: 121 | # version is a semantic version constraint. 122 | # 123 | # see: https://github.com/Masterminds/semver#basic-comparisons 124 | version: ~> 0.1.0 125 | values: 126 | minecraftServer: 127 | difficulty: hard 128 | 129 | - opsgoodness/prometheus-operator: 130 | # repo is the location of a repositry, if other than "stable". This is 131 | # the URL you would normally add using "helm repo add NAME URL". 132 | repo: http://charts.opsgoodness.com 133 | values: 134 | sendAnalytics: false 135 | 136 | # For the above charts, see here for the default configurations: 137 | # 138 | # * stable/factorio: https://git.io/v9Tyr 139 | # * stable/minecraft: https://git.io/v9Tya 140 | # * opsgoodness/prometheus-operator: https://git.io/v9SAY 141 | ` 142 | -------------------------------------------------------------------------------- /config/options.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "errors" 4 | 5 | // DefaultPartialTemplatesPath is the default path used for partials. 6 | const DefaultPartialTemplatesPath = "config/deploy/partials" 7 | 8 | // CLIOptions contains all the options set through the CLI arguments 9 | type CLIOptions struct { 10 | ChartsConfigurationPath string 11 | PartialTemplatesPath string 12 | ChartsConfigurationOptions *ChartsConfigurationOptions 13 | OutputJSON bool 14 | } 15 | 16 | // ChartsConfigurationOptions contains the CLI options relevant for the charts 17 | // configuration. 18 | type ChartsConfigurationOptions struct { 19 | Name string 20 | Namespace string 21 | } 22 | 23 | // NewCLIOptions takes CLI arguments, and returns a CLIOptions struct. 24 | func NewCLIOptions(cli map[string]interface{}) (*CLIOptions, error) { 25 | path, ok := cli["CHARTS_CONFIG"].(string) 26 | if !ok { 27 | return nil, errors.New("Invalid argument: CHARTS_CONFIG") 28 | } 29 | 30 | name, _ := cli["--name"].(string) 31 | namespace, _ := cli["--namespace"].(string) 32 | 33 | c := &CLIOptions{ 34 | OutputJSON: cli["--json"].(bool), 35 | ChartsConfigurationPath: path, 36 | ChartsConfigurationOptions: &ChartsConfigurationOptions{ 37 | Name: name, 38 | Namespace: namespace, 39 | }, 40 | } 41 | 42 | if cli["--partials-dir"] != nil { 43 | c.PartialTemplatesPath, _ = cli["--partials-dir"].(string) 44 | } 45 | 46 | return c, nil 47 | } 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/blendle/kubecrt 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 // indirect 7 | github.com/Masterminds/goutils v1.1.0 // indirect 8 | github.com/Masterminds/semver v1.4.2 9 | github.com/Masterminds/sprig v2.18.0+incompatible // indirect 10 | github.com/cyphar/filepath-securejoin v0.2.2 // indirect 11 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 12 | github.com/ghodss/yaml v1.0.0 13 | github.com/gobwas/glob v0.2.3 // indirect 14 | github.com/golang/protobuf v1.3.1 // indirect 15 | github.com/huandu/xstrings v1.2.0 // indirect 16 | github.com/imdario/mergo v0.3.7 // indirect 17 | github.com/pkg/errors v0.8.1 // indirect 18 | github.com/spf13/pflag v1.0.3 // indirect 19 | github.com/stretchr/testify v1.3.0 // indirect 20 | golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f // indirect 21 | gopkg.in/yaml.v2 v2.2.2 22 | k8s.io/apimachinery v0.0.0-20190515023456-b74e4c97951f // indirect 23 | k8s.io/client-go v11.0.0+incompatible // indirect 24 | k8s.io/helm v2.14.0+incompatible 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= 4 | github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 5 | github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= 6 | github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 7 | github.com/Masterminds/sprig v2.18.0+incompatible h1:QoGhlbC6pter1jxKnjMFxT8EqsLuDE6FEcNbWEpw+lI= 8 | github.com/Masterminds/sprig v2.18.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 9 | github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= 10 | github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 15 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= 16 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 17 | github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 18 | github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 19 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 20 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 21 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 22 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 23 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 24 | github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 25 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 26 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 28 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 29 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 30 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 31 | github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= 32 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 33 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 34 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 35 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 36 | github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= 37 | github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= 38 | github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= 39 | github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 40 | github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 41 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 42 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 43 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 44 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 45 | github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 46 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 47 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 51 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 52 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 53 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 54 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 55 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 56 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 57 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 58 | golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo= 59 | golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 60 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 61 | golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 62 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 63 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 64 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 65 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 66 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 67 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 70 | golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 71 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 72 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 73 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 74 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 75 | gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 76 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 77 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 78 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 79 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 80 | k8s.io/apimachinery v0.0.0-20190515023456-b74e4c97951f h1:cBrF1gFrJrvimOHZzyEHrvtlfqPV+KM7QZt3M0mepEg= 81 | k8s.io/apimachinery v0.0.0-20190515023456-b74e4c97951f/go.mod h1:Ew3b/24/JSgJdn4RsnrLskv3LvMZDlZ1Fl1xopsJftY= 82 | k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o= 83 | k8s.io/client-go v11.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= 84 | k8s.io/helm v2.14.0+incompatible h1:iMrI+LFgJWBCps9yNbHlNKpizYwvRTiH1kt6ZiNDXIU= 85 | k8s.io/helm v2.14.0+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= 86 | k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 87 | k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= 88 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 89 | -------------------------------------------------------------------------------- /helm/helm.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | import ( 4 | "k8s.io/helm/pkg/helm/environment" 5 | "k8s.io/helm/pkg/helm/helmpath" 6 | ) 7 | 8 | var settings environment.EnvSettings 9 | 10 | func init() { 11 | settings.Home = helmpath.Home(environment.DefaultHelmHome) 12 | } 13 | -------------------------------------------------------------------------------- /helm/init.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | 8 | "k8s.io/helm/pkg/getter" 9 | "k8s.io/helm/pkg/helm/helmpath" 10 | "k8s.io/helm/pkg/repo" 11 | ) 12 | 13 | const ( 14 | stableRepository = "stable" 15 | stableRepositoryURL = "https://charts.helm.sh/stable" 16 | ) 17 | 18 | // Init makes sure the Helm home path exists and the required subfolders. 19 | func Init() error { 20 | if err := ensureDirectories(settings.Home); err != nil { 21 | return err 22 | } 23 | 24 | if err := ensureDefaultRepos(settings.Home); err != nil { 25 | return err 26 | } 27 | 28 | if err := ensureRepoFileFormat(settings.Home.RepositoryFile()); err != nil { 29 | return err 30 | } 31 | 32 | if err := ensureUpdatedRepos(settings.Home); err != nil { 33 | return err 34 | } 35 | 36 | return nil 37 | } 38 | 39 | func ensureDirectories(home helmpath.Home) error { 40 | configDirectories := []string{ 41 | home.String(), 42 | home.Repository(), 43 | home.Cache(), 44 | home.Plugins(), 45 | home.Starters(), 46 | } 47 | 48 | for _, p := range configDirectories { 49 | if fi, err := os.Stat(p); err != nil { 50 | if err := os.MkdirAll(p, 0755); err != nil { 51 | return fmt.Errorf("Could not create %s: %s", p, err) 52 | } 53 | } else if !fi.IsDir() { 54 | return fmt.Errorf("%s must be a directory", p) 55 | } 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func ensureRepoFileFormat(file string) error { 62 | r, err := repo.LoadRepositoriesFile(file) 63 | if err == repo.ErrRepoOutOfDate { 64 | if err := r.WriteFile(file, 0644); err != nil { 65 | return err 66 | } 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func ensureDefaultRepos(home helmpath.Home) error { 73 | repoFile := home.RepositoryFile() 74 | if fi, err := os.Stat(repoFile); err != nil { 75 | f := repo.NewRepoFile() 76 | sr, err := initStableRepo(home.CacheIndex(stableRepository)) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | f.Add(sr) 82 | 83 | if err := f.WriteFile(repoFile, 0644); err != nil { 84 | return err 85 | } 86 | } else if fi.IsDir() { 87 | return fmt.Errorf("%s must be a file, not a directory", repoFile) 88 | } 89 | return nil 90 | } 91 | 92 | func ensureUpdatedRepos(home helmpath.Home) error { 93 | f, err := repo.LoadRepositoriesFile(home.RepositoryFile()) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | if len(f.Repositories) == 0 { 99 | return nil 100 | } 101 | 102 | var repos []*repo.ChartRepository 103 | for _, cfg := range f.Repositories { 104 | r, err := repo.NewChartRepository(cfg, getter.All(settings)) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | repos = append(repos, r) 110 | } 111 | 112 | updateCharts(repos, home) 113 | return nil 114 | } 115 | 116 | func initStableRepo(cacheFile string) (*repo.Entry, error) { 117 | c := repo.Entry{ 118 | Name: stableRepository, 119 | URL: stableRepositoryURL, 120 | Cache: cacheFile, 121 | } 122 | r, err := repo.NewChartRepository(&c, getter.All(settings)) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | // In this case, the cacheFile is always absolute. So passing empty string 128 | // is safe. 129 | if err := r.DownloadIndexFile(""); err != nil { 130 | return nil, fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", stableRepositoryURL, err.Error()) 131 | } 132 | 133 | return &c, nil 134 | } 135 | 136 | func updateCharts(repos []*repo.ChartRepository, home helmpath.Home) { 137 | var wg sync.WaitGroup 138 | for _, re := range repos { 139 | wg.Add(1) 140 | go func(re *repo.ChartRepository) { 141 | defer wg.Done() 142 | if re.Config.Name == "local" { 143 | return 144 | } 145 | _ = re.DownloadIndexFile(home.Cache()) 146 | }(re) 147 | } 148 | wg.Wait() 149 | } 150 | -------------------------------------------------------------------------------- /helm/repo_add.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | import ( 4 | "fmt" 5 | 6 | "k8s.io/helm/pkg/getter" 7 | "k8s.io/helm/pkg/repo" 8 | ) 9 | 10 | // AddRepository adds a new repository to the Helm index. 11 | func AddRepository(name, url string) error { 12 | home := settings.Home 13 | 14 | f, err := repo.LoadRepositoriesFile(home.RepositoryFile()) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | cif := home.CacheIndex(name) 20 | c := repo.Entry{ 21 | Name: name, 22 | Cache: cif, 23 | URL: url, 24 | CertFile: "", 25 | KeyFile: "", 26 | CAFile: "", 27 | } 28 | 29 | r, err := repo.NewChartRepository(&c, getter.All(settings)) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | if err := r.DownloadIndexFile(home.Cache()); err != nil { 35 | return fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", url, err.Error()) 36 | } 37 | 38 | f.Update(&c) 39 | 40 | return f.WriteFile(home.RepositoryFile(), 0644) 41 | } 42 | -------------------------------------------------------------------------------- /helm/version.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Masterminds/semver" 7 | 8 | "k8s.io/helm/cmd/helm/search" 9 | "k8s.io/helm/pkg/repo" 10 | ) 11 | 12 | // GetAcceptableVersion accepts a SemVer constraint, and finds the best matching 13 | // chart version. 14 | func GetAcceptableVersion(name, constraint string) (string, error) { 15 | index, err := buildIndex() 16 | if err != nil { 17 | return "", err 18 | } 19 | 20 | var res []*search.Result 21 | res, err = index.Search(name, 10, false) 22 | if err != nil { 23 | return "", err 24 | } 25 | 26 | if len(res) == 0 { 27 | return "", fmt.Errorf(`unable to find chart named "%s"`, name) 28 | } 29 | 30 | search.SortScore(res) 31 | 32 | if constraint != "" { 33 | res, err = applyConstraint(constraint, res) 34 | if err != nil { 35 | return "", err 36 | } 37 | } 38 | 39 | return res[0].Chart.Version, nil 40 | } 41 | 42 | func buildIndex() (*search.Index, error) { 43 | rf, err := repo.LoadRepositoriesFile(settings.Home.RepositoryFile()) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | i := search.NewIndex() 49 | for _, re := range rf.Repositories { 50 | n := re.Name 51 | f := settings.Home.CacheIndex(n) 52 | ind, err := repo.LoadIndexFile(f) 53 | if err != nil { 54 | continue 55 | } 56 | 57 | i.AddRepo(n, ind, true) 58 | } 59 | return i, nil 60 | } 61 | 62 | func applyConstraint(version string, res []*search.Result) ([]*search.Result, error) { 63 | constraint, err := semver.NewConstraint(version) 64 | if err != nil { 65 | return res, fmt.Errorf("invalid chart version/constraint format: %s", err) 66 | } 67 | 68 | data := res[:0] 69 | for _, r := range res { 70 | v, err := semver.NewVersion(r.Chart.Version) 71 | if err != nil || constraint.Check(v) { 72 | data = append(data, r) 73 | } 74 | } 75 | 76 | if len(data) == 0 { 77 | return data, fmt.Errorf("unable to fulfil chart version constraint %s", version) 78 | } 79 | 80 | return data, nil 81 | } 82 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strings" 9 | 10 | "github.com/blendle/kubecrt/chartsconfig" 11 | "github.com/blendle/kubecrt/config" 12 | "github.com/blendle/kubecrt/helm" 13 | "github.com/ghodss/yaml" 14 | ) 15 | 16 | func main() { 17 | cli := config.CLI() 18 | opts, err := config.NewCLIOptions(cli) 19 | if err != nil { 20 | fmt.Fprintf(os.Stderr, "kubecrt arguments error: \n\n%s\n", err) 21 | os.Exit(1) 22 | } 23 | 24 | if err = helm.Init(); err != nil { 25 | fmt.Fprintf(os.Stderr, "error initialising helm: \n\n%s\n", err) 26 | os.Exit(1) 27 | } 28 | 29 | if cli["--repo"] != nil { 30 | for _, r := range strings.Split(cli["--repo"].(string), ",") { 31 | p := strings.SplitN(r, "=", 2) 32 | repo := strings.TrimSpace(string(p[0])) 33 | url := strings.TrimSpace(string(p[1])) 34 | 35 | if err = helm.AddRepository(repo, url); err != nil { 36 | fmt.Fprintf(os.Stderr, "error adding repository: \n\n%s\n", err) 37 | os.Exit(1) 38 | } 39 | } 40 | } 41 | 42 | cfg, err := readInput(opts.ChartsConfigurationPath) 43 | if err != nil { 44 | fmt.Fprintf(os.Stderr, "charts config IO error: \n\n%s\n", err) 45 | os.Exit(1) 46 | } 47 | 48 | cc, err := chartsconfig.NewChartsConfiguration(cfg, opts.PartialTemplatesPath) 49 | if err != nil { 50 | fmt.Fprintf(os.Stderr, "charts config parsing error: \n\n%s\n", err) 51 | os.Exit(1) 52 | } 53 | 54 | name := opts.ChartsConfigurationOptions.Name 55 | if name != "" { 56 | cc.Name = name 57 | } 58 | 59 | namespace := opts.ChartsConfigurationOptions.Namespace 60 | if namespace != "" { 61 | cc.Namespace = namespace 62 | } 63 | 64 | if err = cc.Validate(); err != nil { 65 | fmt.Fprintf(os.Stderr, "charts validation error: \n\n%s\n", err) 66 | os.Exit(1) 67 | } 68 | 69 | out, err := cc.ParseCharts() 70 | if err != nil { 71 | fmt.Fprintf(os.Stderr, "chart parsing error: %s\n", err) 72 | os.Exit(1) 73 | } 74 | 75 | if opts.OutputJSON { 76 | out, err = toJSON(out) 77 | if err != nil { 78 | fmt.Printf("error converting chart to JSON format: %s\n", err) 79 | os.Exit(1) 80 | } 81 | } 82 | 83 | if cli["--output"] == nil { 84 | fmt.Print(string(out)) 85 | return 86 | } 87 | 88 | if err = ioutil.WriteFile(cli["--output"].(string), out, 0644); err != nil { 89 | fmt.Fprintf(os.Stderr, "output IO error: %s\n", err) 90 | os.Exit(1) 91 | } 92 | } 93 | 94 | func readInput(input string) ([]byte, error) { 95 | if input == "-" { 96 | return ioutil.ReadAll(os.Stdin) 97 | } 98 | 99 | return ioutil.ReadFile(input) 100 | } 101 | 102 | func toJSON(input []byte) ([]byte, error) { 103 | var err error 104 | 105 | bs := bytes.Split(input, []byte("---")) 106 | for i := range bs { 107 | if len(bs[i]) == 0 { 108 | continue 109 | } 110 | 111 | bs[i], err = yaml.YAMLToJSON(bs[i]) 112 | if err != nil { 113 | return nil, err 114 | } 115 | } 116 | 117 | return bytes.Join(bs, []byte("\n")), nil 118 | } 119 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script/bootstrap: bootstrap application for first usage 4 | 5 | set -e 6 | 7 | cd "$(dirname "$0")/.." 8 | 9 | glide install --force --strip-vendor --skip-test 10 | 11 | rm -rf ./vendor/k8s.io/{apiserver,apimachinery,client-go} 12 | 13 | cp -r ./vendor/k8s.io/kubernetes/staging/src/k8s.io/{apiserver,apimachinery,client-go} ./vendor/k8s.io 14 | --------------------------------------------------------------------------------