├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── DCO ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── README.md ├── action ├── action.go ├── action_test.go ├── create.go ├── create_test.go ├── doc.go ├── doctor.go ├── doctor_test.go ├── edit.go ├── edit_test.go ├── fetch.go ├── fetch_test.go ├── generate.go ├── generate_test.go ├── helper_test.go ├── info.go ├── info_test.go ├── install.go ├── install_test.go ├── lint.go ├── lint_test.go ├── list.go ├── plugin.go ├── plugin_test.go ├── print_readme.go ├── publish.go ├── remove.go ├── remove_test.go ├── repo.go ├── repo_test.go ├── search.go ├── search_test.go ├── target.go ├── target_test.go ├── template.go ├── template_test.go ├── uninstall.go ├── uninstall_test.go ├── update.go └── update_test.go ├── chart ├── chart.go ├── chart_test.go ├── chartfile.go └── chartfile_test.go ├── cli ├── create.go ├── doctor.go ├── edit.go ├── fetch.go ├── generate.go ├── helm.go ├── home.go ├── info.go ├── install.go ├── lint.go ├── lint_test.go ├── list.go ├── publish.go ├── remove.go ├── repository.go ├── search.go ├── target.go ├── template.go ├── uninstall.go ├── update.go └── util.go ├── codec ├── codec.go ├── json.go ├── json_test.go ├── object.go ├── object_test.go ├── yaml.go └── yaml_test.go ├── config ├── config.go └── config_test.go ├── dependency ├── dependency.go └── dependency_test.go ├── docs ├── README.md ├── architecture.md ├── authoring_charts.md ├── awesome.md ├── chart_tables.md ├── generate-and-template.md ├── index.md ├── modeling_services.md ├── plugins.md ├── using_labels.md └── workspace.md ├── generator ├── generator.go └── generator_test.go ├── glide.lock ├── glide.yaml ├── helmc.go ├── kubectl ├── apply.go ├── apply_test.go ├── cluster_info.go ├── command.go ├── create.go ├── create_test.go ├── delete.go ├── get.go ├── get_test.go ├── kubectl.go └── kubectl_test.go ├── log └── log.go ├── manifest ├── manifest.go └── manifest_test.go ├── mkdocs.yml ├── plugins ├── example │ ├── README.md │ └── helm-example.go └── sec │ ├── README.md │ └── helm-sec.go ├── release ├── latest.go └── latest_test.go ├── search ├── search.go └── search_test.go ├── test └── helm.go ├── testdata ├── Configfile.yaml ├── README.md ├── cache │ └── charts │ │ └── README.md ├── charts │ ├── dep1 │ │ └── Chart.yaml │ ├── dep2 │ │ └── Chart.yaml │ ├── dep3 │ │ └── Chart.yaml │ ├── deptest │ │ └── Chart.yaml │ ├── generate │ │ ├── Chart.yaml │ │ ├── ignore │ │ │ └── ignoreme.yaml │ │ ├── manifests │ │ │ └── pod.yaml │ │ ├── tpl │ │ │ └── pod.tpl.yaml │ │ └── values.toml │ ├── keep │ │ ├── Chart.yaml │ │ └── manifests │ │ │ └── keep-ns.yaml │ ├── kitchensink │ │ ├── Chart.yaml │ │ ├── annoy_testers │ │ │ ├── README.md │ │ │ └── broken.yaml │ │ └── manifests │ │ │ ├── foo.json │ │ │ ├── nested │ │ │ └── nested-pod.yaml │ │ │ ├── sink-configmap.yaml │ │ │ ├── sink-daemonset.yaml │ │ │ ├── sink-deployment.yaml │ │ │ ├── sink-limits.yaml │ │ │ ├── sink-namespace.yaml │ │ │ ├── sink-pod.yaml │ │ │ ├── sink-rc.yml │ │ │ ├── sink-secret.yaml │ │ │ ├── sink-server.yaml │ │ │ ├── sink-serviceaccount.yaml │ │ │ └── sink-volume.yaml │ ├── malformed │ │ ├── Chart.yaml │ │ └── manifests │ │ │ └── malformed.yaml │ ├── misnamed │ │ └── Chart.yaml │ ├── redis │ │ ├── Chart.yaml │ │ └── manifests │ │ │ └── redis-pod.yaml │ ├── searchtest1 │ │ ├── Chart.yaml │ │ └── manifests │ │ │ └── redis-pod.yaml │ └── searchtest2 │ │ ├── Chart.yaml │ │ └── manifests │ │ └── redis-pod.yaml ├── config.yaml ├── daemonset.yaml ├── deployment.yaml ├── generator │ ├── fail.txt │ ├── fail2.txt │ ├── four │ │ ├── five.txt │ │ └── four.txt │ ├── one.yaml │ ├── three.txt │ └── two.yaml ├── git ├── helm-plugin ├── horizontalpodautoscaler.yaml ├── ingress.yaml ├── job.yaml ├── kubectl ├── namespace.yaml ├── pod.yaml ├── policy.json ├── rc.yaml ├── service-keep.json ├── service.json ├── service.yaml ├── serviceaccount.yaml ├── template │ ├── one.json │ ├── one.toml │ ├── one.tpl │ └── one.yaml ├── test-Chart.yaml └── three-pods-and-three-services.yaml ├── util ├── helm.go ├── helm_test.go └── structure.go └── validation └── validation.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | vendor/ 3 | bin/ 4 | _dist/ 5 | _scripts/ci/bintray-ci.json 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | 3 | branches: 4 | only: 5 | - master 6 | - /^v?(?:[0-9]+\.){2}[0-9]+.*$/ 7 | 8 | cache: 9 | directories: 10 | - vendor 11 | 12 | sudo: required 13 | services: 14 | - docker 15 | 16 | install: 17 | - make bootstrap 18 | 19 | script: make build test 20 | 21 | before_deploy: make dist 22 | 23 | deploy: 24 | provider: gcs 25 | access-key-id: 26 | secure: "Uo3WTXHExASQaTJy8G8sskznB5RFz1MWFGzA8z6kqfbknP65Xo2T8cgWfHmSp+d0I8gfT9TjCSsWDqqwd4M19eeI8Vb65wa8qeFn9qQz2YTviCWa5bPHndNhY+cGt3Vb+on67sczAX+rQHEa6FYIjL4m/doKn1c0Mz+n4VERKxqA2KIUz7/pvBXQr/1CJULmsMLyz2FCtS6x3Stb2kvZxMqFe2sIBKUwEAx4vGW6VGBM9RbvA6zkot/NgHplBfnaHlGz2fqfx+iYxYTBcJRKI+tgantR/4Lx9NRAVobSHjfzSZsU6Iit2FEEUmQdsz1x4Dm2fRYbtmvOjGep11GXUeiRZeOdkyHwGcErcMBWozvLS+dOtBbXtfg3xklgrobg5jNi/obJfmpmUrSUIMutSubLNZpRxykYrz5yT97iQRmMYK20tvHSbZ8nWLcQJ7Ronk18BZ9cxE9fc5cGl6qGqmjTnr3VubJI9OiPqhgy06hBiKW2VLJhda1+hdp1UN3P9tavtrb3cFzFzgi7NyTs9VT86P7NMuLplLPqoG7ymIschHWF84N1uLu/21quLVy1xZAQxTbnNy0t4wElWhnUqhCY64cJ/v36Wql5ue4xvLP2we+SOTuwL7vAd2AlDgzXKQmDUcU+UIxxUAro7uZQ9i6eLyYjsSArWVSDT7OMZ3A=" 27 | secret-access-key: 28 | secure: "I9m+xpb4QDa3MBgfkrrDGRRqfTGb8hBtQCAALzsQ3JiWhatbOuQ1hnQxxM9vHmKdOygWRzn5pcWWcD4Ep/LCWRW4ClhQz7cOvkQeuOxpSFtuxAQZsC/Tfzebup4GdsneKXhGqTqcvLOqqHCSnUTO8Qe/vMMAZqmT+Hx1pH2JOD9JRlZK5wrWl5sDb6adgdOEQy9ewRsMfe32mpQHXwMTEVSTq33YAXCxuHu5GEQtANX6XoJ45vVa2ZzCYB5MjClFab+ooGHvqaaGr4EEGEO48yMwRtc55SHp3ZO7wY/NShTHsWS4cFtr5pM/fN4ACTCmVlSQSnknfACaXUm9EcOQbJWjQr158+NYxq9rKXRPq1g1EeVSRXu0qL247spumpgXY6SJ2uGQkx16KjP7/yPQOh2V3YKdoY9Mzlg2bkX0E1P+1PCRJFn1gQO9C3EClphRAEhhGVe24AHNKs2LPCfGUH4q4nw7Z1OuXbnVqL6Z6ykqmedFcxyFuSQXUnxkmkf06G/ZNTuQcYS/nRXMozVnhAIQSvmdT5D31hSoOxk0qsWYgh3YZPzgIkKrQ8qXrvIfQO47gxzBYMu6iQcoM51cKGoWeMnOP9jiTiduaezvZT+FwtZr7rGb79TkM1KFeq96obDS0eyqYMryW+vgiNqKdSJdbx+2p5wEFg4kaEZLoHw=" 29 | bucket: helm-classic 30 | local-dir: _dist 31 | on: 32 | branch: master 33 | acl: public-read 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | |![](https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/Warning.svg/156px-Warning.svg.png) | Helm Classic (the `helm/helm` repository) is **no longer actively developed** but will remain available until `kubernetes/helm` has stabilized. 4 | |---|---| 5 | 6 | Helm and [Deployment Manager](https://github.com/kubernetes/deployment-manager) 7 | have recently joined forces to make deploying and managing software on 8 | Kubernetes as simple as possible. The combined effort now lives in the Kubernetes GitHub organization at 9 | [kubernetes/helm](https://github.com/kubernetes/helm). 10 | 11 | We love getting Pull Requests (PRs) _for bug fixes only_. To make sure we keep code quality 12 | and consistency high, we do have some process in place. In a nutshell: 13 | 14 | - Code should follow Go coding standards and pass `go lint` and `go 15 | vet`. (These tools are automatically run on every pull request.) 16 | - PRs should contain a single ~~feature or~~ fix, and follow the conventions 17 | linked below. 18 | - Contributors must agree to the DCO 19 | - Every patch must be signed off by two core contributors (and this is 20 | made easier when the community at large weighs in on PRs, too). 21 | 22 | Helm Classic follows the contribution guidelines established by the Deis 23 | project. Please take a look at them. 24 | 25 | - [Deis Contribution Guidelines](https://github.com/deis/deis/blob/master/CONTRIBUTING.md) 26 | - [Details on commit messages](http://docs.deis.io/en/latest/contributing/standards/#commit-style-guide) 27 | 28 | Again, thanks for taking the time to contribute. We know the process 29 | isn't trivial, and we really appreciate your helping ensure that Helm Classic 30 | develops into a high quality tool. 31 | 32 | ## Interpreting the Labels in GitHub 33 | 34 | We use labels on GitHub to indicate the state of something. Here are a 35 | few of the more interesting labels. 36 | 37 | - Awaiting review: The PR is ready to be considered for merging. If a PR 38 | does not have this, we assume that it's a work in progress (even if 39 | the issue does not say WIP) 40 | - Proposal: something we're discussing, but haven't decided on 41 | - Enhancement: a feature or chore 42 | - Bug: a bug 43 | - Needs manual testing: one of the core team must manually test before 44 | LGTM 45 | - LGTM1: First LGTM (Looks Good To Me) 46 | - LGTM2: Second LGTM; this means a core contributor can merge it 47 | 48 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 1796 18th St, 6 | San Francisco, CA 94107 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | 12 | Developer's Certificate of Origin 1.1 13 | 14 | By making a contribution to this project, I certify that: 15 | 16 | (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Engine Yard, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Helm Classic Maintainers 2 | 3 | |![](https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/Warning.svg/156px-Warning.svg.png) | Helm Classic (the `helm/helm` repository) is **no longer actively developed** but will remain available until `kubernetes/helm` has stabilized. 4 | |---|---| 5 | 6 | Helm and [Deployment Manager](https://github.com/kubernetes/deployment-manager) 7 | have recently joined forces to make deploying and managing software on 8 | Kubernetes as simple as possible. The combined effort now lives in the Kubernetes GitHub organization at 9 | [kubernetes/helm](https://github.com/kubernetes/helm). 10 | 11 | This document serves to describe the leadership structure of the Helm Classic project, and to list the current 12 | project maintainers. _Maintainers of Helm Classic currently perform bug fixes and critical maintenance only._ 13 | 14 | # What is a maintainer? 15 | 16 | (Unabashedly stolen from the [Docker](https://github.com/docker/docker/blob/master/MAINTAINERS) project) 17 | 18 | There are different types of maintainers, with different responsibilities, but 19 | all maintainers have 3 things in common: 20 | 21 | 1. They share responsibility in the project's success. 22 | 2. They have made a long-term, recurring time investment to improve the project. 23 | 3. They spend that time doing whatever needs to be done, not necessarily what 24 | is the most interesting or fun. 25 | 26 | Maintainers are often under-appreciated, because their work is harder to appreciate. 27 | It's easy to appreciate a really cool and technically advanced feature. It's harder 28 | to appreciate the absence of bugs, the slow but steady improvement in stability, 29 | or the reliability of a release process. But those things distinguish a good 30 | project from a great one. 31 | 32 | # Helm Classic maintainers 33 | 34 | Helm Classic has two groups of maintainers: core and contributing. 35 | 36 | ## Core maintainers 37 | 38 | Core maintainers are knowledgeable about all areas of Helm Classic. Some maintainers work on Helm Classic (and Helm) 39 | full-time, although this is not a requirement. 40 | 41 | The duties of a core maintainer include: 42 | * Classify and respond to GitHub issues and review pull requests 43 | * ~~Help to shape the Helm Classic roadmap and lead efforts to accomplish roadmap milestones~~ 44 | * Participate actively in ~~feature development~~ and bug fixing 45 | * Answer questions and help users in Slack 46 | 47 | The current core maintainers of Helm Classic: 48 | * Matt Butcher - ([@technosophos](https://github.com/technosophos)) 49 | * Gabe Monroy - ([@gabrtv](https://github.com/gabrtv)) 50 | * Kent Rancourt - ([@krancour](https://github.com/krancour)) 51 | * Keerthan Reddy Mala - ([@kmala](https://github.com/kmala)) 52 | 53 | 54 | ### Pull requests 55 | 56 | No pull requests can be merged until at least one core maintainer signs off with an "LGTM" label. 57 | The other LGTM can come from either a core maintainer or contributing maintainer. A maintainer who 58 | creates a pull request should also be the one to merge it, after two LGTMs. 59 | 60 | ## Contributing maintainers 61 | 62 | Contributing maintainers may have deep knowledge about some but not necessarily all areas of Helm Classic. 63 | Core maintainers will enlist contributing maintainers to weigh in on issues, review pull 64 | requests, or join conversations as needed in their areas of expertise. 65 | 66 | The duties of a contributing maintainer are similar to those of a core maintainer, but may be 67 | scoped to relevant areas of the Helm Classic project. 68 | 69 | Contributing maintainers are defined in practice as those who have write access to the Helm Classic 70 | repository. All maintainers can review pull requests and add LGTM labels as appropriate. 71 | 72 | ## Becoming a maintainer 73 | 74 | The Helm Classic project will succeed exactly as its community thrives. It is essential that the breadth 75 | of potential Kubernetes users find Helm Classic useful enough to help it grow. If you use Helm Classic every day, 76 | we want you to help determine where the ship is steered. 77 | 78 | Generally, potential contributing maintainers are selected by the Helm Classic core maintainers based in 79 | part on the following criteria: 80 | * Sustained contributions to the project over a period of time 81 | * A willingness to help Helm Classic users on GitHub and in Slack 82 | * A friendly attitude! 83 | 84 | The Helm Classic core maintainers must agree in unison before inviting a community member to join as a 85 | contributing maintainer. 86 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPO_PATH := github.com/helm/helm-classic 2 | 3 | # The following variables describe the containerized development environment 4 | # and other build options 5 | BIN_DIR := bin 6 | DIST_DIR := _dist 7 | GO_PACKAGES := action chart config dependency log manifest release plugins/sec plugins/example codec 8 | MAIN_GO := helmc.go 9 | HELM_BIN := ${BIN_DIR}/helmc 10 | 11 | VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null)+$(shell git rev-parse --short HEAD) 12 | 13 | DEV_ENV_IMAGE := quay.io/deis/go-dev:0.17.0 14 | DEV_ENV_WORK_DIR := /go/src/${REPO_PATH} 15 | DEV_ENV_CMD := docker run --rm -v ${CURDIR}:${DEV_ENV_WORK_DIR} -w ${DEV_ENV_WORK_DIR} ${DEV_ENV_IMAGE} 16 | DEV_ENV_CMD_INT := docker run -it --rm -v ${CURDIR}:${DEV_ENV_WORK_DIR} -w ${DEV_ENV_WORK_DIR} ${DEV_ENV_IMAGE} 17 | LDFLAGS := "-X ${REPO_PATH}/cli.version=${VERSION}" 18 | 19 | PATH_WITH_HELM = PATH=${DEV_ENV_WORK_DIR}/${BIN_DIR}:$$PATH 20 | 21 | check-docker: 22 | @if [ -z $$(which docker) ]; then \ 23 | echo "Missing \`docker\` client which is required for development and testing"; \ 24 | exit 2; \ 25 | fi 26 | 27 | # Allow developers to step into the containerized development environment 28 | dev: check-docker 29 | ${DEV_ENV_CMD_INT} bash 30 | 31 | # Containerized dependency resolution 32 | bootstrap: check-docker 33 | ${DEV_ENV_CMD} glide install 34 | 35 | # Containerized build of the binary 36 | build: check-docker 37 | ${DEV_ENV_CMD} make native-build 38 | 39 | # Builds the binary for the native OS and architecture. 40 | # This can be run directly to compile for one's own system if desired. 41 | # It can also be run within a container (which will compile for Linux/64) using `make build` 42 | native-build: 43 | go build -o ${HELM_BIN} -ldflags ${LDFLAGS} ${MAIN_GO} 44 | 45 | # Containerized build of binaries for all supported OS and architectures 46 | build-all: check-docker 47 | ${DEV_ENV_CMD} gox -verbose \ 48 | -ldflags ${LDFLAGS} \ 49 | -os="linux darwin " \ 50 | -arch="amd64 386" \ 51 | -output="${DIST_DIR}/helmc-latest-{{.OS}}-{{.Arch}}" . 52 | ifdef TRAVIS_TAG 53 | ${DEV_ENV_CMD} gox -verbose -ldflags ${LDFLAGS} -os="linux darwin" -arch="amd64 386" -output="${DIST_DIR}/${TRAVIS_TAG}/helmc-${TRAVIS_TAG}-{{.OS}}-{{.Arch}}" . 54 | else 55 | ${DEV_ENV_CMD} gox -verbose -ldflags ${LDFLAGS} -os="linux darwin" -arch="amd64 386" -output="${DIST_DIR}/${VERSION}/helmc-${VERSION}-{{.OS}}-{{.Arch}}" . 56 | endif 57 | 58 | clean: 59 | rm -rf ${DIST_DIR} ${BIN_DIR} 60 | 61 | dist: build-all 62 | ${DEV_ENV_CMD} bash -c 'cd ${DIST_DIR} && find * -type d -exec zip -jr helmc-${VERSION}-{}.zip {} \;' 63 | 64 | install: 65 | install -d ${DESTDIR}/usr/local/bin/ 66 | install -m 755 ${HELM_BIN} ${DESTDIR}/usr/local/bin/helmc 67 | 68 | quicktest: 69 | ${DEV_ENV_CMD} bash -c '${PATH_WITH_HELM} go test -short ./ $(addprefix ./,${GO_PACKAGES})' 70 | 71 | test: test-style 72 | ${DEV_ENV_CMD} bash -c '${PATH_WITH_HELM} go test -v ./ $(addprefix ./,${GO_PACKAGES})' 73 | 74 | test-style: 75 | ${DEV_ENV_CMD} gofmt -e -l -s *.go ${GO_PACKAGES} 76 | @${DEV_ENV_CMD} bash -c 'gofmt -e -l -s *.go ${GO_PACKAGES} | read; if [ $$? == 0 ]; then echo "gofmt check failed."; exit 1; fi' 77 | @${DEV_ENV_CMD} bash -c 'for i in . ${GO_PACKAGES}; do golint $$i; done' 78 | @${DEV_ENV_CMD} bash -c 'for i in . ${GO_PACKAGES}; do go vet ${REPO_PATH}/$$i; done' 79 | 80 | .PHONY: check-docker \ 81 | dev \ 82 | bootstrap \ 83 | build \ 84 | native-build \ 85 | build-all \ 86 | clean \ 87 | dist \ 88 | install \ 89 | prep-bintray-json \ 90 | quicktest \ 91 | test \ 92 | test-style 93 | -------------------------------------------------------------------------------- /action/action.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/helm/helm-classic/config" 7 | "github.com/helm/helm-classic/log" 8 | helm "github.com/helm/helm-classic/util" 9 | ) 10 | 11 | const ( 12 | // Chartfile is the name of the YAML file that contains chart metadata. 13 | // One must exist inside the top level directory of every chart. 14 | Chartfile = "Chart.yaml" 15 | ) 16 | 17 | // mustConfig parses a config file or dies trying. 18 | func mustConfig(homedir string) *config.Configfile { 19 | rpath := filepath.Join(homedir, helm.Configfile) 20 | cfg, err := config.Load(rpath) 21 | if err != nil { 22 | log.Warn("Oops! Looks like we had some issues running your command! Running `helmc doctor` to ensure we have all the necessary prerequisites in place...") 23 | Doctor(homedir) 24 | cfg, err = config.Load(rpath) 25 | if err != nil { 26 | log.Die("Oops! Could not load %s. Error: %s", rpath, err) 27 | } 28 | log.Info("Continuing onwards and upwards!") 29 | } 30 | return cfg 31 | } 32 | -------------------------------------------------------------------------------- /action/action_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | type TestRunner struct { 4 | out []byte 5 | err error 6 | } 7 | 8 | func (r TestRunner) ClusterInfo() ([]byte, error) { 9 | return r.out, r.err 10 | } 11 | 12 | func (r TestRunner) Apply(stdin []byte, ns string) ([]byte, error) { 13 | return r.out, r.err 14 | } 15 | 16 | func (r TestRunner) Create(stdin []byte, ns string) ([]byte, error) { 17 | return r.out, r.err 18 | } 19 | 20 | func (r TestRunner) Delete(name, ktype, ns string) ([]byte, error) { 21 | return r.out, r.err 22 | } 23 | 24 | func (r TestRunner) Get(stdin []byte, ns string) ([]byte, error) { 25 | return r.out, r.err 26 | } 27 | -------------------------------------------------------------------------------- /action/create.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "text/template" 8 | 9 | "github.com/helm/helm-classic/chart" 10 | "github.com/helm/helm-classic/log" 11 | helm "github.com/helm/helm-classic/util" 12 | ) 13 | 14 | // readmeSkel is the template for the README.md 15 | const readmeSkel = `# {{.Name}} 16 | 17 | Describe your chart here. Link to upstream repositories, Docker images or any 18 | external documentation. 19 | 20 | If your application requires any specific configuration like Secrets, you may 21 | include that information here. 22 | ` 23 | 24 | // manifestSkel is an example manifest for a new chart 25 | const manifestSkel = `--- 26 | apiVersion: v1 27 | kind: Pod 28 | metadata: 29 | name: example-pod 30 | labels: 31 | heritage: helm 32 | spec: 33 | restartPolicy: Never 34 | containers: 35 | - name: example 36 | image: "alpine:3.2" 37 | command: ["/bin/sleep","9000"] 38 | ` 39 | 40 | // Create a chart 41 | // 42 | // - chartName being created 43 | // - homeDir is the helm home directory for the user 44 | func Create(chartName, homeDir string) { 45 | chart := newSkelChartfile(chartName) 46 | createWithChart(chart, chartName, homeDir) 47 | } 48 | 49 | func createWithChart(chart *chart.Chartfile, chartName, homeDir string) { 50 | chartDir := helm.WorkspaceChartDirectory(homeDir, chartName) 51 | 52 | // create directories 53 | if err := os.MkdirAll(filepath.Join(chartDir, "manifests"), 0755); err != nil { 54 | log.Die("Could not create %q: %s", chartDir, err) 55 | } 56 | 57 | // create Chartfile.yaml 58 | if err := chart.Save(filepath.Join(chartDir, Chartfile)); err != nil { 59 | log.Die("Could not create Chart.yaml: err", err) 60 | } 61 | 62 | // create README.md 63 | if err := createReadme(chartDir, chart); err != nil { 64 | log.Die("Could not create README.md: err", err) 65 | } 66 | 67 | // create example-pod 68 | if err := createExampleManifest(chartDir); err != nil { 69 | log.Die("Could not create example manifest: err", err) 70 | } 71 | 72 | log.Info("Created chart in %s", chartDir) 73 | } 74 | 75 | // newSkelChartfile populates a Chartfile struct with example data 76 | func newSkelChartfile(chartName string) *chart.Chartfile { 77 | return &chart.Chartfile{ 78 | Name: chartName, 79 | Home: "http://example.com/your/project/home", 80 | Version: "0.1.0", 81 | Description: "Provide a brief description of your application here.", 82 | Maintainers: []string{"Your Name "}, 83 | Details: "This section allows you to provide additional details about your application.\nProvide any information that would be useful to users at a glance.", 84 | } 85 | } 86 | 87 | // createReadme populates readmeSkel and saves to the chart directory 88 | func createReadme(chartDir string, c *chart.Chartfile) error { 89 | tmpl := template.Must(template.New("info").Parse(readmeSkel)) 90 | 91 | readmeFile, err := os.Create(filepath.Join(chartDir, "README.md")) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | return tmpl.Execute(readmeFile, c) 97 | } 98 | 99 | // createExampleManifest saves manifestSkel to the manifests directory 100 | func createExampleManifest(chartDir string) error { 101 | return ioutil.WriteFile(filepath.Join(chartDir, "manifests/example-pod.yaml"), []byte(manifestSkel), 0644) 102 | } 103 | -------------------------------------------------------------------------------- /action/create_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/helm/helm-classic/test" 8 | "github.com/helm/helm-classic/util" 9 | ) 10 | 11 | func TestCreate(t *testing.T) { 12 | tmpHome := test.CreateTmpHome() 13 | 14 | Create("mychart", tmpHome) 15 | 16 | // assert chartfile 17 | chartfile, err := ioutil.ReadFile(util.WorkspaceChartDirectory(tmpHome, "mychart/Chart.yaml")) 18 | if err != nil { 19 | t.Errorf("Could not read chartfile: %s", err) 20 | } 21 | actualChartfile := string(chartfile) 22 | expectedChartfile := `name: mychart 23 | home: http://example.com/your/project/home 24 | version: 0.1.0 25 | description: Provide a brief description of your application here. 26 | maintainers: 27 | - Your Name 28 | details: |- 29 | This section allows you to provide additional details about your application. 30 | Provide any information that would be useful to users at a glance. 31 | ` 32 | test.ExpectEquals(t, actualChartfile, expectedChartfile) 33 | 34 | // asset readme 35 | readme, err := ioutil.ReadFile(util.WorkspaceChartDirectory(tmpHome, "mychart/README.md")) 36 | if err != nil { 37 | t.Errorf("Could not read README.md: %s", err) 38 | } 39 | actualReadme := string(readme) 40 | expectedReadme := `# mychart 41 | 42 | Describe your chart here. Link to upstream repositories, Docker images or any 43 | external documentation. 44 | 45 | If your application requires any specific configuration like Secrets, you may 46 | include that information here. 47 | ` 48 | test.ExpectEquals(t, expectedReadme, actualReadme) 49 | 50 | // assert example manifest 51 | manifest, err := ioutil.ReadFile(util.WorkspaceChartDirectory(tmpHome, "mychart/manifests/example-pod.yaml")) 52 | if err != nil { 53 | t.Errorf("Could not read manifest: %s", err) 54 | } 55 | actualManifest := string(manifest) 56 | expectedManifest := `--- 57 | apiVersion: v1 58 | kind: Pod 59 | metadata: 60 | name: example-pod 61 | labels: 62 | heritage: helm 63 | spec: 64 | restartPolicy: Never 65 | containers: 66 | - name: example 67 | image: "alpine:3.2" 68 | command: ["/bin/sleep","9000"] 69 | ` 70 | test.ExpectEquals(t, actualManifest, expectedManifest) 71 | } 72 | -------------------------------------------------------------------------------- /action/doc.go: -------------------------------------------------------------------------------- 1 | // Package action provides implementations for each Helm Classic command. 2 | // 3 | // This is not intended to be a stand-alone library, as 4 | // many of the commands will write to output, and even os.Exit 5 | // when things go wrong. 6 | package action 7 | -------------------------------------------------------------------------------- /action/doctor.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "os/exec" 5 | 6 | "github.com/helm/helm-classic/log" 7 | helm "github.com/helm/helm-classic/util" 8 | ) 9 | 10 | // Doctor helps you see what's wrong with your Helm Classic setup 11 | func Doctor(home string) { 12 | log.Info("Checking things locally...") 13 | CheckLocalPrereqs(home) 14 | CheckKubePrereqs() 15 | 16 | log.Info("Everything looks good! Happy helming!") 17 | } 18 | 19 | // CheckAllPrereqs makes sure we have all the tools we need for overall 20 | // Helm Classic success 21 | func CheckAllPrereqs(home string) { 22 | CheckLocalPrereqs(home) 23 | CheckKubePrereqs() 24 | } 25 | 26 | // CheckKubePrereqs makes sure we have the tools necessary to interact 27 | // with a kubernetes cluster 28 | func CheckKubePrereqs() { 29 | ensureCommand("kubectl") 30 | } 31 | 32 | // CheckLocalPrereqs makes sure we have all the tools we need to work with 33 | // charts locally 34 | func CheckLocalPrereqs(home string) { 35 | helm.EnsureHome(home) 36 | ensureCommand("git") 37 | } 38 | 39 | func ensureCommand(command string) { 40 | if _, err := exec.LookPath(command); err != nil { 41 | log.Die("Could not find '%s' on $PATH: %s", command, err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /action/doctor_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/helm/helm-classic/test" 9 | "github.com/helm/helm-classic/util" 10 | ) 11 | 12 | func TestEnsurePrereqs(t *testing.T) { 13 | pp := os.Getenv("PATH") 14 | defer os.Setenv("PATH", pp) 15 | 16 | os.Setenv("PATH", filepath.Join(test.HelmRoot, "testdata")+":"+pp) 17 | 18 | homedir := test.CreateTmpHome() 19 | CheckAllPrereqs(homedir) 20 | } 21 | 22 | func TestEnsureHome(t *testing.T) { 23 | tmpHome := test.CreateTmpHome() 24 | util.EnsureHome(tmpHome) 25 | } 26 | -------------------------------------------------------------------------------- /action/edit.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | 7 | "github.com/helm/helm-classic/log" 8 | "github.com/helm/helm-classic/util" 9 | ) 10 | 11 | // Edit charts using the shell-defined $EDITOR 12 | // 13 | // - chartName being edited 14 | // - homeDir is the Helm Classic home directory for the user 15 | func Edit(chartName, homeDir string) { 16 | 17 | chartDir := util.WorkspaceChartDirectory(homeDir, chartName) 18 | 19 | if _, err := os.Stat(chartDir); os.IsNotExist(err) { 20 | log.Die("Could not find chart: %s", chartName) 21 | } 22 | 23 | openEditor(chartDir) 24 | } 25 | 26 | // openEditor opens the given filename in an interactive editor 27 | func openEditor(path string) { 28 | editor := os.Getenv("EDITOR") 29 | if editor == "" { 30 | log.Die("must set shell $EDITOR") 31 | } 32 | 33 | editorPath, err := exec.LookPath(editor) 34 | if err != nil { 35 | log.Die("Could not find %s in PATH", editor) 36 | } 37 | 38 | cmd := exec.Command(editorPath, path) 39 | cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr 40 | if err := cmd.Run(); err != nil { 41 | log.Die("Could not open $EDITOR: %s", err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /action/edit_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/helm/helm-classic/test" 9 | ) 10 | 11 | func TestEdit(t *testing.T) { 12 | editor := os.Getenv("EDITOR") 13 | os.Setenv("EDITOR", "echo") 14 | defer os.Setenv("EDITOR", editor) 15 | 16 | tmpHome := test.CreateTmpHome() 17 | defer os.RemoveAll(tmpHome) 18 | test.FakeUpdate(tmpHome) 19 | 20 | Fetch("redis", "", tmpHome) 21 | 22 | expected := path.Join(tmpHome, "workspace/charts/redis") 23 | actual := test.CaptureOutput(func() { 24 | Edit("redis", tmpHome) 25 | }) 26 | 27 | test.ExpectContains(t, actual, expected) 28 | } 29 | -------------------------------------------------------------------------------- /action/fetch.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/helm/helm-classic/chart" 8 | "github.com/helm/helm-classic/dependency" 9 | "github.com/helm/helm-classic/log" 10 | helm "github.com/helm/helm-classic/util" 11 | ) 12 | 13 | // Fetch gets a chart from the source repo and copies to the workdir. 14 | // 15 | // - chartName is the source 16 | // - lname is the local name for that chart (chart-name); if blank, it is set to the chart. 17 | // - homedir is the home directory for the user 18 | func Fetch(chartName, lname, homedir string) { 19 | 20 | r := mustConfig(homedir).Repos 21 | repository, chartName := r.RepoChart(chartName) 22 | 23 | if lname == "" { 24 | lname = chartName 25 | } 26 | 27 | fetch(chartName, lname, homedir, repository) 28 | 29 | chartFilePath := helm.WorkspaceChartDirectory(homedir, lname, Chartfile) 30 | cfile, err := chart.LoadChartfile(chartFilePath) 31 | if err != nil { 32 | log.Die("Source is not a valid chart. Missing Chart.yaml: %s", err) 33 | } 34 | 35 | deps, err := dependency.Resolve(cfile, helm.WorkspaceChartDirectory(homedir)) 36 | if err != nil { 37 | log.Warn("Could not check dependencies: %s", err) 38 | return 39 | } 40 | 41 | if len(deps) > 0 { 42 | log.Warn("Unsatisfied dependencies:") 43 | for _, d := range deps { 44 | log.Msg("\t%s %s", d.Name, d.Version) 45 | } 46 | } 47 | 48 | log.Info("Fetched chart into workspace %s", helm.WorkspaceChartDirectory(homedir, lname)) 49 | log.Info("Done") 50 | } 51 | 52 | func fetch(chartName, lname, homedir, chartpath string) { 53 | src := helm.CacheDirectory(homedir, chartpath, chartName) 54 | dest := helm.WorkspaceChartDirectory(homedir, lname) 55 | 56 | fi, err := os.Stat(src) 57 | if err != nil { 58 | log.Warn("Oops. Looks like there was an issue finding the chart, %s, in %s. Running `helmc update` to ensure you have the latest version of all Charts from Github...", lname, src) 59 | Update(homedir) 60 | fi, err = os.Stat(src) 61 | if err != nil { 62 | log.Die("Chart %s not found in %s", lname, src) 63 | } 64 | log.Info("Good news! Looks like that did the trick. Onwards and upwards!") 65 | } 66 | 67 | if !fi.IsDir() { 68 | log.Die("Malformed chart %s: Chart must be in a directory.", chartName) 69 | } 70 | 71 | if err := os.MkdirAll(dest, 0755); err != nil { 72 | log.Die("Could not create %q: %s", dest, err) 73 | } 74 | 75 | log.Debug("Fetching %s to %s", src, dest) 76 | if err := helm.CopyDir(src, dest); err != nil { 77 | log.Die("Failed copying %s to %s", src, dest) 78 | } 79 | 80 | if err := updateChartfile(src, dest, lname); err != nil { 81 | log.Die("Failed to update Chart.yaml: %s", err) 82 | } 83 | } 84 | 85 | func updateChartfile(src, dest, lname string) error { 86 | sc, err := chart.LoadChartfile(filepath.Join(src, Chartfile)) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | dc, err := chart.LoadChartfile(filepath.Join(dest, Chartfile)) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | dc.Name = lname 97 | dc.From = &chart.Dependency{ 98 | Name: sc.Name, 99 | Version: sc.Version, 100 | Repo: chart.RepoName(src), 101 | } 102 | 103 | return dc.Save(filepath.Join(dest, Chartfile)) 104 | } 105 | -------------------------------------------------------------------------------- /action/fetch_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/helm/helm-classic/test" 7 | "github.com/helm/helm-classic/util" 8 | ) 9 | 10 | func TestFetch(t *testing.T) { 11 | tmpHome := test.CreateTmpHome() 12 | test.FakeUpdate(tmpHome) 13 | chartName := "kitchensink" 14 | 15 | actual := test.CaptureOutput(func() { 16 | Fetch(chartName, "", tmpHome) 17 | }) 18 | 19 | workspacePath := util.WorkspaceChartDirectory(tmpHome, chartName) 20 | test.ExpectContains(t, actual, "Fetched chart into workspace "+workspacePath) 21 | } 22 | -------------------------------------------------------------------------------- /action/generate.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strconv" 7 | 8 | "github.com/helm/helm-classic/generator" 9 | "github.com/helm/helm-classic/log" 10 | "github.com/helm/helm-classic/util" 11 | ) 12 | 13 | // Generate runs generators on the entire chart. 14 | // 15 | // By design, this only operates on workspaces, as it should never be run 16 | // on the cache. 17 | func Generate(chart, homedir string, exclude []string, force bool) { 18 | if abs, err := filepath.Abs(homedir); err == nil { 19 | homedir = abs 20 | } 21 | chartPath := util.WorkspaceChartDirectory(homedir, chart) 22 | 23 | // Although helmc itself may use the new HELMC_HOME environment variable to optionally define its 24 | // home directory, to maintain compatibility with charts created for the ORIGINAL helm, we 25 | // continue to support expansion of these "legacy" environment variables, including HELM_HOME. 26 | os.Setenv("HELM_HOME", homedir) 27 | os.Setenv("HELM_DEFAULT_REPO", mustConfig(homedir).Repos.Default) 28 | os.Setenv("HELM_FORCE_FLAG", strconv.FormatBool(force)) 29 | 30 | count, err := generator.Walk(chartPath, exclude, force) 31 | if err != nil { 32 | log.Die("Failed to complete generation: %s", err) 33 | } 34 | log.Info("Ran %d generators.", count) 35 | } 36 | -------------------------------------------------------------------------------- /action/generate_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/helm/helm-classic/test" 8 | "github.com/helm/helm-classic/util" 9 | ) 10 | 11 | func TestGenerate(t *testing.T) { 12 | ch := "generate" 13 | homedir := test.CreateTmpHome() 14 | test.FakeUpdate(homedir) 15 | Fetch(ch, ch, homedir) 16 | 17 | Generate(ch, homedir, []string{"ignore"}, true) 18 | 19 | // Now we should be able to load and read the `pod.yaml` file. 20 | path := util.WorkspaceChartDirectory(homedir, "generate/manifests/pod.yaml") 21 | d, err := ioutil.ReadFile(path) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | pod := string(d) 26 | test.ExpectContains(t, pod, "image: ozo") 27 | test.ExpectContains(t, pod, "name: www-server") 28 | } 29 | -------------------------------------------------------------------------------- /action/helper_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import "github.com/helm/helm-classic/log" 4 | 5 | func init() { 6 | // Turn on debug output, convert os.Exit(1) to panic() 7 | log.IsDebugging = true 8 | } 9 | -------------------------------------------------------------------------------- /action/info.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "text/template" 5 | 6 | "github.com/helm/helm-classic/chart" 7 | "github.com/helm/helm-classic/log" 8 | helm "github.com/helm/helm-classic/util" 9 | ) 10 | 11 | const defaultInfoFormat = `Name: {{.Name}} 12 | Home: {{.Home}} 13 | Version: {{.Version}} 14 | Description: {{.Description}} 15 | Details: {{.Details}} 16 | ` 17 | 18 | // Info prints information about a chart. 19 | // 20 | // - chartName to display 21 | // - homeDir is the helm home directory for the user 22 | // - format is a optional Go template 23 | func Info(chartName, homedir, format string) { 24 | r := mustConfig(homedir).Repos 25 | table, chartLocal := r.RepoChart(chartName) 26 | chartPath := helm.CacheDirectory(homedir, table, chartLocal, Chartfile) 27 | 28 | if format == "" { 29 | format = defaultInfoFormat 30 | } 31 | 32 | chart, err := chart.LoadChartfile(chartPath) 33 | if err != nil { 34 | log.Die("Could not find chart %s: %s", chartName, err.Error()) 35 | } 36 | 37 | tmpl, err := template.New("info").Parse(format) 38 | if err != nil { 39 | log.Die("%s", err) 40 | } 41 | 42 | if err = tmpl.Execute(log.Stdout, chart); err != nil { 43 | log.Die("%s", err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /action/info_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/helm/helm-classic/test" 7 | ) 8 | 9 | func TestInfo(t *testing.T) { 10 | 11 | tmpHome := test.CreateTmpHome() 12 | test.FakeUpdate(tmpHome) 13 | 14 | format := "" 15 | expected := `Name: kitchensink 16 | Home: http://github.com/helm/helm 17 | Version: 0.0.1 18 | Description: All the things, all semantically, none working 19 | Details: This package provides a sampling of all of the different manifest types. It can be used to test ordering and other properties of a chart.` 20 | 21 | actual := test.CaptureOutput(func() { 22 | Info("kitchensink", tmpHome, format) 23 | }) 24 | 25 | test.ExpectContains(t, actual, expected) 26 | } 27 | 28 | func TestInfoFormat(t *testing.T) { 29 | 30 | tmpHome := test.CreateTmpHome() 31 | test.FakeUpdate(tmpHome) 32 | 33 | format := `Hello {{.Name}}` 34 | expected := `Hello kitchensink` 35 | 36 | actual := test.CaptureOutput(func() { 37 | Info("kitchensink", tmpHome, format) 38 | }) 39 | 40 | test.ExpectContains(t, actual, expected) 41 | } 42 | -------------------------------------------------------------------------------- /action/install_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/helm/helm-classic/kubectl" 10 | "github.com/helm/helm-classic/test" 11 | ) 12 | 13 | func TestInstall(t *testing.T) { 14 | // Todo: add tests 15 | // - with an invalid chart name 16 | // - with failure to check dependencies 17 | // - with failure to check dependencies and force option 18 | // - with chart in current directly 19 | tests := []struct { 20 | name string // Todo: print name on fail 21 | chart string 22 | force bool 23 | expected []string 24 | client kubectl.Runner 25 | }{ 26 | { 27 | name: "with valid input", 28 | chart: "redis", 29 | expected: []string{"hello from redis"}, 30 | client: TestRunner{ 31 | out: []byte("hello from redis"), 32 | }, 33 | }, 34 | { 35 | name: "with dry-run option", 36 | chart: "redis", 37 | expected: []string{"[CMD] kubectl create -f -"}, 38 | client: kubectl.PrintRunner{}, 39 | }, 40 | { 41 | name: "with unsatisfied dependencies", 42 | chart: "kitchensink", 43 | expected: []string{"Stopping install. Re-run with --force to install anyway."}, 44 | client: TestRunner{}, 45 | }, 46 | { 47 | name: "with unsatisfied dependencies and force option", 48 | chart: "kitchensink", 49 | force: true, 50 | expected: []string{"Unsatisfied dependencies", "Running `kubectl create -f`"}, 51 | client: TestRunner{}, 52 | }, 53 | { 54 | name: "with a kubectl error", 55 | chart: "redis", 56 | expected: []string{"Failed to upload manifests: oh snap"}, 57 | client: TestRunner{ 58 | err: errors.New("oh snap"), 59 | }, 60 | }, 61 | } 62 | 63 | tmpHome := test.CreateTmpHome() 64 | defer os.RemoveAll(tmpHome) 65 | test.FakeUpdate(tmpHome) 66 | 67 | // Todo: get rid of this hacky mess 68 | pp := os.Getenv("PATH") 69 | defer os.Setenv("PATH", pp) 70 | os.Setenv("PATH", filepath.Join(test.HelmRoot, "testdata")+":"+pp) 71 | 72 | for _, tt := range tests { 73 | actual := test.CaptureOutput(func() { 74 | Install(tt.chart, tmpHome, "", tt.force, false, []string{}, tt.client) 75 | }) 76 | 77 | for _, exp := range tt.expected { 78 | test.ExpectContains(t, actual, exp) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /action/lint_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/helm/helm-classic/test" 11 | "github.com/helm/helm-classic/util" 12 | 13 | "gopkg.in/yaml.v2" 14 | ) 15 | 16 | func TestLintSuccess(t *testing.T) { 17 | tmpHome := test.CreateTmpHome() 18 | test.FakeUpdate(tmpHome) 19 | 20 | chartName := "goodChart" 21 | 22 | Create(chartName, tmpHome) 23 | 24 | output := test.CaptureOutput(func() { 25 | Lint(util.WorkspaceChartDirectory(tmpHome, chartName)) 26 | }) 27 | 28 | expected := "Chart [goodChart] has passed all necessary checks" 29 | 30 | test.ExpectContains(t, output, expected) 31 | } 32 | 33 | func TestLintMissingReadme(t *testing.T) { 34 | tmpHome := test.CreateTmpHome() 35 | test.FakeUpdate(tmpHome) 36 | 37 | chartName := "badChart" 38 | 39 | Create(chartName, tmpHome) 40 | 41 | os.Remove(filepath.Join(util.WorkspaceChartDirectory(tmpHome, chartName), "README.md")) 42 | 43 | output := test.CaptureOutput(func() { 44 | Lint(util.WorkspaceChartDirectory(tmpHome, chartName)) 45 | }) 46 | 47 | test.ExpectContains(t, output, "README.md is present and not empty : false") 48 | } 49 | 50 | func TestLintMissingChartYaml(t *testing.T) { 51 | tmpHome := test.CreateTmpHome() 52 | test.FakeUpdate(tmpHome) 53 | 54 | chartName := "badChart" 55 | 56 | Create(chartName, tmpHome) 57 | 58 | os.Remove(filepath.Join(util.WorkspaceChartDirectory(tmpHome, chartName), Chartfile)) 59 | 60 | output := test.CaptureOutput(func() { 61 | Lint(util.WorkspaceChartDirectory(tmpHome, chartName)) 62 | }) 63 | 64 | test.ExpectContains(t, output, "Chart.yaml is present : false") 65 | test.ExpectContains(t, output, "Chart [badChart] has failed some necessary checks.") 66 | } 67 | 68 | func TestLintMismatchedChartNameAndDir(t *testing.T) { 69 | tmpHome := test.CreateTmpHome() 70 | chartName := "chart-0" 71 | chartDir := "chart-1" 72 | chart := newSkelChartfile(chartName) 73 | createWithChart(chart, chartDir, tmpHome) 74 | 75 | output := test.CaptureOutput(func() { 76 | Lint(util.WorkspaceChartDirectory(tmpHome, chartDir)) 77 | }) 78 | 79 | test.ExpectContains(t, output, "Name declared in Chart.yaml is the same as directory name. : false") 80 | } 81 | 82 | func TestLintMissingManifestDirectory(t *testing.T) { 83 | tmpHome := test.CreateTmpHome() 84 | test.FakeUpdate(tmpHome) 85 | 86 | chartName := "brokeChart" 87 | 88 | Create(chartName, tmpHome) 89 | 90 | os.RemoveAll(filepath.Join(util.WorkspaceChartDirectory(tmpHome, chartName), "manifests")) 91 | 92 | output := test.CaptureOutput(func() { 93 | Lint(util.WorkspaceChartDirectory(tmpHome, chartName)) 94 | }) 95 | 96 | test.ExpectMatches(t, output, "Manifests directory is present : false") 97 | test.ExpectContains(t, output, "Chart ["+chartName+"] has failed some necessary checks") 98 | } 99 | 100 | func TestLintEmptyChartYaml(t *testing.T) { 101 | tmpHome := test.CreateTmpHome() 102 | test.FakeUpdate(tmpHome) 103 | 104 | chartName := "badChart" 105 | 106 | Create(chartName, tmpHome) 107 | 108 | badChartYaml, _ := yaml.Marshal(make(map[string]string)) 109 | 110 | chartYaml := util.WorkspaceChartDirectory(tmpHome, chartName, Chartfile) 111 | 112 | os.Remove(chartYaml) 113 | ioutil.WriteFile(chartYaml, badChartYaml, 0644) 114 | 115 | output := test.CaptureOutput(func() { 116 | Lint(util.WorkspaceChartDirectory(tmpHome, chartName)) 117 | }) 118 | 119 | test.ExpectContains(t, output, "Chart.yaml has a name field : false") 120 | test.ExpectContains(t, output, "Chart.yaml has a version field : false") 121 | test.ExpectContains(t, output, "Chart.yaml has a description field : false") 122 | test.ExpectContains(t, output, "Chart.yaml has a maintainers field : false") 123 | test.ExpectContains(t, output, fmt.Sprintf("Chart [%s] has failed some necessary checks", chartName)) 124 | } 125 | 126 | func TestLintBadPath(t *testing.T) { 127 | tmpHome := test.CreateTmpHome() 128 | chartName := "badChart" 129 | 130 | output := test.CaptureOutput(func() { 131 | Lint(util.WorkspaceChartDirectory(tmpHome, chartName)) 132 | }) 133 | 134 | msg := "Chart found at " + tmpHome + "/workspace/charts/" + chartName + " : false" 135 | test.ExpectContains(t, output, msg) 136 | } 137 | -------------------------------------------------------------------------------- /action/list.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/helm/helm-classic/chart" 7 | "github.com/helm/helm-classic/log" 8 | helm "github.com/helm/helm-classic/util" 9 | ) 10 | 11 | // List lists all of the local charts. 12 | func List(homedir string) { 13 | md := helm.WorkspaceChartDirectory(homedir, "*") 14 | charts, err := filepath.Glob(md) 15 | if err != nil { 16 | log.Warn("Could not find any charts in %q: %s", md, err) 17 | } 18 | for _, c := range charts { 19 | cname := filepath.Base(c) 20 | if ch, err := chart.LoadChartfile(filepath.Join(c, Chartfile)); err == nil { 21 | log.Info("\t%s (%s %s) - %s", cname, ch.Name, ch.Version, ch.Description) 22 | continue 23 | } 24 | log.Info("\t%s (unknown)", cname) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /action/plugin.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path/filepath" 7 | 8 | "github.com/helm/helm-classic/log" 9 | ) 10 | 11 | // Plugin attempts to execute a plugin. 12 | // 13 | // It looks on the path for an executable named `helm-COMMAND`, and executes 14 | // that, passing it all of the arguments received after the subcommand. 15 | // 16 | // Output is passed directly back to the user. 17 | // 18 | // This ensures that the following environment variables are set: 19 | // 20 | // - $HELM_HOME: points to the user's Helm home directory. 21 | // - $HELM_DEFAULT_REPO: the local name of the default repository. 22 | // - $HELM_COMMAND: the name of the command (as seen by Helm) that resulted in this program being executed. 23 | func Plugin(homedir, cmd string, args []string) { 24 | if abs, err := filepath.Abs(homedir); err == nil { 25 | homedir = abs 26 | } 27 | 28 | // Although helmc itself may use the new HELMC_HOME environment variable to optionally define its 29 | // home directory, to maintain compatibility with plugins created for the ORIGINAL helm, we 30 | // continue to support expansion of these "legacy" environment variables, including HELM_HOME. 31 | os.Setenv("HELM_HOME", homedir) 32 | os.Setenv("HELM_COMMAND", args[0]) 33 | os.Setenv("HELM_DEFAULT_REPO", mustConfig(homedir).Repos.Default) 34 | 35 | cmd = PluginName(cmd) 36 | execPlugin(cmd, args[1:]) 37 | } 38 | 39 | // HasPlugin returns true if the named plugin exists. 40 | func HasPlugin(name string) bool { 41 | name = PluginName(name) 42 | _, err := exec.LookPath(name) 43 | return err == nil 44 | } 45 | 46 | // PluginName returns the full plugin name. 47 | func PluginName(name string) string { 48 | return "helm-" + name 49 | } 50 | 51 | func execPlugin(name string, args []string) { 52 | cmd := exec.Command(name, args...) 53 | cmd.Stdout = os.Stdout 54 | cmd.Stderr = os.Stderr 55 | cmd.Stdin = os.Stdin 56 | 57 | if err := cmd.Run(); err != nil { 58 | log.Die(err.Error()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /action/plugin_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/helm/helm-classic/test" 10 | ) 11 | 12 | func TestPluginName(t *testing.T) { 13 | if PluginName("foo") != "helm-foo" { 14 | t.Errorf("Expected helm-foo, got %s", PluginName("foo")) 15 | } 16 | } 17 | 18 | func TestPlugin(t *testing.T) { 19 | f := "../testdata" 20 | p := "plugin" 21 | a := []string{"myplugin", "-a", "-b", "-c"} 22 | 23 | os.Setenv("PATH", os.ExpandEnv("$PATH:"+test.HelmRoot+"/testdata")) 24 | 25 | buf, err := ioutil.TempFile("", "helm-plugin-test") 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | oldout := os.Stdout 31 | os.Stdout = buf 32 | defer func() { os.Stdout = oldout; buf.Close(); os.Remove(buf.Name()) }() 33 | 34 | test.FakeUpdate(f) 35 | Plugin(f, p, a) 36 | 37 | buf.Seek(0, 0) 38 | b, err := ioutil.ReadAll(buf) 39 | if err != nil { 40 | t.Errorf("Failed to read tmp file: %s", err) 41 | } 42 | 43 | if strings.TrimSpace(string(b)) != "HELLO -a -b -c" { 44 | t.Errorf("Expected 'HELLO -a -b -c', got %v", string(b)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /action/print_readme.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/helm/helm-classic/log" 10 | helm "github.com/helm/helm-classic/util" 11 | ) 12 | 13 | // PrintREADME prints the README file (if it exists) to the console. 14 | func PrintREADME(chart, home string) { 15 | p := helm.WorkspaceChartDirectory(home, chart, "README.*") 16 | files, err := filepath.Glob(p) 17 | if err != nil || len(files) == 0 { 18 | // No README. Skip. 19 | log.Debug("No readme in %s", p) 20 | return 21 | } 22 | 23 | f, err := os.Open(files[0]) 24 | if err != nil { 25 | log.Warn("Could not read README: %s", err) 26 | return 27 | } 28 | log.Msg(strings.Repeat("=", 40)) 29 | io.Copy(log.Stdout, f) 30 | log.Msg(strings.Repeat("=", 40)) 31 | f.Close() 32 | 33 | } 34 | -------------------------------------------------------------------------------- /action/publish.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import "os" 4 | 5 | import ( 6 | "github.com/helm/helm-classic/log" 7 | helm "github.com/helm/helm-classic/util" 8 | ) 9 | 10 | // Publish a chart from the workspace to the cache directory 11 | // 12 | // - chartName being published 13 | // - homeDir is the helm home directory for the user 14 | // - force publishing even if the chart directory already exists 15 | func Publish(chartName, homeDir, repo string, force bool) { 16 | if repo == "" { 17 | repo = "charts" 18 | } 19 | 20 | if !mustConfig(homeDir).Repos.Exists(repo) { 21 | log.Err("Repo %s does not exist", repo) 22 | log.Info("Available repositories") 23 | ListRepos(homeDir) 24 | return 25 | } 26 | 27 | src := helm.WorkspaceChartDirectory(homeDir, chartName) 28 | dst := helm.CacheDirectory(homeDir, repo, chartName) 29 | 30 | if _, err := os.Stat(dst); err == nil { 31 | if force != true { 32 | log.Info("chart already exists, use -f to force") 33 | return 34 | } 35 | } 36 | 37 | if err := helm.CopyDir(src, dst); err != nil { 38 | log.Die("failed to publish directory: %v", err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /action/remove.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/helm/helm-classic/log" 10 | "github.com/helm/helm-classic/manifest" 11 | helm "github.com/helm/helm-classic/util" 12 | ) 13 | 14 | // kubeGetter wraps the kubectl command, override in tests 15 | type kubeGetter func(string) string 16 | 17 | var kubeGet kubeGetter = func(m string) string { 18 | log.Debug("Getting manifests from %s", m) 19 | 20 | a := []string{"get", "-f", m} 21 | out, _ := exec.Command("kubectl", a...).CombinedOutput() 22 | return string(out) 23 | } 24 | 25 | // Remove removes a chart from the workdir. 26 | // 27 | // - chart is the source 28 | // - homedir is the home directory for the user 29 | // - force will remove installed charts from workspace 30 | func Remove(chart, homedir string, force bool) { 31 | chartPath := helm.WorkspaceChartDirectory(homedir, chart) 32 | if _, err := os.Stat(chartPath); err != nil { 33 | log.Err("Chart not found. %s", err) 34 | return 35 | } 36 | 37 | if !force { 38 | var connectionFailure bool 39 | 40 | // check if any chart manifests are installed 41 | installed, err := checkManifests(chartPath) 42 | if err != nil { 43 | if strings.Contains(err.Error(), "unable to connect") { 44 | connectionFailure = true 45 | } else { 46 | log.Die(err.Error()) 47 | } 48 | } 49 | 50 | if connectionFailure { 51 | log.Err("Could not determine if %s is installed. To remove the chart --force flag must be set.", chart) 52 | return 53 | } else if len(installed) > 0 { 54 | log.Err("Found %d installed manifests for %s. To remove a chart that has been installed the --force flag must be set.", len(installed), chart) 55 | return 56 | } 57 | } 58 | 59 | // remove local chart files 60 | if err := os.RemoveAll(chartPath); err != nil { 61 | log.Die("Could not remove chart. %s", err) 62 | } 63 | 64 | log.Info("All clear! You have successfully removed %s from your workspace.", chart) 65 | } 66 | 67 | // checkManifests gets any installed manifests within a chart 68 | func checkManifests(chartPath string) ([]string, error) { 69 | var foundManifests []string 70 | 71 | manifests, err := manifest.Files(chartPath) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | for _, m := range manifests { 77 | out := kubeGet(m) 78 | 79 | if strings.Contains(out, "unable to connect") { 80 | return nil, fmt.Errorf(out) 81 | } 82 | if !strings.Contains(out, "not found") { 83 | foundManifests = append(foundManifests, m) 84 | } 85 | } 86 | 87 | log.Debug("Found %d installed manifests", len(foundManifests)) 88 | 89 | return foundManifests, nil 90 | } 91 | -------------------------------------------------------------------------------- /action/remove_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/helm/helm-classic/test" 8 | ) 9 | 10 | var ( 11 | // mock responses from kubectl 12 | mockFoundGetter = func(string) string { return "installed" } 13 | mockNotFoundGetter = func(string) string { return "not found" } 14 | mockFailConnection = func(string) string { return "unable to connect to a server" } 15 | ) 16 | 17 | func TestTRemove(t *testing.T) { 18 | kg := kubeGet 19 | defer func() { kubeGet = kg }() 20 | 21 | tests := []struct { 22 | chartName string 23 | getter kubeGetter 24 | force bool 25 | expected string 26 | }{ 27 | {"kitchensink", mockNotFoundGetter, false, "All clear! You have successfully removed kitchensink from your workspace."}, 28 | 29 | // when manifests are installed 30 | {"kitchensink", mockFoundGetter, false, "Found 12 installed manifests for kitchensink. To remove a chart that has been installed the --force flag must be set."}, 31 | 32 | // when manifests are installed and force is set 33 | {"kitchensink", mockNotFoundGetter, true, "All clear! You have successfully removed kitchensink from your workspace."}, 34 | 35 | // when kubectl cannot connect 36 | {"kitchensink", mockFailConnection, false, "Could not determine if kitchensink is installed. To remove the chart --force flag must be set."}, 37 | 38 | // when kubectl cannot connect and force is set 39 | {"kitchensink", mockFailConnection, true, "All clear! You have successfully removed kitchensink from your workspace."}, 40 | } 41 | 42 | for _, tt := range tests { 43 | tmpHome := test.CreateTmpHome() 44 | test.FakeUpdate(tmpHome) 45 | 46 | Fetch("kitchensink", "", tmpHome) 47 | 48 | // set the mock getter 49 | kubeGet = tt.getter 50 | 51 | actual := test.CaptureOutput(func() { 52 | Remove(tt.chartName, tmpHome, tt.force) 53 | }) 54 | 55 | test.ExpectContains(t, actual, tt.expected) 56 | 57 | os.Remove(tmpHome) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /action/repo.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "github.com/helm/helm-classic/log" 5 | ) 6 | 7 | // ListRepos lists the repositories. 8 | func ListRepos(homedir string) { 9 | rf := mustConfig(homedir).Repos 10 | 11 | for _, t := range rf.Tables { 12 | n := t.Name 13 | if t.Name == rf.Default { 14 | n += "*" 15 | } 16 | log.Msg("\t%s\t%s", n, t.Repo) 17 | } 18 | } 19 | 20 | // AddRepo adds a repo to the list of repositories. 21 | func AddRepo(homedir, name, repository string) { 22 | cfg := mustConfig(homedir) 23 | 24 | if err := cfg.Repos.Add(name, repository); err != nil { 25 | log.Die(err.Error()) 26 | } 27 | if err := cfg.Save(""); err != nil { 28 | log.Die("Could not save configuration: %s", err) 29 | } 30 | 31 | log.Info("Hooray! Successfully added the repo.") 32 | } 33 | 34 | // DeleteRepo deletes a repository. 35 | func DeleteRepo(homedir, name string) { 36 | cfg := mustConfig(homedir) 37 | 38 | if err := cfg.Repos.Delete(name); err != nil { 39 | log.Die("Failed to delete repository: %s", err) 40 | } 41 | if err := cfg.Save(""); err != nil { 42 | log.Die("Deleted repo, but could not save settings: %s", err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /action/repo_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/helm/helm-classic/log" 7 | "github.com/helm/helm-classic/test" 8 | ) 9 | 10 | func TestListRepos(t *testing.T) { 11 | log.IsDebugging = true 12 | 13 | homedir := test.CreateTmpHome() 14 | test.FakeUpdate(homedir) 15 | 16 | actual := test.CaptureOutput(func() { 17 | ListRepos(homedir) 18 | }) 19 | 20 | test.ExpectContains(t, actual, "charts*\thttps://github.com/helm/charts") 21 | } 22 | -------------------------------------------------------------------------------- /action/search.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "github.com/helm/helm-classic/log" 5 | "github.com/helm/helm-classic/search" 6 | helm "github.com/helm/helm-classic/util" 7 | ) 8 | 9 | // Search looks for packages with 'term' in their name. 10 | func Search(term, homedir string, regexp bool) { 11 | cfg := mustConfig(homedir) 12 | cdir := helm.CacheDirectory(homedir) 13 | 14 | i := search.NewIndex(cfg, cdir) 15 | res, err := i.Search(term, 5, regexp) 16 | if err != nil { 17 | log.Die("Failed to search: %s", err) 18 | } 19 | 20 | if len(res) == 0 { 21 | log.Err("No results found. Try using '--regexp'.") 22 | return 23 | } 24 | 25 | search.SortScore(res) 26 | 27 | for _, r := range res { 28 | c, _ := i.Chart(r.Name) 29 | log.Msg("%s - %s", r.Name, c.Description) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /action/search_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/helm/helm-classic/test" 7 | ) 8 | 9 | func TestSearch(t *testing.T) { 10 | tmpHome := test.CreateTmpHome() 11 | test.FakeUpdate(tmpHome) 12 | 13 | Search("homeslice", tmpHome, false) 14 | } 15 | 16 | func TestSearchNotFound(t *testing.T) { 17 | tmpHome := test.CreateTmpHome() 18 | test.FakeUpdate(tmpHome) 19 | 20 | // test that a "no chart found" message was printed 21 | expected := "No results found" 22 | 23 | actual := test.CaptureOutput(func() { 24 | Search("nonexistent", tmpHome, false) 25 | }) 26 | 27 | test.ExpectContains(t, actual, expected) 28 | } 29 | -------------------------------------------------------------------------------- /action/target.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "github.com/helm/helm-classic/kubectl" 5 | "github.com/helm/helm-classic/log" 6 | ) 7 | 8 | // Target displays information about the cluster 9 | func Target(client kubectl.Runner) { 10 | out, err := client.ClusterInfo() 11 | if err != nil { 12 | log.Err(err.Error()) 13 | } 14 | log.Msg(string(out)) 15 | } 16 | -------------------------------------------------------------------------------- /action/target_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/helm/helm-classic/test" 7 | ) 8 | 9 | func TestTarget(t *testing.T) { 10 | client := TestRunner{ 11 | out: []byte("lookin good"), 12 | } 13 | 14 | expected := "lookin good" 15 | 16 | actual := test.CaptureOutput(func() { 17 | Target(client) 18 | }) 19 | 20 | test.ExpectContains(t, actual, expected) 21 | } 22 | -------------------------------------------------------------------------------- /action/template.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "text/template" 11 | 12 | "github.com/BurntSushi/toml" 13 | "github.com/Masterminds/sprig" 14 | "github.com/helm/helm-classic/log" 15 | "gopkg.in/yaml.v2" 16 | ) 17 | 18 | var err error 19 | 20 | //GenerateTemplate evaluates a template and writes it to an io.Writer 21 | func GenerateTemplate(out io.Writer, in io.Reader, vals interface{}) { 22 | tpl, err := ioutil.ReadAll(in) 23 | if err != nil { 24 | log.Die("Failed to read template file: %s", err) 25 | } 26 | 27 | if err := renderTemplate(out, string(tpl), vals); err != nil { 28 | log.Die("Template rendering failed: %s", err) 29 | } 30 | } 31 | 32 | // Template renders a template to an output file. 33 | func Template(out, in, data string, force bool) error { 34 | var dest io.Writer 35 | _, err = os.Stat(out) 36 | if !(force || os.Getenv("HELM_FORCE_FLAG") == "true") && err == nil { 37 | return fmt.Errorf("File %s already exists. To overwrite it, please re-run this command with the --force/-f flag.", out) 38 | } 39 | if out != "" { 40 | f, err := os.Create(out) 41 | if err != nil { 42 | log.Die("Failed to open %s: %s", out, err) 43 | } 44 | defer func() { 45 | if err := f.Close(); err != nil { 46 | log.Err("Error closing file: %s", err) 47 | } 48 | }() 49 | dest = f 50 | } else { 51 | dest = log.Stdout 52 | } 53 | 54 | inReader, err := os.Open(in) 55 | if err != nil { 56 | log.Die("Failed to open template file: %s", err) 57 | } 58 | 59 | var vals interface{} 60 | if data != "" { 61 | var err error 62 | vals, err = openValues(data) 63 | if err != nil { 64 | log.Die("Error opening value file: %s", err) 65 | } 66 | } 67 | 68 | GenerateTemplate(dest, inReader, vals) 69 | return nil 70 | } 71 | 72 | // openValues opens a values file and tries to parse it with the right parser. 73 | // 74 | // It returns an interface{} containing data, if found. Any error opening or 75 | // parsing the file will be passed back. 76 | func openValues(filename string) (interface{}, error) { 77 | data, err := ioutil.ReadFile(filename) 78 | if err != nil { 79 | // We generate a warning here, but do not require that a values 80 | // file exists. 81 | log.Warn("Skipped file %s: %s", filename, err) 82 | return map[string]interface{}{}, nil 83 | } 84 | 85 | ext := filepath.Ext(filename) 86 | var um func(p []byte, v interface{}) error 87 | switch ext { 88 | case ".yaml", ".yml": 89 | um = yaml.Unmarshal 90 | case ".toml": 91 | um = toml.Unmarshal 92 | case ".json": 93 | um = json.Unmarshal 94 | default: 95 | return nil, fmt.Errorf("Unsupported file type: %s", ext) 96 | } 97 | 98 | var res interface{} 99 | err = um(data, &res) 100 | return res, err 101 | } 102 | 103 | // renderTemplate renders a template and values into an output stream. 104 | // 105 | // tpl should be a string template. 106 | func renderTemplate(out io.Writer, tpl string, vals interface{}) error { 107 | t, err := template.New("helmTpl").Funcs(sprig.TxtFuncMap()).Parse(tpl) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | log.Debug("Vals: %#v", vals) 113 | 114 | if err = t.ExecuteTemplate(out, "helmTpl", vals); err != nil { 115 | return err 116 | } 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /action/template_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/helm/helm-classic/log" 10 | "github.com/helm/helm-classic/test" 11 | "github.com/helm/helm-classic/util" 12 | ) 13 | 14 | func TestTemplate(t *testing.T) { 15 | dir := filepath.Join(test.HelmRoot, "testdata/template") 16 | tpl := filepath.Join(dir, "one.tpl") 17 | val := filepath.Join(dir, "one.toml") 18 | var out bytes.Buffer 19 | o := log.Stdout 20 | log.Stdout = &out 21 | defer func() { log.Stdout = o }() 22 | 23 | // TOML 24 | Template("", tpl, val, false) 25 | if out.String() != "Hello World!\n" { 26 | t.Errorf("Expected Hello World!, got %q", out.String()) 27 | } 28 | 29 | // force false 30 | os.Setenv("HELM_FORCE_FLAG", "false") 31 | if err = Template(tpl, val, "", false); err == nil { 32 | t.Errorf("Expected error but got nil") 33 | } 34 | tpl1 := filepath.Join(dir, "two.yaml") 35 | util.CopyFile(tpl, tpl1) 36 | // force true 37 | if err = Template(tpl1, val, "", true); err != nil { 38 | t.Errorf("error force-generating template (%s)", err.Error()) 39 | } 40 | defer os.Remove(tpl1) 41 | 42 | // YAML 43 | val = filepath.Join(dir, "one.yaml") 44 | out.Reset() 45 | Template("", tpl, val, false) 46 | if out.String() != "Hello World!\n" { 47 | t.Errorf("Expected Hello World!, got %q", out.String()) 48 | } 49 | 50 | // JSON 51 | val = filepath.Join(dir, "one.json") 52 | out.Reset() 53 | Template("", tpl, val, false) 54 | if out.String() != "Hello World!\n" { 55 | t.Errorf("Expected Hello World!, got %q", out.String()) 56 | } 57 | 58 | // No data 59 | out.Reset() 60 | Template("", tpl, "", false) 61 | if out.String() != "Hello Clowns!\n" { 62 | t.Errorf("Expected Hello Clowns!, got %q", out.String()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /action/uninstall.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "golang.org/x/crypto/ssh/terminal" 8 | 9 | "github.com/helm/helm-classic/chart" 10 | "github.com/helm/helm-classic/kubectl" 11 | "github.com/helm/helm-classic/log" 12 | "github.com/helm/helm-classic/manifest" 13 | helm "github.com/helm/helm-classic/util" 14 | ) 15 | 16 | // Uninstall removes a chart from Kubernetes. 17 | // 18 | // Manifests are removed from Kubernetes in the order specified by 19 | // chart.UninstallOrder. Any unknown types are removed before that sequence 20 | // is run. 21 | func Uninstall(chartName, home, namespace string, force bool, client kubectl.Runner) { 22 | // This is a stop-gap until kubectl respects namespaces in manifests. 23 | if namespace == "" { 24 | log.Die("This command requires a namespace. Did you mean '-n default'?") 25 | } 26 | if !chartFetched(chartName, home) { 27 | log.Info("No chart named %q in your workspace. Nothing to delete.", chartName) 28 | return 29 | } 30 | 31 | cd := helm.WorkspaceChartDirectory(home, chartName) 32 | c, err := chart.Load(cd) 33 | if err != nil { 34 | log.Die("Failed to load chart: %s", err) 35 | } 36 | if err := deleteChart(c, namespace, true, client); err != nil { 37 | log.Die("Failed to list charts: %s", err) 38 | } 39 | if !force && !promptConfirm("Uninstall the listed objects?") { 40 | log.Info("Aborted uninstall") 41 | return 42 | } 43 | 44 | CheckKubePrereqs() 45 | 46 | log.Info("Running `kubectl delete` ...") 47 | if err := deleteChart(c, namespace, false, client); err != nil { 48 | log.Die("Failed to completely delete chart: %s", err) 49 | } 50 | log.Info("Done") 51 | } 52 | 53 | // promptConfirm prompts a user to confirm (or deny) something. 54 | // 55 | // True is returned iff the prompt is confirmed. 56 | // Errors are reported to the log, and return false. 57 | // 58 | // Valid confirmations: 59 | // y, yes, true, t, aye-aye 60 | // 61 | // Valid denials: 62 | // n, no, f, false 63 | // 64 | // Any other prompt response will return false, and issue a warning to the 65 | // user. 66 | func promptConfirm(msg string) bool { 67 | oldState, err := terminal.MakeRaw(0) 68 | if err != nil { 69 | log.Err("Could not get terminal: %s", err) 70 | return false 71 | } 72 | defer terminal.Restore(0, oldState) 73 | 74 | f := readerWriter(log.Stdin, log.Stdout) 75 | t := terminal.NewTerminal(f, msg+" (y/N) ") 76 | res, err := t.ReadLine() 77 | if err != nil { 78 | log.Err("Could not read line: %s", err) 79 | return false 80 | } 81 | res = strings.ToLower(res) 82 | switch res { 83 | case "yes", "y", "true", "t", "aye-aye": 84 | return true 85 | case "no", "n", "false", "f": 86 | return false 87 | } 88 | log.Warn("Did not understand answer %q, assuming No", res) 89 | return false 90 | } 91 | 92 | func readerWriter(reader io.Reader, writer io.Writer) *rw { 93 | return &rw{r: reader, w: writer} 94 | } 95 | 96 | // rw is a trivial io.ReadWriter that does not buffer. 97 | type rw struct { 98 | r io.Reader 99 | w io.Writer 100 | } 101 | 102 | func (x *rw) Read(b []byte) (int, error) { 103 | return x.r.Read(b) 104 | } 105 | func (x *rw) Write(b []byte) (int, error) { 106 | return x.w.Write(b) 107 | } 108 | 109 | // deleteChart deletes all of the Kubernetes manifests associated with this chart. 110 | func deleteChart(c *chart.Chart, ns string, dry bool, client kubectl.Runner) error { 111 | // Unknown kinds get uninstalled first because we know that core kinds 112 | // do not depend on them. 113 | for _, kind := range c.UnknownKinds(UninstallOrder) { 114 | uninstallKind(c.Kind[kind], ns, kind, dry, client) 115 | } 116 | 117 | // Uninstall all of the known kinds in a particular order. 118 | for _, kind := range UninstallOrder { 119 | uninstallKind(c.Kind[kind], ns, kind, dry, client) 120 | } 121 | 122 | return nil 123 | } 124 | 125 | func uninstallKind(kind []*manifest.Manifest, ns, ktype string, dry bool, client kubectl.Runner) { 126 | for _, o := range kind { 127 | if dry { 128 | log.Msg("%s/%s", ktype, o.Name) 129 | } else { 130 | // If it's a keeper manifest, skip uninstall. 131 | if data, err := o.VersionedObject.JSON(); err == nil { 132 | if manifest.IsKeeper(data) { 133 | log.Warn("Not uninstalling %s %s because of \"helm-keep\" annotation.\n"+ 134 | "---> Use kubectl to uninstall keeper manifests.\n", ktype, o.Name) 135 | continue 136 | } 137 | } 138 | out, err := client.Delete(o.Name, ktype, ns) 139 | if err != nil { 140 | log.Warn("Could not delete %s %s (Skipping): %s", ktype, o.Name, err) 141 | } 142 | log.Info(string(out)) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /action/uninstall_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "testing" 7 | 8 | "github.com/helm/helm-classic/kubectl" 9 | "github.com/helm/helm-classic/test" 10 | ) 11 | 12 | func TestUninstall(t *testing.T) { 13 | tests := []struct { 14 | name string // Todo: print name on fail 15 | chart string 16 | force bool 17 | expected []string 18 | client kubectl.Runner 19 | }{ 20 | { 21 | name: "with valid input", 22 | chart: "redis", 23 | force: true, 24 | expected: []string{"Running `kubectl delete` ...", "hello from redis"}, 25 | client: TestRunner{ 26 | out: []byte("hello from redis"), 27 | }, 28 | }, 29 | { 30 | name: "with a kubectl error", 31 | chart: "redis", 32 | force: true, 33 | expected: []string{"Running `kubectl delete` ...", "Could not delete Pod redis (Skipping): oh snap"}, 34 | client: TestRunner{ 35 | err: errors.New("oh snap"), 36 | }, 37 | }, 38 | { 39 | name: "with a helmc annotation", 40 | chart: "keep", 41 | force: true, 42 | expected: []string{"Running `kubectl delete` ...", "Not uninstalling", 43 | "because of \"helm-keep\" annotation"}, 44 | }, 45 | } 46 | 47 | tmpHome := test.CreateTmpHome() 48 | defer os.RemoveAll(tmpHome) 49 | test.FakeUpdate(tmpHome) 50 | 51 | for _, tt := range tests { 52 | Fetch(tt.chart, "", tmpHome) 53 | 54 | actual := test.CaptureOutput(func() { 55 | Uninstall(tt.chart, tmpHome, "default", tt.force, tt.client) 56 | }) 57 | 58 | for _, exp := range tt.expected { 59 | test.ExpectContains(t, actual, exp) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /action/update.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/Masterminds/semver" 7 | 8 | "github.com/helm/helm-classic/log" 9 | "github.com/helm/helm-classic/release" 10 | ) 11 | 12 | // Update fetches the remote repo into the home directory. 13 | func Update(home string) { 14 | home, err := filepath.Abs(home) 15 | if err != nil { 16 | log.Die("Could not generate absolute path for %q: %s", home, err) 17 | } 18 | 19 | CheckLocalPrereqs(home) 20 | 21 | rc := mustConfig(home).Repos 22 | if err := rc.UpdateAll(); err != nil { 23 | log.Die("Not all repos could be updated: %s", err) 24 | } 25 | log.Info("Done") 26 | } 27 | 28 | // CheckLatest checks whether this version of Helm Classic is the latest version. 29 | // 30 | // This does not ensure that this is the latest. If a newer version is found, 31 | // this generates a message indicating that. 32 | // 33 | // The passed-in version is the base version that will be checked against the 34 | // remote release list. 35 | func CheckLatest(version string) { 36 | ver, err := release.LatestVersion() 37 | if err != nil { 38 | log.Warn("Skipped Helm Classic version check: %s", err) 39 | return 40 | } 41 | 42 | current, err := semver.NewVersion(version) 43 | if err != nil { 44 | log.Warn("Local version %s is not well-formed", version) 45 | return 46 | } 47 | remote, err := semver.NewVersion(ver) 48 | if err != nil { 49 | log.Warn("Remote version %s is not well-formed", ver) 50 | return 51 | } 52 | 53 | if remote.GreaterThan(current) { 54 | log.Warn("A new version of Helm Classic is available. You have %s. The latest is %v", version, ver) 55 | log.Info("Download version %s by running: %s", ver, "curl -s https://get.helm.sh | bash") 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /action/update_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-github/github" 7 | "github.com/helm/helm-classic/log" 8 | "github.com/helm/helm-classic/release" 9 | "github.com/helm/helm-classic/test" 10 | ) 11 | 12 | func TestCheckLatest(t *testing.T) { 13 | setupTestCheckLatest() 14 | defer func() { 15 | release.RepoService = nil 16 | }() 17 | 18 | log.IsDebugging = true 19 | 20 | actual := test.CaptureOutput(func() { 21 | CheckLatest("0.0.1") 22 | }) 23 | 24 | test.ExpectContains(t, actual, "A new version of Helm Classic") 25 | } 26 | 27 | type MockGHRepoService struct { 28 | Release *github.RepositoryRelease 29 | } 30 | 31 | func setupTestCheckLatest() { 32 | v := "9.8.7" 33 | u := "http://example.com/latest/release" 34 | i := 987 35 | r := &github.RepositoryRelease{ 36 | TagName: &v, 37 | HTMLURL: &u, 38 | ID: &i, 39 | } 40 | release.RepoService = &MockGHRepoService{Release: r} 41 | } 42 | 43 | func (m *MockGHRepoService) GetLatestRelease(o, p string) (*github.RepositoryRelease, *github.Response, error) { 44 | return m.Release, nil, nil 45 | } 46 | -------------------------------------------------------------------------------- /chart/chart.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/helm/helm-classic/manifest" 9 | ) 10 | 11 | // Chart represents a complete chart. 12 | // 13 | // A chart consists of the following parts: 14 | // 15 | // - Chart.yaml: In code, we refer to this as the Chartfile 16 | // - manifests/*.yaml: The Kubernetes manifests 17 | // 18 | // On the Chart object, the manifests are sorted by type into a handful of 19 | // recognized Kubernetes API v1 objects. 20 | // 21 | // TODO: Investigate treating these as unversioned. 22 | type Chart struct { 23 | Chartfile *Chartfile 24 | 25 | // Kind is a map of Kind to an array of manifests. 26 | // 27 | // For example, Kind["Pod"] has an array of Pod manifests. 28 | Kind map[string][]*manifest.Manifest 29 | 30 | // Manifests is an array of Manifest objects. 31 | Manifests []*manifest.Manifest 32 | } 33 | 34 | // Load loads an entire chart. 35 | // 36 | // This includes the Chart.yaml (*Chartfile) and all of the manifests. 37 | // 38 | // If you are just reading the Chart.yaml file, it is substantially more 39 | // performant to use LoadChartfile. 40 | func Load(chart string) (*Chart, error) { 41 | if fi, err := os.Stat(chart); err != nil { 42 | return nil, err 43 | } else if !fi.IsDir() { 44 | return nil, fmt.Errorf("Chart %s is not a directory.", chart) 45 | } 46 | 47 | cf, err := LoadChartfile(filepath.Join(chart, "Chart.yaml")) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | c := &Chart{ 53 | Chartfile: cf, 54 | Kind: map[string][]*manifest.Manifest{}, 55 | } 56 | 57 | ms, err := manifest.ParseDir(chart) 58 | if err != nil { 59 | return c, err 60 | } 61 | 62 | c.attachManifests(ms) 63 | 64 | return c, nil 65 | } 66 | 67 | const ( 68 | // AnnFile is the annotation key for a file's origin. 69 | AnnFile = "chart.helm.sh/file" 70 | 71 | // AnnChartVersion is the annotation key for a chart's version. 72 | AnnChartVersion = "chart.helm.sh/version" 73 | 74 | // AnnChartDesc is the annotation key for a chart's description. 75 | AnnChartDesc = "chart.helm.sh/description" 76 | 77 | // AnnChartName is the annotation key for a chart name. 78 | AnnChartName = "chart.helm.sh/name" 79 | ) 80 | 81 | // attachManifests sorts manifests into their respective categories, adding to the Chart. 82 | func (c *Chart) attachManifests(manifests []*manifest.Manifest) { 83 | c.Manifests = manifests 84 | for _, m := range manifests { 85 | c.Kind[m.Kind] = append(c.Kind[m.Kind], m) 86 | } 87 | } 88 | 89 | // UnknownKinds returns a list of kinds that this chart contains, but which were not in the passed in array. 90 | // 91 | // A Chart will store all kinds that are given to it. This makes it possible to get a list of kinds that are not 92 | // known beforehand. 93 | func (c *Chart) UnknownKinds(known []string) []string { 94 | lookup := make(map[string]bool, len(known)) 95 | for _, k := range known { 96 | lookup[k] = true 97 | } 98 | 99 | u := []string{} 100 | for n := range c.Kind { 101 | if _, ok := lookup[n]; !ok { 102 | u = append(u, n) 103 | } 104 | } 105 | 106 | return u 107 | } 108 | -------------------------------------------------------------------------------- /chart/chart_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import "testing" 4 | 5 | const testfile = "../testdata/test-Chart.yaml" 6 | const testchart = "../testdata/charts/kitchensink" 7 | 8 | func TestLoad(t *testing.T) { 9 | c, err := Load(testchart) 10 | 11 | if err != nil { 12 | t.Errorf("Failed to load chart: %s", err) 13 | } 14 | 15 | if c.Chartfile.Name != "kitchensink" { 16 | t.Errorf("Expected chart name to be 'kitchensink'. Got '%s'.", c.Chartfile.Name) 17 | } 18 | if c.Chartfile.Dependencies[0].Version != "~10.21" { 19 | d := c.Chartfile.Dependencies[0].Version 20 | t.Errorf("Expected dependency 0 to have version '~10.21'. Got '%s'.", d) 21 | } 22 | 23 | if len(c.Kind["Pod"]) != 3 { 24 | t.Errorf("Expected 3 pods, got %d", len(c.Kind["Pod"])) 25 | } 26 | 27 | if len(c.Kind["ReplicationController"]) == 0 { 28 | t.Error("No RCs found") 29 | } 30 | 31 | if len(c.Kind["Deployment"]) == 0 { 32 | t.Error("No Deployments found") 33 | } 34 | 35 | if len(c.Kind["Namespace"]) == 0 { 36 | t.Error("No Namespaces found") 37 | } 38 | 39 | if len(c.Kind["Secret"]) == 0 { 40 | t.Error("Is it secret? Is it safe? NO!") 41 | } 42 | 43 | if len(c.Kind["ConfigMap"]) == 0 { 44 | t.Error("No ConfigMaps found.") 45 | } 46 | 47 | if len(c.Kind["PersistentVolume"]) == 0 { 48 | t.Error("No volumes.") 49 | } 50 | 51 | if len(c.Kind["Service"]) == 0 { 52 | t.Error("No service. Just like [insert mobile provider name here]") 53 | } 54 | } 55 | 56 | func TestLoadChart(t *testing.T) { 57 | f, err := LoadChartfile(testfile) 58 | if err != nil { 59 | t.Errorf("Error loading %s: %s", testfile, err) 60 | } 61 | 62 | if f.Name != "alpine-pod" { 63 | t.Errorf("Expected alpine-pod, got %s", f.Name) 64 | } 65 | 66 | if len(f.Maintainers) != 2 { 67 | t.Errorf("Expected 2 maintainers, got %d", len(f.Maintainers)) 68 | } 69 | 70 | if len(f.Dependencies) != 2 { 71 | t.Errorf("Expected 2 dependencies, got %d", len(f.Dependencies)) 72 | } 73 | 74 | if f.Dependencies[1].Name != "bar" { 75 | t.Errorf("Expected second dependency to be bar: %q", f.Dependencies[1].Name) 76 | } 77 | 78 | if f.PreInstall["mykeys"] != "generate-keypair foo" { 79 | t.Errorf("Expected map value for mykeys.") 80 | } 81 | 82 | if f.Source[0] != "https://example.com/helm" { 83 | t.Errorf("Expected https://example.com/helm, got %s", f.Source) 84 | } 85 | } 86 | 87 | func TestVersionOK(t *testing.T) { 88 | f, err := LoadChartfile(testfile) 89 | if err != nil { 90 | t.Errorf("Error loading %s: %s", testfile, err) 91 | } 92 | 93 | // These are canaries. The SemVer package exhuastively tests the 94 | // various permutations. This will alert us if we wired it up 95 | // incorrectly. 96 | 97 | d := f.Dependencies[1] 98 | if d.VersionOK("1.0.0") { 99 | t.Errorf("1.0.0 should have been marked out of range") 100 | } 101 | 102 | if !d.VersionOK("1.2.3") { 103 | t.Errorf("Version 1.2.3 should have been marked in-range") 104 | } 105 | 106 | } 107 | 108 | func TestUnknownKinds(t *testing.T) { 109 | known := []string{"Pod"} 110 | c, err := Load(testchart) 111 | if err != nil { 112 | t.Errorf("Failed to load chart: %s", err) 113 | } 114 | 115 | unknown := c.UnknownKinds(known) 116 | if len(unknown) < 5 { 117 | t.Errorf("Expected at least 5 unknown chart types, got %d.", len(unknown)) 118 | } 119 | 120 | for _, k := range unknown { 121 | if k == "Pod" { 122 | t.Errorf("Pod is not an unknown kind.") 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /chart/chartfile.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/Masterminds/semver" 10 | "github.com/helm/helm-classic/log" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | // Chartfile describes a Helm Chart (e.g. Chart.yaml) 15 | type Chartfile struct { 16 | Name string `yaml:"name"` 17 | From *Dependency `yaml:"from,omitempty"` 18 | Home string `yaml:"home"` 19 | Source []string `yaml:"source,omitempty"` 20 | Version string `yaml:"version"` 21 | Description string `yaml:"description"` 22 | Maintainers []string `yaml:"maintainers,omitempty"` 23 | Details string `yaml:"details,omitempty"` 24 | Dependencies []*Dependency `yaml:"dependencies,omitempty"` 25 | PreInstall map[string]string `yaml:"preinstall,omitempty"` 26 | } 27 | 28 | // Dependency describes a specific dependency. 29 | type Dependency struct { 30 | Name string `yaml:"name"` 31 | Version string `yaml:"version"` 32 | Repo string `yaml:"repo,omitempty"` 33 | } 34 | 35 | // LoadChartfile loads a Chart.yaml file into a *Chart. 36 | func LoadChartfile(filename string) (*Chartfile, error) { 37 | b, err := ioutil.ReadFile(filename) 38 | if err != nil { 39 | return nil, err 40 | } 41 | var y Chartfile 42 | return &y, yaml.Unmarshal(b, &y) 43 | } 44 | 45 | // Save saves a Chart.yaml file 46 | func (c *Chartfile) Save(filename string) error { 47 | b, err := yaml.Marshal(c) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return ioutil.WriteFile(filename, b, 0644) 53 | } 54 | 55 | // RepoName gets the name of the Git repo, or an empty string if none is found. 56 | func RepoName(chartpath string) string { 57 | wd, err := os.Getwd() 58 | if err != nil { 59 | log.Err("Could not get working directory: %s", err) 60 | return "" 61 | } 62 | defer func() { 63 | if err := os.Chdir(wd); err != nil { 64 | log.Die("Unrecoverable error: %s", err) 65 | } 66 | }() 67 | 68 | if err := os.Chdir(chartpath); err != nil { 69 | log.Err("Could not find chartpath %s: %s", chartpath, err) 70 | return "" 71 | } 72 | 73 | out, err := exec.Command("git", "config", "--get", "remote.origin.url").CombinedOutput() 74 | if err != nil { 75 | log.Err("Git failed to get the origin name: %s %s", err, string(out)) 76 | return "" 77 | } 78 | 79 | return strings.TrimSpace(string(out)) 80 | } 81 | 82 | // VersionOK returns true if the given version meets the constraints. 83 | // 84 | // It returns false if the version string or constraint is unparsable or if the 85 | // version does not meet the constraint. 86 | func (d *Dependency) VersionOK(version string) bool { 87 | c, err := semver.NewConstraint(d.Version) 88 | if err != nil { 89 | return false 90 | } 91 | v, err := semver.NewVersion(version) 92 | if err != nil { 93 | return false 94 | } 95 | 96 | return c.Check(v) 97 | } 98 | -------------------------------------------------------------------------------- /chart/chartfile_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestRepoName(t *testing.T) { 9 | name := RepoName(".") 10 | if name == "" { 11 | t.Errorf("Expected a git URL.") 12 | } 13 | if !strings.HasSuffix(name, ".git") { 14 | t.Errorf("Expected %s to end with '.git'", name) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cli/create.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | ) 7 | 8 | const createDescription = `This will scaffold a new chart named 'chart-name' in your 9 | local workdir. To edit the resulting chart, you may edit the files directly or 10 | use the 'helmc edit' command.` 11 | 12 | var createCmd = cli.Command{ 13 | Name: "create", 14 | Usage: "Create a chart in the local workspace.", 15 | Description: createDescription, 16 | ArgsUsage: "[chart-name]", 17 | Action: func(c *cli.Context) { 18 | minArgs(c, 1, "create") 19 | action.Create(c.Args()[0], home(c)) 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /cli/doctor.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | ) 7 | 8 | const doctorDescription = `This will run a series of checks to ensure that your 9 | experience with helmc is trouble-free.` 10 | 11 | var doctorCmd = cli.Command{ 12 | Name: "doctor", 13 | Usage: "Run a series of checks to surface possible problems", 14 | Description: doctorDescription, 15 | ArgsUsage: "", 16 | Action: func(c *cli.Context) { 17 | action.Doctor(home(c)) 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /cli/edit.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | ) 7 | 8 | const editDescription = `Existing charts in the workspace can be edited using this command. 9 | 'helmc edit' will open all of the chart files in a single editor (as specified 10 | by the $EDITOR environment variable).` 11 | 12 | var editCmd = cli.Command{ 13 | Name: "edit", 14 | Usage: "Edit a named chart in the local workspace.", 15 | Description: editDescription, 16 | ArgsUsage: "[chart-name]", 17 | Action: func(c *cli.Context) { 18 | minArgs(c, 1, "edit") 19 | action.Edit(c.Args()[0], home(c)) 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /cli/fetch.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | ) 7 | 8 | const fetchDescription = `Copy a chart from the Chart repository to a local workspace. 9 | From this point, the copied chart may be safely modified to your needs. 10 | 11 | If an optional 'chart-name' is specified, the chart will be copied to a directory 12 | of that name. For example, 'helmc fetch nginx www' will copy the the contents of 13 | the 'nginx' chart into a directory named 'www' in your workspace.` 14 | 15 | var fetchCmd = cli.Command{ 16 | Name: "fetch", 17 | Usage: "Fetch a Chart to your working directory.", 18 | Description: fetchDescription, 19 | ArgsUsage: "[chart] [chart-name]", 20 | Action: fetch, 21 | Flags: []cli.Flag{ 22 | cli.StringFlag{ 23 | Name: "namespace, n", 24 | Value: "default", 25 | Usage: "The Kubernetes destination namespace.", 26 | }, 27 | }, 28 | } 29 | 30 | func fetch(c *cli.Context) { 31 | home := home(c) 32 | minArgs(c, 1, "fetch") 33 | 34 | a := c.Args() 35 | chart := a[0] 36 | 37 | var lname string 38 | if len(a) == 2 { 39 | lname = a[1] 40 | } 41 | 42 | action.Fetch(chart, lname, home) 43 | } 44 | -------------------------------------------------------------------------------- /cli/generate.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | ) 7 | 8 | var generateDesc = `Read a chart and generate manifests based on generator tools. 9 | 10 | 'helmc generate' reads all of the files in a chart, searching for a generation 11 | header. If it finds the header, it will execute the corresponding command. 12 | 13 | The header is in the form '#helm:generate CMD [ARGS]'. 'CMD' can be any command 14 | that Helm Classic finds on $PATH. Optional 'ARGS' are arguments that will be passed on 15 | to the command. Generator commands can begin with any of the following sequences: 16 | '#helm:generate', '//helm:generate', or '/*helm:generate'. 17 | 18 | For example, to embed a generate instruction in a YAML file, one may do the 19 | following: 20 | 21 | #helm:generate helm tpl mytemplate.yaml 22 | 23 | SPECIAL NOTE: For compatibility with older charts, Helm Classic will translate the 'helm' command 24 | used within any generator header to the equivalent 'helmc' command. 25 | 26 | If CMD is an absolute path, Helm Classic will attempt to execute it even if it is not 27 | on $PATH. Combined with the $HELM_GENERATE_DIR environment variable, charts can 28 | include their own local scripts: 29 | 30 | #helm:generate $HELM_GENERATE_DIR/myscript.sh 31 | 32 | Since '#' is a comment character in YAML, the YAML parser will ignore the 33 | generator line. But 'helm:generate' will read it as specifying that the following 34 | command should be run: 35 | 36 | helmc tpl mytemplate.yaml 37 | 38 | While the 'helmc tpl' command can easily be used in conjunction with the 39 | 'helmc generate' command, you are not limited to just this tool. For example, one 40 | could run a sed substitution just as easily: 41 | 42 | #helm:generate sed -i -e s|ubuntu-debootstrap|fluffy-bunny| my/pod.yaml 43 | 44 | Note that 'helmc generate' does not execute inside of a shell. However, it does 45 | expand environment variables. The following variables are made available by the 46 | Helm Classic system: 47 | 48 | - HELM_HOME: The Helm home directory 49 | - HELM_DEFAULT_REPO: The repository alias for the default repository. 50 | - HELM_GENERATE_FILE: The present file's name 51 | - HELM_GENERATE_DIR: The absolute path to the chart directory of the present chart 52 | 53 | SPECIAL NOTE: For compatibility with older charts, Helm Classic honors these old, "special" 54 | variables and does not replace them with 'HELMC_*' equivalents. 55 | 56 | By default, 'helmc generate' will execute every generator that it finds in a 57 | project. Generators can be mixed, with different files using different 58 | generators. The order of generation is the order in which the directory contents 59 | are listed. 60 | 61 | The environment variables listed above are also available to generators. 62 | 63 | For charts that contain multiple different generator template sets, you may 64 | prevent generators from being run using the '--exclude' flag: 65 | 66 | $ helmc generate --exclude=tpl --exclude=sed foo 67 | 68 | The above will prevent the generator from traversing the 'foo' chart's 'tpl/' 69 | or 'sed/' directories. 70 | ` 71 | 72 | var generateCmd = cli.Command{ 73 | Name: "generate", 74 | Usage: "Run the generator over the given chart.", 75 | ArgsUsage: "[chart-name]", 76 | Description: generateDesc, 77 | Flags: []cli.Flag{ 78 | cli.StringSliceFlag{ 79 | Name: "exclude,x", 80 | Usage: "Files or directories to exclude from this run, relative to the chart.", 81 | }, 82 | cli.BoolFlag{ 83 | Name: "force,f", 84 | Usage: "Force an overwrite if files already exist when generating manifests.", 85 | }, 86 | }, 87 | Action: func(c *cli.Context) { 88 | home := home(c) 89 | minArgs(c, 1, "generate") 90 | force := c.Bool("force") 91 | a := c.Args() 92 | chart := a[0] 93 | action.Generate(chart, home, c.StringSlice("exclude"), force) 94 | }, 95 | } 96 | -------------------------------------------------------------------------------- /cli/helm.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/codegangsta/cli" 7 | "github.com/helm/helm-classic/action" 8 | "github.com/helm/helm-classic/log" 9 | ) 10 | 11 | // version is the version of the app. 12 | // 13 | // This value is overwritten by the linker during build. The default version 14 | // here is SemVer 2, but basically indicates that this was a one-off build 15 | // and should not be trusted. 16 | var version = "0.1.0" 17 | 18 | const globalUsage = `Helm Classic - A Kubernetes package manager 19 | 20 | To begin working with Helm Classic, run the 'helmc update' command: 21 | 22 | $ helmc update 23 | 24 | This will download all of the necessary data. Common actions from this point 25 | include: 26 | 27 | - helmc help COMMAND: see help for a specific command 28 | - helmc search: search for charts 29 | - helmc fetch: make a local working copy of a chart 30 | - helmc install: upload the chart to Kubernetes 31 | 32 | For more information on Helm Classic, go to http://helm.sh. 33 | 34 | ENVIRONMENT: 35 | $HELMC_HOME: Set an alternative location for Helm files. By default, these 36 | are stored in ~/.helmc 37 | 38 | ` 39 | 40 | // Cli is the main entrypoint for the Helm Classic CLI. 41 | func Cli() *cli.App { 42 | app := cli.NewApp() 43 | app.Name = "helmc" 44 | app.Usage = globalUsage 45 | app.Version = version 46 | app.EnableBashCompletion = true 47 | app.After = func(c *cli.Context) error { 48 | if log.ErrorState { 49 | return errors.New("Exiting with errors") 50 | } 51 | return nil 52 | } 53 | 54 | app.Flags = []cli.Flag{ 55 | cli.StringFlag{ 56 | Name: "home", 57 | Value: "$HOME/.helmc", 58 | Usage: "The location of your Helm Classic files", 59 | EnvVar: "HELMC_HOME", 60 | }, 61 | cli.BoolFlag{ 62 | Name: "debug", 63 | Usage: "Enable verbose debugging output", 64 | }, 65 | } 66 | 67 | app.Commands = []cli.Command{ 68 | createCmd, 69 | doctorCmd, 70 | editCmd, 71 | fetchCmd, 72 | homeCmd, 73 | infoCmd, 74 | installCmd, 75 | lintCmd, 76 | listCmd, 77 | publishCmd, 78 | removeCmd, 79 | repositoryCmd, 80 | searchCmd, 81 | targetCmd, 82 | uninstallCmd, 83 | updateCmd, 84 | generateCmd, 85 | tplCmd, 86 | } 87 | 88 | app.CommandNotFound = func(c *cli.Context, command string) { 89 | if action.HasPlugin(command) { 90 | action.Plugin(home(c), command, c.Args()) 91 | return 92 | } 93 | log.Err("No matching command '%s'", command) 94 | cli.ShowAppHelp(c) 95 | log.Die("") 96 | } 97 | 98 | app.Before = func(c *cli.Context) error { 99 | log.IsDebugging = c.Bool("debug") 100 | return nil 101 | } 102 | 103 | return app 104 | } 105 | -------------------------------------------------------------------------------- /cli/home.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/log" 6 | ) 7 | 8 | var homeCmd = cli.Command{ 9 | Name: "home", 10 | Usage: "Displays the location of the Helm Classic home.", 11 | ArgsUsage: "", 12 | Action: func(c *cli.Context) { 13 | log.Msg(home(c)) 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /cli/info.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | ) 7 | 8 | var infoCmd = cli.Command{ 9 | Name: "info", 10 | Usage: "Print information about a Chart.", 11 | ArgsUsage: "[string]", 12 | Flags: []cli.Flag{ 13 | cli.StringFlag{ 14 | Name: "format", 15 | Usage: "Print using a Go template", 16 | }, 17 | }, 18 | Action: func(c *cli.Context) { 19 | minArgs(c, 1, "info") 20 | action.Info(c.Args()[0], home(c), c.String("format")) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cli/install.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | "github.com/helm/helm-classic/kubectl" 7 | ) 8 | 9 | const installDescription = `If the given 'chart-name' is present in your workspace, it 10 | will be uploaded into Kubernetes. If no chart named 'chart-name' is found in 11 | your workspace, Helm Classic will look for a chart with that name, install it into the 12 | workspace, and then immediately upload it to Kubernetes. 13 | 14 | When multiple charts are specified, Helm Classic will attempt to install all of them, 15 | following the resolution process described above. 16 | ` 17 | 18 | var installCmd = cli.Command{ 19 | Name: "install", 20 | Usage: "Install a named package into Kubernetes.", 21 | Description: installDescription, 22 | ArgsUsage: "[chart-name...]", 23 | Action: install, 24 | Flags: []cli.Flag{ 25 | cli.StringFlag{ 26 | Name: "namespace, n", 27 | Value: "", 28 | Usage: "The Kubernetes destination namespace.", 29 | }, 30 | cli.BoolFlag{ 31 | Name: "force, aye-aye", 32 | Usage: "Perform install even if dependencies are unsatisfied.", 33 | }, 34 | cli.BoolFlag{ 35 | Name: "dry-run", 36 | Usage: "Fetch the chart, but only display the underlying kubectl commands.", 37 | }, 38 | cli.BoolFlag{ 39 | Name: "generate,g", 40 | Usage: "Run the generator before installing.", 41 | }, 42 | cli.StringSliceFlag{ 43 | Name: "exclude,x", 44 | Usage: "Files or directories to exclude from the generator (if -g is set).", 45 | }, 46 | }, 47 | } 48 | 49 | func install(c *cli.Context) { 50 | minArgs(c, 1, "install") 51 | h := home(c) 52 | force := c.Bool("force") 53 | 54 | client := kubectl.Client 55 | if c.Bool("dry-run") { 56 | client = kubectl.PrintRunner{} 57 | } 58 | 59 | for _, chart := range c.Args() { 60 | action.Install(chart, h, c.String("namespace"), force, c.Bool("generate"), c.StringSlice("exclude"), client) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cli/lint.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/codegangsta/cli" 8 | "github.com/helm/helm-classic/action" 9 | "github.com/helm/helm-classic/util" 10 | ) 11 | 12 | var lintCmd = cli.Command{ 13 | Name: "lint", 14 | Usage: "Validates given chart", 15 | ArgsUsage: "[chart-name]", 16 | Action: lint, 17 | Flags: []cli.Flag{ 18 | cli.BoolFlag{ 19 | Name: "all", 20 | Usage: "Check all available charts", 21 | }, 22 | }, 23 | } 24 | 25 | func lint(c *cli.Context) { 26 | home := home(c) 27 | 28 | all := c.Bool("all") 29 | 30 | if all { 31 | action.LintAll(home) 32 | return 33 | } 34 | 35 | minArgs(c, 1, "lint") 36 | 37 | a := c.Args() 38 | chartNameOrPath := a[0] 39 | 40 | fromHome := util.WorkspaceChartDirectory(home, chartNameOrPath) 41 | fromAbs := filepath.Clean(chartNameOrPath) 42 | 43 | _, err := os.Stat(fromAbs) 44 | 45 | if err == nil { 46 | action.Lint(fromAbs) 47 | } else { 48 | action.Lint(fromHome) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cli/lint_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/helm/helm-classic/action" 9 | "github.com/helm/helm-classic/test" 10 | "github.com/helm/helm-classic/util" 11 | ) 12 | 13 | func TestLintAllNone(t *testing.T) { 14 | tmpHome := test.CreateTmpHome() 15 | test.FakeUpdate(tmpHome) 16 | 17 | output := test.CaptureOutput(func() { 18 | Cli().Run([]string{"helmc", "--home", tmpHome, "lint", "--all"}) 19 | }) 20 | 21 | test.ExpectContains(t, output, fmt.Sprintf("Could not find any charts in \"%s", tmpHome)) 22 | } 23 | 24 | func TestLintSingle(t *testing.T) { 25 | tmpHome := test.CreateTmpHome() 26 | test.FakeUpdate(tmpHome) 27 | 28 | chartName := "goodChart" 29 | action.Create(chartName, tmpHome) 30 | 31 | output := test.CaptureOutput(func() { 32 | Cli().Run([]string{"helmc", "--home", tmpHome, "lint", chartName}) 33 | }) 34 | 35 | test.ExpectContains(t, output, fmt.Sprintf("Chart [%s] has passed all necessary checks", chartName)) 36 | } 37 | 38 | func TestLintChartByPath(t *testing.T) { 39 | home1 := test.CreateTmpHome() 40 | home2 := test.CreateTmpHome() 41 | 42 | chartName := "goodChart" 43 | action.Create(chartName, home1) 44 | 45 | output := test.CaptureOutput(func() { 46 | Cli().Run([]string{"helmc", "--home", home2, "lint", util.WorkspaceChartDirectory(home1, chartName)}) 47 | }) 48 | 49 | test.ExpectContains(t, output, fmt.Sprintf("Chart [%s] has passed all necessary checks", chartName)) 50 | } 51 | 52 | func TestLintAll(t *testing.T) { 53 | tmpHome := test.CreateTmpHome() 54 | test.FakeUpdate(tmpHome) 55 | 56 | missingReadmeChart := "missingReadme" 57 | 58 | action.Create(missingReadmeChart, tmpHome) 59 | os.Remove(util.WorkspaceChartDirectory(tmpHome, missingReadmeChart, "README.md")) 60 | 61 | action.Create("goodChart", tmpHome) 62 | 63 | output := test.CaptureOutput(func() { 64 | Cli().Run([]string{"helmc", "--home", tmpHome, "lint", "--all"}) 65 | }) 66 | 67 | test.ExpectMatches(t, output, "A README file was not found.*"+missingReadmeChart) 68 | test.ExpectContains(t, output, "Chart [goodChart] has passed all necessary checks") 69 | test.ExpectContains(t, output, "Chart [missingReadme] failed some checks") 70 | } 71 | -------------------------------------------------------------------------------- /cli/list.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | ) 7 | 8 | const listDescription = `This prints all of the packages that are currently installed in 9 | the workspace. Packages are printed by the local name. 10 | ` 11 | 12 | var listCmd = cli.Command{ 13 | Name: "list", 14 | Aliases: []string{"ls"}, 15 | Usage: "List all fetched packages.", 16 | Description: listDescription, 17 | ArgsUsage: "", 18 | Action: func(c *cli.Context) { 19 | action.List(home(c)) 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /cli/publish.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | ) 7 | 8 | const publishDescription = `This copies a chart from the workdir into the cache. Doing so 9 | is the first stage of contributing a chart upstream. 10 | ` 11 | 12 | var publishCmd = cli.Command{ 13 | Name: "publish", 14 | Usage: "Publish a named chart to the git checkout.", 15 | Description: publishDescription, 16 | ArgsUsage: "[chart-name]", 17 | Action: func(c *cli.Context) { 18 | minArgs(c, 1, "publish") 19 | action.Publish(c.Args()[0], home(c), c.String("repo"), c.Bool("force")) 20 | }, 21 | Flags: []cli.Flag{ 22 | cli.BoolFlag{ 23 | Name: "force, f", 24 | Usage: "Force publish over an existing chart.", 25 | }, 26 | cli.StringFlag{ 27 | Name: "repo", 28 | Usage: "Publish to a specific chart repository.", 29 | }, 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /cli/remove.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | ) 7 | 8 | var removeCmd = cli.Command{ 9 | Name: "remove", 10 | Aliases: []string{"rm"}, 11 | Usage: "Remove one or more Charts from your working directory.", 12 | ArgsUsage: "[chart-name] [...]", 13 | Action: remove, 14 | Flags: []cli.Flag{ 15 | cli.BoolFlag{ 16 | Name: "force", 17 | Usage: "Remove Chart from working directory and leave packages installed.", 18 | }, 19 | }, 20 | } 21 | 22 | func remove(c *cli.Context) { 23 | minArgs(c, 1, "remove") 24 | h := home(c) 25 | force := c.Bool("force") 26 | 27 | a := c.Args() 28 | for _, chart := range a { 29 | action.Remove(chart, h, force) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /cli/repository.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | ) 7 | 8 | var repositoryCmd = cli.Command{ 9 | Name: "repository", 10 | Aliases: []string{"repo"}, 11 | Usage: "Work with other Chart repositories.", 12 | Subcommands: []cli.Command{ 13 | { 14 | Name: "add", 15 | Usage: "Add a remote chart repository.", 16 | ArgsUsage: "[name] [git url]", 17 | Action: func(c *cli.Context) { 18 | minArgs(c, 2, "add") 19 | a := c.Args() 20 | action.AddRepo(home(c), a[0], a[1]) 21 | }, 22 | }, 23 | { 24 | Name: "list", 25 | Aliases: []string{"ls"}, 26 | Usage: "List all remote chart repositories.", 27 | Action: func(c *cli.Context) { 28 | action.ListRepos(home(c)) 29 | }, 30 | }, 31 | { 32 | Name: "remove", 33 | Aliases: []string{"rm"}, 34 | Usage: "Remove a remote chart repository.", 35 | ArgsUsage: "[name] [git url]", 36 | Action: func(c *cli.Context) { 37 | minArgs(c, 1, "remove") 38 | action.DeleteRepo(home(c), c.Args()[0]) 39 | }, 40 | }, 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /cli/search.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | ) 7 | 8 | const searchDescription = `This provides a simple interface for searching the chart cache 9 | for charts matching a given pattern. 10 | 11 | If no string is provided, or if the special string '*' is provided, this will 12 | list all available charts. 13 | ` 14 | 15 | var searchCmd = cli.Command{ 16 | Name: "search", 17 | Usage: "Search for a package.", 18 | Description: searchDescription, 19 | ArgsUsage: "[string]", 20 | Action: search, 21 | Flags: []cli.Flag{ 22 | cli.BoolFlag{ 23 | Name: "regexp,r", 24 | Usage: "Use a regular expression instead of a substring match.", 25 | }, 26 | }, 27 | } 28 | 29 | func search(c *cli.Context) { 30 | term := "" 31 | if len(c.Args()) > 0 { 32 | term = c.Args()[0] 33 | } 34 | action.Search(term, home(c), c.Bool("regexp")) 35 | } 36 | -------------------------------------------------------------------------------- /cli/target.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | "github.com/helm/helm-classic/kubectl" 7 | ) 8 | 9 | var targetCmd = cli.Command{ 10 | Name: "target", 11 | Usage: "Displays information about cluster.", 12 | ArgsUsage: "", 13 | Action: func(c *cli.Context) { 14 | client := kubectl.Client 15 | if c.Bool("dry-run") { 16 | client = kubectl.PrintRunner{} 17 | } 18 | action.Target(client) 19 | }, 20 | Flags: []cli.Flag{ 21 | cli.BoolFlag{ 22 | Name: "dry-run", 23 | Usage: "Only display the underlying kubectl commands.", 24 | }, 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /cli/template.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | "github.com/helm/helm-classic/log" 7 | ) 8 | 9 | const tplDescription = `Execute a template inside of a chart. 10 | 11 | This command is not intended to be run directly (though it can be). Instead, it 12 | is a helper for the generate command. Run 'helmc help generate' for more. 13 | 14 | 'helmc template' provides a default implementation of a templating feature for 15 | Kubernetes manifests. Other more sophisticated methods can be plugged in using 16 | the 'helmc generate' system. 17 | 18 | 'helmc template' uses Go's built-in text template system to provide template 19 | substitution inside of a chart. In addition to the built-in template commands, 20 | 'helmc template' supports all of the template functions provided by the Sprig 21 | library (https://github.com/Masterminds/sprig). 22 | 23 | If a values data file is provided, 'helmc template' will use that as a source 24 | for values. If none is specified, only default values will be used. Helm Classic uses 25 | simple extension scanning to determine the file type of the values data file. 26 | 27 | - YAML: .yaml, .yml 28 | - TOML: .toml 29 | - JSON: .json 30 | 31 | If an output file is specified, the results will be written to the output 32 | file instead of STDOUT. Writing to the source template file is unsupported. 33 | (In other words, don't set the source and output to the same file.) 34 | ` 35 | 36 | // tplCmd is the command to handle templating. 37 | // helmc tpl -o dest.txt -d data.toml my_template.tpl 38 | var tplCmd = cli.Command{ 39 | Name: "template", 40 | Aliases: []string{"tpl"}, 41 | Usage: "Run a template command on a file.", 42 | ArgsUsage: "[file]", 43 | Flags: []cli.Flag{ 44 | cli.StringFlag{ 45 | Name: "out,o", 46 | Usage: "The destination file. If unset, results are written to STDOUT.", 47 | }, 48 | cli.StringFlag{ 49 | Name: "values,d", 50 | Usage: "A file containing values to substitute into the template. TOML (.toml), JSON (.json), and YAML (.yaml, .yml) are supported.", 51 | }, 52 | cli.BoolFlag{ 53 | Name: "force,f", 54 | Usage: "Forces to overwrite an exiting file", 55 | }, 56 | }, 57 | Action: func(c *cli.Context) { 58 | minArgs(c, 1, "template") 59 | 60 | a := c.Args() 61 | force := c.Bool("force") 62 | filename := a[0] 63 | err := action.Template(c.String("out"), filename, c.String("values"), force) 64 | if err != nil { 65 | log.Die(err.Error()) 66 | } 67 | }, 68 | } 69 | -------------------------------------------------------------------------------- /cli/uninstall.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | "github.com/helm/helm-classic/kubectl" 7 | ) 8 | 9 | const uninstallDescription = `For each supplied 'chart-name', this will connect to Kubernetes 10 | and remove all of the manifests specified. 11 | 12 | This will not alter the charts in your workspace. 13 | ` 14 | 15 | var uninstallCmd = cli.Command{ 16 | Name: "uninstall", 17 | Usage: "Uninstall a named package from Kubernetes.", 18 | Description: uninstallDescription, 19 | ArgsUsage: "[chart-name...]", 20 | Action: func(c *cli.Context) { 21 | minArgs(c, 1, "uninstall") 22 | 23 | client := kubectl.Client 24 | for _, chart := range c.Args() { 25 | action.Uninstall(chart, home(c), c.String("namespace"), c.Bool("force"), client) 26 | } 27 | }, 28 | Flags: []cli.Flag{ 29 | cli.StringFlag{ 30 | Name: "namespace, n", 31 | Value: "", 32 | Usage: "The Kubernetes destination namespace.", 33 | }, 34 | cli.BoolFlag{ 35 | Name: "force, aye-aye, y", 36 | Usage: "Do not ask for confirmation.", 37 | }, 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /cli/update.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "github.com/helm/helm-classic/action" 6 | ) 7 | 8 | const updateDescription = `This will synchronize the local repository with the upstream GitHub project. 9 | The local cached copy is stored in '~/.helmc/cache' or (if specified) 10 | '$HELMC_HOME/cache'. 11 | 12 | The first time 'helmc update' is run, the necessary directory structures are 13 | created and then the Git repository is pulled in full. 14 | 15 | Subsequent calls to 'helmc update' will simply synchronize the local cache 16 | with the remote.` 17 | 18 | // updateCmd represents the CLI command for fetching the latest version of all charts from Github. 19 | var updateCmd = cli.Command{ 20 | Name: "update", 21 | Aliases: []string{"up"}, 22 | Usage: "Get the latest version of all Charts from GitHub.", 23 | Description: updateDescription, 24 | ArgsUsage: "", 25 | Action: func(c *cli.Context) { 26 | if !c.Bool("no-version-check") { 27 | action.CheckLatest(version) 28 | } 29 | action.Update(home(c)) 30 | }, 31 | Flags: []cli.Flag{ 32 | cli.BoolFlag{ 33 | Name: "no-version-check", 34 | Usage: "Disable Helm Classic's automatic check for newer versions of itself.", 35 | }, 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /cli/util.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/codegangsta/cli" 7 | "github.com/helm/helm-classic/log" 8 | ) 9 | 10 | // home runs the --home flag through os.ExpandEnv. 11 | func home(c *cli.Context) string { 12 | return os.ExpandEnv(c.GlobalString("home")) 13 | } 14 | 15 | // minArgs checks to see if the right number of args are passed. 16 | // 17 | // If not, it prints an error and quits. 18 | func minArgs(c *cli.Context, i int, name string) { 19 | if len(c.Args()) < i { 20 | m := "arguments" 21 | if i == 1 { 22 | m = "argument" 23 | } 24 | log.Err("Expected %d %s", i, m) 25 | cli.ShowCommandHelp(c, name) 26 | log.Die("") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /codec/codec.go: -------------------------------------------------------------------------------- 1 | // Package codec provides a JSON/YAML codec for Manifests. 2 | // 3 | // Usage: 4 | // 5 | // // Decode one manifest from a JSON file. 6 | // man, err := JSON.Decode(b).One() 7 | // // Decode all of the manifests out of this file. 8 | // manifests, err := YAML.Decode(b).All() 9 | // err := YAML.Encode(filename).One("hello") 10 | // // Encode multiple objects to one file (as separate docs). 11 | // err := YAML.Encode(filename).All("one", "two", "three") 12 | package codec 13 | 14 | import ( 15 | "io" 16 | "io/ioutil" 17 | ) 18 | 19 | // JSON is the default JSON encoder/decoder. 20 | var JSON jsonCodec 21 | 22 | // YAML is the default YAML encoder/decoder. 23 | var YAML yamlCodec 24 | 25 | // Encoder describes something capable of encoding to a given format. 26 | // 27 | // An Encoder should be able to encode one object to an output stream, or 28 | // many objects to an output stream. 29 | // 30 | // For example, a single YAML file can contain multiple YAML objects, and 31 | // a single JSONList file can contain many JSON objects. 32 | type Encoder interface { 33 | // Write one object to one file 34 | One(interface{}) error 35 | // Write all objects to one file 36 | All(...interface{}) error 37 | } 38 | 39 | // Decoder decodes an encoded representation into one or many objects. 40 | type Decoder interface { 41 | // Get one object from a file. 42 | One() (*Object, error) 43 | // Get all objects from a file. 44 | All() ([]*Object, error) 45 | } 46 | 47 | // Codec has an encoder and a decoder for a particular encoding. 48 | type Codec interface { 49 | Encode(io.Writer) Encoder 50 | Decode([]byte) Decoder 51 | } 52 | 53 | // DecodeFile returns a decoder pre-populated with the file contents. 54 | func DecodeFile(filename string, c Codec) (Decoder, error) { 55 | data, err := ioutil.ReadFile(filename) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return c.Decode(data), nil 60 | } 61 | -------------------------------------------------------------------------------- /codec/json.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | ) 9 | 10 | type jsonCodec struct{} 11 | 12 | func (c jsonCodec) Encode(dest io.Writer) Encoder { 13 | return &jsonEncoder{ 14 | out: dest, 15 | } 16 | } 17 | 18 | func (c jsonCodec) Decode(b []byte) Decoder { 19 | return &jsonDecoder{data: b} 20 | } 21 | 22 | type jsonEncoder struct { 23 | out io.Writer 24 | } 25 | 26 | func (e *jsonEncoder) One(v interface{}) error { 27 | data, err := json.MarshalIndent(v, "", " ") 28 | if err != nil { 29 | return err 30 | } 31 | e.out.Write(data) 32 | 33 | return nil 34 | } 35 | 36 | // All() encodes multiple JSON objects into one file. 37 | // Right now this encodes to JSONLines. 38 | func (e *jsonEncoder) All(vs ...interface{}) error { 39 | for _, v := range vs { 40 | data, err := json.Marshal(v) 41 | if err != nil { 42 | return err 43 | } 44 | e.out.Write(data) 45 | e.out.Write([]byte("\n")) 46 | } 47 | return nil 48 | } 49 | 50 | type jsonDecoder struct { 51 | data []byte 52 | } 53 | 54 | // All returns all documents in the original. 55 | // JSON does not really support multi-doc, so we try JSON decoding, then fall 56 | // back on JSONList decoding. 57 | // 58 | // Decoding using All will make up to two decoding passes on your data before 59 | // it determines which decoder to use (so it can easily be more than 2). 60 | func (d jsonDecoder) All() ([]*Object, error) { 61 | var phony interface{} 62 | if err := json.Unmarshal(d.data, &phony); err == nil { 63 | // We have a single JSON document. 64 | return []*Object{{data: d.data, dec: jdec}}, nil 65 | } 66 | 67 | lines := bytes.Split(d.data, []byte("\n")) 68 | 69 | // If it is 0, it's not JSON. If it's 1, it should have parsed. 70 | if len(lines) < 2 { 71 | return []*Object{}, errors.New("Data is neither JSON nor JSONL. (linecount)") 72 | } 73 | 74 | println(string(lines[0])) 75 | if err := json.Unmarshal(lines[0], &phony); err != nil { 76 | // Whoops.. failed again. 77 | return []*Object{}, errors.New("Data is neither JSON nor JSONL: " + err.Error()) 78 | } 79 | 80 | buf := make([]*Object, len(lines)) 81 | for i, l := range lines { 82 | buf[i] = &Object{data: l, dec: jdec} 83 | } 84 | return buf, nil 85 | } 86 | 87 | func (d jsonDecoder) One() (*Object, error) { 88 | return &Object{ 89 | data: d.data, 90 | dec: jdec, 91 | }, nil 92 | } 93 | 94 | func jdec(b []byte, v interface{}) error { 95 | return json.Unmarshal(b, v) 96 | } 97 | -------------------------------------------------------------------------------- /codec/json_test.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "path" 7 | "testing" 8 | ) 9 | 10 | func TestJsonDecoderOne(t *testing.T) { 11 | d, err := ioutil.ReadFile(path.Join(testdata, "policy.json")) 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | 16 | m, err := JSON.Decode(d).One() 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | 21 | ref, err := m.Ref() 22 | if err != nil { 23 | t.Errorf("Could not get reference: %s", err) 24 | } 25 | if ref.Kind != "Policy" { 26 | t.Errorf("Expected a Policy, got a %s", ref.Kind) 27 | } 28 | if ref.APIVersion != "v1" { 29 | t.Errorf("Expected v1, got %s", ref.APIVersion) 30 | } 31 | } 32 | 33 | func TestJsonDecoderAll(t *testing.T) { 34 | data := `{"one": "hello"} 35 | {"two": "world"}` 36 | 37 | ms, err := JSON.Decode([]byte(data)).All() 38 | if err != nil { 39 | t.Errorf("Failed to parse multiple JSON entries: %s", err) 40 | } 41 | if len(ms) != 2 { 42 | t.Errorf("Expected 2 JSON items, got %d", len(ms)) 43 | } 44 | } 45 | 46 | func TestJsonEncoderAll(t *testing.T) { 47 | f1 := map[string]string{"one": "hello"} 48 | f2 := map[string]string{"two": "world"} 49 | 50 | var b bytes.Buffer 51 | if err := JSON.Encode(&b).All(f1, f2); err != nil { 52 | t.Errorf("Failed to encode: %s", err) 53 | } 54 | 55 | expect := "{\"one\":\"hello\"}\n{\"two\":\"world\"}\n" 56 | actual := b.String() 57 | if actual != expect { 58 | t.Errorf("Expected [%s], got [%s]", expect, actual) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /codec/object_test.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | import ( 4 | "io/ioutil" 5 | "path" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestManifest(t *testing.T) { 11 | d, err := ioutil.ReadFile(path.Join(testdata, "pod.yaml")) 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | 16 | m, err := YAML.Decode(d).One() 17 | if err != nil { 18 | t.Errorf("Failed parse: %s", err) 19 | } 20 | 21 | pod, err := m.Pod() 22 | if err != nil { 23 | t.Errorf("Failed to decode into pod: %s", err) 24 | } 25 | 26 | if pod.Name != "cassandra" { 27 | t.Errorf("Expected name 'cassandra', got %q", pod.Name) 28 | } 29 | } 30 | 31 | type kindFunc func(m *Object) error 32 | 33 | func assertKind(t *testing.T, file string, kf kindFunc) { 34 | d, err := ioutil.ReadFile(path.Join(testdata, file)) 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | 39 | m, err := YAML.Decode(d).One() 40 | if err != nil { 41 | t.Errorf("Failed parse: %s", err) 42 | } 43 | 44 | if err = kf(m); err != nil { 45 | t.Errorf("Failed to decode %s into its kind: %s", file, err) 46 | } 47 | } 48 | 49 | func TestKnownKinds(t *testing.T) { 50 | kinds := map[string]kindFunc{ 51 | "pod.yaml": func(m *Object) error { _, err := m.Pod(); return err }, 52 | "rc.yaml": func(m *Object) error { _, err := m.RC(); return err }, 53 | "daemonset.yaml": func(m *Object) error { _, err := m.DaemonSet(); return err }, 54 | "horizontalpodautoscaler.yaml": func(m *Object) error { _, err := m.HorizontalPodAutoscaler(); return err }, 55 | "ingress.yaml": func(m *Object) error { _, err := m.Ingress(); return err }, 56 | "job.yaml": func(m *Object) error { _, err := m.Job(); return err }, 57 | "serviceaccount.yaml": func(m *Object) error { _, err := m.ServiceAccount(); return err }, 58 | "service.yaml": func(m *Object) error { _, err := m.Service(); return err }, 59 | "namespace.yaml": func(m *Object) error { _, err := m.Namespace(); return err }, 60 | } 61 | 62 | for uptown, funk := range kinds { 63 | assertKind(t, uptown, funk /*gonna give it to ya*/) 64 | // Don't believe me? Just watch. 65 | } 66 | } 67 | 68 | func TestServiceAccount(t *testing.T) { 69 | d, err := ioutil.ReadFile(path.Join(testdata, "serviceaccount.yaml")) 70 | if err != nil { 71 | t.Error(err) 72 | } 73 | 74 | m, err := YAML.Decode(d).One() 75 | if err != nil { 76 | t.Errorf("Failed parse: %s", err) 77 | } 78 | 79 | _, err = m.ServiceAccount() 80 | if err != nil { 81 | t.Errorf("Failed to decode into pod: %s", err) 82 | } 83 | } 84 | 85 | func TestObjectYAML(t *testing.T) { 86 | d, err := ioutil.ReadFile(path.Join(testdata, "serviceaccount.yaml")) 87 | if err != nil { 88 | t.Error(err) 89 | } 90 | m, err := YAML.Decode(d).One() 91 | if err != nil { 92 | t.Errorf("Failed parse: %s", err) 93 | } 94 | 95 | if out, err := m.YAML(); err != nil { 96 | t.Errorf("Failed to write YAML: %s", err) 97 | } else if len(out) == 0 { 98 | t.Error("YAML len is 0") 99 | } 100 | } 101 | func TestObjectJSON(t *testing.T) { 102 | d, err := ioutil.ReadFile(path.Join(testdata, "serviceaccount.yaml")) 103 | if err != nil { 104 | t.Error(err) 105 | } 106 | m, err := YAML.Decode(d).One() 107 | if err != nil { 108 | t.Errorf("Failed parse: %s", err) 109 | } 110 | 111 | if out, err := m.JSON(); err != nil { 112 | t.Errorf("Failed to write JSON: %s", err) 113 | } else if len(out) == 0 { 114 | t.Error("JSON len is 0") 115 | } 116 | } 117 | 118 | func TestAddLabels(t *testing.T) { 119 | d, err := ioutil.ReadFile(path.Join(testdata, "pod.yaml")) 120 | if err != nil { 121 | t.Error(err) 122 | } 123 | 124 | m, err := YAML.Decode(d).One() 125 | if err != nil { 126 | t.Errorf("Failed parse: %s", err) 127 | } 128 | 129 | labels := map[string]string{ 130 | "foo": "bar", 131 | "drink": "slurm", 132 | } 133 | 134 | if err := m.AddLabels(labels); err != nil { 135 | t.Errorf("Failed to add labels: %s", err) 136 | } 137 | 138 | if !strings.Contains(string(m.data), "drink: slurm") { 139 | t.Errorf("Could not find 'drink:slurm' in \n%s", string(m.data)) 140 | } 141 | 142 | _, err = m.Pod() 143 | if err != nil { 144 | t.Errorf("Failed to decode into pod: %s", err) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /codec/yaml.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "io" 8 | 9 | "github.com/ghodss/yaml" 10 | ) 11 | 12 | type yamlCodec struct{} 13 | 14 | func (c yamlCodec) Decode(b []byte) Decoder { 15 | return &yamlDecoder{data: b} 16 | } 17 | 18 | func (c yamlCodec) Encode(out io.Writer) Encoder { 19 | return &yamlEncoder{out: out} 20 | } 21 | 22 | type yamlEncoder struct { 23 | out io.Writer 24 | } 25 | 26 | func (e *yamlEncoder) One(v interface{}) error { 27 | buf, err := yaml.Marshal(v) 28 | if err != nil { 29 | return err 30 | } 31 | e.out.Write(buf) 32 | return nil 33 | } 34 | 35 | func (e *yamlEncoder) All(vs ...interface{}) error { 36 | c := len(vs) - 1 37 | for i, v := range vs { 38 | if err := e.One(v); err != nil { 39 | return err 40 | } 41 | if i < c { 42 | e.out.Write([]byte(yamlSeparator)) 43 | e.out.Write([]byte("\n")) 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | type yamlDecoder struct { 50 | data []byte 51 | } 52 | 53 | // All returns all documents in a single YAML file. 54 | func (d *yamlDecoder) All() ([]*Object, error) { 55 | scanner := bufio.NewScanner(bytes.NewBuffer(d.data)) 56 | scanner.Split(SplitYAMLDocument) 57 | 58 | ms := []*Object{} 59 | for scanner.Scan() { 60 | m := &Object{ 61 | data: append([]byte(nil), scanner.Bytes()...), 62 | dec: func(b []byte, v interface{}) error { 63 | return yaml.Unmarshal(b, v) 64 | }, 65 | } 66 | ms = append(ms, m) 67 | } 68 | 69 | return ms, scanner.Err() 70 | } 71 | 72 | // One returns no more than one YAML doc, even if the file contains more. 73 | func (d *yamlDecoder) One() (*Object, error) { 74 | ms, err := d.All() 75 | if err != nil { 76 | return nil, err 77 | } 78 | if len(ms) == 0 { 79 | return nil, errors.New("No document") 80 | } 81 | return ms[0], nil 82 | } 83 | 84 | const yamlSeparator = "\n---" 85 | 86 | // SplitYAMLDocument is a bufio.SplitFunc for splitting a YAML document into individual documents. 87 | // 88 | // This is from Kubernetes' 'pkg/util/yaml'.splitYAMLDocument, which is unfortunately 89 | // not exported. 90 | func SplitYAMLDocument(data []byte, atEOF bool) (advance int, token []byte, err error) { 91 | if atEOF && len(data) == 0 { 92 | return 0, nil, nil 93 | } 94 | sep := len([]byte(yamlSeparator)) 95 | if i := bytes.Index(data, []byte(yamlSeparator)); i >= 0 { 96 | // We have a potential document terminator 97 | i += sep 98 | after := data[i:] 99 | if len(after) == 0 { 100 | // we can't read any more characters 101 | if atEOF { 102 | return len(data), data[:len(data)-sep], nil 103 | } 104 | return 0, nil, nil 105 | } 106 | if j := bytes.IndexByte(after, '\n'); j >= 0 { 107 | return i + j + 1, data[0 : i-sep], nil 108 | } 109 | return 0, nil, nil 110 | } 111 | // If we're at EOF, we have a final, non-terminated line. Return it. 112 | if atEOF { 113 | return len(data), data, nil 114 | } 115 | // Request more data. 116 | return 0, nil, nil 117 | } 118 | -------------------------------------------------------------------------------- /codec/yaml_test.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "path" 7 | "testing" 8 | ) 9 | 10 | const testdata = "../testdata" 11 | 12 | func TestYamlDecoderOne(t *testing.T) { 13 | d, err := ioutil.ReadFile(path.Join(testdata, "pod.yaml")) 14 | if err != nil { 15 | t.Error(err) 16 | } 17 | 18 | m, err := YAML.Decode(d).One() 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | 23 | ref, err := m.Ref() 24 | if err != nil { 25 | t.Errorf("Could not get reference: %s", err) 26 | } 27 | if ref.Kind != "Pod" { 28 | t.Errorf("Expected a pod, got a %s", ref.Kind) 29 | } 30 | if ref.APIVersion != "v1" { 31 | t.Errorf("Expected v1, got %s", ref.APIVersion) 32 | } 33 | } 34 | 35 | func TestYamlDecoderAll(t *testing.T) { 36 | d, err := ioutil.ReadFile(path.Join(testdata, "three-pods-and-three-services.yaml")) 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | 41 | ms, err := YAML.Decode(d).All() 42 | if err != nil { 43 | t.Error(err) 44 | } 45 | 46 | if len(ms) != 6 { 47 | t.Errorf("Expected 6 parts, got %d", len(ms)) 48 | } 49 | 50 | for i := 0; i < 3; i++ { 51 | ref, err := ms[i*2].Ref() 52 | if err != nil { 53 | t.Errorf("Expected a reference for pod[%d]: %s", i, err) 54 | } 55 | 56 | if ref.Kind != "Pod" { 57 | t.Errorf("Expected Pod, got %s", ref.Kind) 58 | } 59 | 60 | ref, err = ms[i*2+1].Ref() 61 | if err != nil { 62 | t.Errorf("Expected a reference for service[%d]: %s", i, err) 63 | } 64 | 65 | if ref.Kind != "Service" { 66 | t.Errorf("Expected Service, got %s", ref.Kind) 67 | } 68 | } 69 | } 70 | 71 | func TestYamlEncoderAll(t *testing.T) { 72 | f1 := map[string]string{"one": "hello"} 73 | f2 := map[string]string{"two": "world"} 74 | 75 | var b bytes.Buffer 76 | if err := YAML.Encode(&b).All(f1, f2); err != nil { 77 | t.Errorf("Failed to encode: %s", err) 78 | } 79 | 80 | // This is a little fragile, since whitespace in YAML is not defined. 81 | expect := "one: hello\n\n---\ntwo: world\n" 82 | actual := b.String() 83 | if actual != expect { 84 | t.Errorf("Expected [%s]\nGot [%s]", expect, actual) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/helm/helm-classic/log" 11 | "github.com/helm/helm-classic/test" 12 | "github.com/helm/helm-classic/util" 13 | ) 14 | 15 | func TestEnsureRepo(t *testing.T) { 16 | tmpHome := test.CreateTmpHome() 17 | 18 | repo := "https://github.com/helm/charts" 19 | ensureRepo(repo, filepath.Join(tmpHome, "cache", "charts")) 20 | } 21 | 22 | func TestParseConfigfile(t *testing.T) { 23 | cfg, err := Parse([]byte(util.DefaultConfigfile)) 24 | if err != nil { 25 | t.Fatalf("Could not parse DefaultConfigfile: %s", err) 26 | } 27 | 28 | r := cfg.Repos 29 | if r.Default != "charts" { 30 | t.Errorf("Expected 'charts', got %q", r.Default) 31 | } 32 | 33 | if len(r.Tables) != 1 { 34 | t.Errorf("Expected exactly 1 table.") 35 | } 36 | 37 | if r.Tables[0].Repo != "https://github.com/helm/charts" { 38 | t.Errorf("Wrong URL") 39 | } 40 | 41 | if r.Tables[0].Name != "charts" { 42 | t.Errorf("Wrong table name") 43 | } 44 | } 45 | 46 | func TestLoadConfigfile(t *testing.T) { 47 | cfg, err := Load("../testdata/Configfile.yaml") 48 | if err != nil { 49 | t.Fatalf("Could not load ../testdata/Configfile.yaml: %s", err) 50 | } 51 | 52 | if len(cfg.Repos.Tables) != 3 { 53 | t.Errorf("Expected 3 remotes.") 54 | } 55 | } 56 | 57 | func TestSave(t *testing.T) { 58 | cfg, err := Load("../testdata/Configfile.yaml") 59 | if err != nil { 60 | t.Fatalf("Could not load ../testdata/Configfile.yaml: %s", err) 61 | } 62 | 63 | if err := cfg.Save("../testdata/Configfile-SAVE.yaml"); err != nil { 64 | t.Fatalf("Could not save: %s", err) 65 | } 66 | 67 | if _, err := os.Stat("../testdata/Configfile-SAVE.yaml"); err != nil { 68 | t.Fatalf("Saved file does not exist: %s", err) 69 | } 70 | 71 | if err := os.Remove("../testdata/Configfile-SAVE.yaml"); err != nil { 72 | t.Fatalf("Could not remove file: %s", err) 73 | } 74 | } 75 | 76 | func TestPrintSummary(t *testing.T) { 77 | var b bytes.Buffer 78 | 79 | log.Stdout = &b 80 | log.Stderr = &b 81 | defer func() { 82 | log.Stdout = os.Stdout 83 | log.Stderr = os.Stderr 84 | }() 85 | 86 | diff := `M README.md 87 | M cassandra 88 | A jenkins 89 | M mysql 90 | M owncloud` 91 | 92 | expected := []string{ 93 | "Updated 3 charts\ncassandra mysql owncloud", 94 | "Added 1 charts\njenkins", 95 | } 96 | 97 | printSummary(diff) 98 | actual := b.String() 99 | 100 | for _, exp := range expected { 101 | if !strings.Contains(actual, exp) { 102 | t.Errorf("Expected %q to contain %q", actual, exp) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /dependency/dependency_test.go: -------------------------------------------------------------------------------- 1 | package dependency 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/helm/helm-classic/chart" 8 | ) 9 | 10 | var testInstalldir = "../testdata/charts" 11 | 12 | func init() { 13 | var err error 14 | testInstalldir, err = filepath.Abs(testInstalldir) 15 | if err != nil { 16 | panic(err) 17 | } 18 | } 19 | 20 | func TestResolve(t *testing.T) { 21 | cf, err := chart.LoadChartfile(filepath.Join(testInstalldir, "deptest/Chart.yaml")) 22 | if err != nil { 23 | t.Errorf("Could not load chartfile deptest/Chart.yaml: %s", err) 24 | } 25 | 26 | missed, err := Resolve(cf, testInstalldir) 27 | if err != nil { 28 | t.Errorf("could not resolve deps in %s: %s", testInstalldir, err) 29 | } 30 | if len(missed) != 2 { 31 | t.Fatalf("Expected dep3 and honkIfYouLoveDucks to be returned") 32 | } 33 | 34 | if missed[0].Name != "dep3" { 35 | t.Errorf("Expected dep3 in slot 0. Got %s", missed[0].Name) 36 | } 37 | if missed[1].Name != "honkIfYouLoveDucks" { 38 | t.Errorf("Expected honkIfYouLoveDucks in slot 1. Got %s", missed[1].Name) 39 | } 40 | } 41 | 42 | func TestOptRepoMatch(t *testing.T) { 43 | a := &chart.Dependency{ 44 | Repo: "git@github.com:drink/slurm.git", 45 | } 46 | b := &chart.Dependency{ 47 | Repo: "https://github.com/drink/slurm.git", 48 | } 49 | 50 | if !optRepoMatch(a, b) { 51 | t.Errorf("Expected %s to match %s", a.Repo, b.Repo) 52 | } 53 | 54 | if !optRepoMatch(a, &chart.Dependency{}) { 55 | t.Errorf("Expected empty required repo to match any filled from repo.") 56 | } 57 | 58 | if optRepoMatch(&chart.Dependency{}, a) { 59 | t.Errorf("Expected empty from repo to fail for a non-empty required repo.") 60 | } 61 | } 62 | 63 | func TestCanonicalRepo(t *testing.T) { 64 | expect := "example.com/foo/bar.git" 65 | orig := []string{ 66 | "git@example.com:foo/bar.git", 67 | "git@example.com:/foo/bar.git", 68 | "http://example.com/foo/bar.git", 69 | "https://example.com/foo/bar.git", 70 | "ssh://git@example.com/foo/bar.git", 71 | } 72 | for _, v := range orig { 73 | cv, err := canonicalRepo(v) 74 | if err != nil { 75 | t.Errorf("Failed to parse %s: %s", v, err) 76 | } 77 | if cv != expect { 78 | t.Errorf("Expected %q, got %q for %q", expect, cv, v) 79 | } 80 | } 81 | 82 | expect = "localhost/slurm/bar.git" 83 | orig = []string{ 84 | "file:///slurm/bar.git", 85 | "/slurm/bar.git", 86 | "slurm/bar.git", 87 | } 88 | for _, v := range orig { 89 | cv, err := canonicalRepo(v) 90 | if err != nil { 91 | t.Errorf("Failed to parse %s: %s", v, err) 92 | } 93 | if cv != expect { 94 | t.Errorf("Expected %q, got %q for %q", expect, cv, v) 95 | } 96 | } 97 | } 98 | 99 | func TestSatisfies(t *testing.T) { 100 | a := &chart.Dependency{ 101 | Name: "slurm", 102 | Version: "1.2.3", 103 | Repo: "ssh://git@example.com/drink/slurm.git", 104 | } 105 | b := &chart.Dependency{ 106 | Name: "slurm", 107 | Version: "~1.2", 108 | Repo: "https://example.com/drink/slurm.git", 109 | } 110 | aa := &chart.Dependency{ 111 | Name: "slurm", 112 | Version: "1.3.5", 113 | Repo: "https://example.com/drink/slurm.git", 114 | } 115 | if !satisfies(a, b) { 116 | t.Errorf("Expected a to satisfy b") 117 | } 118 | 119 | if satisfies(aa, b) { 120 | t.Errorf("Expected aa to not satisfy b because of version constraint.") 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Helm Classic: A Kubernetes Package Manager 2 | 3 | Helm Classic provides package management for Kubernetes. This directory provides 4 | a number of documents explaining how to build Helm Classic packages (called 5 | Charts). 6 | -------------------------------------------------------------------------------- /docs/authoring_charts.md: -------------------------------------------------------------------------------- 1 | # Authoring Helm Classic Charts 2 | 3 | It is important for chart authors to understand Helm Classic fundamentals. Before you begin, make sure you are familiar with: 4 | 5 | - How to [model Services in Helm Classic](modeling_services.md) 6 | - How Helm Classic [uses Kubernetes Labels](using_labels.md) 7 | - How the [Helm Classic workspace](workspace.md) is laid out 8 | 9 | ## Background 10 | 11 | Helm Classic Charts consist of three items: 12 | 13 | 1. A `manifests` directory for Kubernetes resources 14 | 2. A `Chart.yaml` file 15 | 3. A `README.md` 16 | 17 | The directory structure of a chart is as follows: 18 | 19 | ``` 20 | |- mychart/ 21 | | 22 | |- manifests/ 23 | | 24 | |- mychart-rc.yaml 25 | |- mychart-service.yaml 26 | |- ... 27 | |- Chart.yaml 28 | |- README.md 29 | ``` 30 | 31 | ## Create a new Chart 32 | 33 | ### Step 1: Create the Chart in your Workspace 34 | 35 | Use `helmc create ` to create a new chart in your workspace. 36 | This will copy the default "skeleton" chart into `~/.helmc/workspace/charts/`. 37 | 38 | ### Step 2: Edit the Chart 39 | 40 | Use `helmc edit ` to open all files in the chart in a single editor. 41 | 42 | For convenience, this will present all the chart files inside a single editor, with `--- : ` delimiters. This makes it easy to modify a chart, add files, and remove files all within a single `helmc edit` command. 43 | 44 | If you prefer to edit files manually, you can use an IDE or any other file-based editor. 45 | 46 | ### Step 3: Test the Chart 47 | 48 | Use `helmc test ` to test installing the chart and validating that the proper Kubernetes resources are created, as evidenced by the `helmc test` output and return code. 49 | 50 | ### Step 4: Publish the Chart 51 | 52 | Use `helmc publish ` to copy a chart from your local workspace into the Git checkout that lives under `~/.helmc/cache`. From here you can submit a pull request. 53 | -------------------------------------------------------------------------------- /docs/chart_tables.md: -------------------------------------------------------------------------------- 1 | # Using Other Repositories 2 | 3 | Helm Classic allows for the use of additional (potentially private) repositories of charts via the `helmc repo` command. 4 | 5 | ## Adding a repository 6 | 7 | `$ helmc repo add mycharts https://github.com/dev/mycharts` will add a chart table with the name `mycharts` pointing to the `dev/mycharts` git repository (any valid git protocol with regular git authentication). 8 | 9 | ## Listing repositories 10 | 11 | ``` 12 | $ helmc repo list 13 | charts* https://github.com/helm/charts 14 | mycharts https://github.com/dev/mycharts 15 | ``` 16 | Note the `*` indicates the default repository. This is configured in a `config.yaml` file in `$HELMC_HOME`. 17 | 18 | ## Using a different repository 19 | 20 | `$ helmc fetch mycharts/app` will fetch the `app` chart from the `mycharts` repo. I can then `helmc install` as normal. 21 | 22 | ## Removing repositories 23 | 24 | `$ helmc repo rm mycharts` Note: there is no confirmation requested. 25 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Helm Classic: A Kubernetes Package Manager 2 | 3 | [Helm Classic](https://helm.sh) helps you find and use software built for Kubernetes. The Helm Classic CLI uses Charts which not only contain metadata about software packages but also Pod, ReplicationController and Service manifests for Kubernetes. With a few Helm Classic commands you can quickly and easily deploy software packages like: 4 | 5 | - Postgres 6 | - etcd 7 | - HAProxy 8 | - redis 9 | 10 | All of the Helm Classic charts live at [github.com/helm/charts](https://github.com/helm/charts). If you want to make your own charts we have a guide for [authoring charts](authoring_charts.md) as well. 11 | 12 | ## Installing Helm Classic 13 | 14 | From a Linux or Mac OS X client: 15 | ``` 16 | curl -s https://get.helm.sh | bash 17 | ``` 18 | 19 | *or*: 20 | 21 | 1. Grab a prebuilt binary from: 22 | - the latest release: [ ![Download](https://api.bintray.com/packages/deis/helm/helm-classic/images/download.svg) ](https://bintray.com/deis/helm/helmc/_latestVersion#files) 23 | - the CI build pipeline: [ ![Download](https://api.bintray.com/packages/deis/helm-ci/helm-classic/images/download.svg) ](https://bintray.com/deis/helm-ci/helmc/_latestVersion#files) 24 | 2. Unzip the package and make sure `helmc` is available on the PATH. 25 | 26 | ### Migration Notes 27 | 28 | If you are a user of the original Helm tool (versions prior to v0.7.0), take note that Helm Classic is a _re-branding_ of that tool-- new name, same great taste! 29 | 30 | __Helm Classic is fully compatible with previously existing Helm charts!__ 31 | 32 | Anyone migrating to Helm Classic from an older version should complete the following steps to fully replace their existing tool with Helm Classic. Doing so will clear the path for eventual installation of the new and improved Helm ([kubernetes/helm][k8s-helm]). 33 | 34 | First, you may optionally define a custom home directory for use by Helm Classic. If opting for this, the instruction should be added to your shell's profile. 35 | 36 | ``` 37 | $ HELMC_HOME=/custom/path 38 | ``` 39 | 40 | Next, we migrate the contents from its old location to its new location (because the default location has changed). 41 | 42 | ``` 43 | $ mv $(helm home) $(helmc home) 44 | ``` 45 | 46 | Finally, remove the old version: 47 | 48 | ``` 49 | $ rm $(which helm) 50 | ``` 51 | 52 | You may now use the new binary, `helmc`, just as you previously used `helm`. Soon, the `helm` name will be taken over by the new and improved Helm ([kubernetes/helm][k8s-helm]) and you will be able to make use of `helmc` in parallel with `helm` for as long as you have that need. 53 | 54 | ## Additional Information 55 | 56 | Learn more about Helm Classic's [architecture](architecture.md). 57 | 58 | Find out how Helm Classic uses [Kubernetes labels](using_labels.md). 59 | 60 | If you are authoring a chart that will be used by other apps, check out how Helm Classic [models services](modeling_services.md). 61 | 62 | ## Thanks 63 | 64 | Helm Classic was inspired by [Homebrew](https://github.com/Homebrew/homebrew). 65 | 66 | [k8s-helm]: https://github.com/kubernetes/helm 67 | -------------------------------------------------------------------------------- /docs/modeling_services.md: -------------------------------------------------------------------------------- 1 | # Modeling Services in Kubernetes 2 | 3 | Generally speaking there are two ways to model [Services](http://kubernetes.io/v1.0/docs/user-guide/services.html) in Kubernetes: 4 | 5 | 1. Service providers (i.e. a database) define their own services 6 | 2. Service consumers (i.e. an application) define the services they consume 7 | 8 | With the first option, service consumers must constantly track the service providers they depend on. When a service provider changes, all consumers must be notified and restarted. This results in tight coupling between service consumers and providers. 9 | 10 | With the second option, consumers defines the services they require with label selectors. Once pods are launched that fulfill the label selectors, the consumer can begin accessing the service. This facilitates looser coupling between service consumers and providers. 11 | 12 | While the first option is more common in traditional orchestration systems, the second is a more natural fit for Kubernetes. As a result, #2 is how we model services in Helm Classic. See the documentation on [using labels](using_labels.md) for more details. 13 | 14 | ## Example: Ponycorn, a Redis-backed Application 15 | 16 | We are authoring a chart for an application called `ponycorn` which relies on a Redis backend. In addition to creating manifests for the replication controllers and pods required by the application, we also include manifests to define the services we consume. 17 | 18 | To model the Redis backend in the Ponycorn chart, we include: 19 | 20 | ```yaml 21 | apiVersion: v1 22 | kind: Service 23 | metadata: 24 | name: ponycorn-redis 25 | labels: 26 | heritage: helm 27 | spec: 28 | ports: 29 | - port: 6379 30 | name: redis 31 | protocol: TCP 32 | selector: 33 | provider: redis 34 | ``` 35 | 36 | For service discovery, the `ponycorn` application uses the `PONYCORN_REDIS_SERVICE_HOST` and `PONYCORN_REDIS_SERVICE_PORT` environment variables. These environment variable names remain constant and point to a static Kubernetes Service VIP. As pods are launched into the cluster that _fulfill_ the `provider: redis` label selector, the Redis service for `ponycorn` comes online. 37 | 38 | Following this approach requires a shared vocabulary among package contributors, but that is a problem that can be addressed (if not solved) by a combination of good tooling and documentation. 39 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Helm Classic Plugins 2 | 3 | Helm Classic supports a basic plugin mechanism not unlike the one found in Git 4 | or similar CLI projects. This feature is still considered experimental. 5 | 6 | The basic model: When `helmc` receives a subcommand that it does not know 7 | (e.g. `helmc foo`), it will look on `$PATH` for an executable named 8 | `helm-foo`. If it finds one, it will set several environment variables 9 | and then execute the named command, returning the results directly to 10 | STDOUT and STDERR. Any flags passed after `foo` are passed on to the 11 | `helm-foo` command. (Flags before foo, such as `helmc -v foo`, are 12 | interpreted by Helm Classic. They may influence the environment, but are not 13 | passed on.) 14 | 15 | The plugin `plugins/sec/helm-sec` provides an example of how plugins can 16 | be built. 17 | -------------------------------------------------------------------------------- /docs/using_labels.md: -------------------------------------------------------------------------------- 1 | # Helm Classic Labels 2 | 3 | Helm Classic is designed to take full advantage of [Kubernetes labels](http://kubernetes.io/v1.0/docs/user-guide/labels.html). 4 | 5 | ## What are Labels? 6 | 7 | From the Kubernetes documentation on the [motivation for labels](http://kubernetes.io/v1.0/docs/user-guide/labels.html#motivation): 8 | 9 | > Labels enable users to map their own organizational structures onto system objects in a loosely coupled fashion, without requiring clients to store these mappings. 10 | > 11 | > Service deployments and batch processing pipelines are often multi-dimensional entities (e.g., multiple partitions or deployments, multiple release tracks, multiple tiers, multiple micro-services per tier). Management often requires cross-cutting operations, which breaks encapsulation of strictly hierarchical representations, especially rigid hierarchies determined by the infrastructure rather than by users. 12 | 13 | To learn more about how labels work, check out [label selectors](http://kubernetes.io/v1.0/docs/user-guide/labels.html#label-selectors) 14 | in the Kubernetes documentation. 15 | 16 | ## Helm Classic Labels 17 | 18 | ### Group Label 19 | 20 | Helm Classic uses the `group` label as a convention for organizing Charts. Services which share the same `group` are able to find each other and communicate automatically. Examples include: 21 | 22 | * frontend 23 | * api 24 | * data 25 | 26 | Groups are user-defined and not included in the Chart repository. 27 | 28 | ### Provider Label 29 | 30 | Helm Classic uses the `provider` label as a convention specifying the type of Service provided by a Chart. Examples include: 31 | 32 | * etcd 33 | * postgres 34 | * s3 35 | 36 | A Chart may have dependencies on specific `provider`(s). Chart authors are responsible for ensuring the `provider` label works consistently across Charts. 37 | 38 | ### Mode Label 39 | 40 | Helm Classic uses the `mode` label as a convention for specifying the operating mode of the service. Examples include: 41 | 42 | * standalone 43 | * clustered 44 | * discovery 45 | 46 | Charts may have dependencies on the operating `mode` of another Chart. 47 | 48 | ### Heritage Label 49 | 50 | All Helm Classic Charts include the label `heritage: helm`. This provides a 51 | convenient and standard way to query which components in a Kubernetes 52 | cluster trace to Helm Classic. 53 | 54 | ## Using Labels 55 | 56 | In Kubernetes, labels are typically edited by hand and stored with manifests in a version control system. Helm Classic makes it easier to use labels effectively using the `helmc` CLI. 57 | 58 | ### Label Workflow (Simple) 59 | 60 | If you want to place a package into a `group` while installing it, pass the group as an argument to `helmc install`. 61 | 62 | ``` 63 | helmc install nginx --group=frontend 64 | helmc install python --group=frontend 65 | ``` 66 | 67 | ### Label Workflow (Advanced) 68 | 69 | Use the `helmc label` command to apply arbitrary labels to Charts in your workspace. 70 | 71 | ``` 72 | helmc fetch nginx 73 | helmc fetch python 74 | helmc label nginx group=frontend other=label 75 | helmc label python group=frontend other=label 76 | helmc install nginx 77 | helmc install python 78 | ``` 79 | 80 | Of course, you can always use `helmc edit` or your own editor to customize labels and other manifest data in your local workspace. 81 | -------------------------------------------------------------------------------- /generator/generator_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | func TestSkip(t *testing.T) { 10 | pass := []string{ 11 | ".foo/.bar/baz", 12 | "_bar/foo", 13 | "/foo/bar/.baz/slurm", 14 | } 15 | for _, p := range pass { 16 | if e := skip(p); e != nil { 17 | t.Errorf("%s should have been nil, got %v", p, e) 18 | } 19 | } 20 | 21 | nopass := []string{ 22 | "foo/.bar", 23 | "foo/_bar", 24 | "foo/..", 25 | "..", 26 | ".", 27 | } 28 | for _, f := range nopass { 29 | if e := skip(f); e != filepath.SkipDir { 30 | t.Errorf("%s should have been nil, got %v", f, e) 31 | } 32 | } 33 | } 34 | 35 | func TestReadGenerator(t *testing.T) { 36 | dir := "../testdata/generator" 37 | pass := []string{"one.yaml", "two.yaml", "three.txt", "four/four.txt", "four/five.txt"} 38 | fail := []string{"fail.txt", "fail2.txt"} 39 | 40 | for _, p := range pass { 41 | f, err := os.Open(filepath.Join(dir, p)) 42 | if err != nil { 43 | t.Errorf("failed to read %s: %s", p, err) 44 | } 45 | out, err := readGenerator(f) 46 | if err != nil { 47 | t.Errorf("%s failed read generator: %s", p, err) 48 | } 49 | f.Close() 50 | 51 | if out != "echo foo bar baz" { 52 | t.Errorf("Expected %s to output 'echo foo bar baz', got %q", p, out) 53 | } 54 | } 55 | for _, p := range fail { 56 | f, err := os.Open(filepath.Join(dir, p)) 57 | if err != nil { 58 | t.Errorf("failed to read %s: %s", p, err) 59 | } 60 | out, err := readGenerator(f) 61 | if err != nil { 62 | t.Errorf("%s failed read generator: %s", p, err) 63 | } 64 | f.Close() 65 | 66 | if out != "" { 67 | t.Errorf("Expected %s to output empty string, got %q", p, out) 68 | } 69 | } 70 | } 71 | 72 | func TestWalk(t *testing.T) { 73 | dir := "../testdata/generator" 74 | count, err := Walk(dir, []string{}, false) 75 | if err != nil { 76 | t.Fatalf("Failed to walk: %s", err) 77 | } 78 | if count != 5 { 79 | t.Errorf("Expected 5 executes, got %d", count) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: e3b30639ce08a893ef3809167f67c665ac85c03ed1611c15721b8fb7fbcc1b64 2 | updated: 2016-05-31T16:45:05.345758638-06:00 3 | imports: 4 | - name: code.google.com/p/goprotobuf 5 | version: 9e6977f30c91c78396e719e164e57f9287fff42c 6 | repo: https://github.com/golang/protobuf 7 | - name: github.com/aokoli/goutils 8 | version: 9c37978a95bd5c709a15883b6242714ea6709e64 9 | - name: github.com/BurntSushi/toml 10 | version: f0aeabca5a127c4078abb8c8d64298b147264b55 11 | - name: github.com/cloudfoundry-incubator/candiedyaml 12 | version: 99c3df83b51532e3615f851d8c2dbb638f5313bf 13 | - name: github.com/codegangsta/cli 14 | version: 71f57d300dd6a780ac1856c005c4b518cfd498ec 15 | - name: github.com/davecgh/go-spew 16 | version: 3e6e67c4dcea3ac2f25fd4731abc0e1deaf36216 17 | subpackages: 18 | - spew 19 | - name: github.com/deis/pkg 20 | version: 7f41ea6de942139d5de67f4d4cb2cccced991f6f 21 | subpackages: 22 | - prettyprint 23 | - name: github.com/docker/docker 24 | version: 0f5c9d301b9b1cca66b3ea0f9dec3b5317d3686d 25 | subpackages: 26 | - pkg/jsonmessage 27 | - pkg/mount 28 | - pkg/parsers 29 | - pkg/symlink 30 | - pkg/term 31 | - pkg/timeutils 32 | - pkg/units 33 | - name: github.com/docker/go-units 34 | version: 0bbddae09c5a5419a8c6dcdd7ff90da3d450393b 35 | - name: github.com/ghodss/yaml 36 | version: e8e0db9016175449df0e9c4b6e6995a9433a395c 37 | - name: github.com/golang/glog 38 | version: 44145f04b68cf362d9c4df2182967c2275eaefed 39 | - name: github.com/google/go-github 40 | version: 81d0490d8aa8400f6760a077f4a2039eb0296e86 41 | subpackages: 42 | - github 43 | - name: github.com/google/go-querystring 44 | version: 9235644dd9e52eeae6fa48efd539fdc351a0af53 45 | subpackages: 46 | - query 47 | - name: github.com/google/gofuzz 48 | version: bbcb9da2d746f8bdbd6a936686a0a6067ada0ec5 49 | - name: github.com/juju/ratelimit 50 | version: 77ed1c8a01217656d2080ad51981f6e99adaa177 51 | - name: github.com/Masterminds/semver 52 | version: 808ed7761c233af2de3f9729a041d68c62527f3a 53 | - name: github.com/Masterminds/sprig 54 | version: e6494bc7e81206ba6db404d2fd96500ffc453407 55 | - name: github.com/Masterminds/vcs 56 | version: 7af28b64c5ec41b1558f5514fd938379822c237c 57 | - name: github.com/opencontainers/runc 58 | version: 7ca2aa4873aea7cb4265b1726acb24b90d8726c6 59 | subpackages: 60 | - libcontainer 61 | - libcontainer/cgroups/fs 62 | - libcontainer/configs 63 | - libcontainer/cgroups 64 | - libcontainer/system 65 | - name: github.com/pborman/uuid 66 | version: c55201b036063326c5b1b89ccfe45a184973d073 67 | - name: github.com/spf13/pflag 68 | version: 08b1a584251b5b62f458943640fc8ebd4d50aaa5 69 | - name: github.com/steveeJ/gexpect 70 | version: ca42424d18c76d0d51a4cccd830d11878e9e5c17 71 | repo: https://github.com/coreos/gexpect 72 | - name: github.com/ugorji/go 73 | version: f4485b318aadd133842532f841dc205a8e339d74 74 | subpackages: 75 | - codec 76 | - name: golang.org/x/crypto 77 | version: 5bcd134fee4dd1475da17714aac19c0aa0142e2f 78 | subpackages: 79 | - ssh/terminal 80 | - nacl/box 81 | - curve25519 82 | - nacl/secretbox 83 | - salsa20/salsa 84 | - poly1305 85 | - name: golang.org/x/net 86 | version: c2528b2dd8352441850638a8bb678c2ad056fd3e 87 | subpackages: 88 | - context 89 | - html 90 | - http2 91 | - internal/timeseries 92 | - trace 93 | - websocket 94 | - name: gopkg.in/yaml.v2 95 | version: a83829b6f1293c91addabc89d0571c246397bbf4 96 | - name: k8s.io/kubernetes 97 | version: 3eed1e3be6848b877ff80a93da3785d9034d0a4f 98 | subpackages: 99 | - pkg 100 | - pkg/api 101 | - pkg/api/unversioned 102 | - pkg/api/v1 103 | - pkg/apis/extensions/v1beta1 104 | - pkg/api/meta 105 | - pkg/api/resource 106 | - pkg/auth/user 107 | - pkg/conversion 108 | - pkg/fields 109 | - pkg/labels 110 | - pkg/runtime 111 | - pkg/runtime/serializer 112 | - pkg/types 113 | - pkg/util 114 | - pkg/util/intstr 115 | - pkg/util/rand 116 | - pkg/util/sets 117 | - pkg/util/parsers 118 | - pkg/apis/extensions 119 | - pkg/util/errors 120 | - third_party/forked/reflect 121 | - pkg/util/validation 122 | - pkg/conversion/queryparams 123 | - pkg/runtime/serializer/json 124 | - pkg/runtime/serializer/recognizer 125 | - pkg/runtime/serializer/versioning 126 | - pkg/util/integer 127 | - pkg/util/wait 128 | - pkg/util/yaml 129 | - pkg/util/runtime 130 | - name: launchpad.net/gocheck 131 | version: 4f90aeace3a26ad7021961c297b22c42160c7b25 132 | repo: https://github.com/go-check/check 133 | - name: speter.net/go/exp/math/dec/inf 134 | version: 42ca6cd68aa922bc3f32f1e056e61b65945d9ad7 135 | repo: https://github.com/belua/inf 136 | vcs: git 137 | devImports: [] 138 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/helm/helm-classic 2 | import: 3 | - package: github.com/codegangsta/cli 4 | version: 71f57d300dd6a780ac1856c005c4b518cfd498ec 5 | - package: github.com/deis/pkg 6 | subpackages: 7 | - prettyprint 8 | - package: github.com/Masterminds/vcs 9 | version: ^1.5.1 10 | - package: github.com/Masterminds/semver 11 | version: ^1.1.0 12 | - package: k8s.io/kubernetes 13 | version: v1.2.4 14 | subpackages: 15 | - pkg 16 | - package: speter.net/go/exp/math/dec/inf 17 | version: 42ca6cd68aa922bc3f32f1e056e61b65945d9ad7 18 | repo: https://github.com/belua/inf 19 | vcs: git 20 | - package: golang.org/x/crypto 21 | version: master 22 | - package: github.com/aokoli/goutils 23 | version: master 24 | - package: github.com/pborman/uuid 25 | version: master 26 | - package: gopkg.in/yaml.v2 27 | version: master 28 | - package: github.com/google/go-querystring 29 | - package: github.com/google/go-github 30 | version: 81d0490d8aa8400f6760a077f4a2039eb0296e86 31 | - package: github.com/steveeJ/gexpect 32 | repo: https://github.com/coreos/gexpect 33 | - package: launchpad.net/gocheck 34 | repo: https://github.com/go-check/check 35 | - package: code.google.com/p/goprotobuf 36 | repo: https://github.com/golang/protobuf 37 | - package: github.com/Masterminds/sprig 38 | version: ">= 2.2.0" 39 | -------------------------------------------------------------------------------- /helmc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/helm/helm-classic/cli" 4 | 5 | func main() { 6 | cli.Cli().RunAndExitOnError() 7 | } 8 | -------------------------------------------------------------------------------- /kubectl/apply.go: -------------------------------------------------------------------------------- 1 | package kubectl 2 | 3 | // Apply uploads a chart to Kubernetes 4 | func (r RealRunner) Apply(stdin []byte, ns string) ([]byte, error) { 5 | args := []string{"apply", "-f", "-"} 6 | 7 | if ns != "" { 8 | args = append([]string{"--namespace=" + ns}, args...) 9 | } 10 | 11 | cmd := command(args...) 12 | assignStdin(cmd, stdin) 13 | 14 | return cmd.CombinedOutput() 15 | } 16 | 17 | // Apply returns the commands to kubectl 18 | func (r PrintRunner) Apply(stdin []byte, ns string) ([]byte, error) { 19 | args := []string{"apply", "-f", "-"} 20 | 21 | if ns != "" { 22 | args = append([]string{"--namespace=" + ns}, args...) 23 | } 24 | 25 | cmd := command(args...) 26 | assignStdin(cmd, stdin) 27 | 28 | return []byte(cmd.String()), nil 29 | } 30 | -------------------------------------------------------------------------------- /kubectl/apply_test.go: -------------------------------------------------------------------------------- 1 | package kubectl 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPrintApply(t *testing.T) { 8 | var client Runner = PrintRunner{} 9 | 10 | expected := `[CMD] kubectl --namespace=default-namespace apply -f - < some stdin data` 11 | 12 | out, err := client.Apply([]byte("some stdin data"), "default-namespace") 13 | if err != nil { 14 | t.Error(err) 15 | } 16 | 17 | actual := string(out) 18 | 19 | if expected != actual { 20 | t.Fatalf("actual %s != expected %s", actual, expected) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /kubectl/cluster_info.go: -------------------------------------------------------------------------------- 1 | package kubectl 2 | 3 | // ClusterInfo returns Kubernetes cluster info 4 | func (r RealRunner) ClusterInfo() ([]byte, error) { 5 | return command("cluster-info").CombinedOutput() 6 | } 7 | 8 | // ClusterInfo returns the commands to kubectl 9 | func (r PrintRunner) ClusterInfo() ([]byte, error) { 10 | cmd := command("cluster-info") 11 | return []byte(cmd.String()), nil 12 | } 13 | -------------------------------------------------------------------------------- /kubectl/command.go: -------------------------------------------------------------------------------- 1 | package kubectl 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | type cmd struct { 12 | *exec.Cmd 13 | } 14 | 15 | func command(args ...string) *cmd { 16 | return &cmd{exec.Command(Path, args...)} 17 | } 18 | 19 | func assignStdin(cmd *cmd, in []byte) { 20 | cmd.Stdin = bytes.NewBuffer(in) 21 | } 22 | 23 | func (c *cmd) String() string { 24 | var stdin string 25 | 26 | if c.Stdin != nil { 27 | b, _ := ioutil.ReadAll(c.Stdin) 28 | stdin = fmt.Sprintf("< %s", string(b)) 29 | } 30 | 31 | return fmt.Sprintf("[CMD] %s %s", strings.Join(c.Args, " "), stdin) 32 | } 33 | -------------------------------------------------------------------------------- /kubectl/create.go: -------------------------------------------------------------------------------- 1 | package kubectl 2 | 3 | // Create uploads a chart to Kubernetes 4 | func (r RealRunner) Create(stdin []byte, ns string) ([]byte, error) { 5 | args := []string{"create", "-f", "-"} 6 | 7 | if ns != "" { 8 | args = append([]string{"--namespace=" + ns}, args...) 9 | } 10 | 11 | cmd := command(args...) 12 | assignStdin(cmd, stdin) 13 | 14 | return cmd.CombinedOutput() 15 | } 16 | 17 | // Create returns the commands to kubectl 18 | func (r PrintRunner) Create(stdin []byte, ns string) ([]byte, error) { 19 | args := []string{"create", "-f", "-"} 20 | 21 | if ns != "" { 22 | args = append([]string{"--namespace=" + ns}, args...) 23 | } 24 | 25 | cmd := command(args...) 26 | assignStdin(cmd, stdin) 27 | 28 | return []byte(cmd.String()), nil 29 | } 30 | -------------------------------------------------------------------------------- /kubectl/create_test.go: -------------------------------------------------------------------------------- 1 | package kubectl 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPrintCreate(t *testing.T) { 8 | var client Runner = PrintRunner{} 9 | 10 | expected := `[CMD] kubectl --namespace=default-namespace create -f - < some stdin data` 11 | 12 | out, err := client.Create([]byte("some stdin data"), "default-namespace") 13 | if err != nil { 14 | t.Error(err) 15 | } 16 | 17 | actual := string(out) 18 | 19 | if expected != actual { 20 | t.Fatalf("actual %s != expected %s", actual, expected) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /kubectl/delete.go: -------------------------------------------------------------------------------- 1 | package kubectl 2 | 3 | // Delete removes a chart from Kubernetes. 4 | func (r RealRunner) Delete(name, ktype, ns string) ([]byte, error) { 5 | 6 | args := []string{"delete", ktype, name} 7 | 8 | if ns != "" { 9 | args = append([]string{"--namespace=" + ns}, args...) 10 | } 11 | return command(args...).CombinedOutput() 12 | } 13 | 14 | // Delete returns the commands to kubectl 15 | func (r PrintRunner) Delete(name, ktype, ns string) ([]byte, error) { 16 | 17 | args := []string{"delete", ktype, name} 18 | 19 | if ns != "" { 20 | args = append([]string{"--namespace=" + ns}, args...) 21 | } 22 | 23 | cmd := command(args...) 24 | return []byte(cmd.String()), nil 25 | } 26 | -------------------------------------------------------------------------------- /kubectl/get.go: -------------------------------------------------------------------------------- 1 | package kubectl 2 | 3 | // Get returns Kubernetes resources 4 | func (r RealRunner) Get(stdin []byte, ns string) ([]byte, error) { 5 | args := []string{"get", "-f", "-"} 6 | 7 | if ns != "" { 8 | args = append([]string{"--namespace=" + ns}, args...) 9 | } 10 | cmd := command(args...) 11 | assignStdin(cmd, stdin) 12 | 13 | return cmd.CombinedOutput() 14 | } 15 | 16 | // Get returns the commands to kubectl 17 | func (r PrintRunner) Get(stdin []byte, ns string) ([]byte, error) { 18 | args := []string{"get", "-f", "-"} 19 | 20 | if ns != "" { 21 | args = append([]string{"--namespace=" + ns}, args...) 22 | } 23 | cmd := command(args...) 24 | assignStdin(cmd, stdin) 25 | 26 | return []byte(cmd.String()), nil 27 | } 28 | -------------------------------------------------------------------------------- /kubectl/get_test.go: -------------------------------------------------------------------------------- 1 | package kubectl 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGet(t *testing.T) { 8 | Client = TestRunner{ 9 | out: []byte("running the get command"), 10 | } 11 | 12 | expects := "running the get command" 13 | out, _ := Client.Get([]byte{}, "") 14 | if string(out) != expects { 15 | t.Errorf("%s != %s", string(out), expects) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /kubectl/kubectl.go: -------------------------------------------------------------------------------- 1 | package kubectl 2 | 3 | // Path is the path of the kubectl binary 4 | var Path = "kubectl" 5 | 6 | // Runner is an interface to wrap kubectl convenience methods 7 | type Runner interface { 8 | // ClusterInfo returns Kubernetes cluster info 9 | ClusterInfo() ([]byte, error) 10 | // Apply updates or uploads a chart to Kubernetes 11 | Apply([]byte, string) ([]byte, error) 12 | // Create uploads a chart to Kubernetes 13 | Create([]byte, string) ([]byte, error) 14 | // Delete removes a chart from Kubernetes. 15 | Delete(string, string, string) ([]byte, error) 16 | // Get returns Kubernetes resources 17 | Get([]byte, string) ([]byte, error) 18 | } 19 | 20 | // RealRunner implements Runner to execute kubectl commands 21 | type RealRunner struct{} 22 | 23 | // PrintRunner implements Runner to return a []byte of the command to be executed 24 | type PrintRunner struct{} 25 | 26 | // Client stores the instance of Runner 27 | var Client Runner = RealRunner{} 28 | -------------------------------------------------------------------------------- /kubectl/kubectl_test.go: -------------------------------------------------------------------------------- 1 | package kubectl 2 | 3 | type TestRunner struct { 4 | Runner 5 | 6 | out []byte 7 | err error 8 | } 9 | 10 | func (r TestRunner) Get(stdin []byte, ns string) ([]byte, error) { 11 | return r.out, r.err 12 | } 13 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | 9 | pretty "github.com/deis/pkg/prettyprint" 10 | ) 11 | 12 | // Stdout is the logging destination for normal messages. 13 | var Stdout io.Writer = os.Stdout 14 | 15 | // Stderr is the logging destination for error messages. 16 | var Stderr io.Writer = os.Stderr 17 | 18 | // Stdin is the input alternative for logging. 19 | // 20 | // Applications that take console-like input should use this. 21 | var Stdin io.Reader = os.Stdin 22 | 23 | // IsDebugging toggles whether or not to enable debug output and behavior. 24 | var IsDebugging = false 25 | 26 | // ErrorState denotes if application is in an error state. 27 | var ErrorState = false 28 | 29 | // New creates a *log.Logger that writes to this source. 30 | func New() *log.Logger { 31 | ll := log.New(Stdout, pretty.Colorize("{{.Yellow}}--->{{.Default}} "), 0) 32 | return ll 33 | } 34 | 35 | // Msg passes through the formatter, but otherwise prints exactly as-is. 36 | // 37 | // No prettification. 38 | func Msg(format string, v ...interface{}) { 39 | fmt.Fprintf(Stdout, appendNewLine(format), v...) 40 | } 41 | 42 | // Die prints an error and then call os.Exit(1). 43 | func Die(format string, v ...interface{}) { 44 | Err(format, v...) 45 | if IsDebugging { 46 | panic(fmt.Sprintf(format, v...)) 47 | } 48 | os.Exit(1) 49 | } 50 | 51 | // CleanExit prints a message and then exits with 0. 52 | func CleanExit(format string, v ...interface{}) { 53 | Info(format, v...) 54 | os.Exit(0) 55 | } 56 | 57 | // Err prints an error message. It does not cause an exit. 58 | func Err(format string, v ...interface{}) { 59 | fmt.Fprint(Stderr, pretty.Colorize("{{.Red}}[ERROR]{{.Default}} ")) 60 | fmt.Fprintf(Stderr, appendNewLine(format), v...) 61 | ErrorState = true 62 | } 63 | 64 | // Info prints a green-tinted message. 65 | func Info(format string, v ...interface{}) { 66 | fmt.Fprint(Stderr, pretty.Colorize("{{.Green}}--->{{.Default}} ")) 67 | fmt.Fprintf(Stderr, appendNewLine(format), v...) 68 | } 69 | 70 | // Debug prints a cyan-tinted message if IsDebugging is true. 71 | func Debug(format string, v ...interface{}) { 72 | if IsDebugging { 73 | fmt.Fprint(Stderr, pretty.Colorize("{{.Cyan}}[DEBUG]{{.Default}} ")) 74 | fmt.Fprintf(Stderr, appendNewLine(format), v...) 75 | } 76 | } 77 | 78 | // Warn prints a yellow-tinted warning message. 79 | func Warn(format string, v ...interface{}) { 80 | fmt.Fprint(Stderr, pretty.Colorize("{{.Yellow}}[WARN]{{.Default}} ")) 81 | fmt.Fprintf(Stderr, appendNewLine(format), v...) 82 | } 83 | 84 | func appendNewLine(format string) string { 85 | return format + "\n" 86 | } 87 | -------------------------------------------------------------------------------- /manifest/manifest.go: -------------------------------------------------------------------------------- 1 | // Package manifest provides tools for working with Kubernetes manifests. 2 | package manifest 3 | 4 | import ( 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | 11 | "github.com/helm/helm-classic/codec" 12 | "github.com/helm/helm-classic/log" 13 | ) 14 | 15 | // Files gets a list of all manifest files inside of a chart. 16 | // 17 | // chartDir should contain the path to a chart (the directory which 18 | // holds a Chart.yaml file). 19 | // 20 | // This returns an error if it can't access the directory. 21 | func Files(chartDir string) ([]string, error) { 22 | dir := filepath.Join(chartDir, "manifests") 23 | files := []string{} 24 | 25 | if _, err := os.Stat(dir); err != nil { 26 | return files, err 27 | } 28 | 29 | // add manifest files 30 | walker := func(fname string, fi os.FileInfo, e error) error { 31 | if e != nil { 32 | log.Warn("Encountered error walking %q: %s", fname, e) 33 | return nil 34 | } 35 | 36 | if fi.IsDir() { 37 | return nil 38 | } 39 | 40 | if filepath.Ext(fname) == ".yaml" || filepath.Ext(fname) == ".yml" { 41 | files = append(files, fname) 42 | } 43 | 44 | return nil 45 | } 46 | filepath.Walk(dir, walker) 47 | 48 | return files, nil 49 | } 50 | 51 | // Manifest represents a Kubernetes manifest object. 52 | type Manifest struct { 53 | Version, Kind, Name string 54 | // Filename of source, "" if no file 55 | Source string 56 | VersionedObject *codec.Object 57 | } 58 | 59 | // Parse takes a filename, loads the file, and parses it into one or more *Manifest objects. 60 | func Parse(filename string) ([]*Manifest, error) { 61 | d, err := ioutil.ReadFile(filename) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | ms := []*Manifest{} 67 | 68 | docs, err := codec.YAML.Decode(d).All() 69 | if err != nil { 70 | return ms, fmt.Errorf("%s: %s", filename, err) 71 | } 72 | 73 | for _, doc := range docs { 74 | ref, err := doc.Meta() 75 | if err != nil { 76 | return nil, fmt.Errorf("%s: %s", filename, err) 77 | } 78 | 79 | m := &Manifest{Version: ref.APIVersion, Kind: ref.Kind, Name: ref.Name, VersionedObject: doc, Source: filename} 80 | ms = append(ms, m) 81 | } 82 | return ms, nil 83 | } 84 | 85 | // ParseDir parses all of the manifests inside of a chart directory. 86 | // 87 | // The directory should be the Chart directory (contains Chart.yaml and manifests/) 88 | // 89 | // This will return an error if the directory does not exist, or if there is an 90 | // error parsing or decoding any yaml files. 91 | func ParseDir(chartDir string) ([]*Manifest, error) { 92 | dir := filepath.Join(chartDir, "manifests") 93 | files := []*Manifest{} 94 | 95 | if _, err := os.Stat(dir); err != nil { 96 | return files, err 97 | } 98 | 99 | // add manifest files 100 | walker := func(fname string, fi os.FileInfo, e error) error { 101 | log.Debug("Parsing %s", fname) 102 | // Chauncey was right. 103 | if e != nil { 104 | return e 105 | } 106 | 107 | if fi.IsDir() { 108 | return nil 109 | } 110 | 111 | if filepath.Ext(fname) != ".yaml" && filepath.Ext(fname) != ".yml" { 112 | log.Debug("Skipping %s. Not a YAML file.", fname) 113 | return nil 114 | } 115 | 116 | m, err := Parse(fname) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | files = append(files, m...) 122 | 123 | return nil 124 | } 125 | 126 | return files, filepath.Walk(dir, walker) 127 | } 128 | 129 | // IsKeeper returns true if a manifest has a "helm-keep": "true" annotation. 130 | func IsKeeper(data []byte) bool { 131 | // Look for ("helm-keep": "true") up to 10 lines after ("annotations:"). 132 | var keep = regexp.MustCompile(`\"annotations\":\s+\{(\n.*){0,10}\"helm-keep\":\s+\"true\"`) 133 | return keep.Match(data) 134 | } 135 | -------------------------------------------------------------------------------- /manifest/manifest_test.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "io/ioutil" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | var testchart = "../testdata/charts/kitchensink" 10 | var testPlainManifest = "../testdata/service.json" 11 | var testKeeperManifest = "../testdata/service-keep.json" 12 | 13 | func TestFiles(t *testing.T) { 14 | fs, err := Files(testchart) 15 | if err != nil { 16 | t.Errorf("Failed to open %s: %s", testchart, err) 17 | } 18 | 19 | if len(fs) == 0 { 20 | t.Errorf("Expected at least one manifest file") 21 | } 22 | } 23 | 24 | func TestParse(t *testing.T) { 25 | 26 | files, _ := Files(testchart) 27 | found := 0 28 | for _, file := range files { 29 | if _, err := Parse(file); err != nil { 30 | t.Errorf("Failed to parse %s: %s", file, err) 31 | } 32 | found++ 33 | } 34 | 35 | if found == 0 { 36 | t.Errorf("Found no manifests to test.") 37 | } 38 | 39 | } 40 | 41 | func TestParseDir(t *testing.T) { 42 | manifests, err := ParseDir(testchart) 43 | if err != nil { 44 | t.Errorf("Failed to parse dir %s: %s", testchart, err) 45 | } 46 | 47 | target, _ := Files(testchart) 48 | if len(manifests) < len(target) { 49 | t.Errorf("Expected at least %d manifests. Got %d", len(target), len(manifests)) 50 | } 51 | 52 | for _, man := range manifests { 53 | if man.Source == "" { 54 | t.Error("No file set in manifest.Source.") 55 | } 56 | if man.Kind == "" { 57 | t.Error("Expected kind") 58 | } 59 | if man.Name == "" { 60 | t.Error("Expected name") 61 | } 62 | } 63 | 64 | // now test parsing bad files in a chart! 65 | testchart = "../testdata/charts/malformed" 66 | manifests, err = ParseDir(testchart) 67 | if err == nil { 68 | t.Errorf("Failed to raise an error when parsing dir %s", testchart) 69 | } 70 | if !strings.Contains(err.Error(), "malformed.yaml") { 71 | t.Errorf("Failed to identify which manifest failed to be parsed. Got %s", err) 72 | } 73 | } 74 | 75 | func TestIsKeeper(t *testing.T) { 76 | // test that an ordinary JSON manifest doesn't look like a keeper 77 | data, err := ioutil.ReadFile(testPlainManifest) 78 | if err != nil { 79 | t.Error(err) 80 | } 81 | if IsKeeper(data) { 82 | t.Errorf("Expected false for %s", testPlainManifest) 83 | } 84 | 85 | // test that a keeper JSON manifest is detected 86 | data, err = ioutil.ReadFile(testKeeperManifest) 87 | if err != nil { 88 | t.Error(err) 89 | } 90 | if !IsKeeper(data) { 91 | t.Errorf("Expected true for %s", testKeeperManifest) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Helm Documentation 2 | pages: 3 | - Home: index.md 4 | - Architecture: architecture.md 5 | - Using Labels: using_labels.md 6 | - Modeling Services: modeling_services.md 7 | - Workspace Layout: workspace.md 8 | - Authoring Chart Basics: authoring_charts.md 9 | - Authoring Awesome Charts: awesome.md 10 | - Additional Repositories: chart_tables.md 11 | - Generate and Template: generate-and-template.md 12 | - Plugin Support: plugins.md 13 | theme: readthedocs 14 | -------------------------------------------------------------------------------- /plugins/example/README.md: -------------------------------------------------------------------------------- 1 | # Helm Classic Example Plugin 2 | 3 | This example plugic highlights a few of the features of the Helm Classic plugin system. 4 | 5 | ## Usage 6 | 7 | In this directory, build the example plugin: 8 | 9 | ``` 10 | $ go build -o helm-example helm-example.go 11 | ``` 12 | 13 | Helm Classic will search the path for plugins. So assuming you have Helm 14 | installed, you can test your plugin like this: 15 | 16 | ``` 17 | $ PATH=$PATH:. helmc example -a foo -b bar baz 18 | Args are: [helm-example -a foo -b bar baz] 19 | Helm Classic home is: /Users/mattbutcher/.helmc 20 | Helm Classic command is: example 21 | Helm Classic default repo is: charts 22 | ``` 23 | 24 | The output of `helm-example` shows the contents of the arguments and 25 | environment variables that Helm Classic passes to plugins. 26 | -------------------------------------------------------------------------------- /plugins/example/helm-example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | fmt.Printf("Args are: %v\n", os.Args) 10 | // Although helmc itself may use the new HELMC_HOME environment variable to optionally define its 11 | // home directory, to maintain compatibility with charts created for the ORIGINAL helm, helmc 12 | // still expands "legacy" Helm environment variables, which Helm Classic plugins continue to use. 13 | fmt.Printf("Helm home is: %s\n", os.Getenv("HELM_HOME")) 14 | fmt.Printf("Helm command is: %s\n", os.Getenv("HELM_COMMAND")) 15 | fmt.Printf("Helm default repo is: %s\n", os.Getenv("HELM_DEFAULT_REPO")) 16 | } 17 | -------------------------------------------------------------------------------- /plugins/sec/README.md: -------------------------------------------------------------------------------- 1 | # Helm Classic Sec: Work On Secrets 2 | 3 | The `helm-sec` plugin provides a tool for working with Kubernetes 4 | secrets. 5 | 6 | It can: 7 | 8 | - Handle encoding secrets for you. 9 | - Generate or manage some kinds of secrets for you. 10 | - Create or modify secrets files for you. 11 | 12 | ## Examples 13 | 14 | The simplest invocation of `helmc sec` generates a secret file and sends 15 | it to stdout: 16 | 17 | ``` 18 | $ helm-sec name value 19 | kind: Secret 20 | apiVersion: v1 21 | metadata: 22 | name: name 23 | data: 24 | name: dmFsdWU= 25 | ``` 26 | 27 | (Note that `dmFsdWU=` is `value` base64 encoded) 28 | 29 | You can send the output to a file by specifying the file name with the 30 | `--file` or `-f` flags: 31 | 32 | ``` 33 | $ helm-sec -f secret.yaml name value 34 | ``` 35 | 36 | And `helmc sec` can generate passwords or tokens for you: 37 | 38 | ``` 39 | $ helmc sec --password mysecret 40 | ---> Password: jb@OTr}k|dG 0 { 111 | t.Fatalf("Expected 0 results, got %d", len(charts)) 112 | } 113 | } 114 | 115 | func TestCalcScore(t *testing.T) { 116 | i := NewIndex(testConfig, testCacheDir) 117 | 118 | fields := []string{"aaa", "bbb", "ccc", "ddd"} 119 | matchline := strings.Join(fields, sep) 120 | if r := i.calcScore(2, matchline); r != 0 { 121 | t.Errorf("Expected 0, got %d", r) 122 | } 123 | if r := i.calcScore(5, matchline); r != 1 { 124 | t.Errorf("Expected 1, got %d", r) 125 | } 126 | if r := i.calcScore(10, matchline); r != 2 { 127 | t.Errorf("Expected 2, got %d", r) 128 | } 129 | if r := i.calcScore(14, matchline); r != 3 { 130 | t.Errorf("Expected 3, got %d", r) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /test/helm.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "reflect" 8 | "regexp" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/helm/helm-classic/log" 13 | "github.com/helm/helm-classic/util" 14 | ) 15 | 16 | const tmpConfigfile = `repos: 17 | default: charts 18 | tables: 19 | - name: charts 20 | repo: https://github.com/helm/charts 21 | ` 22 | 23 | // HelmRoot - dir root of the project 24 | var HelmRoot = filepath.Join(os.Getenv("GOPATH"), "src/github.com/helm/helm-classic/") 25 | 26 | // CreateTmpHome create a temporary directory for $HELMC_HOME 27 | func CreateTmpHome() string { 28 | tmpHome, _ := ioutil.TempDir("", "helmc_home") 29 | defer os.Remove(tmpHome) 30 | return tmpHome 31 | } 32 | 33 | // FakeUpdate add testdata to home path 34 | func FakeUpdate(home string) { 35 | util.EnsureHome(home) 36 | 37 | ioutil.WriteFile(filepath.Join(home, util.Configfile), []byte(tmpConfigfile), 0755) 38 | 39 | // absolute path to testdata charts 40 | testChartsPath := filepath.Join(HelmRoot, "testdata/charts") 41 | 42 | // copy testdata charts into cache 43 | // mock git clone 44 | util.CopyDir(testChartsPath, filepath.Join(home, "cache/charts")) 45 | } 46 | 47 | // ExpectEquals assert a == b 48 | func ExpectEquals(t *testing.T, a interface{}, b interface{}) { 49 | if a != b { 50 | t.Errorf("\n[Expected] type: %v\n%v\n[Got] type: %v\n%v\n", reflect.TypeOf(b), b, reflect.TypeOf(a), a) 51 | } 52 | } 53 | 54 | // ExpectMatches assert a ~= b 55 | func ExpectMatches(t *testing.T, actual string, expected string) { 56 | regexp := regexp.MustCompile(expected) 57 | 58 | if !regexp.Match([]byte(actual)) { 59 | t.Errorf("\n[Expected] %v\nto contain %v\n", actual, expected) 60 | } 61 | } 62 | 63 | // ExpectContains assert b is contained within a 64 | func ExpectContains(t *testing.T, actual string, expected string) { 65 | if !strings.Contains(actual, expected) { 66 | t.Errorf("\n[Expected] %v\nto contain %v\n", actual, expected) 67 | } 68 | } 69 | 70 | // CaptureOutput redirect all log/std streams, capture and replace 71 | func CaptureOutput(fn func()) (out string) { 72 | logStderr := log.Stderr 73 | logStdout := log.Stdout 74 | osStdout := os.Stdout 75 | osStderr := os.Stderr 76 | 77 | defer func() { 78 | log.Stderr = logStderr 79 | log.Stdout = logStdout 80 | os.Stdout = osStdout 81 | os.Stderr = osStderr 82 | 83 | if r := recover(); r != nil { 84 | out = r.(string) 85 | } 86 | }() 87 | 88 | r, w, _ := os.Pipe() 89 | 90 | log.Stderr = w 91 | log.Stdout = w 92 | os.Stdout = w 93 | os.Stderr = w 94 | 95 | fn() 96 | 97 | // read test output and restore previous stdout 98 | w.Close() 99 | b, _ := ioutil.ReadAll(r) 100 | out = string(b) 101 | return 102 | } 103 | -------------------------------------------------------------------------------- /testdata/Configfile.yaml: -------------------------------------------------------------------------------- 1 | # Testing data for package `config`. 2 | repos: 3 | default: technosophos 4 | tables: 5 | - name: charts 6 | repo: https://github.com/helm/charts 7 | - name: technosophos 8 | repo: https://github.com/helm/charts 9 | - name: clown-nose 10 | repo: https://github.com/helm/charts 11 | workspace: 12 | ignore: me 13 | -------------------------------------------------------------------------------- /testdata/README.md: -------------------------------------------------------------------------------- 1 | This directory contains testing data. 2 | 3 | The `workspace` and `cache` symlinks are a convenience to make this 4 | directory appear to be both a cache and a workspace. 5 | -------------------------------------------------------------------------------- /testdata/cache/charts/README.md: -------------------------------------------------------------------------------- 1 | Test fixture for repo tests. 2 | -------------------------------------------------------------------------------- /testdata/charts/dep1/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: dep1 2 | from: 3 | name: dep1 4 | version: 1.2.3 5 | home: http://github.com/helm/helm 6 | version: 1.2.3 7 | description: Example dependency 8 | maintainers: 9 | - Helm 10 | details: 11 | This package is used for dependency testing. 12 | -------------------------------------------------------------------------------- /testdata/charts/dep2/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: dep2 2 | from: 3 | name: dep2 4 | version: 1.2.9 5 | home: http://github.com/helm/helm 6 | version: 1.2.9 7 | description: Example dependency 8 | maintainers: 9 | - Helm 10 | details: 11 | This package is used for dependency testing. 12 | -------------------------------------------------------------------------------- /testdata/charts/dep3/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: dep3clone 2 | from: 3 | name: dep3 4 | version: 5.2.3 5 | home: http://github.com/helm/helm 6 | version: 5.2.3 7 | description: Example dependency 8 | maintainers: 9 | - Helm 10 | details: 11 | This package is used for dependency testing. 12 | -------------------------------------------------------------------------------- /testdata/charts/deptest/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: deptest 2 | home: http://github.com/helm/helm 3 | version: 2.3.4 4 | description: Example dependency 5 | maintainers: 6 | - Helm 7 | details: 8 | This package is used for dependency testing. 9 | dependencies: 10 | - name: dep1 11 | version: "~1.2" 12 | - name: dep2 13 | version: ">1.0.0" 14 | - name: dep3 15 | version: "<=2.0" 16 | - name: kitchensink 17 | version: "*" 18 | - name: honkIfYouLoveDucks 19 | version: "5.6.7" 20 | -------------------------------------------------------------------------------- /testdata/charts/generate/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: generate 2 | home: http://github.com/helm/charts 3 | version: 0.0.1 4 | description: Witness the power of this fully operational battlestation. 5 | maintainers: 6 | - Notta Moon 7 | details: 8 | This package exhibits template support. 9 | -------------------------------------------------------------------------------- /testdata/charts/generate/ignore/ignoreme.yaml: -------------------------------------------------------------------------------- 1 | #helm:generate no-such-command 2 | -------------------------------------------------------------------------------- /testdata/charts/generate/manifests/pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: alpine 5 | labels: 6 | heritage: helm 7 | spec: 8 | restartPolicy: Never 9 | containers: 10 | - name: waiter 11 | image: "alpine:3.2" 12 | command: ["/bin/sleep","9000"] 13 | -------------------------------------------------------------------------------- /testdata/charts/generate/tpl/pod.tpl.yaml: -------------------------------------------------------------------------------- 1 | #helm:generate helm tpl -o manifests/pod.yaml -d $HELM_GENERATE_DIR/values.toml $HELM_GENERATE_FILE 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: www 6 | labels: 7 | heritage: helm 8 | spec: 9 | restartPolicy: Never 10 | containers: 11 | - name: {{default "www-server" .ContainerName}} 12 | image: {{default "nginx" .Image}} 13 | command: ["/bin/sleep","9000"] 14 | -------------------------------------------------------------------------------- /testdata/charts/generate/values.toml: -------------------------------------------------------------------------------- 1 | Image = "ozo" 2 | -------------------------------------------------------------------------------- /testdata/charts/keep/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: keep 2 | home: https://github.com/helm/helm-classic 3 | version: 0.0.1 4 | description: Namespace manifest with keeper annotation. 5 | maintainers: 6 | - Deis Team 7 | details: |- 8 | This package provides a Kubernetes namespace manifest with a "helm-keep" annotation that marks 9 | it for special install and uninstall handling. 10 | Helm Classic v0.8.0 or later is required to use this chart. 11 | -------------------------------------------------------------------------------- /testdata/charts/keep/manifests/keep-ns.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: keep 5 | labels: 6 | name: keep 7 | annotations: 8 | helm-keep: "true" 9 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: kitchensink 2 | home: http://github.com/helm/helm 3 | source: 4 | - https://example.com/helm 5 | version: 0.0.1 6 | description: All the things, all semantically, none working 7 | maintainers: 8 | - Helm 9 | details: 10 | This package provides a sampling of all of the different manifest types. 11 | It can be used to test ordering and other properties of a chart. 12 | dependencies: 13 | - name: bogodep 14 | version: ~10.21 15 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/annoy_testers/README.md: -------------------------------------------------------------------------------- 1 | This directory is here to break things if directories are read 2 | incorrectly. 3 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/annoy_testers/broken.yaml: -------------------------------------------------------------------------------- 1 | can: 2 | you: 3 | say: -syntax 4 | error 5 | ? 6 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/manifests/foo.json: -------------------------------------------------------------------------------- 1 | { 2 | "wedontsupport": "json" 3 | } 4 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/manifests/nested/nested-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: nested 5 | spec: 6 | containers: 7 | - name: nested 8 | image: alpine:3.2 9 | command: 10 | - "sleep" 11 | - "90000" 12 | --- 13 | apiVersion: v1 14 | kind: Pod 15 | metadata: 16 | name: nested2 17 | spec: 18 | containers: 19 | - name: nested2 20 | image: alpine:3.2 21 | command: 22 | - "sleep" 23 | - "90000" 24 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/manifests/sink-configmap.yaml: -------------------------------------------------------------------------------- 1 | #helm:generate helm tpl -d values.toml -o manifests/drone-configmap.yaml $HELM_GENERATE_FILE 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | creationTimestamp: 2016-03-30T21:14:38Z 6 | name: drone 7 | namespace: default 8 | data: 9 | caddy.conf: |- 10 | tls replace@me.com 11 | drone.testing.com { 12 | proxy / drone:80 { 13 | proxy_header X-Forwarded-Proto {scheme} 14 | proxy_header X-Forwarded-For {host} 15 | proxy_header Host {host} 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/manifests/sink-daemonset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: DaemonSet 3 | metadata: 4 | labels: 5 | app: datastore 6 | name: datastore 7 | spec: 8 | template: 9 | metadata: 10 | labels: 11 | app: datastore-shard 12 | spec: 13 | nodeSelector: 14 | app: datastore-node 15 | containers: 16 | name: datastore-shard 17 | image: kubernetes/sharded 18 | ports: 19 | - containerPort: 9042 20 | name: main 21 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/manifests/sink-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | spec: 6 | replicas: 3 7 | template: 8 | metadata: 9 | labels: 10 | app: nginx 11 | spec: 12 | containers: 13 | - name: nginx 14 | image: nginx:1.7.9 15 | ports: 16 | - containerPort: 80 17 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/manifests/sink-limits.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: LimitRange 3 | metadata: 4 | name: kitchensink-limits 5 | namespace: kitchensink 6 | spec: 7 | limits: 8 | - default: 9 | memory: 1000Mi 10 | defaultRequest: 11 | cpu: 250m 12 | memory: 500Mi 13 | type: Container 14 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/manifests/sink-namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: kitchensink 5 | labels: 6 | name: kitchensink 7 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/manifests/sink-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: deis-empty-pod 5 | env: devel 6 | spec: 7 | containers: 8 | - name: deis-empty-pod 9 | image: alpine:3.2 10 | command: 11 | - "sleep" 12 | - "90000" 13 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/manifests/sink-rc.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ReplicationController 3 | metadata: 4 | name: deis-etcd-1 5 | labels: 6 | name: deis-etcd-1 7 | spec: 8 | replicas: 3 9 | selector: 10 | name: deis-etcd-1 11 | template: 12 | metadata: 13 | labels: 14 | name: deis-etcd-1 15 | spec: 16 | #volumes: 17 | #- name: "discovery-token" 18 | #secret: 19 | #secretName: deis-etcd-discovery-token 20 | containers: 21 | - name: deis-etcd-1 22 | image: "192.168.99.100:5000/deis/etcd:0.0.1-2-g0d0e135" 23 | env: 24 | - name: "DEIS_ETCD_CLUSTER_SIZE" 25 | value: "3" 26 | - name: "ETCD_NAME" 27 | value: "deis1" 28 | volumeMounts: 29 | - name: "discovery-token" 30 | readOnly: true 31 | mountPath: "/var/run/secrets/deis/etcd/discovery" 32 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/manifests/sink-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: deis-etcd-discovery-token 5 | data: 6 | token: RTJENjQ1NzYtRDU0Ny00OThDLUFGNzEtM0NGNkVGMzE5QThCCg== 7 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/manifests/sink-server.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: deis-etcd-1 5 | labels: 6 | name: deis-etcd-1 7 | app: deis 8 | spec: 9 | ports: 10 | - name: peer 11 | port: 2380 12 | protocol: TCP 13 | - name: client 14 | port: 4100 15 | targetPort: 4100 16 | protocol: TCP 17 | selector: 18 | name: deis-etcd-1 19 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/manifests/sink-serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: elasticsearch 5 | -------------------------------------------------------------------------------- /testdata/charts/kitchensink/manifests/sink-volume.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: sink-volume 5 | spec: 6 | capacity: 7 | storage: 5Gi 8 | accessModes: 9 | - ReadWriteOnce 10 | persistentVolumeReclaimPolicy: Recycle 11 | nfs: 12 | path: /tmp 13 | server: 172.17.0.2 14 | -------------------------------------------------------------------------------- /testdata/charts/malformed/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: malformed 2 | home: http://github.com/helm/helm 3 | source: 4 | - https://example.com/helm 5 | version: 0.0.1 6 | description: All the things, all malformed, none working 7 | maintainers: 8 | - Helm 9 | details: 10 | This package provides poorly formatted manifests. 11 | It can be used to test how helm parses a chart. 12 | -------------------------------------------------------------------------------- /testdata/charts/malformed/manifests/malformed.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | spec: 4 | containers: 5 | - name: "look-ma-no-end-quote! 6 | image: alpine:3.2 7 | -------------------------------------------------------------------------------- /testdata/charts/misnamed/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: othername 2 | from: 3 | name: dep1 4 | version: 1.2.3 5 | home: http://github.com/helm/helm 6 | version: 1.2.3 7 | description: Example dependency 8 | maintainers: 9 | - Helm 10 | details: 11 | This package is used for dependency testing. 12 | -------------------------------------------------------------------------------- /testdata/charts/redis/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: redis-standalone 2 | home: http://github.com/deis/redis 3 | version: 0.0.1 4 | description: Simple standalone pod running Redis. 5 | maintainers: 6 | - Gabe Monroy 7 | details: 8 | This package provides a basic, standalone instance of Redis deployed as a single pod. 9 | -------------------------------------------------------------------------------- /testdata/charts/redis/manifests/redis-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: redis 5 | spec: 6 | restartPolicy: Never 7 | containers: 8 | - name: redis 9 | image: redis 10 | -------------------------------------------------------------------------------- /testdata/charts/searchtest1/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: homeslice 2 | home: http://github.com/deis/redis 3 | version: 0.0.1 4 | description: Search Test 5 | maintainers: 6 | - Helm Team 7 | details: 8 | This package is for testing. 9 | -------------------------------------------------------------------------------- /testdata/charts/searchtest1/manifests/redis-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: redis 5 | spec: 6 | restartPolicy: Never 7 | containers: 8 | - name: redis 9 | image: redis 10 | -------------------------------------------------------------------------------- /testdata/charts/searchtest2/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: searchtest2 2 | home: http://github.com/deis/redis 3 | version: 0.0.1 4 | description: homeskillet 5 | maintainers: 6 | - Helm Team 7 | details: 8 | This package is for testing. 9 | -------------------------------------------------------------------------------- /testdata/charts/searchtest2/manifests/redis-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: redis 5 | spec: 6 | restartPolicy: Never 7 | containers: 8 | - name: redis 9 | image: redis 10 | -------------------------------------------------------------------------------- /testdata/config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | default: charts 3 | tables: 4 | - name: charts 5 | repo: https://github.com/helm/charts 6 | workspace: 7 | -------------------------------------------------------------------------------- /testdata/daemonset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: DaemonSet 3 | metadata: 4 | name: prometheus-node-exporter 5 | spec: 6 | template: 7 | metadata: 8 | name: prometheus-node-exporter 9 | labels: 10 | daemon: prom-node-exp 11 | spec: 12 | containers: 13 | - name: c 14 | image: prom/prometheus 15 | ports: 16 | - containerPort: 9090 17 | hostPort: 9090 18 | name: serverport 19 | -------------------------------------------------------------------------------- /testdata/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | spec: 6 | replicas: 3 7 | template: 8 | metadata: 9 | labels: 10 | app: nginx 11 | spec: 12 | containers: 13 | - name: nginx 14 | image: nginx:1.7.9 15 | ports: 16 | - containerPort: 80 17 | -------------------------------------------------------------------------------- /testdata/generator/fail.txt: -------------------------------------------------------------------------------- 1 | // This should not be interpreted as a generator 2 | //helm:generate echo foo bar baz 3 | -------------------------------------------------------------------------------- /testdata/generator/fail2.txt: -------------------------------------------------------------------------------- 1 | // helm:generate2 fail fail fail 2 | This should fail. 3 | -------------------------------------------------------------------------------- /testdata/generator/four/five.txt: -------------------------------------------------------------------------------- 1 | //helm:generate echo foo bar baz 2 | Alternate comments. 3 | -------------------------------------------------------------------------------- /testdata/generator/four/four.txt: -------------------------------------------------------------------------------- 1 | /*helm:generate echo foo bar baz*/ 2 | This one also uses c-style comments 3 | -------------------------------------------------------------------------------- /testdata/generator/one.yaml: -------------------------------------------------------------------------------- 1 | #helm:generate echo foo bar baz 2 | 3 | 4 | This is just extra data. 5 | -------------------------------------------------------------------------------- /testdata/generator/three.txt: -------------------------------------------------------------------------------- 1 | /* helm:generate echo foo bar baz */ 2 | This one uses c-style comments. 3 | -------------------------------------------------------------------------------- /testdata/generator/two.yaml: -------------------------------------------------------------------------------- 1 | # helm:generate echo foo bar baz 2 | description: This is a YAML file that does nothing. 3 | -------------------------------------------------------------------------------- /testdata/git: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "I'm a fake" 4 | -------------------------------------------------------------------------------- /testdata/helm-plugin: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo HELLO $@ 4 | -------------------------------------------------------------------------------- /testdata/horizontalpodautoscaler.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: php-apache 5 | namespace: default 6 | spec: 7 | scaleRef: 8 | kind: ReplicationController 9 | name: php-apache 10 | namespace: default 11 | minReplicas: 1 12 | maxReplicas: 10 13 | cpuUtilization: 14 | targetPercentage: 50 15 | -------------------------------------------------------------------------------- /testdata/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: test-ingress 5 | spec: 6 | backend: 7 | serviceName: testsvc 8 | servicePort: 80 9 | -------------------------------------------------------------------------------- /testdata/job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Job 3 | metadata: 4 | name: pi 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: pi 9 | template: 10 | metadata: 11 | name: pi 12 | labels: 13 | app: pi 14 | spec: 15 | containers: 16 | - name: pi 17 | image: perl 18 | command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] 19 | restartPolicy: Never 20 | -------------------------------------------------------------------------------- /testdata/kubectl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "I'm a fake" 4 | -------------------------------------------------------------------------------- /testdata/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: kitchensink 5 | labels: 6 | name: kitchensink 7 | -------------------------------------------------------------------------------- /testdata/pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | name: cassandra 6 | name: cassandra 7 | spec: 8 | containers: 9 | - args: 10 | - /run.sh 11 | resources: 12 | limits: 13 | cpu: "0.1" 14 | image: gcr.io/google_containers/cassandra:v6 15 | name: cassandra 16 | ports: 17 | - name: cql 18 | containerPort: 9042 19 | - name: thrift 20 | containerPort: 9160 21 | volumeMounts: 22 | - name: data 23 | mountPath: /cassandra_data 24 | env: 25 | - name: MAX_HEAP_SIZE 26 | value: 512M 27 | - name: HEAP_NEWSIZE 28 | value: 100M 29 | - name: POD_NAMESPACE 30 | valueFrom: 31 | fieldRef: 32 | fieldPath: metadata.namespace 33 | volumes: 34 | - name: data 35 | emptyDir: {} 36 | -------------------------------------------------------------------------------- /testdata/policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind" : "Policy", 3 | "apiVersion" : "v1", 4 | "predicates" : [ 5 | {"name" : "PodFitsPorts"}, 6 | {"name" : "PodFitsResources"}, 7 | {"name" : "NoDiskConflict"}, 8 | {"name" : "MatchNodeSelector"}, 9 | {"name" : "HostName"} 10 | ], 11 | "priorities" : [ 12 | {"name" : "LeastRequestedPriority", "weight" : 1}, 13 | {"name" : "BalancedResourceAllocation", "weight" : 1}, 14 | {"name" : "ServiceSpreadingPriority", "weight" : 1}, 15 | {"name" : "EqualPriority", "weight" : 1} 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /testdata/rc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ReplicationController 3 | metadata: 4 | labels: 5 | name: cassandra 6 | name: cassandra 7 | spec: 8 | replicas: 1 9 | selector: 10 | name: cassandra 11 | template: 12 | metadata: 13 | labels: 14 | name: cassandra 15 | spec: 16 | containers: 17 | - command: 18 | - /run.sh 19 | resources: 20 | limits: 21 | cpu: 0.1 22 | env: 23 | - name: MAX_HEAP_SIZE 24 | value: 512M 25 | - name: HEAP_NEWSIZE 26 | value: 100M 27 | - name: POD_NAMESPACE 28 | valueFrom: 29 | fieldRef: 30 | fieldPath: metadata.namespace 31 | image: gcr.io/google_containers/cassandra:v6 32 | name: cassandra 33 | ports: 34 | - containerPort: 9042 35 | name: cql 36 | - containerPort: 9160 37 | name: thrift 38 | volumeMounts: 39 | - mountPath: /cassandra_data 40 | name: data 41 | volumes: 42 | - name: data 43 | emptyDir: {} 44 | -------------------------------------------------------------------------------- /testdata/service-keep.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Service", 4 | "metadata": { 5 | "name": "example-todo", 6 | "labels": { 7 | "heritage": "deis" 8 | }, 9 | "annotations": { 10 | "helm-keep": "true" 11 | } 12 | }, 13 | "spec": { 14 | "type": "NodePort", 15 | "ports": [ 16 | { 17 | "port": 3000, 18 | "nodePort": 30000, 19 | "name": "http", 20 | "protocol": "TCP" 21 | } 22 | ], 23 | "selector": { 24 | "name": "example-todo" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /testdata/service.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Service", 4 | "metadata": { 5 | "name": "example-todo", 6 | "labels": { 7 | "heritage": "deis" 8 | }, 9 | "annotations": { 10 | "helm-bleep": "true" 11 | } 12 | }, 13 | "spec": { 14 | "type": "NodePort", 15 | "ports": [ 16 | { 17 | "port": 3000, 18 | "nodePort": 30000, 19 | "name": "http", 20 | "protocol": "TCP" 21 | } 22 | ], 23 | "selector": { 24 | "name": "example-todo" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /testdata/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: example-todo 5 | labels: 6 | heritage: deis 7 | spec: 8 | type: NodePort 9 | ports: 10 | - port: 3000 11 | nodePort: 30000 12 | name: http 13 | protocol: TCP 14 | selector: 15 | name: example-todo 16 | -------------------------------------------------------------------------------- /testdata/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: elasticsearch 5 | -------------------------------------------------------------------------------- /testdata/template/one.json: -------------------------------------------------------------------------------- 1 | {"who": "world"} 2 | -------------------------------------------------------------------------------- /testdata/template/one.toml: -------------------------------------------------------------------------------- 1 | who = "world" 2 | -------------------------------------------------------------------------------- /testdata/template/one.tpl: -------------------------------------------------------------------------------- 1 | {{default "Hello" .hello}} {{default "clowns" .who | title}}! 2 | -------------------------------------------------------------------------------- /testdata/template/one.yaml: -------------------------------------------------------------------------------- 1 | who: world 2 | -------------------------------------------------------------------------------- /testdata/test-Chart.yaml: -------------------------------------------------------------------------------- 1 | name: alpine-pod 2 | home: http://github.com/helm/helm 3 | source: 4 | - https://example.com/helm 5 | version: 0.0.1 6 | description: Simple pod running Alpine Linux. 7 | maintainers: 8 | - Somebody 9 | - Nobody 10 | details: 11 | This package provides a basic Alpine Linux image that can be used for basic 12 | debugging and troubleshooting. By default, it starts up, sleeps for a long 13 | time, and then eventually stops. 14 | dependencies: 15 | - name: foo 16 | version: "> 1.2.3" 17 | - name: bar 18 | version: "1.1.1-3.0.0" 19 | preinstall: 20 | mykeys: generate-keypair foo 21 | -------------------------------------------------------------------------------- /testdata/three-pods-and-three-services.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | labels: 6 | app: etcd 7 | etcd_node: etcd0 8 | name: etcd0 9 | spec: 10 | containers: 11 | - 12 | command: 13 | - /etcd 14 | - "-name" 15 | - etcd0 16 | - "-initial-advertise-peer-urls" 17 | - "http://etcd0:2380" 18 | - "-listen-peer-urls" 19 | - "http://0.0.0.0:2380" 20 | - "-listen-client-urls" 21 | - "http://0.0.0.0:2379" 22 | - "-advertise-client-urls" 23 | - "http://etcd0:2379" 24 | - "-initial-cluster" 25 | - "etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380" 26 | - "-initial-cluster-state" 27 | - new 28 | image: "quay.io/coreos/etcd:latest" 29 | name: etcd0 30 | ports: 31 | - 32 | containerPort: 2379 33 | name: client 34 | protocol: TCP 35 | - 36 | containerPort: 2380 37 | name: server 38 | protocol: TCP 39 | restartPolicy: Never 40 | --- 41 | apiVersion: v1 42 | kind: Service 43 | metadata: 44 | labels: 45 | etcd_node: etcd0 46 | name: etcd0 47 | spec: 48 | ports: 49 | - 50 | name: client 51 | port: 2379 52 | protocol: TCP 53 | targetPort: 2379 54 | - 55 | name: server 56 | port: 2380 57 | protocol: TCP 58 | targetPort: 2380 59 | selector: 60 | etcd_node: etcd0 61 | --- 62 | apiVersion: v1 63 | kind: Pod 64 | metadata: 65 | labels: 66 | app: etcd 67 | etcd_node: etcd1 68 | name: etcd1 69 | spec: 70 | containers: 71 | - 72 | command: 73 | - /etcd 74 | - "-name" 75 | - etcd1 76 | - "-initial-advertise-peer-urls" 77 | - "http://etcd1:2380" 78 | - "-listen-peer-urls" 79 | - "http://0.0.0.0:2380" 80 | - "-listen-client-urls" 81 | - "http://0.0.0.0:2379" 82 | - "-advertise-client-urls" 83 | - "http://etcd1:2379" 84 | - "-initial-cluster" 85 | - "etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380" 86 | - "-initial-cluster-state" 87 | - new 88 | image: "quay.io/coreos/etcd:latest" 89 | name: etcd1 90 | ports: 91 | - 92 | containerPort: 2379 93 | name: client 94 | protocol: TCP 95 | - 96 | containerPort: 2380 97 | name: server 98 | protocol: TCP 99 | restartPolicy: Never 100 | --- 101 | apiVersion: v1 102 | kind: Service 103 | metadata: 104 | labels: 105 | etcd_node: etcd1 106 | name: etcd1 107 | spec: 108 | ports: 109 | - 110 | name: client 111 | port: 2379 112 | protocol: TCP 113 | targetPort: 2379 114 | - 115 | name: server 116 | port: 2380 117 | protocol: TCP 118 | targetPort: 2380 119 | selector: 120 | etcd_node: etcd1 121 | --- 122 | apiVersion: v1 123 | kind: Pod 124 | metadata: 125 | labels: 126 | app: etcd 127 | etcd_node: etcd2 128 | name: etcd2 129 | spec: 130 | containers: 131 | - 132 | command: 133 | - /etcd 134 | - "-name" 135 | - etcd2 136 | - "-initial-advertise-peer-urls" 137 | - "http://etcd2:2380" 138 | - "-listen-peer-urls" 139 | - "http://0.0.0.0:2380" 140 | - "-listen-client-urls" 141 | - "http://0.0.0.0:2379" 142 | - "-advertise-client-urls" 143 | - "http://etcd2:2379" 144 | - "-initial-cluster" 145 | - "etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380" 146 | - "-initial-cluster-state" 147 | - new 148 | image: "quay.io/coreos/etcd:latest" 149 | name: etcd2 150 | ports: 151 | - 152 | containerPort: 2379 153 | name: client 154 | protocol: TCP 155 | - 156 | containerPort: 2380 157 | name: server 158 | protocol: TCP 159 | restartPolicy: Never 160 | --- 161 | apiVersion: v1 162 | kind: Service 163 | metadata: 164 | labels: 165 | etcd_node: etcd2 166 | name: etcd2 167 | spec: 168 | ports: 169 | - 170 | name: client 171 | port: 2379 172 | protocol: TCP 173 | targetPort: 2379 174 | - 175 | name: server 176 | port: 2380 177 | protocol: TCP 178 | targetPort: 2380 179 | selector: 180 | etcd_node: etcd2 181 | -------------------------------------------------------------------------------- /util/helm.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/helm/helm-classic/log" 10 | ) 11 | 12 | // Configfile is the file containing Helm Classic's YAML configuration data. 13 | const Configfile = "config.yaml" 14 | 15 | // DefaultConfigfile is the default Helm Classic configuration. 16 | const DefaultConfigfile = `repos: 17 | default: charts 18 | tables: 19 | - name: charts 20 | repo: https://github.com/helm/charts 21 | workspace: 22 | ` 23 | 24 | // EnsureHome ensures that a HELMC_HOME exists. 25 | func EnsureHome(home string) { 26 | 27 | must := []string{home, CacheDirectory(home), filepath.Join(home, workspacePath)} 28 | 29 | for _, p := range must { 30 | if fi, err := os.Stat(p); err != nil { 31 | log.Debug("Creating %s", p) 32 | if err := os.MkdirAll(p, 0755); err != nil { 33 | log.Die("Could not create %q: %s", p, err) 34 | } 35 | } else if !fi.IsDir() { 36 | log.Die("%s must be a directory.", home) 37 | } 38 | } 39 | 40 | refi := filepath.Join(home, Configfile) 41 | if _, err := os.Stat(refi); err != nil { 42 | log.Info("Creating %s", refi) 43 | // Attempt to create a Repos.yaml 44 | if err := ioutil.WriteFile(refi, []byte(DefaultConfigfile), 0755); err != nil { 45 | log.Die("Could not create %s: %s", refi, err) 46 | } 47 | } 48 | 49 | if err := os.Chdir(home); err != nil { 50 | log.Die("Could not change to directory %q: %s", home, err) 51 | } 52 | } 53 | 54 | // CopyDir copy a directory and its subdirectories. 55 | func CopyDir(src, dst string) error { 56 | 57 | var failure error 58 | 59 | walker := func(fname string, fi os.FileInfo, e error) error { 60 | if e != nil { 61 | log.Warn("Encounter error walking %q: %s", fname, e) 62 | failure = e 63 | return nil 64 | } 65 | 66 | rf, err := filepath.Rel(src, fname) 67 | if err != nil { 68 | log.Warn("Could not find relative path: %s", err) 69 | return nil 70 | } 71 | df := filepath.Join(dst, rf) 72 | 73 | // Handle directories by creating mirrors. 74 | if fi.IsDir() { 75 | if err := os.MkdirAll(df, fi.Mode()); err != nil { 76 | log.Warn("Could not create %q: %s", df, err) 77 | failure = err 78 | } 79 | return nil 80 | } 81 | 82 | // Otherwise, copy files. 83 | in, err := os.Open(fname) 84 | if err != nil { 85 | log.Warn("Skipping file %s: %s", fname, err) 86 | return nil 87 | } 88 | out, err := os.Create(df) 89 | if err != nil { 90 | in.Close() 91 | log.Warn("Skipping file copy %s: %s", fname, err) 92 | return nil 93 | } 94 | out.Chmod(fi.Mode()) 95 | if _, err = io.Copy(out, in); err != nil { 96 | log.Warn("Copy from %s to %s failed: %s", fname, df, err) 97 | } 98 | 99 | if err := out.Close(); err != nil { 100 | log.Warn("Failed to close %q: %s", df, err) 101 | } 102 | if err := in.Close(); err != nil { 103 | log.Warn("Failed to close reader %q: %s", fname, err) 104 | } 105 | 106 | return nil 107 | } 108 | filepath.Walk(src, walker) 109 | return failure 110 | } 111 | 112 | //CopyFile copies file from src to dst 113 | func CopyFile(src string, dst string) error { 114 | data, err := ioutil.ReadFile(src) 115 | if err == nil { 116 | err = ioutil.WriteFile(dst, data, 0644) 117 | } 118 | return err 119 | } 120 | -------------------------------------------------------------------------------- /util/helm_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | const perm = 0755 10 | 11 | func TestCopyDir(t *testing.T) { 12 | srcDir, err := ioutil.TempDir("", "srcDir") 13 | if err != nil { 14 | t.Fatalf("error creating temp directory (%s)", err) 15 | } 16 | destDir, err := ioutil.TempDir("", "destDir") 17 | if err != nil { 18 | t.Fatalf("error creating temp directory (%s)", err) 19 | } 20 | data := []byte("web: example-go") 21 | if err = ioutil.WriteFile(srcDir+"/chart", data, perm); err != nil { 22 | t.Fatalf("error creating %s/chart (%s)", srcDir, err) 23 | } 24 | defer func() { 25 | if err = os.RemoveAll(srcDir); err != nil { 26 | t.Fatalf("failed to remove %s directory (%s)", srcDir, err) 27 | } 28 | if err = os.RemoveAll(destDir); err != nil { 29 | t.Fatalf("failed to remove %s directory (%s)", destDir, err) 30 | } 31 | }() 32 | if err = CopyDir(srcDir, destDir); err != nil { 33 | t.Errorf("[Expected] error not have occured\n[Got] error (%s)\n", err) 34 | } 35 | fileInfo, err := os.Stat(destDir + "/chart") 36 | if err != nil { 37 | t.Fatalf("error getting file info in destination directory (%s)", err) 38 | } 39 | 40 | if perm != fileInfo.Mode() { 41 | t.Errorf("\n[Expected] %d\n[Got] %d\n", perm, fileInfo.Mode()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /util/structure.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "path/filepath" 4 | 5 | // cachePath is the suffix for the cache. 6 | const cachePath = "cache" 7 | 8 | // workspacePath is the user's workspace directory. 9 | const workspacePath = "workspace" 10 | 11 | // workspaceChartPath is the directory that contains a user's workspace charts. 12 | const workspaceChartPath = "workspace/charts" 13 | 14 | // CacheDirectory - File path to cache directory based on home 15 | func CacheDirectory(home string, paths ...string) string { 16 | fragments := append([]string{home, cachePath}, paths...) 17 | return filepath.Join(fragments...) 18 | } 19 | 20 | // WorkspaceChartDirectory - File path to workspace chart directory based on home 21 | func WorkspaceChartDirectory(home string, paths ...string) string { 22 | fragments := append([]string{home, workspaceChartPath}, paths...) 23 | return filepath.Join(fragments...) 24 | } 25 | --------------------------------------------------------------------------------