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
[![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
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;
import "hapi/release/hook.proto";
import "hapi/release/info.proto";
import "hapi/chart/config.proto";
import "hapi/chart/chart.proto";
@ -40,4 +41,11 @@ message Release {
// Manifest is the string representation of the rendered template.
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:
pre:
- curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0
environment:
GLIDE_VERSION: "0.10.1"
GO15VENDOREXPERIMENT: 1
GOPATH: /usr/local/go_workspace
HOME: /home/ubuntu
IMPORT_PATH: "k8s.io/helm"
PATH: $HOME/go/bin:$PATH
GOROOT: $HOME/go
GOVERSION: "1.6.2"
GOPATH: "${HOME}/.go_workspace"
WORKDIR: "${GOPATH}/src/k8s.io/helm"
services:
- docker
dependencies:
pre:
- sudo rm -rf /usr/local/go
- rm -rf "$GOPATH"
override:
- mkdir -p $HOME/go
- wget "https://storage.googleapis.com/golang/go1.6.linux-amd64.tar.gz"
- tar -C $HOME -xzf go1.6.linux-amd64.tar.gz
- go version
# install go
- wget "https://storage.googleapis.com/golang/go${GOVERSION}.linux-amd64.tar.gz"
- sudo tar -C /usr/local -xzf "go${GOVERSION}.linux-amd64.tar.gz"
# 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
- 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:
override:
- cd $GOPATH/src/$IMPORT_PATH && make bootstrap test
- cd "${WORKDIR}" && ./scripts/ci.sh:
parallel: true
deployment:
master-branch:
gcr:
branch: master
commands:
- 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
- cd $GOPATH/src/$IMPORT_PATH
- docker login -e 1234@5678.com -u _json_key -p "$(cat ${HOME}/gcloud-service-key.json)" https://gcr.io
- 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 (
"errors"
"fmt"
"io"
"path/filepath"
"github.com/spf13/cobra"
"k8s.io/helm/pkg/chartutil"
"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.
`
func init() {
RootCommand.AddCommand(createCmd)
type createCmd struct {
name string
out io.Writer
}
var createCmd = &cobra.Command{
Use: "create NAME",
Short: "create a new chart with the given name",
Long: createDesc,
RunE: runCreate,
func newCreateCmd(out io.Writer) *cobra.Command {
cc := &createCmd{
out: out,
}
cmd := &cobra.Command{
Use: "create NAME",
Short: "create a new chart with the given name",
Long: createDesc,
RunE: func(cmd *cobra.Command, args []string) error {
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 {
if len(args) == 0 {
return errors.New("the name of the new chart is required")
}
cname := args[0]
cmd.Printf("Creating %s\n", cname)
func (c *createCmd) run() error {
fmt.Fprintf(c.out, "Creating %s\n", c.name)
chartname := filepath.Base(cname)
chartname := filepath.Base(c.name)
cfile := &chart.Metadata{
Name: chartname,
Description: "A Helm chart for Kubernetes",
Version: "0.1.0",
}
_, err := chartutil.Create(cfile, filepath.Dir(cname))
_, err := chartutil.Create(cfile, filepath.Dir(c.name))
return err
}

@ -18,8 +18,8 @@ package main
import (
"errors"
"fmt"
"os"
"io"
"text/template"
"time"
"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.
`
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 getCommand = &cobra.Command{
Use: "get [flags] RELEASE_NAME",
Short: "download a named release",
Long: getHelp,
RunE: getCmd,
PersistentPreRunE: setupConnection,
type getCmd struct {
release string
out io.Writer
client helm.Interface
}
var getValuesCommand = &cobra.Command{
Use: "values [flags] RELEASE_NAME",
Short: "download the values file for a named release",
Long: getValuesHelp,
RunE: getValues,
}
var getManifestCommand = &cobra.Command{
Use: "manifest [flags] RELEASE_NAME",
Short: "download the manifest for a named release",
Long: getManifestHelp,
RunE: getManifest,
func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command {
get := &getCmd{
out: out,
client: client,
}
cmd := &cobra.Command{
Use: "get [flags] RELEASE_NAME",
Short: "download a named release",
Long: getHelp,
PersistentPreRunE: setupConnection,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errReleaseRequired
}
get.release = args[0]
if get.client == nil {
get.client = 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() {
// 'get' command flags.
getCommand.PersistentFlags().StringVarP(&getOut, "file", "f", "", "output file")
// 'get values' flags.
getValuesCommand.PersistentFlags().BoolVarP(&allValues, "all", "a", false, "dump all (computed) values")
getCommand.AddCommand(getValuesCommand)
getCommand.AddCommand(getManifestCommand)
RootCommand.AddCommand(getCommand)
}
var getTemplate = `VERSION: {{.Release.Version}}
RELEASED: {{.ReleaseDate}}
CHART: {{.Release.Chart.Metadata.Name}}-{{.Release.Chart.Metadata.Version}}
USER-SUPPLIED VALUES:
{{.Release.Config.Raw}}
COMPUTED VALUES:
{{.ComputedValues}}
HOOKS:
{{- range .Release.Hooks }}
---
# {{.Name}}
{{.Manifest}}
{{- end }}
MANIFEST:
{{.Release.Manifest}}
`
// getCmd is the command that implements 'helm get'
func getCmd(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errReleaseRequired
}
res, err := helm.GetReleaseContent(args[0])
func (g *getCmd) run() error {
res, err := g.client.ReleaseContent(g.release)
if err != nil {
return prettyError(err)
}
@ -119,67 +110,25 @@ func getCmd(cmd *cobra.Command, args []string) error {
return err
}
fmt.Printf("CHART: %s-%s\n", res.Release.Chart.Metadata.Name, res.Release.Chart.Metadata.Version)
fmt.Printf("RELEASED: %s\n", timeconv.Format(res.Release.Info.LastDeployed, time.ANSIC))
fmt.Println("USER-SUPPLIED VALUES:")
fmt.Println(res.Release.Config.Raw)
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)
data := map[string]interface{}{
"Release": res.Release,
"ComputedValues": cfgStr,
"ReleaseDate": timeconv.Format(res.Release.Info.LastDeployed, time.ANSIC),
}
// 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)
return tpl(getTemplate, data, g.out)
}
// getManifest implements 'helm get manifest'
func getManifest(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errReleaseRequired
}
res, err := helm.GetReleaseContent(args[0])
func tpl(t string, vals map[string]interface{}, out io.Writer) error {
tt, err := template.New("_").Parse(t)
if err != nil {
return prettyError(err)
return err
}
return getToFile(res.Release.Manifest)
return tt.Execute(out, vals)
}
func getToFile(v interface{}) error {
out := os.Stdout
if len(getOut) > 0 {
t, err := os.Create(getOut)
if err != nil {
return fmt.Errorf("failed to create %s: %s", getOut, err)
}
defer t.Close()
out = t
func ensureHelmClient(h helm.Interface) helm.Interface {
if h != nil {
return h
}
fmt.Fprintln(out, v)
return nil
return helm.NewClient(helm.Host(helm.Config.ServAddr))
}

@ -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.
*/
package main
package main // import "k8s.io/helm/cmd/helm"
import (
"errors"
"fmt"
"io"
"os"
"strings"
@ -63,29 +64,42 @@ Environment:
$HELM_HOST Set an alternative Tiller host. The format is host:port (default ":44134").
`
// RootCommand is the top-level command for Helm.
var RootCommand = &cobra.Command{
Use: "helm",
Short: "The Helm package manager for Kubernetes.",
Long: globalUsage,
PersistentPostRun: teardown,
}
func init() {
func newRootCmd(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "helm",
Short: "The Helm package manager for Kubernetes.",
Long: globalUsage,
SilenceUsage: true,
PersistentPostRun: func(cmd *cobra.Command, args []string) {
teardown()
},
}
home := os.Getenv(homeEnvVar)
if home == "" {
home = "$HOME/.helm"
}
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(&tillerHost, "host", thost, "address of tiller. Overrides $HELM_HOST.")
p.StringVarP(&tillerNamespace, "namespace", "", "", "kubernetes namespace")
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() {
if err := RootCommand.Execute(); err != nil {
cmd := RootCommand
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}
@ -111,7 +125,7 @@ func setupConnection(c *cobra.Command, args []string) error {
return nil
}
func teardown(c *cobra.Command, args []string) {
func teardown() {
if tunnel != nil {
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
origname := name
if filepath.Ext(name) != ".tgz" {
name += ".tgz"
}
@ -143,9 +144,9 @@ func locateChartPath(name string) (string, error) {
if err != nil {
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 name, fmt.Errorf("file %q not found", name)
return name, fmt.Errorf("file %q not found", origname)
}

@ -19,12 +19,16 @@ package main
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/lint"
"k8s.io/helm/pkg/lint/support"
)
var longLintHelp = `
@ -47,27 +51,80 @@ func init() {
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 {
path := "."
paths := []string{"."}
if len(args) > 0 {
path = args[0]
paths = args
}
// Guard: Error out of this is not a chart.
if _, err := os.Stat(filepath.Join(path, "Chart.yaml")); err != nil {
return errLintNoChart
var total int
var failures int
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 {
fmt.Println("Lint OK")
return nil
}
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 {
fmt.Printf("%s\n", i)
// Guard: Error out of this is not a chart.
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 (
"fmt"
"io"
"strings"
"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.
`
var listCommand = &cobra.Command{
Use: "list [flags] [FILTER]",
Short: "list releases",
Long: listHelp,
RunE: listCmd,
Aliases: []string{"ls"},
PersistentPreRunE: setupConnection,
type listCmd struct {
filter string
long bool
limit int
offset string
byDate bool
sortDesc bool
out io.Writer
client helm.Interface
}
var (
listLong bool
listMax int
listOffset string
listByDate bool
listSortDesc bool
)
func init() {
f := listCommand.Flags()
f.BoolVarP(&listLong, "long", "l", false, "output long listing format")
f.BoolVarP(&listByDate, "date", "d", false, "sort by release date")
f.BoolVarP(&listSortDesc, "reverse", "r", false, "reverse the sort order")
f.IntVarP(&listMax, "max", "m", 256, "maximum number of releases to fetch")
f.StringVarP(&listOffset, "offset", "o", "", "the next release name in the list, used to offset from start value")
RootCommand.AddCommand(listCommand)
}
func listCmd(cmd *cobra.Command, args []string) error {
var filter string
if len(args) > 0 {
filter = strings.Join(args, " ")
func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
list := &listCmd{
out: out,
client: client,
}
cmd := &cobra.Command{
Use: "list [flags] [FILTER]",
Short: "list releases",
Long: listHelp,
Aliases: []string{"ls"},
PersistentPreRunE: setupConnection,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
list.filter = strings.Join(args, " ")
}
if list.client == nil {
list.client = helm.NewClient(helm.Host(helm.Config.ServAddr))
}
return list.run()
},
}
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
if listByDate {
if l.byDate {
sortBy = services.ListSort_LAST_RELEASED
}
sortOrder := services.ListSort_ASC
if listSortDesc {
if l.sortDesc {
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 {
return prettyError(err)
}
@ -106,31 +122,32 @@ func listCmd(cmd *cobra.Command, args []string) error {
}
if res.Next != "" {
fmt.Printf("\tnext: %s", res.Next)
fmt.Fprintf(l.out, "\tnext: %s", res.Next)
}
rels := res.Releases
if listLong {
return formatList(rels)
if l.long {
fmt.Fprintln(l.out, formatList(rels))
return nil
}
for _, r := range rels {
fmt.Println(r.Name)
fmt.Fprintln(l.out, r.Name)
}
return nil
}
func formatList(rels []*release.Release) error {
func formatList(rels []*release.Release) string {
table := uitable.New()
table.MaxColWidth = 30
table.AddRow("NAME", "UPDATED", "STATUS", "CHART")
table.AddRow("NAME", "VERSION", "UPDATED", "STATUS", "CHART")
for _, r := range rels {
c := fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version)
t := timeconv.String(r.Info.LastDeployed)
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 nil
return table.String()
}

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

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

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

@ -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
// by "\n---\n").
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
@ -179,6 +186,12 @@ func (p *PrintingKubeClient) Delete(ns string, r io.Reader) error {
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.
//
// 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 {
return nil
}
func (k *mockKubeClient) WatchUntilReady(ns string, r io.Reader) error {
return nil
}
var _ Engine = &mockEngine{}
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) {
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 {
return nil, errMissingChart
}
ts := timeconv.Now()
name, err := s.uniqName(req.Name)
if err != nil {
return nil, err
}
overrides := map[string]interface{}{
"Release": map[string]interface{}{
"Name": name,
"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)
ts := timeconv.Now()
options := chartutil.ReleaseOptions{Name: name, Time: ts, Namespace: s.env.Namespace}
valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options)
if err != nil {
return nil, err
}
overrides["Values"] = vals
renderer := s.engine(req.Chart)
files, err := renderer.Render(req.Chart, overrides)
files, err := renderer.Render(req.Chart, valuesToRender)
if err != nil {
return nil, err
}
hooks, manifests := sortHooks(files)
// Aggregate all non-hooks into one big doc.
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
if strings.HasPrefix(path.Base(name), "_") {
continue
@ -267,7 +266,7 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea
}
// Store a release.
r := &release.Release{
rel := &release.Release{
Name: name,
Chart: req.Chart,
Config: req.Values,
@ -277,22 +276,41 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea
Status: &release.Status{Code: release.Status_UNKNOWN},
},
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}
if req.DryRun {
log.Printf("Dry run for %s", name)
log.Printf("Dry run for %s", r.Name)
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
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 {
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
@ -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
// this stored in the future.
r.Info.Status.Code = release.Status_DEPLOYED
if err := s.env.Releases.Create(r); err != nil {
log.Printf("warning: Failed to record release %q: %s", name, err)
return res, nil
log.Printf("warning: Failed to record release %q: %s", r.Name, err)
}
r.Info.Status.Code = release.Status_DEPLOYED
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) {
if 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)
rel.Info.Status.Code = release.Status_DELETED
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 {
log.Printf("uninstall: Failed deletion of %q: %s", req.Name, 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 {
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.

@ -34,6 +34,16 @@ import (
"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 {
return &releaseServer{
env: mockEnvironment(),
@ -59,6 +69,18 @@ func releaseMock() *release.Release {
},
},
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"},
Templates: []*chart.Template{
{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)
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 {
t.Errorf("No manifest returned: %v", res.Release)
}
@ -97,7 +134,7 @@ func TestInstallRelease(t *testing.T) {
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)
}
}
@ -113,8 +150,9 @@ func TestInstallReleaseDryRun(t *testing.T) {
{Name: "hello", Data: []byte("hello: world")},
{Name: "goodbye", Data: []byte("goodbye: world")},
{Name: "empty", Data: []byte("")},
{Name: "with-partials", Data: []byte("hello: {{ template \"partials/_planet\" . }}")},
{Name: "partials/_planet", Data: []byte("Earth")},
{Name: "with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)},
{Name: "partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)},
{Name: "hooks", Data: []byte(manifestWithHook)},
},
},
DryRun: true,
@ -127,11 +165,11 @@ func TestInstallReleaseDryRun(t *testing.T) {
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)
}
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)
}
@ -139,7 +177,7 @@ func TestInstallReleaseDryRun(t *testing.T) {
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)
}
@ -150,6 +188,14 @@ func TestInstallReleaseDryRun(t *testing.T) {
if _, err := rs.env.Releases.Read(res.Release.Name); err == nil {
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) {
@ -163,6 +209,18 @@ func TestUninstallRelease(t *testing.T) {
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{
@ -182,6 +240,10 @@ func TestUninstallRelease(t *testing.T) {
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 {
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.
*/
package main
package main // import "k8s.io/helm/cmd/tiller"
import (
"fmt"

@ -142,7 +142,9 @@ When a user supplies custom values, these values will override the
values in the chart's `values.yaml` file.
### 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:
```yaml
@ -302,9 +304,9 @@ apache:
```
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
regenerated like this:

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

@ -1,2 +1,2 @@
# 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
replicaCount: 1
# Evaluated by the post-install hook
sleepyTime: "10"
index: >-
<h1>Hello</h1>
<p>This is a test</p>

21
glide.lock generated

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

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

@ -41,4 +41,4 @@ into a Chart.
When creating charts in memory, use the 'k8s.io/helm/pkg/proto/happy/chart'
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
}
//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))
info := header.FileInfo()
if info.IsDir() {

@ -17,18 +17,19 @@ limitations under the License.
package chartutil
import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"strings"
"github.com/ghodss/yaml"
"github.com/golang/protobuf/ptypes/timestamp"
"k8s.io/helm/pkg/proto/hapi/chart"
)
// 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.
const GlobalKey = "global"
@ -92,7 +93,7 @@ func (v Values) Encode(w io.Writer) error {
func tableLookup(v Values, simple string) (Values, error) {
v2, ok := v[simple]
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 {
return vv, nil
@ -105,14 +106,15 @@ func tableLookup(v Values, simple string) (Values, error) {
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.
func ReadValues(data []byte) (vals Values, err error) {
vals = make(map[string]interface{})
if len(data) > 0 {
err = yaml.Unmarshal(data, &vals)
err = yaml.Unmarshal(data, &vals)
if len(vals) == 0 {
vals = Values{}
}
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
// the values destined for its dependencies.
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
// the passed-in values are in the same namespace as the parent chart.
if vals != nil {
@ -288,6 +290,35 @@ func coalesceTables(dst, src map[string]interface{}) map[string]interface{} {
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.
func istable(v interface{}) bool {
_, ok := v.(map[string]interface{})

@ -53,6 +53,18 @@ water:
t.Fatalf("Error parsing bytes: %s", err)
}
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) {

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package client
package client // import "k8s.io/helm/pkg/client"
import (
"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'
package.
*/
package engine
package engine // import "k8s.io/helm/pkg/engine"

@ -19,6 +19,9 @@ package engine
import (
"bytes"
"fmt"
"log"
"path"
"strings"
"text/template"
"github.com/Masterminds/sprig"
@ -31,6 +34,9 @@ type Engine struct {
// FuncMap contains the template functions that will be passed to each
// render call. This may only be modified before the first call to Render.
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.
@ -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
// template engine.
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{}
for fname, r := range tpls {
log.Printf("Preparing template %s", fname)
t = t.New(fname).Funcs(e.FuncMap)
if _, err := t.Parse(r.tpl); err != nil {
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))
var buf bytes.Buffer
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)
}
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()
}
@ -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.
func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable {
templates := map[string]renderable{}
recAllTpls(c, templates, vals, true)
recAllTpls(c, templates, vals, true, "")
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
// scope.
func recAllTpls(c *chart.Chart, templates map[string]renderable, parentVals chartutil.Values, top bool) {
var cvals chartutil.Values
func recAllTpls(c *chart.Chart, templates map[string]renderable, parentVals chartutil.Values, top bool, parentID string) {
// This should never evaluate to a nil map. That will cause problems when
// values are appended later.
cvals := chartutil.Values{}
if top {
// If this is the top of the rendering tree, assume that parentVals
// is already resolved to the authoritative values.
cvals = parentVals
} else if c.Metadata != nil && c.Metadata.Name != "" {
// An error indicates that the table doesn't exist. So we leave it as
// an empty map.
var tmp chartutil.Values
vs, err := parentVals.Table("Values")
if err == nil {
tmp, err = vs.Table(c.Metadata.Name)
} else {
tmp, err = parentVals.Table(c.Metadata.Name)
// If there is a {{.Values.ThisChart}} in the parent metadata,
// copy that into the {{.Values}} for this template.
newVals := chartutil.Values{}
if vs, err := parentVals.Table("Values"); err == nil {
if tmp, err := vs.Table(c.Metadata.Name); err == nil {
newVals = tmp
}
}
//tmp, err := parentVals["Values"].(chartutil.Values).Table(c.Metadata.Name)
if err == nil {
cvals = map[string]interface{}{
"Values": tmp,
"Release": parentVals["Release"],
"Chart": c,
}
cvals = map[string]interface{}{
"Values": newVals,
"Release": parentVals["Release"],
"Chart": c.Metadata,
}
}
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 {
recAllTpls(child, templates, cvals, false)
recAllTpls(child, templates, cvals, false, newParentID)
}
for _, t := range c.Templates {
templates[t.Name] = renderable{
templates[path.Join(newParentID, t.Name)] = renderable{
tpl: string(t.Data),
vals: cvals,
}

@ -46,6 +46,7 @@ func TestRender(t *testing.T) {
Templates: []*chart.Template{
{Name: "test1", Data: []byte("{{.outer | title }} {{.inner | title}}")},
{Name: "test2", Data: []byte("{{.global.callme | lower }}")},
{Name: "test3", Data: []byte("{{.noValue}}")},
},
Values: &chart.Config{
Raw: "outer: DEFAULT\ninner: DEFAULT",
@ -74,14 +75,18 @@ func TestRender(t *testing.T) {
}
expect := "Spouter Inn"
if out["test1"] != expect {
if out["moby/test1"] != expect {
t.Errorf("Expected %q, got %q", expect, out["test1"])
}
expect = "ishmael"
if out["test2"] != expect {
if out["moby/test2"] != expect {
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 {
t.Errorf("Unexpected error: %s", err)
@ -149,18 +154,21 @@ func TestParallelRenderInternals(t *testing.T) {
func TestAllTemplates(t *testing.T) {
ch1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "ch1"},
Templates: []*chart.Template{
{Name: "foo", Data: []byte("foo")},
{Name: "bar", Data: []byte("bar")},
},
Dependencies: []*chart.Chart{
{
Metadata: &chart.Metadata{Name: "laboratory mice"},
Templates: []*chart.Template{
{Name: "pinky", Data: []byte("pinky")},
{Name: "brain", Data: []byte("brain")},
},
Dependencies: []*chart.Chart{
{Templates: []*chart.Template{
Dependencies: []*chart.Chart{{
Metadata: &chart.Metadata{Name: "same thing we do every night"},
Templates: []*chart.Template{
{Name: "innermost", Data: []byte("innermost")},
}},
},
@ -180,11 +188,13 @@ func TestRenderDependency(t *testing.T) {
deptpl := `{{define "myblock"}}World{{end}}`
toptpl := `Hello {{template "myblock"}}`
ch := &chart.Chart{
Metadata: &chart.Metadata{Name: "outerchart"},
Templates: []*chart.Template{
{Name: "outer", Data: []byte(toptpl)},
},
Dependencies: []*chart.Chart{
{
Metadata: &chart.Metadata{Name: "innerchart"},
Templates: []*chart.Template{
{Name: "inner", Data: []byte(deptpl)},
},
@ -203,7 +213,7 @@ func TestRenderDependency(t *testing.T) {
}
expect := "Hello World"
if out["outer"] != expect {
if out["outerchart/outer"] != expect {
t.Errorf("Expected %q, got %q", expect, out["outer"])
}
@ -212,10 +222,11 @@ func TestRenderDependency(t *testing.T) {
func TestRenderNestedValues(t *testing.T) {
e := New()
innerpath := "charts/inner/templates/inner.tpl"
innerpath := "templates/inner.tpl"
outerpath := "templates/outer.tpl"
deepestpath := "charts/inner/charts/deepest/templates/deepest.tpl"
checkrelease := "charts/inner/charts/deepest/templates/release.tpl"
// Ensure namespacing rules are working.
deepestpath := "templates/inner.tpl"
checkrelease := "templates/release.tpl"
deepest := &chart.Chart{
Metadata: &chart.Metadata{Name: "deepest"},
@ -280,19 +291,65 @@ global:
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])
}
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])
}
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])
}
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])
}
}
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.
*/
package helm
package helm // import "k8s.io/helm/pkg/helm"
import (
"google.golang.org/grpc"
@ -24,29 +24,30 @@ import (
)
const (
// $HELM_HOST envvar
// HelmHostEnvVar is the $HELM_HOST envvar
HelmHostEnvVar = "HELM_HOST"
// $HELM_HOME envvar
// HelmHomeEnvVar is the $HELM_HOME envvar
HelmHomeEnvVar = "HELM_HOME"
// Default tiller server host address.
// DefaultHelmHost is the default tiller server host address.
DefaultHelmHost = ":44134"
// Default $HELM_HOME envvar value
// DefaultHelmHome is the default $HELM_HOME envvar value
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 {
opts options
}
// NewClient creates a new client.
func NewClient(opts ...Option) *Client {
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 {
for _, opt := range opts {
opt(&h.opts)
@ -54,10 +55,10 @@ func (h *Client) Option(opts ...Option) *Client {
return h
}
// Initializes the helm client with default options
// Init initializes the helm client with default options
func (h *Client) Init() *Client {
return h.Option(HelmHost(DefaultHelmHost)).
Option(HelmHome(os.ExpandEnv(DefaultHelmHome)))
return h.Option(Host(DefaultHelmHost)).
Option(Home(os.ExpandEnv(DefaultHelmHome)))
}
// 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...)
}
// 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
// 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
// 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 {
ServAddr string
}
// ListReleases lists releases. DEPRECATED.
//
// 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) {
opts := []ReleaseListOption{
@ -36,36 +39,42 @@ func ListReleases(limit int, offset string, sort rls.ListSort_SortBy, order rls.
ReleaseListSort(int32(sort)),
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.
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.
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.
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.
func InstallRelease(vals []byte, rlsName, chStr string, dryRun bool) (*rls.InstallReleaseResponse, error) {
client := NewClient(HelmHost(Config.ServAddr))
client := NewClient(Host(Config.ServAddr))
if dryRun {
client.Option(DryRun())
}
return client.InstallRelease(chStr, ValueOverrides(vals), ReleaseName(rlsName))
}
// UninstallRelease destroys an existing release.
// Soon to be deprecated helm UninstallRelease API.
func UninstallRelease(rlsName string, dryRun bool) (*rls.UninstallReleaseResponse, error) {
client := NewClient(HelmHost(Config.ServAddr))
client := NewClient(Host(Config.ServAddr))
if 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").
func HelmHome(home string) Option {
// Home specifies the location of helm home, (default = "$HOME/.helm").
func Home(home string) Option {
return func(opts *options) {
opts.home = home
}
}
// HelmHost specifies the host address of the Tiller release server, (default = ":44134").
func HelmHost(host string) Option {
// Host specifies the host address of the Tiller release server, (default = ":44134").
func Host(host string) Option {
return func(opts *options) {
opts.host = host
}

@ -64,4 +64,4 @@ Notable differences from .gitignore:
- The evaluation of escape sequences has not been tested for compatibility
- 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.
*/
package kube
package kube // import "k8s.io/helm/pkg/kube"
import (
"fmt"
"io"
"log"
"time"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/apis/batch"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/watch"
)
// 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)
}
// 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
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)
}
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 {
client, err := c.Client()
if err != nil {

@ -14,24 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package lint
package lint // import "k8s.io/helm/pkg/lint"
import (
"path/filepath"
"k8s.io/helm/pkg/lint/rules"
"k8s.io/helm/pkg/lint/support"
"os"
"path/filepath"
)
// 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
current, _ := os.Getwd()
chartDir := filepath.Join(current, basedir)
chartDir, _ := filepath.Abs(basedir)
linter := support.Linter{ChartDir: chartDir}
rules.Chartfile(&linter)
rules.Values(&linter)
rules.Templates(&linter)
return linter.Messages
return linter
}

@ -17,9 +17,10 @@ limitations under the License.
package lint
import (
"k8s.io/helm/pkg/lint/support"
"strings"
"k8s.io/helm/pkg/lint/support"
"testing"
)
@ -29,7 +30,7 @@ const badYamlFileDir = "rules/testdata/albatross"
const goodChartDir = "rules/testdata/goodone"
func TestBadChart(t *testing.T) {
m := All(badChartDir)
m := All(badChartDir).Messages
if len(m) != 4 {
t.Errorf("Number of errors %v", len(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
for _, msg := range m {
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
}
}
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
}
if strings.Contains(msg.Text, "'name' is required") {
if strings.Contains(msg.Err.Error(), "name is required") {
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
}
}
@ -60,27 +61,27 @@ func TestBadChart(t *testing.T) {
}
func TestInvalidYaml(t *testing.T) {
m := All(badYamlFileDir)
m := All(badYamlFileDir).Messages
if len(m) != 1 {
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")
}
}
func TestBadValues(t *testing.T) {
m := All(badValuesFileDir)
m := All(badValuesFileDir).Messages
if len(m) != 1 {
t.Errorf("All didn't fail with expected errors, got %#v", m)
}
if !strings.Contains(m[0].Text, "cannot unmarshal") {
t.Errorf("All didn't have the error for invalid key format: %s", m[0].Text)
if !strings.Contains(m[0].Err.Error(), "cannot unmarshal") {
t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err)
}
}
func TestGoodChart(t *testing.T) {
m := All(goodChartDir)
m := All(goodChartDir).Messages
if len(m) != 0 {
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.
*/
package rules
package rules // import "k8s.io/helm/pkg/lint/rules"
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/semver"
"github.com/asaskevich/govalidator"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/lint/support"
@ -31,95 +33,83 @@ import (
// Chartfile runs a set of linter rules related to Chart.yaml file
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, validateChartYamlNotDirectory(chartPath))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(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
if !validChartFile {
return
}
linter.RunLinterRule(support.ErrorSev, validateChartName(chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartNameDirMatch(linter.ChartDir, chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartNameDirMatch(linter.ChartDir, chartFile))
// Chart metadata
linter.RunLinterRule(support.ErrorSev, validateChartVersion(chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartEngine(chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartMaintainer(chartFile))
linter.RunLinterRule(support.ErrorSev, 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
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartEngine(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile))
}
func validateChartYamlNotDirectory(chartPath string) (lintError support.LintError) {
func validateChartYamlNotDirectory(chartPath string) error {
fi, err := os.Stat(chartPath)
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 {
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 == "" {
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) {
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 == "" {
lintError = fmt.Errorf("Chart.yaml: 'version' value is required")
return
return errors.New("version is required")
}
version, err := semver.NewVersion(cf.Version)
if err != nil {
lintError = fmt.Errorf("Chart.yaml: version '%s' is not a valid SemVer", cf.Version)
return
return fmt.Errorf("version '%s' is not a valid SemVer", cf.Version)
}
c, err := semver.NewConstraint("> 0")
valid, msg := c.Validate(version)
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 == "" {
return
return nil
}
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 {
return
return nil
}
keys = append(keys, str)
}
lintError = fmt.Errorf("Chart.yaml: 'engine %v not valid. Valid options are %v", cf.Engine, keys)
return
return fmt.Errorf("engine '%v' not valid. Valid options are %v", cf.Engine, keys)
}
func validateChartMaintainer(cf *chart.Metadata) (lintError support.LintError) {
func validateChartMaintainer(cf *chart.Metadata) error {
for _, maintainer := range cf.Maintainers {
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) {
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 {
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) {
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"
)
const badChartDir = "testdata/badchartfile"
const goodChartDir = "testdata/goodone"
const (
badChartDir = "testdata/badchartfile"
goodChartDir = "testdata/goodone"
)
var badChartFilePath string = filepath.Join(badChartDir, "Chart.yaml")
var goodChartFilePath string = filepath.Join(goodChartDir, "Chart.yaml")
var nonExistingChartFilePath string = filepath.Join(os.TempDir(), "Chart.yaml")
var (
badChartFilePath = filepath.Join(badChartDir, "Chart.yaml")
goodChartFilePath = filepath.Join(goodChartDir, "Chart.yaml")
nonExistingChartFilePath = filepath.Join(os.TempDir(), "Chart.yaml")
)
var badChart, chatLoadRrr = chartutil.LoadChartfile(badChartFilePath)
var goodChart, _ = chartutil.LoadChartfile(goodChartFilePath)
// 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) {
_ = os.Mkdir(nonExistingChartFilePath, os.ModePerm)
defer os.Remove(nonExistingChartFilePath)
@ -103,10 +95,10 @@ func TestValidateChartVersion(t *testing.T) {
Version string
ErrorMsg string
}{
{"", "'version' value is required"},
{"", "version is required"},
{"0", "0 is less than or equal to 0"},
{"waps", "is not a valid SemVer"},
{"-3", "is not a valid SemVer"},
{"waps", "'waps' 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"}
@ -152,9 +144,9 @@ func TestValidateChartMaintainer(t *testing.T) {
Email string
ErrorMsg string
}{
{"", "", "maintainer requires a name"},
{"", "test@test.com", "maintainer requires a name"},
{"John Snow", "wrongFormatEmail.com", "maintainer invalid email"},
{"", "", "each maintainer requires a name"},
{"", "test@test.com", "each maintainer requires a name"},
{"John Snow", "wrongFormatEmail.com", "invalid email"},
}
var successTest = []struct {
@ -188,8 +180,8 @@ func TestValidateChartSources(t *testing.T) {
for _, test := range failTest {
badChart.Sources = []string{test}
err := validateChartSources(badChart)
if err == nil || !strings.Contains(err.Error(), "invalid URL") {
t.Errorf("validateChartSources(%s) to return \"invalid URL\", got no error", test)
if err == nil || !strings.Contains(err.Error(), "invalid source URL") {
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 {
badChart.Home = test
err := validateChartHome(badChart)
if err == nil || !strings.Contains(err.Error(), "invalid URL") {
t.Errorf("validateChartHome(%s) to return \"invalid URL\", got no error", test)
if err == nil || !strings.Contains(err.Error(), "invalid home URL") {
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))
}
if !strings.Contains(msgs[0].Text, "'name' is required") {
t.Errorf("Unexpected message 0: %s", msgs[0].Text)
if !strings.Contains(msgs[0].Err.Error(), "name is required") {
t.Errorf("Unexpected message 0: %s", msgs[0].Err)
}
if !strings.Contains(msgs[1].Text, "'name' and directory do not match") {
t.Errorf("Unexpected message 1: %s", msgs[1].Text)
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].Err)
}
if !strings.Contains(msgs[2].Text, "'version' 0.0.0 is less than or equal to 0") {
t.Errorf("Unexpected message 2: %s", msgs[2].Text)
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].Err)
}
}

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

@ -17,9 +17,12 @@ limitations under the License.
package rules
import (
"k8s.io/helm/pkg/lint/support"
"os"
"path/filepath"
"strings"
"testing"
"k8s.io/helm/pkg/lint/support"
)
const templateTestBasedir = "./testdata/albatross"
@ -28,8 +31,8 @@ func TestValidateAllowedExtension(t *testing.T) {
var failTest = []string{"/foo", "/test.yml", "/test.toml", "test.yml"}
for _, test := range failTest {
err := validateAllowedExtension(test)
if err == nil || !strings.Contains(err.Error(), "needs to use .yaml or .tpl extension") {
t.Errorf("validateAllowedExtension('%s') to return \"needs to use .yaml or .tpl extension\", got no error", test)
if err == nil || !strings.Contains(err.Error(), "Valid extensions are .yaml or .tpl") {
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"}
@ -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 }} "}
for _, test := range failTest {
err := validateQuotes("testTemplate.yaml", test)
err := validateQuotes(test)
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)
}
@ -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 }}"}
for _, test := range successTest {
err := validateQuotes("testTemplate.yaml", test)
err := validateQuotes(test)
if err != nil {
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 }}"}
for _, test := range failTest {
err := validateQuotes("testTemplate.yaml", test)
if err == nil || !strings.Contains(err.Error(), "Wrap your substitution functions in quotes") {
t.Errorf("validateQuotes('%s') to return \"Wrap your substitution functions in quotes\", got no error", test)
err := validateQuotes(test)
if err == nil || !strings.Contains(err.Error(), "wrap substitution functions in quotes") {
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}
Templates(&linter)
res := linter.Messages
@ -82,7 +85,26 @@ func TestTemplate(t *testing.T) {
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])
}
}
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 (
"fmt"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/lint/support"
"os"
"path/filepath"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/lint/support"
)
// Values lints a chart's values.yaml file.
func Values(linter *support.Linter) {
vf := filepath.Join(linter.ChartDir, "values.yaml")
fileExists := linter.RunLinterRule(support.InfoSev, validateValuesFileExistence(linter, vf))
file := "values.yaml"
vf := filepath.Join(linter.ChartDir, file)
fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(linter, vf))
if !fileExists {
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)
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)
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.
*/
/*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
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.
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 {
// Severity is one of the *Sev constants
Severity int
// Text contains the message text
Text string
Path string
Err error
}
type Linter struct {
Messages []Message
ChartDir string
}
type LintError interface {
error
func (m Message) Error() string {
return fmt.Sprintf("[%s] %s: %s", sev[m.Severity], m.Path, m.Err.Error())
}
// String prints a string representation of this Message.
//
// Implements fmt.Stringer.
func (m Message) String() string {
return fmt.Sprintf("[%s] %s", sev[m.Severity], m.Text)
// NewMessage creates a new Message struct
func NewMessage(severity int, path string, err error) Message {
return Message{Severity: severity, Path: path, Err: err}
}
// Returns true if the validation passed
func (l *Linter) RunLinterRule(severity int, lintError LintError) bool {
// RunLinterRule returns true if the validation passed
func (l *Linter) RunLinterRule(severity int, path string, err error) bool {
// severity is out of bound
if severity < 0 || severity >= len(sev) {
return false
}
if lintError != nil {
l.Messages = append(l.Messages, Message{Text: lintError.Error(), Severity: severity})
if err != nil {
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
import (
"fmt"
"errors"
"testing"
)
var linter Linter = Linter{}
var lintError LintError = fmt.Errorf("Foobar")
var linter = Linter{}
var errLint = errors.New("lint failed")
func TestRunLinterRule(t *testing.T) {
var tests = []struct {
Severity int
LintError error
ExpectedMessages int
ExpectedReturn bool
Severity int
LintError error
ExpectedMessages int
ExpectedReturn bool
ExpectedHighestSeverity int
}{
{ErrorSev, lintError, 1, false},
{WarningSev, lintError, 2, false},
{InfoSev, lintError, 3, false},
{InfoSev, errLint, 1, false, InfoSev},
{WarningSev, errLint, 2, false, WarningSev},
{ErrorSev, errLint, 3, false, ErrorSev},
// 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
{4, lintError, 3, false},
{22, lintError, 3, false},
{-1, lintError, 3, false},
{4, errLint, 4, false, ErrorSev},
{22, errLint, 4, false, ErrorSev},
{-1, errLint, 4, false, ErrorSev},
}
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 {
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 {
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) {
m := Message{ErrorSev, "Foo"}
if m.String() != "[ERROR] Foo" {
t.Errorf("Unexpected output: %s", m.String())
m := Message{ErrorSev, "Chart.yaml", errors.New("Foo")}
if m.Error() != "[ERROR] Chart.yaml: Foo" {
t.Errorf("Unexpected output: %s", m.Error())
}
m = Message{WarningSev, "Bar"}
if m.String() != "[WARNING] Bar" {
t.Errorf("Unexpected output: %s", m.String())
m = Message{WarningSev, "templates/", errors.New("Bar")}
if m.Error() != "[WARNING] templates/: Bar" {
t.Errorf("Unexpected output: %s", m.Error())
}
m = Message{InfoSev, "FooBar"}
if m.String() != "[INFO] FooBar" {
t.Errorf("Unexpected output: %s", m.String())
m = Message{InfoSev, "templates/rc.yaml", errors.New("FooBar")}
if m.Error() != "[INFO] templates/rc.yaml: FooBar" {
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
// 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
import proto "github.com/golang/protobuf/proto"
@ -27,10 +14,6 @@ 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
// Info describes release information.
type Info struct {
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) String() string { return proto.CompactTextString(m) }
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 {
if m != nil {
@ -77,7 +60,7 @@ func init() {
proto.RegisterType((*Info)(nil), "hapi.release.Info")
}
var fileDescriptor0 = []byte{
var fileDescriptor1 = []byte{
// 208 bytes of a gzipped FileDescriptorProto
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,

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

@ -30,8 +30,8 @@ import fmt "fmt"
import math "math"
import hapi_chart3 "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_release1 "k8s.io/helm/pkg/proto/hapi/release"
import (
context "golang.org/x/net/context"
@ -141,7 +141,7 @@ type ListReleasesResponse struct {
// Total is the total number of queryable releases.
Total int64 `protobuf:"varint,3,opt,name=total" json:"total,omitempty"`
// 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{} }
@ -149,7 +149,7 @@ func (m *ListReleasesResponse) String() string { return proto.Compact
func (*ListReleasesResponse) ProtoMessage() {}
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 {
return m.Releases
}
@ -172,7 +172,7 @@ type GetReleaseStatusResponse struct {
// Name is the name of the release.
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
// 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{} }
@ -180,7 +180,7 @@ func (m *GetReleaseStatusResponse) String() string { return proto.Com
func (*GetReleaseStatusResponse) ProtoMessage() {}
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 {
return m.Info
}
@ -201,7 +201,7 @@ func (*GetReleaseContentRequest) Descriptor() ([]byte, []int) { return fileDescr
// GetReleaseContentResponse is a response containing the contents of a release.
type GetReleaseContentResponse struct {
// 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{} }
@ -209,7 +209,7 @@ func (m *GetReleaseContentResponse) String() string { return proto.Co
func (*GetReleaseContentResponse) ProtoMessage() {}
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 {
return m.Release
}
@ -271,7 +271,7 @@ func (m *InstallReleaseRequest) GetValues() *hapi_chart.Config {
// InstallReleaseResponse is the response from a release installation.
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{} }
@ -279,7 +279,7 @@ func (m *InstallReleaseResponse) String() string { return proto.Compa
func (*InstallReleaseResponse) ProtoMessage() {}
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 {
return m.Release
}
@ -300,7 +300,7 @@ func (*UninstallReleaseRequest) Descriptor() ([]byte, []int) { return fileDescri
// UninstallReleaseResponse represents a successful response to an uninstall request.
type UninstallReleaseResponse struct {
// 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{} }
@ -308,7 +308,7 @@ func (m *UninstallReleaseResponse) String() string { return proto.Com
func (*UninstallReleaseResponse) ProtoMessage() {}
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 {
return m.Release
}

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package repo
package repo // import "k8s.io/helm/pkg/repo"
import (
"crypto/sha1"
@ -118,6 +118,7 @@ func (r *ChartRepository) saveIndexFile() error {
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 {
if r.IndexFile == nil {
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
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
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
package version // import "k8s.io/helm/pkg/version"
// Version is the current version of the Helm.
// 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
COVERDIR=${COVERDIR:-.coverage}
COVERMODE=${COVERMODE:-atomic}
PACKAGES=($(go list $(glide novendor)))
covermode=${COVERMODE:-atomic}
coverdir=$(mktemp -d /tmp/coverage.XXXXXXXXXX)
profile="${coverdir}/cover.out"
if [[ ! -d "$COVERDIR" ]]; then
mkdir -p "$COVERDIR"
fi
hash goveralls 2>/dev/null || go get github.com/mattn/goveralls
hash godir 2>/dev/null || go get github.com/Masterminds/godir
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
go test -coverprofile=profile.out -covermode="$COVERMODE" "$d"
if [ -f profile.out ]; then
sed "/mode: $COVERMODE/d" profile.out >> "${COVERDIR}/coverage.out"
rm profile.out
fi
done
echo "mode: $covermode" >"$profile"
grep -h -v "^mode:" "$coverdir"/*.cover >>"$profile"
}
push_to_coveralls() {
goveralls -coverprofile="${profile}" -service=circle-ci
}
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=/sys:/sys:ro \
--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 \
--net=host \
--pid=host \
@ -189,8 +189,10 @@ start_kubernetes() {
sleep 1
done
# Create kube-system namespace in kubernetes
$KUBECTL create namespace kube-system >/dev/null
if [[ $KUBE_VERSION == "1.2"* ]]; then
create_kube_system_namespace
create_kube_dns
fi
# We expect to have at least 3 running pods - etcd, master and kube-proxy.
local attempt=1
@ -218,6 +220,11 @@ setup_firewall() {
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.
create_kube_dns() {
[[ "${ENABLE_CLUSTER_DNS}" = true ]] || return
@ -321,7 +328,6 @@ kube_up() {
generate_kubeconfig
start_kubernetes
create_kube_dns
$KUBECTL cluster-info
}

Loading…
Cancel
Save