Merge branch 'master' of github.com:kubernetes/helm

* 'master' of github.com:kubernetes/helm: (59 commits)
  ref(cmd): refactor status cmd
  ref(helm): fix naming issues from golint
  ref(cmd): refactor create cmd
  chore(*): add canonical import path annotation
  Add reference to text/template Go package doc
  ref(cmd): remove duplicate test cases
  feat(helm): add 'helm get hooks'.
  fix(engine): change template naming
  fix(ci): ensure godir is installed for coverage
  feat(ci): setup test coverage reports with coveralls.io
  feat(*): add version to release
  feat(tiller): support hooks for install
  fix(ci): move docker-build out of parallel step
  docs(ci): add cicleci badge to readme
  fix(ci): add docker-build to the parallel builds
  fix(lint): fix tests
  chore(deps): pin kubernetes to an official release (v1.3.0)
  fix(lint): only return count of actually linted charts
  docs(helm):correct the documentation for the global values usage
  fix(scripts): update local-cluster.sh to work with v1.3
  ...
pull/881/head
Marat G 9 years ago
commit e0b9f1d99a

@ -1,5 +1,7 @@
# Kubernetes Helm # Kubernetes Helm
[![CircleCI](https://circleci.com/gh/kubernetes/helm.svg?style=svg)](https://circleci.com/gh/kubernetes/helm)
Helm is a tool for managing Kubernetes charts. Charts are packages of Helm is a tool for managing Kubernetes charts. Charts are packages of
pre-configured Kubernetes resources. pre-configured Kubernetes resources.

@ -0,0 +1,45 @@
// 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.
syntax = "proto3";
package hapi.release;
import "google/protobuf/timestamp.proto";
option go_package = "release";
// Hook defines a hook object.
message Hook {
enum Event {
UNKNOWN = 0;
PRE_INSTALL = 1;
POST_INSTALL = 2;
PRE_DELETE = 3;
POST_DELETE = 4;
PRE_UPGRADE = 5;
POST_UPGRADE = 6;
}
string name = 1;
// Kind is the Kubernetes kind.
string kind = 2;
// Path is the chart-relative path to the template.
string path = 3;
// Manifest is the manifest contents.
string manifest = 4;
// Events are the events that this hook fires on.
repeated Event events = 5;
// LastRun indicates the date/time this was last run.
google.protobuf.Timestamp last_run = 6;
}

@ -16,6 +16,7 @@ syntax = "proto3";
package hapi.release; package hapi.release;
import "hapi/release/hook.proto";
import "hapi/release/info.proto"; import "hapi/release/info.proto";
import "hapi/chart/config.proto"; import "hapi/chart/config.proto";
import "hapi/chart/chart.proto"; import "hapi/chart/chart.proto";
@ -40,4 +41,11 @@ message Release {
// Manifest is the string representation of the rendered template. // Manifest is the string representation of the rendered template.
string manifest = 5; string manifest = 5;
// Hooks are all of the hooks declared for this release.
repeated hapi.release.Hook hooks = 6;
// Version is an int32 which represents the version of the release.
int32 version = 7;
} }

@ -1,43 +1,45 @@
machine: machine:
pre:
- curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0
environment: environment:
GLIDE_VERSION: "0.10.1" GOVERSION: "1.6.2"
GO15VENDOREXPERIMENT: 1 GOPATH: "${HOME}/.go_workspace"
GOPATH: /usr/local/go_workspace WORKDIR: "${GOPATH}/src/k8s.io/helm"
HOME: /home/ubuntu
IMPORT_PATH: "k8s.io/helm"
PATH: $HOME/go/bin:$PATH
GOROOT: $HOME/go
services: services:
- docker - docker
dependencies: dependencies:
pre:
- sudo rm -rf /usr/local/go
- rm -rf "$GOPATH"
override: override:
- mkdir -p $HOME/go # install go
- wget "https://storage.googleapis.com/golang/go1.6.linux-amd64.tar.gz" - wget "https://storage.googleapis.com/golang/go${GOVERSION}.linux-amd64.tar.gz"
- tar -C $HOME -xzf go1.6.linux-amd64.tar.gz - sudo tar -C /usr/local -xzf "go${GOVERSION}.linux-amd64.tar.gz"
- go version
# move repository to the canonical import path
- mkdir -p "$(dirname ${WORKDIR})"
- cp -R "${HOME}/helm" "${WORKDIR}"
# install dependencies
- cd "${WORKDIR}" && make bootstrap
post:
- go env - go env
- sudo chown -R $(whoami):staff /usr/local
- cd $GOPATH
- mkdir -p $GOPATH/src/$IMPORT_PATH
- cd $HOME/helm
- rsync -az --delete ./ "$GOPATH/src/$IMPORT_PATH/"
- wget "https://github.com/Masterminds/glide/releases/download/$GLIDE_VERSION/glide-$GLIDE_VERSION-linux-amd64.tar.gz"
- mkdir -p $HOME/bin
- tar -vxz -C $HOME/bin --strip=1 -f glide-$GLIDE_VERSION-linux-amd64.tar.gz
- export PATH="$HOME/bin:$PATH" GLIDE_HOME="$HOME/.glide"
test: test:
override: override:
- cd $GOPATH/src/$IMPORT_PATH && make bootstrap test - cd "${WORKDIR}" && ./scripts/ci.sh:
parallel: true
deployment: deployment:
master-branch: gcr:
branch: master branch: master
commands: commands:
- echo $GCLOUD_SERVICE_KEY | base64 --decode > ${HOME}/gcloud-service-key.json - echo $GCLOUD_SERVICE_KEY | base64 --decode > ${HOME}/gcloud-service-key.json
- sudo docker login -e 1234@5678.com -u _json_key -p "$(cat ${HOME}/gcloud-service-key.json)" https://gcr.io - docker login -e 1234@5678.com -u _json_key -p "$(cat ${HOME}/gcloud-service-key.json)" https://gcr.io
- cd $GOPATH/src/$IMPORT_PATH
- make docker-build - make docker-build
- sudo docker push gcr.io/kubernetes-helm/tiller:canary - docker push gcr.io/kubernetes-helm/tiller:canary

@ -18,9 +18,12 @@ package main
import ( import (
"errors" "errors"
"fmt"
"io"
"path/filepath" "path/filepath"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
) )
@ -50,31 +53,40 @@ destination exists and there are files in that directory, conflicting files
will be overwritten, but other files will be left alone. will be overwritten, but other files will be left alone.
` `
func init() { type createCmd struct {
RootCommand.AddCommand(createCmd) name string
out io.Writer
} }
var createCmd = &cobra.Command{ func newCreateCmd(out io.Writer) *cobra.Command {
Use: "create NAME", cc := &createCmd{
Short: "create a new chart with the given name", out: out,
Long: createDesc, }
RunE: runCreate, 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 {
if len(args) == 0 {
return errors.New("the name of the new chart is required")
}
cc.name = args[0]
return cc.run()
},
}
return cmd
} }
func runCreate(cmd *cobra.Command, args []string) error { func (c *createCmd) run() error {
if len(args) == 0 { fmt.Fprintf(c.out, "Creating %s\n", c.name)
return errors.New("the name of the new chart is required")
}
cname := args[0]
cmd.Printf("Creating %s\n", cname)
chartname := filepath.Base(cname) chartname := filepath.Base(c.name)
cfile := &chart.Metadata{ cfile := &chart.Metadata{
Name: chartname, Name: chartname,
Description: "A Helm chart for Kubernetes", Description: "A Helm chart for Kubernetes",
Version: "0.1.0", Version: "0.1.0",
} }
_, err := chartutil.Create(cfile, filepath.Dir(cname)) _, err := chartutil.Create(cfile, filepath.Dir(c.name))
return err return err
} }

@ -18,8 +18,8 @@ package main
import ( import (
"errors" "errors"
"fmt" "io"
"os" "text/template"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -42,70 +42,61 @@ By default, this prints a human readable collection of information about the
chart, the supplied values, and the generated manifest file. chart, the supplied values, and the generated manifest file.
` `
var getValuesHelp = `
This command downloads a values file for a given release.
To save the output to a file, use the -f flag.
`
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.
`
// getOut is the filename to direct output.
//
// If it is blank, output is sent to os.Stdout.
var getOut = ""
var allValues = false
var errReleaseRequired = errors.New("release name is required") var errReleaseRequired = errors.New("release name is required")
var getCommand = &cobra.Command{ type getCmd struct {
Use: "get [flags] RELEASE_NAME", release string
Short: "download a named release", out io.Writer
Long: getHelp, client helm.Interface
RunE: getCmd,
PersistentPreRunE: setupConnection,
} }
var getValuesCommand = &cobra.Command{ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command {
Use: "values [flags] RELEASE_NAME", get := &getCmd{
Short: "download the values file for a named release", out: out,
Long: getValuesHelp, client: client,
RunE: getValues, }
} cmd := &cobra.Command{
Use: "get [flags] RELEASE_NAME",
var getManifestCommand = &cobra.Command{ Short: "download a named release",
Use: "manifest [flags] RELEASE_NAME", Long: getHelp,
Short: "download the manifest for a named release", PersistentPreRunE: setupConnection,
Long: getManifestHelp, RunE: func(cmd *cobra.Command, args []string) error {
RunE: getManifest, if len(args) == 0 {
return errReleaseRequired
}
get.release = args[0]
if get.client == nil {
get.client = helm.NewClient(helm.Host(helm.Config.ServAddr))
}
return get.run()
},
}
cmd.AddCommand(newGetValuesCmd(nil, out))
cmd.AddCommand(newGetManifestCmd(nil, out))
cmd.AddCommand(newGetHooksCmd(nil, out))
return cmd
} }
func init() { var getTemplate = `VERSION: {{.Release.Version}}
// 'get' command flags. RELEASED: {{.ReleaseDate}}
getCommand.PersistentFlags().StringVarP(&getOut, "file", "f", "", "output file") CHART: {{.Release.Chart.Metadata.Name}}-{{.Release.Chart.Metadata.Version}}
USER-SUPPLIED VALUES:
// 'get values' flags. {{.Release.Config.Raw}}
getValuesCommand.PersistentFlags().BoolVarP(&allValues, "all", "a", false, "dump all (computed) values") COMPUTED VALUES:
{{.ComputedValues}}
getCommand.AddCommand(getValuesCommand) HOOKS:
getCommand.AddCommand(getManifestCommand) {{- range .Release.Hooks }}
RootCommand.AddCommand(getCommand) ---
} # {{.Name}}
{{.Manifest}}
{{- end }}
MANIFEST:
{{.Release.Manifest}}
`
// getCmd is the command that implements 'helm get' // getCmd is the command that implements 'helm get'
func getCmd(cmd *cobra.Command, args []string) error { func (g *getCmd) run() error {
if len(args) == 0 { res, err := g.client.ReleaseContent(g.release)
return errReleaseRequired
}
res, err := helm.GetReleaseContent(args[0])
if err != nil { if err != nil {
return prettyError(err) return prettyError(err)
} }
@ -119,67 +110,25 @@ func getCmd(cmd *cobra.Command, args []string) error {
return err return err
} }
fmt.Printf("CHART: %s-%s\n", res.Release.Chart.Metadata.Name, res.Release.Chart.Metadata.Version) data := map[string]interface{}{
fmt.Printf("RELEASED: %s\n", timeconv.Format(res.Release.Info.LastDeployed, time.ANSIC)) "Release": res.Release,
fmt.Println("USER-SUPPLIED VALUES:") "ComputedValues": cfgStr,
fmt.Println(res.Release.Config.Raw) "ReleaseDate": timeconv.Format(res.Release.Info.LastDeployed, time.ANSIC),
fmt.Println("COMPUTED VALUES:")
fmt.Println(cfgStr)
fmt.Println("MANIFEST:")
fmt.Println(res.Release.Manifest)
return nil
}
// getValues implements 'helm get values'
func getValues(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errReleaseRequired
}
res, err := helm.GetReleaseContent(args[0])
if err != nil {
return prettyError(err)
} }
return tpl(getTemplate, data, g.out)
// If the user wants all values, compute the values and return.
if allValues {
cfg, err := chartutil.CoalesceValues(res.Release.Chart, res.Release.Config, nil)
if err != nil {
return err
}
cfgStr, err := cfg.YAML()
if err != nil {
return err
}
return getToFile(cfgStr)
}
return getToFile(res.Release.Config.Raw)
} }
// getManifest implements 'helm get manifest' func tpl(t string, vals map[string]interface{}, out io.Writer) error {
func getManifest(cmd *cobra.Command, args []string) error { tt, err := template.New("_").Parse(t)
if len(args) == 0 {
return errReleaseRequired
}
res, err := helm.GetReleaseContent(args[0])
if err != nil { if err != nil {
return prettyError(err) return err
} }
return getToFile(res.Release.Manifest) return tt.Execute(out, vals)
} }
func getToFile(v interface{}) error { func ensureHelmClient(h helm.Interface) helm.Interface {
out := os.Stdout if h != nil {
if len(getOut) > 0 { return h
t, err := os.Create(getOut)
if err != nil {
return fmt.Errorf("failed to create %s: %s", getOut, err)
}
defer t.Close()
out = t
} }
fmt.Fprintln(out, v) return helm.NewClient(helm.Host(helm.Config.ServAddr))
return nil
} }

@ -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 main
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
}
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,
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()
},
}
return cmd
}
func (g *getHooksCmd) run() error {
res, err := g.client.ReleaseContent(g.release)
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
}

@ -0,0 +1,43 @@
/*
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 main
import (
"io"
"testing"
"github.com/spf13/cobra"
)
func TestGetHooks(t *testing.T) {
tests := []releaseCase{
{
name: "get hooks with release",
args: []string{"aeneas"},
expected: mockHookTemplate,
resp: releaseMock("aeneas"),
},
{
name: "get hooks without args",
args: []string{},
err: true,
},
}
runReleaseCases(t, tests, func(c *fakeReleaseClient, out io.Writer) *cobra.Command {
return newGetHooksCmd(c, out)
})
}

@ -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 main
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
}
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,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errReleaseRequired
}
get.release = args[0]
if get.client == nil {
get.client = helm.NewClient(helm.Host(helm.Config.ServAddr))
}
return get.run()
},
}
return cmd
}
// getManifest implements 'helm get manifest'
func (g *getManifestCmd) run() error {
res, err := g.client.ReleaseContent(g.release)
if err != nil {
return prettyError(err)
}
fmt.Fprintln(g.out, res.Release.Manifest)
return nil
}

@ -0,0 +1,43 @@
/*
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 main
import (
"io"
"testing"
"github.com/spf13/cobra"
)
func TestGetManifest(t *testing.T) {
tests := []releaseCase{
{
name: "get manifest with release",
args: []string{"juno"},
expected: mockManifest,
resp: releaseMock("juno"),
},
{
name: "get manifest without args",
args: []string{},
err: true,
},
}
runReleaseCases(t, tests, func(c *fakeReleaseClient, out io.Writer) *cobra.Command {
return newGetManifestCmd(c, out)
})
}

@ -0,0 +1,44 @@
/*
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 main
import (
"io"
"testing"
"github.com/spf13/cobra"
)
func TestGetCmd(t *testing.T) {
tests := []releaseCase{
{
name: "get with a release",
resp: releaseMock("thomas-guide"),
args: []string{"thomas-guide"},
expected: "VERSION: 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" + mockHookTemplate + "\nMANIFEST:",
},
{
name: "get requires release name arg",
err: true,
},
}
cmd := func(c *fakeReleaseClient, out io.Writer) *cobra.Command {
return newGetCmd(c, out)
}
runReleaseCases(t, tests, cmd)
}

@ -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 main
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
}
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,
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().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)
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, nil)
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
}

@ -0,0 +1,43 @@
/*
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 main
import (
"io"
"testing"
"github.com/spf13/cobra"
)
func TestGetValuesCmd(t *testing.T) {
tests := []releaseCase{
{
name: "get values with a release",
resp: releaseMock("thomas-guide"),
args: []string{"thomas-guide"},
expected: "name: \"value\"",
},
{
name: "get values requires release name arg",
err: true,
},
}
cmd := func(c *fakeReleaseClient, out io.Writer) *cobra.Command {
return newGetValuesCmd(c, out)
}
runReleaseCases(t, tests, cmd)
}

@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main package main // import "k8s.io/helm/cmd/helm"
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"strings" "strings"
@ -63,29 +64,42 @@ Environment:
$HELM_HOST Set an alternative Tiller host. The format is host:port (default ":44134"). $HELM_HOST Set an alternative Tiller host. The format is host:port (default ":44134").
` `
// RootCommand is the top-level command for Helm. func newRootCmd(out io.Writer) *cobra.Command {
var RootCommand = &cobra.Command{ cmd := &cobra.Command{
Use: "helm", Use: "helm",
Short: "The Helm package manager for Kubernetes.", Short: "The Helm package manager for Kubernetes.",
Long: globalUsage, Long: globalUsage,
PersistentPostRun: teardown, SilenceUsage: true,
} PersistentPostRun: func(cmd *cobra.Command, args []string) {
teardown()
func init() { },
}
home := os.Getenv(homeEnvVar) home := os.Getenv(homeEnvVar)
if home == "" { if home == "" {
home = "$HOME/.helm" home = "$HOME/.helm"
} }
thost := os.Getenv(hostEnvVar) thost := os.Getenv(hostEnvVar)
p := RootCommand.PersistentFlags() p := cmd.PersistentFlags()
p.StringVar(&helmHome, "home", home, "location of your Helm config. Overrides $HELM_HOME.") p.StringVar(&helmHome, "home", home, "location of your Helm config. Overrides $HELM_HOME.")
p.StringVar(&tillerHost, "host", thost, "address of tiller. Overrides $HELM_HOST.") p.StringVar(&tillerHost, "host", thost, "address of tiller. Overrides $HELM_HOST.")
p.StringVarP(&tillerNamespace, "namespace", "", "", "kubernetes namespace") p.StringVarP(&tillerNamespace, "namespace", "", "", "kubernetes namespace")
p.BoolVarP(&flagDebug, "debug", "", false, "enable verbose output") p.BoolVarP(&flagDebug, "debug", "", false, "enable verbose output")
cmd.AddCommand(
newCreateCmd(out),
newGetCmd(nil, out),
newListCmd(nil, out),
newStatusCmd(nil, out),
)
return cmd
} }
// RootCommand is the top-level command for Helm.
var RootCommand = newRootCmd(os.Stdout)
func main() { func main() {
if err := RootCommand.Execute(); err != nil { cmd := RootCommand
if err := cmd.Execute(); err != nil {
os.Exit(1) os.Exit(1)
} }
} }
@ -111,7 +125,7 @@ func setupConnection(c *cobra.Command, args []string) error {
return nil return nil
} }
func teardown(c *cobra.Command, args []string) { func teardown() {
if tunnel != nil { if tunnel != nil {
tunnel.Close() tunnel.Close()
} }

@ -0,0 +1,150 @@
/*
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 main
import (
"bytes"
"io"
"regexp"
"testing"
"github.com/golang/protobuf/ptypes/timestamp"
"github.com/spf13/cobra"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/proto/hapi/release"
rls "k8s.io/helm/pkg/proto/hapi/services"
)
var mockHookTemplate = `apiVersion: v1
kind: Job
metadata:
annotations:
"helm.sh/hooks": pre-install
`
var mockManifest = `apiVersion: v1
kind: Secret
metadata:
name: fixture
`
func releaseMock(name string) *release.Release {
date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0}
return &release.Release{
Name: name,
Info: &release.Info{
FirstDeployed: &date,
LastDeployed: &date,
Status: &release.Status{Code: release.Status_DEPLOYED},
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "foo",
Version: "0.1.0-beta.1",
},
Templates: []*chart.Template{
{Name: "foo.tpl", Data: []byte(mockManifest)},
},
},
Config: &chart.Config{Raw: `name: "value"`},
Version: 1,
Hooks: []*release.Hook{
{
Name: "pre-install-hook",
Kind: "Job",
Path: "pre-install-hook.yaml",
Manifest: mockHookTemplate,
LastRun: &date,
Events: []release.Hook_Event{release.Hook_PRE_INSTALL},
},
},
Manifest: mockManifest,
}
}
type fakeReleaseClient struct {
rels []*release.Release
err error
}
func (c *fakeReleaseClient) ListReleases(opts ...helm.ReleaseListOption) (*rls.ListReleasesResponse, error) {
resp := &rls.ListReleasesResponse{
Count: int64(len(c.rels)),
Releases: c.rels,
}
return resp, c.err
}
func (c *fakeReleaseClient) InstallRelease(chStr string, opts ...helm.InstallOption) (*rls.InstallReleaseResponse, error) {
return nil, nil
}
func (c *fakeReleaseClient) DeleteRelease(rlsName string, opts ...helm.DeleteOption) (*rls.UninstallReleaseResponse, error) {
return nil, nil
}
func (c *fakeReleaseClient) ReleaseStatus(rlsName string, opts ...helm.StatusOption) (*rls.GetReleaseStatusResponse, error) {
return nil, nil
}
func (c *fakeReleaseClient) UpdateRelease(rlsName string, opts ...helm.UpdateOption) (*rls.UpdateReleaseResponse, error) {
return nil, nil
}
func (c *fakeReleaseClient) ReleaseContent(rlsName string, opts ...helm.ContentOption) (resp *rls.GetReleaseContentResponse, err error) {
if len(c.rels) > 0 {
resp = &rls.GetReleaseContentResponse{
Release: c.rels[0],
}
}
return resp, c.err
}
// releaseCmd is a command that works with a fakeReleaseClient
type releaseCmd func(c *fakeReleaseClient, 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 {
c := &fakeReleaseClient{
rels: []*release.Release{tt.resp},
}
cmd := rcmd(c, &buf)
err := cmd.RunE(cmd, tt.args)
if (err != nil) != tt.err {
t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err)
}
re := regexp.MustCompile(tt.expected)
if !re.Match(buf.Bytes()) {
t.Errorf("%q. expected\n%q\ngot\n%q", tt.name, tt.expected, buf.String())
}
buf.Reset()
}
}
// releaseCase describes a test case that works with releases.
type releaseCase struct {
name string
args []string
// expected is the string to be matched. This supports regular expressions.
expected string
err bool
resp *release.Release
}

@ -135,6 +135,7 @@ func locateChartPath(name string) (string, error) {
} }
// Try fetching the chart from a remote repo into a tmpdir // Try fetching the chart from a remote repo into a tmpdir
origname := name
if filepath.Ext(name) != ".tgz" { if filepath.Ext(name) != ".tgz" {
name += ".tgz" name += ".tgz"
} }
@ -143,9 +144,9 @@ func locateChartPath(name string) (string, error) {
if err != nil { if err != nil {
return lname, err return lname, err
} }
fmt.Printf("Fetched %s to %s\n", name, lname) fmt.Printf("Fetched %s to %s\n", origname, lname)
return lname, nil return lname, nil
} }
return name, fmt.Errorf("file %q not found", name) return name, fmt.Errorf("file %q not found", origname)
} }

@ -19,12 +19,16 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/lint" "k8s.io/helm/pkg/lint"
"k8s.io/helm/pkg/lint/support"
) )
var longLintHelp = ` var longLintHelp = `
@ -47,27 +51,80 @@ func init() {
RootCommand.AddCommand(lintCommand) RootCommand.AddCommand(lintCommand)
} }
var errLintNoChart = errors.New("no chart found for linting (missing Chart.yaml)") var errLintNoChart = errors.New("No chart found for linting (missing Chart.yaml)")
func lintCmd(cmd *cobra.Command, args []string) error { func lintCmd(cmd *cobra.Command, args []string) error {
path := "." paths := []string{"."}
if len(args) > 0 { if len(args) > 0 {
path = args[0] paths = args
} }
// Guard: Error out of this is not a chart. var total int
if _, err := os.Stat(filepath.Join(path, "Chart.yaml")); err != nil { var failures int
return errLintNoChart for _, path := range paths {
if linter, err := lintChart(path); err != nil {
fmt.Println("==> Skipping", path)
fmt.Println(err)
} 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 >= support.ErrorSev {
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)
} }
issues := lint.All(path) fmt.Printf("%s, no failures\n", msg)
if len(issues) == 0 { return nil
fmt.Println("Lint OK") }
func lintChart(path string) (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
}
base := strings.Split(filepath.Base(path), "-")[0]
chartPath = filepath.Join(tempDir, base)
} else {
chartPath = path
} }
for _, i := range issues { // Guard: Error out of this is not a chart.
fmt.Printf("%s\n", i) if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil {
return linter, errLintNoChart
} }
return nil
return lint.All(chartPath), nil
} }

@ -0,0 +1,37 @@
/*
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 main
import (
"testing"
)
var (
archivedChartPath = "testdata/testcharts/compressedchart-0.1.0.tgz"
chartDirPath = "testdata/testcharts/decompressedchart/"
)
func TestLintChart(t *testing.T) {
if _, err := lintChart(chartDirPath); err != nil {
t.Errorf("%s", err)
}
if _, err := lintChart(archivedChartPath); err != nil {
t.Errorf("%s", err)
}
}

@ -18,6 +18,7 @@ package main
import ( import (
"fmt" "fmt"
"io"
"strings" "strings"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
@ -52,51 +53,66 @@ server's default, which may be much higher than 256. Pairing the '--max'
flag with the '--offset' flag allows you to page through results. flag with the '--offset' flag allows you to page through results.
` `
var listCommand = &cobra.Command{ type listCmd struct {
Use: "list [flags] [FILTER]", filter string
Short: "list releases", long bool
Long: listHelp, limit int
RunE: listCmd, offset string
Aliases: []string{"ls"}, byDate bool
PersistentPreRunE: setupConnection, sortDesc bool
out io.Writer
client helm.Interface
} }
var ( func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
listLong bool list := &listCmd{
listMax int out: out,
listOffset string client: client,
listByDate bool }
listSortDesc bool cmd := &cobra.Command{
) Use: "list [flags] [FILTER]",
Short: "list releases",
func init() { Long: listHelp,
f := listCommand.Flags() Aliases: []string{"ls"},
f.BoolVarP(&listLong, "long", "l", false, "output long listing format") PersistentPreRunE: setupConnection,
f.BoolVarP(&listByDate, "date", "d", false, "sort by release date") RunE: func(cmd *cobra.Command, args []string) error {
f.BoolVarP(&listSortDesc, "reverse", "r", false, "reverse the sort order") if len(args) > 0 {
f.IntVarP(&listMax, "max", "m", 256, "maximum number of releases to fetch") list.filter = strings.Join(args, " ")
f.StringVarP(&listOffset, "offset", "o", "", "the next release name in the list, used to offset from start value") }
if list.client == nil {
RootCommand.AddCommand(listCommand) list.client = helm.NewClient(helm.Host(helm.Config.ServAddr))
} }
return list.run()
func listCmd(cmd *cobra.Command, args []string) error { },
var filter string
if len(args) > 0 {
filter = strings.Join(args, " ")
} }
f := cmd.Flags()
f.BoolVarP(&list.long, "long", "l", false, "output long 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", "", "the next release name in the list, used to offset from start value")
return cmd
}
func (l *listCmd) run() error {
sortBy := services.ListSort_NAME sortBy := services.ListSort_NAME
if listByDate { if l.byDate {
sortBy = services.ListSort_LAST_RELEASED sortBy = services.ListSort_LAST_RELEASED
} }
sortOrder := services.ListSort_ASC sortOrder := services.ListSort_ASC
if listSortDesc { if l.sortDesc {
sortOrder = services.ListSort_DESC sortOrder = services.ListSort_DESC
} }
res, err := helm.ListReleases(listMax, listOffset, sortBy, sortOrder, filter) 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)),
)
if err != nil { if err != nil {
return prettyError(err) return prettyError(err)
} }
@ -106,31 +122,32 @@ func listCmd(cmd *cobra.Command, args []string) error {
} }
if res.Next != "" { if res.Next != "" {
fmt.Printf("\tnext: %s", res.Next) fmt.Fprintf(l.out, "\tnext: %s", res.Next)
} }
rels := res.Releases rels := res.Releases
if listLong {
return formatList(rels) if l.long {
fmt.Fprintln(l.out, formatList(rels))
return nil
} }
for _, r := range rels { for _, r := range rels {
fmt.Println(r.Name) fmt.Fprintln(l.out, r.Name)
} }
return nil return nil
} }
func formatList(rels []*release.Release) error { func formatList(rels []*release.Release) string {
table := uitable.New() table := uitable.New()
table.MaxColWidth = 30 table.MaxColWidth = 30
table.AddRow("NAME", "UPDATED", "STATUS", "CHART") table.AddRow("NAME", "VERSION", "UPDATED", "STATUS", "CHART")
for _, r := range rels { for _, r := range rels {
c := fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version) c := fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version)
t := timeconv.String(r.Info.LastDeployed) t := timeconv.String(r.Info.LastDeployed)
s := r.Info.Status.Code.String() s := r.Info.Status.Code.String()
table.AddRow(r.Name, t, s, c) v := r.Version
table.AddRow(r.Name, v, t, s, c)
} }
fmt.Println(table) return table.String()
return nil
} }

@ -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 main
import (
"bytes"
"regexp"
"testing"
"k8s.io/helm/pkg/proto/hapi/release"
)
func TestListCmd(t *testing.T) {
tests := []struct {
name string
args []string
flags map[string]string
resp []*release.Release
expected string
err bool
}{
{
name: "with a release",
resp: []*release.Release{
releaseMock("thomas-guide"),
},
expected: "thomas-guide",
},
{
name: "list --long",
flags: map[string]string{"long": "1"},
resp: []*release.Release{
releaseMock("atlas"),
},
expected: "NAME \tVERSION\tUPDATED \tSTATUS \tCHART \natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\n",
},
}
var buf bytes.Buffer
for _, tt := range tests {
c := &fakeReleaseClient{
rels: tt.resp,
}
cmd := newListCmd(c, &buf)
for flag, value := range tt.flags {
cmd.Flags().Set(flag, value)
}
err := cmd.RunE(cmd, tt.args)
if (err != nil) != tt.err {
t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err)
}
re := regexp.MustCompile(tt.expected)
if !re.Match(buf.Bytes()) {
t.Errorf("%q. expected %q, got %q", tt.name, tt.expected, buf.String())
}
buf.Reset()
}
}

@ -71,7 +71,7 @@ func runPackage(cmd *cobra.Command, args []string) error {
} }
if filepath.Base(path) != ch.Metadata.Name { 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) return fmt.Errorf("directory name (%s) and Chart.yaml name (%s) must match", filepath.Base(path), ch.Metadata.Name)
} }
// Save to the current working directory. // Save to the current working directory.

@ -22,8 +22,8 @@ import (
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
) )
const testDir = "testdata/" const testDir = "testdata/testcache"
const testFile = "testdata/local-index.yaml" const testFile = "testdata/testcache/local-index.yaml"
type searchTestCase struct { type searchTestCase struct {
in string in string

@ -18,6 +18,7 @@ package main
import ( import (
"fmt" "fmt"
"io"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -29,33 +30,46 @@ var statusHelp = `
This command shows the status of a named release. This command shows the status of a named release.
` `
var statusCommand = &cobra.Command{ type statusCmd struct {
Use: "status [flags] RELEASE_NAME", release string
Short: "displays the status of the named release", out io.Writer
Long: statusHelp, client helm.Interface
RunE: status,
PersistentPreRunE: setupConnection,
} }
func init() { func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command {
RootCommand.AddCommand(statusCommand) status := &statusCmd{
} out: out,
client: client,
func status(cmd *cobra.Command, args []string) error { }
if len(args) == 0 { cmd := &cobra.Command{
return errReleaseRequired Use: "status [flags] RELEASE_NAME",
Short: "displays the status of the named release",
Long: statusHelp,
PersistentPreRunE: 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 = helm.NewClient(helm.Host(helm.Config.ServAddr))
}
return status.run()
},
} }
return cmd
}
res, err := helm.GetReleaseStatus(args[0]) func (s *statusCmd) run() error {
res, err := s.client.ReleaseStatus(s.release)
if err != nil { if err != nil {
return prettyError(err) return prettyError(err)
} }
fmt.Printf("Last Deployed: %s\n", timeconv.String(res.Info.LastDeployed)) fmt.Fprintf(s.out, "Last Deployed: %s\n", timeconv.String(res.Info.LastDeployed))
fmt.Printf("Status: %s\n", res.Info.Status.Code) fmt.Fprintf(s.out, "Status: %s\n", res.Info.Status.Code)
if res.Info.Status.Details != nil { if res.Info.Status.Details != nil {
fmt.Printf("Details: %s\n", res.Info.Status.Details) fmt.Fprintf(s.out, "Details: %s\n", res.Info.Status.Details)
} }
return nil return nil
} }

@ -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

@ -0,0 +1,3 @@
description: A Helm chart for Kubernetes
name: decompressedchart
version: 0.1.0

@ -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

@ -157,6 +157,13 @@ type KubeClient interface {
// reader must contain a YAML stream (one or more YAML documents separated // reader must contain a YAML stream (one or more YAML documents separated
// by "\n---\n"). // by "\n---\n").
Delete(namespace string, reader io.Reader) error Delete(namespace string, reader io.Reader) error
// Watch the resource in reader until it is "ready".
//
// For Jobs, "ready" means the job ran to completion (excited without error).
// For all other kinds, it means the kind was created or modified without
// error.
WatchUntilReady(namespace string, reader io.Reader) error
} }
// PrintingKubeClient implements KubeClient, but simply prints the reader to // PrintingKubeClient implements KubeClient, but simply prints the reader to
@ -179,6 +186,12 @@ func (p *PrintingKubeClient) Delete(ns string, r io.Reader) error {
return err return err
} }
// WatchUntilReady implements KubeClient WatchUntilReady.
func (p *PrintingKubeClient) WatchUntilReady(ns string, r io.Reader) error {
_, err := io.Copy(p.Out, r)
return err
}
// Environment provides the context for executing a client request. // Environment provides the context for executing a client request.
// //
// All services in a context are concurrency safe. // All services in a context are concurrency safe.

@ -83,6 +83,9 @@ func (k *mockKubeClient) Create(ns string, r io.Reader) error {
func (k *mockKubeClient) Delete(ns string, r io.Reader) error { func (k *mockKubeClient) Delete(ns string, r io.Reader) error {
return nil return nil
} }
func (k *mockKubeClient) WatchUntilReady(ns string, r io.Reader) error {
return nil
}
var _ Engine = &mockEngine{} var _ Engine = &mockEngine{}
var _ ReleaseStorage = &mockReleaseStorage{} var _ ReleaseStorage = &mockReleaseStorage{}

@ -0,0 +1,107 @@
package main
import (
"log"
"strings"
"github.com/ghodss/yaml"
"k8s.io/helm/pkg/proto/hapi/release"
)
// hookAnno is the label name for a hook
const hookAnno = "helm.sh/hook"
const (
preInstall = "pre-install"
postInstall = "post-install"
preDelete = "pre-delete"
postDelete = "post-delete"
preUpgrade = "pre-upgrade"
postUpgrade = "post-upgrade"
)
var events = map[string]release.Hook_Event{
preInstall: release.Hook_PRE_INSTALL,
postInstall: release.Hook_POST_INSTALL,
preDelete: release.Hook_PRE_DELETE,
postDelete: release.Hook_POST_DELETE,
preUpgrade: release.Hook_PRE_UPGRADE,
postUpgrade: release.Hook_POST_UPGRADE,
}
type simpleHead struct {
Kind string `json:"kind,omitempty"`
Metadata *struct {
Name string `json:"name"`
Annotations map[string]string `json:"annotations"`
} `json:"metadata,omitempty"`
}
// sortHooks takes a map of filename/YAML contents and sorts them into hook types.
//
// The resulting hooks struct will be populated with all of the generated hooks.
// Any file that does not declare one of the hook types will be placed in the
// 'generic' bucket.
//
// To determine hook type, this looks for a YAML structure like this:
//
// kind: SomeKind
// metadata:
// annotations:
// helm.sh/hook: pre-install
//
// Where HOOK_NAME is one of the known hooks.
//
// If a file declares more than one hook, it will be copied into all of the applicable
// hook buckets. (Note: label keys are not unique within the labels section).
//
// Files that do not parse into the expected format are simply placed into a map and
// returned.
func sortHooks(files map[string]string) (hs []*release.Hook, generic map[string]string) {
hs = []*release.Hook{}
generic = map[string]string{}
for n, c := range files {
var sh simpleHead
err := yaml.Unmarshal([]byte(c), &sh)
if err != nil {
log.Printf("YAML parse error on %s: %s (skipping)", n, err)
}
if sh.Metadata == nil || sh.Metadata.Annotations == nil || len(sh.Metadata.Annotations) == 0 {
generic[n] = c
continue
}
hookTypes, ok := sh.Metadata.Annotations[hookAnno]
if !ok {
generic[n] = c
continue
}
h := &release.Hook{
Name: sh.Metadata.Name,
Kind: sh.Kind,
Path: n,
Manifest: c,
Events: []release.Hook_Event{},
}
isHook := false
for _, hookType := range strings.Split(hookTypes, ",") {
hookType = strings.ToLower(strings.TrimSpace(hookType))
e, ok := events[hookType]
if ok {
isHook = true
h.Events = append(h.Events, e)
}
}
if !isHook {
log.Printf("info: skipping unknown hook: %q", hookTypes)
continue
}
hs = append(hs, h)
}
return
}

@ -0,0 +1,121 @@
package main
import (
"testing"
"k8s.io/helm/pkg/proto/hapi/release"
)
func TestSortHooks(t *testing.T) {
data := []struct {
name string
path string
kind string
hooks []release.Hook_Event
manifest string
}{
{
name: "first",
path: "one",
kind: "Job",
hooks: []release.Hook_Event{release.Hook_PRE_INSTALL},
manifest: `apiVersion: v1
kind: Job
metadata:
name: first
labels:
doesnt: matter
annotations:
"helm.sh/hook": pre-install
`,
},
{
name: "second",
path: "two",
kind: "ReplicaSet",
hooks: []release.Hook_Event{release.Hook_POST_INSTALL},
manifest: `kind: ReplicaSet
metadata:
name: second
annotations:
"helm.sh/hook": post-install
`,
}, {
name: "third",
path: "three",
kind: "ReplicaSet",
hooks: []release.Hook_Event{},
manifest: `kind: ReplicaSet
metadata:
name: third
annotations:
"helm.sh/hook": no-such-hook
`,
}, {
name: "fourth",
path: "four",
kind: "Pod",
hooks: []release.Hook_Event{},
manifest: `kind: Pod
metadata:
name: fourth
annotations:
nothing: here
`,
}, {
name: "fifth",
path: "five",
kind: "ReplicaSet",
hooks: []release.Hook_Event{release.Hook_POST_DELETE, release.Hook_POST_INSTALL},
manifest: `kind: ReplicaSet
metadata:
name: fifth
annotations:
"helm.sh/hook": post-delete, post-install
`,
},
}
manifests := make(map[string]string, len(data))
for _, o := range data {
manifests[o.path] = o.manifest
}
hs, generic := sortHooks(manifests)
if len(generic) != 1 {
t.Errorf("Expected 1 generic manifest, got %d", len(generic))
}
if len(hs) != 3 {
t.Errorf("Expected 3 hooks, got %d", len(hs))
}
for _, out := range hs {
found := false
for _, expect := range data {
if out.Path == expect.path {
found = true
if out.Path != expect.path {
t.Errorf("Expected path %s, got %s", expect.path, out.Path)
}
if out.Name != expect.name {
t.Errorf("Expected name %s, got %s", expect.name, out.Name)
}
if out.Kind != expect.kind {
t.Errorf("Expected kind %s, got %s", expect.kind, out.Kind)
}
for i := 0; i < len(out.Events); i++ {
if out.Events[i] != expect.hooks[i] {
t.Errorf("Expected event %d, got %d", expect.hooks[i], out.Events[i])
}
}
}
}
if !found {
t.Errorf("Result not found: %v", out)
}
}
}

@ -216,43 +216,42 @@ func (s *releaseServer) engine(ch *chart.Chart) environment.Engine {
} }
func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) { func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) {
rel, err := s.prepareRelease(req)
if err != nil {
return nil, err
}
return s.performRelease(rel, req)
}
// prepareRelease builds a release for an install operation.
func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*release.Release, error) {
if req.Chart == nil { if req.Chart == nil {
return nil, errMissingChart return nil, errMissingChart
} }
ts := timeconv.Now()
name, err := s.uniqName(req.Name) name, err := s.uniqName(req.Name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
overrides := map[string]interface{}{ ts := timeconv.Now()
"Release": map[string]interface{}{ options := chartutil.ReleaseOptions{Name: name, Time: ts, Namespace: s.env.Namespace}
"Name": name, valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options)
"Time": ts,
"Namespace": s.env.Namespace,
"Service": "Tiller",
},
"Chart": req.Chart.Metadata,
}
// Render the templates
// TODO: Fix based on whether chart has `engine: SOMETHING` set.
vals, err := chartutil.CoalesceValues(req.Chart, req.Values, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
overrides["Values"] = vals
renderer := s.engine(req.Chart) renderer := s.engine(req.Chart)
files, err := renderer.Render(req.Chart, overrides) files, err := renderer.Render(req.Chart, valuesToRender)
if err != nil { if err != nil {
return nil, err return nil, err
} }
hooks, manifests := sortHooks(files)
// Aggregate all non-hooks into one big doc.
b := bytes.NewBuffer(nil) b := bytes.NewBuffer(nil)
for name, file := range files { for name, file := range manifests {
// Ignore templates that starts with underscore to handle them as partials // Ignore templates that starts with underscore to handle them as partials
if strings.HasPrefix(path.Base(name), "_") { if strings.HasPrefix(path.Base(name), "_") {
continue continue
@ -267,7 +266,7 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea
} }
// Store a release. // Store a release.
r := &release.Release{ rel := &release.Release{
Name: name, Name: name,
Chart: req.Chart, Chart: req.Chart,
Config: req.Values, Config: req.Values,
@ -277,22 +276,41 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea
Status: &release.Status{Code: release.Status_UNKNOWN}, Status: &release.Status{Code: release.Status_UNKNOWN},
}, },
Manifest: b.String(), Manifest: b.String(),
Hooks: hooks,
Version: 1,
} }
return rel, nil
}
// performRelease runs a release.
func (s *releaseServer) performRelease(r *release.Release, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) {
res := &services.InstallReleaseResponse{Release: r} res := &services.InstallReleaseResponse{Release: r}
if req.DryRun { if req.DryRun {
log.Printf("Dry run for %s", name) log.Printf("Dry run for %s", r.Name)
return res, nil return res, nil
} }
if err := s.env.KubeClient.Create(s.env.Namespace, b); err != nil { // pre-install hooks
if err := s.execHook(r.Hooks, r.Name, preInstall); err != nil {
return res, err
}
// regular manifests
kubeCli := s.env.KubeClient
b := bytes.NewBufferString(r.Manifest)
if err := kubeCli.Create(s.env.Namespace, b); err != nil {
r.Info.Status.Code = release.Status_FAILED r.Info.Status.Code = release.Status_FAILED
log.Printf("warning: Release %q failed: %s", name, err) log.Printf("warning: Release %q failed: %s", r.Name, err)
if err := s.env.Releases.Create(r); err != nil { if err := s.env.Releases.Create(r); err != nil {
log.Printf("warning: Failed to record release %q: %s", name, err) log.Printf("warning: Failed to record release %q: %s", r.Name, err)
} }
return res, fmt.Errorf("release %s failed: %s", name, err) return res, fmt.Errorf("release %s failed: %s", r.Name, err)
}
// post-install hooks
if err := s.execHook(r.Hooks, r.Name, postInstall); err != nil {
return res, err
} }
// This is a tricky case. The release has been created, but the result // This is a tricky case. The release has been created, but the result
@ -302,15 +320,51 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea
// //
// One possible strategy would be to do a timed retry to see if we can get // One possible strategy would be to do a timed retry to see if we can get
// this stored in the future. // this stored in the future.
r.Info.Status.Code = release.Status_DEPLOYED
if err := s.env.Releases.Create(r); err != nil { if err := s.env.Releases.Create(r); err != nil {
log.Printf("warning: Failed to record release %q: %s", name, err) log.Printf("warning: Failed to record release %q: %s", r.Name, err)
return res, nil
} }
r.Info.Status.Code = release.Status_DEPLOYED
return res, nil return res, nil
} }
func (s *releaseServer) execHook(hs []*release.Hook, name, hook string) error {
kubeCli := s.env.KubeClient
code, ok := events[hook]
if !ok {
return fmt.Errorf("unknown hook %q", hook)
}
log.Printf("Executing %s hooks for %s", hook, name)
for _, h := range hs {
found := false
for _, e := range h.Events {
if e == code {
found = true
}
}
// If this doesn't implement the hook, skip it.
if !found {
continue
}
b := bytes.NewBufferString(h.Manifest)
if err := kubeCli.Create(s.env.Namespace, b); err != nil {
log.Printf("wrning: Release %q pre-install %s failed: %s", name, h.Path, err)
return err
}
// No way to rewind a bytes.Buffer()?
b.Reset()
b.WriteString(h.Manifest)
if err := kubeCli.WatchUntilReady(s.env.Namespace, b); err != nil {
log.Printf("warning: Release %q pre-install %s could not complete: %s", name, h.Path, err)
return err
}
h.LastRun = timeconv.Now()
}
log.Printf("Hooks complete for %s %s", hook, name)
return nil
}
func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) { func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) {
if req.Name == "" { if req.Name == "" {
log.Printf("uninstall: Release not found: %s", req.Name) log.Printf("uninstall: Release not found: %s", req.Name)
@ -326,20 +380,27 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR
log.Printf("uninstall: Deleting %s", req.Name) log.Printf("uninstall: Deleting %s", req.Name)
rel.Info.Status.Code = release.Status_DELETED rel.Info.Status.Code = release.Status_DELETED
rel.Info.Deleted = timeconv.Now() rel.Info.Deleted = timeconv.Now()
res := &services.UninstallReleaseResponse{Release: rel}
b := bytes.NewBuffer([]byte(rel.Manifest)) if err := s.execHook(rel.Hooks, rel.Name, preDelete); err != nil {
return res, err
}
b := bytes.NewBuffer([]byte(rel.Manifest))
if err := s.env.KubeClient.Delete(s.env.Namespace, b); err != nil { if err := s.env.KubeClient.Delete(s.env.Namespace, b); err != nil {
log.Printf("uninstall: Failed deletion of %q: %s", req.Name, err) log.Printf("uninstall: Failed deletion of %q: %s", req.Name, err)
return nil, err return nil, err
} }
if err := s.execHook(rel.Hooks, rel.Name, postDelete); err != nil {
return res, err
}
if err := s.env.Releases.Update(rel); err != nil { if err := s.env.Releases.Update(rel); err != nil {
log.Printf("uninstall: Failed to store updated release: %s", err) log.Printf("uninstall: Failed to store updated release: %s", err)
} }
res := services.UninstallReleaseResponse{Release: rel} return res, nil
return &res, nil
} }
// byName implements the sort.Interface for []*release.Release. // byName implements the sort.Interface for []*release.Release.

@ -34,6 +34,16 @@ import (
"k8s.io/helm/pkg/timeconv" "k8s.io/helm/pkg/timeconv"
) )
var manifestWithHook = `apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
annotations:
"helm.sh/hook": post-install,pre-delete
data:
name: value
`
func rsFixture() *releaseServer { func rsFixture() *releaseServer {
return &releaseServer{ return &releaseServer{
env: mockEnvironment(), env: mockEnvironment(),
@ -59,6 +69,18 @@ func releaseMock() *release.Release {
}, },
}, },
Config: &chart.Config{Raw: `name = "value"`}, Config: &chart.Config{Raw: `name = "value"`},
Hooks: []*release.Hook{
{
Name: "test-cm",
Kind: "ConfigMap",
Path: "test-cm",
Manifest: manifestWithHook,
Events: []release.Hook_Event{
release.Hook_POST_INSTALL,
release.Hook_PRE_DELETE,
},
},
},
} }
} }
@ -71,6 +93,7 @@ func TestInstallRelease(t *testing.T) {
Metadata: &chart.Metadata{Name: "hello"}, Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{ Templates: []*chart.Template{
{Name: "hello", Data: []byte("hello: world")}, {Name: "hello", Data: []byte("hello: world")},
{Name: "hooks", Data: []byte(manifestWithHook)},
}, },
}, },
} }
@ -89,6 +112,20 @@ func TestInstallRelease(t *testing.T) {
t.Logf("rel: %v", rel) t.Logf("rel: %v", rel)
if len(rel.Hooks) != 1 {
t.Fatalf("Expected 1 hook, got %d", len(rel.Hooks))
}
if rel.Hooks[0].Manifest != manifestWithHook {
t.Errorf("Unexpected manifest: %v", rel.Hooks[0].Manifest)
}
if rel.Hooks[0].Events[0] != release.Hook_POST_INSTALL {
t.Errorf("Expected event 0 is post install")
}
if rel.Hooks[0].Events[1] != release.Hook_PRE_DELETE {
t.Errorf("Expected event 0 is pre-delete")
}
if len(res.Release.Manifest) == 0 { if len(res.Release.Manifest) == 0 {
t.Errorf("No manifest returned: %v", res.Release) t.Errorf("No manifest returned: %v", res.Release)
} }
@ -97,7 +134,7 @@ func TestInstallRelease(t *testing.T) {
t.Errorf("Expected manifest in %v", res) t.Errorf("Expected manifest in %v", res)
} }
if !strings.Contains(rel.Manifest, "---\n# Source: hello\nhello: world") { if !strings.Contains(rel.Manifest, "---\n# Source: hello/hello\nhello: world") {
t.Errorf("unexpected output: %s", rel.Manifest) t.Errorf("unexpected output: %s", rel.Manifest)
} }
} }
@ -113,8 +150,9 @@ func TestInstallReleaseDryRun(t *testing.T) {
{Name: "hello", Data: []byte("hello: world")}, {Name: "hello", Data: []byte("hello: world")},
{Name: "goodbye", Data: []byte("goodbye: world")}, {Name: "goodbye", Data: []byte("goodbye: world")},
{Name: "empty", Data: []byte("")}, {Name: "empty", Data: []byte("")},
{Name: "with-partials", Data: []byte("hello: {{ template \"partials/_planet\" . }}")}, {Name: "with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)},
{Name: "partials/_planet", Data: []byte("Earth")}, {Name: "partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)},
{Name: "hooks", Data: []byte(manifestWithHook)},
}, },
}, },
DryRun: true, DryRun: true,
@ -127,11 +165,11 @@ func TestInstallReleaseDryRun(t *testing.T) {
t.Errorf("Expected release name.") t.Errorf("Expected release name.")
} }
if !strings.Contains(res.Release.Manifest, "---\n# Source: hello\nhello: world") { if !strings.Contains(res.Release.Manifest, "---\n# Source: hello/hello\nhello: world") {
t.Errorf("unexpected output: %s", res.Release.Manifest) t.Errorf("unexpected output: %s", res.Release.Manifest)
} }
if !strings.Contains(res.Release.Manifest, "---\n# Source: goodbye\ngoodbye: world") { if !strings.Contains(res.Release.Manifest, "---\n# Source: hello/goodbye\ngoodbye: world") {
t.Errorf("unexpected output: %s", res.Release.Manifest) t.Errorf("unexpected output: %s", res.Release.Manifest)
} }
@ -139,7 +177,7 @@ func TestInstallReleaseDryRun(t *testing.T) {
t.Errorf("Should contain partial content. %s", res.Release.Manifest) t.Errorf("Should contain partial content. %s", res.Release.Manifest)
} }
if strings.Contains(res.Release.Manifest, "hello: {{ template \"partials/_planet\" . }}") { if strings.Contains(res.Release.Manifest, "hello: {{ template \"_planet\" . }}") {
t.Errorf("Should not contain partial templates itself. %s", res.Release.Manifest) t.Errorf("Should not contain partial templates itself. %s", res.Release.Manifest)
} }
@ -150,6 +188,14 @@ func TestInstallReleaseDryRun(t *testing.T) {
if _, err := rs.env.Releases.Read(res.Release.Name); err == nil { if _, err := rs.env.Releases.Read(res.Release.Name); err == nil {
t.Errorf("Expected no stored release.") t.Errorf("Expected no stored release.")
} }
if l := len(res.Release.Hooks); l != 1 {
t.Fatalf("Expected 1 hook, got %d", l)
}
if res.Release.Hooks[0].LastRun != nil {
t.Error("Expected hook to not be marked as run.")
}
} }
func TestUninstallRelease(t *testing.T) { func TestUninstallRelease(t *testing.T) {
@ -163,6 +209,18 @@ func TestUninstallRelease(t *testing.T) {
Code: release.Status_DEPLOYED, Code: release.Status_DEPLOYED,
}, },
}, },
Hooks: []*release.Hook{
{
Name: "test-cm",
Kind: "ConfigMap",
Path: "test-cm",
Manifest: manifestWithHook,
Events: []release.Hook_Event{
release.Hook_POST_INSTALL,
release.Hook_PRE_DELETE,
},
},
},
}) })
req := &services.UninstallReleaseRequest{ req := &services.UninstallReleaseRequest{
@ -182,6 +240,10 @@ func TestUninstallRelease(t *testing.T) {
t.Errorf("Expected status code to be DELETED, got %d", res.Release.Info.Status.Code) t.Errorf("Expected status code to be DELETED, got %d", res.Release.Info.Status.Code)
} }
if res.Release.Hooks[0].LastRun.Seconds == 0 {
t.Error("Expected LastRun to be greater than zero.")
}
if res.Release.Info.Deleted.Seconds <= 0 { if res.Release.Info.Deleted.Seconds <= 0 {
t.Errorf("Expected valid UNIX date, got %d", res.Release.Info.Deleted.Seconds) t.Errorf("Expected valid UNIX date, got %d", res.Release.Info.Deleted.Seconds)
} }

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main package main // import "k8s.io/helm/cmd/tiller"
import ( import (
"fmt" "fmt"

@ -142,7 +142,9 @@ When a user supplies custom values, these values will override the
values in the chart's `values.yaml` file. values in the chart's `values.yaml` file.
### Template Files ### Template Files
Template files follow the standard conventions for writing Go templates. Template files follow the standard conventions for writing Go templates
(see [the text/template Go package documentation](https://golang.org/pkg/text/template/)
for details).
An example template file might look something like this: An example template file might look something like this:
```yaml ```yaml
@ -302,9 +304,9 @@ apache:
``` ```
The above adds a `global` section with the value `app: MyWordpress`. The above adds a `global` section with the value `app: MyWordpress`.
This value is available to _all_ charts as `.global.app`. This value is available to _all_ charts as `.Values.global.app`.
For example, the `mysql` templates may access `app` as `{{.global.app}}`, and For example, the `mysql` templates may access `app` as `{{.Values.global.app}}`, and
so can the `apache` chart. Effectively, the values file above is so can the `apache` chart. Effectively, the values file above is
regenerated like this: regenerated like this:

@ -16,7 +16,7 @@ Helm and Tiller.
We use Make to build our programs. The simplest way to get started is: We use Make to build our programs. The simplest way to get started is:
```console ```console
$ make boostrap build $ make bootstrap build
``` ```
This will build both Helm and Tiller. `make bootstrap` will attempt to This will build both Helm and Tiller. `make bootstrap` will attempt to

@ -1,2 +1,2 @@
# The pod name # The pod name
name: my-alpine Name: my-alpine

@ -0,0 +1,31 @@
apiVersion: batch/v1
kind: Job
metadata:
name: "{{template "fullname" . }}"
labels:
heritage: {{.Release.Service | quote }}
release: {{.Release.Name | quote }}
chart: "{{.Chart.Name}}-{{.Chart.Version}}"
annotations:
# This is what defines this resource as a hook. Without this line, the
# job is considered part of the release.
"helm.sh/hook": post-install
spec:
template:
metadata:
name: "{{template "fullname" . }}"
labels:
heritage: {{.Release.Service | quote }}
release: {{.Release.Name | quote }}
chart: "{{.Chart.Name}}-{{.Chart.Version}}"
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: Never
containers:
- name: {{template "fullname" .}}-job
image: "alpine:3.3"
# All we're going to do is sleep for a minute, then exit.
command: ["/bin/sleep","{{default "10" .Values.sleepyTime}}"]

@ -0,0 +1,14 @@
# This shows a secret as a pre-install hook.
# A pre-install hook is run before the rest of the chart is loaded.
apiVersion: v1
kind: Secret
metadata:
name: "{{.Release.Name}}-secret"
# This declares the resource to be a hook. By convention, we also name the
# file "pre-install-XXX.yaml", but Helm itself doesn't care about file names.
annotations:
"helm.sh/hook": pre-install
type: Opaque
data:
password: {{ b64enc "secret" }}
username: {{ b64enc "user1" }}

@ -11,6 +11,9 @@ httpPort: 8888
# Number of nginx instances to run # Number of nginx instances to run
replicaCount: 1 replicaCount: 1
# Evaluated by the post-install hook
sleepyTime: "10"
index: >- index: >-
<h1>Hello</h1> <h1>Hello</h1>
<p>This is a test</p> <p>This is a test</p>

21
glide.lock generated

@ -1,5 +1,5 @@
hash: b84a1c02841aebb58710da5c7fe6865ab5d4e43d6c51fa8cd9ec68ceb1ebe37e hash: 141ef5b9c491c91b026ab4007e48502c9a6df9f173c40e1406233dd44f065190
updated: 2016-06-26T14:51:02.02536382-07:00 updated: 2016-07-05T16:51:52.631048739-07:00
imports: imports:
- name: github.com/aokoli/goutils - name: github.com/aokoli/goutils
version: 9c37978a95bd5c709a15883b6242714ea6709e64 version: 9c37978a95bd5c709a15883b6242714ea6709e64
@ -133,7 +133,7 @@ imports:
- ptypes/any - ptypes/any
- ptypes/timestamp - ptypes/timestamp
- name: github.com/google/cadvisor - name: github.com/google/cadvisor
version: 7d22cf63253c17bad8ab64b8eef679718d00342e version: 4dbefc9b671b81257973a33211fb12370c1a526e
subpackages: subpackages:
- api - api
- cache/memory - cache/memory
@ -238,8 +238,6 @@ imports:
version: 490cc6eb5fa45bf8a8b7b73c8bc82a8160e8531d version: 490cc6eb5fa45bf8a8b7b73c8bc82a8160e8531d
- name: github.com/spf13/cobra - name: github.com/spf13/cobra
version: 6a8bd97bdb1fc0d08a83459940498ea49d3e8c93 version: 6a8bd97bdb1fc0d08a83459940498ea49d3e8c93
subpackages:
- cobra
- name: github.com/spf13/pflag - name: github.com/spf13/pflag
version: 367864438f1b1a3c7db4da06a2f55b144e6784e0 version: 367864438f1b1a3c7db4da06a2f55b144e6784e0
- name: github.com/technosophos/moniker - name: github.com/technosophos/moniker
@ -286,11 +284,11 @@ imports:
- name: google.golang.org/grpc - name: google.golang.org/grpc
version: dec33edc378cf4971a2741cfd86ed70a644d6ba3 version: dec33edc378cf4971a2741cfd86ed70a644d6ba3
subpackages: subpackages:
- metadata
- codes - codes
- credentials - credentials
- grpclog - grpclog
- internal - internal
- metadata
- naming - naming
- transport - transport
- peer - peer
@ -299,7 +297,7 @@ imports:
- name: gopkg.in/yaml.v2 - name: gopkg.in/yaml.v2
version: a83829b6f1293c91addabc89d0571c246397bbf4 version: a83829b6f1293c91addabc89d0571c246397bbf4
- name: k8s.io/kubernetes - name: k8s.io/kubernetes
version: caf9a4d87700ba034a7b39cced19bd5628ca6aa3 version: 283137936a498aed572ee22af6774b6fb6e9fd94
subpackages: subpackages:
- pkg/api - pkg/api
- pkg/api/meta - pkg/api/meta
@ -326,13 +324,13 @@ imports:
- pkg/util/intstr - pkg/util/intstr
- pkg/util/rand - pkg/util/rand
- pkg/util/sets - pkg/util/sets
- pkg/util/validation
- pkg/util/validation/field
- pkg/client/unversioned/auth - pkg/client/unversioned/auth
- pkg/client/unversioned/clientcmd/api - pkg/client/unversioned/clientcmd/api
- pkg/client/unversioned/clientcmd/api/latest - pkg/client/unversioned/clientcmd/api/latest
- pkg/util/errors - pkg/util/errors
- pkg/util/homedir - pkg/util/homedir
- pkg/util/validation
- pkg/util/validation/field
- pkg/kubelet/server/portforward - pkg/kubelet/server/portforward
- pkg/util/httpstream - pkg/util/httpstream
- pkg/util/runtime - pkg/util/runtime
@ -361,6 +359,7 @@ imports:
- pkg/util/strategicpatch - pkg/util/strategicpatch
- pkg/watch - pkg/watch
- pkg/util/yaml - pkg/util/yaml
- pkg/api/testapi
- third_party/forked/reflect - third_party/forked/reflect
- pkg/conversion/queryparams - pkg/conversion/queryparams
- pkg/util/json - pkg/util/json
@ -425,7 +424,7 @@ imports:
- pkg/util/framer - pkg/util/framer
- third_party/forked/json - third_party/forked/json
- pkg/util/parsers - pkg/util/parsers
- federation/apis/federation/v1alpha1 - federation/apis/federation/v1beta1
- pkg/apis/apps/v1alpha1 - pkg/apis/apps/v1alpha1
- pkg/apis/authentication.k8s.io - pkg/apis/authentication.k8s.io
- pkg/apis/authentication.k8s.io/v1beta1 - pkg/apis/authentication.k8s.io/v1beta1
@ -452,4 +451,4 @@ imports:
version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4
repo: https://github.com/go-inf/inf.git repo: https://github.com/go-inf/inf.git
vcs: git vcs: git
devImports: [] testImports: []

@ -5,8 +5,8 @@ import:
subpackages: subpackages:
- context - context
- package: github.com/spf13/cobra - package: github.com/spf13/cobra
subpackages: - package: github.com/spf13/pflag
- cobra version: 367864438f1b1a3c7db4da06a2f55b144e6784e0
- package: github.com/Masterminds/sprig - package: github.com/Masterminds/sprig
version: ^2.3 version: ^2.3
- package: gopkg.in/yaml.v2 - package: gopkg.in/yaml.v2
@ -22,7 +22,7 @@ import:
- package: google.golang.org/grpc - package: google.golang.org/grpc
version: dec33edc378cf4971a2741cfd86ed70a644d6ba3 version: dec33edc378cf4971a2741cfd86ed70a644d6ba3
- package: k8s.io/kubernetes - package: k8s.io/kubernetes
version: v1.3.0-beta.2 version: v1.3.0
subpackages: subpackages:
- pkg/api - pkg/api
- pkg/api/meta - pkg/api/meta

@ -41,4 +41,4 @@ into a Chart.
When creating charts in memory, use the 'k8s.io/helm/pkg/proto/happy/chart' When creating charts in memory, use the 'k8s.io/helm/pkg/proto/happy/chart'
package directly. package directly.
*/ */
package chartutil package chartutil // import "k8s.io/helm/pkg/chartutil"

@ -40,6 +40,16 @@ func Expand(dir string, r io.Reader) error {
return err return err
} }
//split header name and create missing directories
d, _ := filepath.Split(header.Name)
fullDir := filepath.Join(dir, d)
_, err = os.Stat(fullDir)
if err != nil && d != "" {
if err := os.MkdirAll(fullDir, 0700); err != nil {
return err
}
}
path := filepath.Clean(filepath.Join(dir, header.Name)) path := filepath.Clean(filepath.Join(dir, header.Name))
info := header.FileInfo() info := header.FileInfo()
if info.IsDir() { if info.IsDir() {

@ -17,18 +17,19 @@ limitations under the License.
package chartutil package chartutil
import ( import (
"errors" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"strings" "strings"
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"github.com/golang/protobuf/ptypes/timestamp"
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
) )
// ErrNoTable indicates that a chart does not have a matching table. // ErrNoTable indicates that a chart does not have a matching table.
var ErrNoTable = errors.New("no table") type ErrNoTable error
// GlobalKey is the name of the Values key that is used for storing global vars. // GlobalKey is the name of the Values key that is used for storing global vars.
const GlobalKey = "global" const GlobalKey = "global"
@ -92,7 +93,7 @@ func (v Values) Encode(w io.Writer) error {
func tableLookup(v Values, simple string) (Values, error) { func tableLookup(v Values, simple string) (Values, error) {
v2, ok := v[simple] v2, ok := v[simple]
if !ok { if !ok {
return v, ErrNoTable return v, ErrNoTable(fmt.Errorf("no table named %q (%v)", simple, v))
} }
if vv, ok := v2.(map[string]interface{}); ok { if vv, ok := v2.(map[string]interface{}); ok {
return vv, nil return vv, nil
@ -105,14 +106,15 @@ func tableLookup(v Values, simple string) (Values, error) {
return vv, nil return vv, nil
} }
return map[string]interface{}{}, ErrNoTable var e ErrNoTable = fmt.Errorf("no table named %q", simple)
return map[string]interface{}{}, e
} }
// ReadValues will parse YAML byte data into a Values. // ReadValues will parse YAML byte data into a Values.
func ReadValues(data []byte) (vals Values, err error) { func ReadValues(data []byte) (vals Values, err error) {
vals = make(map[string]interface{}) err = yaml.Unmarshal(data, &vals)
if len(data) > 0 { if len(vals) == 0 {
err = yaml.Unmarshal(data, &vals) vals = Values{}
} }
return return
} }
@ -138,7 +140,7 @@ func ReadValuesFile(filename string) (Values, error) {
// - A chart has access to all of the variables for it, as well as all of // - A chart has access to all of the variables for it, as well as all of
// the values destined for its dependencies. // the values destined for its dependencies.
func CoalesceValues(chrt *chart.Chart, vals *chart.Config, overrides map[string]interface{}) (Values, error) { func CoalesceValues(chrt *chart.Chart, vals *chart.Config, overrides map[string]interface{}) (Values, error) {
var cvals Values cvals := Values{}
// Parse values if not nil. We merge these at the top level because // Parse values if not nil. We merge these at the top level because
// the passed-in values are in the same namespace as the parent chart. // the passed-in values are in the same namespace as the parent chart.
if vals != nil { if vals != nil {
@ -288,6 +290,35 @@ func coalesceTables(dst, src map[string]interface{}) map[string]interface{} {
return dst return dst
} }
// ReleaseOptions represents the additional release options needed
// for the composition of the final values struct
type ReleaseOptions struct {
Name string
Time *timestamp.Timestamp
Namespace string
}
// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files
func ToRenderValues(chrt *chart.Chart, chrtVals *chart.Config, options ReleaseOptions) (Values, error) {
overrides := map[string]interface{}{
"Release": map[string]interface{}{
"Name": options.Name,
"Time": options.Time,
"Namespace": options.Namespace,
"Service": "Tiller",
},
"Chart": chrt.Metadata,
}
vals, err := CoalesceValues(chrt, chrtVals, nil)
if err != nil {
return overrides, err
}
overrides["Values"] = vals
return overrides, nil
}
// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. // istable is a special-purpose function to see if the present thing matches the definition of a YAML table.
func istable(v interface{}) bool { func istable(v interface{}) bool {
_, ok := v.(map[string]interface{}) _, ok := v.(map[string]interface{})

@ -53,6 +53,18 @@ water:
t.Fatalf("Error parsing bytes: %s", err) t.Fatalf("Error parsing bytes: %s", err)
} }
matchValues(t, data) matchValues(t, data)
tests := []string{`poet: "Coleridge"`, "# Just a comment", ""}
for _, tt := range tests {
data, err = ReadValues([]byte(tt))
if err != nil {
t.Fatalf("Error parsing bytes: %s", err)
}
if data == nil {
t.Errorf(`YAML string "%s" gave a nil map`, tt)
}
}
} }
func TestReadValuesFile(t *testing.T) { func TestReadValuesFile(t *testing.T) {

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package client package client // import "k8s.io/helm/pkg/client"
import ( import (
"bytes" "bytes"

@ -20,4 +20,4 @@ Tiller provides a simple interface for taking a Chart and rendering its template
The 'engine' package implements this interface using Go's built-in 'text/template' The 'engine' package implements this interface using Go's built-in 'text/template'
package. package.
*/ */
package engine package engine // import "k8s.io/helm/pkg/engine"

@ -19,6 +19,9 @@ package engine
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"log"
"path"
"strings"
"text/template" "text/template"
"github.com/Masterminds/sprig" "github.com/Masterminds/sprig"
@ -31,6 +34,9 @@ type Engine struct {
// FuncMap contains the template functions that will be passed to each // FuncMap contains the template functions that will be passed to each
// render call. This may only be modified before the first call to Render. // render call. This may only be modified before the first call to Render.
FuncMap template.FuncMap FuncMap template.FuncMap
// If strict is enabled, template rendering will fail if a template references
// a value that was not passed in.
Strict bool
} }
// New creates a new Go template Engine instance. // New creates a new Go template Engine instance.
@ -92,8 +98,16 @@ func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) {
// to share common blocks, but to make the entire thing feel like a file-based // to share common blocks, but to make the entire thing feel like a file-based
// template engine. // template engine.
t := template.New("gotpl") t := template.New("gotpl")
if e.Strict {
t.Option("missingkey=error")
} else {
// Not that zero will attempt to add default values for types it knows,
// but will still emit <no value> for others. We mitigate that later.
t.Option("missingkey=zero")
}
files := []string{} files := []string{}
for fname, r := range tpls { for fname, r := range tpls {
log.Printf("Preparing template %s", fname)
t = t.New(fname).Funcs(e.FuncMap) t = t.New(fname).Funcs(e.FuncMap)
if _, err := t.Parse(r.tpl); err != nil { if _, err := t.Parse(r.tpl); err != nil {
return map[string]string{}, fmt.Errorf("parse error in %q: %s", fname, err) return map[string]string{}, fmt.Errorf("parse error in %q: %s", fname, err)
@ -104,10 +118,17 @@ func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) {
rendered := make(map[string]string, len(files)) rendered := make(map[string]string, len(files))
var buf bytes.Buffer var buf bytes.Buffer
for _, file := range files { for _, file := range files {
if err := t.ExecuteTemplate(&buf, file, tpls[file].vals); err != nil { // At render time, add information about the template that is being rendered.
vals := tpls[file].vals
vals["Template"] = map[string]interface{}{"Name": file}
if err := t.ExecuteTemplate(&buf, file, vals); err != nil {
return map[string]string{}, fmt.Errorf("render error in %q: %s", file, err) return map[string]string{}, fmt.Errorf("render error in %q: %s", file, err)
} }
rendered[file] = buf.String()
// Work around the issue where Go will emit "<no value>" even if Options(missing=zero)
// is set. Since missing=error will never get here, we do not need to handle
// the Strict case.
rendered[file] = strings.Replace(buf.String(), "<no value>", "", -1)
buf.Reset() buf.Reset()
} }
@ -119,7 +140,7 @@ func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) {
// As it goes, it also prepares the values in a scope-sensitive manner. // As it goes, it also prepares the values in a scope-sensitive manner.
func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable { func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable {
templates := map[string]renderable{} templates := map[string]renderable{}
recAllTpls(c, templates, vals, true) recAllTpls(c, templates, vals, true, "")
return templates return templates
} }
@ -127,39 +148,44 @@ func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable {
// //
// As it recurses, it also sets the values to be appropriate for the template // As it recurses, it also sets the values to be appropriate for the template
// scope. // scope.
func recAllTpls(c *chart.Chart, templates map[string]renderable, parentVals chartutil.Values, top bool) { func recAllTpls(c *chart.Chart, templates map[string]renderable, parentVals chartutil.Values, top bool, parentID string) {
var cvals chartutil.Values // This should never evaluate to a nil map. That will cause problems when
// values are appended later.
cvals := chartutil.Values{}
if top { if top {
// If this is the top of the rendering tree, assume that parentVals // If this is the top of the rendering tree, assume that parentVals
// is already resolved to the authoritative values. // is already resolved to the authoritative values.
cvals = parentVals cvals = parentVals
} else if c.Metadata != nil && c.Metadata.Name != "" { } else if c.Metadata != nil && c.Metadata.Name != "" {
// An error indicates that the table doesn't exist. So we leave it as // If there is a {{.Values.ThisChart}} in the parent metadata,
// an empty map. // copy that into the {{.Values}} for this template.
newVals := chartutil.Values{}
var tmp chartutil.Values if vs, err := parentVals.Table("Values"); err == nil {
vs, err := parentVals.Table("Values") if tmp, err := vs.Table(c.Metadata.Name); err == nil {
if err == nil { newVals = tmp
tmp, err = vs.Table(c.Metadata.Name) }
} else {
tmp, err = parentVals.Table(c.Metadata.Name)
} }
//tmp, err := parentVals["Values"].(chartutil.Values).Table(c.Metadata.Name) cvals = map[string]interface{}{
if err == nil { "Values": newVals,
cvals = map[string]interface{}{ "Release": parentVals["Release"],
"Values": tmp, "Chart": c.Metadata,
"Release": parentVals["Release"],
"Chart": c,
}
} }
} }
newParentID := c.Metadata.Name
if parentID != "" {
// We artificially reconstruct the chart path to child templates. This
// creates a namespaced filename that can be used to track down the source
// of a particular template declaration.
newParentID = path.Join(parentID, "charts", newParentID)
}
for _, child := range c.Dependencies { for _, child := range c.Dependencies {
recAllTpls(child, templates, cvals, false) recAllTpls(child, templates, cvals, false, newParentID)
} }
for _, t := range c.Templates { for _, t := range c.Templates {
templates[t.Name] = renderable{ templates[path.Join(newParentID, t.Name)] = renderable{
tpl: string(t.Data), tpl: string(t.Data),
vals: cvals, vals: cvals,
} }

@ -46,6 +46,7 @@ func TestRender(t *testing.T) {
Templates: []*chart.Template{ Templates: []*chart.Template{
{Name: "test1", Data: []byte("{{.outer | title }} {{.inner | title}}")}, {Name: "test1", Data: []byte("{{.outer | title }} {{.inner | title}}")},
{Name: "test2", Data: []byte("{{.global.callme | lower }}")}, {Name: "test2", Data: []byte("{{.global.callme | lower }}")},
{Name: "test3", Data: []byte("{{.noValue}}")},
}, },
Values: &chart.Config{ Values: &chart.Config{
Raw: "outer: DEFAULT\ninner: DEFAULT", Raw: "outer: DEFAULT\ninner: DEFAULT",
@ -74,14 +75,18 @@ func TestRender(t *testing.T) {
} }
expect := "Spouter Inn" expect := "Spouter Inn"
if out["test1"] != expect { if out["moby/test1"] != expect {
t.Errorf("Expected %q, got %q", expect, out["test1"]) t.Errorf("Expected %q, got %q", expect, out["test1"])
} }
expect = "ishmael" expect = "ishmael"
if out["test2"] != expect { if out["moby/test2"] != expect {
t.Errorf("Expected %q, got %q", expect, out["test2"]) t.Errorf("Expected %q, got %q", expect, out["test2"])
} }
expect = ""
if out["moby/test3"] != expect {
t.Errorf("Expected %q, got %q", expect, out["test3"])
}
if _, err := e.Render(c, v); err != nil { if _, err := e.Render(c, v); err != nil {
t.Errorf("Unexpected error: %s", err) t.Errorf("Unexpected error: %s", err)
@ -149,18 +154,21 @@ func TestParallelRenderInternals(t *testing.T) {
func TestAllTemplates(t *testing.T) { func TestAllTemplates(t *testing.T) {
ch1 := &chart.Chart{ ch1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "ch1"},
Templates: []*chart.Template{ Templates: []*chart.Template{
{Name: "foo", Data: []byte("foo")}, {Name: "foo", Data: []byte("foo")},
{Name: "bar", Data: []byte("bar")}, {Name: "bar", Data: []byte("bar")},
}, },
Dependencies: []*chart.Chart{ Dependencies: []*chart.Chart{
{ {
Metadata: &chart.Metadata{Name: "laboratory mice"},
Templates: []*chart.Template{ Templates: []*chart.Template{
{Name: "pinky", Data: []byte("pinky")}, {Name: "pinky", Data: []byte("pinky")},
{Name: "brain", Data: []byte("brain")}, {Name: "brain", Data: []byte("brain")},
}, },
Dependencies: []*chart.Chart{ Dependencies: []*chart.Chart{{
{Templates: []*chart.Template{ Metadata: &chart.Metadata{Name: "same thing we do every night"},
Templates: []*chart.Template{
{Name: "innermost", Data: []byte("innermost")}, {Name: "innermost", Data: []byte("innermost")},
}}, }},
}, },
@ -180,11 +188,13 @@ func TestRenderDependency(t *testing.T) {
deptpl := `{{define "myblock"}}World{{end}}` deptpl := `{{define "myblock"}}World{{end}}`
toptpl := `Hello {{template "myblock"}}` toptpl := `Hello {{template "myblock"}}`
ch := &chart.Chart{ ch := &chart.Chart{
Metadata: &chart.Metadata{Name: "outerchart"},
Templates: []*chart.Template{ Templates: []*chart.Template{
{Name: "outer", Data: []byte(toptpl)}, {Name: "outer", Data: []byte(toptpl)},
}, },
Dependencies: []*chart.Chart{ Dependencies: []*chart.Chart{
{ {
Metadata: &chart.Metadata{Name: "innerchart"},
Templates: []*chart.Template{ Templates: []*chart.Template{
{Name: "inner", Data: []byte(deptpl)}, {Name: "inner", Data: []byte(deptpl)},
}, },
@ -203,7 +213,7 @@ func TestRenderDependency(t *testing.T) {
} }
expect := "Hello World" expect := "Hello World"
if out["outer"] != expect { if out["outerchart/outer"] != expect {
t.Errorf("Expected %q, got %q", expect, out["outer"]) t.Errorf("Expected %q, got %q", expect, out["outer"])
} }
@ -212,10 +222,11 @@ func TestRenderDependency(t *testing.T) {
func TestRenderNestedValues(t *testing.T) { func TestRenderNestedValues(t *testing.T) {
e := New() e := New()
innerpath := "charts/inner/templates/inner.tpl" innerpath := "templates/inner.tpl"
outerpath := "templates/outer.tpl" outerpath := "templates/outer.tpl"
deepestpath := "charts/inner/charts/deepest/templates/deepest.tpl" // Ensure namespacing rules are working.
checkrelease := "charts/inner/charts/deepest/templates/release.tpl" deepestpath := "templates/inner.tpl"
checkrelease := "templates/release.tpl"
deepest := &chart.Chart{ deepest := &chart.Chart{
Metadata: &chart.Metadata{Name: "deepest"}, Metadata: &chart.Metadata{Name: "deepest"},
@ -280,19 +291,65 @@ global:
t.Fatalf("failed to render templates: %s", err) t.Fatalf("failed to render templates: %s", err)
} }
if out[outerpath] != "Gather ye rosebuds while ye may" { if out["top/"+outerpath] != "Gather ye rosebuds while ye may" {
t.Errorf("Unexpected outer: %q", out[outerpath]) t.Errorf("Unexpected outer: %q", out[outerpath])
} }
if out[innerpath] != "Old time is still a-flyin'" { if out["top/charts/herrick/"+innerpath] != "Old time is still a-flyin'" {
t.Errorf("Unexpected inner: %q", out[innerpath]) t.Errorf("Unexpected inner: %q", out[innerpath])
} }
if out[deepestpath] != "And this same flower that smiles to-day" { if out["top/charts/herrick/charts/deepest/"+deepestpath] != "And this same flower that smiles to-day" {
t.Errorf("Unexpected deepest: %q", out[deepestpath]) t.Errorf("Unexpected deepest: %q", out[deepestpath])
} }
if out[checkrelease] != "Tomorrow will be dyin" { if out["top/charts/herrick/charts/deepest/"+checkrelease] != "Tomorrow will be dyin" {
t.Errorf("Unexpected release: %q", out[checkrelease]) t.Errorf("Unexpected release: %q", out[checkrelease])
} }
} }
func TestRenderBuiltinValues(t *testing.T) {
inner := &chart.Chart{
Metadata: &chart.Metadata{Name: "Latium"},
Templates: []*chart.Template{
{Name: "Lavinia", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)},
},
Values: &chart.Config{Raw: ``},
Dependencies: []*chart.Chart{},
}
outer := &chart.Chart{
Metadata: &chart.Metadata{Name: "Troy"},
Templates: []*chart.Template{
{Name: "Aeneas", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)},
},
Values: &chart.Config{Raw: ``},
Dependencies: []*chart.Chart{inner},
}
inject := chartutil.Values{
"Values": &chart.Config{Raw: ""},
"Chart": outer.Metadata,
"Release": chartutil.Values{
"Name": "Aeneid",
},
}
t.Logf("Calculated values: %v", outer)
out, err := New().Render(outer, inject)
if err != nil {
t.Fatalf("failed to render templates: %s", err)
}
expects := map[string]string{
"Troy/charts/Latium/Lavinia": "Troy/charts/Latium/LaviniaLatiumAeneid",
"Troy/Aeneas": "Troy/AeneasTroyAeneid",
}
for file, expect := range expects {
if out[file] != expect {
t.Errorf("Expected %q, got %q", expect, out[file])
}
}
}

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package helm // import "k8s.io/helm/pkg/helm"
import ( import (
"google.golang.org/grpc" "google.golang.org/grpc"
@ -24,29 +24,30 @@ import (
) )
const ( const (
// $HELM_HOST envvar // HelmHostEnvVar is the $HELM_HOST envvar
HelmHostEnvVar = "HELM_HOST" HelmHostEnvVar = "HELM_HOST"
// $HELM_HOME envvar // HelmHomeEnvVar is the $HELM_HOME envvar
HelmHomeEnvVar = "HELM_HOME" HelmHomeEnvVar = "HELM_HOME"
// Default tiller server host address. // DefaultHelmHost is the default tiller server host address.
DefaultHelmHost = ":44134" DefaultHelmHost = ":44134"
// Default $HELM_HOME envvar value // DefaultHelmHome is the default $HELM_HOME envvar value
DefaultHelmHome = "$HOME/.helm" DefaultHelmHome = "$HOME/.helm"
) )
// Helm client manages client side of the helm-tiller protocol // Client manages client side of the helm-tiller protocol
type Client struct { type Client struct {
opts options opts options
} }
// NewClient creates a new client.
func NewClient(opts ...Option) *Client { func NewClient(opts ...Option) *Client {
return new(Client).Init().Option(opts...) return new(Client).Init().Option(opts...)
} }
// Configure the helm client with the provided options // Option configures the helm client with the provided options
func (h *Client) Option(opts ...Option) *Client { func (h *Client) Option(opts ...Option) *Client {
for _, opt := range opts { for _, opt := range opts {
opt(&h.opts) opt(&h.opts)
@ -54,10 +55,10 @@ func (h *Client) Option(opts ...Option) *Client {
return h return h
} }
// Initializes the helm client with default options // Init initializes the helm client with default options
func (h *Client) Init() *Client { func (h *Client) Init() *Client {
return h.Option(HelmHost(DefaultHelmHost)). return h.Option(Host(DefaultHelmHost)).
Option(HelmHome(os.ExpandEnv(DefaultHelmHome))) Option(Home(os.ExpandEnv(DefaultHelmHome)))
} }
// ListReleases lists the current releases. // ListReleases lists the current releases.
@ -87,7 +88,7 @@ func (h *Client) InstallRelease(chStr string, opts ...InstallOption) (*rls.Insta
return h.opts.rpcInstallRelease(chart, rls.NewReleaseServiceClient(c), opts...) return h.opts.rpcInstallRelease(chart, rls.NewReleaseServiceClient(c), opts...)
} }
// UninstallRelease uninstalls a named release and returns the response. // DeleteRelease uninstalls a named release and returns the response.
// //
// Note: there aren't currently any supported DeleteOptions, but they are // Note: there aren't currently any supported DeleteOptions, but they are
// kept in the API signature as a placeholder for future additions. // kept in the API signature as a placeholder for future additions.

@ -23,10 +23,13 @@ import (
// These APIs are a temporary abstraction layer that captures the interaction between the current cmd/helm and old // These APIs are a temporary abstraction layer that captures the interaction between the current cmd/helm and old
// pkg/helm implementations. Post refactor the cmd/helm package will use the APIs exposed on helm.Client directly. // pkg/helm implementations. Post refactor the cmd/helm package will use the APIs exposed on helm.Client directly.
// Config is the base configuration
var Config struct { var Config struct {
ServAddr string ServAddr string
} }
// ListReleases lists releases. DEPRECATED.
//
// Soon to be deprecated helm ListReleases API. // Soon to be deprecated helm ListReleases API.
func ListReleases(limit int, offset string, sort rls.ListSort_SortBy, order rls.ListSort_SortOrder, filter string) (*rls.ListReleasesResponse, error) { func ListReleases(limit int, offset string, sort rls.ListSort_SortBy, order rls.ListSort_SortOrder, filter string) (*rls.ListReleasesResponse, error) {
opts := []ReleaseListOption{ opts := []ReleaseListOption{
@ -36,36 +39,42 @@ func ListReleases(limit int, offset string, sort rls.ListSort_SortBy, order rls.
ReleaseListSort(int32(sort)), ReleaseListSort(int32(sort)),
ReleaseListOrder(int32(order)), ReleaseListOrder(int32(order)),
} }
return NewClient(HelmHost(Config.ServAddr)).ListReleases(opts...) return NewClient(Host(Config.ServAddr)).ListReleases(opts...)
} }
// GetReleaseStatus gets a release status. DEPRECATED
//
// Soon to be deprecated helm GetReleaseStatus API. // Soon to be deprecated helm GetReleaseStatus API.
func GetReleaseStatus(rlsName string) (*rls.GetReleaseStatusResponse, error) { func GetReleaseStatus(rlsName string) (*rls.GetReleaseStatusResponse, error) {
return NewClient(HelmHost(Config.ServAddr)).ReleaseStatus(rlsName) return NewClient(Host(Config.ServAddr)).ReleaseStatus(rlsName)
} }
// GetReleaseContent gets the content of a release.
// Soon to be deprecated helm GetReleaseContent API. // Soon to be deprecated helm GetReleaseContent API.
func GetReleaseContent(rlsName string) (*rls.GetReleaseContentResponse, error) { func GetReleaseContent(rlsName string) (*rls.GetReleaseContentResponse, error) {
return NewClient(HelmHost(Config.ServAddr)).ReleaseContent(rlsName) return NewClient(Host(Config.ServAddr)).ReleaseContent(rlsName)
} }
// UpdateRelease updates a release.
// Soon to be deprecated helm UpdateRelease API. // Soon to be deprecated helm UpdateRelease API.
func UpdateRelease(rlsName string) (*rls.UpdateReleaseResponse, error) { func UpdateRelease(rlsName string) (*rls.UpdateReleaseResponse, error) {
return NewClient(HelmHost(Config.ServAddr)).UpdateRelease(rlsName) return NewClient(Host(Config.ServAddr)).UpdateRelease(rlsName)
} }
// InstallRelease runs an install for a release.
// Soon to be deprecated helm InstallRelease API. // Soon to be deprecated helm InstallRelease API.
func InstallRelease(vals []byte, rlsName, chStr string, dryRun bool) (*rls.InstallReleaseResponse, error) { func InstallRelease(vals []byte, rlsName, chStr string, dryRun bool) (*rls.InstallReleaseResponse, error) {
client := NewClient(HelmHost(Config.ServAddr)) client := NewClient(Host(Config.ServAddr))
if dryRun { if dryRun {
client.Option(DryRun()) client.Option(DryRun())
} }
return client.InstallRelease(chStr, ValueOverrides(vals), ReleaseName(rlsName)) return client.InstallRelease(chStr, ValueOverrides(vals), ReleaseName(rlsName))
} }
// UninstallRelease destroys an existing release.
// Soon to be deprecated helm UninstallRelease API. // Soon to be deprecated helm UninstallRelease API.
func UninstallRelease(rlsName string, dryRun bool) (*rls.UninstallReleaseResponse, error) { func UninstallRelease(rlsName string, dryRun bool) (*rls.UninstallReleaseResponse, error) {
client := NewClient(HelmHost(Config.ServAddr)) client := NewClient(Host(Config.ServAddr))
if dryRun { if dryRun {
client.Option(DryRun()) client.Option(DryRun())
} }

@ -0,0 +1,31 @@
/*
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 (
rls "k8s.io/helm/pkg/proto/hapi/services"
)
// Interface for helm client for mocking in tests
type Interface interface {
ListReleases(opts ...ReleaseListOption) (*rls.ListReleasesResponse, error)
InstallRelease(chStr string, opts ...InstallOption) (*rls.InstallReleaseResponse, error)
DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error)
ReleaseStatus(rlsName string, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error)
UpdateRelease(rlsName string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error)
ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error)
}

@ -51,15 +51,15 @@ func DryRun() Option {
} }
} }
// HelmHome specifies the location of helm home, (default = "$HOME/.helm"). // Home specifies the location of helm home, (default = "$HOME/.helm").
func HelmHome(home string) Option { func Home(home string) Option {
return func(opts *options) { return func(opts *options) {
opts.home = home opts.home = home
} }
} }
// HelmHost specifies the host address of the Tiller release server, (default = ":44134"). // Host specifies the host address of the Tiller release server, (default = ":44134").
func HelmHost(host string) Option { func Host(host string) Option {
return func(opts *options) { return func(opts *options) {
opts.host = host opts.host = host
} }

@ -64,4 +64,4 @@ Notable differences from .gitignore:
- The evaluation of escape sequences has not been tested for compatibility - The evaluation of escape sequences has not been tested for compatibility
- There is no support for '\!' as a special leading sequence. - There is no support for '\!' as a special leading sequence.
*/ */
package ignore package ignore // import "k8s.io/helm/pkg/ignore"

@ -14,17 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package kube package kube // import "k8s.io/helm/pkg/kube"
import ( import (
"fmt" "fmt"
"io" "io"
"log"
"time"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/apis/batch"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/watch"
) )
// Client represents a client capable of communicating with the Kubernetes API. // Client represents a client capable of communicating with the Kubernetes API.
@ -59,6 +63,24 @@ func (c *Client) Delete(namespace string, reader io.Reader) error {
return perform(c, namespace, reader, deleteResource) return perform(c, namespace, reader, deleteResource)
} }
// WatchUntilReady watches the resource given in the reader, and waits until it is ready.
//
// This function is mainly for hook implementations. It watches for a resource to
// hit a particular milestone. The milestone depends on the Kind.
//
// For most kinds, it checks to see if the resource is marked as Added or Modified
// by the Kubernetes event stream. For some kinds, it does more:
//
// - Jobs: A job is marked "Ready" when it has successfully completed. This is
// ascertained by watching the Status fields in a job's output.
//
// Handling for other kinds will be added as necessary.
func (c *Client) WatchUntilReady(namespace string, reader io.Reader) error {
// For jobs, there's also the option to do poll c.Jobs(namespace).Get():
// https://github.com/adamreese/kubernetes/blob/master/test/e2e/job.go#L291-L300
return perform(c, namespace, reader, watchUntilReady)
}
const includeThirdPartyAPIs = false const includeThirdPartyAPIs = false
func perform(c *Client, namespace string, reader io.Reader, fn ResourceActorFunc) error { func perform(c *Client, namespace string, reader io.Reader, fn ResourceActorFunc) error {
@ -105,6 +127,69 @@ func deleteResource(info *resource.Info) error {
return resource.NewHelper(info.Client, info.Mapping).Delete(info.Namespace, info.Name) return resource.NewHelper(info.Client, info.Mapping).Delete(info.Namespace, info.Name)
} }
func watchUntilReady(info *resource.Info) error {
w, err := resource.NewHelper(info.Client, info.Mapping).WatchSingle(info.Namespace, info.Name, info.ResourceVersion)
if err != nil {
return err
}
kind := info.Mapping.GroupVersionKind.Kind
log.Printf("Watching for changes to %s %s", kind, info.Name)
timeout := time.Minute * 5
// What we watch for depends on the Kind.
// - For a Job, we watch for completion.
// - For all else, we watch until Ready.
// In the future, we might want to add some special logic for types
// like Ingress, Volume, etc.
_, err = watch.Until(timeout, w, func(e watch.Event) (bool, error) {
switch e.Type {
case watch.Added, watch.Modified:
// For things like a secret or a config map, this is the best indicator
// we get. We care mostly about jobs, where what we want to see is
// the status go into a good state. For other types, like ReplicaSet
// we don't really do anything to support these as hooks.
log.Printf("Add/Modify event for %s: %v", info.Name, e.Type)
if kind == "Job" {
return waitForJob(e, info.Name)
}
return true, nil
case watch.Deleted:
log.Printf("Deleted event for %s", info.Name)
return true, nil
case watch.Error:
// Handle error and return with an error.
log.Printf("Error event for %s", info.Name)
return true, fmt.Errorf("Failed to deploy %s", info.Name)
default:
return false, nil
}
})
return err
}
// waitForJob is a helper that waits for a job to complete.
//
// This operates on an event returned from a watcher.
func waitForJob(e watch.Event, name string) (bool, error) {
o, ok := e.Object.(*batch.Job)
if !ok {
return true, fmt.Errorf("Expected %s to be a *batch.Job, got %T", name, o)
}
for _, c := range o.Status.Conditions {
if c.Type == batch.JobComplete && c.Status == api.ConditionTrue {
return true, nil
} else if c.Type == batch.JobFailed && c.Status == api.ConditionTrue {
return true, fmt.Errorf("Job failed: %s", c.Reason)
}
}
log.Printf("%s: Jobs active: %d, jobs failed: %d, jobs succeeded: %d", name, o.Status.Active, o.Status.Failed, o.Status.Succeeded)
return false, nil
}
func (c *Client) ensureNamespace(namespace string) error { func (c *Client) ensureNamespace(namespace string) error {
client, err := c.Client() client, err := c.Client()
if err != nil { if err != nil {

@ -14,24 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package lint package lint // import "k8s.io/helm/pkg/lint"
import ( import (
"path/filepath"
"k8s.io/helm/pkg/lint/rules" "k8s.io/helm/pkg/lint/rules"
"k8s.io/helm/pkg/lint/support" "k8s.io/helm/pkg/lint/support"
"os"
"path/filepath"
) )
// All runs all of the available linters on the given base directory. // All runs all of the available linters on the given base directory.
func All(basedir string) []support.Message { func All(basedir string) support.Linter {
// Using abs path to get directory context // Using abs path to get directory context
current, _ := os.Getwd() chartDir, _ := filepath.Abs(basedir)
chartDir := filepath.Join(current, basedir)
linter := support.Linter{ChartDir: chartDir} linter := support.Linter{ChartDir: chartDir}
rules.Chartfile(&linter) rules.Chartfile(&linter)
rules.Values(&linter) rules.Values(&linter)
rules.Templates(&linter) rules.Templates(&linter)
return linter.Messages return linter
} }

@ -17,9 +17,10 @@ limitations under the License.
package lint package lint
import ( import (
"k8s.io/helm/pkg/lint/support"
"strings" "strings"
"k8s.io/helm/pkg/lint/support"
"testing" "testing"
) )
@ -29,7 +30,7 @@ const badYamlFileDir = "rules/testdata/albatross"
const goodChartDir = "rules/testdata/goodone" const goodChartDir = "rules/testdata/goodone"
func TestBadChart(t *testing.T) { func TestBadChart(t *testing.T) {
m := All(badChartDir) m := All(badChartDir).Messages
if len(m) != 4 { if len(m) != 4 {
t.Errorf("Number of errors %v", len(m)) t.Errorf("Number of errors %v", len(m))
t.Errorf("All didn't fail with expected errors, got %#v", m) t.Errorf("All didn't fail with expected errors, got %#v", m)
@ -38,18 +39,18 @@ func TestBadChart(t *testing.T) {
var w, e, e2, e3 bool var w, e, e2, e3 bool
for _, msg := range m { for _, msg := range m {
if msg.Severity == support.WarningSev { if msg.Severity == support.WarningSev {
if strings.Contains(msg.Text, "Templates directory not found") { if strings.Contains(msg.Err.Error(), "directory not found") {
w = true w = true
} }
} }
if msg.Severity == support.ErrorSev { if msg.Severity == support.ErrorSev {
if strings.Contains(msg.Text, "'version' 0.0.0 is less than or equal to 0") { if strings.Contains(msg.Err.Error(), "version 0.0.0 is less than or equal to 0") {
e = true e = true
} }
if strings.Contains(msg.Text, "'name' is required") { if strings.Contains(msg.Err.Error(), "name is required") {
e2 = true e2 = true
} }
if strings.Contains(msg.Text, "'name' and directory do not match") { if strings.Contains(msg.Err.Error(), "directory name (badchartfile) and chart name () must be the same") {
e3 = true e3 = true
} }
} }
@ -60,27 +61,27 @@ func TestBadChart(t *testing.T) {
} }
func TestInvalidYaml(t *testing.T) { func TestInvalidYaml(t *testing.T) {
m := All(badYamlFileDir) m := All(badYamlFileDir).Messages
if len(m) != 1 { if len(m) != 1 {
t.Errorf("All didn't fail with expected errors, got %#v", m) t.Errorf("All didn't fail with expected errors, got %#v", m)
} }
if !strings.Contains(m[0].Text, "deliberateSyntaxError") { if !strings.Contains(m[0].Err.Error(), "deliberateSyntaxError") {
t.Errorf("All didn't have the error for deliberateSyntaxError") t.Errorf("All didn't have the error for deliberateSyntaxError")
} }
} }
func TestBadValues(t *testing.T) { func TestBadValues(t *testing.T) {
m := All(badValuesFileDir) m := All(badValuesFileDir).Messages
if len(m) != 1 { if len(m) != 1 {
t.Errorf("All didn't fail with expected errors, got %#v", m) t.Errorf("All didn't fail with expected errors, got %#v", m)
} }
if !strings.Contains(m[0].Text, "cannot unmarshal") { if !strings.Contains(m[0].Err.Error(), "cannot unmarshal") {
t.Errorf("All didn't have the error for invalid key format: %s", m[0].Text) t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err)
} }
} }
func TestGoodChart(t *testing.T) { func TestGoodChart(t *testing.T) {
m := All(goodChartDir) m := All(goodChartDir).Messages
if len(m) != 0 { if len(m) != 0 {
t.Errorf("All failed but shouldn't have: %#v", m) t.Errorf("All failed but shouldn't have: %#v", m)
} }

@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package rules package rules // import "k8s.io/helm/pkg/lint/rules"
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/Masterminds/semver" "github.com/Masterminds/semver"
"github.com/asaskevich/govalidator" "github.com/asaskevich/govalidator"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/lint/support" "k8s.io/helm/pkg/lint/support"
@ -31,95 +33,83 @@ import (
// Chartfile runs a set of linter rules related to Chart.yaml file // Chartfile runs a set of linter rules related to Chart.yaml file
func Chartfile(linter *support.Linter) { func Chartfile(linter *support.Linter) {
chartPath := filepath.Join(linter.ChartDir, "Chart.yaml") chartFileName := "Chart.yaml"
chartPath := filepath.Join(linter.ChartDir, chartFileName)
linter.RunLinterRule(support.ErrorSev, validateChartYamlFileExistence(chartPath)) linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(chartPath))
linter.RunLinterRule(support.ErrorSev, validateChartYamlNotDirectory(chartPath))
chartFile, err := chartutil.LoadChartfile(chartPath) chartFile, err := chartutil.LoadChartfile(chartPath)
validChartFile := linter.RunLinterRule(support.ErrorSev, validateChartYamlFormat(err)) validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err))
// Guard clause. Following linter rules require a parseable ChartFile // Guard clause. Following linter rules require a parseable ChartFile
if !validChartFile { if !validChartFile {
return return
} }
linter.RunLinterRule(support.ErrorSev, validateChartName(chartFile)) linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartNameDirMatch(linter.ChartDir, chartFile)) linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartNameDirMatch(linter.ChartDir, chartFile))
// Chart metadata // Chart metadata
linter.RunLinterRule(support.ErrorSev, validateChartVersion(chartFile)) linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartEngine(chartFile)) linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartEngine(chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartMaintainer(chartFile)) linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartSources(chartFile)) linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartHome(chartFile))
}
// Auxiliar validation methods
func validateChartYamlFileExistence(chartPath string) (lintError support.LintError) {
_, err := os.Stat(chartPath)
if err != nil {
lintError = fmt.Errorf("Chart.yaml file does not exists")
}
return
} }
func validateChartYamlNotDirectory(chartPath string) (lintError support.LintError) { func validateChartYamlNotDirectory(chartPath string) error {
fi, err := os.Stat(chartPath) fi, err := os.Stat(chartPath)
if err == nil && fi.IsDir() { if err == nil && fi.IsDir() {
lintError = fmt.Errorf("Chart.yaml is a directory") return errors.New("should be a file, not a directory")
} }
return return nil
} }
func validateChartYamlFormat(chartFileError error) (lintError support.LintError) { func validateChartYamlFormat(chartFileError error) error {
if chartFileError != nil { if chartFileError != nil {
lintError = fmt.Errorf("Chart.yaml is malformed: %s", chartFileError.Error()) return fmt.Errorf("unable to parse YAML\n\t%s", chartFileError.Error())
} }
return return nil
} }
func validateChartName(cf *chart.Metadata) (lintError support.LintError) { func validateChartName(cf *chart.Metadata) error {
if cf.Name == "" { if cf.Name == "" {
lintError = fmt.Errorf("Chart.yaml: 'name' is required") return errors.New("name is required")
} }
return return nil
} }
func validateChartNameDirMatch(chartDir string, cf *chart.Metadata) (lintError support.LintError) { func validateChartNameDirMatch(chartDir string, cf *chart.Metadata) error {
if cf.Name != filepath.Base(chartDir) { if cf.Name != filepath.Base(chartDir) {
lintError = fmt.Errorf("Chart.yaml: 'name' and directory do not match") return fmt.Errorf("directory name (%s) and chart name (%s) must be the same", filepath.Base(chartDir), cf.Name)
} }
return return nil
} }
func validateChartVersion(cf *chart.Metadata) (lintError support.LintError) { func validateChartVersion(cf *chart.Metadata) error {
if cf.Version == "" { if cf.Version == "" {
lintError = fmt.Errorf("Chart.yaml: 'version' value is required") return errors.New("version is required")
return
} }
version, err := semver.NewVersion(cf.Version) version, err := semver.NewVersion(cf.Version)
if err != nil { if err != nil {
lintError = fmt.Errorf("Chart.yaml: version '%s' is not a valid SemVer", cf.Version) return fmt.Errorf("version '%s' is not a valid SemVer", cf.Version)
return
} }
c, err := semver.NewConstraint("> 0") c, err := semver.NewConstraint("> 0")
valid, msg := c.Validate(version) valid, msg := c.Validate(version)
if !valid && len(msg) > 0 { if !valid && len(msg) > 0 {
lintError = fmt.Errorf("Chart.yaml: 'version' %v", msg[0]) return fmt.Errorf("version %v", msg[0])
} }
return return nil
} }
func validateChartEngine(cf *chart.Metadata) (lintError support.LintError) { func validateChartEngine(cf *chart.Metadata) error {
if cf.Engine == "" { if cf.Engine == "" {
return return nil
} }
keys := make([]string, 0, len(chart.Metadata_Engine_value)) keys := make([]string, 0, len(chart.Metadata_Engine_value))
@ -131,39 +121,38 @@ func validateChartEngine(cf *chart.Metadata) (lintError support.LintError) {
} }
if str == cf.Engine { if str == cf.Engine {
return return nil
} }
keys = append(keys, str) keys = append(keys, str)
} }
lintError = fmt.Errorf("Chart.yaml: 'engine %v not valid. Valid options are %v", cf.Engine, keys) return fmt.Errorf("engine '%v' not valid. Valid options are %v", cf.Engine, keys)
return
} }
func validateChartMaintainer(cf *chart.Metadata) (lintError support.LintError) { func validateChartMaintainer(cf *chart.Metadata) error {
for _, maintainer := range cf.Maintainers { for _, maintainer := range cf.Maintainers {
if maintainer.Name == "" { if maintainer.Name == "" {
lintError = fmt.Errorf("Chart.yaml: maintainer requires a name") return errors.New("each maintainer requires a name")
} else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) { } else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) {
lintError = fmt.Errorf("Chart.yaml: maintainer invalid email") return fmt.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name)
} }
} }
return return nil
} }
func validateChartSources(cf *chart.Metadata) (lintError support.LintError) { func validateChartSources(cf *chart.Metadata) error {
for _, source := range cf.Sources { for _, source := range cf.Sources {
if source == "" || !govalidator.IsRequestURL(source) { if source == "" || !govalidator.IsRequestURL(source) {
lintError = fmt.Errorf("Chart.yaml: 'source' invalid URL %s", source) return fmt.Errorf("invalid source URL '%s'", source)
} }
} }
return return nil
} }
func validateChartHome(cf *chart.Metadata) (lintError support.LintError) { func validateChartHome(cf *chart.Metadata) error {
if cf.Home != "" && !govalidator.IsRequestURL(cf.Home) { if cf.Home != "" && !govalidator.IsRequestURL(cf.Home) {
lintError = fmt.Errorf("Chart.yaml: 'home' invalid URL %s", cf.Home) return fmt.Errorf("invalid home URL '%s'", cf.Home)
} }
return return nil
} }

@ -28,29 +28,21 @@ import (
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
) )
const badChartDir = "testdata/badchartfile" const (
const goodChartDir = "testdata/goodone" badChartDir = "testdata/badchartfile"
goodChartDir = "testdata/goodone"
)
var badChartFilePath string = filepath.Join(badChartDir, "Chart.yaml") var (
var goodChartFilePath string = filepath.Join(goodChartDir, "Chart.yaml") badChartFilePath = filepath.Join(badChartDir, "Chart.yaml")
var nonExistingChartFilePath string = filepath.Join(os.TempDir(), "Chart.yaml") goodChartFilePath = filepath.Join(goodChartDir, "Chart.yaml")
nonExistingChartFilePath = filepath.Join(os.TempDir(), "Chart.yaml")
)
var badChart, chatLoadRrr = chartutil.LoadChartfile(badChartFilePath) var badChart, chatLoadRrr = chartutil.LoadChartfile(badChartFilePath)
var goodChart, _ = chartutil.LoadChartfile(goodChartFilePath) var goodChart, _ = chartutil.LoadChartfile(goodChartFilePath)
// Validation functions Test // Validation functions Test
func TestValidateChartYamlFileExistence(t *testing.T) {
err := validateChartYamlFileExistence(nonExistingChartFilePath)
if err == nil {
t.Errorf("validateChartYamlFileExistence to return a linter error, got no error")
}
err = validateChartYamlFileExistence(badChartFilePath)
if err != nil {
t.Errorf("validateChartYamlFileExistence to return no error, got a linter error")
}
}
func TestValidateChartYamlNotDirectory(t *testing.T) { func TestValidateChartYamlNotDirectory(t *testing.T) {
_ = os.Mkdir(nonExistingChartFilePath, os.ModePerm) _ = os.Mkdir(nonExistingChartFilePath, os.ModePerm)
defer os.Remove(nonExistingChartFilePath) defer os.Remove(nonExistingChartFilePath)
@ -103,10 +95,10 @@ func TestValidateChartVersion(t *testing.T) {
Version string Version string
ErrorMsg string ErrorMsg string
}{ }{
{"", "'version' value is required"}, {"", "version is required"},
{"0", "0 is less than or equal to 0"}, {"0", "0 is less than or equal to 0"},
{"waps", "is not a valid SemVer"}, {"waps", "'waps' is not a valid SemVer"},
{"-3", "is not a valid SemVer"}, {"-3", "'-3' is not a valid SemVer"},
} }
var successTest = []string{"0.0.1", "0.0.1+build", "0.0.1-beta"} var successTest = []string{"0.0.1", "0.0.1+build", "0.0.1-beta"}
@ -152,9 +144,9 @@ func TestValidateChartMaintainer(t *testing.T) {
Email string Email string
ErrorMsg string ErrorMsg string
}{ }{
{"", "", "maintainer requires a name"}, {"", "", "each maintainer requires a name"},
{"", "test@test.com", "maintainer requires a name"}, {"", "test@test.com", "each maintainer requires a name"},
{"John Snow", "wrongFormatEmail.com", "maintainer invalid email"}, {"John Snow", "wrongFormatEmail.com", "invalid email"},
} }
var successTest = []struct { var successTest = []struct {
@ -188,8 +180,8 @@ func TestValidateChartSources(t *testing.T) {
for _, test := range failTest { for _, test := range failTest {
badChart.Sources = []string{test} badChart.Sources = []string{test}
err := validateChartSources(badChart) err := validateChartSources(badChart)
if err == nil || !strings.Contains(err.Error(), "invalid URL") { if err == nil || !strings.Contains(err.Error(), "invalid source URL") {
t.Errorf("validateChartSources(%s) to return \"invalid URL\", got no error", test) t.Errorf("validateChartSources(%s) to return \"invalid source URL\", got no error", test)
} }
} }
@ -209,8 +201,8 @@ func TestValidateChartHome(t *testing.T) {
for _, test := range failTest { for _, test := range failTest {
badChart.Home = test badChart.Home = test
err := validateChartHome(badChart) err := validateChartHome(badChart)
if err == nil || !strings.Contains(err.Error(), "invalid URL") { if err == nil || !strings.Contains(err.Error(), "invalid home URL") {
t.Errorf("validateChartHome(%s) to return \"invalid URL\", got no error", test) t.Errorf("validateChartHome(%s) to return \"invalid home URL\", got no error", test)
} }
} }
@ -232,15 +224,15 @@ func TestChartfile(t *testing.T) {
t.Errorf("Expected 3 errors, got %d", len(msgs)) t.Errorf("Expected 3 errors, got %d", len(msgs))
} }
if !strings.Contains(msgs[0].Text, "'name' is required") { if !strings.Contains(msgs[0].Err.Error(), "name is required") {
t.Errorf("Unexpected message 0: %s", msgs[0].Text) t.Errorf("Unexpected message 0: %s", msgs[0].Err)
} }
if !strings.Contains(msgs[1].Text, "'name' and directory do not match") { if !strings.Contains(msgs[1].Err.Error(), "directory name (badchartfile) and chart name () must be the same") {
t.Errorf("Unexpected message 1: %s", msgs[1].Text) t.Errorf("Unexpected message 1: %s", msgs[1].Err)
} }
if !strings.Contains(msgs[2].Text, "'version' 0.0.0 is less than or equal to 0") { if !strings.Contains(msgs[2].Err.Error(), "version 0.0.0 is less than or equal to 0") {
t.Errorf("Unexpected message 2: %s", msgs[2].Text) t.Errorf("Unexpected message 2: %s", msgs[2].Err)
} }
} }

@ -18,24 +18,28 @@ package rules
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os"
"path/filepath"
"regexp"
"strings"
"text/template"
"github.com/Masterminds/sprig" "github.com/Masterminds/sprig"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/engine" "k8s.io/helm/pkg/engine"
"k8s.io/helm/pkg/lint/support" "k8s.io/helm/pkg/lint/support"
"k8s.io/helm/pkg/timeconv" "k8s.io/helm/pkg/timeconv"
"os"
"path/filepath"
"regexp"
"strings"
"text/template"
) )
// Templates lints the templates in the Linter.
func Templates(linter *support.Linter) { func Templates(linter *support.Linter) {
templatesPath := filepath.Join(linter.ChartDir, "templates") path := "templates/"
templatesPath := filepath.Join(linter.ChartDir, path)
templatesDirExist := linter.RunLinterRule(support.WarningSev, validateTemplatesDir(templatesPath)) templatesDirExist := linter.RunLinterRule(support.WarningSev, path, validateTemplatesDir(templatesPath))
// Templates directory is optional for now // Templates directory is optional for now
if !templatesDirExist { if !templatesDirExist {
@ -45,26 +49,23 @@ func Templates(linter *support.Linter) {
// Load chart and parse templates, based on tiller/release_server // Load chart and parse templates, based on tiller/release_server
chart, err := chartutil.Load(linter.ChartDir) chart, err := chartutil.Load(linter.ChartDir)
chartLoaded := linter.RunLinterRule(support.ErrorSev, validateNoError(err)) chartLoaded := linter.RunLinterRule(support.ErrorSev, path, err)
if !chartLoaded { if !chartLoaded {
return return
} }
// Based on cmd/tiller/release_server.go options := chartutil.ReleaseOptions{Name: "testRelease", Time: timeconv.Now(), Namespace: "testNamespace"}
overrides := map[string]interface{}{ valuesToRender, err := chartutil.ToRenderValues(chart, chart.Values, options)
"Release": map[string]interface{}{ if err != nil {
"Name": "testRelease", // FIXME: This seems to generate a duplicate, but I can't find where the first
"Service": "Tiller", // error is coming from.
"Time": timeconv.Now(), //linter.RunLinterRule(support.ErrorSev, err)
}, return
"Chart": chart.Metadata,
} }
renderedContentMap, err := engine.New().Render(chart, valuesToRender)
chartValues, _ := chartutil.CoalesceValues(chart, chart.Values, overrides) renderOk := linter.RunLinterRule(support.ErrorSev, path, err)
renderedContentMap, err := engine.New().Render(chart, chartValues)
renderOk := linter.RunLinterRule(support.ErrorSev, validateNoError(err))
if !renderOk { if !renderOk {
return return
@ -79,8 +80,9 @@ func Templates(linter *support.Linter) {
*/ */
for _, template := range chart.Templates { for _, template := range chart.Templates {
fileName, preExecutedTemplate := template.Name, template.Data fileName, preExecutedTemplate := template.Name, template.Data
path = fileName
linter.RunLinterRule(support.ErrorSev, validateAllowedExtension(fileName)) linter.RunLinterRule(support.ErrorSev, path, validateAllowedExtension(fileName))
// We only apply the following lint rules to yaml files // We only apply the following lint rules to yaml files
if filepath.Ext(fileName) != ".yaml" { if filepath.Ext(fileName) != ".yaml" {
@ -88,9 +90,9 @@ func Templates(linter *support.Linter) {
} }
// Check that all the templates have a matching value // Check that all the templates have a matching value
linter.RunLinterRule(support.WarningSev, validateNonMissingValues(fileName, templatesPath, chartValues, preExecutedTemplate)) linter.RunLinterRule(support.WarningSev, path, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate))
linter.RunLinterRule(support.WarningSev, validateQuotes(fileName, string(preExecutedTemplate))) linter.RunLinterRule(support.WarningSev, path, validateQuotes(string(preExecutedTemplate)))
renderedContent := renderedContentMap[fileName] renderedContent := renderedContentMap[fileName]
var yamlStruct K8sYamlStruct var yamlStruct K8sYamlStruct
@ -98,30 +100,30 @@ func Templates(linter *support.Linter) {
// key will be raised as well // key will be raised as well
err := yaml.Unmarshal([]byte(renderedContent), &yamlStruct) err := yaml.Unmarshal([]byte(renderedContent), &yamlStruct)
validYaml := linter.RunLinterRule(support.ErrorSev, validateYamlContent(fileName, err)) validYaml := linter.RunLinterRule(support.ErrorSev, path, validateYamlContent(err))
if !validYaml { if !validYaml {
continue continue
} }
linter.RunLinterRule(support.ErrorSev, validateNoNamespace(fileName, yamlStruct)) linter.RunLinterRule(support.ErrorSev, path, validateNoNamespace(yamlStruct))
} }
} }
// Validation functions // Validation functions
func validateTemplatesDir(templatesPath string) (lintError support.LintError) { func validateTemplatesDir(templatesPath string) error {
if fi, err := os.Stat(templatesPath); err != nil { if fi, err := os.Stat(templatesPath); err != nil {
lintError = fmt.Errorf("Templates directory not found") return errors.New("directory not found")
} else if err == nil && !fi.IsDir() { } else if err == nil && !fi.IsDir() {
lintError = fmt.Errorf("'templates' is not a directory") return errors.New("not a directory")
} }
return return nil
} }
// Validates that go template tags include the quote pipelined function // Validates that go template tags include the quote pipelined function
// i.e {{ .Foo.bar }} -> {{ .Foo.bar | quote }} // i.e {{ .Foo.bar }} -> {{ .Foo.bar | quote }}
// {{ .Foo.bar }}-{{ .Foo.baz }} -> "{{ .Foo.bar }}-{{ .Foo.baz }}" // {{ .Foo.bar }}-{{ .Foo.baz }} -> "{{ .Foo.bar }}-{{ .Foo.baz }}"
func validateQuotes(templateName string, templateContent string) (lintError support.LintError) { func validateQuotes(templateContent string) error {
// {{ .Foo.bar }} // {{ .Foo.bar }}
r, _ := regexp.Compile(`(?m)(:|-)\s+{{[\w|\.|\s|\']+}}\s*$`) r, _ := regexp.Compile(`(?m)(:|-)\s+{{[\w|\.|\s|\']+}}\s*$`)
functions := r.FindAllString(templateContent, -1) functions := r.FindAllString(templateContent, -1)
@ -129,8 +131,7 @@ func validateQuotes(templateName string, templateContent string) (lintError supp
for _, str := range functions { for _, str := range functions {
if match, _ := regexp.MatchString("quote", str); !match { if match, _ := regexp.MatchString("quote", str); !match {
result := strings.Replace(str, "}}", " | quote }}", -1) result := strings.Replace(str, "}}", " | quote }}", -1)
lintError = fmt.Errorf("templates: \"%s\". Wrap your substitution functions in quotes or use the sprig \"quote\" function: %s -> %s", templateName, str, result) return fmt.Errorf("wrap substitution functions in quotes or use the sprig \"quote\" function: %s -> %s", str, result)
return
} }
} }
@ -140,29 +141,27 @@ func validateQuotes(templateName string, templateContent string) (lintError supp
for _, str := range functions { for _, str := range functions {
result := strings.Replace(str, str, fmt.Sprintf("\"%s\"", str), -1) result := strings.Replace(str, str, fmt.Sprintf("\"%s\"", str), -1)
lintError = fmt.Errorf("templates: \"%s\". Wrap your substitution functions in quotes: %s -> %s", templateName, str, result) return fmt.Errorf("wrap substitution functions in quotes: %s -> %s", str, result)
return
} }
return return nil
} }
func validateAllowedExtension(fileName string) (lintError support.LintError) { func validateAllowedExtension(fileName string) error {
ext := filepath.Ext(fileName) ext := filepath.Ext(fileName)
validExtensions := []string{".yaml", ".tpl"} validExtensions := []string{".yaml", ".tpl"}
for _, b := range validExtensions { for _, b := range validExtensions {
if b == ext { if b == ext {
return return nil
} }
} }
lintError = fmt.Errorf("templates: \"%s\" needs to use .yaml or .tpl extensions", fileName) return fmt.Errorf("file extension '%s' not valid. Valid extensions are .yaml or .tpl", ext)
return
} }
// validateNonMissingValues checks that all the {{}} functions returns a non empty value (<no value> or "") // validateNoMissingValues checks that all the {{}} functions returns a non empty value (<no value> or "")
// and return an error otherwise. // and return an error otherwise.
func validateNonMissingValues(fileName string, templatesPath string, chartValues chartutil.Values, templateContent []byte) (lintError support.LintError) { func validateNoMissingValues(templatesPath string, chartValues chartutil.Values, templateContent []byte) error {
// 1 - Load Main and associated templates // 1 - Load Main and associated templates
// Main template that we will parse dynamically // Main template that we will parse dynamically
tmpl := template.New("tpl").Funcs(sprig.TxtFuncMap()) tmpl := template.New("tpl").Funcs(sprig.TxtFuncMap())
@ -189,8 +188,7 @@ func validateNonMissingValues(fileName string, templatesPath string, chartValues
for _, str := range functions { for _, str := range functions {
newtmpl, err := tmpl.Parse(str) newtmpl, err := tmpl.Parse(str)
if err != nil { if err != nil {
lintError = fmt.Errorf("templates: %s", err.Error()) return err
return
} }
err = newtmpl.ExecuteTemplate(&buf, "tpl", chartValues) err = newtmpl.ExecuteTemplate(&buf, "tpl", chartValues)
@ -208,32 +206,26 @@ func validateNonMissingValues(fileName string, templatesPath string, chartValues
} }
if len(emptyValues) > 0 { if len(emptyValues) > 0 {
lintError = fmt.Errorf("templates: %s: The following functions are not returning any value %v", fileName, emptyValues) return fmt.Errorf("these substitution functions are returning no value: %v", emptyValues)
}
return
}
func validateNoError(readError error) (lintError support.LintError) {
if readError != nil {
lintError = fmt.Errorf("templates: %s", readError.Error())
} }
return return nil
} }
func validateYamlContent(filePath string, err error) (lintError support.LintError) { func validateYamlContent(err error) error {
if err != nil { if err != nil {
lintError = fmt.Errorf("templates: \"%s\". Wrong YAML content.", filePath) return fmt.Errorf("unable to parse YAML\n\t%s", err)
} }
return return nil
} }
func validateNoNamespace(filePath string, yamlStruct K8sYamlStruct) (lintError support.LintError) { func validateNoNamespace(yamlStruct K8sYamlStruct) error {
if yamlStruct.Metadata.Namespace != "" { if yamlStruct.Metadata.Namespace != "" {
lintError = fmt.Errorf("templates: \"%s\". namespace option is currently NOT supported.", filePath) return errors.New("namespace option is currently NOT supported")
} }
return return nil
} }
// K8sYamlStruct stubs a Kubernetes YAML file.
// Need to access for now to Namespace only // Need to access for now to Namespace only
type K8sYamlStruct struct { type K8sYamlStruct struct {
Metadata struct { Metadata struct {

@ -17,9 +17,12 @@ limitations under the License.
package rules package rules
import ( import (
"k8s.io/helm/pkg/lint/support" "os"
"path/filepath"
"strings" "strings"
"testing" "testing"
"k8s.io/helm/pkg/lint/support"
) )
const templateTestBasedir = "./testdata/albatross" const templateTestBasedir = "./testdata/albatross"
@ -28,8 +31,8 @@ func TestValidateAllowedExtension(t *testing.T) {
var failTest = []string{"/foo", "/test.yml", "/test.toml", "test.yml"} var failTest = []string{"/foo", "/test.yml", "/test.toml", "test.yml"}
for _, test := range failTest { for _, test := range failTest {
err := validateAllowedExtension(test) err := validateAllowedExtension(test)
if err == nil || !strings.Contains(err.Error(), "needs to use .yaml or .tpl extension") { if err == nil || !strings.Contains(err.Error(), "Valid extensions are .yaml or .tpl") {
t.Errorf("validateAllowedExtension('%s') to return \"needs to use .yaml or .tpl extension\", got no error", test) t.Errorf("validateAllowedExtension('%s') to return \"Valid extensions are .yaml or .tpl\", got no error", test)
} }
} }
var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml"} var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml"}
@ -46,7 +49,7 @@ func TestValidateQuotes(t *testing.T) {
var failTest = []string{"foo: {{.Release.Service }}", "foo: {{.Release.Service }}", "- {{.Release.Service }}", "foo: {{default 'Never' .restart_policy}}", "- {{.Release.Service }} "} var failTest = []string{"foo: {{.Release.Service }}", "foo: {{.Release.Service }}", "- {{.Release.Service }}", "foo: {{default 'Never' .restart_policy}}", "- {{.Release.Service }} "}
for _, test := range failTest { for _, test := range failTest {
err := validateQuotes("testTemplate.yaml", test) err := validateQuotes(test)
if err == nil || !strings.Contains(err.Error(), "use the sprig \"quote\" function") { if err == nil || !strings.Contains(err.Error(), "use the sprig \"quote\" function") {
t.Errorf("validateQuotes('%s') to return \"use the sprig \"quote\" function:\", got no error.", test) t.Errorf("validateQuotes('%s') to return \"use the sprig \"quote\" function:\", got no error.", test)
} }
@ -55,7 +58,7 @@ func TestValidateQuotes(t *testing.T) {
var successTest = []string{"foo: {{.Release.Service | quote }}", "foo: {{.Release.Service | quote }}", "- {{.Release.Service | quote }}", "foo: {{default 'Never' .restart_policy | quote }}", "foo: \"{{ .Release.Service }}\"", "foo: \"{{ .Release.Service }} {{ .Foo.Bar }}\"", "foo: \"{{ default 'Never' .Release.Service }} {{ .Foo.Bar }}\"", "foo: {{.Release.Service | squote }}"} var successTest = []string{"foo: {{.Release.Service | quote }}", "foo: {{.Release.Service | quote }}", "- {{.Release.Service | quote }}", "foo: {{default 'Never' .restart_policy | quote }}", "foo: \"{{ .Release.Service }}\"", "foo: \"{{ .Release.Service }} {{ .Foo.Bar }}\"", "foo: \"{{ default 'Never' .Release.Service }} {{ .Foo.Bar }}\"", "foo: {{.Release.Service | squote }}"}
for _, test := range successTest { for _, test := range successTest {
err := validateQuotes("testTemplate.yaml", test) err := validateQuotes(test)
if err != nil { if err != nil {
t.Errorf("validateQuotes('%s') to return not error and got \"%s\"", test, err.Error()) t.Errorf("validateQuotes('%s') to return not error and got \"%s\"", test, err.Error())
} }
@ -65,15 +68,15 @@ func TestValidateQuotes(t *testing.T) {
failTest = []string{"foo: {{.Release.Service }}-{{ .Release.Bar }}", "foo: {{.Release.Service }} {{ .Release.Bar }}", "- {{.Release.Service }}-{{ .Release.Bar }}", "- {{.Release.Service }}-{{ .Release.Bar }} {{ .Release.Baz }}", "foo: {{.Release.Service | default }}-{{ .Release.Bar }}"} failTest = []string{"foo: {{.Release.Service }}-{{ .Release.Bar }}", "foo: {{.Release.Service }} {{ .Release.Bar }}", "- {{.Release.Service }}-{{ .Release.Bar }}", "- {{.Release.Service }}-{{ .Release.Bar }} {{ .Release.Baz }}", "foo: {{.Release.Service | default }}-{{ .Release.Bar }}"}
for _, test := range failTest { for _, test := range failTest {
err := validateQuotes("testTemplate.yaml", test) err := validateQuotes(test)
if err == nil || !strings.Contains(err.Error(), "Wrap your substitution functions in quotes") { if err == nil || !strings.Contains(err.Error(), "wrap substitution functions in quotes") {
t.Errorf("validateQuotes('%s') to return \"Wrap your substitution functions in quotes\", got no error", test) t.Errorf("validateQuotes('%s') to return \"wrap substitution functions in quotes\", got no error", test)
} }
} }
} }
func TestTemplate(t *testing.T) { func TestTemplateParsing(t *testing.T) {
linter := support.Linter{ChartDir: templateTestBasedir} linter := support.Linter{ChartDir: templateTestBasedir}
Templates(&linter) Templates(&linter)
res := linter.Messages res := linter.Messages
@ -82,7 +85,26 @@ func TestTemplate(t *testing.T) {
t.Fatalf("Expected one error, got %d, %v", len(res), res) t.Fatalf("Expected one error, got %d, %v", len(res), res)
} }
if !strings.Contains(res[0].Text, "deliberateSyntaxError") { if !strings.Contains(res[0].Err.Error(), "deliberateSyntaxError") {
t.Errorf("Unexpected error: %s", res[0]) t.Errorf("Unexpected error: %s", res[0])
} }
} }
var wrongTemplatePath = filepath.Join(templateTestBasedir, "templates", "fail.yaml")
var ignoredTemplatePath = filepath.Join(templateTestBasedir, "fail.yaml.ignored")
// Test a template with all the existing features:
// namespaces, partial templates
func TestTemplateIntegrationHappyPath(t *testing.T) {
// Rename file so it gets ignored by the linter
os.Rename(wrongTemplatePath, ignoredTemplatePath)
defer os.Rename(ignoredTemplatePath, wrongTemplatePath)
linter := support.Linter{ChartDir: templateTestBasedir}
Templates(&linter)
res := linter.Messages
if len(res) != 0 {
t.Fatalf("Expected no error, got %d, %v", len(res), res)
}
}

@ -0,0 +1,16 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{define "name"}}{{default "nginx" .Values.nameOverride | trunc 24 }}{{end}}
{{/*
Create a default fully qualified app name.
We truncate at 24 chars because some Kubernetes name fields are limited to this
(by the DNS naming spec).
*/}}
{{define "fullname"}}
{{- $name := default "nginx" .Values.nameOverride -}}
{{printf "%s-%s" .Release.Name $name | trunc 24 -}}
{{end}}

@ -1,2 +0,0 @@
metadata:
name: {{.name | default "foo" | title}}

@ -0,0 +1,18 @@
# This is a service gateway to the replica set created by the deployment.
# Take a look at the deployment.yaml for general notes about this chart.
apiVersion: v1
kind: Service
metadata:
name: "{{ .Values.name }}"
labels:
heritage: {{ .Release.Service | quote }}
release: {{ .Release.Name | quote }}
chart: "{{.Chart.Name}}-{{.Chart.Version}}"
spec:
ports:
- port: {{default 80 .Values.httpPort | quote}}
targetPort: 80
protocol: TCP
name: http
selector:
app: {{template "fullname" .}}

@ -18,36 +18,38 @@ package rules
import ( import (
"fmt" "fmt"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/lint/support"
"os" "os"
"path/filepath" "path/filepath"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/lint/support"
) )
// Values lints a chart's values.yaml file. // Values lints a chart's values.yaml file.
func Values(linter *support.Linter) { func Values(linter *support.Linter) {
vf := filepath.Join(linter.ChartDir, "values.yaml") file := "values.yaml"
fileExists := linter.RunLinterRule(support.InfoSev, validateValuesFileExistence(linter, vf)) vf := filepath.Join(linter.ChartDir, file)
fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(linter, vf))
if !fileExists { if !fileExists {
return return
} }
linter.RunLinterRule(support.ErrorSev, validateValuesFile(linter, vf)) linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(linter, vf))
} }
func validateValuesFileExistence(linter *support.Linter, valuesPath string) (lintError support.LintError) { func validateValuesFileExistence(linter *support.Linter, valuesPath string) error {
_, err := os.Stat(valuesPath) _, err := os.Stat(valuesPath)
if err != nil { if err != nil {
lintError = fmt.Errorf("values.yaml file does not exists") return fmt.Errorf("file does not exist")
} }
return return nil
} }
func validateValuesFile(linter *support.Linter, valuesPath string) (lintError support.LintError) { func validateValuesFile(linter *support.Linter, valuesPath string) error {
_, err := chartutil.ReadValuesFile(valuesPath) _, err := chartutil.ReadValuesFile(valuesPath)
if err != nil { if err != nil {
lintError = fmt.Errorf("values.yaml is malformed: %s", err.Error()) return fmt.Errorf("unable to parse YAML\n\t%s", err)
} }
return return nil
} }

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
/*Package lint contains tools for linting charts. /*Package support contains tools for linting charts.
Linting is the process of testing charts for errors or warnings regarding Linting is the process of testing charts for errors or warnings regarding
formatting, compilation, or standards compliance. formatting, compilation, or standards compliance.
*/ */
package support package support // import "k8s.io/helm/pkg/lint/support"

@ -33,39 +33,44 @@ const (
// sev matches the *Sev states. // sev matches the *Sev states.
var sev = []string{"UNKNOWN", "INFO", "WARNING", "ERROR"} var sev = []string{"UNKNOWN", "INFO", "WARNING", "ERROR"}
// Message is a linting output message // Linter encapsulates a linting run of a particular chart.
type Linter struct {
Messages []Message
// The highest severity of all the failing lint rules
HighestSeverity int
ChartDir string
}
// Message describes an error encountered while linting.
type Message struct { type Message struct {
// Severity is one of the *Sev constants // Severity is one of the *Sev constants
Severity int Severity int
// Text contains the message text Path string
Text string Err error
} }
type Linter struct { func (m Message) Error() string {
Messages []Message return fmt.Sprintf("[%s] %s: %s", sev[m.Severity], m.Path, m.Err.Error())
ChartDir string
}
type LintError interface {
error
} }
// String prints a string representation of this Message. // NewMessage creates a new Message struct
// func NewMessage(severity int, path string, err error) Message {
// Implements fmt.Stringer. return Message{Severity: severity, Path: path, Err: err}
func (m Message) String() string {
return fmt.Sprintf("[%s] %s", sev[m.Severity], m.Text)
} }
// Returns true if the validation passed // RunLinterRule returns true if the validation passed
func (l *Linter) RunLinterRule(severity int, lintError LintError) bool { func (l *Linter) RunLinterRule(severity int, path string, err error) bool {
// severity is out of bound // severity is out of bound
if severity < 0 || severity >= len(sev) { if severity < 0 || severity >= len(sev) {
return false return false
} }
if lintError != nil { if err != nil {
l.Messages = append(l.Messages, Message{Text: lintError.Error(), Severity: severity}) l.Messages = append(l.Messages, NewMessage(severity, path, err))
if severity > l.HighestSeverity {
l.HighestSeverity = severity
}
} }
return lintError == nil return err == nil
} }

@ -17,56 +17,63 @@ limitations under the License.
package support package support
import ( import (
"fmt" "errors"
"testing" "testing"
) )
var linter Linter = Linter{} var linter = Linter{}
var lintError LintError = fmt.Errorf("Foobar") var errLint = errors.New("lint failed")
func TestRunLinterRule(t *testing.T) { func TestRunLinterRule(t *testing.T) {
var tests = []struct { var tests = []struct {
Severity int Severity int
LintError error LintError error
ExpectedMessages int ExpectedMessages int
ExpectedReturn bool ExpectedReturn bool
ExpectedHighestSeverity int
}{ }{
{ErrorSev, lintError, 1, false}, {InfoSev, errLint, 1, false, InfoSev},
{WarningSev, lintError, 2, false}, {WarningSev, errLint, 2, false, WarningSev},
{InfoSev, lintError, 3, false}, {ErrorSev, errLint, 3, false, ErrorSev},
// No error so it returns true // No error so it returns true
{ErrorSev, nil, 3, true}, {ErrorSev, nil, 3, true, ErrorSev},
// Retains highest severity
{InfoSev, errLint, 4, false, ErrorSev},
// Invalid severity values // Invalid severity values
{4, lintError, 3, false}, {4, errLint, 4, false, ErrorSev},
{22, lintError, 3, false}, {22, errLint, 4, false, ErrorSev},
{-1, lintError, 3, false}, {-1, errLint, 4, false, ErrorSev},
} }
for _, test := range tests { for _, test := range tests {
isValid := linter.RunLinterRule(test.Severity, test.LintError) isValid := linter.RunLinterRule(test.Severity, "chart", test.LintError)
if len(linter.Messages) != test.ExpectedMessages { if len(linter.Messages) != test.ExpectedMessages {
t.Errorf("RunLinterRule(%d, %v), linter.Messages should have now %d message, we got %d", test.Severity, test.LintError, test.ExpectedMessages, len(linter.Messages)) t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.Messages should now have %d message, we got %d", test.Severity, test.LintError, test.ExpectedMessages, len(linter.Messages))
}
if linter.HighestSeverity != test.ExpectedHighestSeverity {
t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.HighestSeverity should be %d, we got %d", test.Severity, test.LintError, test.ExpectedHighestSeverity, linter.HighestSeverity)
} }
if isValid != test.ExpectedReturn { if isValid != test.ExpectedReturn {
t.Errorf("RunLinterRule(%d, %v), should have returned %t but returned %t", test.Severity, test.LintError, test.ExpectedReturn, isValid) t.Errorf("RunLinterRule(%d, \"chart\", %v), should have returned %t but returned %t", test.Severity, test.LintError, test.ExpectedReturn, isValid)
} }
} }
} }
func TestMessage(t *testing.T) { func TestMessage(t *testing.T) {
m := Message{ErrorSev, "Foo"} m := Message{ErrorSev, "Chart.yaml", errors.New("Foo")}
if m.String() != "[ERROR] Foo" { if m.Error() != "[ERROR] Chart.yaml: Foo" {
t.Errorf("Unexpected output: %s", m.String()) t.Errorf("Unexpected output: %s", m.Error())
} }
m = Message{WarningSev, "Bar"} m = Message{WarningSev, "templates/", errors.New("Bar")}
if m.String() != "[WARNING] Bar" { if m.Error() != "[WARNING] templates/: Bar" {
t.Errorf("Unexpected output: %s", m.String()) t.Errorf("Unexpected output: %s", m.Error())
} }
m = Message{InfoSev, "FooBar"} m = Message{InfoSev, "templates/rc.yaml", errors.New("FooBar")}
if m.String() != "[INFO] FooBar" { if m.Error() != "[INFO] templates/rc.yaml: FooBar" {
t.Errorf("Unexpected output: %s", m.String()) t.Errorf("Unexpected output: %s", m.Error())
} }
} }

@ -0,0 +1,125 @@
// Code generated by protoc-gen-go.
// source: hapi/release/hook.proto
// DO NOT EDIT!
/*
Package release is a generated protocol buffer package.
It is generated from these files:
hapi/release/hook.proto
hapi/release/info.proto
hapi/release/release.proto
hapi/release/status.proto
It has these top-level messages:
Hook
Info
Release
Status
*/
package release
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import google_protobuf "github.com/golang/protobuf/ptypes/timestamp"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
const _ = proto.ProtoPackageIsVersion1
type Hook_Event int32
const (
Hook_UNKNOWN Hook_Event = 0
Hook_PRE_INSTALL Hook_Event = 1
Hook_POST_INSTALL Hook_Event = 2
Hook_PRE_DELETE Hook_Event = 3
Hook_POST_DELETE Hook_Event = 4
Hook_PRE_UPGRADE Hook_Event = 5
Hook_POST_UPGRADE Hook_Event = 6
)
var Hook_Event_name = map[int32]string{
0: "UNKNOWN",
1: "PRE_INSTALL",
2: "POST_INSTALL",
3: "PRE_DELETE",
4: "POST_DELETE",
5: "PRE_UPGRADE",
6: "POST_UPGRADE",
}
var Hook_Event_value = map[string]int32{
"UNKNOWN": 0,
"PRE_INSTALL": 1,
"POST_INSTALL": 2,
"PRE_DELETE": 3,
"POST_DELETE": 4,
"PRE_UPGRADE": 5,
"POST_UPGRADE": 6,
}
func (x Hook_Event) String() string {
return proto.EnumName(Hook_Event_name, int32(x))
}
func (Hook_Event) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0, 0} }
// Hook defines a hook object.
type Hook struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
// Kind is the Kubernetes kind.
Kind string `protobuf:"bytes,2,opt,name=kind" json:"kind,omitempty"`
// Path is the chart-relative path to the template.
Path string `protobuf:"bytes,3,opt,name=path" json:"path,omitempty"`
// Manifest is the manifest contents.
Manifest string `protobuf:"bytes,4,opt,name=manifest" json:"manifest,omitempty"`
// Events are the events that this hook fires on.
Events []Hook_Event `protobuf:"varint,5,rep,name=events,enum=hapi.release.Hook_Event" json:"events,omitempty"`
// LastRun indicates the date/time this was last run.
LastRun *google_protobuf.Timestamp `protobuf:"bytes,6,opt,name=last_run,json=lastRun" json:"last_run,omitempty"`
}
func (m *Hook) Reset() { *m = Hook{} }
func (m *Hook) String() string { return proto.CompactTextString(m) }
func (*Hook) ProtoMessage() {}
func (*Hook) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *Hook) GetLastRun() *google_protobuf.Timestamp {
if m != nil {
return m.LastRun
}
return nil
}
func init() {
proto.RegisterType((*Hook)(nil), "hapi.release.Hook")
proto.RegisterEnum("hapi.release.Hook_Event", Hook_Event_name, Hook_Event_value)
}
var fileDescriptor0 = []byte{
// 300 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0x5f, 0x4f, 0xc2, 0x30,
0x14, 0xc5, 0x05, 0xc6, 0x80, 0x0b, 0xc1, 0xa5, 0x2f, 0x36, 0xbc, 0x48, 0x78, 0xe2, 0xa9, 0x33,
0x18, 0x3f, 0x00, 0x86, 0x46, 0x8d, 0x64, 0x90, 0x32, 0x62, 0xe2, 0x0b, 0x29, 0xb1, 0xc0, 0x02,
0x6b, 0x17, 0xd6, 0xf9, 0xe2, 0xd7, 0xf3, 0x83, 0xb9, 0x76, 0x7f, 0xe2, 0xdb, 0xed, 0xef, 0x9c,
0x7b, 0x7a, 0x0f, 0xdc, 0x9d, 0x78, 0x12, 0xf9, 0x57, 0x71, 0x11, 0x3c, 0x15, 0xfe, 0x49, 0xa9,
0x33, 0x49, 0xae, 0x4a, 0x2b, 0x34, 0x30, 0x02, 0x29, 0x85, 0xd1, 0xfd, 0x51, 0xa9, 0xe3, 0x45,
0xf8, 0x56, 0xdb, 0x67, 0x07, 0x5f, 0x47, 0xb1, 0x48, 0x35, 0x8f, 0x93, 0xc2, 0x3e, 0xf9, 0x6d,
0x82, 0xf3, 0x9a, 0x6f, 0x23, 0x04, 0x8e, 0xe4, 0xb1, 0xc0, 0x8d, 0x71, 0x63, 0xda, 0x63, 0x76,
0x36, 0xec, 0x1c, 0xc9, 0x2f, 0xdc, 0x2c, 0x98, 0x99, 0x0d, 0x4b, 0xb8, 0x3e, 0xe1, 0x56, 0xc1,
0xcc, 0x8c, 0x46, 0xd0, 0x8d, 0xb9, 0x8c, 0x0e, 0x79, 0x32, 0x76, 0x2c, 0xaf, 0xdf, 0xe8, 0x01,
0x5c, 0xf1, 0x2d, 0xa4, 0x4e, 0x71, 0x7b, 0xdc, 0x9a, 0x0e, 0x67, 0x98, 0xfc, 0x3f, 0x90, 0x98,
0xbf, 0x09, 0x35, 0x06, 0x56, 0xfa, 0xd0, 0x13, 0x74, 0x2f, 0x3c, 0xd5, 0xbb, 0x6b, 0x26, 0xb1,
0x9b, 0xa7, 0xf5, 0x67, 0x23, 0x52, 0xd4, 0x20, 0x55, 0x0d, 0x12, 0x56, 0x35, 0x58, 0xc7, 0x78,
0x59, 0x26, 0x27, 0x3f, 0xd0, 0xb6, 0x39, 0xa8, 0x0f, 0x9d, 0x6d, 0xf0, 0x1e, 0xac, 0x3e, 0x02,
0xef, 0x06, 0xdd, 0x42, 0x7f, 0xcd, 0xe8, 0xee, 0x2d, 0xd8, 0x84, 0xf3, 0xe5, 0xd2, 0x6b, 0x20,
0x0f, 0x06, 0xeb, 0xd5, 0x26, 0xac, 0x49, 0x13, 0x0d, 0x01, 0x8c, 0x65, 0x41, 0x97, 0x34, 0xa4,
0x5e, 0xcb, 0xae, 0x18, 0x47, 0x09, 0x9c, 0x2a, 0x63, 0xbb, 0x7e, 0x61, 0xf3, 0x05, 0xf5, 0xda,
0x75, 0x46, 0x45, 0xdc, 0xe7, 0xde, 0x67, 0xa7, 0x6c, 0xb4, 0x77, 0xed, 0x91, 0x8f, 0x7f, 0x01,
0x00, 0x00, 0xff, 0xff, 0x16, 0x64, 0x61, 0x76, 0xa2, 0x01, 0x00, 0x00,
}

@ -2,19 +2,6 @@
// source: hapi/release/info.proto // source: hapi/release/info.proto
// DO NOT EDIT! // DO NOT EDIT!
/*
Package release is a generated protocol buffer package.
It is generated from these files:
hapi/release/info.proto
hapi/release/release.proto
hapi/release/status.proto
It has these top-level messages:
Info
Release
Status
*/
package release package release
import proto "github.com/golang/protobuf/proto" import proto "github.com/golang/protobuf/proto"
@ -27,10 +14,6 @@ var _ = proto.Marshal
var _ = fmt.Errorf var _ = fmt.Errorf
var _ = math.Inf var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
const _ = proto.ProtoPackageIsVersion1
// Info describes release information. // Info describes release information.
type Info struct { type Info struct {
Status *Status `protobuf:"bytes,1,opt,name=status" json:"status,omitempty"` Status *Status `protobuf:"bytes,1,opt,name=status" json:"status,omitempty"`
@ -43,7 +26,7 @@ type Info struct {
func (m *Info) Reset() { *m = Info{} } func (m *Info) Reset() { *m = Info{} }
func (m *Info) String() string { return proto.CompactTextString(m) } func (m *Info) String() string { return proto.CompactTextString(m) }
func (*Info) ProtoMessage() {} func (*Info) ProtoMessage() {}
func (*Info) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } func (*Info) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} }
func (m *Info) GetStatus() *Status { func (m *Info) GetStatus() *Status {
if m != nil { if m != nil {
@ -77,7 +60,7 @@ func init() {
proto.RegisterType((*Info)(nil), "hapi.release.Info") proto.RegisterType((*Info)(nil), "hapi.release.Info")
} }
var fileDescriptor0 = []byte{ var fileDescriptor1 = []byte{
// 208 bytes of a gzipped FileDescriptorProto // 208 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0x48, 0x2c, 0xc8, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0x48, 0x2c, 0xc8,
0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0xcf, 0xcc, 0x4b, 0xcb, 0xd7, 0x2b, 0x28, 0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0xcf, 0xcc, 0x4b, 0xcb, 0xd7, 0x2b, 0x28,

@ -29,12 +29,16 @@ type Release struct {
Config *hapi_chart.Config `protobuf:"bytes,4,opt,name=config" json:"config,omitempty"` Config *hapi_chart.Config `protobuf:"bytes,4,opt,name=config" json:"config,omitempty"`
// Manifest is the string representation of the rendered template. // Manifest is the string representation of the rendered template.
Manifest string `protobuf:"bytes,5,opt,name=manifest" json:"manifest,omitempty"` Manifest string `protobuf:"bytes,5,opt,name=manifest" json:"manifest,omitempty"`
// Hooks are all of the hooks declared for this release.
Hooks []*Hook `protobuf:"bytes,6,rep,name=hooks" json:"hooks,omitempty"`
// Version is an int32 which represents the version of the release.
Version int32 `protobuf:"varint,7,opt,name=version" json:"version,omitempty"`
} }
func (m *Release) Reset() { *m = Release{} } func (m *Release) Reset() { *m = Release{} }
func (m *Release) String() string { return proto.CompactTextString(m) } func (m *Release) String() string { return proto.CompactTextString(m) }
func (*Release) ProtoMessage() {} func (*Release) ProtoMessage() {}
func (*Release) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} } func (*Release) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} }
func (m *Release) GetInfo() *Info { func (m *Release) GetInfo() *Info {
if m != nil { if m != nil {
@ -57,23 +61,32 @@ func (m *Release) GetConfig() *hapi_chart.Config {
return nil return nil
} }
func (m *Release) GetHooks() []*Hook {
if m != nil {
return m.Hooks
}
return nil
}
func init() { func init() {
proto.RegisterType((*Release)(nil), "hapi.release.Release") proto.RegisterType((*Release)(nil), "hapi.release.Release")
} }
var fileDescriptor1 = []byte{ var fileDescriptor2 = []byte{
// 200 bytes of a gzipped FileDescriptorProto // 239 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x54, 0x8f, 0xbd, 0x0e, 0x82, 0x30, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x90, 0x3f, 0x4f, 0x03, 0x31,
0x14, 0x85, 0x83, 0xf2, 0x23, 0xd5, 0xc5, 0x3b, 0x68, 0xc3, 0x64, 0x1c, 0xd4, 0x38, 0x94, 0x44, 0x0c, 0xc5, 0x75, 0xf4, 0xfe, 0x50, 0xc3, 0x82, 0x07, 0xb0, 0x6e, 0xaa, 0x18, 0xa0, 0x62, 0x48,
0xdf, 0x40, 0x27, 0xd7, 0x8e, 0x6e, 0x95, 0x14, 0x69, 0x22, 0x2d, 0x01, 0x9e, 0xcd, 0xe7, 0x93, 0x25, 0xf8, 0x06, 0xb0, 0xc0, 0x9a, 0x91, 0x2d, 0x54, 0x39, 0x2e, 0x82, 0x26, 0x55, 0x72, 0xe2,
0xf6, 0x56, 0x83, 0xcb, 0x85, 0xde, 0xef, 0xcb, 0xe9, 0x29, 0xc9, 0x2a, 0xd1, 0xa8, 0xbc, 0x95, 0xc3, 0x33, 0x91, 0xc4, 0x29, 0xba, 0xc2, 0xe2, 0xc4, 0xfe, 0xbd, 0xbc, 0x3c, 0x19, 0xfa, 0x51,
0x2f, 0x29, 0x3a, 0xf9, 0xfd, 0xb2, 0xa6, 0x35, 0xbd, 0x81, 0x85, 0x65, 0xcc, 0xef, 0xb2, 0xf5, 0xed, 0xcd, 0xc6, 0xeb, 0x4f, 0xad, 0x82, 0x3e, 0x9c, 0x62, 0xef, 0xdd, 0xe4, 0xf0, 0x3c, 0x31,
0x9f, 0xa9, 0x74, 0x69, 0x50, 0xf3, 0xa0, 0xa8, 0x44, 0xdb, 0xe7, 0x85, 0xd1, 0xa5, 0x7a, 0x7a, 0x51, 0x66, 0xfd, 0xd5, 0x91, 0x72, 0x74, 0xee, 0x83, 0x65, 0x7f, 0x80, 0xb1, 0x83, 0x3b, 0x02,
0xb0, 0x1a, 0x03, 0x3b, 0x71, 0xbf, 0x7d, 0x07, 0x24, 0xe1, 0x98, 0x03, 0x40, 0x42, 0x2d, 0x6a, 0xdb, 0x51, 0xf9, 0x69, 0xb3, 0x75, 0x76, 0x30, 0xef, 0x05, 0x5c, 0xce, 0x41, 0xaa, 0x3c, 0xbf,
0x49, 0x83, 0x4d, 0x70, 0x48, 0xb9, 0xfb, 0x87, 0x1d, 0x09, 0x6d, 0x3c, 0x9d, 0x0c, 0xbb, 0xf9, 0xfe, 0xae, 0xa0, 0x93, 0xec, 0x83, 0x08, 0xb5, 0x55, 0x3b, 0x4d, 0xd5, 0xaa, 0x5a, 0x2f, 0x65,
0x09, 0xd8, 0xb8, 0x06, 0xbb, 0x0d, 0x84, 0x3b, 0x0e, 0x7b, 0x12, 0xb9, 0x58, 0x3a, 0x75, 0xe2, 0xbe, 0xe3, 0x0d, 0xd4, 0xc9, 0x9e, 0x4e, 0xe2, 0xec, 0xec, 0x1e, 0xc5, 0x3c, 0x9f, 0x78, 0x89,
0x12, 0x45, 0xbc, 0xe9, 0x6a, 0x27, 0x47, 0x0e, 0x47, 0x12, 0x63, 0x31, 0x1a, 0x8e, 0x23, 0xbd, 0x44, 0x66, 0x8e, 0xb7, 0xd0, 0x64, 0x5b, 0x5a, 0x64, 0xe1, 0x05, 0x0b, 0xf9, 0xa7, 0xa7, 0x54,
0xe9, 0x08, 0xf7, 0x06, 0x64, 0x64, 0x56, 0x0b, 0xad, 0x4a, 0xd9, 0xf5, 0x34, 0x72, 0xa5, 0x7e, 0x25, 0x73, 0xbc, 0x83, 0x96, 0x83, 0x51, 0x3d, 0xb7, 0x2c, 0xca, 0x4c, 0x64, 0x51, 0x60, 0x0f,
0xe7, 0x4b, 0x7a, 0x4f, 0x7c, 0x8d, 0x47, 0xec, 0x9e, 0x72, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, 0xa7, 0x3b, 0x65, 0xcd, 0xa0, 0xc3, 0x44, 0x4d, 0x0e, 0xf5, 0xdb, 0xe3, 0x1a, 0x9a, 0xb4, 0x90,
0xd4, 0xf3, 0x60, 0x0b, 0x40, 0x01, 0x00, 0x00, 0x40, 0xed, 0x6a, 0xf1, 0x3f, 0xd9, 0x73, 0x44, 0x92, 0x05, 0x48, 0xd0, 0x7d, 0x69, 0x1f, 0x8c,
0xb3, 0xd4, 0x45, 0x93, 0x46, 0x1e, 0xda, 0xc7, 0xe5, 0x6b, 0x57, 0x1e, 0xbc, 0xb5, 0x79, 0x1d,
0x0f, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x49, 0x8d, 0x14, 0x1a, 0x9d, 0x01, 0x00, 0x00,
} }

@ -47,7 +47,7 @@ var Status_Code_value = map[string]int32{
func (x Status_Code) String() string { func (x Status_Code) String() string {
return proto.EnumName(Status_Code_name, int32(x)) return proto.EnumName(Status_Code_name, int32(x))
} }
func (Status_Code) EnumDescriptor() ([]byte, []int) { return fileDescriptor2, []int{0, 0} } func (Status_Code) EnumDescriptor() ([]byte, []int) { return fileDescriptor3, []int{0, 0} }
// Status defines the status of a release. // Status defines the status of a release.
type Status struct { type Status struct {
@ -58,7 +58,7 @@ type Status struct {
func (m *Status) Reset() { *m = Status{} } func (m *Status) Reset() { *m = Status{} }
func (m *Status) String() string { return proto.CompactTextString(m) } func (m *Status) String() string { return proto.CompactTextString(m) }
func (*Status) ProtoMessage() {} func (*Status) ProtoMessage() {}
func (*Status) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} } func (*Status) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{0} }
func (m *Status) GetDetails() *google_protobuf1.Any { func (m *Status) GetDetails() *google_protobuf1.Any {
if m != nil { if m != nil {
@ -72,7 +72,7 @@ func init() {
proto.RegisterEnum("hapi.release.Status_Code", Status_Code_name, Status_Code_value) proto.RegisterEnum("hapi.release.Status_Code", Status_Code_name, Status_Code_value)
} }
var fileDescriptor2 = []byte{ var fileDescriptor3 = []byte{
// 226 bytes of a gzipped FileDescriptorProto // 226 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8,
0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0x2f, 0x2e, 0x49, 0x2c, 0x29, 0x2d, 0xd6, 0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0x2f, 0x2e, 0x49, 0x2c, 0x29, 0x2d, 0xd6,

@ -30,8 +30,8 @@ import fmt "fmt"
import math "math" import math "math"
import hapi_chart3 "k8s.io/helm/pkg/proto/hapi/chart" import hapi_chart3 "k8s.io/helm/pkg/proto/hapi/chart"
import hapi_chart "k8s.io/helm/pkg/proto/hapi/chart" import hapi_chart "k8s.io/helm/pkg/proto/hapi/chart"
import hapi_release3 "k8s.io/helm/pkg/proto/hapi/release"
import hapi_release2 "k8s.io/helm/pkg/proto/hapi/release" import hapi_release2 "k8s.io/helm/pkg/proto/hapi/release"
import hapi_release1 "k8s.io/helm/pkg/proto/hapi/release"
import ( import (
context "golang.org/x/net/context" context "golang.org/x/net/context"
@ -141,7 +141,7 @@ type ListReleasesResponse struct {
// Total is the total number of queryable releases. // Total is the total number of queryable releases.
Total int64 `protobuf:"varint,3,opt,name=total" json:"total,omitempty"` Total int64 `protobuf:"varint,3,opt,name=total" json:"total,omitempty"`
// Releases is the list of found release objects. // Releases is the list of found release objects.
Releases []*hapi_release2.Release `protobuf:"bytes,4,rep,name=releases" json:"releases,omitempty"` Releases []*hapi_release3.Release `protobuf:"bytes,4,rep,name=releases" json:"releases,omitempty"`
} }
func (m *ListReleasesResponse) Reset() { *m = ListReleasesResponse{} } func (m *ListReleasesResponse) Reset() { *m = ListReleasesResponse{} }
@ -149,7 +149,7 @@ func (m *ListReleasesResponse) String() string { return proto.Compact
func (*ListReleasesResponse) ProtoMessage() {} func (*ListReleasesResponse) ProtoMessage() {}
func (*ListReleasesResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } func (*ListReleasesResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
func (m *ListReleasesResponse) GetReleases() []*hapi_release2.Release { func (m *ListReleasesResponse) GetReleases() []*hapi_release3.Release {
if m != nil { if m != nil {
return m.Releases return m.Releases
} }
@ -172,7 +172,7 @@ type GetReleaseStatusResponse struct {
// Name is the name of the release. // Name is the name of the release.
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
// Info contains information about the release. // Info contains information about the release.
Info *hapi_release1.Info `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"` Info *hapi_release2.Info `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"`
} }
func (m *GetReleaseStatusResponse) Reset() { *m = GetReleaseStatusResponse{} } func (m *GetReleaseStatusResponse) Reset() { *m = GetReleaseStatusResponse{} }
@ -180,7 +180,7 @@ func (m *GetReleaseStatusResponse) String() string { return proto.Com
func (*GetReleaseStatusResponse) ProtoMessage() {} func (*GetReleaseStatusResponse) ProtoMessage() {}
func (*GetReleaseStatusResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } func (*GetReleaseStatusResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} }
func (m *GetReleaseStatusResponse) GetInfo() *hapi_release1.Info { func (m *GetReleaseStatusResponse) GetInfo() *hapi_release2.Info {
if m != nil { if m != nil {
return m.Info return m.Info
} }
@ -201,7 +201,7 @@ func (*GetReleaseContentRequest) Descriptor() ([]byte, []int) { return fileDescr
// GetReleaseContentResponse is a response containing the contents of a release. // GetReleaseContentResponse is a response containing the contents of a release.
type GetReleaseContentResponse struct { type GetReleaseContentResponse struct {
// The release content // The release content
Release *hapi_release2.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` Release *hapi_release3.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"`
} }
func (m *GetReleaseContentResponse) Reset() { *m = GetReleaseContentResponse{} } func (m *GetReleaseContentResponse) Reset() { *m = GetReleaseContentResponse{} }
@ -209,7 +209,7 @@ func (m *GetReleaseContentResponse) String() string { return proto.Co
func (*GetReleaseContentResponse) ProtoMessage() {} func (*GetReleaseContentResponse) ProtoMessage() {}
func (*GetReleaseContentResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } func (*GetReleaseContentResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} }
func (m *GetReleaseContentResponse) GetRelease() *hapi_release2.Release { func (m *GetReleaseContentResponse) GetRelease() *hapi_release3.Release {
if m != nil { if m != nil {
return m.Release return m.Release
} }
@ -271,7 +271,7 @@ func (m *InstallReleaseRequest) GetValues() *hapi_chart.Config {
// InstallReleaseResponse is the response from a release installation. // InstallReleaseResponse is the response from a release installation.
type InstallReleaseResponse struct { type InstallReleaseResponse struct {
Release *hapi_release2.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` Release *hapi_release3.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"`
} }
func (m *InstallReleaseResponse) Reset() { *m = InstallReleaseResponse{} } func (m *InstallReleaseResponse) Reset() { *m = InstallReleaseResponse{} }
@ -279,7 +279,7 @@ func (m *InstallReleaseResponse) String() string { return proto.Compa
func (*InstallReleaseResponse) ProtoMessage() {} func (*InstallReleaseResponse) ProtoMessage() {}
func (*InstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} } func (*InstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} }
func (m *InstallReleaseResponse) GetRelease() *hapi_release2.Release { func (m *InstallReleaseResponse) GetRelease() *hapi_release3.Release {
if m != nil { if m != nil {
return m.Release return m.Release
} }
@ -300,7 +300,7 @@ func (*UninstallReleaseRequest) Descriptor() ([]byte, []int) { return fileDescri
// UninstallReleaseResponse represents a successful response to an uninstall request. // UninstallReleaseResponse represents a successful response to an uninstall request.
type UninstallReleaseResponse struct { type UninstallReleaseResponse struct {
// Release is the release that was marked deleted. // Release is the release that was marked deleted.
Release *hapi_release2.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` Release *hapi_release3.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"`
} }
func (m *UninstallReleaseResponse) Reset() { *m = UninstallReleaseResponse{} } func (m *UninstallReleaseResponse) Reset() { *m = UninstallReleaseResponse{} }
@ -308,7 +308,7 @@ func (m *UninstallReleaseResponse) String() string { return proto.Com
func (*UninstallReleaseResponse) ProtoMessage() {} func (*UninstallReleaseResponse) ProtoMessage() {}
func (*UninstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{12} } func (*UninstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{12} }
func (m *UninstallReleaseResponse) GetRelease() *hapi_release2.Release { func (m *UninstallReleaseResponse) GetRelease() *hapi_release3.Release {
if m != nil { if m != nil {
return m.Release return m.Release
} }

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package repo package repo // import "k8s.io/helm/pkg/repo"
import ( import (
"crypto/sha1" "crypto/sha1"
@ -118,6 +118,7 @@ func (r *ChartRepository) saveIndexFile() error {
return ioutil.WriteFile(filepath.Join(r.RootPath, indexPath), index, 0644) return ioutil.WriteFile(filepath.Join(r.RootPath, indexPath), index, 0644)
} }
// Index generates an index for the chart repository and writes an index.yaml file
func (r *ChartRepository) Index() error { func (r *ChartRepository) Index() error {
if r.IndexFile == nil { if r.IndexFile == nil {
r.IndexFile = &IndexFile{Entries: make(map[string]*ChartRef)} r.IndexFile = &IndexFile{Entries: make(map[string]*ChartRef)}

@ -20,4 +20,4 @@ Tiller stores releases (see 'cmd/tiller/environment'.Environment). The backend
storage mechanism may be implemented with different backends. This package storage mechanism may be implemented with different backends. This package
and its subpackages provide storage layers for Tiller objects. and its subpackages provide storage layers for Tiller objects.
*/ */
package storage package storage // import "k8s.io/helm/pkg/storage"

@ -20,4 +20,4 @@ The gRPC/Protobuf libraries contain time implementations that require conversion
to and from Go times. This library provides utilities and convenience functions to and from Go times. This library provides utilities and convenience functions
for performing conversions. for performing conversions.
*/ */
package timeconv package timeconv // import "k8s.io/helm/pkg/timeconv"

@ -15,7 +15,7 @@ limitations under the License.
*/ */
// Package version represents the current version of the project. // Package version represents the current version of the project.
package version package version // import "k8s.io/helm/pkg/version"
// Version is the current version of the Helm. // Version is the current version of the Helm.
// Update this whenever making a new release. // Update this whenever making a new release.

@ -0,0 +1,43 @@
#!/usr/bin/env bash
# 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.
# Bash 'Strict Mode'
# http://redsymbol.net/articles/unofficial-bash-strict-mode
set -euo pipefail
IFS=$'\n\t'
HELM_ROOT="${BASH_SOURCE[0]%/*}/.."
cd "$HELM_ROOT"
run_unit_test() {
if [[ "${CIRCLE_BRANCH-}" == "master" ]]; then
echo "Running unit tests with coverage'"
./scripts/coverage.sh --coveralls
else
echo "Running unit tests'"
make test-unit
fi
}
run_style_check() {
echo "Running 'make test-style'"
make test-style
}
case "${CIRCLE_NODE_INDEX-0}" in
0) run_unit_test ;;
1) run_style_check ;;
esac

@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: kube-system

@ -16,22 +16,36 @@
set -euo pipefail set -euo pipefail
COVERDIR=${COVERDIR:-.coverage} covermode=${COVERMODE:-atomic}
COVERMODE=${COVERMODE:-atomic} coverdir=$(mktemp -d /tmp/coverage.XXXXXXXXXX)
PACKAGES=($(go list $(glide novendor))) profile="${coverdir}/cover.out"
if [[ ! -d "$COVERDIR" ]]; then hash goveralls 2>/dev/null || go get github.com/mattn/goveralls
mkdir -p "$COVERDIR" hash godir 2>/dev/null || go get github.com/Masterminds/godir
fi
echo "mode: ${COVERMODE}" > "${COVERDIR}/coverage.out" generate_cover_data() {
for d in $(godir) ; do
local output="${coverdir}/${d//\//-}.cover"
go test -coverprofile="${output}" -covermode="$covermode" "$d"
done
for d in "${PACKAGES[@]}"; do echo "mode: $covermode" >"$profile"
go test -coverprofile=profile.out -covermode="$COVERMODE" "$d" grep -h -v "^mode:" "$coverdir"/*.cover >>"$profile"
if [ -f profile.out ]; then }
sed "/mode: $COVERMODE/d" profile.out >> "${COVERDIR}/coverage.out"
rm profile.out push_to_coveralls() {
fi goveralls -coverprofile="${profile}" -service=circle-ci
done }
generate_cover_data
go tool cover -func "${profile}"
case "$1" in
--html)
go tool cover -html "${profile}"
;;
--coveralls)
push_to_coveralls
;;
esac
go tool cover -html "${COVERDIR}/coverage.out" -o "${COVERDIR}/coverage.html"

@ -169,7 +169,7 @@ start_kubernetes() {
--volume=/:/rootfs:ro \ --volume=/:/rootfs:ro \
--volume=/sys:/sys:ro \ --volume=/sys:/sys:ro \
--volume=/var/lib/docker/:/var/lib/docker:rw \ --volume=/var/lib/docker/:/var/lib/docker:rw \
--volume=/var/lib/kubelet/:/var/lib/kubelet:rw \ --volume=/var/lib/kubelet/:/var/lib/kubelet:rw,rslave \
--volume=/var/run:/var/run:rw \ --volume=/var/run:/var/run:rw \
--net=host \ --net=host \
--pid=host \ --pid=host \
@ -189,8 +189,10 @@ start_kubernetes() {
sleep 1 sleep 1
done done
# Create kube-system namespace in kubernetes if [[ $KUBE_VERSION == "1.2"* ]]; then
$KUBECTL create namespace kube-system >/dev/null create_kube_system_namespace
create_kube_dns
fi
# We expect to have at least 3 running pods - etcd, master and kube-proxy. # We expect to have at least 3 running pods - etcd, master and kube-proxy.
local attempt=1 local attempt=1
@ -218,6 +220,11 @@ setup_firewall() {
fi fi
} }
# Create kube-system namespace in kubernetes
create_kube_system_namespace() {
$KUBECTL create namespace kube-system >/dev/null
}
# Activate skydns in kubernetes and wait for pods to be ready. # Activate skydns in kubernetes and wait for pods to be ready.
create_kube_dns() { create_kube_dns() {
[[ "${ENABLE_CLUSTER_DNS}" = true ]] || return [[ "${ENABLE_CLUSTER_DNS}" = true ]] || return
@ -321,7 +328,6 @@ kube_up() {
generate_kubeconfig generate_kubeconfig
start_kubernetes start_kubernetes
create_kube_dns
$KUBECTL cluster-info $KUBECTL cluster-info
} }

Loading…
Cancel
Save