Commit 85a91394 authored by kiich's avatar kiich Committed by GitHub

Merge branch 'master' into deploymentsReady-when-newRS-has-minimumReplicas

parents e3655bb1 eb4a187d
......@@ -46,4 +46,6 @@ message Hook {
repeated Event events = 5;
// LastRun indicates the date/time this was last run.
google.protobuf.Timestamp last_run = 6;
// Weight indicates the sort order for execution among similar Hook type
int32 weight = 7;
}
......@@ -203,6 +203,9 @@ message UpdateReleaseRequest {
// wait, if true, will wait until all Pods, PVCs, and Services are in a ready state
// before marking the release as successful. It will wait for as long as timeout
bool wait = 9;
// ReuseValues will cause Tiller to reuse the values from the last release.
// This is ignored if reset_values is set.
bool reuse_values = 10;
}
// UpdateReleaseResponse is the response to an update request.
......
......@@ -45,6 +45,14 @@ const (
tillerNamespaceEnvVar = "TILLER_NAMESPACE"
)
var (
tlsCaCertFile string // path to TLS CA certificate file
tlsCertFile string // path to TLS certificate file
tlsKeyFile string // path to TLS key file
tlsVerify bool // enable TLS and verify remote certificates
tlsEnable bool // enable TLS
)
var (
helmHome string
tillerHost string
......
......@@ -68,8 +68,10 @@ type initCmd struct {
upgrade bool
namespace string
dryRun bool
skipRefresh bool
out io.Writer
home helmpath.Home
opts installer.Options
kubeClient internalclientset.Interface
}
......@@ -98,26 +100,106 @@ func newInitCmd(out io.Writer) *cobra.Command {
f.BoolVar(&i.upgrade, "upgrade", false, "upgrade if tiller is already installed")
f.BoolVarP(&i.clientOnly, "client-only", "c", false, "if set does not install tiller")
f.BoolVar(&i.dryRun, "dry-run", false, "do not install local or remote")
f.BoolVar(&i.skipRefresh, "skip-refresh", false, "do not refresh (download) the local repository cache")
// f.BoolVar(&tlsEnable, "tiller-tls", false, "install tiller with TLS enabled")
// f.BoolVar(&tlsVerify, "tiller-tls-verify", false, "install tiller with TLS enabled and to verify remote certificates")
// f.StringVar(&tlsKeyFile, "tiller-tls-key", "", "path to TLS key file to install with tiller")
// f.StringVar(&tlsCertFile, "tiller-tls-cert", "", "path to TLS certificate file to install with tiller")
// f.StringVar(&tlsCaCertFile, "tls-ca-cert", "", "path to CA root certificate")
return cmd
}
// tlsOptions sanitizes the tls flags as well as checks for the existence of required
// tls files indicated by those flags, if any.
func (i *initCmd) tlsOptions() error {
i.opts.EnableTLS = tlsEnable || tlsVerify
i.opts.VerifyTLS = tlsVerify
if i.opts.EnableTLS {
missing := func(file string) bool {
_, err := os.Stat(file)
return os.IsNotExist(err)
}
if i.opts.TLSKeyFile = tlsKeyFile; i.opts.TLSKeyFile == "" || missing(i.opts.TLSKeyFile) {
return errors.New("missing required TLS key file")
}
if i.opts.TLSCertFile = tlsCertFile; i.opts.TLSCertFile == "" || missing(i.opts.TLSCertFile) {
return errors.New("missing required TLS certificate file")
}
if i.opts.VerifyTLS {
if i.opts.TLSCaCertFile = tlsCaCertFile; i.opts.TLSCaCertFile == "" || missing(i.opts.TLSCaCertFile) {
return errors.New("missing required TLS CA file")
}
}
}
return nil
}
// runInit initializes local config and installs tiller to Kubernetes Cluster
func (i *initCmd) run() error {
if err := i.tlsOptions(); err != nil {
return err
}
i.opts.Namespace = i.namespace
i.opts.UseCanary = i.canary
i.opts.ImageSpec = i.image
if flagDebug {
dm, err := installer.DeploymentManifest(i.namespace, i.image, i.canary)
if err != nil {
writeYAMLManifest := func(apiVersion, kind, body string, first, last bool) error {
w := i.out
if !first {
// YAML starting document boundary marker
if _, err := fmt.Fprintln(w, "---"); err != nil {
return err
}
}
if _, err := fmt.Fprintln(w, "apiVersion:", apiVersion); err != nil {
return err
}
if _, err := fmt.Fprintln(w, "kind:", kind); err != nil {
return err
}
if _, err := fmt.Fprint(w, body); err != nil {
return err
}
if !last {
return nil
}
// YAML ending document boundary marker
_, err := fmt.Fprintln(w, "...")
return err
}
fm := fmt.Sprintf("apiVersion: extensions/v1beta1\nkind: Deployment\n%s", dm)
fmt.Fprintln(i.out, fm)
sm, err := installer.ServiceManifest(i.namespace)
if err != nil {
var body string
var err error
// write Deployment manifest
if body, err = installer.DeploymentManifest(&i.opts); err != nil {
return err
}
if err := writeYAMLManifest("extensions/v1beta1", "Deployment", body, true, false); err != nil {
return err
}
// write Service manifest
if body, err = installer.ServiceManifest(i.namespace); err != nil {
return err
}
fm = fmt.Sprintf("apiVersion: v1\nkind: Service\n%s", sm)
fmt.Fprintln(i.out, fm)
if err := writeYAMLManifest("v1", "Service", body, false, !i.opts.EnableTLS); err != nil {
return err
}
// write Secret manifest
if i.opts.EnableTLS {
if body, err = installer.SecretManifest(&i.opts); err != nil {
return err
}
if err := writeYAMLManifest("v1", "Secret", body, false, true); err != nil {
return err
}
}
}
if i.dryRun {
......@@ -127,7 +209,7 @@ func (i *initCmd) run() error {
if err := ensureDirectories(i.home, i.out); err != nil {
return err
}
if err := ensureDefaultRepos(i.home, i.out); err != nil {
if err := ensureDefaultRepos(i.home, i.out, i.skipRefresh); err != nil {
return err
}
if err := ensureRepoFileFormat(i.home.RepositoryFile(), i.out); err != nil {
......@@ -143,13 +225,12 @@ func (i *initCmd) run() error {
}
i.kubeClient = c
}
opts := &installer.Options{Namespace: i.namespace, ImageSpec: i.image, UseCanary: i.canary}
if err := installer.Install(i.kubeClient, opts); err != nil {
if err := installer.Install(i.kubeClient, &i.opts); err != nil {
if !kerrors.IsAlreadyExists(err) {
return fmt.Errorf("error installing: %s", err)
}
if i.upgrade {
if err := installer.Upgrade(i.kubeClient, opts); err != nil {
if err := installer.Upgrade(i.kubeClient, &i.opts); err != nil {
return fmt.Errorf("error when upgrading: %s", err)
}
fmt.Fprintln(i.out, "\nTiller (the helm server side component) has been upgraded to the current version.")
......@@ -194,12 +275,12 @@ func ensureDirectories(home helmpath.Home, out io.Writer) error {
return nil
}
func ensureDefaultRepos(home helmpath.Home, out io.Writer) error {
func ensureDefaultRepos(home helmpath.Home, out io.Writer, skipRefresh bool) error {
repoFile := home.RepositoryFile()
if fi, err := os.Stat(repoFile); err != nil {
fmt.Fprintf(out, "Creating %s \n", repoFile)
f := repo.NewRepoFile()
sr, err := initStableRepo(home.CacheIndex(stableRepository))
sr, err := initStableRepo(home.CacheIndex(stableRepository), skipRefresh)
if err != nil {
return err
}
......@@ -218,7 +299,7 @@ func ensureDefaultRepos(home helmpath.Home, out io.Writer) error {
return nil
}
func initStableRepo(cacheFile string) (*repo.Entry, error) {
func initStableRepo(cacheFile string, skipRefresh bool) (*repo.Entry, error) {
c := repo.Entry{
Name: stableRepository,
URL: stableRepositoryURL,
......@@ -229,6 +310,10 @@ func initStableRepo(cacheFile string) (*repo.Entry, error) {
return nil, err
}
if skipRefresh {
return &c, nil
}
// In this case, the cacheFile is always absolute. So passing empty string
// is safe.
if err := r.DownloadIndexFile(""); err != nil {
......
......@@ -156,13 +156,19 @@ func TestInitCmd_dryRun(t *testing.T) {
if err := cmd.run(); err != nil {
t.Fatal(err)
}
if len(fc.Actions()) != 0 {
t.Error("expected no server calls")
if got := len(fc.Actions()); got != 0 {
t.Errorf("expected no server calls, got %d", got)
}
docs := bytes.Split(buf.Bytes(), []byte("\n---"))
if got, want := len(docs), 2; got != want {
t.Fatalf("Expected document count of %d, got %d", want, got)
}
for _, doc := range docs {
var y map[string]interface{}
if err := yaml.Unmarshal(buf.Bytes(), &y); err != nil {
t.Errorf("Expected parseable YAML, got %q\n\t%s", buf.String(), err)
if err := yaml.Unmarshal(doc, &y); err != nil {
t.Errorf("Expected parseable YAML, got %q\n\t%s", doc, err)
}
}
}
......@@ -179,7 +185,10 @@ func TestEnsureHome(t *testing.T) {
if err := ensureDirectories(hh, b); err != nil {
t.Error(err)
}
if err := ensureDefaultRepos(hh, b); err != nil {
if err := ensureDefaultRepos(hh, b, false); err != nil {
t.Error(err)
}
if err := ensureDefaultRepos(hh, b, true); err != nil {
t.Error(err)
}
if err := ensureRepoFileFormat(hh.RepositoryFile(), b); err != nil {
......
......@@ -17,7 +17,7 @@ limitations under the License.
package installer // import "k8s.io/helm/cmd/helm/installer"
import (
"fmt"
"io/ioutil"
"github.com/ghodss/yaml"
......@@ -28,22 +28,23 @@ import (
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
extensionsclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/extensions/internalversion"
"k8s.io/kubernetes/pkg/util/intstr"
"k8s.io/helm/pkg/version"
)
const defaultImage = "gcr.io/kubernetes-helm/tiller"
// Install uses kubernetes client to install tiller.
//
// Returns an error if the command failed.
func Install(client internalclientset.Interface, opts *Options) error {
if err := createDeployment(client.Extensions(), opts.Namespace, opts.ImageSpec, opts.UseCanary); err != nil {
if err := createDeployment(client.Extensions(), opts); err != nil {
return err
}
if err := createService(client.Core(), opts.Namespace); err != nil {
return err
}
if opts.tls() {
if err := createSecret(client.Core(), opts); err != nil {
return err
}
}
return nil
}
......@@ -55,7 +56,7 @@ func Upgrade(client internalclientset.Interface, opts *Options) error {
if err != nil {
return err
}
obj.Spec.Template.Spec.Containers[0].Image = selectImage(opts.ImageSpec, opts.UseCanary)
obj.Spec.Template.Spec.Containers[0].Image = opts.selectImage()
if _, err := client.Extensions().Deployments(opts.Namespace).Update(obj); err != nil {
return err
}
......@@ -73,15 +74,15 @@ func Upgrade(client internalclientset.Interface, opts *Options) error {
}
// createDeployment creates the Tiller deployment reource
func createDeployment(client extensionsclient.DeploymentsGetter, namespace, image string, canary bool) error {
obj := deployment(namespace, image, canary)
func createDeployment(client extensionsclient.DeploymentsGetter, opts *Options) error {
obj := deployment(opts)
_, err := client.Deployments(obj.Namespace).Create(obj)
return err
}
// deployment gets the deployment object that installs Tiller.
func deployment(namespace, image string, canary bool) *extensions.Deployment {
return generateDeployment(namespace, selectImage(image, canary))
func deployment(opts *Options) *extensions.Deployment {
return generateDeployment(opts)
}
// createService creates the Tiller service resource
......@@ -96,21 +97,10 @@ func service(namespace string) *api.Service {
return generateService(namespace)
}
func selectImage(image string, canary bool) string {
switch {
case canary:
image = defaultImage + ":canary"
case image == "":
image = fmt.Sprintf("%s:%s", defaultImage, version.Version)
}
return image
}
// DeploymentManifest gets the manifest (as a string) that describes the Tiller Deployment
// resource.
func DeploymentManifest(namespace, image string, canary bool) (string, error) {
obj := deployment(namespace, image, canary)
func DeploymentManifest(opts *Options) (string, error) {
obj := deployment(opts)
buf, err := yaml.Marshal(obj)
return string(buf), err
}
......@@ -129,11 +119,11 @@ func generateLabels(labels map[string]string) map[string]string {
return labels
}
func generateDeployment(namespace, image string) *extensions.Deployment {
func generateDeployment(opts *Options) *extensions.Deployment {
labels := generateLabels(map[string]string{"name": "tiller"})
d := &extensions.Deployment{
ObjectMeta: api.ObjectMeta{
Namespace: namespace,
Namespace: opts.Namespace,
Name: "tiller-deploy",
Labels: labels,
},
......@@ -147,13 +137,13 @@ func generateDeployment(namespace, image string) *extensions.Deployment {
Containers: []api.Container{
{
Name: "tiller",
Image: image,
Image: opts.selectImage(),
ImagePullPolicy: "IfNotPresent",
Ports: []api.ContainerPort{
{ContainerPort: 44134, Name: "tiller"},
},
Env: []api.EnvVar{
{Name: "TILLER_NAMESPACE", Value: namespace},
{Name: "TILLER_NAMESPACE", Value: opts.Namespace},
},
LivenessProbe: &api.Probe{
Handler: api.Handler{
......@@ -181,6 +171,37 @@ func generateDeployment(namespace, image string) *extensions.Deployment {
},
},
}
if opts.tls() {
const certsDir = "/etc/certs"
var tlsVerify, tlsEnable = "", "1"
if opts.VerifyTLS {
tlsVerify = "1"
}
// Mount secret to "/etc/certs"
d.Spec.Template.Spec.Containers[0].VolumeMounts = append(d.Spec.Template.Spec.Containers[0].VolumeMounts, api.VolumeMount{
Name: "tiller-certs",
ReadOnly: true,
MountPath: certsDir,
})
// Add environment variable required for enabling TLS
d.Spec.Template.Spec.Containers[0].Env = append(d.Spec.Template.Spec.Containers[0].Env, []api.EnvVar{
{Name: "TILLER_TLS_VERIFY", Value: tlsVerify},
{Name: "TILLER_TLS_ENABLE", Value: tlsEnable},
{Name: "TILLER_TLS_CERTS", Value: certsDir},
}...)
// Add secret volume to deployment
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, api.Volume{
Name: "tiller-certs",
VolumeSource: api.VolumeSource{
Secret: &api.SecretVolumeSource{
SecretName: "tiller-secret",
},
},
})
}
return d
}
......@@ -206,3 +227,54 @@ func generateService(namespace string) *api.Service {
}
return s
}
// SecretManifest gets the manifest (as a string) that describes the Tiller Secret resource.
func SecretManifest(opts *Options) (string, error) {
o, err := generateSecret(opts)
if err != nil {
return "", err
}
buf, err := yaml.Marshal(o)
return string(buf), err
}
// createSecret creates the Tiller secret resource.
func createSecret(client internalversion.SecretsGetter, opts *Options) error {
o, err := generateSecret(opts)
if err != nil {
return err
}
_, err = client.Secrets(o.Namespace).Create(o)
return err
}
// generateSecret builds the secret object that hold Tiller secrets.
func generateSecret(opts *Options) (*api.Secret, error) {
const secretName = "tiller-secret"
labels := generateLabels(map[string]string{"name": "tiller"})
secret := &api.Secret{
Type: api.SecretTypeOpaque,
Data: make(map[string][]byte),
ObjectMeta: api.ObjectMeta{
Name: secretName,
Labels: labels,
Namespace: opts.Namespace,
},
}
var err error
if secret.Data["tls.key"], err = read(opts.TLSKeyFile); err != nil {
return nil, err
}
if secret.Data["tls.crt"], err = read(opts.TLSCertFile); err != nil {
return nil, err
}
if opts.VerifyTLS {
if secret.Data["ca.crt"], err = read(opts.TLSCaCertFile); err != nil {
return nil, err
}
}
return secret, nil
}
func read(path string) (b []byte, err error) { return ioutil.ReadFile(path) }
......@@ -45,7 +45,7 @@ func TestDeploymentManifest(t *testing.T) {
}
for _, tt := range tests {
o, err := DeploymentManifest(api.NamespaceDefault, tt.image, tt.canary)
o, err := DeploymentManifest(&Options{Namespace: api.NamespaceDefault, ImageSpec: tt.image, UseCanary: tt.canary})
if err != nil {
t.Fatalf("%s: error %q", tt.name, err)
}
......@@ -146,7 +146,11 @@ func TestInstall_canary(t *testing.T) {
func TestUpgrade(t *testing.T) {
image := "gcr.io/kubernetes-helm/tiller:v2.0.0"
existingDeployment := deployment(api.NamespaceDefault, "imageToReplace", false)
existingDeployment := deployment(&Options{
Namespace: api.NamespaceDefault,
ImageSpec: "imageToReplace",
UseCanary: false,
})
existingService := service(api.NamespaceDefault)
fc := &fake.Clientset{}
......@@ -178,7 +182,11 @@ func TestUpgrade(t *testing.T) {
func TestUpgrade_serviceNotFound(t *testing.T) {
image := "gcr.io/kubernetes-helm/tiller:v2.0.0"
existingDeployment := deployment(api.NamespaceDefault, "imageToReplace", false)
existingDeployment := deployment(&Options{
Namespace: api.NamespaceDefault,
ImageSpec: "imageToReplace",
UseCanary: false,
})
fc := &fake.Clientset{}
fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) {
......
......@@ -16,6 +16,13 @@ limitations under the License.
package installer // import "k8s.io/helm/cmd/helm/installer"
import (
"fmt"
"k8s.io/helm/pkg/version"
)
const defaultImage = "gcr.io/kubernetes-helm/tiller"
// Options control how to install tiller into a cluster, upgrade, and uninstall tiller from a cluster.
type Options struct {
// EnableTLS instructs tiller to serve with TLS enabled.
......@@ -43,7 +50,7 @@ type Options struct {
// key tiller should use.
//
// Required and valid if and only if EnableTLS or VerifyTLS is set.
TLSKey string
TLSKeyFile string
// TLSCertFile identifies the file containing the pem encoded TLS
// certificate tiller should use.
......@@ -57,3 +64,16 @@ type Options struct {
// Required and valid if and only if VerifyTLS is set.
TLSCaCertFile string
}
func (opts *Options) selectImage() string {
switch {
case opts.UseCanary:
return defaultImage + ":canary"
case opts.ImageSpec == "":
return fmt.Sprintf("%s:%s", defaultImage, version.Version)
default:
return opts.ImageSpec
}
}
func (opts *Options) tls() bool { return opts.EnableTLS || opts.VerifyTLS }
......@@ -55,7 +55,11 @@ func (f *fakeReaperFactory) Reaper(mapping *meta.RESTMapping) (kubectl.Reaper, e
func TestUninstall(t *testing.T) {
existingService := service(api.NamespaceDefault)
existingDeployment := deployment(api.NamespaceDefault, "image", false)
existingDeployment := deployment(&Options{
Namespace: api.NamespaceDefault,
ImageSpec: "image",
UseCanary: false,
})
fc := &fake.Clientset{}
fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) {
......@@ -92,7 +96,7 @@ func TestUninstall(t *testing.T) {
}
func TestUninstall_serviceNotFound(t *testing.T) {
existingDeployment := deployment(api.NamespaceDefault, "imageToReplace", false)
existingDeployment := deployment(&Options{Namespace: api.NamespaceDefault, ImageSpec: "imageToReplace", UseCanary: false})
fc := &fake.Clientset{}
fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) {
......
......@@ -54,6 +54,8 @@ type packageCmd struct {
key string
keyring string
version string
destination string
out io.Writer
home helmpath.Home
}
......@@ -96,6 +98,7 @@ func newPackageCmd(out io.Writer) *cobra.Command {
f.StringVar(&pkg.key, "key", "", "name of the key to use when signing. Used if --sign is true")
f.StringVar(&pkg.keyring, "keyring", defaultKeyring(), "location of a public keyring")
f.StringVar(&pkg.version, "version", "", "set the version on the chart to this semver version")
f.StringVarP(&pkg.destination, "destination", "d", ".", "location to write the chart.")
return cmd
}
......@@ -129,12 +132,19 @@ func (p *packageCmd) run(cmd *cobra.Command, args []string) error {
checkDependencies(ch, reqs, p.out)
}
var dest string
if p.destination == "." {
// Save to the current working directory.
cwd, err := os.Getwd()
dest, err = os.Getwd()
if err != nil {
return err
}
name, err := chartutil.Save(ch, cwd)
} else {
// Otherwise save to set destination
dest = p.destination
}
name, err := chartutil.Save(ch, dest)
if err == nil && flagDebug {
fmt.Fprintf(p.out, "Saved %s to current directory\n", name)
}
......
......@@ -94,6 +94,20 @@ func TestPackage(t *testing.T) {
expect: "",
hasfile: "alpine-0.1.0.tgz",
},
{
name: "package --destination toot",
args: []string{"testdata/testcharts/alpine"},
flags: map[string]string{"destination": "toot"},
expect: "",
hasfile: "toot/alpine-0.1.0.tgz",
},
{
name: "package --destination does-not-exist",
args: []string{"testdata/testcharts/alpine"},
flags: map[string]string{"destination": "does-not-exist"},
expect: "stat does-not-exist: no such file or directory",
err: true,
},
{
name: "package --sign --key=KEY --keyring=KEYRING testdata/testcharts/alpine",
args: []string{"testdata/testcharts/alpine"},
......@@ -124,6 +138,10 @@ func TestPackage(t *testing.T) {
t.Fatal(err)
}
if err := os.Mkdir("toot", 0777); err != nil {
t.Fatal(err)
}
ensureTestHome(helmpath.Home(tmp), t)
oldhome := homePath()
helmHome = tmp
......
......@@ -72,6 +72,7 @@ type upgradeCmd struct {
version string
timeout int64
resetValues bool
reuseValues bool
wait bool
}
......@@ -114,6 +115,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command {
f.StringVar(&upgrade.version, "version", "", "specify the exact chart version to use. If this is not specified, the latest version is used")
f.Int64Var(&upgrade.timeout, "timeout", 300, "time in seconds to wait for any individual kubernetes operation (like Jobs for hooks)")
f.BoolVar(&upgrade.resetValues, "reset-values", false, "when upgrading, reset the values to the ones built into the chart")
f.BoolVar(&upgrade.reuseValues, "reuse-values", false, "when upgrading, reuse the last release's values, and merge in any new values. If '--reset-values' is specified, this is ignored.")
f.BoolVar(&upgrade.wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout")
f.MarkDeprecated("disable-hooks", "use --no-hooks instead")
......@@ -177,6 +179,7 @@ func (u *upgradeCmd) run() error {
helm.UpgradeDisableHooks(u.disableHooks),
helm.UpgradeTimeout(u.timeout),
helm.ResetValues(u.resetValues),
helm.ReuseValues(u.reuseValues),
helm.UpgradeWait(u.wait))
if err != nil {
return fmt.Errorf("UPGRADE FAILED: %v", prettyError(err))
......
......@@ -109,6 +109,13 @@ func TestUpgradeCmd(t *testing.T) {
resp: releaseMock(&releaseOptions{name: "funny-bunny", version: 4, chart: ch2}),
expected: "Release \"funny-bunny\" has been upgraded. Happy Helming!\n",
},
{
name: "upgrade a release with --reuse-values",
args: []string{"funny-bunny", chartPath},
flags: []string{"--reuse-values", "true"},
resp: releaseMock(&releaseOptions{name: "funny-bunny", version: 5, chart: ch2}),
expected: "Release \"funny-bunny\" has been upgraded. Happy Helming!\n",
},
{
name: "install a release with 'upgrade --install'",
args: []string{"zany-bunny", chartPath},
......
......@@ -16,6 +16,8 @@ This will match version 1.2.0 and any patches to that release (1.2.1, 1.2.999, a
Where possible, use `https://` repository URLs, followed by `http://` URLs.
If the repository has been added to the repository index file, the repository name can be used as an alias of URL. Use `alias:` or `@` followed by repository names.
File URLs (`file://...`) are considered a "special case" for charts that are assembled by a fixed deployment pipeline. Charts that use `file://` in a `requirements.yaml` file are not allowed in the official Helm repository.
## Conditions and Tags
......
......@@ -137,7 +137,7 @@ data:
salad: {{ .Values.global.salad }}
```
`mysubchart/tempaltes/configmap.yaml`:
`mysubchart/templates/configmap.yaml`:
```yaml
apiVersion: v1
......
......@@ -302,6 +302,107 @@ helm install --set tags.front-end=true --set subchart2.enabled=false
* The `tags:` key in values must be a top level key. Globals and nested `tags:` tables
are not currently supported.
#### Importing Child Values via requirements.yaml
In some cases it is desirable to allow a child chart's values to propagate to the parent chart and be
shared as common defaults. An additional benefit of using the `exports` format is that it will enable future
tooling to introspect user-settable values.
The keys containing the values to be imported can be specified in the parent chart's `requirements.yaml` file
using a YAML list. Each item in the list is a key which is imported from the child chart's `exports` field.
To import values not contained in the `exports` key, use the [child/parent](#using-the-child/parent-format) format.
Examples of both formats are described below.
##### Using the exports format
If a child chart's `values.yaml` file contains an `exports` field at the root, its contents may be imported
directly into the parent's values by specifying the keys to import as in the example below:
```yaml
# parent's requirements.yaml file
...
import-values:
- data
```
```yaml
# child's values.yaml file
...
exports:
data:
myint: 99
```
Since we are specifying the key `data` in our import list, Helm looks in the the `exports` field of the child
chart for `data` key and imports its contents.
The final parent values would contain our exported field:
```yaml
# parent's values file
...
myint: 99
```
Please note the parent key `data` is not contained in the parent's final values. If you need to specify the
parent key, use the 'child/parent' format.
##### Using the child/parent format
To access values that are not contained in the `exports` key of the child chart's values, you will need to
specify the source key of the values to be imported (`child`) and the destination path in the parent chart's
values (`parent`).
The `import-values` in the example below instructs Helm to take any values found at `child:` path and copy them
to the parent's values at the path specified in `parent:`
```yaml
# parent's requirements.yaml file
dependencies:
- name: subchart1
repository: http://localhost:10191
version: 0.1.0
...
import-values:
- child: default.data
parent: myimports
```
In the above example, values found at `default.data` in the subchart1's values will be imported
to the `myimports` key in the parent chart's values as detailed below:
```yaml
# parent's values.yaml file
myimports:
myint: 0
mybool: false
mystring: "helm rocks!"
```
```yaml
# subchart1's values.yaml file
default:
data:
myint: 999
mybool: true
```
The parent chart's resulting values would be:
```yaml
# parent's final values
myimports:
myint: 999
mybool: true
mystring: "helm rocks!"
```
The parent's final values now contains the `myint` and `mybool` fields imported from subchart1.
## Templates and Values
Helm Chart templates are written in the
......
......@@ -58,16 +58,18 @@ hooks, the lifecycle is altered like this:
1. User runs `helm install foo`
2. Chart is loaded into Tiller
3. After some verification, Tiller renders the `foo` templates
4. Tiller executes the `pre-install` hook (loading hook resources into
4. Tiller prepares to execute the `pre-install` hooks (loading hook resources into
Kubernetes)
5. Tiller waits until the hook is "Ready"
6. Tiller loads the resulting resources into Kubernetes. Note that if the `--wait`
5. Tiller sorts hooks by weight (assigning a weight of 0 by default) and by name for those hooks with the same weight in ascending order.
6. Tiller then loads the hook with the lowest weight first (negative to positive)
7. Tiller waits until the hook is "Ready"
8. Tiller loads the resulting resources into Kubernetes. Note that if the `--wait`
flag is set, Tiller will wait until all resources are in a ready state
and will not run the `post-install` hook until they are ready.
7. Tiller executes the `post-install` hook (loading hook resources)
8. Tiller waits until the hook is "Ready"
9. Tiller returns the release name (and other data) to the client
10. The client exits
9. Tiller executes the `post-install` hook (loading hook resources)
10. Tiller waits until the hook is "Ready"
11. Tiller returns the release name (and other data) to the client
12. The client exits
What does it mean to wait until a hook is ready? This depends on the
resource declared in the hook. If the resources is a `Job` kind, Tiller
......@@ -114,6 +116,7 @@ metadata:
# 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
"helm.sh/hook-weight": "-5"
spec:
template:
metadata:
......@@ -154,3 +157,12 @@ When subcharts declare hooks, those are also evaluated. There is no way
for a top-level chart to disable the hooks declared by subcharts. And
again, there is no guaranteed ordering.
It is also possible to define a weight for a hook which will help build a deterministic executing order. Weights are defined using the following annotation:
```
annotations:
"helm.sh/hook-weight": "5"
```
Hook weights can be positive or negative numbers but must be represented as strings. When Tiller starts the execution cycle of hooks of a particular Kind it will sort those hooks in ascending order.
......@@ -36,8 +36,20 @@ The 'version' field should contain a semantic version or version range.
The 'repository' URL should point to a Chart Repository. Helm expects that by
appending '/index.yaml' to the URL, it should be able to retrieve the chart
repository's index. Note: 'repository' cannot be a repository alias. It must be
a URL.
repository's index.
A repository can also be represented by a repository name defined in the index file
in lieu of a repository URL. If a repository alias is used, it is expected to start with
'alias:' or '@', followed by a repository name. For example,
# requirements.yaml
dependencies:
- name: nginx
version: "1.2.3"
repository: "alias:stable"
Note: In the above example, if the '@' syntax is used, the repository alias '@stable'
must be quoted, as YAML requires to use quotes if the value includes a special character
like '@'.
Starting from 2.2.0, repository can be defined as the path to the directory of
the dependency charts stored locally. The path should start with a prefix of
......@@ -53,7 +65,6 @@ If the dependency chart is retrieved locally, it is not required to have the
repository added to helm by "helm add repo". Version matching is also supported
for this case.
### Options inherited from parent commands
```
......
......@@ -8,17 +8,20 @@ or [pull request](https://github.com/kubernetes/helm/pulls).
## Article, Blogs, How-Tos, and Extra Documentation
- [Using Helm to Deploy to Kubernetes](https://daemonza.github.io/2017/02/20/using-helm-to-deploy-to-kubernetes/)
- [Honestbee's Helm Chart Conventions](https://gist.github.com/so0k/f927a4b60003cedd101a0911757c605a)
- [Deploying Kubernetes Applications with Helm](http://cloudacademy.com/blog/deploying-kubernetes-applications-with-helm/)
- [Releasing backward-incompatible changes: Kubernetes, Jenkins, Prometheus Operator, Helm and Traefik](https://medium.com/@enxebre/releasing-backward-incompatible-changes-kubernetes-jenkins-plugin-prometheus-operator-helm-self-6263ca61a1b1#.e0c7elxhq)
- [CI/CD with Kubernetes, Helm & Wercker ](http://www.slideshare.net/Diacode/cicd-with-kubernetes-helm-wercker-madscalability)
- [The missing CI/CD Kubernetes component: Helm package manager](https://hackernoon.com/the-missing-ci-cd-kubernetes-component-helm-package-manager-1fe002aac680#.691sk2zhu)
- [CI/CD with Jenkins, Kubernetes, and Helm](https://www.youtube.com/watch?v=NVoln4HdZOY)
- [The Workflow "Umbrella" Helm Chart](https://deis.com/blog/2017/workflow-chart-assembly)
- [GitLab, Consumer Driven Contracts, Helm and Kubernetes](https://medium.com/@enxebre/gitlab-consumer-driven-contracts-helm-and-kubernetes-b7235a60a1cb#.xwp1y4tgi)
- [Writing a Helm Chart](https://www.influxdata.com/packaged-kubernetes-deployments-writing-helm-chart/)
- [Creating a Helm Plugin in 3 Steps](http://technosophos.com/2017/03/21/creating-a-helm-plugin.html)
## Videos
## Video, Audio, and Podcast
- [CI/CD with Jenkins, Kubernetes, and Helm](https://www.youtube.com/watch?v=NVoln4HdZOY): AKA "The Infamous Croc Hunter Video".
- [KubeCon2016: Delivering Kubernetes-Native Applications by Michelle Noorali](https://www.youtube.com/watch?v=zBc1goRfk3k&index=49&list=PLj6h78yzYM2PqgIGU1Qmi8nY7dqn9PCr4)
- [Helm with Michelle Noorali and Matthew Butcher](https://gcppodcast.com/post/episode-50-helm-with-michelle-noorali-and-matthew-butcher/): The official Google CloudPlatform Podcast interviews Michelle and Matt about Helm.
## Helm Plugins
......@@ -32,6 +35,7 @@ or [pull request](https://github.com/kubernetes/helm/pulls).
Tools layered on top of Helm or Tiller.
- [Quay App Registry](https://coreos.com/blog/quay-application-registry-for-kubernetes.html) - Open Kubernetes application registry, including a Helm access client
- [Chartify](https://github.com/appscode/chartify) - Generate Helm charts from existing Kubernetes resources.
- [VIM-Kubernetes](https://github.com/andrewstuart/vim-kubernetes) - VIM plugin for Kubernetes and Helm
- [Landscaper](https://github.com/Eneco/landscaper/) - "Landscaper takes a set of Helm Chart references with values (a desired state), and realizes this in a Kubernetes cluster."
......
hash: 0d1c5b7304a853820dcaa296d3aa1f5f3466a8491dcef80cbcaf43c954acb2a8
updated: 2017-03-15T15:56:18.814305691-06:00
hash: df0fa621e6a6f80dbfeb815d9d8aa308c50346a9821e401b19b6f10782da3774
updated: 2017-04-03T17:00:07.670429885-06:00
imports:
- name: cloud.google.com/go
version: 3b1ae45394a234c385be014e9a488f2bb6eef821
......@@ -182,7 +182,7 @@ imports:
- jlexer
- jwriter
- name: github.com/Masterminds/semver
version: 59c29afe1a994eacb71c833025ca7acf874bb1da
version: 3f0ab6d4ab4bed1c61caf056b63a6e62190c7801
- name: github.com/Masterminds/sprig
version: 23597e5f6ad0e4d590e71314bfd0251a4a3cf849
- name: github.com/mattn/go-runewidth
......@@ -300,7 +300,7 @@ imports:
- name: gopkg.in/yaml.v2
version: a83829b6f1293c91addabc89d0571c246397bbf4
- name: k8s.io/kubernetes
version: 00a1fb254bd8e5235575fba1398b958943e39078
version: ea8f6637b639246faa14a8d5c6f864100fcb77a9
subpackages:
- cmd/kubeadm/app/apis/kubeadm
- cmd/kubeadm/app/apis/kubeadm/install
......
......@@ -12,7 +12,7 @@ import:
version: ^2.10
- package: github.com/ghodss/yaml
- package: github.com/Masterminds/semver
version: ~1.2.2
version: ~1.2.3
- package: github.com/technosophos/moniker
- package: github.com/golang/protobuf
version: df1d3ca07d2d07bba352d5b73c4313b4e2a6203e
......
......@@ -62,6 +62,9 @@ type Dependency struct {
Tags []string `json:"tags"`
// Enabled bool determines if chart should be loaded
Enabled bool `json:"enabled"`
// ImportValues holds the mapping of source values to parent key to be imported. Each item can be a
// string or pair of child/parent sublist items.
ImportValues []interface{} `json:"import-values"`
}
// ErrNoRequirementsFile to detect error condition
......@@ -266,3 +269,128 @@ func ProcessRequirementsEnabled(c *chart.Chart, v *chart.Config) error {
return nil
}
// pathToMap creates a nested map given a YAML path in dot notation.
func pathToMap(path string, data map[string]interface{}) map[string]interface{} {
if path == "." {
return data
}
ap := strings.Split(path, ".")
if len(ap) == 0 {
return nil
}
n := []map[string]interface{}{}
// created nested map for each key, adding to slice
for _, v := range ap {
nm := make(map[string]interface{})
nm[v] = make(map[string]interface{})
n = append(n, nm)
}
// find the last key (map) and set our data
for i, d := range n {
for k := range d {
z := i + 1
if z == len(n) {
n[i][k] = data
break
}
n[i][k] = n[z]
}
}
return n[0]
}
// getParents returns a slice of parent charts in reverse order.
func getParents(c *chart.Chart, out []*chart.Chart) []*chart.Chart {
if len(out) == 0 {
out = []*chart.Chart{c}
}
for _, ch := range c.Dependencies {
if len(ch.Dependencies) > 0 {
out = append(out, ch)
out = getParents(ch, out)
}
}
return out
}
// processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field.
func processImportValues(c *chart.Chart, v *chart.Config) error {
reqs, err := LoadRequirements(c)
if err != nil {
return err
}
// combine chart values and its dependencies' values
cvals, err := CoalesceValues(c, v)
if err != nil {
return err
}
nv := v.GetValues()
b := make(map[string]interface{}, len(nv))
// convert values to map
for kk, vvv := range nv {
b[kk] = vvv
}
// import values from each dependency if specified in import-values
for _, r := range reqs.Dependencies {
if len(r.ImportValues) > 0 {
var outiv []interface{}
for _, riv := range r.ImportValues {
switch iv := riv.(type) {
case map[string]interface{}:
nm := map[string]string{
"child": iv["child"].(string),
"parent": iv["parent"].(string),
}
outiv = append(outiv, nm)
s := r.Name + "." + nm["child"]
// get child table
vv, err := cvals.Table(s)
if err != nil {
log.Printf("Warning: ImportValues missing table: %v", err)
continue
}
// create value map from child to be merged into parent
vm := pathToMap(nm["parent"], vv.AsMap())
b = coalesceTables(cvals, vm)
case string:
nm := map[string]string{
"child": "exports." + iv,
"parent": ".",
}
outiv = append(outiv, nm)
s := r.Name + "." + nm["child"]
vm, err := cvals.Table(s)
if err != nil {
log.Printf("Warning: ImportValues missing table: %v", err)
continue
}
b = coalesceTables(b, vm.AsMap())
}
}
// set our formatted import values
r.ImportValues = outiv
}
}
b = coalesceTables(b, cvals)
y, err := yaml.Marshal(b)
if err != nil {
return err
}
// set the new values
c.Values.Raw = string(y)
return nil
}
// ProcessRequirementsImportValues imports specified chart values from child to parent.
func ProcessRequirementsImportValues(c *chart.Chart, v *chart.Config) error {
pc := getParents(c, nil)
for i := len(pc) - 1; i >= 0; i-- {
processImportValues(pc[i], v)
}
return nil
}
......@@ -18,6 +18,8 @@ import (
"sort"
"testing"
"strconv"
"k8s.io/helm/pkg/proto/hapi/chart"
)
......@@ -206,3 +208,114 @@ func extractCharts(c *chart.Chart, out []*chart.Chart) []*chart.Chart {
}
return out
}
func TestProcessRequirementsImportValues(t *testing.T) {
c, err := Load("testdata/subpop")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
v := &chart.Config{Raw: ""}
e := make(map[string]string)
e["imported-chart1.SC1bool"] = "true"
e["imported-chart1.SC1float"] = "3.14"
e["imported-chart1.SC1int"] = "100"
e["imported-chart1.SC1string"] = "dollywood"
e["imported-chart1.SC1extra1"] = "11"
e["imported-chart1.SPextra1"] = "helm rocks"
e["imported-chart1.SC1extra1"] = "11"
e["imported-chartA.SCAbool"] = "false"
e["imported-chartA.SCAfloat"] = "3.1"
e["imported-chartA.SCAint"] = "55"
e["imported-chartA.SCAstring"] = "jabba"
e["imported-chartA.SPextra3"] = "1.337"
e["imported-chartA.SC1extra2"] = "1.337"
e["imported-chartA.SCAnested1.SCAnested2"] = "true"
e["imported-chartA-B.SCAbool"] = "false"
e["imported-chartA-B.SCAfloat"] = "3.1"
e["imported-chartA-B.SCAint"] = "55"
e["imported-chartA-B.SCAstring"] = "jabba"
e["imported-chartA-B.SCBbool"] = "true"
e["imported-chartA-B.SCBfloat"] = "7.77"
e["imported-chartA-B.SCBint"] = "33"
e["imported-chartA-B.SCBstring"] = "boba"
e["imported-chartA-B.SPextra5"] = "k8s"
e["imported-chartA-B.SC1extra5"] = "tiller"
e["overridden-chart1.SC1bool"] = "false"
e["overridden-chart1.SC1float"] = "3.141592"
e["overridden-chart1.SC1int"] = "99"
e["overridden-chart1.SC1string"] = "pollywog"
e["overridden-chart1.SPextra2"] = "42"
e["overridden-chartA.SCAbool"] = "true"
e["overridden-chartA.SCAfloat"] = "41.3"
e["overridden-chartA.SCAint"] = "808"
e["overridden-chartA.SCAstring"] = "jaberwocky"
e["overridden-chartA.SPextra4"] = "true"
e["overridden-chartA-B.SCAbool"] = "true"
e["overridden-chartA-B.SCAfloat"] = "41.3"
e["overridden-chartA-B.SCAint"] = "808"
e["overridden-chartA-B.SCAstring"] = "jaberwocky"
e["overridden-chartA-B.SCBbool"] = "false"
e["overridden-chartA-B.SCBfloat"] = "1.99"
e["overridden-chartA-B.SCBint"] = "77"
e["overridden-chartA-B.SCBstring"] = "jango"
e["overridden-chartA-B.SPextra6"] = "111"
e["overridden-chartA-B.SCAextra1"] = "23"
e["overridden-chartA-B.SCBextra1"] = "13"
e["overridden-chartA-B.SC1extra6"] = "77"
// `exports` style
e["SCBexported1B"] = "1965"
e["SC1extra7"] = "true"
e["SCBexported2A"] = "blaster"
e["global.SC1exported2.all.SC1exported3"] = "SC1expstr"
verifyRequirementsImportValues(t, c, v, e)
}
func verifyRequirementsImportValues(t *testing.T, c *chart.Chart, v *chart.Config, e map[string]string) {
err := ProcessRequirementsImportValues(c, v)
if err != nil {
t.Errorf("Error processing import values requirements %v", err)
}
cv := c.GetValues()
cc, err := ReadValues([]byte(cv.Raw))
if err != nil {
t.Errorf("Error reading import values %v", err)
}
for kk, vv := range e {
pv, err := cc.PathValue(kk)
if err != nil {
t.Fatalf("Error retrieving import values table %v %v", kk, err)
return
}
switch pv.(type) {
case float64:
s := strconv.FormatFloat(pv.(float64), 'f', -1, 64)
if s != vv {
t.Errorf("Failed to match imported float value %v with expected %v", s, vv)
return
}
case bool:
b := strconv.FormatBool(pv.(bool))
if b != vv {
t.Errorf("Failed to match imported bool value %v with expected %v", b, vv)
return
}
default:
if pv.(string) != vv {
t.Errorf("Failed to match imported string value %v with expected %v", pv, vv)
return
}
}
}
}
# Default values for subchart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: nginx
tag: stable
pullPolicy: IfNotPresent
# subchartA
service:
name: nginx
type: ClusterIP
externalPort: 80
internalPort: 80
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
SCAdata:
SCAbool: false
SCAfloat: 3.1
SCAint: 55
SCAstring: "jabba"
SCAnested1:
SCAnested2: true
# Default values for subchart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: nginx
tag: stable
pullPolicy: IfNotPresent
service:
name: nginx
type: ClusterIP
externalPort: 80
internalPort: 80
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
SCBdata:
SCBbool: true
SCBfloat: 7.77
SCBint: 33
SCBstring: "boba"
exports:
SCBexported1:
SCBexported1A:
SCBexported1B: 1965
SCBexported2:
SCBexported2A: "blaster"
global:
kolla:
nova:
api:
all:
port: 8774
metadata:
all:
port: 8775
......@@ -6,10 +6,27 @@ dependencies:
tags:
- front-end
- subcharta
import-values:
- child: SCAdata
parent: imported-chartA
- child: SCAdata
parent: overridden-chartA
- child: SCAdata
parent: imported-chartA-B
- name: subchartb
repository: http://localhost:10191
version: 0.1.0
condition: subchartb.enabled
import-values:
- child: SCBdata
parent: imported-chartB
- child: SCBdata
parent: imported-chartA-B
- child: exports.SCBexported2
parent: exports.SCBexported2
- SCBexported1
tags:
- front-end
- subchartb
# Default values for subchart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: nginx
tag: stable
pullPolicy: IfNotPresent
# subchart1
service:
name: nginx
type: ClusterIP
externalPort: 80
internalPort: 80
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
SC1data:
SC1bool: true
SC1float: 3.14
SC1int: 100
SC1string: "dollywood"
SC1extra1: 11
imported-chartA:
SC1extra2: 1.337
overridden-chartA:
SCAbool: true
SCAfloat: 3.14
SCAint: 100
SCAstring: "jabathehut"
SC1extra3: true
imported-chartA-B:
SC1extra5: "tiller"
overridden-chartA-B:
SCAbool: true
SCAfloat: 3.33
SCAint: 555
SCAstring: "wormwood"
SCAextra1: 23
SCBbool: true
SCBfloat: 0.25
SCBint: 98
SCBstring: "murkwood"
SCBextra1: 13
SC1extra6: 77
SCBexported1A:
SC1extra7: true
exports:
SC1exported1:
global:
SC1exported2:
all:
SC1exported3: "SC1expstr"
\ No newline at end of file
......@@ -6,6 +6,22 @@ dependencies:
tags:
- front-end
- subchart1
import-values:
- child: SC1data
parent: imported-chart1
- child: SC1data
parent: overridden-chart1
- child: imported-chartA
parent: imported-chartA
- child: imported-chartA-B
parent: imported-chartA-B
- child: overridden-chartA-B
parent: overridden-chartA-B
- child: SCBexported1A
parent: .
- SCBexported2
- SC1exported1
- name: subchart2
repository: http://localhost:10191
version: 0.1.0
......
# parent/values.yaml
# switch-like
imported-chart1:
SPextra1: "helm rocks"
overridden-chart1:
SC1bool: false
SC1float: 3.141592
SC1int: 99
SC1string: "pollywog"
SPextra2: 42
imported-chartA:
SPextra3: 1.337
overridden-chartA:
SCAbool: true
SCAfloat: 41.3
SCAint: 808
SCAstring: "jaberwocky"
SPextra4: true
imported-chartA-B:
SPextra5: "k8s"
overridden-chartA-B:
SCAbool: true
SCAfloat: 41.3
SCAint: 808
SCAstring: "jaberwocky"
SCBbool: false
SCBfloat: 1.99
SCBint: 77
SCBstring: "jango"
SPextra6: 111
tags:
front-end: true
back-end: false
......
......@@ -124,6 +124,13 @@ func (m *Manager) Update() error {
}
return err
}
// Hash requirements.yaml
hash, err := resolver.HashReq(req)
if err != nil {
return err
}
// Check that all of the repos we're dependent on actually exist and
// the repo index names.
repoNames, err := m.getRepoNames(req.Dependencies)
......@@ -140,7 +147,7 @@ func (m *Manager) Update() error {
// Now we need to find out which version of a chart best satisfies the
// requirements the requirements.yaml
lock, err := m.resolve(req, repoNames)
lock, err := m.resolve(req, repoNames, hash)
if err != nil {
return err
}
......@@ -172,9 +179,9 @@ func (m *Manager) loadChartDir() (*chart.Chart, error) {
// resolve takes a list of requirements and translates them into an exact version to download.
//
// This returns a lock file, which has all of the requirements normalized to a specific version.
func (m *Manager) resolve(req *chartutil.Requirements, repoNames map[string]string) (*chartutil.RequirementsLock, error) {
func (m *Manager) resolve(req *chartutil.Requirements, repoNames map[string]string, hash string) (*chartutil.RequirementsLock, error) {
res := resolver.New(m.ChartPath, m.HelmHome)
return res.Resolve(req, repoNames)
return res.Resolve(req, repoNames, hash)
}
// downloadAll takes a list of dependencies and downloads them into charts/
......@@ -325,14 +332,7 @@ func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string,
for _, dd := range deps {
// if dep chart is from local path, verify the path is valid
if strings.HasPrefix(dd.Repository, "file://") {
depPath, err := filepath.Abs(strings.TrimPrefix(dd.Repository, "file://"))
if err != nil {
return nil, err
}
if _, err = os.Stat(depPath); os.IsNotExist(err) {
return nil, fmt.Errorf("directory %s not found", depPath)
} else if err != nil {
if _, err := resolver.GetLocalPath(dd.Repository, m.ChartPath); err != nil {
return nil, err
}
......@@ -346,7 +346,13 @@ func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string,
found := false
for _, repo := range repos {
if urlutil.Equal(repo.URL, dd.Repository) {
if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) ||
(strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) {
found = true
dd.Repository = repo.URL
reposMap[dd.Name] = repo.Name
break
} else if urlutil.Equal(repo.URL, dd.Repository) {
found = true
reposMap[dd.Name] = repo.Name
break
......@@ -537,17 +543,11 @@ func tarFromLocalDir(chartpath string, name string, repo string, version string)
return "", fmt.Errorf("wrong format: chart %s repository %s", name, repo)
}
origPath, err := filepath.Abs(strings.TrimPrefix(repo, "file://"))
origPath, err := resolver.GetLocalPath(repo, chartpath)
if err != nil {
return "", err
}
if _, err = os.Stat(origPath); os.IsNotExist(err) {
return "", fmt.Errorf("directory %s not found: %s", origPath, err)
} else if err != nil {
return "", err
}
ch, err := chartutil.LoadDir(origPath)
if err != nil {
return "", err
......
......@@ -120,6 +120,20 @@ func TestGetRepoNames(t *testing.T) {
},
expect: map[string]string{"local-dep": "file://./testdata/signtest"},
},
{
name: "repo alias (alias:)",
req: []*chartutil.Dependency{
{Name: "oedipus-rex", Repository: "alias:testing"},
},
expect: map[string]string{"oedipus-rex": "testing"},
},
{
name: "repo alias (@)",
req: []*chartutil.Dependency{
{Name: "oedipus-rex", Repository: "@testing"},
},
expect: map[string]string{"oedipus-rex": "testing"},
},
}
for _, tt := range tests {
......
......@@ -97,6 +97,10 @@ func (h *Client) InstallReleaseFromChart(chart *chart.Chart, ns string, opts ...
if err != nil {
return nil, err
}
err = chartutil.ProcessRequirementsImportValues(req.Chart, req.Values)
if err != nil {
return nil, err
}
return h.install(ctx, req)
}
......@@ -155,6 +159,7 @@ func (h *Client) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts
req.DisableHooks = h.opts.disableHooks
req.Recreate = h.opts.recreate
req.ResetValues = h.opts.resetValues
req.ReuseValues = h.opts.reuseValues
ctx := NewContext()
if h.opts.before != nil {
......@@ -166,6 +171,10 @@ func (h *Client) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts
if err != nil {
return nil, err
}
err = chartutil.ProcessRequirementsImportValues(req.Chart, req.Values)
if err != nil {
return nil, err
}
return h.update(ctx, req)
}
......
......@@ -87,7 +87,9 @@ func TestListReleases_VerifyOptions(t *testing.T) {
return errSkip
})
NewClient(b4c).ListReleases(ops...)
if _, err := NewClient(b4c).ListReleases(ops...); err != errSkip {
t.Fatalf("did not expect error but got (%v)\n``", err)
}
}
// Verify InstallOption's are applied to an InstallReleaseRequest correctly.
......@@ -99,6 +101,7 @@ func TestInstallRelease_VerifyOptions(t *testing.T) {
var reuseName = true
var dryRun = true
var chartName = "alpine"
var chartPath = filepath.Join(chartsDir, chartName)
var overrides = []byte("key1=value1,key2=value2")
// Expected InstallReleaseRequest message
......@@ -133,7 +136,9 @@ func TestInstallRelease_VerifyOptions(t *testing.T) {
return errSkip
})
NewClient(b4c).InstallRelease(chartName, namespace, ops...)
if _, err := NewClient(b4c).InstallRelease(chartPath, namespace, ops...); err != errSkip {
t.Fatalf("did not expect error but got (%v)\n``", err)
}
}
// Verify DeleteOptions's are applied to an UninstallReleaseRequest correctly.
......@@ -168,13 +173,16 @@ func TestDeleteRelease_VerifyOptions(t *testing.T) {
return errSkip
})
NewClient(b4c).DeleteRelease(releaseName, ops...)
if _, err := NewClient(b4c).DeleteRelease(releaseName, ops...); err != errSkip {
t.Fatalf("did not expect error but got (%v)\n``", err)
}
}
// Verify UpdateOption's are applied to an UpdateReleaseRequest correctly.
func TestUpdateRelease_VerifyOptions(t *testing.T) {
// Options testdata
var chartName = "alpine"
var chartPath = filepath.Join(chartsDir, chartName)
var releaseName = "test"
var disableHooks = true
var overrides = []byte("key1=value1,key2=value2")
......@@ -208,7 +216,9 @@ func TestUpdateRelease_VerifyOptions(t *testing.T) {
return errSkip
})
NewClient(b4c).UpdateRelease(releaseName, chartName, ops...)
if _, err := NewClient(b4c).UpdateRelease(releaseName, chartPath, ops...); err != errSkip {
t.Fatalf("did not expect error but got (%v)\n``", err)
}
}
// Verify RollbackOption's are applied to a RollbackReleaseRequest correctly.
......@@ -246,7 +256,9 @@ func TestRollbackRelease_VerifyOptions(t *testing.T) {
return errSkip
})
NewClient(b4c).RollbackRelease(releaseName, ops...)
if _, err := NewClient(b4c).RollbackRelease(releaseName, ops...); err != errSkip {
t.Fatalf("did not expect error but got (%v)\n``", err)
}
}
// Verify StatusOption's are applied to a GetReleaseStatusRequest correctly.
......@@ -273,7 +285,9 @@ func TestReleaseStatus_VerifyOptions(t *testing.T) {
return errSkip
})
NewClient(b4c).ReleaseStatus(releaseName, StatusReleaseVersion(revision))
if _, err := NewClient(b4c).ReleaseStatus(releaseName, StatusReleaseVersion(revision)); err != errSkip {
t.Fatalf("did not expect error but got (%v)\n``", err)
}
}
// Verify ContentOption's are applied to a GetReleaseContentRequest correctly.
......@@ -300,7 +314,9 @@ func TestReleaseContent_VerifyOptions(t *testing.T) {
return errSkip
})
NewClient(b4c).ReleaseContent(releaseName, ContentReleaseVersion(revision))
if _, err := NewClient(b4c).ReleaseContent(releaseName, ContentReleaseVersion(revision)); err != errSkip {
t.Fatalf("did not expect error but got (%v)\n``", err)
}
}
func assert(t *testing.T, expect, actual interface{}) {
......
......@@ -66,6 +66,8 @@ type options struct {
histReq rls.GetHistoryRequest
// resetValues instructs Tiller to reset values to their defaults.
resetValues bool
// reuseValues instructs Tiller to reuse the values from the last release.
reuseValues bool
// release test options are applied directly to the test release history request
testReq rls.TestReleaseRequest
}
......@@ -323,6 +325,13 @@ func ResetValues(reset bool) UpdateOption {
}
}
// ReuseValues will (if true) trigger resetting the values to their original state.
func ReuseValues(reuse bool) UpdateOption {
return func(opts *options) {
opts.reuseValues = reuse
}
}
// UpgradeRecreate will (if true) recreate pods after upgrade.
func UpgradeRecreate(recreate bool) UpdateOption {
return func(opts *options) {
......
......@@ -23,6 +23,9 @@ import (
// HookAnno is the label name for a hook
const HookAnno = "helm.sh/hook"
// HookWeightAnno is the label name for a hook weight
const HookWeightAnno = "helm.sh/hook-weight"
// Types of hooks
const (
PreInstall = "pre-install"
......
......@@ -159,11 +159,14 @@ func (c *Client) Get(namespace string, reader io.Reader) (string, error) {
if err != nil {
return "", err
}
missing := []string{}
err = perform(c, namespace, infos, func(info *resource.Info) error {
log.Printf("Doing get for: '%s'", info.Name)
log.Printf("Doing get for %s: %q", info.Mapping.GroupVersionKind.Kind, info.Name)
obj, err := resource.NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name, info.Export)
if err != nil {
return err
log.Printf("WARNING: Failed Get for resource %q: %s", info.Name, err)
missing = append(missing, fmt.Sprintf("%v\t\t%s", info.Mapping.Resource, info.Name))
return nil
}
// We need to grab the ObjectReference so we can correctly group the objects.
or, err := api.GetReference(obj)
......@@ -194,7 +197,7 @@ func (c *Client) Get(namespace string, reader io.Reader) (string, error) {
}
for _, o := range ot {
if err := p.PrintObj(o, buf); err != nil {
log.Printf("failed to print object type '%s', object: '%s' :\n %v", t, o, err)
log.Printf("failed to print object type %s, object: %q :\n %v", t, o, err)
return "", err
}
}
......@@ -202,6 +205,12 @@ func (c *Client) Get(namespace string, reader io.Reader) (string, error) {
return "", err
}
}
if len(missing) > 0 {
buf.WriteString("==> MISSING\nKIND\t\tNAME\n")
for _, s := range missing {
fmt.Fprintln(buf, s)
}
}
return buf.String(), nil
}
......@@ -241,17 +250,17 @@ func (c *Client) Update(namespace string, originalReader, targetReader io.Reader
}
kind := info.Mapping.GroupVersionKind.Kind
log.Printf("Created a new %s called %s\n", kind, info.Name)
log.Printf("Created a new %s called %q\n", kind, info.Name)
return nil
}
originalInfo := original.Get(info)
if originalInfo == nil {
return fmt.Errorf("no resource with the name %s found", info.Name)
return fmt.Errorf("no resource with the name %q found", info.Name)
}
if err := updateResource(c, info, originalInfo.Object, recreate); err != nil {
log.Printf("error updating the resource %s:\n\t %v", info.Name, err)
log.Printf("error updating the resource %q:\n\t %v", info.Name, err)
updateErrors = append(updateErrors, err.Error())
}
......@@ -266,9 +275,9 @@ func (c *Client) Update(namespace string, originalReader, targetReader io.Reader
}
for _, info := range original.Difference(target) {
log.Printf("Deleting %s in %s...", info.Name, info.Namespace)
log.Printf("Deleting %q in %s...", info.Name, info.Namespace)
if err := deleteResource(c, info); err != nil {
log.Printf("Failed to delete %s, err: %s", info.Name, err)
log.Printf("Failed to delete %q, err: %s", info.Name, err)
}
}
if shouldWait {
......@@ -286,7 +295,7 @@ func (c *Client) Delete(namespace string, reader io.Reader) error {
return err
}
return perform(c, namespace, infos, func(info *resource.Info) error {
log.Printf("Starting delete for %s %s", info.Name, info.Mapping.GroupVersionKind.Kind)
log.Printf("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind)
err := deleteResource(c, info)
return skipIfNotFound(err)
})
......@@ -358,7 +367,7 @@ func deleteResource(c *Client, info *resource.Info) error {
}
return err
}
log.Printf("Using reaper for deleting %s", info.Name)
log.Printf("Using reaper for deleting %q", info.Name)
return reaper.Stop(info.Namespace, info.Name, 0, nil)
}
......@@ -398,7 +407,7 @@ func updateResource(c *Client, target *resource.Info, currentObj runtime.Object,
return fmt.Errorf("failed to create patch: %s", err)
}
if patch == nil {
log.Printf("Looks like there are no changes for %s", target.Name)
log.Printf("Looks like there are no changes for %s %q", target.Mapping.GroupVersionKind.Kind, target.Name)
// This needs to happen to make sure that tiller has the latest info from the API
// Otherwise there will be no labels and other functions that use labels will panic
if err := target.Get(); err != nil {
......
......@@ -59,6 +59,7 @@ func newPodWithStatus(name string, status api.PodStatus, namespace string) api.P
ObjectMeta: api.ObjectMeta{
Name: name,
Namespace: ns,
SelfLink: "/api/v1/namespaces/default/pods/" + name,
},
Spec: api.PodSpec{
Containers: []api.Container{{
......@@ -279,6 +280,49 @@ func TestBuild(t *testing.T) {
}
}
func TestGet(t *testing.T) {
list := newPodList("starfish", "otter")
f, tf, _, ns := cmdtesting.NewAPIFactory()
tf.Client = &fake.RESTClient{
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
p, m := req.URL.Path, req.Method
//actions = append(actions, p+":"+m)
t.Logf("got request %s %s", p, m)
switch {
case p == "/namespaces/default/pods/starfish" && m == "GET":
return newResponse(404, notFoundBody())
case p == "/namespaces/default/pods/otter" && m == "GET":
return newResponse(200, &list.Items[1])
default:
t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
return nil, nil
}
}),
}
c := &Client{Factory: f}
// Test Success
data := strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n name: otter")
o, err := c.Get("default", data)
if err != nil {
t.Errorf("Expected missing results, got %q", err)
}
if !strings.Contains(o, "==> v1/Pod") && !strings.Contains(o, "otter") {
t.Errorf("Expected v1/Pod otter, got %s", o)
}
// Test failure
data = strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n name: starfish")
o, err = c.Get("default", data)
if err != nil {
t.Errorf("Expected missing results, got %q", err)
}
if !strings.Contains(o, "MISSING") && !strings.Contains(o, "pods\t\tstarfish") {
t.Errorf("Expected missing starfish, got %s", o)
}
}
func TestPerform(t *testing.T) {
tests := []struct {
name string
......
......@@ -100,6 +100,8 @@ type Hook struct {
Events []Hook_Event `protobuf:"varint,5,rep,packed,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"`
// Weight indicates the sort order for execution among similar Hook type
Weight int32 `protobuf:"varint,7,opt,name=weight" json:"weight,omitempty"`
}
func (m *Hook) Reset() { *m = Hook{} }
......@@ -122,28 +124,29 @@ func init() {
func init() { proto.RegisterFile("hapi/release/hook.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 354 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x90, 0xdd, 0x6e, 0xa2, 0x40,
0x18, 0x86, 0x17, 0x41, 0xd0, 0xd1, 0x75, 0x67, 0x27, 0x9b, 0xec, 0xc4, 0x93, 0x35, 0x1e, 0x79,
0x34, 0x6c, 0x6c, 0x7a, 0x01, 0xa8, 0xd3, 0xd6, 0x48, 0xd0, 0x0c, 0x90, 0x26, 0x3d, 0x21, 0x98,
0x8e, 0x4a, 0x14, 0x86, 0x08, 0xf6, 0x72, 0x7a, 0x55, 0xbd, 0xa0, 0x66, 0x86, 0x9f, 0x34, 0xe9,
0xd9, 0xc7, 0xf3, 0x3e, 0x7c, 0x33, 0xef, 0x80, 0xbf, 0xa7, 0x38, 0x4f, 0xec, 0x2b, 0xbf, 0xf0,
0xb8, 0xe0, 0xf6, 0x49, 0x88, 0x33, 0xc9, 0xaf, 0xa2, 0x14, 0x68, 0x28, 0x03, 0x52, 0x07, 0xe3,
0x7f, 0x47, 0x21, 0x8e, 0x17, 0x6e, 0xab, 0x6c, 0x7f, 0x3b, 0xd8, 0x65, 0x92, 0xf2, 0xa2, 0x8c,
0xd3, 0xbc, 0xd2, 0xa7, 0xef, 0x3a, 0x30, 0x9e, 0x84, 0x38, 0x23, 0x04, 0x8c, 0x2c, 0x4e, 0x39,
0xd6, 0x26, 0xda, 0xac, 0xcf, 0xd4, 0x2c, 0xd9, 0x39, 0xc9, 0x5e, 0x71, 0xa7, 0x62, 0x72, 0x96,
0x2c, 0x8f, 0xcb, 0x13, 0xd6, 0x2b, 0x26, 0x67, 0x34, 0x06, 0xbd, 0x34, 0xce, 0x92, 0x03, 0x2f,
0x4a, 0x6c, 0x28, 0xde, 0x7e, 0xa3, 0xff, 0xc0, 0xe4, 0x6f, 0x3c, 0x2b, 0x0b, 0xdc, 0x9d, 0xe8,
0xb3, 0xd1, 0x1c, 0x93, 0xaf, 0x17, 0x24, 0xf2, 0x6c, 0x42, 0xa5, 0xc0, 0x6a, 0x0f, 0xdd, 0x83,
0xde, 0x25, 0x2e, 0xca, 0xe8, 0x7a, 0xcb, 0xb0, 0x39, 0xd1, 0x66, 0x83, 0xf9, 0x98, 0x54, 0x35,
0x48, 0x53, 0x83, 0x04, 0x4d, 0x0d, 0x66, 0x49, 0x97, 0xdd, 0xb2, 0xe9, 0x87, 0x06, 0xba, 0x6a,
0x11, 0x1a, 0x00, 0x2b, 0xf4, 0x36, 0xde, 0xf6, 0xd9, 0x83, 0x3f, 0xd0, 0x2f, 0x30, 0xd8, 0x31,
0x1a, 0xad, 0x3d, 0x3f, 0x70, 0x5c, 0x17, 0x6a, 0x08, 0x82, 0xe1, 0x6e, 0xeb, 0x07, 0x2d, 0xe9,
0xa0, 0x11, 0x00, 0x52, 0x59, 0x51, 0x97, 0x06, 0x14, 0xea, 0xea, 0x17, 0x69, 0xd4, 0xc0, 0x68,
0x76, 0x84, 0xbb, 0x47, 0xe6, 0xac, 0x28, 0xec, 0xb6, 0x3b, 0x1a, 0x62, 0x2a, 0xc2, 0x68, 0xc4,
0xb6, 0xae, 0xbb, 0x70, 0x96, 0x1b, 0x68, 0xa1, 0xdf, 0xe0, 0xa7, 0x72, 0x5a, 0xd4, 0x43, 0x18,
0xfc, 0x61, 0xd4, 0xa5, 0x8e, 0x4f, 0xa3, 0x80, 0xfa, 0x41, 0xe4, 0x87, 0xcb, 0x25, 0xf5, 0x7d,
0xd8, 0xff, 0x96, 0x3c, 0x38, 0x6b, 0x37, 0x64, 0x14, 0x82, 0x45, 0xff, 0xc5, 0xaa, 0xdf, 0x6a,
0x6f, 0xaa, 0xfa, 0x77, 0x9f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x69, 0x41, 0x62, 0x57, 0xfc, 0x01,
0x00, 0x00,
// 371 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x90, 0xdf, 0x8e, 0x93, 0x40,
0x14, 0x87, 0x65, 0x5b, 0xa0, 0x3d, 0x5d, 0xd7, 0x71, 0x62, 0x74, 0xd2, 0x1b, 0xc9, 0x5e, 0x71,
0x35, 0x98, 0x35, 0x3e, 0x00, 0xdb, 0x1d, 0x75, 0xb3, 0x84, 0x36, 0x03, 0xc4, 0xc4, 0x1b, 0xc2,
0xc6, 0x69, 0x21, 0x2d, 0x0c, 0x29, 0x53, 0x7d, 0x33, 0x9f, 0xc4, 0x07, 0x32, 0x33, 0xfc, 0x89,
0x89, 0x77, 0x67, 0xbe, 0xdf, 0xc7, 0x39, 0x9c, 0x03, 0xef, 0xca, 0xa2, 0xad, 0x82, 0xb3, 0x38,
0x89, 0xa2, 0x13, 0x41, 0x29, 0xe5, 0x91, 0xb6, 0x67, 0xa9, 0x24, 0xbe, 0xd6, 0x01, 0x1d, 0x82,
0xf5, 0xfb, 0x83, 0x94, 0x87, 0x93, 0x08, 0x4c, 0xf6, 0x7c, 0xd9, 0x07, 0xaa, 0xaa, 0x45, 0xa7,
0x8a, 0xba, 0xed, 0xf5, 0xdb, 0xdf, 0x33, 0x98, 0x7f, 0x95, 0xf2, 0x88, 0x31, 0xcc, 0x9b, 0xa2,
0x16, 0xc4, 0xf2, 0x2c, 0x7f, 0xc9, 0x4d, 0xad, 0xd9, 0xb1, 0x6a, 0x7e, 0x90, 0xab, 0x9e, 0xe9,
0x5a, 0xb3, 0xb6, 0x50, 0x25, 0x99, 0xf5, 0x4c, 0xd7, 0x78, 0x0d, 0x8b, 0xba, 0x68, 0xaa, 0xbd,
0xe8, 0x14, 0x99, 0x1b, 0x3e, 0xbd, 0xf1, 0x07, 0x70, 0xc4, 0x4f, 0xd1, 0xa8, 0x8e, 0xd8, 0xde,
0xcc, 0xbf, 0xb9, 0x23, 0xf4, 0xdf, 0x1f, 0xa4, 0x7a, 0x36, 0x65, 0x5a, 0xe0, 0x83, 0x87, 0x3f,
0xc1, 0xe2, 0x54, 0x74, 0x2a, 0x3f, 0x5f, 0x1a, 0xe2, 0x78, 0x96, 0xbf, 0xba, 0x5b, 0xd3, 0x7e,
0x0d, 0x3a, 0xae, 0x41, 0xd3, 0x71, 0x0d, 0xee, 0x6a, 0x97, 0x5f, 0x1a, 0xfc, 0x16, 0x9c, 0x5f,
0xa2, 0x3a, 0x94, 0x8a, 0xb8, 0x9e, 0xe5, 0xdb, 0x7c, 0x78, 0xdd, 0xfe, 0xb1, 0xc0, 0x36, 0x03,
0xf0, 0x0a, 0xdc, 0x2c, 0x7e, 0x8a, 0xb7, 0xdf, 0x62, 0xf4, 0x02, 0xbf, 0x82, 0xd5, 0x8e, 0xb3,
0xfc, 0x31, 0x4e, 0xd2, 0x30, 0x8a, 0x90, 0x85, 0x11, 0x5c, 0xef, 0xb6, 0x49, 0x3a, 0x91, 0x2b,
0x7c, 0x03, 0xa0, 0x95, 0x07, 0x16, 0xb1, 0x94, 0xa1, 0x99, 0xf9, 0x44, 0x1b, 0x03, 0x98, 0x8f,
0x3d, 0xb2, 0xdd, 0x17, 0x1e, 0x3e, 0x30, 0x64, 0x4f, 0x3d, 0x46, 0xe2, 0x18, 0xc2, 0x59, 0xce,
0xb7, 0x51, 0x74, 0x1f, 0x6e, 0x9e, 0x90, 0x8b, 0x5f, 0xc3, 0x4b, 0xe3, 0x4c, 0x68, 0x81, 0x09,
0xbc, 0xe1, 0x2c, 0x62, 0x61, 0xc2, 0xf2, 0x94, 0x25, 0x69, 0x9e, 0x64, 0x9b, 0x0d, 0x4b, 0x12,
0xb4, 0xfc, 0x2f, 0xf9, 0x1c, 0x3e, 0x46, 0x19, 0x67, 0x08, 0xee, 0x97, 0xdf, 0xdd, 0xe1, 0x86,
0xcf, 0x8e, 0x39, 0xcb, 0xc7, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x82, 0x3c, 0x7a, 0x0e, 0x14,
0x02, 0x00, 0x00,
}
This diff is collapsed.
......@@ -18,7 +18,7 @@ package releaseutil
import (
"fmt"
"strings"
"regexp"
)
// SimpleHead defines what the structure of the head of a manifest file
......@@ -31,17 +31,18 @@ type SimpleHead struct {
} `json:"metadata,omitempty"`
}
var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*")
// SplitManifests takes a string of manifest and returns a map contains individual manifests
func SplitManifests(bigfile string) map[string]string {
// This is not the best way of doing things, but it's how k8s itself does it.
// Basically, we're quickly splitting a stream of YAML documents into an
// array of YAML docs. In the current implementation, the file name is just
// a place holder, and doesn't have any further meaning.
sep := "\n---\n"
tpl := "manifest-%d"
res := map[string]string{}
tmp := strings.Split(bigfile, sep)
for i, d := range tmp {
// Making sure that any extra whitespace in YAML stream doesn't interfere in splitting documents correctly.
docs := sep.Split(bigfile, -1)
for i, d := range docs {
res[fmt.Sprintf(tpl, i)] = d
}
return res
......
......@@ -161,7 +161,6 @@ func (r *ChartRepository) DownloadIndexFile(cachePath string) error {
if !filepath.IsAbs(cp) {
cp = filepath.Join(cachePath, cp)
}
println("Writing to", cp)
return ioutil.WriteFile(cp, index, 0644)
}
......
......@@ -243,6 +243,7 @@ func loadIndex(data []byte) (*IndexFile, error) {
if err := yaml.Unmarshal(data, i); err != nil {
return i, err
}
i.SortEntries()
if i.APIVersion == "" {
// When we leave Beta, we should remove legacy support and just
// return this error:
......
......@@ -29,6 +29,7 @@ import (
const (
testfile = "testdata/local-index.yaml"
unorderedTestfile = "testdata/local-index-unordered.yaml"
testRepo = "test-repo"
)
......@@ -82,6 +83,18 @@ func TestLoadIndexFile(t *testing.T) {
verifyLocalIndex(t, i)
}
func TestLoadUnorderedIndex(t *testing.T) {
b, err := ioutil.ReadFile(unorderedTestfile)
if err != nil {
t.Fatal(err)
}
i, err := loadIndex(b)
if err != nil {
t.Fatal(err)
}
verifyLocalIndex(t, i)
}
func TestMerge(t *testing.T) {
ind1 := NewIndexFile()
ind1.Add(&chart.Metadata{
......
......@@ -17,6 +17,8 @@ limitations under the License.
package repo
import "testing"
import "io/ioutil"
import "os"
const testRepositoriesFile = "testdata/repositories.yaml"
......@@ -116,3 +118,100 @@ func TestNewPreV1RepositoriesFile(t *testing.T) {
t.Errorf("expected the best charts ever. Got %#v", r.Repositories)
}
}
func TestRemoveRepository(t *testing.T) {
sampleRepository := NewRepoFile()
sampleRepository.Add(
&Entry{
Name: "stable",
URL: "https://example.com/stable/charts",
Cache: "stable-index.yaml",
},
&Entry{
Name: "incubator",
URL: "https://example.com/incubator",
Cache: "incubator-index.yaml",
},
)
removeRepository := "stable"
found := sampleRepository.Remove(removeRepository)
if !found {
t.Errorf("expected repository %s not found", removeRepository)
}
found = sampleRepository.Has(removeRepository)
if found {
t.Errorf("repository %s not deleted", removeRepository)
}
}
func TestUpdateRepository(t *testing.T) {
sampleRepository := NewRepoFile()
sampleRepository.Add(
&Entry{
Name: "stable",
URL: "https://example.com/stable/charts",
Cache: "stable-index.yaml",
},
&Entry{
Name: "incubator",
URL: "https://example.com/incubator",
Cache: "incubator-index.yaml",
},
)
newRepoName := "sample"
sampleRepository.Update(&Entry{Name: newRepoName,
URL: "https://example.com/sample",
Cache: "sample-index.yaml",
})
if !sampleRepository.Has(newRepoName) {
t.Errorf("expected repository %s not found", newRepoName)
}
repoCount := len(sampleRepository.Repositories)
sampleRepository.Update(&Entry{Name: newRepoName,
URL: "https://example.com/sample",
Cache: "sample-index.yaml",
})
if repoCount != len(sampleRepository.Repositories) {
t.Errorf("invalid number of repositories found %d, expected number of repositories %d", len(sampleRepository.Repositories), repoCount)
}
}
func TestWriteFile(t *testing.T) {
sampleRepository := NewRepoFile()
sampleRepository.Add(
&Entry{
Name: "stable",
URL: "https://example.com/stable/charts",
Cache: "stable-index.yaml",
},
&Entry{
Name: "incubator",
URL: "https://example.com/incubator",
Cache: "incubator-index.yaml",
},
)
repoFile, err := ioutil.TempFile("", "helm-repo")
if err != nil {
t.Errorf("failed to create test-file (%v)", err)
}
defer os.Remove(repoFile.Name())
if err := sampleRepository.WriteFile(repoFile.Name(), 744); err != nil {
t.Errorf("failed to write file (%v)", err)
}
repos, err := LoadRepositoriesFile(repoFile.Name())
if err != nil {
t.Errorf("failed to load file (%v)", err)
}
for _, repo := range sampleRepository.Repositories {
if !repos.Has(repo.Name) {
t.Errorf("expected repository %s not found", repo.Name)
}
}
}
apiVersion: v1
entries:
nginx:
- urls:
- https://kubernetes-charts.storage.googleapis.com/nginx-0.1.0.tgz
name: nginx
description: string
version: 0.1.0
home: https://github.com/something
digest: "sha256:1234567890abcdef"
keywords:
- popular
- web server
- proxy
- urls:
- https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz
name: nginx
description: string
version: 0.2.0
home: https://github.com/something/else
digest: "sha256:1234567890abcdef"
keywords:
- popular
- web server
- proxy
alpine:
- urls:
- https://kubernetes-charts.storage.googleapis.com/alpine-1.0.0.tgz
- http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz
name: alpine
description: string
version: 1.0.0
home: https://github.com/something
keywords:
- linux
- alpine
- small
- sumtin
digest: "sha256:1234567890abcdef"
......@@ -47,25 +47,15 @@ func New(chartpath string, helmhome helmpath.Home) *Resolver {
}
// Resolve resolves dependencies and returns a lock file with the resolution.
func (r *Resolver) Resolve(reqs *chartutil.Requirements, repoNames map[string]string) (*chartutil.RequirementsLock, error) {
d, err := HashReq(reqs)
if err != nil {
return nil, err
}
func (r *Resolver) Resolve(reqs *chartutil.Requirements, repoNames map[string]string, d string) (*chartutil.RequirementsLock, error) {
// Now we clone the dependencies, locking as we go.
locked := make([]*chartutil.Dependency, len(reqs.Dependencies))
missing := []string{}
for i, d := range reqs.Dependencies {
if strings.HasPrefix(d.Repository, "file://") {
depPath, err := filepath.Abs(strings.TrimPrefix(d.Repository, "file://"))
if err != nil {
return nil, err
}
if _, err = os.Stat(depPath); os.IsNotExist(err) {
return nil, fmt.Errorf("directory %s not found", depPath)
} else if err != nil {
if _, err := GetLocalPath(d.Repository, r.chartpath); err != nil {
return nil, err
}
......@@ -136,3 +126,28 @@ func HashReq(req *chartutil.Requirements) (string, error) {
s, err := provenance.Digest(bytes.NewBuffer(data))
return "sha256:" + s, err
}
// GetLocalPath generates absolute local path when use
// "file://" in repository of requirements
func GetLocalPath(repo string, chartpath string) (string, error) {
var depPath string
var err error
p := strings.TrimPrefix(repo, "file://")
// root path is absolute
if strings.HasPrefix(p, "/") {
if depPath, err = filepath.Abs(p); err != nil {
return "", err
}
} else {
depPath = filepath.Join(chartpath, p)
}
if _, err = os.Stat(depPath); os.IsNotExist(err) {
return "", fmt.Errorf("directory %s not found", depPath)
} else if err != nil {
return "", err
}
return depPath, nil
}
......@@ -81,12 +81,12 @@ func TestResolve(t *testing.T) {
name: "repo from valid local path",
req: &chartutil.Requirements{
Dependencies: []*chartutil.Dependency{
{Name: "signtest", Repository: "file://../../cmd/helm/testdata/testcharts/signtest", Version: "0.1.0"},
{Name: "signtest", Repository: "file://../../../../cmd/helm/testdata/testcharts/signtest", Version: "0.1.0"},
},
},
expect: &chartutil.RequirementsLock{
Dependencies: []*chartutil.Dependency{
{Name: "signtest", Repository: "file://../../cmd/helm/testdata/testcharts/signtest", Version: "0.1.0"},
{Name: "signtest", Repository: "file://../../../../cmd/helm/testdata/testcharts/signtest", Version: "0.1.0"},
},
},
},
......@@ -104,7 +104,12 @@ func TestResolve(t *testing.T) {
repoNames := map[string]string{"alpine": "kubernetes-charts", "redis": "kubernetes-charts"}
r := New("testdata/chartpath", "testdata/helmhome")
for _, tt := range tests {
l, err := r.Resolve(tt.req, repoNames)
hash, err := HashReq(tt.req)
if err != nil {
t.Fatal(err)
}
l, err := r.Resolve(tt.req, repoNames, hash)
if err != nil {
if tt.err {
continue
......@@ -141,7 +146,7 @@ func TestResolve(t *testing.T) {
}
func TestHashReq(t *testing.T) {
expect := "sha256:c8250374210bd909cef274be64f871bd4e376d4ecd34a1589b5abf90b68866ba"
expect := "sha256:1feffe2016ca113f64159d91c1f77d6a83bcd23510b171d9264741bf9d63f741"
req := &chartutil.Requirements{
Dependencies: []*chartutil.Dependency{
{Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"},
......
/*
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 tiller
import (
"sort"
"k8s.io/helm/pkg/proto/hapi/release"
)
// sortByHookWeight does an in-place sort of hooks by their supplied weight.
func sortByHookWeight(hooks []*release.Hook) []*release.Hook {
hs := newHookWeightSorter(hooks)
sort.Sort(hs)
return hs.hooks
}
type hookWeightSorter struct {
hooks []*release.Hook
}
func newHookWeightSorter(h []*release.Hook) *hookWeightSorter {
return &hookWeightSorter{
hooks: h,
}
}
func (hs *hookWeightSorter) Len() int { return len(hs.hooks) }
func (hs *hookWeightSorter) Swap(i, j int) {
hs.hooks[i], hs.hooks[j] = hs.hooks[j], hs.hooks[i]
}
func (hs *hookWeightSorter) Less(i, j int) bool {
if hs.hooks[i].Weight == hs.hooks[j].Weight {
return hs.hooks[i].Name < hs.hooks[j].Name
}
return hs.hooks[i].Weight < hs.hooks[j].Weight
}
/*
Copyright 2017 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tiller
import (
"testing"
"k8s.io/helm/pkg/proto/hapi/release"
)
func TestHookSorter(t *testing.T) {
hooks := []*release.Hook{
{
Name: "g",
Kind: "pre-install",
Weight: 99,
},
{
Name: "f",
Kind: "pre-install",
Weight: 3,
},
{
Name: "b",
Kind: "pre-install",
Weight: -3,
},
{
Name: "e",
Kind: "pre-install",
Weight: 3,
},
{
Name: "a",
Kind: "pre-install",
Weight: -10,
},
{
Name: "c",
Kind: "pre-install",
Weight: 0,
},
{
Name: "d",
Kind: "pre-install",
Weight: 3,
},
}
res := sortByHookWeight(hooks)
got := ""
expect := "abcdefg"
for _, r := range res {
got += r.Name
}
if got != expect {
t.Errorf("Expected %q, got %q", expect, got)
}
}
......@@ -20,6 +20,7 @@ import (
"fmt"
"log"
"path"
"strconv"
"strings"
"github.com/ghodss/yaml"
......@@ -109,12 +110,20 @@ func sortManifests(files map[string]string, apis chartutil.VersionSet, sort Sort
generic = append(generic, manifest{name: n, content: c, head: &sh})
continue
}
hws, _ := sh.Metadata.Annotations[hooks.HookWeightAnno]
hw, err := strconv.Atoi(hws)
if err != nil {
hw = 0
}
h := &release.Hook{
Name: sh.Metadata.Name,
Kind: sh.Kind,
Path: n,
Manifest: c,
Events: []release.Hook_Event{},
Weight: int32(hw),
}
isHook := false
......
......@@ -23,11 +23,61 @@ import (
// SortOrder is an ordering of Kinds.
type SortOrder []string
// InstallOrder is the order in which manifests should be installed (by Kind)
var InstallOrder SortOrder = []string{"Namespace", "Secret", "ConfigMap", "PersistentVolume", "PersistentVolumeClaim", "ServiceAccount", "Service", "Pod", "ReplicationController", "Deployment", "DaemonSet", "Ingress", "Job"}
// InstallOrder is the order in which manifests should be installed (by Kind).
//
// Those occurring earlier in the list get installed before those occurring later in the list.
var InstallOrder SortOrder = []string{
"Namespace",
"ResourceQuota",
"LimitRange",
"Secret",
"ConfigMap",
"PersistentVolume",
"PersistentVolumeClaim",
"ServiceAccount",
"ClusterRole",
"ClusterRoleBinding",
"Role",
"RoleBinding",
"Service",
"DaemonSet",
"Pod",
"ReplicationController",
"ReplicaSet",
"Deployment",
"StatefulSet",
"Job",
"CronJob",
"Ingress",
}
// UninstallOrder is the order in which manifests should be uninstalled (by Kind)
var UninstallOrder SortOrder = []string{"Service", "Pod", "ReplicationController", "Deployment", "DaemonSet", "ConfigMap", "Secret", "PersistentVolumeClaim", "PersistentVolume", "ServiceAccount", "Ingress", "Job", "Namespace"}
// UninstallOrder is the order in which manifests should be uninstalled (by Kind).
//
// Those occurring earlier in the list get uninstalled before those occurring later in the list.
var UninstallOrder SortOrder = []string{
"Ingress",
"Service",
"CronJob",
"Job",
"StatefulSet",
"Deployment",
"ReplicaSet",
"ReplicationController",
"Pod",
"DaemonSet",
"RoleBinding",
"Role",
"ClusterRoleBinding",
"ClusterRole",
"ServiceAccount",
"PersistentVolumeClaim",
"PersistentVolume",
"ConfigMap",
"Secret",
"LimitRange",
"ResourceQuota",
"Namespace",
}
// sortByKind does an in-place sort of manifests by Kind.
//
......
......@@ -17,6 +17,7 @@ limitations under the License.
package tiller
import (
"bytes"
"testing"
util "k8s.io/helm/pkg/releaseutil"
......@@ -25,14 +26,34 @@ import (
func TestKindSorter(t *testing.T) {
manifests := []manifest{
{
name: "m",
name: "i",
content: "",
head: &util.SimpleHead{Kind: "Deployment"},
head: &util.SimpleHead{Kind: "ClusterRole"},
},
{
name: "l",
name: "j",
content: "",
head: &util.SimpleHead{Kind: "Service"},
head: &util.SimpleHead{Kind: "ClusterRoleBinding"},
},
{
name: "e",
content: "",
head: &util.SimpleHead{Kind: "ConfigMap"},
},
{
name: "u",
content: "",
head: &util.SimpleHead{Kind: "CronJob"},
},
{
name: "n",
content: "",
head: &util.SimpleHead{Kind: "DaemonSet"},
},
{
name: "r",
content: "",
head: &util.SimpleHead{Kind: "Deployment"},
},
{
name: "!",
......@@ -40,35 +61,107 @@ func TestKindSorter(t *testing.T) {
head: &util.SimpleHead{Kind: "HonkyTonkSet"},
},
{
name: "h",
name: "v",
content: "",
head: &util.SimpleHead{Kind: "Ingress"},
},
{
name: "t",
content: "",
head: &util.SimpleHead{Kind: "Job"},
},
{
name: "c",
content: "",
head: &util.SimpleHead{Kind: "LimitRange"},
},
{
name: "a",
content: "",
head: &util.SimpleHead{Kind: "Namespace"},
},
{
name: "e",
name: "f",
content: "",
head: &util.SimpleHead{Kind: "ConfigMap"},
head: &util.SimpleHead{Kind: "PersistentVolume"},
},
{
name: "g",
content: "",
head: &util.SimpleHead{Kind: "PersistentVolumeClaim"},
},
{
name: "o",
content: "",
head: &util.SimpleHead{Kind: "Pod"},
},
{
name: "q",
content: "",
head: &util.SimpleHead{Kind: "ReplicaSet"},
},
{
name: "p",
content: "",
head: &util.SimpleHead{Kind: "ReplicationController"},
},
{
name: "b",
content: "",
head: &util.SimpleHead{Kind: "ResourceQuota"},
},
{
name: "k",
content: "",
head: &util.SimpleHead{Kind: "Role"},
},
{
name: "l",
content: "",
head: &util.SimpleHead{Kind: "RoleBinding"},
},
{
name: "d",
content: "",
head: &util.SimpleHead{Kind: "Secret"},
},
{
name: "m",
content: "",
head: &util.SimpleHead{Kind: "Service"},
},
{
name: "h",
content: "",
head: &util.SimpleHead{Kind: "ServiceAccount"},
},
{
name: "s",
content: "",
head: &util.SimpleHead{Kind: "StatefulSet"},
},
}
res := sortByKind(manifests, InstallOrder)
got := ""
expect := "helm!"
for _, r := range res {
got += r.name
for _, test := range []struct {
description string
order SortOrder
expected string
}{
{"install", InstallOrder, "abcdefghijklmnopqrstuv!"},
{"uninstall", UninstallOrder, "vmutsrqponlkjihgfedcba!"},
} {
var buf bytes.Buffer
t.Run(test.description, func(t *testing.T) {
if got, want := len(test.expected), len(manifests); got != want {
t.Fatalf("Expected %d names in order, got %d", want, got)
}
if got != expect {
t.Errorf("Expected %q, got %q", expect, got)
defer buf.Reset()
for _, r := range sortByKind(manifests, test.order) {
buf.WriteString(r.name)
}
expect = "lmeh!"
got = ""
res = sortByKind(manifests, UninstallOrder)
for _, r := range res {
got += r.name
if got := buf.String(); got != test.expected {
t.Errorf("Expected %q, got %q", test.expected, got)
}
if got != expect {
t.Errorf("Expected %q, got %q", expect, got)
})
}
}
......@@ -354,13 +354,33 @@ func (s *ReleaseServer) performUpdate(originalRelease, updatedRelease *release.R
//
// This is skipped if the req.ResetValues flag is set, in which case the
// request values are not altered.
func (s *ReleaseServer) reuseValues(req *services.UpdateReleaseRequest, current *release.Release) {
func (s *ReleaseServer) reuseValues(req *services.UpdateReleaseRequest, current *release.Release) error {
if req.ResetValues {
// If ResetValues is set, we comletely ignore current.Config.
log.Print("Reset values to the chart's original version.")
return
return nil
}
// If the ReuseValues flag is set, we always copy the old values over the new config's values.
if req.ReuseValues {
log.Print("Reusing the old release's values")
// We have to regenerate the old coalesced values:
oldVals, err := chartutil.CoalesceValues(current.Chart, current.Config)
if err != nil {
err := fmt.Errorf("failed to rebuild old values: %s", err)
log.Print(err)
return err
}
// If req.Values is empty, but current. config is not, copy current into the
nv, err := oldVals.YAML()
if err != nil {
return err
}
req.Chart.Values = &chart.Config{Raw: nv}
return nil
}
// If req.Values is empty, but current.Config is not, copy current into the
// request.
if (req.Values == nil || req.Values.Raw == "" || req.Values.Raw == "{}\n") &&
current.Config != nil &&
......@@ -369,6 +389,7 @@ func (s *ReleaseServer) reuseValues(req *services.UpdateReleaseRequest, current
log.Printf("Copying values from %s (v%d) to new release.", current.Name, current.Version)
req.Values = current.Config
}
return nil
}
// prepareUpdate builds an updated release for an update operation.
......@@ -388,7 +409,9 @@ func (s *ReleaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*rele
}
// If new values were not supplied in the upgrade, re-use the existing values.
s.reuseValues(req, currentRelease)
if err := s.reuseValues(req, currentRelease); err != nil {
return nil, nil, err
}
// Increment revision count. This is passed to templates, and also stored on
// the release object.
......@@ -911,18 +934,19 @@ func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook strin
}
log.Printf("Executing %s hooks for %s", hook, name)
executingHooks := []*release.Hook{}
for _, h := range hs {
found := false
for _, e := range h.Events {
if e == code {
found = true
executingHooks = append(executingHooks, h)
}
}
// If this doesn't implement the hook, skip it.
if !found {
continue
}
executingHooks = sortByHookWeight(executingHooks)
for _, h := range executingHooks {
b := bytes.NewBufferString(h.Manifest)
if err := kubeCli.Create(namespace, b, timeout, false); err != nil {
log.Printf("warning: Release %q %s %s failed: %s", name, hook, h.Path, err)
......@@ -937,6 +961,7 @@ func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook strin
}
h.LastRun = timeconv.Now()
}
log.Printf("Hooks complete for %s %s", hook, name)
return nil
}
......
......@@ -717,7 +717,7 @@ func TestUpdateRelease(t *testing.T) {
t.Errorf("Expected description %q, got %q", edesc, got)
}
}
func TestUpdateReleaseResetValues(t *testing.T) {
func TestUpdateRelease_ResetValues(t *testing.T) {
c := helm.NewContext()
rs := rsFixture()
rel := releaseStub()
......@@ -744,6 +744,71 @@ func TestUpdateReleaseResetValues(t *testing.T) {
}
}
func TestUpdateRelease_ReuseValues(t *testing.T) {
c := helm.NewContext()
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: "templates/hello", Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)},
},
// Since reuseValues is set, this should get ignored.
Values: &chart.Config{Raw: "foo: bar\n"},
},
Values: &chart.Config{Raw: "name2: val2"},
ReuseValues: true,
}
res, err := rs.UpdateRelease(c, req)
if err != nil {
t.Fatalf("Failed updated: %s", err)
}
// This should have been overwritten with the old value.
expect := "name: value\n"
if res.Release.Chart.Values != nil && res.Release.Chart.Values.Raw != expect {
t.Errorf("Expected chart values to be %q, got %q", expect, res.Release.Chart.Values.Raw)
}
// This should have the newly-passed overrides.
expect = "name2: val2"
if res.Release.Config != nil && res.Release.Config.Raw != expect {
t.Errorf("Expected request config to be %q, got %q", expect, res.Release.Config.Raw)
}
}
func TestUpdateRelease_ResetReuseValues(t *testing.T) {
// This verifies that when both reset and reuse are set, reset wins.
c := helm.NewContext()
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: "templates/hello", Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)},
},
},
ResetValues: true,
ReuseValues: true,
}
res, err := rs.UpdateRelease(c, req)
if err != nil {
t.Fatalf("Failed updated: %s", err)
}
// This should have been unset. Config: &chart.Config{Raw: `name: value`},
if res.Release.Config != nil && res.Release.Config.Raw != "" {
t.Errorf("Expected chart config to be empty, got %q", res.Release.Config.Raw)
}
}
func TestUpdateReleaseFailure(t *testing.T) {
c := helm.NewContext()
rs := rsFixture()
......
/*
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 version represents the current version of the project.
package version // import "k8s.io/helm/pkg/version"
import "testing"
import "k8s.io/helm/pkg/proto/hapi/version"
func TestGetVersionProto(t *testing.T) {
tests := []struct {
version string
buildMetadata string
gitCommit string
gitTreeState string
expected version.Version
}{
{"", "", "", "", version.Version{SemVer: "", GitCommit: "", GitTreeState: ""}},
{"v1.0.0", "", "", "", version.Version{SemVer: "v1.0.0", GitCommit: "", GitTreeState: ""}},
{"v1.0.0", "79d5c5f7", "", "", version.Version{SemVer: "v1.0.0+79d5c5f7", GitCommit: "", GitTreeState: ""}},
{"v1.0.0", "79d5c5f7", "0d399baec2acda578a217d1aec8d7d707c71e44d", "", version.Version{SemVer: "v1.0.0+79d5c5f7", GitCommit: "0d399baec2acda578a217d1aec8d7d707c71e44d", GitTreeState: ""}},
{"v1.0.0", "79d5c5f7", "0d399baec2acda578a217d1aec8d7d707c71e44d", "clean", version.Version{SemVer: "v1.0.0+79d5c5f7", GitCommit: "0d399baec2acda578a217d1aec8d7d707c71e44d", GitTreeState: "clean"}},
}
for _, tt := range tests {
Version = tt.version
BuildMetadata = tt.buildMetadata
GitCommit = tt.gitCommit
GitTreeState = tt.gitTreeState
if versionProto := GetVersionProto(); *versionProto != tt.expected {
t.Errorf("expected Semver(%s), GitCommit(%s) and GitTreeState(%s) to be %v", tt.expected, tt.gitCommit, tt.gitTreeState, *versionProto)
}
}
}
......@@ -62,9 +62,8 @@ verifySupported() {
fi
}
# downloadFile downloads the latest binary package and also the checksum
# for that binary.
downloadFile() {
# checkLatestVersion checks the latest available version.
checkLatestVersion() {
# Use the GitHub API to find the latest version for this project.
local latest_url="https://api.github.com/repos/kubernetes/helm/releases/latest"
if type "curl" > /dev/null; then
......@@ -72,7 +71,28 @@ downloadFile() {
elif type "wget" > /dev/null; then
TAG=$(wget -q -O - $latest_url | awk '/\"tag_name\":/{gsub( /[,\"]/,"", $2); print $2}')
fi
}
# checkHelmInstalledVersion checks which version of helm is installed and
# if it needs to be updated.
checkHelmInstalledVersion() {
if [[ -f "${HELM_INSTALL_DIR}/${PROJECT_NAME}" ]]; then
local version=$(helm version | grep '^Client' | cut -d'"' -f2)
if [[ "$version" == "$TAG" ]]; then
echo "Helm ${version} is up-to-date."
return 0
else
echo "Helm ${TAG} is available. Upgrading from version ${version}."
return 1
fi
else
return 1
fi
}
# downloadFile downloads the latest binary package and also the checksum
# for that binary.
downloadFile() {
HELM_DIST="helm-$TAG-$OS-$ARCH.tar.gz"
DOWNLOAD_URL="https://kubernetes-helm.storage.googleapis.com/$HELM_DIST"
CHECKSUM_URL="$DOWNLOAD_URL.sha256"
......@@ -140,6 +160,9 @@ set -e
initArch
initOS
verifySupported
downloadFile
installFile
checkLatestVersion
if ! checkHelmInstalledVersion; then
downloadFile
installFile
fi
testVersion
......@@ -30,7 +30,6 @@ gometalinter.v1 \
--enable deadcode \
--severity deadcode:error \
--enable gofmt \
--enable gosimple \
--enable ineffassign \
--enable misspell \
--enable vet \
......
MUTABLE_VERSION ?= canary
GIT_COMMIT := $(shell git rev-parse HEAD)
GIT_SHA := $(shell git rev-parse --short HEAD)
GIT_TAG := $(shell git describe --tags --abbrev=0 2>/dev/null)
GIT_DIRTY = $(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean")
GIT_COMMIT ?= $(shell git rev-parse HEAD)
GIT_SHA ?= $(shell git rev-parse --short HEAD)
GIT_TAG ?= $(shell git describe --tags --abbrev=0 2>/dev/null)
GIT_DIRTY ?= $(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean")
ifdef VERSION
DOCKER_VERSION = $(VERSION)
......
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