Commit ae2d6c50 authored by fibonacci1729's avatar fibonacci1729

Merge branch 'master' into feat/storage-memory

parents 9d3a1ed2 ae4ff5cd
.DS_Store
.coverage/
.vimrc
_dist/
_proto/*.pb.go
bin/
rootfs/tiller
vendor/
_proto/*.pb.go
.vimrc
.DS_Store
......@@ -162,6 +162,9 @@ message UpdateReleaseRequest {
hapi.chart.Config values = 3;
// dry_run, if true, will run through the release logic, but neither create
bool dry_run = 4;
// DisableHooks causes the server to skip running any hooks for the upgrade.
bool disable_hooks = 5;
}
// UpdateReleaseResponse is the response to an update request.
......
......@@ -84,12 +84,13 @@ func newRootCmd(out io.Writer) *cobra.Command {
cmd.AddCommand(
newCreateCmd(out),
newDeleteCmd(nil, out),
newGetCmd(nil, out),
newInitCmd(out),
newInspectCmd(nil, out),
newInstallCmd(nil, out),
newListCmd(nil, out),
newStatusCmd(nil, out),
newInstallCmd(nil, out),
newDeleteCmd(nil, out),
newInspectCmd(nil, out),
newUpgradeCmd(nil, out),
)
return cmd
......
......@@ -19,6 +19,7 @@ package main
import (
"errors"
"fmt"
"io"
"os"
"github.com/spf13/cobra"
......@@ -32,58 +33,54 @@ Kubernetes Cluster and sets up local configuration in $HELM_HOME (default: ~/.he
`
var (
tillerImg string
clientOnly bool
defaultRepository = "kubernetes-charts"
defaultRepositoryURL = "http://storage.googleapis.com/kubernetes-charts"
)
func init() {
f := initCmd.Flags()
f.StringVarP(&tillerImg, "tiller-image", "i", "", "override tiller image")
f.BoolVarP(&clientOnly, "client-only", "c", false, "If set does not install tiller")
RootCommand.AddCommand(initCmd)
type initCmd struct {
image string
clientOnly bool
out io.Writer
}
var initCmd = &cobra.Command{
func newInitCmd(out io.Writer) *cobra.Command {
i := &initCmd{
out: out,
}
cmd := &cobra.Command{
Use: "init",
Short: "initialize Helm on both client and server",
Long: initDesc,
RunE: runInit,
}
// runInit initializes local config and installs tiller to Kubernetes Cluster
func runInit(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 0 {
return errors.New("This command does not accept arguments. \n")
return errors.New("This command does not accept arguments")
}
return i.run()
},
}
cmd.Flags().StringVarP(&i.image, "tiller-image", "i", "", "override tiller image")
cmd.Flags().BoolVarP(&i.clientOnly, "client-only", "c", false, "If set does not install tiller")
return cmd
}
// runInit initializes local config and installs tiller to Kubernetes Cluster
func (i *initCmd) run() error {
if err := ensureHome(); err != nil {
return err
}
if !clientOnly {
if err := installTiller(); err != nil {
return err
if !i.clientOnly {
if err := client.Install(tillerNamespace, i.image, flagDebug); err != nil {
return fmt.Errorf("error installing: %s", err)
}
fmt.Fprintln(i.out, "\nTiller (the helm server side component) has been installed into your Kubernetes Cluster.")
} else {
fmt.Println("Not installing tiller due to 'client-only' flag having been set")
fmt.Fprintln(i.out, "Not installing tiller due to 'client-only' flag having been set")
}
fmt.Println("Happy Helming!")
return nil
}
func installTiller() error {
if err := client.Install(tillerNamespace, tillerImg, flagDebug); err != nil {
return fmt.Errorf("error installing: %s", err)
}
fmt.Println("\nTiller (the helm server side component) has been installed into your Kubernetes Cluster.")
fmt.Fprintln(i.out, "Happy Helming!")
return nil
}
// requireHome checks to see if $HELM_HOME exists, and returns an error if it does not.
func requireHome() error {
dirs := []string{homePath(), repositoryDirectory(), cacheDirectory(), localRepoDirectory()}
for _, d := range dirs {
......
......@@ -130,11 +130,10 @@ func (i *inspectCmd) run() error {
fmt.Fprintln(i.out, string(cf))
}
if (i.output == valuesOnly || i.output == both) && chrt.Values != nil {
if i.output == both {
fmt.Fprintln(i.out, "---")
}
if i.output == valuesOnly || i.output == both {
fmt.Fprintln(i.out, chrt.Values.Raw)
}
......
......@@ -61,4 +61,17 @@ func TestInspect(t *testing.T) {
t.Errorf("Expected\n%q\nGot\n%q\n", expect[i], got)
}
}
// Regression tests for missing values. See issue #1024.
b.Reset()
insp = &inspectCmd{
chartpath: "testdata/testcharts/novals",
output: "values",
out: b,
}
insp.run()
if b.Len() != 0 {
t.Errorf("expected empty values buffer, got %q", b.String())
}
}
......@@ -17,6 +17,7 @@ limitations under the License.
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
......@@ -24,6 +25,9 @@ import (
"path/filepath"
"strings"
"text/template"
"github.com/Masterminds/sprig"
"github.com/ghodss/yaml"
"github.com/spf13/cobra"
......@@ -59,10 +63,11 @@ type installCmd struct {
chartPath string
dryRun bool
disableHooks bool
reuseName bool
replace bool
out io.Writer
client helm.Interface
values *values
nameTemplate string
}
func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
......@@ -98,8 +103,9 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
f.StringVar(&inst.namespace, "namespace", "default", "the namespace to install the release into")
f.BoolVar(&inst.dryRun, "dry-run", false, "simulate an install")
f.BoolVar(&inst.disableHooks, "no-hooks", false, "prevent hooks from running during install")
f.BoolVar(&inst.reuseName, "reuse-name", false, "force Tiller to re-use the given name, even if that name is already used. This is unsafe in production")
f.BoolVar(&inst.replace, "replace", false, "re-use the given name, even if that name is already used. This is unsafe in production")
f.Var(inst.values, "set", "set values on the command line. Separate values with commas: key1=val1,key2=val2")
f.StringVar(&inst.nameTemplate, "name-template", "", "specify template used to name the release")
return cmd
}
......@@ -113,13 +119,23 @@ func (i *installCmd) run() error {
return err
}
// If template is specified, try to run the template.
if i.nameTemplate != "" {
i.name, err = generateName(i.nameTemplate)
if err != nil {
return err
}
// Print the final name so the user knows what the final name of the release is.
fmt.Printf("final name: %s\n", i.name)
}
res, err := i.client.InstallRelease(
i.chartPath,
i.namespace,
helm.ValueOverrides(rawVals),
helm.ReleaseName(i.name),
helm.InstallDryRun(i.dryRun),
helm.InstallReuseName(i.reuseName),
helm.InstallReuseName(i.replace),
helm.InstallDisableHooks(i.disableHooks))
if err != nil {
return prettyError(err)
......@@ -249,3 +265,16 @@ func locateChartPath(name string) (string, error) {
return name, fmt.Errorf("file %q not found", origname)
}
func generateName(nameTemplate string) (string, error) {
t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
if err != nil {
return "", err
}
var b bytes.Buffer
err = t.Execute(&b, nil)
if err != nil {
return "", err
}
return b.String(), nil
}
......@@ -19,6 +19,7 @@ package main
import (
"fmt"
"io"
"regexp"
"strings"
"testing"
......@@ -59,12 +60,20 @@ func TestInstall(t *testing.T) {
},
// Install, re-use name
{
name: "install and reuse name",
name: "install and replace release",
args: []string{"testdata/testcharts/alpine"},
flags: strings.Split("--name aeneas --reuse-name", " "),
flags: strings.Split("--name aeneas --replace", " "),
expected: "aeneas",
resp: releaseMock(&releaseOptions{name: "aeneas"}),
},
// Install, using the name-template
{
name: "install with name-template",
args: []string{"testdata/testcharts/alpine"},
flags: []string{"--name-template", "{{upper \"foobar\"}}"},
expected: "FOOBAR",
resp: releaseMock(&releaseOptions{name: "FOOBAR"}),
},
}
runReleaseCases(t, tests, func(c *fakeReleaseClient, out io.Writer) *cobra.Command {
......@@ -113,3 +122,78 @@ sailor: sinbad
t.Errorf("Expected String() to be \n%s\nGot\n%s\n", y, out)
}
}
type nameTemplateTestCase struct {
tpl string
expected string
expectedErrorStr string
}
func TestNameTemplate(t *testing.T) {
testCases := []nameTemplateTestCase{
// Just a straight up nop please
{
tpl: "foobar",
expected: "foobar",
expectedErrorStr: "",
},
// Random numbers at the end for fun & profit
{
tpl: "foobar-{{randNumeric 6}}",
expected: "foobar-[0-9]{6}$",
expectedErrorStr: "",
},
// Random numbers in the middle for fun & profit
{
tpl: "foobar-{{randNumeric 4}}-baz",
expected: "foobar-[0-9]{4}-baz$",
expectedErrorStr: "",
},
// No such function
{
tpl: "foobar-{{randInt}}",
expected: "",
expectedErrorStr: "function \"randInt\" not defined",
},
// Invalid template
{
tpl: "foobar-{{",
expected: "",
expectedErrorStr: "unexpected unclosed action",
},
}
for _, tc := range testCases {
n, err := generateName(tc.tpl)
if err != nil {
if tc.expectedErrorStr == "" {
t.Errorf("Was not expecting error, but got: %v", err)
continue
}
re, compErr := regexp.Compile(tc.expectedErrorStr)
if compErr != nil {
t.Errorf("Expected error string failed to compile: %v", compErr)
continue
}
if !re.MatchString(err.Error()) {
t.Errorf("Error didn't match for %s expected %s but got %v", tc.tpl, tc.expectedErrorStr, err)
continue
}
}
if err == nil && tc.expectedErrorStr != "" {
t.Errorf("Was expecting error %s but didn't get an error back", tc.expectedErrorStr)
}
if tc.expected != "" {
re, err := regexp.Compile(tc.expected)
if err != nil {
t.Errorf("Expected string failed to compile: %v", err)
continue
}
if !re.MatchString(n) {
t.Errorf("Returned name didn't match for %s expected %s but got %s", tc.tpl, tc.expected, n)
}
}
}
}
......@@ -47,7 +47,10 @@ var lintCommand = &cobra.Command{
RunE: lintCmd,
}
var flagStrict bool
func init() {
lintCommand.Flags().BoolVarP(&flagStrict, "strict", "", false, "fail on lint warnings")
RootCommand.AddCommand(lintCommand)
}
......@@ -59,6 +62,13 @@ func lintCmd(cmd *cobra.Command, args []string) error {
paths = args
}
var lowestTolerance int
if flagStrict {
lowestTolerance = support.WarningSev
} else {
lowestTolerance = support.ErrorSev
}
var total int
var failures int
for _, path := range paths {
......@@ -77,7 +87,7 @@ func lintCmd(cmd *cobra.Command, args []string) error {
}
total = total + 1
if linter.HighestSeverity >= support.ErrorSev {
if linter.HighestSeverity >= lowestTolerance {
failures = failures + 1
}
}
......
......@@ -17,17 +17,19 @@ limitations under the License.
package main
import (
"os"
"path/filepath"
"github.com/spf13/cobra"
"k8s.io/helm/pkg/repo"
)
var serveDesc = `This command starts a local chart repository server that serves the charts saved in your $HELM_HOME/local/ directory.`
//TODO: add repoPath flag to be passed in in case you want
// to serve charts from a different local dir
var serveDesc = `This command starts a local chart repository server that serves charts from a local directory.`
var repoPath string
func init() {
serveCmd.Flags().StringVar(&repoPath, "repo-path", localRepoDirectory(), "The local directory path from which to serve charts.")
RootCommand.AddCommand(serveCmd)
}
......@@ -35,9 +37,19 @@ var serveCmd = &cobra.Command{
Use: "serve",
Short: "start a local http web server",
Long: serveDesc,
Run: serve,
RunE: serve,
}
func serve(cmd *cobra.Command, args []string) {
repo.StartLocalRepo(localRepoDirectory())
func serve(cmd *cobra.Command, args []string) error {
repoPath, err := filepath.Abs(repoPath)
if err != nil {
return err
}
if _, err := os.Stat(repoPath); os.IsNotExist(err) {
return err
}
repo.StartLocalRepo(repoPath)
return nil
}
description: Deploy a basic Alpine Linux pod
home: https://k8s.io/helm
name: novals
sources:
- https://github.com/kubernetes/helm
version: 0.2.0
#Alpine: A simple Helm chart
Run a single pod of Alpine Linux.
This example was generated using the command `helm create alpine`.
The `templates/` directory contains a very simple pod resource with a
couple of parameters.
The `values.yaml` file contains the default values for the
`alpine-pod.yaml` template.
You can install this example using `helm install docs/examples/alpine`.
apiVersion: v1
kind: Pod
metadata:
name: "{{.Release.Name}}-{{.Values.Name}}"
labels:
# The "heritage" label is used to track which tool deployed a given chart.
# It is useful for admins who want to see what releases a particular tool
# is responsible for.
heritage: {{.Release.Service | quote }}
# The "release" convention makes it easy to tie a release to all of the
# Kubernetes resources that were created as part of that release.
release: {{.Release.Name | quote }}
# This makes it easy to audit chart usage.
chart: "{{.Chart.Name}}-{{.Chart.Version}}"
annotations:
"helm.sh/created": {{.Release.Time.Seconds | quote }}
spec:
# This shows how to use a simple value. This will look for a passed-in value
# called restartPolicy. If it is not found, it will use the default value.
# {{default "Never" .restartPolicy}} is a slightly optimized version of the
# more conventional syntax: {{.restartPolicy | default "Never"}}
restartPolicy: {{default "Never" .Values.restartPolicy}}
containers:
- name: waiter
image: "alpine:3.3"
command: ["/bin/sleep","9000"]
......@@ -39,6 +39,7 @@ type upgradeCmd struct {
out io.Writer
client helm.Interface
dryRun bool
disableHooks bool
valuesFile string
}
......@@ -70,6 +71,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command {
f := cmd.Flags()
f.StringVarP(&upgrade.valuesFile, "values", "f", "", "path to a values YAML file")
f.BoolVar(&upgrade.dryRun, "dry-run", false, "simulate an upgrade")
f.BoolVar(&upgrade.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks")
return cmd
}
......@@ -88,12 +90,13 @@ func (u *upgradeCmd) run() error {
}
}
_, err = u.client.UpdateRelease(u.release, chartPath, helm.UpdateValueOverrides(rawVals), helm.UpgradeDryRun(u.dryRun))
_, err = u.client.UpdateRelease(u.release, chartPath, helm.UpdateValueOverrides(rawVals), helm.UpgradeDryRun(u.dryRun), helm.UpgradeDisableHooks(u.disableHooks))
if err != nil {
return prettyError(err)
}
fmt.Fprintf(u.out, "It's not you. It's me\nYour upgrade looks valid but this command is still under active development.\nHang tight.\n")
success := u.release + " has been upgraded. Happy Helming!\n"
fmt.Fprintf(u.out, success)
return nil
......
......@@ -52,18 +52,22 @@ func TestUpgradeCmd(t *testing.T) {
Description: "A Helm chart for Kubernetes",
Version: "0.1.2",
}
chartPath, err = chartutil.Create(cfile, tmpChart)
if err != nil {
t.Errorf("Error creating chart: %v", err)
}
ch, _ = chartutil.Load(chartPath)
ch, err = chartutil.Load(chartPath)
if err != nil {
t.Errorf("Error loading updated chart: %v", err)
}
tests := []releaseCase{
{
name: "upgrade a release",
args: []string{"funny-bunny", chartPath},
resp: releaseMock(&releaseOptions{name: "funny-bunny", version: 2, chart: ch}),
expected: "It's not you. It's me\nYour upgrade looks valid but this command is still under active development.\nHang tight.\n",
expected: "funny-bunny has been upgraded. Happy Helming!\n",
},
}
......
......@@ -117,6 +117,15 @@ type KubeClient interface {
// For all other kinds, it means the kind was created or modified without
// error.
WatchUntilReady(namespace string, reader io.Reader) error
// Update updates one or more resources or creates the resource
// if it doesn't exist
//
// namespace must contain a valid existing namespace
//
// reader must contain a YAML stream (one or more YAML documents separated
// by "\n---\n").
Update(namespace string, originalReader, modifiedReader io.Reader) error
}
// PrintingKubeClient implements KubeClient, but simply prints the reader to
......@@ -145,6 +154,12 @@ func (p *PrintingKubeClient) WatchUntilReady(ns string, r io.Reader) error {
return err
}
// Update implements KubeClient Update.
func (p *PrintingKubeClient) Update(ns string, currentReader, modifiedReader io.Reader) error {
_, err := io.Copy(p.Out, modifiedReader)
return err
}
// Environment provides the context for executing a client request.
//
// All services in a context are concurrency safe.
......
......@@ -87,6 +87,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) Update(ns string, currentReader, modifiedReader io.Reader) error {
return nil
}
func (k *mockKubeClient) WatchUntilReady(ns string, r io.Reader) error {
return nil
}
......
......@@ -24,7 +24,6 @@ import (
"regexp"
"sort"
"github.com/Masterminds/semver"
"github.com/ghodss/yaml"
"github.com/technosophos/moniker"
ctx "golang.org/x/net/context"
......@@ -171,65 +170,107 @@ func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleas
}
func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) {
rel, err := s.prepareUpdate(req)
currentRelease, updatedRelease, err := s.prepareUpdate(req)
if err != nil {
return nil, err
}
// TODO: perform update
res, err := s.performUpdate(currentRelease, updatedRelease, req)
if err != nil {
return nil, err
}
if err := s.env.Releases.Update(updatedRelease); err != nil {
return nil, err
}
return res, nil
}
func (s *releaseServer) performUpdate(originalRelease, updatedRelease *release.Release, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) {
res := &services.UpdateReleaseResponse{Release: updatedRelease}
if req.DryRun {
log.Printf("Dry run for %s", updatedRelease.Name)
return res, nil
}
// pre-ugrade hooks
if !req.DisableHooks {
if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, preUpgrade); err != nil {
return res, err
}
}
kubeCli := s.env.KubeClient
original := bytes.NewBufferString(originalRelease.Manifest)
modified := bytes.NewBufferString(updatedRelease.Manifest)
if err := kubeCli.Update(updatedRelease.Namespace, original, modified); err != nil {
return nil, fmt.Errorf("Update of %s failed: %s", updatedRelease.Name, err)
}
// post-upgrade hooks
if !req.DisableHooks {
if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, postUpgrade); err != nil {
return res, err
}
}
return &services.UpdateReleaseResponse{Release: rel}, nil
updatedRelease.Info.Status.Code = release.Status_DEPLOYED
return res, nil
}
// prepareUpdate builds a release for an update operation.
func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*release.Release, error) {
// prepareUpdate builds an updated release for an update operation.
func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*release.Release, *release.Release, error) {
if req.Name == "" {
return nil, errMissingRelease
return nil, nil, errMissingRelease
}
if req.Chart == nil {
return nil, errMissingChart
return nil, nil, errMissingChart
}
// finds the non-deleted release with the given name
rel, err := s.env.Releases.Get(req.Name)
currentRelease, err := s.env.Releases.Get(req.Name)
if err != nil {
return nil, err
return nil, nil, err
}
//validate chart name is same as previous release
givenChart := req.Chart.Metadata.Name
releasedChart := rel.Chart.Metadata.Name
if givenChart != releasedChart {
return nil, fmt.Errorf("Given chart, %s, does not match chart originally released, %s", givenChart, releasedChart)
ts := timeconv.Now()
options := chartutil.ReleaseOptions{
Name: req.Name,
Time: ts,
Namespace: currentRelease.Namespace,
}
// validate new chart version is higher than old
givenChartVersion := req.Chart.Metadata.Version
releasedChartVersion := rel.Chart.Metadata.Version
c, err := semver.NewConstraint("> " + releasedChartVersion)
valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options)
if err != nil {
return nil, err
return nil, nil, err
}
v, err := semver.NewVersion(givenChartVersion)
hooks, manifestDoc, err := s.renderResources(req.Chart, valuesToRender)
if err != nil {
return nil, err
}
if a := c.Check(v); !a {
return nil, fmt.Errorf("Given chart (%s-%v) must be a higher version than released chart (%s-%v)", givenChart, givenChartVersion, releasedChart, releasedChartVersion)
return nil, nil, err
}
// Store an updated release.
updatedRelease := &release.Release{
Name: req.Name,
Namespace: currentRelease.Namespace,
Chart: req.Chart,
Config: req.Values,
Version: rel.Version + 1,
Info: &release.Info{
FirstDeployed: currentRelease.Info.FirstDeployed,
LastDeployed: ts,
Status: &release.Status{Code: release.Status_UNKNOWN},
},
Version: currentRelease.Version + 1,
Manifest: manifestDoc.String(),
Hooks: hooks,
}
return updatedRelease, nil
return currentRelease, updatedRelease, nil
}
func (s *releaseServer) uniqName(start string, reuse bool) (string, error) {
......@@ -240,10 +281,12 @@ func (s *releaseServer) uniqName(start string, reuse bool) (string, error) {
if start != "" {
if rel, err := s.env.Releases.Get(start); err == driver.ErrReleaseNotFound {
return start, nil
} else if reuse && rel.Info.Status.Code == release.Status_DELETED {
} else if st := rel.Info.Status.Code; reuse && (st == release.Status_DELETED || st == release.Status_FAILED) {
// Allowe re-use of names if the previous release is marked deleted.
log.Printf("reusing name %q", start)
return start, nil
} else if reuse {
return "", errors.New("cannot re-use a name that is still in use")
}
return "", fmt.Errorf("a release named %q already exists", start)
......@@ -306,12 +349,36 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re
return nil, err
}
renderer := s.engine(req.Chart)
files, err := renderer.Render(req.Chart, valuesToRender)
hooks, manifestDoc, err := s.renderResources(req.Chart, valuesToRender)
if err != nil {
return nil, err
}
// Store a release.
rel := &release.Release{
Name: name,
Namespace: req.Namespace,
Chart: req.Chart,
Config: req.Values,
Info: &release.Info{
FirstDeployed: ts,
LastDeployed: ts,
Status: &release.Status{Code: release.Status_UNKNOWN},
},
Manifest: manifestDoc.String(),
Hooks: hooks,
Version: 1,
}
return rel, nil
}
func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values) ([]*release.Hook, *bytes.Buffer, error) {
renderer := s.engine(ch)
files, err := renderer.Render(ch, values)
if err != nil {
return nil, nil, err
}
// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
// as partials are not used after renderer.Render. Empty manifests are also
// removed here.
......@@ -319,7 +386,7 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re
if err != nil {
// By catching parse errors here, we can prevent bogus releases from going
// to Kubernetes.
return nil, err
return nil, nil, err
}
// Aggregate all valid manifests into one big doc.
......@@ -329,22 +396,7 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re
b.WriteString(file)
}
// Store a release.
rel := &release.Release{
Name: name,
Namespace: req.Namespace,
Chart: req.Chart,
Config: req.Values,
Info: &release.Info{
FirstDeployed: ts,
LastDeployed: ts,
Status: &release.Status{Code: release.Status_UNKNOWN},
},
Manifest: b.String(),
Hooks: hooks,
Version: 1,
}
return rel, nil
return hooks, b, nil
}
// validateYAML checks to see if YAML is well-formed.
......
......@@ -45,6 +45,16 @@ data:
name: value
`
var manifestWithUpgradeHooks = `apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
annotations:
"helm.sh/hook": post-upgrade,pre-upgrade
data:
name: value
`
func rsFixture() *releaseServer {
return &releaseServer{
env: mockEnvironment(),
......@@ -82,6 +92,7 @@ func releaseStub() *release.Release {
},
Chart: chartStub(),
Config: &chart.Config{Raw: `name = "value"`},
Version: 1,
Hooks: []*release.Hook{
{
Name: "test-cm",
......@@ -290,6 +301,105 @@ func TestInstallReleaseReuseName(t *testing.T) {
}
}
func TestUpdateRelease(t *testing.T) {
c := context.Background()
rs := rsFixture()
rel := releaseStub()
rs.env.Releases.Create(rel)
req := &services.UpdateReleaseRequest{
Name: rel.Name,
Chart: &chart.Chart{
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "hello", Data: []byte("hello: world")},
{Name: "hooks", Data: []byte(manifestWithUpgradeHooks)},
},
},
}
res, err := rs.UpdateRelease(c, req)
if err != nil {
t.Errorf("Failed updated: %s", err)
}
if res.Release.Name == "" {
t.Errorf("Expected release name.")
}
if res.Release.Name != rel.Name {
t.Errorf("Updated release name does not match previous release name. Expected %s, got %s", rel.Name, res.Release.Name)
}
if res.Release.Namespace != rel.Namespace {
t.Errorf("Expected release namespace '%s', got '%s'.", rel.Namespace, res.Release.Namespace)
}
updated, err := rs.env.Releases.Get(res.Release.Name)
if err != nil {
t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases)
}
if len(updated.Hooks) != 1 {
t.Fatalf("Expected 1 hook, got %d", len(updated.Hooks))
}
if updated.Hooks[0].Manifest != manifestWithUpgradeHooks {
t.Errorf("Unexpected manifest: %v", updated.Hooks[0].Manifest)
}
if updated.Hooks[0].Events[0] != release.Hook_POST_UPGRADE {
t.Errorf("Expected event 0 to be post upgrade")
}
if updated.Hooks[0].Events[1] != release.Hook_PRE_UPGRADE {
t.Errorf("Expected event 0 to be pre upgrade")
}
if len(res.Release.Manifest) == 0 {
t.Errorf("No manifest returned: %v", res.Release)
}
if len(updated.Manifest) == 0 {
t.Errorf("Expected manifest in %v", res)
}
if !strings.Contains(updated.Manifest, "---\n# Source: hello/hello\nhello: world") {
t.Errorf("unexpected output: %s", rel.Manifest)
}
if res.Release.Version != 2 {
t.Errorf("Expected release version to be %v, got %v", 2, res.Release.Version)
}
}
func TestUpdateReleaseNoHooks(t *testing.T) {
c := context.Background()
rs := rsFixture()
rel := releaseStub()
rs.env.Releases.Create(rel)
req := &services.UpdateReleaseRequest{
Name: rel.Name,
DisableHooks: true,
Chart: &chart.Chart{
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "hello", Data: []byte("hello: world")},
{Name: "hooks", Data: []byte(manifestWithUpgradeHooks)},
},
},
}
res, err := rs.UpdateRelease(c, req)
if err != nil {
t.Errorf("Failed updated: %s", err)
}
if hl := res.Release.Hooks[0].LastRun; hl != nil {
t.Errorf("Expected that no hooks were run. Got %d", hl)
}
}
func TestUninstallRelease(t *testing.T) {
c := context.Background()
rs := rsFixture()
......
......@@ -75,9 +75,8 @@ The Go files generated from the `proto` definitions are stored in
Docker images are built by cross-compiling Linux binaries and then
building a Docker image from the files in `rootfs`.
The `scripts/` directory contains a number of utility scripts, including
`local-cluster.sh`, which can start a full Kubernetes instance inside of
a Docker container.
The `scripts/` directory contains a number of utility scripts. Most of these
are used by the CI/CD pipeline.
Go dependencies are managed with
[Glide](https://github.com/Masterminds/glide) and stored in the
......
......@@ -202,6 +202,11 @@ sensitive_.
- `Chart`: The contents of the `Chart.yaml`. Thus, the chart version is
obtainable as `Chart.Version` and the maintainers are in
`Chart.Maintainers`.
- `Files`: A map-like object containing all non-special files in the chart. This
will not give you access to templates, but will give you access to additional
files that are present. Files can be accessed using `{{index .Files "file.name"}}`
or using the `{{.Files.Get name}}` or `{{.Files.GetString name}}` functions. Note that
file data is returned as a `[]byte` unless `{{.Files.GetString}}` is used.
**NOTE:** Any unknown Chart.yaml fields will be dropped. They will not
be accessible inside of the `Chart` object. Thus, Chart.yaml cannot be
......
......@@ -66,11 +66,31 @@ GCR registry.
## Running a Local Cluster
You can run tests locally using the `scripts/local-cluster.sh` script to
start Kubernetes inside of a Docker container. For OS X, you will need
to be running `docker-machine`.
For development, we highly recommend using the
[Kubernetes Minikube](https://github.com/kubernetes/minikube)
developer-oriented distribution. Once this is installed, you can use
`helm init` to install into the cluster.
Tiller should run on any >= 1.2 Kubernetes cluster with beta extensions.
For developing on Tiller, it is sometimes more expedient to run Tiller locally
instead of packaging it into an image and running it in-cluster. You can do
this by telling the Helm client to us a local instance.
```console
$ make build
$ bin/tiller
```
And to configure the Helm client, use the `--host` flag or export the `HELM_HOST`
environment variable:
```console
$ export HELM_HOST=localhost:44134
$ helm install foo
```
(Note that you do not need to use `helm init` when you are running Tiller directly)
Tiller should run on any >= 1.3 Kubernetes cluster.
## Contribution Guidelines
......
hash: 141ef5b9c491c91b026ab4007e48502c9a6df9f173c40e1406233dd44f065190
updated: 2016-07-05T16:51:52.631048739-07:00
hash: d3f3df18316dca3703f5d073e8f9b1e6bfdb27e8d7fc9c5d742afeddebb022db
updated: 2016-07-30T23:52:42.581826208-07:00
imports:
- name: github.com/aokoli/goutils
version: 9c37978a95bd5c709a15883b6242714ea6709e64
- name: github.com/asaskevich/govalidator
version: df81827fdd59d8b4fb93d8910b286ab7a3919520
version: 7664702784775e51966f0885f5cd27435916517b
- name: github.com/beorn7/perks
version: 3ac7bf7a47d159a033b107610db8a1b6575507a4
subpackages:
......@@ -133,7 +133,7 @@ imports:
- ptypes/any
- ptypes/timestamp
- name: github.com/google/cadvisor
version: 4dbefc9b671b81257973a33211fb12370c1a526e
version: c2ea32971ae033041f0fb0f309b1dee94fd1d55f
subpackages:
- api
- cache/memory
......@@ -267,15 +267,15 @@ imports:
- name: google.golang.org/appengine
version: 12d5545dc1cfa6047a286d5e853841b6471f4c19
subpackages:
- urlfetch
- internal
- internal/urlfetch
- internal/app_identity
- internal/modules
- internal/base
- internal/datastore
- internal/log
- internal/modules
- internal/remote_api
- urlfetch
- internal/urlfetch
- name: google.golang.org/cloud
version: eb47ba841d53d93506cfbfbc03927daf9cc48f88
subpackages:
......@@ -297,40 +297,67 @@ imports:
- name: gopkg.in/yaml.v2
version: a83829b6f1293c91addabc89d0571c246397bbf4
- name: k8s.io/kubernetes
version: 283137936a498aed572ee22af6774b6fb6e9fd94
version: e7f022c926583ed8e755a52f23abc4cf8b532d12
subpackages:
- pkg/api
- pkg/api/meta
- pkg/api/error
- pkg/client/restclient
- pkg/client/unversioned
- pkg/apis/batch
- pkg/client/unversioned/clientcmd
- pkg/client/unversioned/fake
- pkg/client/unversioned/portforward
- pkg/client/unversioned/remotecommand
- pkg/kubectl
- pkg/kubectl/cmd/util
- pkg/kubectl/resource
- pkg/labels
- pkg/runtime
- pkg/watch
- pkg/api/errors
- pkg/client/unversioned/testclient
- pkg/api/meta/metatypes
- pkg/api/resource
- pkg/api/unversioned
- pkg/auth/user
- pkg/conversion
- pkg/fields
- pkg/runtime
- pkg/runtime/serializer
- pkg/types
- pkg/util
- pkg/util/intstr
- pkg/util/rand
- pkg/util/sets
- pkg/api/install
- pkg/apimachinery/registered
- pkg/apis/apps
- pkg/apis/apps/install
- pkg/apis/authentication.k8s.io/install
- pkg/apis/authorization/install
- pkg/apis/autoscaling
- pkg/apis/autoscaling/install
- pkg/apis/batch/install
- pkg/apis/batch/v2alpha1
- pkg/apis/componentconfig/install
- pkg/apis/extensions
- pkg/apis/extensions/install
- pkg/apis/policy
- pkg/apis/policy/install
- pkg/apis/rbac
- pkg/apis/rbac/install
- pkg/client/typed/discovery
- pkg/util/net
- pkg/util/wait
- pkg/version
- plugin/pkg/client/auth
- 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
......@@ -339,42 +366,53 @@ imports:
- pkg/util/httpstream/spdy
- federation/apis/federation
- federation/client/clientset_generated/federation_internalclientset
- pkg/api/service
- pkg/api/annotations
- pkg/api/util
- pkg/api/v1
- pkg/api/validation
- pkg/apimachinery
- pkg/apimachinery/registered
- pkg/apis/apps
- pkg/apis/autoscaling
- pkg/apis/batch
- pkg/apis/extensions
- pkg/apis/policy
- pkg/apis/rbac
- pkg/client/typed/discovery
- pkg/apis/batch/v1
- pkg/client/clientset_generated/internalclientset
- pkg/client/unversioned/adapters/internalclientset
- pkg/credentialprovider
- pkg/fieldpath
- pkg/kubelet/qos/util
- pkg/util/deployment
- pkg/util/integer
- pkg/util/jsonpath
- pkg/util/slice
- pkg/api/service
- pkg/apimachinery
- pkg/controller
- pkg/kubectl
- pkg/registry/thirdpartyresourcedata
- pkg/runtime/serializer/json
- pkg/util/flag
- pkg/util/strategicpatch
- pkg/watch
- pkg/util/yaml
- pkg/api/testapi
- third_party/forked/reflect
- pkg/conversion/queryparams
- pkg/util/json
- pkg/api/testapi
- third_party/forked/reflect
- pkg/runtime/serializer/protobuf
- pkg/runtime/serializer/recognizer
- pkg/runtime/serializer/versioning
- pkg/util/wait
- pkg/api/v1
- pkg/watch/versioned
- pkg/apis/apps/v1alpha1
- pkg/apis/authentication.k8s.io
- pkg/apis/authentication.k8s.io/v1beta1
- pkg/apis/authorization
- pkg/apis/authorization/v1beta1
- pkg/apis/autoscaling/v1
- pkg/apis/componentconfig
- pkg/apis/componentconfig/v1alpha1
- pkg/apis/extensions/v1beta1
- pkg/apis/policy/v1alpha1
- pkg/apis/rbac/v1alpha1
- pkg/client/metrics
- pkg/runtime/serializer/streaming
- pkg/util/crypto
- pkg/util/flowcontrol
- pkg/util/net
- pkg/version
- pkg/watch/versioned
- plugin/pkg/client/auth/gcp
- plugin/pkg/client/auth/oidc
- pkg/client/unversioned/clientcmd/api/v1
- pkg/httplog
- pkg/util/wsstream
......@@ -382,71 +420,35 @@ imports:
- federation/apis/federation/install
- federation/client/clientset_generated/federation_internalclientset/typed/core/unversioned
- federation/client/clientset_generated/federation_internalclientset/typed/federation/unversioned
- pkg/util/net/sets
- pkg/util/parsers
- pkg/api/endpoints
- pkg/api/pod
- pkg/api/unversioned/validation
- pkg/api/util
- pkg/capabilities
- pkg/api/install
- pkg/apis/apps/install
- pkg/apis/authentication.k8s.io/install
- pkg/apis/authorization/install
- pkg/apis/autoscaling/install
- pkg/apis/batch/install
- pkg/apis/batch/v2alpha1
- pkg/apis/componentconfig/install
- pkg/apis/extensions/install
- pkg/apis/policy/install
- pkg/apis/rbac/install
- plugin/pkg/client/auth
- pkg/client/clientset_generated/internalclientset
- pkg/client/clientset_generated/internalclientset/typed/autoscaling/unversioned
- pkg/client/clientset_generated/internalclientset/typed/batch/unversioned
- pkg/client/clientset_generated/internalclientset/typed/core/unversioned
- pkg/client/clientset_generated/internalclientset/typed/extensions/unversioned
- pkg/client/clientset_generated/internalclientset/typed/rbac/unversioned
- pkg/util/labels
- pkg/util/pod
- pkg/util/replicaset
- third_party/golang/template
- pkg/util/net/sets
- pkg/client/cache
- pkg/client/record
- pkg/controller/framework
- pkg/util/hash
- pkg/util/integer
- pkg/api/annotations
- pkg/apis/batch/v1
- pkg/credentialprovider
- pkg/fieldpath
- pkg/kubelet/qos/util
- pkg/util/deployment
- pkg/util/jsonpath
- pkg/util/slice
- pkg/api/rest
- pkg/apis/extensions/v1beta1
- pkg/apis/extensions/validation
- pkg/registry/generic
- pkg/util/framer
- third_party/forked/json
- pkg/util/parsers
- pkg/kubelet/qos
- pkg/master/ports
- federation/apis/federation/v1beta1
- pkg/apis/apps/v1alpha1
- pkg/apis/authentication.k8s.io
- pkg/apis/authentication.k8s.io/v1beta1
- pkg/apis/authorization
- pkg/apis/authorization/v1beta1
- pkg/apis/autoscaling/v1
- pkg/apis/componentconfig
- pkg/apis/componentconfig/v1alpha1
- pkg/apis/policy/v1alpha1
- pkg/apis/rbac/v1alpha1
- plugin/pkg/client/auth/gcp
- plugin/pkg/client/auth/oidc
- pkg/client/clientset_generated/internalclientset/typed/autoscaling/unversioned
- pkg/client/clientset_generated/internalclientset/typed/rbac/unversioned
- pkg/util/labels
- pkg/util/pod
- pkg/util/replicaset
- third_party/golang/template
- pkg/security/podsecuritypolicy/util
- pkg/storage
- pkg/kubelet/qos
- pkg/master/ports
- name: speter.net/go/exp/math/dec/inf
version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4
repo: https://github.com/go-inf/inf.git
......
......@@ -22,22 +22,32 @@ import:
- package: google.golang.org/grpc
version: dec33edc378cf4971a2741cfd86ed70a644d6ba3
- package: k8s.io/kubernetes
version: v1.3.0
version: ~1.3
subpackages:
- pkg/api
- pkg/api/meta
- pkg/api/error
- pkg/api/unversioned
- pkg/apimachinery/registered
- pkg/client/restclient
- pkg/client/unversioned
- pkg/apis/batch
- pkg/client/unversioned/clientcmd
- pkg/client/unversioned/fake
- pkg/client/unversioned/portforward
- pkg/client/unversioned/remotecommand
- pkg/kubectl
- pkg/kubectl/cmd/util
- pkg/kubectl/resource
- pkg/labels
- pkg/runtime
- pkg/watch
- pkg/util/strategicpatch
- pkg/util/yaml
- package: github.com/gosuri/uitable
- package: speter.net/go/exp/math/dec/inf
version: ^0.9.0
repo: https://github.com/go-inf/inf.git
vcs: git
- package: github.com/asaskevich/govalidator
- package: github.com/satori/go.uuid
version: ^4.0.0
......@@ -48,7 +48,23 @@ const defaultIgnore = `# 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
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj
`
// Create creates a new chart in a directory.
......
......@@ -32,11 +32,14 @@ func NewFiles(from []*any.Any) Files {
return files
}
// Get a file by path.
// GetBytes gets a file by path.
//
// The returned data is raw. In a template context, this is identical to calling
// {{index .Files $path}}.
//
// This is intended to be accessed from within a template, so a missed key returns
// an empty []byte.
func (f Files) Get(name string) []byte {
func (f Files) GetBytes(name string) []byte {
v, ok := f[name]
if !ok {
return []byte{}
......@@ -44,10 +47,12 @@ func (f Files) Get(name string) []byte {
return v
}
// GetString returns a string representation of the given file.
// Get returns a string representation of the given file.
//
// Fetch the contents of a file as a string. It is designed to be called in a
// template.
//
// This is a convenience for the otherwise cumbersome template logic
// for '{{.Files.Get "foo" | printf "%s"}}'.
func (f Files) GetString(name string) string {
return string(f.Get(name))
// {{.Files.Get "foo"}}
func (f Files) Get(name string) string {
return string(f.GetBytes(name))
}
......@@ -43,10 +43,10 @@ func TestNewFiles(t *testing.T) {
}
for i, f := range cases {
if got := string(files.Get(f.path)); got != f.data {
if got := string(files.GetBytes(f.path)); got != f.data {
t.Errorf("%d: expected %q, got %q", i, f.data, got)
}
if got := files.GetString(f.path); got != f.data {
if got := files.Get(f.path); got != f.data {
t.Errorf("%d: expected %q, got %q", i, f.data, got)
}
}
......
......@@ -218,6 +218,11 @@ func LoadDir(dir string) (*chart.Chart, error) {
return err
}
if fi.IsDir() {
// Directory-based ignore rules should involve skipping the entire
// contents of that directory.
if rules.Ignore(n, fi) {
return filepath.SkipDir
}
return nil
}
......
......@@ -49,8 +49,9 @@ func verifyChart(t *testing.T, c *chart.Chart) {
t.Errorf("Expected 1 template, got %d", len(c.Templates))
}
if len(c.Files) != 5 {
t.Errorf("Expected 5 extra files, got %d", len(c.Files))
numfiles := 6
if len(c.Files) != numfiles {
t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files))
for _, n := range c.Files {
t.Logf("\t%s", n.TypeUrl)
}
......
......@@ -9,4 +9,4 @@ tar -zcvf frobnitz/charts/mariner-4.3.2.tgz mariner
# Pack the frobnitz chart.
echo "Packing frobnitz"
tar -zcvf frobnitz-1.2.3.tgz frobnitz
tar --exclude=ignore/* -zcvf frobnitz-1.2.3.tgz frobnitz
......@@ -66,19 +66,13 @@ func Install(namespace, image string, verbose bool) error {
// InstallYAML is the installation YAML for DM.
const InstallYAML = `
---
apiVersion: v1
kind: ReplicationController
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: helm
name: tiller
name: tiller-rc
name: tiller-deploy
namespace: {{ .Namespace }}
spec:
replicas: 1
selector:
app: helm
name: tiller
template:
metadata:
labels:
......
......@@ -88,6 +88,28 @@ type renderable struct {
vals chartutil.Values
}
// alterFuncMap takes the Engine's FuncMap and adds context-specific functions.
//
// The resulting FuncMap is only valid for the passed-in template.
func (e *Engine) alterFuncMap(t *template.Template) template.FuncMap {
// Clone the func map because we are adding context-specific functions.
var funcMap template.FuncMap = map[string]interface{}{}
for k, v := range e.FuncMap {
funcMap[k] = v
}
// Add the 'include' function here so we can close over t.
funcMap["include"] = func(name string, data interface{}) string {
buf := bytes.NewBuffer(nil)
if err := t.ExecuteTemplate(buf, name, data); err != nil {
buf.WriteString(err.Error())
}
return buf.String()
}
return funcMap
}
// render takes a map of templates/values and renders them.
func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) {
// Basically, what we do here is start with an empty parent template and then
......@@ -105,10 +127,13 @@ func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) {
// but will still emit <no value> for others. We mitigate that later.
t.Option("missingkey=zero")
}
funcMap := e.alterFuncMap(t)
files := []string{}
for fname, r := range tpls {
log.Printf("Preparing template %s", fname)
t = t.New(fname).Funcs(e.FuncMap)
t = t.New(fname).Funcs(funcMap)
if _, err := t.Parse(r.tpl); err != nil {
return map[string]string{}, fmt.Errorf("parse error in %q: %s", fname, err)
}
......
......@@ -312,7 +312,7 @@ func TestRenderBuiltinValues(t *testing.T) {
Metadata: &chart.Metadata{Name: "Latium"},
Templates: []*chart.Template{
{Name: "Lavinia", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)},
{Name: "From", Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.GetString "book/title.txt"}}`)},
{Name: "From", Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)},
},
Values: &chart.Config{Raw: ``},
Dependencies: []*chart.Chart{},
......@@ -358,3 +358,33 @@ func TestRenderBuiltinValues(t *testing.T) {
}
}
func TestAlterFuncMap(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "conrad"},
Templates: []*chart.Template{
{Name: "quote", Data: []byte(`{{include "conrad/_partial" . | indent 2}} dead.`)},
{Name: "_partial", Data: []byte(`{{.Release.Name}} - he`)},
},
Values: &chart.Config{Raw: ``},
Dependencies: []*chart.Chart{},
}
v := chartutil.Values{
"Values": &chart.Config{Raw: ""},
"Chart": c.Metadata,
"Release": chartutil.Values{
"Name": "Mistah Kurtz",
},
}
out, err := New().Render(c, v)
if err != nil {
t.Fatal(err)
}
expect := " Mistah Kurtz - he dead."
if got := out["conrad/quote"]; got != expect {
t.Errorf("Expected %q, got %q (%v)", expect, got, out)
}
}
......@@ -143,6 +143,13 @@ func DeleteDryRun(dry bool) DeleteOption {
}
}
// UpgradeDisableHooks will disable hooks for an upgrade operation.
func UpgradeDisableHooks(disable bool) UpdateOption {
return func(opts *options) {
opts.disableHooks = disable
}
}
// UpgradeDryRun will (if true) execute an upgrade as a dry run.
func UpgradeDryRun(dry bool) UpdateOption {
return func(opts *options) {
......@@ -237,9 +244,15 @@ func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient,
// Executes tiller.UpdateRelease RPC.
func (o *options) rpcUpdateRelease(rlsName string, chr *cpb.Chart, rlc rls.ReleaseServiceClient, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) {
//TODO: handle dryRun
for _, opt := range opts {
opt(o)
}
o.updateReq.Chart = chr
o.updateReq.DryRun = o.dryRun
o.updateReq.Name = rlsName
return rlc.UpdateRelease(context.TODO(), &rls.UpdateReleaseRequest{Name: rlsName, Chart: chr})
return rlc.UpdateRelease(context.TODO(), &o.updateReq)
}
// Executes tiller.GetReleaseStatus RPC.
......
......@@ -65,7 +65,6 @@ func Parse(file io.Reader) (*Rules, error) {
if err := s.Err(); err != nil {
return r, err
}
return r, nil
}
......@@ -97,8 +96,10 @@ func (r *Rules) Ignore(path string, fi os.FileInfo) bool {
continue
}
// If the rule is looking for directories, and this is not a directory,
// skip it.
if p.mustDir && !fi.IsDir() {
return false
continue
}
if p.match(path, fi) {
return true
......
......@@ -20,15 +20,21 @@ import (
"fmt"
"io"
"log"
"reflect"
"time"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apimachinery/registered"
"k8s.io/kubernetes/pkg/apis/batch"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
"k8s.io/kubernetes/pkg/kubectl"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util/strategicpatch"
"k8s.io/kubernetes/pkg/util/yaml"
"k8s.io/kubernetes/pkg/watch"
)
......@@ -57,6 +63,77 @@ func (c *Client) Create(namespace string, reader io.Reader) error {
return perform(c, namespace, reader, createResource)
}
// Update reads in the current configuration and a modified configuration from io.reader
// and creates resources that don't already exists, updates resources that have been modified
// and deletes resources from the current configuration that are not present in the
// modified configuration
//
// Namespace will set the namespaces
func (c *Client) Update(namespace string, currentReader, modifiedReader io.Reader) error {
current := c.NewBuilder(includeThirdPartyAPIs).
ContinueOnError().
NamespaceParam(namespace).
DefaultNamespace().
Stream(currentReader, "").
Flatten().
Do()
modified := c.NewBuilder(includeThirdPartyAPIs).
ContinueOnError().
NamespaceParam(namespace).
DefaultNamespace().
Stream(modifiedReader, "").
Flatten().
Do()
currentInfos, err := current.Infos()
if err != nil {
return err
}
modifiedInfos := []*resource.Info{}
modified.Visit(func(info *resource.Info, err error) error {
modifiedInfos = append(modifiedInfos, info)
if err != nil {
return err
}
resourceName := info.Name
helper := resource.NewHelper(info.Client, info.Mapping)
if _, err := helper.Get(info.Namespace, resourceName, info.Export); err != nil {
if !errors.IsNotFound(err) {
return fmt.Errorf("Could not get information about the resource: err: %s", err)
}
// Since the resource does not exist, create it.
if err := createResource(info); err != nil {
return err
}
kind := info.Mapping.GroupVersionKind.Kind
log.Printf("Created a new %s called %s\n", kind, resourceName)
return nil
}
currentObj, err := getCurrentObject(resourceName, currentInfos)
if err != nil {
return err
}
if err := updateResource(info, currentObj); err != nil {
log.Printf("error updating the resource %s:\n\t %v", resourceName, err)
return err
}
return err
})
deleteUnwantedResources(currentInfos, modifiedInfos)
return nil
}
// Delete deletes kubernetes resources from an io.reader
//
// Namespace will set the namespace
......@@ -136,6 +213,51 @@ func createResource(info *resource.Info) error {
return err
}
func deleteResource(info *resource.Info) error {
return resource.NewHelper(info.Client, info.Mapping).Delete(info.Namespace, info.Name)
}
func updateResource(modified *resource.Info, currentObj runtime.Object) error {
encoder := api.Codecs.LegacyCodec(registered.EnabledVersions()...)
originalSerialization, err := runtime.Encode(encoder, currentObj)
if err != nil {
return err
}
editedSerialization, err := runtime.Encode(encoder, modified.Object)
if err != nil {
return err
}
originalJS, err := yaml.ToJSON(originalSerialization)
if err != nil {
return err
}
editedJS, err := yaml.ToJSON(editedSerialization)
if err != nil {
return err
}
if reflect.DeepEqual(originalJS, editedJS) {
return fmt.Errorf("Looks like there are no changes for %s", modified.Name)
}
patch, err := strategicpatch.CreateStrategicMergePatch(originalJS, editedJS, currentObj)
if err != nil {
return err
}
// send patch to server
helper := resource.NewHelper(modified.Client, modified.Mapping)
if _, err = helper.Patch(modified.Namespace, modified.Name, api.StrategicMergePatchType, patch); err != nil {
return err
}
return nil
}
func watchUntilReady(info *resource.Info) error {
w, err := resource.NewHelper(info.Client, info.Mapping).WatchSingle(info.Namespace, info.Name, info.ResourceVersion)
if err != nil {
......@@ -213,3 +335,37 @@ func (c *Client) ensureNamespace(namespace string) error {
}
return nil
}
func deleteUnwantedResources(currentInfos, modifiedInfos []*resource.Info) {
for _, cInfo := range currentInfos {
found := false
for _, m := range modifiedInfos {
if m.Name == cInfo.Name {
found = true
}
}
if !found {
log.Printf("Deleting %s...", cInfo.Name)
if err := deleteResource(cInfo); err != nil {
log.Printf("Failed to delete %s, err: %s", cInfo.Name, err)
}
}
}
}
func getCurrentObject(targetName string, infos []*resource.Info) (runtime.Object, error) {
var curr *resource.Info
for _, currInfo := range infos {
if currInfo.Name == targetName {
curr = currInfo
}
}
if curr == nil {
return nil, fmt.Errorf("No resource with the name %s found.", targetName)
}
encoder := api.Codecs.LegacyCodec(registered.EnabledVersions()...)
defaultVersion := unversioned.GroupVersion{}
return resource.AsVersionedObject([]*resource.Info{curr}, false, defaultVersion, encoder)
}
......@@ -17,15 +17,55 @@ limitations under the License.
package kube
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"strings"
"testing"
"k8s.io/kubernetes/pkg/api/meta"
"k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/api/unversioned"
api "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/client/unversioned/fake"
"k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/runtime"
)
func TestUpdateResource(t *testing.T) {
tests := []struct {
name string
namespace string
modified *resource.Info
currentObj runtime.Object
err bool
errMessage string
}{
{
name: "no changes when updating resources",
modified: createFakeInfo("nginx", nil),
currentObj: createFakePod("nginx", nil),
err: true,
errMessage: "Looks like there are no changes for nginx",
},
//{
//name: "valid update input",
//modified: createFakeInfo("nginx", map[string]string{"app": "nginx"}),
//currentObj: createFakePod("nginx", nil),
//},
}
for _, tt := range tests {
err := updateResource(tt.modified, tt.currentObj)
if err != nil && err.Error() != tt.errMessage {
t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err)
}
}
}
func TestPerform(t *testing.T) {
tests := []struct {
name string
......@@ -214,3 +254,53 @@ spec:
ports:
- containerPort: 80
`
func createFakePod(name string, labels map[string]string) runtime.Object {
objectMeta := createObjectMeta(name, labels)
object := &api.Pod{
ObjectMeta: objectMeta,
}
return object
}
func createFakeInfo(name string, labels map[string]string) *resource.Info {
pod := createFakePod(name, labels)
marshaledObj, _ := json.Marshal(pod)
mapping := &meta.RESTMapping{
Resource: name,
Scope: meta.RESTScopeNamespace,
GroupVersionKind: unversioned.GroupVersionKind{
Kind: "Pod",
Version: "v1",
}}
client := &fake.RESTClient{
Codec: testapi.Default.Codec(),
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
header := http.Header{}
header.Set("Content-Type", runtime.ContentTypeJSON)
return &http.Response{
StatusCode: 200,
Header: header,
Body: ioutil.NopCloser(bytes.NewReader(marshaledObj)),
}, nil
})}
info := resource.NewInfo(client, mapping, "default", "nginx", false)
info.Object = pod
return info
}
func createObjectMeta(name string, labels map[string]string) api.ObjectMeta {
objectMeta := api.ObjectMeta{Name: name, Namespace: "default"}
if labels != nil {
objectMeta.Labels = labels
}
return objectMeta
}
......@@ -51,7 +51,7 @@ func (c *Client) ForwardPort(namespace, podName string, remote int) (*Tunnel, er
}
// Build a url to the portforward endpoing
// example: http://localhost:8080/api/v1/namespaces/helm/pods/tiller-rc-9itlq/portforward
// example: http://localhost:8080/api/v1/namespaces/helm/pods/tiller-deploy-9itlq/portforward
u := client.RESTClient.Post().
Resource("pods").
Namespace(namespace).
......
This diff is collapsed.
......@@ -98,8 +98,7 @@ func LoadChartRepository(dir, url string) (*ChartRepository, error) {
return nil
}
r.IndexFile = i
} else {
// TODO: check for tgz extension
} else if strings.HasSuffix(f.Name(), ".tgz") {
r.ChartPaths = append(r.ChartPaths, path)
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment