From 41a60d2edbae34a1d119b7b0e728d0dfbb240f97 Mon Sep 17 00:00:00 2001 From: Andrew Lavery Date: Tue, 14 Aug 2018 14:27:58 -0700 Subject: [PATCH] WIP - dep fails to solve for imports --- pkg/api/asset.go | 7 + pkg/helm/README.md | 6 + pkg/helm/completion.go | 229 ++++++ pkg/helm/create.go | 105 +++ pkg/helm/create_test.go | 164 ++++ pkg/helm/delete.go | 101 +++ pkg/helm/delete_test.go | 73 ++ pkg/helm/dependency.go | 276 +++++++ pkg/helm/dependency_build.go | 85 ++ pkg/helm/dependency_build_test.go | 120 +++ pkg/helm/dependency_test.go | 58 ++ pkg/helm/dependency_update.go | 105 +++ pkg/helm/dependency_update_test.go | 273 +++++++ pkg/helm/docs.go | 80 ++ pkg/helm/exports.go | 15 + pkg/helm/fetch.go | 186 +++++ pkg/helm/fetch_test.go | 179 +++++ pkg/helm/get.go | 89 +++ pkg/helm/get_hooks.go | 75 ++ pkg/helm/get_hooks_test.go | 47 ++ pkg/helm/get_manifest.go | 75 ++ pkg/helm/get_manifest_test.go | 47 ++ pkg/helm/get_test.go | 48 ++ pkg/helm/get_values.go | 89 +++ pkg/helm/get_values_test.go | 47 ++ pkg/helm/helm.go | 310 ++++++++ pkg/helm/helm_test.go | 238 ++++++ pkg/helm/history.go | 172 ++++ pkg/helm/history_test.go | 85 ++ pkg/helm/home.go | 51 ++ pkg/helm/init.go | 493 ++++++++++++ pkg/helm/init_test.go | 357 +++++++++ pkg/helm/init_unix.go | 29 + pkg/helm/init_windows.go | 29 + pkg/helm/inspect.go | 264 +++++++ pkg/helm/inspect_test.go | 80 ++ pkg/helm/install.go | 530 +++++++++++++ pkg/helm/install_test.go | 283 +++++++ pkg/helm/installer/install.go | 372 +++++++++ pkg/helm/installer/install_test.go | 740 ++++++++++++++++++ pkg/helm/installer/options.go | 164 ++++ pkg/helm/installer/uninstall.go | 71 ++ pkg/helm/installer/uninstall_test.go | 88 +++ pkg/helm/lint.go | 208 +++++ pkg/helm/lint_test.go | 54 ++ pkg/helm/list.go | 254 ++++++ pkg/helm/list_test.go | 128 +++ pkg/helm/load_plugins.go | 160 ++++ pkg/helm/package.go | 237 ++++++ pkg/helm/package_test.go | 253 ++++++ pkg/helm/plugin.go | 72 ++ pkg/helm/plugin_install.go | 92 +++ pkg/helm/plugin_list.go | 60 ++ pkg/helm/plugin_remove.go | 99 +++ pkg/helm/plugin_test.go | 187 +++++ pkg/helm/plugin_update.go | 113 +++ pkg/helm/printer.go | 82 ++ pkg/helm/release_testing.go | 110 +++ pkg/helm/release_testing_test.go | 106 +++ pkg/helm/repo.go | 47 ++ pkg/helm/repo_add.go | 117 +++ pkg/helm/repo_add_test.go | 103 +++ pkg/helm/repo_index.go | 105 +++ pkg/helm/repo_index_test.go | 171 ++++ pkg/helm/repo_list.go | 66 ++ pkg/helm/repo_remove.go | 92 +++ pkg/helm/repo_remove_test.go | 81 ++ pkg/helm/repo_update.go | 109 +++ pkg/helm/repo_update_test.go | 106 +++ pkg/helm/reset.go | 131 ++++ pkg/helm/reset_test.go | 170 ++++ pkg/helm/rollback.go | 107 +++ pkg/helm/rollback_test.go | 61 ++ pkg/helm/search.go | 162 ++++ pkg/helm/search/search.go | 236 ++++++ pkg/helm/search/search_test.go | 301 +++++++ pkg/helm/search_test.go | 97 +++ pkg/helm/serve.go | 102 +++ pkg/helm/status.go | 157 ++++ pkg/helm/status_test.go | 151 ++++ pkg/helm/template.go | 325 ++++++++ pkg/helm/template_test.go | 167 ++++ pkg/helm/testdata/helm-test-key.pub | Bin 0 -> 1243 bytes pkg/helm/testdata/helm-test-key.secret | Bin 0 -> 2545 bytes .../testdata/helmhome/plugins/args/args.sh | 2 + .../helmhome/plugins/args/plugin.yaml | 4 + .../helmhome/plugins/echo/plugin.yaml | 4 + .../testdata/helmhome/plugins/env/plugin.yaml | 4 + .../helmhome/plugins/fullenv/fullenv.sh | 10 + .../helmhome/plugins/fullenv/plugin.yaml | 4 + .../repository/cache/testing-index.yaml | 48 ++ .../helmhome/repository/local/index.yaml | 0 .../helmhome/repository/repositories.yaml | 6 + pkg/helm/testdata/repositories.yaml | 6 + pkg/helm/testdata/testcache/foobar-index.yaml | 24 + pkg/helm/testdata/testcache/local-index.yaml | 27 + .../testdata/testcharts/alpine/Chart.yaml | 6 + pkg/helm/testdata/testcharts/alpine/README.md | 13 + .../testcharts/alpine/extra_values.yaml | 2 + .../testcharts/alpine/more_values.yaml | 2 + .../alpine/templates/alpine-pod.yaml | 27 + .../testdata/testcharts/alpine/values.yaml | 2 + .../chart-bad-requirements/.helmignore | 21 + .../chart-bad-requirements/Chart.yaml | 3 + .../charts/reqsubchart/.helmignore | 21 + .../charts/reqsubchart/Chart.yaml | 3 + .../charts/reqsubchart/values.yaml | 4 + .../chart-bad-requirements/requirements.yaml | 4 + .../chart-bad-requirements/values.yaml | 4 + .../testcharts/chart-missing-deps/.helmignore | 21 + .../testcharts/chart-missing-deps/Chart.yaml | 3 + .../charts/reqsubchart/.helmignore | 21 + .../charts/reqsubchart/Chart.yaml | 3 + .../charts/reqsubchart/values.yaml | 4 + .../chart-missing-deps/requirements.yaml | 7 + .../testcharts/chart-missing-deps/values.yaml | 4 + .../testcharts/compressedchart-0.1.0.tgz | Bin 0 -> 542 bytes .../testcharts/compressedchart-0.2.0.tgz | Bin 0 -> 540 bytes .../testcharts/compressedchart-0.3.0.tgz | Bin 0 -> 538 bytes .../compressedchart-with-hyphens-0.1.0.tgz | Bin 0 -> 548 bytes .../testcharts/decompressedchart/.helmignore | 5 + .../testcharts/decompressedchart/Chart.yaml | 3 + .../testcharts/decompressedchart/values.yaml | 4 + .../testdata/testcharts/novals/Chart.yaml | 6 + pkg/helm/testdata/testcharts/novals/README.md | 13 + .../novals/templates/alpine-pod.yaml | 26 + .../testdata/testcharts/reqtest-0.1.0.tgz | Bin 0 -> 911 bytes .../testdata/testcharts/reqtest/.helmignore | 21 + .../testdata/testcharts/reqtest/Chart.yaml | 3 + .../reqtest/charts/reqsubchart/.helmignore | 21 + .../reqtest/charts/reqsubchart/Chart.yaml | 3 + .../reqtest/charts/reqsubchart/values.yaml | 4 + .../reqtest/charts/reqsubchart2/.helmignore | 21 + .../reqtest/charts/reqsubchart2/Chart.yaml | 3 + .../reqtest/charts/reqsubchart2/values.yaml | 4 + .../reqtest/charts/reqsubchart3-0.2.0.tgz | Bin 0 -> 593 bytes .../testcharts/reqtest/requirements.lock | 3 + .../testcharts/reqtest/requirements.yaml | 10 + .../testdata/testcharts/reqtest/values.yaml | 4 + .../testdata/testcharts/signtest-0.1.0.tgz | Bin 0 -> 471 bytes .../testcharts/signtest-0.1.0.tgz.prov | 20 + .../testdata/testcharts/signtest/.helmignore | 5 + .../testdata/testcharts/signtest/Chart.yaml | 3 + .../testcharts/signtest/alpine/Chart.yaml | 6 + .../testcharts/signtest/alpine/README.md | 9 + .../signtest/alpine/templates/alpine-pod.yaml | 16 + .../testcharts/signtest/alpine/values.yaml | 2 + .../testcharts/signtest/templates/pod.yaml | 10 + .../testdata/testcharts/signtest/values.yaml | 0 pkg/helm/testdata/testserver/index.yaml | 1 + .../testserver/repository/repositories.yaml | 6 + pkg/helm/upgrade.go | 246 ++++++ pkg/helm/upgrade_test.go | 170 ++++ pkg/helm/verify.go | 70 ++ pkg/helm/verify_test.go | 95 +++ pkg/helm/version.go | 151 ++++ pkg/helm/version_test.go | 65 ++ pkg/lifecycle/render/helm/fetch.go | 14 +- 158 files changed, 13670 insertions(+), 1 deletion(-) create mode 100644 pkg/helm/README.md create mode 100644 pkg/helm/completion.go create mode 100644 pkg/helm/create.go create mode 100644 pkg/helm/create_test.go create mode 100644 pkg/helm/delete.go create mode 100644 pkg/helm/delete_test.go create mode 100644 pkg/helm/dependency.go create mode 100644 pkg/helm/dependency_build.go create mode 100644 pkg/helm/dependency_build_test.go create mode 100644 pkg/helm/dependency_test.go create mode 100644 pkg/helm/dependency_update.go create mode 100644 pkg/helm/dependency_update_test.go create mode 100644 pkg/helm/docs.go create mode 100644 pkg/helm/exports.go create mode 100644 pkg/helm/fetch.go create mode 100644 pkg/helm/fetch_test.go create mode 100644 pkg/helm/get.go create mode 100644 pkg/helm/get_hooks.go create mode 100644 pkg/helm/get_hooks_test.go create mode 100644 pkg/helm/get_manifest.go create mode 100644 pkg/helm/get_manifest_test.go create mode 100644 pkg/helm/get_test.go create mode 100644 pkg/helm/get_values.go create mode 100644 pkg/helm/get_values_test.go create mode 100644 pkg/helm/helm.go create mode 100644 pkg/helm/helm_test.go create mode 100644 pkg/helm/history.go create mode 100644 pkg/helm/history_test.go create mode 100644 pkg/helm/home.go create mode 100644 pkg/helm/init.go create mode 100644 pkg/helm/init_test.go create mode 100644 pkg/helm/init_unix.go create mode 100644 pkg/helm/init_windows.go create mode 100644 pkg/helm/inspect.go create mode 100644 pkg/helm/inspect_test.go create mode 100644 pkg/helm/install.go create mode 100644 pkg/helm/install_test.go create mode 100644 pkg/helm/installer/install.go create mode 100644 pkg/helm/installer/install_test.go create mode 100644 pkg/helm/installer/options.go create mode 100644 pkg/helm/installer/uninstall.go create mode 100644 pkg/helm/installer/uninstall_test.go create mode 100644 pkg/helm/lint.go create mode 100644 pkg/helm/lint_test.go create mode 100644 pkg/helm/list.go create mode 100644 pkg/helm/list_test.go create mode 100644 pkg/helm/load_plugins.go create mode 100644 pkg/helm/package.go create mode 100644 pkg/helm/package_test.go create mode 100644 pkg/helm/plugin.go create mode 100644 pkg/helm/plugin_install.go create mode 100644 pkg/helm/plugin_list.go create mode 100644 pkg/helm/plugin_remove.go create mode 100644 pkg/helm/plugin_test.go create mode 100644 pkg/helm/plugin_update.go create mode 100644 pkg/helm/printer.go create mode 100644 pkg/helm/release_testing.go create mode 100644 pkg/helm/release_testing_test.go create mode 100644 pkg/helm/repo.go create mode 100644 pkg/helm/repo_add.go create mode 100644 pkg/helm/repo_add_test.go create mode 100644 pkg/helm/repo_index.go create mode 100644 pkg/helm/repo_index_test.go create mode 100644 pkg/helm/repo_list.go create mode 100644 pkg/helm/repo_remove.go create mode 100644 pkg/helm/repo_remove_test.go create mode 100644 pkg/helm/repo_update.go create mode 100644 pkg/helm/repo_update_test.go create mode 100644 pkg/helm/reset.go create mode 100644 pkg/helm/reset_test.go create mode 100644 pkg/helm/rollback.go create mode 100644 pkg/helm/rollback_test.go create mode 100644 pkg/helm/search.go create mode 100644 pkg/helm/search/search.go create mode 100644 pkg/helm/search/search_test.go create mode 100644 pkg/helm/search_test.go create mode 100644 pkg/helm/serve.go create mode 100644 pkg/helm/status.go create mode 100644 pkg/helm/status_test.go create mode 100644 pkg/helm/template.go create mode 100644 pkg/helm/template_test.go create mode 100644 pkg/helm/testdata/helm-test-key.pub create mode 100644 pkg/helm/testdata/helm-test-key.secret create mode 100644 pkg/helm/testdata/helmhome/plugins/args/args.sh create mode 100644 pkg/helm/testdata/helmhome/plugins/args/plugin.yaml create mode 100644 pkg/helm/testdata/helmhome/plugins/echo/plugin.yaml create mode 100644 pkg/helm/testdata/helmhome/plugins/env/plugin.yaml create mode 100644 pkg/helm/testdata/helmhome/plugins/fullenv/fullenv.sh create mode 100644 pkg/helm/testdata/helmhome/plugins/fullenv/plugin.yaml create mode 100644 pkg/helm/testdata/helmhome/repository/cache/testing-index.yaml create mode 100644 pkg/helm/testdata/helmhome/repository/local/index.yaml create mode 100644 pkg/helm/testdata/helmhome/repository/repositories.yaml create mode 100644 pkg/helm/testdata/repositories.yaml create mode 100644 pkg/helm/testdata/testcache/foobar-index.yaml create mode 100644 pkg/helm/testdata/testcache/local-index.yaml create mode 100644 pkg/helm/testdata/testcharts/alpine/Chart.yaml create mode 100644 pkg/helm/testdata/testcharts/alpine/README.md create mode 100644 pkg/helm/testdata/testcharts/alpine/extra_values.yaml create mode 100644 pkg/helm/testdata/testcharts/alpine/more_values.yaml create mode 100644 pkg/helm/testdata/testcharts/alpine/templates/alpine-pod.yaml create mode 100644 pkg/helm/testdata/testcharts/alpine/values.yaml create mode 100644 pkg/helm/testdata/testcharts/chart-bad-requirements/.helmignore create mode 100644 pkg/helm/testdata/testcharts/chart-bad-requirements/Chart.yaml create mode 100644 pkg/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/.helmignore create mode 100644 pkg/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/Chart.yaml create mode 100644 pkg/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/values.yaml create mode 100644 pkg/helm/testdata/testcharts/chart-bad-requirements/requirements.yaml create mode 100644 pkg/helm/testdata/testcharts/chart-bad-requirements/values.yaml create mode 100644 pkg/helm/testdata/testcharts/chart-missing-deps/.helmignore create mode 100644 pkg/helm/testdata/testcharts/chart-missing-deps/Chart.yaml create mode 100644 pkg/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/.helmignore create mode 100644 pkg/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/Chart.yaml create mode 100644 pkg/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/values.yaml create mode 100644 pkg/helm/testdata/testcharts/chart-missing-deps/requirements.yaml create mode 100644 pkg/helm/testdata/testcharts/chart-missing-deps/values.yaml create mode 100644 pkg/helm/testdata/testcharts/compressedchart-0.1.0.tgz create mode 100644 pkg/helm/testdata/testcharts/compressedchart-0.2.0.tgz create mode 100644 pkg/helm/testdata/testcharts/compressedchart-0.3.0.tgz create mode 100644 pkg/helm/testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz create mode 100644 pkg/helm/testdata/testcharts/decompressedchart/.helmignore create mode 100644 pkg/helm/testdata/testcharts/decompressedchart/Chart.yaml create mode 100644 pkg/helm/testdata/testcharts/decompressedchart/values.yaml create mode 100644 pkg/helm/testdata/testcharts/novals/Chart.yaml create mode 100644 pkg/helm/testdata/testcharts/novals/README.md create mode 100644 pkg/helm/testdata/testcharts/novals/templates/alpine-pod.yaml create mode 100644 pkg/helm/testdata/testcharts/reqtest-0.1.0.tgz create mode 100644 pkg/helm/testdata/testcharts/reqtest/.helmignore create mode 100644 pkg/helm/testdata/testcharts/reqtest/Chart.yaml create mode 100644 pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart/.helmignore create mode 100644 pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml create mode 100644 pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart/values.yaml create mode 100644 pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart2/.helmignore create mode 100644 pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml create mode 100644 pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart2/values.yaml create mode 100644 pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz create mode 100644 pkg/helm/testdata/testcharts/reqtest/requirements.lock create mode 100644 pkg/helm/testdata/testcharts/reqtest/requirements.yaml create mode 100644 pkg/helm/testdata/testcharts/reqtest/values.yaml create mode 100644 pkg/helm/testdata/testcharts/signtest-0.1.0.tgz create mode 100644 pkg/helm/testdata/testcharts/signtest-0.1.0.tgz.prov create mode 100644 pkg/helm/testdata/testcharts/signtest/.helmignore create mode 100644 pkg/helm/testdata/testcharts/signtest/Chart.yaml create mode 100644 pkg/helm/testdata/testcharts/signtest/alpine/Chart.yaml create mode 100644 pkg/helm/testdata/testcharts/signtest/alpine/README.md create mode 100644 pkg/helm/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml create mode 100644 pkg/helm/testdata/testcharts/signtest/alpine/values.yaml create mode 100644 pkg/helm/testdata/testcharts/signtest/templates/pod.yaml create mode 100644 pkg/helm/testdata/testcharts/signtest/values.yaml create mode 100644 pkg/helm/testdata/testserver/index.yaml create mode 100644 pkg/helm/testdata/testserver/repository/repositories.yaml create mode 100644 pkg/helm/upgrade.go create mode 100644 pkg/helm/upgrade_test.go create mode 100644 pkg/helm/verify.go create mode 100644 pkg/helm/verify_test.go create mode 100644 pkg/helm/version.go create mode 100644 pkg/helm/version_test.go diff --git a/pkg/api/asset.go b/pkg/api/asset.go index 156daa42b..8c9510a80 100644 --- a/pkg/api/asset.go +++ b/pkg/api/asset.go @@ -73,6 +73,12 @@ type WebAsset struct { URL string `json:"url" yaml:"url" hcl:"url"` } +type HelmGitAsset struct { + Name string + URL string + Version string +} + // HelmAsset is an asset that declares a helm chart on github type HelmAsset struct { AssetShared `json:",inline" yaml:",inline" hcl:",inline"` @@ -80,6 +86,7 @@ type HelmAsset struct { HelmOpts []string `json:"helm_opts" yaml:"helm_opts" hcl:"helm_opts"` // GitHub references a github asset from which to pull the chart GitHub *GitHubAsset `json:"github" yaml:"github" hcl:"github"` + Git *HelmGitAsset // Local is an escape hatch, most impls will use github or some sort of ChartMuseum thing Local *LocalHelmOpts `json:"local,omitempty" yaml:"local,omitempty" hcl:"local,omitempty"` } diff --git a/pkg/helm/README.md b/pkg/helm/README.md new file mode 100644 index 000000000..b45732688 --- /dev/null +++ b/pkg/helm/README.md @@ -0,0 +1,6 @@ +All files in this directory besides `exports.go` are copied from [Helm v2.9.1](https://github.com/helm/helm/tree/v2.9.1/cmd/helm) +except for `exports.go` which exports the commands that Ship uses elsewhere. The copied files have three changes - `package helm` is replaced with `package helm` with this command: +``` +sed -i 's/package main/package helm/g' `ag -l 'package main' .` +``` +the `root()` function within `helm.go` has been commented out, and `canonical paths` (`// import "k8s.io/helm/cmd/helm/installer"` and `// import "k8s.io/helm/cmd/helm"`) have been removed. diff --git a/pkg/helm/completion.go b/pkg/helm/completion.go new file mode 100644 index 000000000..7abb3400b --- /dev/null +++ b/pkg/helm/completion.go @@ -0,0 +1,229 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "fmt" + "io" + + "github.com/spf13/cobra" +) + +const completionDesc = ` +Generate autocompletions script for Helm for the specified shell (bash or zsh). + +This command can generate shell autocompletions. e.g. + + $ helm completion bash + +Can be sourced as such + + $ source <(helm completion bash) +` + +var ( + completionShells = map[string]func(out io.Writer, cmd *cobra.Command) error{ + "bash": runCompletionBash, + "zsh": runCompletionZsh, + } +) + +func newCompletionCmd(out io.Writer) *cobra.Command { + shells := []string{} + for s := range completionShells { + shells = append(shells, s) + } + + cmd := &cobra.Command{ + Use: "completion SHELL", + Short: "Generate autocompletions script for the specified shell (bash or zsh)", + Long: completionDesc, + RunE: func(cmd *cobra.Command, args []string) error { + return runCompletion(out, cmd, args) + }, + ValidArgs: shells, + } + + return cmd +} + +func runCompletion(out io.Writer, cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("shell not specified") + } + if len(args) > 1 { + return fmt.Errorf("too many arguments, expected only the shell type") + } + run, found := completionShells[args[0]] + if !found { + return fmt.Errorf("unsupported shell type %q", args[0]) + } + + return run(out, cmd) +} + +func runCompletionBash(out io.Writer, cmd *cobra.Command) error { + return cmd.Root().GenBashCompletion(out) +} + +func runCompletionZsh(out io.Writer, cmd *cobra.Command) error { + zshInitialization := ` +__helm_bash_source() { + alias shopt=':' + alias _expand=_bash_expand + alias _complete=_bash_comp + emulate -L sh + setopt kshglob noshglob braceexpand + source "$@" +} +__helm_type() { + # -t is not supported by zsh + if [ "$1" == "-t" ]; then + shift + # fake Bash 4 to disable "complete -o nospace". Instead + # "compopt +-o nospace" is used in the code to toggle trailing + # spaces. We don't support that, but leave trailing spaces on + # all the time + if [ "$1" = "__helm_compopt" ]; then + echo builtin + return 0 + fi + fi + type "$@" +} +__helm_compgen() { + local completions w + completions=( $(compgen "$@") ) || return $? + # filter by given word as prefix + while [[ "$1" = -* && "$1" != -- ]]; do + shift + shift + done + if [[ "$1" == -- ]]; then + shift + fi + for w in "${completions[@]}"; do + if [[ "${w}" = "$1"* ]]; then + echo "${w}" + fi + done +} +__helm_compopt() { + true # don't do anything. Not supported by bashcompinit in zsh +} +__helm_declare() { + if [ "$1" == "-F" ]; then + whence -w "$@" + else + builtin declare "$@" + fi +} +__helm_ltrim_colon_completions() +{ + if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then + # Remove colon-word prefix from COMPREPLY items + local colon_word=${1%${1##*:}} + local i=${#COMPREPLY[*]} + while [[ $((--i)) -ge 0 ]]; do + COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"} + done + fi +} +__helm_get_comp_words_by_ref() { + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[${COMP_CWORD}-1]}" + words=("${COMP_WORDS[@]}") + cword=("${COMP_CWORD[@]}") +} +__helm_filedir() { + local RET OLD_IFS w qw + __debug "_filedir $@ cur=$cur" + if [[ "$1" = \~* ]]; then + # somehow does not work. Maybe, zsh does not call this at all + eval echo "$1" + return 0 + fi + OLD_IFS="$IFS" + IFS=$'\n' + if [ "$1" = "-d" ]; then + shift + RET=( $(compgen -d) ) + else + RET=( $(compgen -f) ) + fi + IFS="$OLD_IFS" + IFS="," __debug "RET=${RET[@]} len=${#RET[@]}" + for w in ${RET[@]}; do + if [[ ! "${w}" = "${cur}"* ]]; then + continue + fi + if eval "[[ \"\${w}\" = *.$1 || -d \"\${w}\" ]]"; then + qw="$(__helm_quote "${w}")" + if [ -d "${w}" ]; then + COMPREPLY+=("${qw}/") + else + COMPREPLY+=("${qw}") + fi + fi + done +} +__helm_quote() { + if [[ $1 == \'* || $1 == \"* ]]; then + # Leave out first character + printf %q "${1:1}" + else + printf %q "$1" + fi +} +autoload -U +X bashcompinit && bashcompinit +# use word boundary patterns for BSD or GNU sed +LWORD='[[:<:]]' +RWORD='[[:>:]]' +if sed --help 2>&1 | grep -q GNU; then + LWORD='\<' + RWORD='\>' +fi +__helm_convert_bash_to_zsh() { + sed \ + -e 's/declare -F/whence -w/' \ + -e 's/_get_comp_words_by_ref "\$@"/_get_comp_words_by_ref "\$*"/' \ + -e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \ + -e 's/flags+=("\(--.*\)=")/flags+=("\1"); two_word_flags+=("\1")/' \ + -e 's/must_have_one_flag+=("\(--.*\)=")/must_have_one_flag+=("\1")/' \ + -e "s/${LWORD}_filedir${RWORD}/__helm_filedir/g" \ + -e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__helm_get_comp_words_by_ref/g" \ + -e "s/${LWORD}__ltrim_colon_completions${RWORD}/__helm_ltrim_colon_completions/g" \ + -e "s/${LWORD}compgen${RWORD}/__helm_compgen/g" \ + -e "s/${LWORD}compopt${RWORD}/__helm_compopt/g" \ + -e "s/${LWORD}declare${RWORD}/__helm_declare/g" \ + -e "s/\\\$(type${RWORD}/\$(__helm_type/g" \ + <<'BASH_COMPLETION_EOF' +` + out.Write([]byte(zshInitialization)) + + buf := new(bytes.Buffer) + cmd.Root().GenBashCompletion(buf) + out.Write(buf.Bytes()) + + zshTail := ` +BASH_COMPLETION_EOF +} +__helm_bash_source <(__helm_convert_bash_to_zsh) +` + out.Write([]byte(zshTail)) + return nil +} diff --git a/pkg/helm/create.go b/pkg/helm/create.go new file mode 100644 index 000000000..1fe9e5024 --- /dev/null +++ b/pkg/helm/create.go @@ -0,0 +1,105 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "errors" + "fmt" + "io" + "path/filepath" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/proto/hapi/chart" +) + +const createDesc = ` +This command creates a chart directory along with the common files and +directories used in a chart. + +For example, 'helm create foo' will create a directory structure that looks +something like this: + + foo/ + | + |- .helmignore # Contains patterns to ignore when packaging Helm charts. + | + |- Chart.yaml # Information about your chart + | + |- values.yaml # The default values for your templates + | + |- charts/ # Charts that this chart depends on + | + |- templates/ # The template files + +'helm create' takes a path for an argument. If directories in the given path +do not exist, Helm will attempt to create them as it goes. If the given +destination exists and there are files in that directory, conflicting files +will be overwritten, but other files will be left alone. +` + +type createCmd struct { + home helmpath.Home + name string + out io.Writer + starter string +} + +func newCreateCmd(out io.Writer) *cobra.Command { + cc := &createCmd{out: out} + + cmd := &cobra.Command{ + Use: "create NAME", + Short: "create a new chart with the given name", + Long: createDesc, + RunE: func(cmd *cobra.Command, args []string) error { + cc.home = settings.Home + if len(args) == 0 { + return errors.New("the name of the new chart is required") + } + cc.name = args[0] + return cc.run() + }, + } + + cmd.Flags().StringVarP(&cc.starter, "starter", "p", "", "the named Helm starter scaffold") + return cmd +} + +func (c *createCmd) run() error { + fmt.Fprintf(c.out, "Creating %s\n", c.name) + + chartname := filepath.Base(c.name) + cfile := &chart.Metadata{ + Name: chartname, + Description: "A Helm chart for Kubernetes", + Version: "0.1.0", + AppVersion: "1.0", + ApiVersion: chartutil.ApiVersionV1, + } + + if c.starter != "" { + // Create from the starter + lstarter := filepath.Join(c.home.Starters(), c.starter) + return chartutil.CreateFrom(cfile, filepath.Dir(c.name), lstarter) + } + + _, err := chartutil.Create(cfile, filepath.Dir(c.name)) + return err +} diff --git a/pkg/helm/create_test.go b/pkg/helm/create_test.go new file mode 100644 index 000000000..f9467bd8b --- /dev/null +++ b/pkg/helm/create_test.go @@ -0,0 +1,164 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/proto/hapi/chart" +) + +func TestCreateCmd(t *testing.T) { + cname := "testchart" + // Make a temp dir + tdir, err := ioutil.TempDir("", "helm-create-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tdir) + + // CD into it + pwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(tdir); err != nil { + t.Fatal(err) + } + defer os.Chdir(pwd) + + // Run a create + cmd := newCreateCmd(ioutil.Discard) + if err := cmd.RunE(cmd, []string{cname}); err != nil { + t.Errorf("Failed to run create: %s", err) + return + } + + // Test that the chart is there + if fi, err := os.Stat(cname); err != nil { + t.Fatalf("no chart directory: %s", err) + } else if !fi.IsDir() { + t.Fatalf("chart is not directory") + } + + c, err := chartutil.LoadDir(cname) + if err != nil { + t.Fatal(err) + } + + if c.Metadata.Name != cname { + t.Errorf("Expected %q name, got %q", cname, c.Metadata.Name) + } + if c.Metadata.ApiVersion != chartutil.ApiVersionV1 { + t.Errorf("Wrong API version: %q", c.Metadata.ApiVersion) + } +} + +func TestCreateStarterCmd(t *testing.T) { + cname := "testchart" + // Make a temp dir + tdir, err := ioutil.TempDir("", "helm-create-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tdir) + + thome, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + cleanup := resetEnv() + defer func() { + os.RemoveAll(thome.String()) + cleanup() + }() + + settings.Home = thome + + // Create a starter. + starterchart := filepath.Join(thome.String(), "starters") + os.Mkdir(starterchart, 0755) + if dest, err := chartutil.Create(&chart.Metadata{Name: "starterchart"}, starterchart); err != nil { + t.Fatalf("Could not create chart: %s", err) + } else { + t.Logf("Created %s", dest) + } + tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl") + if err := ioutil.WriteFile(tplpath, []byte("test"), 0755); err != nil { + t.Fatalf("Could not write template: %s", err) + } + + // CD into it + pwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(tdir); err != nil { + t.Fatal(err) + } + defer os.Chdir(pwd) + + // Run a create + cmd := newCreateCmd(ioutil.Discard) + cmd.ParseFlags([]string{"--starter", "starterchart"}) + if err := cmd.RunE(cmd, []string{cname}); err != nil { + t.Errorf("Failed to run create: %s", err) + return + } + + // Test that the chart is there + if fi, err := os.Stat(cname); err != nil { + t.Fatalf("no chart directory: %s", err) + } else if !fi.IsDir() { + t.Fatalf("chart is not directory") + } + + c, err := chartutil.LoadDir(cname) + if err != nil { + t.Fatal(err) + } + + if c.Metadata.Name != cname { + t.Errorf("Expected %q name, got %q", cname, c.Metadata.Name) + } + if c.Metadata.ApiVersion != chartutil.ApiVersionV1 { + t.Errorf("Wrong API version: %q", c.Metadata.ApiVersion) + } + + if l := len(c.Templates); l != 6 { + t.Errorf("Expected 5 templates, got %d", l) + } + + found := false + for _, tpl := range c.Templates { + if tpl.Name == "templates/foo.tpl" { + found = true + data := tpl.Data + if string(data) != "test" { + t.Errorf("Expected template 'test', got %q", string(data)) + } + } + } + if !found { + t.Error("Did not find foo.tpl") + } + +} diff --git a/pkg/helm/delete.go b/pkg/helm/delete.go new file mode 100644 index 000000000..48dddff89 --- /dev/null +++ b/pkg/helm/delete.go @@ -0,0 +1,101 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "errors" + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" +) + +const deleteDesc = ` +This command takes a release name, and then deletes the release from Kubernetes. +It removes all of the resources associated with the last release of the chart. + +Use the '--dry-run' flag to see which releases will be deleted without actually +deleting them. +` + +type deleteCmd struct { + name string + dryRun bool + disableHooks bool + purge bool + timeout int64 + + out io.Writer + client helm.Interface +} + +func newDeleteCmd(c helm.Interface, out io.Writer) *cobra.Command { + del := &deleteCmd{ + out: out, + client: c, + } + + cmd := &cobra.Command{ + Use: "delete [flags] RELEASE_NAME [...]", + Aliases: []string{"del"}, + SuggestFor: []string{"remove", "rm"}, + Short: "given a release name, delete the release from Kubernetes", + Long: deleteDesc, + PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("command 'delete' requires a release name") + } + del.client = ensureHelmClient(del.client) + + for i := 0; i < len(args); i++ { + del.name = args[i] + if err := del.run(); err != nil { + return err + } + + fmt.Fprintf(out, "release \"%s\" deleted\n", del.name) + } + return nil + }, + } + + f := cmd.Flags() + f.BoolVar(&del.dryRun, "dry-run", false, "simulate a delete") + f.BoolVar(&del.disableHooks, "no-hooks", false, "prevent hooks from running during deletion") + f.BoolVar(&del.purge, "purge", false, "remove the release from the store and make its name free for later use") + f.Int64Var(&del.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") + + return cmd +} + +func (d *deleteCmd) run() error { + opts := []helm.DeleteOption{ + helm.DeleteDryRun(d.dryRun), + helm.DeleteDisableHooks(d.disableHooks), + helm.DeletePurge(d.purge), + helm.DeleteTimeout(d.timeout), + } + res, err := d.client.DeleteRelease(d.name, opts...) + if res != nil && res.Info != "" { + fmt.Fprintln(d.out, res.Info) + } + + return prettyError(err) +} diff --git a/pkg/helm/delete_test.go b/pkg/helm/delete_test.go new file mode 100644 index 000000000..f41ce4c60 --- /dev/null +++ b/pkg/helm/delete_test.go @@ -0,0 +1,73 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestDelete(t *testing.T) { + + tests := []releaseCase{ + { + name: "basic delete", + args: []string{"aeneas"}, + flags: []string{}, + expected: "", // Output of a delete is an empty string and exit 0. + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})}, + }, + { + name: "delete with timeout", + args: []string{"aeneas"}, + flags: []string{"--timeout", "120"}, + expected: "", + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})}, + }, + { + name: "delete without hooks", + args: []string{"aeneas"}, + flags: []string{"--no-hooks"}, + expected: "", + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})}, + }, + { + name: "purge", + args: []string{"aeneas"}, + flags: []string{"--purge"}, + expected: "", + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})}, + }, + { + name: "delete without release", + args: []string{}, + err: true, + }, + } + runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newDeleteCmd(c, out) + }) +} diff --git a/pkg/helm/dependency.go b/pkg/helm/dependency.go new file mode 100644 index 000000000..404fc703f --- /dev/null +++ b/pkg/helm/dependency.go @@ -0,0 +1,276 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/Masterminds/semver" + "github.com/gosuri/uitable" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/chartutil" +) + +const dependencyDesc = ` +Manage the dependencies of a chart. + +Helm charts store their dependencies in 'charts/'. For chart developers, it is +often easier to manage a single dependency file ('requirements.yaml') +which declares all dependencies. + +The dependency commands operate on that file, making it easy to synchronize +between the desired dependencies and the actual dependencies stored in the +'charts/' directory. + +A 'requirements.yaml' file is a YAML file in which developers can declare chart +dependencies, along with the location of the chart and the desired version. +For example, this requirements file declares two dependencies: + + # requirements.yaml + dependencies: + - name: nginx + version: "1.2.3" + repository: "https://example.com/charts" + - name: memcached + version: "3.2.1" + repository: "https://another.example.com/charts" + +The 'name' should be the name of a chart, where that name must match the name +in that chart's 'Chart.yaml' file. + +The 'version' field should contain a semantic version or version range. + +The 'repository' URL should point to a Chart Repository. Helm expects that by +appending '/index.yaml' to the URL, it should be able to retrieve the chart +repository's index. Note: 'repository' can be an alias. The alias must start +with 'alias:' or '@'. + +Starting from 2.2.0, repository can be defined as the path to the directory of +the dependency charts stored locally. The path should start with a prefix of +"file://". For example, + + # requirements.yaml + dependencies: + - name: nginx + version: "1.2.3" + repository: "file://../dependency_chart/nginx" + +If the dependency chart is retrieved locally, it is not required to have the +repository added to helm by "helm add repo". Version matching is also supported +for this case. +` + +const dependencyListDesc = ` +List all of the dependencies declared in a chart. + +This can take chart archives and chart directories as input. It will not alter +the contents of a chart. + +This will produce an error if the chart cannot be loaded. It will emit a warning +if it cannot find a requirements.yaml. +` + +func newDependencyCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "dependency update|build|list", + Aliases: []string{"dep", "dependencies"}, + Short: "manage a chart's dependencies", + Long: dependencyDesc, + } + + cmd.AddCommand(newDependencyListCmd(out)) + cmd.AddCommand(newDependencyUpdateCmd(out)) + cmd.AddCommand(newDependencyBuildCmd(out)) + + return cmd +} + +type dependencyListCmd struct { + out io.Writer + chartpath string +} + +func newDependencyListCmd(out io.Writer) *cobra.Command { + dlc := &dependencyListCmd{out: out} + + cmd := &cobra.Command{ + Use: "list [flags] CHART", + Aliases: []string{"ls"}, + Short: "list the dependencies for the given chart", + Long: dependencyListDesc, + RunE: func(cmd *cobra.Command, args []string) error { + cp := "." + if len(args) > 0 { + cp = args[0] + } + + var err error + dlc.chartpath, err = filepath.Abs(cp) + if err != nil { + return err + } + return dlc.run() + }, + } + return cmd +} + +func (l *dependencyListCmd) run() error { + c, err := chartutil.Load(l.chartpath) + if err != nil { + return err + } + + r, err := chartutil.LoadRequirements(c) + if err != nil { + if err == chartutil.ErrRequirementsNotFound { + fmt.Fprintf(l.out, "WARNING: no requirements at %s/charts\n", l.chartpath) + return nil + } + return err + } + + l.printRequirements(r, l.out) + fmt.Fprintln(l.out) + l.printMissing(r) + return nil +} + +func (l *dependencyListCmd) dependencyStatus(dep *chartutil.Dependency) string { + filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*") + archives, err := filepath.Glob(filepath.Join(l.chartpath, "charts", filename)) + if err != nil { + return "bad pattern" + } else if len(archives) > 1 { + return "too many matches" + } else if len(archives) == 1 { + archive := archives[0] + if _, err := os.Stat(archive); err == nil { + c, err := chartutil.Load(archive) + if err != nil { + return "corrupt" + } + if c.Metadata.Name != dep.Name { + return "misnamed" + } + + if c.Metadata.Version != dep.Version { + constraint, err := semver.NewConstraint(dep.Version) + if err != nil { + return "invalid version" + } + + v, err := semver.NewVersion(c.Metadata.Version) + if err != nil { + return "invalid version" + } + + if constraint.Check(v) { + return "ok" + } + return "wrong version" + } + return "ok" + } + } + + folder := filepath.Join(l.chartpath, "charts", dep.Name) + if fi, err := os.Stat(folder); err != nil { + return "missing" + } else if !fi.IsDir() { + return "mispackaged" + } + + c, err := chartutil.Load(folder) + if err != nil { + return "corrupt" + } + + if c.Metadata.Name != dep.Name { + return "misnamed" + } + + if c.Metadata.Version != dep.Version { + constraint, err := semver.NewConstraint(dep.Version) + if err != nil { + return "invalid version" + } + + v, err := semver.NewVersion(c.Metadata.Version) + if err != nil { + return "invalid version" + } + + if constraint.Check(v) { + return "unpacked" + } + return "wrong version" + } + + return "unpacked" +} + +// printRequirements prints all of the requirements in the yaml file. +func (l *dependencyListCmd) printRequirements(reqs *chartutil.Requirements, out io.Writer) { + table := uitable.New() + table.MaxColWidth = 80 + table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS") + for _, row := range reqs.Dependencies { + table.AddRow(row.Name, row.Version, row.Repository, l.dependencyStatus(row)) + } + fmt.Fprintln(out, table) +} + +// printMissing prints warnings about charts that are present on disk, but are not in the requirements. +func (l *dependencyListCmd) printMissing(reqs *chartutil.Requirements) { + folder := filepath.Join(l.chartpath, "charts/*") + files, err := filepath.Glob(folder) + if err != nil { + fmt.Fprintln(l.out, err) + return + } + + for _, f := range files { + fi, err := os.Stat(f) + if err != nil { + fmt.Fprintf(l.out, "Warning: %s\n", err) + } + // Skip anything that is not a directory and not a tgz file. + if !fi.IsDir() && filepath.Ext(f) != ".tgz" { + continue + } + c, err := chartutil.Load(f) + if err != nil { + fmt.Fprintf(l.out, "WARNING: %q is not a chart.\n", f) + continue + } + found := false + for _, d := range reqs.Dependencies { + if d.Name == c.Metadata.Name { + found = true + break + } + } + if !found { + fmt.Fprintf(l.out, "WARNING: %q is not in requirements.yaml.\n", f) + } + } + +} diff --git a/pkg/helm/dependency_build.go b/pkg/helm/dependency_build.go new file mode 100644 index 000000000..f8ee90ae7 --- /dev/null +++ b/pkg/helm/dependency_build.go @@ -0,0 +1,85 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/downloader" + "k8s.io/helm/pkg/getter" + "k8s.io/helm/pkg/helm/helmpath" +) + +const dependencyBuildDesc = ` +Build out the charts/ directory from the requirements.lock file. + +Build is used to reconstruct a chart's dependencies to the state specified in +the lock file. This will not re-negotiate dependencies, as 'helm dependency update' +does. + +If no lock file is found, 'helm dependency build' will mirror the behavior +of 'helm dependency update'. +` + +type dependencyBuildCmd struct { + out io.Writer + chartpath string + verify bool + keyring string + helmhome helmpath.Home +} + +func newDependencyBuildCmd(out io.Writer) *cobra.Command { + dbc := &dependencyBuildCmd{out: out} + + cmd := &cobra.Command{ + Use: "build [flags] CHART", + Short: "rebuild the charts/ directory based on the requirements.lock file", + Long: dependencyBuildDesc, + RunE: func(cmd *cobra.Command, args []string) error { + dbc.helmhome = settings.Home + dbc.chartpath = "." + + if len(args) > 0 { + dbc.chartpath = args[0] + } + return dbc.run() + }, + } + + f := cmd.Flags() + f.BoolVar(&dbc.verify, "verify", false, "verify the packages against signatures") + f.StringVar(&dbc.keyring, "keyring", defaultKeyring(), "keyring containing public keys") + + return cmd +} + +func (d *dependencyBuildCmd) run() error { + man := &downloader.Manager{ + Out: d.out, + ChartPath: d.chartpath, + HelmHome: d.helmhome, + Keyring: d.keyring, + Getters: getter.All(settings), + } + if d.verify { + man.Verify = downloader.VerifyIfPossible + } + + return man.Build() +} diff --git a/pkg/helm/dependency_build_test.go b/pkg/helm/dependency_build_test.go new file mode 100644 index 000000000..e6adc6353 --- /dev/null +++ b/pkg/helm/dependency_build_test.go @@ -0,0 +1,120 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/provenance" + "k8s.io/helm/pkg/repo" + "k8s.io/helm/pkg/repo/repotest" +) + +func TestDependencyBuildCmd(t *testing.T) { + hh, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + cleanup := resetEnv() + defer func() { + os.RemoveAll(hh.String()) + cleanup() + }() + + settings.Home = hh + + srv := repotest.NewServer(hh.String()) + defer srv.Stop() + _, err = srv.CopyCharts("testdata/testcharts/*.tgz") + if err != nil { + t.Fatal(err) + } + + chartname := "depbuild" + if err := createTestingChart(hh.String(), chartname, srv.URL()); err != nil { + t.Fatal(err) + } + + out := bytes.NewBuffer(nil) + dbc := &dependencyBuildCmd{out: out} + dbc.helmhome = helmpath.Home(hh) + dbc.chartpath = filepath.Join(hh.String(), chartname) + + // In the first pass, we basically want the same results as an update. + if err := dbc.run(); err != nil { + output := out.String() + t.Logf("Output: %s", output) + t.Fatal(err) + } + + output := out.String() + if !strings.Contains(output, `update from the "test" chart repository`) { + t.Errorf("Repo did not get updated\n%s", output) + } + + // Make sure the actual file got downloaded. + expect := filepath.Join(hh.String(), chartname, "charts/reqtest-0.1.0.tgz") + if _, err := os.Stat(expect); err != nil { + t.Fatal(err) + } + + // In the second pass, we want to remove the chart's request dependency, + // then see if it restores from the lock. + lockfile := filepath.Join(hh.String(), chartname, "requirements.lock") + if _, err := os.Stat(lockfile); err != nil { + t.Fatal(err) + } + if err := os.RemoveAll(expect); err != nil { + t.Fatal(err) + } + + if err := dbc.run(); err != nil { + output := out.String() + t.Logf("Output: %s", output) + t.Fatal(err) + } + + // Now repeat the test that the dependency exists. + expect = filepath.Join(hh.String(), chartname, "charts/reqtest-0.1.0.tgz") + if _, err := os.Stat(expect); err != nil { + t.Fatal(err) + } + + // Make sure that build is also fetching the correct version. + hash, err := provenance.DigestFile(expect) + if err != nil { + t.Fatal(err) + } + + i, err := repo.LoadIndexFile(dbc.helmhome.CacheIndex("test")) + if err != nil { + t.Fatal(err) + } + + reqver := i.Entries["reqtest"][0] + if h := reqver.Digest; h != hash { + t.Errorf("Failed hash match: expected %s, got %s", hash, h) + } + if v := reqver.Version; v != "0.1.0" { + t.Errorf("mismatched versions. Expected %q, got %q", "0.1.0", v) + } + +} diff --git a/pkg/helm/dependency_test.go b/pkg/helm/dependency_test.go new file mode 100644 index 000000000..2839f6517 --- /dev/null +++ b/pkg/helm/dependency_test.go @@ -0,0 +1,58 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" +) + +func TestDependencyListCmd(t *testing.T) { + + tests := []releaseCase{ + { + name: "No such chart", + args: []string{"/no/such/chart"}, + err: true, + }, + { + name: "No requirements.yaml", + args: []string{"testdata/testcharts/alpine"}, + expected: "WARNING: no requirements at ", + }, + { + name: "Requirements in chart dir", + args: []string{"testdata/testcharts/reqtest"}, + expected: "NAME \tVERSION\tREPOSITORY \tSTATUS \n" + + "reqsubchart \t0.1.0 \thttps://example.com/charts\tunpacked\n" + + "reqsubchart2\t0.2.0 \thttps://example.com/charts\tunpacked\n" + + "reqsubchart3\t>=0.1.0\thttps://example.com/charts\tok \n\n", + }, + { + name: "Requirements in chart archive", + args: []string{"testdata/testcharts/reqtest-0.1.0.tgz"}, + expected: "NAME \tVERSION\tREPOSITORY \tSTATUS \nreqsubchart \t0.1.0 \thttps://example.com/charts\tmissing\nreqsubchart2\t0.2.0 \thttps://example.com/charts\tmissing\n", + }, + } + + runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newDependencyListCmd(out) + }) +} diff --git a/pkg/helm/dependency_update.go b/pkg/helm/dependency_update.go new file mode 100644 index 000000000..462a9f192 --- /dev/null +++ b/pkg/helm/dependency_update.go @@ -0,0 +1,105 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + "path/filepath" + + "github.com/spf13/cobra" + "k8s.io/helm/pkg/downloader" + "k8s.io/helm/pkg/getter" + "k8s.io/helm/pkg/helm/helmpath" +) + +const dependencyUpDesc = ` +Update the on-disk dependencies to mirror the requirements.yaml file. + +This command verifies that the required charts, as expressed in 'requirements.yaml', +are present in 'charts/' and are at an acceptable version. It will pull down +the latest charts that satisfy the dependencies, and clean up old dependencies. + +On successful update, this will generate a lock file that can be used to +rebuild the requirements to an exact version. + +Dependencies are not required to be represented in 'requirements.yaml'. For that +reason, an update command will not remove charts unless they are (a) present +in the requirements.yaml file, but (b) at the wrong version. +` + +// dependencyUpdateCmd describes a 'helm dependency update' +type dependencyUpdateCmd struct { + out io.Writer + chartpath string + helmhome helmpath.Home + verify bool + keyring string + skipRefresh bool +} + +// newDependencyUpdateCmd creates a new dependency update command. +func newDependencyUpdateCmd(out io.Writer) *cobra.Command { + duc := &dependencyUpdateCmd{out: out} + + cmd := &cobra.Command{ + Use: "update [flags] CHART", + Aliases: []string{"up"}, + Short: "update charts/ based on the contents of requirements.yaml", + Long: dependencyUpDesc, + RunE: func(cmd *cobra.Command, args []string) error { + cp := "." + if len(args) > 0 { + cp = args[0] + } + + var err error + duc.chartpath, err = filepath.Abs(cp) + if err != nil { + return err + } + + duc.helmhome = settings.Home + + return duc.run() + }, + } + + f := cmd.Flags() + f.BoolVar(&duc.verify, "verify", false, "verify the packages against signatures") + f.StringVar(&duc.keyring, "keyring", defaultKeyring(), "keyring containing public keys") + f.BoolVar(&duc.skipRefresh, "skip-refresh", false, "do not refresh the local repository cache") + + return cmd +} + +// run runs the full dependency update process. +func (d *dependencyUpdateCmd) run() error { + man := &downloader.Manager{ + Out: d.out, + ChartPath: d.chartpath, + HelmHome: d.helmhome, + Keyring: d.keyring, + SkipUpdate: d.skipRefresh, + Getters: getter.All(settings), + } + if d.verify { + man.Verify = downloader.VerifyAlways + } + if settings.Debug { + man.Debug = true + } + return man.Update() +} diff --git a/pkg/helm/dependency_update_test.go b/pkg/helm/dependency_update_test.go new file mode 100644 index 000000000..f1f2bf944 --- /dev/null +++ b/pkg/helm/dependency_update_test.go @@ -0,0 +1,273 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/ghodss/yaml" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/provenance" + "k8s.io/helm/pkg/repo" + "k8s.io/helm/pkg/repo/repotest" +) + +func TestDependencyUpdateCmd(t *testing.T) { + hh, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + cleanup := resetEnv() + defer func() { + os.RemoveAll(hh.String()) + cleanup() + }() + + settings.Home = hh + + srv := repotest.NewServer(hh.String()) + defer srv.Stop() + copied, err := srv.CopyCharts("testdata/testcharts/*.tgz") + if err != nil { + t.Fatal(err) + } + t.Logf("Copied charts:\n%s", strings.Join(copied, "\n")) + t.Logf("Listening on directory %s", srv.Root()) + + chartname := "depup" + if err := createTestingChart(hh.String(), chartname, srv.URL()); err != nil { + t.Fatal(err) + } + + out := bytes.NewBuffer(nil) + duc := &dependencyUpdateCmd{out: out} + duc.helmhome = helmpath.Home(hh) + duc.chartpath = filepath.Join(hh.String(), chartname) + + if err := duc.run(); err != nil { + output := out.String() + t.Logf("Output: %s", output) + t.Fatal(err) + } + + output := out.String() + // This is written directly to stdout, so we have to capture as is. + if !strings.Contains(output, `update from the "test" chart repository`) { + t.Errorf("Repo did not get updated\n%s", output) + } + + // Make sure the actual file got downloaded. + expect := filepath.Join(hh.String(), chartname, "charts/reqtest-0.1.0.tgz") + if _, err := os.Stat(expect); err != nil { + t.Fatal(err) + } + + hash, err := provenance.DigestFile(expect) + if err != nil { + t.Fatal(err) + } + + i, err := repo.LoadIndexFile(duc.helmhome.CacheIndex("test")) + if err != nil { + t.Fatal(err) + } + + reqver := i.Entries["reqtest"][0] + if h := reqver.Digest; h != hash { + t.Errorf("Failed hash match: expected %s, got %s", hash, h) + } + + // Now change the dependencies and update. This verifies that on update, + // old dependencies are cleansed and new dependencies are added. + reqfile := &chartutil.Requirements{ + Dependencies: []*chartutil.Dependency{ + {Name: "reqtest", Version: "0.1.0", Repository: srv.URL()}, + {Name: "compressedchart", Version: "0.3.0", Repository: srv.URL()}, + }, + } + dir := filepath.Join(hh.String(), chartname) + if err := writeRequirements(dir, reqfile); err != nil { + t.Fatal(err) + } + if err := duc.run(); err != nil { + output := out.String() + t.Logf("Output: %s", output) + t.Fatal(err) + } + + // In this second run, we should see compressedchart-0.3.0.tgz, and not + // the 0.1.0 version. + expect = filepath.Join(hh.String(), chartname, "charts/compressedchart-0.3.0.tgz") + if _, err := os.Stat(expect); err != nil { + t.Fatalf("Expected %q: %s", expect, err) + } + dontExpect := filepath.Join(hh.String(), chartname, "charts/compressedchart-0.1.0.tgz") + if _, err := os.Stat(dontExpect); err == nil { + t.Fatalf("Unexpected %q", dontExpect) + } +} + +func TestDependencyUpdateCmd_SkipRefresh(t *testing.T) { + hh, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + cleanup := resetEnv() + defer func() { + os.RemoveAll(hh.String()) + cleanup() + }() + + settings.Home = hh + + srv := repotest.NewServer(hh.String()) + defer srv.Stop() + copied, err := srv.CopyCharts("testdata/testcharts/*.tgz") + if err != nil { + t.Fatal(err) + } + t.Logf("Copied charts:\n%s", strings.Join(copied, "\n")) + t.Logf("Listening on directory %s", srv.Root()) + + chartname := "depup" + if err := createTestingChart(hh.String(), chartname, srv.URL()); err != nil { + t.Fatal(err) + } + + out := bytes.NewBuffer(nil) + duc := &dependencyUpdateCmd{out: out} + duc.helmhome = helmpath.Home(hh) + duc.chartpath = filepath.Join(hh.String(), chartname) + duc.skipRefresh = true + + if err := duc.run(); err == nil { + t.Fatal("Expected failure to find the repo with skipRefresh") + } + + output := out.String() + // This is written directly to stdout, so we have to capture as is. + if strings.Contains(output, `update from the "test" chart repository`) { + t.Errorf("Repo was unexpectedly updated\n%s", output) + } +} + +func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) { + hh, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + cleanup := resetEnv() + defer func() { + os.RemoveAll(hh.String()) + cleanup() + }() + + settings.Home = hh + + srv := repotest.NewServer(hh.String()) + defer srv.Stop() + copied, err := srv.CopyCharts("testdata/testcharts/*.tgz") + if err != nil { + t.Fatal(err) + } + t.Logf("Copied charts:\n%s", strings.Join(copied, "\n")) + t.Logf("Listening on directory %s", srv.Root()) + + chartname := "depupdelete" + if err := createTestingChart(hh.String(), chartname, srv.URL()); err != nil { + t.Fatal(err) + } + + out := bytes.NewBuffer(nil) + duc := &dependencyUpdateCmd{out: out} + duc.helmhome = helmpath.Home(hh) + duc.chartpath = filepath.Join(hh.String(), chartname) + + if err := duc.run(); err != nil { + output := out.String() + t.Logf("Output: %s", output) + t.Fatal(err) + } + + // Chart repo is down + srv.Stop() + + if err := duc.run(); err == nil { + output := out.String() + t.Logf("Output: %s", output) + t.Fatal("Expected error, got nil") + } + + // Make sure charts dir still has dependencies + files, err := ioutil.ReadDir(filepath.Join(duc.chartpath, "charts")) + if err != nil { + t.Fatal(err) + } + dependencies := []string{"compressedchart-0.1.0.tgz", "reqtest-0.1.0.tgz"} + + if len(dependencies) != len(files) { + t.Fatalf("Expected %d chart dependencies, got %d", len(dependencies), len(files)) + } + for index, file := range files { + if dependencies[index] != file.Name() { + t.Fatalf("Chart dependency %s not matching %s", dependencies[index], file.Name()) + } + } + + // Make sure tmpcharts is deleted + if _, err := os.Stat(filepath.Join(duc.chartpath, "tmpcharts")); !os.IsNotExist(err) { + t.Fatalf("tmpcharts dir still exists") + } +} + +// createTestingChart creates a basic chart that depends on reqtest-0.1.0 +// +// The baseURL can be used to point to a particular repository server. +func createTestingChart(dest, name, baseURL string) error { + cfile := &chart.Metadata{ + Name: name, + Version: "1.2.3", + } + dir := filepath.Join(dest, name) + _, err := chartutil.Create(cfile, dest) + if err != nil { + return err + } + req := &chartutil.Requirements{ + Dependencies: []*chartutil.Dependency{ + {Name: "reqtest", Version: "0.1.0", Repository: baseURL}, + {Name: "compressedchart", Version: "0.1.0", Repository: baseURL}, + }, + } + return writeRequirements(dir, req) +} + +func writeRequirements(dir string, req *chartutil.Requirements) error { + data, err := yaml.Marshal(req) + if err != nil { + return err + } + + return ioutil.WriteFile(filepath.Join(dir, "requirements.yaml"), data, 0655) +} diff --git a/pkg/helm/docs.go b/pkg/helm/docs.go new file mode 100644 index 000000000..b7d7a8e85 --- /dev/null +++ b/pkg/helm/docs.go @@ -0,0 +1,80 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "path/filepath" + + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" +) + +const docsDesc = ` +Generate documentation files for Helm. + +This command can generate documentation for Helm in the following formats: + +- Markdown +- Man pages + +It can also generate bash autocompletions. + + $ helm docs markdown -dir mydocs/ +` + +type docsCmd struct { + out io.Writer + dest string + docTypeString string + topCmd *cobra.Command +} + +func newDocsCmd(out io.Writer) *cobra.Command { + dc := &docsCmd{out: out} + + cmd := &cobra.Command{ + Use: "docs", + Short: "Generate documentation as markdown or man pages", + Long: docsDesc, + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + dc.topCmd = cmd.Root() + return dc.run() + }, + } + + f := cmd.Flags() + f.StringVar(&dc.dest, "dir", "./", "directory to which documentation is written") + f.StringVar(&dc.docTypeString, "type", "markdown", "the type of documentation to generate (markdown, man, bash)") + + return cmd +} + +func (d *docsCmd) run() error { + switch d.docTypeString { + case "markdown", "mdown", "md": + return doc.GenMarkdownTree(d.topCmd, d.dest) + case "man": + manHdr := &doc.GenManHeader{Title: "HELM", Section: "1"} + return doc.GenManTree(d.topCmd, manHdr, d.dest) + case "bash": + return d.topCmd.GenBashCompletionFile(filepath.Join(d.dest, "completions.bash")) + default: + return fmt.Errorf("unknown doc type %q. Try 'markdown' or 'man'", d.docTypeString) + } +} diff --git a/pkg/helm/exports.go b/pkg/helm/exports.go new file mode 100644 index 000000000..83edecbb1 --- /dev/null +++ b/pkg/helm/exports.go @@ -0,0 +1,15 @@ +package helm + +func Fetch(url, version, name, dest string) error { + toFetch := fetchCmd{ + untar: true, + untardir: dest, + destdir: dest, + repoURL: url, + version: version, + chartRef: name, + } + + err := toFetch.run() + return err +} diff --git a/pkg/helm/fetch.go b/pkg/helm/fetch.go new file mode 100644 index 000000000..136f783e2 --- /dev/null +++ b/pkg/helm/fetch.go @@ -0,0 +1,186 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + "github.com/spf13/cobra" + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/downloader" + "k8s.io/helm/pkg/getter" + "k8s.io/helm/pkg/repo" +) + +const fetchDesc = ` +Retrieve a package from a package repository, and download it locally. + +This is useful for fetching packages to inspect, modify, or repackage. It can +also be used to perform cryptographic verification of a chart without installing +the chart. + +There are options for unpacking the chart after download. This will create a +directory for the chart and uncompress into that directory. + +If the --verify flag is specified, the requested chart MUST have a provenance +file, and MUST pass the verification process. Failure in any part of this will +result in an error, and the chart will not be saved locally. +` + +type fetchCmd struct { + untar bool + untardir string + chartRef string + destdir string + version string + repoURL string + username string + password string + + verify bool + verifyLater bool + keyring string + + certFile string + keyFile string + caFile string + + devel bool + + out io.Writer +} + +func newFetchCmd(out io.Writer) *cobra.Command { + fch := &fetchCmd{out: out} + + cmd := &cobra.Command{ + Use: "fetch [flags] [chart URL | repo/chartname] [...]", + Short: "download a chart from a repository and (optionally) unpack it in local directory", + Long: fetchDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("need at least one argument, url or repo/name of the chart") + } + + if fch.version == "" && fch.devel { + debug("setting version to >0.0.0-0") + fch.version = ">0.0.0-0" + } + + for i := 0; i < len(args); i++ { + fch.chartRef = args[i] + if err := fch.run(); err != nil { + return err + } + } + return nil + }, + } + + f := cmd.Flags() + f.BoolVar(&fch.untar, "untar", false, "if set to true, will untar the chart after downloading it") + f.StringVar(&fch.untardir, "untardir", ".", "if untar is specified, this flag specifies the name of the directory into which the chart is expanded") + f.BoolVar(&fch.verify, "verify", false, "verify the package against its signature") + f.BoolVar(&fch.verifyLater, "prov", false, "fetch the provenance file, but don't perform verification") + f.StringVar(&fch.version, "version", "", "specific version of a chart. Without this, the latest version is fetched") + f.StringVar(&fch.keyring, "keyring", defaultKeyring(), "keyring containing public keys") + f.StringVarP(&fch.destdir, "destination", "d", ".", "location to write the chart. If this and tardir are specified, tardir is appended to this") + f.StringVar(&fch.repoURL, "repo", "", "chart repository url where to locate the requested chart") + f.StringVar(&fch.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&fch.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.StringVar(&fch.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") + f.BoolVar(&fch.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") + f.StringVar(&fch.username, "username", "", "chart repository username") + f.StringVar(&fch.password, "password", "", "chart repository password") + + return cmd +} + +func (f *fetchCmd) run() error { + c := downloader.ChartDownloader{ + HelmHome: settings.Home, + Out: f.out, + Keyring: f.keyring, + Verify: downloader.VerifyNever, + Getters: getter.All(settings), + Username: f.username, + Password: f.password, + } + + if f.verify { + c.Verify = downloader.VerifyAlways + } else if f.verifyLater { + c.Verify = downloader.VerifyLater + } + + // If untar is set, we fetch to a tempdir, then untar and copy after + // verification. + dest := f.destdir + if f.untar { + var err error + dest, err = ioutil.TempDir("", "helm-") + if err != nil { + return fmt.Errorf("Failed to untar: %s", err) + } + defer os.RemoveAll(dest) + } + + if f.repoURL != "" { + chartURL, err := repo.FindChartInAuthRepoURL(f.repoURL, f.username, f.password, f.chartRef, f.version, f.certFile, f.keyFile, f.caFile, getter.All(settings)) + if err != nil { + return err + } + f.chartRef = chartURL + } + + saved, v, err := c.DownloadTo(f.chartRef, f.version, dest) + if err != nil { + return err + } + + if f.verify { + fmt.Fprintf(f.out, "Verification: %v\n", v) + } + + // After verification, untar the chart into the requested directory. + if f.untar { + ud := f.untardir + if !filepath.IsAbs(ud) { + ud = filepath.Join(f.destdir, ud) + } + if fi, err := os.Stat(ud); err != nil { + if err := os.MkdirAll(ud, 0755); err != nil { + return fmt.Errorf("Failed to untar (mkdir): %s", err) + } + + } else if !fi.IsDir() { + return fmt.Errorf("Failed to untar: %s is not a directory", ud) + } + + return chartutil.ExpandFile(ud, saved) + } + return nil +} + +// defaultKeyring returns the expanded path to the default keyring. +func defaultKeyring() string { + return os.ExpandEnv("$HOME/.gnupg/pubring.gpg") +} diff --git a/pkg/helm/fetch_test.go b/pkg/helm/fetch_test.go new file mode 100644 index 000000000..26adab678 --- /dev/null +++ b/pkg/helm/fetch_test.go @@ -0,0 +1,179 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "regexp" + "testing" + + "k8s.io/helm/pkg/repo/repotest" +) + +func TestFetchCmd(t *testing.T) { + hh, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + cleanup := resetEnv() + defer func() { + os.RemoveAll(hh.String()) + cleanup() + }() + srv := repotest.NewServer(hh.String()) + defer srv.Stop() + + settings.Home = hh + + // all flags will get "--home=TMDIR -d outdir" appended. + tests := []struct { + name string + chart string + flags []string + fail bool + failExpect string + expectFile string + expectDir bool + expectVerify bool + }{ + { + name: "Basic chart fetch", + chart: "test/signtest", + expectFile: "./signtest-0.1.0.tgz", + }, + { + name: "Chart fetch with version", + chart: "test/signtest", + flags: []string{"--version", "0.1.0"}, + expectFile: "./signtest-0.1.0.tgz", + }, + { + name: "Fail chart fetch with non-existent version", + chart: "test/signtest", + flags: []string{"--version", "99.1.0"}, + fail: true, + failExpect: "no such chart", + }, + { + name: "Fail fetching non-existent chart", + chart: "test/nosuchthing", + failExpect: "Failed to fetch", + fail: true, + }, + { + name: "Fetch and verify", + chart: "test/signtest", + flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"}, + expectFile: "./signtest-0.1.0.tgz", + expectVerify: true, + }, + { + name: "Fetch and fail verify", + chart: "test/reqtest", + flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"}, + failExpect: "Failed to fetch provenance", + fail: true, + }, + { + name: "Fetch and untar", + chart: "test/signtest", + flags: []string{"--untar", "--untardir", "signtest"}, + expectFile: "./signtest", + expectDir: true, + }, + { + name: "Fetch, verify, untar", + chart: "test/signtest", + flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub", "--untar", "--untardir", "signtest"}, + expectFile: "./signtest", + expectDir: true, + expectVerify: true, + }, + { + name: "Chart fetch using repo URL", + chart: "signtest", + expectFile: "./signtest-0.1.0.tgz", + flags: []string{"--repo", srv.URL()}, + }, + { + name: "Fail fetching non-existent chart on repo URL", + chart: "someChart", + flags: []string{"--repo", srv.URL()}, + failExpect: "Failed to fetch chart", + fail: true, + }, + { + name: "Specific version chart fetch using repo URL", + chart: "signtest", + expectFile: "./signtest-0.1.0.tgz", + flags: []string{"--repo", srv.URL(), "--version", "0.1.0"}, + }, + { + name: "Specific version chart fetch using repo URL", + chart: "signtest", + flags: []string{"--repo", srv.URL(), "--version", "0.2.0"}, + failExpect: "Failed to fetch chart version", + fail: true, + }, + } + + if _, err := srv.CopyCharts("testdata/testcharts/*.tgz*"); err != nil { + t.Fatal(err) + } + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + + for _, tt := range tests { + outdir := filepath.Join(hh.String(), "testout") + os.RemoveAll(outdir) + os.Mkdir(outdir, 0755) + + buf := bytes.NewBuffer(nil) + cmd := newFetchCmd(buf) + tt.flags = append(tt.flags, "-d", outdir) + cmd.ParseFlags(tt.flags) + if err := cmd.RunE(cmd, []string{tt.chart}); err != nil { + if tt.fail { + continue + } + t.Errorf("%q reported error: %s", tt.name, err) + continue + } + if tt.expectVerify { + pointerAddressPattern := "0[xX][A-Fa-f0-9]+" + sha256Pattern := "[A-Fa-f0-9]{64}" + verificationRegex := regexp.MustCompile( + fmt.Sprintf("Verification: &{%s sha256:%s signtest-0.1.0.tgz}\n", pointerAddressPattern, sha256Pattern)) + if !verificationRegex.MatchString(buf.String()) { + t.Errorf("%q: expected match for regex %s, got %s", tt.name, verificationRegex, buf.String()) + } + } + + ef := filepath.Join(outdir, tt.expectFile) + fi, err := os.Stat(ef) + if err != nil { + t.Errorf("%q: expected a file at %s. %s", tt.name, ef, err) + } + if fi.IsDir() != tt.expectDir { + t.Errorf("%q: expected directory=%t, but it's not.", tt.name, tt.expectDir) + } + } +} diff --git a/pkg/helm/get.go b/pkg/helm/get.go new file mode 100644 index 000000000..fd0273b61 --- /dev/null +++ b/pkg/helm/get.go @@ -0,0 +1,89 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "errors" + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" +) + +var getHelp = ` +This command shows the details of a named release. + +It can be used to get extended information about the release, including: + + - The values used to generate the release + - The chart used to generate the release + - The generated manifest file + +By default, this prints a human readable collection of information about the +chart, the supplied values, and the generated manifest file. +` + +var errReleaseRequired = errors.New("release name is required") + +type getCmd struct { + release string + out io.Writer + client helm.Interface + version int32 +} + +func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { + get := &getCmd{ + out: out, + client: client, + } + + cmd := &cobra.Command{ + Use: "get [flags] RELEASE_NAME", + Short: "download a named release", + Long: getHelp, + PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errReleaseRequired + } + get.release = args[0] + if get.client == nil { + get.client = newClient() + } + return get.run() + }, + } + + cmd.Flags().Int32Var(&get.version, "revision", 0, "get the named release with revision") + + cmd.AddCommand(addFlagsTLS(newGetValuesCmd(nil, out))) + cmd.AddCommand(addFlagsTLS(newGetManifestCmd(nil, out))) + cmd.AddCommand(addFlagsTLS(newGetHooksCmd(nil, out))) + + return cmd +} + +// getCmd is the command that implements 'helm get' +func (g *getCmd) run() error { + res, err := g.client.ReleaseContent(g.release, helm.ContentReleaseVersion(g.version)) + if err != nil { + return prettyError(err) + } + return printRelease(g.out, res.Release) +} diff --git a/pkg/helm/get_hooks.go b/pkg/helm/get_hooks.go new file mode 100644 index 000000000..705474628 --- /dev/null +++ b/pkg/helm/get_hooks.go @@ -0,0 +1,75 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" +) + +const getHooksHelp = ` +This command downloads hooks for a given release. + +Hooks are formatted in YAML and separated by the YAML '---\n' separator. +` + +type getHooksCmd struct { + release string + out io.Writer + client helm.Interface + version int32 +} + +func newGetHooksCmd(client helm.Interface, out io.Writer) *cobra.Command { + ghc := &getHooksCmd{ + out: out, + client: client, + } + cmd := &cobra.Command{ + Use: "hooks [flags] RELEASE_NAME", + Short: "download all hooks for a named release", + Long: getHooksHelp, + PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errReleaseRequired + } + ghc.release = args[0] + ghc.client = ensureHelmClient(ghc.client) + return ghc.run() + }, + } + cmd.Flags().Int32Var(&ghc.version, "revision", 0, "get the named release with revision") + return cmd +} + +func (g *getHooksCmd) run() error { + res, err := g.client.ReleaseContent(g.release, helm.ContentReleaseVersion(g.version)) + if err != nil { + fmt.Fprintln(g.out, g.release) + return prettyError(err) + } + + for _, hook := range res.Release.Hooks { + fmt.Fprintf(g.out, "---\n# %s\n%s", hook.Name, hook.Manifest) + } + return nil +} diff --git a/pkg/helm/get_hooks_test.go b/pkg/helm/get_hooks_test.go new file mode 100644 index 000000000..305306009 --- /dev/null +++ b/pkg/helm/get_hooks_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestGetHooks(t *testing.T) { + tests := []releaseCase{ + { + name: "get hooks with release", + args: []string{"aeneas"}, + expected: helm.MockHookTemplate, + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})}, + }, + { + name: "get hooks without args", + args: []string{}, + err: true, + }, + } + runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newGetHooksCmd(c, out) + }) +} diff --git a/pkg/helm/get_manifest.go b/pkg/helm/get_manifest.go new file mode 100644 index 000000000..80e5f7b8e --- /dev/null +++ b/pkg/helm/get_manifest.go @@ -0,0 +1,75 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" +) + +var getManifestHelp = ` +This command fetches the generated manifest for a given release. + +A manifest is a YAML-encoded representation of the Kubernetes resources that +were generated from this release's chart(s). If a chart is dependent on other +charts, those resources will also be included in the manifest. +` + +type getManifestCmd struct { + release string + out io.Writer + client helm.Interface + version int32 +} + +func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command { + get := &getManifestCmd{ + out: out, + client: client, + } + cmd := &cobra.Command{ + Use: "manifest [flags] RELEASE_NAME", + Short: "download the manifest for a named release", + Long: getManifestHelp, + PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errReleaseRequired + } + get.release = args[0] + get.client = ensureHelmClient(get.client) + return get.run() + }, + } + + cmd.Flags().Int32Var(&get.version, "revision", 0, "get the named release with revision") + return cmd +} + +// getManifest implements 'helm get manifest' +func (g *getManifestCmd) run() error { + res, err := g.client.ReleaseContent(g.release, helm.ContentReleaseVersion(g.version)) + if err != nil { + return prettyError(err) + } + fmt.Fprintln(g.out, res.Release.Manifest) + return nil +} diff --git a/pkg/helm/get_manifest_test.go b/pkg/helm/get_manifest_test.go new file mode 100644 index 000000000..cdbbc468f --- /dev/null +++ b/pkg/helm/get_manifest_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestGetManifest(t *testing.T) { + tests := []releaseCase{ + { + name: "get manifest with release", + args: []string{"juno"}, + expected: helm.MockManifest, + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "juno"}), + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "juno"})}, + }, + { + name: "get manifest without args", + args: []string{}, + err: true, + }, + } + runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newGetManifestCmd(c, out) + }) +} diff --git a/pkg/helm/get_test.go b/pkg/helm/get_test.go new file mode 100644 index 000000000..830ee9d57 --- /dev/null +++ b/pkg/helm/get_test.go @@ -0,0 +1,48 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestGetCmd(t *testing.T) { + tests := []releaseCase{ + { + name: "get with a release", + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"}), + args: []string{"thomas-guide"}, + expected: "REVISION: 1\nRELEASED: (.*)\nCHART: foo-0.1.0-beta.1\nUSER-SUPPLIED VALUES:\nname: \"value\"\nCOMPUTED VALUES:\nname: value\n\nHOOKS:\n---\n# pre-install-hook\n" + helm.MockHookTemplate + "\nMANIFEST:", + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"})}, + }, + { + name: "get requires release name arg", + err: true, + }, + } + + cmd := func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newGetCmd(c, out) + } + runReleaseCases(t, tests, cmd) +} diff --git a/pkg/helm/get_values.go b/pkg/helm/get_values.go new file mode 100644 index 000000000..221f1157b --- /dev/null +++ b/pkg/helm/get_values.go @@ -0,0 +1,89 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/helm" +) + +var getValuesHelp = ` +This command downloads a values file for a given release. +` + +type getValuesCmd struct { + release string + allValues bool + out io.Writer + client helm.Interface + version int32 +} + +func newGetValuesCmd(client helm.Interface, out io.Writer) *cobra.Command { + get := &getValuesCmd{ + out: out, + client: client, + } + cmd := &cobra.Command{ + Use: "values [flags] RELEASE_NAME", + Short: "download the values file for a named release", + Long: getValuesHelp, + PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errReleaseRequired + } + get.release = args[0] + get.client = ensureHelmClient(get.client) + return get.run() + }, + } + + cmd.Flags().Int32Var(&get.version, "revision", 0, "get the named release with revision") + cmd.Flags().BoolVarP(&get.allValues, "all", "a", false, "dump all (computed) values") + return cmd +} + +// getValues implements 'helm get values' +func (g *getValuesCmd) run() error { + res, err := g.client.ReleaseContent(g.release, helm.ContentReleaseVersion(g.version)) + if err != nil { + return prettyError(err) + } + + // If the user wants all values, compute the values and return. + if g.allValues { + cfg, err := chartutil.CoalesceValues(res.Release.Chart, res.Release.Config) + if err != nil { + return err + } + cfgStr, err := cfg.YAML() + if err != nil { + return err + } + fmt.Fprintln(g.out, cfgStr) + return nil + } + + fmt.Fprintln(g.out, res.Release.Config.Raw) + return nil +} diff --git a/pkg/helm/get_values_test.go b/pkg/helm/get_values_test.go new file mode 100644 index 000000000..32387c388 --- /dev/null +++ b/pkg/helm/get_values_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestGetValuesCmd(t *testing.T) { + tests := []releaseCase{ + { + name: "get values with a release", + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"}), + args: []string{"thomas-guide"}, + expected: "name: \"value\"", + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"})}, + }, + { + name: "get values requires release name arg", + err: true, + }, + } + cmd := func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newGetValuesCmd(c, out) + } + runReleaseCases(t, tests, cmd) +} diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go new file mode 100644 index 000000000..b8d044660 --- /dev/null +++ b/pkg/helm/helm.go @@ -0,0 +1,310 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "strings" + + "github.com/spf13/cobra" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/status" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + + // Import to initialize client auth plugins. + _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/helm/pkg/helm" + helm_env "k8s.io/helm/pkg/helm/environment" + "k8s.io/helm/pkg/helm/portforwarder" + "k8s.io/helm/pkg/kube" + "k8s.io/helm/pkg/tlsutil" +) + +var ( + tlsCaCertFile string // path to TLS CA certificate file + tlsCertFile string // path to TLS certificate file + tlsKeyFile string // path to TLS key file + tlsVerify bool // enable TLS and verify remote certificates + tlsEnable bool // enable TLS + + tlsCaCertDefault = "$HELM_HOME/ca.pem" + tlsCertDefault = "$HELM_HOME/cert.pem" + tlsKeyDefault = "$HELM_HOME/key.pem" + + tillerTunnel *kube.Tunnel + settings helm_env.EnvSettings +) + +var globalUsage = `The Kubernetes package manager + +To begin working with Helm, run the 'helm init' command: + + $ helm init + +This will install Tiller to your running Kubernetes cluster. +It will also set up any necessary local configuration. + +Common actions from this point include: + +- helm search: search for charts +- helm fetch: download a chart to your local directory to view +- helm install: upload the chart to Kubernetes +- helm list: list releases of charts + +Environment: + $HELM_HOME set an alternative location for Helm files. By default, these are stored in ~/.helm + $HELM_HOST set an alternative Tiller host. The format is host:port + $HELM_NO_PLUGINS disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. + $TILLER_NAMESPACE set an alternative Tiller namespace (default "kube-system") + $KUBECONFIG set an alternative Kubernetes configuration file (default "~/.kube/config") +` + +func newRootCmd(args []string) *cobra.Command { + cmd := &cobra.Command{ + Use: "helm", + Short: "The Helm package manager for Kubernetes.", + Long: globalUsage, + SilenceUsage: true, + PersistentPreRun: func(*cobra.Command, []string) { + tlsCaCertFile = os.ExpandEnv(tlsCaCertFile) + tlsCertFile = os.ExpandEnv(tlsCertFile) + tlsKeyFile = os.ExpandEnv(tlsKeyFile) + }, + PersistentPostRun: func(*cobra.Command, []string) { + teardown() + }, + } + flags := cmd.PersistentFlags() + + settings.AddFlags(flags) + + out := cmd.OutOrStdout() + + cmd.AddCommand( + // chart commands + newCreateCmd(out), + newDependencyCmd(out), + newFetchCmd(out), + newInspectCmd(out), + newLintCmd(out), + newPackageCmd(out), + newRepoCmd(out), + newSearchCmd(out), + newServeCmd(out), + newVerifyCmd(out), + + // release commands + addFlagsTLS(newDeleteCmd(nil, out)), + addFlagsTLS(newGetCmd(nil, out)), + addFlagsTLS(newHistoryCmd(nil, out)), + addFlagsTLS(newInstallCmd(nil, out)), + addFlagsTLS(newListCmd(nil, out)), + addFlagsTLS(newRollbackCmd(nil, out)), + addFlagsTLS(newStatusCmd(nil, out)), + addFlagsTLS(newUpgradeCmd(nil, out)), + + addFlagsTLS(newReleaseTestCmd(nil, out)), + addFlagsTLS(newResetCmd(nil, out)), + addFlagsTLS(newVersionCmd(nil, out)), + + newCompletionCmd(out), + newHomeCmd(out), + newInitCmd(out), + newPluginCmd(out), + newTemplateCmd(out), + + // Hidden documentation generator command: 'helm docs' + newDocsCmd(out), + + // Deprecated + markDeprecated(newRepoUpdateCmd(out), "use 'helm repo update'\n"), + ) + + flags.Parse(args) + + // set defaults from environment + settings.Init(flags) + + // Find and add plugins + loadPlugins(cmd, out) + + return cmd +} + +func init() { + // Tell gRPC not to log to console. + grpclog.SetLogger(log.New(ioutil.Discard, "", log.LstdFlags)) +} + +// func main() { +// cmd := newRootCmd(os.Args[1:]) +// if err := cmd.Execute(); err != nil { +// os.Exit(1) +// } +// } + +func markDeprecated(cmd *cobra.Command, notice string) *cobra.Command { + cmd.Deprecated = notice + return cmd +} + +func setupConnection() error { + if settings.TillerHost == "" { + config, client, err := getKubeClient(settings.KubeContext) + if err != nil { + return err + } + + tunnel, err := portforwarder.New(settings.TillerNamespace, client, config) + if err != nil { + return err + } + + settings.TillerHost = fmt.Sprintf("127.0.0.1:%d", tunnel.Local) + debug("Created tunnel using local port: '%d'\n", tunnel.Local) + } + + // Set up the gRPC config. + debug("SERVER: %q\n", settings.TillerHost) + + // Plugin support. + return nil +} + +func teardown() { + if tillerTunnel != nil { + tillerTunnel.Close() + } +} + +func checkArgsLength(argsReceived int, requiredArgs ...string) error { + expectedNum := len(requiredArgs) + if argsReceived != expectedNum { + arg := "arguments" + if expectedNum == 1 { + arg = "argument" + } + return fmt.Errorf("This command needs %v %s: %s", expectedNum, arg, strings.Join(requiredArgs, ", ")) + } + return nil +} + +// prettyError unwraps or rewrites certain errors to make them more user-friendly. +func prettyError(err error) error { + // Add this check can prevent the object creation if err is nil. + if err == nil { + return nil + } + // If it's grpc's error, make it more user-friendly. + if s, ok := status.FromError(err); ok { + return fmt.Errorf(s.Message()) + } + // Else return the original error. + return err +} + +// configForContext creates a Kubernetes REST client configuration for a given kubeconfig context. +func configForContext(context string) (*rest.Config, error) { + config, err := kube.GetConfig(context).ClientConfig() + if err != nil { + return nil, fmt.Errorf("could not get Kubernetes config for context %q: %s", context, err) + } + return config, nil +} + +// getKubeClient creates a Kubernetes config and client for a given kubeconfig context. +func getKubeClient(context string) (*rest.Config, kubernetes.Interface, error) { + config, err := configForContext(context) + if err != nil { + return nil, nil, err + } + client, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, nil, fmt.Errorf("could not get Kubernetes client: %s", err) + } + return config, client, nil +} + +// getInternalKubeClient creates a Kubernetes config and an "internal" client for a given kubeconfig context. +// +// Prefer the similar getKubeClient if you don't need to use such an internal client. +func getInternalKubeClient(context string) (internalclientset.Interface, error) { + config, err := configForContext(context) + if err != nil { + return nil, err + } + client, err := internalclientset.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("could not get Kubernetes client: %s", err) + } + return client, nil +} + +// ensureHelmClient returns a new helm client impl. if h is not nil. +func ensureHelmClient(h helm.Interface) helm.Interface { + if h != nil { + return h + } + return newClient() +} + +func newClient() helm.Interface { + options := []helm.Option{helm.Host(settings.TillerHost), helm.ConnectTimeout(settings.TillerConnectionTimeout)} + + if tlsVerify || tlsEnable { + if tlsCaCertFile == "" { + tlsCaCertFile = settings.Home.TLSCaCert() + } + if tlsCertFile == "" { + tlsCertFile = settings.Home.TLSCert() + } + if tlsKeyFile == "" { + tlsKeyFile = settings.Home.TLSKey() + } + debug("Key=%q, Cert=%q, CA=%q\n", tlsKeyFile, tlsCertFile, tlsCaCertFile) + tlsopts := tlsutil.Options{KeyFile: tlsKeyFile, CertFile: tlsCertFile, InsecureSkipVerify: true} + if tlsVerify { + tlsopts.CaCertFile = tlsCaCertFile + tlsopts.InsecureSkipVerify = false + } + tlscfg, err := tlsutil.ClientConfig(tlsopts) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } + options = append(options, helm.WithTLS(tlscfg)) + } + return helm.NewClient(options...) +} + +// addFlagsTLS adds the flags for supporting client side TLS to the +// helm command (only those that invoke communicate to Tiller.) +func addFlagsTLS(cmd *cobra.Command) *cobra.Command { + + // add flags + cmd.Flags().StringVar(&tlsCaCertFile, "tls-ca-cert", tlsCaCertDefault, "path to TLS CA certificate file") + cmd.Flags().StringVar(&tlsCertFile, "tls-cert", tlsCertDefault, "path to TLS certificate file") + cmd.Flags().StringVar(&tlsKeyFile, "tls-key", tlsKeyDefault, "path to TLS key file") + cmd.Flags().BoolVar(&tlsVerify, "tls-verify", false, "enable TLS for request and verify remote") + cmd.Flags().BoolVar(&tlsEnable, "tls", false, "enable TLS for request") + return cmd +} diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go new file mode 100644 index 000000000..58b4763fb --- /dev/null +++ b/pkg/helm/helm_test.go @@ -0,0 +1,238 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/repo" +) + +// releaseCmd is a command that works with a FakeClient +type releaseCmd func(c *helm.FakeClient, out io.Writer) *cobra.Command + +// runReleaseCases runs a set of release cases through the given releaseCmd. +func runReleaseCases(t *testing.T, tests []releaseCase, rcmd releaseCmd) { + var buf bytes.Buffer + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &helm.FakeClient{Rels: tt.rels} + cmd := rcmd(c, &buf) + cmd.ParseFlags(tt.flags) + err := cmd.RunE(cmd, tt.args) + if (err != nil) != tt.err { + t.Errorf("expected error, got '%v'", err) + } + re := regexp.MustCompile(tt.expected) + if !re.Match(buf.Bytes()) { + t.Errorf("expected\n%q\ngot\n%q", tt.expected, buf.String()) + } + buf.Reset() + }) + } +} + +// releaseCase describes a test case that works with releases. +type releaseCase struct { + name string + args []string + flags []string + // expected is the string to be matched. This supports regular expressions. + expected string + err bool + resp *release.Release + // Rels are the available releases at the start of the test. + rels []*release.Release +} + +// tempHelmHome sets up a Helm Home in a temp dir. +// +// This does not clean up the directory. You must do that yourself. +// You must also set helmHome yourself. +func tempHelmHome(t *testing.T) (helmpath.Home, error) { + oldhome := settings.Home + dir, err := ioutil.TempDir("", "helm_home-") + if err != nil { + return helmpath.Home("n/"), err + } + + settings.Home = helmpath.Home(dir) + if err := ensureTestHome(settings.Home, t); err != nil { + return helmpath.Home("n/"), err + } + settings.Home = oldhome + return helmpath.Home(dir), nil +} + +// ensureTestHome creates a home directory like ensureHome, but without remote references. +// +// t is used only for logging. +func ensureTestHome(home helmpath.Home, t *testing.T) error { + configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository(), home.Plugins(), home.Starters()} + for _, p := range configDirectories { + if fi, err := os.Stat(p); err != nil { + if err := os.MkdirAll(p, 0755); err != nil { + return fmt.Errorf("Could not create %s: %s", p, err) + } + } else if !fi.IsDir() { + return fmt.Errorf("%s must be a directory", p) + } + } + + repoFile := home.RepositoryFile() + if fi, err := os.Stat(repoFile); err != nil { + rf := repo.NewRepoFile() + rf.Add(&repo.Entry{ + Name: "charts", + URL: "http://example.com/foo", + Cache: "charts-index.yaml", + }, &repo.Entry{ + Name: "local", + URL: "http://localhost.com:7743/foo", + Cache: "local-index.yaml", + }) + if err := rf.WriteFile(repoFile, 0644); err != nil { + return err + } + } else if fi.IsDir() { + return fmt.Errorf("%s must be a file, not a directory", repoFile) + } + if r, err := repo.LoadRepositoriesFile(repoFile); err == repo.ErrRepoOutOfDate { + t.Log("Updating repository file format...") + if err := r.WriteFile(repoFile, 0644); err != nil { + return err + } + } + + localRepoIndexFile := home.LocalRepository(localRepositoryIndexFile) + if fi, err := os.Stat(localRepoIndexFile); err != nil { + i := repo.NewIndexFile() + if err := i.WriteFile(localRepoIndexFile, 0644); err != nil { + return err + } + + //TODO: take this out and replace with helm update functionality + os.Symlink(localRepoIndexFile, home.CacheIndex("local")) + } else if fi.IsDir() { + return fmt.Errorf("%s must be a file, not a directory", localRepoIndexFile) + } + + t.Logf("$HELM_HOME has been configured at %s.\n", settings.Home.String()) + return nil + +} + +func TestRootCmd(t *testing.T) { + cleanup := resetEnv() + defer cleanup() + + tests := []struct { + name string + args []string + envars map[string]string + home string + }{ + { + name: "defaults", + args: []string{"home"}, + home: filepath.Join(os.Getenv("HOME"), "/.helm"), + }, + { + name: "with --home set", + args: []string{"--home", "/foo"}, + home: "/foo", + }, + { + name: "subcommands with --home set", + args: []string{"home", "--home", "/foo"}, + home: "/foo", + }, + { + name: "with $HELM_HOME set", + args: []string{"home"}, + envars: map[string]string{"HELM_HOME": "/bar"}, + home: "/bar", + }, + { + name: "subcommands with $HELM_HOME set", + args: []string{"home"}, + envars: map[string]string{"HELM_HOME": "/bar"}, + home: "/bar", + }, + { + name: "with $HELM_HOME and --home set", + args: []string{"home", "--home", "/foo"}, + envars: map[string]string{"HELM_HOME": "/bar"}, + home: "/foo", + }, + } + + // ensure not set locally + os.Unsetenv("HELM_HOME") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer os.Unsetenv("HELM_HOME") + + for k, v := range tt.envars { + os.Setenv(k, v) + } + + cmd := newRootCmd(tt.args) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tt.args) + cmd.Run = func(*cobra.Command, []string) {} + if err := cmd.Execute(); err != nil { + t.Errorf("unexpected error: %s", err) + } + + if settings.Home.String() != tt.home { + t.Errorf("expected home %q, got %q", tt.home, settings.Home) + } + homeFlag := cmd.Flag("home").Value.String() + homeFlag = os.ExpandEnv(homeFlag) + if homeFlag != tt.home { + t.Errorf("expected home %q, got %q", tt.home, homeFlag) + } + }) + } +} + +func resetEnv() func() { + origSettings := settings + origEnv := os.Environ() + return func() { + settings = origSettings + for _, pair := range origEnv { + kv := strings.SplitN(pair, "=", 2) + os.Setenv(kv[0], kv[1]) + } + } +} diff --git a/pkg/helm/history.go b/pkg/helm/history.go new file mode 100644 index 000000000..74cf211d6 --- /dev/null +++ b/pkg/helm/history.go @@ -0,0 +1,172 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/ghodss/yaml" + "github.com/gosuri/uitable" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/timeconv" +) + +type releaseInfo struct { + Revision int32 `json:"revision"` + Updated string `json:"updated"` + Status string `json:"status"` + Chart string `json:"chart"` + Description string `json:"description"` +} + +type releaseHistory []releaseInfo + +var historyHelp = ` +History prints historical revisions for a given release. + +A default maximum of 256 revisions will be returned. Setting '--max' +configures the maximum length of the revision list returned. + +The historical release set is printed as a formatted table, e.g: + + $ helm history angry-bird --max=4 + REVISION UPDATED STATUS CHART DESCRIPTION + 1 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Initial install + 2 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Upgraded successfully + 3 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Rolled back to 2 + 4 Mon Oct 3 10:15:13 2016 DEPLOYED alpine-0.1.0 Upgraded successfully +` + +type historyCmd struct { + max int32 + rls string + out io.Writer + helmc helm.Interface + colWidth uint + outputFormat string +} + +func newHistoryCmd(c helm.Interface, w io.Writer) *cobra.Command { + his := &historyCmd{out: w, helmc: c} + + cmd := &cobra.Command{ + Use: "history [flags] RELEASE_NAME", + Long: historyHelp, + Short: "fetch release history", + Aliases: []string{"hist"}, + PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + RunE: func(cmd *cobra.Command, args []string) error { + switch { + case len(args) == 0: + return errReleaseRequired + case his.helmc == nil: + his.helmc = newClient() + } + his.rls = args[0] + return his.run() + }, + } + + f := cmd.Flags() + f.Int32Var(&his.max, "max", 256, "maximum number of revision to include in history") + f.UintVar(&his.colWidth, "col-width", 60, "specifies the max column width of output") + f.StringVarP(&his.outputFormat, "output", "o", "table", "prints the output in the specified format (json|table|yaml)") + + return cmd +} + +func (cmd *historyCmd) run() error { + r, err := cmd.helmc.ReleaseHistory(cmd.rls, helm.WithMaxHistory(cmd.max)) + if err != nil { + return prettyError(err) + } + if len(r.Releases) == 0 { + return nil + } + + releaseHistory := getReleaseHistory(r.Releases) + + var history []byte + var formattingError error + + switch cmd.outputFormat { + case "yaml": + history, formattingError = yaml.Marshal(releaseHistory) + case "json": + history, formattingError = json.Marshal(releaseHistory) + case "table": + history = formatAsTable(releaseHistory, cmd.colWidth) + default: + return fmt.Errorf("unknown output format %q", cmd.outputFormat) + } + + if formattingError != nil { + return prettyError(formattingError) + } + + fmt.Fprintln(cmd.out, string(history)) + return nil +} + +func getReleaseHistory(rls []*release.Release) (history releaseHistory) { + for i := len(rls) - 1; i >= 0; i-- { + r := rls[i] + c := formatChartname(r.Chart) + t := timeconv.String(r.Info.LastDeployed) + s := r.Info.Status.Code.String() + v := r.Version + d := r.Info.Description + + rInfo := releaseInfo{ + Revision: v, + Updated: t, + Status: s, + Chart: c, + Description: d, + } + history = append(history, rInfo) + } + + return history +} + +func formatAsTable(releases releaseHistory, colWidth uint) []byte { + tbl := uitable.New() + + tbl.MaxColWidth = colWidth + tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "DESCRIPTION") + for i := 0; i <= len(releases)-1; i++ { + r := releases[i] + tbl.AddRow(r.Revision, r.Updated, r.Status, r.Chart, r.Description) + } + return tbl.Bytes() +} + +func formatChartname(c *chart.Chart) string { + if c == nil || c.Metadata == nil { + // This is an edge case that has happened in prod, though we don't + // know how: https://github.com/kubernetes/helm/issues/1347 + return "MISSING" + } + return fmt.Sprintf("%s-%s", c.Metadata.Name, c.Metadata.Version) +} diff --git a/pkg/helm/history_test.go b/pkg/helm/history_test.go new file mode 100644 index 000000000..85cf9e24a --- /dev/null +++ b/pkg/helm/history_test.go @@ -0,0 +1,85 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + rpb "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestHistoryCmd(t *testing.T) { + mk := func(name string, vers int32, code rpb.Status_Code) *rpb.Release { + return helm.ReleaseMock(&helm.MockReleaseOptions{ + Name: name, + Version: vers, + StatusCode: code, + }) + } + + tests := []releaseCase{ + { + name: "get history for release", + args: []string{"angry-bird"}, + rels: []*rpb.Release{ + mk("angry-bird", 4, rpb.Status_DEPLOYED), + mk("angry-bird", 3, rpb.Status_SUPERSEDED), + mk("angry-bird", 2, rpb.Status_SUPERSEDED), + mk("angry-bird", 1, rpb.Status_SUPERSEDED), + }, + expected: "REVISION\tUPDATED \tSTATUS \tCHART \tDESCRIPTION \n1 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n2 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\tRelease mock\n", + }, + { + name: "get history with max limit set", + args: []string{"angry-bird"}, + flags: []string{"--max", "2"}, + rels: []*rpb.Release{ + mk("angry-bird", 4, rpb.Status_DEPLOYED), + mk("angry-bird", 3, rpb.Status_SUPERSEDED), + }, + expected: "REVISION\tUPDATED \tSTATUS \tCHART \tDESCRIPTION \n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\tRelease mock\n", + }, + { + name: "get history with yaml output format", + args: []string{"angry-bird"}, + flags: []string{"--output", "yaml"}, + rels: []*rpb.Release{ + mk("angry-bird", 4, rpb.Status_DEPLOYED), + mk("angry-bird", 3, rpb.Status_SUPERSEDED), + }, + expected: "- chart: foo-0.1.0-beta.1\n description: Release mock\n revision: 3\n status: SUPERSEDED\n updated: (.*)\n- chart: foo-0.1.0-beta.1\n description: Release mock\n revision: 4\n status: DEPLOYED\n updated: (.*)\n\n", + }, + { + name: "get history with json output format", + args: []string{"angry-bird"}, + flags: []string{"--output", "json"}, + rels: []*rpb.Release{ + mk("angry-bird", 4, rpb.Status_DEPLOYED), + mk("angry-bird", 3, rpb.Status_SUPERSEDED), + }, + expected: `[{"revision":3,"updated":".*","status":"SUPERSEDED","chart":"foo\-0.1.0-beta.1","description":"Release mock"},{"revision":4,"updated":".*","status":"DEPLOYED","chart":"foo\-0.1.0-beta.1","description":"Release mock"}]\n`, + }, + } + + runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newHistoryCmd(c, out) + }) +} diff --git a/pkg/helm/home.go b/pkg/helm/home.go new file mode 100644 index 000000000..22c4f5ae3 --- /dev/null +++ b/pkg/helm/home.go @@ -0,0 +1,51 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" +) + +var longHomeHelp = ` +This command displays the location of HELM_HOME. This is where +any helm configuration files live. +` + +func newHomeCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "home", + Short: "displays the location of HELM_HOME", + Long: longHomeHelp, + Run: func(cmd *cobra.Command, args []string) { + h := settings.Home + fmt.Fprintln(out, h) + if settings.Debug { + fmt.Fprintf(out, "Repository: %s\n", h.Repository()) + fmt.Fprintf(out, "RepositoryFile: %s\n", h.RepositoryFile()) + fmt.Fprintf(out, "Cache: %s\n", h.Cache()) + fmt.Fprintf(out, "Stable CacheIndex: %s\n", h.CacheIndex("stable")) + fmt.Fprintf(out, "Starters: %s\n", h.Starters()) + fmt.Fprintf(out, "LocalRepository: %s\n", h.LocalRepository()) + fmt.Fprintf(out, "Plugins: %s\n", h.Plugins()) + } + }, + } + return cmd +} diff --git a/pkg/helm/init.go b/pkg/helm/init.go new file mode 100644 index 000000000..d2317df30 --- /dev/null +++ b/pkg/helm/init.go @@ -0,0 +1,493 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "time" + + "github.com/spf13/cobra" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/kubernetes" + + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/helm/cmd/helm/installer" + "k8s.io/helm/pkg/getter" + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helm/portforwarder" + "k8s.io/helm/pkg/repo" +) + +const initDesc = ` +This command installs Tiller (the Helm server-side component) onto your +Kubernetes Cluster and sets up local configuration in $HELM_HOME (default ~/.helm/). + +As with the rest of the Helm commands, 'helm init' discovers Kubernetes clusters +by reading $KUBECONFIG (default '~/.kube/config') and using the default context. + +To set up just a local environment, use '--client-only'. That will configure +$HELM_HOME, but not attempt to connect to a Kubernetes cluster and install the Tiller +deployment. + +When installing Tiller, 'helm init' will attempt to install the latest released +version. You can specify an alternative image with '--tiller-image'. For those +frequently working on the latest code, the flag '--canary-image' will install +the latest pre-release version of Tiller (e.g. the HEAD commit in the GitHub +repository on the master branch). + +To dump a manifest containing the Tiller deployment YAML, combine the +'--dry-run' and '--debug' flags. +` + +const ( + stableRepository = "stable" + localRepository = "local" + localRepositoryIndexFile = "index.yaml" +) + +var ( + stableRepositoryURL = "https://kubernetes-charts.storage.googleapis.com" + // This is the IPv4 loopback, not localhost, because we have to force IPv4 + // for Dockerized Helm: https://github.com/kubernetes/helm/issues/1410 + localRepositoryURL = "http://127.0.0.1:8879/charts" +) + +type initCmd struct { + image string + clientOnly bool + canary bool + upgrade bool + namespace string + dryRun bool + forceUpgrade bool + skipRefresh bool + out io.Writer + client helm.Interface + home helmpath.Home + opts installer.Options + kubeClient kubernetes.Interface + serviceAccount string + maxHistory int + replicas int + wait bool +} + +func newInitCmd(out io.Writer) *cobra.Command { + i := &initCmd{out: out} + + cmd := &cobra.Command{ + Use: "init", + Short: "initialize Helm on both client and server", + Long: initDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + return errors.New("This command does not accept arguments") + } + i.namespace = settings.TillerNamespace + i.home = settings.Home + i.client = ensureHelmClient(i.client) + + return i.run() + }, + } + + f := cmd.Flags() + f.StringVarP(&i.image, "tiller-image", "i", "", "override Tiller image") + f.BoolVar(&i.canary, "canary-image", false, "use the canary Tiller image") + f.BoolVar(&i.upgrade, "upgrade", false, "upgrade if Tiller is already installed") + f.BoolVar(&i.forceUpgrade, "force-upgrade", false, "force upgrade of Tiller to the current helm version") + f.BoolVarP(&i.clientOnly, "client-only", "c", false, "if set does not install Tiller") + f.BoolVar(&i.dryRun, "dry-run", false, "do not install local or remote") + f.BoolVar(&i.skipRefresh, "skip-refresh", false, "do not refresh (download) the local repository cache") + f.BoolVar(&i.wait, "wait", false, "block until Tiller is running and ready to receive requests") + + f.BoolVar(&tlsEnable, "tiller-tls", false, "install Tiller with TLS enabled") + f.BoolVar(&tlsVerify, "tiller-tls-verify", false, "install Tiller with TLS enabled and to verify remote certificates") + f.StringVar(&tlsKeyFile, "tiller-tls-key", "", "path to TLS key file to install with Tiller") + f.StringVar(&tlsCertFile, "tiller-tls-cert", "", "path to TLS certificate file to install with Tiller") + f.StringVar(&tlsCaCertFile, "tls-ca-cert", "", "path to CA root certificate") + + f.StringVar(&stableRepositoryURL, "stable-repo-url", stableRepositoryURL, "URL for stable repository") + f.StringVar(&localRepositoryURL, "local-repo-url", localRepositoryURL, "URL for local repository") + + f.BoolVar(&i.opts.EnableHostNetwork, "net-host", false, "install Tiller with net=host") + f.StringVar(&i.serviceAccount, "service-account", "", "name of service account") + f.IntVar(&i.maxHistory, "history-max", 0, "limit the maximum number of revisions saved per release. Use 0 for no limit.") + f.IntVar(&i.replicas, "replicas", 1, "amount of tiller instances to run on the cluster") + + f.StringVar(&i.opts.NodeSelectors, "node-selectors", "", "labels to specify the node on which Tiller is installed (app=tiller,helm=rocks)") + f.VarP(&i.opts.Output, "output", "o", "skip installation and output Tiller's manifest in specified format (json or yaml)") + f.StringArrayVar(&i.opts.Values, "override", []string{}, "override values for the Tiller Deployment manifest (can specify multiple or separate values with commas: key1=val1,key2=val2)") + + return cmd +} + +// tlsOptions sanitizes the tls flags as well as checks for the existence of required +// tls files indicated by those flags, if any. +func (i *initCmd) tlsOptions() error { + i.opts.EnableTLS = tlsEnable || tlsVerify + i.opts.VerifyTLS = tlsVerify + + if i.opts.EnableTLS { + missing := func(file string) bool { + _, err := os.Stat(file) + return os.IsNotExist(err) + } + if i.opts.TLSKeyFile = tlsKeyFile; i.opts.TLSKeyFile == "" || missing(i.opts.TLSKeyFile) { + return errors.New("missing required TLS key file") + } + if i.opts.TLSCertFile = tlsCertFile; i.opts.TLSCertFile == "" || missing(i.opts.TLSCertFile) { + return errors.New("missing required TLS certificate file") + } + if i.opts.VerifyTLS { + if i.opts.TLSCaCertFile = tlsCaCertFile; i.opts.TLSCaCertFile == "" || missing(i.opts.TLSCaCertFile) { + return errors.New("missing required TLS CA file") + } + } + } + return nil +} + +// run initializes local config and installs Tiller to Kubernetes cluster. +func (i *initCmd) run() error { + if err := i.tlsOptions(); err != nil { + return err + } + i.opts.Namespace = i.namespace + i.opts.UseCanary = i.canary + i.opts.ImageSpec = i.image + i.opts.ForceUpgrade = i.forceUpgrade + i.opts.ServiceAccount = i.serviceAccount + i.opts.MaxHistory = i.maxHistory + i.opts.Replicas = i.replicas + + writeYAMLManifest := func(apiVersion, kind, body string, first, last bool) error { + w := i.out + if !first { + // YAML starting document boundary marker + if _, err := fmt.Fprintln(w, "---"); err != nil { + return err + } + } + if _, err := fmt.Fprintln(w, "apiVersion:", apiVersion); err != nil { + return err + } + if _, err := fmt.Fprintln(w, "kind:", kind); err != nil { + return err + } + if _, err := fmt.Fprint(w, body); err != nil { + return err + } + if !last { + return nil + } + // YAML ending document boundary marker + _, err := fmt.Fprintln(w, "...") + return err + } + if len(i.opts.Output) > 0 { + var body string + var err error + const tm = `{"apiVersion":"extensions/v1beta1","kind":"Deployment",` + if body, err = installer.DeploymentManifest(&i.opts); err != nil { + return err + } + switch i.opts.Output.String() { + case "json": + var out bytes.Buffer + jsonb, err := yaml.ToJSON([]byte(body)) + if err != nil { + return err + } + buf := bytes.NewBuffer(make([]byte, 0, len(tm)+len(jsonb)-1)) + buf.WriteString(tm) + // Drop the opening object delimiter ('{'). + buf.Write(jsonb[1:]) + if err := json.Indent(&out, buf.Bytes(), "", " "); err != nil { + return err + } + if _, err = i.out.Write(out.Bytes()); err != nil { + return err + } + + return nil + case "yaml": + if err := writeYAMLManifest("extensions/v1beta1", "Deployment", body, true, false); err != nil { + return err + } + return nil + default: + return fmt.Errorf("unknown output format: %q", i.opts.Output) + } + } + if settings.Debug { + + var body string + var err error + + // write Deployment manifest + if body, err = installer.DeploymentManifest(&i.opts); err != nil { + return err + } + if err := writeYAMLManifest("extensions/v1beta1", "Deployment", body, true, false); err != nil { + return err + } + + // write Service manifest + if body, err = installer.ServiceManifest(i.namespace); err != nil { + return err + } + if err := writeYAMLManifest("v1", "Service", body, false, !i.opts.EnableTLS); err != nil { + return err + } + + // write Secret manifest + if i.opts.EnableTLS { + if body, err = installer.SecretManifest(&i.opts); err != nil { + return err + } + if err := writeYAMLManifest("v1", "Secret", body, false, true); err != nil { + return err + } + } + } + + if i.dryRun { + return nil + } + + if err := ensureDirectories(i.home, i.out); err != nil { + return err + } + if err := ensureDefaultRepos(i.home, i.out, i.skipRefresh); err != nil { + return err + } + if err := ensureRepoFileFormat(i.home.RepositoryFile(), i.out); err != nil { + return err + } + fmt.Fprintf(i.out, "$HELM_HOME has been configured at %s.\n", settings.Home) + + if !i.clientOnly { + if i.kubeClient == nil { + _, c, err := getKubeClient(settings.KubeContext) + if err != nil { + return fmt.Errorf("could not get kubernetes client: %s", err) + } + i.kubeClient = c + } + if err := installer.Install(i.kubeClient, &i.opts); err != nil { + if !apierrors.IsAlreadyExists(err) { + return fmt.Errorf("error installing: %s", err) + } + if i.upgrade { + if err := installer.Upgrade(i.kubeClient, &i.opts); err != nil { + return fmt.Errorf("error when upgrading: %s", err) + } + if err := i.ping(); err != nil { + return err + } + fmt.Fprintln(i.out, "\nTiller (the Helm server-side component) has been upgraded to the current version.") + } else { + fmt.Fprintln(i.out, "Warning: Tiller is already installed in the cluster.\n"+ + "(Use --client-only to suppress this message, or --upgrade to upgrade Tiller to the current version.)") + } + } else { + fmt.Fprintln(i.out, "\nTiller (the Helm server-side component) has been installed into your Kubernetes Cluster.\n\n"+ + "Please note: by default, Tiller is deployed with an insecure 'allow unauthenticated users' policy.\n"+ + "For more information on securing your installation see: https://docs.helm.sh/using_helm/#securing-your-helm-installation") + } + if err := i.ping(); err != nil { + return err + } + } else { + fmt.Fprintln(i.out, "Not installing Tiller due to 'client-only' flag having been set") + } + + fmt.Fprintln(i.out, "Happy Helming!") + return nil +} + +func (i *initCmd) ping() error { + if i.wait { + _, kubeClient, err := getKubeClient(settings.KubeContext) + if err != nil { + return err + } + if !watchTillerUntilReady(settings.TillerNamespace, kubeClient, settings.TillerConnectionTimeout) { + return fmt.Errorf("tiller was not found. polling deadline exceeded") + } + + // establish a connection to Tiller now that we've effectively guaranteed it's available + if err := setupConnection(); err != nil { + return err + } + i.client = newClient() + if err := i.client.PingTiller(); err != nil { + return fmt.Errorf("could not ping Tiller: %s", err) + } + } + + return nil +} + +// ensureDirectories checks to see if $HELM_HOME exists. +// +// If $HELM_HOME does not exist, this function will create it. +func ensureDirectories(home helmpath.Home, out io.Writer) error { + configDirectories := []string{ + home.String(), + home.Repository(), + home.Cache(), + home.LocalRepository(), + home.Plugins(), + home.Starters(), + home.Archive(), + } + for _, p := range configDirectories { + if fi, err := os.Stat(p); err != nil { + fmt.Fprintf(out, "Creating %s \n", p) + if err := os.MkdirAll(p, 0755); err != nil { + return fmt.Errorf("Could not create %s: %s", p, err) + } + } else if !fi.IsDir() { + return fmt.Errorf("%s must be a directory", p) + } + } + + return nil +} + +func ensureDefaultRepos(home helmpath.Home, out io.Writer, skipRefresh bool) error { + repoFile := home.RepositoryFile() + if fi, err := os.Stat(repoFile); err != nil { + fmt.Fprintf(out, "Creating %s \n", repoFile) + f := repo.NewRepoFile() + sr, err := initStableRepo(home.CacheIndex(stableRepository), out, skipRefresh, home) + if err != nil { + return err + } + lr, err := initLocalRepo(home.LocalRepository(localRepositoryIndexFile), home.CacheIndex("local"), out, home) + if err != nil { + return err + } + f.Add(sr) + f.Add(lr) + if err := f.WriteFile(repoFile, 0644); err != nil { + return err + } + } else if fi.IsDir() { + return fmt.Errorf("%s must be a file, not a directory", repoFile) + } + return nil +} + +func initStableRepo(cacheFile string, out io.Writer, skipRefresh bool, home helmpath.Home) (*repo.Entry, error) { + fmt.Fprintf(out, "Adding %s repo with URL: %s \n", stableRepository, stableRepositoryURL) + c := repo.Entry{ + Name: stableRepository, + URL: stableRepositoryURL, + Cache: cacheFile, + } + r, err := repo.NewChartRepository(&c, getter.All(settings)) + if err != nil { + return nil, err + } + + if skipRefresh { + return &c, nil + } + + // In this case, the cacheFile is always absolute. So passing empty string + // is safe. + if err := r.DownloadIndexFile(""); err != nil { + return nil, fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", stableRepositoryURL, err.Error()) + } + + return &c, nil +} + +func initLocalRepo(indexFile, cacheFile string, out io.Writer, home helmpath.Home) (*repo.Entry, error) { + if fi, err := os.Stat(indexFile); err != nil { + fmt.Fprintf(out, "Adding %s repo with URL: %s \n", localRepository, localRepositoryURL) + i := repo.NewIndexFile() + if err := i.WriteFile(indexFile, 0644); err != nil { + return nil, err + } + + //TODO: take this out and replace with helm update functionality + if err := createLink(indexFile, cacheFile, home); err != nil { + return nil, err + } + } else if fi.IsDir() { + return nil, fmt.Errorf("%s must be a file, not a directory", indexFile) + } + + return &repo.Entry{ + Name: localRepository, + URL: localRepositoryURL, + Cache: cacheFile, + }, nil +} + +func ensureRepoFileFormat(file string, out io.Writer) error { + r, err := repo.LoadRepositoriesFile(file) + if err == repo.ErrRepoOutOfDate { + fmt.Fprintln(out, "Updating repository file format...") + if err := r.WriteFile(file, 0644); err != nil { + return err + } + } + + return nil +} + +// watchTillerUntilReady waits for the tiller pod to become available. This is useful in situations where we +// want to wait before we call New(). +// +// Returns true if it exists. If the timeout was reached and it could not find the pod, it returns false. +func watchTillerUntilReady(namespace string, client kubernetes.Interface, timeout int64) bool { + deadlinePollingChan := time.NewTimer(time.Duration(timeout) * time.Second).C + checkTillerPodTicker := time.NewTicker(500 * time.Millisecond) + doneChan := make(chan bool) + + defer checkTillerPodTicker.Stop() + + go func() { + for range checkTillerPodTicker.C { + _, err := portforwarder.GetTillerPodName(client.CoreV1(), namespace) + if err == nil { + doneChan <- true + break + } + } + }() + + for { + select { + case <-deadlinePollingChan: + return false + case <-doneChan: + return true + } + } +} diff --git a/pkg/helm/init_test.go b/pkg/helm/init_test.go new file mode 100644 index 000000000..154417f50 --- /dev/null +++ b/pkg/helm/init_test.go @@ -0,0 +1,357 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/ghodss/yaml" + + "k8s.io/api/core/v1" + "k8s.io/api/extensions/v1beta1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + testcore "k8s.io/client-go/testing" + + "encoding/json" + + "k8s.io/helm/cmd/helm/installer" + "k8s.io/helm/pkg/helm/helmpath" +) + +func TestInitCmd(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(home) + + var buf bytes.Buffer + fc := fake.NewSimpleClientset() + cmd := &initCmd{ + out: &buf, + home: helmpath.Home(home), + kubeClient: fc, + namespace: v1.NamespaceDefault, + } + if err := cmd.run(); err != nil { + t.Errorf("expected error: %v", err) + } + actions := fc.Actions() + if len(actions) != 2 { + t.Errorf("Expected 2 actions, got %d", len(actions)) + } + if !actions[0].Matches("create", "deployments") { + t.Errorf("unexpected action: %v, expected create deployment", actions[0]) + } + if !actions[1].Matches("create", "services") { + t.Errorf("unexpected action: %v, expected create service", actions[1]) + } + expected := "Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster." + if !strings.Contains(buf.String(), expected) { + t.Errorf("expected %q, got %q", expected, buf.String()) + } +} + +func TestInitCmd_exists(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(home) + + var buf bytes.Buffer + fc := fake.NewSimpleClientset(&v1beta1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "tiller-deploy", + }, + }) + fc.PrependReactor("*", "*", func(action testcore.Action) (bool, runtime.Object, error) { + return true, nil, apierrors.NewAlreadyExists(v1.Resource("deployments"), "1") + }) + cmd := &initCmd{ + out: &buf, + home: helmpath.Home(home), + kubeClient: fc, + namespace: v1.NamespaceDefault, + } + if err := cmd.run(); err != nil { + t.Errorf("expected error: %v", err) + } + expected := "Warning: Tiller is already installed in the cluster.\n" + + "(Use --client-only to suppress this message, or --upgrade to upgrade Tiller to the current version.)" + if !strings.Contains(buf.String(), expected) { + t.Errorf("expected %q, got %q", expected, buf.String()) + } +} + +func TestInitCmd_clientOnly(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(home) + + var buf bytes.Buffer + fc := fake.NewSimpleClientset() + cmd := &initCmd{ + out: &buf, + home: helmpath.Home(home), + kubeClient: fc, + clientOnly: true, + namespace: v1.NamespaceDefault, + } + if err := cmd.run(); err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(fc.Actions()) != 0 { + t.Error("expected client call") + } + expected := "Not installing Tiller due to 'client-only' flag having been set" + if !strings.Contains(buf.String(), expected) { + t.Errorf("expected %q, got %q", expected, buf.String()) + } +} + +func TestInitCmd_dryRun(t *testing.T) { + // This is purely defensive in this case. + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + cleanup := resetEnv() + defer func() { + os.Remove(home) + cleanup() + }() + + settings.Debug = true + + var buf bytes.Buffer + fc := fake.NewSimpleClientset() + cmd := &initCmd{ + out: &buf, + home: helmpath.Home(home), + kubeClient: fc, + clientOnly: true, + dryRun: true, + namespace: v1.NamespaceDefault, + } + if err := cmd.run(); err != nil { + t.Fatal(err) + } + if got := len(fc.Actions()); got != 0 { + t.Errorf("expected no server calls, got %d", got) + } + + docs := bytes.Split(buf.Bytes(), []byte("\n---")) + if got, want := len(docs), 2; got != want { + t.Fatalf("Expected document count of %d, got %d", want, got) + } + for _, doc := range docs { + var y map[string]interface{} + if err := yaml.Unmarshal(doc, &y); err != nil { + t.Errorf("Expected parseable YAML, got %q\n\t%s", doc, err) + } + } +} + +func TestEnsureHome(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(home) + + b := bytes.NewBuffer(nil) + hh := helmpath.Home(home) + settings.Home = hh + if err := ensureDirectories(hh, b); err != nil { + t.Error(err) + } + if err := ensureDefaultRepos(hh, b, false); err != nil { + t.Error(err) + } + if err := ensureDefaultRepos(hh, b, true); err != nil { + t.Error(err) + } + if err := ensureRepoFileFormat(hh.RepositoryFile(), b); err != nil { + t.Error(err) + } + + expectedDirs := []string{hh.String(), hh.Repository(), hh.Cache(), hh.LocalRepository()} + for _, dir := range expectedDirs { + if fi, err := os.Stat(dir); err != nil { + t.Errorf("%s", err) + } else if !fi.IsDir() { + t.Errorf("%s is not a directory", fi) + } + } + + if fi, err := os.Stat(hh.RepositoryFile()); err != nil { + t.Error(err) + } else if fi.IsDir() { + t.Errorf("%s should not be a directory", fi) + } + + if fi, err := os.Stat(hh.LocalRepository(localRepositoryIndexFile)); err != nil { + t.Errorf("%s", err) + } else if fi.IsDir() { + t.Errorf("%s should not be a directory", fi) + } +} + +func TestInitCmd_tlsOptions(t *testing.T) { + const testDir = "../../testdata" + + // tls certificates in testDir + var ( + testCaCertFile = filepath.Join(testDir, "ca.pem") + testCertFile = filepath.Join(testDir, "crt.pem") + testKeyFile = filepath.Join(testDir, "key.pem") + ) + + // these tests verify the effects of permuting the "--tls" and "--tls-verify" flags + // and the install options yieled as a result of (*initCmd).tlsOptions() + // during helm init. + var tests = []struct { + certFile string + keyFile string + caFile string + enable bool + verify bool + describe string + }{ + { // --tls and --tls-verify specified (--tls=true,--tls-verify=true) + certFile: testCertFile, + keyFile: testKeyFile, + caFile: testCaCertFile, + enable: true, + verify: true, + describe: "--tls and --tls-verify specified (--tls=true,--tls-verify=true)", + }, + { // --tls-verify implies --tls (--tls=false,--tls-verify=true) + certFile: testCertFile, + keyFile: testKeyFile, + caFile: testCaCertFile, + enable: false, + verify: true, + describe: "--tls-verify implies --tls (--tls=false,--tls-verify=true)", + }, + { // no --tls-verify (--tls=true,--tls-verify=false) + certFile: testCertFile, + keyFile: testKeyFile, + caFile: "", + enable: true, + verify: false, + describe: "no --tls-verify (--tls=true,--tls-verify=false)", + }, + { // tls is disabled (--tls=false,--tls-verify=false) + certFile: "", + keyFile: "", + caFile: "", + enable: false, + verify: false, + describe: "tls is disabled (--tls=false,--tls-verify=false)", + }, + } + + for _, tt := range tests { + // emulate tls file specific flags + tlsCaCertFile, tlsCertFile, tlsKeyFile = tt.caFile, tt.certFile, tt.keyFile + + // emulate tls enable/verify flags + tlsEnable, tlsVerify = tt.enable, tt.verify + + cmd := &initCmd{} + if err := cmd.tlsOptions(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // expected result options + expect := installer.Options{ + TLSCaCertFile: tt.caFile, + TLSCertFile: tt.certFile, + TLSKeyFile: tt.keyFile, + VerifyTLS: tt.verify, + EnableTLS: tt.enable || tt.verify, + } + + if !reflect.DeepEqual(cmd.opts, expect) { + t.Errorf("%s: got %#+v, want %#+v", tt.describe, cmd.opts, expect) + } + } +} + +// TestInitCmd_output tests that init -o formats are unmarshal-able +func TestInitCmd_output(t *testing.T) { + // This is purely defensive in this case. + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + dbg := settings.Debug + settings.Debug = true + defer func() { + os.Remove(home) + settings.Debug = dbg + }() + fc := fake.NewSimpleClientset() + tests := []struct { + expectF func([]byte, interface{}) error + expectName string + }{ + { + json.Unmarshal, + "json", + }, + { + yaml.Unmarshal, + "yaml", + }, + } + for _, s := range tests { + var buf bytes.Buffer + cmd := &initCmd{ + out: &buf, + home: helmpath.Home(home), + kubeClient: fc, + opts: installer.Options{Output: installer.OutputFormat(s.expectName)}, + namespace: v1.NamespaceDefault, + } + if err := cmd.run(); err != nil { + t.Fatal(err) + } + if got := len(fc.Actions()); got != 0 { + t.Errorf("expected no server calls, got %d", got) + } + d := &v1beta1.Deployment{} + if err = s.expectF(buf.Bytes(), &d); err != nil { + t.Errorf("error unmarshalling init %s output %s %s", s.expectName, err, buf.String()) + } + } + +} diff --git a/pkg/helm/init_unix.go b/pkg/helm/init_unix.go new file mode 100644 index 000000000..bdd795fb3 --- /dev/null +++ b/pkg/helm/init_unix.go @@ -0,0 +1,29 @@ +// +build !windows + +/* +Copyright 2017 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "os" + + "k8s.io/helm/pkg/helm/helmpath" +) + +func createLink(indexFile, cacheFile string, home helmpath.Home) error { + return os.Symlink(indexFile, cacheFile) +} diff --git a/pkg/helm/init_windows.go b/pkg/helm/init_windows.go new file mode 100644 index 000000000..2b18516d6 --- /dev/null +++ b/pkg/helm/init_windows.go @@ -0,0 +1,29 @@ +// +build windows + +/* +Copyright 2017 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "os" + + "k8s.io/helm/pkg/helm/helmpath" +) + +func createLink(indexFile, cacheFile string, home helmpath.Home) error { + return os.Link(indexFile, cacheFile) +} diff --git a/pkg/helm/inspect.go b/pkg/helm/inspect.go new file mode 100644 index 000000000..14023de26 --- /dev/null +++ b/pkg/helm/inspect.go @@ -0,0 +1,264 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "strings" + + "github.com/ghodss/yaml" + "github.com/golang/protobuf/ptypes/any" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/kubernetes/pkg/util/slice" +) + +const inspectDesc = ` +This command inspects a chart and displays information. It takes a chart reference +('stable/drupal'), a full path to a directory or packaged chart, or a URL. + +Inspect prints the contents of the Chart.yaml file and the values.yaml file. +` + +const inspectValuesDesc = ` +This command inspects a chart (directory, file, or URL) and displays the contents +of the values.yaml file +` + +const inspectChartDesc = ` +This command inspects a chart (directory, file, or URL) and displays the contents +of the Charts.yaml file +` + +const readmeChartDesc = ` +This command inspects a chart (directory, file, or URL) and displays the contents +of the README file +` + +type inspectCmd struct { + chartpath string + output string + verify bool + keyring string + out io.Writer + version string + repoURL string + username string + password string + + certFile string + keyFile string + caFile string +} + +const ( + chartOnly = "chart" + valuesOnly = "values" + readmeOnly = "readme" + all = "all" +) + +var readmeFileNames = []string{"readme.md", "readme.txt", "readme"} + +func newInspectCmd(out io.Writer) *cobra.Command { + insp := &inspectCmd{ + out: out, + output: all, + } + + inspectCommand := &cobra.Command{ + Use: "inspect [CHART]", + Short: "inspect a chart", + Long: inspectDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkArgsLength(len(args), "chart name"); err != nil { + return err + } + cp, err := locateChartPath(insp.repoURL, insp.username, insp.password, args[0], insp.version, insp.verify, insp.keyring, + insp.certFile, insp.keyFile, insp.caFile) + if err != nil { + return err + } + insp.chartpath = cp + return insp.run() + }, + } + + valuesSubCmd := &cobra.Command{ + Use: "values [CHART]", + Short: "shows inspect values", + Long: inspectValuesDesc, + RunE: func(cmd *cobra.Command, args []string) error { + insp.output = valuesOnly + if err := checkArgsLength(len(args), "chart name"); err != nil { + return err + } + cp, err := locateChartPath(insp.repoURL, insp.username, insp.password, args[0], insp.version, insp.verify, insp.keyring, + insp.certFile, insp.keyFile, insp.caFile) + if err != nil { + return err + } + insp.chartpath = cp + return insp.run() + }, + } + + chartSubCmd := &cobra.Command{ + Use: "chart [CHART]", + Short: "shows inspect chart", + Long: inspectChartDesc, + RunE: func(cmd *cobra.Command, args []string) error { + insp.output = chartOnly + if err := checkArgsLength(len(args), "chart name"); err != nil { + return err + } + cp, err := locateChartPath(insp.repoURL, insp.username, insp.password, args[0], insp.version, insp.verify, insp.keyring, + insp.certFile, insp.keyFile, insp.caFile) + if err != nil { + return err + } + insp.chartpath = cp + return insp.run() + }, + } + + readmeSubCmd := &cobra.Command{ + Use: "readme [CHART]", + Short: "shows inspect readme", + Long: readmeChartDesc, + RunE: func(cmd *cobra.Command, args []string) error { + insp.output = readmeOnly + if err := checkArgsLength(len(args), "chart name"); err != nil { + return err + } + cp, err := locateChartPath(insp.repoURL, insp.username, insp.password, args[0], insp.version, insp.verify, insp.keyring, + insp.certFile, insp.keyFile, insp.caFile) + if err != nil { + return err + } + insp.chartpath = cp + return insp.run() + }, + } + + cmds := []*cobra.Command{inspectCommand, readmeSubCmd, valuesSubCmd, chartSubCmd} + vflag := "verify" + vdesc := "verify the provenance data for this chart" + for _, subCmd := range cmds { + subCmd.Flags().BoolVar(&insp.verify, vflag, false, vdesc) + } + + kflag := "keyring" + kdesc := "path to the keyring containing public verification keys" + kdefault := defaultKeyring() + for _, subCmd := range cmds { + subCmd.Flags().StringVar(&insp.keyring, kflag, kdefault, kdesc) + } + + verflag := "version" + verdesc := "version of the chart. By default, the newest chart is shown" + for _, subCmd := range cmds { + subCmd.Flags().StringVar(&insp.version, verflag, "", verdesc) + } + + repoURL := "repo" + repoURLdesc := "chart repository url where to locate the requested chart" + for _, subCmd := range cmds { + subCmd.Flags().StringVar(&insp.repoURL, repoURL, "", repoURLdesc) + } + + username := "username" + usernamedesc := "chart repository username where to locate the requested chart" + inspectCommand.Flags().StringVar(&insp.username, username, "", usernamedesc) + valuesSubCmd.Flags().StringVar(&insp.username, username, "", usernamedesc) + chartSubCmd.Flags().StringVar(&insp.username, username, "", usernamedesc) + + password := "password" + passworddesc := "chart repository password where to locate the requested chart" + inspectCommand.Flags().StringVar(&insp.password, password, "", passworddesc) + valuesSubCmd.Flags().StringVar(&insp.password, password, "", passworddesc) + chartSubCmd.Flags().StringVar(&insp.password, password, "", passworddesc) + + certFile := "cert-file" + certFiledesc := "verify certificates of HTTPS-enabled servers using this CA bundle" + for _, subCmd := range cmds { + subCmd.Flags().StringVar(&insp.certFile, certFile, "", certFiledesc) + } + + keyFile := "key-file" + keyFiledesc := "identify HTTPS client using this SSL key file" + for _, subCmd := range cmds { + subCmd.Flags().StringVar(&insp.keyFile, keyFile, "", keyFiledesc) + } + + caFile := "ca-file" + caFiledesc := "chart repository url where to locate the requested chart" + for _, subCmd := range cmds { + subCmd.Flags().StringVar(&insp.caFile, caFile, "", caFiledesc) + } + + for _, subCmd := range cmds[1:] { + inspectCommand.AddCommand(subCmd) + } + + return inspectCommand +} + +func (i *inspectCmd) run() error { + chrt, err := chartutil.Load(i.chartpath) + if err != nil { + return err + } + cf, err := yaml.Marshal(chrt.Metadata) + if err != nil { + return err + } + + if i.output == chartOnly || i.output == all { + fmt.Fprintln(i.out, string(cf)) + } + + if (i.output == valuesOnly || i.output == all) && chrt.Values != nil { + if i.output == all { + fmt.Fprintln(i.out, "---") + } + fmt.Fprintln(i.out, chrt.Values.Raw) + } + + if i.output == readmeOnly || i.output == all { + if i.output == all { + fmt.Fprintln(i.out, "---") + } + readme := findReadme(chrt.Files) + if readme == nil { + return nil + } + fmt.Fprintln(i.out, string(readme.Value)) + } + return nil +} + +func findReadme(files []*any.Any) (file *any.Any) { + for _, file := range files { + if slice.ContainsString(readmeFileNames, strings.ToLower(file.TypeUrl), nil) { + return file + } + } + return nil +} diff --git a/pkg/helm/inspect_test.go b/pkg/helm/inspect_test.go new file mode 100644 index 000000000..40418c1d2 --- /dev/null +++ b/pkg/helm/inspect_test.go @@ -0,0 +1,80 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "io/ioutil" + "strings" + "testing" +) + +func TestInspect(t *testing.T) { + b := bytes.NewBuffer(nil) + + insp := &inspectCmd{ + chartpath: "testdata/testcharts/alpine", + output: all, + out: b, + } + insp.run() + + // Load the data from the textfixture directly. + cdata, err := ioutil.ReadFile("testdata/testcharts/alpine/Chart.yaml") + if err != nil { + t.Fatal(err) + } + data, err := ioutil.ReadFile("testdata/testcharts/alpine/values.yaml") + if err != nil { + t.Fatal(err) + } + readmeData, err := ioutil.ReadFile("testdata/testcharts/alpine/README.md") + if err != nil { + t.Fatal(err) + } + parts := strings.SplitN(b.String(), "---", 3) + if len(parts) != 3 { + t.Fatalf("Expected 2 parts, got %d", len(parts)) + } + + expect := []string{ + strings.Replace(strings.TrimSpace(string(cdata)), "\r", "", -1), + strings.Replace(strings.TrimSpace(string(data)), "\r", "", -1), + strings.Replace(strings.TrimSpace(string(readmeData)), "\r", "", -1), + } + + // Problem: ghodss/yaml doesn't marshal into struct order. To solve, we + // have to carefully craft the Chart.yaml to match. + for i, got := range parts { + got = strings.Replace(strings.TrimSpace(got), "\r", "", -1) + if got != expect[i] { + t.Errorf("Expected\n%q\nGot\n%q\n", expect[i], got) + } + } + + // Regression tests for missing values. See issue #1024. + b.Reset() + insp = &inspectCmd{ + chartpath: "testdata/testcharts/novals", + output: "values", + out: b, + } + insp.run() + if b.Len() != 0 { + t.Errorf("expected empty values buffer, got %q", b.String()) + } +} diff --git a/pkg/helm/install.go b/pkg/helm/install.go new file mode 100644 index 000000000..9a57a14fe --- /dev/null +++ b/pkg/helm/install.go @@ -0,0 +1,530 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/Masterminds/sprig" + "github.com/ghodss/yaml" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/downloader" + "k8s.io/helm/pkg/getter" + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/kube" + "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/repo" + "k8s.io/helm/pkg/strvals" +) + +const installDesc = ` +This command installs a chart archive. + +The install argument must be a chart reference, a path to a packaged chart, +a path to an unpacked chart directory or a URL. + +To override values in a chart, use either the '--values' flag and pass in a file +or use the '--set' flag and pass configuration from the command line, to force +a string value use '--set-string'. + + $ helm install -f myvalues.yaml ./redis + +or + + $ helm install --set name=prod ./redis + +or + + $ helm install --set-string long_int=1234567890 ./redis + +You can specify the '--values'/'-f' flag multiple times. The priority will be given to the +last (right-most) file specified. For example, if both myvalues.yaml and override.yaml +contained a key called 'Test', the value set in override.yaml would take precedence: + + $ helm install -f myvalues.yaml -f override.yaml ./redis + +You can specify the '--set' flag multiple times. The priority will be given to the +last (right-most) set specified. For example, if both 'bar' and 'newbar' values are +set for a key called 'foo', the 'newbar' value would take precedence: + + $ helm install --set foo=bar --set foo=newbar ./redis + + +To check the generated manifests of a release without installing the chart, +the '--debug' and '--dry-run' flags can be combined. This will still require a +round-trip to the Tiller server. + +If --verify is set, the chart MUST have a provenance file, and the provenance +file MUST pass all verification steps. + +There are five different ways you can express the chart you want to install: + +1. By chart reference: helm install stable/mariadb +2. By path to a packaged chart: helm install ./nginx-1.2.3.tgz +3. By path to an unpacked chart directory: helm install ./nginx +4. By absolute URL: helm install https://example.com/charts/nginx-1.2.3.tgz +5. By chart reference and repo url: helm install --repo https://example.com/charts/ nginx + +CHART REFERENCES + +A chart reference is a convenient way of reference a chart in a chart repository. + +When you use a chart reference with a repo prefix ('stable/mariadb'), Helm will look in the local +configuration for a chart repository named 'stable', and will then look for a +chart in that repository whose name is 'mariadb'. It will install the latest +version of that chart unless you also supply a version number with the +'--version' flag. + +To see the list of chart repositories, use 'helm repo list'. To search for +charts in a repository, use 'helm search'. +` + +type installCmd struct { + name string + namespace string + valueFiles valueFiles + chartPath string + dryRun bool + disableHooks bool + replace bool + verify bool + keyring string + out io.Writer + client helm.Interface + values []string + stringValues []string + nameTemplate string + version string + timeout int64 + wait bool + repoURL string + username string + password string + devel bool + depUp bool + + certFile string + keyFile string + caFile string +} + +type valueFiles []string + +func (v *valueFiles) String() string { + return fmt.Sprint(*v) +} + +func (v *valueFiles) Type() string { + return "valueFiles" +} + +func (v *valueFiles) Set(value string) error { + for _, filePath := range strings.Split(value, ",") { + *v = append(*v, filePath) + } + return nil +} + +func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { + inst := &installCmd{ + out: out, + client: c, + } + + cmd := &cobra.Command{ + Use: "install [CHART]", + Short: "install a chart archive", + Long: installDesc, + PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkArgsLength(len(args), "chart name"); err != nil { + return err + } + + debug("Original chart version: %q", inst.version) + if inst.version == "" && inst.devel { + debug("setting version to >0.0.0-0") + inst.version = ">0.0.0-0" + } + + cp, err := locateChartPath(inst.repoURL, inst.username, inst.password, args[0], inst.version, inst.verify, inst.keyring, + inst.certFile, inst.keyFile, inst.caFile) + if err != nil { + return err + } + inst.chartPath = cp + inst.client = ensureHelmClient(inst.client) + return inst.run() + }, + } + + f := cmd.Flags() + f.VarP(&inst.valueFiles, "values", "f", "specify values in a YAML file or a URL(can specify multiple)") + f.StringVarP(&inst.name, "name", "n", "", "release name. If unspecified, it will autogenerate one for you") + f.StringVar(&inst.namespace, "namespace", "", "namespace to install the release into. Defaults to the current kube config namespace.") + f.BoolVar(&inst.dryRun, "dry-run", false, "simulate an install") + f.BoolVar(&inst.disableHooks, "no-hooks", false, "prevent hooks from running during install") + f.BoolVar(&inst.replace, "replace", false, "re-use the given name, even if that name is already used. This is unsafe in production") + f.StringArrayVar(&inst.values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + f.StringArrayVar(&inst.stringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + f.StringVar(&inst.nameTemplate, "name-template", "", "specify template used to name the release") + f.BoolVar(&inst.verify, "verify", false, "verify the package before installing it") + f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "location of public keys used for verification") + f.StringVar(&inst.version, "version", "", "specify the exact chart version to install. If this is not specified, the latest version is installed") + f.Int64Var(&inst.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") + f.BoolVar(&inst.wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout") + f.StringVar(&inst.repoURL, "repo", "", "chart repository url where to locate the requested chart") + f.StringVar(&inst.username, "username", "", "chart repository username where to locate the requested chart") + f.StringVar(&inst.password, "password", "", "chart repository password where to locate the requested chart") + f.StringVar(&inst.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&inst.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.StringVar(&inst.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") + f.BoolVar(&inst.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") + f.BoolVar(&inst.depUp, "dep-up", false, "run helm dependency update before installing the chart") + + return cmd +} + +func (i *installCmd) run() error { + debug("CHART PATH: %s\n", i.chartPath) + + if i.namespace == "" { + i.namespace = defaultNamespace() + } + + rawVals, err := vals(i.valueFiles, i.values, i.stringValues) + if err != nil { + return err + } + + // If template is specified, try to run the template. + if i.nameTemplate != "" { + i.name, err = generateName(i.nameTemplate) + if err != nil { + return err + } + // Print the final name so the user knows what the final name of the release is. + fmt.Printf("FINAL NAME: %s\n", i.name) + } + + // Check chart requirements to make sure all dependencies are present in /charts + chartRequested, err := chartutil.Load(i.chartPath) + if err != nil { + return prettyError(err) + } + + if req, err := chartutil.LoadRequirements(chartRequested); err == nil { + // If checkDependencies returns an error, we have unfulfilled dependencies. + // As of Helm 2.4.0, this is treated as a stopping condition: + // https://github.com/kubernetes/helm/issues/2209 + if err := checkDependencies(chartRequested, req); err != nil { + if i.depUp { + man := &downloader.Manager{ + Out: i.out, + ChartPath: i.chartPath, + HelmHome: settings.Home, + Keyring: defaultKeyring(), + SkipUpdate: false, + Getters: getter.All(settings), + } + if err := man.Update(); err != nil { + return prettyError(err) + } + } else { + return prettyError(err) + } + + } + } else if err != chartutil.ErrRequirementsNotFound { + return fmt.Errorf("cannot load requirements: %v", err) + } + + res, err := i.client.InstallReleaseFromChart( + chartRequested, + i.namespace, + helm.ValueOverrides(rawVals), + helm.ReleaseName(i.name), + helm.InstallDryRun(i.dryRun), + helm.InstallReuseName(i.replace), + helm.InstallDisableHooks(i.disableHooks), + helm.InstallTimeout(i.timeout), + helm.InstallWait(i.wait)) + if err != nil { + return prettyError(err) + } + + rel := res.GetRelease() + if rel == nil { + return nil + } + i.printRelease(rel) + + // If this is a dry run, we can't display status. + if i.dryRun { + return nil + } + + // Print the status like status command does + status, err := i.client.ReleaseStatus(rel.Name) + if err != nil { + return prettyError(err) + } + PrintStatus(i.out, status) + return nil +} + +// Merges source and destination map, preferring values from the source map +func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} { + for k, v := range src { + // If the key doesn't exist already, then just set the key to that value + if _, exists := dest[k]; !exists { + dest[k] = v + continue + } + nextMap, ok := v.(map[string]interface{}) + // If it isn't another map, overwrite the value + if !ok { + dest[k] = v + continue + } + // If the key doesn't exist already, then just set the key to that value + if _, exists := dest[k]; !exists { + dest[k] = nextMap + continue + } + // Edge case: If the key exists in the destination, but isn't a map + destMap, isMap := dest[k].(map[string]interface{}) + // If the source map has a map for this key, prefer it + if !isMap { + dest[k] = v + continue + } + // If we got to this point, it is a map in both, so merge them + dest[k] = mergeValues(destMap, nextMap) + } + return dest +} + +// vals merges values from files specified via -f/--values and +// directly via --set or --set-string, marshaling them to YAML +func vals(valueFiles valueFiles, values []string, stringValues []string) ([]byte, error) { + base := map[string]interface{}{} + + // User specified a values files via -f/--values + for _, filePath := range valueFiles { + currentMap := map[string]interface{}{} + + var bytes []byte + var err error + if strings.TrimSpace(filePath) == "-" { + bytes, err = ioutil.ReadAll(os.Stdin) + } else { + bytes, err = readFile(filePath) + } + + if err != nil { + return []byte{}, err + } + + if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { + return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err) + } + // Merge with the previous map + base = mergeValues(base, currentMap) + } + + // User specified a value via --set + for _, value := range values { + if err := strvals.ParseInto(value, base); err != nil { + return []byte{}, fmt.Errorf("failed parsing --set data: %s", err) + } + } + + // User specified a value via --set-string + for _, value := range stringValues { + if err := strvals.ParseIntoString(value, base); err != nil { + return []byte{}, fmt.Errorf("failed parsing --set-string data: %s", err) + } + } + + return yaml.Marshal(base) +} + +// printRelease prints info about a release if the Debug is true. +func (i *installCmd) printRelease(rel *release.Release) { + if rel == nil { + return + } + // TODO: Switch to text/template like everything else. + fmt.Fprintf(i.out, "NAME: %s\n", rel.Name) + if settings.Debug { + printRelease(i.out, rel) + } +} + +// locateChartPath looks for a chart directory in known places, and returns either the full path or an error. +// +// This does not ensure that the chart is well-formed; only that the requested filename exists. +// +// Order of resolution: +// - current working directory +// - if path is absolute or begins with '.', error out here +// - chart repos in $HELM_HOME +// - URL +// +// If 'verify' is true, this will attempt to also verify the chart. +func locateChartPath(repoURL, username, password, name, version string, verify bool, keyring, + certFile, keyFile, caFile string) (string, error) { + name = strings.TrimSpace(name) + version = strings.TrimSpace(version) + if fi, err := os.Stat(name); err == nil { + abs, err := filepath.Abs(name) + if err != nil { + return abs, err + } + if verify { + if fi.IsDir() { + return "", errors.New("cannot verify a directory") + } + if _, err := downloader.VerifyChart(abs, keyring); err != nil { + return "", err + } + } + return abs, nil + } + if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { + return name, fmt.Errorf("path %q not found", name) + } + + crepo := filepath.Join(settings.Home.Repository(), name) + if _, err := os.Stat(crepo); err == nil { + return filepath.Abs(crepo) + } + + dl := downloader.ChartDownloader{ + HelmHome: settings.Home, + Out: os.Stdout, + Keyring: keyring, + Getters: getter.All(settings), + Username: username, + Password: password, + } + if verify { + dl.Verify = downloader.VerifyAlways + } + if repoURL != "" { + chartURL, err := repo.FindChartInAuthRepoURL(repoURL, username, password, name, version, + certFile, keyFile, caFile, getter.All(settings)) + if err != nil { + return "", err + } + name = chartURL + } + + if _, err := os.Stat(settings.Home.Archive()); os.IsNotExist(err) { + os.MkdirAll(settings.Home.Archive(), 0744) + } + + filename, _, err := dl.DownloadTo(name, version, settings.Home.Archive()) + if err == nil { + lname, err := filepath.Abs(filename) + if err != nil { + return filename, err + } + debug("Fetched %s to %s\n", name, filename) + return lname, nil + } else if settings.Debug { + return filename, err + } + + return filename, fmt.Errorf("failed to download %q (hint: running `helm repo update` may help)", name) +} + +func generateName(nameTemplate string) (string, error) { + t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate) + if err != nil { + return "", err + } + var b bytes.Buffer + err = t.Execute(&b, nil) + if err != nil { + return "", err + } + return b.String(), nil +} + +func defaultNamespace() string { + if ns, _, err := kube.GetConfig(settings.KubeContext).Namespace(); err == nil { + return ns + } + return "default" +} + +func checkDependencies(ch *chart.Chart, reqs *chartutil.Requirements) error { + missing := []string{} + + deps := ch.GetDependencies() + for _, r := range reqs.Dependencies { + found := false + for _, d := range deps { + if d.Metadata.Name == r.Name { + found = true + break + } + } + if !found { + missing = append(missing, r.Name) + } + } + + if len(missing) > 0 { + return fmt.Errorf("found in requirements.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", ")) + } + return nil +} + +//readFile load a file from the local directory or a remote file with a url. +func readFile(filePath string) ([]byte, error) { + u, _ := url.Parse(filePath) + p := getter.All(settings) + + // FIXME: maybe someone handle other protocols like ftp. + getterConstructor, err := p.ByScheme(u.Scheme) + + if err != nil { + return ioutil.ReadFile(filePath) + } + + getter, err := getterConstructor(filePath, "", "", "") + if err != nil { + return []byte{}, err + } + data, err := getter.Get(filePath) + return data.Bytes(), err +} diff --git a/pkg/helm/install_test.go b/pkg/helm/install_test.go new file mode 100644 index 000000000..063a4ea0e --- /dev/null +++ b/pkg/helm/install_test.go @@ -0,0 +1,283 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + "reflect" + "regexp" + "strings" + "testing" + + "github.com/spf13/cobra" + "k8s.io/helm/pkg/helm" +) + +func TestInstall(t *testing.T) { + tests := []releaseCase{ + // Install, base case + { + name: "basic install", + args: []string{"testdata/testcharts/alpine"}, + flags: strings.Split("--name aeneas", " "), + expected: "aeneas", + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), + }, + // Install, no hooks + { + name: "install without hooks", + args: []string{"testdata/testcharts/alpine"}, + flags: strings.Split("--name aeneas --no-hooks", " "), + expected: "aeneas", + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), + }, + // Install, values from cli + { + name: "install with values", + args: []string{"testdata/testcharts/alpine"}, + flags: strings.Split("--name virgil --set foo=bar", " "), + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "virgil"}), + expected: "virgil", + }, + // Install, values from cli via multiple --set + { + name: "install with multiple values", + args: []string{"testdata/testcharts/alpine"}, + flags: strings.Split("--name virgil --set foo=bar --set bar=foo", " "), + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "virgil"}), + expected: "virgil", + }, + // Install, values from yaml + { + name: "install with values", + args: []string{"testdata/testcharts/alpine"}, + flags: strings.Split("--name virgil -f testdata/testcharts/alpine/extra_values.yaml", " "), + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "virgil"}), + expected: "virgil", + }, + // Install, values from multiple yaml + { + name: "install with values", + args: []string{"testdata/testcharts/alpine"}, + flags: strings.Split("--name virgil -f testdata/testcharts/alpine/extra_values.yaml -f testdata/testcharts/alpine/more_values.yaml", " "), + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "virgil"}), + expected: "virgil", + }, + // Install, no charts + { + name: "install with no chart specified", + args: []string{}, + err: true, + }, + // Install, re-use name + { + name: "install and replace release", + args: []string{"testdata/testcharts/alpine"}, + flags: strings.Split("--name aeneas --replace", " "), + expected: "aeneas", + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), + }, + // Install, with timeout + { + name: "install with a timeout", + args: []string{"testdata/testcharts/alpine"}, + flags: strings.Split("--name foobar --timeout 120", " "), + expected: "foobar", + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "foobar"}), + }, + // Install, with wait + { + name: "install with a wait", + args: []string{"testdata/testcharts/alpine"}, + flags: strings.Split("--name apollo --wait", " "), + expected: "apollo", + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "apollo"}), + }, + // Install, using the name-template + { + name: "install with name-template", + args: []string{"testdata/testcharts/alpine"}, + flags: []string{"--name-template", "{{upper \"foobar\"}}"}, + expected: "FOOBAR", + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "FOOBAR"}), + }, + // Install, perform chart verification along the way. + { + name: "install with verification, missing provenance", + args: []string{"testdata/testcharts/compressedchart-0.1.0.tgz"}, + flags: strings.Split("--verify --keyring testdata/helm-test-key.pub", " "), + err: true, + }, + { + name: "install with verification, directory instead of file", + args: []string{"testdata/testcharts/signtest"}, + flags: strings.Split("--verify --keyring testdata/helm-test-key.pub", " "), + err: true, + }, + { + name: "install with verification, valid", + args: []string{"testdata/testcharts/signtest-0.1.0.tgz"}, + flags: strings.Split("--verify --keyring testdata/helm-test-key.pub", " "), + }, + // Install, chart with missing dependencies in /charts + { + name: "install chart with missing dependencies", + args: []string{"testdata/testcharts/chart-missing-deps"}, + err: true, + }, + // Install, chart with bad requirements.yaml in /charts + { + name: "install chart with bad requirements.yaml", + args: []string{"testdata/testcharts/chart-bad-requirements"}, + err: true, + }, + } + + runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newInstallCmd(c, out) + }) +} + +type nameTemplateTestCase struct { + tpl string + expected string + expectedErrorStr string +} + +func TestNameTemplate(t *testing.T) { + testCases := []nameTemplateTestCase{ + // Just a straight up nop please + { + tpl: "foobar", + expected: "foobar", + expectedErrorStr: "", + }, + // Random numbers at the end for fun & profit + { + tpl: "foobar-{{randNumeric 6}}", + expected: "foobar-[0-9]{6}$", + expectedErrorStr: "", + }, + // Random numbers in the middle for fun & profit + { + tpl: "foobar-{{randNumeric 4}}-baz", + expected: "foobar-[0-9]{4}-baz$", + expectedErrorStr: "", + }, + // No such function + { + tpl: "foobar-{{randInt}}", + expected: "", + expectedErrorStr: "function \"randInt\" not defined", + }, + // Invalid template + { + tpl: "foobar-{{", + expected: "", + expectedErrorStr: "unexpected unclosed action", + }, + } + + for _, tc := range testCases { + + n, err := generateName(tc.tpl) + if err != nil { + if tc.expectedErrorStr == "" { + t.Errorf("Was not expecting error, but got: %v", err) + continue + } + re, compErr := regexp.Compile(tc.expectedErrorStr) + if compErr != nil { + t.Errorf("Expected error string failed to compile: %v", compErr) + continue + } + if !re.MatchString(err.Error()) { + t.Errorf("Error didn't match for %s expected %s but got %v", tc.tpl, tc.expectedErrorStr, err) + continue + } + } + if err == nil && tc.expectedErrorStr != "" { + t.Errorf("Was expecting error %s but didn't get an error back", tc.expectedErrorStr) + } + + if tc.expected != "" { + re, err := regexp.Compile(tc.expected) + if err != nil { + t.Errorf("Expected string failed to compile: %v", err) + continue + } + if !re.MatchString(n) { + t.Errorf("Returned name didn't match for %s expected %s but got %s", tc.tpl, tc.expected, n) + } + } + } +} + +func TestMergeValues(t *testing.T) { + nestedMap := map[string]interface{}{ + "foo": "bar", + "baz": map[string]string{ + "cool": "stuff", + }, + } + anotherNestedMap := map[string]interface{}{ + "foo": "bar", + "baz": map[string]string{ + "cool": "things", + "awesome": "stuff", + }, + } + flatMap := map[string]interface{}{ + "foo": "bar", + "baz": "stuff", + } + anotherFlatMap := map[string]interface{}{ + "testing": "fun", + } + + testMap := mergeValues(flatMap, nestedMap) + equal := reflect.DeepEqual(testMap, nestedMap) + if !equal { + t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap) + } + + testMap = mergeValues(nestedMap, flatMap) + equal = reflect.DeepEqual(testMap, flatMap) + if !equal { + t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap) + } + + testMap = mergeValues(nestedMap, anotherNestedMap) + equal = reflect.DeepEqual(testMap, anotherNestedMap) + if !equal { + t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap) + } + + testMap = mergeValues(anotherFlatMap, anotherNestedMap) + expectedMap := map[string]interface{}{ + "testing": "fun", + "foo": "bar", + "baz": map[string]string{ + "cool": "things", + "awesome": "stuff", + }, + } + equal = reflect.DeepEqual(testMap, expectedMap) + if !equal { + t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap) + } +} diff --git a/pkg/helm/installer/install.go b/pkg/helm/installer/install.go new file mode 100644 index 000000000..707d17d01 --- /dev/null +++ b/pkg/helm/installer/install.go @@ -0,0 +1,372 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package installer + +import ( + "errors" + "fmt" + "io/ioutil" + "strings" + + "github.com/Masterminds/semver" + "github.com/ghodss/yaml" + "k8s.io/api/core/v1" + "k8s.io/api/extensions/v1beta1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/kubernetes" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + extensionsclient "k8s.io/client-go/kubernetes/typed/extensions/v1beta1" + "k8s.io/helm/pkg/version" + + "k8s.io/helm/pkg/chartutil" +) + +// Install uses Kubernetes client to install Tiller. +// +// Returns an error if the command failed. +func Install(client kubernetes.Interface, opts *Options) error { + if err := createDeployment(client.ExtensionsV1beta1(), opts); err != nil { + return err + } + if err := createService(client.CoreV1(), opts.Namespace); err != nil { + return err + } + if opts.tls() { + if err := createSecret(client.CoreV1(), opts); err != nil { + return err + } + } + return nil +} + +// Upgrade uses Kubernetes client to upgrade Tiller to current version. +// +// Returns an error if the command failed. +func Upgrade(client kubernetes.Interface, opts *Options) error { + obj, err := client.ExtensionsV1beta1().Deployments(opts.Namespace).Get(deploymentName, metav1.GetOptions{}) + if err != nil { + return err + } + tillerImage := obj.Spec.Template.Spec.Containers[0].Image + if semverCompare(tillerImage) == -1 && !opts.ForceUpgrade { + return errors.New("current Tiller version is newer, use --force-upgrade to downgrade") + } + obj.Spec.Template.Spec.Containers[0].Image = opts.selectImage() + obj.Spec.Template.Spec.Containers[0].ImagePullPolicy = opts.pullPolicy() + obj.Spec.Template.Spec.ServiceAccountName = opts.ServiceAccount + if _, err := client.ExtensionsV1beta1().Deployments(opts.Namespace).Update(obj); err != nil { + return err + } + // If the service does not exists that would mean we are upgrading from a Tiller version + // that didn't deploy the service, so install it. + _, err = client.CoreV1().Services(opts.Namespace).Get(serviceName, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + return createService(client.CoreV1(), opts.Namespace) + } + return err +} + +// semverCompare returns whether the client's version is older, equal or newer than the given image's version. +func semverCompare(image string) int { + split := strings.Split(image, ":") + if len(split) < 2 { + // If we don't know the version, we consider the client version newer. + return 1 + } + tillerVersion, err := semver.NewVersion(split[1]) + if err != nil { + // same thing with unparsable tiller versions (e.g. canary releases). + return 1 + } + clientVersion, err := semver.NewVersion(version.Version) + if err != nil { + // aaaaaand same thing with unparsable helm versions (e.g. canary releases). + return 1 + } + return clientVersion.Compare(tillerVersion) +} + +// createDeployment creates the Tiller Deployment resource. +func createDeployment(client extensionsclient.DeploymentsGetter, opts *Options) error { + obj, err := deployment(opts) + if err != nil { + return err + } + _, err = client.Deployments(obj.Namespace).Create(obj) + return err + +} + +// deployment gets the deployment object that installs Tiller. +func deployment(opts *Options) (*v1beta1.Deployment, error) { + return generateDeployment(opts) +} + +// createService creates the Tiller service resource +func createService(client corev1.ServicesGetter, namespace string) error { + obj := service(namespace) + _, err := client.Services(obj.Namespace).Create(obj) + return err +} + +// service gets the service object that installs Tiller. +func service(namespace string) *v1.Service { + return generateService(namespace) +} + +// DeploymentManifest gets the manifest (as a string) that describes the Tiller Deployment +// resource. +func DeploymentManifest(opts *Options) (string, error) { + obj, err := deployment(opts) + if err != nil { + return "", err + } + buf, err := yaml.Marshal(obj) + return string(buf), err +} + +// ServiceManifest gets the manifest (as a string) that describes the Tiller Service +// resource. +func ServiceManifest(namespace string) (string, error) { + obj := service(namespace) + buf, err := yaml.Marshal(obj) + return string(buf), err +} + +func generateLabels(labels map[string]string) map[string]string { + labels["app"] = "helm" + return labels +} + +// parseNodeSelectors parses a comma delimited list of key=values pairs into a map. +func parseNodeSelectorsInto(labels string, m map[string]string) error { + kv := strings.Split(labels, ",") + for _, v := range kv { + el := strings.Split(v, "=") + if len(el) == 2 { + m[el[0]] = el[1] + } else { + return fmt.Errorf("invalid nodeSelector label: %q", kv) + } + } + return nil +} +func generateDeployment(opts *Options) (*v1beta1.Deployment, error) { + labels := generateLabels(map[string]string{"name": "tiller"}) + nodeSelectors := map[string]string{} + if len(opts.NodeSelectors) > 0 { + err := parseNodeSelectorsInto(opts.NodeSelectors, nodeSelectors) + if err != nil { + return nil, err + } + } + d := &v1beta1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: opts.Namespace, + Name: deploymentName, + Labels: labels, + }, + Spec: v1beta1.DeploymentSpec{ + Replicas: opts.getReplicas(), + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: v1.PodSpec{ + ServiceAccountName: opts.ServiceAccount, + Containers: []v1.Container{ + { + Name: "tiller", + Image: opts.selectImage(), + ImagePullPolicy: opts.pullPolicy(), + Ports: []v1.ContainerPort{ + {ContainerPort: 44134, Name: "tiller"}, + {ContainerPort: 44135, Name: "http"}, + }, + Env: []v1.EnvVar{ + {Name: "TILLER_NAMESPACE", Value: opts.Namespace}, + {Name: "TILLER_HISTORY_MAX", Value: fmt.Sprintf("%d", opts.MaxHistory)}, + }, + LivenessProbe: &v1.Probe{ + Handler: v1.Handler{ + HTTPGet: &v1.HTTPGetAction{ + Path: "/liveness", + Port: intstr.FromInt(44135), + }, + }, + InitialDelaySeconds: 1, + TimeoutSeconds: 1, + }, + ReadinessProbe: &v1.Probe{ + Handler: v1.Handler{ + HTTPGet: &v1.HTTPGetAction{ + Path: "/readiness", + Port: intstr.FromInt(44135), + }, + }, + InitialDelaySeconds: 1, + TimeoutSeconds: 1, + }, + }, + }, + HostNetwork: opts.EnableHostNetwork, + NodeSelector: nodeSelectors, + }, + }, + }, + } + + if opts.tls() { + const certsDir = "/etc/certs" + + var tlsVerify, tlsEnable = "", "1" + if opts.VerifyTLS { + tlsVerify = "1" + } + + // Mount secret to "/etc/certs" + d.Spec.Template.Spec.Containers[0].VolumeMounts = append(d.Spec.Template.Spec.Containers[0].VolumeMounts, v1.VolumeMount{ + Name: "tiller-certs", + ReadOnly: true, + MountPath: certsDir, + }) + // Add environment variable required for enabling TLS + d.Spec.Template.Spec.Containers[0].Env = append(d.Spec.Template.Spec.Containers[0].Env, []v1.EnvVar{ + {Name: "TILLER_TLS_VERIFY", Value: tlsVerify}, + {Name: "TILLER_TLS_ENABLE", Value: tlsEnable}, + {Name: "TILLER_TLS_CERTS", Value: certsDir}, + }...) + // Add secret volume to deployment + d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, v1.Volume{ + Name: "tiller-certs", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: "tiller-secret", + }, + }, + }) + } + // if --override values were specified, ultimately convert values and deployment to maps, + // merge them and convert back to Deployment + if len(opts.Values) > 0 { + // base deployment struct + var dd v1beta1.Deployment + // get YAML from original deployment + dy, err := yaml.Marshal(d) + if err != nil { + return nil, fmt.Errorf("Error marshalling base Tiller Deployment: %s", err) + } + // convert deployment YAML to values + dv, err := chartutil.ReadValues(dy) + if err != nil { + return nil, fmt.Errorf("Error converting Deployment manifest: %s ", err) + } + dm := dv.AsMap() + // merge --set values into our map + sm, err := opts.valuesMap(dm) + if err != nil { + return nil, fmt.Errorf("Error merging --set values into Deployment manifest") + } + finalY, err := yaml.Marshal(sm) + if err != nil { + return nil, fmt.Errorf("Error marshalling merged map to YAML: %s ", err) + } + // convert merged values back into deployment + err = yaml.Unmarshal(finalY, &dd) + if err != nil { + return nil, fmt.Errorf("Error unmarshalling Values to Deployment manifest: %s ", err) + } + d = &dd + } + + return d, nil +} + +func generateService(namespace string) *v1.Service { + labels := generateLabels(map[string]string{"name": "tiller"}) + s := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: serviceName, + Labels: labels, + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeClusterIP, + Ports: []v1.ServicePort{ + { + Name: "tiller", + Port: 44134, + TargetPort: intstr.FromString("tiller"), + }, + }, + Selector: labels, + }, + } + return s +} + +// SecretManifest gets the manifest (as a string) that describes the Tiller Secret resource. +func SecretManifest(opts *Options) (string, error) { + o, err := generateSecret(opts) + if err != nil { + return "", err + } + buf, err := yaml.Marshal(o) + return string(buf), err +} + +// createSecret creates the Tiller secret resource. +func createSecret(client corev1.SecretsGetter, opts *Options) error { + o, err := generateSecret(opts) + if err != nil { + return err + } + _, err = client.Secrets(o.Namespace).Create(o) + return err +} + +// generateSecret builds the secret object that hold Tiller secrets. +func generateSecret(opts *Options) (*v1.Secret, error) { + + labels := generateLabels(map[string]string{"name": "tiller"}) + secret := &v1.Secret{ + Type: v1.SecretTypeOpaque, + Data: make(map[string][]byte), + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Labels: labels, + Namespace: opts.Namespace, + }, + } + var err error + if secret.Data["tls.key"], err = read(opts.TLSKeyFile); err != nil { + return nil, err + } + if secret.Data["tls.crt"], err = read(opts.TLSCertFile); err != nil { + return nil, err + } + if opts.VerifyTLS { + if secret.Data["ca.crt"], err = read(opts.TLSCaCertFile); err != nil { + return nil, err + } + } + return secret, nil +} + +func read(path string) (b []byte, err error) { return ioutil.ReadFile(path) } diff --git a/pkg/helm/installer/install_test.go b/pkg/helm/installer/install_test.go new file mode 100644 index 000000000..dbb7143e3 --- /dev/null +++ b/pkg/helm/installer/install_test.go @@ -0,0 +1,740 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package installer // import "k8s.io/helm/cmd/helm/installer" + +import ( + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/ghodss/yaml" + "k8s.io/api/core/v1" + "k8s.io/api/extensions/v1beta1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + testcore "k8s.io/client-go/testing" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/version" +) + +func TestDeploymentManifest(t *testing.T) { + tests := []struct { + name string + image string + canary bool + expect string + imagePullPolicy v1.PullPolicy + }{ + {"default", "", false, "gcr.io/kubernetes-helm/tiller:" + version.Version, "IfNotPresent"}, + {"canary", "example.com/tiller", true, "gcr.io/kubernetes-helm/tiller:canary", "Always"}, + {"custom", "example.com/tiller:latest", false, "example.com/tiller:latest", "IfNotPresent"}, + } + + for _, tt := range tests { + o, err := DeploymentManifest(&Options{Namespace: v1.NamespaceDefault, ImageSpec: tt.image, UseCanary: tt.canary}) + if err != nil { + t.Fatalf("%s: error %q", tt.name, err) + } + var dep v1beta1.Deployment + if err := yaml.Unmarshal([]byte(o), &dep); err != nil { + t.Fatalf("%s: error %q", tt.name, err) + } + + if got := dep.Spec.Template.Spec.Containers[0].Image; got != tt.expect { + t.Errorf("%s: expected image %q, got %q", tt.name, tt.expect, got) + } + + if got := dep.Spec.Template.Spec.Containers[0].ImagePullPolicy; got != tt.imagePullPolicy { + t.Errorf("%s: expected imagePullPolicy %q, got %q", tt.name, tt.imagePullPolicy, got) + } + + if got := dep.Spec.Template.Spec.Containers[0].Env[0].Value; got != v1.NamespaceDefault { + t.Errorf("%s: expected namespace %q, got %q", tt.name, v1.NamespaceDefault, got) + } + } +} + +func TestDeploymentManifestForServiceAccount(t *testing.T) { + tests := []struct { + name string + image string + canary bool + expect string + imagePullPolicy v1.PullPolicy + serviceAccount string + }{ + {"withSA", "", false, "gcr.io/kubernetes-helm/tiller:latest", "IfNotPresent", "service-account"}, + {"withoutSA", "", false, "gcr.io/kubernetes-helm/tiller:latest", "IfNotPresent", ""}, + } + for _, tt := range tests { + o, err := DeploymentManifest(&Options{Namespace: v1.NamespaceDefault, ImageSpec: tt.image, UseCanary: tt.canary, ServiceAccount: tt.serviceAccount}) + if err != nil { + t.Fatalf("%s: error %q", tt.name, err) + } + + var d v1beta1.Deployment + if err := yaml.Unmarshal([]byte(o), &d); err != nil { + t.Fatalf("%s: error %q", tt.name, err) + } + if got := d.Spec.Template.Spec.ServiceAccountName; got != tt.serviceAccount { + t.Errorf("%s: expected service account value %q, got %q", tt.name, tt.serviceAccount, got) + } + } +} + +func TestDeploymentManifest_WithTLS(t *testing.T) { + tests := []struct { + opts Options + name string + enable string + verify string + }{ + { + Options{Namespace: v1.NamespaceDefault, EnableTLS: true, VerifyTLS: true}, + "tls enable (true), tls verify (true)", + "1", + "1", + }, + { + Options{Namespace: v1.NamespaceDefault, EnableTLS: true, VerifyTLS: false}, + "tls enable (true), tls verify (false)", + "1", + "", + }, + { + Options{Namespace: v1.NamespaceDefault, EnableTLS: false, VerifyTLS: true}, + "tls enable (false), tls verify (true)", + "1", + "1", + }, + } + for _, tt := range tests { + o, err := DeploymentManifest(&tt.opts) + if err != nil { + t.Fatalf("%s: error %q", tt.name, err) + } + + var d v1beta1.Deployment + if err := yaml.Unmarshal([]byte(o), &d); err != nil { + t.Fatalf("%s: error %q", tt.name, err) + } + // verify environment variable in deployment reflect the use of tls being enabled. + if got := d.Spec.Template.Spec.Containers[0].Env[2].Value; got != tt.verify { + t.Errorf("%s: expected tls verify env value %q, got %q", tt.name, tt.verify, got) + } + if got := d.Spec.Template.Spec.Containers[0].Env[3].Value; got != tt.enable { + t.Errorf("%s: expected tls enable env value %q, got %q", tt.name, tt.enable, got) + } + } +} + +func TestServiceManifest(t *testing.T) { + o, err := ServiceManifest(v1.NamespaceDefault) + if err != nil { + t.Fatalf("error %q", err) + } + var svc v1.Service + if err := yaml.Unmarshal([]byte(o), &svc); err != nil { + t.Fatalf("error %q", err) + } + + if got := svc.ObjectMeta.Namespace; got != v1.NamespaceDefault { + t.Errorf("expected namespace %s, got %s", v1.NamespaceDefault, got) + } +} + +func TestSecretManifest(t *testing.T) { + o, err := SecretManifest(&Options{ + VerifyTLS: true, + EnableTLS: true, + Namespace: v1.NamespaceDefault, + TLSKeyFile: tlsTestFile(t, "key.pem"), + TLSCertFile: tlsTestFile(t, "crt.pem"), + TLSCaCertFile: tlsTestFile(t, "ca.pem"), + }) + + if err != nil { + t.Fatalf("error %q", err) + } + + var obj v1.Secret + if err := yaml.Unmarshal([]byte(o), &obj); err != nil { + t.Fatalf("error %q", err) + } + + if got := obj.ObjectMeta.Namespace; got != v1.NamespaceDefault { + t.Errorf("expected namespace %s, got %s", v1.NamespaceDefault, got) + } + if _, ok := obj.Data["tls.key"]; !ok { + t.Errorf("missing 'tls.key' in generated secret object") + } + if _, ok := obj.Data["tls.crt"]; !ok { + t.Errorf("missing 'tls.crt' in generated secret object") + } + if _, ok := obj.Data["ca.crt"]; !ok { + t.Errorf("missing 'ca.crt' in generated secret object") + } +} + +func TestInstall(t *testing.T) { + image := "gcr.io/kubernetes-helm/tiller:v2.0.0" + + fc := &fake.Clientset{} + fc.AddReactor("create", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.CreateAction).GetObject().(*v1beta1.Deployment) + l := obj.GetLabels() + if reflect.DeepEqual(l, map[string]string{"app": "helm"}) { + t.Errorf("expected labels = '', got '%s'", l) + } + i := obj.Spec.Template.Spec.Containers[0].Image + if i != image { + t.Errorf("expected image = '%s', got '%s'", image, i) + } + ports := len(obj.Spec.Template.Spec.Containers[0].Ports) + if ports != 2 { + t.Errorf("expected ports = 2, got '%d'", ports) + } + replicas := obj.Spec.Replicas + if int(*replicas) != 1 { + t.Errorf("expected replicas = 1, got '%d'", replicas) + } + return true, obj, nil + }) + fc.AddReactor("create", "services", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.CreateAction).GetObject().(*v1.Service) + l := obj.GetLabels() + if reflect.DeepEqual(l, map[string]string{"app": "helm"}) { + t.Errorf("expected labels = '', got '%s'", l) + } + n := obj.ObjectMeta.Namespace + if n != v1.NamespaceDefault { + t.Errorf("expected namespace = '%s', got '%s'", v1.NamespaceDefault, n) + } + return true, obj, nil + }) + + opts := &Options{Namespace: v1.NamespaceDefault, ImageSpec: image} + if err := Install(fc, opts); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 2 { + t.Errorf("unexpected actions: %v, expected 2 actions got %d", actions, len(actions)) + } +} + +func TestInstallHA(t *testing.T) { + image := "gcr.io/kubernetes-helm/tiller:v2.0.0" + + fc := &fake.Clientset{} + fc.AddReactor("create", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.CreateAction).GetObject().(*v1beta1.Deployment) + replicas := obj.Spec.Replicas + if int(*replicas) != 2 { + t.Errorf("expected replicas = 2, got '%d'", replicas) + } + return true, obj, nil + }) + + opts := &Options{ + Namespace: v1.NamespaceDefault, + ImageSpec: image, + Replicas: 2, + } + if err := Install(fc, opts); err != nil { + t.Errorf("unexpected error: %#+v", err) + } +} + +func TestInstall_WithTLS(t *testing.T) { + image := "gcr.io/kubernetes-helm/tiller:v2.0.0" + name := "tiller-secret" + + fc := &fake.Clientset{} + fc.AddReactor("create", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.CreateAction).GetObject().(*v1beta1.Deployment) + l := obj.GetLabels() + if reflect.DeepEqual(l, map[string]string{"app": "helm"}) { + t.Errorf("expected labels = '', got '%s'", l) + } + i := obj.Spec.Template.Spec.Containers[0].Image + if i != image { + t.Errorf("expected image = '%s', got '%s'", image, i) + } + return true, obj, nil + }) + fc.AddReactor("create", "services", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.CreateAction).GetObject().(*v1.Service) + l := obj.GetLabels() + if reflect.DeepEqual(l, map[string]string{"app": "helm"}) { + t.Errorf("expected labels = '', got '%s'", l) + } + n := obj.ObjectMeta.Namespace + if n != v1.NamespaceDefault { + t.Errorf("expected namespace = '%s', got '%s'", v1.NamespaceDefault, n) + } + return true, obj, nil + }) + fc.AddReactor("create", "secrets", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.CreateAction).GetObject().(*v1.Secret) + if l := obj.GetLabels(); reflect.DeepEqual(l, map[string]string{"app": "helm"}) { + t.Errorf("expected labels = '', got '%s'", l) + } + if n := obj.ObjectMeta.Namespace; n != v1.NamespaceDefault { + t.Errorf("expected namespace = '%s', got '%s'", v1.NamespaceDefault, n) + } + if s := obj.ObjectMeta.Name; s != name { + t.Errorf("expected name = '%s', got '%s'", name, s) + } + if _, ok := obj.Data["tls.key"]; !ok { + t.Errorf("missing 'tls.key' in generated secret object") + } + if _, ok := obj.Data["tls.crt"]; !ok { + t.Errorf("missing 'tls.crt' in generated secret object") + } + if _, ok := obj.Data["ca.crt"]; !ok { + t.Errorf("missing 'ca.crt' in generated secret object") + } + return true, obj, nil + }) + + opts := &Options{ + Namespace: v1.NamespaceDefault, + ImageSpec: image, + EnableTLS: true, + VerifyTLS: true, + TLSKeyFile: tlsTestFile(t, "key.pem"), + TLSCertFile: tlsTestFile(t, "crt.pem"), + TLSCaCertFile: tlsTestFile(t, "ca.pem"), + } + + if err := Install(fc, opts); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 3 { + t.Errorf("unexpected actions: %v, expected 3 actions got %d", actions, len(actions)) + } +} + +func TestInstall_canary(t *testing.T) { + fc := &fake.Clientset{} + fc.AddReactor("create", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.CreateAction).GetObject().(*v1beta1.Deployment) + i := obj.Spec.Template.Spec.Containers[0].Image + if i != "gcr.io/kubernetes-helm/tiller:canary" { + t.Errorf("expected canary image, got '%s'", i) + } + return true, obj, nil + }) + fc.AddReactor("create", "services", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.CreateAction).GetObject().(*v1.Service) + return true, obj, nil + }) + + opts := &Options{Namespace: v1.NamespaceDefault, UseCanary: true} + if err := Install(fc, opts); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 2 { + t.Errorf("unexpected actions: %v, expected 2 actions got %d", actions, len(actions)) + } +} + +func TestUpgrade(t *testing.T) { + image := "gcr.io/kubernetes-helm/tiller:v2.0.0" + serviceAccount := "newServiceAccount" + existingDeployment, _ := deployment(&Options{ + Namespace: v1.NamespaceDefault, + ImageSpec: "imageToReplace:v1.0.0", + ServiceAccount: "serviceAccountToReplace", + UseCanary: false, + }) + existingService := service(v1.NamespaceDefault) + + fc := &fake.Clientset{} + fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingDeployment, nil + }) + fc.AddReactor("update", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.UpdateAction).GetObject().(*v1beta1.Deployment) + i := obj.Spec.Template.Spec.Containers[0].Image + if i != image { + t.Errorf("expected image = '%s', got '%s'", image, i) + } + sa := obj.Spec.Template.Spec.ServiceAccountName + if sa != serviceAccount { + t.Errorf("expected serviceAccountName = '%s', got '%s'", serviceAccount, sa) + } + return true, obj, nil + }) + fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingService, nil + }) + + opts := &Options{Namespace: v1.NamespaceDefault, ImageSpec: image, ServiceAccount: serviceAccount} + if err := Upgrade(fc, opts); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 3 { + t.Errorf("unexpected actions: %v, expected 3 actions got %d", actions, len(actions)) + } +} + +func TestUpgrade_serviceNotFound(t *testing.T) { + image := "gcr.io/kubernetes-helm/tiller:v2.0.0" + + existingDeployment, _ := deployment(&Options{ + Namespace: v1.NamespaceDefault, + ImageSpec: "imageToReplace", + UseCanary: false, + }) + + fc := &fake.Clientset{} + fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingDeployment, nil + }) + fc.AddReactor("update", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.UpdateAction).GetObject().(*v1beta1.Deployment) + i := obj.Spec.Template.Spec.Containers[0].Image + if i != image { + t.Errorf("expected image = '%s', got '%s'", image, i) + } + return true, obj, nil + }) + fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { + return true, nil, apierrors.NewNotFound(v1.Resource("services"), "1") + }) + fc.AddReactor("create", "services", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.CreateAction).GetObject().(*v1.Service) + n := obj.ObjectMeta.Namespace + if n != v1.NamespaceDefault { + t.Errorf("expected namespace = '%s', got '%s'", v1.NamespaceDefault, n) + } + return true, obj, nil + }) + + opts := &Options{Namespace: v1.NamespaceDefault, ImageSpec: image} + if err := Upgrade(fc, opts); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 4 { + t.Errorf("unexpected actions: %v, expected 4 actions got %d", actions, len(actions)) + } +} + +func TestUgrade_newerVersion(t *testing.T) { + image := "gcr.io/kubernetes-helm/tiller:v2.0.0" + serviceAccount := "newServiceAccount" + existingDeployment, _ := deployment(&Options{ + Namespace: v1.NamespaceDefault, + ImageSpec: "imageToReplace:v100.5.0", + ServiceAccount: "serviceAccountToReplace", + UseCanary: false, + }) + existingService := service(v1.NamespaceDefault) + + fc := &fake.Clientset{} + fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingDeployment, nil + }) + fc.AddReactor("update", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.UpdateAction).GetObject().(*v1beta1.Deployment) + i := obj.Spec.Template.Spec.Containers[0].Image + if i != image { + t.Errorf("expected image = '%s', got '%s'", image, i) + } + sa := obj.Spec.Template.Spec.ServiceAccountName + if sa != serviceAccount { + t.Errorf("expected serviceAccountName = '%s', got '%s'", serviceAccount, sa) + } + return true, obj, nil + }) + fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingService, nil + }) + + opts := &Options{ + Namespace: v1.NamespaceDefault, + ImageSpec: image, + ServiceAccount: serviceAccount, + ForceUpgrade: false, + } + if err := Upgrade(fc, opts); err == nil { + t.Errorf("Expected error because the deployed version is newer") + } + + if actions := fc.Actions(); len(actions) != 1 { + t.Errorf("unexpected actions: %v, expected 1 action got %d", actions, len(actions)) + } + + opts = &Options{ + Namespace: v1.NamespaceDefault, + ImageSpec: image, + ServiceAccount: serviceAccount, + ForceUpgrade: true, + } + if err := Upgrade(fc, opts); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 4 { + t.Errorf("unexpected actions: %v, expected 4 action got %d", actions, len(actions)) + } +} + +func TestUpgrade_identical(t *testing.T) { + image := "gcr.io/kubernetes-helm/tiller:v2.0.0" + serviceAccount := "newServiceAccount" + existingDeployment, _ := deployment(&Options{ + Namespace: v1.NamespaceDefault, + ImageSpec: "imageToReplace:v2.0.0", + ServiceAccount: "serviceAccountToReplace", + UseCanary: false, + }) + existingService := service(v1.NamespaceDefault) + + fc := &fake.Clientset{} + fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingDeployment, nil + }) + fc.AddReactor("update", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.UpdateAction).GetObject().(*v1beta1.Deployment) + i := obj.Spec.Template.Spec.Containers[0].Image + if i != image { + t.Errorf("expected image = '%s', got '%s'", image, i) + } + sa := obj.Spec.Template.Spec.ServiceAccountName + if sa != serviceAccount { + t.Errorf("expected serviceAccountName = '%s', got '%s'", serviceAccount, sa) + } + return true, obj, nil + }) + fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingService, nil + }) + + opts := &Options{Namespace: v1.NamespaceDefault, ImageSpec: image, ServiceAccount: serviceAccount} + if err := Upgrade(fc, opts); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 3 { + t.Errorf("unexpected actions: %v, expected 3 actions got %d", actions, len(actions)) + } +} + +func TestUpgrade_canaryClient(t *testing.T) { + image := "gcr.io/kubernetes-helm/tiller:canary" + serviceAccount := "newServiceAccount" + existingDeployment, _ := deployment(&Options{ + Namespace: v1.NamespaceDefault, + ImageSpec: "imageToReplace:v1.0.0", + ServiceAccount: "serviceAccountToReplace", + UseCanary: false, + }) + existingService := service(v1.NamespaceDefault) + + fc := &fake.Clientset{} + fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingDeployment, nil + }) + fc.AddReactor("update", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.UpdateAction).GetObject().(*v1beta1.Deployment) + i := obj.Spec.Template.Spec.Containers[0].Image + if i != image { + t.Errorf("expected image = '%s', got '%s'", image, i) + } + sa := obj.Spec.Template.Spec.ServiceAccountName + if sa != serviceAccount { + t.Errorf("expected serviceAccountName = '%s', got '%s'", serviceAccount, sa) + } + return true, obj, nil + }) + fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingService, nil + }) + + opts := &Options{Namespace: v1.NamespaceDefault, ImageSpec: image, ServiceAccount: serviceAccount} + if err := Upgrade(fc, opts); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 3 { + t.Errorf("unexpected actions: %v, expected 3 actions got %d", actions, len(actions)) + } +} + +func TestUpgrade_canaryServer(t *testing.T) { + image := "gcr.io/kubernetes-helm/tiller:v2.0.0" + serviceAccount := "newServiceAccount" + existingDeployment, _ := deployment(&Options{ + Namespace: v1.NamespaceDefault, + ImageSpec: "imageToReplace:canary", + ServiceAccount: "serviceAccountToReplace", + UseCanary: false, + }) + existingService := service(v1.NamespaceDefault) + + fc := &fake.Clientset{} + fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingDeployment, nil + }) + fc.AddReactor("update", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + obj := action.(testcore.UpdateAction).GetObject().(*v1beta1.Deployment) + i := obj.Spec.Template.Spec.Containers[0].Image + if i != image { + t.Errorf("expected image = '%s', got '%s'", image, i) + } + sa := obj.Spec.Template.Spec.ServiceAccountName + if sa != serviceAccount { + t.Errorf("expected serviceAccountName = '%s', got '%s'", serviceAccount, sa) + } + return true, obj, nil + }) + fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingService, nil + }) + + opts := &Options{Namespace: v1.NamespaceDefault, ImageSpec: image, ServiceAccount: serviceAccount} + if err := Upgrade(fc, opts); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 3 { + t.Errorf("unexpected actions: %v, expected 3 actions got %d", actions, len(actions)) + } +} + +func tlsTestFile(t *testing.T, path string) string { + const tlsTestDir = "../../../testdata" + path = filepath.Join(tlsTestDir, path) + if _, err := os.Stat(path); os.IsNotExist(err) { + t.Fatalf("tls test file %s does not exist", path) + } + return path +} +func TestDeploymentManifest_WithNodeSelectors(t *testing.T) { + tests := []struct { + opts Options + name string + expect map[string]interface{} + }{ + { + Options{Namespace: v1.NamespaceDefault, NodeSelectors: "app=tiller"}, + "nodeSelector app=tiller", + map[string]interface{}{"app": "tiller"}, + }, + { + Options{Namespace: v1.NamespaceDefault, NodeSelectors: "app=tiller,helm=rocks"}, + "nodeSelector app=tiller, helm=rocks", + map[string]interface{}{"app": "tiller", "helm": "rocks"}, + }, + // note: nodeSelector key and value are strings + { + Options{Namespace: v1.NamespaceDefault, NodeSelectors: "app=tiller,minCoolness=1"}, + "nodeSelector app=tiller, helm=rocks", + map[string]interface{}{"app": "tiller", "minCoolness": "1"}, + }, + } + for _, tt := range tests { + o, err := DeploymentManifest(&tt.opts) + if err != nil { + t.Fatalf("%s: error %q", tt.name, err) + } + + var d v1beta1.Deployment + if err := yaml.Unmarshal([]byte(o), &d); err != nil { + t.Fatalf("%s: error %q", tt.name, err) + } + // Verify that environment variables in Deployment reflect the use of TLS being enabled. + got := d.Spec.Template.Spec.NodeSelector + for k, v := range tt.expect { + if got[k] != v { + t.Errorf("%s: expected nodeSelector value %q, got %q", tt.name, tt.expect, got) + } + } + } +} +func TestDeploymentManifest_WithSetValues(t *testing.T) { + tests := []struct { + opts Options + name string + expectPath string + expect interface{} + }{ + { + Options{Namespace: v1.NamespaceDefault, Values: []string{"spec.template.spec.nodeselector.app=tiller"}}, + "setValues spec.template.spec.nodeSelector.app=tiller", + "spec.template.spec.nodeSelector.app", + "tiller", + }, + { + Options{Namespace: v1.NamespaceDefault, Values: []string{"spec.replicas=2"}}, + "setValues spec.replicas=2", + "spec.replicas", + 2, + }, + { + Options{Namespace: v1.NamespaceDefault, Values: []string{"spec.template.spec.activedeadlineseconds=120"}}, + "setValues spec.template.spec.activedeadlineseconds=120", + "spec.template.spec.activeDeadlineSeconds", + 120, + }, + } + for _, tt := range tests { + o, err := DeploymentManifest(&tt.opts) + if err != nil { + t.Fatalf("%s: error %q", tt.name, err) + } + values, err := chartutil.ReadValues([]byte(o)) + if err != nil { + t.Errorf("Error converting Deployment manifest to Values: %s", err) + } + // path value + pv, err := values.PathValue(tt.expectPath) + if err != nil { + t.Errorf("Error retrieving path value from Deployment Values: %s", err) + } + + // convert our expected value to match the result type for comparison + ev := tt.expect + switch pvt := pv.(type) { + case float64: + floatType := reflect.TypeOf(float64(0)) + v := reflect.ValueOf(ev) + v = reflect.Indirect(v) + if !v.Type().ConvertibleTo(floatType) { + t.Fatalf("Error converting expected value %v to float64", v.Type()) + } + fv := v.Convert(floatType) + if fv.Float() != pvt { + t.Errorf("%s: expected value %q, got %q", tt.name, tt.expect, pv) + } + default: + if pv != tt.expect { + t.Errorf("%s: expected value %q, got %q", tt.name, tt.expect, pv) + } + } + } +} diff --git a/pkg/helm/installer/options.go b/pkg/helm/installer/options.go new file mode 100644 index 000000000..13cf43dcc --- /dev/null +++ b/pkg/helm/installer/options.go @@ -0,0 +1,164 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package installer // import "k8s.io/helm/cmd/helm/installer" + +import ( + "fmt" + + "k8s.io/api/core/v1" + "k8s.io/helm/pkg/strvals" + "k8s.io/helm/pkg/version" +) + +const defaultImage = "gcr.io/kubernetes-helm/tiller" + +// Options control how to install Tiller into a cluster, upgrade, and uninstall Tiller from a cluster. +type Options struct { + // EnableTLS instructs Tiller to serve with TLS enabled. + // + // Implied by VerifyTLS. If set the TLSKey and TLSCert are required. + EnableTLS bool + + // VerifyTLS instructs Tiller to serve with TLS enabled verify remote certificates. + // + // If set TLSKey, TLSCert, TLSCaCert are required. + VerifyTLS bool + + // UseCanary indicates that Tiller should deploy using the latest Tiller image. + UseCanary bool + + // Namespace is the Kubernetes namespace to use to deploy Tiller. + Namespace string + + // ServiceAccount is the Kubernetes service account to add to Tiller. + ServiceAccount string + + // Force allows to force upgrading tiller if deployed version is greater than current version + ForceUpgrade bool + + // ImageSpec indentifies the image Tiller will use when deployed. + // + // Valid if and only if UseCanary is false. + ImageSpec string + + // TLSKeyFile identifies the file containing the pem encoded TLS private + // key Tiller should use. + // + // Required and valid if and only if EnableTLS or VerifyTLS is set. + TLSKeyFile string + + // TLSCertFile identifies the file containing the pem encoded TLS + // certificate Tiller should use. + // + // Required and valid if and only if EnableTLS or VerifyTLS is set. + TLSCertFile string + + // TLSCaCertFile identifies the file containing the pem encoded TLS CA + // certificate Tiller should use to verify remotes certificates. + // + // Required and valid if and only if VerifyTLS is set. + TLSCaCertFile string + + // EnableHostNetwork installs Tiller with net=host. + EnableHostNetwork bool + + // MaxHistory sets the maximum number of release versions stored per release. + // + // Less than or equal to zero means no limit. + MaxHistory int + + // Replicas sets the amount of Tiller replicas to start + // + // Less than or equals to 1 means 1. + Replicas int + + // NodeSelectors determine which nodes Tiller can land on. + NodeSelectors string + + // Output dumps the Tiller manifest in the specified format (e.g. JSON) but skips Helm/Tiller installation. + Output OutputFormat + + // Set merges additional values into the Tiller Deployment manifest. + Values []string +} + +func (opts *Options) selectImage() string { + switch { + case opts.UseCanary: + return defaultImage + ":canary" + case opts.ImageSpec == "": + return fmt.Sprintf("%s:%s", defaultImage, version.Version) + default: + return opts.ImageSpec + } +} + +func (opts *Options) pullPolicy() v1.PullPolicy { + if opts.UseCanary { + return v1.PullAlways + } + return v1.PullIfNotPresent +} + +func (opts *Options) getReplicas() *int32 { + replicas := int32(1) + if opts.Replicas > 1 { + replicas = int32(opts.Replicas) + } + return &replicas +} + +func (opts *Options) tls() bool { return opts.EnableTLS || opts.VerifyTLS } + +// valuesMap returns user set values in map format +func (opts *Options) valuesMap(m map[string]interface{}) (map[string]interface{}, error) { + for _, skv := range opts.Values { + if err := strvals.ParseInto(skv, m); err != nil { + return nil, err + } + } + return m, nil +} + +// OutputFormat defines valid values for init output (json, yaml) +type OutputFormat string + +// String returns the string value of the OutputFormat +func (f *OutputFormat) String() string { + return string(*f) +} + +// Type returns the string value of the OutputFormat +func (f *OutputFormat) Type() string { + return "OutputFormat" +} + +const ( + fmtJSON OutputFormat = "json" + fmtYAML OutputFormat = "yaml" +) + +// Set validates and sets the value of the OutputFormat +func (f *OutputFormat) Set(s string) error { + for _, of := range []OutputFormat{fmtJSON, fmtYAML} { + if s == string(of) { + *f = of + return nil + } + } + return fmt.Errorf("unknown output format %q", s) +} diff --git a/pkg/helm/installer/uninstall.go b/pkg/helm/installer/uninstall.go new file mode 100644 index 000000000..818827ddb --- /dev/null +++ b/pkg/helm/installer/uninstall.go @@ -0,0 +1,71 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package installer // import "k8s.io/helm/cmd/helm/installer" + +import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + coreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" + "k8s.io/kubernetes/pkg/kubectl" +) + +const ( + deploymentName = "tiller-deploy" + serviceName = "tiller-deploy" + secretName = "tiller-secret" +) + +// Uninstall uses Kubernetes client to uninstall Tiller. +func Uninstall(client internalclientset.Interface, opts *Options) error { + if err := deleteService(client.Core(), opts.Namespace); err != nil { + return err + } + if err := deleteDeployment(client, opts.Namespace); err != nil { + return err + } + return deleteSecret(client.Core(), opts.Namespace) +} + +// deleteService deletes the Tiller Service resource +func deleteService(client coreclient.ServicesGetter, namespace string) error { + err := client.Services(namespace).Delete(serviceName, &metav1.DeleteOptions{}) + return ingoreNotFound(err) +} + +// deleteDeployment deletes the Tiller Deployment resource +// We need to use the reaper instead of the kube API because GC for deployment dependents +// is not yet supported at the k8s server level (<= 1.5) +func deleteDeployment(client internalclientset.Interface, namespace string) error { + reaper, _ := kubectl.ReaperFor(extensions.Kind("Deployment"), client) + err := reaper.Stop(namespace, deploymentName, 0, nil) + return ingoreNotFound(err) +} + +// deleteSecret deletes the Tiller Secret resource +func deleteSecret(client coreclient.SecretsGetter, namespace string) error { + err := client.Secrets(namespace).Delete(secretName, &metav1.DeleteOptions{}) + return ingoreNotFound(err) +} + +func ingoreNotFound(err error) error { + if apierrors.IsNotFound(err) { + return nil + } + return err +} diff --git a/pkg/helm/installer/uninstall_test.go b/pkg/helm/installer/uninstall_test.go new file mode 100644 index 000000000..91b257d47 --- /dev/null +++ b/pkg/helm/installer/uninstall_test.go @@ -0,0 +1,88 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package installer // import "k8s.io/helm/cmd/helm/installer" + +import ( + "testing" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + testcore "k8s.io/client-go/testing" + "k8s.io/kubernetes/pkg/apis/core" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" +) + +func TestUninstall(t *testing.T) { + fc := &fake.Clientset{} + opts := &Options{Namespace: core.NamespaceDefault} + if err := Uninstall(fc, opts); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 7 { + t.Errorf("unexpected actions: %v, expected 7 actions got %d", actions, len(actions)) + } +} + +func TestUninstall_serviceNotFound(t *testing.T) { + fc := &fake.Clientset{} + fc.AddReactor("delete", "services", func(action testcore.Action) (bool, runtime.Object, error) { + return true, nil, apierrors.NewNotFound(schema.GroupResource{Resource: "services"}, "1") + }) + + opts := &Options{Namespace: core.NamespaceDefault} + if err := Uninstall(fc, opts); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 7 { + t.Errorf("unexpected actions: %v, expected 7 actions got %d", actions, len(actions)) + } +} + +func TestUninstall_deploymentNotFound(t *testing.T) { + fc := &fake.Clientset{} + fc.AddReactor("delete", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + return true, nil, apierrors.NewNotFound(core.Resource("deployments"), "1") + }) + + opts := &Options{Namespace: core.NamespaceDefault} + if err := Uninstall(fc, opts); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 7 { + t.Errorf("unexpected actions: %v, expected 7 actions got %d", actions, len(actions)) + } +} + +func TestUninstall_secretNotFound(t *testing.T) { + fc := &fake.Clientset{} + fc.AddReactor("delete", "secrets", func(action testcore.Action) (bool, runtime.Object, error) { + return true, nil, apierrors.NewNotFound(core.Resource("secrets"), "1") + }) + + opts := &Options{Namespace: core.NamespaceDefault} + if err := Uninstall(fc, opts); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 7 { + t.Errorf("unexpected actions: %v, expect 7 actions got %d", actions, len(actions)) + } +} diff --git a/pkg/helm/lint.go b/pkg/helm/lint.go new file mode 100644 index 000000000..7b01301be --- /dev/null +++ b/pkg/helm/lint.go @@ -0,0 +1,208 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/ghodss/yaml" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/lint" + "k8s.io/helm/pkg/lint/support" + "k8s.io/helm/pkg/strvals" +) + +var longLintHelp = ` +This command takes a path to a chart and runs a series of tests to verify that +the chart is well-formed. + +If the linter encounters things that will cause the chart to fail installation, +it will emit [ERROR] messages. If it encounters issues that break with convention +or recommendation, it will emit [WARNING] messages. +` + +type lintCmd struct { + valueFiles valueFiles + values []string + sValues []string + namespace string + strict bool + paths []string + out io.Writer +} + +func newLintCmd(out io.Writer) *cobra.Command { + l := &lintCmd{ + paths: []string{"."}, + out: out, + } + cmd := &cobra.Command{ + Use: "lint [flags] PATH", + Short: "examines a chart for possible issues", + Long: longLintHelp, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + l.paths = args + } + return l.run() + }, + } + + cmd.Flags().VarP(&l.valueFiles, "values", "f", "specify values in a YAML file (can specify multiple)") + cmd.Flags().StringArrayVar(&l.values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + cmd.Flags().StringArrayVar(&l.sValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + cmd.Flags().StringVar(&l.namespace, "namespace", "default", "namespace to install the release into (only used if --install is set)") + cmd.Flags().BoolVar(&l.strict, "strict", false, "fail on lint warnings") + + return cmd +} + +var errLintNoChart = errors.New("No chart found for linting (missing Chart.yaml)") + +func (l *lintCmd) run() error { + var lowestTolerance int + if l.strict { + lowestTolerance = support.WarningSev + } else { + lowestTolerance = support.ErrorSev + } + + // Get the raw values + rvals, err := l.vals() + if err != nil { + return err + } + + var total int + var failures int + for _, path := range l.paths { + if linter, err := lintChart(path, rvals, l.namespace, l.strict); err != nil { + fmt.Println("==> Skipping", path) + fmt.Println(err) + if err == errLintNoChart { + failures = failures + 1 + } + } else { + fmt.Println("==> Linting", path) + + if len(linter.Messages) == 0 { + fmt.Println("Lint OK") + } + + for _, msg := range linter.Messages { + fmt.Println(msg) + } + + total = total + 1 + if linter.HighestSeverity >= lowestTolerance { + failures = failures + 1 + } + } + fmt.Println("") + } + + msg := fmt.Sprintf("%d chart(s) linted", total) + if failures > 0 { + return fmt.Errorf("%s, %d chart(s) failed", msg, failures) + } + + fmt.Fprintf(l.out, "%s, no failures\n", msg) + + return nil +} + +func lintChart(path string, vals []byte, namespace string, strict bool) (support.Linter, error) { + var chartPath string + linter := support.Linter{} + + if strings.HasSuffix(path, ".tgz") { + tempDir, err := ioutil.TempDir("", "helm-lint") + if err != nil { + return linter, err + } + defer os.RemoveAll(tempDir) + + file, err := os.Open(path) + if err != nil { + return linter, err + } + defer file.Close() + + if err = chartutil.Expand(tempDir, file); err != nil { + return linter, err + } + + lastHyphenIndex := strings.LastIndex(filepath.Base(path), "-") + if lastHyphenIndex <= 0 { + return linter, fmt.Errorf("unable to parse chart archive %q, missing '-'", filepath.Base(path)) + } + base := filepath.Base(path)[:lastHyphenIndex] + chartPath = filepath.Join(tempDir, base) + } else { + chartPath = path + } + + // Guard: Error out of this is not a chart. + if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil { + return linter, errLintNoChart + } + + return lint.All(chartPath, vals, namespace, strict), nil +} + +func (l *lintCmd) vals() ([]byte, error) { + base := map[string]interface{}{} + + // User specified a values files via -f/--values + for _, filePath := range l.valueFiles { + currentMap := map[string]interface{}{} + bytes, err := ioutil.ReadFile(filePath) + if err != nil { + return []byte{}, err + } + + if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { + return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err) + } + // Merge with the previous map + base = mergeValues(base, currentMap) + } + + // User specified a value via --set + for _, value := range l.values { + if err := strvals.ParseInto(value, base); err != nil { + return []byte{}, fmt.Errorf("failed parsing --set data: %s", err) + } + } + + // User specified a value via --set-string + for _, value := range l.sValues { + if err := strvals.ParseIntoString(value, base); err != nil { + return []byte{}, fmt.Errorf("failed parsing --set-string data: %s", err) + } + } + + return yaml.Marshal(base) +} diff --git a/pkg/helm/lint_test.go b/pkg/helm/lint_test.go new file mode 100644 index 000000000..2c2b8912e --- /dev/null +++ b/pkg/helm/lint_test.go @@ -0,0 +1,54 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "testing" +) + +var ( + values = []byte{} + namespace = "testNamespace" + strict = false + archivedChartPath = "testdata/testcharts/compressedchart-0.1.0.tgz" + archivedChartPathWithHyphens = "testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz" + invalidArchivedChartPath = "testdata/testcharts/invalidcompressedchart0.1.0.tgz" + chartDirPath = "testdata/testcharts/decompressedchart/" + chartMissingManifest = "testdata/testcharts/chart-missing-manifest" +) + +func TestLintChart(t *testing.T) { + if _, err := lintChart(chartDirPath, values, namespace, strict); err != nil { + t.Errorf("%s", err) + } + + if _, err := lintChart(archivedChartPath, values, namespace, strict); err != nil { + t.Errorf("%s", err) + } + + if _, err := lintChart(archivedChartPathWithHyphens, values, namespace, strict); err != nil { + t.Errorf("%s", err) + } + + if _, err := lintChart(invalidArchivedChartPath, values, namespace, strict); err == nil { + t.Errorf("Expected a chart parsing error") + } + + if _, err := lintChart(chartMissingManifest, values, namespace, strict); err == nil { + t.Errorf("Expected a chart parsing error") + } +} diff --git a/pkg/helm/list.go b/pkg/helm/list.go new file mode 100644 index 000000000..a8999d36a --- /dev/null +++ b/pkg/helm/list.go @@ -0,0 +1,254 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "strings" + + "github.com/gosuri/uitable" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/proto/hapi/services" + "k8s.io/helm/pkg/timeconv" +) + +var listHelp = ` +This command lists all of the releases. + +By default, it lists only releases that are deployed or failed. Flags like +'--deleted' and '--all' will alter this behavior. Such flags can be combined: +'--deleted --failed'. + +By default, items are sorted alphabetically. Use the '-d' flag to sort by +release date. + +If an argument is provided, it will be treated as a filter. Filters are +regular expressions (Perl compatible) that are applied to the list of releases. +Only items that match the filter will be returned. + + $ helm list 'ara[a-z]+' + NAME UPDATED CHART + maudlin-arachnid Mon May 9 16:07:08 2016 alpine-0.1.0 + +If no results are found, 'helm list' will exit 0, but with no output (or in +the case of no '-q' flag, only headers). + +By default, up to 256 items may be returned. To limit this, use the '--max' flag. +Setting '--max' to 0 will not return all results. Rather, it will return the +server's default, which may be much higher than 256. Pairing the '--max' +flag with the '--offset' flag allows you to page through results. +` + +type listCmd struct { + filter string + short bool + limit int + offset string + byDate bool + sortDesc bool + out io.Writer + all bool + deleted bool + deleting bool + deployed bool + failed bool + namespace string + superseded bool + pending bool + client helm.Interface + colWidth uint +} + +func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { + list := &listCmd{ + out: out, + client: client, + } + + cmd := &cobra.Command{ + Use: "list [flags] [FILTER]", + Short: "list releases", + Long: listHelp, + Aliases: []string{"ls"}, + PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + list.filter = strings.Join(args, " ") + } + if list.client == nil { + list.client = newClient() + } + return list.run() + }, + } + + f := cmd.Flags() + f.BoolVarP(&list.short, "short", "q", false, "output short (quiet) listing format") + f.BoolVarP(&list.byDate, "date", "d", false, "sort by release date") + f.BoolVarP(&list.sortDesc, "reverse", "r", false, "reverse the sort order") + f.IntVarP(&list.limit, "max", "m", 256, "maximum number of releases to fetch") + f.StringVarP(&list.offset, "offset", "o", "", "next release name in the list, used to offset from start value") + f.BoolVarP(&list.all, "all", "a", false, "show all releases, not just the ones marked DEPLOYED") + f.BoolVar(&list.deleted, "deleted", false, "show deleted releases") + f.BoolVar(&list.deleting, "deleting", false, "show releases that are currently being deleted") + f.BoolVar(&list.deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled") + f.BoolVar(&list.failed, "failed", false, "show failed releases") + f.BoolVar(&list.pending, "pending", false, "show pending releases") + f.StringVar(&list.namespace, "namespace", "", "show releases within a specific namespace") + f.UintVar(&list.colWidth, "col-width", 60, "specifies the max column width of output") + + // TODO: Do we want this as a feature of 'helm list'? + //f.BoolVar(&list.superseded, "history", true, "show historical releases") + + return cmd +} + +func (l *listCmd) run() error { + sortBy := services.ListSort_NAME + if l.byDate { + sortBy = services.ListSort_LAST_RELEASED + } + + sortOrder := services.ListSort_ASC + if l.sortDesc { + sortOrder = services.ListSort_DESC + } + + stats := l.statusCodes() + + res, err := l.client.ListReleases( + helm.ReleaseListLimit(l.limit), + helm.ReleaseListOffset(l.offset), + helm.ReleaseListFilter(l.filter), + helm.ReleaseListSort(int32(sortBy)), + helm.ReleaseListOrder(int32(sortOrder)), + helm.ReleaseListStatuses(stats), + helm.ReleaseListNamespace(l.namespace), + ) + + if err != nil { + return prettyError(err) + } + + if len(res.GetReleases()) == 0 { + return nil + } + + if res.Next != "" && !l.short { + fmt.Fprintf(l.out, "\tnext: %s\n", res.Next) + } + + rels := filterList(res.Releases) + + if l.short { + for _, r := range rels { + fmt.Fprintln(l.out, r.Name) + } + return nil + } + fmt.Fprintln(l.out, formatList(rels, l.colWidth)) + return nil +} + +// filterList returns a list scrubbed of old releases. +func filterList(rels []*release.Release) []*release.Release { + idx := map[string]int32{} + + for _, r := range rels { + name, version := r.GetName(), r.GetVersion() + if max, ok := idx[name]; ok { + // check if we have a greater version already + if max > version { + continue + } + } + idx[name] = version + } + + uniq := make([]*release.Release, 0, len(idx)) + for _, r := range rels { + if idx[r.GetName()] == r.GetVersion() { + uniq = append(uniq, r) + } + } + return uniq +} + +// statusCodes gets the list of status codes that are to be included in the results. +func (l *listCmd) statusCodes() []release.Status_Code { + if l.all { + return []release.Status_Code{ + release.Status_UNKNOWN, + release.Status_DEPLOYED, + release.Status_DELETED, + release.Status_DELETING, + release.Status_FAILED, + release.Status_PENDING_INSTALL, + release.Status_PENDING_UPGRADE, + release.Status_PENDING_ROLLBACK, + } + } + status := []release.Status_Code{} + if l.deployed { + status = append(status, release.Status_DEPLOYED) + } + if l.deleted { + status = append(status, release.Status_DELETED) + } + if l.deleting { + status = append(status, release.Status_DELETING) + } + if l.failed { + status = append(status, release.Status_FAILED) + } + if l.superseded { + status = append(status, release.Status_SUPERSEDED) + } + if l.pending { + status = append(status, release.Status_PENDING_INSTALL, release.Status_PENDING_UPGRADE, release.Status_PENDING_ROLLBACK) + } + + // Default case. + if len(status) == 0 { + status = append(status, release.Status_DEPLOYED, release.Status_FAILED) + } + return status +} + +func formatList(rels []*release.Release, colWidth uint) string { + table := uitable.New() + + table.MaxColWidth = colWidth + table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "NAMESPACE") + for _, r := range rels { + md := r.GetChart().GetMetadata() + c := fmt.Sprintf("%s-%s", md.GetName(), md.GetVersion()) + t := "-" + if tspb := r.GetInfo().GetLastDeployed(); tspb != nil { + t = timeconv.String(tspb) + } + s := r.GetInfo().GetStatus().GetCode().String() + v := r.GetVersion() + n := r.GetNamespace() + table.AddRow(r.GetName(), v, t, s, c, n) + } + return table.String() +} diff --git a/pkg/helm/list_test.go b/pkg/helm/list_test.go new file mode 100644 index 000000000..b8468b6d9 --- /dev/null +++ b/pkg/helm/list_test.go @@ -0,0 +1,128 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestListCmd(t *testing.T) { + tests := []releaseCase{ + { + name: "with a release", + rels: []*release.Release{ + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"}), + }, + expected: "thomas-guide", + }, + { + name: "list", + rels: []*release.Release{ + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas"}), + }, + expected: "NAME \tREVISION\tUPDATED \tSTATUS \tCHART \tNAMESPACE\natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\tdefault \n", + }, + { + name: "list, one deployed, one failed", + flags: []string{"-q"}, + rels: []*release.Release{ + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", StatusCode: release.Status_FAILED}), + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), + }, + expected: "thomas-guide\natlas-guide", + }, + { + name: "with a release, multiple flags", + flags: []string{"--deleted", "--deployed", "--failed", "-q"}, + rels: []*release.Release{ + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", StatusCode: release.Status_DELETED}), + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), + }, + // Note: We're really only testing that the flags parsed correctly. Which results are returned + // depends on the backend. And until pkg/helm is done, we can't mock this. + expected: "thomas-guide\natlas-guide", + }, + { + name: "with a release, multiple flags", + flags: []string{"--all", "-q"}, + rels: []*release.Release{ + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", StatusCode: release.Status_DELETED}), + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), + }, + // See note on previous test. + expected: "thomas-guide\natlas-guide", + }, + { + name: "with a release, multiple flags, deleting", + flags: []string{"--all", "-q"}, + rels: []*release.Release{ + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", StatusCode: release.Status_DELETING}), + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), + }, + // See note on previous test. + expected: "thomas-guide\natlas-guide", + }, + { + name: "namespace defined, multiple flags", + flags: []string{"--all", "-q", "--namespace test123"}, + rels: []*release.Release{ + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Namespace: "test123"}), + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Namespace: "test321"}), + }, + // See note on previous test. + expected: "thomas-guide", + }, + { + name: "with a pending release, multiple flags", + flags: []string{"--all", "-q"}, + rels: []*release.Release{ + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", StatusCode: release.Status_PENDING_INSTALL}), + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), + }, + expected: "thomas-guide\natlas-guide", + }, + { + name: "with a pending release, pending flag", + flags: []string{"--pending", "-q"}, + rels: []*release.Release{ + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", StatusCode: release.Status_PENDING_INSTALL}), + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "wild-idea", StatusCode: release.Status_PENDING_UPGRADE}), + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "crazy-maps", StatusCode: release.Status_PENDING_ROLLBACK}), + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), + }, + expected: "thomas-guide\nwild-idea\ncrazy-maps", + }, + { + name: "with old releases", + rels: []*release.Release{ + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"}), + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", StatusCode: release.Status_FAILED}), + }, + expected: "thomas-guide", + }, + } + + runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newListCmd(c, out) + }) +} diff --git a/pkg/helm/load_plugins.go b/pkg/helm/load_plugins.go new file mode 100644 index 000000000..ee769d497 --- /dev/null +++ b/pkg/helm/load_plugins.go @@ -0,0 +1,160 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/plugin" +) + +// loadPlugins loads plugins into the command list. +// +// This follows a different pattern than the other commands because it has +// to inspect its environment and then add commands to the base command +// as it finds them. +func loadPlugins(baseCmd *cobra.Command, out io.Writer) { + + // If HELM_NO_PLUGINS is set to 1, do not load plugins. + if os.Getenv("HELM_NO_PLUGINS") == "1" { + return + } + + // debug("HELM_PLUGIN_DIRS=%s", settings.PluginDirs()) + found, err := findPlugins(settings.PluginDirs()) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err) + return + } + + processParent := func(cmd *cobra.Command, args []string) ([]string, error) { + k, u := manuallyProcessArgs(args) + if err := cmd.Parent().ParseFlags(k); err != nil { + return nil, err + } + return u, nil + } + + // Now we create commands for all of these. + for _, plug := range found { + plug := plug + md := plug.Metadata + if md.Usage == "" { + md.Usage = fmt.Sprintf("the %q plugin", md.Name) + } + + c := &cobra.Command{ + Use: md.Name, + Short: md.Usage, + Long: md.Description, + RunE: func(cmd *cobra.Command, args []string) error { + u, err := processParent(cmd, args) + if err != nil { + return err + } + + // Call setupEnv before PrepareCommand because + // PrepareCommand uses os.ExpandEnv and expects the + // setupEnv vars. + plugin.SetupPluginEnv(settings, md.Name, plug.Dir) + main, argv := plug.PrepareCommand(u) + + prog := exec.Command(main, argv...) + prog.Env = os.Environ() + prog.Stdin = os.Stdin + prog.Stdout = out + prog.Stderr = os.Stderr + if err := prog.Run(); err != nil { + if eerr, ok := err.(*exec.ExitError); ok { + os.Stderr.Write(eerr.Stderr) + return fmt.Errorf("plugin %q exited with error", md.Name) + } + return err + } + return nil + }, + // This passes all the flags to the subcommand. + DisableFlagParsing: true, + } + + if md.UseTunnel { + c.PreRunE = func(cmd *cobra.Command, args []string) error { + // Parse the parent flag, but not the local flags. + if _, err := processParent(cmd, args); err != nil { + return err + } + return setupConnection() + } + } + + // TODO: Make sure a command with this name does not already exist. + baseCmd.AddCommand(c) + } +} + +// manuallyProcessArgs processes an arg array, removing special args. +// +// Returns two sets of args: known and unknown (in that order) +func manuallyProcessArgs(args []string) ([]string, []string) { + known := []string{} + unknown := []string{} + kvargs := []string{"--host", "--kube-context", "--home", "--tiller-namespace"} + knownArg := func(a string) bool { + for _, pre := range kvargs { + if strings.HasPrefix(a, pre+"=") { + return true + } + } + return false + } + for i := 0; i < len(args); i++ { + switch a := args[i]; a { + case "--debug": + known = append(known, a) + case "--host", "--kube-context", "--home": + known = append(known, a, args[i+1]) + i++ + default: + if knownArg(a) { + known = append(known, a) + continue + } + unknown = append(unknown, a) + } + } + return known, unknown +} + +// findPlugins returns a list of YAML files that describe plugins. +func findPlugins(plugdirs string) ([]*plugin.Plugin, error) { + found := []*plugin.Plugin{} + // Let's get all UNIXy and allow path separators + for _, p := range filepath.SplitList(plugdirs) { + matches, err := plugin.LoadAll(p) + if err != nil { + return matches, err + } + found = append(found, matches...) + } + return found, nil +} diff --git a/pkg/helm/package.go b/pkg/helm/package.go new file mode 100644 index 000000000..478422d4b --- /dev/null +++ b/pkg/helm/package.go @@ -0,0 +1,237 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "syscall" + + "github.com/Masterminds/semver" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/downloader" + "k8s.io/helm/pkg/getter" + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/provenance" + "k8s.io/helm/pkg/repo" +) + +const packageDesc = ` +This command packages a chart into a versioned chart archive file. If a path +is given, this will look at that path for a chart (which must contain a +Chart.yaml file) and then package that directory. + +If no path is given, this will look in the present working directory for a +Chart.yaml file, and (if found) build the current directory into a chart. + +Versioned chart archives are used by Helm package repositories. +` + +type packageCmd struct { + save bool + sign bool + path string + key string + keyring string + version string + appVersion string + destination string + dependencyUpdate bool + + out io.Writer + home helmpath.Home +} + +func newPackageCmd(out io.Writer) *cobra.Command { + pkg := &packageCmd{out: out} + + cmd := &cobra.Command{ + Use: "package [flags] [CHART_PATH] [...]", + Short: "package a chart directory into a chart archive", + Long: packageDesc, + RunE: func(cmd *cobra.Command, args []string) error { + pkg.home = settings.Home + if len(args) == 0 { + return fmt.Errorf("need at least one argument, the path to the chart") + } + if pkg.sign { + if pkg.key == "" { + return errors.New("--key is required for signing a package") + } + if pkg.keyring == "" { + return errors.New("--keyring is required for signing a package") + } + } + for i := 0; i < len(args); i++ { + pkg.path = args[i] + if err := pkg.run(); err != nil { + return err + } + } + return nil + }, + } + + f := cmd.Flags() + f.BoolVar(&pkg.save, "save", true, "save packaged chart to local chart repository") + f.BoolVar(&pkg.sign, "sign", false, "use a PGP private key to sign this package") + f.StringVar(&pkg.key, "key", "", "name of the key to use when signing. Used if --sign is true") + f.StringVar(&pkg.keyring, "keyring", defaultKeyring(), "location of a public keyring") + f.StringVar(&pkg.version, "version", "", "set the version on the chart to this semver version") + f.StringVar(&pkg.appVersion, "app-version", "", "set the appVersion on the chart to this version") + f.StringVarP(&pkg.destination, "destination", "d", ".", "location to write the chart.") + f.BoolVarP(&pkg.dependencyUpdate, "dependency-update", "u", false, `update dependencies from "requirements.yaml" to dir "charts/" before packaging`) + + return cmd +} + +func (p *packageCmd) run() error { + path, err := filepath.Abs(p.path) + if err != nil { + return err + } + + if p.dependencyUpdate { + downloadManager := &downloader.Manager{ + Out: p.out, + ChartPath: path, + HelmHome: settings.Home, + Keyring: p.keyring, + Getters: getter.All(settings), + Debug: settings.Debug, + } + + if err := downloadManager.Update(); err != nil { + return err + } + } + + ch, err := chartutil.LoadDir(path) + if err != nil { + return err + } + + // If version is set, modify the version. + if len(p.version) != 0 { + if err := setVersion(ch, p.version); err != nil { + return err + } + debug("Setting version to %s", p.version) + } + + if p.appVersion != "" { + ch.Metadata.AppVersion = p.appVersion + debug("Setting appVersion to %s", p.appVersion) + } + + if filepath.Base(path) != ch.Metadata.Name { + return fmt.Errorf("directory name (%s) and Chart.yaml name (%s) must match", filepath.Base(path), ch.Metadata.Name) + } + + if reqs, err := chartutil.LoadRequirements(ch); err == nil { + if err := checkDependencies(ch, reqs); err != nil { + return err + } + } else { + if err != chartutil.ErrRequirementsNotFound { + return err + } + } + + var dest string + if p.destination == "." { + // Save to the current working directory. + dest, err = os.Getwd() + if err != nil { + return err + } + } else { + // Otherwise save to set destination + dest = p.destination + } + + name, err := chartutil.Save(ch, dest) + if err == nil { + fmt.Fprintf(p.out, "Successfully packaged chart and saved it to: %s\n", name) + } else { + return fmt.Errorf("Failed to save: %s", err) + } + + // Save to $HELM_HOME/local directory. This is second, because we don't want + // the case where we saved here, but didn't save to the default destination. + if p.save { + lr := p.home.LocalRepository() + if err := repo.AddChartToLocalRepo(ch, lr); err != nil { + return err + } + debug("Successfully saved %s to %s\n", name, lr) + } + + if p.sign { + err = p.clearsign(name) + } + + return err +} + +func setVersion(ch *chart.Chart, ver string) error { + // Verify that version is a SemVer, and error out if it is not. + if _, err := semver.NewVersion(ver); err != nil { + return err + } + + // Set the version field on the chart. + ch.Metadata.Version = ver + return nil +} + +func (p *packageCmd) clearsign(filename string) error { + // Load keyring + signer, err := provenance.NewFromKeyring(p.keyring, p.key) + if err != nil { + return err + } + + if err := signer.DecryptKey(promptUser); err != nil { + return err + } + + sig, err := signer.ClearSign(filename) + if err != nil { + return err + } + + debug(sig) + + return ioutil.WriteFile(filename+".prov", []byte(sig), 0755) +} + +// promptUser implements provenance.PassphraseFetcher +func promptUser(name string) ([]byte, error) { + fmt.Printf("Password for key %q > ", name) + pw, err := terminal.ReadPassword(int(syscall.Stdin)) + fmt.Println() + return pw, err +} diff --git a/pkg/helm/package_test.go b/pkg/helm/package_test.go new file mode 100644 index 000000000..3f54c725c --- /dev/null +++ b/pkg/helm/package_test.go @@ -0,0 +1,253 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/proto/hapi/chart" +) + +func TestSetVersion(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "prow", + Version: "0.0.1", + }, + } + expect := "1.2.3-beta.5" + if err := setVersion(c, expect); err != nil { + t.Fatal(err) + } + + if c.Metadata.Version != expect { + t.Errorf("Expected %q, got %q", expect, c.Metadata.Version) + } + + if err := setVersion(c, "monkeyface"); err == nil { + t.Error("Expected bogus version to return an error.") + } +} + +func TestPackage(t *testing.T) { + + tests := []struct { + name string + flags map[string]string + args []string + expect string + hasfile string + err bool + }{ + { + name: "package without chart path", + args: []string{}, + flags: map[string]string{}, + expect: "need at least one argument, the path to the chart", + err: true, + }, + { + name: "package --sign, no --key", + args: []string{"testdata/testcharts/alpine"}, + flags: map[string]string{"sign": "1"}, + expect: "key is required for signing a package", + err: true, + }, + { + name: "package --sign, no --keyring", + args: []string{"testdata/testcharts/alpine"}, + flags: map[string]string{"sign": "1", "key": "nosuchkey", "keyring": ""}, + expect: "keyring is required for signing a package", + err: true, + }, + { + name: "package testdata/testcharts/alpine, no save", + args: []string{"testdata/testcharts/alpine"}, + flags: map[string]string{"save": "0"}, + expect: "", + hasfile: "alpine-0.1.0.tgz", + }, + { + name: "package testdata/testcharts/alpine", + args: []string{"testdata/testcharts/alpine"}, + expect: "", + hasfile: "alpine-0.1.0.tgz", + }, + { + name: "package --destination toot", + args: []string{"testdata/testcharts/alpine"}, + flags: map[string]string{"destination": "toot"}, + expect: "", + hasfile: "toot/alpine-0.1.0.tgz", + }, + { + name: "package --destination does-not-exist", + args: []string{"testdata/testcharts/alpine"}, + flags: map[string]string{"destination": "does-not-exist"}, + expect: "stat does-not-exist: no such file or directory", + err: true, + }, + { + name: "package --sign --key=KEY --keyring=KEYRING testdata/testcharts/alpine", + args: []string{"testdata/testcharts/alpine"}, + flags: map[string]string{"sign": "1", "keyring": "testdata/helm-test-key.secret", "key": "helm-test"}, + expect: "", + hasfile: "alpine-0.1.0.tgz", + }, + { + name: "package testdata/testcharts/chart-missing-deps", + args: []string{"testdata/testcharts/chart-missing-deps"}, + hasfile: "chart-missing-deps-0.1.0.tgz", + err: true, + }, + } + + // Because these tests are destructive, we run them in a tempdir. + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + tmp, err := ioutil.TempDir("", "helm-package-test-") + if err != nil { + t.Fatal(err) + } + + t.Logf("Running tests in %s", tmp) + if err := os.Chdir(tmp); err != nil { + t.Fatal(err) + } + + if err := os.Mkdir("toot", 0777); err != nil { + t.Fatal(err) + } + + ensureTestHome(helmpath.Home(tmp), t) + cleanup := resetEnv() + defer func() { + os.Chdir(origDir) + os.RemoveAll(tmp) + cleanup() + }() + + settings.Home = helmpath.Home(tmp) + + for _, tt := range tests { + buf := bytes.NewBuffer(nil) + c := newPackageCmd(buf) + + // This is an unfortunate byproduct of the tmpdir + if v, ok := tt.flags["keyring"]; ok && len(v) > 0 { + tt.flags["keyring"] = filepath.Join(origDir, v) + } + + setFlags(c, tt.flags) + re := regexp.MustCompile(tt.expect) + + adjustedArgs := make([]string, len(tt.args)) + for i, f := range tt.args { + adjustedArgs[i] = filepath.Join(origDir, f) + } + + err := c.RunE(c, adjustedArgs) + if err != nil { + if tt.err && re.MatchString(err.Error()) { + continue + } + t.Errorf("%q: expected error %q, got %q", tt.name, tt.expect, err) + continue + } + + if !re.Match(buf.Bytes()) { + t.Errorf("%q: expected output %q, got %q", tt.name, tt.expect, buf.String()) + } + + if len(tt.hasfile) > 0 { + if fi, err := os.Stat(tt.hasfile); err != nil { + t.Errorf("%q: expected file %q, got err %q", tt.name, tt.hasfile, err) + } else if fi.Size() == 0 { + t.Errorf("%q: file %q has zero bytes.", tt.name, tt.hasfile) + } + } + + if v, ok := tt.flags["sign"]; ok && v == "1" { + if fi, err := os.Stat(tt.hasfile + ".prov"); err != nil { + t.Errorf("%q: expected provenance file", tt.name) + } else if fi.Size() == 0 { + t.Errorf("%q: provenance file is empty", tt.name) + } + } + } +} + +func TestSetAppVersion(t *testing.T) { + var ch *chart.Chart + expectedAppVersion := "app-version-foo" + tmp, _ := ioutil.TempDir("", "helm-package-app-version-") + + thome, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + cleanup := resetEnv() + defer func() { + os.RemoveAll(tmp) + os.RemoveAll(thome.String()) + cleanup() + }() + + settings.Home = helmpath.Home(thome) + + c := newPackageCmd(&bytes.Buffer{}) + flags := map[string]string{ + "destination": tmp, + "app-version": expectedAppVersion, + } + setFlags(c, flags) + err = c.RunE(c, []string{"testdata/testcharts/alpine"}) + if err != nil { + t.Errorf("unexpected error %q", err) + } + + chartPath := filepath.Join(tmp, "alpine-0.1.0.tgz") + if fi, err := os.Stat(chartPath); err != nil { + t.Errorf("expected file %q, got err %q", chartPath, err) + } else if fi.Size() == 0 { + t.Errorf("file %q has zero bytes.", chartPath) + } + ch, err = chartutil.Load(chartPath) + if err != nil { + t.Errorf("unexpected error loading packaged chart: %v", err) + } + if ch.Metadata.AppVersion != expectedAppVersion { + t.Errorf("expected app-version %q, found %q", expectedAppVersion, ch.Metadata.AppVersion) + } +} + +func setFlags(cmd *cobra.Command, flags map[string]string) { + dest := cmd.Flags() + for f, v := range flags { + dest.Set(f, v) + } +} diff --git a/pkg/helm/plugin.go b/pkg/helm/plugin.go new file mode 100644 index 000000000..d176ebd52 --- /dev/null +++ b/pkg/helm/plugin.go @@ -0,0 +1,72 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "os" + "os/exec" + + "k8s.io/helm/pkg/plugin" + + "github.com/spf13/cobra" +) + +const pluginHelp = ` +Manage client-side Helm plugins. +` + +func newPluginCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "plugin", + Short: "add, list, or remove Helm plugins", + Long: pluginHelp, + } + cmd.AddCommand( + newPluginInstallCmd(out), + newPluginListCmd(out), + newPluginRemoveCmd(out), + newPluginUpdateCmd(out), + ) + return cmd +} + +// runHook will execute a plugin hook. +func runHook(p *plugin.Plugin, event string) error { + hook := p.Metadata.Hooks.Get(event) + if hook == "" { + return nil + } + + prog := exec.Command("sh", "-c", hook) + // TODO make this work on windows + // I think its ... ¯\_(ツ)_/¯ + // prog := exec.Command("cmd", "/C", p.Metadata.Hooks.Install()) + + debug("running %s hook: %s", event, prog) + + plugin.SetupPluginEnv(settings, p.Metadata.Name, p.Dir) + prog.Stdout, prog.Stderr = os.Stdout, os.Stderr + if err := prog.Run(); err != nil { + if eerr, ok := err.(*exec.ExitError); ok { + os.Stderr.Write(eerr.Stderr) + return fmt.Errorf("plugin %s hook for %q exited with error", event, p.Metadata.Name) + } + return err + } + return nil +} diff --git a/pkg/helm/plugin_install.go b/pkg/helm/plugin_install.go new file mode 100644 index 000000000..116697c82 --- /dev/null +++ b/pkg/helm/plugin_install.go @@ -0,0 +1,92 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/plugin" + "k8s.io/helm/pkg/plugin/installer" + + "github.com/spf13/cobra" +) + +type pluginInstallCmd struct { + source string + version string + home helmpath.Home + out io.Writer +} + +const pluginInstallDesc = ` +This command allows you to install a plugin from a url to a VCS repo or a local path. + +Example usage: + $ helm plugin install https://github.com/technosophos/helm-template +` + +func newPluginInstallCmd(out io.Writer) *cobra.Command { + pcmd := &pluginInstallCmd{out: out} + cmd := &cobra.Command{ + Use: "install [options] ...", + Short: "install one or more Helm plugins", + Long: pluginInstallDesc, + PreRunE: func(cmd *cobra.Command, args []string) error { + return pcmd.complete(args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return pcmd.run() + }, + } + cmd.Flags().StringVar(&pcmd.version, "version", "", "specify a version constraint. If this is not specified, the latest version is installed") + return cmd +} + +func (pcmd *pluginInstallCmd) complete(args []string) error { + if err := checkArgsLength(len(args), "plugin"); err != nil { + return err + } + pcmd.source = args[0] + pcmd.home = settings.Home + return nil +} + +func (pcmd *pluginInstallCmd) run() error { + installer.Debug = settings.Debug + + i, err := installer.NewForSource(pcmd.source, pcmd.version, pcmd.home) + if err != nil { + return err + } + if err := installer.Install(i); err != nil { + return err + } + + debug("loading plugin from %s", i.Path()) + p, err := plugin.LoadDir(i.Path()) + if err != nil { + return err + } + + if err := runHook(p, plugin.Install); err != nil { + return err + } + + fmt.Fprintf(pcmd.out, "Installed plugin: %s\n", p.Metadata.Name) + return nil +} diff --git a/pkg/helm/plugin_list.go b/pkg/helm/plugin_list.go new file mode 100644 index 000000000..f30e337b7 --- /dev/null +++ b/pkg/helm/plugin_list.go @@ -0,0 +1,60 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + + "k8s.io/helm/pkg/helm/helmpath" + + "github.com/gosuri/uitable" + "github.com/spf13/cobra" +) + +type pluginListCmd struct { + home helmpath.Home + out io.Writer +} + +func newPluginListCmd(out io.Writer) *cobra.Command { + pcmd := &pluginListCmd{out: out} + cmd := &cobra.Command{ + Use: "list", + Short: "list installed Helm plugins", + RunE: func(cmd *cobra.Command, args []string) error { + pcmd.home = settings.Home + return pcmd.run() + }, + } + return cmd +} + +func (pcmd *pluginListCmd) run() error { + debug("pluginDirs: %s", settings.PluginDirs()) + plugins, err := findPlugins(settings.PluginDirs()) + if err != nil { + return err + } + + table := uitable.New() + table.AddRow("NAME", "VERSION", "DESCRIPTION") + for _, p := range plugins { + table.AddRow(p.Metadata.Name, p.Metadata.Version, p.Metadata.Description) + } + fmt.Fprintln(pcmd.out, table) + return nil +} diff --git a/pkg/helm/plugin_remove.go b/pkg/helm/plugin_remove.go new file mode 100644 index 000000000..cf3f124eb --- /dev/null +++ b/pkg/helm/plugin_remove.go @@ -0,0 +1,99 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "errors" + "fmt" + "io" + "os" + "strings" + + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/plugin" + + "github.com/spf13/cobra" +) + +type pluginRemoveCmd struct { + names []string + home helmpath.Home + out io.Writer +} + +func newPluginRemoveCmd(out io.Writer) *cobra.Command { + pcmd := &pluginRemoveCmd{out: out} + cmd := &cobra.Command{ + Use: "remove ...", + Short: "remove one or more Helm plugins", + PreRunE: func(cmd *cobra.Command, args []string) error { + return pcmd.complete(args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return pcmd.run() + }, + } + return cmd +} + +func (pcmd *pluginRemoveCmd) complete(args []string) error { + if len(args) == 0 { + return errors.New("please provide plugin name to remove") + } + pcmd.names = args + pcmd.home = settings.Home + return nil +} + +func (pcmd *pluginRemoveCmd) run() error { + debug("loading installed plugins from %s", settings.PluginDirs()) + plugins, err := findPlugins(settings.PluginDirs()) + if err != nil { + return err + } + var errorPlugins []string + for _, name := range pcmd.names { + if found := findPlugin(plugins, name); found != nil { + if err := removePlugin(found); err != nil { + errorPlugins = append(errorPlugins, fmt.Sprintf("Failed to remove plugin %s, got error (%v)", name, err)) + } else { + fmt.Fprintf(pcmd.out, "Removed plugin: %s\n", name) + } + } else { + errorPlugins = append(errorPlugins, fmt.Sprintf("Plugin: %s not found", name)) + } + } + if len(errorPlugins) > 0 { + return fmt.Errorf(strings.Join(errorPlugins, "\n")) + } + return nil +} + +func removePlugin(p *plugin.Plugin) error { + if err := os.Remove(p.Dir); err != nil { + return err + } + return runHook(p, plugin.Delete) +} + +func findPlugin(plugins []*plugin.Plugin, name string) *plugin.Plugin { + for _, p := range plugins { + if p.Metadata.Name == name { + return p + } + } + return nil +} diff --git a/pkg/helm/plugin_test.go b/pkg/helm/plugin_test.go new file mode 100644 index 000000000..5725eaeed --- /dev/null +++ b/pkg/helm/plugin_test.go @@ -0,0 +1,187 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/plugin" + + "github.com/spf13/cobra" +) + +func TestManuallyProcessArgs(t *testing.T) { + input := []string{ + "--debug", + "--foo", "bar", + "--host", "example.com", + "--kube-context", "test1", + "--home=/tmp", + "--tiller-namespace=hello", + "command", + } + + expectKnown := []string{ + "--debug", "--host", "example.com", "--kube-context", "test1", "--home=/tmp", "--tiller-namespace=hello", + } + + expectUnknown := []string{ + "--foo", "bar", "command", + } + + known, unknown := manuallyProcessArgs(input) + + for i, k := range known { + if k != expectKnown[i] { + t.Errorf("expected known flag %d to be %q, got %q", i, expectKnown[i], k) + } + } + for i, k := range unknown { + if k != expectUnknown[i] { + t.Errorf("expected unknown flag %d to be %q, got %q", i, expectUnknown[i], k) + } + } + +} + +func TestLoadPlugins(t *testing.T) { + cleanup := resetEnv() + defer cleanup() + + settings.Home = "testdata/helmhome" + + os.Setenv("HELM_HOME", settings.Home.String()) + hh := settings.Home + + out := bytes.NewBuffer(nil) + cmd := &cobra.Command{} + loadPlugins(cmd, out) + + envs := strings.Join([]string{ + "fullenv", + hh.Plugins() + "/fullenv", + hh.Plugins(), + hh.String(), + hh.Repository(), + hh.RepositoryFile(), + hh.Cache(), + hh.LocalRepository(), + os.Args[0], + }, "\n") + + // Test that the YAML file was correctly converted to a command. + tests := []struct { + use string + short string + long string + expect string + args []string + }{ + {"args", "echo args", "This echos args", "-a -b -c\n", []string{"-a", "-b", "-c"}}, + {"echo", "echo stuff", "This echos stuff", "hello\n", []string{}}, + {"env", "env stuff", "show the env", hh.String() + "\n", []string{}}, + {"fullenv", "show env vars", "show all env vars", envs + "\n", []string{}}, + } + + plugins := cmd.Commands() + + if len(plugins) != len(tests) { + t.Fatalf("Expected %d plugins, got %d", len(tests), len(plugins)) + } + + for i := 0; i < len(plugins); i++ { + out.Reset() + tt := tests[i] + pp := plugins[i] + if pp.Use != tt.use { + t.Errorf("%d: Expected Use=%q, got %q", i, tt.use, pp.Use) + } + if pp.Short != tt.short { + t.Errorf("%d: Expected Use=%q, got %q", i, tt.short, pp.Short) + } + if pp.Long != tt.long { + t.Errorf("%d: Expected Use=%q, got %q", i, tt.long, pp.Long) + } + + // Currently, plugins assume a Linux subsystem. Skip the execution + // tests until this is fixed + if runtime.GOOS != "windows" { + if err := pp.RunE(pp, tt.args); err != nil { + t.Errorf("Error running %s: %s", tt.use, err) + } + if out.String() != tt.expect { + t.Errorf("Expected %s to output:\n%s\ngot\n%s", tt.use, tt.expect, out.String()) + } + } + } +} + +func TestLoadPlugins_HelmNoPlugins(t *testing.T) { + cleanup := resetEnv() + defer cleanup() + + settings.Home = "testdata/helmhome" + + os.Setenv("HELM_NO_PLUGINS", "1") + + out := bytes.NewBuffer(nil) + cmd := &cobra.Command{} + loadPlugins(cmd, out) + plugins := cmd.Commands() + + if len(plugins) != 0 { + t.Fatalf("Expected 0 plugins, got %d", len(plugins)) + } +} + +func TestSetupEnv(t *testing.T) { + name := "pequod" + settings.Home = helmpath.Home("testdata/helmhome") + base := filepath.Join(settings.Home.Plugins(), name) + settings.Debug = true + defer func() { + settings.Debug = false + }() + + plugin.SetupPluginEnv(settings, name, base) + for _, tt := range []struct { + name string + expect string + }{ + {"HELM_PLUGIN_NAME", name}, + {"HELM_PLUGIN_DIR", base}, + {"HELM_PLUGIN", settings.Home.Plugins()}, + {"HELM_DEBUG", "1"}, + {"HELM_HOME", settings.Home.String()}, + {"HELM_PATH_REPOSITORY", settings.Home.Repository()}, + {"HELM_PATH_REPOSITORY_FILE", settings.Home.RepositoryFile()}, + {"HELM_PATH_CACHE", settings.Home.Cache()}, + {"HELM_PATH_LOCAL_REPOSITORY", settings.Home.LocalRepository()}, + {"HELM_PATH_STARTER", settings.Home.Starters()}, + {"TILLER_HOST", settings.TillerHost}, + {"TILLER_NAMESPACE", settings.TillerNamespace}, + } { + if got := os.Getenv(tt.name); got != tt.expect { + t.Errorf("Expected $%s=%q, got %q", tt.name, tt.expect, got) + } + } +} diff --git a/pkg/helm/plugin_update.go b/pkg/helm/plugin_update.go new file mode 100644 index 000000000..235e60518 --- /dev/null +++ b/pkg/helm/plugin_update.go @@ -0,0 +1,113 @@ +/* +Copyright 2017 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "errors" + "fmt" + "io" + "path/filepath" + "strings" + + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/plugin" + "k8s.io/helm/pkg/plugin/installer" + + "github.com/spf13/cobra" +) + +type pluginUpdateCmd struct { + names []string + home helmpath.Home + out io.Writer +} + +func newPluginUpdateCmd(out io.Writer) *cobra.Command { + pcmd := &pluginUpdateCmd{out: out} + cmd := &cobra.Command{ + Use: "update ...", + Short: "update one or more Helm plugins", + PreRunE: func(cmd *cobra.Command, args []string) error { + return pcmd.complete(args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return pcmd.run() + }, + } + return cmd +} + +func (pcmd *pluginUpdateCmd) complete(args []string) error { + if len(args) == 0 { + return errors.New("please provide plugin name to update") + } + pcmd.names = args + pcmd.home = settings.Home + return nil +} + +func (pcmd *pluginUpdateCmd) run() error { + installer.Debug = settings.Debug + debug("loading installed plugins from %s", settings.PluginDirs()) + plugins, err := findPlugins(settings.PluginDirs()) + if err != nil { + return err + } + var errorPlugins []string + + for _, name := range pcmd.names { + if found := findPlugin(plugins, name); found != nil { + if err := updatePlugin(found, pcmd.home); err != nil { + errorPlugins = append(errorPlugins, fmt.Sprintf("Failed to update plugin %s, got error (%v)", name, err)) + } else { + fmt.Fprintf(pcmd.out, "Updated plugin: %s\n", name) + } + } else { + errorPlugins = append(errorPlugins, fmt.Sprintf("Plugin: %s not found", name)) + } + } + if len(errorPlugins) > 0 { + return fmt.Errorf(strings.Join(errorPlugins, "\n")) + } + return nil +} + +func updatePlugin(p *plugin.Plugin, home helmpath.Home) error { + exactLocation, err := filepath.EvalSymlinks(p.Dir) + if err != nil { + return err + } + absExactLocation, err := filepath.Abs(exactLocation) + if err != nil { + return err + } + + i, err := installer.FindSource(absExactLocation, home) + if err != nil { + return err + } + if err := installer.Update(i); err != nil { + return err + } + + debug("loading plugin from %s", i.Path()) + updatedPlugin, err := plugin.LoadDir(i.Path()) + if err != nil { + return err + } + + return runHook(updatedPlugin, plugin.Update) +} diff --git a/pkg/helm/printer.go b/pkg/helm/printer.go new file mode 100644 index 000000000..fdc9b621b --- /dev/null +++ b/pkg/helm/printer.go @@ -0,0 +1,82 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "text/template" + "time" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/timeconv" +) + +var printReleaseTemplate = `REVISION: {{.Release.Version}} +RELEASED: {{.ReleaseDate}} +CHART: {{.Release.Chart.Metadata.Name}}-{{.Release.Chart.Metadata.Version}} +USER-SUPPLIED VALUES: +{{.Release.Config.Raw}} +COMPUTED VALUES: +{{.ComputedValues}} +HOOKS: +{{- range .Release.Hooks }} +--- +# {{.Name}} +{{.Manifest}} +{{- end }} +MANIFEST: +{{.Release.Manifest}} +` + +func printRelease(out io.Writer, rel *release.Release) error { + if rel == nil { + return nil + } + + cfg, err := chartutil.CoalesceValues(rel.Chart, rel.Config) + if err != nil { + return err + } + cfgStr, err := cfg.YAML() + if err != nil { + return err + } + + data := map[string]interface{}{ + "Release": rel, + "ComputedValues": cfgStr, + "ReleaseDate": timeconv.Format(rel.Info.LastDeployed, time.ANSIC), + } + return tpl(printReleaseTemplate, data, out) +} + +func tpl(t string, vals map[string]interface{}, out io.Writer) error { + tt, err := template.New("_").Parse(t) + if err != nil { + return err + } + return tt.Execute(out, vals) +} + +func debug(format string, args ...interface{}) { + if settings.Debug { + format = fmt.Sprintf("[debug] %s\n", format) + fmt.Printf(format, args...) + } +} diff --git a/pkg/helm/release_testing.go b/pkg/helm/release_testing.go new file mode 100644 index 000000000..631b3db5a --- /dev/null +++ b/pkg/helm/release_testing.go @@ -0,0 +1,110 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" +) + +const releaseTestDesc = ` +The test command runs the tests for a release. + +The argument this command takes is the name of a deployed release. +The tests to be run are defined in the chart that was installed. +` + +type releaseTestCmd struct { + name string + out io.Writer + client helm.Interface + timeout int64 + cleanup bool +} + +func newReleaseTestCmd(c helm.Interface, out io.Writer) *cobra.Command { + rlsTest := &releaseTestCmd{ + out: out, + client: c, + } + + cmd := &cobra.Command{ + Use: "test [RELEASE]", + Short: "test a release", + Long: releaseTestDesc, + PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkArgsLength(len(args), "release name"); err != nil { + return err + } + + rlsTest.name = args[0] + rlsTest.client = ensureHelmClient(rlsTest.client) + return rlsTest.run() + }, + } + + f := cmd.Flags() + f.Int64Var(&rlsTest.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") + f.BoolVar(&rlsTest.cleanup, "cleanup", false, "delete test pods upon completion") + + return cmd +} + +func (t *releaseTestCmd) run() (err error) { + c, errc := t.client.RunReleaseTest( + t.name, + helm.ReleaseTestTimeout(t.timeout), + helm.ReleaseTestCleanup(t.cleanup), + ) + testErr := &testErr{} + + for { + select { + case err := <-errc: + if prettyError(err) == nil && testErr.failed > 0 { + return testErr.Error() + } + return prettyError(err) + case res, ok := <-c: + if !ok { + break + } + + if res.Status == release.TestRun_FAILURE { + testErr.failed++ + } + + fmt.Fprintf(t.out, res.Msg+"\n") + + } + } + +} + +type testErr struct { + failed int +} + +func (err *testErr) Error() error { + return fmt.Errorf("%v test(s) failed", err.failed) +} diff --git a/pkg/helm/release_testing_test.go b/pkg/helm/release_testing_test.go new file mode 100644 index 000000000..edd11c94f --- /dev/null +++ b/pkg/helm/release_testing_test.go @@ -0,0 +1,106 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "testing" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestReleaseTesting(t *testing.T) { + tests := []struct { + name string + args []string + flags []string + responses map[string]release.TestRun_Status + fail bool + }{ + { + name: "basic test", + args: []string{"example-release"}, + flags: []string{}, + responses: map[string]release.TestRun_Status{"PASSED: green lights everywhere": release.TestRun_SUCCESS}, + fail: false, + }, + { + name: "test failure", + args: []string{"example-fail"}, + flags: []string{}, + responses: map[string]release.TestRun_Status{"FAILURE: red lights everywhere": release.TestRun_FAILURE}, + fail: true, + }, + { + name: "test unknown", + args: []string{"example-unknown"}, + flags: []string{}, + responses: map[string]release.TestRun_Status{"UNKNOWN: yellow lights everywhere": release.TestRun_UNKNOWN}, + fail: false, + }, + { + name: "test error", + args: []string{"example-error"}, + flags: []string{}, + responses: map[string]release.TestRun_Status{"ERROR: yellow lights everywhere": release.TestRun_FAILURE}, + fail: true, + }, + { + name: "test running", + args: []string{"example-running"}, + flags: []string{}, + responses: map[string]release.TestRun_Status{"RUNNING: things are happpeningggg": release.TestRun_RUNNING}, + fail: false, + }, + { + name: "multiple tests example", + args: []string{"example-suite"}, + flags: []string{}, + responses: map[string]release.TestRun_Status{ + "RUNNING: things are happpeningggg": release.TestRun_RUNNING, + "PASSED: party time": release.TestRun_SUCCESS, + "RUNNING: things are happening again": release.TestRun_RUNNING, + "FAILURE: good thing u checked :)": release.TestRun_FAILURE, + "RUNNING: things are happpeningggg yet again": release.TestRun_RUNNING, + "PASSED: feel free to party again": release.TestRun_SUCCESS}, + fail: true, + }, + } + + for _, tt := range tests { + c := &helm.FakeClient{Responses: tt.responses} + + buf := bytes.NewBuffer(nil) + cmd := newReleaseTestCmd(c, buf) + cmd.ParseFlags(tt.flags) + + err := cmd.RunE(cmd, tt.args) + if err == nil && tt.fail { + t.Errorf("%q did not fail but should have failed", tt.name) + } + + if err != nil { + if tt.fail { + continue + } else { + t.Errorf("%q reported error: %s", tt.name, err) + } + } + + } +} diff --git a/pkg/helm/repo.go b/pkg/helm/repo.go new file mode 100644 index 000000000..a3416200a --- /dev/null +++ b/pkg/helm/repo.go @@ -0,0 +1,47 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + + "github.com/spf13/cobra" +) + +var repoHelm = ` +This command consists of multiple subcommands to interact with chart repositories. + +It can be used to add, remove, list, and index chart repositories. +Example usage: + $ helm repo add [NAME] [REPO_URL] +` + +func newRepoCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "repo [FLAGS] add|remove|list|index|update [ARGS]", + Short: "add, list, remove, update, and index chart repositories", + Long: repoHelm, + } + + cmd.AddCommand(newRepoAddCmd(out)) + cmd.AddCommand(newRepoListCmd(out)) + cmd.AddCommand(newRepoRemoveCmd(out)) + cmd.AddCommand(newRepoIndexCmd(out)) + cmd.AddCommand(newRepoUpdateCmd(out)) + + return cmd +} diff --git a/pkg/helm/repo_add.go b/pkg/helm/repo_add.go new file mode 100644 index 000000000..782a21637 --- /dev/null +++ b/pkg/helm/repo_add.go @@ -0,0 +1,117 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/getter" + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/repo" +) + +type repoAddCmd struct { + name string + url string + username string + password string + home helmpath.Home + noupdate bool + + certFile string + keyFile string + caFile string + + out io.Writer +} + +func newRepoAddCmd(out io.Writer) *cobra.Command { + add := &repoAddCmd{out: out} + + cmd := &cobra.Command{ + Use: "add [flags] [NAME] [URL]", + Short: "add a chart repository", + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkArgsLength(len(args), "name for the chart repository", "the url of the chart repository"); err != nil { + return err + } + + add.name = args[0] + add.url = args[1] + add.home = settings.Home + + return add.run() + }, + } + + f := cmd.Flags() + f.StringVar(&add.username, "username", "", "chart repository username") + f.StringVar(&add.password, "password", "", "chart repository password") + f.BoolVar(&add.noupdate, "no-update", false, "raise error if repo is already registered") + f.StringVar(&add.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&add.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.StringVar(&add.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") + + return cmd +} + +func (a *repoAddCmd) run() error { + if err := addRepository(a.name, a.url, a.username, a.password, a.home, a.certFile, a.keyFile, a.caFile, a.noupdate); err != nil { + return err + } + fmt.Fprintf(a.out, "%q has been added to your repositories\n", a.name) + return nil +} + +func addRepository(name, url, username, password string, home helmpath.Home, certFile, keyFile, caFile string, noUpdate bool) error { + f, err := repo.LoadRepositoriesFile(home.RepositoryFile()) + if err != nil { + return err + } + + if noUpdate && f.Has(name) { + return fmt.Errorf("repository name (%s) already exists, please specify a different name", name) + } + + cif := home.CacheIndex(name) + c := repo.Entry{ + Name: name, + Cache: cif, + URL: url, + Username: username, + Password: password, + CertFile: certFile, + KeyFile: keyFile, + CAFile: caFile, + } + + r, err := repo.NewChartRepository(&c, getter.All(settings)) + if err != nil { + return err + } + + if err := r.DownloadIndexFile(home.Cache()); err != nil { + return fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", url, err.Error()) + } + + f.Update(&c) + + return f.WriteFile(home.RepositoryFile(), 0644) +} diff --git a/pkg/helm/repo_add_test.go b/pkg/helm/repo_add_test.go new file mode 100644 index 000000000..e4e957d7e --- /dev/null +++ b/pkg/helm/repo_add_test.go @@ -0,0 +1,103 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + "os" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/repo" + "k8s.io/helm/pkg/repo/repotest" +) + +var testName = "test-name" + +func TestRepoAddCmd(t *testing.T) { + srv, thome, err := repotest.NewTempServer("testdata/testserver/*.*") + if err != nil { + t.Fatal(err) + } + + cleanup := resetEnv() + defer func() { + srv.Stop() + os.RemoveAll(thome.String()) + cleanup() + }() + if err := ensureTestHome(thome, t); err != nil { + t.Fatal(err) + } + + settings.Home = thome + + tests := []releaseCase{ + { + name: "add a repository", + args: []string{testName, srv.URL()}, + expected: "\"" + testName + "\" has been added to your repositories", + }, + } + + runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newRepoAddCmd(out) + }) +} + +func TestRepoAdd(t *testing.T) { + ts, thome, err := repotest.NewTempServer("testdata/testserver/*.*") + if err != nil { + t.Fatal(err) + } + + cleanup := resetEnv() + hh := thome + defer func() { + ts.Stop() + os.RemoveAll(thome.String()) + cleanup() + }() + if err := ensureTestHome(hh, t); err != nil { + t.Fatal(err) + } + + settings.Home = thome + + if err := addRepository(testName, ts.URL(), "", "", hh, "", "", "", true); err != nil { + t.Error(err) + } + + f, err := repo.LoadRepositoriesFile(hh.RepositoryFile()) + if err != nil { + t.Error(err) + } + + if !f.Has(testName) { + t.Errorf("%s was not successfully inserted into %s", testName, hh.RepositoryFile()) + } + + if err := addRepository(testName, ts.URL(), "", "", hh, "", "", "", false); err != nil { + t.Errorf("Repository was not updated: %s", err) + } + + if err := addRepository(testName, ts.URL(), "", "", hh, "", "", "", false); err != nil { + t.Errorf("Duplicate repository name was added") + } +} diff --git a/pkg/helm/repo_index.go b/pkg/helm/repo_index.go new file mode 100644 index 000000000..9f38b8e77 --- /dev/null +++ b/pkg/helm/repo_index.go @@ -0,0 +1,105 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/repo" +) + +const repoIndexDesc = ` +Read the current directory and generate an index file based on the charts found. + +This tool is used for creating an 'index.yaml' file for a chart repository. To +set an absolute URL to the charts, use '--url' flag. + +To merge the generated index with an existing index file, use the '--merge' +flag. In this case, the charts found in the current directory will be merged +into the existing index, with local charts taking priority over existing charts. +` + +type repoIndexCmd struct { + dir string + url string + out io.Writer + merge string +} + +func newRepoIndexCmd(out io.Writer) *cobra.Command { + index := &repoIndexCmd{out: out} + + cmd := &cobra.Command{ + Use: "index [flags] [DIR]", + Short: "generate an index file given a directory containing packaged charts", + Long: repoIndexDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkArgsLength(len(args), "path to a directory"); err != nil { + return err + } + + index.dir = args[0] + + return index.run() + }, + } + + f := cmd.Flags() + f.StringVar(&index.url, "url", "", "url of chart repository") + f.StringVar(&index.merge, "merge", "", "merge the generated index into the given index") + + return cmd +} + +func (i *repoIndexCmd) run() error { + path, err := filepath.Abs(i.dir) + if err != nil { + return err + } + + return index(path, i.url, i.merge) +} + +func index(dir, url, mergeTo string) error { + out := filepath.Join(dir, "index.yaml") + + i, err := repo.IndexDirectory(dir, url) + if err != nil { + return err + } + if mergeTo != "" { + // if index.yaml is missing then create an empty one to merge into + var i2 *repo.IndexFile + if _, err := os.Stat(mergeTo); os.IsNotExist(err) { + i2 = repo.NewIndexFile() + i2.WriteFile(mergeTo, 0755) + } else { + i2, err = repo.LoadIndexFile(mergeTo) + if err != nil { + return fmt.Errorf("Merge failed: %s", err) + } + } + i.Merge(i2) + } + i.SortEntries() + return i.WriteFile(out, 0755) +} diff --git a/pkg/helm/repo_index_test.go b/pkg/helm/repo_index_test.go new file mode 100644 index 000000000..e7ad6b1f8 --- /dev/null +++ b/pkg/helm/repo_index_test.go @@ -0,0 +1,171 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "k8s.io/helm/pkg/repo" +) + +func TestRepoIndexCmd(t *testing.T) { + + dir, err := ioutil.TempDir("", "helm-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + comp := filepath.Join(dir, "compressedchart-0.1.0.tgz") + if err := linkOrCopy("testdata/testcharts/compressedchart-0.1.0.tgz", comp); err != nil { + t.Fatal(err) + } + comp2 := filepath.Join(dir, "compressedchart-0.2.0.tgz") + if err := linkOrCopy("testdata/testcharts/compressedchart-0.2.0.tgz", comp2); err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + c := newRepoIndexCmd(buf) + + if err := c.RunE(c, []string{dir}); err != nil { + t.Error(err) + } + + destIndex := filepath.Join(dir, "index.yaml") + + index, err := repo.LoadIndexFile(destIndex) + if err != nil { + t.Fatal(err) + } + + if len(index.Entries) != 1 { + t.Errorf("expected 1 entry, got %d: %#v", len(index.Entries), index.Entries) + } + + vs := index.Entries["compressedchart"] + if len(vs) != 2 { + t.Errorf("expected 2 versions, got %d: %#v", len(vs), vs) + } + + expectedVersion := "0.2.0" + if vs[0].Version != expectedVersion { + t.Errorf("expected %q, got %q", expectedVersion, vs[0].Version) + } + + // Test with `--merge` + + // Remove first two charts. + if err := os.Remove(comp); err != nil { + t.Fatal(err) + } + if err := os.Remove(comp2); err != nil { + t.Fatal(err) + } + // Add a new chart and a new version of an existing chart + if err := linkOrCopy("testdata/testcharts/reqtest-0.1.0.tgz", filepath.Join(dir, "reqtest-0.1.0.tgz")); err != nil { + t.Fatal(err) + } + if err := linkOrCopy("testdata/testcharts/compressedchart-0.3.0.tgz", filepath.Join(dir, "compressedchart-0.3.0.tgz")); err != nil { + t.Fatal(err) + } + + c.ParseFlags([]string{"--merge", destIndex}) + if err := c.RunE(c, []string{dir}); err != nil { + t.Error(err) + } + + index, err = repo.LoadIndexFile(destIndex) + if err != nil { + t.Fatal(err) + } + + if len(index.Entries) != 2 { + t.Errorf("expected 2 entries, got %d: %#v", len(index.Entries), index.Entries) + } + + vs = index.Entries["compressedchart"] + if len(vs) != 3 { + t.Errorf("expected 3 versions, got %d: %#v", len(vs), vs) + } + + expectedVersion = "0.3.0" + if vs[0].Version != expectedVersion { + t.Errorf("expected %q, got %q", expectedVersion, vs[0].Version) + } + + // test that index.yaml gets generated on merge even when it doesn't exist + if err := os.Remove(destIndex); err != nil { + t.Fatal(err) + } + + c.ParseFlags([]string{"--merge", destIndex}) + if err := c.RunE(c, []string{dir}); err != nil { + t.Error(err) + } + + _, err = repo.LoadIndexFile(destIndex) + if err != nil { + t.Fatal(err) + } + + // verify it didn't create an empty index.yaml and the merged happened + if len(index.Entries) != 2 { + t.Errorf("expected 2 entries, got %d: %#v", len(index.Entries), index.Entries) + } + + vs = index.Entries["compressedchart"] + if len(vs) != 3 { + t.Errorf("expected 3 versions, got %d: %#v", len(vs), vs) + } + + expectedVersion = "0.3.0" + if vs[0].Version != expectedVersion { + t.Errorf("expected %q, got %q", expectedVersion, vs[0].Version) + } +} + +func linkOrCopy(old, new string) error { + if err := os.Link(old, new); err != nil { + return copyFile(old, new) + } + + return nil +} + +func copyFile(dst, src string) error { + i, err := os.Open(dst) + if err != nil { + return err + } + defer i.Close() + + o, err := os.Create(src) + if err != nil { + return err + } + defer o.Close() + + _, err = io.Copy(o, i) + + return err +} diff --git a/pkg/helm/repo_list.go b/pkg/helm/repo_list.go new file mode 100644 index 000000000..4895255b7 --- /dev/null +++ b/pkg/helm/repo_list.go @@ -0,0 +1,66 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "errors" + "fmt" + "io" + + "github.com/gosuri/uitable" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/repo" +) + +type repoListCmd struct { + out io.Writer + home helmpath.Home +} + +func newRepoListCmd(out io.Writer) *cobra.Command { + list := &repoListCmd{out: out} + + cmd := &cobra.Command{ + Use: "list [flags]", + Short: "list chart repositories", + RunE: func(cmd *cobra.Command, args []string) error { + list.home = settings.Home + return list.run() + }, + } + + return cmd +} + +func (a *repoListCmd) run() error { + f, err := repo.LoadRepositoriesFile(a.home.RepositoryFile()) + if err != nil { + return err + } + if len(f.Repositories) == 0 { + return errors.New("no repositories to show") + } + table := uitable.New() + table.AddRow("NAME", "URL") + for _, re := range f.Repositories { + table.AddRow(re.Name, re.URL) + } + fmt.Fprintln(a.out, table) + return nil +} diff --git a/pkg/helm/repo_remove.go b/pkg/helm/repo_remove.go new file mode 100644 index 000000000..634b77161 --- /dev/null +++ b/pkg/helm/repo_remove.go @@ -0,0 +1,92 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/repo" +) + +type repoRemoveCmd struct { + out io.Writer + name string + home helmpath.Home +} + +func newRepoRemoveCmd(out io.Writer) *cobra.Command { + remove := &repoRemoveCmd{out: out} + + cmd := &cobra.Command{ + Use: "remove [flags] [NAME]", + Aliases: []string{"rm"}, + Short: "remove a chart repository", + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkArgsLength(len(args), "name of chart repository"); err != nil { + return err + } + remove.name = args[0] + remove.home = settings.Home + + return remove.run() + }, + } + + return cmd +} + +func (r *repoRemoveCmd) run() error { + return removeRepoLine(r.out, r.name, r.home) +} + +func removeRepoLine(out io.Writer, name string, home helmpath.Home) error { + repoFile := home.RepositoryFile() + r, err := repo.LoadRepositoriesFile(repoFile) + if err != nil { + return err + } + + if !r.Remove(name) { + return fmt.Errorf("no repo named %q found", name) + } + if err := r.WriteFile(repoFile, 0644); err != nil { + return err + } + + if err := removeRepoCache(name, home); err != nil { + return err + } + + fmt.Fprintf(out, "%q has been removed from your repositories\n", name) + + return nil +} + +func removeRepoCache(name string, home helmpath.Home) error { + if _, err := os.Stat(home.CacheIndex(name)); err == nil { + err = os.Remove(home.CacheIndex(name)) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/helm/repo_remove_test.go b/pkg/helm/repo_remove_test.go new file mode 100644 index 000000000..a4655fc00 --- /dev/null +++ b/pkg/helm/repo_remove_test.go @@ -0,0 +1,81 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "os" + "strings" + "testing" + + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/repo" + "k8s.io/helm/pkg/repo/repotest" +) + +func TestRepoRemove(t *testing.T) { + ts, thome, err := repotest.NewTempServer("testdata/testserver/*.*") + if err != nil { + t.Fatal(err) + } + + hh := helmpath.Home(thome) + cleanup := resetEnv() + defer func() { + ts.Stop() + os.RemoveAll(thome.String()) + cleanup() + }() + if err := ensureTestHome(hh, t); err != nil { + t.Fatal(err) + } + + settings.Home = thome + + b := bytes.NewBuffer(nil) + + if err := removeRepoLine(b, testName, hh); err == nil { + t.Errorf("Expected error removing %s, but did not get one.", testName) + } + if err := addRepository(testName, ts.URL(), "", "", hh, "", "", "", true); err != nil { + t.Error(err) + } + + mf, _ := os.Create(hh.CacheIndex(testName)) + mf.Close() + + b.Reset() + if err := removeRepoLine(b, testName, hh); err != nil { + t.Errorf("Error removing %s from repositories", testName) + } + if !strings.Contains(b.String(), "has been removed") { + t.Errorf("Unexpected output: %s", b.String()) + } + + if _, err := os.Stat(hh.CacheIndex(testName)); err == nil { + t.Errorf("Error cache file was not removed for repository %s", testName) + } + + f, err := repo.LoadRepositoriesFile(hh.RepositoryFile()) + if err != nil { + t.Error(err) + } + + if f.Has(testName) { + t.Errorf("%s was not successfully removed from repositories list", testName) + } +} diff --git a/pkg/helm/repo_update.go b/pkg/helm/repo_update.go new file mode 100644 index 000000000..9a4cb0ba7 --- /dev/null +++ b/pkg/helm/repo_update.go @@ -0,0 +1,109 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "errors" + "fmt" + "io" + "sync" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/getter" + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/repo" +) + +const updateDesc = ` +Update gets the latest information about charts from the respective chart repositories. +Information is cached locally, where it is used by commands like 'helm search'. + +'helm update' is the deprecated form of 'helm repo update'. It will be removed in +future releases. +` + +var errNoRepositories = errors.New("no repositories found. You must add one before updating") + +type repoUpdateCmd struct { + update func([]*repo.ChartRepository, io.Writer, helmpath.Home) + home helmpath.Home + out io.Writer +} + +func newRepoUpdateCmd(out io.Writer) *cobra.Command { + u := &repoUpdateCmd{ + out: out, + update: updateCharts, + } + cmd := &cobra.Command{ + Use: "update", + Aliases: []string{"up"}, + Short: "update information of available charts locally from chart repositories", + Long: updateDesc, + RunE: func(cmd *cobra.Command, args []string) error { + u.home = settings.Home + return u.run() + }, + } + return cmd +} + +func (u *repoUpdateCmd) run() error { + f, err := repo.LoadRepositoriesFile(u.home.RepositoryFile()) + if err != nil { + return err + } + + if len(f.Repositories) == 0 { + return errNoRepositories + } + var repos []*repo.ChartRepository + for _, cfg := range f.Repositories { + r, err := repo.NewChartRepository(cfg, getter.All(settings)) + if err != nil { + return err + } + repos = append(repos, r) + } + + u.update(repos, u.out, u.home) + return nil +} + +func updateCharts(repos []*repo.ChartRepository, out io.Writer, home helmpath.Home) { + fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") + var wg sync.WaitGroup + for _, re := range repos { + wg.Add(1) + go func(re *repo.ChartRepository) { + defer wg.Done() + if re.Config.Name == localRepository { + fmt.Fprintf(out, "...Skip %s chart repository\n", re.Config.Name) + return + } + err := re.DownloadIndexFile(home.Cache()) + if err != nil { + fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err) + } else { + fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name) + } + }(re) + } + wg.Wait() + fmt.Fprintln(out, "Update Complete. ⎈ Happy Helming!⎈ ") +} diff --git a/pkg/helm/repo_update_test.go b/pkg/helm/repo_update_test.go new file mode 100644 index 000000000..1b71312a4 --- /dev/null +++ b/pkg/helm/repo_update_test.go @@ -0,0 +1,106 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + "testing" + + "k8s.io/helm/pkg/getter" + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/repo" + "k8s.io/helm/pkg/repo/repotest" +) + +func TestUpdateCmd(t *testing.T) { + thome, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + + cleanup := resetEnv() + defer func() { + os.RemoveAll(thome.String()) + cleanup() + }() + + settings.Home = thome + + out := bytes.NewBuffer(nil) + // Instead of using the HTTP updater, we provide our own for this test. + // The TestUpdateCharts test verifies the HTTP behavior independently. + updater := func(repos []*repo.ChartRepository, out io.Writer, hh helmpath.Home) { + for _, re := range repos { + fmt.Fprintln(out, re.Config.Name) + } + } + uc := &repoUpdateCmd{ + update: updater, + home: helmpath.Home(thome), + out: out, + } + if err := uc.run(); err != nil { + t.Fatal(err) + } + + if got := out.String(); !strings.Contains(got, "charts") || !strings.Contains(got, "local") { + t.Errorf("Expected 'charts' and 'local' (in any order) got %q", got) + } +} + +func TestUpdateCharts(t *testing.T) { + ts, thome, err := repotest.NewTempServer("testdata/testserver/*.*") + if err != nil { + t.Fatal(err) + } + + hh := helmpath.Home(thome) + cleanup := resetEnv() + defer func() { + ts.Stop() + os.RemoveAll(thome.String()) + cleanup() + }() + if err := ensureTestHome(hh, t); err != nil { + t.Fatal(err) + } + + settings.Home = thome + + r, err := repo.NewChartRepository(&repo.Entry{ + Name: "charts", + URL: ts.URL(), + Cache: hh.CacheIndex("charts"), + }, getter.All(settings)) + if err != nil { + t.Error(err) + } + + b := bytes.NewBuffer(nil) + updateCharts([]*repo.ChartRepository{r}, b, hh) + + got := b.String() + if strings.Contains(got, "Unable to get an update") { + t.Errorf("Failed to get a repo: %q", got) + } + if !strings.Contains(got, "Update Complete.") { + t.Error("Update was not successful") + } +} diff --git a/pkg/helm/reset.go b/pkg/helm/reset.go new file mode 100644 index 000000000..1a000a441 --- /dev/null +++ b/pkg/helm/reset.go @@ -0,0 +1,131 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "errors" + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + + "k8s.io/helm/cmd/helm/installer" + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/proto/hapi/release" +) + +const resetDesc = ` +This command uninstalls Tiller (the Helm server-side component) from your +Kubernetes Cluster and optionally deletes local configuration in +$HELM_HOME (default ~/.helm/) +` + +type resetCmd struct { + force bool + removeHelmHome bool + namespace string + out io.Writer + home helmpath.Home + client helm.Interface + kubeClient internalclientset.Interface +} + +func newResetCmd(client helm.Interface, out io.Writer) *cobra.Command { + d := &resetCmd{ + out: out, + client: client, + } + + cmd := &cobra.Command{ + Use: "reset", + Short: "uninstalls Tiller from a cluster", + Long: resetDesc, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := setupConnection(); !d.force && err != nil { + return err + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + return errors.New("This command does not accept arguments") + } + + d.namespace = settings.TillerNamespace + d.home = settings.Home + d.client = ensureHelmClient(d.client) + + return d.run() + }, + } + + f := cmd.Flags() + f.BoolVarP(&d.force, "force", "f", false, "forces Tiller uninstall even if there are releases installed, or if Tiller is not in ready state. Releases are not deleted.)") + f.BoolVar(&d.removeHelmHome, "remove-helm-home", false, "if set deletes $HELM_HOME") + + return cmd +} + +// runReset uninstalls tiller from Kubernetes Cluster and deletes local config +func (d *resetCmd) run() error { + if d.kubeClient == nil { + c, err := getInternalKubeClient(settings.KubeContext) + if err != nil { + return fmt.Errorf("could not get kubernetes client: %s", err) + } + d.kubeClient = c + } + + res, err := d.client.ListReleases( + helm.ReleaseListStatuses([]release.Status_Code{release.Status_DEPLOYED}), + ) + if !d.force && err != nil { + return prettyError(err) + } + + if !d.force && res != nil && len(res.Releases) > 0 { + return fmt.Errorf("there are still %d deployed releases (Tip: use --force to remove Tiller. Releases will not be deleted.)", len(res.Releases)) + } + + if err := installer.Uninstall(d.kubeClient, &installer.Options{Namespace: d.namespace}); err != nil { + return fmt.Errorf("error unstalling Tiller: %s", err) + } + + if d.removeHelmHome { + if err := deleteDirectories(d.home, d.out); err != nil { + return err + } + } + + fmt.Fprintln(d.out, "Tiller (the Helm server-side component) has been uninstalled from your Kubernetes Cluster.") + return nil +} + +// deleteDirectories deletes $HELM_HOME +func deleteDirectories(home helmpath.Home, out io.Writer) error { + if _, err := os.Stat(home.String()); err == nil { + fmt.Fprintf(out, "Deleting %s \n", home.String()) + if err := os.RemoveAll(home.String()); err != nil { + return fmt.Errorf("Could not remove %s: %s", home.String(), err) + } + } + + return nil +} diff --git a/pkg/helm/reset_test.go b/pkg/helm/reset_test.go new file mode 100644 index 000000000..7472a390e --- /dev/null +++ b/pkg/helm/reset_test.go @@ -0,0 +1,170 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "io/ioutil" + "os" + "strings" + "testing" + + "k8s.io/kubernetes/pkg/apis/core" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestResetCmd(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.Remove(home) + + var buf bytes.Buffer + c := &helm.FakeClient{} + fc := fake.NewSimpleClientset() + cmd := &resetCmd{ + out: &buf, + home: helmpath.Home(home), + client: c, + kubeClient: fc, + namespace: core.NamespaceDefault, + } + if err := cmd.run(); err != nil { + t.Errorf("unexpected error: %v", err) + } + actions := fc.Actions() + if len(actions) != 3 { + t.Errorf("Expected 3 actions, got %d", len(actions)) + } + expected := "Tiller (the Helm server-side component) has been uninstalled from your Kubernetes Cluster." + if !strings.Contains(buf.String(), expected) { + t.Errorf("expected %q, got %q", expected, buf.String()) + } + if _, err := os.Stat(home); err != nil { + t.Errorf("Helm home directory %s does not exists", home) + } +} + +func TestResetCmd_removeHelmHome(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.Remove(home) + + var buf bytes.Buffer + c := &helm.FakeClient{} + fc := fake.NewSimpleClientset() + cmd := &resetCmd{ + removeHelmHome: true, + out: &buf, + home: helmpath.Home(home), + client: c, + kubeClient: fc, + namespace: core.NamespaceDefault, + } + if err := cmd.run(); err != nil { + t.Errorf("unexpected error: %v", err) + } + actions := fc.Actions() + if len(actions) != 3 { + t.Errorf("Expected 3 actions, got %d", len(actions)) + } + expected := "Tiller (the Helm server-side component) has been uninstalled from your Kubernetes Cluster." + if !strings.Contains(buf.String(), expected) { + t.Errorf("expected %q, got %q", expected, buf.String()) + } + if _, err := os.Stat(home); err == nil { + t.Errorf("Helm home directory %s already exists", home) + } +} + +func TestReset_deployedReleases(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.Remove(home) + + var buf bytes.Buffer + resp := []*release.Release{ + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), + } + c := &helm.FakeClient{ + Rels: resp, + } + fc := fake.NewSimpleClientset() + cmd := &resetCmd{ + out: &buf, + home: helmpath.Home(home), + client: c, + kubeClient: fc, + namespace: core.NamespaceDefault, + } + err = cmd.run() + expected := "there are still 1 deployed releases (Tip: use --force to remove Tiller. Releases will not be deleted.)" + if !strings.Contains(err.Error(), expected) { + t.Errorf("unexpected error: %v", err) + } + if _, err := os.Stat(home); err != nil { + t.Errorf("Helm home directory %s does not exists", home) + } +} + +func TestReset_forceFlag(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.Remove(home) + + var buf bytes.Buffer + resp := []*release.Release{ + helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), + } + c := &helm.FakeClient{ + Rels: resp, + } + fc := fake.NewSimpleClientset() + cmd := &resetCmd{ + force: true, + out: &buf, + home: helmpath.Home(home), + client: c, + kubeClient: fc, + namespace: core.NamespaceDefault, + } + if err := cmd.run(); err != nil { + t.Errorf("unexpected error: %v", err) + } + actions := fc.Actions() + if len(actions) != 3 { + t.Errorf("Expected 3 actions, got %d", len(actions)) + } + expected := "Tiller (the Helm server-side component) has been uninstalled from your Kubernetes Cluster." + if !strings.Contains(buf.String(), expected) { + t.Errorf("expected %q, got %q", expected, buf.String()) + } + if _, err := os.Stat(home); err != nil { + t.Errorf("Helm home directory %s does not exists", home) + } +} diff --git a/pkg/helm/rollback.go b/pkg/helm/rollback.go new file mode 100644 index 000000000..90bd66f92 --- /dev/null +++ b/pkg/helm/rollback.go @@ -0,0 +1,107 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "strconv" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" +) + +const rollbackDesc = ` +This command rolls back a release to a previous revision. + +The first argument of the rollback command is the name of a release, and the +second is a revision (version) number. To see revision numbers, run +'helm history RELEASE'. +` + +type rollbackCmd struct { + name string + revision int32 + dryRun bool + recreate bool + force bool + disableHooks bool + out io.Writer + client helm.Interface + timeout int64 + wait bool +} + +func newRollbackCmd(c helm.Interface, out io.Writer) *cobra.Command { + rollback := &rollbackCmd{ + out: out, + client: c, + } + + cmd := &cobra.Command{ + Use: "rollback [flags] [RELEASE] [REVISION]", + Short: "roll back a release to a previous revision", + Long: rollbackDesc, + PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkArgsLength(len(args), "release name", "revision number"); err != nil { + return err + } + + rollback.name = args[0] + + v64, err := strconv.ParseInt(args[1], 10, 32) + if err != nil { + return fmt.Errorf("invalid revision number '%q': %s", args[1], err) + } + + rollback.revision = int32(v64) + rollback.client = ensureHelmClient(rollback.client) + return rollback.run() + }, + } + + f := cmd.Flags() + f.BoolVar(&rollback.dryRun, "dry-run", false, "simulate a rollback") + f.BoolVar(&rollback.recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") + f.BoolVar(&rollback.force, "force", false, "force resource update through delete/recreate if needed") + f.BoolVar(&rollback.disableHooks, "no-hooks", false, "prevent hooks from running during rollback") + f.Int64Var(&rollback.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") + f.BoolVar(&rollback.wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout") + + return cmd +} + +func (r *rollbackCmd) run() error { + _, err := r.client.RollbackRelease( + r.name, + helm.RollbackDryRun(r.dryRun), + helm.RollbackRecreate(r.recreate), + helm.RollbackForce(r.force), + helm.RollbackDisableHooks(r.disableHooks), + helm.RollbackVersion(r.revision), + helm.RollbackTimeout(r.timeout), + helm.RollbackWait(r.wait)) + if err != nil { + return prettyError(err) + } + + fmt.Fprintf(r.out, "Rollback was a success! Happy Helming!\n") + + return nil +} diff --git a/pkg/helm/rollback_test.go b/pkg/helm/rollback_test.go new file mode 100644 index 000000000..64b6cf697 --- /dev/null +++ b/pkg/helm/rollback_test.go @@ -0,0 +1,61 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" +) + +func TestRollbackCmd(t *testing.T) { + + tests := []releaseCase{ + { + name: "rollback a release", + args: []string{"funny-honey", "1"}, + expected: "Rollback was a success! Happy Helming!", + }, + { + name: "rollback a release with timeout", + args: []string{"funny-honey", "1"}, + flags: []string{"--timeout", "120"}, + expected: "Rollback was a success! Happy Helming!", + }, + { + name: "rollback a release with wait", + args: []string{"funny-honey", "1"}, + flags: []string{"--wait"}, + expected: "Rollback was a success! Happy Helming!", + }, + { + name: "rollback a release without revision", + args: []string{"funny-honey"}, + err: true, + }, + } + + cmd := func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newRollbackCmd(c, out) + } + + runReleaseCases(t, tests, cmd) + +} diff --git a/pkg/helm/search.go b/pkg/helm/search.go new file mode 100644 index 000000000..ba857369c --- /dev/null +++ b/pkg/helm/search.go @@ -0,0 +1,162 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "strings" + + "github.com/Masterminds/semver" + "github.com/gosuri/uitable" + "github.com/spf13/cobra" + + "k8s.io/helm/cmd/helm/search" + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/repo" +) + +const searchDesc = ` +Search reads through all of the repositories configured on the system, and +looks for matches. + +Repositories are managed with 'helm repo' commands. +` + +// searchMaxScore suggests that any score higher than this is not considered a match. +const searchMaxScore = 25 + +type searchCmd struct { + out io.Writer + helmhome helmpath.Home + + versions bool + regexp bool + version string +} + +func newSearchCmd(out io.Writer) *cobra.Command { + sc := &searchCmd{out: out} + + cmd := &cobra.Command{ + Use: "search [keyword]", + Short: "search for a keyword in charts", + Long: searchDesc, + RunE: func(cmd *cobra.Command, args []string) error { + sc.helmhome = settings.Home + return sc.run(args) + }, + } + + f := cmd.Flags() + f.BoolVarP(&sc.regexp, "regexp", "r", false, "use regular expressions for searching") + f.BoolVarP(&sc.versions, "versions", "l", false, "show the long listing, with each version of each chart on its own line") + f.StringVarP(&sc.version, "version", "v", "", "search using semantic versioning constraints") + + return cmd +} + +func (s *searchCmd) run(args []string) error { + index, err := s.buildIndex() + if err != nil { + return err + } + + var res []*search.Result + if len(args) == 0 { + res = index.All() + } else { + q := strings.Join(args, " ") + res, err = index.Search(q, searchMaxScore, s.regexp) + if err != nil { + return err + } + } + + search.SortScore(res) + data, err := s.applyConstraint(res) + if err != nil { + return err + } + + fmt.Fprintln(s.out, s.formatSearchResults(data)) + + return nil +} + +func (s *searchCmd) applyConstraint(res []*search.Result) ([]*search.Result, error) { + if len(s.version) == 0 { + return res, nil + } + + constraint, err := semver.NewConstraint(s.version) + if err != nil { + return res, fmt.Errorf("an invalid version/constraint format: %s", err) + } + + data := res[:0] + foundNames := map[string]bool{} + for _, r := range res { + if _, found := foundNames[r.Name]; found { + continue + } + v, err := semver.NewVersion(r.Chart.Version) + if err != nil || constraint.Check(v) { + data = append(data, r) + if !s.versions { + foundNames[r.Name] = true // If user hasn't requested all versions, only show the latest that matches + } + } + } + + return data, nil +} + +func (s *searchCmd) formatSearchResults(res []*search.Result) string { + if len(res) == 0 { + return "No results found" + } + table := uitable.New() + table.MaxColWidth = 50 + table.AddRow("NAME", "CHART VERSION", "APP VERSION", "DESCRIPTION") + for _, r := range res { + table.AddRow(r.Name, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description) + } + return table.String() +} + +func (s *searchCmd) buildIndex() (*search.Index, error) { + // Load the repositories.yaml + rf, err := repo.LoadRepositoriesFile(s.helmhome.RepositoryFile()) + if err != nil { + return nil, err + } + + i := search.NewIndex() + for _, re := range rf.Repositories { + n := re.Name + f := s.helmhome.CacheIndex(n) + ind, err := repo.LoadIndexFile(f) + if err != nil { + fmt.Fprintf(s.out, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n) + continue + } + + i.AddRepo(n, ind, s.versions || len(s.version) > 0) + } + return i, nil +} diff --git a/pkg/helm/search/search.go b/pkg/helm/search/search.go new file mode 100644 index 000000000..6c4cb4aa4 --- /dev/null +++ b/pkg/helm/search/search.go @@ -0,0 +1,236 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/*Package search provides client-side repository searching. + +This supports building an in-memory search index based on the contents of +multiple repositories, and then using string matching or regular expressions +to find matches. +*/ +package search + +import ( + "errors" + "path" + "regexp" + "sort" + "strings" + + "github.com/Masterminds/semver" + "k8s.io/helm/pkg/repo" +) + +// Result is a search result. +// +// Score indicates how close it is to match. The higher the score, the longer +// the distance. +type Result struct { + Name string + Score int + Chart *repo.ChartVersion +} + +// Index is a searchable index of chart information. +type Index struct { + lines map[string]string + charts map[string]*repo.ChartVersion +} + +const sep = "\v" + +// NewIndex creats a new Index. +func NewIndex() *Index { + return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartVersion{}} +} + +// verSep is a separator for version fields in map keys. +const verSep = "$$" + +// AddRepo adds a repository index to the search index. +func (i *Index) AddRepo(rname string, ind *repo.IndexFile, all bool) { + ind.SortEntries() + for name, ref := range ind.Entries { + if len(ref) == 0 { + // Skip chart names that have zero releases. + continue + } + // By convention, an index file is supposed to have the newest at the + // 0 slot, so our best bet is to grab the 0 entry and build the index + // entry off of that. + // Note: Do not use filePath.Join since on Windows it will return \ + // which results in a repo name that cannot be understood. + fname := path.Join(rname, name) + if !all { + i.lines[fname] = indstr(rname, ref[0]) + i.charts[fname] = ref[0] + continue + } + + // If 'all' is set, then we go through all of the refs, and add them all + // to the index. This will generate a lot of near-duplicate entries. + for _, rr := range ref { + versionedName := fname + verSep + rr.Version + i.lines[versionedName] = indstr(rname, rr) + i.charts[versionedName] = rr + } + } +} + +// All returns all charts in the index as if they were search results. +// +// Each will be given a score of 0. +func (i *Index) All() []*Result { + res := make([]*Result, len(i.charts)) + j := 0 + for name, ch := range i.charts { + parts := strings.Split(name, verSep) + res[j] = &Result{ + Name: parts[0], + Chart: ch, + } + j++ + } + return res +} + +// Search searches an index for the given term. +// +// Threshold indicates the maximum score a term may have before being marked +// irrelevant. (Low score means higher relevance. Golf, not bowling.) +// +// If regexp is true, the term is treated as a regular expression. Otherwise, +// term is treated as a literal string. +func (i *Index) Search(term string, threshold int, regexp bool) ([]*Result, error) { + if regexp { + return i.SearchRegexp(term, threshold) + } + return i.SearchLiteral(term, threshold), nil +} + +// calcScore calculates a score for a match. +func (i *Index) calcScore(index int, matchline string) int { + + // This is currently tied to the fact that sep is a single char. + splits := []int{} + s := rune(sep[0]) + for i, ch := range matchline { + if ch == s { + splits = append(splits, i) + } + } + + for i, pos := range splits { + if index > pos { + continue + } + return i + } + return len(splits) +} + +// SearchLiteral does a literal string search (no regexp). +func (i *Index) SearchLiteral(term string, threshold int) []*Result { + term = strings.ToLower(term) + buf := []*Result{} + for k, v := range i.lines { + lk := strings.ToLower(k) + lv := strings.ToLower(v) + res := strings.Index(lv, term) + if score := i.calcScore(res, lv); res != -1 && score < threshold { + parts := strings.Split(lk, verSep) // Remove version, if it is there. + buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) + } + } + return buf +} + +// SearchRegexp searches using a regular expression. +func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) { + matcher, err := regexp.Compile(re) + if err != nil { + return []*Result{}, err + } + buf := []*Result{} + for k, v := range i.lines { + ind := matcher.FindStringIndex(v) + if len(ind) == 0 { + continue + } + if score := i.calcScore(ind[0], v); ind[0] >= 0 && score < threshold { + parts := strings.Split(k, verSep) // Remove version, if it is there. + buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) + } + } + return buf, nil +} + +// Chart returns the ChartVersion for a particular name. +func (i *Index) Chart(name string) (*repo.ChartVersion, error) { + c, ok := i.charts[name] + if !ok { + return nil, errors.New("no such chart") + } + return c, nil +} + +// SortScore does an in-place sort of the results. +// +// Lowest scores are highest on the list. Matching scores are subsorted alphabetically. +func SortScore(r []*Result) { + sort.Sort(scoreSorter(r)) +} + +// scoreSorter sorts results by score, and subsorts by alpha Name. +type scoreSorter []*Result + +// Len returns the length of this scoreSorter. +func (s scoreSorter) Len() int { return len(s) } + +// Swap performs an in-place swap. +func (s scoreSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Less compares a to b, and returns true if a is less than b. +func (s scoreSorter) Less(a, b int) bool { + first := s[a] + second := s[b] + + if first.Score > second.Score { + return false + } + if first.Score < second.Score { + return true + } + if first.Name == second.Name { + v1, err := semver.NewVersion(first.Chart.Version) + if err != nil { + return true + } + v2, err := semver.NewVersion(second.Chart.Version) + if err != nil { + return true + } + // Sort so that the newest chart is higher than the oldest chart. This is + // the opposite of what you'd expect in a function called Less. + return v1.GreaterThan(v2) + } + return first.Name < second.Name +} + +func indstr(name string, ref *repo.ChartVersion) string { + i := ref.Name + sep + name + "/" + ref.Name + sep + + ref.Description + sep + strings.Join(ref.Keywords, " ") + return i +} diff --git a/pkg/helm/search/search_test.go b/pkg/helm/search/search_test.go new file mode 100644 index 000000000..574f55448 --- /dev/null +++ b/pkg/helm/search/search_test.go @@ -0,0 +1,301 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package search + +import ( + "strings" + "testing" + + "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/repo" +) + +func TestSortScore(t *testing.T) { + in := []*Result{ + {Name: "bbb", Score: 0, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.3"}}}, + {Name: "aaa", Score: 5}, + {Name: "abb", Score: 5}, + {Name: "aab", Score: 0}, + {Name: "bab", Score: 5}, + {Name: "ver", Score: 5, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.4"}}}, + {Name: "ver", Score: 5, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.3"}}}, + } + expect := []string{"aab", "bbb", "aaa", "abb", "bab", "ver", "ver"} + expectScore := []int{0, 0, 5, 5, 5, 5, 5} + SortScore(in) + + // Test Score + for i := 0; i < len(expectScore); i++ { + if expectScore[i] != in[i].Score { + t.Errorf("Sort error on index %d: expected %d, got %d", i, expectScore[i], in[i].Score) + } + } + // Test Name + for i := 0; i < len(expect); i++ { + if expect[i] != in[i].Name { + t.Errorf("Sort error: expected %s, got %s", expect[i], in[i].Name) + } + } + + // Test version of last two items + if in[5].Chart.Version != "1.2.4" { + t.Errorf("Expected 1.2.4, got %s", in[5].Chart.Version) + } + if in[6].Chart.Version != "1.2.3" { + t.Error("Expected 1.2.3 to be last") + } +} + +var indexfileEntries = map[string]repo.ChartVersions{ + "niña": { + { + URLs: []string{"http://example.com/charts/nina-0.1.0.tgz"}, + Metadata: &chart.Metadata{ + Name: "niña", + Version: "0.1.0", + Description: "One boat", + }, + }, + }, + "pinta": { + { + URLs: []string{"http://example.com/charts/pinta-0.1.0.tgz"}, + Metadata: &chart.Metadata{ + Name: "pinta", + Version: "0.1.0", + Description: "Two ship", + }, + }, + }, + "santa-maria": { + { + URLs: []string{"http://example.com/charts/santa-maria-1.2.3.tgz"}, + Metadata: &chart.Metadata{ + Name: "santa-maria", + Version: "1.2.3", + Description: "Three boat", + }, + }, + { + URLs: []string{"http://example.com/charts/santa-maria-1.2.2-rc-1.tgz"}, + Metadata: &chart.Metadata{ + Name: "santa-maria", + Version: "1.2.2-RC-1", + Description: "Three boat", + }, + }, + }, +} + +func loadTestIndex(t *testing.T, all bool) *Index { + i := NewIndex() + i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all) + i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{ + "pinta": { + { + URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"}, + Metadata: &chart.Metadata{ + Name: "pinta", + Version: "2.0.0", + Description: "Two ship, version two", + }, + }, + }, + }}, all) + return i +} + +func TestAll(t *testing.T) { + i := loadTestIndex(t, false) + all := i.All() + if len(all) != 4 { + t.Errorf("Expected 4 entries, got %d", len(all)) + } + + i = loadTestIndex(t, true) + all = i.All() + if len(all) != 5 { + t.Errorf("Expected 5 entries, got %d", len(all)) + } +} + +func TestAddRepo_Sort(t *testing.T) { + i := loadTestIndex(t, true) + sr, err := i.Search("TESTING/SANTA-MARIA", 100, false) + if err != nil { + t.Fatal(err) + } + SortScore(sr) + + ch := sr[0] + expect := "1.2.3" + if ch.Chart.Version != expect { + t.Errorf("Expected %q, got %q", expect, ch.Chart.Version) + } +} + +func TestSearchByName(t *testing.T) { + + tests := []struct { + name string + query string + expect []*Result + regexp bool + fail bool + failMsg string + }{ + { + name: "basic search for one result", + query: "santa-maria", + expect: []*Result{ + {Name: "testing/santa-maria"}, + }, + }, + { + name: "basic search for two results", + query: "pinta", + expect: []*Result{ + {Name: "testing/pinta"}, + {Name: "ztesting/pinta"}, + }, + }, + { + name: "repo-specific search for one result", + query: "ztesting/pinta", + expect: []*Result{ + {Name: "ztesting/pinta"}, + }, + }, + { + name: "partial name search", + query: "santa", + expect: []*Result{ + {Name: "testing/santa-maria"}, + }, + }, + { + name: "description search, one result", + query: "Three", + expect: []*Result{ + {Name: "testing/santa-maria"}, + }, + }, + { + name: "description search, two results", + query: "two", + expect: []*Result{ + {Name: "testing/pinta"}, + {Name: "ztesting/pinta"}, + }, + }, + { + name: "description upper search, two results", + query: "TWO", + expect: []*Result{ + {Name: "testing/pinta"}, + {Name: "ztesting/pinta"}, + }, + }, + { + name: "nothing found", + query: "mayflower", + expect: []*Result{}, + }, + { + name: "regexp, one result", + query: "Th[ref]*", + expect: []*Result{ + {Name: "testing/santa-maria"}, + }, + regexp: true, + }, + { + name: "regexp, fail compile", + query: "th[", + expect: []*Result{}, + regexp: true, + fail: true, + failMsg: "error parsing regexp:", + }, + } + + i := loadTestIndex(t, false) + + for _, tt := range tests { + + charts, err := i.Search(tt.query, 100, tt.regexp) + if err != nil { + if tt.fail { + if !strings.Contains(err.Error(), tt.failMsg) { + t.Fatalf("%s: Unexpected error message: %s", tt.name, err) + } + continue + } + t.Fatalf("%s: %s", tt.name, err) + } + // Give us predictably ordered results. + SortScore(charts) + + l := len(charts) + if l != len(tt.expect) { + t.Fatalf("%s: Expected %d result, got %d", tt.name, len(tt.expect), l) + } + // For empty result sets, just keep going. + if l == 0 { + continue + } + + for i, got := range charts { + ex := tt.expect[i] + if got.Name != ex.Name { + t.Errorf("%s[%d]: Expected name %q, got %q", tt.name, i, ex.Name, got.Name) + } + } + + } +} + +func TestSearchByNameAll(t *testing.T) { + // Test with the All bit turned on. + i := loadTestIndex(t, true) + cs, err := i.Search("santa-maria", 100, false) + if err != nil { + t.Fatal(err) + } + if len(cs) != 2 { + t.Errorf("expected 2 charts, got %d", len(cs)) + } +} + +func TestCalcScore(t *testing.T) { + i := NewIndex() + + fields := []string{"aaa", "bbb", "ccc", "ddd"} + matchline := strings.Join(fields, sep) + if r := i.calcScore(2, matchline); r != 0 { + t.Errorf("Expected 0, got %d", r) + } + if r := i.calcScore(5, matchline); r != 1 { + t.Errorf("Expected 1, got %d", r) + } + if r := i.calcScore(10, matchline); r != 2 { + t.Errorf("Expected 2, got %d", r) + } + if r := i.calcScore(14, matchline); r != 3 { + t.Errorf("Expected 3, got %d", r) + } +} diff --git a/pkg/helm/search_test.go b/pkg/helm/search_test.go new file mode 100644 index 000000000..4366e46e5 --- /dev/null +++ b/pkg/helm/search_test.go @@ -0,0 +1,97 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" +) + +func TestSearchCmd(t *testing.T) { + tests := []releaseCase{ + { + name: "search for 'maria', expect one match", + args: []string{"maria"}, + expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/mariadb\t0.3.0 \t \tChart for MariaDB", + }, + { + name: "search for 'alpine', expect two matches", + args: []string{"alpine"}, + expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/alpine\t0.2.0 \t2.3.4 \tDeploy a basic Alpine Linux pod", + }, + { + name: "search for 'alpine' with versions, expect three matches", + args: []string{"alpine"}, + flags: []string{"--versions"}, + expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/alpine\t0.2.0 \t2.3.4 \tDeploy a basic Alpine Linux pod\ntesting/alpine\t0.1.0 \t1.2.3 \tDeploy a basic Alpine Linux pod", + }, + { + name: "search for 'alpine' with version constraint, expect one match with version 0.1.0", + args: []string{"alpine"}, + flags: []string{"--version", ">= 0.1, < 0.2"}, + expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/alpine\t0.1.0 \t1.2.3 \tDeploy a basic Alpine Linux pod", + }, + { + name: "search for 'alpine' with version constraint, expect one match with version 0.1.0", + args: []string{"alpine"}, + flags: []string{"--versions", "--version", ">= 0.1, < 0.2"}, + expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/alpine\t0.1.0 \t1.2.3 \tDeploy a basic Alpine Linux pod", + }, + { + name: "search for 'alpine' with version constraint, expect one match with version 0.2.0", + args: []string{"alpine"}, + flags: []string{"--version", ">= 0.1"}, + expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/alpine\t0.2.0 \t2.3.4 \tDeploy a basic Alpine Linux pod", + }, + { + name: "search for 'alpine' with version constraint and --versions, expect two matches", + args: []string{"alpine"}, + flags: []string{"--versions", "--version", ">= 0.1"}, + expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/alpine\t0.2.0 \t2.3.4 \tDeploy a basic Alpine Linux pod\ntesting/alpine\t0.1.0 \t1.2.3 \tDeploy a basic Alpine Linux pod", + }, + { + name: "search for 'syzygy', expect no matches", + args: []string{"syzygy"}, + expected: "No results found", + }, + { + name: "search for 'alp[a-z]+', expect two matches", + args: []string{"alp[a-z]+"}, + flags: []string{"--regexp"}, + expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/alpine\t0.2.0 \t2.3.4 \tDeploy a basic Alpine Linux pod", + }, + { + name: "search for 'alp[', expect failure to compile regexp", + args: []string{"alp["}, + flags: []string{"--regexp"}, + err: true, + }, + } + + cleanup := resetEnv() + defer cleanup() + + settings.Home = "testdata/helmhome" + + runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newSearchCmd(out) + }) +} diff --git a/pkg/helm/serve.go b/pkg/helm/serve.go new file mode 100644 index 000000000..e8e664f5d --- /dev/null +++ b/pkg/helm/serve.go @@ -0,0 +1,102 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/repo" +) + +const serveDesc = ` +This command starts a local chart repository server that serves charts from a local directory. + +The new server will provide HTTP access to a repository. By default, it will +scan all of the charts in '$HELM_HOME/repository/local' and serve those over +the local IPv4 TCP port (default '127.0.0.1:8879'). + +This command is intended to be used for educational and testing purposes only. +It is best to rely on a dedicated web server or a cloud-hosted solution like +Google Cloud Storage for production use. + +See https://github.com/kubernetes/helm/blob/master/docs/chart_repository.md#hosting-chart-repositories +for more information on hosting chart repositories in a production setting. +` + +type serveCmd struct { + out io.Writer + url string + address string + repoPath string +} + +func newServeCmd(out io.Writer) *cobra.Command { + srv := &serveCmd{out: out} + cmd := &cobra.Command{ + Use: "serve", + Short: "start a local http web server", + Long: serveDesc, + PreRunE: func(cmd *cobra.Command, args []string) error { + return srv.complete() + }, + RunE: func(cmd *cobra.Command, args []string) error { + return srv.run() + }, + } + + f := cmd.Flags() + f.StringVar(&srv.repoPath, "repo-path", "", "local directory path from which to serve charts") + f.StringVar(&srv.address, "address", "127.0.0.1:8879", "address to listen on") + f.StringVar(&srv.url, "url", "", "external URL of chart repository") + + return cmd +} + +func (s *serveCmd) complete() error { + if s.repoPath == "" { + s.repoPath = settings.Home.LocalRepository() + } + return nil +} + +func (s *serveCmd) run() error { + repoPath, err := filepath.Abs(s.repoPath) + if err != nil { + return err + } + if _, err := os.Stat(repoPath); os.IsNotExist(err) { + return err + } + + fmt.Fprintln(s.out, "Regenerating index. This may take a moment.") + if len(s.url) > 0 { + err = index(repoPath, s.url, "") + } else { + err = index(repoPath, "http://"+s.address, "") + } + if err != nil { + return err + } + + fmt.Fprintf(s.out, "Now serving you on %s\n", s.address) + return repo.StartLocalRepo(repoPath, s.address) +} diff --git a/pkg/helm/status.go b/pkg/helm/status.go new file mode 100644 index 000000000..3e6d6f73b --- /dev/null +++ b/pkg/helm/status.go @@ -0,0 +1,157 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "encoding/json" + "fmt" + "io" + "regexp" + "text/tabwriter" + + "github.com/ghodss/yaml" + "github.com/gosuri/uitable" + "github.com/gosuri/uitable/util/strutil" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/proto/hapi/services" + "k8s.io/helm/pkg/timeconv" +) + +var statusHelp = ` +This command shows the status of a named release. +The status consists of: +- last deployment time +- k8s namespace in which the release lives +- state of the release (can be: UNKNOWN, DEPLOYED, DELETED, SUPERSEDED, FAILED or DELETING) +- list of resources that this release consists of, sorted by kind +- details on last test suite run, if applicable +- additional notes provided by the chart +` + +type statusCmd struct { + release string + out io.Writer + client helm.Interface + version int32 + outfmt string +} + +func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command { + status := &statusCmd{ + out: out, + client: client, + } + + cmd := &cobra.Command{ + Use: "status [flags] RELEASE_NAME", + Short: "displays the status of the named release", + Long: statusHelp, + PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errReleaseRequired + } + status.release = args[0] + if status.client == nil { + status.client = newClient() + } + return status.run() + }, + } + + cmd.PersistentFlags().Int32Var(&status.version, "revision", 0, "if set, display the status of the named release with revision") + cmd.PersistentFlags().StringVarP(&status.outfmt, "output", "o", "", "output the status in the specified format (json or yaml)") + + return cmd +} + +func (s *statusCmd) run() error { + res, err := s.client.ReleaseStatus(s.release, helm.StatusReleaseVersion(s.version)) + if err != nil { + return prettyError(err) + } + + switch s.outfmt { + case "": + PrintStatus(s.out, res) + return nil + case "json": + data, err := json.Marshal(res) + if err != nil { + return fmt.Errorf("Failed to Marshal JSON output: %s", err) + } + s.out.Write(data) + return nil + case "yaml": + data, err := yaml.Marshal(res) + if err != nil { + return fmt.Errorf("Failed to Marshal YAML output: %s", err) + } + s.out.Write(data) + return nil + } + + return fmt.Errorf("Unknown output format %q", s.outfmt) +} + +// PrintStatus prints out the status of a release. Shared because also used by +// install / upgrade +func PrintStatus(out io.Writer, res *services.GetReleaseStatusResponse) { + if res.Info.LastDeployed != nil { + fmt.Fprintf(out, "LAST DEPLOYED: %s\n", timeconv.String(res.Info.LastDeployed)) + } + fmt.Fprintf(out, "NAMESPACE: %s\n", res.Namespace) + fmt.Fprintf(out, "STATUS: %s\n", res.Info.Status.Code) + fmt.Fprintf(out, "\n") + if len(res.Info.Status.Resources) > 0 { + re := regexp.MustCompile(" +") + + w := tabwriter.NewWriter(out, 0, 0, 2, ' ', tabwriter.TabIndent) + fmt.Fprintf(w, "RESOURCES:\n%s\n", re.ReplaceAllString(res.Info.Status.Resources, "\t")) + w.Flush() + } + if res.Info.Status.LastTestSuiteRun != nil { + lastRun := res.Info.Status.LastTestSuiteRun + fmt.Fprintf(out, "TEST SUITE:\n%s\n%s\n\n%s\n", + fmt.Sprintf("Last Started: %s", timeconv.String(lastRun.StartedAt)), + fmt.Sprintf("Last Completed: %s", timeconv.String(lastRun.CompletedAt)), + formatTestResults(lastRun.Results)) + } + + if len(res.Info.Status.Notes) > 0 { + fmt.Fprintf(out, "NOTES:\n%s\n", res.Info.Status.Notes) + } +} + +func formatTestResults(results []*release.TestRun) string { + tbl := uitable.New() + tbl.MaxColWidth = 50 + tbl.AddRow("TEST", "STATUS", "INFO", "STARTED", "COMPLETED") + for i := 0; i < len(results); i++ { + r := results[i] + n := r.Name + s := strutil.PadRight(r.Status.String(), 10, ' ') + i := r.Info + ts := timeconv.String(r.StartedAt) + tc := timeconv.String(r.CompletedAt) + tbl.AddRow(n, s, i, ts, tc) + } + return tbl.String() +} diff --git a/pkg/helm/status_test.go b/pkg/helm/status_test.go new file mode 100644 index 000000000..b8ea1e8e9 --- /dev/null +++ b/pkg/helm/status_test.go @@ -0,0 +1,151 @@ +/* +Copyright 2017 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "testing" + + "github.com/golang/protobuf/ptypes/timestamp" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/timeconv" +) + +var ( + date = timestamp.Timestamp{Seconds: 242085845, Nanos: 0} + dateString = timeconv.String(&date) +) + +func TestStatusCmd(t *testing.T) { + tests := []releaseCase{ + { + name: "get status of a deployed release", + args: []string{"flummoxed-chickadee"}, + expected: outputWithStatus("DEPLOYED\n\n"), + rels: []*release.Release{ + releaseMockWithStatus(&release.Status{ + Code: release.Status_DEPLOYED, + }), + }, + }, + { + name: "get status of a deployed release with notes", + args: []string{"flummoxed-chickadee"}, + expected: outputWithStatus("DEPLOYED\n\nNOTES:\nrelease notes\n"), + rels: []*release.Release{ + releaseMockWithStatus(&release.Status{ + Code: release.Status_DEPLOYED, + Notes: "release notes", + }), + }, + }, + { + name: "get status of a deployed release with notes in json", + args: []string{"flummoxed-chickadee"}, + flags: []string{"-o", "json"}, + expected: `{"name":"flummoxed-chickadee","info":{"status":{"code":1,"notes":"release notes"},"first_deployed":{"seconds":242085845},"last_deployed":{"seconds":242085845}}}`, + rels: []*release.Release{ + releaseMockWithStatus(&release.Status{ + Code: release.Status_DEPLOYED, + Notes: "release notes", + }), + }, + }, + { + name: "get status of a deployed release with resources", + args: []string{"flummoxed-chickadee"}, + expected: outputWithStatus("DEPLOYED\n\nRESOURCES:\nresource A\nresource B\n\n"), + rels: []*release.Release{ + releaseMockWithStatus(&release.Status{ + Code: release.Status_DEPLOYED, + Resources: "resource A\nresource B\n", + }), + }, + }, + { + name: "get status of a deployed release with resources in YAML", + args: []string{"flummoxed-chickadee"}, + flags: []string{"-o", "yaml"}, + expected: "info:\n (.*)first_deployed:\n (.*)seconds: 242085845\n (.*)last_deployed:\n (.*)seconds: 242085845\n (.*)status:\n code: 1\n (.*)resources: |\n (.*)resource A\n (.*)resource B\nname: flummoxed-chickadee\n", + rels: []*release.Release{ + releaseMockWithStatus(&release.Status{ + Code: release.Status_DEPLOYED, + Resources: "resource A\nresource B\n", + }), + }, + }, + { + name: "get status of a deployed release with test suite", + args: []string{"flummoxed-chickadee"}, + expected: outputWithStatus( + fmt.Sprintf("DEPLOYED\n\nTEST SUITE:\nLast Started: %s\nLast Completed: %s\n\n", dateString, dateString) + + "TEST \tSTATUS (.*)\tINFO (.*)\tSTARTED (.*)\tCOMPLETED (.*)\n" + + fmt.Sprintf("test run 1\tSUCCESS (.*)\textra info\t%s\t%s\n", dateString, dateString) + + fmt.Sprintf("test run 2\tFAILURE (.*)\t (.*)\t%s\t%s\n", dateString, dateString)), + rels: []*release.Release{ + releaseMockWithStatus(&release.Status{ + Code: release.Status_DEPLOYED, + LastTestSuiteRun: &release.TestSuite{ + StartedAt: &date, + CompletedAt: &date, + Results: []*release.TestRun{ + { + Name: "test run 1", + Status: release.TestRun_SUCCESS, + Info: "extra info", + StartedAt: &date, + CompletedAt: &date, + }, + { + Name: "test run 2", + Status: release.TestRun_FAILURE, + StartedAt: &date, + CompletedAt: &date, + }, + }, + }, + }), + }, + }, + } + + runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newStatusCmd(c, out) + }) + +} + +func outputWithStatus(status string) string { + return fmt.Sprintf("LAST DEPLOYED: %s\nNAMESPACE: \nSTATUS: %s", + dateString, + status) +} + +func releaseMockWithStatus(status *release.Status) *release.Release { + return &release.Release{ + Name: "flummoxed-chickadee", + Info: &release.Info{ + FirstDeployed: &date, + LastDeployed: &date, + Status: status, + }, + } +} diff --git a/pkg/helm/template.go b/pkg/helm/template.go new file mode 100644 index 000000000..379b20bc0 --- /dev/null +++ b/pkg/helm/template.go @@ -0,0 +1,325 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/Masterminds/semver" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/engine" + "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/proto/hapi/release" + util "k8s.io/helm/pkg/releaseutil" + "k8s.io/helm/pkg/tiller" + "k8s.io/helm/pkg/timeconv" + tversion "k8s.io/helm/pkg/version" +) + +const defaultDirectoryPermission = 0755 + +var ( + whitespaceRegex = regexp.MustCompile(`^\s*$`) + + // defaultKubeVersion is the default value of --kube-version flag + defaultKubeVersion = fmt.Sprintf("%s.%s", chartutil.DefaultKubeVersion.Major, chartutil.DefaultKubeVersion.Minor) +) + +const templateDesc = ` +Render chart templates locally and display the output. + +This does not require Tiller. However, any values that would normally be +looked up or retrieved in-cluster will be faked locally. Additionally, none +of the server-side testing of chart validity (e.g. whether an API is supported) +is done. + +To render just one template in a chart, use '-x': + + $ helm template mychart -x templates/deployment.yaml +` + +type templateCmd struct { + namespace string + valueFiles valueFiles + chartPath string + out io.Writer + values []string + stringValues []string + nameTemplate string + showNotes bool + releaseName string + renderFiles []string + kubeVersion string + outputDir string +} + +func newTemplateCmd(out io.Writer) *cobra.Command { + + t := &templateCmd{ + out: out, + } + + cmd := &cobra.Command{ + Use: "template [flags] CHART", + Short: fmt.Sprintf("locally render templates"), + Long: templateDesc, + RunE: t.run, + } + + f := cmd.Flags() + f.BoolVar(&t.showNotes, "notes", false, "show the computed NOTES.txt file as well") + f.StringVarP(&t.releaseName, "name", "n", "RELEASE-NAME", "release name") + f.StringArrayVarP(&t.renderFiles, "execute", "x", []string{}, "only execute the given templates") + f.VarP(&t.valueFiles, "values", "f", "specify values in a YAML file (can specify multiple)") + f.StringVar(&t.namespace, "namespace", "", "namespace to install the release into") + f.StringArrayVar(&t.values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + f.StringArrayVar(&t.stringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + f.StringVar(&t.nameTemplate, "name-template", "", "specify template used to name the release") + f.StringVar(&t.kubeVersion, "kube-version", defaultKubeVersion, "kubernetes version used as Capabilities.KubeVersion.Major/Minor") + f.StringVar(&t.outputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout") + + return cmd +} + +func (t *templateCmd) run(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return errors.New("chart is required") + } + // verify chart path exists + if _, err := os.Stat(args[0]); err == nil { + if t.chartPath, err = filepath.Abs(args[0]); err != nil { + return err + } + } else { + return err + } + // verify specified templates exist relative to chart + rf := []string{} + var af string + var err error + if len(t.renderFiles) > 0 { + for _, f := range t.renderFiles { + if !filepath.IsAbs(f) { + af, err = filepath.Abs(filepath.Join(t.chartPath, f)) + if err != nil { + return fmt.Errorf("could not resolve template path: %s", err) + } + } else { + af = f + } + rf = append(rf, af) + + if _, err := os.Stat(af); err != nil { + return fmt.Errorf("could not resolve template path: %s", err) + } + } + } + + // verify that output-dir exists if provided + if t.outputDir != "" { + _, err = os.Stat(t.outputDir) + if os.IsNotExist(err) { + return fmt.Errorf("output-dir '%s' does not exist", t.outputDir) + } + } + + if t.namespace == "" { + t.namespace = defaultNamespace() + } + // get combined values and create config + rawVals, err := vals(t.valueFiles, t.values, t.stringValues) + if err != nil { + return err + } + config := &chart.Config{Raw: string(rawVals), Values: map[string]*chart.Value{}} + + // If template is specified, try to run the template. + if t.nameTemplate != "" { + t.releaseName, err = generateName(t.nameTemplate) + if err != nil { + return err + } + } + + // Check chart requirements to make sure all dependencies are present in /charts + c, err := chartutil.Load(t.chartPath) + if err != nil { + return prettyError(err) + } + + if req, err := chartutil.LoadRequirements(c); err == nil { + if err := checkDependencies(c, req); err != nil { + return prettyError(err) + } + } else if err != chartutil.ErrRequirementsNotFound { + return fmt.Errorf("cannot load requirements: %v", err) + } + options := chartutil.ReleaseOptions{ + Name: t.releaseName, + Time: timeconv.Now(), + Namespace: t.namespace, + } + + err = chartutil.ProcessRequirementsEnabled(c, config) + if err != nil { + return err + } + err = chartutil.ProcessRequirementsImportValues(c) + if err != nil { + return err + } + + // Set up engine. + renderer := engine.New() + + caps := &chartutil.Capabilities{ + APIVersions: chartutil.DefaultVersionSet, + KubeVersion: chartutil.DefaultKubeVersion, + TillerVersion: tversion.GetVersionProto(), + } + + // kubernetes version + kv, err := semver.NewVersion(t.kubeVersion) + if err != nil { + return fmt.Errorf("could not parse a kubernetes version: %v", err) + } + caps.KubeVersion.Major = fmt.Sprint(kv.Major()) + caps.KubeVersion.Minor = fmt.Sprint(kv.Minor()) + caps.KubeVersion.GitVersion = fmt.Sprintf("v%d.%d.0", kv.Major(), kv.Minor()) + + vals, err := chartutil.ToRenderValuesCaps(c, config, options, caps) + if err != nil { + return err + } + + out, err := renderer.Render(c, vals) + listManifests := []tiller.Manifest{} + if err != nil { + return err + } + // extract kind and name + re := regexp.MustCompile("kind:(.*)\n") + for k, v := range out { + match := re.FindStringSubmatch(v) + h := "Unknown" + if len(match) == 2 { + h = strings.TrimSpace(match[1]) + } + m := tiller.Manifest{Name: k, Content: v, Head: &util.SimpleHead{Kind: h}} + listManifests = append(listManifests, m) + } + in := func(needle string, haystack []string) bool { + // make needle path absolute + d := strings.Split(needle, string(os.PathSeparator)) + dd := d[1:] + an := filepath.Join(t.chartPath, strings.Join(dd, string(os.PathSeparator))) + + for _, h := range haystack { + if h == an { + return true + } + } + return false + } + if settings.Debug { + rel := &release.Release{ + Name: t.releaseName, + Chart: c, + Config: config, + Version: 1, + Namespace: t.namespace, + Info: &release.Info{LastDeployed: timeconv.Timestamp(time.Now())}, + } + printRelease(os.Stdout, rel) + } + + for _, m := range tiller.SortByKind(listManifests) { + if len(t.renderFiles) > 0 && !in(m.Name, rf) { + continue + } + data := m.Content + b := filepath.Base(m.Name) + if !t.showNotes && b == "NOTES.txt" { + continue + } + if strings.HasPrefix(b, "_") { + continue + } + + if t.outputDir != "" { + // blank template after execution + if whitespaceRegex.MatchString(data) { + continue + } + err = writeToFile(t.outputDir, m.Name, data) + if err != nil { + return err + } + continue + } + fmt.Printf("---\n# Source: %s\n", m.Name) + fmt.Println(data) + } + return nil +} + +// write the to / +func writeToFile(outputDir string, name string, data string) error { + outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator)) + + err := ensureDirectoryForFile(outfileName) + if err != nil { + return err + } + + f, err := os.Create(outfileName) + if err != nil { + return err + } + + defer f.Close() + + _, err = f.WriteString(fmt.Sprintf("##---\n# Source: %s\n%s", name, data)) + + if err != nil { + return err + } + + fmt.Printf("wrote %s\n", outfileName) + return nil +} + +// check if the directory exists to create file. creates if don't exists +func ensureDirectoryForFile(file string) error { + baseDir := path.Dir(file) + _, err := os.Stat(baseDir) + if err != nil && !os.IsNotExist(err) { + return err + } + + return os.MkdirAll(baseDir, defaultDirectoryPermission) +} diff --git a/pkg/helm/template_test.go b/pkg/helm/template_test.go new file mode 100644 index 000000000..ceae133aa --- /dev/null +++ b/pkg/helm/template_test.go @@ -0,0 +1,167 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" +) + +var chartPath = "./../../pkg/chartutil/testdata/subpop/charts/subchart1" + +func TestTemplateCmd(t *testing.T) { + absChartPath, err := filepath.Abs(chartPath) + if err != nil { + t.Fatal(err) + } + tests := []struct { + name string + desc string + args []string + expectKey string + expectValue string + }{ + { + name: "check_name", + desc: "check for a known name in chart", + args: []string{chartPath}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "protocol: TCP\n name: nginx", + }, + { + name: "check_set_name", + desc: "verify --set values exist", + args: []string{chartPath, "-x", "templates/service.yaml", "--set", "service.name=apache"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "protocol: TCP\n name: apache", + }, + { + name: "check_execute", + desc: "verify --execute single template", + args: []string{chartPath, "-x", "templates/service.yaml", "--set", "service.name=apache"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "protocol: TCP\n name: apache", + }, + { + name: "check_execute_absolute", + desc: "verify --execute single template", + args: []string{chartPath, "-x", absChartPath + "/" + "templates/service.yaml", "--set", "service.name=apache"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "protocol: TCP\n name: apache", + }, + { + name: "check_namespace", + desc: "verify --namespace", + args: []string{chartPath, "--namespace", "test"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "namespace: \"test\"", + }, + { + name: "check_release_name", + desc: "verify --release exists", + args: []string{chartPath, "--name", "test"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "release-name: \"test\"", + }, + { + name: "check_notes", + desc: "verify --notes shows notes", + args: []string{chartPath, "--notes", "true"}, + expectKey: "subchart1/templates/NOTES.txt", + expectValue: "Sample notes for subchart1", + }, + { + name: "check_values_files", + desc: "verify --values files values exist", + args: []string{chartPath, "--values", chartPath + "/charts/subchartA/values.yaml"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "name: apache", + }, + { + name: "check_name_template", + desc: "verify --name-template result exists", + args: []string{chartPath, "--name-template", "foobar-{{ b64enc \"abc\" }}-baz"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "release-name: \"foobar-YWJj-baz\"", + }, + { + name: "check_kube_version", + desc: "verify --kube-version overrides the kubernetes version", + args: []string{chartPath, "--kube-version", "1.6"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "kube-version/major: \"1\"\n kube-version/minor: \"6\"\n kube-version/gitversion: \"v1.6.0\"", + }, + } + + var buf bytes.Buffer + for _, tt := range tests { + t.Run(tt.name, func(T *testing.T) { + // capture stdout + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + // execute template command + out := bytes.NewBuffer(nil) + cmd := newTemplateCmd(out) + cmd.SetArgs(tt.args) + err := cmd.Execute() + if err != nil { + t.Errorf("expected: %v, got %v", tt.expectValue, err) + } + // restore stdout + w.Close() + os.Stdout = old + var b bytes.Buffer + io.Copy(&b, r) + r.Close() + // scan yaml into map[]yaml + scanner := bufio.NewScanner(&b) + next := false + lastKey := "" + m := map[string]string{} + for scanner.Scan() { + if scanner.Text() == "---" { + next = true + } else if next { + // remove '# Source: ' + head := "# Source: " + lastKey = scanner.Text()[len(head):] + next = false + } else { + m[lastKey] = m[lastKey] + scanner.Text() + "\n" + } + } + if err := scanner.Err(); err != nil { + fmt.Fprintln(os.Stderr, "reading standard input:", err) + } + if v, ok := m[tt.expectKey]; ok { + if !strings.Contains(v, tt.expectValue) { + t.Errorf("failed to match expected value %s in %s", tt.expectValue, v) + } + } else { + t.Errorf("could not find key %s", tt.expectKey) + } + buf.Reset() + }) + } +} diff --git a/pkg/helm/testdata/helm-test-key.pub b/pkg/helm/testdata/helm-test-key.pub new file mode 100644 index 0000000000000000000000000000000000000000..38714f25adaf701b08e11fd559a587074bbde0e4 GIT binary patch literal 1243 zcmV<11SI>J0SyFKmTjH^2mr{k15wFPQdpTAAEclY4oV^-7nTy5x$&xF;PC4il}o-Pk4@#$knlp#(|J0GE?qli_lr;7-o zyY8vBsN_GJe;#w<`JdR7riNL&RJlcS)FG+W=91;dYS6NZ2tY?kZ8Sw9{r=e4|L3E{ zRod|EPC!PWgW&pe&4qiqKQAijj;G~fyjcC^m%0p54Tn{h%YFKd0VC4n=#~SRc@BVd znj66*)Om%*SEQfX{1*Tb0RRECT}WkYZ6H)-b98BLXCNq4XlZjGYh`&Lb7*gMY-AvB zZftoVVr3w8b7f>8W^ZyJbY*jNX>MmOAVg0fPES-IR8mz_R4yqXJZNQXZ7p6uVakV zT7GGV$jaKjyjfI_a~N1!Hk?5C$0wa&4)R=i$v7t&ZMycW#RkavpF%A?>MTT2anNDzOQUm<++zEOykJ9-@&c2QXq3owqf7fek`=L@+7iF zv;IW2Q>Q&r+V@cWDF&hAUUsCKlDinerKgvJUJCl$5gjb7NhM{mBP%!M^mX-iS8xFf zuLB{@MDqvtZzF#Bxd9CXSC(y_0SExW>8~h=U8|!do4*OJj2u#!KDe3v+1T+aVzU5di=Ji2)x37y$|Z2?YXImTjH_8w>yn2@r%kznCAv zhhp0`2mpxVj5j%o&5i)?`r7iES|8dA@p2kk@+XS(tjBGN)6>tm^=gayCn`gTEC*K74Y~{I_PREk) z)PstIMx1RxB@cK8%Mey%;nVnKriAKUk2Ky?dBMG3uXItKL$3N(#3P^pQa*K$l)wUy F^>pMLK0g2e literal 0 HcmV?d00001 diff --git a/pkg/helm/testdata/helm-test-key.secret b/pkg/helm/testdata/helm-test-key.secret new file mode 100644 index 0000000000000000000000000000000000000000..a966aef93ed97d01d764f29940738df6df2d9d24 GIT binary patch literal 2545 zcmVclY4oV^-7nTy5x$&xF;PC4il}o-Pk4@#$knlp#(|J0GE?qli_lr;7-o zyY8vBsN_GJe;#w<`JdR7riNL&RJlcS)FG+W=91;dYS6NZ2tY?kZ8Sw9{r=e4|L3E{ zRod|EPC!PWgW&pe&4qiqKQAijj;G~fyjcC^m%0p54Tn{h%YFKd0VC4n=#~SRc@BVd znj66*)Om%*SEQfX{1*Tb0RRC22mUT~!#(ymA#eaSp1lpODzX${Vf^l{qDyu}xC-Z; zRnH<54GSVm<$?Ua1k#(+mu~3_*CIx=sPuoZB#9t`5)>)SncaZ0<~%)I$~BM-5aP3W z%`ewoaI;P40uHnDeE!9-_o2Lr{wDfL45jGGU-JZ36T9ToJqMX(TnRN-EvGi{o6aI#oT_2HU(J8=theYZsj5h?ml@F2 zqCpxqkdZi=~i+&Z}q^cR< zq>lNT5cnJ5X@K!3vOww0B>@Bg*7x*i59vbegj}$ELl?K2l`+`uY;jn;@-#}^!(c8$ z&Y`@LLxZ_Y>^#gGbxsy-2s=w7cVmR@z_%b#0_e^qDmIrpKw6U7N;6^TN}@&nxKj6i zje++&m}XQA&G8O8FX86?Frxrjmu5ktfDRyHBb|j&n&H#v>T!Mdmk8Y#1OV>P(*gow;}0v-BdsmdUSV3M9tIkRO0OTBw16eYCzxs>OEG!?i}$^8yY+hFlb3GJ~F z@#2Vimrfeb0(o3X?>!tSIROL!mGC1>cHXGVp;VD$oE{N!h=IF(C(PNLd6^nZO^!ix zHnE%@Y*d~bJl_M}WW0D1EM+&xdQI5#y67>-8{P4^*?j9-RL!3>e89fC4fbJFTGXY* zQA`Z&jZ*qV!0N>p>(<2RFPDhHj^h*B*O(i139Dwv{>MY%puY021Or@)I~ufINM&qo zAXH^@bZKs9AShI5X>%ZJWqBZTXm53FWFT*DY^`Z*m}XWpi|CZf7na zL{A`2PgEdOQdLt_E-4^9Xk~0|Ep%mbbZKs9Kxk!bZ7y?YK8XQ01QP)Y03iheSC(y_ z0viJb3ke7Z0|gZd2?z@X76JnS00JHX0vCV)3JDN|JHMD8!G~grMF;@2`RLQ-(ihS@ zk(ZDi8>PUMNBttVp^;f=(#~5ORUCP*V~o^Verbou%G$oXSyYd67+6|1oIv=;C!Jsp z@?3ezI42oxy7sHZ2FUrJLM=V)2rULvozb@g^vZ~+Ui10l{t^9T2DBYydv1DFI?mTjH^2mrz9 zuPBIJtD_~GzX`6498#D*yg_W@HI~u}LQvFZ zjHz2I7O5nm<}d0gU&SbRw}dGu2{gYWzK!Qb2tL4r=Ttf(&pz_gadeY}n}E@spby2h zn?Jq8s}cOpE&?`36cTnn-abrV^*hkY1rlNa6>W(?OAePZXMfE&?IzWku7z=T;E66)b)o_po?XSOFY;U!8IY8l|z)F~<`!sdiAt3+}0RRC2 z2mUKrERA52qzq^qU4-%uqeMA@h`$YTvMnKwO3MFdg819*{h|i5{tcC;Av-jm`%7`? zISDa>*_u$~x5)kpVt_aYB_e`#K)Xd5tcJ05BQ>ps?qeo`#OS{-ilRZ+9`nljqxsy1 zp;Lu#*--$l?6qncfhI%m^w(3lOt}ywL5?%+_Ov|T=-O)O#|1&>=}51a%Sb~KTR2_K z!};{n;NgPO;;v%0;n-j>b-Y|l)x=^&d84lKmr8o*+q*$Sul50u>9%n+e!b~90-}xc znpRXgsh*hBzGXpmnXaxdFnD1FEnbiC?537`DY#mL7&iHNEY4|+!A|s9dFssoYIy_z z+imR1K+cnVPeX&M1X~ed#U~gsS6HR0zgm1NR~u@{BN;*5Gvl)42%Kq{=4gSyFIAOo zw)ZGKn^3RZn+iXfb*zL1mnJJGsvTLnDB5DF8)!;+KX&@(mJ7k5LnTlXYxI(#)c`4{ zo6I4Djv|uTRI{JJ9glvUHq0WkzV7H91OVbY6u%#c1Z-!^cIjhIC)Ek7Hx7cRvtc6M zYLV(#kP^D1#2+7pDzLBFanZFqRw>On{`4qC48A)&{zk{n0CZEKY$SfN1Rk^!_V}?Oa05R<~;U7Vou+rQZcj7^Zr@2q2}K8g2gzsQ|Y$Hp^5`riTL^4T#Q?}_!b9ge@36zaVNe`|(D|D@%b z?q#ETmMPVDW6=SC^ zp>(H_BkTP!*5u$7;(xt$0Z3AJ%*#wE`2MxJYbiqwMV z55Sy@$Oto*a)E^@IG(B#1R_APzVCxJvjDnj!`b?U+KFs4f-?w1(lA6SB!RPKJ5Wx? zlJL}niiAd-Z9pXtcm~T5R%GGR_+_Sq>RpdC-c)(PyDc zVQyr3R8em|NM&qo0PL4di`y^|hWG4WG2L8RXjD7vWMOYhb13u^wwIoYVoz*QSu&EG zG;H_3cVsVVQK6y4WNFYth!rF2Bbs;KaiN>mptV>QH8<|nYyZu5ypb29krLPQd4DCs zYno+?eY?M(<+D7yfbmxF7dq>>q3MquC*0hBLW#C8qIE*68@PoxC>!V_0oK~U+irzM zp+lP}-rx-c;gW37*#6O!Wh_medN+}OCDi|h%MR_h3E+_aXIIyu{^#jc)c+}%z!KNI zlMaTH?`0nZ1xqIIxfT}a!{N*A`*&07)o|yqgtd_9J1nt~+#vWoF+>rxTo?;Z!^*e) za3B=@-09AM!={y-G7C#!$Suuo`fK9pkYAqq?>T!y{qK)u-(s#kG8G)sf11w%{Vx`I z9`%0;$nd3x`+N3*GzdSSe9Q)yTWw@{`S1`Wc-DmaXEjxTqEg!6XmcF&|HU8me?E`; zKM51~D|p3hMXl1%r=D?m(;lNxvj$(SQ_+>I=5K g`+DbSZ3(MHEDIu$NaT2a1^@v6|5o&hN&pl90NmINwg3PC literal 0 HcmV?d00001 diff --git a/pkg/helm/testdata/testcharts/compressedchart-0.2.0.tgz b/pkg/helm/testdata/testcharts/compressedchart-0.2.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..ba96a80c9c54db1f77a16b45623a9dd6ce816d76 GIT binary patch literal 540 zcmV+%0^|K3iwG0|32ul0|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PL4di`zUDg|qgrIPYZh78+H_I2oAT&@2Y}3DZqiMX_&eQCSj_ zoHR`5zZcm`E-QSt>6!|J_tAB{Md1ZoBKy7$rn!AAdSAp4Q@#_=pq4Yj^E`Wo`rjYRzQ%$*(h3ggpXalI{uhfp zPx?Oxx1^N8{XKgx8p3y!k8W_WHBOZm4-ZL#&zhnBSxu^ul{St}HpjvIzxeI`pU;#2 z&%ziVV;*<9OVs_rV_TEPs+Fpgx`GwOS5d`8{df)Vt+mDl4_}i~sFYcu5uvE1u3o~C z5U*Hb+ne>sKi!=8vngu_@LDeMt42ql3>X}Nlh$c%eyak e!8poTCDXMakw_$t=SKhl0RR87^wW$06aWBmqzKpm literal 0 HcmV?d00001 diff --git a/pkg/helm/testdata/testcharts/compressedchart-0.3.0.tgz b/pkg/helm/testdata/testcharts/compressedchart-0.3.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..89776bfa80fd8e37cb819073ce30792fbcb28fe3 GIT binary patch literal 538 zcmV+#0_FW5iwG0|32ul0|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PL4fYuhjw#&hXs31R)S-sgGc!Zg;A_f!_OaN*6{_+(qrsKLqLob$`stSdR^ z{gRKz^L@Q*ALo;NJizHz@DExagd1>v>^S|+JrPRsg_AZ&qpxs{8&VCrNWjWCyla=_ zGzIdR77bmY7cSYFobS4FHf20}?A;{(m%jejLbc>iYXFDzKbc)XlU>WOi zI7l4~wx)Fm)L10~d6r@4EzNfHd*ac~UmpAOJbU{3-yh4q#)3W43J&R?=aZiP=kq*I z`acIZq?Ezk9s4L6!cUYBZg88ag)XxpG*u%0Y~ZDJw30ul#72f1FJ3Wx(rOE%yPk1Lu;iemCYG_q?YG-t c9A&JM=~|FTBofE-D*yoh|LpYS&Hxku0CM*Yp8x;= literal 0 HcmV?d00001 diff --git a/pkg/helm/testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz b/pkg/helm/testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..379210a92c1795429d2c4baae389f1e971f26824 GIT binary patch literal 548 zcmV+<0^9u`iwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PL4fi`y^|#dG$jc->rDXjD0A64=|)92WW)wwIoYVoz*QSrU?* zG;H^~7dcDXrjTqgP3fZFMMBsbi zPWu0B_M89nr2n%p#E0nPPIpGV%QZGNX)If*N~tSYQG5|qH0t|nfN!leE_nEwltQJ< z5{(E&Ep_!Aj+6*;9W6i9KdlR0WlY0RR8Xa#gbc6aWCh@%&~0 literal 0 HcmV?d00001 diff --git a/pkg/helm/testdata/testcharts/decompressedchart/.helmignore b/pkg/helm/testdata/testcharts/decompressedchart/.helmignore new file mode 100644 index 000000000..435b756d8 --- /dev/null +++ b/pkg/helm/testdata/testcharts/decompressedchart/.helmignore @@ -0,0 +1,5 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +.git diff --git a/pkg/helm/testdata/testcharts/decompressedchart/Chart.yaml b/pkg/helm/testdata/testcharts/decompressedchart/Chart.yaml new file mode 100644 index 000000000..3e65afdfa --- /dev/null +++ b/pkg/helm/testdata/testcharts/decompressedchart/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: decompressedchart +version: 0.1.0 diff --git a/pkg/helm/testdata/testcharts/decompressedchart/values.yaml b/pkg/helm/testdata/testcharts/decompressedchart/values.yaml new file mode 100644 index 000000000..a940d1fd9 --- /dev/null +++ b/pkg/helm/testdata/testcharts/decompressedchart/values.yaml @@ -0,0 +1,4 @@ +# Default values for decompressedchart. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. + name: my-decompressed-chart diff --git a/pkg/helm/testdata/testcharts/novals/Chart.yaml b/pkg/helm/testdata/testcharts/novals/Chart.yaml new file mode 100644 index 000000000..ce1a81da6 --- /dev/null +++ b/pkg/helm/testdata/testcharts/novals/Chart.yaml @@ -0,0 +1,6 @@ +description: Deploy a basic Alpine Linux pod +home: https://k8s.io/helm +name: novals +sources: +- https://github.com/kubernetes/helm +version: 0.2.0 diff --git a/pkg/helm/testdata/testcharts/novals/README.md b/pkg/helm/testdata/testcharts/novals/README.md new file mode 100644 index 000000000..3c32de5db --- /dev/null +++ b/pkg/helm/testdata/testcharts/novals/README.md @@ -0,0 +1,13 @@ +#Alpine: A simple Helm chart + +Run a single pod of Alpine Linux. + +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.yaml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install docs/examples/alpine`. diff --git a/pkg/helm/testdata/testcharts/novals/templates/alpine-pod.yaml b/pkg/helm/testdata/testcharts/novals/templates/alpine-pod.yaml new file mode 100644 index 000000000..c15ab8efc --- /dev/null +++ b/pkg/helm/testdata/testcharts/novals/templates/alpine-pod.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{.Release.Name}}-{{.Values.Name}}" + labels: + # The "heritage" label is used to track which tool deployed a given chart. + # It is useful for admins who want to see what releases a particular tool + # is responsible for. + heritage: {{.Release.Service | quote }} + # The "release" convention makes it easy to tie a release to all of the + # Kubernetes resources that were created as part of that release. + release: {{.Release.Name | quote }} + # This makes it easy to audit chart usage. + chart: "{{.Chart.Name}}-{{.Chart.Version}}" + annotations: + "helm.sh/created": {{.Release.Time.Seconds | quote }} +spec: + # This shows how to use a simple value. This will look for a passed-in value + # called restartPolicy. If it is not found, it will use the default value. + # {{default "Never" .restartPolicy}} is a slightly optimized version of the + # more conventional syntax: {{.restartPolicy | default "Never"}} + restartPolicy: {{default "Never" .Values.restartPolicy}} + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/testdata/testcharts/reqtest-0.1.0.tgz b/pkg/helm/testdata/testcharts/reqtest-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..356bc93030395fa1c045c8aaefb60c9d80a079a5 GIT binary patch literal 911 zcmV;A191EwiwG0|32ul0|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PI=cZ=^O5zGwc5>E)%Zy8PMzIo?i5rB?kZ<#bY2Rh5AWxB)|L zlU(}Hzr6$8=1yd@=(ZfIWc?{JtYZ)M?tGtT28n-bRN6T&nAG+itI8L%!zDyP&|eAT ztLwSp{e9o>`N6680_I=I7PLw;Nss@(cE+1~BFIpsk~f;yB8J!S9hMcOoiD&uE#ZeY zK`D?t#1gE+7~Z>!b%Rp%Q(W7#UF*=hFxVFx{@<{&MfG_EV2b~~=a2axMS5P4G z`RApkwULSQx~j;)+w)7vNN6lO=i2GpVfmJw{3D&d-E0rq>P9#p3?;O`w&?{; zSzp`gwxKp**VO8Y?*FBsZ<*wEtKj>KZ|Q-JtpCDPTQ<*-Im0;WdWsUZ;XhqlF0n$P zm0i~9^^DJ$jQ`i}i2tWPr35hJ5+28q^FPA|MTR2fsABm24=dwDbsfXcwFXW{b?*|G zSvd-nbZ%!c_^ubO+*d1a{l<%8KZw1^4qmOJv$N{fxe{Wp>4@1wK|BK+U`rpP2ObzgPV+a3dD+x~V|6%FjW9B008H7+U)=U literal 0 HcmV?d00001 diff --git a/pkg/helm/testdata/testcharts/reqtest/.helmignore b/pkg/helm/testdata/testcharts/reqtest/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/pkg/helm/testdata/testcharts/reqtest/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/pkg/helm/testdata/testcharts/reqtest/Chart.yaml b/pkg/helm/testdata/testcharts/reqtest/Chart.yaml new file mode 100644 index 000000000..e2fbe4b01 --- /dev/null +++ b/pkg/helm/testdata/testcharts/reqtest/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: reqtest +version: 0.1.0 diff --git a/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart/.helmignore b/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml b/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml new file mode 100644 index 000000000..c3813bc8c --- /dev/null +++ b/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: reqsubchart +version: 0.1.0 diff --git a/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart/values.yaml b/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart/values.yaml new file mode 100644 index 000000000..0f0b63f2a --- /dev/null +++ b/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart/values.yaml @@ -0,0 +1,4 @@ +# Default values for reqsubchart. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name: value diff --git a/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart2/.helmignore b/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart2/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart2/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml b/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml new file mode 100644 index 000000000..9f7c22a71 --- /dev/null +++ b/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: reqsubchart2 +version: 0.2.0 diff --git a/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart2/values.yaml b/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart2/values.yaml new file mode 100644 index 000000000..0f0b63f2a --- /dev/null +++ b/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart2/values.yaml @@ -0,0 +1,4 @@ +# Default values for reqsubchart. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name: value diff --git a/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz b/pkg/helm/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..84b0fb65e6b05967ef186f30248384033a7d0a5f GIT binary patch literal 593 zcmV-X0Ch>%v#7WE? zC$pWF@vy(0OR{yXP?-eX(8PUI*^YCzFFk*K*8IZsSza@3BX4#;_`d%tNnotgbgca_ zp6Y8Li2NjtVm}T-@Pjx?;u$3O^+fqzUV4)LGIe-59RwOlI$wuLG7u&KF% ztQWEns)CN?=d9w!b>{H776we;b*;A8!2Kejl5GYJvw4lyFFIsZDCf54vp7enP<^K1`_~`y8+5p!@E919de7pP^{r6u)AHHHP>bw=ewcnKgQip z?CF2aWRK_ku@8W|^dAQ4FZn8IB%Q%XT5P~2G ff*=TjAP9mW2!bF8f*=Tj_$z(_p3(*m04M+eV_7RC literal 0 HcmV?d00001 diff --git a/pkg/helm/testdata/testcharts/reqtest/requirements.lock b/pkg/helm/testdata/testcharts/reqtest/requirements.lock new file mode 100644 index 000000000..ab1ae8cc0 --- /dev/null +++ b/pkg/helm/testdata/testcharts/reqtest/requirements.lock @@ -0,0 +1,3 @@ +dependencies: [] +digest: Not implemented +generated: 2016-09-13T17:25:17.593788787-06:00 diff --git a/pkg/helm/testdata/testcharts/reqtest/requirements.yaml b/pkg/helm/testdata/testcharts/reqtest/requirements.yaml new file mode 100644 index 000000000..1ddedc742 --- /dev/null +++ b/pkg/helm/testdata/testcharts/reqtest/requirements.yaml @@ -0,0 +1,10 @@ +dependencies: + - name: reqsubchart + version: 0.1.0 + repository: "https://example.com/charts" + - name: reqsubchart2 + version: 0.2.0 + repository: "https://example.com/charts" + - name: reqsubchart3 + version: ">=0.1.0" + repository: "https://example.com/charts" diff --git a/pkg/helm/testdata/testcharts/reqtest/values.yaml b/pkg/helm/testdata/testcharts/reqtest/values.yaml new file mode 100644 index 000000000..d57f76b07 --- /dev/null +++ b/pkg/helm/testdata/testcharts/reqtest/values.yaml @@ -0,0 +1,4 @@ +# Default values for reqtest. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name: value diff --git a/pkg/helm/testdata/testcharts/signtest-0.1.0.tgz b/pkg/helm/testdata/testcharts/signtest-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..6de9d988d47cfea649eaa5b23ba09d8a5fee04ed GIT binary patch literal 471 zcmV;|0Vw_-iwG0|32ul0|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PL1s>(ek4#&_LMab!0N8r#jSu)EfR!Yhji_ntJ+cZn_Q$NgS zvpkzm;N}PUn+EH+q3y3-=lpU{L>1c7h~5dUR^fjXXQ78U)Tn=deive8Xe@3wX&i_1nlSlsVp($*z=7V%F7C^xM zSQIRo!k1Q9pohcP^~Vpd=yk`P!wPC4(Fbg>l-wYAgBYs_dM=Cwr=jqDYbjbN8Xoju zz+u-*PRsk`(N#iL^pHpB#6N4v`e~pI-g=LV{4bY(@IPBd{_mkFeD*jS6?h%LKkQpn zPz*v=LN!Ei`JFc-ufYxM(D&Ln>QK!{XrwNHOrdNk`Xv}7y2Z|u@7iDHxvD(y*l_=| z0ndAbwfI5Suoo2f>;;2QN*+L~km-*EJsOZgkHDk|z~{R{vA N|NqlRq0#^l007yS;m800 literal 0 HcmV?d00001 diff --git a/pkg/helm/testdata/testcharts/signtest-0.1.0.tgz.prov b/pkg/helm/testdata/testcharts/signtest-0.1.0.tgz.prov new file mode 100644 index 000000000..94235399a --- /dev/null +++ b/pkg/helm/testdata/testcharts/signtest-0.1.0.tgz.prov @@ -0,0 +1,20 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +description: A Helm chart for Kubernetes +name: signtest +version: 0.1.0 + +... +files: + signtest-0.1.0.tgz: sha256:dee72947753628425b82814516bdaa37aef49f25e8820dd2a6e15a33a007823b +-----BEGIN PGP SIGNATURE----- + +wsBcBAEBCgAQBQJXomNHCRCEO7+YH8GHYgAALywIAG1Me852Fpn1GYu8Q1GCcw4g +l2k7vOFchdDwDhdSVbkh4YyvTaIO3iE2Jtk1rxw+RIJiUr0eLO/rnIJuxZS8WKki +DR1LI9J1VD4dxN3uDETtWDWq7ScoPsRY5mJvYZXC8whrWEt/H2kfqmoA9LloRPWp +flOE0iktA4UciZOblTj6nAk3iDyjh/4HYL4a6tT0LjjKI7OTw4YyHfjHad1ywVCz +9dMUc1rPgTnl+fnRiSPSrlZIWKOt1mcQ4fVrU3nwtRUwTId2k8FtygL0G6M+Y6t0 +S6yaU7qfk9uTxkdkUF7Bf1X3ukxfe+cNBC32vf4m8LY4NkcYfSqK2fGtQsnVr6s= +=NyOM +-----END PGP SIGNATURE----- \ No newline at end of file diff --git a/pkg/helm/testdata/testcharts/signtest/.helmignore b/pkg/helm/testdata/testcharts/signtest/.helmignore new file mode 100644 index 000000000..435b756d8 --- /dev/null +++ b/pkg/helm/testdata/testcharts/signtest/.helmignore @@ -0,0 +1,5 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +.git diff --git a/pkg/helm/testdata/testcharts/signtest/Chart.yaml b/pkg/helm/testdata/testcharts/signtest/Chart.yaml new file mode 100644 index 000000000..90964b44a --- /dev/null +++ b/pkg/helm/testdata/testcharts/signtest/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: signtest +version: 0.1.0 diff --git a/pkg/helm/testdata/testcharts/signtest/alpine/Chart.yaml b/pkg/helm/testdata/testcharts/signtest/alpine/Chart.yaml new file mode 100644 index 000000000..6fbb27f18 --- /dev/null +++ b/pkg/helm/testdata/testcharts/signtest/alpine/Chart.yaml @@ -0,0 +1,6 @@ +description: Deploy a basic Alpine Linux pod +home: https://k8s.io/helm +name: alpine +sources: +- https://github.com/kubernetes/helm +version: 0.1.0 diff --git a/pkg/helm/testdata/testcharts/signtest/alpine/README.md b/pkg/helm/testdata/testcharts/signtest/alpine/README.md new file mode 100644 index 000000000..5bd595747 --- /dev/null +++ b/pkg/helm/testdata/testcharts/signtest/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.yaml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install docs/examples/alpine`. diff --git a/pkg/helm/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml b/pkg/helm/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..08cf3c2c1 --- /dev/null +++ b/pkg/helm/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + heritage: {{.Release.Service}} + chartName: {{.Chart.Name}} + chartVersion: {{.Chart.Version | quote}} + annotations: + "helm.sh/created": "{{.Release.Time.Seconds}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/testdata/testcharts/signtest/alpine/values.yaml b/pkg/helm/testdata/testcharts/signtest/alpine/values.yaml new file mode 100644 index 000000000..bb6c06ae4 --- /dev/null +++ b/pkg/helm/testdata/testcharts/signtest/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: my-alpine diff --git a/pkg/helm/testdata/testcharts/signtest/templates/pod.yaml b/pkg/helm/testdata/testcharts/signtest/templates/pod.yaml new file mode 100644 index 000000000..9b00ccaf7 --- /dev/null +++ b/pkg/helm/testdata/testcharts/signtest/templates/pod.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: signtest +spec: + restartPolicy: Never + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/testdata/testcharts/signtest/values.yaml b/pkg/helm/testdata/testcharts/signtest/values.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/helm/testdata/testserver/index.yaml b/pkg/helm/testdata/testserver/index.yaml new file mode 100644 index 000000000..9cde8e8dd --- /dev/null +++ b/pkg/helm/testdata/testserver/index.yaml @@ -0,0 +1 @@ +apiVersion: v1 diff --git a/pkg/helm/testdata/testserver/repository/repositories.yaml b/pkg/helm/testdata/testserver/repository/repositories.yaml new file mode 100644 index 000000000..271301c95 --- /dev/null +++ b/pkg/helm/testdata/testserver/repository/repositories.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +generated: 2016-10-04T13:50:02.87649685-06:00 +repositories: +- cache: "" + name: test + url: http://127.0.0.1:49216 diff --git a/pkg/helm/upgrade.go b/pkg/helm/upgrade.go new file mode 100644 index 000000000..2d2a4d57e --- /dev/null +++ b/pkg/helm/upgrade.go @@ -0,0 +1,246 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "strings" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/storage/driver" +) + +const upgradeDesc = ` +This command upgrades a release to a new version of a chart. + +The upgrade arguments must be a release and chart. The chart +argument can be either: a chart reference('stable/mariadb'), a path to a chart directory, +a packaged chart, or a fully qualified URL. For chart references, the latest +version will be specified unless the '--version' flag is set. + +To override values in a chart, use either the '--values' flag and pass in a file +or use the '--set' flag and pass configuration from the command line, to force string +values, use '--set-string'. + +You can specify the '--values'/'-f' flag multiple times. The priority will be given to the +last (right-most) file specified. For example, if both myvalues.yaml and override.yaml +contained a key called 'Test', the value set in override.yaml would take precedence: + + $ helm upgrade -f myvalues.yaml -f override.yaml redis ./redis + +You can specify the '--set' flag multiple times. The priority will be given to the +last (right-most) set specified. For example, if both 'bar' and 'newbar' values are +set for a key called 'foo', the 'newbar' value would take precedence: + + $ helm upgrade --set foo=bar --set foo=newbar redis ./redis +` + +type upgradeCmd struct { + release string + chart string + out io.Writer + client helm.Interface + dryRun bool + recreate bool + force bool + disableHooks bool + valueFiles valueFiles + values []string + stringValues []string + verify bool + keyring string + install bool + namespace string + version string + timeout int64 + resetValues bool + reuseValues bool + wait bool + repoURL string + username string + password string + devel bool + + certFile string + keyFile string + caFile string +} + +func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { + + upgrade := &upgradeCmd{ + out: out, + client: client, + } + + cmd := &cobra.Command{ + Use: "upgrade [RELEASE] [CHART]", + Short: "upgrade a release", + Long: upgradeDesc, + PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkArgsLength(len(args), "release name", "chart path"); err != nil { + return err + } + + if upgrade.version == "" && upgrade.devel { + debug("setting version to >0.0.0-0") + upgrade.version = ">0.0.0-0" + } + + upgrade.release = args[0] + upgrade.chart = args[1] + upgrade.client = ensureHelmClient(upgrade.client) + + return upgrade.run() + }, + } + + f := cmd.Flags() + f.VarP(&upgrade.valueFiles, "values", "f", "specify values in a YAML file or a URL(can specify multiple)") + f.BoolVar(&upgrade.dryRun, "dry-run", false, "simulate an upgrade") + f.BoolVar(&upgrade.recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") + f.BoolVar(&upgrade.force, "force", false, "force resource update through delete/recreate if needed") + f.StringArrayVar(&upgrade.values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + f.StringArrayVar(&upgrade.stringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + f.BoolVar(&upgrade.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks. DEPRECATED. Use no-hooks") + f.BoolVar(&upgrade.disableHooks, "no-hooks", false, "disable pre/post upgrade hooks") + f.BoolVar(&upgrade.verify, "verify", false, "verify the provenance of the chart before upgrading") + f.StringVar(&upgrade.keyring, "keyring", defaultKeyring(), "path to the keyring that contains public signing keys") + f.BoolVarP(&upgrade.install, "install", "i", false, "if a release by this name doesn't already exist, run an install") + f.StringVar(&upgrade.namespace, "namespace", "", "namespace to install the release into (only used if --install is set). Defaults to the current kube config namespace") + f.StringVar(&upgrade.version, "version", "", "specify the exact chart version to use. If this is not specified, the latest version is used") + f.Int64Var(&upgrade.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") + f.BoolVar(&upgrade.resetValues, "reset-values", false, "when upgrading, reset the values to the ones built into the chart") + f.BoolVar(&upgrade.reuseValues, "reuse-values", false, "when upgrading, reuse the last release's values, and merge in any new values. If '--reset-values' is specified, this is ignored.") + f.BoolVar(&upgrade.wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout") + f.StringVar(&upgrade.repoURL, "repo", "", "chart repository url where to locate the requested chart") + f.StringVar(&upgrade.username, "username", "", "chart repository username where to locate the requested chart") + f.StringVar(&upgrade.password, "password", "", "chart repository password where to locate the requested chart") + f.StringVar(&upgrade.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&upgrade.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.StringVar(&upgrade.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") + f.BoolVar(&upgrade.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") + + f.MarkDeprecated("disable-hooks", "use --no-hooks instead") + + return cmd +} + +func (u *upgradeCmd) run() error { + chartPath, err := locateChartPath(u.repoURL, u.username, u.password, u.chart, u.version, u.verify, u.keyring, u.certFile, u.keyFile, u.caFile) + if err != nil { + return err + } + + if u.install { + // If a release does not exist, install it. If another error occurs during + // the check, ignore the error and continue with the upgrade. + // + // The returned error is a grpc.rpcError that wraps the message from the original error. + // So we're stuck doing string matching against the wrapped error, which is nested somewhere + // inside of the grpc.rpcError message. + releaseHistory, err := u.client.ReleaseHistory(u.release, helm.WithMaxHistory(1)) + + if err == nil { + if u.namespace == "" { + u.namespace = defaultNamespace() + } + previousReleaseNamespace := releaseHistory.Releases[0].Namespace + if previousReleaseNamespace != u.namespace { + fmt.Fprintf(u.out, + "WARNING: Namespace %q doesn't match with previous. Release will be deployed to %s\n", + u.namespace, previousReleaseNamespace, + ) + } + } + + if err != nil && strings.Contains(err.Error(), driver.ErrReleaseNotFound(u.release).Error()) { + fmt.Fprintf(u.out, "Release %q does not exist. Installing it now.\n", u.release) + ic := &installCmd{ + chartPath: chartPath, + client: u.client, + out: u.out, + name: u.release, + valueFiles: u.valueFiles, + dryRun: u.dryRun, + verify: u.verify, + disableHooks: u.disableHooks, + keyring: u.keyring, + values: u.values, + stringValues: u.stringValues, + namespace: u.namespace, + timeout: u.timeout, + wait: u.wait, + } + return ic.run() + } + } + + rawVals, err := vals(u.valueFiles, u.values, u.stringValues) + if err != nil { + return err + } + + // Check chart requirements to make sure all dependencies are present in /charts + if ch, err := chartutil.Load(chartPath); err == nil { + if req, err := chartutil.LoadRequirements(ch); err == nil { + if err := checkDependencies(ch, req); err != nil { + return err + } + } else if err != chartutil.ErrRequirementsNotFound { + return fmt.Errorf("cannot load requirements: %v", err) + } + } else { + return prettyError(err) + } + + resp, err := u.client.UpdateRelease( + u.release, + chartPath, + helm.UpdateValueOverrides(rawVals), + helm.UpgradeDryRun(u.dryRun), + helm.UpgradeRecreate(u.recreate), + helm.UpgradeForce(u.force), + helm.UpgradeDisableHooks(u.disableHooks), + helm.UpgradeTimeout(u.timeout), + helm.ResetValues(u.resetValues), + helm.ReuseValues(u.reuseValues), + helm.UpgradeWait(u.wait)) + if err != nil { + return fmt.Errorf("UPGRADE FAILED: %v", prettyError(err)) + } + + if settings.Debug { + printRelease(u.out, resp.Release) + } + + fmt.Fprintf(u.out, "Release %q has been upgraded. Happy Helming!\n", u.release) + + // Print the status like status command does + status, err := u.client.ReleaseStatus(u.release) + if err != nil { + return prettyError(err) + } + PrintStatus(u.out, status) + + return nil +} diff --git a/pkg/helm/upgrade_test.go b/pkg/helm/upgrade_test.go new file mode 100644 index 000000000..9b453f1fe --- /dev/null +++ b/pkg/helm/upgrade_test.go @@ -0,0 +1,170 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "io" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestUpgradeCmd(t *testing.T) { + tmpChart, _ := ioutil.TempDir("testdata", "tmp") + defer os.RemoveAll(tmpChart) + cfile := &chart.Metadata{ + Name: "testUpgradeChart", + Description: "A Helm chart for Kubernetes", + Version: "0.1.0", + } + chartPath, err := chartutil.Create(cfile, tmpChart) + if err != nil { + t.Errorf("Error creating chart for upgrade: %v", err) + } + ch, _ := chartutil.Load(chartPath) + _ = helm.ReleaseMock(&helm.MockReleaseOptions{ + Name: "funny-bunny", + Chart: ch, + }) + + // update chart version + cfile = &chart.Metadata{ + Name: "testUpgradeChart", + Description: "A Helm chart for Kubernetes", + Version: "0.1.2", + } + + chartPath, err = chartutil.Create(cfile, tmpChart) + if err != nil { + t.Errorf("Error creating chart: %v", err) + } + ch, err = chartutil.Load(chartPath) + if err != nil { + t.Errorf("Error loading updated chart: %v", err) + } + + // update chart version again + cfile = &chart.Metadata{ + Name: "testUpgradeChart", + Description: "A Helm chart for Kubernetes", + Version: "0.1.3", + } + + chartPath, err = chartutil.Create(cfile, tmpChart) + if err != nil { + t.Errorf("Error creating chart: %v", err) + } + var ch2 *chart.Chart + ch2, err = chartutil.Load(chartPath) + if err != nil { + t.Errorf("Error loading updated chart: %v", err) + } + + originalDepsPath := filepath.Join("testdata/testcharts/reqtest") + missingDepsPath := filepath.Join("testdata/testcharts/chart-missing-deps") + badDepsPath := filepath.Join("testdata/testcharts/chart-bad-requirements") + var ch3 *chart.Chart + ch3, err = chartutil.Load(originalDepsPath) + if err != nil { + t.Errorf("Error loading chart with missing dependencies: %v", err) + } + + tests := []releaseCase{ + { + name: "upgrade a release", + args: []string{"funny-bunny", chartPath}, + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 2, Chart: ch}), + expected: "Release \"funny-bunny\" has been upgraded. Happy Helming!\n", + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 2, Chart: ch})}, + }, + { + name: "upgrade a release with timeout", + args: []string{"funny-bunny", chartPath}, + flags: []string{"--timeout", "120"}, + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 3, Chart: ch2}), + expected: "Release \"funny-bunny\" has been upgraded. Happy Helming!\n", + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 3, Chart: ch2})}, + }, + { + name: "upgrade a release with --reset-values", + args: []string{"funny-bunny", chartPath}, + flags: []string{"--reset-values", "true"}, + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 4, Chart: ch2}), + expected: "Release \"funny-bunny\" has been upgraded. Happy Helming!\n", + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 4, Chart: ch2})}, + }, + { + name: "upgrade a release with --reuse-values", + args: []string{"funny-bunny", chartPath}, + flags: []string{"--reuse-values", "true"}, + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 5, Chart: ch2}), + expected: "Release \"funny-bunny\" has been upgraded. Happy Helming!\n", + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 5, Chart: ch2})}, + }, + { + name: "install a release with 'upgrade --install'", + args: []string{"zany-bunny", chartPath}, + flags: []string{"-i"}, + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "zany-bunny", Version: 1, Chart: ch}), + expected: "Release \"zany-bunny\" has been upgraded. Happy Helming!\n", + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "zany-bunny", Version: 1, Chart: ch})}, + }, + { + name: "install a release with 'upgrade --install' and timeout", + args: []string{"crazy-bunny", chartPath}, + flags: []string{"-i", "--timeout", "120"}, + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "crazy-bunny", Version: 1, Chart: ch}), + expected: "Release \"crazy-bunny\" has been upgraded. Happy Helming!\n", + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "crazy-bunny", Version: 1, Chart: ch})}, + }, + { + name: "upgrade a release with wait", + args: []string{"crazy-bunny", chartPath}, + flags: []string{"--wait"}, + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "crazy-bunny", Version: 2, Chart: ch2}), + expected: "Release \"crazy-bunny\" has been upgraded. Happy Helming!\n", + rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "crazy-bunny", Version: 2, Chart: ch2})}, + }, + { + name: "upgrade a release with missing dependencies", + args: []string{"bonkers-bunny", missingDepsPath}, + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "bonkers-bunny", Version: 1, Chart: ch3}), + err: true, + }, + { + name: "upgrade a release with bad dependencies", + args: []string{"bonkers-bunny", badDepsPath}, + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "bonkers-bunny", Version: 1, Chart: ch3}), + err: true, + }, + } + + cmd := func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newUpgradeCmd(c, out) + } + + runReleaseCases(t, tests, cmd) + +} diff --git a/pkg/helm/verify.go b/pkg/helm/verify.go new file mode 100644 index 000000000..bbdca9589 --- /dev/null +++ b/pkg/helm/verify.go @@ -0,0 +1,70 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "errors" + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/downloader" +) + +const verifyDesc = ` +Verify that the given chart has a valid provenance file. + +Provenance files provide crytographic verification that a chart has not been +tampered with, and was packaged by a trusted provider. + +This command can be used to verify a local chart. Several other commands provide +'--verify' flags that run the same validation. To generate a signed package, use +the 'helm package --sign' command. +` + +type verifyCmd struct { + keyring string + chartfile string + + out io.Writer +} + +func newVerifyCmd(out io.Writer) *cobra.Command { + vc := &verifyCmd{out: out} + + cmd := &cobra.Command{ + Use: "verify [flags] PATH", + Short: "verify that a chart at the given path has been signed and is valid", + Long: verifyDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("a path to a package file is required") + } + vc.chartfile = args[0] + return vc.run() + }, + } + + f := cmd.Flags() + f.StringVar(&vc.keyring, "keyring", defaultKeyring(), "keyring containing public keys") + + return cmd +} + +func (v *verifyCmd) run() error { + _, err := downloader.VerifyChart(v.chartfile, v.keyring) + return err +} diff --git a/pkg/helm/verify_test.go b/pkg/helm/verify_test.go new file mode 100644 index 000000000..d957e360d --- /dev/null +++ b/pkg/helm/verify_test.go @@ -0,0 +1,95 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "bytes" + "fmt" + "runtime" + "testing" +) + +func TestVerifyCmd(t *testing.T) { + + statExe := "stat" + statPathMsg := "no such file or directory" + statFileMsg := statPathMsg + if runtime.GOOS == "windows" { + statExe = "GetFileAttributesEx" + statPathMsg = "The system cannot find the path specified." + statFileMsg = "The system cannot find the file specified." + } + + tests := []struct { + name string + args []string + flags []string + expect string + err bool + }{ + { + name: "verify requires a chart", + expect: "a path to a package file is required", + err: true, + }, + { + name: "verify requires that chart exists", + args: []string{"no/such/file"}, + expect: fmt.Sprintf("%s no/such/file: %s", statExe, statPathMsg), + err: true, + }, + { + name: "verify requires that chart is not a directory", + args: []string{"testdata/testcharts/signtest"}, + expect: "unpacked charts cannot be verified", + err: true, + }, + { + name: "verify requires that chart has prov file", + args: []string{"testdata/testcharts/compressedchart-0.1.0.tgz"}, + expect: fmt.Sprintf("could not load provenance file testdata/testcharts/compressedchart-0.1.0.tgz.prov: %s testdata/testcharts/compressedchart-0.1.0.tgz.prov: %s", statExe, statFileMsg), + err: true, + }, + { + name: "verify validates a properly signed chart", + args: []string{"testdata/testcharts/signtest-0.1.0.tgz"}, + flags: []string{"--keyring", "testdata/helm-test-key.pub"}, + expect: "", + err: false, + }, + } + + for _, tt := range tests { + b := bytes.NewBuffer(nil) + vc := newVerifyCmd(b) + vc.ParseFlags(tt.flags) + err := vc.RunE(vc, tt.args) + if tt.err { + if err == nil { + t.Errorf("Expected error, but got none: %q", b.String()) + } + if err.Error() != tt.expect { + t.Errorf("Expected error %q, got %q", tt.expect, err) + } + continue + } else if err != nil { + t.Errorf("Unexpected error: %s", err) + } + if b.String() != tt.expect { + t.Errorf("Expected %q, got %q", tt.expect, b.String()) + } + } +} diff --git a/pkg/helm/version.go b/pkg/helm/version.go new file mode 100644 index 000000000..88fd557e4 --- /dev/null +++ b/pkg/helm/version.go @@ -0,0 +1,151 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "errors" + "fmt" + "io" + + "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + + apiVersion "k8s.io/apimachinery/pkg/version" + "k8s.io/helm/pkg/helm" + pb "k8s.io/helm/pkg/proto/hapi/version" + "k8s.io/helm/pkg/version" +) + +const versionDesc = ` +Show the client and server versions for Helm and tiller. + +This will print a representation of the client and server versions of Helm and +Tiller. The output will look something like this: + +Client: &version.Version{SemVer:"v2.0.0", GitCommit:"ff52399e51bb880526e9cd0ed8386f6433b74da1", GitTreeState:"clean"} +Server: &version.Version{SemVer:"v2.0.0", GitCommit:"b0c113dfb9f612a9add796549da66c0d294508a3", GitTreeState:"clean"} + +- SemVer is the semantic version of the release. +- GitCommit is the SHA for the commit that this version was built from. +- GitTreeState is "clean" if there are no local code changes when this binary was + built, and "dirty" if the binary was built from locally modified code. + +To print just the client version, use '--client'. To print just the server version, +use '--server'. +` + +type versionCmd struct { + out io.Writer + client helm.Interface + showClient bool + showServer bool + short bool + template string +} + +func newVersionCmd(c helm.Interface, out io.Writer) *cobra.Command { + version := &versionCmd{ + client: c, + out: out, + } + + cmd := &cobra.Command{ + Use: "version", + Short: "print the client/server version information", + Long: versionDesc, + RunE: func(cmd *cobra.Command, args []string) error { + // If neither is explicitly set, show both. + if !version.showClient && !version.showServer { + version.showClient, version.showServer = true, true + } + if version.showServer { + // We do this manually instead of in PreRun because we only + // need a tunnel if server version is requested. + setupConnection() + } + version.client = ensureHelmClient(version.client) + return version.run() + }, + } + f := cmd.Flags() + f.BoolVarP(&version.showClient, "client", "c", false, "client version only") + f.BoolVarP(&version.showServer, "server", "s", false, "server version only") + f.BoolVar(&version.short, "short", false, "print the version number") + f.StringVar(&version.template, "template", "", "template for version string format") + + return cmd +} + +func (v *versionCmd) run() error { + // Store map data for template rendering + data := map[string]interface{}{} + + if v.showClient { + cv := version.GetVersionProto() + if v.template != "" { + data["Client"] = cv + } else { + fmt.Fprintf(v.out, "Client: %s\n", formatVersion(cv, v.short)) + } + } + + if !v.showServer { + return tpl(v.template, data, v.out) + } + + if settings.Debug { + k8sVersion, err := getK8sVersion() + if err != nil { + return err + } + fmt.Fprintf(v.out, "Kubernetes: %#v\n", k8sVersion) + } + + resp, err := v.client.GetVersion() + if err != nil { + if grpc.Code(err) == codes.Unimplemented { + return errors.New("server is too old to know its version") + } + debug("%s", err) + return errors.New("cannot connect to Tiller") + } + + if v.template != "" { + data["Server"] = resp.Version + } else { + fmt.Fprintf(v.out, "Server: %s\n", formatVersion(resp.Version, v.short)) + } + return tpl(v.template, data, v.out) +} + +func getK8sVersion() (*apiVersion.Info, error) { + var v *apiVersion.Info + _, client, err := getKubeClient(settings.KubeContext) + if err != nil { + return v, err + } + v, err = client.Discovery().ServerVersion() + return v, err +} + +func formatVersion(v *pb.Version, short bool) string { + if short { + return fmt.Sprintf("%s+g%s", v.SemVer, v.GitCommit[:7]) + } + return fmt.Sprintf("%#v", v) +} diff --git a/pkg/helm/version_test.go b/pkg/helm/version_test.go new file mode 100644 index 000000000..310ff38ee --- /dev/null +++ b/pkg/helm/version_test.go @@ -0,0 +1,65 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm + +import ( + "fmt" + "io" + "regexp" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/version" +) + +func TestVersion(t *testing.T) { + lver := regexp.QuoteMeta(version.GetVersionProto().SemVer) + sver := regexp.QuoteMeta("1.2.3-fakeclient+testonly") + clientVersion := fmt.Sprintf("Client: &version\\.Version{SemVer:\"%s\", GitCommit:\"\", GitTreeState:\"\"}\n", lver) + serverVersion := fmt.Sprintf("Server: &version\\.Version{SemVer:\"%s\", GitCommit:\"\", GitTreeState:\"\"}\n", sver) + + tests := []releaseCase{ + { + name: "default", + args: []string{}, + expected: clientVersion + serverVersion, + }, + { + name: "client", + args: []string{}, + flags: []string{"-c"}, + expected: clientVersion, + }, + { + name: "server", + args: []string{}, + flags: []string{"-s"}, + expected: serverVersion, + }, + { + name: "template", + args: []string{}, + flags: []string{"--template", "{{ .Client.SemVer }} {{ .Server.SemVer }}"}, + expected: lver + " " + sver, + }, + } + settings.TillerHost = "fake-localhost" + runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { + return newVersionCmd(c, out) + }) +} diff --git a/pkg/lifecycle/render/helm/fetch.go b/pkg/lifecycle/render/helm/fetch.go index fdd519670..741faf874 100644 --- a/pkg/lifecycle/render/helm/fetch.go +++ b/pkg/lifecycle/render/helm/fetch.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/libyaml" "github.com/replicatedhq/ship/pkg/api" + "github.com/replicatedhq/ship/pkg/helm" "github.com/replicatedhq/ship/pkg/lifecycle/render/github" "github.com/spf13/afero" ) @@ -46,7 +47,7 @@ func (f *ClientFetcher) FetchChart( debug.Log("event", "chart.fetch", "source", "local", "root", asset.Local.ChartRoot) return asset.Local.ChartRoot, nil } else if asset.GitHub != nil { - checkoutDir, err := f.FS.TempDir("/tmp", "helmchart") + checkoutDir, err := f.FS.TempDir("", "helmchart") if err != nil { return "", errors.Wrap(err, "get chart checkout tmpdir") } @@ -63,6 +64,17 @@ func (f *ClientFetcher) FetchChart( } return path.Join(checkoutDir, asset.GitHub.Path), nil + } else if asset.Git != nil { + checkoutDir, err := f.FS.TempDir("", "helmchart") + if err != nil { + return "", errors.Wrap(err, "get chart checkout tmpdir gitAsset") + } + + err = helm.Fetch(asset.Git.URL, asset.Git.Version, asset.Git.Name, checkoutDir) + if err != nil { + return "", errors.Wrap(err, "get chart via helm fetch") + } + return checkoutDir, nil } debug.Log("event", "chart.fetch.fail", "reason", "unsupported")