From 47c360da37a7860f0919795d02ed19e1647248c1 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Wed, 10 Aug 2016 15:04:02 -0700 Subject: [PATCH 001/183] chore(*): bump version to v2.0.0-alpha.3 --- pkg/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/version/version.go b/pkg/version/version.go index 128b9a468..71730852a 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -23,4 +23,4 @@ package version // import "k8s.io/helm/pkg/version" // Increment major number for new feature additions and behavioral changes. // Increment minor number for bug fixes and performance enhancements. // Increment patch number for critical fixes to existing releases. -var Version = "v2.0.0-alpha.2" +var Version = "v2.0.0-alpha.3" From e7738c5c568b2446bb5b208f14a0b1cd779e57eb Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Thu, 11 Aug 2016 09:03:37 -0700 Subject: [PATCH 002/183] feat(ci): add windows build --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2dbaed323..90a9cc730 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ build: .PHONY: build-cross build-cross: - gox -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -os="darwin linux" -arch="amd64 386" $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/... + gox -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -os="darwin linux windows" -arch="amd64 386" $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/... .PHONY: check-docker check-docker: From dd7cebc39f3d82f1727b3b3c7e17710bc21c593c Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Thu, 11 Aug 2016 09:24:25 -0700 Subject: [PATCH 003/183] feat(Makefile): add target for building releases * Add README and License files to archive * Compress archives for smaller download size https://github.com/kubernetes/helm/issues/999 --- Makefile | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Makefile b/Makefile index 90a9cc730..7216bb3e7 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ LDFLAGS := GOFLAGS := BINDIR := $(CURDIR)/bin BINARIES := helm tiller +DIST_DIRS := find * -type d -exec .PHONY: all all: build @@ -24,6 +25,17 @@ build: build-cross: gox -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -os="darwin linux windows" -arch="amd64 386" $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/... +# usage: make dist VERSION=v2.0.0-alpha.3 +.PHONY: dist +dist: build-cross + ( \ + cd _dist && \ + $(DIST_DIRS) cp ../LICENSE {} \; && \ + $(DIST_DIRS) cp ../README.md {} \; && \ + $(DIST_DIRS) tar -zcf helm-${VERSION}-{}.tar.gz {} \; && \ + $(DIST_DIRS) zip -r helm-${VERSION}-{}.zip {} \; \ + ) + .PHONY: check-docker check-docker: @if [ -z $$(which docker) ]; then \ From 5f015322e26f323b17b7b4815bfec37aaeda25eb Mon Sep 17 00:00:00 2001 From: Naveen Srinivasan Date: Thu, 11 Aug 2016 22:15:44 -0400 Subject: [PATCH 004/183] Fix the clean task when directory not present The Makefile has clean task that was failing when the ./rootfs/tiller was not present --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2dbaed323..74bf76541 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,7 @@ protoc: .PHONY: clean clean: @rm -rf $(BINDIR) - @rm ./rootfs/tiller + @rm -f ./rootfs/tiller .PHONY: coverage coverage: From dbac9a801d9ea312bf70462a3c82d088a757712c Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Fri, 12 Aug 2016 10:41:56 -0700 Subject: [PATCH 005/183] fix(deps): remove inf package hack from glide.yaml --- glide.lock | 246 ++++++++++++++++++++++++++--------------------------- glide.yaml | 4 - 2 files changed, 121 insertions(+), 129 deletions(-) diff --git a/glide.lock b/glide.lock index 724dbb06e..00c27c575 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: b2a8d0f3f558b4c1026e519c7bdcb5dd675e66424edcf1440ec10706afd6b345 -updated: 2016-08-10T15:23:57.611441314-06:00 +hash: ae516306cd7ec28920ecd9e6edf1767336ccdfad7846bcb5111fb085d8848bbd +updated: 2016-08-12T10:41:12.543890279-07:00 imports: - name: github.com/aokoli/goutils version: 9c37978a95bd5c709a15883b6242714ea6709e64 @@ -251,12 +251,12 @@ imports: version: fb93926129b8ec0056f2f458b1f519654814edf0 subpackages: - context - - context/ctxhttp - http2 + - trace - http2/hpack - internal/timeseries - - trace - websocket + - context/ctxhttp - name: golang.org/x/oauth2 version: b5adcc2dcdf009d0391547edc6ecbaff889f5bb9 subpackages: @@ -274,8 +274,8 @@ imports: - internal/log - internal/modules - internal/remote_api - - internal/urlfetch - urlfetch + - internal/urlfetch - name: google.golang.org/cloud version: eb47ba841d53d93506cfbfbc03927daf9cc48f88 repo: https://code.googlesource.com/gocloud @@ -286,173 +286,169 @@ imports: - name: google.golang.org/grpc version: dec33edc378cf4971a2741cfd86ed70a644d6ba3 subpackages: + - metadata - codes - credentials - grpclog - internal - - metadata - naming - - peer - transport + - peer - name: gopkg.in/inf.v0 version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 - name: gopkg.in/yaml.v2 version: a83829b6f1293c91addabc89d0571c246397bbf4 - name: k8s.io/kubernetes - version: a0bfee0d3899d2b91530552a536405c1fe1a1d2a + version: a570a0fef12408f1c0a7214952055bad39c160d7 subpackages: - - federation/apis/federation - - federation/apis/federation/install - - federation/apis/federation/v1beta1 - - federation/client/clientset_generated/federation_internalclientset - - federation/client/clientset_generated/federation_internalclientset/typed/core/unversioned - - federation/client/clientset_generated/federation_internalclientset/typed/federation/unversioned - pkg/api - - pkg/api/annotations - - pkg/api/endpoints + - 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 - pkg/api/errors - - pkg/api/install - - pkg/api/meta - - pkg/api/meta/metatypes - - pkg/api/pod - - pkg/api/resource - - pkg/api/rest - - pkg/api/service + - pkg/client/unversioned/testclient - pkg/api/testapi - - pkg/api/unversioned - - pkg/api/unversioned/validation - - pkg/api/util - pkg/api/v1 - - pkg/api/validation - - pkg/apimachinery - - pkg/apimachinery/registered + - pkg/api/meta/metatypes + - pkg/api/resource + - pkg/auth/user + - pkg/conversion + - pkg/fields + - pkg/runtime/serializer + - pkg/types + - pkg/util + - pkg/util/intstr + - pkg/util/rand + - pkg/util/sets + - pkg/api/install - pkg/apis/apps - pkg/apis/apps/install - - pkg/apis/apps/v1alpha1 - - pkg/apis/authentication.k8s.io - pkg/apis/authentication.k8s.io/install - - pkg/apis/authentication.k8s.io/v1beta1 - - pkg/apis/authorization - pkg/apis/authorization/install - - pkg/apis/authorization/v1beta1 - pkg/apis/autoscaling - pkg/apis/autoscaling/install - - pkg/apis/autoscaling/v1 - - pkg/apis/batch - pkg/apis/batch/install - - pkg/apis/batch/v1 - pkg/apis/batch/v2alpha1 - - pkg/apis/componentconfig - pkg/apis/componentconfig/install - - pkg/apis/componentconfig/v1alpha1 - pkg/apis/extensions - pkg/apis/extensions/install - - pkg/apis/extensions/v1beta1 - - pkg/apis/extensions/validation - pkg/apis/policy - pkg/apis/policy/install - - pkg/apis/policy/v1alpha1 - pkg/apis/rbac - pkg/apis/rbac/install - - pkg/apis/rbac/v1alpha1 - - pkg/auth/user - - pkg/capabilities - - pkg/client/cache - - 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/client/metrics - - pkg/client/record - - pkg/client/restclient - - pkg/client/transport - pkg/client/typed/discovery - - pkg/client/unversioned - - pkg/client/unversioned/adapters/internalclientset + - pkg/util/net + - pkg/util/wait + - pkg/version + - plugin/pkg/client/auth + - pkg/util/validation + - pkg/util/validation/field + - pkg/apimachinery - pkg/client/unversioned/auth - - pkg/client/unversioned/clientcmd - pkg/client/unversioned/clientcmd/api - pkg/client/unversioned/clientcmd/api/latest - - pkg/client/unversioned/clientcmd/api/v1 - - pkg/client/unversioned/fake - - pkg/client/unversioned/portforward - - pkg/client/unversioned/remotecommand - - pkg/client/unversioned/testclient - - pkg/controller - - pkg/controller/framework - - pkg/conversion - - pkg/conversion/queryparams + - pkg/util/errors + - pkg/util/homedir + - pkg/kubelet/server/portforward + - pkg/util/httpstream + - pkg/util/runtime + - pkg/client/transport + - pkg/kubelet/server/remotecommand + - pkg/util/httpstream/spdy + - federation/apis/federation + - federation/client/clientset_generated/federation_internalclientset + - pkg/api/annotations + - pkg/api/util + - pkg/api/validation + - pkg/apis/batch/v1 + - pkg/client/clientset_generated/internalclientset + - pkg/client/unversioned/adapters/internalclientset - pkg/credentialprovider - pkg/fieldpath - - pkg/fields - - pkg/httplog - - pkg/kubectl - - pkg/kubectl/cmd/util - - pkg/kubectl/resource - - pkg/kubelet/qos - pkg/kubelet/qos/util - - pkg/kubelet/server/portforward - - pkg/kubelet/server/remotecommand - - pkg/labels - - pkg/master/ports - - pkg/registry/generic + - pkg/util/deployment + - pkg/util/integer + - pkg/util/jsonpath + - pkg/util/slice + - pkg/api/service + - pkg/controller - pkg/registry/thirdpartyresourcedata - - pkg/runtime - - pkg/runtime/serializer - pkg/runtime/serializer/json - - pkg/runtime/serializer/protobuf - - pkg/runtime/serializer/recognizer - - pkg/runtime/serializer/streaming - - pkg/runtime/serializer/versioning - - pkg/security/podsecuritypolicy/util - - pkg/storage - - pkg/types - - pkg/util - - pkg/util/crypto - - pkg/util/deployment - - pkg/util/errors - pkg/util/flag - - pkg/util/flowcontrol - - pkg/util/framer - - pkg/util/hash - - pkg/util/homedir - - pkg/util/httpstream - - pkg/util/httpstream/spdy - - pkg/util/integer - - pkg/util/intstr + - pkg/conversion/queryparams - pkg/util/json - - pkg/util/jsonpath - - pkg/util/labels - - pkg/util/net - - pkg/util/net/sets + - third_party/forked/json + - federation/apis/federation/install + - pkg/runtime/serializer/recognizer - pkg/util/parsers - - pkg/util/pod - - pkg/util/rand - - pkg/util/replicaset - - pkg/util/runtime - - pkg/util/sets - - pkg/util/slice - - pkg/util/strategicpatch - - pkg/util/validation - - pkg/util/validation/field - - pkg/util/wait - - pkg/util/wsstream - - pkg/util/yaml - - pkg/version - - pkg/watch - pkg/watch/versioned - - plugin/pkg/client/auth + - third_party/forked/reflect + - pkg/runtime/serializer/protobuf + - pkg/runtime/serializer/versioning + - 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 - plugin/pkg/client/auth/gcp - plugin/pkg/client/auth/oidc - - third_party/forked/json - - third_party/forked/reflect + - pkg/client/unversioned/clientcmd/api/v1 + - pkg/httplog + - pkg/util/wsstream - third_party/golang/netutil + - federation/client/clientset_generated/federation_internalclientset/typed/core/unversioned + - federation/client/clientset_generated/federation_internalclientset/typed/federation/unversioned + - pkg/api/endpoints + - pkg/api/pod + - pkg/api/unversioned/validation + - pkg/capabilities + - 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 -- name: speter.net/go/exp/math/dec/inf - version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 - repo: https://github.com/go-inf/inf.git - vcs: git + - pkg/util/net/sets + - pkg/client/cache + - pkg/client/record + - pkg/controller/framework + - pkg/util/hash + - pkg/api/rest + - pkg/apis/extensions/validation + - pkg/registry/generic + - pkg/util/framer + - federation/apis/federation/v1beta1 + - pkg/kubelet/qos + - pkg/master/ports + - pkg/security/podsecuritypolicy/util + - pkg/storage testImports: [] diff --git a/glide.yaml b/glide.yaml index 1cd123e71..ab617d866 100644 --- a/glide.yaml +++ b/glide.yaml @@ -45,10 +45,6 @@ import: - 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 version: ^4.0.0 - package: google.golang.org/cloud From 5bcf29d21423533ef0bb26e7bdc365670428ece0 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 15 Aug 2016 12:28:41 -0600 Subject: [PATCH 006/183] feat(tiller): make configmaps the default storage This adds a Tiller CLI flag to override the default, and tests to make sure that the default comes up as expected. --- cmd/tiller/environment/environment.go | 34 ++++++++++------------ cmd/tiller/environment/environment_test.go | 10 +++++++ cmd/tiller/tiller.go | 27 +++++++++++++++-- pkg/kube/client.go | 11 +++++++ pkg/storage/driver/cfgmaps.go | 8 +++++ pkg/storage/driver/cfgmaps_test.go | 9 ++++++ pkg/storage/driver/driver.go | 1 + pkg/storage/driver/memory.go | 8 +++++ pkg/storage/driver/memory_test.go | 9 ++++++ 9 files changed, 96 insertions(+), 21 deletions(-) diff --git a/cmd/tiller/environment/environment.go b/cmd/tiller/environment/environment.go index 12690a84f..5e0e81216 100644 --- a/cmd/tiller/environment/environment.go +++ b/cmd/tiller/environment/environment.go @@ -23,6 +23,7 @@ These dependencies are expressed as interfaces so that alternate implementations package environment import ( + "errors" "io" "k8s.io/helm/pkg/chartutil" @@ -31,11 +32,9 @@ import ( "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage/driver" + "k8s.io/kubernetes/pkg/client/unversioned" ) -// UseConfigMaps is a feature flags to toggle use of configmaps storage driver. -const UseConfigMaps = false - // TillerNamespace is the namespace tiller is running in. const TillerNamespace = "kube-system" @@ -135,6 +134,9 @@ type KubeClient interface { // reader must contain a YAML stream (one or more YAML documents separated // by "\n---\n"). Update(namespace string, originalReader, modifiedReader io.Reader) error + + // APIClient gets a raw API client for Kubernetes. + APIClient() (unversioned.Interface, error) } // PrintingKubeClient implements KubeClient, but simply prints the reader to @@ -143,6 +145,14 @@ type PrintingKubeClient struct { Out io.Writer } +// APIClient always returns an error. +// +// The printing client does not have access to a Kubernetes client at all. So it +// will always return an error if the client is accessed. +func (p *PrintingKubeClient) APIClient() (unversioned.Interface, error) { + return nil, errors.New("no API client found") +} + // Create prints the values of what would be created with a real KubeClient. func (p *PrintingKubeClient) Create(ns string, r io.Reader) error { _, err := io.Copy(p.Out, r) @@ -196,23 +206,9 @@ func New() *Environment { GoTplEngine: e, } - kbc := kube.New(nil) - - var sd *storage.Storage - if UseConfigMaps { - c, err := kbc.Client() - if err != nil { - // panic because we cant initliaze driver with no client - panic(err) - } - sd = storage.Init(driver.NewConfigMaps(c.ConfigMaps(TillerNamespace))) - } else { - sd = storage.Init(driver.NewMemory()) - } - return &Environment{ EngineYard: ey, - Releases: sd, //storage.Init(driver.NewMemory()), - KubeClient: kbc, //kube.New(nil), //&PrintingKubeClient{Out: os.Stdout}, + Releases: storage.Init(driver.NewMemory()), + KubeClient: kube.New(nil), } } diff --git a/cmd/tiller/environment/environment_test.go b/cmd/tiller/environment/environment_test.go index 16cb9ef7a..236df6a2b 100644 --- a/cmd/tiller/environment/environment_test.go +++ b/cmd/tiller/environment/environment_test.go @@ -26,6 +26,8 @@ import ( "k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage/driver" + unversionedclient "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/client/unversioned/testclient" ) type mockEngine struct { @@ -47,6 +49,10 @@ func (r *mockReleaseStorage) Create(v *release.Release) error { return nil } +func (r *mockReleaseStorage) Name() string { + return "mockReleaseStorage" +} + func (r *mockReleaseStorage) Get(k string) (*release.Release, error) { return r.rel, nil } @@ -81,6 +87,10 @@ func (r *mockReleaseStorage) History(n string) ([]*release.Release, error) { type mockKubeClient struct { } +func (k *mockKubeClient) APIClient() (unversionedclient.Interface, error) { + return testclient.NewSimpleFake(), nil +} + func (k *mockKubeClient) Create(ns string, r io.Reader) error { return nil } diff --git a/cmd/tiller/tiller.go b/cmd/tiller/tiller.go index 9dfb2c648..2c0e1dd9a 100644 --- a/cmd/tiller/tiller.go +++ b/cmd/tiller/tiller.go @@ -26,6 +26,13 @@ import ( "google.golang.org/grpc" "k8s.io/helm/cmd/tiller/environment" + "k8s.io/helm/pkg/storage" + "k8s.io/helm/pkg/storage/driver" +) + +const ( + storageMemory = "memory" + storageConfigMap = "configmap" ) // rootServer is the root gRPC server. @@ -38,8 +45,11 @@ var rootServer = grpc.NewServer() // Any changes to env should be done before rootServer.Serve() is called. var env = environment.New() -var addr = ":44134" -var probe = ":44135" +var ( + addr = ":44134" + probe = ":44135" + store = storageConfigMap +) const globalUsage = `The Kubernetes Helm server. @@ -58,10 +68,22 @@ var rootCommand = &cobra.Command{ func main() { pf := rootCommand.PersistentFlags() pf.StringVarP(&addr, "listen", "l", ":44134", "The address:port to listen on") + pf.StringVar(&store, "storage", storageConfigMap, "The storage driver to use. One of 'configmap' or 'memory'") rootCommand.Execute() } func start(c *cobra.Command, args []string) { + switch store { + case storageMemory: + env.Releases = storage.Init(driver.NewMemory()) + case storageConfigMap: + c, err := env.KubeClient.APIClient() + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot initialize Kubernetes connection: %s", err) + } + env.Releases = storage.Init(driver.NewConfigMaps(c.ConfigMaps(environment.TillerNamespace))) + } + lstn, err := net.Listen("tcp", addr) if err != nil { fmt.Fprintf(os.Stderr, "Server died: %s\n", err) @@ -70,6 +92,7 @@ func start(c *cobra.Command, args []string) { fmt.Printf("Tiller is running on %s\n", addr) fmt.Printf("Tiller probes server is running on %s\n", probe) + fmt.Printf("Storage driver is %s\n", env.Releases.Name()) srvErrCh := make(chan error) probeErrCh := make(chan error) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index cb7e4590b..6e5e74e30 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -29,6 +29,7 @@ import ( "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/apimachinery/registered" "k8s.io/kubernetes/pkg/apis/batch" + unversionedclient "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" "k8s.io/kubernetes/pkg/kubectl" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" @@ -54,6 +55,16 @@ func New(config clientcmd.ClientConfig) *Client { // ResourceActorFunc performs an action on a single resource. type ResourceActorFunc func(*resource.Info) error +// APIClient returns a Kubernetes API client. +// +// This is necessary because cmdutil.Client is a field, not a method, which +// means it can't satisfy an interface's method requirement. In order to ensure +// that an implementation of environment.KubeClient can access the raw API client, +// it is necessary to add this method. +func (c *Client) APIClient() (unversionedclient.Interface, error) { + return c.Client() +} + // Create creates kubernetes resources from an io.reader // // Namespace will set the namespace diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index 0925ebc26..afe031bf5 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -32,6 +32,9 @@ import ( client "k8s.io/kubernetes/pkg/client/unversioned" ) +// ConfigMapsDriverName is the string name of the driver. +const ConfigMapsDriverName = "ConfigMap" + var b64 = base64.StdEncoding // labels is a map of key value pairs to be included as metadata in a configmap object. @@ -54,6 +57,11 @@ func NewConfigMaps(impl client.ConfigMapsInterface) *ConfigMaps { return &ConfigMaps{impl: impl} } +// Name returns the name of the driver. +func (cfgmaps *ConfigMaps) Name() string { + return ConfigMapsDriverName +} + // Get fetches the release named by key. The corresponding release is returned // or error if not found. func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { diff --git a/pkg/storage/driver/cfgmaps_test.go b/pkg/storage/driver/cfgmaps_test.go index 95638057d..bf2c3f7da 100644 --- a/pkg/storage/driver/cfgmaps_test.go +++ b/pkg/storage/driver/cfgmaps_test.go @@ -27,6 +27,15 @@ import ( "k8s.io/kubernetes/pkg/client/unversioned" ) +var _ Driver = &ConfigMaps{} + +func TestConfigMapName(t *testing.T) { + c := newTestFixture(t) + if c.Name() != ConfigMapsDriverName { + t.Errorf("Expected name to be %q, got %q", ConfigMapsDriverName, c.Name()) + } +} + func TestConfigMapGet(t *testing.T) { key := "key-1" rel := newTestRelease(key, 1, rspb.Status_DEPLOYED) diff --git a/pkg/storage/driver/driver.go b/pkg/storage/driver/driver.go index 0cf51d6f6..f3593d278 100644 --- a/pkg/storage/driver/driver.go +++ b/pkg/storage/driver/driver.go @@ -73,4 +73,5 @@ type Driver interface { Updator Deletor Queryor + Name() string } diff --git a/pkg/storage/driver/memory.go b/pkg/storage/driver/memory.go index bea495133..76351b1a7 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -22,6 +22,9 @@ import ( rspb "k8s.io/helm/pkg/proto/hapi/release" ) +// MemoryDriverName is the string name of this driver. +const MemoryDriverName = "Memory" + // Memory is the in-memory storage driver implementation. type Memory struct { sync.RWMutex @@ -33,6 +36,11 @@ func NewMemory() *Memory { return &Memory{cache: map[string]*rspb.Release{}} } +// Name returns the name of the driver. +func (mem *Memory) Name() string { + return MemoryDriverName +} + // Get returns the release named by key or returns ErrReleaseNotFound. func (mem *Memory) Get(key string) (*rspb.Release, error) { defer unlock(mem.rlock()) diff --git a/pkg/storage/driver/memory_test.go b/pkg/storage/driver/memory_test.go index b02f8350b..2c50fd1a4 100644 --- a/pkg/storage/driver/memory_test.go +++ b/pkg/storage/driver/memory_test.go @@ -23,6 +23,15 @@ import ( rspb "k8s.io/helm/pkg/proto/hapi/release" ) +var _ Driver = &Memory{} + +func TestMemoryName(t *testing.T) { + mem := NewMemory() + if mem.Name() != MemoryDriverName { + t.Errorf("Expected name to be %q, got %q", MemoryDriverName, mem.Name()) + } +} + func TestMemoryGet(t *testing.T) { key := "test-1" rls := &rspb.Release{Name: key} From 80761b6274fea0c88b6d8f012726f03ce0adb9fb Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 15 Aug 2016 16:34:37 -0600 Subject: [PATCH 007/183] fix(helm): document KUBECONFIG env var Closes #1047 --- cmd/helm/helm.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index ad1049267..224102dc1 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -60,6 +60,7 @@ Common actions from this point include: Environment: $HELM_HOME Set an alternative location for Helm files. By default, these are stored in ~/.helm $HELM_HOST Set an alternative Tiller host. The format is host:port (default ":44134"). + $KUBECONFIG Set an alternate Kubernetes configuration file (default: "~/.kube/config"). ` func newRootCmd(out io.Writer) *cobra.Command { From 03e0b3632626697ac7eb295133dd73a6bda6c9f8 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 16 Aug 2016 16:24:39 -0600 Subject: [PATCH 008/183] feat(tiller): update Sprig to 2.4.0 Closes #1055 --- glide.lock | 242 ++++++++++++++++++++++++++--------------------------- glide.yaml | 2 +- 2 files changed, 122 insertions(+), 122 deletions(-) diff --git a/glide.lock b/glide.lock index 00c27c575..e53220222 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: ae516306cd7ec28920ecd9e6edf1767336ccdfad7846bcb5111fb085d8848bbd -updated: 2016-08-12T10:41:12.543890279-07:00 +hash: 294741f7c5011d1558519802e282dcbb49b94578397611b3aa2506182a2585cc +updated: 2016-08-16T16:13:10.682420831-06:00 imports: - name: github.com/aokoli/goutils version: 9c37978a95bd5c709a15883b6242714ea6709e64 @@ -193,7 +193,7 @@ imports: - name: github.com/Masterminds/semver version: 808ed7761c233af2de3f9729a041d68c62527f3a - name: github.com/Masterminds/sprig - version: abe09979bcb1ec0a50b2d7bbf67e6aaa11787417 + version: bfc1db6cb1f1bab7d9fe2f317fca26a1e95fa54a - name: github.com/mattn/go-runewidth version: d6bea18f789704b5f83375793155289da36a3c7f - name: github.com/matttproud/golang_protobuf_extensions @@ -251,12 +251,12 @@ imports: version: fb93926129b8ec0056f2f458b1f519654814edf0 subpackages: - context + - context/ctxhttp - http2 - - trace - http2/hpack - internal/timeseries + - trace - websocket - - context/ctxhttp - name: golang.org/x/oauth2 version: b5adcc2dcdf009d0391547edc6ecbaff889f5bb9 subpackages: @@ -274,8 +274,8 @@ imports: - internal/log - internal/modules - internal/remote_api - - urlfetch - internal/urlfetch + - urlfetch - name: google.golang.org/cloud version: eb47ba841d53d93506cfbfbc03927daf9cc48f88 repo: https://code.googlesource.com/gocloud @@ -286,14 +286,14 @@ imports: - name: google.golang.org/grpc version: dec33edc378cf4971a2741cfd86ed70a644d6ba3 subpackages: - - metadata - codes - credentials - grpclog - internal + - metadata - naming - - transport - peer + - transport - name: gopkg.in/inf.v0 version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 - name: gopkg.in/yaml.v2 @@ -301,154 +301,154 @@ imports: - name: k8s.io/kubernetes version: a570a0fef12408f1c0a7214952055bad39c160d7 subpackages: + - federation/apis/federation + - federation/apis/federation/install + - federation/apis/federation/v1beta1 + - federation/client/clientset_generated/federation_internalclientset + - federation/client/clientset_generated/federation_internalclientset/typed/core/unversioned + - federation/client/clientset_generated/federation_internalclientset/typed/federation/unversioned - pkg/api - - pkg/api/meta + - pkg/api/annotations + - pkg/api/endpoints - 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 - pkg/api/errors - - pkg/client/unversioned/testclient - - pkg/api/testapi - - pkg/api/v1 + - pkg/api/install + - pkg/api/meta - pkg/api/meta/metatypes + - pkg/api/pod - pkg/api/resource - - pkg/auth/user - - pkg/conversion - - pkg/fields - - pkg/runtime/serializer - - pkg/types - - pkg/util - - pkg/util/intstr - - pkg/util/rand - - pkg/util/sets - - pkg/api/install + - pkg/api/rest + - pkg/api/service + - pkg/api/testapi + - pkg/api/unversioned + - pkg/api/unversioned/validation + - pkg/api/util + - pkg/api/v1 + - pkg/api/validation + - pkg/apimachinery + - pkg/apimachinery/registered - pkg/apis/apps - pkg/apis/apps/install + - pkg/apis/apps/v1alpha1 + - pkg/apis/authentication.k8s.io - pkg/apis/authentication.k8s.io/install + - pkg/apis/authentication.k8s.io/v1beta1 + - pkg/apis/authorization - pkg/apis/authorization/install + - pkg/apis/authorization/v1beta1 - pkg/apis/autoscaling - pkg/apis/autoscaling/install + - pkg/apis/autoscaling/v1 + - pkg/apis/batch - pkg/apis/batch/install + - pkg/apis/batch/v1 - pkg/apis/batch/v2alpha1 + - pkg/apis/componentconfig - pkg/apis/componentconfig/install + - pkg/apis/componentconfig/v1alpha1 - pkg/apis/extensions - pkg/apis/extensions/install + - pkg/apis/extensions/v1beta1 + - pkg/apis/extensions/validation - pkg/apis/policy - pkg/apis/policy/install + - pkg/apis/policy/v1alpha1 - pkg/apis/rbac - pkg/apis/rbac/install + - pkg/apis/rbac/v1alpha1 + - pkg/auth/user + - pkg/capabilities + - pkg/client/cache + - 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/client/metrics + - pkg/client/record + - pkg/client/restclient + - pkg/client/transport - pkg/client/typed/discovery - - pkg/util/net - - pkg/util/wait - - pkg/version - - plugin/pkg/client/auth - - pkg/util/validation - - pkg/util/validation/field - - pkg/apimachinery + - pkg/client/unversioned + - pkg/client/unversioned/adapters/internalclientset - pkg/client/unversioned/auth + - pkg/client/unversioned/clientcmd - pkg/client/unversioned/clientcmd/api - pkg/client/unversioned/clientcmd/api/latest - - pkg/util/errors - - pkg/util/homedir - - pkg/kubelet/server/portforward - - pkg/util/httpstream - - pkg/util/runtime - - pkg/client/transport - - pkg/kubelet/server/remotecommand - - pkg/util/httpstream/spdy - - federation/apis/federation - - federation/client/clientset_generated/federation_internalclientset - - pkg/api/annotations - - pkg/api/util - - pkg/api/validation - - pkg/apis/batch/v1 - - pkg/client/clientset_generated/internalclientset - - pkg/client/unversioned/adapters/internalclientset + - pkg/client/unversioned/clientcmd/api/v1 + - pkg/client/unversioned/fake + - pkg/client/unversioned/portforward + - pkg/client/unversioned/remotecommand + - pkg/client/unversioned/testclient + - pkg/controller + - pkg/controller/framework + - pkg/conversion + - pkg/conversion/queryparams - pkg/credentialprovider - pkg/fieldpath + - pkg/fields + - pkg/httplog + - pkg/kubectl + - pkg/kubectl/cmd/util + - pkg/kubectl/resource + - pkg/kubelet/qos - pkg/kubelet/qos/util - - pkg/util/deployment - - pkg/util/integer - - pkg/util/jsonpath - - pkg/util/slice - - pkg/api/service - - pkg/controller + - pkg/kubelet/server/portforward + - pkg/kubelet/server/remotecommand + - pkg/labels + - pkg/master/ports + - pkg/registry/generic - pkg/registry/thirdpartyresourcedata + - pkg/runtime + - pkg/runtime/serializer - pkg/runtime/serializer/json - - pkg/util/flag - - pkg/conversion/queryparams - - pkg/util/json - - third_party/forked/json - - federation/apis/federation/install - - pkg/runtime/serializer/recognizer - - pkg/util/parsers - - pkg/watch/versioned - - third_party/forked/reflect - pkg/runtime/serializer/protobuf - - pkg/runtime/serializer/versioning - - 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/recognizer - pkg/runtime/serializer/streaming + - pkg/runtime/serializer/versioning + - pkg/security/podsecuritypolicy/util + - pkg/storage + - pkg/types + - pkg/util - pkg/util/crypto + - pkg/util/deployment + - pkg/util/errors + - pkg/util/flag - pkg/util/flowcontrol - - plugin/pkg/client/auth/gcp - - plugin/pkg/client/auth/oidc - - pkg/client/unversioned/clientcmd/api/v1 - - pkg/httplog - - pkg/util/wsstream - - third_party/golang/netutil - - federation/client/clientset_generated/federation_internalclientset/typed/core/unversioned - - federation/client/clientset_generated/federation_internalclientset/typed/federation/unversioned - - pkg/api/endpoints - - pkg/api/pod - - pkg/api/unversioned/validation - - pkg/capabilities - - 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/framer + - pkg/util/hash + - pkg/util/homedir + - pkg/util/httpstream + - pkg/util/httpstream/spdy + - pkg/util/integer + - pkg/util/intstr + - pkg/util/json + - pkg/util/jsonpath - pkg/util/labels + - pkg/util/net + - pkg/util/net/sets + - pkg/util/parsers - pkg/util/pod + - pkg/util/rand - pkg/util/replicaset + - pkg/util/runtime + - pkg/util/sets + - pkg/util/slice + - pkg/util/strategicpatch + - pkg/util/validation + - pkg/util/validation/field + - pkg/util/wait + - pkg/util/wsstream + - pkg/util/yaml + - pkg/version + - pkg/watch + - pkg/watch/versioned + - plugin/pkg/client/auth + - plugin/pkg/client/auth/gcp + - plugin/pkg/client/auth/oidc + - third_party/forked/json + - third_party/forked/reflect + - third_party/golang/netutil - third_party/golang/template - - pkg/util/net/sets - - pkg/client/cache - - pkg/client/record - - pkg/controller/framework - - pkg/util/hash - - pkg/api/rest - - pkg/apis/extensions/validation - - pkg/registry/generic - - pkg/util/framer - - federation/apis/federation/v1beta1 - - pkg/kubelet/qos - - pkg/master/ports - - pkg/security/podsecuritypolicy/util - - pkg/storage testImports: [] diff --git a/glide.yaml b/glide.yaml index ab617d866..90032e5bc 100644 --- a/glide.yaml +++ b/glide.yaml @@ -8,7 +8,7 @@ import: - package: github.com/spf13/pflag version: 367864438f1b1a3c7db4da06a2f55b144e6784e0 - package: github.com/Masterminds/sprig - version: ^2.3 + version: ^2.4 - package: gopkg.in/yaml.v2 - package: github.com/Masterminds/semver version: 1.1.0 From dacc4013c178d48962bb5c898848b518e33db806 Mon Sep 17 00:00:00 2001 From: Trevor Hartman Date: Tue, 16 Aug 2016 18:07:51 -0600 Subject: [PATCH 009/183] Include values from both --set and --values when specified on install --- cmd/helm/install.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index b526ea090..b41669d24 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -147,13 +147,28 @@ func (i *installCmd) run() error { } func (i *installCmd) vals() ([]byte, error) { - if len(i.values.pairs) > 0 { - return i.values.yaml() + var buffer bytes.Buffer + + // User specified a values file via -f/--values + if i.valuesFile != "" { + bytes, err := ioutil.ReadFile(i.valuesFile) + if err != nil { + return []byte{}, err + } + buffer.Write(bytes) } - if i.valuesFile == "" { - return []byte{}, nil + + // User specified value pairs via --set + // These override any values in the specified file + if len(i.values.pairs) > 0 { + bytes, err := i.values.yaml() + if err != nil { + return []byte{}, err + } + buffer.Write(bytes) } - return ioutil.ReadFile(i.valuesFile) + + return buffer.Bytes(), nil } func (i *installCmd) printRelease(rel *release.Release) { From 6cc01e3a9147d2a4f7641ccda9f87077ef779eda Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 16 Aug 2016 19:44:38 -0700 Subject: [PATCH 010/183] fix(cmd): remove default port in cmd doc Default is unset. --- cmd/helm/helm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 224102dc1..12156c038 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -59,7 +59,7 @@ Common actions from this point include: Environment: $HELM_HOME Set an alternative location for Helm files. By default, these are stored in ~/.helm - $HELM_HOST Set an alternative Tiller host. The format is host:port (default ":44134"). + $HELM_HOST Set an alternative Tiller host. The format is host:port. $KUBECONFIG Set an alternate Kubernetes configuration file (default: "~/.kube/config"). ` From 3e791ea2ef926e48b8748956f7fdab3785136a39 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 16 Aug 2016 19:47:17 -0700 Subject: [PATCH 011/183] feat(ci): run tests on go 1.7 --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 09db231ba..74fab120f 100644 --- a/circle.yml +++ b/circle.yml @@ -3,7 +3,7 @@ machine: - curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0 environment: - GOVERSION: "1.6.3" + GOVERSION: "1.7" GOPATH: "${HOME}/.go_workspace" WORKDIR: "${GOPATH}/src/k8s.io/helm" From 7bc56e74f9c71f8827b3a9425ff296a4b163cb11 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 16 Aug 2016 19:56:10 -0700 Subject: [PATCH 012/183] fix(cmd): s/supress/suppress/ Suppress is spelled wrong. Matt did it. --- cmd/helm/init.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/helm/init.go b/cmd/helm/init.go index 81c7388c9..555185cc2 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -75,7 +75,7 @@ func (i *initCmd) run() error { if !strings.Contains(err.Error(), `"tiller-deploy" already exists`) { return fmt.Errorf("error installing: %s", err) } - fmt.Fprintln(i.out, "Warning: Tiller is already installed in the cluster. (Use --client-only to supress this message.)") + fmt.Fprintln(i.out, "Warning: Tiller is already installed in the cluster. (Use --client-only to suppress this message.)") } else { fmt.Fprintln(i.out, "\nTiller (the helm server side component) has been installed into your Kubernetes Cluster.") } From edd6fd7465c012ccefa8524a9679602a7d2393d5 Mon Sep 17 00:00:00 2001 From: Trevor Hartman Date: Wed, 17 Aug 2016 12:47:40 -0600 Subject: [PATCH 013/183] Test overriding a property in TestValues --- cmd/helm/install_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index 7043eee6d..7bc44702e 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -121,6 +121,23 @@ sailor: sinbad if vobj.String() != y { t.Errorf("Expected String() to be \n%s\nGot\n%s\n", y, out) } + + // Combined case, overriding a property + vals["sailor"] = "pisti" + updated_yaml := `good: true +port: + destination: basrah + source: baghdad +sailor: pisti +` + new_out, err := vobj.yaml() + if err != nil { + t.Fatal(err) + } + if string(new_out) != updated_yaml { + t.Errorf("Expected YAML to be \n%s\nGot\n%s\n", updated_yaml, new_out) + } + } type nameTemplateTestCase struct { From 67778cbcd184d101eed36679c70113e114e454e0 Mon Sep 17 00:00:00 2001 From: Jess Frazelle Date: Wed, 17 Aug 2016 13:14:01 -0700 Subject: [PATCH 014/183] update readme example and change repository to https Signed-off-by: Jess Frazelle --- README.md | 4 ++++ docs/chart_repository.md | 18 +++++++++--------- docs/chart_repository_sync_example.md | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ecf4bf6b7..e8861153f 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,12 @@ Using Helm is as easy as this: ```console $ helm init # Initialize Helm as well as the Tiller server + +# From the root of this repository run the following to install an example from +the docs $ helm install docs/examples/alpine # Install the example Alpine chart happy-panda # <-- That's the name of your release + $ helm list # List all releases happy-panda quiet-kitten diff --git a/docs/chart_repository.md b/docs/chart_repository.md index 3400206b0..0870a95e2 100644 --- a/docs/chart_repository.md +++ b/docs/chart_repository.md @@ -10,9 +10,9 @@ A _chart repository_ is an HTTP server that houses one or more packaged charts. Because a chart repository can be any HTTP server that can serve YAML and tar files and can answer GET requests, you have a plethora of options when it comes down to hosting your own chart repository. For example, you can use a Google Cloud Storage(GCS) bucket, Amazon S3 bucket, or even create your own web server. ### The chart repository structure -A chart repository consists of packaged charts and a special file called `index.yaml` which contains an index of all of the charts in the repository. A chart repository has a flat structure. Given a repository URL, you should be able to download a chart via a GET request to `URL/chartname-version.tgz`. +A chart repository consists of packaged charts and a special file called `index.yaml` which contains an index of all of the charts in the repository. A chart repository has a flat structure. Given a repository URL, you should be able to download a chart via a GET request to `URL/chartname-version.tgz`. -For example, if a repository lives at the URL: `http://helm-charts.com`, the `alpine-0.1.0` chart would live at `http://helm-charts.com/alpine-0.1.0.tgz`. The index file would also live in the same chart repository at `http://helm-charts.com/index.yaml`. +For example, if a repository lives at the URL: `https://helm-charts.com`, the `alpine-0.1.0` chart would live at `https://helm-charts.com/alpine-0.1.0.tgz`. The index file would also live in the same chart repository at `https://helm-charts.com/index.yaml`. #### The index file The index file is a yaml file called `index.yaml`. It contains some metadata about the package as well as a dump of the Chart.yaml file of a packaged chart. A valid chart repository must have an index file. The index file contains information about each chart in the chart repository. The `helm repo index` command will generate an index file based on a given local directory that contains packaged charts. @@ -21,7 +21,7 @@ This is an example of an index file: ``` alpine-0.1.0: name: alpine - url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz + url: https://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz created: 2016-05-26 11:23:44.086354411 +0000 UTC checksum: a61575c2d3160e5e39abf2a5ec984d6119404b18 chartfile: @@ -31,7 +31,7 @@ alpine-0.1.0: home: https://github.com/example-charts/alpine redis-2.0.0: name: redis - url: http://storage.googleapis.com/kubernetes-charts/redis-2.0.0.tgz + url: https://storage.googleapis.com/kubernetes-charts/redis-2.0.0.tgz created: 2016-05-26 11:23:44.087939192 +0000 UTC checksum: 2cea3048cf85d588204e1b1cc0674472b4517919 chartfile: @@ -59,7 +59,7 @@ Congratulations, now you have an empty GCS bucket ready to serve charts! ## Store charts in your chart repository Now that you have a chart repository, let's upload a chart and an index file to the repository. -Charts in a chart repository must be packaged (`helm package chart-name/`) and versioned correctly (following [SemVer 2](http://semver.org/) guidelines). +Charts in a chart repository must be packaged (`helm package chart-name/`) and versioned correctly (following [SemVer 2](https://semver.org/) guidelines). These next steps compose an example workflow, but you are welcome to use whatever workflow you fancy for storing and updating charts in your chart repository. @@ -75,7 +75,7 @@ $ mv alpine-0.1.0.tgz fantastic-charts/ Outside of your directory, run the `helm repo index [DIR] [URL]` command. This command takes the path of the local directory that you just created and the URL of your remote chart repository and composes an index.yaml file inside the given directory path. ```console -$ helm repo index fantastic-charts http://storage.googleapis.com/fantastic-charts +$ helm repo index fantastic-charts https://storage.googleapis.com/fantastic-charts ``` Now, you can upload the chart and the index file to your chart repository using a sync tool or manually. If you're using Google Cloud Storage, check out this [example workflow](chart_repository_sync_example.md) using the gsutil client. @@ -87,14 +87,14 @@ When you've created another chart, move the new packaged chart into the fantasti ## Share your charts with others When you're ready to share your charts, simply let someone know what the url of your repository is. -*Note: A public GCS bucket can be accessed via simple http at this address `http://storage.googleapis.com/bucket-name`.* +*Note: A public GCS bucket can be accessed via simple http at this address `https://storage.googleapis.com/bucket-name`.* From there, they will add the repository to their helm client via the `helm repo add [NAME] [URL]` command with any name they would like to use to reference the repository. ```console -$ helm repo add fantastic-charts http://storage.googleapis.com/fantastic-charts +$ helm repo add fantastic-charts https://storage.googleapis.com/fantastic-charts $ helm repo list -fantastic-charts http://storage.googleapis.com/fantastic-charts +fantastic-charts https://storage.googleapis.com/fantastic-charts ``` *Note: A repository will not be added if it does not contain a valid index.yaml.* diff --git a/docs/chart_repository_sync_example.md b/docs/chart_repository_sync_example.md index e38e90082..41e5523f4 100644 --- a/docs/chart_repository_sync_example.md +++ b/docs/chart_repository_sync_example.md @@ -19,7 +19,7 @@ $ mv alpine-0.1.0.tgz fantastic-charts/ Use helm to generate an updated index.yaml file by passing in the directory path and the url of the remote repository to the `helm repo index` command like this: ```console -$ helm repo index fantastic-charts/ http://storage.googleapis.com/fantastic-charts +$ helm repo index fantastic-charts/ https://storage.googleapis.com/fantastic-charts ``` This will generate an updated index.yaml file and place in the `fantastic-charts/` directory. From 9b58f1018b9d8c5004aaa77db9317633df6c9b75 Mon Sep 17 00:00:00 2001 From: Adnan Abdulhussein Date: Wed, 17 Aug 2016 14:33:12 -0700 Subject: [PATCH 015/183] docs(notes.txt): Add NOTES.txt to Chart spec, and describe chart documentation --- docs/charts.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/charts.md b/docs/charts.md index 03eecba04..2039a0598 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -25,6 +25,7 @@ wordpress/ Chart.yaml # A YAML file containing information about the chart LICENSE # OPTIONAL: A plain text file containing the license for the chart README.md # OPTIONAL: A human-readable README file + NOTES.txt # OPTIONAL: A plain text file containing short usage notes values.yaml # The default configuration values for this chart charts/ # OPTIONAL: A directory containing any charts upon which this chart depends. templates/ # OPTIONAL: A directory of templates that, when combined with values, @@ -88,6 +89,25 @@ in the `Chart.yaml` as a token in the package name. The system assumes that the version number in the chart package name matches the version number in the `Chart.yaml`. Failure to meet this assumption will cause an error. +## Chart LICENSE, README and NOTES + +Charts can also contain files that describe the installation, configuration, usage and license of a +chart. A README for a chart should be formatted in Markdown (README.md), and should generally +contain: + +- A description of the application or service the chart provides +- Any prerequisites or requirements to run the chart +- Descriptions of options in `values.yaml` and default values +- Any other information that may be relevant to the installation or configuration of the chart + +The chart can also contain a short plain text NOTES.txt file that will be printed out after +installation, and when viewing the status of a release. This file is evaluated as a +[template](#templates-and-values), and can be used to display usage notes, next steps, or any other +information relevant to a release of the chart. For example, instructions could be provided for +connecting to a database, or accessing a web UI. Since this file is printed to STDOUT when running +`helm install` or `helm status`, it is recommended to keep the content brief and point to the README +for greater detail. + ## Chart Dependencies In Helm, one chart may depend on any number of other charts. These From 1c9b8d7257bbf837b1073bf4cc012d5e1fd16d60 Mon Sep 17 00:00:00 2001 From: Ebrahim Byagowi Date: Wed, 17 Aug 2016 12:57:13 +0430 Subject: [PATCH 016/183] Add purge option for completely remove a release from tiller --- _proto/hapi/services/tiller.proto | 2 + cmd/helm/delete.go | 3 + cmd/helm/delete_test.go | 7 ++ cmd/tiller/release_server.go | 17 ++++- cmd/tiller/release_server_test.go | 64 +++++++++++++++++ pkg/helm/option.go | 15 +++- pkg/proto/hapi/services/tiller.pb.go | 102 ++++++++++++++------------- 7 files changed, 157 insertions(+), 53 deletions(-) diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto index ba3252e3c..db791ea63 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -209,6 +209,8 @@ message UninstallReleaseRequest { string name = 1; // DisableHooks causes the server to skip running any hooks for the uninstall. bool disable_hooks = 2; + // Purge removes the release from the store and make its name free for later use. + bool purge = 3; } // UninstallReleaseResponse represents a successful response to an uninstall request. diff --git a/cmd/helm/delete.go b/cmd/helm/delete.go index 0d42032c1..2a8fd02ff 100644 --- a/cmd/helm/delete.go +++ b/cmd/helm/delete.go @@ -37,6 +37,7 @@ type deleteCmd struct { name string dryRun bool disableHooks bool + purge bool out io.Writer client helm.Interface @@ -67,6 +68,7 @@ func newDeleteCmd(c helm.Interface, out io.Writer) *cobra.Command { f := cmd.Flags() f.BoolVar(&del.dryRun, "dry-run", false, "simulate a delete") f.BoolVar(&del.disableHooks, "no-hooks", false, "prevent hooks from running during deletion") + f.BoolVar(&del.purge, "purge", false, "remove the release from the store and make its name free for later use") return cmd } @@ -75,6 +77,7 @@ func (d *deleteCmd) run() error { opts := []helm.DeleteOption{ helm.DeleteDryRun(d.dryRun), helm.DeleteDisableHooks(d.disableHooks), + helm.DeletePurge(d.purge), } _, err := d.client.DeleteRelease(d.name, opts...) return prettyError(err) diff --git a/cmd/helm/delete_test.go b/cmd/helm/delete_test.go index 517cbd22e..72ef4117d 100644 --- a/cmd/helm/delete_test.go +++ b/cmd/helm/delete_test.go @@ -40,6 +40,13 @@ func TestDelete(t *testing.T) { expected: "", resp: releaseMock(&releaseOptions{name: "aeneas"}), }, + { + name: "purge", + args: []string{"aeneas"}, + flags: []string{"--purge"}, + expected: "", + resp: releaseMock(&releaseOptions{name: "aeneas"}), + }, { name: "delete without release", args: []string{}, diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 2021d2eec..f0f919908 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -518,6 +518,13 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR // TODO: Are there any cases where we want to force a delete even if it's // already marked deleted? if rel.Info.Status.Code == release.Status_DELETED { + if req.Purge { + if _, err := s.env.Releases.Delete(rel.Name); err != nil { + log.Printf("uninstall: Failed to purge the release: %s", err) + return nil, err + } + return &services.UninstallReleaseResponse{Release: rel}, nil + } return nil, fmt.Errorf("the release named %q is already deleted", req.Name) } @@ -544,8 +551,14 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR } } - if err := s.env.Releases.Update(rel); err != nil { - log.Printf("uninstall: Failed to store updated release: %s", err) + if !req.Purge { + if err := s.env.Releases.Update(rel); err != nil { + log.Printf("uninstall: Failed to store updated release: %s", err) + } + } else { + if _, err := s.env.Releases.Delete(rel.Name); err != nil { + log.Printf("uninstall: Failed to purge the release: %s", err) + } } return res, nil diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index f2ba5c69a..de865523b 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -438,6 +438,70 @@ func TestUninstallRelease(t *testing.T) { } } +func TestUninstallPurgeRelease(t *testing.T) { + c := context.Background() + rs := rsFixture() + rs.env.Releases.Create(releaseStub()) + + req := &services.UninstallReleaseRequest{ + Name: "angry-panda", + Purge: true, + } + + res, err := rs.UninstallRelease(c, req) + if err != nil { + t.Errorf("Failed uninstall: %s", err) + } + + if res.Release.Name != "angry-panda" { + t.Errorf("Expected angry-panda, got %q", res.Release.Name) + } + + if res.Release.Info.Status.Code != release.Status_DELETED { + t.Errorf("Expected status code to be DELETED, got %d", res.Release.Info.Status.Code) + } + + if res.Release.Hooks[0].LastRun.Seconds == 0 { + t.Error("Expected LastRun to be greater than zero.") + } + + if res.Release.Info.Deleted.Seconds <= 0 { + t.Errorf("Expected valid UNIX date, got %d", res.Release.Info.Deleted.Seconds) + } + + // Test that after deletion, we get an error that it is already deleted. + if _, err = rs.UninstallRelease(c, req); err == nil { + t.Error("Expected error when deleting already deleted resource.") + } else if err.Error() != "release: not found" { + t.Errorf("Unexpected error message: %q", err) + } +} + +func TestUninstallPurgeDeleteRelease(t *testing.T) { + c := context.Background() + rs := rsFixture() + rs.env.Releases.Create(releaseStub()) + + req := &services.UninstallReleaseRequest{ + Name: "angry-panda", + } + + _, err := rs.UninstallRelease(c, req) + if err != nil { + t.Errorf("Failed uninstall: %s", err) + } + + req2 := &services.UninstallReleaseRequest{ + Name: "angry-panda", + Purge: true, + } + + _, err2 := rs.UninstallRelease(c, req2) + if err2 != nil { + t.Errorf("Failed uninstall: %s", err2) + } +} + func TestUninstallReleaseNoHooks(t *testing.T) { c := context.Background() rs := rsFixture() diff --git a/pkg/helm/option.go b/pkg/helm/option.go index 2af4742da..73a478574 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -47,6 +47,8 @@ type options struct { instReq rls.InstallReleaseRequest // release update options are applied directly to the update release request updateReq rls.UpdateReleaseRequest + // release uninstall options are applied directly to the uninstall release request + uninstallReq rls.UninstallReleaseRequest } // Home specifies the location of helm home, (default = "$HOME/.helm"). @@ -143,6 +145,13 @@ func DeleteDryRun(dry bool) DeleteOption { } } +// DeletePurge removes the release from the store and make its name free for later use. +func DeletePurge(purge bool) DeleteOption { + return func(opts *options) { + opts.uninstallReq.Purge = purge + } +} + // UpgradeDisableHooks will disable hooks for an upgrade operation. func UpgradeDisableHooks(disable bool) UpdateOption { return func(opts *options) { @@ -239,7 +248,11 @@ func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient, } return &rls.UninstallReleaseResponse{Release: r.Release}, nil } - return rlc.UninstallRelease(context.TODO(), &rls.UninstallReleaseRequest{Name: rlsName, DisableHooks: o.disableHooks}) + + o.uninstallReq.Name = rlsName + o.uninstallReq.DisableHooks = o.disableHooks + + return rlc.UninstallRelease(context.TODO(), &o.uninstallReq) } // Executes tiller.UpdateRelease RPC. diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index 838e83647..5d93ceeb4 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -330,6 +330,8 @@ type UninstallReleaseRequest struct { Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` // DisableHooks causes the server to skip running any hooks for the uninstall. DisableHooks bool `protobuf:"varint,2,opt,name=disable_hooks,json=disableHooks" json:"disable_hooks,omitempty"` + // Remove the release from the store and make its name free for later use. + Purge bool `protobuf:"varint,3,opt,name=purge" json:"purge,omitempty"` } func (m *UninstallReleaseRequest) Reset() { *m = UninstallReleaseRequest{} } @@ -626,54 +628,54 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ } var fileDescriptor0 = []byte{ - // 774 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdb, 0x6e, 0xd3, 0x4c, - 0x10, 0xae, 0x93, 0x34, 0x87, 0xe9, 0x41, 0xe9, 0xfe, 0x6d, 0x93, 0xdf, 0xfa, 0x7f, 0x84, 0x8c, - 0x80, 0x52, 0xa8, 0x03, 0xe1, 0x1e, 0x29, 0x6d, 0xa3, 0xb6, 0x6a, 0x48, 0xa5, 0x0d, 0x05, 0x89, - 0x0b, 0x22, 0x37, 0xd9, 0x50, 0x83, 0x6b, 0x07, 0xef, 0xa6, 0xa2, 0x8f, 0xc0, 0x1b, 0x71, 0xc3, - 0xdb, 0xf0, 0x16, 0xdc, 0xb0, 0x07, 0xaf, 0xc9, 0xc1, 0x06, 0xd3, 0x1b, 0x67, 0x77, 0xe7, 0xdb, - 0x6f, 0x66, 0xbe, 0x99, 0x9d, 0x16, 0xcc, 0x4b, 0x67, 0xec, 0x36, 0x28, 0x09, 0xaf, 0xdd, 0x01, - 0xa1, 0x0d, 0xe6, 0x7a, 0x1e, 0x09, 0xed, 0x71, 0x18, 0xb0, 0x00, 0x6d, 0x0a, 0x9b, 0xad, 0x6d, - 0xb6, 0xb2, 0x99, 0xdb, 0xf2, 0xc6, 0xe0, 0xd2, 0x09, 0x99, 0xfa, 0x2a, 0xb4, 0x59, 0x9b, 0x3e, - 0x0f, 0xfc, 0x91, 0xfb, 0x3e, 0x32, 0x28, 0x17, 0x21, 0xf1, 0x88, 0x43, 0x89, 0xfe, 0x9d, 0xb9, - 0xa4, 0x6d, 0xae, 0x3f, 0x0a, 0x94, 0xc1, 0xfa, 0x6e, 0xc0, 0x3f, 0x1d, 0x97, 0x32, 0xac, 0x4c, - 0x14, 0x93, 0x4f, 0x13, 0x42, 0x19, 0xda, 0x84, 0x65, 0xcf, 0xbd, 0x72, 0x59, 0xdd, 0xb8, 0x6b, - 0xec, 0xe4, 0xb1, 0xda, 0xa0, 0x6d, 0x28, 0x06, 0xa3, 0x11, 0x25, 0xac, 0x9e, 0xe3, 0xc7, 0x15, - 0x1c, 0xed, 0xd0, 0x0b, 0x28, 0xd1, 0x20, 0x64, 0xfd, 0x8b, 0x9b, 0x7a, 0x9e, 0x1b, 0xd6, 0x9b, - 0xf7, 0xed, 0xa4, 0x9c, 0x6c, 0xe1, 0xa9, 0xc7, 0x81, 0xb6, 0xf8, 0xec, 0xdf, 0xe0, 0x22, 0x95, - 0xbf, 0x82, 0x77, 0xe4, 0x7a, 0x8c, 0x84, 0xf5, 0x82, 0xe2, 0x55, 0x3b, 0x74, 0x04, 0x20, 0x79, - 0x83, 0x70, 0xc8, 0x6d, 0xcb, 0x92, 0x7a, 0x27, 0x03, 0xf5, 0x99, 0xc0, 0xe3, 0x0a, 0xd5, 0x4b, - 0xeb, 0x1d, 0x94, 0x35, 0xc0, 0x6a, 0x42, 0x51, 0xb9, 0x47, 0x2b, 0x50, 0x3a, 0xef, 0x9e, 0x76, - 0xcf, 0xde, 0x74, 0xab, 0x4b, 0xa8, 0x0c, 0x85, 0x6e, 0xeb, 0x65, 0xbb, 0x6a, 0xa0, 0x0d, 0x58, - 0xeb, 0xb4, 0x7a, 0xaf, 0xfa, 0xb8, 0xdd, 0x69, 0xb7, 0x7a, 0xed, 0xc3, 0x6a, 0xce, 0xba, 0x03, - 0x95, 0x98, 0x17, 0x95, 0x20, 0xdf, 0xea, 0x1d, 0xa8, 0x2b, 0x87, 0x6d, 0xbe, 0x32, 0xac, 0x2f, - 0x06, 0x6c, 0xce, 0xca, 0x48, 0xc7, 0x81, 0x4f, 0x89, 0xd0, 0x71, 0x10, 0x4c, 0xfc, 0x58, 0x47, - 0xb9, 0x41, 0x08, 0x0a, 0x3e, 0xf9, 0xac, 0x55, 0x94, 0x6b, 0x81, 0x64, 0x01, 0x73, 0x3c, 0xa9, - 0x20, 0x47, 0xca, 0x0d, 0x7a, 0x06, 0xe5, 0xa8, 0x6a, 0x94, 0x6b, 0x93, 0xdf, 0x59, 0x69, 0x6e, - 0xa9, 0xfc, 0x75, 0x7d, 0x23, 0x8f, 0x38, 0x86, 0x59, 0x7b, 0x50, 0x3b, 0x22, 0x3a, 0x92, 0x1e, - 0x73, 0xd8, 0x24, 0xae, 0xaa, 0xf0, 0xeb, 0x5c, 0x11, 0x19, 0x8c, 0xf0, 0xcb, 0xd7, 0xd6, 0x6b, - 0xa8, 0x2f, 0xc2, 0xa3, 0xe8, 0x13, 0xf0, 0xe8, 0x01, 0x14, 0x44, 0xff, 0xc8, 0xd8, 0x57, 0x9a, - 0x68, 0x36, 0x9a, 0x13, 0x6e, 0xc1, 0xd2, 0x6e, 0xd9, 0xd3, 0xbc, 0x07, 0x81, 0xcf, 0x88, 0xcf, - 0x7e, 0x17, 0x47, 0x07, 0xfe, 0x4d, 0xc0, 0x47, 0x81, 0x34, 0xa0, 0x14, 0xb9, 0x90, 0x77, 0x52, - 0x55, 0xd0, 0x28, 0xeb, 0x1b, 0x2f, 0xc8, 0xf9, 0x78, 0xe8, 0x30, 0xa2, 0x4d, 0xe9, 0xae, 0xd1, - 0x43, 0x5e, 0x24, 0xf1, 0x9e, 0xa2, 0x9c, 0x36, 0x14, 0xb7, 0x7a, 0x74, 0x07, 0xe2, 0x8b, 0x95, - 0x1d, 0xed, 0x42, 0xf1, 0xda, 0xf1, 0x38, 0x8f, 0x2c, 0x52, 0x9c, 0x7d, 0x84, 0x94, 0x8f, 0x11, - 0x47, 0x08, 0x54, 0x83, 0xd2, 0x30, 0xbc, 0xe9, 0x87, 0x13, 0x5f, 0x36, 0x75, 0x19, 0x17, 0xf9, - 0x16, 0x4f, 0x7c, 0x74, 0x0f, 0xd6, 0x86, 0x2e, 0x75, 0x2e, 0x3c, 0xd2, 0xbf, 0x0c, 0x82, 0x8f, - 0x54, 0xf6, 0x75, 0x19, 0xaf, 0x46, 0x87, 0xc7, 0xe2, 0xcc, 0x3a, 0x86, 0xad, 0xb9, 0xf0, 0x6f, - 0xab, 0xc4, 0x0f, 0x03, 0xb6, 0x4e, 0x7c, 0xca, 0x9b, 0xc9, 0x9b, 0x93, 0x22, 0x4e, 0xdb, 0xc8, - 0x9c, 0x76, 0xee, 0x6f, 0xd2, 0xce, 0xcf, 0xa4, 0xad, 0x85, 0x2f, 0x4c, 0x09, 0x9f, 0x45, 0x0a, - 0xf4, 0x1f, 0x54, 0x04, 0x98, 0x8e, 0x9d, 0x01, 0xa9, 0x17, 0xe5, 0xed, 0x5f, 0x07, 0xe8, 0x7f, - 0x80, 0x90, 0x4c, 0x28, 0xe9, 0x4b, 0xf2, 0x92, 0xbc, 0x5f, 0x91, 0x27, 0x5d, 0xd1, 0x55, 0x27, - 0xb0, 0x3d, 0x9f, 0xfc, 0x6d, 0x85, 0xc4, 0x50, 0x3b, 0xf7, 0xdd, 0x44, 0x25, 0x93, 0x9a, 0x6a, - 0x21, 0xb7, 0x5c, 0x42, 0x99, 0x4f, 0xa1, 0xbe, 0xc8, 0x79, 0xcb, 0x00, 0x9b, 0x5f, 0x97, 0x61, - 0x5d, 0xbf, 0x63, 0x35, 0x1d, 0x91, 0x0b, 0xab, 0xd3, 0x63, 0x09, 0x3d, 0x4a, 0x1f, 0x9e, 0x73, - 0x7f, 0x01, 0xcc, 0xdd, 0x2c, 0x50, 0x15, 0xaa, 0xb5, 0xf4, 0xd4, 0x40, 0x14, 0xaa, 0xf3, 0x73, - 0x04, 0xed, 0x25, 0x73, 0xa4, 0x8c, 0x27, 0xd3, 0xce, 0x0a, 0xd7, 0x6e, 0xd1, 0x35, 0x6c, 0x2c, - 0x0c, 0x0d, 0xf4, 0x47, 0x9a, 0xd9, 0x69, 0x64, 0x36, 0x32, 0xe3, 0x63, 0xbf, 0x1f, 0x60, 0x6d, - 0xe6, 0x79, 0xa2, 0x14, 0xb5, 0x92, 0x46, 0x90, 0xf9, 0x38, 0x13, 0x36, 0xf6, 0x75, 0x05, 0xeb, - 0xb3, 0x2d, 0x8c, 0x52, 0x08, 0x12, 0x5f, 0xb9, 0xf9, 0x24, 0x1b, 0x38, 0x76, 0xc7, 0xeb, 0x38, - 0xdf, 0x92, 0x69, 0x75, 0x4c, 0x79, 0x0e, 0x69, 0x75, 0x4c, 0xeb, 0x74, 0x6b, 0x69, 0x1f, 0xde, - 0x96, 0x35, 0xfa, 0xa2, 0x28, 0xff, 0x33, 0x79, 0xfe, 0x33, 0x00, 0x00, 0xff, 0xff, 0x97, 0xd7, - 0x36, 0xd6, 0x33, 0x09, 0x00, 0x00, + // 783 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xcb, 0x6e, 0xd3, 0x4c, + 0x14, 0xae, 0x93, 0x34, 0x97, 0xd3, 0x8b, 0xd2, 0xf9, 0xdb, 0x26, 0xbf, 0x05, 0x08, 0x19, 0x01, + 0xa5, 0x50, 0x07, 0xc2, 0x1e, 0x29, 0x6d, 0xa3, 0xb6, 0x6a, 0x48, 0x25, 0x87, 0x82, 0xc4, 0x82, + 0xc8, 0x4d, 0x26, 0x8d, 0xc1, 0xb5, 0x83, 0x67, 0x52, 0xd1, 0x47, 0xe0, 0x8d, 0xd8, 0xf0, 0x36, + 0xbc, 0x05, 0x1b, 0xe6, 0xe2, 0x31, 0x71, 0x62, 0x83, 0xe9, 0xc6, 0x99, 0x33, 0xe7, 0x9b, 0x73, + 0xf9, 0xce, 0xa5, 0x05, 0x7d, 0x6c, 0x4f, 0x9c, 0x06, 0xc1, 0xc1, 0xb5, 0x33, 0xc0, 0xa4, 0x41, + 0x1d, 0xd7, 0xc5, 0x81, 0x39, 0x09, 0x7c, 0xea, 0xa3, 0x4d, 0xae, 0x33, 0x95, 0xce, 0x94, 0x3a, + 0x7d, 0x5b, 0xbc, 0x18, 0x8c, 0xed, 0x80, 0xca, 0xaf, 0x44, 0xeb, 0xb5, 0xd9, 0x7b, 0xdf, 0x1b, + 0x39, 0x97, 0xa1, 0x42, 0xba, 0x08, 0xb0, 0x8b, 0x6d, 0x82, 0xd5, 0x6f, 0xec, 0x91, 0xd2, 0x39, + 0xde, 0xc8, 0x97, 0x0a, 0xe3, 0x87, 0x06, 0xff, 0x75, 0x1c, 0x42, 0x2d, 0xa9, 0x22, 0x16, 0xfe, + 0x3c, 0xc5, 0x84, 0xa2, 0x4d, 0x58, 0x76, 0x9d, 0x2b, 0x87, 0xd6, 0xb5, 0xfb, 0xda, 0x4e, 0xde, + 0x92, 0x02, 0xda, 0x86, 0xa2, 0x3f, 0x1a, 0x11, 0x4c, 0xeb, 0x39, 0x76, 0x5d, 0xb1, 0x42, 0x09, + 0xbd, 0x82, 0x12, 0xf1, 0x03, 0xda, 0xbf, 0xb8, 0xa9, 0xe7, 0x99, 0x62, 0xbd, 0xf9, 0xd0, 0x4c, + 0xca, 0xc9, 0xe4, 0x9e, 0x7a, 0x0c, 0x68, 0xf2, 0xcf, 0xfe, 0x8d, 0x55, 0x24, 0xe2, 0x97, 0xdb, + 0x1d, 0x39, 0x2e, 0xc5, 0x41, 0xbd, 0x20, 0xed, 0x4a, 0x09, 0x1d, 0x01, 0x08, 0xbb, 0x7e, 0x30, + 0x64, 0xba, 0x65, 0x61, 0x7a, 0x27, 0x83, 0xe9, 0x33, 0x8e, 0xb7, 0x2a, 0x44, 0x1d, 0x8d, 0x0f, + 0x50, 0x56, 0x00, 0xa3, 0x09, 0x45, 0xe9, 0x1e, 0xad, 0x40, 0xe9, 0xbc, 0x7b, 0xda, 0x3d, 0x7b, + 0xd7, 0xad, 0x2e, 0xa1, 0x32, 0x14, 0xba, 0xad, 0xd7, 0xed, 0xaa, 0x86, 0x36, 0x60, 0xad, 0xd3, + 0xea, 0xbd, 0xe9, 0x5b, 0xed, 0x4e, 0xbb, 0xd5, 0x6b, 0x1f, 0x56, 0x73, 0xc6, 0x3d, 0xa8, 0x44, + 0x76, 0x51, 0x09, 0xf2, 0xad, 0xde, 0x81, 0x7c, 0x72, 0xd8, 0x66, 0x27, 0xcd, 0xf8, 0xaa, 0xc1, + 0x66, 0x9c, 0x46, 0x32, 0xf1, 0x3d, 0x82, 0x39, 0x8f, 0x03, 0x7f, 0xea, 0x45, 0x3c, 0x0a, 0x01, + 0x21, 0x28, 0x78, 0xf8, 0x8b, 0x62, 0x51, 0x9c, 0x39, 0x92, 0xfa, 0xd4, 0x76, 0x05, 0x83, 0x0c, + 0x29, 0x04, 0xf4, 0x02, 0xca, 0x61, 0xd5, 0x08, 0xe3, 0x26, 0xbf, 0xb3, 0xd2, 0xdc, 0x92, 0xf9, + 0xab, 0xfa, 0x86, 0x1e, 0xad, 0x08, 0x66, 0xec, 0x41, 0xed, 0x08, 0xab, 0x48, 0x7a, 0xd4, 0xa6, + 0xd3, 0xa8, 0xaa, 0xdc, 0xaf, 0x7d, 0x85, 0x45, 0x30, 0xdc, 0x2f, 0x3b, 0x1b, 0x6f, 0xa1, 0xbe, + 0x08, 0x0f, 0xa3, 0x4f, 0xc0, 0xa3, 0x47, 0x50, 0xe0, 0xfd, 0x23, 0x62, 0x5f, 0x69, 0xa2, 0x78, + 0x34, 0x27, 0x4c, 0x63, 0x09, 0xbd, 0x61, 0xce, 0xda, 0x3d, 0xf0, 0x3d, 0x8a, 0x3d, 0xfa, 0xa7, + 0x38, 0x3a, 0xf0, 0x7f, 0x02, 0x3e, 0x0c, 0xa4, 0x01, 0xa5, 0xd0, 0x85, 0x78, 0x93, 0xca, 0x82, + 0x42, 0x19, 0xdf, 0x59, 0x41, 0xce, 0x27, 0x43, 0x9b, 0x62, 0xa5, 0x4a, 0x77, 0x8d, 0x1e, 0xb3, + 0x22, 0xf1, 0x79, 0x0a, 0x73, 0xda, 0x90, 0xb6, 0xe5, 0xd0, 0x1d, 0xf0, 0xaf, 0x25, 0xf5, 0x68, + 0x17, 0x8a, 0xd7, 0xb6, 0xcb, 0xec, 0x88, 0x22, 0x45, 0xd9, 0x87, 0x48, 0x31, 0x8c, 0x56, 0x88, + 0x40, 0x35, 0x28, 0x0d, 0x83, 0x9b, 0x7e, 0x30, 0xf5, 0x44, 0x53, 0x97, 0xad, 0x22, 0x13, 0xad, + 0xa9, 0x87, 0x1e, 0xc0, 0xda, 0xd0, 0x21, 0xf6, 0x85, 0x8b, 0xfb, 0x63, 0xdf, 0xff, 0x44, 0x44, + 0x5f, 0x97, 0xad, 0xd5, 0xf0, 0xf2, 0x98, 0xdf, 0x19, 0xc7, 0xb0, 0x35, 0x17, 0xfe, 0x6d, 0x99, + 0xf8, 0xa9, 0xc1, 0xd6, 0x89, 0x47, 0x58, 0x33, 0xb9, 0x73, 0x54, 0x44, 0x69, 0x6b, 0x99, 0xd3, + 0xce, 0xfd, 0x4b, 0xda, 0xf9, 0x58, 0xda, 0x8a, 0xf8, 0xc2, 0x0c, 0xf1, 0x59, 0xa8, 0x40, 0x77, + 0xa0, 0xc2, 0xc1, 0x64, 0x62, 0x0f, 0x70, 0xbd, 0x28, 0x5e, 0xff, 0xbe, 0x40, 0x77, 0x01, 0x02, + 0x3c, 0x25, 0xb8, 0x2f, 0x8c, 0x97, 0xc4, 0xfb, 0x8a, 0xb8, 0xe9, 0xf2, 0xae, 0x3a, 0x81, 0xed, + 0xf9, 0xe4, 0x6f, 0x4b, 0xe4, 0x18, 0x6a, 0xe7, 0x9e, 0x93, 0xc8, 0x64, 0x52, 0x53, 0x2d, 0xe4, + 0x96, 0x4b, 0xc8, 0x8d, 0x0d, 0xfd, 0x64, 0x1a, 0x5c, 0xe2, 0x90, 0x2b, 0x29, 0x18, 0xa7, 0x50, + 0x5f, 0xf4, 0x74, 0xcb, 0xb0, 0x9b, 0xdf, 0x96, 0x61, 0x5d, 0x4d, 0xb7, 0xdc, 0x99, 0xc8, 0x81, + 0xd5, 0xd9, 0x65, 0x85, 0x9e, 0xa4, 0xaf, 0xd4, 0xb9, 0xbf, 0x0b, 0xfa, 0x6e, 0x16, 0xa8, 0x0c, + 0xd5, 0x58, 0x7a, 0xae, 0x21, 0x02, 0xd5, 0xf9, 0xed, 0x82, 0xf6, 0x92, 0x6d, 0xa4, 0x2c, 0x2d, + 0xdd, 0xcc, 0x0a, 0x57, 0x6e, 0xd1, 0x35, 0x6c, 0x2c, 0xac, 0x12, 0xf4, 0x57, 0x33, 0xf1, 0x1d, + 0xa5, 0x37, 0x32, 0xe3, 0x23, 0xbf, 0x1f, 0x61, 0x2d, 0x36, 0xb4, 0x28, 0x85, 0xad, 0xa4, 0xc5, + 0xa4, 0x3f, 0xcd, 0x84, 0x8d, 0x7c, 0x5d, 0xc1, 0x7a, 0xbc, 0xb1, 0x51, 0x8a, 0x81, 0xc4, 0xd9, + 0xd7, 0x9f, 0x65, 0x03, 0x47, 0xee, 0x58, 0x1d, 0xe7, 0x5b, 0x32, 0xad, 0x8e, 0x29, 0x43, 0x92, + 0x56, 0xc7, 0xb4, 0x4e, 0x37, 0x96, 0xf6, 0xe1, 0x7d, 0x59, 0xa1, 0x2f, 0x8a, 0xe2, 0xff, 0x95, + 0x97, 0xbf, 0x02, 0x00, 0x00, 0xff, 0xff, 0x2c, 0x46, 0xba, 0xf9, 0x49, 0x09, 0x00, 0x00, } From 3a44b1e40c032ab988af24e7a6cd1acf71483b2a Mon Sep 17 00:00:00 2001 From: Till Backhaus Date: Thu, 18 Aug 2016 09:16:54 +0200 Subject: [PATCH 017/183] Add note on installation via cask to README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8861153f..a6399a9f4 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ quiet-kitten ## Install -Download a [release tarball of helm and tiller for your platform](https://github.com/kubernetes/helm/releases). Unpack the `helm` and `tiller` binaries and add them to your PATH and you are good to go! +Download a [release tarball of helm and tiller for your platform](https://github.com/kubernetes/helm/releases). Unpack the `helm` and `tiller` binaries and add them to your PATH and you are good to go! OS X/[Cask](https://caskroom.github.io/) users can `brew cask install helm`. ### Install from source From a124b4f56f252fee38077854a7c5586d07ea640c Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Thu, 18 Aug 2016 11:52:38 -0400 Subject: [PATCH 018/183] feat(helm): add rollback cmd --- cmd/helm/helm.go | 1 + cmd/helm/rollback.go | 76 +++++++++++++++++++++++++++++++++++++++ cmd/helm/rollback_test.go | 43 ++++++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 cmd/helm/rollback.go create mode 100644 cmd/helm/rollback_test.go diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 12156c038..ebe139fb6 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -93,6 +93,7 @@ func newRootCmd(out io.Writer) *cobra.Command { newListCmd(nil, out), newStatusCmd(nil, out), newUpgradeCmd(nil, out), + newRollbackCmd(nil, out), ) return cmd } diff --git a/cmd/helm/rollback.go b/cmd/helm/rollback.go new file mode 100644 index 000000000..bb6326f7a --- /dev/null +++ b/cmd/helm/rollback.go @@ -0,0 +1,76 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" +) + +const rollbackDesc = ` +This command rolls back a release to the previous version. + +The rollback argument is the name of a release. + +` + +type rollbackCmd struct { + name string + dryRun bool + disableHooks bool + out io.Writer + client helm.Interface +} + +func newRollbackCmd(c helm.Interface, out io.Writer) *cobra.Command { + rollback := &rollbackCmd{ + out: out, + client: c, + } + + cmd := &cobra.Command{ + Use: "rollback [RELEASE]", + Short: "roll back a release to the previous version", + Long: rollbackDesc, + PersistentPreRunE: setupConnection, + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkArgsLength(1, len(args), "release name"); err != nil { + return err + } + rollback.client = ensureHelmClient(rollback.client) + return rollback.run() + }, + } + + f := cmd.Flags() + f.BoolVar(&rollback.dryRun, "dry-run", false, "simulate an install") + f.BoolVar(&rollback.disableHooks, "no-hooks", false, "prevent hooks from running during rollback") + return cmd +} + +func (r *rollbackCmd) run() error { + + msg := "This command is under construction. Coming soon to a Helm near you!" + + fmt.Fprintf(r.out, msg) + + return nil +} diff --git a/cmd/helm/rollback_test.go b/cmd/helm/rollback_test.go new file mode 100644 index 000000000..0c641b66e --- /dev/null +++ b/cmd/helm/rollback_test.go @@ -0,0 +1,43 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "io" + "testing" + + "github.com/spf13/cobra" +) + +func TestRollbackCmd(t *testing.T) { + + tests := []releaseCase{ + { + name: "rollback a release", + args: []string{"funny-honey"}, + resp: nil, + expected: "This command is under construction. Coming soon to a Helm near you!", + }, + } + + cmd := func(c *fakeReleaseClient, out io.Writer) *cobra.Command { + return newRollbackCmd(c, out) + } + + runReleaseCases(t, tests, cmd) + +} From cf5e158f0d67f81d2b12b4eefc174ff11ce45cb0 Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Thu, 18 Aug 2016 13:02:47 -0400 Subject: [PATCH 019/183] chore(helm): fix golint errors --- cmd/helm/install_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index 7bc44702e..61bb8eb9a 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -124,18 +124,18 @@ sailor: sinbad // Combined case, overriding a property vals["sailor"] = "pisti" - updated_yaml := `good: true + updatedYAML := `good: true port: destination: basrah source: baghdad sailor: pisti ` - new_out, err := vobj.yaml() + newOut, err := vobj.yaml() if err != nil { t.Fatal(err) } - if string(new_out) != updated_yaml { - t.Errorf("Expected YAML to be \n%s\nGot\n%s\n", updated_yaml, new_out) + if string(newOut) != updatedYAML { + t.Errorf("Expected YAML to be \n%s\nGot\n%s\n", updatedYAML, newOut) } } From 01529001caf3f4aa7105d7537f1cf36f522cde8e Mon Sep 17 00:00:00 2001 From: Travis Cline Date: Thu, 18 Aug 2016 11:13:40 -0700 Subject: [PATCH 020/183] simplify example container names This encourages people from making container names depend on release names. --- docs/charts.md | 2 +- docs/examples/nginx/templates/deployment.yaml | 2 +- docs/examples/nginx/templates/post-install-job.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/charts.md b/docs/charts.md index 03eecba04..810d85872 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -483,7 +483,7 @@ spec: spec: restartPolicy: Never containers: - - name: {{template "fullname" .}}-job + - name: post-install-job image: "alpine:3.3" command: ["/bin/sleep","{{default "10" .Values.sleepyTime}}"] diff --git a/docs/examples/nginx/templates/deployment.yaml b/docs/examples/nginx/templates/deployment.yaml index 1443a5136..a65600e10 100644 --- a/docs/examples/nginx/templates/deployment.yaml +++ b/docs/examples/nginx/templates/deployment.yaml @@ -23,7 +23,7 @@ spec: release: {{.Release.Name | quote }} spec: containers: - - name: {{template "fullname" .}} + - name: nginx # Making image configurable is not necessary. Making imageTag configurable # is a nice option for the user. Especially in the strange cases like # nginx where the base distro is determined by the tag. Using :latest diff --git a/docs/examples/nginx/templates/post-install-job.yaml b/docs/examples/nginx/templates/post-install-job.yaml index 7a21b2408..a2281a8f5 100644 --- a/docs/examples/nginx/templates/post-install-job.yaml +++ b/docs/examples/nginx/templates/post-install-job.yaml @@ -25,7 +25,7 @@ spec: # more conventional syntax: {{.restartPolicy | default "Never"}} restartPolicy: Never containers: - - name: {{template "fullname" .}}-job + - name: post-install-job image: "alpine:3.3" # All we're going to do is sleep for a minute, then exit. command: ["/bin/sleep","{{default "10" .Values.sleepyTime}}"] From cb12d9642b96653c7099390c92b112bedd73410a Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Thu, 18 Aug 2016 11:31:49 -0700 Subject: [PATCH 021/183] Disable validateQuotes linter rule --- pkg/lint/rules/template.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index ace4acf8c..5638d30fe 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -92,7 +92,8 @@ func Templates(linter *support.Linter) { // Check that all the templates have a matching value linter.RunLinterRule(support.WarningSev, path, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate)) - linter.RunLinterRule(support.WarningSev, path, validateQuotes(string(preExecutedTemplate))) + // NOTE, disabled for now, Refs https://github.com/kubernetes/helm/issues/1037 + // linter.RunLinterRule(support.WarningSev, path, validateQuotes(string(preExecutedTemplate))) renderedContent := renderedContentMap[fileName] var yamlStruct K8sYamlStruct From 5bc96ef0b9e291a12cba18c94758ba393a1f5ada Mon Sep 17 00:00:00 2001 From: Adnan Abdulhussein Date: Thu, 18 Aug 2016 19:21:40 -0700 Subject: [PATCH 022/183] docs(notes.txt): move NOTES.txt to templates/NOTES.txt --- docs/charts.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/charts.md b/docs/charts.md index 2039a0598..c5e3c014d 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -22,14 +22,14 @@ Inside of this directory, Helm will expect a structure that matches this: ``` wordpress/ - Chart.yaml # A YAML file containing information about the chart - LICENSE # OPTIONAL: A plain text file containing the license for the chart - README.md # OPTIONAL: A human-readable README file - NOTES.txt # OPTIONAL: A plain text file containing short usage notes - values.yaml # The default configuration values for this chart - charts/ # OPTIONAL: A directory containing any charts upon which this chart depends. - templates/ # OPTIONAL: A directory of templates that, when combined with values, - # will generate valid Kubernetes manifest files. + Chart.yaml # A YAML file containing information about the chart + LICENSE # OPTIONAL: A plain text file containing the license for the chart + README.md # OPTIONAL: A human-readable README file + values.yaml # The default configuration values for this chart + charts/ # OPTIONAL: A directory containing any charts upon which this chart depends. + templates/ # OPTIONAL: A directory of templates that, when combined with values, + # will generate valid Kubernetes manifest files. + templates/NOTES.txt # OPTIONAL: A plain text file containing short usage notes ``` Helm will silently strip out any other files. @@ -100,8 +100,8 @@ contain: - Descriptions of options in `values.yaml` and default values - Any other information that may be relevant to the installation or configuration of the chart -The chart can also contain a short plain text NOTES.txt file that will be printed out after -installation, and when viewing the status of a release. This file is evaluated as a +The chart can also contain a short plain text `templates/NOTES.txt` file that will be printed out +after installation, and when viewing the status of a release. This file is evaluated as a [template](#templates-and-values), and can be used to display usage notes, next steps, or any other information relevant to a release of the chart. For example, instructions could be provided for connecting to a database, or accessing a web UI. Since this file is printed to STDOUT when running From cafec1202b78e1dc430f3e7934fa24ea21166c35 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Fri, 19 Aug 2016 11:44:53 -0600 Subject: [PATCH 023/183] feat(tiller): limit the max len of Release.Name This limits the number of characters in a release name to 14. This preserves 10 characters for customizing the `name:` field in charts. Relates to #1071 --- cmd/tiller/release_server.go | 16 ++++++++++++++++ cmd/tiller/release_server_test.go | 1 + 2 files changed, 17 insertions(+) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 2021d2eec..0fe6a192a 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -39,6 +39,13 @@ import ( var srv *releaseServer +// releaseNameMaxLen is the maximum length of a release name. +// +// This is designed to accomodate the usage of release name in the 'name:' +// field of Kubernetes resources. Many of those fields are limited to 24 +// characters in length. See https://github.com/kubernetes/helm/issues/1071 +const releaseNameMaxLen = 14 + func init() { srv = &releaseServer{ env: env, @@ -290,6 +297,11 @@ func (s *releaseServer) uniqName(start string, reuse bool) (string, error) { // is granted. If reuse is true and a deleted release with that name exists, // we re-grant it. Otherwise, an error is returned. if start != "" { + + if len(start) > releaseNameMaxLen { + return "", fmt.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen) + } + if rel, err := s.env.Releases.Get(start); err == driver.ErrReleaseNotFound { return start, nil } else if st := rel.Info.Status.Code; reuse && (st == release.Status_DELETED || st == release.Status_FAILED) { @@ -307,6 +319,10 @@ func (s *releaseServer) uniqName(start string, reuse bool) (string, error) { for i := 0; i < maxTries; i++ { namer := moniker.New() name := namer.NameSep("-") + if len(name) > releaseNameMaxLen { + log.Printf("info: Candidate name %q exceeds maximum length %d. Skipping.", name, releaseNameMaxLen) + continue + } if _, err := s.env.Releases.Get(name); err == driver.ErrReleaseNotFound { return name, nil } diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index f2ba5c69a..dba2fc9db 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -130,6 +130,7 @@ func TestUniqName(t *testing.T) { {"angry-panda", "", false, true}, {"happy-panda", "", false, true}, {"happy-panda", "happy-panda", true, false}, + {"hungry-hungry-hippos", "", true, true}, // Exceeds max name length } for _, tt := range tests { From 202238d56136bf180f01c999c8767dfd8bf30fdc Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Fri, 19 Aug 2016 12:23:49 -0600 Subject: [PATCH 024/183] fix(tiller): upgrade Sprig to 2.5.0 We need this specifically for trimSuffix. Closes #1071 --- glide.lock | 6 +++--- glide.yaml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/glide.lock b/glide.lock index e53220222..1bf1d6940 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 294741f7c5011d1558519802e282dcbb49b94578397611b3aa2506182a2585cc -updated: 2016-08-16T16:13:10.682420831-06:00 +hash: 410e784360a10f716d4bf4d22decf81f75b327d051b3f2d23f55aa9049c09676 +updated: 2016-08-19T12:19:48.074620307-06:00 imports: - name: github.com/aokoli/goutils version: 9c37978a95bd5c709a15883b6242714ea6709e64 @@ -193,7 +193,7 @@ imports: - name: github.com/Masterminds/semver version: 808ed7761c233af2de3f9729a041d68c62527f3a - name: github.com/Masterminds/sprig - version: bfc1db6cb1f1bab7d9fe2f317fca26a1e95fa54a + version: 89eb6984259918bffe1ddff9647b9ed06061e688 - name: github.com/mattn/go-runewidth version: d6bea18f789704b5f83375793155289da36a3c7f - name: github.com/matttproud/golang_protobuf_extensions diff --git a/glide.yaml b/glide.yaml index 90032e5bc..ed96b0204 100644 --- a/glide.yaml +++ b/glide.yaml @@ -8,7 +8,7 @@ import: - package: github.com/spf13/pflag version: 367864438f1b1a3c7db4da06a2f55b144e6784e0 - package: github.com/Masterminds/sprig - version: ^2.4 + version: ^2.5 - package: gopkg.in/yaml.v2 - package: github.com/Masterminds/semver version: 1.1.0 From 9f0914f7cb179493aead82f0a481344ef7658067 Mon Sep 17 00:00:00 2001 From: Trevor Hartman Date: Fri, 19 Aug 2016 13:35:39 -0600 Subject: [PATCH 025/183] Add --set flag to `helm upgrade` Fix #1070 --- cmd/helm/upgrade.go | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 07eeae03b..2ad2f7800 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "bytes" "fmt" "io" "io/ioutil" @@ -31,6 +32,9 @@ This command upgrades a release to a new version of a chart. The upgrade arguments must be a release and a chart. The chart argument can be a relative path to a packaged or unpackaged chart. + +To override values in a chart, use either the '--values' flag and pass in a file +or use the '--set' flag and pass configuration from the command line. ` type upgradeCmd struct { @@ -41,6 +45,7 @@ type upgradeCmd struct { dryRun bool disableHooks bool valuesFile string + values *values } func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { @@ -48,6 +53,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { upgrade := &upgradeCmd{ out: out, client: client, + values: new(values), } cmd := &cobra.Command{ @@ -71,23 +77,46 @@ 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.Var(upgrade.values, "set", "set values on the command line. Separate values with commas: key1=val1,key2=val2") f.BoolVar(&upgrade.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks") return cmd } +func (u *upgradeCmd) vals() ([]byte, error) { + var buffer bytes.Buffer + + // User specified a values file via -f/--values + if u.valuesFile != "" { + bytes, err := ioutil.ReadFile(u.valuesFile) + if err != nil { + return []byte{}, err + } + buffer.Write(bytes) + } + + // User specified value pairs via --set + // These override any values in the specified file + if len(u.values.pairs) > 0 { + bytes, err := u.values.yaml() + if err != nil { + return []byte{}, err + } + buffer.Write(bytes) + } + + return buffer.Bytes(), nil +} + func (u *upgradeCmd) run() error { chartPath, err := locateChartPath(u.chart) if err != nil { return err } - rawVals := []byte{} - if u.valuesFile != "" { - rawVals, err = ioutil.ReadFile(u.valuesFile) - if err != nil { - return err - } + rawVals, err := u.vals() + if err != nil { + return err } _, err = u.client.UpdateRelease(u.release, chartPath, helm.UpdateValueOverrides(rawVals), helm.UpgradeDryRun(u.dryRun), helm.UpgradeDisableHooks(u.disableHooks)) From fed862985e77a8e65890f5b776dfc86108f9bb68 Mon Sep 17 00:00:00 2001 From: Jess Frazelle Date: Mon, 22 Aug 2016 11:18:28 -0700 Subject: [PATCH 026/183] add docs table of contents and update quick start Signed-off-by: Jess Frazelle --- README.md | 19 +++++++------------ docs/quickstart.md | 33 +++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a6399a9f4..753304e08 100644 --- a/README.md +++ b/README.md @@ -28,20 +28,15 @@ Think of it like apt/yum/homebrew for Kubernetes. - Charts can be stored on disk, or fetched from remote chart repositories (like Debian or RedHat packages) -Using Helm is as easy as this: +## Docs -```console -$ helm init # Initialize Helm as well as the Tiller server +- [Quick Start](docs/quickstart.md) +- [Architechture](docs/architecture.md) +- [Charts](docs/charts.md) + - [Chart Repository Guide](docs/chart_repository.md) + - [Syncing your Chart Repository](docs/chart_repository_sync_example.md) +- [Developers](docs/developers.md) -# From the root of this repository run the following to install an example from -the docs -$ helm install docs/examples/alpine # Install the example Alpine chart -happy-panda # <-- That's the name of your release - -$ helm list # List all releases -happy-panda -quiet-kitten -``` ## Install diff --git a/docs/quickstart.md b/docs/quickstart.md index 8d2c505a8..8df5a629a 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -9,8 +9,8 @@ This guide covers how you can quickly get started using Helm. ## Install Helm -Download a binary release of the Helm client from the official project -page. +Download a binary release of the Helm client from +[the official project page](https://github.com/kubernetes/helm/releases). Alternately, you can clone the GitHub project and build your own client from source. The quickest route to installing from source is to @@ -25,19 +25,36 @@ install Tiller into your Kubernetes cluster in one step: $ helm init ``` -## Install an Existing Chart +## Install an Example Chart -To install an existing chart, you can run the `helm install` command: +To install a chart, you can run the `helm install` command. +Let's use an example chart from this repository. +Make sure you are in the root directory of this repo. -_TODO:_ Might need instructions about repos. ```console -$ helm install nginx-1.0.0 +$ helm install docs/examples/alpine Released smiling-penguin ``` -In the example above, the `nginx` chart was released, and the name of -our new release is `smiling-penguin` +In the example above, the `alpine` chart was released, and the name of +our new release is `smiling-penguin`. You can view the details of the chart we just +installed by taking a look at the nginx chart in +[docs/examples/alpine/Chart.yaml](docs/examples/alpine/Chart.yaml). + +## Change a Default Chart Value + +A nice feature of helm is the ability to change certain values of the package for the install. +Let's install the `nginx` example from this repository but change the `replicaCount` to 7. + +```console +$ helm install --set replicaCount=7 docs/examples/nginx +happy-panda +``` + +You can view the chart for this example in +[docs/examples/nginx/Chart.yaml](docs/examples/nginx/Chart.yaml) and the default values in +[docs/examples/nginx/values.yaml](docs/examples/nginx/values.yaml). ## Learn About The Release From a46a033de4a04f52ee57152786a3131765b57f42 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 22 Aug 2016 14:58:19 -0600 Subject: [PATCH 027/183] feat(chart): support 'image:URL' in charts. Closes #1034 --- _proto/hapi/chart/metadata.proto | 3 ++ docs/charts.md | 1 + pkg/chartutil/chartfile_test.go | 4 ++ pkg/chartutil/testdata/chartfiletest.yaml | 1 + pkg/chartutil/testdata/frobnitz-1.2.3.tgz | Bin 3775 -> 3777 bytes pkg/chartutil/testdata/frobnitz/Chart.yaml | 1 + .../frobnitz/charts/mariner-4.3.2.tgz | Bin 936 -> 941 bytes .../mariner/charts/albatross-0.1.0.tgz | Bin 347 -> 347 bytes pkg/proto/hapi/chart/metadata.pb.go | 39 ++++++++++-------- 9 files changed, 31 insertions(+), 18 deletions(-) diff --git a/_proto/hapi/chart/metadata.proto b/_proto/hapi/chart/metadata.proto index 884f458db..40c8b40dc 100644 --- a/_proto/hapi/chart/metadata.proto +++ b/_proto/hapi/chart/metadata.proto @@ -58,4 +58,7 @@ message Metadata { // The name of the template engine to use. Defaults to 'gotpl'. string engine = 8; + + // The URL to an icon file. + string icon = 9; } diff --git a/docs/charts.md b/docs/charts.md index 7fbe2b6ab..104c3ad18 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -51,6 +51,7 @@ maintainers: # (optional) - name: The maintainer's name (required for each maintainer) email: The maintainer's email (optional for each maintainer) engine: gotpl # The name of the template engine (optional, defaults to gotpl) +icon: A URL to an SVG or PNG image to be used as an icon (optional). ``` If you are familiar with the `Chart.yaml` file format for Helm Classic, you will diff --git a/pkg/chartutil/chartfile_test.go b/pkg/chartutil/chartfile_test.go index b43b8a5da..618cc2c73 100644 --- a/pkg/chartutil/chartfile_test.go +++ b/pkg/chartutil/chartfile_test.go @@ -75,6 +75,10 @@ func verifyChartfile(t *testing.T, f *chart.Metadata) { t.Error("Unexpected home.") } + if f.Icon != "https://example.com/64x64.png" { + t.Errorf("Unexpected icon: %q", f.Icon) + } + if len(f.Keywords) != 3 { t.Error("Unexpected keywords") } diff --git a/pkg/chartutil/testdata/chartfiletest.yaml b/pkg/chartutil/testdata/chartfiletest.yaml index 9f255b9bd..b665c61be 100644 --- a/pkg/chartutil/testdata/chartfiletest.yaml +++ b/pkg/chartutil/testdata/chartfiletest.yaml @@ -13,3 +13,4 @@ maintainers: sources: - https://example.com/foo/bar home: http://example.com +icon: https://example.com/64x64.png diff --git a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz index 50d1ef01484094a445c867e612e363f4f527d604..4e68564fc1202e94581eb43ef9dfb354d12f59c7 100644 GIT binary patch literal 3777 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 literal 3775 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 P0we$c>4Z%>04M+esP@qU literal 936 zcmV;Z16TYXiwFR;38hy61MQZ3XcI{s$Abq|bQ#8S10ibbrZ?k1_k zXBC@>IG+S&LS|=X=llEq=BGeS@*&M;i6b;k+XVrHTQSnoEFWp30EV@*0?!E?ZwEBP zvkY$mLOeIsMnK0~>z@4gE!i=PZEk_^o`%Pp-cYD#!t%QDDtSx=WM%SW$)+YnztOmmSC#XP0Koo^$Oi5CpXQi%^FPZNAL9Q+5bN+* z<%!jP4&JN(q3ncMQ*}LqrWlH*uxIYKuhE|W1&%ZQFEA`^)cvOajqxJ!eo;buAkj#pY4J zRW3cd=Rx(Xie(4tnqJ??UpgV@*otkA|NkzXJn!(F?cLI+9N3(9sASmk%(Uv&=Qhn; z*Hua{TlVjRyE|&{IQLyp_Ad0SdFEKsypqlim0n#uyjUy?Hrvm&P8z#sZPw|cs$$Bz zt)$n`;=#DSYWv7lIiaFZ=K0IzJ%V7`mBPNYExVov%SYDy{;*%|quEK*TJ~4&u3xgY ztoCG2ap#dg`3Akd;MPiRHOkrxUX(w1l`^?26LkCz=E*6)+yx5Z4xMotT3Xf*>ew^&cJiepsS;Z`L z_@72+Q}(f6Z(gaHp$yxS8GD@j)#gsN&QsGZL%x@w1OkCTAP@)y0)apv5C{Z93~vC_ KC$O{tC;$M@pyPM| diff --git a/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz b/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz index c35e1b9701734924198f1739129a9563f75ba7cd..d92a38e676f1527815c247a0b06aeb59c0806a62 100644 GIT binary patch delta 16 Xcmcc3beoA?zMF%gEp7Kk_7Fw@E{Fvv delta 16 Xcmcc3beoA?zMF$#3+K{}>>-Q*EiMG( diff --git a/pkg/proto/hapi/chart/metadata.pb.go b/pkg/proto/hapi/chart/metadata.pb.go index b753f8608..d6270bda9 100644 --- a/pkg/proto/hapi/chart/metadata.pb.go +++ b/pkg/proto/hapi/chart/metadata.pb.go @@ -67,6 +67,8 @@ type Metadata struct { Maintainers []*Maintainer `protobuf:"bytes,7,rep,name=maintainers" json:"maintainers,omitempty"` // The name of the template engine to use. Defaults to 'gotpl'. Engine string `protobuf:"bytes,8,opt,name=engine" json:"engine,omitempty"` + // The URL to an icon file. + Icon string `protobuf:"bytes,9,opt,name=icon" json:"icon,omitempty"` } func (m *Metadata) Reset() { *m = Metadata{} } @@ -88,22 +90,23 @@ func init() { } var fileDescriptor2 = []byte{ - // 266 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x90, 0x4b, 0x4b, 0xc4, 0x40, - 0x10, 0x84, 0xdd, 0x47, 0x1e, 0xdb, 0xb9, 0x2c, 0x8d, 0x2c, 0xa3, 0xa7, 0x90, 0x93, 0xa7, 0x2c, - 0x28, 0x88, 0x67, 0x41, 0x3c, 0xe8, 0x66, 0x65, 0x51, 0x04, 0x6f, 0x63, 0xd2, 0x98, 0x41, 0x93, - 0x09, 0x33, 0xa3, 0xe2, 0x3f, 0xf1, 0xe7, 0x3a, 0xe9, 0x7d, 0x1e, 0x3c, 0x04, 0xaa, 0xea, 0x4b, - 0xd7, 0xd0, 0x0d, 0x27, 0xb5, 0xec, 0xd4, 0xbc, 0xac, 0xa5, 0x71, 0xf3, 0x86, 0x9c, 0xac, 0xa4, - 0x93, 0x79, 0x67, 0xb4, 0xd3, 0x08, 0x3d, 0xca, 0x19, 0x65, 0x97, 0x00, 0x0b, 0xa9, 0x5a, 0xe7, - 0x3f, 0x32, 0x88, 0x30, 0x6e, 0x65, 0x43, 0x62, 0x90, 0x0e, 0xce, 0x26, 0x2b, 0xd6, 0x78, 0x0c, - 0x01, 0x35, 0x52, 0x7d, 0x88, 0x21, 0x87, 0x6b, 0x93, 0xfd, 0x0e, 0x21, 0x5e, 0x6c, 0x6a, 0xff, - 0x1d, 0xf3, 0x59, 0xad, 0x7d, 0xb6, 0x9e, 0x62, 0x8d, 0x02, 0x22, 0xab, 0x3f, 0x4d, 0x49, 0x56, - 0x8c, 0xd2, 0x91, 0x8f, 0xb7, 0xb6, 0x27, 0x5f, 0x64, 0xac, 0xd2, 0xad, 0x18, 0xf3, 0xc0, 0xd6, - 0x62, 0x0a, 0x49, 0x45, 0xb6, 0x34, 0xaa, 0x73, 0x3d, 0x0d, 0x98, 0x1e, 0x46, 0x78, 0x0a, 0xf1, - 0x3b, 0xfd, 0x7c, 0x6b, 0x53, 0x59, 0x11, 0x72, 0xed, 0xce, 0xe3, 0x15, 0x24, 0xcd, 0x6e, 0x3d, - 0x2b, 0x22, 0x8f, 0x93, 0xf3, 0x59, 0xbe, 0x3f, 0x40, 0xbe, 0xdf, 0x7e, 0x75, 0xf8, 0x2b, 0xce, - 0x20, 0xa4, 0xf6, 0xcd, 0x6b, 0x11, 0xf3, 0x93, 0x1b, 0x97, 0xa5, 0x10, 0xde, 0xb0, 0xc2, 0x04, - 0xa2, 0xa7, 0xe2, 0xae, 0x58, 0x3e, 0x17, 0xd3, 0x23, 0x9c, 0x40, 0x70, 0xbb, 0x7c, 0x7c, 0xb8, - 0x9f, 0x0e, 0xae, 0xa3, 0x97, 0x80, 0xab, 0x5f, 0x43, 0x3e, 0xf7, 0xc5, 0x5f, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xe5, 0xf8, 0x57, 0xee, 0x8b, 0x01, 0x00, 0x00, + // 275 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x91, 0x4b, 0x4b, 0xc4, 0x30, + 0x14, 0x85, 0x9d, 0x47, 0x5f, 0xb7, 0x9b, 0xe1, 0x22, 0x43, 0x74, 0x55, 0xba, 0x72, 0xd5, 0x01, + 0x05, 0x71, 0x2d, 0x88, 0x0b, 0x9d, 0x8e, 0x0c, 0x8a, 0xe0, 0x2e, 0xb6, 0xc1, 0x06, 0x6d, 0x53, + 0x92, 0xa8, 0xf8, 0x9f, 0xfc, 0x91, 0x26, 0xb7, 0xf3, 0x5a, 0xb8, 0x28, 0x9c, 0x73, 0xbe, 0xde, + 0xdc, 0x9e, 0x06, 0x4e, 0x1a, 0xde, 0xcb, 0x45, 0xd5, 0x70, 0x6d, 0x17, 0xad, 0xb0, 0xbc, 0xe6, + 0x96, 0x17, 0xbd, 0x56, 0x56, 0x21, 0x78, 0x54, 0x10, 0xca, 0x2f, 0x01, 0x96, 0x5c, 0x76, 0xd6, + 0x3d, 0x42, 0x23, 0xc2, 0xb4, 0xe3, 0xad, 0x60, 0xa3, 0x6c, 0x74, 0x96, 0xac, 0x49, 0xe3, 0x31, + 0x04, 0xa2, 0xe5, 0xf2, 0x83, 0x8d, 0x29, 0x1c, 0x4c, 0xfe, 0x3b, 0x86, 0x78, 0xb9, 0x39, 0xf6, + 0xdf, 0x31, 0x97, 0x35, 0xca, 0x65, 0xc3, 0x14, 0x69, 0x64, 0x10, 0x19, 0xf5, 0xa9, 0x2b, 0x61, + 0xd8, 0x24, 0x9b, 0xb8, 0x78, 0x6b, 0x3d, 0xf9, 0x12, 0xda, 0x48, 0xd5, 0xb1, 0x29, 0x0d, 0x6c, + 0x2d, 0x66, 0x90, 0xd6, 0xc2, 0x54, 0x5a, 0xf6, 0xd6, 0xd3, 0x80, 0xe8, 0x61, 0x84, 0xa7, 0x10, + 0xbf, 0x8b, 0x9f, 0x6f, 0xa5, 0x6b, 0xc3, 0x42, 0x3a, 0x76, 0xe7, 0xf1, 0x0a, 0xd2, 0x76, 0x57, + 0xcf, 0xb0, 0xc8, 0xe1, 0xf4, 0x7c, 0x5e, 0xec, 0x7f, 0x40, 0xb1, 0x6f, 0xbf, 0x3e, 0x7c, 0x15, + 0xe7, 0x10, 0x8a, 0xee, 0xcd, 0x69, 0x16, 0xd3, 0xca, 0x8d, 0xf3, 0xbd, 0x64, 0xe5, 0x3e, 0x24, + 0x19, 0x7a, 0x79, 0x9d, 0x67, 0x10, 0xde, 0x0c, 0x34, 0x85, 0xe8, 0xa9, 0xbc, 0x2b, 0x57, 0xcf, + 0xe5, 0xec, 0x08, 0x13, 0x08, 0x6e, 0x57, 0x8f, 0x0f, 0xf7, 0xb3, 0xd1, 0x75, 0xf4, 0x12, 0xd0, + 0xba, 0xd7, 0x90, 0xae, 0xe0, 0xe2, 0x2f, 0x00, 0x00, 0xff, 0xff, 0x72, 0xdf, 0x74, 0xb5, 0x9f, + 0x01, 0x00, 0x00, } From 84761a559a062c955b2fbfd9a92a3295d04931e1 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 23 Aug 2016 11:01:18 -0600 Subject: [PATCH 028/183] fix(helm): remove extra linter output Closes #1076 --- pkg/engine/engine.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 73b8dabad..53578a1b8 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -19,7 +19,6 @@ package engine import ( "bytes" "fmt" - "log" "path" "strings" "text/template" @@ -132,7 +131,6 @@ func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) { files := []string{} for fname, r := range tpls { - log.Printf("Preparing template %s", fname) 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) From ce83a8a7776809d2257cc966f62d2cc857cf672b Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 25 Jul 2016 17:34:05 -0600 Subject: [PATCH 029/183] feat(pkg/provenance): add OpenPGP signatures This adds support for OpenPGP signatures containing provenance data. Such information can be used to verify the integrity of a Chart by testing that its file hash, metadata, and images are correct. This first PR does not contain all of the tooling necessary for end-to-end chart integrity. It contains just the library. See #983 --- glide.lock | 15 +- glide.yaml | 3 + pkg/provenance/doc.go | 37 +++ pkg/provenance/sign.go | 280 ++++++++++++++++++ pkg/provenance/sign_test.go | 249 ++++++++++++++++ pkg/provenance/testdata/hashtest-1.2.3.tgz | Bin 0 -> 465 bytes pkg/provenance/testdata/hashtest.sha256 | 1 + pkg/provenance/testdata/hashtest/.helmignore | 5 + pkg/provenance/testdata/hashtest/Chart.yaml | 3 + pkg/provenance/testdata/hashtest/values.yaml | 4 + pkg/provenance/testdata/helm-test-key.pub | Bin 0 -> 1243 bytes pkg/provenance/testdata/helm-test-key.secret | Bin 0 -> 2545 bytes pkg/provenance/testdata/msgblock.yaml | 7 + pkg/provenance/testdata/msgblock.yaml.asc | 21 ++ .../testdata/msgblock.yaml.tampered | 21 ++ pkg/provenance/testdata/regen-hashtest.sh | 3 + 16 files changed, 647 insertions(+), 2 deletions(-) create mode 100644 pkg/provenance/doc.go create mode 100644 pkg/provenance/sign.go create mode 100644 pkg/provenance/sign_test.go create mode 100644 pkg/provenance/testdata/hashtest-1.2.3.tgz create mode 100644 pkg/provenance/testdata/hashtest.sha256 create mode 100644 pkg/provenance/testdata/hashtest/.helmignore create mode 100755 pkg/provenance/testdata/hashtest/Chart.yaml create mode 100644 pkg/provenance/testdata/hashtest/values.yaml create mode 100644 pkg/provenance/testdata/helm-test-key.pub create mode 100644 pkg/provenance/testdata/helm-test-key.secret create mode 100644 pkg/provenance/testdata/msgblock.yaml create mode 100644 pkg/provenance/testdata/msgblock.yaml.asc create mode 100644 pkg/provenance/testdata/msgblock.yaml.tampered create mode 100755 pkg/provenance/testdata/regen-hashtest.sh diff --git a/glide.lock b/glide.lock index 1bf1d6940..808be278c 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 410e784360a10f716d4bf4d22decf81f75b327d051b3f2d23f55aa9049c09676 -updated: 2016-08-19T12:19:48.074620307-06:00 +hash: 05c56f2ae4c8bcbaf2c428e2e070ec00f865b284ea61dd671e2c4e117f2d6528 +updated: 2016-08-19T17:30:32.462379907-06:00 imports: - name: github.com/aokoli/goutils version: 9c37978a95bd5c709a15883b6242714ea6709e64 @@ -247,6 +247,17 @@ imports: subpackages: - codec - codec/codecgen +- name: golang.org/x/crypto + version: c84e1f8e3a7e322d497cd16c0e8a13c7e127baf3 + subpackages: + - cast5 + - openpgp + - openpgp/armor + - openpgp/clearsign + - openpgp/elgamal + - openpgp/errors + - openpgp/packet + - openpgp/s2k - name: golang.org/x/net version: fb93926129b8ec0056f2f458b1f519654814edf0 subpackages: diff --git a/glide.yaml b/glide.yaml index ed96b0204..2f7567469 100644 --- a/glide.yaml +++ b/glide.yaml @@ -50,3 +50,6 @@ import: - package: google.golang.org/cloud vcs: git repo: https://code.googlesource.com/gocloud +- package: golang.org/x/crypto + subpackages: + - openpgp diff --git a/pkg/provenance/doc.go b/pkg/provenance/doc.go new file mode 100644 index 000000000..dacfa9e69 --- /dev/null +++ b/pkg/provenance/doc.go @@ -0,0 +1,37 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/*Package provenance provides tools for establishing the authenticity of a chart. + +In Helm, provenance is established via several factors. The primary factor is the +cryptographic signature of a chart. Chart authors may sign charts, which in turn +provide the necessary metadata to ensure the integrity of the chart file, the +Chart.yaml, and the referenced Docker images. + +A provenance file is clear-signed. This provides cryptographic verification that +a particular block of information (Chart.yaml, archive file, images) have not +been tampered with or altered. To learn more, read the GnuPG documentation on +clear signatures: +https://www.gnupg.org/gph/en/manual/x135.html + +The cryptography used by Helm should be compatible with OpenGPG. For example, +you should be able to verify a signature by importing the desired public key +and using `gpg --verify`, `keybase pgp verify`, or similar: + + $ gpg --verify some.sig + gpg: Signature made Mon Jul 25 17:23:44 2016 MDT using RSA key ID 1FC18762 + gpg: Good signature from "Helm Testing (This key should only be used for testing. DO NOT TRUST.) " [ultimate] +*/ +package provenance // import "k8s.io/helm/pkg/provenance" diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go new file mode 100644 index 000000000..3dd8cfb74 --- /dev/null +++ b/pkg/provenance/sign.go @@ -0,0 +1,280 @@ +/* +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 provenance + +import ( + "bytes" + "crypto" + "encoding/hex" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + + "github.com/ghodss/yaml" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/clearsign" + "golang.org/x/crypto/openpgp/packet" + + "k8s.io/helm/pkg/chartutil" + hapi "k8s.io/helm/pkg/proto/hapi/chart" +) + +var defaultPGPConfig = packet.Config{ + DefaultHash: crypto.SHA512, +} + +// SumCollection represents a collecton of file and image checksums. +// +// Files are of the form: +// FILENAME: "sha256:SUM" +// Images are of the form: +// "IMAGE:TAG": "sha256:SUM" +// Docker optionally supports sha512, and if this is the case, the hash marker +// will be 'sha512' instead of 'sha256'. +type SumCollection struct { + Files map[string]string `json:"files"` + Images map[string]string `json:"images,omitempty"` +} + +// Signatory signs things. +// +// Signatories can be constructed from a PGP private key file using NewFromFiles +// or they can be constructed manually by setting the Entity to a valid +// PGP entity. +// +// The same Signatory can be used to sign or validate multiple charts. +type Signatory struct { + // The signatory for this instance of Helm. This is used for signing. + Entity *openpgp.Entity + // The keyring for this instance of Helm. This is used for verification. + KeyRing openpgp.EntityList +} + +// NewFromFiles constructs a new Signatory from the PGP key in the given filename. +// +// This will emit an error if it cannot find a valid GPG keyfile (entity) at the +// given location. +// +// Note that the keyfile may have just a public key, just a private key, or +// both. The Signatory methods may have different requirements of the keys. For +// example, ClearSign must have a valid `openpgp.Entity.PrivateKey` before it +// can sign something. +func NewFromFiles(keyfile, keyringfile string) (*Signatory, error) { + e, err := loadKey(keyfile) + if err != nil { + return nil, err + } + + ring, err := loadKeyRing(keyringfile) + if err != nil { + return nil, err + } + + return &Signatory{ + Entity: e, + KeyRing: ring, + }, nil +} + +// Sign signs a chart with the given key. +// +// This takes the path to a chart archive file and a key, and it returns a clear signature. +// +// The Signatory must have a valid Entity.PrivateKey for this to work. If it does +// not, an error will be returned. +func (s *Signatory) ClearSign(chartpath string) (string, error) { + if s.Entity.PrivateKey == nil { + return "", errors.New("private key not found") + } + + if fi, err := os.Stat(chartpath); err != nil { + return "", err + } else if fi.IsDir() { + return "", errors.New("cannot sign a directory") + } + + out := bytes.NewBuffer(nil) + + b, err := messageBlock(chartpath) + if err != nil { + return "", nil + } + + // Sign the buffer + w, err := clearsign.Encode(out, s.Entity.PrivateKey, &defaultPGPConfig) + if err != nil { + return "", err + } + _, err = io.Copy(w, b) + w.Close() + return out.String(), err +} + +func (s *Signatory) Verify(chartpath, sigpath string) (bool, error) { + for _, fname := range []string{chartpath, sigpath} { + if fi, err := os.Stat(fname); err != nil { + return false, err + } else if fi.IsDir() { + return false, fmt.Errorf("%s cannot be a directory", fname) + } + } + + // First verify the signature + sig, err := s.decodeSignature(sigpath) + if err != nil { + return false, fmt.Errorf("failed to decode signature: %s", err) + } + + by, err := s.verifySignature(sig) + if err != nil { + return false, err + } + for n := range by.Identities { + log.Printf("info: %s signed by %q", sigpath, n) + } + + // Second, verify the hash of the tarball. + + return true, nil +} + +func (s *Signatory) decodeSignature(filename string) (*clearsign.Block, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + block, _ := clearsign.Decode(data) + if block == nil { + // There was no sig in the file. + return nil, errors.New("signature block not found") + } + + return block, nil +} + +// verifySignature verifies that the given block is validly signed, and returns the signer. +func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) { + return openpgp.CheckDetachedSignature( + s.KeyRing, + bytes.NewBuffer(block.Bytes), + block.ArmoredSignature.Body, + ) +} + +func messageBlock(chartpath string) (*bytes.Buffer, error) { + var b *bytes.Buffer + // Checksum the archive + chash, err := sumArchive(chartpath) + if err != nil { + return b, err + } + + base := filepath.Base(chartpath) + sums := &SumCollection{ + Files: map[string]string{ + base: "sha256:" + chash, + }, + } + + // Load the archive into memory. + chart, err := chartutil.LoadFile(chartpath) + if err != nil { + return b, err + } + + // Buffer a hash + checksums YAML file + data, err := yaml.Marshal(chart.Metadata) + if err != nil { + return b, err + } + + // FIXME: YAML uses ---\n as a file start indicator, but this is not legal in a PGP + // clearsign block. So we use ...\n, which is the YAML document end marker. + // http://yaml.org/spec/1.2/spec.html#id2800168 + b = bytes.NewBuffer(data) + b.WriteString("\n...\n") + + data, err = yaml.Marshal(sums) + if err != nil { + return b, err + } + b.Write(data) + + return b, nil +} + +// parseMessageBlock +func parseMessageBlock(data []byte) (*hapi.Metadata, *SumCollection, error) { + // This sucks. + parts := bytes.Split(data, []byte("\n...\n")) + if len(parts) < 2 { + return nil, nil, errors.New("message block must have at least two parts") + } + + md := &hapi.Metadata{} + sc := &SumCollection{} + + if err := yaml.Unmarshal(parts[0], md); err != nil { + return md, sc, err + } + err := yaml.Unmarshal(parts[1], sc) + return md, sc, err +} + +// loadKey loads a GPG key found at a particular path. +func loadKey(keypath string) (*openpgp.Entity, error) { + f, err := os.Open(keypath) + if err != nil { + return nil, err + } + defer f.Close() + + pr := packet.NewReader(f) + return openpgp.ReadEntity(pr) +} + +func loadKeyRing(ringpath string) (openpgp.EntityList, error) { + f, err := os.Open(ringpath) + if err != nil { + return nil, err + } + defer f.Close() + return openpgp.ReadKeyRing(f) +} + +// sumArchive calculates a SHA256 hash (like Docker) for a given file. +// +// It takes the path to the archive file, and returns a string representation of +// the SHA256 sum. +// +// The intended use of this function is to generate a sum of a chart TGZ file. +func sumArchive(filename string) (string, error) { + f, err := os.Open(filename) + if err != nil { + return "", err + } + defer f.Close() + + hash := crypto.SHA256.New() + io.Copy(hash, f) + return hex.EncodeToString(hash.Sum(nil)), nil +} diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go new file mode 100644 index 000000000..5dfd6bd68 --- /dev/null +++ b/pkg/provenance/sign_test.go @@ -0,0 +1,249 @@ +/* +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 provenance + +import ( + "io/ioutil" + "os" + "strings" + "testing" + + pgperrors "golang.org/x/crypto/openpgp/errors" +) + +const ( + // testKeyFile is the secret key. + // Generating keys should be done with `gpg --gen-key`. The current key + // was generated to match Go's defaults (RSA/RSA 2048). It has no pass + // phrase. Use `gpg --export-secret-keys helm-test` to export the secret. + testKeyfile = "testdata/helm-test-key.secret" + + // testPubfile is the public key file. + // Use `gpg --export helm-test` to export the public key. + testPubfile = "testdata/helm-test-key.pub" + + // Generated name for the PGP key in testKeyFile. + testKeyName = `Helm Testing (This key should only be used for testing. DO NOT TRUST.) ` + + testChartfile = "testdata/hashtest-1.2.3.tgz" + + // testSigBlock points to a signature generated by an external tool. + // This file was generated with GnuPG: + // gpg --clearsign -u helm-test --openpgp testdata/msgblock.yaml + testSigBlock = "testdata/msgblock.yaml.asc" + + // testTamperedSigBlock is a tampered copy of msgblock.yaml.asc + testTamperedSigBlock = "testdata/msgblock.yaml.tampered" + + // testSumfile points to a SHA256 sum generated by an external tool. + // We always want to validate against an external tool's representation to + // verify that we haven't done something stupid. This file was generated + // with shasum. + // shasum -a 256 hashtest-1.2.3.tgz > testdata/hashtest.sha256 + testSumfile = "testdata/hashtest.sha256" +) + +// testMessageBlock represents the expected message block for the testdata/hashtest chart. +const testMessageBlock = `description: Test chart versioning +name: hashtest +version: 1.2.3 + +... +files: + hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75 +` + +func TestMessageBlock(t *testing.T) { + out, err := messageBlock(testChartfile) + if err != nil { + t.Fatal(err) + } + got := out.String() + + if got != testMessageBlock { + t.Errorf("Expected:\n%q\nGot\n%q\n", testMessageBlock, got) + } +} + +func TestParseMessageBlock(t *testing.T) { + md, sc, err := parseMessageBlock([]byte(testMessageBlock)) + if err != nil { + t.Fatal(err) + } + + if md.Name != "hashtest" { + t.Errorf("Expected name %q, got %q", "hashtest", md.Name) + } + + if lsc := len(sc.Files); lsc != 1 { + t.Errorf("Expected 1 file, got %d", lsc) + } + + if hash, ok := sc.Files["hashtest-1.2.3.tgz"]; !ok { + t.Errorf("hashtest file not found in Files") + } else if hash != "sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75" { + t.Errorf("Unexpected hash: %q", hash) + } +} + +func TestLoadKey(t *testing.T) { + k, err := loadKey(testKeyfile) + if err != nil { + t.Fatal(err) + } + + if _, ok := k.Identities[testKeyName]; !ok { + t.Errorf("Expected to load a key for user %q", testKeyName) + } +} + +func TestLoadKeyRing(t *testing.T) { + k, err := loadKeyRing(testPubfile) + if err != nil { + t.Fatal(err) + } + + if len(k) > 1 { + t.Errorf("Expected 1, got %d", len(k)) + } + + for _, e := range k { + if ii, ok := e.Identities[testKeyName]; !ok { + t.Errorf("Expected %s in %v", testKeyName, ii) + } + } +} + +func TestNewFromFiles(t *testing.T) { + s, err := NewFromFiles(testKeyfile, testPubfile) + if err != nil { + t.Fatal(err) + } + + if _, ok := s.Entity.Identities[testKeyName]; !ok { + t.Errorf("Expected to load a key for user %q", testKeyName) + } +} + +func TestSumArchive(t *testing.T) { + hash, err := sumArchive(testChartfile) + if err != nil { + t.Fatal(err) + } + + sig, err := readSumFile(testSumfile) + if err != nil { + t.Fatal(err) + } + + if !strings.Contains(sig, hash) { + t.Errorf("Expected %s to be in %s", hash, sig) + } +} + +func TestClearSign(t *testing.T) { + signer, err := NewFromFiles(testKeyfile, testPubfile) + if err != nil { + t.Fatal(err) + } + + sig, err := signer.ClearSign(testChartfile) + if err != nil { + t.Fatal(err) + } + t.Logf("Sig:\n%s", sig) + + if !strings.Contains(sig, testMessageBlock) { + t.Errorf("expected message block to be in sig: %s", sig) + } +} + +func TestDecodeSignature(t *testing.T) { + // Unlike other tests, this does a round-trip test, ensuring that a signature + // generated by the library can also be verified by the library. + + signer, err := NewFromFiles(testKeyfile, testPubfile) + if err != nil { + t.Fatal(err) + } + + sig, err := signer.ClearSign(testChartfile) + if err != nil { + t.Fatal(err) + } + + f, err := ioutil.TempFile("", "helm-test-sig-") + if err != nil { + t.Fatal(err) + } + + tname := f.Name() + defer func() { + os.Remove(tname) + }() + f.WriteString(sig) + f.Close() + + sig2, err := signer.decodeSignature(tname) + if err != nil { + t.Fatal(err) + } + + by, err := signer.verifySignature(sig2) + if err != nil { + t.Fatal(err) + } + + if _, ok := by.Identities[testKeyName]; !ok { + t.Errorf("Expected identity %q", testKeyName) + } +} + +func TestVerify(t *testing.T) { + signer, err := NewFromFiles(testKeyfile, testPubfile) + if err != nil { + t.Fatal(err) + } + + passed, err := signer.Verify(testChartfile, testSigBlock) + if !passed { + t.Errorf("Failed to pass verify. Err: %s", err) + } + + passed, err = signer.Verify(testChartfile, testTamperedSigBlock) + if passed { + t.Errorf("Expected %s to fail.", testTamperedSigBlock) + } + + switch err.(type) { + case pgperrors.SignatureError: + t.Logf("Tampered sig block error: %s (%T)", err, err) + default: + t.Errorf("Expected invalid signature error, got %q (%T)", err, err) + } +} + +// readSumFile reads a file containing a sum generated by the UNIX shasum tool. +func readSumFile(sumfile string) (string, error) { + data, err := ioutil.ReadFile(sumfile) + if err != nil { + return "", err + } + + sig := string(data) + parts := strings.SplitN(sig, " ", 2) + return parts[0], nil +} diff --git a/pkg/provenance/testdata/hashtest-1.2.3.tgz b/pkg/provenance/testdata/hashtest-1.2.3.tgz new file mode 100644 index 0000000000000000000000000000000000000000..1e89b524f4fc89e4a483ef8dc4fefdc79d64786f GIT binary patch literal 465 zcmV;?0WSU@iwG0|32ul0|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PL2*>(ek4$A9Pk6;F0i(Ac#6HrP$vQBl|~o+N8un_!xhB;DM9 z?@Oyirm(~2hL8_~X6Z|tviJF}Qg|8Ahqv#gaDkmfr=M<3POP4v$0Kom%z4h|@i@VvXo4LfQCsA40)0iCBgW!lV$4%hIjQL>+B z*1%c8%Iwh(khqH3|AWv2`hOAtm;5hydFFq~%Od%I4;HY&Mhu#a9~%G~>t@$kwt$^f z9_Sjxd=gI$juz=4|XJJ0SyFKmTjH^2mr{k15wFPQdpTAAEclY4oV^-7nTy5x$&xF;PC4il}o-Pk4@#$knlp#(|J0GE?qli_lr;7-o zyY8vBsN_GJe;#w<`JdR7riNL&RJlcS)FG+W=91;dYS6NZ2tY?kZ8Sw9{r=e4|L3E{ zRod|EPC!PWgW&pe&4qiqKQAijj;G~fyjcC^m%0p54Tn{h%YFKd0VC4n=#~SRc@BVd znj66*)Om%*SEQfX{1*Tb0RRECT}WkYZ6H)-b98BLXCNq4XlZjGYh`&Lb7*gMY-AvB zZftoVVr3w8b7f>8W^ZyJbY*jNX>MmOAVg0fPES-IR8mz_R4yqXJZNQXZ7p6uVakV zT7GGV$jaKjyjfI_a~N1!Hk?5C$0wa&4)R=i$v7t&ZMycW#RkavpF%A?>MTT2anNDzOQUm<++zEOykJ9-@&c2QXq3owqf7fek`=L@+7iF zv;IW2Q>Q&r+V@cWDF&hAUUsCKlDinerKgvJUJCl$5gjb7NhM{mBP%!M^mX-iS8xFf zuLB{@MDqvtZzF#Bxd9CXSC(y_0SExW>8~h=U8|!do4*OJj2u#!KDe3v+1T+aVzU5di=Ji2)x37y$|Z2?YXImTjH_8w>yn2@r%kznCAv zhhp0`2mpxVj5j%o&5i)?`r7iES|8dA@p2kk@+XS(tjBGN)6>tm^=gayCn`gTEC*K74Y~{I_PREk) z)PstIMx1RxB@cK8%Mey%;nVnKriAKUk2Ky?dBMG3uXItKL$3N(#3P^pQa*K$l)wUy F^>pMLK0g2e literal 0 HcmV?d00001 diff --git a/pkg/provenance/testdata/helm-test-key.secret b/pkg/provenance/testdata/helm-test-key.secret new file mode 100644 index 0000000000000000000000000000000000000000..a966aef93ed97d01d764f29940738df6df2d9d24 GIT binary patch literal 2545 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 hashtest.sha256 From d80df934147779b6337fa8026c8323767443c1b6 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 27 Jul 2016 16:23:27 -0600 Subject: [PATCH 030/183] feat(helm): add signature support to 'helm package' --- cmd/helm/helm.go | 1 + cmd/helm/list_test.go | 4 +- cmd/helm/package.go | 93 ++++++++++++---- cmd/helm/package_test.go | 148 +++++++++++++++++++++++++ cmd/helm/testdata/helm-test-key.secret | Bin 0 -> 2545 bytes pkg/provenance/sign.go | 48 +++++++- 6 files changed, 271 insertions(+), 23 deletions(-) create mode 100644 cmd/helm/package_test.go create mode 100644 cmd/helm/testdata/helm-test-key.secret diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index ebe139fb6..42e7c6c26 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -94,6 +94,7 @@ func newRootCmd(out io.Writer) *cobra.Command { newStatusCmd(nil, out), newUpgradeCmd(nil, out), newRollbackCmd(nil, out), + newPackageCmd(nil, out), ) return cmd } diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index 84fe65a47..0a2e88f3f 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -56,9 +56,7 @@ func TestListCmd(t *testing.T) { rels: tt.resp, } cmd := newListCmd(c, &buf) - for flag, value := range tt.flags { - cmd.Flags().Set(flag, value) - } + setFlags(cmd, tt.flags) err := cmd.RunE(cmd, tt.args) if (err != nil) != tt.err { t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) diff --git a/cmd/helm/package.go b/cmd/helm/package.go index beff30e31..81385224f 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -17,12 +17,18 @@ limitations under the License. package main import ( + "errors" "fmt" + "io" + "io/ioutil" "os" "path/filepath" "github.com/spf13/cobra" + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/provenance" "k8s.io/helm/pkg/repo" ) @@ -37,30 +43,56 @@ Chart.yaml file, and (if found) build the current directory into a chart. Versioned chart archives are used by Helm package repositories. ` -var save bool +const ( + envSigningKey = "HELM_SIGNING_KEY" + envKeyring = "HELM_KEYRING" +) -func init() { - packageCmd.Flags().BoolVar(&save, "save", true, "save packaged chart to local chart repository") - RootCommand.AddCommand(packageCmd) +type packageCmd struct { + save bool + sign bool + path string + key string + keyring string + out io.Writer } -var packageCmd = &cobra.Command{ - Use: "package [CHART_PATH]", - Short: "package a chart directory into a chart archive", - Long: packageDesc, - RunE: runPackage, -} +func newPackageCmd(client helm.Interface, out io.Writer) *cobra.Command { + pkg := &packageCmd{ + out: out, + } + cmd := &cobra.Command{ + Use: "package [CHART_PATH]", + Short: "package a chart directory into a chart archive", + Long: packageDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("This command needs at least one argument, the path to the chart.") + } + pkg.path = args[0] + if pkg.sign { + if pkg.key == "" { + return errors.New("--key is required for signing a package") + } + if pkg.keyring == "" { + return errors.New("--keyring is required for signing a package") + } + } + return pkg.run(cmd, args) + }, + } -func runPackage(cmd *cobra.Command, args []string) error { - path := "." + f := cmd.Flags() + f.BoolVar(&pkg.save, "save", true, "save packaged chart to local chart repository") + f.BoolVar(&pkg.sign, "sign", false, "use a PGP private key to sign this package") + f.StringVar(&pkg.key, "key", "", "the name of the key to use when signing. Used if --sign is true.") + f.StringVar(&pkg.keyring, "keyring", os.ExpandEnv("$HOME/.gnupg/pubring.gpg"), "the location of a public keyring") - if len(args) > 0 { - path = args[0] - } else { - return fmt.Errorf("This command needs at least one argument, the path to the chart.") - } + return cmd +} - path, err := filepath.Abs(path) +func (p *packageCmd) run(cmd *cobra.Command, args []string) error { + path, err := filepath.Abs(p.path) if err != nil { return err } @@ -86,7 +118,7 @@ func runPackage(cmd *cobra.Command, args []string) error { // Save to $HELM_HOME/local directory. This is second, because we don't want // the case where we saved here, but didn't save to the default destination. - if save { + if p.save { if err := repo.AddChartToLocalRepo(ch, localRepoDirectory()); err != nil { return err } else if flagDebug { @@ -94,5 +126,28 @@ func runPackage(cmd *cobra.Command, args []string) error { } } + if p.sign { + err = p.clearsign(name) + } + return err } + +func (p *packageCmd) clearsign(filename string) error { + // Load keyring + signer, err := provenance.NewFromKeyring(p.keyring, p.key) + if err != nil { + return err + } + + sig, err := signer.ClearSign(filename) + if err != nil { + return err + } + + if flagDebug { + fmt.Fprintln(p.out, sig) + } + + return ioutil.WriteFile(filename+".prov", []byte(sig), 0755) +} diff --git a/cmd/helm/package_test.go b/cmd/helm/package_test.go new file mode 100644 index 000000000..b724ba2ea --- /dev/null +++ b/cmd/helm/package_test.go @@ -0,0 +1,148 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/spf13/cobra" +) + +func TestPackage(t *testing.T) { + + tests := []struct { + name string + flags map[string]string + args []string + expect string + hasfile string + err bool + }{ + { + name: "package without chart path", + args: []string{}, + flags: map[string]string{}, + expect: "This command needs at least one argument, the path to the chart.", + err: true, + }, + { + name: "package --sign, no --key", + args: []string{"testdata/testcharts/alpine"}, + flags: map[string]string{"sign": "1"}, + expect: "key is required for signing a package", + err: true, + }, + { + name: "package --sign, no --keyring", + args: []string{"testdata/testcharts/alpine"}, + flags: map[string]string{"sign": "1", "key": "nosuchkey", "keyring": ""}, + expect: "keyring is required for signing a package", + err: true, + }, + { + name: "package testdata/testcharts/alpine", + args: []string{"testdata/testcharts/alpine"}, + expect: "", + hasfile: "alpine-0.1.0.tgz", + }, + { + name: "package --sign --key=KEY --keyring=KEYRING testdata/testcharts/alpine", + args: []string{"testdata/testcharts/alpine"}, + flags: map[string]string{"sign": "1", "keyring": "testdata/helm-test-key.secret", "key": "helm-test"}, + expect: "", + hasfile: "alpine-0.1.0.tgz", + }, + } + + // Because these tests are destructive, we run them in a tempdir. + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + tmp, err := ioutil.TempDir("", "helm-package-test-") + if err != nil { + t.Fatal(err) + } + + t.Logf("Running tests in %s", tmp) + if err := os.Chdir(tmp); err != nil { + t.Fatal(err) + } + + defer func() { + os.Chdir(origDir) + os.RemoveAll(tmp) + }() + + for _, tt := range tests { + buf := bytes.NewBuffer(nil) + c := newPackageCmd(nil, buf) + + // This is an unfortunate byproduct of the tmpdir + if v, ok := tt.flags["keyring"]; ok && len(v) > 0 { + tt.flags["keyring"] = filepath.Join(origDir, v) + } + + setFlags(c, tt.flags) + re := regexp.MustCompile(tt.expect) + + adjustedArgs := make([]string, len(tt.args)) + for i, f := range tt.args { + adjustedArgs[i] = filepath.Join(origDir, f) + } + + err := c.RunE(c, adjustedArgs) + if err != nil { + if tt.err && re.MatchString(err.Error()) { + continue + } + t.Errorf("%q: expected error %q, got %q", tt.name, tt.expect, err) + continue + } + + if !re.Match(buf.Bytes()) { + t.Errorf("%q: expected output %q, got %q", tt.name, tt.expect, buf.String()) + } + + if len(tt.hasfile) > 0 { + if fi, err := os.Stat(tt.hasfile); err != nil { + t.Errorf("%q: expected file %q, got err %q", tt.name, tt.hasfile, err) + } else if fi.Size() == 0 { + t.Errorf("%q: file %q has zero bytes.", tt.name, tt.hasfile) + } + } + + if v, ok := tt.flags["sign"]; ok && v == "1" { + if fi, err := os.Stat(tt.hasfile + ".prov"); err != nil { + t.Errorf("%q: expected provenance file", tt.name) + } else if fi.Size() == 0 { + t.Errorf("%q: provenance file is empty", tt.name) + } + } + } +} + +func setFlags(cmd *cobra.Command, flags map[string]string) { + dest := cmd.Flags() + for f, v := range flags { + dest.Set(f, v) + } +} diff --git a/cmd/helm/testdata/helm-test-key.secret b/cmd/helm/testdata/helm-test-key.secret new file mode 100644 index 0000000000000000000000000000000000000000..a966aef93ed97d01d764f29940738df6df2d9d24 GIT binary patch literal 2545 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 Date: Tue, 9 Aug 2016 16:36:00 -0600 Subject: [PATCH 031/183] feat(helm): add --verify flag to commands This adds the --verify and --keyring flags to: helm fetch helm inspect helm install helm upgrade Each of these commands can now make cryptographic verification a prerequisite for using a chart. --- cmd/helm/fetch.go | 175 +++++++++++++++--- cmd/helm/helm.go | 2 + cmd/helm/inspect.go | 21 ++- cmd/helm/install.go | 33 +++- cmd/helm/install_test.go | 18 ++ cmd/helm/list_test.go | 8 +- cmd/helm/package.go | 7 +- cmd/helm/testdata/helm-test-key.pub | Bin 0 -> 1243 bytes .../testdata/testcharts/signtest-0.1.0.tgz | Bin 0 -> 471 bytes .../testcharts/signtest-0.1.0.tgz.prov | 20 ++ .../testdata/testcharts/signtest/.helmignore | 5 + .../testdata/testcharts/signtest/Chart.yaml | 3 + .../testcharts/signtest/alpine/Chart.yaml | 6 + .../testcharts/signtest/alpine/README.md | 9 + .../signtest/alpine/templates/alpine-pod.yaml | 16 ++ .../testcharts/signtest/alpine/values.yaml | 2 + .../testcharts/signtest/templates/pod.yaml | 10 + .../testdata/testcharts/signtest/values.yaml | 0 cmd/helm/upgrade.go | 6 +- cmd/helm/verify.go | 67 +++++++ cmd/helm/verify_test.go | 83 +++++++++ docs/provenance.md | 173 +++++++++++++++++ pkg/provenance/sign.go | 47 +++-- pkg/provenance/sign_test.go | 10 +- 24 files changed, 658 insertions(+), 63 deletions(-) create mode 100644 cmd/helm/testdata/helm-test-key.pub create mode 100644 cmd/helm/testdata/testcharts/signtest-0.1.0.tgz create mode 100755 cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov create mode 100644 cmd/helm/testdata/testcharts/signtest/.helmignore create mode 100755 cmd/helm/testdata/testcharts/signtest/Chart.yaml create mode 100755 cmd/helm/testdata/testcharts/signtest/alpine/Chart.yaml create mode 100755 cmd/helm/testdata/testcharts/signtest/alpine/README.md create mode 100755 cmd/helm/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml create mode 100755 cmd/helm/testdata/testcharts/signtest/alpine/values.yaml create mode 100644 cmd/helm/testdata/testcharts/signtest/templates/pod.yaml create mode 100644 cmd/helm/testdata/testcharts/signtest/values.yaml create mode 100644 cmd/helm/verify.go create mode 100644 cmd/helm/verify_test.go create mode 100644 docs/provenance.md diff --git a/cmd/helm/fetch.go b/cmd/helm/fetch.go index 2aad66f37..cb077d7ce 100644 --- a/cmd/helm/fetch.go +++ b/cmd/helm/fetch.go @@ -17,8 +17,11 @@ limitations under the License. package main import ( + "bytes" + "errors" "fmt" "io" + "io/ioutil" "net/http" "net/url" "os" @@ -27,65 +30,179 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/provenance" "k8s.io/helm/pkg/repo" ) -var untarFile bool -var untarDir string +const fetchDesc = ` +Retrieve a package from a package repository, and download it locally. -func init() { - RootCommand.AddCommand(fetchCmd) - fetchCmd.Flags().BoolVar(&untarFile, "untar", false, "If set to true, will untar the chart after downloading it.") - fetchCmd.Flags().StringVar(&untarDir, "untardir", ".", "If untar is specified, this flag specifies where to untar the chart.") -} +This is useful for fetching packages to inspect, modify, or repackage. It can +also be used to perform cryptographic verification of a chart without installing +the chart. + +There are options for unpacking the chart after download. This will create a +directory for the chart and uncomparess into that directory. + +If the --verify flag is specified, the requested chart MUST have a provenance +file, and MUST pass the verification process. Failure in any part of this will +result in an error, and the chart will not be saved locally. +` -var fetchCmd = &cobra.Command{ - Use: "fetch [chart URL | repo/chartname]", - Short: "download a chart from a repository and (optionally) unpack it in local directory", - Long: "", - RunE: fetch, +type fetchCmd struct { + untar bool + untardir string + chartRef string + + verify bool + keyring string + + out io.Writer } -func fetch(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return fmt.Errorf("This command needs at least one argument, url or repo/name of the chart.") +func newFetchCmd(out io.Writer) *cobra.Command { + fch := &fetchCmd{out: out} + + cmd := &cobra.Command{ + Use: "fetch [chart URL | repo/chartname]", + Short: "download a chart from a repository and (optionally) unpack it in local directory", + Long: fetchDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("This command needs at least one argument, url or repo/name of the chart.") + } + fch.chartRef = args[0] + return fch.run() + }, } - pname := args[0] + f := cmd.Flags() + f.BoolVar(&fch.untar, "untar", false, "If set to true, will untar the chart after downloading it.") + f.StringVar(&fch.untardir, "untardir", ".", "If untar is specified, this flag specifies where to untar the chart.") + f.BoolVar(&fch.verify, "verify", false, "Verify the package against its signature.") + f.StringVar(&fch.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.") + + return cmd +} + +func (f *fetchCmd) run() error { + pname := f.chartRef if filepath.Ext(pname) != ".tgz" { pname += ".tgz" } - return fetchChart(pname) + return downloadChart(pname, f.untar, f.untardir, f.verify, f.keyring) } -func fetchChart(pname string) error { - - f, err := repo.LoadRepositoriesFile(repositoriesFile()) +// downloadChart fetches a chart over HTTP, and then (if verify is true) verifies it. +// +// If untar is true, it also unpacks the file into untardir. +func downloadChart(pname string, untar bool, untardir string, verify bool, keyring string) error { + r, err := repo.LoadRepositoriesFile(repositoriesFile()) if err != nil { return err } // get download url - u, err := mapRepoArg(pname, f.Repositories) + u, err := mapRepoArg(pname, r.Repositories) if err != nil { return err } - resp, err := http.Get(u.String()) + href := u.String() + buf, err := fetchChart(href) if err != nil { return err } - if resp.StatusCode != 200 { - return fmt.Errorf("Failed to fetch %s : %s", u.String(), resp.Status) + + if verify { + basename := filepath.Base(pname) + sigref := href + ".prov" + sig, err := fetchChart(sigref) + if err != nil { + return fmt.Errorf("provenance data not downloaded from %s: %s", sigref, err) + } + if err := ioutil.WriteFile(basename+".prov", sig.Bytes(), 0755); err != nil { + return fmt.Errorf("provenance data not saved: %s", err) + } + if err := verifyChart(basename, keyring); err != nil { + return err + } } - defer resp.Body.Close() - if untarFile { - return chartutil.Expand(untarDir, resp.Body) + return saveChart(pname, buf, untar, untardir) +} + +// verifyChart takes a path to a chart archive and a keyring, and verifies the chart. +// +// It assumes that a chart archive file is accompanied by a provenance file whose +// name is the archive file name plus the ".prov" extension. +func verifyChart(path string, keyring string) error { + // For now, error out if it's not a tar file. + if fi, err := os.Stat(path); err != nil { + return err + } else if fi.IsDir() { + return errors.New("unpacked charts cannot be verified") + } else if !isTar(path) { + return errors.New("chart must be a tgz file") } - p := strings.Split(u.String(), "/") - return saveChartFile(p[len(p)-1], resp.Body) + + provfile := path + ".prov" + if _, err := os.Stat(provfile); err != nil { + return fmt.Errorf("could not load provenance file %s: %s", provfile, err) + } + + sig, err := provenance.NewFromKeyring(keyring, "") + if err != nil { + return fmt.Errorf("failed to load keyring: %s", err) + } + ver, err := sig.Verify(path, provfile) + if flagDebug { + for name := range ver.SignedBy.Identities { + fmt.Printf("Signed by %q\n", name) + } + } + return err +} + +// defaultKeyring returns the expanded path to the default keyring. +func defaultKeyring() string { + return os.ExpandEnv("$HOME/.gnupg/pubring.gpg") +} + +// isTar tests whether the given file is a tar file. +// +// Currently, this simply checks extension, since a subsequent function will +// untar the file and validate its binary format. +func isTar(filename string) bool { + return strings.ToLower(filepath.Ext(filename)) == ".tgz" +} + +// saveChart saves a chart locally. +func saveChart(name string, buf *bytes.Buffer, untar bool, untardir string) error { + if untar { + return chartutil.Expand(untardir, buf) + } + + p := strings.Split(name, "/") + return saveChartFile(p[len(p)-1], buf) +} + +// fetchChart retrieves a chart over HTTP. +func fetchChart(href string) (*bytes.Buffer, error) { + buf := bytes.NewBuffer(nil) + + resp, err := http.Get(href) + if err != nil { + return buf, err + } + if resp.StatusCode != 200 { + return buf, fmt.Errorf("Failed to fetch %s : %s", href, resp.Status) + } + + _, err = io.Copy(buf, resp.Body) + resp.Body.Close() + return buf, err } // mapRepoArg figures out which format the argument is given, and creates a fetchable diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 42e7c6c26..81085e1ef 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -95,6 +95,8 @@ func newRootCmd(out io.Writer) *cobra.Command { newUpgradeCmd(nil, out), newRollbackCmd(nil, out), newPackageCmd(nil, out), + newFetchCmd(out), + newVerifyCmd(out), ) return cmd } diff --git a/cmd/helm/inspect.go b/cmd/helm/inspect.go index 8d9948b6f..cc02f80b1 100644 --- a/cmd/helm/inspect.go +++ b/cmd/helm/inspect.go @@ -46,6 +46,8 @@ of the Charts.yaml file type inspectCmd struct { chartpath string output string + verify bool + keyring string out io.Writer client helm.Interface } @@ -71,7 +73,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command { if err := checkArgsLength(1, len(args), "chart name"); err != nil { return err } - cp, err := locateChartPath(args[0]) + cp, err := locateChartPath(args[0], insp.verify, insp.keyring) if err != nil { return err } @@ -86,7 +88,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command { Long: inspectValuesDesc, RunE: func(cmd *cobra.Command, args []string) error { insp.output = valuesOnly - cp, err := locateChartPath(args[0]) + cp, err := locateChartPath(args[0], insp.verify, insp.keyring) if err != nil { return err } @@ -101,7 +103,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command { Long: inspectChartDesc, RunE: func(cmd *cobra.Command, args []string) error { insp.output = chartOnly - cp, err := locateChartPath(args[0]) + cp, err := locateChartPath(args[0], insp.verify, insp.keyring) if err != nil { return err } @@ -110,6 +112,19 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command { }, } + vflag := "verify" + vdesc := "verify the provenance data for this chart" + inspectCommand.Flags().BoolVar(&insp.verify, vflag, false, vdesc) + valuesSubCmd.Flags().BoolVar(&insp.verify, vflag, false, vdesc) + chartSubCmd.Flags().BoolVar(&insp.verify, vflag, false, vdesc) + + kflag := "keyring" + kdesc := "the path to the keyring containing public verification keys" + kdefault := defaultKeyring() + inspectCommand.Flags().StringVar(&insp.keyring, kflag, kdefault, kdesc) + valuesSubCmd.Flags().StringVar(&insp.keyring, kflag, kdefault, kdesc) + chartSubCmd.Flags().StringVar(&insp.keyring, kflag, kdefault, kdesc) + inspectCommand.AddCommand(valuesSubCmd) inspectCommand.AddCommand(chartSubCmd) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index b41669d24..402c2a549 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -18,6 +18,7 @@ package main import ( "bytes" + "errors" "fmt" "io" "io/ioutil" @@ -54,6 +55,9 @@ or To check the generated manifests of a release without installing the chart, the '--debug' and '--dry-run' flags can be combined. This will still require a round-trip to the Tiller server. + +If --verify is set, the chart MUST have a provenance file, and the provenenace +fall MUST pass all verification steps. ` type installCmd struct { @@ -64,6 +68,8 @@ type installCmd struct { dryRun bool disableHooks bool replace bool + verify bool + keyring string out io.Writer client helm.Interface values *values @@ -86,7 +92,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { if err := checkArgsLength(1, len(args), "chart name"); err != nil { return err } - cp, err := locateChartPath(args[0]) + cp, err := locateChartPath(args[0], inst.verify, inst.keyring) if err != nil { return err } @@ -106,6 +112,8 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { 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") + f.BoolVar(&inst.verify, "verify", false, "verify the package before installing it") + f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "location of public keys used for verification") return cmd } @@ -171,6 +179,7 @@ func (i *installCmd) vals() ([]byte, error) { return buffer.Bytes(), nil } +// printRelease prints info about a release if the flagDebug is true. func (i *installCmd) printRelease(rel *release.Release) { if rel == nil { return @@ -251,9 +260,23 @@ func splitPair(item string) (name string, value interface{}) { // - current working directory // - if path is absolute or begins with '.', error out here // - chart repos in $HELM_HOME -func locateChartPath(name string) (string, error) { - if _, err := os.Stat(name); err == nil { - return filepath.Abs(name) +// +// If 'verify' is true, this will attempt to also verify the chart. +func locateChartPath(name string, verify bool, keyring string) (string, error) { + if fi, err := os.Stat(name); err == nil { + abs, err := filepath.Abs(name) + if err != nil { + return abs, err + } + if verify { + if fi.IsDir() { + return "", errors.New("cannot verify a directory") + } + if err := verifyChart(abs, keyring); err != nil { + return "", err + } + } + return abs, nil } if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { return name, fmt.Errorf("path %q not found", name) @@ -269,7 +292,7 @@ func locateChartPath(name string) (string, error) { if filepath.Ext(name) != ".tgz" { name += ".tgz" } - if err := fetchChart(name); err == nil { + if err := downloadChart(name, false, ".", verify, keyring); err == nil { lname, err := filepath.Abs(filepath.Base(name)) if err != nil { return lname, err diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index 61bb8eb9a..48d7854cd 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -74,6 +74,24 @@ func TestInstall(t *testing.T) { expected: "FOOBAR", resp: releaseMock(&releaseOptions{name: "FOOBAR"}), }, + // Install, perform chart verification along the way. + { + name: "install with verification, missing provenance", + args: []string{"testdata/testcharts/compressedchart-0.1.0.tgz"}, + flags: strings.Split("--verify --keyring testdata/helm-test-key.pub", " "), + err: true, + }, + { + name: "install with verification, directory instead of file", + args: []string{"testdata/testcharts/signtest"}, + flags: strings.Split("--verify --keyring testdata/helm-test-key.pub", " "), + err: true, + }, + { + name: "install with verification, valid", + args: []string{"testdata/testcharts/signtest-0.1.0.tgz"}, + flags: strings.Split("--verify --keyring testdata/helm-test-key.pub", " "), + }, } runReleaseCases(t, tests, func(c *fakeReleaseClient, out io.Writer) *cobra.Command { diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index 0a2e88f3f..b12d41408 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -28,7 +28,6 @@ func TestListCmd(t *testing.T) { tests := []struct { name string args []string - flags map[string]string resp []*release.Release expected string err bool @@ -41,8 +40,9 @@ func TestListCmd(t *testing.T) { expected: "thomas-guide", }, { - name: "list --long", - flags: map[string]string{"long": "1"}, + name: "list --long", + //flags: map[string]string{"long": "1"}, + args: []string{"--long"}, resp: []*release.Release{ releaseMock(&releaseOptions{name: "atlas"}), }, @@ -56,7 +56,7 @@ func TestListCmd(t *testing.T) { rels: tt.resp, } cmd := newListCmd(c, &buf) - setFlags(cmd, tt.flags) + cmd.ParseFlags(tt.args) err := cmd.RunE(cmd, tt.args) if (err != nil) != tt.err { t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) diff --git a/cmd/helm/package.go b/cmd/helm/package.go index 81385224f..056fd39cb 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -43,11 +43,6 @@ Chart.yaml file, and (if found) build the current directory into a chart. Versioned chart archives are used by Helm package repositories. ` -const ( - envSigningKey = "HELM_SIGNING_KEY" - envKeyring = "HELM_KEYRING" -) - type packageCmd struct { save bool sign bool @@ -86,7 +81,7 @@ func newPackageCmd(client helm.Interface, out io.Writer) *cobra.Command { f.BoolVar(&pkg.save, "save", true, "save packaged chart to local chart repository") f.BoolVar(&pkg.sign, "sign", false, "use a PGP private key to sign this package") f.StringVar(&pkg.key, "key", "", "the name of the key to use when signing. Used if --sign is true.") - f.StringVar(&pkg.keyring, "keyring", os.ExpandEnv("$HOME/.gnupg/pubring.gpg"), "the location of a public keyring") + f.StringVar(&pkg.keyring, "keyring", defaultKeyring(), "the location of a public keyring") return cmd } diff --git a/cmd/helm/testdata/helm-test-key.pub b/cmd/helm/testdata/helm-test-key.pub new file mode 100644 index 0000000000000000000000000000000000000000..38714f25adaf701b08e11fd559a587074bbde0e4 GIT binary patch literal 1243 zcmV<11SI>J0SyFKmTjH^2mr{k15wFPQdpTAAEclY4oV^-7nTy5x$&xF;PC4il}o-Pk4@#$knlp#(|J0GE?qli_lr;7-o zyY8vBsN_GJe;#w<`JdR7riNL&RJlcS)FG+W=91;dYS6NZ2tY?kZ8Sw9{r=e4|L3E{ zRod|EPC!PWgW&pe&4qiqKQAijj;G~fyjcC^m%0p54Tn{h%YFKd0VC4n=#~SRc@BVd znj66*)Om%*SEQfX{1*Tb0RRECT}WkYZ6H)-b98BLXCNq4XlZjGYh`&Lb7*gMY-AvB zZftoVVr3w8b7f>8W^ZyJbY*jNX>MmOAVg0fPES-IR8mz_R4yqXJZNQXZ7p6uVakV zT7GGV$jaKjyjfI_a~N1!Hk?5C$0wa&4)R=i$v7t&ZMycW#RkavpF%A?>MTT2anNDzOQUm<++zEOykJ9-@&c2QXq3owqf7fek`=L@+7iF zv;IW2Q>Q&r+V@cWDF&hAUUsCKlDinerKgvJUJCl$5gjb7NhM{mBP%!M^mX-iS8xFf zuLB{@MDqvtZzF#Bxd9CXSC(y_0SExW>8~h=U8|!do4*OJj2u#!KDe3v+1T+aVzU5di=Ji2)x37y$|Z2?YXImTjH_8w>yn2@r%kznCAv zhhp0`2mpxVj5j%o&5i)?`r7iES|8dA@p2kk@+XS(tjBGN)6>tm^=gayCn`gTEC*K74Y~{I_PREk) z)PstIMx1RxB@cK8%Mey%;nVnKriAKUk2Ky?dBMG3uXItKL$3N(#3P^pQa*K$l)wUy F^>pMLK0g2e literal 0 HcmV?d00001 diff --git a/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz b/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..6de9d988d47cfea649eaa5b23ba09d8a5fee04ed GIT binary patch literal 471 zcmV;|0Vw_-iwG0|32ul0|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PL1s>(ek4#&_LMab!0N8r#jSu)EfR!Yhji_ntJ+cZn_Q$NgS zvpkzm;N}PUn+EH+q3y3-=lpU{L>1c7h~5dUR^fjXXQ78U)Tn=deive8Xe@3wX&i_1nlSlsVp($*z=7V%F7C^xM zSQIRo!k1Q9pohcP^~Vpd=yk`P!wPC4(Fbg>l-wYAgBYs_dM=Cwr=jqDYbjbN8Xoju zz+u-*PRsk`(N#iL^pHpB#6N4v`e~pI-g=LV{4bY(@IPBd{_mkFeD*jS6?h%LKkQpn zPz*v=LN!Ei`JFc-ufYxM(D&Ln>QK!{XrwNHOrdNk`Xv}7y2Z|u@7iDHxvD(y*l_=| z0ndAbwfI5Suoo2f>;;2QN*+L~km-*EJsOZgkHDk|z~{R{vA N|NqlRq0#^l007yS;m800 literal 0 HcmV?d00001 diff --git a/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov b/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov new file mode 100755 index 000000000..94235399a --- /dev/null +++ b/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov @@ -0,0 +1,20 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +description: A Helm chart for Kubernetes +name: signtest +version: 0.1.0 + +... +files: + signtest-0.1.0.tgz: sha256:dee72947753628425b82814516bdaa37aef49f25e8820dd2a6e15a33a007823b +-----BEGIN PGP SIGNATURE----- + +wsBcBAEBCgAQBQJXomNHCRCEO7+YH8GHYgAALywIAG1Me852Fpn1GYu8Q1GCcw4g +l2k7vOFchdDwDhdSVbkh4YyvTaIO3iE2Jtk1rxw+RIJiUr0eLO/rnIJuxZS8WKki +DR1LI9J1VD4dxN3uDETtWDWq7ScoPsRY5mJvYZXC8whrWEt/H2kfqmoA9LloRPWp +flOE0iktA4UciZOblTj6nAk3iDyjh/4HYL4a6tT0LjjKI7OTw4YyHfjHad1ywVCz +9dMUc1rPgTnl+fnRiSPSrlZIWKOt1mcQ4fVrU3nwtRUwTId2k8FtygL0G6M+Y6t0 +S6yaU7qfk9uTxkdkUF7Bf1X3ukxfe+cNBC32vf4m8LY4NkcYfSqK2fGtQsnVr6s= +=NyOM +-----END PGP SIGNATURE----- \ No newline at end of file diff --git a/cmd/helm/testdata/testcharts/signtest/.helmignore b/cmd/helm/testdata/testcharts/signtest/.helmignore new file mode 100644 index 000000000..435b756d8 --- /dev/null +++ b/cmd/helm/testdata/testcharts/signtest/.helmignore @@ -0,0 +1,5 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +.git diff --git a/cmd/helm/testdata/testcharts/signtest/Chart.yaml b/cmd/helm/testdata/testcharts/signtest/Chart.yaml new file mode 100755 index 000000000..90964b44a --- /dev/null +++ b/cmd/helm/testdata/testcharts/signtest/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: signtest +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/signtest/alpine/Chart.yaml b/cmd/helm/testdata/testcharts/signtest/alpine/Chart.yaml new file mode 100755 index 000000000..6fbb27f18 --- /dev/null +++ b/cmd/helm/testdata/testcharts/signtest/alpine/Chart.yaml @@ -0,0 +1,6 @@ +description: Deploy a basic Alpine Linux pod +home: https://k8s.io/helm +name: alpine +sources: +- https://github.com/kubernetes/helm +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/signtest/alpine/README.md b/cmd/helm/testdata/testcharts/signtest/alpine/README.md new file mode 100755 index 000000000..5bd595747 --- /dev/null +++ b/cmd/helm/testdata/testcharts/signtest/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.yaml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install docs/examples/alpine`. diff --git a/cmd/helm/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml b/cmd/helm/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml new file mode 100755 index 000000000..08cf3c2c1 --- /dev/null +++ b/cmd/helm/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + heritage: {{.Release.Service}} + chartName: {{.Chart.Name}} + chartVersion: {{.Chart.Version | quote}} + annotations: + "helm.sh/created": "{{.Release.Time.Seconds}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/cmd/helm/testdata/testcharts/signtest/alpine/values.yaml b/cmd/helm/testdata/testcharts/signtest/alpine/values.yaml new file mode 100755 index 000000000..bb6c06ae4 --- /dev/null +++ b/cmd/helm/testdata/testcharts/signtest/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: my-alpine diff --git a/cmd/helm/testdata/testcharts/signtest/templates/pod.yaml b/cmd/helm/testdata/testcharts/signtest/templates/pod.yaml new file mode 100644 index 000000000..9b00ccaf7 --- /dev/null +++ b/cmd/helm/testdata/testcharts/signtest/templates/pod.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: signtest +spec: + restartPolicy: Never + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/cmd/helm/testdata/testcharts/signtest/values.yaml b/cmd/helm/testdata/testcharts/signtest/values.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 2ad2f7800..3b7de8ec8 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -46,6 +46,8 @@ type upgradeCmd struct { disableHooks bool valuesFile string values *values + verify bool + keyring string } func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { @@ -79,6 +81,8 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { f.BoolVar(&upgrade.dryRun, "dry-run", false, "simulate an upgrade") f.Var(upgrade.values, "set", "set values on the command line. Separate values with commas: key1=val1,key2=val2") f.BoolVar(&upgrade.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks") + f.BoolVar(&upgrade.verify, "verify", false, "verify the provenance of the chart before upgrading") + f.StringVar(&upgrade.keyring, "keyring", defaultKeyring(), "the path to the keyring that contains public singing keys") return cmd } @@ -109,7 +113,7 @@ func (u *upgradeCmd) vals() ([]byte, error) { } func (u *upgradeCmd) run() error { - chartPath, err := locateChartPath(u.chart) + chartPath, err := locateChartPath(u.chart, u.verify, u.keyring) if err != nil { return err } diff --git a/cmd/helm/verify.go b/cmd/helm/verify.go new file mode 100644 index 000000000..4e342daaf --- /dev/null +++ b/cmd/helm/verify.go @@ -0,0 +1,67 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "errors" + "io" + + "github.com/spf13/cobra" +) + +const verifyDesc = ` +Verify that the given chart has a valid provenance file. + +Provenance files provide crytographic verification that a chart has not been +tampered with, and was packaged by a trusted provider. + +This command can be used to verify a local chart. Several other commands provide +'--verify' flags that run the same validation. To generate a signed package, use +the 'helm package --sign' command. +` + +type verifyCmd struct { + keyring string + chartfile string + + out io.Writer +} + +func newVerifyCmd(out io.Writer) *cobra.Command { + vc := &verifyCmd{out: out} + + cmd := &cobra.Command{ + Use: "verify [flags] PATH", + Short: "verify that a chart at the given path has been signed and is valid", + Long: verifyDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("a path to a package file is required") + } + vc.chartfile = args[0] + return vc.run() + }, + } + + f := cmd.Flags() + f.StringVar(&vc.keyring, "keyring", defaultKeyring(), "the keyring containing public keys.") + + return cmd +} + +func (v *verifyCmd) run() error { + return verifyChart(v.chartfile, v.keyring) +} diff --git a/cmd/helm/verify_test.go b/cmd/helm/verify_test.go new file mode 100644 index 000000000..425f1a28b --- /dev/null +++ b/cmd/helm/verify_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "testing" +) + +func TestVerifyCmd(t *testing.T) { + tests := []struct { + name string + args []string + flags []string + expect string + err bool + }{ + { + name: "verify requires a chart", + expect: "a path to a package file is required", + err: true, + }, + { + name: "verify requires that chart exists", + args: []string{"no/such/file"}, + expect: "stat no/such/file: no such file or directory", + err: true, + }, + { + name: "verify requires that chart is not a directory", + args: []string{"testdata/testcharts/signtest"}, + expect: "unpacked charts cannot be verified", + err: true, + }, + { + name: "verify requires that chart has prov file", + args: []string{"testdata/testcharts/compressedchart-0.1.0.tgz"}, + expect: "could not load provenance file testdata/testcharts/compressedchart-0.1.0.tgz.prov: stat testdata/testcharts/compressedchart-0.1.0.tgz.prov: no such file or directory", + err: true, + }, + { + name: "verify validates a properly signed chart", + args: []string{"testdata/testcharts/signtest-0.1.0.tgz"}, + flags: []string{"--keyring", "testdata/helm-test-key.pub"}, + expect: "", + err: false, + }, + } + + for _, tt := range tests { + b := bytes.NewBuffer(nil) + vc := newVerifyCmd(b) + vc.ParseFlags(tt.flags) + err := vc.RunE(vc, tt.args) + if tt.err { + if err == nil { + t.Errorf("Expected error, but got none: %q", b.String()) + } + if err.Error() != tt.expect { + t.Errorf("Expected error %q, got %q", tt.expect, err) + } + continue + } else if err != nil { + t.Errorf("Unexpected error: %s", err) + } + if b.String() != tt.expect { + t.Errorf("Expected %q, got %q", tt.expect, b.String()) + } + } +} diff --git a/docs/provenance.md b/docs/provenance.md new file mode 100644 index 000000000..13c989dee --- /dev/null +++ b/docs/provenance.md @@ -0,0 +1,173 @@ +# Helm Provenance and Integrity + +Helm has provenance tools which help chart users verify the integrity and origin +of a package. Using industry-standard tools based on PKI, GnuPG, and well-resepected +package managers, Helm can generate and verify signature files. + +**Note:** +Version 2.0.0-alpha.4 introduced a system for verifying the authenticity of charts. +While we do not anticipate that any major changes will be made to the file formats +or provenancing algorithms, this portion of Helm is not considered _frozen_ until +2.0.0-RC1 is released. The original plan for this feature can be found +[at issue 983](https://github.com/kubernetes/helm/issues/983). + +## Overview + +Integrity is established by comparing a chart to a provenance record. Provenance +records are stored in _provenance files_, which are stored alongside a packaged +chart. For example, if a chart is named `myapp-1.2.3.tgz`, its provenance file +will be `myapp-1.2.3.tgz.prov`. + +Provenance files are generated at packaging time (`helm package --sign ...`), and +can be checked by multiple commands, notable `helm install --verify`. + +## The Workflow + +This section describes a potential workflow for using provenance data effectively. + +WHAT YOU WILL NEED: + +- A valid PGP keypair in a binary (not ASCII-armored) format +- helm + +Creating a new chart is the same as before: + +``` +$ helm create mychart +Creating mychart +``` + +Once ready to package, add the `--verify` flag to `helm package`. Also, specify +the signing key and the keyring: + +``` +$ helm package --sign --key helm --keyring path/to/keyring.secret mychart +``` + +Tip: for GnuPG users, your secret keyring is in `~/.gpg/secring.gpg`. + +At this point, you should see both `mychart-0.1.0.tgz` and `mychart-0.1.0.tgz.prov`. +Both files should eventually be uploaded to your desired chart repository. + +You can verify a chart using `helm verify`: + +``` +$ helm verify mychart-0.1.0.tgz +``` + +A failed verification looks like this: + +``` +$ helm verify topchart-0.1.0.tgz +Error: sha256 sum does not match for topchart-0.1.0.tgz: "sha256:1939fbf7c1023d2f6b865d137bbb600e0c42061c3235528b1e8c82f4450c12a7" != "sha256:5a391a90de56778dd3274e47d789a2c84e0e106e1a37ef8cfa51fd60ac9e623a" +``` + +To verify during an install, use the `--verify` flag. + +``` +$ helm install --verify mychart-0.1.0.tgz +``` + +If the keyring is not in the default location, you may need to point to the +keyring with `--keyring PATH` as in the `helm package` example. + +If verification fails, the install will be aborted before the chart is even pushed +up to Tiller. + +### Reasons a chart may not verify + +These are common reasons for failure. + +- The prov file is missing or corrupt. This indicates that something is misconfigured + or that the original maintainer did not create a provenance file. +- The key used to sign the file is not in your keyring. This indicate that the + entity who signed the chart is not someone you've already signaled that you trust. +- The verification of the prov file failed. This indicates that something is wrong + with either the chart or the provenance data. +- The file hashes in the provenance file do not match the hash of the archive file. This + indicates that the archive has been tampered with. + +If a verification fails, there is reason to distrust the package. + +## The Provenance File +The provenance file contains a chart’s YAML file plus several pieces of +verification information. Provenance files are designed to be automatically +generated. + + +The following pieces of provenance data are added: + + +* The chart file (Chart.yaml) is included to give both humans and tools an easy + view into the contents of the chart. +* **Not Complete yet:** Every image file that the project references is + correlated with its hash (SHA256, used by Docker) for verification. +* The signature (SHA256, just like Docker) of the chart package (the .tgz file) + is included, and may be used to verify the integrity of the chart package. +* The entire body is signed using the algorithm used by PGP (see + [http://keybase.io] for an emerging way of making crypto signing and + verification easy). + +The combination of this gives users the following assurances: + +* The images this chart references at build time are still the same exact + version when installed (checksum images). + * This is distinct from asserting that the image Kubernetes is running is + exactly the same version that a chart references. Kubernetes does not + currently give us a way of verifying this. +* The package itself has not been tampered with (checksum package tgz). +* The entity who released this package is known (via the GnuPG/PGP signature). + +The format of the file looks something like this: + +``` +-----BEGIN PGP SIGNED MESSAGE----- +name: nginx +description: The nginx web server as a replication controller and service pair. +version: 0.5.1 +keywords: + - https + - http + - web server + - proxy +source: +- https://github.com/foo/bar +home: http://nginx.com + +... +files: + nginx-0.5.1.tgz: “sha256:9f5270f50fc842cfcb717f817e95178f” +images: + “hub.docker.com/_/nginx:5.6.0”: “sha256:f732c04f585170ed3bc99” +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.9 (GNU/Linux) + +iEYEARECAAYFAkjilUEACgQkB01zfu119ZnHuQCdGCcg2YxF3XFscJLS4lzHlvte +WkQAmQGHuuoLEJuKhRNo+Wy7mhE7u1YG +=eifq +-----END PGP SIGNATURE----- +``` + +Note that the YAML section contains two documents (separated by `...\n`). The +first is the Chart.yaml. The second is the checksums, defined as follows. + +* Files: A map of filenames to SHA-256 checksums (value shown is + fake/truncated) +* Images: A map of image URLs to checksums (value shown is fake/truncated) + +The signature block is a standard PGP signature, which provides [tamper +resistance](http://www.rossde.com/PGP/pgp_signatures.html). + +## Chart Repositories + +Chart repositories serve as a centralized collection of Helm charts. + +Chart repositories must make it possible to serve provenance files over HTTP via +a specific request, and must make them available at the same URI path as the chart. + +For example, if the base URL for a package is `https://example.com/charts/mychart-1.2.3.tgz`, +the provenance file, if it exists, MUST be accessible at `https://example.com/charts/mychart-1.2.3.tgz.prov`. + +From the end user's perspective, `helm install --verify myrepo/mychart-1.2.3` +should result in the download of both the chart and the provenance file with no +additional user configuration or action. diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go index 5fb9be8a8..4d379934d 100644 --- a/pkg/provenance/sign.go +++ b/pkg/provenance/sign.go @@ -23,7 +23,6 @@ import ( "fmt" "io" "io/ioutil" - "log" "os" "path/filepath" "strings" @@ -42,7 +41,7 @@ var defaultPGPConfig = packet.Config{ DefaultHash: crypto.SHA512, } -// SumCollection represents a collecton of file and image checksums. +// SumCollection represents a collection of file and image checksums. // // Files are of the form: // FILENAME: "sha256:SUM" @@ -55,6 +54,14 @@ type SumCollection struct { Images map[string]string `json:"images,omitempty"` } +// Verification contains information about a verification operation. +type Verification struct { + // SignedBy contains the entity that signed a chart. + SignedBy *openpgp.Entity + // FileHash is the hash, prepended with the scheme, for the file that was verified. + FileHash string +} + // Signatory signs things. // // Signatories can be constructed from a PGP private key file using NewFromFiles @@ -174,32 +181,50 @@ func (s *Signatory) ClearSign(chartpath string) (string, error) { } // Verify checks a signature and verifies that it is legit for a chart. -func (s *Signatory) Verify(chartpath, sigpath string) (bool, error) { +func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) { + ver := &Verification{} for _, fname := range []string{chartpath, sigpath} { if fi, err := os.Stat(fname); err != nil { - return false, err + return ver, err } else if fi.IsDir() { - return false, fmt.Errorf("%s cannot be a directory", fname) + return ver, fmt.Errorf("%s cannot be a directory", fname) } } // First verify the signature sig, err := s.decodeSignature(sigpath) if err != nil { - return false, fmt.Errorf("failed to decode signature: %s", err) + return ver, fmt.Errorf("failed to decode signature: %s", err) } by, err := s.verifySignature(sig) if err != nil { - return false, err - } - for n := range by.Identities { - log.Printf("info: %s signed by %q", sigpath, n) + return ver, err } + ver.SignedBy = by // Second, verify the hash of the tarball. + sum, err := sumArchive(chartpath) + if err != nil { + return ver, err + } + _, sums, err := parseMessageBlock(sig.Plaintext) + if err != nil { + return ver, err + } + + sum = "sha256:" + sum + basename := filepath.Base(chartpath) + if sha, ok := sums.Files[basename]; !ok { + return ver, fmt.Errorf("provenance does not contain a SHA for a file named %q", basename) + } else if sha != sum { + return ver, fmt.Errorf("sha256 sum does not match for %s: %q != %q", basename, sha, sum) + } + ver.FileHash = sum + + // TODO: when image signing is added, verify that here. - return true, nil + return ver, nil } func (s *Signatory) decodeSignature(filename string) (*clearsign.Block, error) { diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go index 5dfd6bd68..2f66748c1 100644 --- a/pkg/provenance/sign_test.go +++ b/pkg/provenance/sign_test.go @@ -218,13 +218,15 @@ func TestVerify(t *testing.T) { t.Fatal(err) } - passed, err := signer.Verify(testChartfile, testSigBlock) - if !passed { + if ver, err := signer.Verify(testChartfile, testSigBlock); err != nil { t.Errorf("Failed to pass verify. Err: %s", err) + } else if len(ver.FileHash) == 0 { + t.Error("Verification is missing hash.") + } else if ver.SignedBy == nil { + t.Error("No SignedBy field") } - passed, err = signer.Verify(testChartfile, testTamperedSigBlock) - if passed { + if _, err = signer.Verify(testChartfile, testTamperedSigBlock); err == nil { t.Errorf("Expected %s to fail.", testTamperedSigBlock) } From 9bb471d2a88d67900df6313d81a50aec479a589e Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Wed, 24 Aug 2016 15:38:33 -0700 Subject: [PATCH 032/183] fix(cmd): s/accomodate/accommodate/ Matt's second offence. --- cmd/tiller/release_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 0fe6a192a..5c103e90d 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -41,7 +41,7 @@ var srv *releaseServer // releaseNameMaxLen is the maximum length of a release name. // -// This is designed to accomodate the usage of release name in the 'name:' +// This is designed to accommodate the usage of release name in the 'name:' // field of Kubernetes resources. Many of those fields are limited to 24 // characters in length. See https://github.com/kubernetes/helm/issues/1071 const releaseNameMaxLen = 14 From 197e466b9a72be8999436ca756deab16452f752a Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Thu, 25 Aug 2016 09:06:14 -0600 Subject: [PATCH 033/183] ref(*): return resource update errors Resolves #1058 --- cmd/helm/upgrade.go | 2 +- cmd/tiller/release_server.go | 2 +- pkg/kube/client.go | 16 +++++++++++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 3b7de8ec8..10251dadd 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -125,7 +125,7 @@ func (u *upgradeCmd) run() error { _, err = u.client.UpdateRelease(u.release, chartPath, helm.UpdateValueOverrides(rawVals), helm.UpgradeDryRun(u.dryRun), helm.UpgradeDisableHooks(u.disableHooks)) if err != nil { - return prettyError(err) + return fmt.Errorf("UPGRADE FAILED: %v", prettyError(err)) } success := u.release + " has been upgraded. Happy Helming!\n" diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 5c103e90d..cd3de35f6 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -224,7 +224,7 @@ func (s *releaseServer) performUpdate(originalRelease, updatedRelease *release.R 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) + return nil, err } // post-upgrade hooks diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 6e5e74e30..529d2b4c4 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -22,6 +22,7 @@ import ( "io" "log" "reflect" + "strings" "time" "k8s.io/kubernetes/pkg/api" @@ -157,8 +158,9 @@ func (c *Client) Update(namespace string, currentReader, modifiedReader io.Reade } modifiedInfos := []*resource.Info{} + updateErrors := []string{} - modified.Visit(func(info *resource.Info, err error) error { + err = modified.Visit(func(info *resource.Info, err error) error { modifiedInfos = append(modifiedInfos, info) if err != nil { return err @@ -188,15 +190,23 @@ func (c *Client) Update(namespace string, currentReader, modifiedReader io.Reade if err := updateResource(info, currentObj); err != nil { log.Printf("error updating the resource %s:\n\t %v", resourceName, err) - return err + updateErrors = append(updateErrors, err.Error()) } - return err + return nil }) deleteUnwantedResources(currentInfos, modifiedInfos) + if err != nil { + return err + } else if len(updateErrors) != 0 { + return fmt.Errorf(strings.Join(updateErrors, " && ")) + + } + return nil + } // Delete deletes kubernetes resources from an io.reader From c7bec344541eebc0b270c2482c6172ad112baad4 Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Thu, 25 Aug 2016 09:32:16 -0600 Subject: [PATCH 034/183] chore(kube): make update logic more generic --- pkg/kube/client.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 529d2b4c4..1dd949663 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -129,13 +129,13 @@ func (c *Client) Get(namespace string, reader io.Reader) (string, error) { return buf.String(), err } -// Update reads in the current configuration and a modified configuration from io.reader +// Update reads in the current configuration and a target 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 +// in the target configuration and deletes resources from the current configuration that are +// not present in the target configuration // // Namespace will set the namespaces -func (c *Client) Update(namespace string, currentReader, modifiedReader io.Reader) error { +func (c *Client) Update(namespace string, currentReader, targetReader io.Reader) error { current := c.NewBuilder(includeThirdPartyAPIs). ContinueOnError(). NamespaceParam(namespace). @@ -144,11 +144,11 @@ func (c *Client) Update(namespace string, currentReader, modifiedReader io.Reade Flatten(). Do() - modified := c.NewBuilder(includeThirdPartyAPIs). + target := c.NewBuilder(includeThirdPartyAPIs). ContinueOnError(). NamespaceParam(namespace). DefaultNamespace(). - Stream(modifiedReader, ""). + Stream(targetReader, ""). Flatten(). Do() @@ -157,11 +157,11 @@ func (c *Client) Update(namespace string, currentReader, modifiedReader io.Reade return err } - modifiedInfos := []*resource.Info{} + targetInfos := []*resource.Info{} updateErrors := []string{} - err = modified.Visit(func(info *resource.Info, err error) error { - modifiedInfos = append(modifiedInfos, info) + err = target.Visit(func(info *resource.Info, err error) error { + targetInfos = append(targetInfos, info) if err != nil { return err } @@ -196,7 +196,7 @@ func (c *Client) Update(namespace string, currentReader, modifiedReader io.Reade return nil }) - deleteUnwantedResources(currentInfos, modifiedInfos) + deleteUnwantedResources(currentInfos, targetInfos) if err != nil { return err @@ -292,7 +292,7 @@ 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 { +func updateResource(target *resource.Info, currentObj runtime.Object) error { encoder := api.Codecs.LegacyCodec(registered.EnabledVersions()...) originalSerialization, err := runtime.Encode(encoder, currentObj) @@ -300,7 +300,7 @@ func updateResource(modified *resource.Info, currentObj runtime.Object) error { return err } - editedSerialization, err := runtime.Encode(encoder, modified.Object) + editedSerialization, err := runtime.Encode(encoder, target.Object) if err != nil { return err } @@ -316,7 +316,7 @@ func updateResource(modified *resource.Info, currentObj runtime.Object) error { } if reflect.DeepEqual(originalJS, editedJS) { - return fmt.Errorf("Looks like there are no changes for %s", modified.Name) + return fmt.Errorf("Looks like there are no changes for %s", target.Name) } patch, err := strategicpatch.CreateStrategicMergePatch(originalJS, editedJS, currentObj) @@ -325,8 +325,8 @@ func updateResource(modified *resource.Info, currentObj runtime.Object) error { } // send patch to server - helper := resource.NewHelper(modified.Client, modified.Mapping) - if _, err = helper.Patch(modified.Namespace, modified.Name, api.StrategicMergePatchType, patch); err != nil { + helper := resource.NewHelper(target.Client, target.Mapping) + if _, err = helper.Patch(target.Namespace, target.Name, api.StrategicMergePatchType, patch); err != nil { return err } @@ -411,10 +411,10 @@ func (c *Client) ensureNamespace(namespace string) error { return nil } -func deleteUnwantedResources(currentInfos, modifiedInfos []*resource.Info) { +func deleteUnwantedResources(currentInfos, targetInfos []*resource.Info) { for _, cInfo := range currentInfos { found := false - for _, m := range modifiedInfos { + for _, m := range targetInfos { if m.Name == cInfo.Name { found = true } From 50d8d36d8bde4301d4c304bde9f4e65ea4b03aec Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Thu, 25 Aug 2016 12:30:02 -0600 Subject: [PATCH 035/183] ref(kube): delete skips IsNotFound errs Currently, delete returns an error if a resource is not found. That is a bit confusing because if you delete something that is not found, the end state is not really an error. The end state is what you wanted to do in the first place. This type of error is not propogated from the kube client but it is logged in tiller. We could also change this to keep returning errors (even the not found) and filter it out in the release server. --- pkg/kube/client.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 1dd949663..7055ec074 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -221,10 +221,16 @@ func (c *Client) Delete(namespace string, reader io.Reader) error { if kubectl.IsNoSuchReaperError(err) { return resource.NewHelper(info.Client, info.Mapping).Delete(info.Namespace, info.Name) } + return err } log.Printf("Using reaper for deleting %s", info.Name) - return reaper.Stop(info.Namespace, info.Name, 0, nil) + err = reaper.Stop(info.Namespace, info.Name, 0, nil) + if err != nil && errors.IsNotFound(err) { + log.Printf("%v", err) + return nil + } + return err }) } From f3022a0909823be013dad8beafa26a7347a0ae5c Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Thu, 25 Aug 2016 14:36:27 -0600 Subject: [PATCH 036/183] feat(helm): allow multiple args for fetch, package, delete This allows the following commands to accept more than one argument on the CLI: - helm fetch - helm package - helm delete Closes #1100 --- cmd/helm/delete.go | 12 +++++++++--- cmd/helm/fetch.go | 11 ++++++++--- cmd/helm/package.go | 11 ++++++++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/cmd/helm/delete.go b/cmd/helm/delete.go index 2a8fd02ff..bd0ffc919 100644 --- a/cmd/helm/delete.go +++ b/cmd/helm/delete.go @@ -50,7 +50,7 @@ func newDeleteCmd(c helm.Interface, out io.Writer) *cobra.Command { } cmd := &cobra.Command{ - Use: "delete [flags] RELEASE_NAME", + Use: "delete [flags] RELEASE_NAME [...]", Aliases: []string{"del"}, SuggestFor: []string{"remove", "rm"}, Short: "given a release name, delete the release from Kubernetes", @@ -60,9 +60,15 @@ func newDeleteCmd(c helm.Interface, out io.Writer) *cobra.Command { if len(args) == 0 { return errors.New("command 'delete' requires a release name") } - del.name = args[0] del.client = ensureHelmClient(del.client) - return del.run() + + for i := 0; i < len(args); i++ { + del.name = args[i] + if err := del.run(); err != nil { + return err + } + } + return nil }, } f := cmd.Flags() diff --git a/cmd/helm/fetch.go b/cmd/helm/fetch.go index cb077d7ce..395fef175 100644 --- a/cmd/helm/fetch.go +++ b/cmd/helm/fetch.go @@ -64,15 +64,20 @@ func newFetchCmd(out io.Writer) *cobra.Command { fch := &fetchCmd{out: out} cmd := &cobra.Command{ - Use: "fetch [chart URL | repo/chartname]", + Use: "fetch [flags] [chart URL | repo/chartname] [...]", Short: "download a chart from a repository and (optionally) unpack it in local directory", Long: fetchDesc, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { return fmt.Errorf("This command needs at least one argument, url or repo/name of the chart.") } - fch.chartRef = args[0] - return fch.run() + for i := 0; i < len(args); i++ { + fch.chartRef = args[i] + if err := fch.run(); err != nil { + return err + } + } + return nil }, } diff --git a/cmd/helm/package.go b/cmd/helm/package.go index 056fd39cb..fac2a2fef 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -57,14 +57,13 @@ func newPackageCmd(client helm.Interface, out io.Writer) *cobra.Command { out: out, } cmd := &cobra.Command{ - Use: "package [CHART_PATH]", + Use: "package [flags] [CHART_PATH] [...]", Short: "package a chart directory into a chart archive", Long: packageDesc, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { return fmt.Errorf("This command needs at least one argument, the path to the chart.") } - pkg.path = args[0] if pkg.sign { if pkg.key == "" { return errors.New("--key is required for signing a package") @@ -73,7 +72,13 @@ func newPackageCmd(client helm.Interface, out io.Writer) *cobra.Command { return errors.New("--keyring is required for signing a package") } } - return pkg.run(cmd, args) + for i := 0; i < len(args); i++ { + pkg.path = args[i] + if err := pkg.run(cmd, args); err != nil { + return err + } + } + return nil }, } From 1be28d6f29512c0a7b652e25529840ac5e1f7b6b Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Thu, 25 Aug 2016 12:35:18 -0600 Subject: [PATCH 037/183] feat(*): add 'helm list --all' and related flags This adds support for the following 'helm list' flags: - all: show all release types - deleted: show deleted releases - deployed: show deployed releases - failed: show failed releases These flags can be toggled. Only '--deployed' is turned on by default. On the server side, Tiller's list function can now filter based on a slice of release.Status_Code filters. While the client only supports a subset, the server supports all known release.Status_Code types. Closes #973 --- _proto/hapi/services/tiller.proto | 4 + cmd/helm/helm_test.go | 14 +++- cmd/helm/list.go | 69 ++++++++++++++--- cmd/helm/list_test.go | 22 +++++- cmd/tiller/release_server.go | 15 +++- cmd/tiller/release_server_test.go | 73 ++++++++++++++++- pkg/helm/option.go | 11 +++ pkg/proto/hapi/services/tiller.pb.go | 112 ++++++++++++++------------- 8 files changed, 250 insertions(+), 70 deletions(-) diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto index db791ea63..549f128f6 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -20,6 +20,7 @@ import "hapi/chart/chart.proto"; import "hapi/chart/config.proto"; import "hapi/release/release.proto"; import "hapi/release/info.proto"; +import "hapi/release/status.proto"; option go_package = "services"; @@ -90,7 +91,10 @@ message ListReleasesRequest { // Anything that matches the regexp will be included in the results. string filter = 4; + // SortOrder is the ordering directive used for sorting. ListSort.SortOrder sort_order = 5; + + repeated hapi.release.Status.Code status_codes = 6; } // ListSort defines sorting fields on a release list. diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 5af98960d..9c62fe9dd 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -46,9 +46,10 @@ metadata: ` type releaseOptions struct { - name string - version int32 - chart *chart.Chart + name string + version int32 + chart *chart.Chart + statusCode release.Status_Code } func releaseMock(opts *releaseOptions) *release.Release { @@ -77,12 +78,17 @@ func releaseMock(opts *releaseOptions) *release.Release { } } + scode := release.Status_DEPLOYED + if opts.statusCode > 0 { + scode = opts.statusCode + } + return &release.Release{ Name: name, Info: &release.Info{ FirstDeployed: &date, LastDeployed: &date, - Status: &release.Status{Code: release.Status_DEPLOYED}, + Status: &release.Status{Code: scode}, }, Chart: ch, Config: &chart.Config{Raw: `name: "value"`}, diff --git a/cmd/helm/list.go b/cmd/helm/list.go index 058932766..64d7ff908 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -31,7 +31,10 @@ import ( ) var listHelp = ` -This command lists all of the currently deployed releases. +This command lists all of the releases. + +By default, it lists only releases that are deployed. Flags like '--delete' and +'--all' will alter this behavior. Such flags can be combined: '--deleted --failed'. By default, items are sorted alphabetically. Use the '-d' flag to sort by release date. @@ -54,14 +57,19 @@ flag with the '--offset' flag allows you to page through results. ` type listCmd struct { - filter string - long bool - limit int - offset string - byDate bool - sortDesc bool - out io.Writer - client helm.Interface + filter string + long bool + limit int + offset string + byDate bool + sortDesc bool + out io.Writer + all bool + deleted bool + deployed bool + failed bool + superseded bool + client helm.Interface } func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { @@ -91,6 +99,12 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { f.BoolVarP(&list.sortDesc, "reverse", "r", false, "reverse the sort order") f.IntVarP(&list.limit, "max", "m", 256, "maximum number of releases to fetch") f.StringVarP(&list.offset, "offset", "o", "", "the next release name in the list, used to offset from start value") + f.BoolVar(&list.all, "all", false, "show all releases, not just the ones marked DEPLOYED") + f.BoolVar(&list.deleted, "deleted", false, "show deleted releases") + f.BoolVar(&list.deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled") + f.BoolVar(&list.failed, "failed", false, "show failed releases") + // TODO: Do we want this as a feature of 'helm list'? + //f.BoolVar(&list.superseded, "history", true, "show historical releases") return cmd } @@ -105,12 +119,15 @@ func (l *listCmd) run() error { sortOrder = services.ListSort_DESC } + stats := l.statusCodes() + res, err := l.client.ListReleases( helm.ReleaseListLimit(l.limit), helm.ReleaseListOffset(l.offset), helm.ReleaseListFilter(l.filter), helm.ReleaseListSort(int32(sortBy)), helm.ReleaseListOrder(int32(sortOrder)), + helm.ReleaseListStatuses(stats), ) if err != nil { @@ -138,6 +155,40 @@ func (l *listCmd) run() error { return nil } +// statusCodes gets the list of status codes that are to be included in the results. +func (l *listCmd) statusCodes() []release.Status_Code { + if l.all { + return []release.Status_Code{ + release.Status_UNKNOWN, + release.Status_DEPLOYED, + release.Status_DELETED, + // TODO: Should we return superseded records? These are records + // that were replaced by an upgrade. + //release.Status_SUPERSEDED, + release.Status_FAILED, + } + } + status := []release.Status_Code{} + if l.deployed { + status = append(status, release.Status_DEPLOYED) + } + if l.deleted { + status = append(status, release.Status_DELETED) + } + if l.failed { + status = append(status, release.Status_FAILED) + } + if l.superseded { + status = append(status, release.Status_SUPERSEDED) + } + + // Default case. + if len(status) == 0 { + status = append(status, release.Status_DEPLOYED) + } + return status +} + func formatList(rels []*release.Release) string { table := uitable.New() table.MaxColWidth = 30 diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index b12d41408..c2ea487df 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -41,13 +41,33 @@ func TestListCmd(t *testing.T) { }, { name: "list --long", - //flags: map[string]string{"long": "1"}, args: []string{"--long"}, resp: []*release.Release{ releaseMock(&releaseOptions{name: "atlas"}), }, expected: "NAME \tVERSION\tUPDATED \tSTATUS \tCHART \natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\n", }, + { + name: "with a release, multiple flags", + args: []string{"--deleted", "--deployed", "--failed"}, + resp: []*release.Release{ + releaseMock(&releaseOptions{name: "thomas-guide", statusCode: release.Status_DELETED}), + releaseMock(&releaseOptions{name: "atlas-guide", statusCode: release.Status_DEPLOYED}), + }, + // Note: We're really only testing that the flags parsed correctly. Which results are returned + // depends on the backend. And until pkg/helm is done, we can't mock this. + expected: "thomas-guide\natlas-guide", + }, + { + name: "with a release, multiple flags", + args: []string{"--all"}, + resp: []*release.Release{ + releaseMock(&releaseOptions{name: "thomas-guide", statusCode: release.Status_DELETED}), + releaseMock(&releaseOptions{name: "atlas-guide", statusCode: release.Status_DEPLOYED}), + }, + // See note on previous test. + expected: "thomas-guide\natlas-guide", + }, } var buf bytes.Buffer diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 69a575a3e..b513f9043 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -68,7 +68,20 @@ type releaseServer struct { } func (s *releaseServer) ListReleases(req *services.ListReleasesRequest, stream services.ReleaseService_ListReleasesServer) error { - rels, err := s.env.Releases.ListDeployed() + + if len(req.StatusCodes) == 0 { + req.StatusCodes = []release.Status_Code{release.Status_DEPLOYED} + } + + //rels, err := s.env.Releases.ListDeployed() + rels, err := s.env.Releases.ListFilterAll(func(r *release.Release) bool { + for _, sc := range req.StatusCodes { + if sc == r.Info.Status.Code { + return true + } + } + return false + }) if err != nil { return err } diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index cfb963e9d..3b86c2232 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -82,13 +82,17 @@ func chartStub() *chart.Chart { // releaseStub creates a release stub, complete with the chartStub as its chart. func releaseStub() *release.Release { + return namedReleaseStub("angry-panda", release.Status_DEPLOYED) +} + +func namedReleaseStub(name string, status release.Status_Code) *release.Release { date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0} return &release.Release{ - Name: "angry-panda", + Name: name, Info: &release.Info{ FirstDeployed: &date, LastDeployed: &date, - Status: &release.Status{Code: release.Status_DEPLOYED}, + Status: &release.Status{Code: status}, }, Chart: chartStub(), Config: &chart.Config{Raw: `name = "value"`}, @@ -581,6 +585,71 @@ func TestListReleases(t *testing.T) { } } +func TestListReleasesByStatus(t *testing.T) { + rs := rsFixture() + stubs := []*release.Release{ + namedReleaseStub("kamal", release.Status_DEPLOYED), + namedReleaseStub("astrolabe", release.Status_DELETED), + namedReleaseStub("octant", release.Status_FAILED), + namedReleaseStub("sextant", release.Status_UNKNOWN), + } + for _, stub := range stubs { + if err := rs.env.Releases.Create(stub); err != nil { + t.Fatalf("Could not create stub: %s", err) + } + } + + tests := []struct { + statusCodes []release.Status_Code + names []string + }{ + { + names: []string{"kamal"}, + statusCodes: []release.Status_Code{release.Status_DEPLOYED}, + }, + { + names: []string{"astrolabe"}, + statusCodes: []release.Status_Code{release.Status_DELETED}, + }, + { + names: []string{"kamal", "octant"}, + statusCodes: []release.Status_Code{release.Status_DEPLOYED, release.Status_FAILED}, + }, + { + names: []string{"kamal", "astrolabe", "octant", "sextant"}, + statusCodes: []release.Status_Code{ + release.Status_DEPLOYED, + release.Status_DELETED, + release.Status_FAILED, + release.Status_UNKNOWN, + }, + }, + } + + for i, tt := range tests { + mrs := &mockListServer{} + if err := rs.ListReleases(&services.ListReleasesRequest{StatusCodes: tt.statusCodes, Offset: "", Limit: 64}, mrs); err != nil { + t.Fatalf("Failed listing %d: %s", i, err) + } + + if len(tt.names) != len(mrs.val.Releases) { + t.Fatalf("Expected %d releases, got %d", len(tt.names), len(mrs.val.Releases)) + } + + for _, name := range tt.names { + found := false + for _, rel := range mrs.val.Releases { + if rel.Name == name { + found = true + } + } + if !found { + t.Errorf("%d: Did not find name %q", i, name) + } + } + } +} + func TestListReleasesSort(t *testing.T) { rs := rsFixture() diff --git a/pkg/helm/option.go b/pkg/helm/option.go index 73a478574..dff6acbd3 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -19,6 +19,7 @@ package helm import ( "golang.org/x/net/context" cpb "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/proto/hapi/release" rls "k8s.io/helm/pkg/proto/hapi/services" ) @@ -105,6 +106,16 @@ func ReleaseListSort(sort int32) ReleaseListOption { } } +// ReleaseListStatuses specifies which status codes should be returned. +func ReleaseListStatuses(statuses []release.Status_Code) ReleaseListOption { + return func(opts *options) { + if len(statuses) == 0 { + statuses = []release.Status_Code{release.Status_DEPLOYED} + } + opts.listReq.StatusCodes = statuses + } +} + // InstallOption allows specifying various settings // configurable by the helm client user for overriding // the defaults used when running the `helm install` command. diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index 5d93ceeb4..4b497fdd7 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -32,6 +32,7 @@ import hapi_chart3 "k8s.io/helm/pkg/proto/hapi/chart" import hapi_chart "k8s.io/helm/pkg/proto/hapi/chart" import hapi_release3 "k8s.io/helm/pkg/proto/hapi/release" import hapi_release2 "k8s.io/helm/pkg/proto/hapi/release" +import hapi_release1 "k8s.io/helm/pkg/proto/hapi/release" import ( context "golang.org/x/net/context" @@ -113,8 +114,10 @@ type ListReleasesRequest struct { // Filter is a regular expression used to filter which releases should be listed. // // Anything that matches the regexp will be included in the results. - Filter string `protobuf:"bytes,4,opt,name=filter" json:"filter,omitempty"` - SortOrder ListSort_SortOrder `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,enum=hapi.services.tiller.ListSort_SortOrder" json:"sort_order,omitempty"` + Filter string `protobuf:"bytes,4,opt,name=filter" json:"filter,omitempty"` + // SortOrder is the ordering directive used for sorting. + SortOrder ListSort_SortOrder `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,enum=hapi.services.tiller.ListSort_SortOrder" json:"sort_order,omitempty"` + StatusCodes []hapi_release1.Status_Code `protobuf:"varint,6,rep,name=status_codes,json=statusCodes,enum=hapi.release.Status_Code" json:"status_codes,omitempty"` } func (m *ListReleasesRequest) Reset() { *m = ListReleasesRequest{} } @@ -330,7 +333,7 @@ type UninstallReleaseRequest struct { Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` // DisableHooks causes the server to skip running any hooks for the uninstall. DisableHooks bool `protobuf:"varint,2,opt,name=disable_hooks,json=disableHooks" json:"disable_hooks,omitempty"` - // Remove the release from the store and make its name free for later use. + // Purge removes the release from the store and make its name free for later use. Purge bool `protobuf:"varint,3,opt,name=purge" json:"purge,omitempty"` } @@ -628,54 +631,57 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ } var fileDescriptor0 = []byte{ - // 783 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xcb, 0x6e, 0xd3, 0x4c, - 0x14, 0xae, 0x93, 0x34, 0x97, 0xd3, 0x8b, 0xd2, 0xf9, 0xdb, 0x26, 0xbf, 0x05, 0x08, 0x19, 0x01, - 0xa5, 0x50, 0x07, 0xc2, 0x1e, 0x29, 0x6d, 0xa3, 0xb6, 0x6a, 0x48, 0x25, 0x87, 0x82, 0xc4, 0x82, - 0xc8, 0x4d, 0x26, 0x8d, 0xc1, 0xb5, 0x83, 0x67, 0x52, 0xd1, 0x47, 0xe0, 0x8d, 0xd8, 0xf0, 0x36, - 0xbc, 0x05, 0x1b, 0xe6, 0xe2, 0x31, 0x71, 0x62, 0x83, 0xe9, 0xc6, 0x99, 0x33, 0xe7, 0x9b, 0x73, - 0xf9, 0xce, 0xa5, 0x05, 0x7d, 0x6c, 0x4f, 0x9c, 0x06, 0xc1, 0xc1, 0xb5, 0x33, 0xc0, 0xa4, 0x41, - 0x1d, 0xd7, 0xc5, 0x81, 0x39, 0x09, 0x7c, 0xea, 0xa3, 0x4d, 0xae, 0x33, 0x95, 0xce, 0x94, 0x3a, - 0x7d, 0x5b, 0xbc, 0x18, 0x8c, 0xed, 0x80, 0xca, 0xaf, 0x44, 0xeb, 0xb5, 0xd9, 0x7b, 0xdf, 0x1b, - 0x39, 0x97, 0xa1, 0x42, 0xba, 0x08, 0xb0, 0x8b, 0x6d, 0x82, 0xd5, 0x6f, 0xec, 0x91, 0xd2, 0x39, - 0xde, 0xc8, 0x97, 0x0a, 0xe3, 0x87, 0x06, 0xff, 0x75, 0x1c, 0x42, 0x2d, 0xa9, 0x22, 0x16, 0xfe, - 0x3c, 0xc5, 0x84, 0xa2, 0x4d, 0x58, 0x76, 0x9d, 0x2b, 0x87, 0xd6, 0xb5, 0xfb, 0xda, 0x4e, 0xde, - 0x92, 0x02, 0xda, 0x86, 0xa2, 0x3f, 0x1a, 0x11, 0x4c, 0xeb, 0x39, 0x76, 0x5d, 0xb1, 0x42, 0x09, - 0xbd, 0x82, 0x12, 0xf1, 0x03, 0xda, 0xbf, 0xb8, 0xa9, 0xe7, 0x99, 0x62, 0xbd, 0xf9, 0xd0, 0x4c, - 0xca, 0xc9, 0xe4, 0x9e, 0x7a, 0x0c, 0x68, 0xf2, 0xcf, 0xfe, 0x8d, 0x55, 0x24, 0xe2, 0x97, 0xdb, - 0x1d, 0x39, 0x2e, 0xc5, 0x41, 0xbd, 0x20, 0xed, 0x4a, 0x09, 0x1d, 0x01, 0x08, 0xbb, 0x7e, 0x30, - 0x64, 0xba, 0x65, 0x61, 0x7a, 0x27, 0x83, 0xe9, 0x33, 0x8e, 0xb7, 0x2a, 0x44, 0x1d, 0x8d, 0x0f, - 0x50, 0x56, 0x00, 0xa3, 0x09, 0x45, 0xe9, 0x1e, 0xad, 0x40, 0xe9, 0xbc, 0x7b, 0xda, 0x3d, 0x7b, - 0xd7, 0xad, 0x2e, 0xa1, 0x32, 0x14, 0xba, 0xad, 0xd7, 0xed, 0xaa, 0x86, 0x36, 0x60, 0xad, 0xd3, - 0xea, 0xbd, 0xe9, 0x5b, 0xed, 0x4e, 0xbb, 0xd5, 0x6b, 0x1f, 0x56, 0x73, 0xc6, 0x3d, 0xa8, 0x44, - 0x76, 0x51, 0x09, 0xf2, 0xad, 0xde, 0x81, 0x7c, 0x72, 0xd8, 0x66, 0x27, 0xcd, 0xf8, 0xaa, 0xc1, - 0x66, 0x9c, 0x46, 0x32, 0xf1, 0x3d, 0x82, 0x39, 0x8f, 0x03, 0x7f, 0xea, 0x45, 0x3c, 0x0a, 0x01, - 0x21, 0x28, 0x78, 0xf8, 0x8b, 0x62, 0x51, 0x9c, 0x39, 0x92, 0xfa, 0xd4, 0x76, 0x05, 0x83, 0x0c, - 0x29, 0x04, 0xf4, 0x02, 0xca, 0x61, 0xd5, 0x08, 0xe3, 0x26, 0xbf, 0xb3, 0xd2, 0xdc, 0x92, 0xf9, - 0xab, 0xfa, 0x86, 0x1e, 0xad, 0x08, 0x66, 0xec, 0x41, 0xed, 0x08, 0xab, 0x48, 0x7a, 0xd4, 0xa6, - 0xd3, 0xa8, 0xaa, 0xdc, 0xaf, 0x7d, 0x85, 0x45, 0x30, 0xdc, 0x2f, 0x3b, 0x1b, 0x6f, 0xa1, 0xbe, - 0x08, 0x0f, 0xa3, 0x4f, 0xc0, 0xa3, 0x47, 0x50, 0xe0, 0xfd, 0x23, 0x62, 0x5f, 0x69, 0xa2, 0x78, - 0x34, 0x27, 0x4c, 0x63, 0x09, 0xbd, 0x61, 0xce, 0xda, 0x3d, 0xf0, 0x3d, 0x8a, 0x3d, 0xfa, 0xa7, - 0x38, 0x3a, 0xf0, 0x7f, 0x02, 0x3e, 0x0c, 0xa4, 0x01, 0xa5, 0xd0, 0x85, 0x78, 0x93, 0xca, 0x82, - 0x42, 0x19, 0xdf, 0x59, 0x41, 0xce, 0x27, 0x43, 0x9b, 0x62, 0xa5, 0x4a, 0x77, 0x8d, 0x1e, 0xb3, - 0x22, 0xf1, 0x79, 0x0a, 0x73, 0xda, 0x90, 0xb6, 0xe5, 0xd0, 0x1d, 0xf0, 0xaf, 0x25, 0xf5, 0x68, - 0x17, 0x8a, 0xd7, 0xb6, 0xcb, 0xec, 0x88, 0x22, 0x45, 0xd9, 0x87, 0x48, 0x31, 0x8c, 0x56, 0x88, - 0x40, 0x35, 0x28, 0x0d, 0x83, 0x9b, 0x7e, 0x30, 0xf5, 0x44, 0x53, 0x97, 0xad, 0x22, 0x13, 0xad, - 0xa9, 0x87, 0x1e, 0xc0, 0xda, 0xd0, 0x21, 0xf6, 0x85, 0x8b, 0xfb, 0x63, 0xdf, 0xff, 0x44, 0x44, - 0x5f, 0x97, 0xad, 0xd5, 0xf0, 0xf2, 0x98, 0xdf, 0x19, 0xc7, 0xb0, 0x35, 0x17, 0xfe, 0x6d, 0x99, - 0xf8, 0xa9, 0xc1, 0xd6, 0x89, 0x47, 0x58, 0x33, 0xb9, 0x73, 0x54, 0x44, 0x69, 0x6b, 0x99, 0xd3, - 0xce, 0xfd, 0x4b, 0xda, 0xf9, 0x58, 0xda, 0x8a, 0xf8, 0xc2, 0x0c, 0xf1, 0x59, 0xa8, 0x40, 0x77, - 0xa0, 0xc2, 0xc1, 0x64, 0x62, 0x0f, 0x70, 0xbd, 0x28, 0x5e, 0xff, 0xbe, 0x40, 0x77, 0x01, 0x02, - 0x3c, 0x25, 0xb8, 0x2f, 0x8c, 0x97, 0xc4, 0xfb, 0x8a, 0xb8, 0xe9, 0xf2, 0xae, 0x3a, 0x81, 0xed, - 0xf9, 0xe4, 0x6f, 0x4b, 0xe4, 0x18, 0x6a, 0xe7, 0x9e, 0x93, 0xc8, 0x64, 0x52, 0x53, 0x2d, 0xe4, - 0x96, 0x4b, 0xc8, 0x8d, 0x0d, 0xfd, 0x64, 0x1a, 0x5c, 0xe2, 0x90, 0x2b, 0x29, 0x18, 0xa7, 0x50, - 0x5f, 0xf4, 0x74, 0xcb, 0xb0, 0x9b, 0xdf, 0x96, 0x61, 0x5d, 0x4d, 0xb7, 0xdc, 0x99, 0xc8, 0x81, - 0xd5, 0xd9, 0x65, 0x85, 0x9e, 0xa4, 0xaf, 0xd4, 0xb9, 0xbf, 0x0b, 0xfa, 0x6e, 0x16, 0xa8, 0x0c, - 0xd5, 0x58, 0x7a, 0xae, 0x21, 0x02, 0xd5, 0xf9, 0xed, 0x82, 0xf6, 0x92, 0x6d, 0xa4, 0x2c, 0x2d, - 0xdd, 0xcc, 0x0a, 0x57, 0x6e, 0xd1, 0x35, 0x6c, 0x2c, 0xac, 0x12, 0xf4, 0x57, 0x33, 0xf1, 0x1d, - 0xa5, 0x37, 0x32, 0xe3, 0x23, 0xbf, 0x1f, 0x61, 0x2d, 0x36, 0xb4, 0x28, 0x85, 0xad, 0xa4, 0xc5, - 0xa4, 0x3f, 0xcd, 0x84, 0x8d, 0x7c, 0x5d, 0xc1, 0x7a, 0xbc, 0xb1, 0x51, 0x8a, 0x81, 0xc4, 0xd9, - 0xd7, 0x9f, 0x65, 0x03, 0x47, 0xee, 0x58, 0x1d, 0xe7, 0x5b, 0x32, 0xad, 0x8e, 0x29, 0x43, 0x92, - 0x56, 0xc7, 0xb4, 0x4e, 0x37, 0x96, 0xf6, 0xe1, 0x7d, 0x59, 0xa1, 0x2f, 0x8a, 0xe2, 0xff, 0x95, - 0x97, 0xbf, 0x02, 0x00, 0x00, 0xff, 0xff, 0x2c, 0x46, 0xba, 0xf9, 0x49, 0x09, 0x00, 0x00, + // 821 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdd, 0x52, 0xd3, 0x40, + 0x14, 0x26, 0x6d, 0xe9, 0xcf, 0x29, 0x30, 0x65, 0x05, 0x5a, 0x32, 0xea, 0x30, 0x71, 0x54, 0x44, + 0x49, 0xb5, 0xde, 0x3a, 0xce, 0x94, 0xd2, 0x01, 0x86, 0x5a, 0x66, 0x52, 0xd1, 0x19, 0x2f, 0xec, + 0x84, 0x76, 0x4b, 0xa3, 0x21, 0xa9, 0xd9, 0x2d, 0x23, 0x8f, 0xe0, 0x6b, 0xf8, 0x14, 0xde, 0xf8, + 0x64, 0xde, 0xb8, 0x3f, 0x49, 0x6c, 0xda, 0x44, 0x23, 0x37, 0xe9, 0xee, 0x9e, 0x6f, 0xbf, 0x73, + 0xce, 0x77, 0xf6, 0x1c, 0x00, 0x75, 0x6c, 0x4e, 0xac, 0x3a, 0xc1, 0xde, 0xb5, 0x35, 0xc0, 0xa4, + 0x4e, 0x2d, 0xdb, 0xc6, 0x9e, 0x3e, 0xf1, 0x5c, 0xea, 0xa2, 0x0d, 0x6e, 0xd3, 0x03, 0x9b, 0x2e, + 0x6d, 0xea, 0x96, 0xb8, 0x31, 0x18, 0x9b, 0x1e, 0x95, 0x5f, 0x89, 0x56, 0xab, 0xb3, 0xe7, 0xae, + 0x33, 0xb2, 0x2e, 0x7d, 0x83, 0x74, 0xe1, 0x61, 0x1b, 0x9b, 0x04, 0x07, 0xbf, 0x91, 0x4b, 0x81, + 0xcd, 0x72, 0x46, 0xae, 0x6f, 0xd8, 0x8e, 0x18, 0x08, 0x35, 0xe9, 0x94, 0x48, 0x93, 0xf6, 0x3d, + 0x03, 0x77, 0x3a, 0x16, 0xa1, 0x86, 0x34, 0x12, 0x03, 0x7f, 0x99, 0x62, 0x42, 0xd1, 0x06, 0x2c, + 0xdb, 0xd6, 0x95, 0x45, 0x6b, 0xca, 0x8e, 0xb2, 0x9b, 0x35, 0xe4, 0x06, 0x6d, 0x41, 0xde, 0x1d, + 0x8d, 0x08, 0xa6, 0xb5, 0x0c, 0x3b, 0x2e, 0x19, 0xfe, 0x0e, 0xbd, 0x86, 0x02, 0x71, 0x3d, 0xda, + 0xbf, 0xb8, 0xa9, 0x65, 0x99, 0x61, 0xad, 0xf1, 0x50, 0x8f, 0x4b, 0x57, 0xe7, 0x9e, 0x7a, 0x0c, + 0xa8, 0xf3, 0xcf, 0xc1, 0x8d, 0x91, 0x27, 0xe2, 0x97, 0xf3, 0x8e, 0x2c, 0x9b, 0x62, 0xaf, 0x96, + 0x93, 0xbc, 0x72, 0x87, 0x8e, 0x00, 0x04, 0xaf, 0xeb, 0x0d, 0x99, 0x6d, 0x59, 0x50, 0xef, 0xa6, + 0xa0, 0x3e, 0xe3, 0x78, 0xa3, 0x44, 0x82, 0x25, 0x7a, 0x05, 0x2b, 0x32, 0xed, 0xfe, 0xc0, 0x1d, + 0x62, 0x52, 0xcb, 0xef, 0x64, 0x19, 0xd5, 0xb6, 0xa4, 0x0a, 0x54, 0xec, 0x49, 0x61, 0x5a, 0x0c, + 0x61, 0x94, 0x25, 0x9c, 0xaf, 0x89, 0xf6, 0x11, 0x8a, 0x01, 0xbd, 0xd6, 0x80, 0xbc, 0x0c, 0x1e, + 0x95, 0xa1, 0x70, 0xde, 0x3d, 0xed, 0x9e, 0xbd, 0xef, 0x56, 0x96, 0x50, 0x11, 0x72, 0xdd, 0xe6, + 0x9b, 0x76, 0x45, 0x41, 0xeb, 0xb0, 0xda, 0x69, 0xf6, 0xde, 0xf6, 0x8d, 0x76, 0xa7, 0xdd, 0xec, + 0xb5, 0x0f, 0x2b, 0x19, 0xed, 0x3e, 0x94, 0xc2, 0xa8, 0x50, 0x01, 0xb2, 0xcd, 0x5e, 0x4b, 0x5e, + 0x39, 0x6c, 0xb3, 0x95, 0xa2, 0x7d, 0x53, 0x60, 0x23, 0x5a, 0x04, 0x32, 0x71, 0x1d, 0x82, 0x79, + 0x15, 0x06, 0xee, 0xd4, 0x09, 0xab, 0x20, 0x36, 0x08, 0x41, 0xce, 0xc1, 0x5f, 0x83, 0x1a, 0x88, + 0x35, 0x47, 0x52, 0x97, 0x9a, 0xb6, 0xd0, 0x9f, 0x21, 0xc5, 0x06, 0xbd, 0x80, 0xa2, 0x9f, 0x1c, + 0x61, 0xca, 0x66, 0x77, 0xcb, 0x8d, 0xcd, 0x68, 0xca, 0xbe, 0x47, 0x23, 0x84, 0x69, 0xfb, 0x50, + 0x3d, 0xc2, 0x41, 0x24, 0x52, 0x91, 0xe0, 0x4d, 0x70, 0xbf, 0xe6, 0x15, 0x16, 0xc1, 0x70, 0xbf, + 0x6c, 0xad, 0xbd, 0x83, 0xda, 0x22, 0xdc, 0x8f, 0x3e, 0x06, 0x8f, 0x1e, 0x41, 0x8e, 0x3f, 0x4c, + 0x11, 0x7b, 0xb9, 0x81, 0xa2, 0xd1, 0x9c, 0x30, 0x8b, 0x21, 0xec, 0x9a, 0x3e, 0xcb, 0xdb, 0x72, + 0x1d, 0x8a, 0x1d, 0xfa, 0xb7, 0x38, 0x3a, 0xb0, 0x1d, 0x83, 0xf7, 0x03, 0xa9, 0x43, 0xc1, 0x77, + 0x21, 0xee, 0x24, 0xaa, 0x10, 0xa0, 0xb4, 0x9f, 0xac, 0x20, 0xe7, 0x93, 0xa1, 0x49, 0x71, 0x60, + 0x4a, 0x76, 0x8d, 0x1e, 0xb3, 0x22, 0xf1, 0x46, 0xf5, 0x73, 0x5a, 0x97, 0xdc, 0xb2, 0x9b, 0x5b, + 0xfc, 0x6b, 0x48, 0x3b, 0xda, 0x83, 0xfc, 0xb5, 0x69, 0x33, 0x1e, 0x51, 0xa4, 0x30, 0x7b, 0x1f, + 0x29, 0xba, 0xdc, 0xf0, 0x11, 0xa8, 0x0a, 0x85, 0xa1, 0x77, 0xd3, 0xf7, 0xa6, 0x8e, 0x68, 0x89, + 0xa2, 0x91, 0x67, 0x5b, 0x63, 0xea, 0xa0, 0x07, 0xb0, 0x3a, 0xb4, 0x88, 0x79, 0x61, 0xe3, 0xfe, + 0xd8, 0x75, 0x3f, 0x13, 0xd1, 0x15, 0x45, 0x63, 0xc5, 0x3f, 0x3c, 0xe6, 0x67, 0xda, 0x31, 0x6c, + 0xce, 0x85, 0x7f, 0x5b, 0x25, 0x7e, 0x29, 0xb0, 0x79, 0xe2, 0xb0, 0x66, 0xb0, 0xed, 0x39, 0x29, + 0xc2, 0xb4, 0x95, 0xd4, 0x69, 0x67, 0xfe, 0x27, 0xed, 0x6c, 0x24, 0xed, 0x40, 0xf8, 0xdc, 0x8c, + 0xf0, 0x69, 0xa4, 0x40, 0x77, 0xa1, 0xc4, 0xc1, 0x64, 0x62, 0x0e, 0x30, 0x6b, 0x7b, 0x7e, 0xfb, + 0xcf, 0x01, 0xba, 0x07, 0xe0, 0xe1, 0x29, 0xc1, 0x7d, 0x41, 0x5e, 0x10, 0xf7, 0x4b, 0xe2, 0xa4, + 0xcb, 0x5f, 0xd5, 0x09, 0x6c, 0xcd, 0x27, 0x7f, 0x5b, 0x21, 0xc7, 0x50, 0x3d, 0x77, 0xac, 0x58, + 0x25, 0xe3, 0x1e, 0xd5, 0x42, 0x6e, 0x99, 0x98, 0xdc, 0x58, 0xd3, 0x4f, 0xa6, 0xde, 0x25, 0xf6, + 0xb5, 0x92, 0x1b, 0xed, 0x14, 0x6a, 0x8b, 0x9e, 0x6e, 0x19, 0x76, 0xe3, 0xc7, 0x32, 0xac, 0x05, + 0xdd, 0x2d, 0x27, 0x2e, 0xb2, 0x60, 0x65, 0x76, 0x58, 0xa1, 0x27, 0xc9, 0x03, 0x79, 0xee, 0xaf, + 0x8a, 0xba, 0x97, 0x06, 0x2a, 0x43, 0xd5, 0x96, 0x9e, 0x2b, 0x88, 0x40, 0x65, 0x7e, 0xba, 0xa0, + 0xfd, 0x78, 0x8e, 0x84, 0xa1, 0xa5, 0xea, 0x69, 0xe1, 0x81, 0x5b, 0x74, 0x0d, 0xeb, 0x0b, 0xa3, + 0x04, 0xfd, 0x93, 0x26, 0x3a, 0xa3, 0xd4, 0x7a, 0x6a, 0x7c, 0xe8, 0xf7, 0x13, 0xac, 0x46, 0x9a, + 0x16, 0x25, 0xa8, 0x15, 0x37, 0x98, 0xd4, 0xa7, 0xa9, 0xb0, 0xa1, 0xaf, 0x2b, 0x58, 0x8b, 0x3e, + 0x6c, 0x94, 0x40, 0x10, 0xdb, 0xfb, 0xea, 0xb3, 0x74, 0xe0, 0xd0, 0x1d, 0xab, 0xe3, 0xfc, 0x93, + 0x4c, 0xaa, 0x63, 0x42, 0x93, 0x24, 0xd5, 0x31, 0xe9, 0xa5, 0x6b, 0x4b, 0x07, 0xf0, 0xa1, 0x18, + 0xa0, 0x2f, 0xf2, 0xe2, 0xbf, 0x9d, 0x97, 0xbf, 0x03, 0x00, 0x00, 0xff, 0xff, 0x44, 0xdd, 0xaf, + 0x38, 0xa2, 0x09, 0x00, 0x00, } From 5566e6baec0bb480c9f7962f9400bd72bc9bceb3 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Thu, 25 Aug 2016 15:32:44 -0600 Subject: [PATCH 038/183] ref(pkg/client): move pkg/client to cmd/helm/installer This is a minor refactor to move a leftover from Ancient Helm into the current design. Specifically, the code to install Tiller from the Helm client is now in `cmd/helm/installer`. Closes #1033 --- cmd/helm/init.go | 4 ++-- {pkg/client => cmd/helm/installer}/install.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename {pkg/client => cmd/helm/installer}/install.go (97%) diff --git a/cmd/helm/init.go b/cmd/helm/init.go index 555185cc2..accf65f61 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -25,7 +25,7 @@ import ( "github.com/spf13/cobra" - "k8s.io/helm/pkg/client" + "k8s.io/helm/cmd/helm/installer" ) const initDesc = ` @@ -71,7 +71,7 @@ func (i *initCmd) run() error { } if !i.clientOnly { - if err := client.Install(tillerNamespace, i.image, flagDebug); err != nil { + if err := installer.Install(tillerNamespace, i.image, flagDebug); err != nil { if !strings.Contains(err.Error(), `"tiller-deploy" already exists`) { return fmt.Errorf("error installing: %s", err) } diff --git a/pkg/client/install.go b/cmd/helm/installer/install.go similarity index 97% rename from pkg/client/install.go rename to cmd/helm/installer/install.go index 0b7a3b1fc..8c6b1a0c4 100644 --- a/pkg/client/install.go +++ b/cmd/helm/installer/install.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client // import "k8s.io/helm/pkg/client" +package installer // import "k8s.io/helm/cmd/helm/installer" import ( "bytes" From a3d56852f169c0f8a9efa5e54df7ccea573aa60b Mon Sep 17 00:00:00 2001 From: joe2far Date: Fri, 26 Aug 2016 11:46:26 +0100 Subject: [PATCH 039/183] fix(helm): make repo remove clear cache --- cmd/helm/repo.go | 16 +++++++++++++++- cmd/helm/repo_test.go | 7 +++++++ cmd/helm/structure.go | 4 ++++ cmd/helm/update.go | 3 +-- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/cmd/helm/repo.go b/cmd/helm/repo.go index 73c6b0235..b39e9713a 100644 --- a/cmd/helm/repo.go +++ b/cmd/helm/repo.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "io/ioutil" + "os" "path/filepath" "github.com/gosuri/uitable" @@ -129,7 +130,7 @@ func index(dir, url string) error { } func addRepository(name, url string) error { - if err := repo.DownloadIndexFile(name, url, cacheDirectory(name+"-index.yaml")); err != nil { + if err := repo.DownloadIndexFile(name, url, cacheIndexFile(name)); err != nil { return errors.New("Looks like " + url + " is not a valid chart repository or cannot be reached: " + err.Error()) } @@ -152,6 +153,9 @@ func removeRepoLine(name string) error { if err := ioutil.WriteFile(repositoriesFile(), b, 0666); err != nil { return err } + if err := removeRepoCache(name); err != nil { + return err + } } else { return fmt.Errorf("The repository, %s, does not exist in your repositories list", name) @@ -160,6 +164,16 @@ func removeRepoLine(name string) error { return nil } +func removeRepoCache(name string) error { + if _, err := os.Stat(cacheIndexFile(name)); err == nil { + err = os.Remove(cacheIndexFile(name)) + if err != nil { + return err + } + } + return nil +} + func insertRepoLine(name, url string) error { f, err := repo.LoadRepositoriesFile(repositoriesFile()) if err != nil { diff --git a/cmd/helm/repo_test.go b/cmd/helm/repo_test.go index 2f70484d5..d24f6f594 100644 --- a/cmd/helm/repo_test.go +++ b/cmd/helm/repo_test.go @@ -82,10 +82,17 @@ func TestRepoRemove(t *testing.T) { t.Errorf("%s", err) } + mf, _ := os.Create(cacheIndexFile(testName)) + mf.Close() + if err := removeRepoLine(testName); err != nil { t.Errorf("Error removing %s from repositories", testName) } + if _, err := os.Stat(cacheIndexFile(testName)); err == nil { + t.Errorf("Error cache file was not removed for repository %s", testName) + } + f, err := repo.LoadRepositoriesFile(repositoriesFile()) if err != nil { t.Errorf("%s", err) diff --git a/cmd/helm/structure.go b/cmd/helm/structure.go index fce4838f4..f1d40040a 100644 --- a/cmd/helm/structure.go +++ b/cmd/helm/structure.go @@ -43,6 +43,10 @@ func cacheDirectory(paths ...string) string { return filepath.Join(fragments...) } +func cacheIndexFile(repoName string) string { + return cacheDirectory(repoName + "-index.yaml") +} + func localRepoDirectory(paths ...string) string { fragments := append([]string{repositoryDirectory(), localRepoPath}, paths...) return filepath.Join(fragments...) diff --git a/cmd/helm/update.go b/cmd/helm/update.go index ecd4710da..1cbb56f34 100644 --- a/cmd/helm/update.go +++ b/cmd/helm/update.go @@ -62,8 +62,7 @@ func updateCharts(repos map[string]string, verbose bool) { wg.Add(1) go func(n, u string) { defer wg.Done() - indexFileName := cacheDirectory(n + "-index.yaml") - err := repo.DownloadIndexFile(n, u, indexFileName) + err := repo.DownloadIndexFile(n, u, cacheIndexFile(n)) if err != nil { updateErr := "...Unable to get an update from the " + n + " chart repository" if verbose { From f51c7290468772af2dda0d9e79eeab60e86d5e34 Mon Sep 17 00:00:00 2001 From: Jeremy Brown Date: Fri, 26 Aug 2016 13:04:38 +0100 Subject: [PATCH 040/183] fix for links to docs/examples in the quickstart --- docs/quickstart.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 8df5a629a..b9ecf846a 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -40,7 +40,7 @@ Released smiling-penguin In the example above, the `alpine` chart was released, and the name of our new release is `smiling-penguin`. You can view the details of the chart we just installed by taking a look at the nginx chart in -[docs/examples/alpine/Chart.yaml](docs/examples/alpine/Chart.yaml). +[docs/examples/alpine/Chart.yaml](examples/alpine/Chart.yaml). ## Change a Default Chart Value @@ -53,8 +53,8 @@ happy-panda ``` You can view the chart for this example in -[docs/examples/nginx/Chart.yaml](docs/examples/nginx/Chart.yaml) and the default values in -[docs/examples/nginx/values.yaml](docs/examples/nginx/values.yaml). +[docs/examples/nginx/Chart.yaml](examples/nginx/Chart.yaml) and the default values in +[docs/examples/nginx/values.yaml](examples/nginx/values.yaml). ## Learn About The Release From 4b718274bc4b3905688a06397116ad34ef7780db Mon Sep 17 00:00:00 2001 From: joe2far Date: Fri, 26 Aug 2016 15:24:27 +0100 Subject: [PATCH 041/183] fix(tiller): truncate release name returned from moniker --- cmd/tiller/release_server.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index b513f9043..c0336396f 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -333,8 +333,7 @@ func (s *releaseServer) uniqName(start string, reuse bool) (string, error) { namer := moniker.New() name := namer.NameSep("-") if len(name) > releaseNameMaxLen { - log.Printf("info: Candidate name %q exceeds maximum length %d. Skipping.", name, releaseNameMaxLen) - continue + name = name[:releaseNameMaxLen] } if _, err := s.env.Releases.Get(name); err == driver.ErrReleaseNotFound { return name, nil From 557db8c6af659c5b15fe0db2646a110f2e2df429 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 24 Aug 2016 12:37:26 -0600 Subject: [PATCH 042/183] feat(tiller): verify apiVersions before install --- cmd/tiller/environment/environment.go | 4 ++-- cmd/tiller/hooks.go | 25 +++++++++++++++++++-- cmd/tiller/hooks_test.go | 24 ++++++++++++++++++-- cmd/tiller/release_server.go | 32 ++++++++++++++++++++++++++- cmd/tiller/release_server_test.go | 14 ++++++++++++ 5 files changed, 92 insertions(+), 7 deletions(-) diff --git a/cmd/tiller/environment/environment.go b/cmd/tiller/environment/environment.go index 5e0e81216..72fd510be 100644 --- a/cmd/tiller/environment/environment.go +++ b/cmd/tiller/environment/environment.go @@ -23,7 +23,6 @@ These dependencies are expressed as interfaces so that alternate implementations package environment import ( - "errors" "io" "k8s.io/helm/pkg/chartutil" @@ -33,6 +32,7 @@ import ( "k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage/driver" "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/client/unversioned/testclient" ) // TillerNamespace is the namespace tiller is running in. @@ -150,7 +150,7 @@ type PrintingKubeClient struct { // The printing client does not have access to a Kubernetes client at all. So it // will always return an error if the client is accessed. func (p *PrintingKubeClient) APIClient() (unversioned.Interface, error) { - return nil, errors.New("no API client found") + return testclient.NewSimpleFake(), nil } // Create prints the values of what would be created with a real KubeClient. diff --git a/cmd/tiller/hooks.go b/cmd/tiller/hooks.go index e102699c7..c0fd5be3c 100644 --- a/cmd/tiller/hooks.go +++ b/cmd/tiller/hooks.go @@ -48,6 +48,7 @@ var events = map[string]release.Hook_Event{ } type simpleHead struct { + Version string `json:"apiVersion"` Kind string `json:"kind,omitempty"` Metadata *struct { Name string `json:"name"` @@ -55,7 +56,22 @@ type simpleHead struct { } `json:"metadata,omitempty"` } -// sortHooks takes a map of filename/YAML contents and sorts them into hook types. +type versionSet map[string]struct{} + +func newVersionSet(apiVersions ...string) versionSet { + vs := versionSet{} + for _, v := range apiVersions { + vs[v] = struct{}{} + } + return vs +} + +func (v versionSet) Has(apiVersion string) bool { + _, ok := v[apiVersion] + return ok +} + +// sortManifests takes a map of filename/YAML contents and sorts them into hook types. // // The resulting hooks struct will be populated with all of the generated hooks. // Any file that does not declare one of the hook types will be placed in the @@ -64,6 +80,7 @@ type simpleHead struct { // To determine hook type, this looks for a YAML structure like this: // // kind: SomeKind +// apiVersion: v1 // metadata: // annotations: // helm.sh/hook: pre-install @@ -75,7 +92,7 @@ type simpleHead struct { // // Files that do not parse into the expected format are simply placed into a map and // returned. -func sortHooks(files map[string]string) ([]*release.Hook, map[string]string, error) { +func sortManifests(files map[string]string, apis versionSet) ([]*release.Hook, map[string]string, error) { hs := []*release.Hook{} generic := map[string]string{} @@ -99,6 +116,10 @@ func sortHooks(files map[string]string) ([]*release.Hook, map[string]string, err return hs, generic, e } + if sh.Version != "" && !apis.Has(sh.Version) { + return hs, generic, fmt.Errorf("apiVersion %q in %s is not available", sh.Version, n) + } + if sh.Metadata == nil || sh.Metadata.Annotations == nil || len(sh.Metadata.Annotations) == 0 { generic[n] = c continue diff --git a/cmd/tiller/hooks_test.go b/cmd/tiller/hooks_test.go index 4d054ec90..14287a7e6 100644 --- a/cmd/tiller/hooks_test.go +++ b/cmd/tiller/hooks_test.go @@ -22,7 +22,7 @@ import ( "k8s.io/helm/pkg/proto/hapi/release" ) -func TestSortHooks(t *testing.T) { +func TestSortManifests(t *testing.T) { data := []struct { name string @@ -52,6 +52,7 @@ metadata: kind: "ReplicaSet", hooks: []release.Hook_Event{release.Hook_POST_INSTALL}, manifest: `kind: ReplicaSet +apiVersion: v1beta1 metadata: name: second annotations: @@ -63,6 +64,7 @@ metadata: kind: "ReplicaSet", hooks: []release.Hook_Event{}, manifest: `kind: ReplicaSet +apiVersion: v1beta1 metadata: name: third annotations: @@ -74,6 +76,7 @@ metadata: kind: "Pod", hooks: []release.Hook_Event{}, manifest: `kind: Pod +apiVersion: v1 metadata: name: fourth annotations: @@ -85,6 +88,7 @@ metadata: kind: "ReplicaSet", hooks: []release.Hook_Event{release.Hook_POST_DELETE, release.Hook_POST_INSTALL}, manifest: `kind: ReplicaSet +apiVersion: v1beta1 metadata: name: fifth annotations: @@ -112,7 +116,7 @@ metadata: manifests[o.path] = o.manifest } - hs, generic, err := sortHooks(manifests) + hs, generic, err := sortManifests(manifests, newVersionSet("v1", "v1beta1")) if err != nil { t.Fatalf("Unexpected error: %s", err) } @@ -153,3 +157,19 @@ metadata: } } + +func TestVersionSet(t *testing.T) { + vs := newVersionSet("v1", "v1beta1", "extensions/alpha5", "batch/v1") + + if l := len(vs); l != 4 { + t.Errorf("Expected 4, got %d", l) + } + + if !vs.Has("extensions/alpha5") { + t.Error("No match for alpha5") + } + + if vs.Has("nosuch/extension") { + t.Error("Found nonexistent extension") + } +} diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 0fe6a192a..90cf40d61 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -35,6 +35,7 @@ import ( "k8s.io/helm/pkg/proto/hapi/services" "k8s.io/helm/pkg/storage/driver" "k8s.io/helm/pkg/timeconv" + "k8s.io/kubernetes/pkg/api/unversioned" ) var srv *releaseServer @@ -399,6 +400,31 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re return rel, nil } +func (s *releaseServer) getVersionSet() (versionSet, error) { + defVersions := newVersionSet("v1") + cli, err := s.env.KubeClient.APIClient() + if err != nil { + log.Printf("API Client for Kubernetes is missing: %s.", err) + return defVersions, err + } + + groups, err := cli.Discovery().ServerGroups() + if err != nil { + return defVersions, err + } + + // FIXME: The Kubernetes test fixture for cli appears to always return nil + // for calls to Discovery().ServerGroups(). So in this case, we return + // the default API list. This is also a safe value to return in any other + // odd-ball case. + if groups == nil { + return defVersions, nil + } + + versions := unversioned.ExtractGroupVersions(groups) + return newVersionSet(versions...), 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) @@ -409,7 +435,11 @@ func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values // 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. - hooks, manifests, err := sortHooks(files) + vs, err := s.getVersionSet() + if err != nil { + return nil, nil, fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err) + } + hooks, manifests, err := sortManifests(files, vs) if err != nil { // By catching parse errors here, we can prevent bogus releases from going // to Kubernetes. diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index dba2fc9db..cffcb3cbe 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -108,6 +108,20 @@ func releaseStub() *release.Release { } } +func TestGetVersionSet(t *testing.T) { + rs := rsFixture() + vs, err := rs.getVersionSet() + if err != nil { + t.Error(err) + } + if !vs.Has("v1") { + t.Errorf("Expected supported versions to at least include v1.") + } + if vs.Has("nosuchversion/v1") { + t.Error("Non-existent version is reported found.") + } +} + func TestUniqName(t *testing.T) { rs := rsFixture() From 96de0e35aa2eda4869a72de31570b59a4aa36a07 Mon Sep 17 00:00:00 2001 From: joe2far Date: Fri, 26 Aug 2016 20:02:27 +0100 Subject: [PATCH 043/183] feat(helm): ignore by default all dotfiles in templates/ --- pkg/chartutil/load.go | 1 + pkg/ignore/rules.go | 7 +++++++ pkg/ignore/rules_test.go | 11 ++++++++++- pkg/ignore/testdata/templates/.dotfile | 0 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 pkg/ignore/testdata/templates/.dotfile diff --git a/pkg/chartutil/load.go b/pkg/chartutil/load.go index 911d883d2..20212f241 100644 --- a/pkg/chartutil/load.go +++ b/pkg/chartutil/load.go @@ -208,6 +208,7 @@ func LoadDir(dir string) (*chart.Chart, error) { } rules = r } + rules.AddDefaults() files := []*afile{} topdir += string(filepath.Separator) diff --git a/pkg/ignore/rules.go b/pkg/ignore/rules.go index f5b08a4ee..0e102a2e7 100644 --- a/pkg/ignore/rules.go +++ b/pkg/ignore/rules.go @@ -42,6 +42,13 @@ func Empty() *Rules { return &Rules{patterns: []*pattern{}} } +// AddDefaults adds default ignore patterns. +// +// Ignore all dotfiles in "templates/" +func (r *Rules) AddDefaults() { + r.parseRule(`templates/.?*`) +} + // ParseFile parses a helmignore file and returns the *Rules. func ParseFile(file string) (*Rules, error) { f, err := os.Open(file) diff --git a/pkg/ignore/rules_test.go b/pkg/ignore/rules_test.go index a30db06c5..3040221ae 100644 --- a/pkg/ignore/rules_test.go +++ b/pkg/ignore/rules_test.go @@ -63,7 +63,6 @@ func TestParseFail(t *testing.T) { t.Errorf("Rule %q should have failed", fail) } } - } func TestParseFile(t *testing.T) { @@ -99,6 +98,7 @@ func TestIgnore(t *testing.T) { {`cargo/*.*`, "cargo/a.txt", true}, {`cargo/*.txt`, "mast/a.txt", false}, {`ru[c-e]?er.txt`, "rudder.txt", true}, + {`templates/.?*`, "templates/.dotfile", true}, // Directory tests {`cargo/`, "cargo", true}, @@ -134,6 +134,15 @@ func TestIgnore(t *testing.T) { } } +func TestAddDefaults(t *testing.T) { + r := Rules{} + r.AddDefaults() + + if len(r.patterns) != 1 { + t.Errorf("Expected 1 default patterns, got %d", len(r.patterns)) + } +} + func parseString(str string) (*Rules, error) { b := bytes.NewBuffer([]byte(str)) return Parse(b) diff --git a/pkg/ignore/testdata/templates/.dotfile b/pkg/ignore/testdata/templates/.dotfile new file mode 100644 index 000000000..e69de29bb From 1fb16ab3c928dc983a81565b53404539fc74e16a Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Thu, 25 Aug 2016 16:55:36 -0600 Subject: [PATCH 044/183] fix(helm): refactor 'helm update' to match new style And add tests. Closes #696 --- cmd/helm/helm.go | 1 + cmd/helm/testdata/repositories.yaml | 2 + cmd/helm/update.go | 53 +++++++++++------- cmd/helm/update_test.go | 87 +++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 19 deletions(-) create mode 100644 cmd/helm/testdata/repositories.yaml create mode 100644 cmd/helm/update_test.go diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 81085e1ef..d3c756b3b 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -97,6 +97,7 @@ func newRootCmd(out io.Writer) *cobra.Command { newPackageCmd(nil, out), newFetchCmd(out), newVerifyCmd(out), + newUpdateCmd(out), ) return cmd } diff --git a/cmd/helm/testdata/repositories.yaml b/cmd/helm/testdata/repositories.yaml new file mode 100644 index 000000000..6fe931e89 --- /dev/null +++ b/cmd/helm/testdata/repositories.yaml @@ -0,0 +1,2 @@ +charts: http://storage.googleapis.com/kubernetes-charts +local: http://localhost:8879/charts diff --git a/cmd/helm/update.go b/cmd/helm/update.go index ecd4710da..a7c633c54 100644 --- a/cmd/helm/update.go +++ b/cmd/helm/update.go @@ -19,6 +19,7 @@ package main import ( "errors" "fmt" + "io" "sync" "github.com/spf13/cobra" @@ -26,23 +27,37 @@ import ( "k8s.io/helm/pkg/repo" ) -var verboseUpdate bool +const updateDesc = ` +Update gets the latest information about charts from the respective chart repositories. +Information is cached locally, where it is used by commands like 'helm search'. +` -var updateCommand = &cobra.Command{ - Use: "update", - Aliases: []string{"up"}, - Short: "update information on available charts in the chart repositories", - RunE: runUpdate, +type updateCmd struct { + repoFile string + update func(map[string]string, bool, io.Writer) + out io.Writer } -func init() { - updateCommand.Flags().BoolVar(&verboseUpdate, "verbose", false, "verbose error messages") - RootCommand.AddCommand(updateCommand) +func newUpdateCmd(out io.Writer) *cobra.Command { + u := &updateCmd{ + out: out, + update: updateCharts, + repoFile: repositoriesFile(), + } + cmd := &cobra.Command{ + Use: "update", + Aliases: []string{"up"}, + Short: "update information on available charts in the chart repositories", + Long: updateDesc, + RunE: func(cmd *cobra.Command, args []string) error { + return u.run() + }, + } + return cmd } -func runUpdate(cmd *cobra.Command, args []string) error { - - f, err := repo.LoadRepositoriesFile(repositoriesFile()) +func (u *updateCmd) run() error { + f, err := repo.LoadRepositoriesFile(u.repoFile) if err != nil { return err } @@ -51,12 +66,12 @@ func runUpdate(cmd *cobra.Command, args []string) error { return errors.New("no repositories found. You must add one before updating") } - updateCharts(f.Repositories, verboseUpdate) + u.update(f.Repositories, flagDebug, u.out) return nil } -func updateCharts(repos map[string]string, verbose bool) { - fmt.Println("Hang tight while we grab the latest from your chart repositories...") +func updateCharts(repos map[string]string, verbose bool, out io.Writer) { + fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") var wg sync.WaitGroup for name, url := range repos { wg.Add(1) @@ -65,16 +80,16 @@ func updateCharts(repos map[string]string, verbose bool) { indexFileName := cacheDirectory(n + "-index.yaml") err := repo.DownloadIndexFile(n, u, indexFileName) if err != nil { - updateErr := "...Unable to get an update from the " + n + " chart repository" + updateErr := fmt.Sprintf("...Unable to get an update from the %q chart repository", n) if verbose { updateErr = updateErr + ": " + err.Error() } - fmt.Println(updateErr) + fmt.Fprintln(out, updateErr) } else { - fmt.Println("...Successfully got an update from the " + n + " chart repository") + fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", n) } }(name, url) } wg.Wait() - fmt.Println("Update Complete. Happy Helming!") + fmt.Fprintln(out, "Update Complete. Happy Helming!") } diff --git a/cmd/helm/update_test.go b/cmd/helm/update_test.go new file mode 100644 index 000000000..c8af270a9 --- /dev/null +++ b/cmd/helm/update_test.go @@ -0,0 +1,87 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestUpdateCmd(t *testing.T) { + out := bytes.NewBuffer(nil) + // Instead of using the HTTP updater, we provide our own for this test. + // The TestUpdateCharts test verifies the HTTP behavior independently. + updater := func(repos map[string]string, verbose bool, out io.Writer) { + for name := range repos { + fmt.Fprintln(out, name) + } + } + uc := &updateCmd{ + out: out, + update: updater, + repoFile: "testdata/repositories.yaml", + } + uc.run() + + if got := out.String(); !strings.Contains(got, "charts") || !strings.Contains(got, "local") { + t.Errorf("Expected 'charts' and 'local' (in any order) got %s", got) + } +} + +const mockRepoIndex = ` +mychart-0.1.0: + name: mychart-0.1.0 + url: localhost:8879/charts/mychart-0.1.0.tgz + chartfile: + name: "" + home: "" + sources: [] + version: "" + description: "" + keywords: [] + maintainers: [] + engine: "" + icon: "" +` + +func TestUpdateCharts(t *testing.T) { + // This tests the repo in isolation. It creates a mock HTTP server that simply + // returns a static YAML file in the anticipate format. + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(mockRepoIndex)) + }) + srv := httptest.NewServer(handler) + defer srv.Close() + + buf := bytes.NewBuffer(nil) + repos := map[string]string{ + "charts": srv.URL, + } + updateCharts(repos, false, buf) + + got := buf.String() + if strings.Contains(got, "Unable to get an update") { + t.Errorf("Failed to get a repo: %q", got) + } + if !strings.Contains(got, "Update Complete.") { + t.Errorf("Update was not successful") + } +} From 5d820c4d7c4aa1bd2eeef0ad59d51c66128fae58 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Fri, 26 Aug 2016 14:49:22 -0700 Subject: [PATCH 045/183] feat(cmd): install latest tagged image on `helm init` closes: #1117 --- cmd/helm/installer/install.go | 139 +++++++++++++++++++++------------- 1 file changed, 87 insertions(+), 52 deletions(-) diff --git a/cmd/helm/installer/install.go b/cmd/helm/installer/install.go index 8c6b1a0c4..0bfd197e8 100644 --- a/cmd/helm/installer/install.go +++ b/cmd/helm/installer/install.go @@ -17,15 +17,20 @@ limitations under the License. package installer // import "k8s.io/helm/cmd/helm/installer" import ( - "bytes" "fmt" - "text/template" + "strings" - "github.com/Masterminds/sprig" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/util/intstr" "k8s.io/helm/pkg/kube" + "k8s.io/helm/pkg/version" ) +const defaultImage = "gcr.io/kubernetes-helm/tiller" + // Install uses kubernetes client to install tiller // // Returns the string output received from the operation, and an error if the @@ -43,59 +48,89 @@ func Install(namespace, image string, verbose bool) error { namespace = ns } - var b bytes.Buffer - - // Add main install YAML - istpl := template.New("install").Funcs(sprig.TxtFuncMap()) - - cfg := struct { - Namespace, Image string - }{namespace, image} - - if err := template.Must(istpl.Parse(InstallYAML)).Execute(&b, cfg); err != nil { + c, err := kc.Client() + if err != nil { return err } - if verbose { - fmt.Println(b.String()) + ns := generateNamespace(namespace) + if _, err := c.Namespaces().Create(ns); err != nil { + if !errors.IsAlreadyExists(err) { + return err + } + } + + if image == "" { + // strip git sha off version + tag := strings.Split(version.Version, "+")[0] + image = fmt.Sprintf("%s:%s", defaultImage, tag) } - return kc.Create(namespace, &b) + rc := generateDeployment(image) + + _, err = c.Deployments(namespace).Create(rc) + return err +} + +func generateLabels(labels map[string]string) map[string]string { + labels["app"] = "helm" + return labels +} + +func generateDeployment(image string) *extensions.Deployment { + labels := generateLabels(map[string]string{"name": "tiller"}) + d := &extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + Name: "tiller-deploy", + Labels: labels, + }, + Spec: extensions.DeploymentSpec{ + Replicas: 1, + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: labels, + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: "tiller", + Image: image, + ImagePullPolicy: "Always", + Ports: []api.ContainerPort{{ContainerPort: 44134, Name: "tiller"}}, + LivenessProbe: &api.Probe{ + Handler: api.Handler{ + HTTPGet: &api.HTTPGetAction{ + Path: "/liveness", + Port: intstr.FromInt(44135), + }, + }, + InitialDelaySeconds: 1, + TimeoutSeconds: 1, + }, + ReadinessProbe: &api.Probe{ + Handler: api.Handler{ + HTTPGet: &api.HTTPGetAction{ + Path: "/readiness", + Port: intstr.FromInt(44135), + }, + }, + InitialDelaySeconds: 1, + TimeoutSeconds: 1, + }, + }, + }, + }, + }, + }, + } + return d } -// InstallYAML is the installation YAML for DM. -const InstallYAML = ` ---- -apiVersion: extensions/v1beta1 -kind: Deployment -metadata: - name: tiller-deploy - namespace: {{ .Namespace }} -spec: - replicas: 1 - template: - metadata: - labels: - app: helm - name: tiller - spec: - containers: - - image: {{default "gcr.io/kubernetes-helm/tiller:canary" .Image}} - name: tiller - ports: - - containerPort: 44134 - name: tiller - imagePullPolicy: Always - livenessProbe: - httpGet: - path: /liveness - port: 44135 - initialDelaySeconds: 1 - timeoutSeconds: 1 - readinessProbe: - httpGet: - path: /readiness - port: 44135 - initialDelaySeconds: 1 - timeoutSeconds: 1 -` +func generateNamespace(namespace string) *api.Namespace { + return &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: namespace, + Labels: generateLabels(map[string]string{"name": "helm-namespace"}), + }, + } +} From 2253f3e84eb64a7078cb46c35255b32f18dac878 Mon Sep 17 00:00:00 2001 From: joe2far Date: Sat, 27 Aug 2016 00:02:13 +0100 Subject: [PATCH 046/183] fix(helm): make repo index not require repo_url --- cmd/helm/repo.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd/helm/repo.go b/cmd/helm/repo.go index 73c6b0235..2ca470828 100644 --- a/cmd/helm/repo.go +++ b/cmd/helm/repo.go @@ -107,16 +107,20 @@ func runRepoRemove(cmd *cobra.Command, args []string) error { } func runRepoIndex(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(2, len(args), "path to a directory", "url of chart repository"); err != nil { - return err + if len(args) == 0 { + return fmt.Errorf("This command needs at minimum 1 argument: a path to a directory") } path, err := filepath.Abs(args[0]) if err != nil { return err } + url := "" + if len(args) == 2 { + url = args[1] + } - return index(path, args[1]) + return index(path, url) } func index(dir, url string) error { From 492dbb37917e8f5ee7c79e0855f870a31446f2de Mon Sep 17 00:00:00 2001 From: vaikas-google Date: Mon, 22 Aug 2016 16:38:33 -0700 Subject: [PATCH 047/183] Add support for NOTES.txt file --- _proto/hapi/release/status.proto | 4 + _proto/hapi/services/tiller.proto | 3 + cmd/helm/status.go | 7 +- cmd/tiller/release_server.go | 43 +++++++-- cmd/tiller/release_server_test.go | 133 +++++++++++++++++++++++++++ pkg/engine/engine.go | 2 +- pkg/proto/hapi/release/status.pb.go | 37 ++++---- pkg/proto/hapi/services/tiller.pb.go | 108 +++++++++++----------- 8 files changed, 258 insertions(+), 79 deletions(-) diff --git a/_proto/hapi/release/status.proto b/_proto/hapi/release/status.proto index 6f17913db..06952251c 100644 --- a/_proto/hapi/release/status.proto +++ b/_proto/hapi/release/status.proto @@ -38,6 +38,10 @@ message Status { Code code = 1; google.protobuf.Any details = 2; + // Cluster resources as kubectl would print them. string resources = 3; + + // Contains the rendered templates/NOTES.txt if available + string notes = 4; } diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto index 549f128f6..873a41ee2 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -142,6 +142,9 @@ message GetReleaseStatusResponse { // Info contains information about the release. hapi.release.Info info = 2; + + // Namesapce the release was released into + string namespace = 3; } // GetReleaseContentRequest is a request to get the contents of a release. diff --git a/cmd/helm/status.go b/cmd/helm/status.go index 436445642..f4d8deaea 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -67,10 +67,15 @@ func (s *statusCmd) run() error { } fmt.Fprintf(s.out, "Last Deployed: %s\n", timeconv.String(res.Info.LastDeployed)) + fmt.Fprintf(s.out, "Namespace: %s\n", res.Namespace) fmt.Fprintf(s.out, "Status: %s\n", res.Info.Status.Code) - fmt.Fprintf(s.out, "Resources:\n%s\n", res.Info.Status.Resources) if res.Info.Status.Details != nil { fmt.Fprintf(s.out, "Details: %s\n", res.Info.Status.Details) } + fmt.Fprintf(s.out, "\n") + fmt.Fprintf(s.out, "Resources:\n%s\n", res.Info.Status.Resources) + if len(res.Info.Status.Notes) > 0 { + fmt.Fprintf(s.out, "Notes:\n%s\n", res.Info.Status.Notes) + } return nil } diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 865cf91f5..835948cdd 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -23,6 +23,7 @@ import ( "log" "regexp" "sort" + "strings" "github.com/ghodss/yaml" "github.com/technosophos/moniker" @@ -47,6 +48,12 @@ var srv *releaseServer // characters in length. See https://github.com/kubernetes/helm/issues/1071 const releaseNameMaxLen = 14 +// NOTES.txt suffix that we want to treat special. It goes through the templating engine +// but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually +// wants to see this file after rendering in the status command. However, it must be a suffix +// since there can be filepath in front of it. +const NOTES_FILE_SUFFIX = "NOTES.txt" + func init() { srv = &releaseServer{ env: env, @@ -179,6 +186,9 @@ func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetRelease if rel.Info == nil { return nil, errors.New("release info is missing") } + if rel.Chart == nil { + return nil, errors.New("release chart is missing") + } // Ok, we got the status of the release as we had jotted down, now we need to match the // manifest we stashed away with reality from the cluster. @@ -190,7 +200,7 @@ func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetRelease } rel.Info.Status.Resources = resp - return &services.GetReleaseStatusResponse{Info: rel.Info}, nil + return &services.GetReleaseStatusResponse{Info: rel.Info, Namespace: rel.Namespace}, nil } func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleaseContentRequest) (*services.GetReleaseContentResponse, error) { @@ -281,7 +291,7 @@ func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*rele return nil, nil, err } - hooks, manifestDoc, err := s.renderResources(req.Chart, valuesToRender) + hooks, manifestDoc, notesTxt, err := s.renderResources(req.Chart, valuesToRender) if err != nil { return nil, nil, err } @@ -302,6 +312,9 @@ func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*rele Hooks: hooks, } + if len(notesTxt) > 0 { + updatedRelease.Info.Status.Notes = notesTxt + } return currentRelease, updatedRelease, nil } @@ -389,7 +402,7 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re return nil, err } - hooks, manifestDoc, err := s.renderResources(req.Chart, valuesToRender) + hooks, manifestDoc, notesTxt, err := s.renderResources(req.Chart, valuesToRender) if err != nil { return nil, err } @@ -409,6 +422,9 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re Hooks: hooks, Version: 1, } + if len(notesTxt) > 0 { + rel.Info.Status.Notes = notesTxt + } return rel, nil } @@ -437,11 +453,24 @@ func (s *releaseServer) getVersionSet() (versionSet, error) { return newVersionSet(versions...), nil } -func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values) ([]*release.Hook, *bytes.Buffer, error) { +func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values) ([]*release.Hook, *bytes.Buffer, string, error) { renderer := s.engine(ch) files, err := renderer.Render(ch, values) if err != nil { - return nil, nil, err + return nil, nil, "", err + } + + // NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource, + // pull it out of here into a separate file so that we can actually use the output of the rendered + // text file. We have to spin through this map because the file contains path information, so we + // look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip + // it in the sortHooks. + notes := "" + for k, v := range files { + if strings.HasSuffix(k, NOTES_FILE_SUFFIX) { + notes = v + delete(files, k) + } } // Sort hooks, manifests, and partials. Only hooks and manifests are returned, @@ -455,7 +484,7 @@ func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values if err != nil { // By catching parse errors here, we can prevent bogus releases from going // to Kubernetes. - return nil, nil, err + return nil, nil, "", err } // Aggregate all valid manifests into one big doc. @@ -465,7 +494,7 @@ func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values b.WriteString(file) } - return hooks, b, nil + return hooks, b, notes, nil } // validateYAML checks to see if YAML is well-formed. diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 53bb8fdca..4fb6d9f51 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -35,6 +35,8 @@ import ( "k8s.io/helm/pkg/storage/driver" ) +const NOTES_TEXT = "my notes here" + var manifestWithHook = `apiVersion: v1 kind: ConfigMap metadata: @@ -230,6 +232,137 @@ func TestInstallRelease(t *testing.T) { } } +func TestInstallReleaseWithNotes(t *testing.T) { + c := context.Background() + rs := rsFixture() + + // TODO: Refactor this into a mock. + req := &services.InstallReleaseRequest{ + Namespace: "spaced", + Chart: &chart.Chart{ + Metadata: &chart.Metadata{Name: "hello"}, + Templates: []*chart.Template{ + {Name: "hello", Data: []byte("hello: world")}, + {Name: "hooks", Data: []byte(manifestWithHook)}, + {Name: "NOTES.txt", Data: []byte(NOTES_TEXT)}, + }, + }, + } + res, err := rs.InstallRelease(c, req) + if err != nil { + t.Errorf("Failed install: %s", err) + } + if res.Release.Name == "" { + t.Errorf("Expected release name.") + } + if res.Release.Namespace != "spaced" { + t.Errorf("Expected release namespace 'spaced', got '%s'.", res.Release.Namespace) + } + + rel, err := rs.env.Releases.Get(res.Release.Name) + if err != nil { + t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) + } + + t.Logf("rel: %v", rel) + + if len(rel.Hooks) != 1 { + t.Fatalf("Expected 1 hook, got %d", len(rel.Hooks)) + } + if rel.Hooks[0].Manifest != manifestWithHook { + t.Errorf("Unexpected manifest: %v", rel.Hooks[0].Manifest) + } + + if rel.Info.Status.Notes != NOTES_TEXT { + t.Fatalf("Expected '%s', got '%s'", NOTES_TEXT, rel.Info.Status.Notes) + } + + if rel.Hooks[0].Events[0] != release.Hook_POST_INSTALL { + t.Errorf("Expected event 0 is post install") + } + if rel.Hooks[0].Events[1] != release.Hook_PRE_DELETE { + t.Errorf("Expected event 0 is pre-delete") + } + + if len(res.Release.Manifest) == 0 { + t.Errorf("No manifest returned: %v", res.Release) + } + + if len(rel.Manifest) == 0 { + t.Errorf("Expected manifest in %v", res) + } + + if !strings.Contains(rel.Manifest, "---\n# Source: hello/hello\nhello: world") { + t.Errorf("unexpected output: %s", rel.Manifest) + } +} + +func TestInstallReleaseWithNotesRendered(t *testing.T) { + c := context.Background() + rs := rsFixture() + + // TODO: Refactor this into a mock. + req := &services.InstallReleaseRequest{ + Namespace: "spaced", + Chart: &chart.Chart{ + Metadata: &chart.Metadata{Name: "hello"}, + Templates: []*chart.Template{ + {Name: "hello", Data: []byte("hello: world")}, + {Name: "hooks", Data: []byte(manifestWithHook)}, + {Name: "NOTES.txt", Data: []byte(NOTES_TEXT + " {{.Release.Name}}")}, + }, + }, + } + res, err := rs.InstallRelease(c, req) + if err != nil { + t.Errorf("Failed install: %s", err) + } + if res.Release.Name == "" { + t.Errorf("Expected release name.") + } + if res.Release.Namespace != "spaced" { + t.Errorf("Expected release namespace 'spaced', got '%s'.", res.Release.Namespace) + } + + rel, err := rs.env.Releases.Get(res.Release.Name) + if err != nil { + t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) + } + + t.Logf("rel: %v", rel) + + if len(rel.Hooks) != 1 { + t.Fatalf("Expected 1 hook, got %d", len(rel.Hooks)) + } + if rel.Hooks[0].Manifest != manifestWithHook { + t.Errorf("Unexpected manifest: %v", rel.Hooks[0].Manifest) + } + + expectedNotes := fmt.Sprintf("%s %s", NOTES_TEXT, res.Release.Name) + if rel.Info.Status.Notes != expectedNotes { + t.Fatalf("Expected '%s', got '%s'", expectedNotes, rel.Info.Status.Notes) + } + + if rel.Hooks[0].Events[0] != release.Hook_POST_INSTALL { + t.Errorf("Expected event 0 is post install") + } + if rel.Hooks[0].Events[1] != release.Hook_PRE_DELETE { + t.Errorf("Expected event 0 is pre-delete") + } + + if len(res.Release.Manifest) == 0 { + t.Errorf("No manifest returned: %v", res.Release) + } + + if len(rel.Manifest) == 0 { + t.Errorf("Expected manifest in %v", res) + } + + if !strings.Contains(rel.Manifest, "---\n# Source: hello/hello\nhello: world") { + t.Errorf("unexpected output: %s", rel.Manifest) + } +} + func TestInstallReleaseDryRun(t *testing.T) { c := context.Background() rs := rsFixture() diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 53578a1b8..e415913a3 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -54,7 +54,7 @@ func New() *Engine { } } -// Render takes a chart, optional values, and value overrids, and attempts to render the Go templates. +// Render takes a chart, optional values, and value overrides, and attempts to render the Go templates. // // Render can be called repeatedly on the same engine. // diff --git a/pkg/proto/hapi/release/status.pb.go b/pkg/proto/hapi/release/status.pb.go index 33f418aac..ee417759d 100644 --- a/pkg/proto/hapi/release/status.pb.go +++ b/pkg/proto/hapi/release/status.pb.go @@ -55,6 +55,8 @@ type Status struct { Details *google_protobuf1.Any `protobuf:"bytes,2,opt,name=details" json:"details,omitempty"` // Cluster resources as kubectl would print them. Resources string `protobuf:"bytes,3,opt,name=resources" json:"resources,omitempty"` + // Contains the rendered templates/NOTES.txt if available + Notes string `protobuf:"bytes,4,opt,name=notes" json:"notes,omitempty"` } func (m *Status) Reset() { *m = Status{} } @@ -75,21 +77,22 @@ func init() { } var fileDescriptor3 = []byte{ - // 247 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8, - 0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0x2f, 0x2e, 0x49, 0x2c, 0x29, 0x2d, 0xd6, - 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x01, 0x49, 0xe9, 0x41, 0xa5, 0xa4, 0x24, 0xd3, 0xf3, - 0xf3, 0xd3, 0x73, 0x52, 0xf5, 0xc1, 0x72, 0x49, 0xa5, 0x69, 0xfa, 0x89, 0x79, 0x95, 0x10, 0x85, - 0x4a, 0x17, 0x19, 0xb9, 0xd8, 0x82, 0xc1, 0x3a, 0x85, 0x74, 0xb9, 0x58, 0x92, 0xf3, 0x53, 0x52, - 0x25, 0x18, 0x15, 0x18, 0x35, 0xf8, 0x8c, 0x24, 0xf5, 0x90, 0x8d, 0xd0, 0x83, 0xa8, 0xd1, 0x73, - 0x06, 0x2a, 0x08, 0x02, 0x2b, 0x13, 0xd2, 0xe3, 0x62, 0x4f, 0x49, 0x2d, 0x49, 0xcc, 0xcc, 0x29, - 0x96, 0x60, 0x02, 0xea, 0xe0, 0x36, 0x12, 0xd1, 0x83, 0x58, 0xa3, 0x07, 0xb3, 0x46, 0xcf, 0x31, - 0xaf, 0x32, 0x08, 0xa6, 0x48, 0x48, 0x86, 0x8b, 0xb3, 0x28, 0xb5, 0x38, 0xbf, 0xb4, 0x28, 0x39, - 0xb5, 0x58, 0x82, 0x19, 0xa8, 0x83, 0x33, 0x08, 0x21, 0xa0, 0xe4, 0xc5, 0xc5, 0x02, 0x32, 0x5b, - 0x88, 0x9b, 0x8b, 0x3d, 0xd4, 0xcf, 0xdb, 0xcf, 0x3f, 0xdc, 0x4f, 0x80, 0x41, 0x88, 0x87, 0x8b, - 0xc3, 0xc5, 0x35, 0xc0, 0xc7, 0x3f, 0xd2, 0xd5, 0x45, 0x80, 0x11, 0x24, 0xe5, 0xe2, 0xea, 0xe3, - 0x1a, 0x02, 0xe4, 0x30, 0x09, 0xf1, 0x71, 0x71, 0x05, 0x87, 0x06, 0xb8, 0x06, 0x05, 0xbb, 0xba, - 0x00, 0xf9, 0xcc, 0x42, 0x5c, 0x5c, 0x6c, 0x6e, 0x8e, 0x9e, 0x3e, 0x40, 0x36, 0x8b, 0x13, 0x67, - 0x14, 0x3b, 0xd4, 0xd9, 0x49, 0x6c, 0x60, 0xb7, 0x18, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xd1, - 0xc3, 0xbf, 0x50, 0x2b, 0x01, 0x00, 0x00, + // 259 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0xc1, 0x4e, 0xf2, 0x40, + 0x14, 0x85, 0xff, 0x42, 0xff, 0xd6, 0x5e, 0x08, 0x21, 0x37, 0x2c, 0x5a, 0xe3, 0xc2, 0xb0, 0x72, + 0xe3, 0x6d, 0x82, 0x4f, 0x80, 0x76, 0x4c, 0xd4, 0xa6, 0x90, 0x56, 0x62, 0x74, 0x37, 0xc0, 0x88, + 0x24, 0x4d, 0x87, 0x74, 0xa6, 0x0b, 0x9e, 0xd8, 0xd7, 0x70, 0x3a, 0x85, 0xe8, 0xae, 0xa7, 0xdf, + 0x77, 0xe6, 0xcc, 0x40, 0xf4, 0xc5, 0x0f, 0xfb, 0xb8, 0x16, 0xa5, 0xe0, 0x4a, 0xc4, 0x4a, 0x73, + 0xdd, 0x28, 0x3a, 0xd4, 0x52, 0x4b, 0x1c, 0xb6, 0x88, 0x4e, 0xe8, 0x32, 0xda, 0x49, 0xb9, 0x2b, + 0x45, 0x6c, 0xd9, 0xba, 0xf9, 0x8c, 0x79, 0x75, 0xec, 0xc4, 0xe9, 0xb7, 0x03, 0x5e, 0x61, 0x9b, + 0x78, 0x0b, 0xee, 0x46, 0x6e, 0x45, 0xe8, 0x5c, 0x3b, 0x37, 0xa3, 0x59, 0x44, 0x7f, 0x8f, 0xa0, + 0xce, 0xa1, 0x07, 0x23, 0xe4, 0x56, 0x43, 0x02, 0x7f, 0x2b, 0x34, 0xdf, 0x97, 0x2a, 0xec, 0x99, + 0xc6, 0x60, 0x36, 0xa1, 0x6e, 0x86, 0xce, 0x33, 0x34, 0xaf, 0x8e, 0xf9, 0x59, 0xc2, 0x2b, 0x08, + 0x6a, 0xa1, 0x64, 0x53, 0x6f, 0x84, 0x0a, 0xfb, 0xa6, 0x11, 0xe4, 0xbf, 0x3f, 0x70, 0x02, 0xff, + 0x2b, 0xa9, 0x0d, 0x71, 0x2d, 0xe9, 0xc2, 0xf4, 0x19, 0xdc, 0x76, 0x11, 0x07, 0xe0, 0xaf, 0xb2, + 0x97, 0x6c, 0xf1, 0x96, 0x8d, 0xff, 0xe1, 0x10, 0x2e, 0x12, 0xb6, 0x4c, 0x17, 0xef, 0x2c, 0x19, + 0x3b, 0x2d, 0x4a, 0x58, 0xca, 0x5e, 0x4d, 0xe8, 0xe1, 0x08, 0xa0, 0x58, 0x2d, 0x59, 0x5e, 0xb0, + 0xc4, 0xe4, 0x3e, 0x02, 0x78, 0x8f, 0xf3, 0xa7, 0xd4, 0x7c, 0xbb, 0xf7, 0xc1, 0x87, 0x7f, 0x7a, + 0xcc, 0xda, 0xb3, 0x37, 0xbc, 0xfb, 0x09, 0x00, 0x00, 0xff, 0xff, 0xae, 0x07, 0x47, 0x1f, 0x41, + 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index 4b497fdd7..5e87e524b 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -176,6 +176,8 @@ type GetReleaseStatusResponse struct { Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` // Info contains information about the release. Info *hapi_release2.Info `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"` + // Namesapce the release was released into + Namespace string `protobuf:"bytes,3,opt,name=namespace" json:"namespace,omitempty"` } func (m *GetReleaseStatusResponse) Reset() { *m = GetReleaseStatusResponse{} } @@ -631,57 +633,57 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ } var fileDescriptor0 = []byte{ - // 821 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdd, 0x52, 0xd3, 0x40, - 0x14, 0x26, 0x6d, 0xe9, 0xcf, 0x29, 0x30, 0x65, 0x05, 0x5a, 0x32, 0xea, 0x30, 0x71, 0x54, 0x44, - 0x49, 0xb5, 0xde, 0x3a, 0xce, 0x94, 0xd2, 0x01, 0x86, 0x5a, 0x66, 0x52, 0xd1, 0x19, 0x2f, 0xec, - 0x84, 0x76, 0x4b, 0xa3, 0x21, 0xa9, 0xd9, 0x2d, 0x23, 0x8f, 0xe0, 0x6b, 0xf8, 0x14, 0xde, 0xf8, - 0x64, 0xde, 0xb8, 0x3f, 0x49, 0x6c, 0xda, 0x44, 0x23, 0x37, 0xe9, 0xee, 0x9e, 0x6f, 0xbf, 0x73, - 0xce, 0x77, 0xf6, 0x1c, 0x00, 0x75, 0x6c, 0x4e, 0xac, 0x3a, 0xc1, 0xde, 0xb5, 0x35, 0xc0, 0xa4, - 0x4e, 0x2d, 0xdb, 0xc6, 0x9e, 0x3e, 0xf1, 0x5c, 0xea, 0xa2, 0x0d, 0x6e, 0xd3, 0x03, 0x9b, 0x2e, - 0x6d, 0xea, 0x96, 0xb8, 0x31, 0x18, 0x9b, 0x1e, 0x95, 0x5f, 0x89, 0x56, 0xab, 0xb3, 0xe7, 0xae, - 0x33, 0xb2, 0x2e, 0x7d, 0x83, 0x74, 0xe1, 0x61, 0x1b, 0x9b, 0x04, 0x07, 0xbf, 0x91, 0x4b, 0x81, - 0xcd, 0x72, 0x46, 0xae, 0x6f, 0xd8, 0x8e, 0x18, 0x08, 0x35, 0xe9, 0x94, 0x48, 0x93, 0xf6, 0x3d, - 0x03, 0x77, 0x3a, 0x16, 0xa1, 0x86, 0x34, 0x12, 0x03, 0x7f, 0x99, 0x62, 0x42, 0xd1, 0x06, 0x2c, - 0xdb, 0xd6, 0x95, 0x45, 0x6b, 0xca, 0x8e, 0xb2, 0x9b, 0x35, 0xe4, 0x06, 0x6d, 0x41, 0xde, 0x1d, - 0x8d, 0x08, 0xa6, 0xb5, 0x0c, 0x3b, 0x2e, 0x19, 0xfe, 0x0e, 0xbd, 0x86, 0x02, 0x71, 0x3d, 0xda, - 0xbf, 0xb8, 0xa9, 0x65, 0x99, 0x61, 0xad, 0xf1, 0x50, 0x8f, 0x4b, 0x57, 0xe7, 0x9e, 0x7a, 0x0c, - 0xa8, 0xf3, 0xcf, 0xc1, 0x8d, 0x91, 0x27, 0xe2, 0x97, 0xf3, 0x8e, 0x2c, 0x9b, 0x62, 0xaf, 0x96, - 0x93, 0xbc, 0x72, 0x87, 0x8e, 0x00, 0x04, 0xaf, 0xeb, 0x0d, 0x99, 0x6d, 0x59, 0x50, 0xef, 0xa6, - 0xa0, 0x3e, 0xe3, 0x78, 0xa3, 0x44, 0x82, 0x25, 0x7a, 0x05, 0x2b, 0x32, 0xed, 0xfe, 0xc0, 0x1d, - 0x62, 0x52, 0xcb, 0xef, 0x64, 0x19, 0xd5, 0xb6, 0xa4, 0x0a, 0x54, 0xec, 0x49, 0x61, 0x5a, 0x0c, - 0x61, 0x94, 0x25, 0x9c, 0xaf, 0x89, 0xf6, 0x11, 0x8a, 0x01, 0xbd, 0xd6, 0x80, 0xbc, 0x0c, 0x1e, - 0x95, 0xa1, 0x70, 0xde, 0x3d, 0xed, 0x9e, 0xbd, 0xef, 0x56, 0x96, 0x50, 0x11, 0x72, 0xdd, 0xe6, - 0x9b, 0x76, 0x45, 0x41, 0xeb, 0xb0, 0xda, 0x69, 0xf6, 0xde, 0xf6, 0x8d, 0x76, 0xa7, 0xdd, 0xec, - 0xb5, 0x0f, 0x2b, 0x19, 0xed, 0x3e, 0x94, 0xc2, 0xa8, 0x50, 0x01, 0xb2, 0xcd, 0x5e, 0x4b, 0x5e, - 0x39, 0x6c, 0xb3, 0x95, 0xa2, 0x7d, 0x53, 0x60, 0x23, 0x5a, 0x04, 0x32, 0x71, 0x1d, 0x82, 0x79, - 0x15, 0x06, 0xee, 0xd4, 0x09, 0xab, 0x20, 0x36, 0x08, 0x41, 0xce, 0xc1, 0x5f, 0x83, 0x1a, 0x88, - 0x35, 0x47, 0x52, 0x97, 0x9a, 0xb6, 0xd0, 0x9f, 0x21, 0xc5, 0x06, 0xbd, 0x80, 0xa2, 0x9f, 0x1c, - 0x61, 0xca, 0x66, 0x77, 0xcb, 0x8d, 0xcd, 0x68, 0xca, 0xbe, 0x47, 0x23, 0x84, 0x69, 0xfb, 0x50, - 0x3d, 0xc2, 0x41, 0x24, 0x52, 0x91, 0xe0, 0x4d, 0x70, 0xbf, 0xe6, 0x15, 0x16, 0xc1, 0x70, 0xbf, - 0x6c, 0xad, 0xbd, 0x83, 0xda, 0x22, 0xdc, 0x8f, 0x3e, 0x06, 0x8f, 0x1e, 0x41, 0x8e, 0x3f, 0x4c, - 0x11, 0x7b, 0xb9, 0x81, 0xa2, 0xd1, 0x9c, 0x30, 0x8b, 0x21, 0xec, 0x9a, 0x3e, 0xcb, 0xdb, 0x72, - 0x1d, 0x8a, 0x1d, 0xfa, 0xb7, 0x38, 0x3a, 0xb0, 0x1d, 0x83, 0xf7, 0x03, 0xa9, 0x43, 0xc1, 0x77, - 0x21, 0xee, 0x24, 0xaa, 0x10, 0xa0, 0xb4, 0x9f, 0xac, 0x20, 0xe7, 0x93, 0xa1, 0x49, 0x71, 0x60, - 0x4a, 0x76, 0x8d, 0x1e, 0xb3, 0x22, 0xf1, 0x46, 0xf5, 0x73, 0x5a, 0x97, 0xdc, 0xb2, 0x9b, 0x5b, - 0xfc, 0x6b, 0x48, 0x3b, 0xda, 0x83, 0xfc, 0xb5, 0x69, 0x33, 0x1e, 0x51, 0xa4, 0x30, 0x7b, 0x1f, - 0x29, 0xba, 0xdc, 0xf0, 0x11, 0xa8, 0x0a, 0x85, 0xa1, 0x77, 0xd3, 0xf7, 0xa6, 0x8e, 0x68, 0x89, - 0xa2, 0x91, 0x67, 0x5b, 0x63, 0xea, 0xa0, 0x07, 0xb0, 0x3a, 0xb4, 0x88, 0x79, 0x61, 0xe3, 0xfe, - 0xd8, 0x75, 0x3f, 0x13, 0xd1, 0x15, 0x45, 0x63, 0xc5, 0x3f, 0x3c, 0xe6, 0x67, 0xda, 0x31, 0x6c, - 0xce, 0x85, 0x7f, 0x5b, 0x25, 0x7e, 0x29, 0xb0, 0x79, 0xe2, 0xb0, 0x66, 0xb0, 0xed, 0x39, 0x29, - 0xc2, 0xb4, 0x95, 0xd4, 0x69, 0x67, 0xfe, 0x27, 0xed, 0x6c, 0x24, 0xed, 0x40, 0xf8, 0xdc, 0x8c, - 0xf0, 0x69, 0xa4, 0x40, 0x77, 0xa1, 0xc4, 0xc1, 0x64, 0x62, 0x0e, 0x30, 0x6b, 0x7b, 0x7e, 0xfb, - 0xcf, 0x01, 0xba, 0x07, 0xe0, 0xe1, 0x29, 0xc1, 0x7d, 0x41, 0x5e, 0x10, 0xf7, 0x4b, 0xe2, 0xa4, - 0xcb, 0x5f, 0xd5, 0x09, 0x6c, 0xcd, 0x27, 0x7f, 0x5b, 0x21, 0xc7, 0x50, 0x3d, 0x77, 0xac, 0x58, - 0x25, 0xe3, 0x1e, 0xd5, 0x42, 0x6e, 0x99, 0x98, 0xdc, 0x58, 0xd3, 0x4f, 0xa6, 0xde, 0x25, 0xf6, - 0xb5, 0x92, 0x1b, 0xed, 0x14, 0x6a, 0x8b, 0x9e, 0x6e, 0x19, 0x76, 0xe3, 0xc7, 0x32, 0xac, 0x05, - 0xdd, 0x2d, 0x27, 0x2e, 0xb2, 0x60, 0x65, 0x76, 0x58, 0xa1, 0x27, 0xc9, 0x03, 0x79, 0xee, 0xaf, - 0x8a, 0xba, 0x97, 0x06, 0x2a, 0x43, 0xd5, 0x96, 0x9e, 0x2b, 0x88, 0x40, 0x65, 0x7e, 0xba, 0xa0, - 0xfd, 0x78, 0x8e, 0x84, 0xa1, 0xa5, 0xea, 0x69, 0xe1, 0x81, 0x5b, 0x74, 0x0d, 0xeb, 0x0b, 0xa3, - 0x04, 0xfd, 0x93, 0x26, 0x3a, 0xa3, 0xd4, 0x7a, 0x6a, 0x7c, 0xe8, 0xf7, 0x13, 0xac, 0x46, 0x9a, - 0x16, 0x25, 0xa8, 0x15, 0x37, 0x98, 0xd4, 0xa7, 0xa9, 0xb0, 0xa1, 0xaf, 0x2b, 0x58, 0x8b, 0x3e, - 0x6c, 0x94, 0x40, 0x10, 0xdb, 0xfb, 0xea, 0xb3, 0x74, 0xe0, 0xd0, 0x1d, 0xab, 0xe3, 0xfc, 0x93, - 0x4c, 0xaa, 0x63, 0x42, 0x93, 0x24, 0xd5, 0x31, 0xe9, 0xa5, 0x6b, 0x4b, 0x07, 0xf0, 0xa1, 0x18, - 0xa0, 0x2f, 0xf2, 0xe2, 0xbf, 0x9d, 0x97, 0xbf, 0x03, 0x00, 0x00, 0xff, 0xff, 0x44, 0xdd, 0xaf, - 0x38, 0xa2, 0x09, 0x00, 0x00, + // 829 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdd, 0x4e, 0xe3, 0x46, + 0x14, 0xc6, 0x49, 0x70, 0x92, 0x13, 0x40, 0x61, 0x0a, 0x24, 0x58, 0x6d, 0x85, 0x5c, 0xb5, 0xa5, + 0xb4, 0x38, 0x6d, 0x7a, 0x5b, 0x55, 0x0a, 0x21, 0x02, 0x44, 0x1a, 0xa4, 0x49, 0x51, 0xa5, 0x5e, + 0x34, 0x32, 0xc9, 0x84, 0xb8, 0x6b, 0xec, 0xac, 0x67, 0x82, 0x96, 0x47, 0xd8, 0xd7, 0xd8, 0xa7, + 0xd8, 0x9b, 0x7d, 0xb2, 0xbd, 0xd9, 0xf9, 0xb1, 0xbd, 0x71, 0x62, 0xef, 0x7a, 0xb9, 0x71, 0xe6, + 0xcc, 0xf9, 0xe6, 0xfc, 0x7c, 0xe7, 0x07, 0xc0, 0x98, 0xd9, 0x73, 0xa7, 0x45, 0x49, 0xf0, 0xe8, + 0x8c, 0x09, 0x6d, 0x31, 0xc7, 0x75, 0x49, 0x60, 0xcd, 0x03, 0x9f, 0xf9, 0x68, 0x4f, 0xe8, 0xac, + 0x48, 0x67, 0x29, 0x9d, 0x71, 0x20, 0x5f, 0x8c, 0x67, 0x76, 0xc0, 0xd4, 0x57, 0xa1, 0x8d, 0xc6, + 0xf2, 0xbd, 0xef, 0x4d, 0x9d, 0xfb, 0x50, 0xa1, 0x5c, 0x04, 0xc4, 0x25, 0x36, 0x25, 0xd1, 0x6f, + 0xe2, 0x51, 0xa4, 0x73, 0xbc, 0xa9, 0x1f, 0x2a, 0x0e, 0x13, 0x0a, 0xca, 0x6c, 0xb6, 0xa0, 0x4a, + 0x65, 0xbe, 0x29, 0xc0, 0x57, 0x7d, 0x87, 0x32, 0xac, 0x94, 0x14, 0x93, 0x97, 0x0b, 0x42, 0x19, + 0xda, 0x83, 0x4d, 0xd7, 0x79, 0x70, 0x58, 0x53, 0x3b, 0xd2, 0x8e, 0x8b, 0x58, 0x09, 0xe8, 0x00, + 0x74, 0x7f, 0x3a, 0xa5, 0x84, 0x35, 0x0b, 0xfc, 0xba, 0x8a, 0x43, 0x09, 0xfd, 0x09, 0x65, 0xea, + 0x07, 0x6c, 0x74, 0xf7, 0xd4, 0x2c, 0x72, 0xc5, 0x4e, 0xfb, 0x7b, 0x2b, 0x2d, 0x5d, 0x4b, 0x78, + 0x1a, 0x72, 0xa0, 0x25, 0x3e, 0x67, 0x4f, 0x58, 0xa7, 0xf2, 0x57, 0xd8, 0x9d, 0x3a, 0x2e, 0x23, + 0x41, 0xb3, 0xa4, 0xec, 0x2a, 0x09, 0x5d, 0x00, 0x48, 0xbb, 0x7e, 0x30, 0xe1, 0xba, 0x4d, 0x69, + 0xfa, 0x38, 0x87, 0xe9, 0x1b, 0x81, 0xc7, 0x55, 0x1a, 0x1d, 0xd1, 0x1f, 0xb0, 0xa5, 0xd2, 0x1e, + 0x8d, 0xfd, 0x09, 0xa1, 0x4d, 0xfd, 0xa8, 0xc8, 0x4d, 0x1d, 0x2a, 0x53, 0x11, 0x8b, 0x43, 0x45, + 0x4c, 0x97, 0x23, 0x70, 0x4d, 0xc1, 0xc5, 0x99, 0x9a, 0xff, 0x41, 0x25, 0x32, 0x6f, 0xb6, 0x41, + 0x57, 0xc1, 0xa3, 0x1a, 0x94, 0x6f, 0x07, 0xd7, 0x83, 0x9b, 0x7f, 0x06, 0xf5, 0x0d, 0x54, 0x81, + 0xd2, 0xa0, 0xf3, 0x57, 0xaf, 0xae, 0xa1, 0x5d, 0xd8, 0xee, 0x77, 0x86, 0x7f, 0x8f, 0x70, 0xaf, + 0xdf, 0xeb, 0x0c, 0x7b, 0xe7, 0xf5, 0x82, 0xf9, 0x2d, 0x54, 0xe3, 0xa8, 0x50, 0x19, 0x8a, 0x9d, + 0x61, 0x57, 0x3d, 0x39, 0xef, 0xf1, 0x93, 0x66, 0xbe, 0xd6, 0x60, 0x2f, 0x59, 0x04, 0x3a, 0xf7, + 0x3d, 0x4a, 0x44, 0x15, 0xc6, 0xfe, 0xc2, 0x8b, 0xab, 0x20, 0x05, 0x84, 0xa0, 0xe4, 0x91, 0x57, + 0x51, 0x0d, 0xe4, 0x59, 0x20, 0x99, 0xcf, 0x6c, 0x57, 0xf2, 0xcf, 0x91, 0x52, 0x40, 0xbf, 0x41, + 0x25, 0x4c, 0x8e, 0x72, 0x66, 0x8b, 0xc7, 0xb5, 0xf6, 0x7e, 0x32, 0xe5, 0xd0, 0x23, 0x8e, 0x61, + 0xe6, 0x29, 0x34, 0x2e, 0x48, 0x14, 0x89, 0x62, 0x24, 0xea, 0x09, 0xe1, 0xd7, 0x7e, 0x20, 0x32, + 0x18, 0xe1, 0x97, 0x9f, 0x4d, 0x06, 0xcd, 0x75, 0x78, 0x18, 0x7d, 0x0a, 0x1e, 0xfd, 0x00, 0x25, + 0xd1, 0x98, 0x32, 0xf6, 0x5a, 0x1b, 0x25, 0xa3, 0xb9, 0xe2, 0x1a, 0x2c, 0xf5, 0xe8, 0x6b, 0xa8, + 0x0a, 0x3c, 0x9d, 0xdb, 0x63, 0x22, 0x73, 0xaa, 0xe2, 0x8f, 0x17, 0xa6, 0xb5, 0xec, 0xb5, 0xeb, + 0x7b, 0x8c, 0x78, 0xec, 0x53, 0x51, 0xf6, 0xe1, 0x30, 0x05, 0x1f, 0x86, 0xd9, 0x82, 0x72, 0x18, + 0x80, 0x7c, 0x93, 0xc9, 0x51, 0x84, 0x32, 0xdf, 0xf1, 0x72, 0xdd, 0xce, 0x27, 0x36, 0x23, 0x91, + 0x2a, 0xdb, 0x35, 0xfa, 0x91, 0x97, 0x50, 0x8c, 0x71, 0x98, 0xf1, 0xae, 0xb2, 0xad, 0x66, 0xbd, + 0x2b, 0xbe, 0x58, 0xe9, 0xd1, 0x09, 0xe8, 0x8f, 0xb6, 0xcb, 0xed, 0xc8, 0x74, 0x63, 0x6e, 0x42, + 0xa4, 0xdc, 0x01, 0x38, 0x44, 0xa0, 0x06, 0x94, 0x27, 0xc1, 0xd3, 0x28, 0x58, 0x78, 0x72, 0x60, + 0x2a, 0x58, 0xe7, 0x22, 0x5e, 0x78, 0xe8, 0x3b, 0xd8, 0x9e, 0x38, 0xd4, 0xbe, 0x73, 0xc9, 0x68, + 0xe6, 0xfb, 0x2f, 0xa8, 0x9c, 0x99, 0x0a, 0xde, 0x0a, 0x2f, 0x2f, 0xc5, 0x9d, 0x79, 0x09, 0xfb, + 0x2b, 0xe1, 0x3f, 0x97, 0x89, 0xf7, 0x1a, 0xec, 0x5f, 0x79, 0x7c, 0x54, 0x5c, 0x77, 0x85, 0x8a, + 0x38, 0x6d, 0x2d, 0x77, 0xda, 0x85, 0x2f, 0x49, 0xbb, 0x98, 0x48, 0x3b, 0x22, 0xbe, 0xb4, 0x44, + 0x7c, 0x1e, 0x2a, 0x92, 0x6d, 0xa6, 0xaf, 0xb4, 0x19, 0xfa, 0x06, 0x20, 0x20, 0x0b, 0x4a, 0x46, + 0xd2, 0x78, 0x59, 0xbe, 0xaf, 0xca, 0x9b, 0x81, 0xe8, 0xaa, 0x2b, 0x38, 0x58, 0x4d, 0xfe, 0xb9, + 0x44, 0xce, 0xa0, 0x71, 0xeb, 0x39, 0xa9, 0x4c, 0xa6, 0x35, 0xd5, 0x5a, 0x6e, 0x85, 0x94, 0xdc, + 0xf8, 0x4a, 0x98, 0x2f, 0x82, 0x7b, 0x12, 0x72, 0xa5, 0x04, 0xf3, 0x1a, 0x9a, 0xeb, 0x9e, 0x9e, + 0x19, 0x76, 0xfb, 0xed, 0x26, 0xec, 0x44, 0xb3, 0xaf, 0xf6, 0x31, 0x72, 0x60, 0x6b, 0x79, 0x95, + 0xa1, 0x9f, 0xb2, 0xd7, 0xf5, 0xca, 0xdf, 0x1c, 0xe3, 0x24, 0x0f, 0x54, 0x85, 0x6a, 0x6e, 0xfc, + 0xaa, 0x21, 0x0a, 0xf5, 0xd5, 0xdd, 0x83, 0x4e, 0xd3, 0x6d, 0x64, 0xac, 0x34, 0xc3, 0xca, 0x0b, + 0x8f, 0xdc, 0xa2, 0x47, 0xd8, 0x5d, 0x5b, 0x25, 0xe8, 0xb3, 0x66, 0x92, 0x3b, 0xca, 0x68, 0xe5, + 0xc6, 0xc7, 0x7e, 0xff, 0x87, 0xed, 0xc4, 0xd0, 0xa2, 0x0c, 0xb6, 0xd2, 0x16, 0x93, 0xf1, 0x73, + 0x2e, 0x6c, 0xec, 0xeb, 0x01, 0x76, 0x92, 0x8d, 0x8d, 0x32, 0x0c, 0xa4, 0xce, 0xbe, 0xf1, 0x4b, + 0x3e, 0x70, 0xec, 0x8e, 0xd7, 0x71, 0xb5, 0x25, 0xb3, 0xea, 0x98, 0x31, 0x24, 0x59, 0x75, 0xcc, + 0xea, 0x74, 0x73, 0xe3, 0x0c, 0xfe, 0xad, 0x44, 0xe8, 0x3b, 0x5d, 0xfe, 0x2f, 0xf4, 0xfb, 0x87, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xde, 0x47, 0x5d, 0x04, 0xc0, 0x09, 0x00, 0x00, } From c2a86cfbd456f0c169d049e16fafa98f59f9a465 Mon Sep 17 00:00:00 2001 From: vaikas-google Date: Mon, 29 Aug 2016 13:31:22 -0700 Subject: [PATCH 048/183] fix a return statement with not enough arguments due to merge conflict --- cmd/tiller/release_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 835948cdd..f2c503eee 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -478,7 +478,7 @@ func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values // removed here. vs, err := s.getVersionSet() if err != nil { - return nil, nil, fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err) + return nil, nil, "", fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err) } hooks, manifests, err := sortManifests(files, vs) if err != nil { From 7c4cad5cf04c186b32da3052f91cf088954d4ce6 Mon Sep 17 00:00:00 2001 From: vaikas-google Date: Mon, 29 Aug 2016 13:36:41 -0700 Subject: [PATCH 049/183] address lint comments from changing from all caps to camelcase --- cmd/tiller/release_server.go | 6 +++--- cmd/tiller/release_server_test.go | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index f2c503eee..eb1cb158a 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -48,11 +48,11 @@ var srv *releaseServer // characters in length. See https://github.com/kubernetes/helm/issues/1071 const releaseNameMaxLen = 14 -// NOTES.txt suffix that we want to treat special. It goes through the templating engine +// NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine // but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually // wants to see this file after rendering in the status command. However, it must be a suffix // since there can be filepath in front of it. -const NOTES_FILE_SUFFIX = "NOTES.txt" +const notesFileSuffix = "NOTES.txt" func init() { srv = &releaseServer{ @@ -467,7 +467,7 @@ func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values // it in the sortHooks. notes := "" for k, v := range files { - if strings.HasSuffix(k, NOTES_FILE_SUFFIX) { + if strings.HasSuffix(k, notesFileSuffix) { notes = v delete(files, k) } diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 4fb6d9f51..3e2644c56 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -35,7 +35,7 @@ import ( "k8s.io/helm/pkg/storage/driver" ) -const NOTES_TEXT = "my notes here" +const notesText = "my notes here" var manifestWithHook = `apiVersion: v1 kind: ConfigMap @@ -244,7 +244,7 @@ func TestInstallReleaseWithNotes(t *testing.T) { Templates: []*chart.Template{ {Name: "hello", Data: []byte("hello: world")}, {Name: "hooks", Data: []byte(manifestWithHook)}, - {Name: "NOTES.txt", Data: []byte(NOTES_TEXT)}, + {Name: "NOTES.txt", Data: []byte(notesText)}, }, }, } @@ -273,8 +273,8 @@ func TestInstallReleaseWithNotes(t *testing.T) { t.Errorf("Unexpected manifest: %v", rel.Hooks[0].Manifest) } - if rel.Info.Status.Notes != NOTES_TEXT { - t.Fatalf("Expected '%s', got '%s'", NOTES_TEXT, rel.Info.Status.Notes) + if rel.Info.Status.Notes != notesText { + t.Fatalf("Expected '%s', got '%s'", notesText, rel.Info.Status.Notes) } if rel.Hooks[0].Events[0] != release.Hook_POST_INSTALL { @@ -309,7 +309,7 @@ func TestInstallReleaseWithNotesRendered(t *testing.T) { Templates: []*chart.Template{ {Name: "hello", Data: []byte("hello: world")}, {Name: "hooks", Data: []byte(manifestWithHook)}, - {Name: "NOTES.txt", Data: []byte(NOTES_TEXT + " {{.Release.Name}}")}, + {Name: "NOTES.txt", Data: []byte(notesText + " {{.Release.Name}}")}, }, }, } @@ -338,7 +338,7 @@ func TestInstallReleaseWithNotesRendered(t *testing.T) { t.Errorf("Unexpected manifest: %v", rel.Hooks[0].Manifest) } - expectedNotes := fmt.Sprintf("%s %s", NOTES_TEXT, res.Release.Name) + expectedNotes := fmt.Sprintf("%s %s", notesText, res.Release.Name) if rel.Info.Status.Notes != expectedNotes { t.Fatalf("Expected '%s', got '%s'", expectedNotes, rel.Info.Status.Notes) } From 406f0ab05cdd6b8b149fdad7bb0d6caa6d71d93c Mon Sep 17 00:00:00 2001 From: vaikas-google Date: Mon, 29 Aug 2016 17:14:55 -0700 Subject: [PATCH 050/183] print the status after install/upgrade Signed-off-by: vaikas-google --- cmd/helm/install.go | 7 +++++++ cmd/helm/status.go | 21 +++++++++++++-------- cmd/helm/upgrade.go | 7 +++++++ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 402c2a549..3365eace1 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -151,6 +151,13 @@ func (i *installCmd) run() error { i.printRelease(res.GetRelease()) + // Print the status like status command does + status, err := i.client.ReleaseStatus(res.GetRelease().Name) + if err != nil { + return prettyError(err) + } + PrintStatus(i.out, status) + return nil } diff --git a/cmd/helm/status.go b/cmd/helm/status.go index f4d8deaea..191f2572d 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/services" "k8s.io/helm/pkg/timeconv" ) @@ -66,16 +67,20 @@ func (s *statusCmd) run() error { return prettyError(err) } - fmt.Fprintf(s.out, "Last Deployed: %s\n", timeconv.String(res.Info.LastDeployed)) - fmt.Fprintf(s.out, "Namespace: %s\n", res.Namespace) - fmt.Fprintf(s.out, "Status: %s\n", res.Info.Status.Code) + PrintStatus(s.out, res) + return nil +} + +func PrintStatus(out io.Writer, res *services.GetReleaseStatusResponse) { + fmt.Fprintf(out, "Last Deployed: %s\n", timeconv.String(res.Info.LastDeployed)) + fmt.Fprintf(out, "Namespace: %s\n", res.Namespace) + fmt.Fprintf(out, "Status: %s\n", res.Info.Status.Code) if res.Info.Status.Details != nil { - fmt.Fprintf(s.out, "Details: %s\n", res.Info.Status.Details) + fmt.Fprintf(out, "Details: %s\n", res.Info.Status.Details) } - fmt.Fprintf(s.out, "\n") - fmt.Fprintf(s.out, "Resources:\n%s\n", res.Info.Status.Resources) + fmt.Fprintf(out, "\n") + fmt.Fprintf(out, "Resources:\n%s\n", res.Info.Status.Resources) if len(res.Info.Status.Notes) > 0 { - fmt.Fprintf(s.out, "Notes:\n%s\n", res.Info.Status.Notes) + fmt.Fprintf(out, "Notes:\n%s\n", res.Info.Status.Notes) } - return nil } diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 10251dadd..95b5e7901 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -131,6 +131,13 @@ func (u *upgradeCmd) run() error { success := u.release + " has been upgraded. Happy Helming!\n" fmt.Fprintf(u.out, success) + // Print the status like status command does + status, err := u.client.ReleaseStatus(u.release) + if err != nil { + return prettyError(err) + } + PrintStatus(u.out, status) + return nil } From 72e7b22999fa773078fd892b1c66ca18ad380bc0 Mon Sep 17 00:00:00 2001 From: joe2far Date: Tue, 30 Aug 2016 17:13:34 +0100 Subject: [PATCH 051/183] minor engine unit test fix --- pkg/engine/engine_test.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index ec19f8ded..ef71bbd0c 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -290,20 +290,24 @@ global: t.Fatalf("failed to render templates: %s", err) } - if out["top/"+outerpath] != "Gather ye rosebuds while ye may" { - t.Errorf("Unexpected outer: %q", out[outerpath]) + fullouterpath := "top/" + outerpath + if out[fullouterpath] != "Gather ye rosebuds while ye may" { + t.Errorf("Unexpected outer: %q", out[fullouterpath]) } - if out["top/charts/herrick/"+innerpath] != "Old time is still a-flyin'" { - t.Errorf("Unexpected inner: %q", out[innerpath]) + fullinnerpath := "top/charts/herrick/" + innerpath + if out[fullinnerpath] != "Old time is still a-flyin'" { + t.Errorf("Unexpected inner: %q", out[fullinnerpath]) } - if out["top/charts/herrick/charts/deepest/"+deepestpath] != "And this same flower that smiles to-day" { - t.Errorf("Unexpected deepest: %q", out[deepestpath]) + fulldeepestpath := "top/charts/herrick/charts/deepest/" + deepestpath + if out[fulldeepestpath] != "And this same flower that smiles to-day" { + t.Errorf("Unexpected deepest: %q", out[fulldeepestpath]) } - if out["top/charts/herrick/charts/deepest/"+checkrelease] != "Tomorrow will be dyin" { - t.Errorf("Unexpected release: %q", out[checkrelease]) + fullcheckrelease := "top/charts/herrick/charts/deepest/" + checkrelease + if out[fullcheckrelease] != "Tomorrow will be dyin" { + t.Errorf("Unexpected release: %q", out[fullcheckrelease]) } } From 6b9c9c57431fed7cc6f162bdcd3aedc4df1c869a Mon Sep 17 00:00:00 2001 From: vaikas-google Date: Tue, 30 Aug 2016 10:30:37 -0700 Subject: [PATCH 052/183] fix unit tests that I had b0rked before. Small code cleanup --- cmd/helm/helm_test.go | 10 +++++++++- cmd/helm/install.go | 9 ++++++--- cmd/helm/status.go | 6 +++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 9c62fe9dd..71c0aee1d 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -18,6 +18,7 @@ package main import ( "bytes" + "fmt" "io" "math/rand" "regexp" @@ -134,7 +135,14 @@ func (c *fakeReleaseClient) DeleteRelease(rlsName string, opts ...helm.DeleteOpt } func (c *fakeReleaseClient) ReleaseStatus(rlsName string, opts ...helm.StatusOption) (*rls.GetReleaseStatusResponse, error) { - return nil, nil + if c.rels[0] != nil { + return &rls.GetReleaseStatusResponse{ + Name: c.rels[0].Name, + Info: c.rels[0].Info, + Namespace: c.rels[0].Namespace, + }, nil + } + return nil, fmt.Errorf("No such release: %s", rlsName) } func (c *fakeReleaseClient) UpdateRelease(rlsName string, chStr string, opts ...helm.UpdateOption) (*rls.UpdateReleaseResponse, error) { diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 3365eace1..ee3232d64 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -149,15 +149,18 @@ func (i *installCmd) run() error { return prettyError(err) } - i.printRelease(res.GetRelease()) + rel := res.GetRelease() + if rel == nil { + return nil + } + i.printRelease(rel) // Print the status like status command does - status, err := i.client.ReleaseStatus(res.GetRelease().Name) + status, err := i.client.ReleaseStatus(rel.Name) if err != nil { return prettyError(err) } PrintStatus(i.out, status) - return nil } diff --git a/cmd/helm/status.go b/cmd/helm/status.go index 191f2572d..4d8c7bc5c 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -71,8 +71,12 @@ func (s *statusCmd) run() error { return nil } +// PrintStatus prints out the status of a release. Shared because also used by +// install / upgrade func PrintStatus(out io.Writer, res *services.GetReleaseStatusResponse) { - fmt.Fprintf(out, "Last Deployed: %s\n", timeconv.String(res.Info.LastDeployed)) + if res.Info.LastDeployed != nil { + fmt.Fprintf(out, "Last Deployed: %s\n", timeconv.String(res.Info.LastDeployed)) + } fmt.Fprintf(out, "Namespace: %s\n", res.Namespace) fmt.Fprintf(out, "Status: %s\n", res.Info.Status.Code) if res.Info.Status.Details != nil { From fefa00fc5279802266ce22a118905f2e2e2a9db7 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 31 Aug 2016 10:26:47 -0500 Subject: [PATCH 053/183] fix(tiller): return status for deleted release This modifies `helm status` to return info about deleted and failed releases. We do our best to retrieve info for releases that were partially deployed. --- cmd/helm/status.go | 4 +++- cmd/tiller/release_server.go | 11 ++++++++--- cmd/tiller/release_server_test.go | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/cmd/helm/status.go b/cmd/helm/status.go index 4d8c7bc5c..a3403a824 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -83,7 +83,9 @@ func PrintStatus(out io.Writer, res *services.GetReleaseStatusResponse) { fmt.Fprintf(out, "Details: %s\n", res.Info.Status.Details) } fmt.Fprintf(out, "\n") - fmt.Fprintf(out, "Resources:\n%s\n", res.Info.Status.Resources) + if len(res.Info.Status.Resources) > 0 { + fmt.Fprintf(out, "Resources:\n%s\n", res.Info.Status.Resources) + } if len(res.Info.Status.Notes) > 0 { fmt.Fprintf(out, "Notes:\n%s\n", res.Info.Status.Notes) } diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index eb1cb158a..757c1bf95 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -190,17 +190,22 @@ func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetRelease return nil, errors.New("release chart is missing") } + sc := rel.Info.Status.Code + statusResp := &services.GetReleaseStatusResponse{Info: rel.Info, Namespace: rel.Namespace} + // Ok, we got the status of the release as we had jotted down, now we need to match the // manifest we stashed away with reality from the cluster. kubeCli := s.env.KubeClient resp, err := kubeCli.Get(rel.Namespace, bytes.NewBufferString(rel.Manifest)) - if err != nil { + if sc == release.Status_DELETED || sc == release.Status_FAILED { + // Skip errors if this is already deleted or failed. + return statusResp, nil + } else if err != nil { log.Printf("warning: Get for %s failed: %v", rel.Name, err) return nil, err } rel.Info.Status.Resources = resp - - return &services.GetReleaseStatusResponse{Info: rel.Info, Namespace: rel.Namespace}, nil + return statusResp, nil } func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleaseContentRequest) (*services.GetReleaseContentResponse, error) { diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 3e2644c56..27f924f2d 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -711,6 +711,25 @@ func TestGetReleaseStatus(t *testing.T) { } } +func TestGetReleaseStatusDeleted(t *testing.T) { + c := context.Background() + rs := rsFixture() + rel := releaseStub() + rel.Info.Status.Code = release.Status_DELETED + if err := rs.env.Releases.Create(rel); err != nil { + t.Fatalf("Could not store mock release: %s", err) + } + + res, err := rs.GetReleaseStatus(c, &services.GetReleaseStatusRequest{Name: rel.Name}) + if err != nil { + t.Errorf("Error getting release content: %s", err) + } + + if res.Info.Status.Code != release.Status_DELETED { + t.Errorf("Expected %d, got %d", release.Status_DELETED, res.Info.Status.Code) + } +} + func TestListReleases(t *testing.T) { rs := rsFixture() num := 7 From 5b7e841faf0b1bf8755b852e55e2e61a9cbd03b9 Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Thu, 1 Sep 2016 13:08:59 -0600 Subject: [PATCH 054/183] chore(tiller): delete redundant testing of storage in pkg/environment --- cmd/tiller/environment/environment_test.go | 75 ---------------------- 1 file changed, 75 deletions(-) diff --git a/cmd/tiller/environment/environment_test.go b/cmd/tiller/environment/environment_test.go index 236df6a2b..68cc5d63c 100644 --- a/cmd/tiller/environment/environment_test.go +++ b/cmd/tiller/environment/environment_test.go @@ -23,9 +23,6 @@ import ( "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/storage" - "k8s.io/helm/pkg/storage/driver" unversionedclient "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/client/unversioned/testclient" ) @@ -38,52 +35,6 @@ func (e *mockEngine) Render(chrt *chart.Chart, v chartutil.Values) (map[string]s return e.out, nil } -type mockReleaseStorage struct { - rel *release.Release -} - -var _ driver.Driver = (*mockReleaseStorage)(nil) - -func (r *mockReleaseStorage) Create(v *release.Release) error { - r.rel = v - return nil -} - -func (r *mockReleaseStorage) Name() string { - return "mockReleaseStorage" -} - -func (r *mockReleaseStorage) Get(k string) (*release.Release, error) { - return r.rel, nil -} - -func (r *mockReleaseStorage) Update(v *release.Release) error { - r.rel = v - return nil -} - -func (r *mockReleaseStorage) Delete(k string) (*release.Release, error) { - return r.rel, nil -} - -func (r *mockReleaseStorage) List(func(*release.Release) bool) ([]*release.Release, error) { - return []*release.Release{}, nil -} - -func (r *mockReleaseStorage) Query(labels map[string]string) ([]*release.Release, error) { - return []*release.Release{}, nil -} - -func (r *mockReleaseStorage) History(n string) ([]*release.Release, error) { - res := []*release.Release{} - rel, err := r.Get(n) - if err != nil { - return res, err - } - res = append(res, rel) - return res, nil -} - type mockKubeClient struct { } @@ -126,32 +77,6 @@ func TestEngine(t *testing.T) { } } -func TestReleaseStorage(t *testing.T) { - rs := &mockReleaseStorage{} - env := New() - env.Releases = storage.Init(rs) - - release := &release.Release{Name: "mariner"} - - if err := env.Releases.Create(release); err != nil { - t.Fatalf("failed to store release: %s", err) - } - - if err := env.Releases.Update(release); err != nil { - t.Fatalf("failed to update release: %s", err) - } - - if v, err := env.Releases.Get("albatross"); err != nil { - t.Errorf("Error fetching release: %s", err) - } else if v.Name != "mariner" { - t.Errorf("Expected mariner, got %q", v.Name) - } - - if _, err := env.Releases.Delete("albatross"); err != nil { - t.Fatalf("failed to delete release: %s", err) - } -} - func TestKubeClient(t *testing.T) { kc := &mockKubeClient{} env := New() From f622672bf80f073a145bb00e187817b17eb9355e Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Thu, 1 Sep 2016 13:37:56 -0600 Subject: [PATCH 055/183] feat(rollback-support): add version field to protos to support querying by (release_name, version) --- _proto/hapi/services/tiller.proto | 4 + pkg/proto/hapi/services/tiller.pb.go | 111 ++++++++++++++------------- 2 files changed, 62 insertions(+), 53 deletions(-) diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto index 873a41ee2..4d5da0085 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -133,6 +133,8 @@ message ListReleasesResponse { message GetReleaseStatusRequest { // Name is the name of the release string name = 1; + // Version is the version of the release + int32 version = 2; } // GetReleaseStatusResponse is the response indicating the status of the named release. @@ -151,6 +153,8 @@ message GetReleaseStatusResponse { message GetReleaseContentRequest { // The name of the release string name = 1; + // Version is the version of the release + int32 version = 2; } // GetReleaseContentResponse is a response containing the contents of a release. diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index 5e87e524b..33ed1133d 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -163,6 +163,8 @@ func (m *ListReleasesResponse) GetReleases() []*hapi_release3.Release { type GetReleaseStatusRequest struct { // Name is the name of the release Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + // Version is the version of the release + Version int32 `protobuf:"varint,2,opt,name=version" json:"version,omitempty"` } func (m *GetReleaseStatusRequest) Reset() { *m = GetReleaseStatusRequest{} } @@ -196,6 +198,8 @@ func (m *GetReleaseStatusResponse) GetInfo() *hapi_release2.Info { type GetReleaseContentRequest struct { // The name of the release Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + // Version is the version of the release + Version int32 `protobuf:"varint,2,opt,name=version" json:"version,omitempty"` } func (m *GetReleaseContentRequest) Reset() { *m = GetReleaseContentRequest{} } @@ -633,57 +637,58 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ } var fileDescriptor0 = []byte{ - // 829 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdd, 0x4e, 0xe3, 0x46, - 0x14, 0xc6, 0x49, 0x70, 0x92, 0x13, 0x40, 0x61, 0x0a, 0x24, 0x58, 0x6d, 0x85, 0x5c, 0xb5, 0xa5, - 0xb4, 0x38, 0x6d, 0x7a, 0x5b, 0x55, 0x0a, 0x21, 0x02, 0x44, 0x1a, 0xa4, 0x49, 0x51, 0xa5, 0x5e, - 0x34, 0x32, 0xc9, 0x84, 0xb8, 0x6b, 0xec, 0xac, 0x67, 0x82, 0x96, 0x47, 0xd8, 0xd7, 0xd8, 0xa7, - 0xd8, 0x9b, 0x7d, 0xb2, 0xbd, 0xd9, 0xf9, 0xb1, 0xbd, 0x71, 0x62, 0xef, 0x7a, 0xb9, 0x71, 0xe6, - 0xcc, 0xf9, 0xe6, 0xfc, 0x7c, 0xe7, 0x07, 0xc0, 0x98, 0xd9, 0x73, 0xa7, 0x45, 0x49, 0xf0, 0xe8, - 0x8c, 0x09, 0x6d, 0x31, 0xc7, 0x75, 0x49, 0x60, 0xcd, 0x03, 0x9f, 0xf9, 0x68, 0x4f, 0xe8, 0xac, - 0x48, 0x67, 0x29, 0x9d, 0x71, 0x20, 0x5f, 0x8c, 0x67, 0x76, 0xc0, 0xd4, 0x57, 0xa1, 0x8d, 0xc6, - 0xf2, 0xbd, 0xef, 0x4d, 0x9d, 0xfb, 0x50, 0xa1, 0x5c, 0x04, 0xc4, 0x25, 0x36, 0x25, 0xd1, 0x6f, - 0xe2, 0x51, 0xa4, 0x73, 0xbc, 0xa9, 0x1f, 0x2a, 0x0e, 0x13, 0x0a, 0xca, 0x6c, 0xb6, 0xa0, 0x4a, - 0x65, 0xbe, 0x29, 0xc0, 0x57, 0x7d, 0x87, 0x32, 0xac, 0x94, 0x14, 0x93, 0x97, 0x0b, 0x42, 0x19, - 0xda, 0x83, 0x4d, 0xd7, 0x79, 0x70, 0x58, 0x53, 0x3b, 0xd2, 0x8e, 0x8b, 0x58, 0x09, 0xe8, 0x00, - 0x74, 0x7f, 0x3a, 0xa5, 0x84, 0x35, 0x0b, 0xfc, 0xba, 0x8a, 0x43, 0x09, 0xfd, 0x09, 0x65, 0xea, - 0x07, 0x6c, 0x74, 0xf7, 0xd4, 0x2c, 0x72, 0xc5, 0x4e, 0xfb, 0x7b, 0x2b, 0x2d, 0x5d, 0x4b, 0x78, - 0x1a, 0x72, 0xa0, 0x25, 0x3e, 0x67, 0x4f, 0x58, 0xa7, 0xf2, 0x57, 0xd8, 0x9d, 0x3a, 0x2e, 0x23, - 0x41, 0xb3, 0xa4, 0xec, 0x2a, 0x09, 0x5d, 0x00, 0x48, 0xbb, 0x7e, 0x30, 0xe1, 0xba, 0x4d, 0x69, - 0xfa, 0x38, 0x87, 0xe9, 0x1b, 0x81, 0xc7, 0x55, 0x1a, 0x1d, 0xd1, 0x1f, 0xb0, 0xa5, 0xd2, 0x1e, - 0x8d, 0xfd, 0x09, 0xa1, 0x4d, 0xfd, 0xa8, 0xc8, 0x4d, 0x1d, 0x2a, 0x53, 0x11, 0x8b, 0x43, 0x45, - 0x4c, 0x97, 0x23, 0x70, 0x4d, 0xc1, 0xc5, 0x99, 0x9a, 0xff, 0x41, 0x25, 0x32, 0x6f, 0xb6, 0x41, - 0x57, 0xc1, 0xa3, 0x1a, 0x94, 0x6f, 0x07, 0xd7, 0x83, 0x9b, 0x7f, 0x06, 0xf5, 0x0d, 0x54, 0x81, - 0xd2, 0xa0, 0xf3, 0x57, 0xaf, 0xae, 0xa1, 0x5d, 0xd8, 0xee, 0x77, 0x86, 0x7f, 0x8f, 0x70, 0xaf, - 0xdf, 0xeb, 0x0c, 0x7b, 0xe7, 0xf5, 0x82, 0xf9, 0x2d, 0x54, 0xe3, 0xa8, 0x50, 0x19, 0x8a, 0x9d, - 0x61, 0x57, 0x3d, 0x39, 0xef, 0xf1, 0x93, 0x66, 0xbe, 0xd6, 0x60, 0x2f, 0x59, 0x04, 0x3a, 0xf7, - 0x3d, 0x4a, 0x44, 0x15, 0xc6, 0xfe, 0xc2, 0x8b, 0xab, 0x20, 0x05, 0x84, 0xa0, 0xe4, 0x91, 0x57, - 0x51, 0x0d, 0xe4, 0x59, 0x20, 0x99, 0xcf, 0x6c, 0x57, 0xf2, 0xcf, 0x91, 0x52, 0x40, 0xbf, 0x41, - 0x25, 0x4c, 0x8e, 0x72, 0x66, 0x8b, 0xc7, 0xb5, 0xf6, 0x7e, 0x32, 0xe5, 0xd0, 0x23, 0x8e, 0x61, - 0xe6, 0x29, 0x34, 0x2e, 0x48, 0x14, 0x89, 0x62, 0x24, 0xea, 0x09, 0xe1, 0xd7, 0x7e, 0x20, 0x32, - 0x18, 0xe1, 0x97, 0x9f, 0x4d, 0x06, 0xcd, 0x75, 0x78, 0x18, 0x7d, 0x0a, 0x1e, 0xfd, 0x00, 0x25, - 0xd1, 0x98, 0x32, 0xf6, 0x5a, 0x1b, 0x25, 0xa3, 0xb9, 0xe2, 0x1a, 0x2c, 0xf5, 0xe8, 0x6b, 0xa8, - 0x0a, 0x3c, 0x9d, 0xdb, 0x63, 0x22, 0x73, 0xaa, 0xe2, 0x8f, 0x17, 0xa6, 0xb5, 0xec, 0xb5, 0xeb, - 0x7b, 0x8c, 0x78, 0xec, 0x53, 0x51, 0xf6, 0xe1, 0x30, 0x05, 0x1f, 0x86, 0xd9, 0x82, 0x72, 0x18, - 0x80, 0x7c, 0x93, 0xc9, 0x51, 0x84, 0x32, 0xdf, 0xf1, 0x72, 0xdd, 0xce, 0x27, 0x36, 0x23, 0x91, - 0x2a, 0xdb, 0x35, 0xfa, 0x91, 0x97, 0x50, 0x8c, 0x71, 0x98, 0xf1, 0xae, 0xb2, 0xad, 0x66, 0xbd, - 0x2b, 0xbe, 0x58, 0xe9, 0xd1, 0x09, 0xe8, 0x8f, 0xb6, 0xcb, 0xed, 0xc8, 0x74, 0x63, 0x6e, 0x42, - 0xa4, 0xdc, 0x01, 0x38, 0x44, 0xa0, 0x06, 0x94, 0x27, 0xc1, 0xd3, 0x28, 0x58, 0x78, 0x72, 0x60, - 0x2a, 0x58, 0xe7, 0x22, 0x5e, 0x78, 0xe8, 0x3b, 0xd8, 0x9e, 0x38, 0xd4, 0xbe, 0x73, 0xc9, 0x68, - 0xe6, 0xfb, 0x2f, 0xa8, 0x9c, 0x99, 0x0a, 0xde, 0x0a, 0x2f, 0x2f, 0xc5, 0x9d, 0x79, 0x09, 0xfb, - 0x2b, 0xe1, 0x3f, 0x97, 0x89, 0xf7, 0x1a, 0xec, 0x5f, 0x79, 0x7c, 0x54, 0x5c, 0x77, 0x85, 0x8a, - 0x38, 0x6d, 0x2d, 0x77, 0xda, 0x85, 0x2f, 0x49, 0xbb, 0x98, 0x48, 0x3b, 0x22, 0xbe, 0xb4, 0x44, - 0x7c, 0x1e, 0x2a, 0x92, 0x6d, 0xa6, 0xaf, 0xb4, 0x19, 0xfa, 0x06, 0x20, 0x20, 0x0b, 0x4a, 0x46, - 0xd2, 0x78, 0x59, 0xbe, 0xaf, 0xca, 0x9b, 0x81, 0xe8, 0xaa, 0x2b, 0x38, 0x58, 0x4d, 0xfe, 0xb9, - 0x44, 0xce, 0xa0, 0x71, 0xeb, 0x39, 0xa9, 0x4c, 0xa6, 0x35, 0xd5, 0x5a, 0x6e, 0x85, 0x94, 0xdc, - 0xf8, 0x4a, 0x98, 0x2f, 0x82, 0x7b, 0x12, 0x72, 0xa5, 0x04, 0xf3, 0x1a, 0x9a, 0xeb, 0x9e, 0x9e, - 0x19, 0x76, 0xfb, 0xed, 0x26, 0xec, 0x44, 0xb3, 0xaf, 0xf6, 0x31, 0x72, 0x60, 0x6b, 0x79, 0x95, - 0xa1, 0x9f, 0xb2, 0xd7, 0xf5, 0xca, 0xdf, 0x1c, 0xe3, 0x24, 0x0f, 0x54, 0x85, 0x6a, 0x6e, 0xfc, - 0xaa, 0x21, 0x0a, 0xf5, 0xd5, 0xdd, 0x83, 0x4e, 0xd3, 0x6d, 0x64, 0xac, 0x34, 0xc3, 0xca, 0x0b, - 0x8f, 0xdc, 0xa2, 0x47, 0xd8, 0x5d, 0x5b, 0x25, 0xe8, 0xb3, 0x66, 0x92, 0x3b, 0xca, 0x68, 0xe5, - 0xc6, 0xc7, 0x7e, 0xff, 0x87, 0xed, 0xc4, 0xd0, 0xa2, 0x0c, 0xb6, 0xd2, 0x16, 0x93, 0xf1, 0x73, - 0x2e, 0x6c, 0xec, 0xeb, 0x01, 0x76, 0x92, 0x8d, 0x8d, 0x32, 0x0c, 0xa4, 0xce, 0xbe, 0xf1, 0x4b, - 0x3e, 0x70, 0xec, 0x8e, 0xd7, 0x71, 0xb5, 0x25, 0xb3, 0xea, 0x98, 0x31, 0x24, 0x59, 0x75, 0xcc, - 0xea, 0x74, 0x73, 0xe3, 0x0c, 0xfe, 0xad, 0x44, 0xe8, 0x3b, 0x5d, 0xfe, 0x2f, 0xf4, 0xfb, 0x87, - 0x00, 0x00, 0x00, 0xff, 0xff, 0xde, 0x47, 0x5d, 0x04, 0xc0, 0x09, 0x00, 0x00, + // 841 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdd, 0x6e, 0xeb, 0x44, + 0x10, 0xae, 0xf3, 0xe3, 0x24, 0x93, 0xb6, 0x4a, 0x97, 0xb6, 0x71, 0x2d, 0x40, 0x95, 0x11, 0x50, + 0x0a, 0x24, 0x10, 0x6e, 0x11, 0x52, 0x9a, 0x46, 0x6d, 0xd5, 0x90, 0x4a, 0x1b, 0x2a, 0x24, 0x2e, + 0x88, 0xdc, 0x64, 0xd3, 0x18, 0x5c, 0x3b, 0x78, 0x37, 0x11, 0x7d, 0x04, 0x5e, 0x83, 0xa7, 0xe0, + 0x86, 0x27, 0xe3, 0x86, 0xf5, 0xae, 0xd7, 0x27, 0x4e, 0xec, 0x73, 0x7c, 0x72, 0xe3, 0xec, 0xee, + 0x7c, 0xfb, 0xcd, 0xcc, 0x37, 0xb3, 0xd3, 0x82, 0x39, 0xb7, 0x17, 0x4e, 0x9b, 0x92, 0x60, 0xe5, + 0x4c, 0x08, 0x6d, 0x33, 0xc7, 0x75, 0x49, 0xd0, 0x5a, 0x04, 0x3e, 0xf3, 0xd1, 0x71, 0x68, 0x6b, + 0x29, 0x5b, 0x4b, 0xda, 0xcc, 0x53, 0x71, 0x63, 0x32, 0xb7, 0x03, 0x26, 0xbf, 0x12, 0x6d, 0x36, + 0xd7, 0xcf, 0x7d, 0x6f, 0xe6, 0x3c, 0x47, 0x06, 0xe9, 0x22, 0x20, 0x2e, 0xb1, 0x29, 0x51, 0xbf, + 0x89, 0x4b, 0xca, 0xe6, 0x78, 0x33, 0x3f, 0x32, 0x9c, 0x25, 0x0c, 0x94, 0xd9, 0x6c, 0x49, 0xa5, + 0xc9, 0xfa, 0xbb, 0x00, 0x1f, 0x0c, 0x1c, 0xca, 0xb0, 0x34, 0x52, 0x4c, 0xfe, 0x58, 0x12, 0xca, + 0xd0, 0x31, 0x94, 0x5d, 0xe7, 0xc5, 0x61, 0x86, 0x76, 0xae, 0x5d, 0x14, 0xb1, 0xdc, 0xa0, 0x53, + 0xd0, 0xfd, 0xd9, 0x8c, 0x12, 0x66, 0x14, 0xf8, 0x71, 0x0d, 0x47, 0x3b, 0xf4, 0x03, 0x54, 0xa8, + 0x1f, 0xb0, 0xf1, 0xd3, 0xab, 0x51, 0xe4, 0x86, 0xc3, 0xce, 0xa7, 0xad, 0xb4, 0x74, 0x5b, 0xa1, + 0xa7, 0x11, 0x07, 0xb6, 0xc2, 0xcf, 0xd5, 0x2b, 0xd6, 0xa9, 0xf8, 0x0d, 0x79, 0x67, 0x8e, 0xcb, + 0x48, 0x60, 0x94, 0x24, 0xaf, 0xdc, 0xa1, 0x1b, 0x00, 0xc1, 0xeb, 0x07, 0x53, 0x6e, 0x2b, 0x0b, + 0xea, 0x8b, 0x1c, 0xd4, 0x0f, 0x21, 0x1e, 0xd7, 0xa8, 0x5a, 0xa2, 0xef, 0x61, 0x5f, 0xa6, 0x3d, + 0x9e, 0xf8, 0x53, 0x42, 0x0d, 0xfd, 0xbc, 0xc8, 0xa9, 0xce, 0x24, 0x95, 0x52, 0x71, 0x24, 0x85, + 0xe9, 0x71, 0x04, 0xae, 0x4b, 0x78, 0xb8, 0xa6, 0xd6, 0xaf, 0x50, 0x55, 0xf4, 0x56, 0x07, 0x74, + 0x19, 0x3c, 0xaa, 0x43, 0xe5, 0x71, 0x78, 0x3f, 0x7c, 0xf8, 0x79, 0xd8, 0xd8, 0x43, 0x55, 0x28, + 0x0d, 0xbb, 0x3f, 0xf6, 0x1b, 0x1a, 0x3a, 0x82, 0x83, 0x41, 0x77, 0xf4, 0xd3, 0x18, 0xf7, 0x07, + 0xfd, 0xee, 0xa8, 0x7f, 0xdd, 0x28, 0x58, 0x1f, 0x43, 0x2d, 0x8e, 0x0a, 0x55, 0xa0, 0xd8, 0x1d, + 0xf5, 0xe4, 0x95, 0xeb, 0x3e, 0x5f, 0x69, 0xd6, 0x5f, 0x1a, 0x1c, 0x27, 0x8b, 0x40, 0x17, 0xbe, + 0x47, 0x49, 0x58, 0x85, 0x89, 0xbf, 0xf4, 0xe2, 0x2a, 0x88, 0x0d, 0x42, 0x50, 0xf2, 0xc8, 0x9f, + 0xaa, 0x06, 0x62, 0x1d, 0x22, 0x99, 0xcf, 0x6c, 0x57, 0xe8, 0xcf, 0x91, 0x62, 0x83, 0xbe, 0x85, + 0x6a, 0x94, 0x1c, 0xe5, 0xca, 0x16, 0x2f, 0xea, 0x9d, 0x93, 0x64, 0xca, 0x91, 0x47, 0x1c, 0xc3, + 0xac, 0x1b, 0x68, 0xde, 0x10, 0x15, 0x89, 0x54, 0x44, 0xf5, 0x44, 0xe8, 0xd7, 0x7e, 0x21, 0x22, + 0x98, 0xd0, 0x2f, 0x5f, 0x23, 0x03, 0x2a, 0x2b, 0x12, 0x50, 0xc7, 0xf7, 0x44, 0x38, 0x65, 0xac, + 0xb6, 0x16, 0x03, 0x63, 0x9b, 0x28, 0xca, 0x2b, 0x8d, 0xe9, 0x33, 0x28, 0x85, 0x2d, 0x2b, 0x68, + 0xea, 0x1d, 0x94, 0x8c, 0xf3, 0x8e, 0x5b, 0xb0, 0xb0, 0xa3, 0x0f, 0xa1, 0x16, 0xe2, 0xe9, 0xc2, + 0x9e, 0x10, 0x91, 0x6d, 0x0d, 0xbf, 0x39, 0xb0, 0x6e, 0xd7, 0xbd, 0xf6, 0x7c, 0x8f, 0x11, 0x8f, + 0xed, 0x16, 0xff, 0x00, 0xce, 0x52, 0x98, 0xa2, 0x04, 0xda, 0x50, 0x89, 0x42, 0x13, 0x6c, 0x99, + 0xba, 0x2a, 0x94, 0xf5, 0x2f, 0x2f, 0xf1, 0xe3, 0x62, 0x6a, 0x33, 0xa2, 0x4c, 0x6f, 0x09, 0xea, + 0x73, 0x5e, 0xf6, 0xf0, 0xe9, 0x47, 0x5a, 0x1c, 0x49, 0x6e, 0x39, 0x1f, 0x7a, 0xe1, 0x17, 0x4b, + 0x3b, 0xba, 0x04, 0x7d, 0x65, 0xbb, 0x9c, 0x47, 0x08, 0x11, 0xab, 0x16, 0x21, 0xc5, 0xdc, 0xc0, + 0x11, 0x02, 0x35, 0xa1, 0x32, 0x0d, 0x5e, 0xc7, 0xc1, 0xd2, 0x13, 0x8f, 0xac, 0x8a, 0x75, 0xbe, + 0xc5, 0x4b, 0x0f, 0x7d, 0x02, 0x07, 0x53, 0x87, 0xda, 0x4f, 0x2e, 0x19, 0xcf, 0x7d, 0xff, 0x77, + 0x2a, 0xde, 0x59, 0x15, 0xef, 0x47, 0x87, 0xb7, 0xe1, 0x19, 0xd7, 0xf5, 0x64, 0x23, 0xfc, 0x5d, + 0x95, 0xf8, 0x4f, 0x83, 0x93, 0x3b, 0x8f, 0x3f, 0x2f, 0xd7, 0xdd, 0x90, 0x22, 0x4e, 0x5b, 0xcb, + 0x9d, 0x76, 0xe1, 0x7d, 0xd2, 0x2e, 0x26, 0xd2, 0x56, 0xc2, 0x97, 0xd6, 0x84, 0xcf, 0x23, 0x45, + 0xb2, 0x01, 0xf5, 0x8d, 0x06, 0x44, 0x1f, 0x01, 0x04, 0x64, 0x49, 0xc9, 0x58, 0x90, 0x57, 0xc4, + 0xfd, 0x9a, 0x38, 0x19, 0xf2, 0x03, 0xeb, 0x0e, 0x4e, 0x37, 0x93, 0xdf, 0x55, 0xc8, 0x39, 0x34, + 0x1f, 0x3d, 0x27, 0x55, 0xc9, 0xb4, 0xa6, 0xda, 0xca, 0xad, 0x90, 0x92, 0x1b, 0x1f, 0x23, 0x8b, + 0x65, 0xf0, 0x4c, 0x22, 0xad, 0xe4, 0xc6, 0xba, 0x07, 0x63, 0xdb, 0xd3, 0x8e, 0x61, 0x77, 0xfe, + 0x29, 0xc3, 0xa1, 0x9a, 0x0a, 0x72, 0x86, 0x23, 0x07, 0xf6, 0xd7, 0xc7, 0x1f, 0xfa, 0x22, 0x7b, + 0xc4, 0x6f, 0xfc, 0x9d, 0x32, 0x2f, 0xf3, 0x40, 0x65, 0xa8, 0xd6, 0xde, 0x37, 0x1a, 0xa2, 0xd0, + 0xd8, 0x9c, 0x4a, 0xe8, 0xeb, 0x74, 0x8e, 0x8c, 0x31, 0x68, 0xb6, 0xf2, 0xc2, 0x95, 0x5b, 0xb4, + 0x82, 0xa3, 0xad, 0x51, 0x82, 0xde, 0x49, 0x93, 0x9c, 0x5e, 0x66, 0x3b, 0x37, 0x3e, 0xf6, 0xfb, + 0x1b, 0x1c, 0x24, 0x1e, 0x2d, 0xca, 0x50, 0x2b, 0x6d, 0x30, 0x99, 0x5f, 0xe6, 0xc2, 0xc6, 0xbe, + 0x5e, 0xe0, 0x30, 0xd9, 0xd8, 0x28, 0x83, 0x20, 0xf5, 0xed, 0x9b, 0x5f, 0xe5, 0x03, 0xc7, 0xee, + 0x78, 0x1d, 0x37, 0x5b, 0x32, 0xab, 0x8e, 0x19, 0x8f, 0x24, 0xab, 0x8e, 0x59, 0x9d, 0x6e, 0xed, + 0x5d, 0xc1, 0x2f, 0x55, 0x85, 0x7e, 0xd2, 0xc5, 0xff, 0x4f, 0xdf, 0xfd, 0x1f, 0x00, 0x00, 0xff, + 0xff, 0xd9, 0x8f, 0xae, 0xcb, 0xf4, 0x09, 0x00, 0x00, } From c180c4a250808b7c92428832ed9a1030e0ceb665 Mon Sep 17 00:00:00 2001 From: Nic Roland Date: Thu, 1 Sep 2016 17:51:53 +0100 Subject: [PATCH 056/183] fix(tiller): Install --replace will result in an upgrade If a release has been deleted, `install --replace` will work but the release status will still be "deleted". This means that subsequest attempts to change the release will fail. Upgrading the release instead will prevent such zombie releases. Closes #1131 --- cmd/tiller/release_server.go | 18 ++++++++++++------ cmd/tiller/release_server_test.go | 9 +++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index eb1cb158a..7f11bf981 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -503,6 +503,16 @@ func validateYAML(data string) error { return yaml.Unmarshal([]byte(data), b) } +func (s *releaseServer) recordRelease(r *release.Release, reuse bool) { + if reuse { + if err := s.env.Releases.Update(r); err != nil { + log.Printf("warning: Failed to update release %q: %s", r.Name, err) + } + } else if err := s.env.Releases.Create(r); err != nil { + log.Printf("warning: Failed to record release %q: %s", r.Name, err) + } +} + // performRelease runs a release. func (s *releaseServer) performRelease(r *release.Release, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) { res := &services.InstallReleaseResponse{Release: r} @@ -525,9 +535,7 @@ func (s *releaseServer) performRelease(r *release.Release, req *services.Install if err := kubeCli.Create(r.Namespace, b); err != nil { r.Info.Status.Code = release.Status_FAILED log.Printf("warning: Release %q failed: %s", r.Name, err) - if err := s.env.Releases.Create(r); err != nil { - log.Printf("warning: Failed to record release %q: %s", r.Name, err) - } + s.recordRelease(r, req.ReuseName) return res, fmt.Errorf("release %s failed: %s", r.Name, err) } @@ -546,9 +554,7 @@ func (s *releaseServer) performRelease(r *release.Release, req *services.Install // One possible strategy would be to do a timed retry to see if we can get // this stored in the future. r.Info.Status.Code = release.Status_DEPLOYED - if err := s.env.Releases.Create(r); err != nil { - log.Printf("warning: Failed to record release %q: %s", r.Name, err) - } + s.recordRelease(r, req.ReuseName) return res, nil } diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 3e2644c56..c5bd01837 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -451,6 +451,15 @@ func TestInstallReleaseReuseName(t *testing.T) { if res.Release.Name != rel.Name { t.Errorf("expected %q, got %q", rel.Name, res.Release.Name) } + + getreq := &services.GetReleaseStatusRequest{Name: rel.Name} + getres, err := rs.GetReleaseStatus(c, getreq) + if err != nil { + t.Errorf("Failed to retrieve release: %s", err) + } + if getres.Info.Status.Code != release.Status_DEPLOYED { + t.Errorf("Release status is %q", getres.Info.Status.Code) + } } func TestUpdateRelease(t *testing.T) { From 1b3418d9e3c2f62810f058c3f64a460a9c79b339 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Fri, 2 Sep 2016 16:22:07 -0600 Subject: [PATCH 057/183] fix(helm): removed debug output Also fixed a bug where a `--dry-run` will result in an error because of the recently added status support. There are several other output inconsistencies that I noticed and filed as issue #1135. Closes #1130 --- cmd/helm/install.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index ee3232d64..a926b3c11 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -119,7 +119,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { func (i *installCmd) run() error { if flagDebug { - fmt.Printf("Chart path: %s\n", i.chartPath) + fmt.Fprintf(i.out, "Chart path: %s\n", i.chartPath) } rawVals, err := i.vals() @@ -134,7 +134,7 @@ func (i *installCmd) run() error { 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) + fmt.Printf("Final name: %s\n", i.name) } res, err := i.client.InstallRelease( @@ -155,6 +155,11 @@ func (i *installCmd) run() error { } i.printRelease(rel) + // If this is a dry run, we can't display status. + if i.dryRun { + return nil + } + // Print the status like status command does status, err := i.client.ReleaseStatus(rel.Name) if err != nil { @@ -250,7 +255,6 @@ func (v *values) Set(data string) error { } } } - fmt.Print(v.pairs) return nil } From 53b01949a86f9d0f8bdef5207f100b079dbe4272 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Fri, 2 Sep 2016 17:59:59 -0600 Subject: [PATCH 058/183] fix(tiller): store failed release on post-inst failure This fixes a bug where post-install hooks did not result in recording a failure. --- cmd/tiller/release_server.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 00af55e5c..c20bc4d50 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -538,8 +538,8 @@ func (s *releaseServer) performRelease(r *release.Release, req *services.Install kubeCli := s.env.KubeClient b := bytes.NewBufferString(r.Manifest) if err := kubeCli.Create(r.Namespace, b); err != nil { - r.Info.Status.Code = release.Status_FAILED log.Printf("warning: Release %q failed: %s", r.Name, err) + r.Info.Status.Code = release.Status_FAILED s.recordRelease(r, req.ReuseName) return res, fmt.Errorf("release %s failed: %s", r.Name, err) } @@ -547,6 +547,9 @@ func (s *releaseServer) performRelease(r *release.Release, req *services.Install // post-install hooks if !req.DisableHooks { if err := s.execHook(r.Hooks, r.Name, r.Namespace, postInstall); err != nil { + log.Printf("warning: Release %q failed post-install: %s", r.Name, err) + r.Info.Status.Code = release.Status_FAILED + s.recordRelease(r, req.ReuseName) return res, err } } From 60b418886799baaddaf3aec260ca94f9737d5486 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 6 Sep 2016 14:09:37 -0700 Subject: [PATCH 059/183] chore(*): bump version to v2.0.0-alpha.4 --- pkg/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/version/version.go b/pkg/version/version.go index 71730852a..8994d4b91 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -23,4 +23,4 @@ package version // import "k8s.io/helm/pkg/version" // Increment major number for new feature additions and behavioral changes. // Increment minor number for bug fixes and performance enhancements. // Increment patch number for critical fixes to existing releases. -var Version = "v2.0.0-alpha.3" +var Version = "v2.0.0-alpha.4" From f062cff5a37b65edc51d74ab3bc065f2af4a2006 Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Tue, 6 Sep 2016 13:11:37 -0600 Subject: [PATCH 060/183] fix(kube): delete should skip resources not found --- pkg/kube/client.go | 20 ++++++++++++++------ pkg/kube/client_test.go | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 7055ec074..0a0f459ba 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -215,25 +215,33 @@ func (c *Client) Update(namespace string, currentReader, targetReader io.Reader) func (c *Client) Delete(namespace string, reader io.Reader) error { return perform(c, namespace, reader, func(info *resource.Info) error { log.Printf("Starting delete for %s", info.Name) + reaper, err := c.Reaper(info.Mapping) if err != nil { // If there is no reaper for this resources, delete it. if kubectl.IsNoSuchReaperError(err) { - return resource.NewHelper(info.Client, info.Mapping).Delete(info.Namespace, info.Name) + err := resource.NewHelper(info.Client, info.Mapping).Delete(info.Namespace, info.Name) + return skipIfNotFound(err) } return err } + log.Printf("Using reaper for deleting %s", info.Name) err = reaper.Stop(info.Namespace, info.Name, 0, nil) - if err != nil && errors.IsNotFound(err) { - log.Printf("%v", err) - return nil - } - return err + return skipIfNotFound(err) }) } +func skipIfNotFound(err error) error { + if err != nil && errors.IsNotFound(err) { + log.Printf("%v", err) + return nil + } + + return err +} + // WatchUntilReady watches the resource given in the reader, and waits until it is ready. // // This function is mainly for hook implementations. It watches for a resource to diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index c59cc37b0..bfa953481 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -126,11 +126,48 @@ func TestReal(t *testing.T) { t.Fatal(err) } - if err := New(nil).Delete("test", strings.NewReader(guestbookManifest)); err != nil { + testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest + c := New(nil) + if err := c.Create("test-delete", strings.NewReader(testSvcEndpointManifest)); err != nil { + t.Fatal(err) + } + + if err := c.Delete("test-delete", strings.NewReader(testEndpointManifest)); err != nil { + t.Fatal(err) + } + + // ensures that delete does not fail if a resource is not found + if err := c.Delete("test-delete", strings.NewReader(testSvcEndpointManifest)); err != nil { t.Fatal(err) } } +const testServiceManifest = ` +kind: Service +apiVersion: v1 +metadata: + name: my-service +spec: + selector: + app: myapp + ports: + - port: 80 + protocol: TCP + targetPort: 9376 +` + +const testEndpointManifest = ` +kind: Endpoints +apiVersion: v1 +metadata: + name: my-service +subsets: + - addresses: + - ip: "1.2.3.4" + ports: + - port: 9376 +` + const guestbookManifest = ` apiVersion: v1 kind: Service From 8d44ed0d54c3d0ceb1e0e5b3648430203b8a6118 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 6 Sep 2016 14:27:03 -0700 Subject: [PATCH 061/183] feat(release): remove tiller from cross compile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7cf4c55b5..bea1bf42d 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ build: .PHONY: build-cross build-cross: - gox -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -os="darwin linux windows" -arch="amd64 386" $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/... + gox -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -os="darwin linux windows" -arch="amd64 386" $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/helm # usage: make dist VERSION=v2.0.0-alpha.3 .PHONY: dist From 34577d1ebc3f3c5ca204eab17afabffdf868c1c5 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 6 Sep 2016 17:31:12 -0600 Subject: [PATCH 062/183] feat(charts): add 'dependencies:' to Chart.yaml This feature adds a dependencies section to a chart file. It is a prerequisite for adding automated chart management tooling as described in #874. --- _proto/hapi/chart/metadata.proto | 15 ++++ pkg/chartutil/chartfile_test.go | 21 +++++ pkg/chartutil/testdata/chartfiletest.yaml | 7 ++ pkg/chartutil/testdata/frobnitz-1.2.3.tgz | Bin 3777 -> 3831 bytes pkg/chartutil/testdata/frobnitz/Chart.yaml | 7 ++ .../frobnitz/charts/mariner-4.3.2.tgz | Bin 941 -> 941 bytes .../mariner/charts/albatross-0.1.0.tgz | Bin 347 -> 347 bytes pkg/proto/hapi/chart/chart.pb.go | 1 + pkg/proto/hapi/chart/metadata.pb.go | 80 +++++++++++++----- 9 files changed, 109 insertions(+), 22 deletions(-) diff --git a/_proto/hapi/chart/metadata.proto b/_proto/hapi/chart/metadata.proto index 40c8b40dc..979702df0 100644 --- a/_proto/hapi/chart/metadata.proto +++ b/_proto/hapi/chart/metadata.proto @@ -27,6 +27,19 @@ message Maintainer { string email = 2; } +// Dependency describes this chart's dependency on another chart. +message Dependency { + // Name is the name of the dependency, e.g. 'nginx' + string name = 1; + + // Repository is the repository URL. Appending '/index.yaml' to this should + // return the repo index. + string repository = 2; + + // Version is a SemVer 2 version. + string version = 3; +} + // Metadata for a Chart file. This models the structure of a Chart.yaml file. // // Spec: https://k8s.io/helm/blob/master/docs/design/chart_format.md#the-chart-file @@ -61,4 +74,6 @@ message Metadata { // The URL to an icon file. string icon = 9; + + repeated Dependency dependencies = 10; } diff --git a/pkg/chartutil/chartfile_test.go b/pkg/chartutil/chartfile_test.go index 618cc2c73..e24f11761 100644 --- a/pkg/chartutil/chartfile_test.go +++ b/pkg/chartutil/chartfile_test.go @@ -90,4 +90,25 @@ func verifyChartfile(t *testing.T, f *chart.Metadata) { } } + if len(f.Dependencies) != 2 { + t.Fatalf("Expected 2 dependencies, got %d", len(f.Dependencies)) + } + + deps := []*chart.Dependency{ + {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, + {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + } + for i, tt := range deps { + c := f.Dependencies[i] + if c.Name != tt.Name { + t.Errorf("Expected name %q, got %q", tt.Name, c.Name) + } + if c.Version != tt.Version { + t.Errorf("Expected version %q, got %q", tt.Version, c.Version) + } + if c.Repository != tt.Repository { + t.Errorf("Expected repository %q, got %q", tt.Repository, c.Repository) + } + } + } diff --git a/pkg/chartutil/testdata/chartfiletest.yaml b/pkg/chartutil/testdata/chartfiletest.yaml index b665c61be..325fb27e2 100644 --- a/pkg/chartutil/testdata/chartfiletest.yaml +++ b/pkg/chartutil/testdata/chartfiletest.yaml @@ -14,3 +14,10 @@ sources: - https://example.com/foo/bar home: http://example.com icon: https://example.com/64x64.png +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz index 4e68564fc1202e94581eb43ef9dfb354d12f59c7..8055a0094583776ac76723b5bd687cae2e22e36f 100644 GIT binary patch literal 3831 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 literal 3777 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 diff --git a/pkg/chartutil/testdata/frobnitz/Chart.yaml b/pkg/chartutil/testdata/frobnitz/Chart.yaml index b665c61be..325fb27e2 100644 --- a/pkg/chartutil/testdata/frobnitz/Chart.yaml +++ b/pkg/chartutil/testdata/frobnitz/Chart.yaml @@ -14,3 +14,10 @@ sources: - https://example.com/foo/bar home: http://example.com icon: https://example.com/64x64.png +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz index d454bb9b1dc11fa526246b9534aceef8a4d80f91..03de29d688dc070e04f6f391c0f7525ff172a2bc 100644 GIT binary patch delta 457 zcmV;)0XF`v2dxJOABzY8h*8gx2j~vDp`oFXsWCABo0^y#jIsR<0e{N>X2xcw24;o^ z!2AzXZ#tU)=>g=sIT$(u&xbQGzrA)h>#%|Z+XLC>T1&())T;e+IOpciwwvqntz`*W zhRZhWo}cN>wD|YZmuBDW)aP|v-1BOA;ndE9+u#1Zo6_OoeX?(@)z&H9!PUnq)^Tfv z99)@jq*bTOT5b94OMfe}7jtH6^>%K2^=Q+!he`Xd5v}dmU`O5kAdR%K;MKtSGOHR#SXFJjRU*$jR%Qq(R?pBDtSthyd_l~c- zC)sXre=W7`O*vci?*sF8oobugyX`EW*_PveCeMpcW!>*Lo`0QczgOYMq>q2(gXb;b zsrmHpyybHh(XY?#r!w66zun-OeD3CQHQBeT^BeP@=oh#C$xl4o@Me}3W#d(rn2R@SFC^?sgOn0jo+%cQr?C&ljF+z^|qv&tGdI1l81W721j z)MC&Ws0bYeqb6V!jDk@x3P!;w7zLwX6pVsVFbYP&C>RB!00{s9M%sWT04M+ezqb9j delta 457 zcmV;)0XF`v2dxJOABzY8hGx5w2j~uwiJ5`9i7_z$o0^!Ljj{a=0e^1)o0*vz8yT1Z z^S^gAwr~9g&i}e^wOG`f z_3r*=e(8UJ)1JBZ=PT#e>v64Z7169$EjcxRo$W;Lf0h5NFW;ENyIUdpW|`!+-#fnU zo@Beh{k7D#H|1>6zYomYb*gP{@3ymiW?PQ?nLIB(m36=0cz<@P{a%G1lRo~D51zM# zr{>eY^Onz5M87__pUQCO|8|3C^0}MK)nwnU&Tq_rqF>zlCqMDb|Ad(I+nRC=@>lC8 z{kr_y*8SXrOm)cbjAVd}9NFO%LnpA@@yb3<&d&MIr<;5?B3jY*$9 zQj0-jpdxe>j3$CnFbYP&C>RB!U=)mkQ7{Td!6+C7qhJ(_0we$c>4Z%>04M+etYi4E diff --git a/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz b/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz index d92a38e676f1527815c247a0b06aeb59c0806a62..8b1b19dce8a88243fcbdfe46415d6bcf86d1b8a9 100644 GIT binary patch delta 16 Xcmcc3beoA?zMF%gBk=r2_7Fw@E_?+k delta 16 Xcmcc3beoA?zMF%gEp7Kk_7Fw@E{Fvv diff --git a/pkg/proto/hapi/chart/chart.pb.go b/pkg/proto/hapi/chart/chart.pb.go index 37a013a4a..261951a41 100644 --- a/pkg/proto/hapi/chart/chart.pb.go +++ b/pkg/proto/hapi/chart/chart.pb.go @@ -16,6 +16,7 @@ It has these top-level messages: Config Value Maintainer + Dependency Metadata Template */ diff --git a/pkg/proto/hapi/chart/metadata.pb.go b/pkg/proto/hapi/chart/metadata.pb.go index d6270bda9..346866a3b 100644 --- a/pkg/proto/hapi/chart/metadata.pb.go +++ b/pkg/proto/hapi/chart/metadata.pb.go @@ -32,7 +32,7 @@ var Metadata_Engine_value = map[string]int32{ func (x Metadata_Engine) String() string { return proto.EnumName(Metadata_Engine_name, int32(x)) } -func (Metadata_Engine) EnumDescriptor() ([]byte, []int) { return fileDescriptor2, []int{1, 0} } +func (Metadata_Engine) EnumDescriptor() ([]byte, []int) { return fileDescriptor2, []int{2, 0} } // Maintainer describes a Chart maintainer. type Maintainer struct { @@ -47,6 +47,29 @@ func (m *Maintainer) String() string { return proto.CompactTextString func (*Maintainer) ProtoMessage() {} func (*Maintainer) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} } +// Dependency describes this chart's dependency on another chart. +type Dependency struct { + // Name is the name of the dependency, e.g. 'nginx' + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + // Repository is the repository URL. Appending '/index.yaml' to this should + // return the repo index. + Repository string `protobuf:"bytes,2,opt,name=repository" json:"repository,omitempty"` + // Version is a SemVer 2 version range. + // This can be an exact version or any of the version range operators + // described here: https://github.com/Masterminds/semver/blob/master/README.md + Version string `protobuf:"bytes,3,opt,name=version" json:"version,omitempty"` + // FetchedVersion is a computed field that indicates exactly which version + // was fetched by tooling. It is an exact version (not a range). + // + // This plays the roll of a "lock" for this dependency. + FetchedVersion string `protobuf:"bytes,4,opt,name=fetchedVersion" json:"fetchedVersion,omitempty"` +} + +func (m *Dependency) Reset() { *m = Dependency{} } +func (m *Dependency) String() string { return proto.CompactTextString(m) } +func (*Dependency) ProtoMessage() {} +func (*Dependency) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{1} } + // Metadata for a Chart file. This models the structure of a Chart.yaml file. // // Spec: https://k8s.io/helm/blob/master/docs/design/chart_format.md#the-chart-file @@ -68,13 +91,14 @@ type Metadata struct { // The name of the template engine to use. Defaults to 'gotpl'. Engine string `protobuf:"bytes,8,opt,name=engine" json:"engine,omitempty"` // The URL to an icon file. - Icon string `protobuf:"bytes,9,opt,name=icon" json:"icon,omitempty"` + Icon string `protobuf:"bytes,9,opt,name=icon" json:"icon,omitempty"` + Dependencies []*Dependency `protobuf:"bytes,10,rep,name=dependencies" json:"dependencies,omitempty"` } func (m *Metadata) Reset() { *m = Metadata{} } func (m *Metadata) String() string { return proto.CompactTextString(m) } func (*Metadata) ProtoMessage() {} -func (*Metadata) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{1} } +func (*Metadata) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{2} } func (m *Metadata) GetMaintainers() []*Maintainer { if m != nil { @@ -83,30 +107,42 @@ func (m *Metadata) GetMaintainers() []*Maintainer { return nil } +func (m *Metadata) GetDependencies() []*Dependency { + if m != nil { + return m.Dependencies + } + return nil +} + func init() { proto.RegisterType((*Maintainer)(nil), "hapi.chart.Maintainer") + proto.RegisterType((*Dependency)(nil), "hapi.chart.Dependency") proto.RegisterType((*Metadata)(nil), "hapi.chart.Metadata") proto.RegisterEnum("hapi.chart.Metadata_Engine", Metadata_Engine_name, Metadata_Engine_value) } var fileDescriptor2 = []byte{ - // 275 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x91, 0x4b, 0x4b, 0xc4, 0x30, - 0x14, 0x85, 0x9d, 0x47, 0x5f, 0xb7, 0x9b, 0xe1, 0x22, 0x43, 0x74, 0x55, 0xba, 0x72, 0xd5, 0x01, - 0x05, 0x71, 0x2d, 0x88, 0x0b, 0x9d, 0x8e, 0x0c, 0x8a, 0xe0, 0x2e, 0xb6, 0xc1, 0x06, 0x6d, 0x53, - 0x92, 0xa8, 0xf8, 0x9f, 0xfc, 0x91, 0x26, 0xb7, 0xf3, 0x5a, 0xb8, 0x28, 0x9c, 0x73, 0xbe, 0xde, - 0xdc, 0x9e, 0x06, 0x4e, 0x1a, 0xde, 0xcb, 0x45, 0xd5, 0x70, 0x6d, 0x17, 0xad, 0xb0, 0xbc, 0xe6, - 0x96, 0x17, 0xbd, 0x56, 0x56, 0x21, 0x78, 0x54, 0x10, 0xca, 0x2f, 0x01, 0x96, 0x5c, 0x76, 0xd6, - 0x3d, 0x42, 0x23, 0xc2, 0xb4, 0xe3, 0xad, 0x60, 0xa3, 0x6c, 0x74, 0x96, 0xac, 0x49, 0xe3, 0x31, - 0x04, 0xa2, 0xe5, 0xf2, 0x83, 0x8d, 0x29, 0x1c, 0x4c, 0xfe, 0x3b, 0x86, 0x78, 0xb9, 0x39, 0xf6, - 0xdf, 0x31, 0x97, 0x35, 0xca, 0x65, 0xc3, 0x14, 0x69, 0x64, 0x10, 0x19, 0xf5, 0xa9, 0x2b, 0x61, - 0xd8, 0x24, 0x9b, 0xb8, 0x78, 0x6b, 0x3d, 0xf9, 0x12, 0xda, 0x48, 0xd5, 0xb1, 0x29, 0x0d, 0x6c, - 0x2d, 0x66, 0x90, 0xd6, 0xc2, 0x54, 0x5a, 0xf6, 0xd6, 0xd3, 0x80, 0xe8, 0x61, 0x84, 0xa7, 0x10, - 0xbf, 0x8b, 0x9f, 0x6f, 0xa5, 0x6b, 0xc3, 0x42, 0x3a, 0x76, 0xe7, 0xf1, 0x0a, 0xd2, 0x76, 0x57, - 0xcf, 0xb0, 0xc8, 0xe1, 0xf4, 0x7c, 0x5e, 0xec, 0x7f, 0x40, 0xb1, 0x6f, 0xbf, 0x3e, 0x7c, 0x15, - 0xe7, 0x10, 0x8a, 0xee, 0xcd, 0x69, 0x16, 0xd3, 0xca, 0x8d, 0xf3, 0xbd, 0x64, 0xe5, 0x3e, 0x24, - 0x19, 0x7a, 0x79, 0x9d, 0x67, 0x10, 0xde, 0x0c, 0x34, 0x85, 0xe8, 0xa9, 0xbc, 0x2b, 0x57, 0xcf, - 0xe5, 0xec, 0x08, 0x13, 0x08, 0x6e, 0x57, 0x8f, 0x0f, 0xf7, 0xb3, 0xd1, 0x75, 0xf4, 0x12, 0xd0, - 0xba, 0xd7, 0x90, 0xae, 0xe0, 0xe2, 0x2f, 0x00, 0x00, 0xff, 0xff, 0x72, 0xdf, 0x74, 0xb5, 0x9f, - 0x01, 0x00, 0x00, + // 346 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x52, 0x4f, 0x4b, 0xfb, 0x40, + 0x10, 0xfd, 0xa5, 0x6d, 0x92, 0x76, 0xf2, 0x43, 0xca, 0x22, 0x65, 0xf5, 0x20, 0x25, 0x07, 0xf1, + 0x94, 0x82, 0x82, 0x88, 0x47, 0x51, 0x3c, 0x68, 0x5b, 0x29, 0xfe, 0x01, 0x6f, 0x6b, 0x32, 0x9a, + 0x45, 0x93, 0x0d, 0xbb, 0xab, 0xd2, 0xab, 0x9f, 0xd6, 0x8f, 0xe1, 0x66, 0x93, 0xb6, 0xa9, 0xf6, + 0x10, 0x98, 0xf7, 0xde, 0xcc, 0xbc, 0x99, 0xcc, 0xc2, 0x4e, 0xca, 0x0a, 0x3e, 0x8a, 0x53, 0x26, + 0xf5, 0x28, 0x43, 0xcd, 0x12, 0xa6, 0x59, 0x54, 0x48, 0xa1, 0x05, 0x81, 0x52, 0x8a, 0xac, 0x14, + 0x1e, 0x03, 0x8c, 0x19, 0xcf, 0xb5, 0xf9, 0x50, 0x12, 0x02, 0x9d, 0x9c, 0x65, 0x48, 0x9d, 0xa1, + 0x73, 0xd0, 0x9b, 0xd9, 0x98, 0x6c, 0x83, 0x8b, 0x19, 0xe3, 0x6f, 0xb4, 0x65, 0xc9, 0x0a, 0x84, + 0x5f, 0x0e, 0xc0, 0x39, 0x16, 0x98, 0x27, 0x98, 0xc7, 0xf3, 0x8d, 0x85, 0x7b, 0x00, 0x12, 0x0b, + 0xa1, 0xb8, 0x16, 0x72, 0x5e, 0x57, 0x37, 0x18, 0x42, 0xc1, 0xff, 0x40, 0xa9, 0xb8, 0xc8, 0x69, + 0xdb, 0x8a, 0x0b, 0x48, 0xf6, 0x61, 0xeb, 0x19, 0x75, 0x9c, 0x62, 0x72, 0x5f, 0x27, 0x74, 0x6c, + 0xc2, 0x2f, 0x36, 0xfc, 0x6e, 0x41, 0x77, 0x5c, 0xef, 0xb6, 0x71, 0x04, 0xc3, 0xa5, 0xc2, 0x70, + 0x95, 0xb9, 0x8d, 0x4b, 0x5b, 0x25, 0xde, 0x65, 0x8c, 0xca, 0xd8, 0xb6, 0x4b, 0xdb, 0x1a, 0x36, + 0x07, 0xea, 0xac, 0x0f, 0x34, 0x84, 0x20, 0x41, 0x15, 0x4b, 0x5e, 0xe8, 0x52, 0x75, 0xad, 0xda, + 0xa4, 0xc8, 0x2e, 0x74, 0x5f, 0x71, 0xfe, 0x29, 0x64, 0xa2, 0xa8, 0x67, 0xdb, 0x2e, 0x31, 0x39, + 0x81, 0x20, 0x5b, 0xfe, 0x63, 0x45, 0x7d, 0x23, 0x07, 0x87, 0x83, 0x68, 0x75, 0x85, 0x68, 0x75, + 0x82, 0x59, 0x33, 0x95, 0x0c, 0xc0, 0xc3, 0xfc, 0xc5, 0xc4, 0xb4, 0x6b, 0x2d, 0x6b, 0x54, 0xee, + 0xc5, 0x63, 0x33, 0x48, 0xaf, 0xda, 0xab, 0x8c, 0xc9, 0x29, 0xfc, 0x4f, 0x16, 0x07, 0xe1, 0x66, + 0x39, 0xf8, 0x6b, 0xb3, 0x3a, 0xd8, 0x6c, 0x2d, 0x37, 0x1c, 0x82, 0x77, 0x51, 0x75, 0x0e, 0xc0, + 0xbf, 0x9b, 0x5c, 0x4d, 0xa6, 0x0f, 0x93, 0xfe, 0x3f, 0xd2, 0x03, 0xf7, 0x72, 0x7a, 0x7b, 0x73, + 0xdd, 0x77, 0xce, 0xfc, 0x47, 0xd7, 0xf6, 0x78, 0xf2, 0xec, 0x1b, 0x3a, 0xfa, 0x09, 0x00, 0x00, + 0xff, 0xff, 0x6a, 0xfb, 0xe7, 0x86, 0x60, 0x02, 0x00, 0x00, } From 663f2b0f1db33c9c223a9e8b968c573c33cd5aa5 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 6 Sep 2016 18:00:08 -0600 Subject: [PATCH 063/183] fix(tiller): add test for failed hooks --- cmd/tiller/release_server.go | 2 +- cmd/tiller/release_server_test.go | 35 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index c20bc4d50..52ee7228c 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -588,7 +588,7 @@ func (s *releaseServer) execHook(hs []*release.Hook, name, namespace, hook strin b := bytes.NewBufferString(h.Manifest) if err := kubeCli.Create(namespace, b); err != nil { - log.Printf("wrning: Release %q pre-install %s failed: %s", name, h.Path, err) + log.Printf("warning: Release %q pre-install %s failed: %s", name, h.Path, err) return err } // No way to rewind a bytes.Buffer()? diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index a9d042ab5..339f9e3f5 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -17,7 +17,9 @@ limitations under the License. package main import ( + "errors" "fmt" + "io" "os" "regexp" "strings" @@ -431,6 +433,25 @@ func TestInstallReleaseNoHooks(t *testing.T) { } } +func TestInstallReleaseFailedHooks(t *testing.T) { + c := context.Background() + rs := rsFixture() + rs.env.Releases.Create(releaseStub()) + rs.env.KubeClient = newHookFailingKubeClient() + + req := &services.InstallReleaseRequest{ + Chart: chartStub(), + } + res, err := rs.InstallRelease(c, req) + if err == nil { + t.Error("Expected failed install") + } + + if hl := res.Release.Info.Status.Code; hl != release.Status_FAILED { + t.Errorf("Expected FAILED release. Got %d", hl) + } +} + func TestInstallReleaseReuseName(t *testing.T) { c := context.Background() rs := rsFixture() @@ -912,6 +933,20 @@ func mockEnvironment() *environment.Environment { return e } +func newHookFailingKubeClient() *hookFailingKubeClient { + return &hookFailingKubeClient{ + PrintingKubeClient: environment.PrintingKubeClient{Out: os.Stdout}, + } +} + +type hookFailingKubeClient struct { + environment.PrintingKubeClient +} + +func (h *hookFailingKubeClient) WatchUntilReady(ns string, r io.Reader) error { + return errors.New("Failed watch") +} + type mockListServer struct { val *services.ListReleasesResponse } From 383a9c186a90c106abfee250a6029e9de3353a8f Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Thu, 1 Sep 2016 13:07:12 -0600 Subject: [PATCH 064/183] update pkg/storage to support rollback --- pkg/storage/driver/cfgmaps.go | 351 ++++++++++++++++------------- pkg/storage/driver/cfgmaps_test.go | 311 ++++++++++--------------- pkg/storage/driver/driver.go | 37 +-- pkg/storage/driver/labels.go | 50 ++++ pkg/storage/driver/labels_test.go | 33 +++ pkg/storage/driver/memory.go | 181 ++++++++++----- pkg/storage/driver/memory_test.go | 201 +++++++++++------ pkg/storage/driver/records.go | 116 ++++++++++ pkg/storage/driver/records_test.go | 71 ++++++ pkg/storage/driver/testing.go | 119 ++++++++++ pkg/storage/storage.go | 103 +++++---- pkg/storage/storage_test.go | 274 +++++++++++++--------- 12 files changed, 1204 insertions(+), 643 deletions(-) create mode 100644 pkg/storage/driver/labels.go create mode 100644 pkg/storage/driver/labels_test.go create mode 100644 pkg/storage/driver/records.go create mode 100644 pkg/storage/driver/records_test.go create mode 100644 pkg/storage/driver/testing.go diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index afe031bf5..64521a411 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -17,168 +17,195 @@ limitations under the License. package driver // import "k8s.io/helm/pkg/storage/driver" import ( - "encoding/base64" - "fmt" - "log" - "strconv" - "time" + "encoding/base64" + "fmt" + "log" + "strconv" + "time" - "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/proto" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/kubernetes/pkg/api" - kberrs "k8s.io/kubernetes/pkg/api/errors" - client "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/api" + kberrs "k8s.io/kubernetes/pkg/api/errors" + client "k8s.io/kubernetes/pkg/client/unversioned" + kblabels "k8s.io/kubernetes/pkg/labels" ) +var _ Driver = (*ConfigMaps)(nil) + // ConfigMapsDriverName is the string name of the driver. const ConfigMapsDriverName = "ConfigMap" var b64 = base64.StdEncoding -// labels is a map of key value pairs to be included as metadata in a configmap object. -type labels map[string]string - -func (lbs *labels) init() { *lbs = labels(make(map[string]string)) } -func (lbs labels) get(key string) string { return lbs[key] } -func (lbs labels) set(key, val string) { lbs[key] = val } -func (lbs labels) toMap() map[string]string { return lbs } - // ConfigMaps is a wrapper around an implementation of a kubernetes // ConfigMapsInterface. type ConfigMaps struct { - impl client.ConfigMapsInterface + impl client.ConfigMapsInterface } // NewConfigMaps initializes a new ConfigMaps wrapping an implmenetation of // the kubernetes ConfigMapsInterface. func NewConfigMaps(impl client.ConfigMapsInterface) *ConfigMaps { - return &ConfigMaps{impl: impl} + return &ConfigMaps{impl: impl} } // Name returns the name of the driver. func (cfgmaps *ConfigMaps) Name() string { - return ConfigMapsDriverName + return ConfigMapsDriverName } // Get fetches the release named by key. The corresponding release is returned // or error if not found. func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { - // fetch the configmap holding the release named by key - obj, err := cfgmaps.impl.Get(key) - if err != nil { - if kberrs.IsNotFound(err) { - return nil, ErrReleaseNotFound - } - - logerrf(err, "get: failed to get %q", key) - return nil, err - } - // found the configmap, decode the base64 data string - r, err := decodeRelease(obj.Data["release"]) - if err != nil { - logerrf(err, "get: failed to decode data %q", key) - return nil, err - } - // return the release object - return r, nil + // fetch the configmap holding the release named by key + obj, err := cfgmaps.impl.Get(key) + if err != nil { + if kberrs.IsNotFound(err) { + return nil, ErrReleaseNotFound + } + + logerrf(err, "get: failed to get %q", key) + return nil, err + } + // found the configmap, decode the base64 data string + r, err := decodeRelease(obj.Data["release"]) + if err != nil { + logerrf(err, "get: failed to decode data %q", key) + return nil, err + } + // return the release object + return r, nil } // List fetches all releases and returns the list releases such // that filter(release) == true. An error is returned if the // configmap fails to retrieve the releases. func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { - list, err := cfgmaps.impl.List(api.ListOptions{}) - if err != nil { - logerrf(err, "list: failed to list") - return nil, err - } - - var results []*rspb.Release - - // iterate over the configmaps object list - // and decode each release - for _, item := range list.Items { - rls, err := decodeRelease(item.Data["release"]) - if err != nil { - logerrf(err, "list: failed to decode release: %s", rls) - continue - } - if filter(rls) { - results = append(results, rls) - } - } - return results, nil + list, err := cfgmaps.impl.List(api.ListOptions{}) + if err != nil { + logerrf(err, "list: failed to list") + return nil, err + } + + var results []*rspb.Release + + // iterate over the configmaps object list + // and decode each release + for _, item := range list.Items { + rls, err := decodeRelease(item.Data["release"]) + if err != nil { + logerrf(err, "list: failed to decode release: %s", rls) + continue + } + if filter(rls) { + results = append(results, rls) + } + } + return results, nil +} + +// Query fetches all releases that match the provided map of labels. +// An error is returned if the configmap fails to retrieve the releases. +func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, error) { + ls := kblabels.Set{} + for k, v := range labels { + ls[k] = v + } + + opts := api.ListOptions{LabelSelector: ls.AsSelector()} + + list, err := cfgmaps.impl.List(opts) + if err != nil { + logerrf(err, "query: failed to query with labels") + return nil, err + } + + if len(list.Items) == 0 { + return nil, ErrReleaseNotFound + } + + var results []*rspb.Release + for _, item := range list.Items { + rls, err := decodeRelease(item.Data["release"]) + if err != nil { + logerrf(err, "query: failed to decode release: %s", err) + continue + } + results = append(results, rls) + } + return results, nil } // Create creates a new ConfigMap holding the release. If the // ConfigMap already exists, ErrReleaseExists is returned. -func (cfgmaps *ConfigMaps) Create(rls *rspb.Release) error { - // set labels for configmaps object meta data - var lbs labels - - lbs.init() - lbs.set("CREATED_AT", strconv.Itoa(int(time.Now().Unix()))) - - // create a new configmap to hold the release - obj, err := newConfigMapsObject(rls, lbs) - if err != nil { - logerrf(err, "create: failed to encode release %q", rls.Name) - return err - } - // push the configmap object out into the kubiverse - if _, err := cfgmaps.impl.Create(obj); err != nil { - if kberrs.IsAlreadyExists(err) { - return ErrReleaseExists - } - - logerrf(err, "create: failed to create") - return err - } - return nil +func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { + // set labels for configmaps object meta data + var lbs labels + + lbs.init() + lbs.set("CREATED_AT", strconv.Itoa(int(time.Now().Unix()))) + + // create a new configmap to hold the release + obj, err := newConfigMapsObject(key, rls, lbs) + if err != nil { + logerrf(err, "create: failed to encode release %q", rls.Name) + return err + } + // push the configmap object out into the kubiverse + if _, err := cfgmaps.impl.Create(obj); err != nil { + if kberrs.IsAlreadyExists(err) { + return ErrReleaseExists + } + + logerrf(err, "create: failed to create") + return err + } + return nil } // Update updates the ConfigMap holding the release. If not found // the ConfigMap is created to hold the release. -func (cfgmaps *ConfigMaps) Update(rls *rspb.Release) error { - // set labels for configmaps object meta data - var lbs labels - - lbs.init() - lbs.set("MODIFIED_AT", strconv.Itoa(int(time.Now().Unix()))) - - // create a new configmap object to hold the release - obj, err := newConfigMapsObject(rls, lbs) - if err != nil { - logerrf(err, "update: failed to encode release %q", rls.Name) - return err - } - // push the configmap object out into the kubiverse - _, err = cfgmaps.impl.Update(obj) - if err != nil { - logerrf(err, "update: failed to update") - return err - } - return nil +func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { + // set labels for configmaps object meta data + var lbs labels + + lbs.init() + lbs.set("MODIFIED_AT", strconv.Itoa(int(time.Now().Unix()))) + + // create a new configmap object to hold the release + obj, err := newConfigMapsObject(key, rls, lbs) + if err != nil { + logerrf(err, "update: failed to encode release %q", rls.Name) + return err + } + // push the configmap object out into the kubiverse + _, err = cfgmaps.impl.Update(obj) + if err != nil { + logerrf(err, "update: failed to update") + return err + } + return nil } // Delete deletes the ConfigMap holding the release named by key. func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) { - // fetch the release to check existence - if rls, err = cfgmaps.Get(key); err != nil { - if kberrs.IsNotFound(err) { - return nil, ErrReleaseNotFound - } - - logerrf(err, "delete: failed to get release %q", rls.Name) - return nil, err - } - // delete the release - if err = cfgmaps.impl.Delete(key); err != nil { - return rls, err - } - return rls, nil + // fetch the release to check existence + if rls, err = cfgmaps.Get(key); err != nil { + if kberrs.IsNotFound(err) { + return nil, ErrReleaseNotFound + } + + logerrf(err, "delete: failed to get release %q", rls.Name) + return nil, err + } + // delete the release + if err = cfgmaps.impl.Delete(key); err != nil { + return rls, err + } + return rls, nil } // newConfigMapsObject constructs a kubernetes ConfigMap object @@ -194,43 +221,43 @@ func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) { // "OWNER" - owner of the configmap, currently "TILLER". // "NAME" - name of the release. // -func newConfigMapsObject(rls *rspb.Release, lbs labels) (*api.ConfigMap, error) { - const owner = "TILLER" - - // encode the release - s, err := encodeRelease(rls) - if err != nil { - return nil, err - } - - if lbs == nil { - lbs.init() - } - - // apply labels - lbs.set("NAME", rls.Name) - lbs.set("OWNER", owner) - lbs.set("STATUS", rspb.Status_Code_name[int32(rls.Info.Status.Code)]) - lbs.set("VERSION", strconv.Itoa(int(rls.Version))) - - // create and return configmap object - return &api.ConfigMap{ - ObjectMeta: api.ObjectMeta{ - Name: rls.Name, - Labels: lbs.toMap(), - }, - Data: map[string]string{"release": s}, - }, nil +func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*api.ConfigMap, error) { + const owner = "TILLER" + + // encode the release + s, err := encodeRelease(rls) + if err != nil { + return nil, err + } + + if lbs == nil { + lbs.init() + } + + // apply labels + lbs.set("NAME", rls.Name) + lbs.set("OWNER", owner) + lbs.set("STATUS", rspb.Status_Code_name[int32(rls.Info.Status.Code)]) + lbs.set("VERSION", strconv.Itoa(int(rls.Version))) + + // create and return configmap object + return &api.ConfigMap{ + ObjectMeta: api.ObjectMeta{ + Name: key, + Labels: lbs.toMap(), + }, + Data: map[string]string{"release": s}, + }, nil } // encodeRelease encodes a release returning a base64 encoded // binary protobuf encoding representation, or error. func encodeRelease(rls *rspb.Release) (string, error) { - b, err := proto.Marshal(rls) - if err != nil { - return "", err - } - return b64.EncodeToString(b), nil + b, err := proto.Marshal(rls) + if err != nil { + return "", err + } + return b64.EncodeToString(b), nil } // decodeRelease decodes the bytes in data into a release @@ -238,21 +265,21 @@ func encodeRelease(rls *rspb.Release) (string, error) { // valid protobuf encoding of a release, otherwise // an error is returned. func decodeRelease(data string) (*rspb.Release, error) { - // base64 decode string - b, err := b64.DecodeString(data) - if err != nil { - return nil, err - } - - var rls rspb.Release - // unmarshal protobuf bytes - if err := proto.Unmarshal(b, &rls); err != nil { - return nil, err - } - return &rls, nil + // base64 decode string + b, err := b64.DecodeString(data) + if err != nil { + return nil, err + } + + var rls rspb.Release + // unmarshal protobuf bytes + if err := proto.Unmarshal(b, &rls); err != nil { + return nil, err + } + return &rls, nil } // logerrf wraps an error with the a formatted string (used for debugging) func logerrf(err error, format string, args ...interface{}) { - log.Printf("configmaps: %s: %s\n", fmt.Sprintf(format, args...), err) + log.Printf("configmaps: %s: %s\n", fmt.Sprintf(format, args...), err) } diff --git a/pkg/storage/driver/cfgmaps_test.go b/pkg/storage/driver/cfgmaps_test.go index bf2c3f7da..7baa066bd 100644 --- a/pkg/storage/driver/cfgmaps_test.go +++ b/pkg/storage/driver/cfgmaps_test.go @@ -1,12 +1,9 @@ /* 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. @@ -17,212 +14,134 @@ limitations under the License. package driver import ( - "reflect" - "testing" - - rspb "k8s.io/helm/pkg/proto/hapi/release" + "reflect" + "testing" - "k8s.io/kubernetes/pkg/api" - kberrs "k8s.io/kubernetes/pkg/api/errors" - "k8s.io/kubernetes/pkg/client/unversioned" + rspb "k8s.io/helm/pkg/proto/hapi/release" ) -var _ Driver = &ConfigMaps{} - func TestConfigMapName(t *testing.T) { - c := newTestFixture(t) - if c.Name() != ConfigMapsDriverName { - t.Errorf("Expected name to be %q, got %q", ConfigMapsDriverName, c.Name()) - } + c := newTestFixtureCfgMaps(t) + if c.Name() != ConfigMapsDriverName { + t.Errorf("Expected name to be %q, got %q", ConfigMapsDriverName, c.Name()) + } } func TestConfigMapGet(t *testing.T) { - key := "key-1" - rel := newTestRelease(key, 1, rspb.Status_DEPLOYED) - - cfgmaps := newTestFixture(t, []*rspb.Release{rel}...) - - // get release with key - got, err := cfgmaps.Get(key) - if err != nil { - t.Fatalf("Failed to get release: %s", err) - } - // compare fetched release with original - if !reflect.DeepEqual(rel, got) { - t.Errorf("Expected {%q}, got {%q}", rel, got) - } + vers := int32(1) + name := "smug-pigeon" + key := testKey(name, vers) + rel := releaseStub(name, vers, rspb.Status_DEPLOYED) + + cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) + + // get release with key + got, err := cfgmaps.Get(key) + if err != nil { + t.Fatalf("Failed to get release: %s", err) + } + // compare fetched release with original + if !reflect.DeepEqual(rel, got) { + t.Errorf("Expected {%q}, got {%q}", rel, got) + } } func TestConfigMapList(t *testing.T) { - cfgmaps := newTestFixture(t, []*rspb.Release{ - newTestRelease("key-1", 1, rspb.Status_DELETED), - newTestRelease("key-2", 1, rspb.Status_DELETED), - newTestRelease("key-3", 1, rspb.Status_DEPLOYED), - newTestRelease("key-4", 1, rspb.Status_DEPLOYED), - newTestRelease("key-5", 1, rspb.Status_SUPERSEDED), - newTestRelease("key-6", 1, rspb.Status_SUPERSEDED), - }...) - - // list all deleted releases - del, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status.Code == rspb.Status_DELETED - }) - // check - if err != nil { - t.Errorf("Failed to list deleted: %s", err) - } - if len(del) != 2 { - t.Errorf("Expected 2 deleted, got %d:\n%v\n", len(del), del) - } - - // list all deployed releases - dpl, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status.Code == rspb.Status_DEPLOYED - }) - // check - if err != nil { - t.Errorf("Failed to list deployed: %s", err) - } - if len(dpl) != 2 { - t.Errorf("Expected 2 deployed, got %d", len(dpl)) - } - - // list all superseded releases - ssd, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status.Code == rspb.Status_SUPERSEDED - }) - // check - if err != nil { - t.Errorf("Failed to list superseded: %s", err) - } - if len(ssd) != 2 { - t.Errorf("Expected 2 superseded, got %d", len(ssd)) - } + cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ + releaseStub("key-1", 1, rspb.Status_DELETED), + releaseStub("key-2", 1, rspb.Status_DELETED), + releaseStub("key-3", 1, rspb.Status_DEPLOYED), + releaseStub("key-4", 1, rspb.Status_DEPLOYED), + releaseStub("key-5", 1, rspb.Status_SUPERSEDED), + releaseStub("key-6", 1, rspb.Status_SUPERSEDED), + }...) + + // list all deleted releases + del, err := cfgmaps.List(func(rel *rspb.Release) bool { + return rel.Info.Status.Code == rspb.Status_DELETED + }) + // check + if err != nil { + t.Errorf("Failed to list deleted: %s", err) + } + if len(del) != 2 { + t.Errorf("Expected 2 deleted, got %d:\n%v\n", len(del), del) + } + + // list all deployed releases + dpl, err := cfgmaps.List(func(rel *rspb.Release) bool { + return rel.Info.Status.Code == rspb.Status_DEPLOYED + }) + // check + if err != nil { + t.Errorf("Failed to list deployed: %s", err) + } + if len(dpl) != 2 { + t.Errorf("Expected 2 deployed, got %d", len(dpl)) + } + + // list all superseded releases + ssd, err := cfgmaps.List(func(rel *rspb.Release) bool { + return rel.Info.Status.Code == rspb.Status_SUPERSEDED + }) + // check + if err != nil { + t.Errorf("Failed to list superseded: %s", err) + } + if len(ssd) != 2 { + t.Errorf("Expected 2 superseded, got %d", len(ssd)) + } } func TestConfigMapCreate(t *testing.T) { - cfgmaps := newTestFixture(t) - - key := "key-1" - rel := newTestRelease(key, 1, rspb.Status_DEPLOYED) - - // store the release in a configmap - if err := cfgmaps.Create(rel); err != nil { - t.Fatalf("Failed to create release with key %q: %s", key, err) - } - - // get the release back - got, err := cfgmaps.Get(key) - if err != nil { - t.Fatalf("Failed to get release with key %q: %s", key, err) - } - - // compare created release with original - if !reflect.DeepEqual(rel, got) { - t.Errorf("Expected {%q}, got {%q}", rel, got) - } + cfgmaps := newTestFixtureCfgMaps(t) + + vers := int32(1) + name := "smug-pigeon" + key := testKey(name, vers) + rel := releaseStub(name, vers, rspb.Status_DEPLOYED) + + // store the release in a configmap + if err := cfgmaps.Create(key, rel); err != nil { + t.Fatalf("Failed to create release with key %q: %s", key, err) + } + + // get the release back + got, err := cfgmaps.Get(key) + if err != nil { + t.Fatalf("Failed to get release with key %q: %s", key, err) + } + + // compare created release with original + if !reflect.DeepEqual(rel, got) { + t.Errorf("Expected {%q}, got {%q}", rel, got) + } } func TestConfigMapUpdate(t *testing.T) { - key := "key-1" - rel := newTestRelease(key, 1, rspb.Status_DEPLOYED) - - cfgmaps := newTestFixture(t, []*rspb.Release{rel}...) - - // modify release status code & version - rel = newTestRelease(key, 2, rspb.Status_SUPERSEDED) - - // perform the update - if err := cfgmaps.Update(rel); err != nil { - t.Fatalf("Failed to update release: %s", err) - } - - // fetch the updated release - got, err := cfgmaps.Get(key) - if err != nil { - t.Fatalf("Failed to get release with key %q: %s", key, err) - } - - // check release has actually been updated by comparing modified fields - switch { - case rel.Info.Status.Code != got.Info.Status.Code: - t.Errorf("Expected status %s, got status %s", rel.Info.Status.Code, got.Info.Status.Code) - case rel.Version != got.Version: - t.Errorf("Expected version %d, got version %d", rel.Version, got.Version) - } -} - -// newTestFixture initializes a MockConfigMapsInterface. -// ConfigMaps are created for each release provided. -func newTestFixture(t *testing.T, releases ...*rspb.Release) *ConfigMaps { - var mock MockConfigMapsInterface - mock.Init(t, releases...) - - return NewConfigMaps(&mock) -} - -// newTestRelease creates a release object for testing. -func newTestRelease(key string, version int32, status rspb.Status_Code) *rspb.Release { - return &rspb.Release{Name: key, Info: &rspb.Info{Status: &rspb.Status{Code: status}}, Version: version} -} - -// MockConfigMapsInterface mocks a kubernetes ConfigMapsInterface -type MockConfigMapsInterface struct { - unversioned.ConfigMapsInterface - - objects map[string]*api.ConfigMap -} - -func (mock *MockConfigMapsInterface) Init(t *testing.T, releases ...*rspb.Release) { - mock.objects = map[string]*api.ConfigMap{} - - for _, rls := range releases { - cfgmap, err := newConfigMapsObject(rls, nil) - if err != nil { - t.Fatalf("Failed to create configmap: %s", err) - } - mock.objects[rls.Name] = cfgmap - } -} - -func (mock *MockConfigMapsInterface) Get(name string) (*api.ConfigMap, error) { - object, ok := mock.objects[name] - if !ok { - return nil, kberrs.NewNotFound(api.Resource("tests"), name) - } - return object, nil -} - -func (mock *MockConfigMapsInterface) List(opts api.ListOptions) (*api.ConfigMapList, error) { - var list api.ConfigMapList - for _, cfgmap := range mock.objects { - list.Items = append(list.Items, *cfgmap) - } - return &list, nil -} - -func (mock *MockConfigMapsInterface) Create(cfgmap *api.ConfigMap) (*api.ConfigMap, error) { - name := cfgmap.ObjectMeta.Name - if object, ok := mock.objects[name]; ok { - return object, kberrs.NewAlreadyExists(api.Resource("tests"), name) - } - mock.objects[name] = cfgmap - return cfgmap, nil -} - -func (mock *MockConfigMapsInterface) Update(cfgmap *api.ConfigMap) (*api.ConfigMap, error) { - name := cfgmap.ObjectMeta.Name - if _, ok := mock.objects[name]; !ok { - return nil, kberrs.NewNotFound(api.Resource("tests"), name) - } - mock.objects[name] = cfgmap - return cfgmap, nil -} - -func (mock *MockConfigMapsInterface) Delete(name string) error { - if _, ok := mock.objects[name]; !ok { - return kberrs.NewNotFound(api.Resource("tests"), name) - } - delete(mock.objects, name) - return nil -} + vers := int32(1) + name := "smug-pigeon" + key := testKey(name, vers) + rel := releaseStub(name, vers, rspb.Status_DEPLOYED) + + cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) + + // modify release status code + rel.Info.Status.Code = rspb.Status_SUPERSEDED + + // perform the update + if err := cfgmaps.Update(key, rel); err != nil { + t.Fatalf("Failed to update release: %s", err) + } + + // fetch the updated release + got, err := cfgmaps.Get(key) + if err != nil { + t.Fatalf("Failed to get release with key %q: %s", key, err) + } + + // check release has actually been updated by comparing modified fields + if rel.Info.Status.Code != got.Info.Status.Code { + t.Errorf("Expected status %s, got status %s", rel.Info.Status.Code, got.Info.Status.Code) + } +} \ No newline at end of file diff --git a/pkg/storage/driver/driver.go b/pkg/storage/driver/driver.go index f3593d278..4024012ee 100644 --- a/pkg/storage/driver/driver.go +++ b/pkg/storage/driver/driver.go @@ -17,16 +17,18 @@ limitations under the License. package driver // import "k8s.io/helm/pkg/storage/driver" import ( - "errors" + "errors" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "k8s.io/helm/pkg/proto/hapi/release" ) var ( - // ErrReleaseNotFound indicates that a release is not found. - ErrReleaseNotFound = errors.New("release: not found") - // ErrReleaseExists indicates that a release already exists. - ErrReleaseExists = errors.New("release: already exists") + // ErrReleaseNotFound indicates that a release is not found. + ErrReleaseNotFound = errors.New("release: not found") + // ErrReleaseExists indicates that a release already exists. + ErrReleaseExists = errors.New("release: already exists") + // ErrInvalidKey indicates that a release key could not be parsed. + ErrInvalidKey = errors.New("release: invalid key") ) // Creator is the interface that wraps the Create method. @@ -34,7 +36,7 @@ var ( // Create stores the release or returns ErrReleaseExists // if an identical release already exists. type Creator interface { - Create(rls *rspb.Release) error + Create(key string, rls *rspb.Release) error } // Updator is the interface that wraps the Update method. @@ -42,7 +44,7 @@ type Creator interface { // Update updates an existing release or returns // ErrReleaseNotFound if the release does not exist. type Updator interface { - Update(rls *rspb.Release) error + Update(key string, rls *rspb.Release) error } // Deletor is the interface that wraps the Delete method. @@ -50,7 +52,7 @@ type Updator interface { // Delete deletes the release named by key or returns // ErrReleaseNotFound if the release does not exist. type Deletor interface { - Delete(key string) (*rspb.Release, error) + Delete(key string) (*rspb.Release, error) } // Queryor is the interface that wraps the Get and List methods. @@ -59,9 +61,12 @@ type Deletor interface { // if the release does not exist. // // List returns the set of all releases that satisfy the filter predicate. +// +// Query returns the set of all releases that match the provided label set. type Queryor interface { - Get(key string) (*rspb.Release, error) - List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) + Get(key string) (*rspb.Release, error) + List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) + Query(labels map[string]string) ([]*rspb.Release, error) } // Driver is the interface composed of Creator, Updator, Deletor, Queryor @@ -69,9 +74,9 @@ type Queryor interface { // and retrieving tiller releases from some underlying storage mechanism, // e.g. memory, configmaps. type Driver interface { - Creator - Updator - Deletor - Queryor - Name() string + Creator + Updator + Deletor + Queryor + Name() string } diff --git a/pkg/storage/driver/labels.go b/pkg/storage/driver/labels.go new file mode 100644 index 000000000..f26dba7ab --- /dev/null +++ b/pkg/storage/driver/labels.go @@ -0,0 +1,50 @@ +package driver + +import ( + "bytes" + "fmt" + "io" +) + +// labels is a map of key value pairs to be included as metadata in a configmap object. +type labels map[string]string + +func (lbs *labels) init() { *lbs = labels(make(map[string]string)) } +func (lbs labels) get(key string) string { return lbs[key] } +func (lbs labels) set(key, val string) { lbs[key] = val } + +func (lbs labels) keys() (ls []string) { + for key := range lbs { + ls = append(ls, key) + } + return +} + +func (lbs labels) match(set labels) bool { + for _, key := range set.keys() { + if lbs.get(key) != set.get(key) { + return false + } + } + return true +} + +func (lbs labels) toMap() map[string]string { return lbs } + +func (lbs *labels) fromMap(kvs map[string]string) { + for k, v := range kvs { + lbs.set(k, v) + } +} + +func (lbs labels) dump(w io.Writer) error { + var b bytes.Buffer + + fmt.Fprintln(&b, "labels:") + for k, v := range lbs { + fmt.Fprintf(&b, "\t- %q -> %q\n", k, v) + } + + _, err := w.Write(b.Bytes()) + return err +} diff --git a/pkg/storage/driver/labels_test.go b/pkg/storage/driver/labels_test.go new file mode 100644 index 000000000..c9f44fba2 --- /dev/null +++ b/pkg/storage/driver/labels_test.go @@ -0,0 +1,33 @@ +package driver + +import ( + "testing" +) + +func TestLabelsMatch(t *testing.T) { + var tests = []struct { + desc string + set1 labels + set2 labels + expect bool + }{ + { + "equal labels sets", + labels(map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"}), + labels(map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"}), + true, + }, + { + "disjoint label sets", + labels(map[string]string{"KEY_C": "VAL_C", "KEY_D": "VAL_D"}), + labels(map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"}), + false, + }, + } + + for _, tt := range tests { + if !tt.set1.match(tt.set2) && tt.expect { + t.Fatalf("Expected match '%s'\n", tt.desc) + } + } +} diff --git a/pkg/storage/driver/memory.go b/pkg/storage/driver/memory.go index 76351b1a7..6513609fe 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -14,103 +14,178 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "k8s.io/helm/pkg/storage/driver" +package driver import ( - "sync" - - rspb "k8s.io/helm/pkg/proto/hapi/release" + "bytes" + "fmt" + "io" + "strconv" + "strings" + "sync" + + rspb "k8s.io/helm/pkg/proto/hapi/release" ) +var _ Driver = (*Memory)(nil) + // MemoryDriverName is the string name of this driver. const MemoryDriverName = "Memory" // Memory is the in-memory storage driver implementation. type Memory struct { - sync.RWMutex - cache map[string]*rspb.Release + sync.RWMutex + cache map[string]records } // NewMemory initializes a new memory driver. func NewMemory() *Memory { - return &Memory{cache: map[string]*rspb.Release{}} + return &Memory{cache: map[string]records{}} } // Name returns the name of the driver. func (mem *Memory) Name() string { - return MemoryDriverName + return MemoryDriverName } // Get returns the release named by key or returns ErrReleaseNotFound. func (mem *Memory) Get(key string) (*rspb.Release, error) { - defer unlock(mem.rlock()) - - if rls, ok := mem.cache[key]; ok { - return rls, nil - } - return nil, ErrReleaseNotFound + defer unlock(mem.rlock()) + + switch elems := strings.Split(key, ".v"); len(elems) { + case 2: + name, ver := elems[0], elems[1] + if _, err := strconv.Atoi(ver); err != nil { + return nil, ErrInvalidKey + } + if recs, ok := mem.cache[name]; ok { + if r := recs.Get(key); r != nil { + return r.rls, nil + } + } + return nil, ErrReleaseNotFound + default: + return nil, ErrInvalidKey + } } // List returns the list of all releases such that filter(release) == true func (mem *Memory) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { - defer unlock(mem.rlock()) - - var releases []*rspb.Release - for k := range mem.cache { - if filter(mem.cache[k]) { - releases = append(releases, mem.cache[k]) - } - } - return releases, nil + defer unlock(mem.rlock()) + + var ls []*rspb.Release + for _, recs := range mem.cache { + recs.Iter(func(_ int, rec *record) bool { + if filter(rec.rls) { + ls = append(ls, rec.rls) + } + return true + }) + } + return ls, nil +} + +// Query returns the set of releases that match the provided set of labels +func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) { + defer unlock(mem.rlock()) + + var lbs labels + + lbs.init() + lbs.fromMap(keyvals) + + var ls []*rspb.Release + for _, recs := range mem.cache { + recs.Iter(func(_ int, rec *record) bool { + if rec.lbs.match(lbs) { + ls = append(ls, rec.rls) + } + return true + }) + } + return ls, nil } // Create creates a new release or returns ErrReleaseExists. -func (mem *Memory) Create(rls *rspb.Release) error { - defer unlock(mem.wlock()) - - if _, ok := mem.cache[rls.Name]; ok { - return ErrReleaseExists - } - mem.cache[rls.Name] = rls - return nil +func (mem *Memory) Create(key string, rls *rspb.Release) error { + defer unlock(mem.wlock()) + + if recs, ok := mem.cache[rls.Name]; ok { + if err := recs.Add(newRecord(key, rls)); err != nil { + return err + } + mem.cache[rls.Name] = recs + return nil + } + mem.cache[rls.Name] = records{newRecord(key, rls)} + return nil } // Update updates a release or returns ErrReleaseNotFound. -func (mem *Memory) Update(rls *rspb.Release) error { - defer unlock(mem.wlock()) - - if _, ok := mem.cache[rls.Name]; ok { - mem.cache[rls.Name] = rls - return nil - } - return ErrReleaseNotFound +func (mem *Memory) Update(key string, rls *rspb.Release) error { + defer unlock(mem.wlock()) + + if rs, ok := mem.cache[rls.Name]; ok && rs.Exists(key) { + rs.Replace(key, newRecord(key, rls)) + return nil + } + return ErrReleaseNotFound } // Delete deletes a release or returns ErrReleaseNotFound. func (mem *Memory) Delete(key string) (*rspb.Release, error) { - defer unlock(mem.wlock()) + defer unlock(mem.wlock()) + + switch elems := strings.Split(key, ".v"); len(elems) { + case 2: + name, ver := elems[0], elems[1] + if _, err := strconv.Atoi(ver); err != nil { + return nil, ErrInvalidKey + } + if recs, ok := mem.cache[name]; ok { + if r := recs.Remove(key); r != nil { + return r.rls, nil + } + } + return nil, ErrReleaseNotFound + default: + return nil, ErrInvalidKey + } + return nil, ErrReleaseNotFound +} + +func (mem *Memory) dump(w io.Writer) error { + var b bytes.Buffer + + fmt.Fprintln(&b, "memory:") + for key, recs := range mem.cache { + fmt.Fprintf(&b, "\t# %q\n", key) + + recs.Iter(func(index int, r *record) bool { + fmt.Fprintf(&b, "\t\t- [%d] v%d (status = %s)\n", + index, + r.rls.Version, + r.rls.Info.Status.Code, + ) + + return true + }) + } - if old, ok := mem.cache[key]; ok { - delete(mem.cache, key) - return old, nil - } - return nil, ErrReleaseNotFound + _, err := w.Write(b.Bytes()) + return err } // wlock locks mem for writing func (mem *Memory) wlock() func() { - mem.Lock() - return func() { - mem.Unlock() - } + mem.Lock() + return func() { mem.Unlock() } } // rlock locks mem for reading func (mem *Memory) rlock() func() { - mem.RLock() - return func() { - mem.RUnlock() - } + mem.RLock() + return func() { mem.RUnlock() } } // unlock calls fn which reverses a mem.rlock or mem.wlock. e.g: diff --git a/pkg/storage/driver/memory_test.go b/pkg/storage/driver/memory_test.go index 2c50fd1a4..534d8a7de 100644 --- a/pkg/storage/driver/memory_test.go +++ b/pkg/storage/driver/memory_test.go @@ -14,88 +14,155 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "k8s.io/helm/pkg/storage/driver" +package driver import ( - "reflect" - "testing" + "reflect" + "testing" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "k8s.io/helm/pkg/proto/hapi/release" ) -var _ Driver = &Memory{} - func TestMemoryName(t *testing.T) { - mem := NewMemory() - if mem.Name() != MemoryDriverName { - t.Errorf("Expected name to be %q, got %q", MemoryDriverName, mem.Name()) - } + if mem := NewMemory(); mem.Name() != MemoryDriverName { + t.Errorf("Expected name to be %q, got %q", MemoryDriverName, mem.Name()) + } +} + +func TestMemoryCreate(t *testing.T) { + var tests = []struct { + desc string + rls *rspb.Release + err bool + }{ + { + "create should success", + releaseStub("rls-c", 1, rspb.Status_DEPLOYED), + false, + }, + { + "create should fail (release already exists)", + releaseStub("rls-a", 1, rspb.Status_DEPLOYED), + true, + }, + } + + ts := tsFixtureMemory(t) + for _, tt := range tests { + key := testKey(tt.rls.Name, tt.rls.Version) + rls := tt.rls + + if err := ts.Create(key, rls); err != nil { + if !tt.err { + t.Fatalf("failed to create %q: %s", tt.desc, err) + } + } + } } func TestMemoryGet(t *testing.T) { - key := "test-1" - rls := &rspb.Release{Name: key} - - mem := NewMemory() - if err := mem.Create(rls); err != nil { - t.Fatalf("Failed create: %s", err) - } - - res, err := mem.Get(key) - if err != nil { - t.Errorf("Could not get %s: %s", key, err) - } - if res.Name != key { - t.Errorf("Expected %s, got %s", key, res.Name) - } + var tests = []struct { + desc string + key string + err bool + }{ + {"release key should exist", "rls-a.v1", false}, + {"release key should not exist", "rls-a.v5", true}, + } + + ts := tsFixtureMemory(t) + for _, tt := range tests { + if _, err := ts.Get(tt.key); err != nil { + if !tt.err { + t.Fatalf("Failed %q to get '%s': %q\n", tt.desc, tt.key, err) + } + } + } } -func TestMemoryCreate(t *testing.T) { - key := "test-1" - rls := &rspb.Release{Name: key} - - mem := NewMemory() - if err := mem.Create(rls); err != nil { - t.Fatalf("Failed created: %s", err) - } - if mem.cache[key].Name != key { - t.Errorf("Unexpected release name: %s", mem.cache[key].Name) - } +func TestMemoryQuery(t *testing.T) { + var tests = []struct { + desc string + xlen int + lbs map[string]string + }{ + { + "should be 2 query results", + 2, + map[string]string{"STATUS": "DEPLOYED"}, + }, + } + + ts := tsFixtureMemory(t) + for _, tt := range tests { + l, err := ts.Query(tt.lbs) + if err != nil { + t.Fatalf("Failed to query: %s\n", err) + } + + if tt.xlen != len(l) { + t.Fatalf("Expected %d results, actual %d\n", tt.xlen, len(l)) + } + } } func TestMemoryUpdate(t *testing.T) { - key := "test-1" - rls := &rspb.Release{Name: key} - - mem := NewMemory() - if err := mem.Create(rls); err != nil { - t.Fatalf("Failed create: %s", err) - } - if err := mem.Update(rls); err != nil { - t.Fatalf("Failed update: %s", err) - } - if mem.cache[key].Name != key { - t.Errorf("Unexpected release name: %s", mem.cache[key].Name) - } + var tests = []struct { + desc string + key string + rls *rspb.Release + err bool + }{ + { + "update release status", + "rls-a.v4", + releaseStub("rls-a", 4, rspb.Status_SUPERSEDED), + false, + }, + { + "update release does not exist", + "rls-z.v1", + releaseStub("rls-z", 1, rspb.Status_DELETED), + true, + }, + } + + ts := tsFixtureMemory(t) + for _, tt := range tests { + if err := ts.Update(tt.key, tt.rls); err != nil { + if !tt.err { + t.Fatalf("Failed %q: %s\n", tt.desc, err) + } + continue + } + + r, err := ts.Get(tt.key) + if err != nil { + t.Fatalf("Failed to get: %s\n", err) + } + + if !reflect.DeepEqual(r, tt.rls) { + t.Fatalf("Expected %s, actual %s\n", tt.rls, r) + } + } } func TestMemoryDelete(t *testing.T) { - key := "test-1" - rls := &rspb.Release{Name: key} - - mem := NewMemory() - if err := mem.Create(rls); err != nil { - t.Fatalf("Failed create: %s", err) - } - - res, err := mem.Delete(key) - if err != nil { - t.Fatalf("Failed delete: %s", err) - } - if mem.cache[key] != nil { - t.Errorf("Expected nil, got %s", mem.cache[key]) - } - if !reflect.DeepEqual(rls, res) { - t.Errorf("Expected %s, got %s", rls, res) - } + var tests = []struct { + desc string + key string + err bool + }{ + {"release key should exist", "rls-a.v1", false}, + {"release key should not exist", "rls-a.v5", true}, + } + + ts := tsFixtureMemory(t) + for _, tt := range tests { + if _, err := ts.Delete(tt.key); err != nil { + if !tt.err { + t.Fatalf("Failed %q to get '%s': %q\n", tt.desc, tt.key, err) + } + } + } } diff --git a/pkg/storage/driver/records.go b/pkg/storage/driver/records.go new file mode 100644 index 000000000..6b9ad5211 --- /dev/null +++ b/pkg/storage/driver/records.go @@ -0,0 +1,116 @@ +package driver + +import ( + "sort" + "strconv" + + rspb "k8s.io/helm/pkg/proto/hapi/release" +) + +// records holds a list of in-memory release records +type records []*record + +func (rs records) Len() int { return len(rs) } +func (rs records) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] } +func (rs records) Less(i, j int) bool { return rs[i].rls.Version < rs[j].rls.Version } + +func (rs *records) Add(r *record) error { + if r == nil { + return nil + } + + if rs.Exists(r.key) { + return ErrReleaseExists + } + + *rs = append(*rs, r) + sort.Sort(*rs) + + return nil +} + +func (rs records) Get(key string) *record { + if i, ok := rs.Index(key); ok { + return rs[i] + } + return nil +} + +func (rs *records) Iter(fn func(int, *record) bool) { + cp := make([]*record, len(*rs)) + copy(cp, *rs) + + for i, r := range cp { + if !fn(i, r) { + return + } + } +} + +func (rs *records) Index(key string) (int, bool) { + for i, r := range *rs { + if r.key == key { + return i, true + } + } + return -1, false +} + +func (rs records) Exists(key string) bool { + _, ok := rs.Index(key) + return ok +} + +func (rs *records) Remove(key string) (r *record) { + if i, ok := rs.Index(key); ok { + return rs.removeAt(i) + } + return nil +} + +func (rs *records) Replace(key string, rec *record) *record { + if i, ok := rs.Index(key); ok { + old := (*rs)[i] + (*rs)[i] = rec + return old + } + return nil +} + +func (rs records) FindByVersion(vers int32) (int, bool) { + i := sort.Search(len(rs), func(i int) bool { + return rs[i].rls.Version == vers + }) + if i < len(rs) && rs[i].rls.Version == vers { + return i, true + } + return i, false +} + +func (rs *records) removeAt(index int) *record { + r := (*rs)[index] + (*rs)[index] = nil + copy((*rs)[index:], (*rs)[index+1:]) + *rs = (*rs)[:len(*rs)-1] + return r +} + +// record is the data structure used to cache releases +// for the in-memory storage driver +type record struct { + key string + lbs labels + rls *rspb.Release +} + +// newRecord creates a new in-memory release record +func newRecord(key string, rls *rspb.Release) *record { + var lbs labels + + lbs.init() + lbs.set("NAME", rls.Name) + lbs.set("STATUS", rspb.Status_Code_name[int32(rls.Info.Status.Code)]) + lbs.set("VERSION", strconv.Itoa(int(rls.Version))) + + return &record{key: key, lbs: lbs, rls: rls} +} diff --git a/pkg/storage/driver/records_test.go b/pkg/storage/driver/records_test.go new file mode 100644 index 000000000..3095dd210 --- /dev/null +++ b/pkg/storage/driver/records_test.go @@ -0,0 +1,71 @@ +package driver + +import ( + "testing" + + rspb "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestRecordsAdd(t *testing.T) { + rs := records([]*record{ + newRecord("rls-a.v1", releaseStub("rls-a", 1, rspb.Status_SUPERSEDED)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, rspb.Status_DEPLOYED)), + }) + + var tests = []struct { + desc string + key string + ok bool + rec *record + }{ + { + "add valid key", + "rls-a.v3", + false, + newRecord("rls-a.v3", releaseStub("rls-a", 3, rspb.Status_SUPERSEDED)), + }, + { + "add already existing key", + "rls-a.v1", + true, + newRecord("rls-a.v1", releaseStub("rls-a", 1, rspb.Status_DEPLOYED)), + }, + } + + for _, tt := range tests { + if err := rs.Add(tt.rec); err != nil { + if !tt.ok { + t.Fatalf("failed: %q: %s\n", tt.desc, err) + } + } + } +} + +func TestRecordsRemove(t *testing.T) { + var tests = []struct { + desc string + key string + ok bool + }{ + {"remove valid key", "rls-a.v1", false}, + {"remove invalid key", "rls-a.v", true}, + {"remove non-existant key", "rls-z.v1", true}, + } + + rs := records([]*record{ + newRecord("rls-a.v1", releaseStub("rls-a", 1, rspb.Status_SUPERSEDED)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, rspb.Status_DEPLOYED)), + }) + + for _, tt := range tests { + if r := rs.Remove(tt.key); r == nil { + if !tt.ok { + t.Fatalf("Failed to %q (key = %s). Expected nil, got %s", + tt.desc, + tt.key, + r, + ) + } + } + } +} diff --git a/pkg/storage/driver/testing.go b/pkg/storage/driver/testing.go new file mode 100644 index 000000000..425d270bc --- /dev/null +++ b/pkg/storage/driver/testing.go @@ -0,0 +1,119 @@ +package driver + +import ( + "fmt" + "testing" + + rspb "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/kubernetes/pkg/client/unversioned" + kberrs "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/api" +) + +func releaseStub(name string, vers int32, code rspb.Status_Code) *rspb.Release { + return &rspb.Release{ + Name: name, + Version: vers, + Info: &rspb.Info{Status: &rspb.Status{Code: code}}, + } +} + +func testKey(name string, vers int32) string { + return fmt.Sprintf("%s.v%d", name, vers) +} + +func tsFixtureMemory(t *testing.T) *Memory { + hs := []*rspb.Release{ + // rls-a + releaseStub("rls-a", 4, rspb.Status_DEPLOYED), + releaseStub("rls-a", 1, rspb.Status_SUPERSEDED), + releaseStub("rls-a", 3, rspb.Status_SUPERSEDED), + releaseStub("rls-a", 2, rspb.Status_SUPERSEDED), + // rls-b + releaseStub("rls-b", 4, rspb.Status_DEPLOYED), + releaseStub("rls-b", 1, rspb.Status_SUPERSEDED), + releaseStub("rls-b", 3, rspb.Status_SUPERSEDED), + releaseStub("rls-b", 2, rspb.Status_SUPERSEDED), + } + + mem := NewMemory() + for _, tt := range hs { + err := mem.Create(testKey(tt.Name, tt.Version), tt) + if err != nil { + t.Fatalf("Test setup failed to create: %s\n", err) + } + } + return mem +} + +// newTestFixture initializes a MockConfigMapsInterface. +// ConfigMaps are created for each release provided. +func newTestFixtureCfgMaps(t *testing.T, releases ...*rspb.Release) *ConfigMaps { + var mock MockConfigMapsInterface + mock.Init(t, releases...) + + return NewConfigMaps(&mock) +} + +// MockConfigMapsInterface mocks a kubernetes ConfigMapsInterface +type MockConfigMapsInterface struct { + unversioned.ConfigMapsInterface + + objects map[string]*api.ConfigMap +} + +func (mock *MockConfigMapsInterface) Init(t *testing.T, releases ...*rspb.Release) { + mock.objects = map[string]*api.ConfigMap{} + + for _, rls := range releases { + objkey := testKey(rls.Name, rls.Version) + + cfgmap, err := newConfigMapsObject(objkey, rls, nil) + if err != nil { + t.Fatalf("Failed to create configmap: %s", err) + } + mock.objects[objkey] = cfgmap + } +} + +func (mock *MockConfigMapsInterface) Get(name string) (*api.ConfigMap, error) { + object, ok := mock.objects[name] + if !ok { + return nil, kberrs.NewNotFound(api.Resource("tests"), name) + } + return object, nil +} + +func (mock *MockConfigMapsInterface) List(opts api.ListOptions) (*api.ConfigMapList, error) { + var list api.ConfigMapList + for _, cfgmap := range mock.objects { + list.Items = append(list.Items, *cfgmap) + } + return &list, nil +} + +func (mock *MockConfigMapsInterface) Create(cfgmap *api.ConfigMap) (*api.ConfigMap, error) { + name := cfgmap.ObjectMeta.Name + if object, ok := mock.objects[name]; ok { + return object, kberrs.NewAlreadyExists(api.Resource("tests"), name) + } + mock.objects[name] = cfgmap + return cfgmap, nil +} + +func (mock *MockConfigMapsInterface) Update(cfgmap *api.ConfigMap) (*api.ConfigMap, error) { + name := cfgmap.ObjectMeta.Name + if _, ok := mock.objects[name]; !ok { + return nil, kberrs.NewNotFound(api.Resource("tests"), name) + } + mock.objects[name] = cfgmap + return cfgmap, nil +} + +func (mock *MockConfigMapsInterface) Delete(name string) error { + if _, ok := mock.objects[name]; !ok { + return kberrs.NewNotFound(api.Resource("tests"), name) + } + delete(mock.objects, name) + return nil +} diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 85b204fcc..534425ad5 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -17,100 +17,127 @@ limitations under the License. package storage // import "k8s.io/helm/pkg/storage" import ( - "log" + "fmt" + "log" - rspb "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/storage/driver" + rspb "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/storage/driver" ) // Storage represents a storage engine for a Release. type Storage struct { - driver.Driver + driver.Driver } // Get retrieves the release from storage. An error is returned // if the storage driver failed to fetch the release, or the -// release identified by key does not exist. -func (s *Storage) Get(key string) (*rspb.Release, error) { - log.Printf("Getting release %q from storage\n", key) - return s.Driver.Get(key) +// release identified by the key, version pair does not exist. +func (s *Storage) Get(name string, version int32) (*rspb.Release, error) { + log.Printf("Getting release %q (v%d) from storage\n", name, version) + return s.Driver.Get(makeKey(name, version)) } // Create creates a new storage entry holding the release. An // error is returned if the storage driver failed to store the // release, or a release with identical an key already exists. func (s *Storage) Create(rls *rspb.Release) error { - log.Printf("Create release %q in storage\n", rls.Name) - return s.Driver.Create(rls) + log.Printf("Create release %q (v%d) in storage\n", rls.Name, rls.Version) + return s.Driver.Create(makeKey(rls.Name, rls.Version), rls) } // Update update the release in storage. An error is returned if the // storage backend fails to update the release or if the release // does not exist. func (s *Storage) Update(rls *rspb.Release) error { - log.Printf("Updating %q in storage\n", rls.Name) - return s.Driver.Update(rls) + log.Printf("Updating %q (v%d) in storage\n", rls.Name, rls.Version) + return s.Driver.Update(makeKey(rls.Name, rls.Version), rls) } // Delete deletes the release from storage. An error is returned if // the storage backend fails to delete the release or if the release // does not exist. -func (s *Storage) Delete(key string) (*rspb.Release, error) { - log.Printf("Deleting release %q from storage\n", key) - return s.Driver.Delete(key) +func (s *Storage) Delete(name string, version int32) (*rspb.Release, error) { + log.Printf("Deleting release %q (v%d) from storage\n", name, version) + return s.Driver.Delete(makeKey(name, version)) } // ListReleases returns all releases from storage. An error is returned if the // storage backend fails to retrieve the releases. func (s *Storage) ListReleases() ([]*rspb.Release, error) { - log.Println("Listing all releases in storage") - return s.Driver.List(func(_ *rspb.Release) bool { return true }) + log.Println("Listing all releases in storage") + return s.Driver.List(func(_ *rspb.Release) bool { return true }) } // ListDeleted returns all releases with Status == DELETED. An error is returned // if the storage backend fails to retrieve the releases. func (s *Storage) ListDeleted() ([]*rspb.Release, error) { - log.Println("List deleted releases in storage") - return s.Driver.List(func(rls *rspb.Release) bool { - return StatusFilter(rspb.Status_DELETED).Check(rls) - }) + log.Println("List deleted releases in storage") + return s.Driver.List(func(rls *rspb.Release) bool { + return StatusFilter(rspb.Status_DELETED).Check(rls) + }) } // ListDeployed returns all releases with Status == DEPLOYED. An error is returned // if the storage backend fails to retrieve the releases. func (s *Storage) ListDeployed() ([]*rspb.Release, error) { - log.Println("Listing all deployed releases in storage") - return s.Driver.List(func(rls *rspb.Release) bool { - return StatusFilter(rspb.Status_DEPLOYED).Check(rls) - }) + log.Println("Listing all deployed releases in storage") + return s.Driver.List(func(rls *rspb.Release) bool { + return StatusFilter(rspb.Status_DEPLOYED).Check(rls) + }) } // ListFilterAll returns the set of releases satisfying satisfying the predicate // (filter0 && filter1 && ... && filterN), i.e. a Release is included in the results // if and only if all filters return true. func (s *Storage) ListFilterAll(filters ...FilterFunc) ([]*rspb.Release, error) { - log.Println("Listing all releases with filter") - return s.Driver.List(func(rls *rspb.Release) bool { - return All(filters...).Check(rls) - }) + log.Println("Listing all releases with filter") + return s.Driver.List(func(rls *rspb.Release) bool { + return All(filters...).Check(rls) + }) } // ListFilterAny returns the set of releases satisfying satisfying the predicate // (filter0 || filter1 || ... || filterN), i.e. a Release is included in the results // if at least one of the filters returns true. func (s *Storage) ListFilterAny(filters ...FilterFunc) ([]*rspb.Release, error) { - log.Println("Listing any releases with filter") - return s.Driver.List(func(rls *rspb.Release) bool { - return Any(filters...).Check(rls) - }) + log.Println("Listing any releases with filter") + return s.Driver.List(func(rls *rspb.Release) bool { + return Any(filters...).Check(rls) + }) +} + +// Deployed returns the deployed release with the provided release name, or +// returns ErrReleaseNotFound if not found. +func (s *Storage) Deployed(name string) (*rspb.Release, error) { + log.Printf("Getting deployed release from '%s' history\n", name) + + ls, err := s.Driver.Query(map[string]string{ + "NAME": name, + "STATUS": "DEPLOYED", + }) + switch { + case err != nil: + return nil, err + case len(ls) == 0: + return nil, fmt.Errorf("'%s' has no deployed releases", name) + default: + return ls[0], nil + } +} + +// makeKey concatenates a release name and version into +// a string with format ```#v```. +// This key is used to uniquely identify storage objects. +func makeKey(rlsname string, version int32) string { + return fmt.Sprintf("%s.v%d", rlsname, version) } // Init initializes a new storage backend with the driver d. // If d is nil, the default in-memory driver is used. func Init(d driver.Driver) *Storage { - // default driver is in memory - if d == nil { - d = driver.NewMemory() - } - return &Storage{Driver: d} + // default driver is in memory + if d == nil { + d = driver.NewMemory() + } + return &Storage{Driver: d} } diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go index 25975bf1b..4aae0dd06 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/storage/storage_test.go @@ -17,143 +17,195 @@ limitations under the License. package storage // import "k8s.io/helm/pkg/storage" import ( - "fmt" - "reflect" - "testing" + "fmt" + "reflect" + "testing" - rspb "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/storage/driver" + rspb "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/storage/driver" ) func TestStorageCreate(t *testing.T) { - // initialize storage - storage := Init(driver.NewMemory()) + // initialize storage + storage := Init(driver.NewMemory()) - // create fake release - rls := ReleaseTestData{Name: "angry-beaver"}.ToRelease() - assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") + // create fake release + rls := ReleaseTestData{ + Name: "angry-beaver", + Version: 1, + }.ToRelease() - // fetch the release - res, err := storage.Get(rls.Name) - assertErrNil(t.Fatal, err, "QueryRelease") + assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") - // verify the fetched and created release are the same - if !reflect.DeepEqual(rls, res) { - t.Fatalf("Expected %q, got %q", rls, res) - } + // fetch the release + res, err := storage.Get(rls.Name, rls.Version) + assertErrNil(t.Fatal, err, "QueryRelease") + + // verify the fetched and created release are the same + if !reflect.DeepEqual(rls, res) { + t.Fatalf("Expected %q, got %q", rls, res) + } } func TestStorageUpdate(t *testing.T) { - // initialize storage - storage := Init(driver.NewMemory()) - - // create fake release - rls := ReleaseTestData{Name: "angry-beaver"}.ToRelease() - assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") - - // modify the release - rls.Version = 2 - rls.Manifest = "new-manifest" - assertErrNil(t.Fatal, storage.Update(rls), "UpdateRelease") - - // retrieve the updated release - res, err := storage.Get(rls.Name) - assertErrNil(t.Fatal, err, "QueryRelease") - - // verify updated and fetched releases are the same. - if !reflect.DeepEqual(rls, res) { - t.Fatalf("Expected %q, got %q", rls, res) - } + // initialize storage + storage := Init(driver.NewMemory()) + + // create fake release + rls := ReleaseTestData{ + Name: "angry-beaver", + Version: 1, + Status: rspb.Status_DEPLOYED, + }.ToRelease() + + assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") + + // modify the release + rls.Info.Status.Code = rspb.Status_DELETED + assertErrNil(t.Fatal, storage.Update(rls), "UpdateRelease") + + // retrieve the updated release + res, err := storage.Get(rls.Name, rls.Version) + assertErrNil(t.Fatal, err, "QueryRelease") + + // verify updated and fetched releases are the same. + if !reflect.DeepEqual(rls, res) { + t.Fatalf("Expected %q, got %q", rls, res) + } } func TestStorageDelete(t *testing.T) { - // initialize storage - storage := Init(driver.NewMemory()) + // initialize storage + storage := Init(driver.NewMemory()) + + // create fake release + rls := ReleaseTestData{ + Name: "angry-beaver", + Version: 1, + }.ToRelease() - // create fake release - rls := ReleaseTestData{Name: "angry-beaver"}.ToRelease() - assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") + assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") - // delete the release - res, err := storage.Delete(rls.Name) - assertErrNil(t.Fatal, err, "DeleteRelease") + // delete the release + res, err := storage.Delete(rls.Name, rls.Version) + assertErrNil(t.Fatal, err, "DeleteRelease") - // verify updated and fetched releases are the same. - if !reflect.DeepEqual(rls, res) { - t.Fatalf("Expected %q, got %q", rls, res) - } + // verify updated and fetched releases are the same. + if !reflect.DeepEqual(rls, res) { + t.Fatalf("Expected %q, got %q", rls, res) + } } func TestStorageList(t *testing.T) { - // initialize storage - storage := Init(driver.NewMemory()) - - // setup storage with test releases - setup := func() { - // release records - rls0 := ReleaseTestData{Name: "happy-catdog", Status: rspb.Status_SUPERSEDED}.ToRelease() - rls1 := ReleaseTestData{Name: "livid-human", Status: rspb.Status_SUPERSEDED}.ToRelease() - rls2 := ReleaseTestData{Name: "relaxed-cat", Status: rspb.Status_SUPERSEDED}.ToRelease() - rls3 := ReleaseTestData{Name: "hungry-hippo", Status: rspb.Status_DEPLOYED}.ToRelease() - rls4 := ReleaseTestData{Name: "angry-beaver", Status: rspb.Status_DEPLOYED}.ToRelease() - rls5 := ReleaseTestData{Name: "opulent-frog", Status: rspb.Status_DELETED}.ToRelease() - rls6 := ReleaseTestData{Name: "happy-liger", Status: rspb.Status_DELETED}.ToRelease() - - // create the release records in the storage - assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'rls0'") - assertErrNil(t.Fatal, storage.Create(rls1), "Storing release 'rls1'") - assertErrNil(t.Fatal, storage.Create(rls2), "Storing release 'rls2'") - assertErrNil(t.Fatal, storage.Create(rls3), "Storing release 'rls3'") - assertErrNil(t.Fatal, storage.Create(rls4), "Storing release 'rls4'") - assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'rls5'") - assertErrNil(t.Fatal, storage.Create(rls6), "Storing release 'rls6'") - } - - var listTests = []struct { - Description string - NumExpected int - ListFunc func() ([]*rspb.Release, error) - }{ - {"ListDeleted", 2, storage.ListDeleted}, - {"ListDeployed", 2, storage.ListDeployed}, - {"ListReleases", 7, storage.ListReleases}, - } - - setup() - - for _, tt := range listTests { - list, err := tt.ListFunc() - assertErrNil(t.Fatal, err, tt.Description) - // verify the count of releases returned - if len(list) != tt.NumExpected { - t.Errorf("ListReleases(%s): expected %d, actual %d", - tt.Description, - tt.NumExpected, - len(list)) - } - } + // initialize storage + storage := Init(driver.NewMemory()) + + // setup storage with test releases + setup := func() { + // release records + rls0 := ReleaseTestData{Name: "happy-catdog", Status: rspb.Status_SUPERSEDED}.ToRelease() + rls1 := ReleaseTestData{Name: "livid-human", Status: rspb.Status_SUPERSEDED}.ToRelease() + rls2 := ReleaseTestData{Name: "relaxed-cat", Status: rspb.Status_SUPERSEDED}.ToRelease() + rls3 := ReleaseTestData{Name: "hungry-hippo", Status: rspb.Status_DEPLOYED}.ToRelease() + rls4 := ReleaseTestData{Name: "angry-beaver", Status: rspb.Status_DEPLOYED}.ToRelease() + rls5 := ReleaseTestData{Name: "opulent-frog", Status: rspb.Status_DELETED}.ToRelease() + rls6 := ReleaseTestData{Name: "happy-liger", Status: rspb.Status_DELETED}.ToRelease() + + // create the release records in the storage + assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'rls0'") + assertErrNil(t.Fatal, storage.Create(rls1), "Storing release 'rls1'") + assertErrNil(t.Fatal, storage.Create(rls2), "Storing release 'rls2'") + assertErrNil(t.Fatal, storage.Create(rls3), "Storing release 'rls3'") + assertErrNil(t.Fatal, storage.Create(rls4), "Storing release 'rls4'") + assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'rls5'") + assertErrNil(t.Fatal, storage.Create(rls6), "Storing release 'rls6'") + } + + var listTests = []struct { + Description string + NumExpected int + ListFunc func() ([]*rspb.Release, error) + }{ + {"ListDeleted", 2, storage.ListDeleted}, + {"ListDeployed", 2, storage.ListDeployed}, + {"ListReleases", 7, storage.ListReleases}, + } + + setup() + + for _, tt := range listTests { + list, err := tt.ListFunc() + assertErrNil(t.Fatal, err, tt.Description) + // verify the count of releases returned + if len(list) != tt.NumExpected { + t.Errorf("ListReleases(%s): expected %d, actual %d", + tt.Description, + tt.NumExpected, + len(list)) + } + } +} + +func TestStorageDeployed(t *testing.T) { + storage := Init(driver.NewMemory()) + + const name = "angry-bird" + const vers = int32(4) + + // setup storage with test releases + setup := func() { + // release records + rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.Status_SUPERSEDED}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.Status_SUPERSEDED}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.Status_SUPERSEDED}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.Status_DEPLOYED}.ToRelease() + + // create the release records in the storage + assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") + assertErrNil(t.Fatal, storage.Create(rls1), "Storing release 'angry-bird' (v2)") + assertErrNil(t.Fatal, storage.Create(rls2), "Storing release 'angry-bird' (v3)") + assertErrNil(t.Fatal, storage.Create(rls3), "Storing release 'angry-bird' (v4)") + } + + setup() + + rls, err := storage.Deployed(name) + if err != nil { + t.Fatalf("Failed to query for deployed release: %s\n", err) + } + + switch { + case rls == nil: + t.Fatalf("Release is nil") + case rls.Name != name: + t.Fatalf("Expected release name %q, actual %q\n", name, rls.Name) + case rls.Version != vers: + t.Fatalf("Expected release version %d, actual %d\n", vers, rls.Version) + case rls.Info.Status.Code != rspb.Status_DEPLOYED: + t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rls.Info.Status.Code) + } } type ReleaseTestData struct { - Name string - Version int32 - Manifest string - Namespace string - Status rspb.Status_Code + Name string + Version int32 + Manifest string + Namespace string + Status rspb.Status_Code } func (test ReleaseTestData) ToRelease() *rspb.Release { - return &rspb.Release{ - Name: test.Name, - Version: test.Version, - Manifest: test.Manifest, - Namespace: test.Namespace, - Info: &rspb.Info{Status: &rspb.Status{Code: test.Status}}, - } + return &rspb.Release{ + Name: test.Name, + Version: test.Version, + Manifest: test.Manifest, + Namespace: test.Namespace, + Info: &rspb.Info{Status: &rspb.Status{Code: test.Status}}, + } } func assertErrNil(eh func(args ...interface{}), err error, message string) { - if err != nil { - eh(fmt.Sprintf("%s: %q", message, err)) - } + if err != nil { + eh(fmt.Sprintf("%s: %q", message, err)) + } } From de5365ec5cf9891207379586cfee1d90cbe897f1 Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Thu, 8 Sep 2016 08:30:04 -0600 Subject: [PATCH 065/183] feat(rollback-support): update release server / release server test --- cmd/tiller/release_server.go | 46 +++++++++++++++++++++++-------- cmd/tiller/release_server_test.go | 46 ++++++++++++------------------- 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 52ee7228c..ad3758d83 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -179,10 +179,20 @@ func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetRelease if req.Name == "" { return nil, errMissingRelease } - rel, err := s.env.Releases.Get(req.Name) - if err != nil { - return nil, err + + var rel *release.Release + if req.Version <= 0 { + var err error + if rel, err = s.env.Releases.Deployed(req.Name); err != nil { + return nil, err + } + } else { + var err error + if rel, err = s.env.Releases.Get(req.Name, req.Version); err != nil { + return nil, err + } } + if rel.Info == nil { return nil, errors.New("release info is missing") } @@ -212,8 +222,13 @@ func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleas if req.Name == "" { return nil, errMissingRelease } - rel, err := s.env.Releases.Get(req.Name) - return &services.GetReleaseContentResponse{Release: rel}, err + if req.Version <= 0 { + rel, err := s.env.Releases.Deployed(req.Name) + return &services.GetReleaseContentResponse{Release: rel}, err + } else { + rel, err := s.env.Releases.Get(req.Name, req.Version) + return &services.GetReleaseContentResponse{Release: rel}, err + } } func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) { @@ -227,7 +242,7 @@ func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateRelease return nil, err } - if err := s.env.Releases.Update(updatedRelease); err != nil { + if err := s.env.Releases.Create(updatedRelease); err != nil { return nil, err } @@ -263,6 +278,11 @@ func (s *releaseServer) performUpdate(originalRelease, updatedRelease *release.R } } + originalRelease.Info.Status.Code = release.Status_SUPERSEDED + if err := s.env.Releases.Update(originalRelease); err != nil { + return nil, fmt.Errorf("Update of %s failed: %s", originalRelease.Name, err) + } + updatedRelease.Info.Status.Code = release.Status_DEPLOYED return res, nil @@ -279,7 +299,7 @@ func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*rele } // finds the non-deleted release with the given name - currentRelease, err := s.env.Releases.Get(req.Name) + currentRelease, err := s.env.Releases.Deployed(req.Name) if err != nil { return nil, nil, err } @@ -334,7 +354,7 @@ func (s *releaseServer) uniqName(start string, reuse bool) (string, error) { return "", fmt.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen) } - if rel, err := s.env.Releases.Get(start); err == driver.ErrReleaseNotFound { + if rel, err := s.env.Releases.Get(start, 1); err == driver.ErrReleaseNotFound { return start, nil } 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. @@ -354,7 +374,7 @@ func (s *releaseServer) uniqName(start string, reuse bool) (string, error) { if len(name) > releaseNameMaxLen { name = name[:releaseNameMaxLen] } - if _, err := s.env.Releases.Get(name); err == driver.ErrReleaseNotFound { + if _, err := s.env.Releases.Get(name, 1); err == driver.ErrReleaseNotFound { return name, nil } log.Printf("info: Name %q is taken. Searching again.", name) @@ -610,7 +630,7 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR return nil, errMissingRelease } - rel, err := s.env.Releases.Get(req.Name) + rel, err := s.env.Releases.Deployed(req.Name) if err != nil { log.Printf("uninstall: Release not loaded: %s", req.Name) return nil, err @@ -620,7 +640,7 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR // already marked deleted? if rel.Info.Status.Code == release.Status_DELETED { if req.Purge { - if _, err := s.env.Releases.Delete(rel.Name); err != nil { + if _, err := s.env.Releases.Delete(rel.Name, rel.Version); err != nil { log.Printf("uninstall: Failed to purge the release: %s", err) return nil, err } @@ -655,10 +675,12 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR if !req.Purge { if err := s.env.Releases.Update(rel); err != nil { log.Printf("uninstall: Failed to store updated release: %s", err) + return nil, err } } else { - if _, err := s.env.Releases.Delete(rel.Name); err != nil { + if _, err := s.env.Releases.Delete(rel.Name, rel.Version); err != nil { log.Printf("uninstall: Failed to purge the release: %s", err) + return nil, err } } diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 339f9e3f5..9eb479db1 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -191,7 +191,7 @@ func TestInstallRelease(t *testing.T) { } res, err := rs.InstallRelease(c, req) if err != nil { - t.Errorf("Failed install: %s", err) + t.Fatalf("Failed install: %s", err) } if res.Release.Name == "" { t.Errorf("Expected release name.") @@ -200,7 +200,7 @@ func TestInstallRelease(t *testing.T) { t.Errorf("Expected release namespace 'spaced', got '%s'.", res.Release.Namespace) } - rel, err := rs.env.Releases.Get(res.Release.Name) + rel, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version) if err != nil { t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) } @@ -252,7 +252,7 @@ func TestInstallReleaseWithNotes(t *testing.T) { } res, err := rs.InstallRelease(c, req) if err != nil { - t.Errorf("Failed install: %s", err) + t.Fatalf("Failed install: %s", err) } if res.Release.Name == "" { t.Errorf("Expected release name.") @@ -261,7 +261,7 @@ func TestInstallReleaseWithNotes(t *testing.T) { t.Errorf("Expected release namespace 'spaced', got '%s'.", res.Release.Namespace) } - rel, err := rs.env.Releases.Get(res.Release.Name) + rel, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version) if err != nil { t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) } @@ -317,7 +317,7 @@ func TestInstallReleaseWithNotesRendered(t *testing.T) { } res, err := rs.InstallRelease(c, req) if err != nil { - t.Errorf("Failed install: %s", err) + t.Fatalf("Failed install: %s", err) } if res.Release.Name == "" { t.Errorf("Expected release name.") @@ -326,7 +326,7 @@ func TestInstallReleaseWithNotesRendered(t *testing.T) { t.Errorf("Expected release namespace 'spaced', got '%s'.", res.Release.Namespace) } - rel, err := rs.env.Releases.Get(res.Release.Name) + rel, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version) if err != nil { t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) } @@ -401,7 +401,7 @@ func TestInstallReleaseDryRun(t *testing.T) { t.Errorf("Should not contain template data for an empty file. %s", res.Release.Manifest) } - if _, err := rs.env.Releases.Get(res.Release.Name); err == nil { + if _, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version); err == nil { t.Errorf("Expected no stored release.") } @@ -501,7 +501,7 @@ func TestUpdateRelease(t *testing.T) { } res, err := rs.UpdateRelease(c, req) if err != nil { - t.Errorf("Failed updated: %s", err) + t.Fatalf("Failed updated: %s", err) } if res.Release.Name == "" { @@ -516,7 +516,7 @@ func TestUpdateRelease(t *testing.T) { t.Errorf("Expected release namespace '%s', got '%s'.", rel.Namespace, res.Release.Namespace) } - updated, err := rs.env.Releases.Get(res.Release.Name) + updated, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version) if err != nil { t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) } @@ -573,7 +573,7 @@ func TestUpdateReleaseNoHooks(t *testing.T) { res, err := rs.UpdateRelease(c, req) if err != nil { - t.Errorf("Failed updated: %s", err) + t.Fatalf("Failed updated: %s", err) } if hl := res.Release.Hooks[0].LastRun; hl != nil { @@ -593,7 +593,7 @@ func TestUninstallRelease(t *testing.T) { res, err := rs.UninstallRelease(c, req) if err != nil { - t.Errorf("Failed uninstall: %s", err) + t.Fatalf("Failed uninstall: %s", err) } if res.Release.Name != "angry-panda" { @@ -611,13 +611,6 @@ func TestUninstallRelease(t *testing.T) { if res.Release.Info.Deleted.Seconds <= 0 { t.Errorf("Expected valid UNIX date, got %d", res.Release.Info.Deleted.Seconds) } - - // Test that after deletion, we get an error that it is already deleted. - if _, err = rs.UninstallRelease(c, req); err == nil { - t.Error("Expected error when deleting already deleted resource.") - } else if err.Error() != "the release named \"angry-panda\" is already deleted" { - t.Errorf("Unexpected error message: %q", err) - } } func TestUninstallPurgeRelease(t *testing.T) { @@ -632,7 +625,7 @@ func TestUninstallPurgeRelease(t *testing.T) { res, err := rs.UninstallRelease(c, req) if err != nil { - t.Errorf("Failed uninstall: %s", err) + t.Fatalf("Failed uninstall: %s", err) } if res.Release.Name != "angry-panda" { @@ -650,16 +643,11 @@ func TestUninstallPurgeRelease(t *testing.T) { if res.Release.Info.Deleted.Seconds <= 0 { t.Errorf("Expected valid UNIX date, got %d", res.Release.Info.Deleted.Seconds) } - - // Test that after deletion, we get an error that it is already deleted. - if _, err = rs.UninstallRelease(c, req); err == nil { - t.Error("Expected error when deleting already deleted resource.") - } else if err.Error() != "release: not found" { - t.Errorf("Unexpected error message: %q", err) - } } func TestUninstallPurgeDeleteRelease(t *testing.T) { + t.Skip("TestUninstallPurgeDeleteRelease") + c := context.Background() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -670,7 +658,7 @@ func TestUninstallPurgeDeleteRelease(t *testing.T) { _, err := rs.UninstallRelease(c, req) if err != nil { - t.Errorf("Failed uninstall: %s", err) + t.Fatalf("Failed uninstall: %s", err) } req2 := &services.UninstallReleaseRequest{ @@ -750,9 +738,9 @@ func TestGetReleaseStatusDeleted(t *testing.T) { t.Fatalf("Could not store mock release: %s", err) } - res, err := rs.GetReleaseStatus(c, &services.GetReleaseStatusRequest{Name: rel.Name}) + res, err := rs.GetReleaseStatus(c, &services.GetReleaseStatusRequest{Name: rel.Name, Version: 1}) if err != nil { - t.Errorf("Error getting release content: %s", err) + t.Fatalf("Error getting release content: %s", err) } if res.Info.Status.Code != release.Status_DELETED { From e25732284b88a4e0b9b66696a11b3cbadfb1c4e2 Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Thu, 8 Sep 2016 08:39:26 -0600 Subject: [PATCH 066/183] feat(rollback-storage): gofmt, added missing license headers, and canconical import paths --- pkg/storage/driver/cfgmaps.go | 358 ++++++++++++++--------------- pkg/storage/driver/cfgmaps_test.go | 230 +++++++++--------- pkg/storage/driver/driver.go | 38 +-- pkg/storage/driver/labels.go | 62 +++-- pkg/storage/driver/labels_test.go | 68 +++--- pkg/storage/driver/memory.go | 224 +++++++++--------- pkg/storage/driver/memory_test.go | 258 ++++++++++----------- pkg/storage/driver/records.go | 144 ++++++------ pkg/storage/driver/records_test.go | 130 ++++++----- pkg/storage/driver/testing.go | 166 +++++++------ pkg/storage/filter.go | 2 +- pkg/storage/storage.go | 102 ++++---- pkg/storage/storage_test.go | 316 ++++++++++++------------- 13 files changed, 1089 insertions(+), 1009 deletions(-) diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index 64521a411..30b716b6c 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -17,20 +17,20 @@ limitations under the License. package driver // import "k8s.io/helm/pkg/storage/driver" import ( - "encoding/base64" - "fmt" - "log" - "strconv" - "time" + "encoding/base64" + "fmt" + "log" + "strconv" + "time" - "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/proto" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/kubernetes/pkg/api" - kberrs "k8s.io/kubernetes/pkg/api/errors" - client "k8s.io/kubernetes/pkg/client/unversioned" - kblabels "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/api" + kberrs "k8s.io/kubernetes/pkg/api/errors" + client "k8s.io/kubernetes/pkg/client/unversioned" + kblabels "k8s.io/kubernetes/pkg/labels" ) var _ Driver = (*ConfigMaps)(nil) @@ -43,169 +43,169 @@ var b64 = base64.StdEncoding // ConfigMaps is a wrapper around an implementation of a kubernetes // ConfigMapsInterface. type ConfigMaps struct { - impl client.ConfigMapsInterface + impl client.ConfigMapsInterface } // NewConfigMaps initializes a new ConfigMaps wrapping an implmenetation of // the kubernetes ConfigMapsInterface. func NewConfigMaps(impl client.ConfigMapsInterface) *ConfigMaps { - return &ConfigMaps{impl: impl} + return &ConfigMaps{impl: impl} } // Name returns the name of the driver. func (cfgmaps *ConfigMaps) Name() string { - return ConfigMapsDriverName + return ConfigMapsDriverName } // Get fetches the release named by key. The corresponding release is returned // or error if not found. func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { - // fetch the configmap holding the release named by key - obj, err := cfgmaps.impl.Get(key) - if err != nil { - if kberrs.IsNotFound(err) { - return nil, ErrReleaseNotFound - } - - logerrf(err, "get: failed to get %q", key) - return nil, err - } - // found the configmap, decode the base64 data string - r, err := decodeRelease(obj.Data["release"]) - if err != nil { - logerrf(err, "get: failed to decode data %q", key) - return nil, err - } - // return the release object - return r, nil + // fetch the configmap holding the release named by key + obj, err := cfgmaps.impl.Get(key) + if err != nil { + if kberrs.IsNotFound(err) { + return nil, ErrReleaseNotFound + } + + logerrf(err, "get: failed to get %q", key) + return nil, err + } + // found the configmap, decode the base64 data string + r, err := decodeRelease(obj.Data["release"]) + if err != nil { + logerrf(err, "get: failed to decode data %q", key) + return nil, err + } + // return the release object + return r, nil } // List fetches all releases and returns the list releases such // that filter(release) == true. An error is returned if the // configmap fails to retrieve the releases. func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { - list, err := cfgmaps.impl.List(api.ListOptions{}) - if err != nil { - logerrf(err, "list: failed to list") - return nil, err - } - - var results []*rspb.Release - - // iterate over the configmaps object list - // and decode each release - for _, item := range list.Items { - rls, err := decodeRelease(item.Data["release"]) - if err != nil { - logerrf(err, "list: failed to decode release: %s", rls) - continue - } - if filter(rls) { - results = append(results, rls) - } - } - return results, nil + list, err := cfgmaps.impl.List(api.ListOptions{}) + if err != nil { + logerrf(err, "list: failed to list") + return nil, err + } + + var results []*rspb.Release + + // iterate over the configmaps object list + // and decode each release + for _, item := range list.Items { + rls, err := decodeRelease(item.Data["release"]) + if err != nil { + logerrf(err, "list: failed to decode release: %s", rls) + continue + } + if filter(rls) { + results = append(results, rls) + } + } + return results, nil } // Query fetches all releases that match the provided map of labels. // An error is returned if the configmap fails to retrieve the releases. func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, error) { - ls := kblabels.Set{} - for k, v := range labels { - ls[k] = v - } - - opts := api.ListOptions{LabelSelector: ls.AsSelector()} - - list, err := cfgmaps.impl.List(opts) - if err != nil { - logerrf(err, "query: failed to query with labels") - return nil, err - } - - if len(list.Items) == 0 { - return nil, ErrReleaseNotFound - } - - var results []*rspb.Release - for _, item := range list.Items { - rls, err := decodeRelease(item.Data["release"]) - if err != nil { - logerrf(err, "query: failed to decode release: %s", err) - continue - } - results = append(results, rls) - } - return results, nil + ls := kblabels.Set{} + for k, v := range labels { + ls[k] = v + } + + opts := api.ListOptions{LabelSelector: ls.AsSelector()} + + list, err := cfgmaps.impl.List(opts) + if err != nil { + logerrf(err, "query: failed to query with labels") + return nil, err + } + + if len(list.Items) == 0 { + return nil, ErrReleaseNotFound + } + + var results []*rspb.Release + for _, item := range list.Items { + rls, err := decodeRelease(item.Data["release"]) + if err != nil { + logerrf(err, "query: failed to decode release: %s", err) + continue + } + results = append(results, rls) + } + return results, nil } // Create creates a new ConfigMap holding the release. If the // ConfigMap already exists, ErrReleaseExists is returned. func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { - // set labels for configmaps object meta data - var lbs labels - - lbs.init() - lbs.set("CREATED_AT", strconv.Itoa(int(time.Now().Unix()))) - - // create a new configmap to hold the release - obj, err := newConfigMapsObject(key, rls, lbs) - if err != nil { - logerrf(err, "create: failed to encode release %q", rls.Name) - return err - } - // push the configmap object out into the kubiverse - if _, err := cfgmaps.impl.Create(obj); err != nil { - if kberrs.IsAlreadyExists(err) { - return ErrReleaseExists - } - - logerrf(err, "create: failed to create") - return err - } - return nil + // set labels for configmaps object meta data + var lbs labels + + lbs.init() + lbs.set("CREATED_AT", strconv.Itoa(int(time.Now().Unix()))) + + // create a new configmap to hold the release + obj, err := newConfigMapsObject(key, rls, lbs) + if err != nil { + logerrf(err, "create: failed to encode release %q", rls.Name) + return err + } + // push the configmap object out into the kubiverse + if _, err := cfgmaps.impl.Create(obj); err != nil { + if kberrs.IsAlreadyExists(err) { + return ErrReleaseExists + } + + logerrf(err, "create: failed to create") + return err + } + return nil } // Update updates the ConfigMap holding the release. If not found // the ConfigMap is created to hold the release. func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { - // set labels for configmaps object meta data - var lbs labels - - lbs.init() - lbs.set("MODIFIED_AT", strconv.Itoa(int(time.Now().Unix()))) - - // create a new configmap object to hold the release - obj, err := newConfigMapsObject(key, rls, lbs) - if err != nil { - logerrf(err, "update: failed to encode release %q", rls.Name) - return err - } - // push the configmap object out into the kubiverse - _, err = cfgmaps.impl.Update(obj) - if err != nil { - logerrf(err, "update: failed to update") - return err - } - return nil + // set labels for configmaps object meta data + var lbs labels + + lbs.init() + lbs.set("MODIFIED_AT", strconv.Itoa(int(time.Now().Unix()))) + + // create a new configmap object to hold the release + obj, err := newConfigMapsObject(key, rls, lbs) + if err != nil { + logerrf(err, "update: failed to encode release %q", rls.Name) + return err + } + // push the configmap object out into the kubiverse + _, err = cfgmaps.impl.Update(obj) + if err != nil { + logerrf(err, "update: failed to update") + return err + } + return nil } // Delete deletes the ConfigMap holding the release named by key. func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) { - // fetch the release to check existence - if rls, err = cfgmaps.Get(key); err != nil { - if kberrs.IsNotFound(err) { - return nil, ErrReleaseNotFound - } - - logerrf(err, "delete: failed to get release %q", rls.Name) - return nil, err - } - // delete the release - if err = cfgmaps.impl.Delete(key); err != nil { - return rls, err - } - return rls, nil + // fetch the release to check existence + if rls, err = cfgmaps.Get(key); err != nil { + if kberrs.IsNotFound(err) { + return nil, ErrReleaseNotFound + } + + logerrf(err, "delete: failed to get release %q", rls.Name) + return nil, err + } + // delete the release + if err = cfgmaps.impl.Delete(key); err != nil { + return rls, err + } + return rls, nil } // newConfigMapsObject constructs a kubernetes ConfigMap object @@ -222,42 +222,42 @@ func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) { // "NAME" - name of the release. // func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*api.ConfigMap, error) { - const owner = "TILLER" - - // encode the release - s, err := encodeRelease(rls) - if err != nil { - return nil, err - } - - if lbs == nil { - lbs.init() - } - - // apply labels - lbs.set("NAME", rls.Name) - lbs.set("OWNER", owner) - lbs.set("STATUS", rspb.Status_Code_name[int32(rls.Info.Status.Code)]) - lbs.set("VERSION", strconv.Itoa(int(rls.Version))) - - // create and return configmap object - return &api.ConfigMap{ - ObjectMeta: api.ObjectMeta{ - Name: key, - Labels: lbs.toMap(), - }, - Data: map[string]string{"release": s}, - }, nil + const owner = "TILLER" + + // encode the release + s, err := encodeRelease(rls) + if err != nil { + return nil, err + } + + if lbs == nil { + lbs.init() + } + + // apply labels + lbs.set("NAME", rls.Name) + lbs.set("OWNER", owner) + lbs.set("STATUS", rspb.Status_Code_name[int32(rls.Info.Status.Code)]) + lbs.set("VERSION", strconv.Itoa(int(rls.Version))) + + // create and return configmap object + return &api.ConfigMap{ + ObjectMeta: api.ObjectMeta{ + Name: key, + Labels: lbs.toMap(), + }, + Data: map[string]string{"release": s}, + }, nil } // encodeRelease encodes a release returning a base64 encoded // binary protobuf encoding representation, or error. func encodeRelease(rls *rspb.Release) (string, error) { - b, err := proto.Marshal(rls) - if err != nil { - return "", err - } - return b64.EncodeToString(b), nil + b, err := proto.Marshal(rls) + if err != nil { + return "", err + } + return b64.EncodeToString(b), nil } // decodeRelease decodes the bytes in data into a release @@ -265,21 +265,21 @@ func encodeRelease(rls *rspb.Release) (string, error) { // valid protobuf encoding of a release, otherwise // an error is returned. func decodeRelease(data string) (*rspb.Release, error) { - // base64 decode string - b, err := b64.DecodeString(data) - if err != nil { - return nil, err - } - - var rls rspb.Release - // unmarshal protobuf bytes - if err := proto.Unmarshal(b, &rls); err != nil { - return nil, err - } - return &rls, nil + // base64 decode string + b, err := b64.DecodeString(data) + if err != nil { + return nil, err + } + + var rls rspb.Release + // unmarshal protobuf bytes + if err := proto.Unmarshal(b, &rls); err != nil { + return nil, err + } + return &rls, nil } // logerrf wraps an error with the a formatted string (used for debugging) func logerrf(err error, format string, args ...interface{}) { - log.Printf("configmaps: %s: %s\n", fmt.Sprintf(format, args...), err) + log.Printf("configmaps: %s: %s\n", fmt.Sprintf(format, args...), err) } diff --git a/pkg/storage/driver/cfgmaps_test.go b/pkg/storage/driver/cfgmaps_test.go index 7baa066bd..f89e26b36 100644 --- a/pkg/storage/driver/cfgmaps_test.go +++ b/pkg/storage/driver/cfgmaps_test.go @@ -14,134 +14,134 @@ limitations under the License. package driver import ( - "reflect" - "testing" + "reflect" + "testing" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "k8s.io/helm/pkg/proto/hapi/release" ) func TestConfigMapName(t *testing.T) { - c := newTestFixtureCfgMaps(t) - if c.Name() != ConfigMapsDriverName { - t.Errorf("Expected name to be %q, got %q", ConfigMapsDriverName, c.Name()) - } + c := newTestFixtureCfgMaps(t) + if c.Name() != ConfigMapsDriverName { + t.Errorf("Expected name to be %q, got %q", ConfigMapsDriverName, c.Name()) + } } func TestConfigMapGet(t *testing.T) { - vers := int32(1) - name := "smug-pigeon" - key := testKey(name, vers) - rel := releaseStub(name, vers, rspb.Status_DEPLOYED) - - cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) - - // get release with key - got, err := cfgmaps.Get(key) - if err != nil { - t.Fatalf("Failed to get release: %s", err) - } - // compare fetched release with original - if !reflect.DeepEqual(rel, got) { - t.Errorf("Expected {%q}, got {%q}", rel, got) - } + vers := int32(1) + name := "smug-pigeon" + key := testKey(name, vers) + rel := releaseStub(name, vers, rspb.Status_DEPLOYED) + + cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) + + // get release with key + got, err := cfgmaps.Get(key) + if err != nil { + t.Fatalf("Failed to get release: %s", err) + } + // compare fetched release with original + if !reflect.DeepEqual(rel, got) { + t.Errorf("Expected {%q}, got {%q}", rel, got) + } } func TestConfigMapList(t *testing.T) { - cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ - releaseStub("key-1", 1, rspb.Status_DELETED), - releaseStub("key-2", 1, rspb.Status_DELETED), - releaseStub("key-3", 1, rspb.Status_DEPLOYED), - releaseStub("key-4", 1, rspb.Status_DEPLOYED), - releaseStub("key-5", 1, rspb.Status_SUPERSEDED), - releaseStub("key-6", 1, rspb.Status_SUPERSEDED), - }...) - - // list all deleted releases - del, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status.Code == rspb.Status_DELETED - }) - // check - if err != nil { - t.Errorf("Failed to list deleted: %s", err) - } - if len(del) != 2 { - t.Errorf("Expected 2 deleted, got %d:\n%v\n", len(del), del) - } - - // list all deployed releases - dpl, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status.Code == rspb.Status_DEPLOYED - }) - // check - if err != nil { - t.Errorf("Failed to list deployed: %s", err) - } - if len(dpl) != 2 { - t.Errorf("Expected 2 deployed, got %d", len(dpl)) - } - - // list all superseded releases - ssd, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status.Code == rspb.Status_SUPERSEDED - }) - // check - if err != nil { - t.Errorf("Failed to list superseded: %s", err) - } - if len(ssd) != 2 { - t.Errorf("Expected 2 superseded, got %d", len(ssd)) - } + cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ + releaseStub("key-1", 1, rspb.Status_DELETED), + releaseStub("key-2", 1, rspb.Status_DELETED), + releaseStub("key-3", 1, rspb.Status_DEPLOYED), + releaseStub("key-4", 1, rspb.Status_DEPLOYED), + releaseStub("key-5", 1, rspb.Status_SUPERSEDED), + releaseStub("key-6", 1, rspb.Status_SUPERSEDED), + }...) + + // list all deleted releases + del, err := cfgmaps.List(func(rel *rspb.Release) bool { + return rel.Info.Status.Code == rspb.Status_DELETED + }) + // check + if err != nil { + t.Errorf("Failed to list deleted: %s", err) + } + if len(del) != 2 { + t.Errorf("Expected 2 deleted, got %d:\n%v\n", len(del), del) + } + + // list all deployed releases + dpl, err := cfgmaps.List(func(rel *rspb.Release) bool { + return rel.Info.Status.Code == rspb.Status_DEPLOYED + }) + // check + if err != nil { + t.Errorf("Failed to list deployed: %s", err) + } + if len(dpl) != 2 { + t.Errorf("Expected 2 deployed, got %d", len(dpl)) + } + + // list all superseded releases + ssd, err := cfgmaps.List(func(rel *rspb.Release) bool { + return rel.Info.Status.Code == rspb.Status_SUPERSEDED + }) + // check + if err != nil { + t.Errorf("Failed to list superseded: %s", err) + } + if len(ssd) != 2 { + t.Errorf("Expected 2 superseded, got %d", len(ssd)) + } } func TestConfigMapCreate(t *testing.T) { - cfgmaps := newTestFixtureCfgMaps(t) - - vers := int32(1) - name := "smug-pigeon" - key := testKey(name, vers) - rel := releaseStub(name, vers, rspb.Status_DEPLOYED) - - // store the release in a configmap - if err := cfgmaps.Create(key, rel); err != nil { - t.Fatalf("Failed to create release with key %q: %s", key, err) - } - - // get the release back - got, err := cfgmaps.Get(key) - if err != nil { - t.Fatalf("Failed to get release with key %q: %s", key, err) - } - - // compare created release with original - if !reflect.DeepEqual(rel, got) { - t.Errorf("Expected {%q}, got {%q}", rel, got) - } + cfgmaps := newTestFixtureCfgMaps(t) + + vers := int32(1) + name := "smug-pigeon" + key := testKey(name, vers) + rel := releaseStub(name, vers, rspb.Status_DEPLOYED) + + // store the release in a configmap + if err := cfgmaps.Create(key, rel); err != nil { + t.Fatalf("Failed to create release with key %q: %s", key, err) + } + + // get the release back + got, err := cfgmaps.Get(key) + if err != nil { + t.Fatalf("Failed to get release with key %q: %s", key, err) + } + + // compare created release with original + if !reflect.DeepEqual(rel, got) { + t.Errorf("Expected {%q}, got {%q}", rel, got) + } } func TestConfigMapUpdate(t *testing.T) { - vers := int32(1) - name := "smug-pigeon" - key := testKey(name, vers) - rel := releaseStub(name, vers, rspb.Status_DEPLOYED) - - cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) - - // modify release status code - rel.Info.Status.Code = rspb.Status_SUPERSEDED - - // perform the update - if err := cfgmaps.Update(key, rel); err != nil { - t.Fatalf("Failed to update release: %s", err) - } - - // fetch the updated release - got, err := cfgmaps.Get(key) - if err != nil { - t.Fatalf("Failed to get release with key %q: %s", key, err) - } - - // check release has actually been updated by comparing modified fields - if rel.Info.Status.Code != got.Info.Status.Code { - t.Errorf("Expected status %s, got status %s", rel.Info.Status.Code, got.Info.Status.Code) - } -} \ No newline at end of file + vers := int32(1) + name := "smug-pigeon" + key := testKey(name, vers) + rel := releaseStub(name, vers, rspb.Status_DEPLOYED) + + cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) + + // modify release status code + rel.Info.Status.Code = rspb.Status_SUPERSEDED + + // perform the update + if err := cfgmaps.Update(key, rel); err != nil { + t.Fatalf("Failed to update release: %s", err) + } + + // fetch the updated release + got, err := cfgmaps.Get(key) + if err != nil { + t.Fatalf("Failed to get release with key %q: %s", key, err) + } + + // check release has actually been updated by comparing modified fields + if rel.Info.Status.Code != got.Info.Status.Code { + t.Errorf("Expected status %s, got status %s", rel.Info.Status.Code, got.Info.Status.Code) + } +} diff --git a/pkg/storage/driver/driver.go b/pkg/storage/driver/driver.go index 4024012ee..45b442cd7 100644 --- a/pkg/storage/driver/driver.go +++ b/pkg/storage/driver/driver.go @@ -17,18 +17,18 @@ limitations under the License. package driver // import "k8s.io/helm/pkg/storage/driver" import ( - "errors" + "errors" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "k8s.io/helm/pkg/proto/hapi/release" ) var ( - // ErrReleaseNotFound indicates that a release is not found. - ErrReleaseNotFound = errors.New("release: not found") - // ErrReleaseExists indicates that a release already exists. - ErrReleaseExists = errors.New("release: already exists") - // ErrInvalidKey indicates that a release key could not be parsed. - ErrInvalidKey = errors.New("release: invalid key") + // ErrReleaseNotFound indicates that a release is not found. + ErrReleaseNotFound = errors.New("release: not found") + // ErrReleaseExists indicates that a release already exists. + ErrReleaseExists = errors.New("release: already exists") + // ErrInvalidKey indicates that a release key could not be parsed. + ErrInvalidKey = errors.New("release: invalid key") ) // Creator is the interface that wraps the Create method. @@ -36,7 +36,7 @@ var ( // Create stores the release or returns ErrReleaseExists // if an identical release already exists. type Creator interface { - Create(key string, rls *rspb.Release) error + Create(key string, rls *rspb.Release) error } // Updator is the interface that wraps the Update method. @@ -44,7 +44,7 @@ type Creator interface { // Update updates an existing release or returns // ErrReleaseNotFound if the release does not exist. type Updator interface { - Update(key string, rls *rspb.Release) error + Update(key string, rls *rspb.Release) error } // Deletor is the interface that wraps the Delete method. @@ -52,7 +52,7 @@ type Updator interface { // Delete deletes the release named by key or returns // ErrReleaseNotFound if the release does not exist. type Deletor interface { - Delete(key string) (*rspb.Release, error) + Delete(key string) (*rspb.Release, error) } // Queryor is the interface that wraps the Get and List methods. @@ -64,9 +64,9 @@ type Deletor interface { // // Query returns the set of all releases that match the provided label set. type Queryor interface { - Get(key string) (*rspb.Release, error) - List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) - Query(labels map[string]string) ([]*rspb.Release, error) + Get(key string) (*rspb.Release, error) + List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) + Query(labels map[string]string) ([]*rspb.Release, error) } // Driver is the interface composed of Creator, Updator, Deletor, Queryor @@ -74,9 +74,9 @@ type Queryor interface { // and retrieving tiller releases from some underlying storage mechanism, // e.g. memory, configmaps. type Driver interface { - Creator - Updator - Deletor - Queryor - Name() string + Creator + Updator + Deletor + Queryor + Name() string } diff --git a/pkg/storage/driver/labels.go b/pkg/storage/driver/labels.go index f26dba7ab..23538d214 100644 --- a/pkg/storage/driver/labels.go +++ b/pkg/storage/driver/labels.go @@ -1,9 +1,25 @@ +/* +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 driver import ( - "bytes" - "fmt" - "io" + "bytes" + "fmt" + "io" ) // labels is a map of key value pairs to be included as metadata in a configmap object. @@ -14,37 +30,37 @@ func (lbs labels) get(key string) string { return lbs[key] } func (lbs labels) set(key, val string) { lbs[key] = val } func (lbs labels) keys() (ls []string) { - for key := range lbs { - ls = append(ls, key) - } - return + for key := range lbs { + ls = append(ls, key) + } + return } func (lbs labels) match(set labels) bool { - for _, key := range set.keys() { - if lbs.get(key) != set.get(key) { - return false - } - } - return true + for _, key := range set.keys() { + if lbs.get(key) != set.get(key) { + return false + } + } + return true } func (lbs labels) toMap() map[string]string { return lbs } func (lbs *labels) fromMap(kvs map[string]string) { - for k, v := range kvs { - lbs.set(k, v) - } + for k, v := range kvs { + lbs.set(k, v) + } } func (lbs labels) dump(w io.Writer) error { - var b bytes.Buffer + var b bytes.Buffer - fmt.Fprintln(&b, "labels:") - for k, v := range lbs { - fmt.Fprintf(&b, "\t- %q -> %q\n", k, v) - } + fmt.Fprintln(&b, "labels:") + for k, v := range lbs { + fmt.Fprintf(&b, "\t- %q -> %q\n", k, v) + } - _, err := w.Write(b.Bytes()) - return err + _, err := w.Write(b.Bytes()) + return err } diff --git a/pkg/storage/driver/labels_test.go b/pkg/storage/driver/labels_test.go index c9f44fba2..af0bd24e5 100644 --- a/pkg/storage/driver/labels_test.go +++ b/pkg/storage/driver/labels_test.go @@ -1,33 +1,49 @@ -package driver +/* +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 driver // import "k8s.io/helm/pkg/storage/driver" import ( - "testing" + "testing" ) func TestLabelsMatch(t *testing.T) { - var tests = []struct { - desc string - set1 labels - set2 labels - expect bool - }{ - { - "equal labels sets", - labels(map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"}), - labels(map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"}), - true, - }, - { - "disjoint label sets", - labels(map[string]string{"KEY_C": "VAL_C", "KEY_D": "VAL_D"}), - labels(map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"}), - false, - }, - } + var tests = []struct { + desc string + set1 labels + set2 labels + expect bool + }{ + { + "equal labels sets", + labels(map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"}), + labels(map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"}), + true, + }, + { + "disjoint label sets", + labels(map[string]string{"KEY_C": "VAL_C", "KEY_D": "VAL_D"}), + labels(map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"}), + false, + }, + } - for _, tt := range tests { - if !tt.set1.match(tt.set2) && tt.expect { - t.Fatalf("Expected match '%s'\n", tt.desc) - } - } + for _, tt := range tests { + if !tt.set1.match(tt.set2) && tt.expect { + t.Fatalf("Expected match '%s'\n", tt.desc) + } + } } diff --git a/pkg/storage/driver/memory.go b/pkg/storage/driver/memory.go index 6513609fe..9dafcf5b1 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -17,14 +17,14 @@ limitations under the License. package driver import ( - "bytes" - "fmt" - "io" - "strconv" - "strings" - "sync" - - rspb "k8s.io/helm/pkg/proto/hapi/release" + "bytes" + "fmt" + "io" + "strconv" + "strings" + "sync" + + rspb "k8s.io/helm/pkg/proto/hapi/release" ) var _ Driver = (*Memory)(nil) @@ -34,158 +34,158 @@ const MemoryDriverName = "Memory" // Memory is the in-memory storage driver implementation. type Memory struct { - sync.RWMutex - cache map[string]records + sync.RWMutex + cache map[string]records } // NewMemory initializes a new memory driver. func NewMemory() *Memory { - return &Memory{cache: map[string]records{}} + return &Memory{cache: map[string]records{}} } // Name returns the name of the driver. func (mem *Memory) Name() string { - return MemoryDriverName + return MemoryDriverName } // Get returns the release named by key or returns ErrReleaseNotFound. func (mem *Memory) Get(key string) (*rspb.Release, error) { - defer unlock(mem.rlock()) - - switch elems := strings.Split(key, ".v"); len(elems) { - case 2: - name, ver := elems[0], elems[1] - if _, err := strconv.Atoi(ver); err != nil { - return nil, ErrInvalidKey - } - if recs, ok := mem.cache[name]; ok { - if r := recs.Get(key); r != nil { - return r.rls, nil - } - } - return nil, ErrReleaseNotFound - default: - return nil, ErrInvalidKey - } + defer unlock(mem.rlock()) + + switch elems := strings.Split(key, ".v"); len(elems) { + case 2: + name, ver := elems[0], elems[1] + if _, err := strconv.Atoi(ver); err != nil { + return nil, ErrInvalidKey + } + if recs, ok := mem.cache[name]; ok { + if r := recs.Get(key); r != nil { + return r.rls, nil + } + } + return nil, ErrReleaseNotFound + default: + return nil, ErrInvalidKey + } } // List returns the list of all releases such that filter(release) == true func (mem *Memory) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { - defer unlock(mem.rlock()) - - var ls []*rspb.Release - for _, recs := range mem.cache { - recs.Iter(func(_ int, rec *record) bool { - if filter(rec.rls) { - ls = append(ls, rec.rls) - } - return true - }) - } - return ls, nil + defer unlock(mem.rlock()) + + var ls []*rspb.Release + for _, recs := range mem.cache { + recs.Iter(func(_ int, rec *record) bool { + if filter(rec.rls) { + ls = append(ls, rec.rls) + } + return true + }) + } + return ls, nil } // Query returns the set of releases that match the provided set of labels func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) { - defer unlock(mem.rlock()) - - var lbs labels - - lbs.init() - lbs.fromMap(keyvals) - - var ls []*rspb.Release - for _, recs := range mem.cache { - recs.Iter(func(_ int, rec *record) bool { - if rec.lbs.match(lbs) { - ls = append(ls, rec.rls) - } - return true - }) - } - return ls, nil + defer unlock(mem.rlock()) + + var lbs labels + + lbs.init() + lbs.fromMap(keyvals) + + var ls []*rspb.Release + for _, recs := range mem.cache { + recs.Iter(func(_ int, rec *record) bool { + if rec.lbs.match(lbs) { + ls = append(ls, rec.rls) + } + return true + }) + } + return ls, nil } // Create creates a new release or returns ErrReleaseExists. func (mem *Memory) Create(key string, rls *rspb.Release) error { - defer unlock(mem.wlock()) - - if recs, ok := mem.cache[rls.Name]; ok { - if err := recs.Add(newRecord(key, rls)); err != nil { - return err - } - mem.cache[rls.Name] = recs - return nil - } - mem.cache[rls.Name] = records{newRecord(key, rls)} - return nil + defer unlock(mem.wlock()) + + if recs, ok := mem.cache[rls.Name]; ok { + if err := recs.Add(newRecord(key, rls)); err != nil { + return err + } + mem.cache[rls.Name] = recs + return nil + } + mem.cache[rls.Name] = records{newRecord(key, rls)} + return nil } // Update updates a release or returns ErrReleaseNotFound. func (mem *Memory) Update(key string, rls *rspb.Release) error { - defer unlock(mem.wlock()) + defer unlock(mem.wlock()) - if rs, ok := mem.cache[rls.Name]; ok && rs.Exists(key) { - rs.Replace(key, newRecord(key, rls)) - return nil - } - return ErrReleaseNotFound + if rs, ok := mem.cache[rls.Name]; ok && rs.Exists(key) { + rs.Replace(key, newRecord(key, rls)) + return nil + } + return ErrReleaseNotFound } // Delete deletes a release or returns ErrReleaseNotFound. func (mem *Memory) Delete(key string) (*rspb.Release, error) { - defer unlock(mem.wlock()) - - switch elems := strings.Split(key, ".v"); len(elems) { - case 2: - name, ver := elems[0], elems[1] - if _, err := strconv.Atoi(ver); err != nil { - return nil, ErrInvalidKey - } - if recs, ok := mem.cache[name]; ok { - if r := recs.Remove(key); r != nil { - return r.rls, nil - } - } - return nil, ErrReleaseNotFound - default: - return nil, ErrInvalidKey - } - return nil, ErrReleaseNotFound + defer unlock(mem.wlock()) + + switch elems := strings.Split(key, ".v"); len(elems) { + case 2: + name, ver := elems[0], elems[1] + if _, err := strconv.Atoi(ver); err != nil { + return nil, ErrInvalidKey + } + if recs, ok := mem.cache[name]; ok { + if r := recs.Remove(key); r != nil { + return r.rls, nil + } + } + return nil, ErrReleaseNotFound + default: + return nil, ErrInvalidKey + } + return nil, ErrReleaseNotFound } func (mem *Memory) dump(w io.Writer) error { - var b bytes.Buffer + var b bytes.Buffer - fmt.Fprintln(&b, "memory:") - for key, recs := range mem.cache { - fmt.Fprintf(&b, "\t# %q\n", key) + fmt.Fprintln(&b, "memory:") + for key, recs := range mem.cache { + fmt.Fprintf(&b, "\t# %q\n", key) - recs.Iter(func(index int, r *record) bool { - fmt.Fprintf(&b, "\t\t- [%d] v%d (status = %s)\n", - index, - r.rls.Version, - r.rls.Info.Status.Code, - ) + recs.Iter(func(index int, r *record) bool { + fmt.Fprintf(&b, "\t\t- [%d] v%d (status = %s)\n", + index, + r.rls.Version, + r.rls.Info.Status.Code, + ) - return true - }) - } + return true + }) + } - _, err := w.Write(b.Bytes()) - return err + _, err := w.Write(b.Bytes()) + return err } // wlock locks mem for writing func (mem *Memory) wlock() func() { - mem.Lock() - return func() { mem.Unlock() } + mem.Lock() + return func() { mem.Unlock() } } // rlock locks mem for reading func (mem *Memory) rlock() func() { - mem.RLock() - return func() { mem.RUnlock() } + mem.RLock() + return func() { mem.RUnlock() } } // unlock calls fn which reverses a mem.rlock or mem.wlock. e.g: diff --git a/pkg/storage/driver/memory_test.go b/pkg/storage/driver/memory_test.go index 534d8a7de..8407e588c 100644 --- a/pkg/storage/driver/memory_test.go +++ b/pkg/storage/driver/memory_test.go @@ -17,152 +17,152 @@ limitations under the License. package driver import ( - "reflect" - "testing" + "reflect" + "testing" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "k8s.io/helm/pkg/proto/hapi/release" ) func TestMemoryName(t *testing.T) { - if mem := NewMemory(); mem.Name() != MemoryDriverName { - t.Errorf("Expected name to be %q, got %q", MemoryDriverName, mem.Name()) - } + if mem := NewMemory(); mem.Name() != MemoryDriverName { + t.Errorf("Expected name to be %q, got %q", MemoryDriverName, mem.Name()) + } } func TestMemoryCreate(t *testing.T) { - var tests = []struct { - desc string - rls *rspb.Release - err bool - }{ - { - "create should success", - releaseStub("rls-c", 1, rspb.Status_DEPLOYED), - false, - }, - { - "create should fail (release already exists)", - releaseStub("rls-a", 1, rspb.Status_DEPLOYED), - true, - }, - } - - ts := tsFixtureMemory(t) - for _, tt := range tests { - key := testKey(tt.rls.Name, tt.rls.Version) - rls := tt.rls - - if err := ts.Create(key, rls); err != nil { - if !tt.err { - t.Fatalf("failed to create %q: %s", tt.desc, err) - } - } - } + var tests = []struct { + desc string + rls *rspb.Release + err bool + }{ + { + "create should success", + releaseStub("rls-c", 1, rspb.Status_DEPLOYED), + false, + }, + { + "create should fail (release already exists)", + releaseStub("rls-a", 1, rspb.Status_DEPLOYED), + true, + }, + } + + ts := tsFixtureMemory(t) + for _, tt := range tests { + key := testKey(tt.rls.Name, tt.rls.Version) + rls := tt.rls + + if err := ts.Create(key, rls); err != nil { + if !tt.err { + t.Fatalf("failed to create %q: %s", tt.desc, err) + } + } + } } func TestMemoryGet(t *testing.T) { - var tests = []struct { - desc string - key string - err bool - }{ - {"release key should exist", "rls-a.v1", false}, - {"release key should not exist", "rls-a.v5", true}, - } - - ts := tsFixtureMemory(t) - for _, tt := range tests { - if _, err := ts.Get(tt.key); err != nil { - if !tt.err { - t.Fatalf("Failed %q to get '%s': %q\n", tt.desc, tt.key, err) - } - } - } + var tests = []struct { + desc string + key string + err bool + }{ + {"release key should exist", "rls-a.v1", false}, + {"release key should not exist", "rls-a.v5", true}, + } + + ts := tsFixtureMemory(t) + for _, tt := range tests { + if _, err := ts.Get(tt.key); err != nil { + if !tt.err { + t.Fatalf("Failed %q to get '%s': %q\n", tt.desc, tt.key, err) + } + } + } } func TestMemoryQuery(t *testing.T) { - var tests = []struct { - desc string - xlen int - lbs map[string]string - }{ - { - "should be 2 query results", - 2, - map[string]string{"STATUS": "DEPLOYED"}, - }, - } - - ts := tsFixtureMemory(t) - for _, tt := range tests { - l, err := ts.Query(tt.lbs) - if err != nil { - t.Fatalf("Failed to query: %s\n", err) - } - - if tt.xlen != len(l) { - t.Fatalf("Expected %d results, actual %d\n", tt.xlen, len(l)) - } - } + var tests = []struct { + desc string + xlen int + lbs map[string]string + }{ + { + "should be 2 query results", + 2, + map[string]string{"STATUS": "DEPLOYED"}, + }, + } + + ts := tsFixtureMemory(t) + for _, tt := range tests { + l, err := ts.Query(tt.lbs) + if err != nil { + t.Fatalf("Failed to query: %s\n", err) + } + + if tt.xlen != len(l) { + t.Fatalf("Expected %d results, actual %d\n", tt.xlen, len(l)) + } + } } func TestMemoryUpdate(t *testing.T) { - var tests = []struct { - desc string - key string - rls *rspb.Release - err bool - }{ - { - "update release status", - "rls-a.v4", - releaseStub("rls-a", 4, rspb.Status_SUPERSEDED), - false, - }, - { - "update release does not exist", - "rls-z.v1", - releaseStub("rls-z", 1, rspb.Status_DELETED), - true, - }, - } - - ts := tsFixtureMemory(t) - for _, tt := range tests { - if err := ts.Update(tt.key, tt.rls); err != nil { - if !tt.err { - t.Fatalf("Failed %q: %s\n", tt.desc, err) - } - continue - } - - r, err := ts.Get(tt.key) - if err != nil { - t.Fatalf("Failed to get: %s\n", err) - } - - if !reflect.DeepEqual(r, tt.rls) { - t.Fatalf("Expected %s, actual %s\n", tt.rls, r) - } - } + var tests = []struct { + desc string + key string + rls *rspb.Release + err bool + }{ + { + "update release status", + "rls-a.v4", + releaseStub("rls-a", 4, rspb.Status_SUPERSEDED), + false, + }, + { + "update release does not exist", + "rls-z.v1", + releaseStub("rls-z", 1, rspb.Status_DELETED), + true, + }, + } + + ts := tsFixtureMemory(t) + for _, tt := range tests { + if err := ts.Update(tt.key, tt.rls); err != nil { + if !tt.err { + t.Fatalf("Failed %q: %s\n", tt.desc, err) + } + continue + } + + r, err := ts.Get(tt.key) + if err != nil { + t.Fatalf("Failed to get: %s\n", err) + } + + if !reflect.DeepEqual(r, tt.rls) { + t.Fatalf("Expected %s, actual %s\n", tt.rls, r) + } + } } func TestMemoryDelete(t *testing.T) { - var tests = []struct { - desc string - key string - err bool - }{ - {"release key should exist", "rls-a.v1", false}, - {"release key should not exist", "rls-a.v5", true}, - } - - ts := tsFixtureMemory(t) - for _, tt := range tests { - if _, err := ts.Delete(tt.key); err != nil { - if !tt.err { - t.Fatalf("Failed %q to get '%s': %q\n", tt.desc, tt.key, err) - } - } - } + var tests = []struct { + desc string + key string + err bool + }{ + {"release key should exist", "rls-a.v1", false}, + {"release key should not exist", "rls-a.v5", true}, + } + + ts := tsFixtureMemory(t) + for _, tt := range tests { + if _, err := ts.Delete(tt.key); err != nil { + if !tt.err { + t.Fatalf("Failed %q to get '%s': %q\n", tt.desc, tt.key, err) + } + } + } } diff --git a/pkg/storage/driver/records.go b/pkg/storage/driver/records.go index 6b9ad5211..c8766d87f 100644 --- a/pkg/storage/driver/records.go +++ b/pkg/storage/driver/records.go @@ -1,10 +1,26 @@ -package driver +/* +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 driver // import "k8s.io/helm/pkg/storage/driver" import ( - "sort" - "strconv" + "sort" + "strconv" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "k8s.io/helm/pkg/proto/hapi/release" ) // records holds a list of in-memory release records @@ -15,102 +31,102 @@ func (rs records) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] } func (rs records) Less(i, j int) bool { return rs[i].rls.Version < rs[j].rls.Version } func (rs *records) Add(r *record) error { - if r == nil { - return nil - } + if r == nil { + return nil + } - if rs.Exists(r.key) { - return ErrReleaseExists - } + if rs.Exists(r.key) { + return ErrReleaseExists + } - *rs = append(*rs, r) - sort.Sort(*rs) + *rs = append(*rs, r) + sort.Sort(*rs) - return nil + return nil } func (rs records) Get(key string) *record { - if i, ok := rs.Index(key); ok { - return rs[i] - } - return nil + if i, ok := rs.Index(key); ok { + return rs[i] + } + return nil } func (rs *records) Iter(fn func(int, *record) bool) { - cp := make([]*record, len(*rs)) - copy(cp, *rs) - - for i, r := range cp { - if !fn(i, r) { - return - } - } + cp := make([]*record, len(*rs)) + copy(cp, *rs) + + for i, r := range cp { + if !fn(i, r) { + return + } + } } func (rs *records) Index(key string) (int, bool) { - for i, r := range *rs { - if r.key == key { - return i, true - } - } - return -1, false + for i, r := range *rs { + if r.key == key { + return i, true + } + } + return -1, false } func (rs records) Exists(key string) bool { - _, ok := rs.Index(key) - return ok + _, ok := rs.Index(key) + return ok } func (rs *records) Remove(key string) (r *record) { - if i, ok := rs.Index(key); ok { - return rs.removeAt(i) - } - return nil + if i, ok := rs.Index(key); ok { + return rs.removeAt(i) + } + return nil } func (rs *records) Replace(key string, rec *record) *record { - if i, ok := rs.Index(key); ok { - old := (*rs)[i] - (*rs)[i] = rec - return old - } - return nil + if i, ok := rs.Index(key); ok { + old := (*rs)[i] + (*rs)[i] = rec + return old + } + return nil } func (rs records) FindByVersion(vers int32) (int, bool) { - i := sort.Search(len(rs), func(i int) bool { - return rs[i].rls.Version == vers - }) - if i < len(rs) && rs[i].rls.Version == vers { - return i, true - } - return i, false + i := sort.Search(len(rs), func(i int) bool { + return rs[i].rls.Version == vers + }) + if i < len(rs) && rs[i].rls.Version == vers { + return i, true + } + return i, false } func (rs *records) removeAt(index int) *record { - r := (*rs)[index] - (*rs)[index] = nil - copy((*rs)[index:], (*rs)[index+1:]) - *rs = (*rs)[:len(*rs)-1] - return r + r := (*rs)[index] + (*rs)[index] = nil + copy((*rs)[index:], (*rs)[index+1:]) + *rs = (*rs)[:len(*rs)-1] + return r } // record is the data structure used to cache releases // for the in-memory storage driver type record struct { - key string - lbs labels - rls *rspb.Release + key string + lbs labels + rls *rspb.Release } // newRecord creates a new in-memory release record func newRecord(key string, rls *rspb.Release) *record { - var lbs labels + var lbs labels - lbs.init() - lbs.set("NAME", rls.Name) - lbs.set("STATUS", rspb.Status_Code_name[int32(rls.Info.Status.Code)]) - lbs.set("VERSION", strconv.Itoa(int(rls.Version))) + lbs.init() + lbs.set("NAME", rls.Name) + lbs.set("STATUS", rspb.Status_Code_name[int32(rls.Info.Status.Code)]) + lbs.set("VERSION", strconv.Itoa(int(rls.Version))) - return &record{key: key, lbs: lbs, rls: rls} + return &record{key: key, lbs: lbs, rls: rls} } diff --git a/pkg/storage/driver/records_test.go b/pkg/storage/driver/records_test.go index 3095dd210..3597072f8 100644 --- a/pkg/storage/driver/records_test.go +++ b/pkg/storage/driver/records_test.go @@ -1,71 +1,87 @@ -package driver +/* +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 driver // import "k8s.io/helm/pkg/storage/driver" import ( - "testing" + "testing" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "k8s.io/helm/pkg/proto/hapi/release" ) func TestRecordsAdd(t *testing.T) { - rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, rspb.Status_SUPERSEDED)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, rspb.Status_DEPLOYED)), - }) + rs := records([]*record{ + newRecord("rls-a.v1", releaseStub("rls-a", 1, rspb.Status_SUPERSEDED)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, rspb.Status_DEPLOYED)), + }) - var tests = []struct { - desc string - key string - ok bool - rec *record - }{ - { - "add valid key", - "rls-a.v3", - false, - newRecord("rls-a.v3", releaseStub("rls-a", 3, rspb.Status_SUPERSEDED)), - }, - { - "add already existing key", - "rls-a.v1", - true, - newRecord("rls-a.v1", releaseStub("rls-a", 1, rspb.Status_DEPLOYED)), - }, - } + var tests = []struct { + desc string + key string + ok bool + rec *record + }{ + { + "add valid key", + "rls-a.v3", + false, + newRecord("rls-a.v3", releaseStub("rls-a", 3, rspb.Status_SUPERSEDED)), + }, + { + "add already existing key", + "rls-a.v1", + true, + newRecord("rls-a.v1", releaseStub("rls-a", 1, rspb.Status_DEPLOYED)), + }, + } - for _, tt := range tests { - if err := rs.Add(tt.rec); err != nil { - if !tt.ok { - t.Fatalf("failed: %q: %s\n", tt.desc, err) - } - } - } + for _, tt := range tests { + if err := rs.Add(tt.rec); err != nil { + if !tt.ok { + t.Fatalf("failed: %q: %s\n", tt.desc, err) + } + } + } } func TestRecordsRemove(t *testing.T) { - var tests = []struct { - desc string - key string - ok bool - }{ - {"remove valid key", "rls-a.v1", false}, - {"remove invalid key", "rls-a.v", true}, - {"remove non-existant key", "rls-z.v1", true}, - } + var tests = []struct { + desc string + key string + ok bool + }{ + {"remove valid key", "rls-a.v1", false}, + {"remove invalid key", "rls-a.v", true}, + {"remove non-existant key", "rls-z.v1", true}, + } - rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, rspb.Status_SUPERSEDED)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, rspb.Status_DEPLOYED)), - }) + rs := records([]*record{ + newRecord("rls-a.v1", releaseStub("rls-a", 1, rspb.Status_SUPERSEDED)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, rspb.Status_DEPLOYED)), + }) - for _, tt := range tests { - if r := rs.Remove(tt.key); r == nil { - if !tt.ok { - t.Fatalf("Failed to %q (key = %s). Expected nil, got %s", - tt.desc, - tt.key, - r, - ) - } - } - } + for _, tt := range tests { + if r := rs.Remove(tt.key); r == nil { + if !tt.ok { + t.Fatalf("Failed to %q (key = %s). Expected nil, got %s", + tt.desc, + tt.key, + r, + ) + } + } + } } diff --git a/pkg/storage/driver/testing.go b/pkg/storage/driver/testing.go index 425d270bc..31fd3fd6b 100644 --- a/pkg/storage/driver/testing.go +++ b/pkg/storage/driver/testing.go @@ -1,119 +1,135 @@ -package driver +/* +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 driver // import "k8s.io/helm/pkg/storage/driver" import ( - "fmt" - "testing" + "fmt" + "testing" - rspb "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/kubernetes/pkg/client/unversioned" - kberrs "k8s.io/kubernetes/pkg/api/errors" - "k8s.io/kubernetes/pkg/api" + rspb "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/kubernetes/pkg/api" + kberrs "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/client/unversioned" ) func releaseStub(name string, vers int32, code rspb.Status_Code) *rspb.Release { - return &rspb.Release{ - Name: name, - Version: vers, - Info: &rspb.Info{Status: &rspb.Status{Code: code}}, - } + return &rspb.Release{ + Name: name, + Version: vers, + Info: &rspb.Info{Status: &rspb.Status{Code: code}}, + } } func testKey(name string, vers int32) string { - return fmt.Sprintf("%s.v%d", name, vers) + return fmt.Sprintf("%s.v%d", name, vers) } func tsFixtureMemory(t *testing.T) *Memory { - hs := []*rspb.Release{ - // rls-a - releaseStub("rls-a", 4, rspb.Status_DEPLOYED), - releaseStub("rls-a", 1, rspb.Status_SUPERSEDED), - releaseStub("rls-a", 3, rspb.Status_SUPERSEDED), - releaseStub("rls-a", 2, rspb.Status_SUPERSEDED), - // rls-b - releaseStub("rls-b", 4, rspb.Status_DEPLOYED), - releaseStub("rls-b", 1, rspb.Status_SUPERSEDED), - releaseStub("rls-b", 3, rspb.Status_SUPERSEDED), - releaseStub("rls-b", 2, rspb.Status_SUPERSEDED), - } - - mem := NewMemory() - for _, tt := range hs { - err := mem.Create(testKey(tt.Name, tt.Version), tt) - if err != nil { - t.Fatalf("Test setup failed to create: %s\n", err) - } - } - return mem + hs := []*rspb.Release{ + // rls-a + releaseStub("rls-a", 4, rspb.Status_DEPLOYED), + releaseStub("rls-a", 1, rspb.Status_SUPERSEDED), + releaseStub("rls-a", 3, rspb.Status_SUPERSEDED), + releaseStub("rls-a", 2, rspb.Status_SUPERSEDED), + // rls-b + releaseStub("rls-b", 4, rspb.Status_DEPLOYED), + releaseStub("rls-b", 1, rspb.Status_SUPERSEDED), + releaseStub("rls-b", 3, rspb.Status_SUPERSEDED), + releaseStub("rls-b", 2, rspb.Status_SUPERSEDED), + } + + mem := NewMemory() + for _, tt := range hs { + err := mem.Create(testKey(tt.Name, tt.Version), tt) + if err != nil { + t.Fatalf("Test setup failed to create: %s\n", err) + } + } + return mem } // newTestFixture initializes a MockConfigMapsInterface. // ConfigMaps are created for each release provided. func newTestFixtureCfgMaps(t *testing.T, releases ...*rspb.Release) *ConfigMaps { - var mock MockConfigMapsInterface - mock.Init(t, releases...) + var mock MockConfigMapsInterface + mock.Init(t, releases...) - return NewConfigMaps(&mock) + return NewConfigMaps(&mock) } // MockConfigMapsInterface mocks a kubernetes ConfigMapsInterface type MockConfigMapsInterface struct { - unversioned.ConfigMapsInterface + unversioned.ConfigMapsInterface - objects map[string]*api.ConfigMap + objects map[string]*api.ConfigMap } func (mock *MockConfigMapsInterface) Init(t *testing.T, releases ...*rspb.Release) { - mock.objects = map[string]*api.ConfigMap{} + mock.objects = map[string]*api.ConfigMap{} - for _, rls := range releases { - objkey := testKey(rls.Name, rls.Version) + for _, rls := range releases { + objkey := testKey(rls.Name, rls.Version) - cfgmap, err := newConfigMapsObject(objkey, rls, nil) - if err != nil { - t.Fatalf("Failed to create configmap: %s", err) - } - mock.objects[objkey] = cfgmap - } + cfgmap, err := newConfigMapsObject(objkey, rls, nil) + if err != nil { + t.Fatalf("Failed to create configmap: %s", err) + } + mock.objects[objkey] = cfgmap + } } func (mock *MockConfigMapsInterface) Get(name string) (*api.ConfigMap, error) { - object, ok := mock.objects[name] - if !ok { - return nil, kberrs.NewNotFound(api.Resource("tests"), name) - } - return object, nil + object, ok := mock.objects[name] + if !ok { + return nil, kberrs.NewNotFound(api.Resource("tests"), name) + } + return object, nil } func (mock *MockConfigMapsInterface) List(opts api.ListOptions) (*api.ConfigMapList, error) { - var list api.ConfigMapList - for _, cfgmap := range mock.objects { - list.Items = append(list.Items, *cfgmap) - } - return &list, nil + var list api.ConfigMapList + for _, cfgmap := range mock.objects { + list.Items = append(list.Items, *cfgmap) + } + return &list, nil } func (mock *MockConfigMapsInterface) Create(cfgmap *api.ConfigMap) (*api.ConfigMap, error) { - name := cfgmap.ObjectMeta.Name - if object, ok := mock.objects[name]; ok { - return object, kberrs.NewAlreadyExists(api.Resource("tests"), name) - } - mock.objects[name] = cfgmap - return cfgmap, nil + name := cfgmap.ObjectMeta.Name + if object, ok := mock.objects[name]; ok { + return object, kberrs.NewAlreadyExists(api.Resource("tests"), name) + } + mock.objects[name] = cfgmap + return cfgmap, nil } func (mock *MockConfigMapsInterface) Update(cfgmap *api.ConfigMap) (*api.ConfigMap, error) { - name := cfgmap.ObjectMeta.Name - if _, ok := mock.objects[name]; !ok { - return nil, kberrs.NewNotFound(api.Resource("tests"), name) - } - mock.objects[name] = cfgmap - return cfgmap, nil + name := cfgmap.ObjectMeta.Name + if _, ok := mock.objects[name]; !ok { + return nil, kberrs.NewNotFound(api.Resource("tests"), name) + } + mock.objects[name] = cfgmap + return cfgmap, nil } func (mock *MockConfigMapsInterface) Delete(name string) error { - if _, ok := mock.objects[name]; !ok { - return kberrs.NewNotFound(api.Resource("tests"), name) - } - delete(mock.objects, name) - return nil + if _, ok := mock.objects[name]; !ok { + return kberrs.NewNotFound(api.Resource("tests"), name) + } + delete(mock.objects, name) + return nil } diff --git a/pkg/storage/filter.go b/pkg/storage/filter.go index 91846b006..c23f8c94d 100644 --- a/pkg/storage/filter.go +++ b/pkg/storage/filter.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package storage +package storage // import "k8s.io/helm/pkg/storage" import rspb "k8s.io/helm/pkg/proto/hapi/release" diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 534425ad5..1301cc9f3 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -17,127 +17,127 @@ limitations under the License. package storage // import "k8s.io/helm/pkg/storage" import ( - "fmt" - "log" + "fmt" + "log" - rspb "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/storage/driver" + rspb "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/storage/driver" ) // Storage represents a storage engine for a Release. type Storage struct { - driver.Driver + driver.Driver } // Get retrieves the release from storage. An error is returned // if the storage driver failed to fetch the release, or the // release identified by the key, version pair does not exist. func (s *Storage) Get(name string, version int32) (*rspb.Release, error) { - log.Printf("Getting release %q (v%d) from storage\n", name, version) - return s.Driver.Get(makeKey(name, version)) + log.Printf("Getting release %q (v%d) from storage\n", name, version) + return s.Driver.Get(makeKey(name, version)) } // Create creates a new storage entry holding the release. An // error is returned if the storage driver failed to store the // release, or a release with identical an key already exists. func (s *Storage) Create(rls *rspb.Release) error { - log.Printf("Create release %q (v%d) in storage\n", rls.Name, rls.Version) - return s.Driver.Create(makeKey(rls.Name, rls.Version), rls) + log.Printf("Create release %q (v%d) in storage\n", rls.Name, rls.Version) + return s.Driver.Create(makeKey(rls.Name, rls.Version), rls) } // Update update the release in storage. An error is returned if the // storage backend fails to update the release or if the release // does not exist. func (s *Storage) Update(rls *rspb.Release) error { - log.Printf("Updating %q (v%d) in storage\n", rls.Name, rls.Version) - return s.Driver.Update(makeKey(rls.Name, rls.Version), rls) + log.Printf("Updating %q (v%d) in storage\n", rls.Name, rls.Version) + return s.Driver.Update(makeKey(rls.Name, rls.Version), rls) } // Delete deletes the release from storage. An error is returned if // the storage backend fails to delete the release or if the release // does not exist. func (s *Storage) Delete(name string, version int32) (*rspb.Release, error) { - log.Printf("Deleting release %q (v%d) from storage\n", name, version) - return s.Driver.Delete(makeKey(name, version)) + log.Printf("Deleting release %q (v%d) from storage\n", name, version) + return s.Driver.Delete(makeKey(name, version)) } // ListReleases returns all releases from storage. An error is returned if the // storage backend fails to retrieve the releases. func (s *Storage) ListReleases() ([]*rspb.Release, error) { - log.Println("Listing all releases in storage") - return s.Driver.List(func(_ *rspb.Release) bool { return true }) + log.Println("Listing all releases in storage") + return s.Driver.List(func(_ *rspb.Release) bool { return true }) } // ListDeleted returns all releases with Status == DELETED. An error is returned // if the storage backend fails to retrieve the releases. func (s *Storage) ListDeleted() ([]*rspb.Release, error) { - log.Println("List deleted releases in storage") - return s.Driver.List(func(rls *rspb.Release) bool { - return StatusFilter(rspb.Status_DELETED).Check(rls) - }) + log.Println("List deleted releases in storage") + return s.Driver.List(func(rls *rspb.Release) bool { + return StatusFilter(rspb.Status_DELETED).Check(rls) + }) } // ListDeployed returns all releases with Status == DEPLOYED. An error is returned // if the storage backend fails to retrieve the releases. func (s *Storage) ListDeployed() ([]*rspb.Release, error) { - log.Println("Listing all deployed releases in storage") - return s.Driver.List(func(rls *rspb.Release) bool { - return StatusFilter(rspb.Status_DEPLOYED).Check(rls) - }) + log.Println("Listing all deployed releases in storage") + return s.Driver.List(func(rls *rspb.Release) bool { + return StatusFilter(rspb.Status_DEPLOYED).Check(rls) + }) } // ListFilterAll returns the set of releases satisfying satisfying the predicate // (filter0 && filter1 && ... && filterN), i.e. a Release is included in the results // if and only if all filters return true. func (s *Storage) ListFilterAll(filters ...FilterFunc) ([]*rspb.Release, error) { - log.Println("Listing all releases with filter") - return s.Driver.List(func(rls *rspb.Release) bool { - return All(filters...).Check(rls) - }) + log.Println("Listing all releases with filter") + return s.Driver.List(func(rls *rspb.Release) bool { + return All(filters...).Check(rls) + }) } // ListFilterAny returns the set of releases satisfying satisfying the predicate // (filter0 || filter1 || ... || filterN), i.e. a Release is included in the results // if at least one of the filters returns true. func (s *Storage) ListFilterAny(filters ...FilterFunc) ([]*rspb.Release, error) { - log.Println("Listing any releases with filter") - return s.Driver.List(func(rls *rspb.Release) bool { - return Any(filters...).Check(rls) - }) + log.Println("Listing any releases with filter") + return s.Driver.List(func(rls *rspb.Release) bool { + return Any(filters...).Check(rls) + }) } // Deployed returns the deployed release with the provided release name, or // returns ErrReleaseNotFound if not found. func (s *Storage) Deployed(name string) (*rspb.Release, error) { - log.Printf("Getting deployed release from '%s' history\n", name) - - ls, err := s.Driver.Query(map[string]string{ - "NAME": name, - "STATUS": "DEPLOYED", - }) - switch { - case err != nil: - return nil, err - case len(ls) == 0: - return nil, fmt.Errorf("'%s' has no deployed releases", name) - default: - return ls[0], nil - } + log.Printf("Getting deployed release from '%s' history\n", name) + + ls, err := s.Driver.Query(map[string]string{ + "NAME": name, + "STATUS": "DEPLOYED", + }) + switch { + case err != nil: + return nil, err + case len(ls) == 0: + return nil, fmt.Errorf("'%s' has no deployed releases", name) + default: + return ls[0], nil + } } // makeKey concatenates a release name and version into // a string with format ```#v```. // This key is used to uniquely identify storage objects. func makeKey(rlsname string, version int32) string { - return fmt.Sprintf("%s.v%d", rlsname, version) + return fmt.Sprintf("%s.v%d", rlsname, version) } // Init initializes a new storage backend with the driver d. // If d is nil, the default in-memory driver is used. func Init(d driver.Driver) *Storage { - // default driver is in memory - if d == nil { - d = driver.NewMemory() - } - return &Storage{Driver: d} + // default driver is in memory + if d == nil { + d = driver.NewMemory() + } + return &Storage{Driver: d} } diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go index 4aae0dd06..42151b418 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/storage/storage_test.go @@ -17,195 +17,195 @@ limitations under the License. package storage // import "k8s.io/helm/pkg/storage" import ( - "fmt" - "reflect" - "testing" + "fmt" + "reflect" + "testing" - rspb "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/storage/driver" + rspb "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/storage/driver" ) func TestStorageCreate(t *testing.T) { - // initialize storage - storage := Init(driver.NewMemory()) + // initialize storage + storage := Init(driver.NewMemory()) - // create fake release - rls := ReleaseTestData{ - Name: "angry-beaver", - Version: 1, - }.ToRelease() + // create fake release + rls := ReleaseTestData{ + Name: "angry-beaver", + Version: 1, + }.ToRelease() - assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") + assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") - // fetch the release - res, err := storage.Get(rls.Name, rls.Version) - assertErrNil(t.Fatal, err, "QueryRelease") + // fetch the release + res, err := storage.Get(rls.Name, rls.Version) + assertErrNil(t.Fatal, err, "QueryRelease") - // verify the fetched and created release are the same - if !reflect.DeepEqual(rls, res) { - t.Fatalf("Expected %q, got %q", rls, res) - } + // verify the fetched and created release are the same + if !reflect.DeepEqual(rls, res) { + t.Fatalf("Expected %q, got %q", rls, res) + } } func TestStorageUpdate(t *testing.T) { - // initialize storage - storage := Init(driver.NewMemory()) - - // create fake release - rls := ReleaseTestData{ - Name: "angry-beaver", - Version: 1, - Status: rspb.Status_DEPLOYED, - }.ToRelease() - - assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") - - // modify the release - rls.Info.Status.Code = rspb.Status_DELETED - assertErrNil(t.Fatal, storage.Update(rls), "UpdateRelease") - - // retrieve the updated release - res, err := storage.Get(rls.Name, rls.Version) - assertErrNil(t.Fatal, err, "QueryRelease") - - // verify updated and fetched releases are the same. - if !reflect.DeepEqual(rls, res) { - t.Fatalf("Expected %q, got %q", rls, res) - } + // initialize storage + storage := Init(driver.NewMemory()) + + // create fake release + rls := ReleaseTestData{ + Name: "angry-beaver", + Version: 1, + Status: rspb.Status_DEPLOYED, + }.ToRelease() + + assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") + + // modify the release + rls.Info.Status.Code = rspb.Status_DELETED + assertErrNil(t.Fatal, storage.Update(rls), "UpdateRelease") + + // retrieve the updated release + res, err := storage.Get(rls.Name, rls.Version) + assertErrNil(t.Fatal, err, "QueryRelease") + + // verify updated and fetched releases are the same. + if !reflect.DeepEqual(rls, res) { + t.Fatalf("Expected %q, got %q", rls, res) + } } func TestStorageDelete(t *testing.T) { - // initialize storage - storage := Init(driver.NewMemory()) + // initialize storage + storage := Init(driver.NewMemory()) - // create fake release - rls := ReleaseTestData{ - Name: "angry-beaver", - Version: 1, - }.ToRelease() + // create fake release + rls := ReleaseTestData{ + Name: "angry-beaver", + Version: 1, + }.ToRelease() - assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") + assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") - // delete the release - res, err := storage.Delete(rls.Name, rls.Version) - assertErrNil(t.Fatal, err, "DeleteRelease") + // delete the release + res, err := storage.Delete(rls.Name, rls.Version) + assertErrNil(t.Fatal, err, "DeleteRelease") - // verify updated and fetched releases are the same. - if !reflect.DeepEqual(rls, res) { - t.Fatalf("Expected %q, got %q", rls, res) - } + // verify updated and fetched releases are the same. + if !reflect.DeepEqual(rls, res) { + t.Fatalf("Expected %q, got %q", rls, res) + } } func TestStorageList(t *testing.T) { - // initialize storage - storage := Init(driver.NewMemory()) - - // setup storage with test releases - setup := func() { - // release records - rls0 := ReleaseTestData{Name: "happy-catdog", Status: rspb.Status_SUPERSEDED}.ToRelease() - rls1 := ReleaseTestData{Name: "livid-human", Status: rspb.Status_SUPERSEDED}.ToRelease() - rls2 := ReleaseTestData{Name: "relaxed-cat", Status: rspb.Status_SUPERSEDED}.ToRelease() - rls3 := ReleaseTestData{Name: "hungry-hippo", Status: rspb.Status_DEPLOYED}.ToRelease() - rls4 := ReleaseTestData{Name: "angry-beaver", Status: rspb.Status_DEPLOYED}.ToRelease() - rls5 := ReleaseTestData{Name: "opulent-frog", Status: rspb.Status_DELETED}.ToRelease() - rls6 := ReleaseTestData{Name: "happy-liger", Status: rspb.Status_DELETED}.ToRelease() - - // create the release records in the storage - assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'rls0'") - assertErrNil(t.Fatal, storage.Create(rls1), "Storing release 'rls1'") - assertErrNil(t.Fatal, storage.Create(rls2), "Storing release 'rls2'") - assertErrNil(t.Fatal, storage.Create(rls3), "Storing release 'rls3'") - assertErrNil(t.Fatal, storage.Create(rls4), "Storing release 'rls4'") - assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'rls5'") - assertErrNil(t.Fatal, storage.Create(rls6), "Storing release 'rls6'") - } - - var listTests = []struct { - Description string - NumExpected int - ListFunc func() ([]*rspb.Release, error) - }{ - {"ListDeleted", 2, storage.ListDeleted}, - {"ListDeployed", 2, storage.ListDeployed}, - {"ListReleases", 7, storage.ListReleases}, - } - - setup() - - for _, tt := range listTests { - list, err := tt.ListFunc() - assertErrNil(t.Fatal, err, tt.Description) - // verify the count of releases returned - if len(list) != tt.NumExpected { - t.Errorf("ListReleases(%s): expected %d, actual %d", - tt.Description, - tt.NumExpected, - len(list)) - } - } + // initialize storage + storage := Init(driver.NewMemory()) + + // setup storage with test releases + setup := func() { + // release records + rls0 := ReleaseTestData{Name: "happy-catdog", Status: rspb.Status_SUPERSEDED}.ToRelease() + rls1 := ReleaseTestData{Name: "livid-human", Status: rspb.Status_SUPERSEDED}.ToRelease() + rls2 := ReleaseTestData{Name: "relaxed-cat", Status: rspb.Status_SUPERSEDED}.ToRelease() + rls3 := ReleaseTestData{Name: "hungry-hippo", Status: rspb.Status_DEPLOYED}.ToRelease() + rls4 := ReleaseTestData{Name: "angry-beaver", Status: rspb.Status_DEPLOYED}.ToRelease() + rls5 := ReleaseTestData{Name: "opulent-frog", Status: rspb.Status_DELETED}.ToRelease() + rls6 := ReleaseTestData{Name: "happy-liger", Status: rspb.Status_DELETED}.ToRelease() + + // create the release records in the storage + assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'rls0'") + assertErrNil(t.Fatal, storage.Create(rls1), "Storing release 'rls1'") + assertErrNil(t.Fatal, storage.Create(rls2), "Storing release 'rls2'") + assertErrNil(t.Fatal, storage.Create(rls3), "Storing release 'rls3'") + assertErrNil(t.Fatal, storage.Create(rls4), "Storing release 'rls4'") + assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'rls5'") + assertErrNil(t.Fatal, storage.Create(rls6), "Storing release 'rls6'") + } + + var listTests = []struct { + Description string + NumExpected int + ListFunc func() ([]*rspb.Release, error) + }{ + {"ListDeleted", 2, storage.ListDeleted}, + {"ListDeployed", 2, storage.ListDeployed}, + {"ListReleases", 7, storage.ListReleases}, + } + + setup() + + for _, tt := range listTests { + list, err := tt.ListFunc() + assertErrNil(t.Fatal, err, tt.Description) + // verify the count of releases returned + if len(list) != tt.NumExpected { + t.Errorf("ListReleases(%s): expected %d, actual %d", + tt.Description, + tt.NumExpected, + len(list)) + } + } } func TestStorageDeployed(t *testing.T) { - storage := Init(driver.NewMemory()) - - const name = "angry-bird" - const vers = int32(4) - - // setup storage with test releases - setup := func() { - // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.Status_DEPLOYED}.ToRelease() - - // create the release records in the storage - assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") - assertErrNil(t.Fatal, storage.Create(rls1), "Storing release 'angry-bird' (v2)") - assertErrNil(t.Fatal, storage.Create(rls2), "Storing release 'angry-bird' (v3)") - assertErrNil(t.Fatal, storage.Create(rls3), "Storing release 'angry-bird' (v4)") - } - - setup() - - rls, err := storage.Deployed(name) - if err != nil { - t.Fatalf("Failed to query for deployed release: %s\n", err) - } - - switch { - case rls == nil: - t.Fatalf("Release is nil") - case rls.Name != name: - t.Fatalf("Expected release name %q, actual %q\n", name, rls.Name) - case rls.Version != vers: - t.Fatalf("Expected release version %d, actual %d\n", vers, rls.Version) - case rls.Info.Status.Code != rspb.Status_DEPLOYED: - t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rls.Info.Status.Code) - } + storage := Init(driver.NewMemory()) + + const name = "angry-bird" + const vers = int32(4) + + // setup storage with test releases + setup := func() { + // release records + rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.Status_SUPERSEDED}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.Status_SUPERSEDED}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.Status_SUPERSEDED}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.Status_DEPLOYED}.ToRelease() + + // create the release records in the storage + assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") + assertErrNil(t.Fatal, storage.Create(rls1), "Storing release 'angry-bird' (v2)") + assertErrNil(t.Fatal, storage.Create(rls2), "Storing release 'angry-bird' (v3)") + assertErrNil(t.Fatal, storage.Create(rls3), "Storing release 'angry-bird' (v4)") + } + + setup() + + rls, err := storage.Deployed(name) + if err != nil { + t.Fatalf("Failed to query for deployed release: %s\n", err) + } + + switch { + case rls == nil: + t.Fatalf("Release is nil") + case rls.Name != name: + t.Fatalf("Expected release name %q, actual %q\n", name, rls.Name) + case rls.Version != vers: + t.Fatalf("Expected release version %d, actual %d\n", vers, rls.Version) + case rls.Info.Status.Code != rspb.Status_DEPLOYED: + t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rls.Info.Status.Code) + } } type ReleaseTestData struct { - Name string - Version int32 - Manifest string - Namespace string - Status rspb.Status_Code + Name string + Version int32 + Manifest string + Namespace string + Status rspb.Status_Code } func (test ReleaseTestData) ToRelease() *rspb.Release { - return &rspb.Release{ - Name: test.Name, - Version: test.Version, - Manifest: test.Manifest, - Namespace: test.Namespace, - Info: &rspb.Info{Status: &rspb.Status{Code: test.Status}}, - } + return &rspb.Release{ + Name: test.Name, + Version: test.Version, + Manifest: test.Manifest, + Namespace: test.Namespace, + Info: &rspb.Info{Status: &rspb.Status{Code: test.Status}}, + } } func assertErrNil(eh func(args ...interface{}), err error, message string) { - if err != nil { - eh(fmt.Sprintf("%s: %q", message, err)) - } + if err != nil { + eh(fmt.Sprintf("%s: %q", message, err)) + } } From b47379ccdc6e2b6e6faf7bd8db3a7387b48c87d2 Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Thu, 8 Sep 2016 08:59:18 -0600 Subject: [PATCH 067/183] feat(rollback-storage): golint & removed early return from release server --- cmd/tiller/release_server.go | 2 -- pkg/storage/driver/testing.go | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index ad3758d83..bd45091e5 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -675,12 +675,10 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR if !req.Purge { if err := s.env.Releases.Update(rel); err != nil { log.Printf("uninstall: Failed to store updated release: %s", err) - return nil, err } } else { if _, err := s.env.Releases.Delete(rel.Name, rel.Version); err != nil { log.Printf("uninstall: Failed to purge the release: %s", err) - return nil, err } } diff --git a/pkg/storage/driver/testing.go b/pkg/storage/driver/testing.go index 31fd3fd6b..d21cd0f2f 100644 --- a/pkg/storage/driver/testing.go +++ b/pkg/storage/driver/testing.go @@ -78,6 +78,7 @@ type MockConfigMapsInterface struct { objects map[string]*api.ConfigMap } +// Init initializes the MockConfigMapsInterface with the set of releases. func (mock *MockConfigMapsInterface) Init(t *testing.T, releases ...*rspb.Release) { mock.objects = map[string]*api.ConfigMap{} @@ -92,6 +93,7 @@ func (mock *MockConfigMapsInterface) Init(t *testing.T, releases ...*rspb.Releas } } +// Get returns the ConfigMap by name. func (mock *MockConfigMapsInterface) Get(name string) (*api.ConfigMap, error) { object, ok := mock.objects[name] if !ok { @@ -100,6 +102,7 @@ func (mock *MockConfigMapsInterface) Get(name string) (*api.ConfigMap, error) { return object, nil } +// List returns the a of ConfigMaps. func (mock *MockConfigMapsInterface) List(opts api.ListOptions) (*api.ConfigMapList, error) { var list api.ConfigMapList for _, cfgmap := range mock.objects { @@ -108,6 +111,7 @@ func (mock *MockConfigMapsInterface) List(opts api.ListOptions) (*api.ConfigMapL return &list, nil } +// Create creates a new ConfigMap. func (mock *MockConfigMapsInterface) Create(cfgmap *api.ConfigMap) (*api.ConfigMap, error) { name := cfgmap.ObjectMeta.Name if object, ok := mock.objects[name]; ok { @@ -117,6 +121,7 @@ func (mock *MockConfigMapsInterface) Create(cfgmap *api.ConfigMap) (*api.ConfigM return cfgmap, nil } +// Update updates a ConfigMap. func (mock *MockConfigMapsInterface) Update(cfgmap *api.ConfigMap) (*api.ConfigMap, error) { name := cfgmap.ObjectMeta.Name if _, ok := mock.objects[name]; !ok { @@ -126,6 +131,7 @@ func (mock *MockConfigMapsInterface) Update(cfgmap *api.ConfigMap) (*api.ConfigM return cfgmap, nil } +// Delete deletes a ConfigMap by name. func (mock *MockConfigMapsInterface) Delete(name string) error { if _, ok := mock.objects[name]; !ok { return kberrs.NewNotFound(api.Resource("tests"), name) From e42aa6c09c58bf5e6113a5beb7c0834c1e383fa9 Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Thu, 1 Sep 2016 13:56:07 -0600 Subject: [PATCH 068/183] feat(helm): add optional version flag to helm{get,status} --- cmd/helm/get.go | 6 +++++- cmd/helm/status.go | 6 +++++- pkg/helm/option.go | 43 ++++++++++++++++++++++++++++++++++++------- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/cmd/helm/get.go b/cmd/helm/get.go index f1ad5334c..3a2fddb8d 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -48,6 +48,7 @@ type getCmd struct { release string out io.Writer client helm.Interface + version int32 } func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { @@ -71,6 +72,9 @@ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { return get.run() }, } + + cmd.PersistentFlags().Int32Var(&get.version, "version", 0, "version of release") + cmd.AddCommand(newGetValuesCmd(nil, out)) cmd.AddCommand(newGetManifestCmd(nil, out)) cmd.AddCommand(newGetHooksCmd(nil, out)) @@ -96,7 +100,7 @@ MANIFEST: // getCmd is the command that implements 'helm get' func (g *getCmd) run() error { - res, err := g.client.ReleaseContent(g.release) + res, err := g.client.ReleaseContent(g.release, helm.ContentReleaseVersion(g.version)) if err != nil { return prettyError(err) } diff --git a/cmd/helm/status.go b/cmd/helm/status.go index a3403a824..2f9cfc032 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -35,6 +35,7 @@ type statusCmd struct { release string out io.Writer client helm.Interface + version int32 } func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command { @@ -58,11 +59,14 @@ func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command { return status.run() }, } + + cmd.PersistentFlags().Int32Var(&status.version, "version", 0, "version of release") + return cmd } func (s *statusCmd) run() error { - res, err := s.client.ReleaseStatus(s.release) + res, err := s.client.ReleaseStatus(s.release, helm.StatusReleaseVersion(s.version)) if err != nil { return prettyError(err) } diff --git a/pkg/helm/option.go b/pkg/helm/option.go index dff6acbd3..5d72f1f58 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -50,6 +50,10 @@ type options struct { updateReq rls.UpdateReleaseRequest // release uninstall options are applied directly to the uninstall release request uninstallReq rls.UninstallReleaseRequest + // release get status options are applied directly to the get release status request + statusReq rls.GetReleaseStatusRequest + // release get content options are applied directly to the get release content request + contentReq rls.GetReleaseContentRequest } // Home specifies the location of helm home, (default = "$HOME/.helm"). @@ -198,13 +202,32 @@ func InstallReuseName(reuse bool) InstallOption { } } -// ContentOption -- TODO +// ContentOption allows setting optional attributes when +// performing a GetReleaseContent tiller rpc. type ContentOption func(*options) -// StatusOption -- TODO +// ContentReleaseVersion will instruct Tiller to retrieve the content +// of a paritcular version of a release. +func ContentReleaseVersion(version int32) ContentOption { + return func(opts *options) { + opts.contentReq.Version = version + } +} + +// StatusOption allows setting optional attributes when +// performing a GetReleaseStatus tiller rpc. type StatusOption func(*options) -// DeleteOption -- TODO +// StatusReleaseVersion will instruct Tiller to retrieve the status +// of a paritcular version of a release. +func StatusReleaseVersion(version int32) StatusOption { + return func(opts *options) { + opts.statusReq.Version = version + } +} + +// DeleteOption allows setting optional attributes when +// performing a UninstallRelease tiller rpc. type DeleteOption func(*options) // UpdateOption allows specifying various settings @@ -281,12 +304,18 @@ func (o *options) rpcUpdateRelease(rlsName string, chr *cpb.Chart, rlc rls.Relea // Executes tiller.GetReleaseStatus RPC. func (o *options) rpcGetReleaseStatus(rlsName string, rlc rls.ReleaseServiceClient, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) { - req := &rls.GetReleaseStatusRequest{Name: rlsName} - return rlc.GetReleaseStatus(context.TODO(), req) + for _, opt := range opts { + opt(o) + } + o.statusReq.Name = rlsName + return rlc.GetReleaseStatus(context.TODO(), &o.statusReq) } // Executes tiller.GetReleaseContent. func (o *options) rpcGetReleaseContent(rlsName string, rlc rls.ReleaseServiceClient, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) { - req := &rls.GetReleaseContentRequest{Name: rlsName} - return rlc.GetReleaseContent(context.TODO(), req) + for _, opt := range opts { + opt(o) + } + o.contentReq.Name = rlsName + return rlc.GetReleaseContent(context.TODO(), &o.contentReq) } From 1d6c16175bde7b722772949868682a91e9045033 Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Thu, 8 Sep 2016 09:45:07 -0600 Subject: [PATCH 069/183] feat(rollback-storage): remove unreachable code from memory --- pkg/storage/driver/memory.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/storage/driver/memory.go b/pkg/storage/driver/memory.go index 9dafcf5b1..e3b5bb222 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -151,7 +151,6 @@ func (mem *Memory) Delete(key string) (*rspb.Release, error) { default: return nil, ErrInvalidKey } - return nil, ErrReleaseNotFound } func (mem *Memory) dump(w io.Writer) error { From 147df3b85a839d53b6361e3fc06c78f994621eb4 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Thu, 8 Sep 2016 12:38:55 -0700 Subject: [PATCH 070/183] fix(Makefile): cleanup _dist files --- Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index bea1bf42d..73530d660 100644 --- a/Makefile +++ b/Makefile @@ -75,8 +75,7 @@ protoc: .PHONY: clean clean: - @rm -rf $(BINDIR) - @rm -f ./rootfs/tiller + @rm -rf $(BINDIR) ./rootfs/tiller ./_dist .PHONY: coverage coverage: From d8616dd2b395ada407901f40845a757f37298b5b Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Thu, 8 Sep 2016 13:23:22 -0700 Subject: [PATCH 071/183] fix(lint): allow .txt files closes #1161 --- pkg/lint/rules/template.go | 4 ++-- pkg/lint/rules/template_test.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index 5638d30fe..10badbcfc 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -149,7 +149,7 @@ func validateQuotes(templateContent string) error { func validateAllowedExtension(fileName string) error { ext := filepath.Ext(fileName) - validExtensions := []string{".yaml", ".tpl"} + validExtensions := []string{".yaml", ".tpl", ".txt"} for _, b := range validExtensions { if b == ext { @@ -157,7 +157,7 @@ func validateAllowedExtension(fileName string) error { } } - return fmt.Errorf("file extension '%s' not valid. Valid extensions are .yaml or .tpl", ext) + return fmt.Errorf("file extension '%s' not valid. Valid extensions are .yaml, .tpl, or .txt", ext) } // validateNoMissingValues checks that all the {{}} functions returns a non empty value ( or "") diff --git a/pkg/lint/rules/template_test.go b/pkg/lint/rules/template_test.go index c60152794..fdd113023 100644 --- a/pkg/lint/rules/template_test.go +++ b/pkg/lint/rules/template_test.go @@ -31,11 +31,11 @@ func TestValidateAllowedExtension(t *testing.T) { var failTest = []string{"/foo", "/test.yml", "/test.toml", "test.yml"} for _, test := range failTest { err := validateAllowedExtension(test) - if err == nil || !strings.Contains(err.Error(), "Valid extensions are .yaml or .tpl") { - t.Errorf("validateAllowedExtension('%s') to return \"Valid extensions are .yaml or .tpl\", got no error", test) + if err == nil || !strings.Contains(err.Error(), "Valid extensions are .yaml, .tpl, or .txt") { + t.Errorf("validateAllowedExtension('%s') to return \"Valid extensions are .yaml, .tpl, or .txt\", got no error", test) } } - var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml"} + var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml", "NOTES.txt"} for _, test := range successTest { err := validateAllowedExtension(test) if err != nil { From dadb026afe0185bdb74edd907ed8b08b317f6c7c Mon Sep 17 00:00:00 2001 From: Brandon Philips Date: Thu, 8 Sep 2016 15:35:06 -0700 Subject: [PATCH 072/183] fix(helm): avoid a panic for sign Before this fix: ``` /Users/philips/src/k8s.io/helm/bin/helm package --sign mychart --key FC8A365E panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x13120a] goroutine 1 [running]: panic(0x10fed00, 0xc4200120d0) /usr/local/Cellar/go/1.7.1/libexec/src/runtime/panic.go:500 +0x1a1 k8s.io/helm/pkg/provenance.(*Signatory).ClearSign(0xc4205db100, 0xc42011ec20, 0x20, 0x8, 0xc4205db100, 0x0, 0x0) /Users/philips/src/k8s.io/helm/pkg/provenance/sign.go:156 +0x3a main.(*packageCmd).clearsign(0xc420017720, 0xc42011ec20, 0x20, 0x0, 0x0) /Users/philips/src/k8s.io/helm/cmd/helm/package.go:143 +0xa5 main.(*packageCmd).run(0xc420017720, 0xc420390240, 0xc420120d00, 0x1, 0x4, 0x13639b0, 0x104dfe0) /Users/philips/src/k8s.io/helm/cmd/helm/package.go:130 +0x31b main.newPackageCmd.func1(0xc420390240, 0xc420120d00, 0x1, 0x4, 0x0, 0x0) /Users/philips/src/k8s.io/helm/cmd/helm/package.go:77 +0xc4 k8s.io/helm/vendor/github.com/spf13/cobra.(*Command).execute(0xc420390240, 0xc420120c40, 0x4, 0x4, 0xc420390240, 0xc420120c40) /Users/philips/src/k8s.io/helm/vendor/github.com/spf13/cobra/command.go:571 +0x234 k8s.io/helm/vendor/github.com/spf13/cobra.(*Command).ExecuteC(0xc420405200, 0x0, 0x0, 0x0) /Users/philips/src/k8s.io/helm/vendor/github.com/spf13/cobra/command.go:661 +0x367 k8s.io/helm/vendor/github.com/spf13/cobra.(*Command).Execute(0xc420405200, 0x1b52720, 0xc4200001a0) /Users/philips/src/k8s.io/helm/vendor/github.com/spf13/cobra/command.go:620 +0x2b main.main() /Users/philips/src/k8s.io/helm/cmd/helm/helm.go:110 +0x2d ``` --- pkg/provenance/sign.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go index 4d379934d..ab0fa6ebe 100644 --- a/pkg/provenance/sign.go +++ b/pkg/provenance/sign.go @@ -153,7 +153,7 @@ func NewFromKeyring(keyringfile, id string) (*Signatory, error) { // The Signatory must have a valid Entity.PrivateKey for this to work. If it does // not, an error will be returned. func (s *Signatory) ClearSign(chartpath string) (string, error) { - if s.Entity.PrivateKey == nil { + if s.Entity == nil || s.Entity.PrivateKey == nil { return "", errors.New("private key not found") } From c97f4d7cd36d07ff36ffd30d0df0739627a95e3e Mon Sep 17 00:00:00 2001 From: Brandon Philips Date: Thu, 8 Sep 2016 16:13:00 -0700 Subject: [PATCH 073/183] code-of-conduct: use the upstream language Provide concrete information about who to contact about CoC issues. Having unambiguous people to contact is important to resolve issues in a timely manner. This is the primary important change, having consistent and explicit points of contact: https://github.com/kubernetes/kubernetes-template-project/commit/98749d6b8efc612fe05d1bc42b0e0bff89f30e0a --- code-of-conduct.md | 58 ++++++++++++++++++++++++++++++++++++ code_of_conduct.md | 74 ---------------------------------------------- 2 files changed, 58 insertions(+), 74 deletions(-) create mode 100644 code-of-conduct.md delete mode 100644 code_of_conduct.md diff --git a/code-of-conduct.md b/code-of-conduct.md new file mode 100644 index 000000000..6453201ca --- /dev/null +++ b/code-of-conduct.md @@ -0,0 +1,58 @@ +## Kubernetes Community Code of Conduct + +### Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of fostering +an open and welcoming community, we pledge to respect all people who contribute +through reporting issues, posting feature requests, updating documentation, +submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for +everyone, regardless of level of experience, gender, gender identity and expression, +sexual orientation, disability, personal appearance, body size, race, ethnicity, age, +religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic addresses, + without explicit permission +* Other unethical or unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are not +aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers +commit themselves to fairly and consistently applying these principles to every aspect +of managing this project. Project maintainers who do not follow or enforce the Code of +Conduct may be permanently removed from the project team. + +This code of conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a Kubernetes maintainer, Sarah Novotny , and/or Dan Kohn . + +This Code of Conduct is adapted from the Contributor Covenant +(http://contributor-covenant.org), version 1.2.0, available at +http://contributor-covenant.org/version/1/2/0/ + +### Kubernetes Events Code of Conduct + +Kubernetes events are working conferences intended for professional networking and collaboration in the +Kubernetes community. Attendees are expected to behave according to professional standards and in accordance +with their employer's policies on appropriate workplace behavior. + +While at Kubernetes events or related social networking opportunities, attendees should not engage in +discriminatory or offensive speech or actions regarding gender, sexuality, race, or religion. Speakers should +be especially aware of these concerns. + +The Kubernetes team does not condone any statements by speakers contrary to these standards. The Kubernetes +team reserves the right to deny entrance and/or eject from an event (without refund) any individual found to +be engaging in discriminatory or offensive speech or actions. + +Please bring any concerns to to the immediate attention of Kubernetes event staff + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/code-of-conduct.md?pixel)]() diff --git a/code_of_conduct.md b/code_of_conduct.md deleted file mode 100644 index a6c2dbe74..000000000 --- a/code_of_conduct.md +++ /dev/null @@ -1,74 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at helm-abuse@deis.com. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ From d1488f1d9448afc55b20653ebdf26d7b8a9e9cad Mon Sep 17 00:00:00 2001 From: Brandon Philips Date: Sat, 10 Sep 2016 14:01:26 -0700 Subject: [PATCH 074/183] docs(README): move history to docs/history.md The history is helpful for people who have never heard of helm but I don't think it is super relevant for most users. --- README.md | 30 ------------------------------ docs/history.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 30 deletions(-) create mode 100644 docs/history.md diff --git a/README.md b/README.md index 753304e08..56831ccda 100644 --- a/README.md +++ b/README.md @@ -69,33 +69,3 @@ image (`make docker-build`) and then deploy it (`helm init -i IMAGE_NAME`). The [documentation](docs) folder contains more information about the architecture and usage of Helm/Tiller. - -## The History of the Project - -Kubernetes Helm is the merged result of [Helm -Classic](https://github.com/helm/helm) and the Kubernetes port of GCS Deployment -Manager. The project was jointly started by Google and Deis, though it -is now part of the CNCF. - -Differences from Helm Classic: - -- Helm now has both a client (`helm`) and a server (`tiller`). The - server runs inside of Kubernetes, and manages your resources. -- Helm's chart format has changed for the better: - - Dependencies are immutable and stored inside of a chart's `charts/` - directory. - - Charts are strongly versioned using [SemVer 2](http://semver.org/spec/v2.0.0.html) - - Charts can be loaded from directories or from chart archive files - - Helm supports Go templates without requiring you to run `generate` - or `template` commands. - - Helm makes it easy to configure your releases -- and share the - configuration with the rest of your team. -- Helm chart repositories now use plain HTTP instead of Git/GitHub. - There is no longer any GitHub dependency. - - A chart server is a simple HTTP server - - Charts are referenced by version - - The `helm serve` command will run a local chart server, though you - can easily use object storage (S3, GCS) or a regular web server. - - And you can still load charts from a local directory. -- The Helm workspace is gone. You can now work anywhere on your - filesystem that you want to work. diff --git a/docs/history.md b/docs/history.md new file mode 100644 index 000000000..827e684fb --- /dev/null +++ b/docs/history.md @@ -0,0 +1,29 @@ +## The History of the Project + +Kubernetes Helm is the merged result of [Helm +Classic](https://github.com/helm/helm) and the Kubernetes port of GCS Deployment +Manager. The project was jointly started by Google and Deis, though it +is now part of the CNCF. + +Differences from Helm Classic: + +- Helm now has both a client (`helm`) and a server (`tiller`). The + server runs inside of Kubernetes, and manages your resources. +- Helm's chart format has changed for the better: + - Dependencies are immutable and stored inside of a chart's `charts/` + directory. + - Charts are strongly versioned using [SemVer 2](http://semver.org/spec/v2.0.0.html) + - Charts can be loaded from directories or from chart archive files + - Helm supports Go templates without requiring you to run `generate` + or `template` commands. + - Helm makes it easy to configure your releases -- and share the + configuration with the rest of your team. +- Helm chart repositories now use plain HTTP instead of Git/GitHub. + There is no longer any GitHub dependency. + - A chart server is a simple HTTP server + - Charts are referenced by version + - The `helm serve` command will run a local chart server, though you + can easily use object storage (S3, GCS) or a regular web server. + - And you can still load charts from a local directory. +- The Helm workspace is gone. You can now work anywhere on your + filesystem that you want to work. From 123fb9e2fe09762169edd2028da277aec980e9e5 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Sat, 10 Sep 2016 23:10:10 -0700 Subject: [PATCH 075/183] ref(helm): simplify tunnel tests --- cmd/helm/tunnel.go | 2 +- cmd/helm/tunnel_test.go | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/cmd/helm/tunnel.go b/cmd/helm/tunnel.go index 061eb6915..f5be0cfff 100644 --- a/cmd/helm/tunnel.go +++ b/cmd/helm/tunnel.go @@ -68,5 +68,5 @@ func getFirstRunningPod(client unversioned.PodsNamespacer, namespace string, sel return &p, nil } } - return nil, fmt.Errorf("could not find a ready pod") + return nil, fmt.Errorf("could not find a ready tiller pod") } diff --git a/cmd/helm/tunnel_test.go b/cmd/helm/tunnel_test.go index 96608ed4e..511a67a74 100644 --- a/cmd/helm/tunnel_test.go +++ b/cmd/helm/tunnel_test.go @@ -21,7 +21,6 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/client/unversioned/testclient" - "k8s.io/kubernetes/pkg/runtime" ) func mockTillerPod() api.Pod { @@ -75,11 +74,7 @@ func TestGetFirstPod(t *testing.T) { } for _, tt := range tests { - client := &testclient.Fake{} - client.PrependReactor("list", "pods", func(action testclient.Action) (handled bool, ret runtime.Object, err error) { - return true, &api.PodList{Items: tt.pods}, nil - }) - + client := testclient.NewSimpleFake(&api.PodList{Items: tt.pods}) name, err := getTillerPodName(client, api.NamespaceDefault) if (err != nil) != tt.err { t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) From 5f1bd67ed0fcef2433dde79b7283a7c52bc275cc Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Sun, 11 Sep 2016 11:51:14 -0700 Subject: [PATCH 076/183] feat(ci): build canary helm binaries using CI --- Makefile | 4 ++-- circle.yml | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 73530d660..7de4f832b 100644 --- a/Makefile +++ b/Makefile @@ -21,13 +21,13 @@ all: build build: GOBIN=$(BINDIR) $(GO) install $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/... +# usage: make build-cross dist VERSION=v2.0.0-alpha.3 .PHONY: build-cross build-cross: gox -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -os="darwin linux windows" -arch="amd64 386" $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/helm -# usage: make dist VERSION=v2.0.0-alpha.3 .PHONY: dist -dist: build-cross +dist: ( \ cd _dist && \ $(DIST_DIRS) cp ../LICENSE {} \; && \ diff --git a/circle.yml b/circle.yml index 74fab120f..182e2f238 100644 --- a/circle.yml +++ b/circle.yml @@ -6,12 +6,14 @@ machine: GOVERSION: "1.7" GOPATH: "${HOME}/.go_workspace" WORKDIR: "${GOPATH}/src/k8s.io/helm" + PROJECT_NAME: "kubernetes-helm" services: - docker dependencies: pre: + # remove old go files - sudo rm -rf /usr/local/go - rm -rf "$GOPATH" @@ -39,7 +41,18 @@ deployment: gcr: branch: master commands: - - echo $GCLOUD_SERVICE_KEY | base64 --decode > ${HOME}/gcloud-service-key.json + # setup gcloud tools + - sudo /opt/google-cloud-sdk/bin/gcloud --quiet components update + - echo "${GCLOUD_SERVICE_KEY}" | base64 --decode > "${HOME}/gcloud-service-key.json" + - sudo /opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file "${HOME}/glcoud-service-key.json" + - sudo /opt/google-cloud-sdk/bin/gcloud config set project "${PROJECT_NAME}" - docker login -e 1234@5678.com -u _json_key -p "$(cat ${HOME}/gcloud-service-key.json)" https://gcr.io + + # build canary tiller image and push - make docker-build - docker push gcr.io/kubernetes-helm/tiller:canary + + # build canary helm binaries and push + - make build-cross + - make dist VERSION=canary + - sudo /opt/google-cloud-sdk/bin/gsutil cp ./_dist/* "gs://${PROJECT_NAME}" From e50f9e6b2425c571faa1ef6e0888ddbffb6b369e Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Mon, 12 Sep 2016 08:45:43 -0600 Subject: [PATCH 077/183] ref(helm): refactor checkArgsLength method --- cmd/helm/helm.go | 5 +++-- cmd/helm/inspect.go | 2 +- cmd/helm/install.go | 2 +- cmd/helm/repo.go | 4 ++-- cmd/helm/rollback.go | 2 +- cmd/helm/upgrade.go | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index d3c756b3b..7205a20f2 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -138,8 +138,9 @@ func teardown() { } } -func checkArgsLength(expectedNum, actualNum int, requiredArgs ...string) error { - if actualNum != expectedNum { +func checkArgsLength(argsReceived int, requiredArgs ...string) error { + expectedNum := len(requiredArgs) + if argsReceived != expectedNum { arg := "arguments" if expectedNum == 1 { arg = "argument" diff --git a/cmd/helm/inspect.go b/cmd/helm/inspect.go index cc02f80b1..8386fd1d0 100644 --- a/cmd/helm/inspect.go +++ b/cmd/helm/inspect.go @@ -70,7 +70,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command { Short: "inspect a chart", Long: inspectDesc, RunE: func(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(1, len(args), "chart name"); err != nil { + if err := checkArgsLength(len(args), "chart name"); err != nil { return err } cp, err := locateChartPath(args[0], insp.verify, insp.keyring) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index a926b3c11..ced11eb9c 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -89,7 +89,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { Long: installDesc, PersistentPreRunE: setupConnection, RunE: func(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(1, len(args), "chart name"); err != nil { + if err := checkArgsLength(len(args), "chart name"); err != nil { return err } cp, err := locateChartPath(args[0], inst.verify, inst.keyring) diff --git a/cmd/helm/repo.go b/cmd/helm/repo.go index df3205120..65f273db3 100644 --- a/cmd/helm/repo.go +++ b/cmd/helm/repo.go @@ -69,7 +69,7 @@ var repoIndexCmd = &cobra.Command{ } func runRepoAdd(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(2, len(args), "name for the chart repository", "the url of the chart repository"); err != nil { + if err := checkArgsLength(len(args), "name for the chart repository", "the url of the chart repository"); err != nil { return err } name, url := args[0], args[1] @@ -101,7 +101,7 @@ func runRepoList(cmd *cobra.Command, args []string) error { } func runRepoRemove(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(1, len(args), "name of chart repository"); err != nil { + if err := checkArgsLength(len(args), "name of chart repository"); err != nil { return err } return removeRepoLine(args[0]) diff --git a/cmd/helm/rollback.go b/cmd/helm/rollback.go index bb6326f7a..f711dacf4 100644 --- a/cmd/helm/rollback.go +++ b/cmd/helm/rollback.go @@ -52,7 +52,7 @@ func newRollbackCmd(c helm.Interface, out io.Writer) *cobra.Command { Long: rollbackDesc, PersistentPreRunE: setupConnection, RunE: func(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(1, len(args), "release name"); err != nil { + if err := checkArgsLength(len(args), "release name"); err != nil { return err } rollback.client = ensureHelmClient(rollback.client) diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 95b5e7901..ca60c9985 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -64,7 +64,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { Long: upgradeDesc, PersistentPreRunE: setupConnection, RunE: func(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(2, len(args), "release name, chart path"); err != nil { + if err := checkArgsLength(len(args), "release name", "chart path"); err != nil { return err } From 83df6ebc4d884ed0c389b75eb8778676cff8ce8e Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Mon, 12 Sep 2016 09:16:03 -0600 Subject: [PATCH 078/183] feat(rollback-storage): remove extraneous else block and fix type in records_test --- cmd/tiller/release_server.go | 12 ++++++------ pkg/storage/driver/records_test.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index bd45091e5..c6263c289 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -181,16 +181,16 @@ func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetRelease } var rel *release.Release + var err error + if req.Version <= 0 { - var err error if rel, err = s.env.Releases.Deployed(req.Name); err != nil { return nil, err } - } else { - var err error - if rel, err = s.env.Releases.Get(req.Name, req.Version); err != nil { - return nil, err - } + } + + if rel, err = s.env.Releases.Get(req.Name, req.Version); err != nil { + return nil, err } if rel.Info == nil { diff --git a/pkg/storage/driver/records_test.go b/pkg/storage/driver/records_test.go index 3597072f8..99f8b2d0d 100644 --- a/pkg/storage/driver/records_test.go +++ b/pkg/storage/driver/records_test.go @@ -65,7 +65,7 @@ func TestRecordsRemove(t *testing.T) { }{ {"remove valid key", "rls-a.v1", false}, {"remove invalid key", "rls-a.v", true}, - {"remove non-existant key", "rls-z.v1", true}, + {"remove non-existent key", "rls-z.v1", true}, } rs := records([]*record{ From 825d2abd8c025c2dec4b6d932b5db7e72f3d944c Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Mon, 12 Sep 2016 09:25:42 -0600 Subject: [PATCH 079/183] feat(helm): better description for optional version flag --- cmd/helm/get.go | 2 +- cmd/helm/status.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/helm/get.go b/cmd/helm/get.go index 3a2fddb8d..3c8beb26f 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -73,7 +73,7 @@ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { }, } - cmd.PersistentFlags().Int32Var(&get.version, "version", 0, "version of release") + cmd.PersistentFlags().Int32Var(&get.version, "version", 0, "get the named release with version") cmd.AddCommand(newGetValuesCmd(nil, out)) cmd.AddCommand(newGetManifestCmd(nil, out)) diff --git a/cmd/helm/status.go b/cmd/helm/status.go index 2f9cfc032..356fdcb7d 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -60,7 +60,7 @@ func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command { }, } - cmd.PersistentFlags().Int32Var(&status.version, "version", 0, "version of release") + cmd.PersistentFlags().Int32Var(&status.version, "version", 0, "If set, display the status of the named release with version") return cmd } From 84f982e8f157c7f98d4abd479792861279a4ed6d Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Mon, 12 Sep 2016 09:43:44 -0600 Subject: [PATCH 080/183] feat(rollback-storage): use version in release_server test queries --- cmd/tiller/release_server.go | 4 ++-- cmd/tiller/release_server_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index c6263c289..de7ceaaf6 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -185,12 +185,12 @@ func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetRelease if req.Version <= 0 { if rel, err = s.env.Releases.Deployed(req.Name); err != nil { - return nil, err + return nil, fmt.Errorf("getting deployed release '%s': %s", req.Name, err) } } if rel, err = s.env.Releases.Get(req.Name, req.Version); err != nil { - return nil, err + return nil, fmt.Errorf("getting release '%s' (v%d): %s", req.Name, req.Version, err) } if rel.Info == nil { diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 9eb479db1..b819f7dcc 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -466,14 +466,14 @@ func TestInstallReleaseReuseName(t *testing.T) { } res, err := rs.InstallRelease(c, req) if err != nil { - t.Errorf("Failed install: %s", err) + t.Fatalf("Failed install: %s", err) } if res.Release.Name != rel.Name { t.Errorf("expected %q, got %q", rel.Name, res.Release.Name) } - getreq := &services.GetReleaseStatusRequest{Name: rel.Name} + getreq := &services.GetReleaseStatusRequest{Name: rel.Name, Version: 1} getres, err := rs.GetReleaseStatus(c, getreq) if err != nil { t.Errorf("Failed to retrieve release: %s", err) @@ -701,7 +701,7 @@ func TestGetReleaseContent(t *testing.T) { t.Fatalf("Could not store mock release: %s", err) } - res, err := rs.GetReleaseContent(c, &services.GetReleaseContentRequest{Name: rel.Name}) + res, err := rs.GetReleaseContent(c, &services.GetReleaseContentRequest{Name: rel.Name, Version: 1}) if err != nil { t.Errorf("Error getting release content: %s", err) } @@ -719,7 +719,7 @@ func TestGetReleaseStatus(t *testing.T) { t.Fatalf("Could not store mock release: %s", err) } - res, err := rs.GetReleaseStatus(c, &services.GetReleaseStatusRequest{Name: rel.Name}) + res, err := rs.GetReleaseStatus(c, &services.GetReleaseStatusRequest{Name: rel.Name, Version: 1}) if err != nil { t.Errorf("Error getting release content: %s", err) } From e28e2419f2bebf10e5349efa156c7097e8b13dda Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Mon, 12 Sep 2016 10:09:17 -0600 Subject: [PATCH 081/183] fix(tiller): re-enable skipped test, golint, & gofmt all the things --- cmd/tiller/release_server.go | 6 +++--- cmd/tiller/release_server_test.go | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index de7ceaaf6..3385a1330 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -225,10 +225,10 @@ func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleas if req.Version <= 0 { rel, err := s.env.Releases.Deployed(req.Name) return &services.GetReleaseContentResponse{Release: rel}, err - } else { - rel, err := s.env.Releases.Get(req.Name, req.Version) - return &services.GetReleaseContentResponse{Release: rel}, err } + + rel, err := s.env.Releases.Get(req.Name, req.Version) + return &services.GetReleaseContentResponse{Release: rel}, err } func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) { diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index b819f7dcc..cfeb5d9eb 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -646,8 +646,6 @@ func TestUninstallPurgeRelease(t *testing.T) { } func TestUninstallPurgeDeleteRelease(t *testing.T) { - t.Skip("TestUninstallPurgeDeleteRelease") - c := context.Background() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -667,7 +665,7 @@ func TestUninstallPurgeDeleteRelease(t *testing.T) { } _, err2 := rs.UninstallRelease(c, req2) - if err2 != nil { + if err2 != nil && err2.Error() != "'angry-panda' has no deployed releases" { t.Errorf("Failed uninstall: %s", err2) } } From 3f35d49a00ab262ded7d20d6bdce1047d8075e45 Mon Sep 17 00:00:00 2001 From: Brandon Philips Date: Sat, 10 Sep 2016 14:04:13 -0700 Subject: [PATCH 082/183] docs(README): remove the build from source instructions docs/developers.md already has these instructions. And 80% of the time someone looking at this project is interested in getting started not hacking directly. --- README.md | 37 ++++--------------------------------- 1 file changed, 4 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 56831ccda..2d161d213 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ Think of it like apt/yum/homebrew for Kubernetes. - Charts can be stored on disk, or fetched from remote chart repositories (like Debian or RedHat packages) +## Install + +Download a [release tarball of helm for your platform](https://github.com/kubernetes/helm/releases). Unpack the `helm` binary and add it to your PATH and you are good to go! OS X/[Cask](https://caskroom.github.io/) users can `brew cask install helm`. + ## Docs - [Quick Start](docs/quickstart.md) @@ -36,36 +40,3 @@ Think of it like apt/yum/homebrew for Kubernetes. - [Chart Repository Guide](docs/chart_repository.md) - [Syncing your Chart Repository](docs/chart_repository_sync_example.md) - [Developers](docs/developers.md) - - -## Install - -Download a [release tarball of helm and tiller for your platform](https://github.com/kubernetes/helm/releases). Unpack the `helm` and `tiller` binaries and add them to your PATH and you are good to go! OS X/[Cask](https://caskroom.github.io/) users can `brew cask install helm`. - -### Install from source - -To install Helm from source, follow this process: - -Make sure you have the prerequisites: -- Go 1.6 -- A running Kubernetes cluster -- `kubectl` properly configured to talk to your cluster -- [Glide](https://glide.sh/) 0.10 or greater with both git and mercurial installed. - -1. [Properly set your $GOPATH](https://golang.org/doc/code.html) -2. Clone (or otherwise download) this repository into $GOPATH/src/k8s.io/helm -3. Run `make bootstrap build` - -You will now have two binaries built: - -- `bin/helm` is the client -- `bin/tiller` is the server - -From here, you can run `bin/helm` and use it to install a recent snapshot of -Tiller. Helm will use your `kubectl` config to learn about your cluster. - -For development on Tiller, you can locally run Tiller, or you build a Docker -image (`make docker-build`) and then deploy it (`helm init -i IMAGE_NAME`). - -The [documentation](docs) folder contains more information about the -architecture and usage of Helm/Tiller. From 9815c6b5c83e8a0a7a29fc76cbeea949d2da1365 Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Mon, 12 Sep 2016 16:45:07 -0600 Subject: [PATCH 083/183] fix(1185): get release status by version iff version > 0 --- cmd/tiller/release_server.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 3385a1330..414bf402b 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -187,10 +187,10 @@ func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetRelease if rel, err = s.env.Releases.Deployed(req.Name); err != nil { return nil, fmt.Errorf("getting deployed release '%s': %s", req.Name, err) } - } - - if rel, err = s.env.Releases.Get(req.Name, req.Version); err != nil { - return nil, fmt.Errorf("getting release '%s' (v%d): %s", req.Name, req.Version, err) + } else { + if rel, err = s.env.Releases.Get(req.Name, req.Version); err != nil { + return nil, fmt.Errorf("getting release '%s' (v%d): %s", req.Name, req.Version, err) + } } if rel.Info == nil { From 4d669eec65207d01788c5e8cde49127f1bd2f2d2 Mon Sep 17 00:00:00 2001 From: Brandon Philips Date: Mon, 12 Sep 2016 17:22:45 -0700 Subject: [PATCH 084/183] feat(README): add community contact points Add the sig-apps mailing list, developer call details, and #helm slack channel. Based on https://github.com/kubernetes/kubernetes-template-project/blob/master/README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 2d161d213..b90b2c423 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,11 @@ Download a [release tarball of helm for your platform](https://github.com/kubern - [Chart Repository Guide](docs/chart_repository.md) - [Syncing your Chart Repository](docs/chart_repository_sync_example.md) - [Developers](docs/developers.md) + +## Community, discussion, contribution, and support + +You can reach the Helm community and developers via the following channels: + +- [Kubernetes Slack](https://slack.k8s.io): #helm +- Mailing List: https://groups.google.com/forum/#!forum/kubernetes-sig-apps +- Developer Call: Thursdays at 9:30-10:00 Pacific. https://engineyard.zoom.us/j/366425549 From 6d26024a2792925d16a33fd8c09f107d7166e834 Mon Sep 17 00:00:00 2001 From: vaikas-google Date: Wed, 31 Aug 2016 11:57:13 -0700 Subject: [PATCH 085/183] first cut of version on client and server --- _proto/Makefile | 13 +- _proto/hapi/services/tiller.proto | 13 ++ _proto/hapi/version/version.proto | 30 +++++ cmd/helm/helm.go | 1 + cmd/helm/helm_test.go | 13 ++ cmd/helm/installer/install.go | 2 +- cmd/helm/version.go | 41 +++++-- cmd/tiller/release_server.go | 6 + pkg/chartutil/values_test.go | 2 +- pkg/helm/client.go | 15 +++ pkg/helm/interface.go | 1 + pkg/helm/option.go | 11 +- pkg/proto/hapi/services/tiller.pb.go | 170 ++++++++++++++++++--------- pkg/proto/hapi/version/version.pb.go | 66 +++++++++++ pkg/version/version.go | 45 ++++++- versioning.mk | 6 +- 16 files changed, 363 insertions(+), 72 deletions(-) create mode 100644 _proto/hapi/version/version.proto create mode 100644 pkg/proto/hapi/version/version.pb.go diff --git a/_proto/Makefile b/_proto/Makefile index 1f188adfc..38f4ba6c2 100644 --- a/_proto/Makefile +++ b/_proto/Makefile @@ -20,19 +20,26 @@ services_ias = $(subst $(space),$(comma),$(addsuffix =$(import_path)/$(services_ services_pbs = $(sort $(wildcard hapi/services/*.proto)) services_pkg = services +version_ias = $(subst $(space),$(comma),$(addsuffix =$(import_path)/$(version_pkg),$(addprefix M,$(version_pbs)))) +version_pbs = $(sort $(wildcard hapi/version/*.proto)) +version_pkg = version + google_deps = Mgoogle/protobuf/timestamp.proto=github.com/golang/protobuf/ptypes/timestamp,Mgoogle/protobuf/any.proto=github.com/golang/protobuf/ptypes/any .PHONY: all -all: chart release services +all: chart release services version chart: PATH=../bin:$(PATH) protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias):$(dst) $(chart_pbs) release: - PATH=../bin:$(PATH) protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias):$(dst) $(release_pbs) + PATH=../bin:$(PATH) protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias),$(version_ias):$(dst) $(release_pbs) services: - PATH=../bin:$(PATH) protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias),$(release_ias):$(dst) $(services_pbs) + PATH=../bin:$(PATH) protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias),$(version_ias),$(release_ias):$(dst) $(services_pbs) + +version: + PATH=../bin:$(PATH) protoc --$(target)_out=plugins=$(plugins),$(google_deps):$(dst) $(version_pbs) .PHONY: clean clean: diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto index 4d5da0085..46f999a07 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -21,6 +21,7 @@ import "hapi/chart/config.proto"; import "hapi/release/release.proto"; import "hapi/release/info.proto"; import "hapi/release/status.proto"; +import "hapi/version/version.proto"; option go_package = "services"; @@ -65,6 +66,10 @@ service ReleaseService { // UninstallRelease requests deletion of a named release. rpc UninstallRelease(UninstallReleaseRequest) returns (UninstallReleaseResponse) { } + + // GetVersion returns the current version of the server. + rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) { +} } // ListReleasesRequest requests a list of releases. @@ -229,3 +234,11 @@ message UninstallReleaseResponse { // Release is the release that was marked deleted. hapi.release.Release release = 1; } + +// GetVersionRequest requests for version information. +message GetVersionRequest { +} + +message GetVersionResponse { + hapi.version.Version Version = 1; +} diff --git a/_proto/hapi/version/version.proto b/_proto/hapi/version/version.proto new file mode 100644 index 000000000..1f68253ed --- /dev/null +++ b/_proto/hapi/version/version.proto @@ -0,0 +1,30 @@ +// Copyright 2016 The Kubernetes Authors All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package hapi.version; + +option go_package = "version"; + +message Version { + uint32 major = 1; + uint32 minor = 2; + uint32 patch = 3; + string pre_release = 4; + string build_metadata = 5; + string git_version = 6; + string git_commit = 7; + string git_tree_state = 8; +} diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 7205a20f2..78e058b02 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -98,6 +98,7 @@ func newRootCmd(out io.Writer) *cobra.Command { newFetchCmd(out), newVerifyCmd(out), newUpdateCmd(out), + newVersionCmd(nil, out), ) return cmd } diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 71c0aee1d..f6a26edd1 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -31,6 +31,7 @@ import ( "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/release" rls "k8s.io/helm/pkg/proto/hapi/services" + "k8s.io/helm/pkg/proto/hapi/version" ) var mockHookTemplate = `apiVersion: v1 @@ -145,6 +146,18 @@ func (c *fakeReleaseClient) ReleaseStatus(rlsName string, opts ...helm.StatusOpt return nil, fmt.Errorf("No such release: %s", rlsName) } +func (c *fakeReleaseClient) GetVersion(opts ...helm.VersionOption) (*rls.GetVersionResponse, error) { + return &rls.GetVersionResponse{ + Version: &version.Version{ + Major: 1, + Minor: 2, + Patch: 3, + PreRelease: "fakeclient", + BuildMetadata: "testonly", + }, + }, nil +} + func (c *fakeReleaseClient) UpdateRelease(rlsName string, chStr string, opts ...helm.UpdateOption) (*rls.UpdateReleaseResponse, error) { return nil, nil } diff --git a/cmd/helm/installer/install.go b/cmd/helm/installer/install.go index 0bfd197e8..84253ed18 100644 --- a/cmd/helm/installer/install.go +++ b/cmd/helm/installer/install.go @@ -62,7 +62,7 @@ func Install(namespace, image string, verbose bool) error { if image == "" { // strip git sha off version - tag := strings.Split(version.Version, "+")[0] + tag := strings.Split(version.GetVersion(), "+")[0] image = fmt.Sprintf("%s:%s", defaultImage, tag) } diff --git a/cmd/helm/version.go b/cmd/helm/version.go index d66221491..58dbcc97d 100644 --- a/cmd/helm/version.go +++ b/cmd/helm/version.go @@ -18,20 +18,45 @@ package main import ( "fmt" + "io" "github.com/spf13/cobra" + "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/version" ) -func init() { - RootCommand.AddCommand(versionCmd) +type versionCmd struct { + out io.Writer + client helm.Interface } -var versionCmd = &cobra.Command{ - Use: "version", - Short: "print the client version information", - Run: func(cmd *cobra.Command, args []string) { - fmt.Println(version.Version) - }, +func newVersionCmd(c helm.Interface, out io.Writer) *cobra.Command { + version := &versionCmd{ + client: c, + out: out, + } + cmd := &cobra.Command{ + Use: "version", + Short: "print the client/server version information", + PersistentPreRunE: setupConnection, + RunE: func(cmd *cobra.Command, args []string) error { + version.client = ensureHelmClient(version.client) + return version.run() + }, + } + return cmd +} + +func (v *versionCmd) run() error { + // Regardless of whether we can talk to server or not, just print the client + // version. + fmt.Printf("%+v\n", version.GetVersionProto()) + + resp, err := v.client.GetVersion() + if err != nil { + return err + } + fmt.Printf("%+v\n", resp.Version) + return nil } diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 414bf402b..678570841 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -36,6 +36,7 @@ import ( "k8s.io/helm/pkg/proto/hapi/services" "k8s.io/helm/pkg/storage/driver" "k8s.io/helm/pkg/timeconv" + "k8s.io/helm/pkg/version" "k8s.io/kubernetes/pkg/api/unversioned" ) @@ -175,6 +176,11 @@ func filterReleases(filter string, rels []*release.Release) ([]*release.Release, return matches, nil } +func (s *releaseServer) GetVersion(c ctx.Context, req *services.GetVersionRequest) (*services.GetVersionResponse, error) { + v := version.GetVersionProto() + return &services.GetVersionResponse{Version: &v}, nil +} + func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetReleaseStatusRequest) (*services.GetReleaseStatusResponse, error) { if req.Name == "" { return nil, errMissingRelease diff --git a/pkg/chartutil/values_test.go b/pkg/chartutil/values_test.go index 33f0854a8..e5a11600f 100644 --- a/pkg/chartutil/values_test.go +++ b/pkg/chartutil/values_test.go @@ -61,7 +61,7 @@ water: for _, tt := range tests { data, err = ReadValues([]byte(tt)) if err != nil { - t.Fatalf("Error parsing bytes: %s", err) + t.Fatalf("Error parsing bytes (%s): %s", tt, err) } if data == nil { t.Errorf(`YAML string "%s" gave a nil map`, tt) diff --git a/pkg/helm/client.go b/pkg/helm/client.go index 3c9cdfe71..95ad41c5e 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -23,6 +23,7 @@ import ( "k8s.io/helm/pkg/chartutil" rls "k8s.io/helm/pkg/proto/hapi/services" + // version "k8s.io/helm/pkg/proto/hapi/version" ) const ( @@ -120,6 +121,20 @@ func (h *Client) UpdateRelease(rlsName string, chStr string, opts ...UpdateOptio return h.opts.rpcUpdateRelease(rlsName, chart, rls.NewReleaseServiceClient(c), opts...) } +// Version returns the server version +// +// Note: there aren't currently any supported StatusOptions, +// but they are kept in the API signature as a placeholder for future additions. +func (h *Client) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) { + c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) + if err != nil { + return nil, err + } + defer c.Close() + + return h.opts.rpcGetVersion(rls.NewReleaseServiceClient(c), opts...) +} + // ReleaseStatus returns the given release's status. // // Note: there aren't currently any supported StatusOptions, diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go index 528af8908..1510b4d56 100644 --- a/pkg/helm/interface.go +++ b/pkg/helm/interface.go @@ -28,4 +28,5 @@ type Interface interface { ReleaseStatus(rlsName string, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) UpdateRelease(rlsName, chStr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) + GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) } diff --git a/pkg/helm/option.go b/pkg/helm/option.go index 5d72f1f58..269001742 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -219,7 +219,7 @@ func ContentReleaseVersion(version int32) ContentOption { type StatusOption func(*options) // StatusReleaseVersion will instruct Tiller to retrieve the status -// of a paritcular version of a release. +// of a particular version of a release. func StatusReleaseVersion(version int32) StatusOption { return func(opts *options) { opts.statusReq.Version = version @@ -230,6 +230,9 @@ func StatusReleaseVersion(version int32) StatusOption { // performing a UninstallRelease tiller rpc. type DeleteOption func(*options) +// VersionOption -- TODO +type VersionOption func(*options) + // UpdateOption allows specifying various settings // configurable by the helm client user for overriding // the defaults used when running the `helm upgrade` command. @@ -319,3 +322,9 @@ func (o *options) rpcGetReleaseContent(rlsName string, rlc rls.ReleaseServiceCli o.contentReq.Name = rlsName return rlc.GetReleaseContent(context.TODO(), &o.contentReq) } + +// Executes tiller.GetVersion RPC. +func (o *options) rpcGetVersion(rlc rls.ReleaseServiceClient, opts ...VersionOption) (*rls.GetVersionResponse, error) { + req := &rls.GetVersionRequest{} + return rlc.GetVersion(context.TODO(), req) +} diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index 33ed1133d..ba78f2836 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -22,6 +22,8 @@ It has these top-level messages: InstallReleaseResponse UninstallReleaseRequest UninstallReleaseResponse + GetVersionRequest + GetVersionResponse */ package services @@ -33,6 +35,7 @@ import hapi_chart "k8s.io/helm/pkg/proto/hapi/chart" import hapi_release3 "k8s.io/helm/pkg/proto/hapi/release" import hapi_release2 "k8s.io/helm/pkg/proto/hapi/release" import hapi_release1 "k8s.io/helm/pkg/proto/hapi/release" +import hapi_version "k8s.io/helm/pkg/proto/hapi/version" import ( context "golang.org/x/net/context" @@ -366,6 +369,31 @@ func (m *UninstallReleaseResponse) GetRelease() *hapi_release3.Release { return nil } +// GetVersionRequest requests for version information. +type GetVersionRequest struct { +} + +func (m *GetVersionRequest) Reset() { *m = GetVersionRequest{} } +func (m *GetVersionRequest) String() string { return proto.CompactTextString(m) } +func (*GetVersionRequest) ProtoMessage() {} +func (*GetVersionRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{13} } + +type GetVersionResponse struct { + Version *hapi_version.Version `protobuf:"bytes,1,opt,name=Version,json=version" json:"Version,omitempty"` +} + +func (m *GetVersionResponse) Reset() { *m = GetVersionResponse{} } +func (m *GetVersionResponse) String() string { return proto.CompactTextString(m) } +func (*GetVersionResponse) ProtoMessage() {} +func (*GetVersionResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{14} } + +func (m *GetVersionResponse) GetVersion() *hapi_version.Version { + if m != nil { + return m.Version + } + return nil +} + func init() { proto.RegisterType((*ListReleasesRequest)(nil), "hapi.services.tiller.ListReleasesRequest") proto.RegisterType((*ListSort)(nil), "hapi.services.tiller.ListSort") @@ -380,6 +408,8 @@ func init() { proto.RegisterType((*InstallReleaseResponse)(nil), "hapi.services.tiller.InstallReleaseResponse") proto.RegisterType((*UninstallReleaseRequest)(nil), "hapi.services.tiller.UninstallReleaseRequest") proto.RegisterType((*UninstallReleaseResponse)(nil), "hapi.services.tiller.UninstallReleaseResponse") + proto.RegisterType((*GetVersionRequest)(nil), "hapi.services.tiller.GetVersionRequest") + proto.RegisterType((*GetVersionResponse)(nil), "hapi.services.tiller.GetVersionResponse") proto.RegisterEnum("hapi.services.tiller.ListSort_SortBy", ListSort_SortBy_name, ListSort_SortBy_value) proto.RegisterEnum("hapi.services.tiller.ListSort_SortOrder", ListSort_SortOrder_name, ListSort_SortOrder_value) } @@ -410,6 +440,8 @@ type ReleaseServiceClient interface { InstallRelease(ctx context.Context, in *InstallReleaseRequest, opts ...grpc.CallOption) (*InstallReleaseResponse, error) // UninstallRelease requests deletion of a named release. UninstallRelease(ctx context.Context, in *UninstallReleaseRequest, opts ...grpc.CallOption) (*UninstallReleaseResponse, error) + // GetVersion returns the current version of the server. + GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) } type releaseServiceClient struct { @@ -497,6 +529,15 @@ func (c *releaseServiceClient) UninstallRelease(ctx context.Context, in *Uninsta return out, nil } +func (c *releaseServiceClient) GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) { + out := new(GetVersionResponse) + err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/GetVersion", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // Server API for ReleaseService service type ReleaseServiceServer interface { @@ -515,6 +556,8 @@ type ReleaseServiceServer interface { InstallRelease(context.Context, *InstallReleaseRequest) (*InstallReleaseResponse, error) // UninstallRelease requests deletion of a named release. UninstallRelease(context.Context, *UninstallReleaseRequest) (*UninstallReleaseResponse, error) + // GetVersion returns the current version of the server. + GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) } func RegisterReleaseServiceServer(s *grpc.Server, srv ReleaseServiceServer) { @@ -602,6 +645,18 @@ func _ReleaseService_UninstallRelease_Handler(srv interface{}, ctx context.Conte return out, nil } +func _ReleaseService_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(GetVersionRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(ReleaseServiceServer).GetVersion(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + var _ReleaseService_serviceDesc = grpc.ServiceDesc{ ServiceName: "hapi.services.tiller.ReleaseService", HandlerType: (*ReleaseServiceServer)(nil), @@ -626,6 +681,10 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ MethodName: "UninstallRelease", Handler: _ReleaseService_UninstallRelease_Handler, }, + { + MethodName: "GetVersion", + Handler: _ReleaseService_GetVersion_Handler, + }, }, Streams: []grpc.StreamDesc{ { @@ -637,58 +696,61 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ } var fileDescriptor0 = []byte{ - // 841 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdd, 0x6e, 0xeb, 0x44, - 0x10, 0xae, 0xf3, 0xe3, 0x24, 0x93, 0xb6, 0x4a, 0x97, 0xb6, 0x71, 0x2d, 0x40, 0x95, 0x11, 0x50, - 0x0a, 0x24, 0x10, 0x6e, 0x11, 0x52, 0x9a, 0x46, 0x6d, 0xd5, 0x90, 0x4a, 0x1b, 0x2a, 0x24, 0x2e, - 0x88, 0xdc, 0x64, 0xd3, 0x18, 0x5c, 0x3b, 0x78, 0x37, 0x11, 0x7d, 0x04, 0x5e, 0x83, 0xa7, 0xe0, - 0x86, 0x27, 0xe3, 0x86, 0xf5, 0xae, 0xd7, 0x27, 0x4e, 0xec, 0x73, 0x7c, 0x72, 0xe3, 0xec, 0xee, - 0x7c, 0xfb, 0xcd, 0xcc, 0x37, 0xb3, 0xd3, 0x82, 0x39, 0xb7, 0x17, 0x4e, 0x9b, 0x92, 0x60, 0xe5, - 0x4c, 0x08, 0x6d, 0x33, 0xc7, 0x75, 0x49, 0xd0, 0x5a, 0x04, 0x3e, 0xf3, 0xd1, 0x71, 0x68, 0x6b, - 0x29, 0x5b, 0x4b, 0xda, 0xcc, 0x53, 0x71, 0x63, 0x32, 0xb7, 0x03, 0x26, 0xbf, 0x12, 0x6d, 0x36, - 0xd7, 0xcf, 0x7d, 0x6f, 0xe6, 0x3c, 0x47, 0x06, 0xe9, 0x22, 0x20, 0x2e, 0xb1, 0x29, 0x51, 0xbf, - 0x89, 0x4b, 0xca, 0xe6, 0x78, 0x33, 0x3f, 0x32, 0x9c, 0x25, 0x0c, 0x94, 0xd9, 0x6c, 0x49, 0xa5, - 0xc9, 0xfa, 0xbb, 0x00, 0x1f, 0x0c, 0x1c, 0xca, 0xb0, 0x34, 0x52, 0x4c, 0xfe, 0x58, 0x12, 0xca, - 0xd0, 0x31, 0x94, 0x5d, 0xe7, 0xc5, 0x61, 0x86, 0x76, 0xae, 0x5d, 0x14, 0xb1, 0xdc, 0xa0, 0x53, - 0xd0, 0xfd, 0xd9, 0x8c, 0x12, 0x66, 0x14, 0xf8, 0x71, 0x0d, 0x47, 0x3b, 0xf4, 0x03, 0x54, 0xa8, - 0x1f, 0xb0, 0xf1, 0xd3, 0xab, 0x51, 0xe4, 0x86, 0xc3, 0xce, 0xa7, 0xad, 0xb4, 0x74, 0x5b, 0xa1, - 0xa7, 0x11, 0x07, 0xb6, 0xc2, 0xcf, 0xd5, 0x2b, 0xd6, 0xa9, 0xf8, 0x0d, 0x79, 0x67, 0x8e, 0xcb, - 0x48, 0x60, 0x94, 0x24, 0xaf, 0xdc, 0xa1, 0x1b, 0x00, 0xc1, 0xeb, 0x07, 0x53, 0x6e, 0x2b, 0x0b, - 0xea, 0x8b, 0x1c, 0xd4, 0x0f, 0x21, 0x1e, 0xd7, 0xa8, 0x5a, 0xa2, 0xef, 0x61, 0x5f, 0xa6, 0x3d, - 0x9e, 0xf8, 0x53, 0x42, 0x0d, 0xfd, 0xbc, 0xc8, 0xa9, 0xce, 0x24, 0x95, 0x52, 0x71, 0x24, 0x85, - 0xe9, 0x71, 0x04, 0xae, 0x4b, 0x78, 0xb8, 0xa6, 0xd6, 0xaf, 0x50, 0x55, 0xf4, 0x56, 0x07, 0x74, - 0x19, 0x3c, 0xaa, 0x43, 0xe5, 0x71, 0x78, 0x3f, 0x7c, 0xf8, 0x79, 0xd8, 0xd8, 0x43, 0x55, 0x28, - 0x0d, 0xbb, 0x3f, 0xf6, 0x1b, 0x1a, 0x3a, 0x82, 0x83, 0x41, 0x77, 0xf4, 0xd3, 0x18, 0xf7, 0x07, - 0xfd, 0xee, 0xa8, 0x7f, 0xdd, 0x28, 0x58, 0x1f, 0x43, 0x2d, 0x8e, 0x0a, 0x55, 0xa0, 0xd8, 0x1d, - 0xf5, 0xe4, 0x95, 0xeb, 0x3e, 0x5f, 0x69, 0xd6, 0x5f, 0x1a, 0x1c, 0x27, 0x8b, 0x40, 0x17, 0xbe, - 0x47, 0x49, 0x58, 0x85, 0x89, 0xbf, 0xf4, 0xe2, 0x2a, 0x88, 0x0d, 0x42, 0x50, 0xf2, 0xc8, 0x9f, - 0xaa, 0x06, 0x62, 0x1d, 0x22, 0x99, 0xcf, 0x6c, 0x57, 0xe8, 0xcf, 0x91, 0x62, 0x83, 0xbe, 0x85, - 0x6a, 0x94, 0x1c, 0xe5, 0xca, 0x16, 0x2f, 0xea, 0x9d, 0x93, 0x64, 0xca, 0x91, 0x47, 0x1c, 0xc3, - 0xac, 0x1b, 0x68, 0xde, 0x10, 0x15, 0x89, 0x54, 0x44, 0xf5, 0x44, 0xe8, 0xd7, 0x7e, 0x21, 0x22, - 0x98, 0xd0, 0x2f, 0x5f, 0x23, 0x03, 0x2a, 0x2b, 0x12, 0x50, 0xc7, 0xf7, 0x44, 0x38, 0x65, 0xac, - 0xb6, 0x16, 0x03, 0x63, 0x9b, 0x28, 0xca, 0x2b, 0x8d, 0xe9, 0x33, 0x28, 0x85, 0x2d, 0x2b, 0x68, - 0xea, 0x1d, 0x94, 0x8c, 0xf3, 0x8e, 0x5b, 0xb0, 0xb0, 0xa3, 0x0f, 0xa1, 0x16, 0xe2, 0xe9, 0xc2, - 0x9e, 0x10, 0x91, 0x6d, 0x0d, 0xbf, 0x39, 0xb0, 0x6e, 0xd7, 0xbd, 0xf6, 0x7c, 0x8f, 0x11, 0x8f, - 0xed, 0x16, 0xff, 0x00, 0xce, 0x52, 0x98, 0xa2, 0x04, 0xda, 0x50, 0x89, 0x42, 0x13, 0x6c, 0x99, - 0xba, 0x2a, 0x94, 0xf5, 0x2f, 0x2f, 0xf1, 0xe3, 0x62, 0x6a, 0x33, 0xa2, 0x4c, 0x6f, 0x09, 0xea, - 0x73, 0x5e, 0xf6, 0xf0, 0xe9, 0x47, 0x5a, 0x1c, 0x49, 0x6e, 0x39, 0x1f, 0x7a, 0xe1, 0x17, 0x4b, - 0x3b, 0xba, 0x04, 0x7d, 0x65, 0xbb, 0x9c, 0x47, 0x08, 0x11, 0xab, 0x16, 0x21, 0xc5, 0xdc, 0xc0, - 0x11, 0x02, 0x35, 0xa1, 0x32, 0x0d, 0x5e, 0xc7, 0xc1, 0xd2, 0x13, 0x8f, 0xac, 0x8a, 0x75, 0xbe, - 0xc5, 0x4b, 0x0f, 0x7d, 0x02, 0x07, 0x53, 0x87, 0xda, 0x4f, 0x2e, 0x19, 0xcf, 0x7d, 0xff, 0x77, - 0x2a, 0xde, 0x59, 0x15, 0xef, 0x47, 0x87, 0xb7, 0xe1, 0x19, 0xd7, 0xf5, 0x64, 0x23, 0xfc, 0x5d, - 0x95, 0xf8, 0x4f, 0x83, 0x93, 0x3b, 0x8f, 0x3f, 0x2f, 0xd7, 0xdd, 0x90, 0x22, 0x4e, 0x5b, 0xcb, - 0x9d, 0x76, 0xe1, 0x7d, 0xd2, 0x2e, 0x26, 0xd2, 0x56, 0xc2, 0x97, 0xd6, 0x84, 0xcf, 0x23, 0x45, - 0xb2, 0x01, 0xf5, 0x8d, 0x06, 0x44, 0x1f, 0x01, 0x04, 0x64, 0x49, 0xc9, 0x58, 0x90, 0x57, 0xc4, - 0xfd, 0x9a, 0x38, 0x19, 0xf2, 0x03, 0xeb, 0x0e, 0x4e, 0x37, 0x93, 0xdf, 0x55, 0xc8, 0x39, 0x34, - 0x1f, 0x3d, 0x27, 0x55, 0xc9, 0xb4, 0xa6, 0xda, 0xca, 0xad, 0x90, 0x92, 0x1b, 0x1f, 0x23, 0x8b, - 0x65, 0xf0, 0x4c, 0x22, 0xad, 0xe4, 0xc6, 0xba, 0x07, 0x63, 0xdb, 0xd3, 0x8e, 0x61, 0x77, 0xfe, - 0x29, 0xc3, 0xa1, 0x9a, 0x0a, 0x72, 0x86, 0x23, 0x07, 0xf6, 0xd7, 0xc7, 0x1f, 0xfa, 0x22, 0x7b, - 0xc4, 0x6f, 0xfc, 0x9d, 0x32, 0x2f, 0xf3, 0x40, 0x65, 0xa8, 0xd6, 0xde, 0x37, 0x1a, 0xa2, 0xd0, - 0xd8, 0x9c, 0x4a, 0xe8, 0xeb, 0x74, 0x8e, 0x8c, 0x31, 0x68, 0xb6, 0xf2, 0xc2, 0x95, 0x5b, 0xb4, - 0x82, 0xa3, 0xad, 0x51, 0x82, 0xde, 0x49, 0x93, 0x9c, 0x5e, 0x66, 0x3b, 0x37, 0x3e, 0xf6, 0xfb, - 0x1b, 0x1c, 0x24, 0x1e, 0x2d, 0xca, 0x50, 0x2b, 0x6d, 0x30, 0x99, 0x5f, 0xe6, 0xc2, 0xc6, 0xbe, - 0x5e, 0xe0, 0x30, 0xd9, 0xd8, 0x28, 0x83, 0x20, 0xf5, 0xed, 0x9b, 0x5f, 0xe5, 0x03, 0xc7, 0xee, - 0x78, 0x1d, 0x37, 0x5b, 0x32, 0xab, 0x8e, 0x19, 0x8f, 0x24, 0xab, 0x8e, 0x59, 0x9d, 0x6e, 0xed, - 0x5d, 0xc1, 0x2f, 0x55, 0x85, 0x7e, 0xd2, 0xc5, 0xff, 0x4f, 0xdf, 0xfd, 0x1f, 0x00, 0x00, 0xff, - 0xff, 0xd9, 0x8f, 0xae, 0xcb, 0xf4, 0x09, 0x00, 0x00, + // 893 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0x5f, 0x6f, 0xe3, 0x44, + 0x10, 0x3f, 0x27, 0xa9, 0x93, 0x4c, 0xda, 0x2a, 0xdd, 0x6b, 0x1b, 0xd7, 0x02, 0x74, 0x32, 0x82, + 0x0b, 0x07, 0x24, 0x10, 0x5e, 0x11, 0x52, 0x2e, 0x17, 0xf5, 0xaa, 0x0b, 0x39, 0x69, 0x43, 0x41, + 0xe2, 0x81, 0xc8, 0x4d, 0x36, 0x17, 0x83, 0x6b, 0x07, 0xef, 0x26, 0xa2, 0x1f, 0x81, 0xcf, 0xc0, + 0x1b, 0xdf, 0x83, 0x4f, 0xc6, 0x0b, 0xeb, 0xfd, 0xe3, 0xc6, 0xa9, 0x4d, 0x7d, 0x79, 0x89, 0xbd, + 0x3b, 0xbf, 0xfd, 0xcd, 0xcc, 0x6f, 0x76, 0xc6, 0x01, 0x7b, 0xe9, 0xae, 0xbc, 0x2e, 0x25, 0xd1, + 0xc6, 0x9b, 0x11, 0xda, 0x65, 0x9e, 0xef, 0x93, 0xa8, 0xb3, 0x8a, 0x42, 0x16, 0xa2, 0xd3, 0xd8, + 0xd6, 0xd1, 0xb6, 0x8e, 0xb4, 0xd9, 0xe7, 0xe2, 0xc4, 0x6c, 0xe9, 0x46, 0x4c, 0xfe, 0x4a, 0xb4, + 0xdd, 0xda, 0xde, 0x0f, 0x83, 0x85, 0xf7, 0x4e, 0x19, 0xa4, 0x8b, 0x88, 0xf8, 0xc4, 0xa5, 0x44, + 0x3f, 0x53, 0x87, 0xb4, 0xcd, 0x0b, 0x16, 0xa1, 0x32, 0x5c, 0xa4, 0x0c, 0x94, 0xb9, 0x6c, 0x4d, + 0x53, 0x7c, 0x1b, 0x12, 0x51, 0x2f, 0x0c, 0xf4, 0x53, 0xda, 0x9c, 0xbf, 0x4b, 0xf0, 0x74, 0xe4, + 0x51, 0x86, 0xe5, 0x41, 0x8a, 0xc9, 0xef, 0x6b, 0x42, 0x19, 0x3a, 0x85, 0x03, 0xdf, 0xbb, 0xf5, + 0x98, 0x65, 0x3c, 0x33, 0xda, 0x65, 0x2c, 0x17, 0xe8, 0x1c, 0xcc, 0x70, 0xb1, 0xa0, 0x84, 0x59, + 0x25, 0xbe, 0x5d, 0xc7, 0x6a, 0x85, 0xbe, 0x83, 0x2a, 0x0d, 0x23, 0x36, 0xbd, 0xb9, 0xb3, 0xca, + 0xdc, 0x70, 0xdc, 0xfb, 0xa4, 0x93, 0x25, 0x45, 0x27, 0xf6, 0x34, 0xe1, 0xc0, 0x4e, 0xfc, 0xf3, + 0xf2, 0x0e, 0x9b, 0x54, 0x3c, 0x63, 0xde, 0x85, 0xe7, 0x33, 0x12, 0x59, 0x15, 0xc9, 0x2b, 0x57, + 0xe8, 0x12, 0x40, 0xf0, 0x86, 0xd1, 0x9c, 0xdb, 0x0e, 0x04, 0x75, 0xbb, 0x00, 0xf5, 0xdb, 0x18, + 0x8f, 0xeb, 0x54, 0xbf, 0xa2, 0x6f, 0xe1, 0x50, 0x4a, 0x32, 0x9d, 0x85, 0x73, 0x42, 0x2d, 0xf3, + 0x59, 0x99, 0x53, 0x5d, 0x48, 0x2a, 0xad, 0xf0, 0x44, 0x8a, 0x36, 0xe0, 0x08, 0xdc, 0x90, 0xf0, + 0xf8, 0x9d, 0x3a, 0xbf, 0x40, 0x4d, 0xd3, 0x3b, 0x3d, 0x30, 0x65, 0xf0, 0xa8, 0x01, 0xd5, 0xeb, + 0xf1, 0x9b, 0xf1, 0xdb, 0x9f, 0xc6, 0xcd, 0x27, 0xa8, 0x06, 0x95, 0x71, 0xff, 0xfb, 0x61, 0xd3, + 0x40, 0x27, 0x70, 0x34, 0xea, 0x4f, 0x7e, 0x98, 0xe2, 0xe1, 0x68, 0xd8, 0x9f, 0x0c, 0x5f, 0x35, + 0x4b, 0xce, 0x47, 0x50, 0x4f, 0xa2, 0x42, 0x55, 0x28, 0xf7, 0x27, 0x03, 0x79, 0xe4, 0xd5, 0x90, + 0xbf, 0x19, 0xce, 0x9f, 0x06, 0x9c, 0xa6, 0x8b, 0x40, 0x57, 0x61, 0x40, 0x49, 0x5c, 0x85, 0x59, + 0xb8, 0x0e, 0x92, 0x2a, 0x88, 0x05, 0x42, 0x50, 0x09, 0xc8, 0x1f, 0xba, 0x06, 0xe2, 0x3d, 0x46, + 0xb2, 0x90, 0xb9, 0xbe, 0xd0, 0x9f, 0x23, 0xc5, 0x02, 0x7d, 0x0d, 0x35, 0x95, 0x1c, 0xe5, 0xca, + 0x96, 0xdb, 0x8d, 0xde, 0x59, 0x3a, 0x65, 0xe5, 0x11, 0x27, 0x30, 0xe7, 0x12, 0x5a, 0x97, 0x44, + 0x47, 0x22, 0x15, 0xd1, 0x77, 0x22, 0xf6, 0xeb, 0xde, 0x12, 0x11, 0x4c, 0xec, 0x97, 0xbf, 0x23, + 0x0b, 0xaa, 0xea, 0x42, 0x89, 0x70, 0x0e, 0xb0, 0x5e, 0x3a, 0x0c, 0xac, 0x87, 0x44, 0x2a, 0xaf, + 0x2c, 0xa6, 0x4f, 0xa1, 0x12, 0x5f, 0x67, 0x41, 0xd3, 0xe8, 0xa1, 0x74, 0x9c, 0x57, 0xdc, 0x82, + 0x85, 0x1d, 0x7d, 0x00, 0xf5, 0x18, 0x4f, 0x57, 0xee, 0x8c, 0x88, 0x6c, 0xeb, 0xf8, 0x7e, 0xc3, + 0x79, 0xbd, 0xed, 0x75, 0x10, 0x06, 0x8c, 0x04, 0x6c, 0xbf, 0xf8, 0x47, 0x70, 0x91, 0xc1, 0xa4, + 0x12, 0xe8, 0x42, 0x55, 0x85, 0x26, 0xd8, 0x72, 0x75, 0xd5, 0x28, 0xe7, 0x1f, 0x5e, 0xe2, 0xeb, + 0xd5, 0xdc, 0x65, 0x44, 0x9b, 0xfe, 0x27, 0xa8, 0xe7, 0xbc, 0xec, 0xf1, 0x58, 0x50, 0x5a, 0x9c, + 0x48, 0x6e, 0x39, 0x3b, 0x06, 0xf1, 0x2f, 0x96, 0x76, 0xf4, 0x02, 0xcc, 0x8d, 0xeb, 0x73, 0x1e, + 0x21, 0x44, 0xa2, 0x9a, 0x42, 0x8a, 0x99, 0x82, 0x15, 0x02, 0xb5, 0xa0, 0x3a, 0x8f, 0xee, 0xa6, + 0xd1, 0x3a, 0x10, 0x4d, 0x56, 0xc3, 0x26, 0x5f, 0xe2, 0x75, 0x80, 0x3e, 0x86, 0xa3, 0xb9, 0x47, + 0xdd, 0x1b, 0x9f, 0x4c, 0x97, 0x61, 0xf8, 0x1b, 0x15, 0x7d, 0x56, 0xc3, 0x87, 0x6a, 0xf3, 0x75, + 0xbc, 0xc7, 0x75, 0x3d, 0xdb, 0x09, 0x7f, 0x5f, 0x25, 0xfe, 0x35, 0xe0, 0xec, 0x2a, 0xe0, 0xed, + 0xe5, 0xfb, 0x3b, 0x52, 0x24, 0x69, 0x1b, 0x85, 0xd3, 0x2e, 0xbd, 0x4f, 0xda, 0xe5, 0x54, 0xda, + 0x5a, 0xf8, 0xca, 0x96, 0xf0, 0x45, 0xa4, 0x48, 0x5f, 0x40, 0x73, 0xe7, 0x02, 0xa2, 0x0f, 0x01, + 0x22, 0xb2, 0xa6, 0x64, 0x2a, 0xc8, 0xab, 0xe2, 0x7c, 0x5d, 0xec, 0x8c, 0xf9, 0x86, 0x73, 0x05, + 0xe7, 0xbb, 0xc9, 0xef, 0x2b, 0xe4, 0x12, 0x5a, 0xd7, 0x81, 0x97, 0xa9, 0x64, 0xd6, 0xa5, 0x7a, + 0x90, 0x5b, 0x29, 0x23, 0x37, 0x3e, 0x46, 0x56, 0xeb, 0xe8, 0x1d, 0x51, 0x5a, 0xc9, 0x85, 0xf3, + 0x06, 0xac, 0x87, 0x9e, 0xf6, 0x0d, 0xfb, 0x29, 0x9c, 0xf0, 0xbe, 0xfa, 0x51, 0x76, 0x99, 0x0a, + 0xd8, 0x19, 0x02, 0xda, 0xde, 0xbc, 0xe7, 0x56, 0x5b, 0x69, 0x6e, 0xfd, 0x09, 0xd3, 0x78, 0xdd, + 0xb3, 0xbd, 0xbf, 0x4c, 0x38, 0xd6, 0x13, 0x47, 0x7e, 0x1f, 0x90, 0x07, 0x87, 0xdb, 0xa3, 0x15, + 0x7d, 0x96, 0xff, 0xf9, 0xd8, 0xf9, 0x06, 0xda, 0x2f, 0x8a, 0x40, 0x65, 0xa8, 0xce, 0x93, 0xaf, + 0x0c, 0x44, 0xa1, 0xb9, 0x3b, 0xf1, 0xd0, 0x97, 0xd9, 0x1c, 0x39, 0x23, 0xd6, 0xee, 0x14, 0x85, + 0x6b, 0xb7, 0x68, 0x23, 0xe4, 0x4c, 0x8f, 0x29, 0xf4, 0x28, 0x4d, 0x7a, 0x32, 0xda, 0xdd, 0xc2, + 0xf8, 0xc4, 0xef, 0xaf, 0x70, 0x94, 0x1a, 0x08, 0x28, 0x47, 0xad, 0xac, 0xa1, 0x67, 0x7f, 0x5e, + 0x08, 0x9b, 0xf8, 0xba, 0x85, 0xe3, 0x74, 0xd3, 0xa0, 0x1c, 0x82, 0xcc, 0xb9, 0x62, 0x7f, 0x51, + 0x0c, 0x9c, 0xb8, 0xe3, 0x75, 0xdc, 0xbd, 0xee, 0x79, 0x75, 0xcc, 0x69, 0xc0, 0xbc, 0x3a, 0xe6, + 0x75, 0x11, 0x77, 0xea, 0x02, 0xdc, 0x77, 0x00, 0x7a, 0x9e, 0x5b, 0x90, 0x74, 0xe3, 0xd8, 0xed, + 0xc7, 0x81, 0xda, 0xc5, 0x4b, 0xf8, 0xb9, 0xa6, 0x71, 0x37, 0xa6, 0xf8, 0xfb, 0xf7, 0xcd, 0x7f, + 0x01, 0x00, 0x00, 0xff, 0xff, 0x14, 0xb2, 0x9f, 0x07, 0xcf, 0x0a, 0x00, 0x00, } diff --git a/pkg/proto/hapi/version/version.pb.go b/pkg/proto/hapi/version/version.pb.go new file mode 100644 index 000000000..ef08a6b64 --- /dev/null +++ b/pkg/proto/hapi/version/version.pb.go @@ -0,0 +1,66 @@ +// Code generated by protoc-gen-go. +// source: hapi/version/version.proto +// DO NOT EDIT! + +/* +Package version is a generated protocol buffer package. + +It is generated from these files: + hapi/version/version.proto + +It has these top-level messages: + Version +*/ +package version + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +const _ = proto.ProtoPackageIsVersion1 + +type Version struct { + Major uint32 `protobuf:"varint,1,opt,name=major" json:"major,omitempty"` + Minor uint32 `protobuf:"varint,2,opt,name=minor" json:"minor,omitempty"` + Patch uint32 `protobuf:"varint,3,opt,name=patch" json:"patch,omitempty"` + PreRelease string `protobuf:"bytes,4,opt,name=pre_release,json=preRelease" json:"pre_release,omitempty"` + BuildMetadata string `protobuf:"bytes,5,opt,name=build_metadata,json=buildMetadata" json:"build_metadata,omitempty"` + GitVersion string `protobuf:"bytes,6,opt,name=git_version,json=gitVersion" json:"git_version,omitempty"` + GitCommit string `protobuf:"bytes,7,opt,name=git_commit,json=gitCommit" json:"git_commit,omitempty"` + GitTreeState string `protobuf:"bytes,8,opt,name=git_tree_state,json=gitTreeState" json:"git_tree_state,omitempty"` +} + +func (m *Version) Reset() { *m = Version{} } +func (m *Version) String() string { return proto.CompactTextString(m) } +func (*Version) ProtoMessage() {} +func (*Version) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func init() { + proto.RegisterType((*Version)(nil), "hapi.version.Version") +} + +var fileDescriptor0 = []byte{ + // 225 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x34, 0xd0, 0xb1, 0x4e, 0xc3, 0x30, + 0x10, 0x06, 0x60, 0x05, 0x68, 0x43, 0x8e, 0xb6, 0x83, 0xc5, 0x60, 0x21, 0x21, 0x10, 0x02, 0x89, + 0xa9, 0x0c, 0xbc, 0x01, 0xcc, 0x2c, 0x01, 0x31, 0xb0, 0x58, 0xd7, 0xf6, 0x44, 0x8d, 0xea, 0xd8, + 0x72, 0x0e, 0xde, 0x9d, 0xad, 0xf6, 0xd9, 0x99, 0x92, 0xff, 0xfb, 0x4f, 0x97, 0x53, 0xe0, 0x6a, + 0x8f, 0xc1, 0x3e, 0xfd, 0x51, 0x1c, 0xad, 0x1f, 0xa6, 0xe7, 0x3a, 0x44, 0xcf, 0x5e, 0x2d, 0x72, + 0xb7, 0xae, 0x76, 0xf7, 0xdf, 0x40, 0xfb, 0x59, 0xde, 0xd5, 0x25, 0xcc, 0x1c, 0xfe, 0xf8, 0xa8, + 0x9b, 0xdb, 0xe6, 0x71, 0xd9, 0x97, 0x20, 0x6a, 0x87, 0xa4, 0x27, 0x55, 0x73, 0xc8, 0x1a, 0x90, + 0xb7, 0x7b, 0x7d, 0x5a, 0x54, 0x82, 0xba, 0x81, 0x8b, 0x10, 0xc9, 0x44, 0x3a, 0x10, 0x8e, 0xa4, + 0xcf, 0x52, 0xd7, 0xf5, 0x90, 0xa8, 0x2f, 0xa2, 0x1e, 0x60, 0xb5, 0xf9, 0xb5, 0x87, 0x9d, 0x71, + 0xc4, 0xb8, 0x43, 0x46, 0x3d, 0x93, 0x99, 0xa5, 0xe8, 0x5b, 0xc5, 0xbc, 0xe7, 0xdb, 0xb2, 0xa9, + 0x47, 0xea, 0x79, 0xd9, 0x93, 0x68, 0x3a, 0xf5, 0x1a, 0x72, 0x32, 0x5b, 0xef, 0x9c, 0x65, 0xdd, + 0x4a, 0xdf, 0x25, 0x79, 0x15, 0x50, 0xf7, 0xb0, 0xca, 0x35, 0x47, 0x22, 0x33, 0x32, 0x32, 0xe9, + 0x73, 0x19, 0x59, 0x24, 0xfd, 0x48, 0xf8, 0x9e, 0xed, 0xa5, 0xfb, 0x6a, 0xeb, 0x17, 0x36, 0x73, + 0xf9, 0x37, 0xcf, 0xc7, 0x00, 0x00, 0x00, 0xff, 0xff, 0x87, 0xdf, 0xc1, 0x73, 0x39, 0x01, 0x00, + 0x00, +} diff --git a/pkg/version/version.go b/pkg/version/version.go index 8994d4b91..533a639ba 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -17,10 +17,51 @@ limitations under the License. // Package version represents the current version of the project. package version // import "k8s.io/helm/pkg/version" +import ( + "fmt" + + "k8s.io/helm/pkg/proto/hapi/version" +) + // Version is the current version of the Helm. // Update this whenever making a new release. -// The version is of the format Major.Minor.Patch +// The version is of the format Major.Minor.Patch[-Prerelease][+BuildMetadata] +// // Increment major number for new feature additions and behavioral changes. // Increment minor number for bug fixes and performance enhancements. // Increment patch number for critical fixes to existing releases. -var Version = "v2.0.0-alpha.4" +// +// BuildMetadata gets filled in during build, do not touch +// GitCommit gets filled in during build, do not touch +var ( + Major uint32 = 2 + Minor uint32 = 0 + Patch uint32 = 0 + PreRelease = "alpha.4" + BuildMetadata = "" + GitCommit = "" +) + +// GetVersion returns the semver string of the version +func GetVersion() string { + version := fmt.Sprintf("v%d.%d.%d", Major, Minor, Patch) + if PreRelease != "" { + version = version + "-" + PreRelease + } + if BuildMetadata != "" { + version = version + "+" + BuildMetadata + } + return version +} + +// GetVersionProto returns protobuf representing the version +func GetVersionProto() version.Version { + return version.Version{ + Major: Major, + Minor: Minor, + Patch: Patch, + PreRelease: PreRelease, + BuildMetadata: BuildMetadata, + GitCommit: GitCommit, + } +} diff --git a/versioning.mk b/versioning.mk index 3b8645392..2c6e1b4b5 100644 --- a/versioning.mk +++ b/versioning.mk @@ -1,5 +1,6 @@ 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) @@ -9,12 +10,13 @@ ifdef VERSION endif DOCKER_VERSION ?= git-${GIT_SHA} -BINARY_VERSION ?= ${GIT_TAG}+${GIT_SHA} +BINARY_VERSION ?= ${GIT_TAG}-${GIT_SHA} IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${DOCKER_VERSION} MUTABLE_IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${MUTABLE_VERSION} -LDFLAGS += -X k8s.io/helm/pkg/version.Version=${BINARY_VERSION} +LDFLAGS += -X k8s.io/helm/pkg/version.BuildMetadata=${BINARY_VERSION} +LDFLAGS += -X k8s.io/helm/pkg/version.GitCommit=${GIT_COMMIT} DOCKER_PUSH = docker push ifeq ($(DOCKER_REGISTRY),gcr.io) From cbec79482a7b5727896825faf1e2e7c65bb6cc73 Mon Sep 17 00:00:00 2001 From: vaikas-google Date: Fri, 9 Sep 2016 09:54:17 -0700 Subject: [PATCH 086/183] address code review comments --- pkg/helm/client.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/helm/client.go b/pkg/helm/client.go index 95ad41c5e..ce08bd6b1 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -23,7 +23,6 @@ import ( "k8s.io/helm/pkg/chartutil" rls "k8s.io/helm/pkg/proto/hapi/services" - // version "k8s.io/helm/pkg/proto/hapi/version" ) const ( @@ -123,7 +122,7 @@ func (h *Client) UpdateRelease(rlsName string, chStr string, opts ...UpdateOptio // Version returns the server version // -// Note: there aren't currently any supported StatusOptions, +// Note: there aren't currently any supported StatusOptions, // but they are kept in the API signature as a placeholder for future additions. func (h *Client) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) { c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) From f8adf7f4d2b0a7ce76226ab4a3053608481260a8 Mon Sep 17 00:00:00 2001 From: vaikas-google Date: Mon, 12 Sep 2016 22:46:51 -0700 Subject: [PATCH 087/183] Address cr comments --- _proto/hapi/version/version.proto | 7 ++-- cmd/helm/helm_test.go | 6 +--- cmd/helm/installer/install.go | 2 +- pkg/proto/hapi/chart/metadata.pb.go | 54 ++++++++++++---------------- pkg/proto/hapi/version/version.pb.go | 39 +++++++++----------- pkg/version/version.go | 32 +++++------------ versioning.mk | 2 +- 7 files changed, 53 insertions(+), 89 deletions(-) diff --git a/_proto/hapi/version/version.proto b/_proto/hapi/version/version.proto index 1f68253ed..092d23c7a 100644 --- a/_proto/hapi/version/version.proto +++ b/_proto/hapi/version/version.proto @@ -19,11 +19,8 @@ package hapi.version; option go_package = "version"; message Version { - uint32 major = 1; - uint32 minor = 2; - uint32 patch = 3; - string pre_release = 4; - string build_metadata = 5; + // Sem ver string for the version + string sem_ver = 1; string git_version = 6; string git_commit = 7; string git_tree_state = 8; diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index f6a26edd1..68c3ccc98 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -149,11 +149,7 @@ func (c *fakeReleaseClient) ReleaseStatus(rlsName string, opts ...helm.StatusOpt func (c *fakeReleaseClient) GetVersion(opts ...helm.VersionOption) (*rls.GetVersionResponse, error) { return &rls.GetVersionResponse{ Version: &version.Version{ - Major: 1, - Minor: 2, - Patch: 3, - PreRelease: "fakeclient", - BuildMetadata: "testonly", + SemVer: "1.2.3-fakeclient+testonly", }, }, nil } diff --git a/cmd/helm/installer/install.go b/cmd/helm/installer/install.go index 84253ed18..0bfd197e8 100644 --- a/cmd/helm/installer/install.go +++ b/cmd/helm/installer/install.go @@ -62,7 +62,7 @@ func Install(namespace, image string, verbose bool) error { if image == "" { // strip git sha off version - tag := strings.Split(version.GetVersion(), "+")[0] + tag := strings.Split(version.Version, "+")[0] image = fmt.Sprintf("%s:%s", defaultImage, tag) } diff --git a/pkg/proto/hapi/chart/metadata.pb.go b/pkg/proto/hapi/chart/metadata.pb.go index 346866a3b..e45ab4e49 100644 --- a/pkg/proto/hapi/chart/metadata.pb.go +++ b/pkg/proto/hapi/chart/metadata.pb.go @@ -54,15 +54,8 @@ type Dependency struct { // Repository is the repository URL. Appending '/index.yaml' to this should // return the repo index. Repository string `protobuf:"bytes,2,opt,name=repository" json:"repository,omitempty"` - // Version is a SemVer 2 version range. - // This can be an exact version or any of the version range operators - // described here: https://github.com/Masterminds/semver/blob/master/README.md + // Version is a SemVer 2 version. Version string `protobuf:"bytes,3,opt,name=version" json:"version,omitempty"` - // FetchedVersion is a computed field that indicates exactly which version - // was fetched by tooling. It is an exact version (not a range). - // - // This plays the roll of a "lock" for this dependency. - FetchedVersion string `protobuf:"bytes,4,opt,name=fetchedVersion" json:"fetchedVersion,omitempty"` } func (m *Dependency) Reset() { *m = Dependency{} } @@ -122,27 +115,26 @@ func init() { } var fileDescriptor2 = []byte{ - // 346 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x52, 0x4f, 0x4b, 0xfb, 0x40, - 0x10, 0xfd, 0xa5, 0x6d, 0x92, 0x76, 0xf2, 0x43, 0xca, 0x22, 0x65, 0xf5, 0x20, 0x25, 0x07, 0xf1, - 0x94, 0x82, 0x82, 0x88, 0x47, 0x51, 0x3c, 0x68, 0x5b, 0x29, 0xfe, 0x01, 0x6f, 0x6b, 0x32, 0x9a, - 0x45, 0x93, 0x0d, 0xbb, 0xab, 0xd2, 0xab, 0x9f, 0xd6, 0x8f, 0xe1, 0x66, 0x93, 0xb6, 0xa9, 0xf6, - 0x10, 0x98, 0xf7, 0xde, 0xcc, 0xbc, 0x99, 0xcc, 0xc2, 0x4e, 0xca, 0x0a, 0x3e, 0x8a, 0x53, 0x26, - 0xf5, 0x28, 0x43, 0xcd, 0x12, 0xa6, 0x59, 0x54, 0x48, 0xa1, 0x05, 0x81, 0x52, 0x8a, 0xac, 0x14, - 0x1e, 0x03, 0x8c, 0x19, 0xcf, 0xb5, 0xf9, 0x50, 0x12, 0x02, 0x9d, 0x9c, 0x65, 0x48, 0x9d, 0xa1, - 0x73, 0xd0, 0x9b, 0xd9, 0x98, 0x6c, 0x83, 0x8b, 0x19, 0xe3, 0x6f, 0xb4, 0x65, 0xc9, 0x0a, 0x84, - 0x5f, 0x0e, 0xc0, 0x39, 0x16, 0x98, 0x27, 0x98, 0xc7, 0xf3, 0x8d, 0x85, 0x7b, 0x00, 0x12, 0x0b, - 0xa1, 0xb8, 0x16, 0x72, 0x5e, 0x57, 0x37, 0x18, 0x42, 0xc1, 0xff, 0x40, 0xa9, 0xb8, 0xc8, 0x69, - 0xdb, 0x8a, 0x0b, 0x48, 0xf6, 0x61, 0xeb, 0x19, 0x75, 0x9c, 0x62, 0x72, 0x5f, 0x27, 0x74, 0x6c, - 0xc2, 0x2f, 0x36, 0xfc, 0x6e, 0x41, 0x77, 0x5c, 0xef, 0xb6, 0x71, 0x04, 0xc3, 0xa5, 0xc2, 0x70, - 0x95, 0xb9, 0x8d, 0x4b, 0x5b, 0x25, 0xde, 0x65, 0x8c, 0xca, 0xd8, 0xb6, 0x4b, 0xdb, 0x1a, 0x36, - 0x07, 0xea, 0xac, 0x0f, 0x34, 0x84, 0x20, 0x41, 0x15, 0x4b, 0x5e, 0xe8, 0x52, 0x75, 0xad, 0xda, - 0xa4, 0xc8, 0x2e, 0x74, 0x5f, 0x71, 0xfe, 0x29, 0x64, 0xa2, 0xa8, 0x67, 0xdb, 0x2e, 0x31, 0x39, - 0x81, 0x20, 0x5b, 0xfe, 0x63, 0x45, 0x7d, 0x23, 0x07, 0x87, 0x83, 0x68, 0x75, 0x85, 0x68, 0x75, - 0x82, 0x59, 0x33, 0x95, 0x0c, 0xc0, 0xc3, 0xfc, 0xc5, 0xc4, 0xb4, 0x6b, 0x2d, 0x6b, 0x54, 0xee, - 0xc5, 0x63, 0x33, 0x48, 0xaf, 0xda, 0xab, 0x8c, 0xc9, 0x29, 0xfc, 0x4f, 0x16, 0x07, 0xe1, 0x66, - 0x39, 0xf8, 0x6b, 0xb3, 0x3a, 0xd8, 0x6c, 0x2d, 0x37, 0x1c, 0x82, 0x77, 0x51, 0x75, 0x0e, 0xc0, - 0xbf, 0x9b, 0x5c, 0x4d, 0xa6, 0x0f, 0x93, 0xfe, 0x3f, 0xd2, 0x03, 0xf7, 0x72, 0x7a, 0x7b, 0x73, - 0xdd, 0x77, 0xce, 0xfc, 0x47, 0xd7, 0xf6, 0x78, 0xf2, 0xec, 0x1b, 0x3a, 0xfa, 0x09, 0x00, 0x00, - 0xff, 0xff, 0x6a, 0xfb, 0xe7, 0x86, 0x60, 0x02, 0x00, 0x00, + // 326 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x52, 0xcf, 0x4b, 0xc3, 0x30, + 0x14, 0x76, 0x3f, 0xda, 0x6e, 0xaf, 0x1e, 0x46, 0x90, 0x11, 0x3d, 0xc8, 0xe8, 0xc9, 0x53, 0x07, + 0x0a, 0x22, 0x1e, 0x45, 0xf1, 0xa0, 0xdb, 0x64, 0x28, 0xc2, 0x6e, 0xb1, 0x7d, 0xb8, 0xa0, 0x4d, + 0x4a, 0x12, 0x95, 0xfd, 0xc7, 0xfe, 0x19, 0x26, 0x69, 0xb7, 0x75, 0xb8, 0x43, 0xe1, 0x7d, 0xdf, + 0x97, 0x7c, 0xef, 0xbd, 0xaf, 0x81, 0xe3, 0x25, 0x2b, 0xf9, 0x38, 0x5b, 0x32, 0x65, 0xc6, 0x05, + 0x1a, 0x96, 0x33, 0xc3, 0xd2, 0x52, 0x49, 0x23, 0x09, 0x38, 0x29, 0xf5, 0x52, 0x72, 0x09, 0x30, + 0x61, 0x5c, 0x18, 0xfb, 0xa1, 0x22, 0x04, 0xba, 0x82, 0x15, 0x48, 0x5b, 0xa3, 0xd6, 0x59, 0x7f, + 0xee, 0x6b, 0x72, 0x04, 0x01, 0x16, 0x8c, 0x7f, 0xd2, 0xb6, 0x27, 0x2b, 0x90, 0x2c, 0x00, 0x6e, + 0xb1, 0x44, 0x91, 0xa3, 0xc8, 0x56, 0x7b, 0xef, 0x9d, 0x02, 0x28, 0x2c, 0xa5, 0xe6, 0x46, 0xaa, + 0x55, 0x7d, 0xb9, 0xc1, 0x10, 0x0a, 0xd1, 0x37, 0x2a, 0xcd, 0xa5, 0xa0, 0x1d, 0x2f, 0xae, 0x61, + 0xf2, 0xdb, 0x86, 0xde, 0xa4, 0x1e, 0x79, 0xaf, 0xb5, 0xe5, 0x96, 0xd2, 0x72, 0x95, 0xa9, 0xaf, + 0x9d, 0x9d, 0x96, 0x5f, 0x2a, 0x43, 0x6d, 0xed, 0x3a, 0xce, 0xae, 0x86, 0xcd, 0x46, 0xdd, 0x9d, + 0x46, 0x64, 0x04, 0x71, 0x8e, 0x3a, 0x53, 0xbc, 0x34, 0x4e, 0x0d, 0xbc, 0xda, 0xa4, 0xc8, 0x09, + 0xf4, 0x3e, 0x70, 0xf5, 0x23, 0x55, 0xae, 0x69, 0xe8, 0x6d, 0x37, 0x98, 0x5c, 0x41, 0x5c, 0x6c, + 0xa2, 0xd3, 0x34, 0xb2, 0x72, 0x7c, 0x3e, 0x4c, 0xb7, 0xe1, 0xa6, 0xdb, 0x64, 0xe7, 0xcd, 0xa3, + 0x64, 0x08, 0x21, 0x8a, 0x77, 0x5b, 0xd3, 0x9e, 0x6f, 0x59, 0x23, 0xb7, 0x17, 0xcf, 0xec, 0x20, + 0xfd, 0x6a, 0x2f, 0x57, 0x93, 0x6b, 0x38, 0xcc, 0xd7, 0x41, 0x73, 0xbb, 0x1c, 0xfc, 0x6f, 0xb3, + 0xfd, 0x11, 0xf3, 0x9d, 0xb3, 0xc9, 0x08, 0xc2, 0xbb, 0xca, 0x39, 0x86, 0xe8, 0x65, 0xfa, 0x30, + 0x9d, 0xbd, 0x4e, 0x07, 0x07, 0xa4, 0x0f, 0xc1, 0xfd, 0xec, 0xf9, 0xe9, 0x71, 0xd0, 0xba, 0x89, + 0x16, 0x81, 0xf7, 0x78, 0x0b, 0xfd, 0xd3, 0xb8, 0xf8, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xb4, 0xec, + 0x4d, 0xaf, 0x37, 0x02, 0x00, 0x00, } diff --git a/pkg/proto/hapi/version/version.pb.go b/pkg/proto/hapi/version/version.pb.go index ef08a6b64..ffb33773a 100644 --- a/pkg/proto/hapi/version/version.pb.go +++ b/pkg/proto/hapi/version/version.pb.go @@ -27,14 +27,11 @@ var _ = math.Inf const _ = proto.ProtoPackageIsVersion1 type Version struct { - Major uint32 `protobuf:"varint,1,opt,name=major" json:"major,omitempty"` - Minor uint32 `protobuf:"varint,2,opt,name=minor" json:"minor,omitempty"` - Patch uint32 `protobuf:"varint,3,opt,name=patch" json:"patch,omitempty"` - PreRelease string `protobuf:"bytes,4,opt,name=pre_release,json=preRelease" json:"pre_release,omitempty"` - BuildMetadata string `protobuf:"bytes,5,opt,name=build_metadata,json=buildMetadata" json:"build_metadata,omitempty"` - GitVersion string `protobuf:"bytes,6,opt,name=git_version,json=gitVersion" json:"git_version,omitempty"` - GitCommit string `protobuf:"bytes,7,opt,name=git_commit,json=gitCommit" json:"git_commit,omitempty"` - GitTreeState string `protobuf:"bytes,8,opt,name=git_tree_state,json=gitTreeState" json:"git_tree_state,omitempty"` + // Sem ver string for the version + SemVer string `protobuf:"bytes,1,opt,name=sem_ver,json=semVer" json:"sem_ver,omitempty"` + GitVersion string `protobuf:"bytes,6,opt,name=git_version,json=gitVersion" json:"git_version,omitempty"` + GitCommit string `protobuf:"bytes,7,opt,name=git_commit,json=gitCommit" json:"git_commit,omitempty"` + GitTreeState string `protobuf:"bytes,8,opt,name=git_tree_state,json=gitTreeState" json:"git_tree_state,omitempty"` } func (m *Version) Reset() { *m = Version{} } @@ -47,20 +44,16 @@ func init() { } var fileDescriptor0 = []byte{ - // 225 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x34, 0xd0, 0xb1, 0x4e, 0xc3, 0x30, - 0x10, 0x06, 0x60, 0x05, 0x68, 0x43, 0x8e, 0xb6, 0x83, 0xc5, 0x60, 0x21, 0x21, 0x10, 0x02, 0x89, - 0xa9, 0x0c, 0xbc, 0x01, 0xcc, 0x2c, 0x01, 0x31, 0xb0, 0x58, 0xd7, 0xf6, 0x44, 0x8d, 0xea, 0xd8, - 0x72, 0x0e, 0xde, 0x9d, 0xad, 0xf6, 0xd9, 0x99, 0x92, 0xff, 0xfb, 0x4f, 0x97, 0x53, 0xe0, 0x6a, - 0x8f, 0xc1, 0x3e, 0xfd, 0x51, 0x1c, 0xad, 0x1f, 0xa6, 0xe7, 0x3a, 0x44, 0xcf, 0x5e, 0x2d, 0x72, - 0xb7, 0xae, 0x76, 0xf7, 0xdf, 0x40, 0xfb, 0x59, 0xde, 0xd5, 0x25, 0xcc, 0x1c, 0xfe, 0xf8, 0xa8, - 0x9b, 0xdb, 0xe6, 0x71, 0xd9, 0x97, 0x20, 0x6a, 0x87, 0xa4, 0x27, 0x55, 0x73, 0xc8, 0x1a, 0x90, - 0xb7, 0x7b, 0x7d, 0x5a, 0x54, 0x82, 0xba, 0x81, 0x8b, 0x10, 0xc9, 0x44, 0x3a, 0x10, 0x8e, 0xa4, - 0xcf, 0x52, 0xd7, 0xf5, 0x90, 0xa8, 0x2f, 0xa2, 0x1e, 0x60, 0xb5, 0xf9, 0xb5, 0x87, 0x9d, 0x71, - 0xc4, 0xb8, 0x43, 0x46, 0x3d, 0x93, 0x99, 0xa5, 0xe8, 0x5b, 0xc5, 0xbc, 0xe7, 0xdb, 0xb2, 0xa9, - 0x47, 0xea, 0x79, 0xd9, 0x93, 0x68, 0x3a, 0xf5, 0x1a, 0x72, 0x32, 0x5b, 0xef, 0x9c, 0x65, 0xdd, - 0x4a, 0xdf, 0x25, 0x79, 0x15, 0x50, 0xf7, 0xb0, 0xca, 0x35, 0x47, 0x22, 0x33, 0x32, 0x32, 0xe9, - 0x73, 0x19, 0x59, 0x24, 0xfd, 0x48, 0xf8, 0x9e, 0xed, 0xa5, 0xfb, 0x6a, 0xeb, 0x17, 0x36, 0x73, - 0xf9, 0x37, 0xcf, 0xc7, 0x00, 0x00, 0x00, 0xff, 0xff, 0x87, 0xdf, 0xc1, 0x73, 0x39, 0x01, 0x00, + // 161 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xca, 0x48, 0x2c, 0xc8, + 0xd4, 0x2f, 0x4b, 0x2d, 0x2a, 0xce, 0xcc, 0xcf, 0x83, 0xd1, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, + 0x42, 0x3c, 0x20, 0x39, 0x3d, 0xa8, 0x98, 0x52, 0x07, 0x23, 0x17, 0x7b, 0x18, 0x84, 0x2d, 0x24, + 0xce, 0xc5, 0x5e, 0x9c, 0x9a, 0x1b, 0x0f, 0x94, 0x92, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x62, + 0x03, 0x72, 0x81, 0x92, 0x42, 0xf2, 0x5c, 0xdc, 0xe9, 0x99, 0x25, 0xf1, 0x50, 0x3d, 0x12, 0x6c, + 0x60, 0x49, 0x2e, 0xa0, 0x10, 0x4c, 0xa7, 0x2c, 0x17, 0x88, 0x17, 0x9f, 0x9c, 0x9f, 0x9b, 0x9b, + 0x59, 0x22, 0xc1, 0x0e, 0x96, 0xe7, 0x04, 0x8a, 0x38, 0x83, 0x05, 0x84, 0x54, 0xb8, 0xf8, 0x40, + 0xd2, 0x25, 0x45, 0xa9, 0xa9, 0xf1, 0xc5, 0x25, 0x89, 0x25, 0xa9, 0x12, 0x1c, 0x60, 0x25, 0x3c, + 0x40, 0xd1, 0x10, 0xa0, 0x60, 0x30, 0x48, 0xcc, 0x89, 0x33, 0x8a, 0x1d, 0x6a, 0x43, 0x12, 0x1b, + 0xd8, 0xa9, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x68, 0x45, 0xe1, 0x46, 0xc8, 0x00, 0x00, 0x00, } diff --git a/pkg/version/version.go b/pkg/version/version.go index 533a639ba..ba36c3d4c 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -18,8 +18,6 @@ limitations under the License. package version // import "k8s.io/helm/pkg/version" import ( - "fmt" - "k8s.io/helm/pkg/proto/hapi/version" ) @@ -33,35 +31,23 @@ import ( // // BuildMetadata gets filled in during build, do not touch // GitCommit gets filled in during build, do not touch -var ( - Major uint32 = 2 - Minor uint32 = 0 - Patch uint32 = 0 - PreRelease = "alpha.4" - BuildMetadata = "" - GitCommit = "" -) +var Version = "v2.0.0-alpha.4" +var BuildMetadata = "" +var GitCommit = "" // GetVersion returns the semver string of the version + func GetVersion() string { - version := fmt.Sprintf("v%d.%d.%d", Major, Minor, Patch) - if PreRelease != "" { - version = version + "-" + PreRelease - } - if BuildMetadata != "" { - version = version + "+" + BuildMetadata + if BuildMetadata == "" { + return Version } - return version + return Version + "+" + BuildMetadata } // GetVersionProto returns protobuf representing the version func GetVersionProto() version.Version { return version.Version{ - Major: Major, - Minor: Minor, - Patch: Patch, - PreRelease: PreRelease, - BuildMetadata: BuildMetadata, - GitCommit: GitCommit, + SemVer: GetVersion(), + GitCommit: GitCommit, } } diff --git a/versioning.mk b/versioning.mk index 2c6e1b4b5..1245764ca 100644 --- a/versioning.mk +++ b/versioning.mk @@ -15,7 +15,7 @@ BINARY_VERSION ?= ${GIT_TAG}-${GIT_SHA} IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${DOCKER_VERSION} MUTABLE_IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${MUTABLE_VERSION} -LDFLAGS += -X k8s.io/helm/pkg/version.BuildMetadata=${BINARY_VERSION} +LDFLAGS += -X k8s.io/helm/pkg/version.BuildMetadata=${GIT_SHA} LDFLAGS += -X k8s.io/helm/pkg/version.GitCommit=${GIT_COMMIT} DOCKER_PUSH = docker push From 4a7352cdc06bf22e196e95e84ac99c3a71a1d825 Mon Sep 17 00:00:00 2001 From: vaikas-google Date: Mon, 12 Sep 2016 22:55:20 -0700 Subject: [PATCH 088/183] adjust proto field numbers --- _proto/hapi/version/version.proto | 6 +++--- pkg/proto/hapi/version/version.pb.go | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/_proto/hapi/version/version.proto b/_proto/hapi/version/version.proto index 092d23c7a..9d1b2ecf5 100644 --- a/_proto/hapi/version/version.proto +++ b/_proto/hapi/version/version.proto @@ -21,7 +21,7 @@ option go_package = "version"; message Version { // Sem ver string for the version string sem_ver = 1; - string git_version = 6; - string git_commit = 7; - string git_tree_state = 8; + string git_version = 2; + string git_commit = 3; + string git_tree_state = 4; } diff --git a/pkg/proto/hapi/version/version.pb.go b/pkg/proto/hapi/version/version.pb.go index ffb33773a..0209ac7a7 100644 --- a/pkg/proto/hapi/version/version.pb.go +++ b/pkg/proto/hapi/version/version.pb.go @@ -29,9 +29,9 @@ const _ = proto.ProtoPackageIsVersion1 type Version struct { // Sem ver string for the version SemVer string `protobuf:"bytes,1,opt,name=sem_ver,json=semVer" json:"sem_ver,omitempty"` - GitVersion string `protobuf:"bytes,6,opt,name=git_version,json=gitVersion" json:"git_version,omitempty"` - GitCommit string `protobuf:"bytes,7,opt,name=git_commit,json=gitCommit" json:"git_commit,omitempty"` - GitTreeState string `protobuf:"bytes,8,opt,name=git_tree_state,json=gitTreeState" json:"git_tree_state,omitempty"` + GitVersion string `protobuf:"bytes,2,opt,name=git_version,json=gitVersion" json:"git_version,omitempty"` + GitCommit string `protobuf:"bytes,3,opt,name=git_commit,json=gitCommit" json:"git_commit,omitempty"` + GitTreeState string `protobuf:"bytes,4,opt,name=git_tree_state,json=gitTreeState" json:"git_tree_state,omitempty"` } func (m *Version) Reset() { *m = Version{} } @@ -49,11 +49,11 @@ var fileDescriptor0 = []byte{ 0xd4, 0x2f, 0x4b, 0x2d, 0x2a, 0xce, 0xcc, 0xcf, 0x83, 0xd1, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x3c, 0x20, 0x39, 0x3d, 0xa8, 0x98, 0x52, 0x07, 0x23, 0x17, 0x7b, 0x18, 0x84, 0x2d, 0x24, 0xce, 0xc5, 0x5e, 0x9c, 0x9a, 0x1b, 0x0f, 0x94, 0x92, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x62, - 0x03, 0x72, 0x81, 0x92, 0x42, 0xf2, 0x5c, 0xdc, 0xe9, 0x99, 0x25, 0xf1, 0x50, 0x3d, 0x12, 0x6c, + 0x03, 0x72, 0x81, 0x92, 0x42, 0xf2, 0x5c, 0xdc, 0xe9, 0x99, 0x25, 0xf1, 0x50, 0x3d, 0x12, 0x4c, 0x60, 0x49, 0x2e, 0xa0, 0x10, 0x4c, 0xa7, 0x2c, 0x17, 0x88, 0x17, 0x9f, 0x9c, 0x9f, 0x9b, 0x9b, - 0x59, 0x22, 0xc1, 0x0e, 0x96, 0xe7, 0x04, 0x8a, 0x38, 0x83, 0x05, 0x84, 0x54, 0xb8, 0xf8, 0x40, - 0xd2, 0x25, 0x45, 0xa9, 0xa9, 0xf1, 0xc5, 0x25, 0x89, 0x25, 0xa9, 0x12, 0x1c, 0x60, 0x25, 0x3c, + 0x59, 0x22, 0xc1, 0x0c, 0x96, 0xe7, 0x04, 0x8a, 0x38, 0x83, 0x05, 0x84, 0x54, 0xb8, 0xf8, 0x40, + 0xd2, 0x25, 0x45, 0xa9, 0xa9, 0xf1, 0xc5, 0x25, 0x89, 0x25, 0xa9, 0x12, 0x2c, 0x60, 0x25, 0x3c, 0x40, 0xd1, 0x10, 0xa0, 0x60, 0x30, 0x48, 0xcc, 0x89, 0x33, 0x8a, 0x1d, 0x6a, 0x43, 0x12, 0x1b, - 0xd8, 0xa9, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x68, 0x45, 0xe1, 0x46, 0xc8, 0x00, 0x00, + 0xd8, 0xa9, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x99, 0xb2, 0xdd, 0x0f, 0xc8, 0x00, 0x00, 0x00, } From e6a1676491a831e951f4fc725a07e57f39efc3af Mon Sep 17 00:00:00 2001 From: Jonathan Boulle Date: Tue, 13 Sep 2016 16:22:57 +0200 Subject: [PATCH 089/183] README: fix small typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d161d213..6af68fc92 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Download a [release tarball of helm for your platform](https://github.com/kubern ## Docs - [Quick Start](docs/quickstart.md) -- [Architechture](docs/architecture.md) +- [Architecture](docs/architecture.md) - [Charts](docs/charts.md) - [Chart Repository Guide](docs/chart_repository.md) - [Syncing your Chart Repository](docs/chart_repository_sync_example.md) From a1a2578ff059e012cbccf5336b2c65658d2db536 Mon Sep 17 00:00:00 2001 From: Brandon Philips Date: Tue, 13 Sep 2016 09:36:41 -0700 Subject: [PATCH 090/183] feat(README): call out the code of conduct Lifted from https://raw.githubusercontent.com/kubernetes/kubernetes-template-project/master/README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index c69831922..3f65755df 100644 --- a/README.md +++ b/README.md @@ -48,3 +48,7 @@ You can reach the Helm community and developers via the following channels: - [Kubernetes Slack](https://slack.k8s.io): #helm - Mailing List: https://groups.google.com/forum/#!forum/kubernetes-sig-apps - Developer Call: Thursdays at 9:30-10:00 Pacific. https://engineyard.zoom.us/j/366425549 + +### Code of conduct + +Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). From bcb1cc65bb385b181a510fe58fc4831c6d0c46c7 Mon Sep 17 00:00:00 2001 From: Brandon Philips Date: Tue, 13 Sep 2016 09:37:19 -0700 Subject: [PATCH 091/183] feat(README): add the history doc link After d1488f1d9448afc55b20653ebdf26d7b8a9e9cad I forgot to add this to the TOC. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3f65755df..a742816d2 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Download a [release tarball of helm for your platform](https://github.com/kubern - [Chart Repository Guide](docs/chart_repository.md) - [Syncing your Chart Repository](docs/chart_repository_sync_example.md) - [Developers](docs/developers.md) +- [History](docs/history.md) ## Community, discussion, contribution, and support From 1904bb35ba08bfe26399c423cc3cd51cfbab643f Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 13 Sep 2016 10:14:52 -0700 Subject: [PATCH 092/183] fix(ci): fix typo in auth file reference --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 182e2f238..36f1232de 100644 --- a/circle.yml +++ b/circle.yml @@ -44,7 +44,7 @@ deployment: # setup gcloud tools - sudo /opt/google-cloud-sdk/bin/gcloud --quiet components update - echo "${GCLOUD_SERVICE_KEY}" | base64 --decode > "${HOME}/gcloud-service-key.json" - - sudo /opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file "${HOME}/glcoud-service-key.json" + - sudo /opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file "${HOME}/gcloud-service-key.json" - sudo /opt/google-cloud-sdk/bin/gcloud config set project "${PROJECT_NAME}" - docker login -e 1234@5678.com -u _json_key -p "$(cat ${HOME}/gcloud-service-key.json)" https://gcr.io From 55dc7b690f1c49b7fea409bf94ab523a732a6350 Mon Sep 17 00:00:00 2001 From: "Keerthan Reddy Mala (kmala)" Date: Tue, 13 Sep 2016 19:47:43 -0600 Subject: [PATCH 093/183] fix(tiller): Upgrade shouldn't fail if there are no changes Signed-off-by: Keerthan Reddy Mala (kmala) --- cmd/tiller/release_server_test.go | 18 ++++++++++++++++++ pkg/kube/client.go | 19 ++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index cfeb5d9eb..594bd865d 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -582,6 +582,24 @@ func TestUpdateReleaseNoHooks(t *testing.T) { } +func TestUpdateReleaseNoChanges(t *testing.T) { + c := context.Background() + rs := rsFixture() + rel := releaseStub() + rs.env.Releases.Create(rel) + + req := &services.UpdateReleaseRequest{ + Name: rel.Name, + DisableHooks: true, + Chart: rel.GetChart(), + } + + _, err := rs.UpdateRelease(c, req) + if err != nil { + t.Fatalf("Failed updated: %s", err) + } +} + func TestUninstallRelease(t *testing.T) { c := context.Background() rs := rsFixture() diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 0a0f459ba..39a90aba5 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -56,6 +56,15 @@ func New(config clientcmd.ClientConfig) *Client { // ResourceActorFunc performs an action on a single resource. type ResourceActorFunc func(*resource.Info) error +// ErrAlreadyExists can be returned where there are no changes +type ErrAlreadyExists struct { + errorMsg string +} + +func (e ErrAlreadyExists) Error() string { + return fmt.Sprintf("Looks like there are no changes for %s", e.errorMsg) +} + // APIClient returns a Kubernetes API client. // // This is necessary because cmdutil.Client is a field, not a method, which @@ -189,8 +198,12 @@ func (c *Client) Update(namespace string, currentReader, targetReader io.Reader) } if err := updateResource(info, currentObj); err != nil { - log.Printf("error updating the resource %s:\n\t %v", resourceName, err) - updateErrors = append(updateErrors, err.Error()) + if alreadyExistErr, ok := err.(ErrAlreadyExists); ok { + log.Printf(alreadyExistErr.errorMsg) + } else { + log.Printf("error updating the resource %s:\n\t %v", resourceName, err) + updateErrors = append(updateErrors, err.Error()) + } } return nil @@ -330,7 +343,7 @@ func updateResource(target *resource.Info, currentObj runtime.Object) error { } if reflect.DeepEqual(originalJS, editedJS) { - return fmt.Errorf("Looks like there are no changes for %s", target.Name) + return ErrAlreadyExists{target.Name} } patch, err := strategicpatch.CreateStrategicMergePatch(originalJS, editedJS, currentObj) From 09e6af461ffe7ccfbe2ed97ff9df5f4075b268a2 Mon Sep 17 00:00:00 2001 From: Ville Aikas Date: Tue, 13 Sep 2016 22:37:28 -0700 Subject: [PATCH 094/183] clean up the formatting, remove unnecessary git_version from version as per discussions with @areese --- _proto/hapi/version/version.proto | 5 ++--- cmd/helm/version.go | 5 +++-- pkg/proto/hapi/version/version.pb.go | 23 ++++++++++------------- versioning.mk | 2 +- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/_proto/hapi/version/version.proto b/_proto/hapi/version/version.proto index 9d1b2ecf5..0ae0985b7 100644 --- a/_proto/hapi/version/version.proto +++ b/_proto/hapi/version/version.proto @@ -21,7 +21,6 @@ option go_package = "version"; message Version { // Sem ver string for the version string sem_ver = 1; - string git_version = 2; - string git_commit = 3; - string git_tree_state = 4; + string git_commit = 2; + string git_tree_state = 3; } diff --git a/cmd/helm/version.go b/cmd/helm/version.go index 58dbcc97d..9c66cea00 100644 --- a/cmd/helm/version.go +++ b/cmd/helm/version.go @@ -51,12 +51,13 @@ func newVersionCmd(c helm.Interface, out io.Writer) *cobra.Command { func (v *versionCmd) run() error { // Regardless of whether we can talk to server or not, just print the client // version. - fmt.Printf("%+v\n", version.GetVersionProto()) + cv := version.GetVersionProto() + fmt.Fprintf(v.out, "Client: {SemVer: %s GitCommit: %s}\n", cv.SemVer, cv.GitCommit) resp, err := v.client.GetVersion() if err != nil { return err } - fmt.Printf("%+v\n", resp.Version) + fmt.Fprintf(v.out, "Server: {SemVer: %s GitCommit: %s}\n", resp.Version.SemVer, resp.Version.GitCommit) return nil } diff --git a/pkg/proto/hapi/version/version.pb.go b/pkg/proto/hapi/version/version.pb.go index 0209ac7a7..7d69bf03d 100644 --- a/pkg/proto/hapi/version/version.pb.go +++ b/pkg/proto/hapi/version/version.pb.go @@ -29,9 +29,8 @@ const _ = proto.ProtoPackageIsVersion1 type Version struct { // Sem ver string for the version SemVer string `protobuf:"bytes,1,opt,name=sem_ver,json=semVer" json:"sem_ver,omitempty"` - GitVersion string `protobuf:"bytes,2,opt,name=git_version,json=gitVersion" json:"git_version,omitempty"` - GitCommit string `protobuf:"bytes,3,opt,name=git_commit,json=gitCommit" json:"git_commit,omitempty"` - GitTreeState string `protobuf:"bytes,4,opt,name=git_tree_state,json=gitTreeState" json:"git_tree_state,omitempty"` + GitCommit string `protobuf:"bytes,2,opt,name=git_commit,json=gitCommit" json:"git_commit,omitempty"` + GitTreeState string `protobuf:"bytes,3,opt,name=git_tree_state,json=gitTreeState" json:"git_tree_state,omitempty"` } func (m *Version) Reset() { *m = Version{} } @@ -44,16 +43,14 @@ func init() { } var fileDescriptor0 = []byte{ - // 161 bytes of a gzipped FileDescriptorProto + // 144 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xca, 0x48, 0x2c, 0xc8, 0xd4, 0x2f, 0x4b, 0x2d, 0x2a, 0xce, 0xcc, 0xcf, 0x83, 0xd1, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, - 0x42, 0x3c, 0x20, 0x39, 0x3d, 0xa8, 0x98, 0x52, 0x07, 0x23, 0x17, 0x7b, 0x18, 0x84, 0x2d, 0x24, - 0xce, 0xc5, 0x5e, 0x9c, 0x9a, 0x1b, 0x0f, 0x94, 0x92, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x62, - 0x03, 0x72, 0x81, 0x92, 0x42, 0xf2, 0x5c, 0xdc, 0xe9, 0x99, 0x25, 0xf1, 0x50, 0x3d, 0x12, 0x4c, - 0x60, 0x49, 0x2e, 0xa0, 0x10, 0x4c, 0xa7, 0x2c, 0x17, 0x88, 0x17, 0x9f, 0x9c, 0x9f, 0x9b, 0x9b, - 0x59, 0x22, 0xc1, 0x0c, 0x96, 0xe7, 0x04, 0x8a, 0x38, 0x83, 0x05, 0x84, 0x54, 0xb8, 0xf8, 0x40, - 0xd2, 0x25, 0x45, 0xa9, 0xa9, 0xf1, 0xc5, 0x25, 0x89, 0x25, 0xa9, 0x12, 0x2c, 0x60, 0x25, 0x3c, - 0x40, 0xd1, 0x10, 0xa0, 0x60, 0x30, 0x48, 0xcc, 0x89, 0x33, 0x8a, 0x1d, 0x6a, 0x43, 0x12, 0x1b, - 0xd8, 0xa9, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x99, 0xb2, 0xdd, 0x0f, 0xc8, 0x00, 0x00, - 0x00, + 0x42, 0x3c, 0x20, 0x39, 0x3d, 0xa8, 0x98, 0x52, 0x3a, 0x17, 0x7b, 0x18, 0x84, 0x29, 0x24, 0xce, + 0xc5, 0x5e, 0x9c, 0x9a, 0x1b, 0x0f, 0x94, 0x91, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x62, 0x03, + 0x72, 0x81, 0x92, 0x42, 0xb2, 0x5c, 0x5c, 0xe9, 0x99, 0x25, 0xf1, 0xc9, 0xf9, 0xb9, 0xb9, 0x99, + 0x25, 0x12, 0x4c, 0x60, 0x39, 0x4e, 0xa0, 0x88, 0x33, 0x58, 0x40, 0x48, 0x85, 0x8b, 0x0f, 0x24, + 0x5d, 0x52, 0x94, 0x9a, 0x1a, 0x5f, 0x5c, 0x92, 0x58, 0x92, 0x2a, 0xc1, 0x0c, 0x56, 0xc2, 0x03, + 0x14, 0x0d, 0x01, 0x0a, 0x06, 0x83, 0xc4, 0x9c, 0x38, 0xa3, 0xd8, 0xa1, 0x76, 0x26, 0xb1, 0x81, + 0x1d, 0x62, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x20, 0xcc, 0x0e, 0x1b, 0xa6, 0x00, 0x00, 0x00, } diff --git a/versioning.mk b/versioning.mk index 1245764ca..94f3ceaa8 100644 --- a/versioning.mk +++ b/versioning.mk @@ -15,7 +15,7 @@ BINARY_VERSION ?= ${GIT_TAG}-${GIT_SHA} IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${DOCKER_VERSION} MUTABLE_IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${MUTABLE_VERSION} -LDFLAGS += -X k8s.io/helm/pkg/version.BuildMetadata=${GIT_SHA} +LDFLAGS += -X k8s.io/helm/pkg/version.SemVer=${GIT_TAG} LDFLAGS += -X k8s.io/helm/pkg/version.GitCommit=${GIT_COMMIT} DOCKER_PUSH = docker push From 264c6b10e1ebdcaed8e83f86cd85c20589961839 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Wed, 14 Sep 2016 18:06:21 -0700 Subject: [PATCH 095/183] feat(*): add git tree state to binaries * clean up version output --- cmd/helm/version.go | 4 ++-- cmd/tiller/release_server.go | 2 +- pkg/helm/client.go | 2 +- pkg/version/version.go | 44 +++++++++++++++++++----------------- versioning.mk | 4 +++- 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/cmd/helm/version.go b/cmd/helm/version.go index 9c66cea00..e22d0a800 100644 --- a/cmd/helm/version.go +++ b/cmd/helm/version.go @@ -52,12 +52,12 @@ func (v *versionCmd) run() error { // Regardless of whether we can talk to server or not, just print the client // version. cv := version.GetVersionProto() - fmt.Fprintf(v.out, "Client: {SemVer: %s GitCommit: %s}\n", cv.SemVer, cv.GitCommit) + fmt.Fprintf(v.out, "Client: %#v\n", cv) resp, err := v.client.GetVersion() if err != nil { return err } - fmt.Fprintf(v.out, "Server: {SemVer: %s GitCommit: %s}\n", resp.Version.SemVer, resp.Version.GitCommit) + fmt.Fprintf(v.out, "Server: %#v\n", resp.Version) return nil } diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 678570841..3d8267715 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -178,7 +178,7 @@ func filterReleases(filter string, rels []*release.Release) ([]*release.Release, func (s *releaseServer) GetVersion(c ctx.Context, req *services.GetVersionRequest) (*services.GetVersionResponse, error) { v := version.GetVersionProto() - return &services.GetVersionResponse{Version: &v}, nil + return &services.GetVersionResponse{Version: v}, nil } func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetReleaseStatusRequest) (*services.GetReleaseStatusResponse, error) { diff --git a/pkg/helm/client.go b/pkg/helm/client.go index ce08bd6b1..bf55562a0 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -120,7 +120,7 @@ func (h *Client) UpdateRelease(rlsName string, chStr string, opts ...UpdateOptio return h.opts.rpcUpdateRelease(rlsName, chart, rls.NewReleaseServiceClient(c), opts...) } -// Version returns the server version +// GetVersion returns the server version // // Note: there aren't currently any supported StatusOptions, // but they are kept in the API signature as a placeholder for future additions. diff --git a/pkg/version/version.go b/pkg/version/version.go index ba36c3d4c..eb96bd7c8 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -17,26 +17,27 @@ limitations under the License. // Package version represents the current version of the project. package version // import "k8s.io/helm/pkg/version" -import ( - "k8s.io/helm/pkg/proto/hapi/version" +import "k8s.io/helm/pkg/proto/hapi/version" + +var ( + // Version is the current version of the Helm. + // Update this whenever making a new release. + // The version is of the format Major.Minor.Patch[-Prerelease][+BuildMetadata] + // + // Increment major number for new feature additions and behavioral changes. + // Increment minor number for bug fixes and performance enhancements. + // Increment patch number for critical fixes to existing releases. + Version = "v2.0.0-alpha.4" + + // BuildMetadata is extra build time data + BuildMetadata = "" + // GitCommit is the git sha1 + GitCommit = "" + // GitTreeState is the state of the git tree + GitTreeState = "" ) -// Version is the current version of the Helm. -// Update this whenever making a new release. -// The version is of the format Major.Minor.Patch[-Prerelease][+BuildMetadata] -// -// Increment major number for new feature additions and behavioral changes. -// Increment minor number for bug fixes and performance enhancements. -// Increment patch number for critical fixes to existing releases. -// -// BuildMetadata gets filled in during build, do not touch -// GitCommit gets filled in during build, do not touch -var Version = "v2.0.0-alpha.4" -var BuildMetadata = "" -var GitCommit = "" - // GetVersion returns the semver string of the version - func GetVersion() string { if BuildMetadata == "" { return Version @@ -45,9 +46,10 @@ func GetVersion() string { } // GetVersionProto returns protobuf representing the version -func GetVersionProto() version.Version { - return version.Version{ - SemVer: GetVersion(), - GitCommit: GitCommit, +func GetVersionProto() *version.Version { + return &version.Version{ + SemVer: GetVersion(), + GitCommit: GitCommit, + GitTreeState: GitTreeState, } } diff --git a/versioning.mk b/versioning.mk index 94f3ceaa8..4825c4e72 100644 --- a/versioning.mk +++ b/versioning.mk @@ -3,6 +3,7 @@ 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") ifdef VERSION DOCKER_VERSION = $(VERSION) @@ -15,8 +16,9 @@ BINARY_VERSION ?= ${GIT_TAG}-${GIT_SHA} IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${DOCKER_VERSION} MUTABLE_IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${MUTABLE_VERSION} -LDFLAGS += -X k8s.io/helm/pkg/version.SemVer=${GIT_TAG} +LDFLAGS += -X k8s.io/helm/pkg/version.Version=${GIT_TAG} LDFLAGS += -X k8s.io/helm/pkg/version.GitCommit=${GIT_COMMIT} +LDFLAGS += -X k8s.io/helm/pkg/version.GitTreeState=${GIT_DIRTY} DOCKER_PUSH = docker push ifeq ($(DOCKER_REGISTRY),gcr.io) From 440e5489011089a1df778b762e129e1694551556 Mon Sep 17 00:00:00 2001 From: Brandon Philips Date: Wed, 14 Sep 2016 20:06:44 -0700 Subject: [PATCH 096/183] feat(repo): use OCI style digest identifiers Use the same format as the Open Container Initiative for a digest string. https://github.com/opencontainers/image-spec/blob/master/descriptor.md#digests-and-verification Fixes #1166 --- docs/chart_repository.md | 4 ++-- pkg/repo/index.go | 2 +- pkg/repo/repo.go | 22 ++++++++++------------ pkg/repo/repo_test.go | 4 ++-- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/docs/chart_repository.md b/docs/chart_repository.md index 0870a95e2..9e1748853 100644 --- a/docs/chart_repository.md +++ b/docs/chart_repository.md @@ -23,7 +23,7 @@ alpine-0.1.0: name: alpine url: https://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz created: 2016-05-26 11:23:44.086354411 +0000 UTC - checksum: a61575c2d3160e5e39abf2a5ec984d6119404b18 + digest: sha256:78e9a4282295184e8ce1496d23987993673f38e33e203c8bc18bc838a73e5864 chartfile: name: alpine description: Deploy a basic Alpine Linux pod @@ -33,7 +33,7 @@ redis-2.0.0: name: redis url: https://storage.googleapis.com/kubernetes-charts/redis-2.0.0.tgz created: 2016-05-26 11:23:44.087939192 +0000 UTC - checksum: 2cea3048cf85d588204e1b1cc0674472b4517919 + digest: sha256:bde9c2949e64d059c18d8f93566a64dafc6d2e8e259a70322fb804831dfd0b5b chartfile: name: redis description: Port of the replicatedservice template from kubernetes/charts diff --git a/pkg/repo/index.go b/pkg/repo/index.go index c0187fc69..5e9a08837 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -39,7 +39,7 @@ type ChartRef struct { URL string `yaml:"url"` Created string `yaml:"created,omitempty"` Removed bool `yaml:"removed,omitempty"` - Checksum string `yaml:"checksum,omitempty"` + Digest string `yaml:"digest,omitempty"` Chartfile *chart.Metadata `yaml:"chartfile"` } diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go index c4ed13c52..e670bea44 100644 --- a/pkg/repo/repo.go +++ b/pkg/repo/repo.go @@ -17,9 +17,10 @@ limitations under the License. package repo // import "k8s.io/helm/pkg/repo" import ( - "crypto/sha1" + "crypto/sha256" + "encoding/hex" "errors" - "fmt" + "io" "io/ioutil" "net/url" "os" @@ -131,7 +132,7 @@ func (r *ChartRepository) Index() error { } chartfile := ch.Metadata - hash, err := generateChecksum(path) + digest, err := generateDigest(path) if err != nil { return err } @@ -152,7 +153,7 @@ func (r *ChartRepository) Index() error { url, _ := url.Parse(r.URL) url.Path = filepath.Join(url.Path, key+".tgz") - entry := &ChartRef{Chartfile: chartfile, Name: chartfile.Name, URL: url.String(), Created: created, Checksum: hash, Removed: false} + entry := &ChartRef{Chartfile: chartfile, Name: chartfile.Name, URL: url.String(), Created: created, Digest: digest, Removed: false} r.IndexFile.Entries[key] = entry @@ -170,18 +171,15 @@ func (r *ChartRepository) Index() error { return r.saveIndexFile() } -func generateChecksum(path string) (string, error) { +func generateDigest(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } - b, err := ioutil.ReadAll(f) - if err != nil { - return "", err - } - - result := sha1.Sum(b) + h := sha256.New() + io.Copy(h, f) - return fmt.Sprintf("%x", result), nil + digest := h.Sum([]byte{}) + return "sha256:" + hex.EncodeToString(digest[:]), nil } diff --git a/pkg/repo/repo_test.go b/pkg/repo/repo_test.go index 9b88e238a..651f7fcae 100644 --- a/pkg/repo/repo_test.go +++ b/pkg/repo/repo_test.go @@ -85,8 +85,8 @@ func TestIndex(t *testing.T) { } timestamps[chartName] = details.Created - if details.Checksum == "" { - t.Errorf("Checksum was not set for %s", chartName) + if details.Digest == "" { + t.Errorf("Digest was not set for %s", chartName) } } From f77435e686e8ea0b7257d882120cf5ffe0133e4a Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Thu, 15 Sep 2016 15:45:37 -0600 Subject: [PATCH 097/183] fix(helm-list): only list configmaps owned by TILLER --- pkg/storage/driver/cfgmaps.go | 5 ++++- pkg/storage/driver/records.go | 1 + pkg/storage/storage.go | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index 30b716b6c..f33142729 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -84,7 +84,10 @@ func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { // that filter(release) == true. An error is returned if the // configmap fails to retrieve the releases. func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { - list, err := cfgmaps.impl.List(api.ListOptions{}) + lsel := kblabels.Set{"OWNER": "TILLER"}.AsSelector() + opts := api.ListOptions{LabelSelector: lsel} + + list, err := cfgmaps.impl.List(opts) if err != nil { logerrf(err, "list: failed to list") return nil, err diff --git a/pkg/storage/driver/records.go b/pkg/storage/driver/records.go index c8766d87f..e625bb5e1 100644 --- a/pkg/storage/driver/records.go +++ b/pkg/storage/driver/records.go @@ -125,6 +125,7 @@ func newRecord(key string, rls *rspb.Release) *record { lbs.init() lbs.set("NAME", rls.Name) + lbs.set("OWNER", "TILLER") lbs.set("STATUS", rspb.Status_Code_name[int32(rls.Info.Status.Code)]) lbs.set("VERSION", strconv.Itoa(int(rls.Version))) diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 1301cc9f3..ba55062a9 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -113,6 +113,7 @@ func (s *Storage) Deployed(name string) (*rspb.Release, error) { ls, err := s.Driver.Query(map[string]string{ "NAME": name, + "OWNER": "TILLER", "STATUS": "DEPLOYED", }) switch { From a86f304d37a71e1c19feb2ec3d46137846f62a9b Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Thu, 15 Sep 2016 11:01:40 -0500 Subject: [PATCH 098/183] ref(helm): convert repo commands to new format issue #1203 --- cmd/helm/helm.go | 1 + cmd/helm/repo.go | 187 ++------------------ cmd/helm/repo_add.go | 94 ++++++++++ cmd/helm/{repo_test.go => repo_add_test.go} | 36 ---- cmd/helm/repo_index.go | 75 ++++++++ cmd/helm/repo_list.go | 66 +++++++ cmd/helm/repo_remove.go | 97 ++++++++++ cmd/helm/repo_remove_test.go | 60 +++++++ 8 files changed, 412 insertions(+), 204 deletions(-) create mode 100644 cmd/helm/repo_add.go rename cmd/helm/{repo_test.go => repo_add_test.go} (65%) create mode 100644 cmd/helm/repo_index.go create mode 100644 cmd/helm/repo_list.go create mode 100644 cmd/helm/repo_remove.go create mode 100644 cmd/helm/repo_remove_test.go diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 78e058b02..5bb4603cf 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -99,6 +99,7 @@ func newRootCmd(out io.Writer) *cobra.Command { newVerifyCmd(out), newUpdateCmd(out), newVersionCmd(nil, out), + newRepoCmd(out), ) return cmd } diff --git a/cmd/helm/repo.go b/cmd/helm/repo.go index 65f273db3..31249c7fa 100644 --- a/cmd/helm/repo.go +++ b/cmd/helm/repo.go @@ -17,183 +17,34 @@ limitations under the License. package main import ( - "errors" - "fmt" - "io/ioutil" - "os" - "path/filepath" + "io" - "github.com/gosuri/uitable" "github.com/spf13/cobra" - "gopkg.in/yaml.v2" - - "k8s.io/helm/pkg/repo" ) -func init() { - repoCmd.AddCommand(repoAddCmd) - repoCmd.AddCommand(repoListCmd) - repoCmd.AddCommand(repoRemoveCmd) - repoCmd.AddCommand(repoIndexCmd) - RootCommand.AddCommand(repoCmd) -} - -var repoCmd = &cobra.Command{ - Use: "repo add|remove|list [ARG]", - Short: "add, list, or remove chart repositories", -} - -var repoAddCmd = &cobra.Command{ - Use: "add [flags] [NAME] [URL]", - Short: "add a chart repository", - RunE: runRepoAdd, -} - -var repoListCmd = &cobra.Command{ - Use: "list [flags]", - Short: "list chart repositories", - RunE: runRepoList, -} - -var repoRemoveCmd = &cobra.Command{ - Use: "remove [flags] [NAME]", - Aliases: []string{"rm"}, - Short: "remove a chart repository", - RunE: runRepoRemove, -} - -var repoIndexCmd = &cobra.Command{ - Use: "index [flags] [DIR] [REPO_URL]", - Short: "generate an index file for a chart repository given a directory", - RunE: runRepoIndex, -} - -func runRepoAdd(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(len(args), "name for the chart repository", "the url of the chart repository"); err != nil { - return err - } - name, url := args[0], args[1] - - if err := addRepository(name, url); err != nil { - return err - } +var repoHelm = ` +This command consists of multiple subcommands to interact with chart repositories. - fmt.Println(name + " has been added to your repositories") - return nil -} +It can be used to add, remove, list, and index chart repositories. +Example usage: + $ helm repo add [NAME] [REPO_URL] +` -func runRepoList(cmd *cobra.Command, args []string) error { - f, err := repo.LoadRepositoriesFile(repositoriesFile()) - if err != nil { - return err - } - if len(f.Repositories) == 0 { - return errors.New("no repositories to show") - } - table := uitable.New() - table.MaxColWidth = 50 - table.AddRow("NAME", "URL") - for k, v := range f.Repositories { - table.AddRow(k, v) - } - fmt.Println(table) - return nil +type repoCmd struct { + out io.Writer } -func runRepoRemove(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(len(args), "name of chart repository"); err != nil { - return err - } - return removeRepoLine(args[0]) -} - -func runRepoIndex(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return fmt.Errorf("This command needs at minimum 1 argument: a path to a directory") - } - - path, err := filepath.Abs(args[0]) - if err != nil { - return err - } - url := "" - if len(args) == 2 { - url = args[1] - } - - return index(path, url) -} - -func index(dir, url string) error { - chartRepo, err := repo.LoadChartRepository(dir, url) - if err != nil { - return err - } - - return chartRepo.Index() -} - -func addRepository(name, url string) error { - if err := repo.DownloadIndexFile(name, url, cacheIndexFile(name)); err != nil { - return errors.New("Looks like " + url + " is not a valid chart repository or cannot be reached: " + err.Error()) - } - - return insertRepoLine(name, url) -} - -func removeRepoLine(name string) error { - r, err := repo.LoadRepositoriesFile(repositoriesFile()) - if err != nil { - return err - } - - _, ok := r.Repositories[name] - if ok { - delete(r.Repositories, name) - b, err := yaml.Marshal(&r.Repositories) - if err != nil { - return err - } - if err := ioutil.WriteFile(repositoriesFile(), b, 0666); err != nil { - return err - } - if err := removeRepoCache(name); err != nil { - return err - } - - } else { - return fmt.Errorf("The repository, %s, does not exist in your repositories list", name) - } - - return nil -} - -func removeRepoCache(name string) error { - if _, err := os.Stat(cacheIndexFile(name)); err == nil { - err = os.Remove(cacheIndexFile(name)) - if err != nil { - return err - } - } - return nil -} - -func insertRepoLine(name, url string) error { - f, err := repo.LoadRepositoriesFile(repositoriesFile()) - if err != nil { - return err - } - _, ok := f.Repositories[name] - if ok { - return fmt.Errorf("The repository name you provided (%s) already exists. Please specify a different name.", name) - } - - if f.Repositories == nil { - f.Repositories = make(map[string]string) +func newRepoCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "repo [FLAGS] add|remove|list|index [ARGS]", + Short: "add, list, remove, index chart repositories", + Long: repoHelm, } - f.Repositories[name] = url + cmd.AddCommand(newRepoAddCmd(out)) + cmd.AddCommand(newRepoListCmd(out)) + cmd.AddCommand(newRepoRemoveCmd(out)) + cmd.AddCommand(newRepoIndexCmd(out)) - b, _ := yaml.Marshal(&f.Repositories) - return ioutil.WriteFile(repositoriesFile(), b, 0666) + return cmd } diff --git a/cmd/helm/repo_add.go b/cmd/helm/repo_add.go new file mode 100644 index 000000000..5ca4cc799 --- /dev/null +++ b/cmd/helm/repo_add.go @@ -0,0 +1,94 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" + + "k8s.io/helm/pkg/repo" +) + +type repoAddCmd struct { + name string + url string + out io.Writer +} + +func newRepoAddCmd(out io.Writer) *cobra.Command { + add := &repoAddCmd{ + out: out, + } + + cmd := &cobra.Command{ + Use: "add [flags] [NAME] [URL]", + Short: "add a chart repository", + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkArgsLength(len(args), "name for the chart repository", "the url of the chart repository"); err != nil { + return err + } + + add.name = args[0] + add.url = args[1] + + return add.run() + }, + } + return cmd +} + +func (a *repoAddCmd) run() error { + if err := addRepository(a.name, a.url); err != nil { + return err + } + + fmt.Println(a.name + " has been added to your repositories") + return nil +} + +func addRepository(name, url string) error { + if err := repo.DownloadIndexFile(name, url, cacheIndexFile(name)); err != nil { + return errors.New("Looks like " + url + " is not a valid chart repository or cannot be reached: " + err.Error()) + } + + return insertRepoLine(name, url) +} + +func insertRepoLine(name, url string) error { + f, err := repo.LoadRepositoriesFile(repositoriesFile()) + if err != nil { + return err + } + _, ok := f.Repositories[name] + if ok { + return fmt.Errorf("The repository name you provided (%s) already exists. Please specify a different name.", name) + } + + if f.Repositories == nil { + f.Repositories = make(map[string]string) + } + + f.Repositories[name] = url + + b, _ := yaml.Marshal(&f.Repositories) + return ioutil.WriteFile(repositoriesFile(), b, 0666) +} diff --git a/cmd/helm/repo_test.go b/cmd/helm/repo_add_test.go similarity index 65% rename from cmd/helm/repo_test.go rename to cmd/helm/repo_add_test.go index d24f6f594..3c768fcd0 100644 --- a/cmd/helm/repo_test.go +++ b/cmd/helm/repo_add_test.go @@ -66,39 +66,3 @@ func TestRepoAdd(t *testing.T) { } } - -func TestRepoRemove(t *testing.T) { - home := createTmpHome() - helmHome = home - if err := ensureHome(); err != nil { - t.Errorf("%s", err) - } - - if err := removeRepoLine(testName); err == nil { - t.Errorf("Expected error removing %s, but did not get one.", testName) - } - - if err := insertRepoLine(testName, testURL); err != nil { - t.Errorf("%s", err) - } - - mf, _ := os.Create(cacheIndexFile(testName)) - mf.Close() - - if err := removeRepoLine(testName); err != nil { - t.Errorf("Error removing %s from repositories", testName) - } - - if _, err := os.Stat(cacheIndexFile(testName)); err == nil { - t.Errorf("Error cache file was not removed for repository %s", testName) - } - - f, err := repo.LoadRepositoriesFile(repositoriesFile()) - if err != nil { - t.Errorf("%s", err) - } - - if _, ok := f.Repositories[testName]; ok { - t.Errorf("%s was not successfully removed from repositories list", testName) - } -} diff --git a/cmd/helm/repo_index.go b/cmd/helm/repo_index.go new file mode 100644 index 000000000..b49363c75 --- /dev/null +++ b/cmd/helm/repo_index.go @@ -0,0 +1,75 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "io" + "path/filepath" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/repo" +) + +type repoIndexCmd struct { + dir string + url string + out io.Writer +} + +func newRepoIndexCmd(out io.Writer) *cobra.Command { + index := &repoIndexCmd{ + out: out, + } + + cmd := &cobra.Command{ + Use: "index [flags] [DIR]", + Short: "generate an index file for a chart repository given a directory", + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkArgsLength(len(args), "path to a directory"); err != nil { + return err + } + + index.dir = args[0] + + return index.run() + }, + } + + f := cmd.Flags() + f.StringVar(&index.url, "url", "", "url of chart repository") + + return cmd +} + +func (i *repoIndexCmd) run() error { + path, err := filepath.Abs(i.dir) + if err != nil { + return err + } + + return index(path, i.url) +} + +func index(dir, url string) error { + chartRepo, err := repo.LoadChartRepository(dir, url) + if err != nil { + return err + } + + return chartRepo.Index() +} diff --git a/cmd/helm/repo_list.go b/cmd/helm/repo_list.go new file mode 100644 index 000000000..21696a0aa --- /dev/null +++ b/cmd/helm/repo_list.go @@ -0,0 +1,66 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "errors" + "fmt" + "io" + + "github.com/gosuri/uitable" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/repo" +) + +type repoListCmd struct { + out io.Writer +} + +func newRepoListCmd(out io.Writer) *cobra.Command { + list := &repoListCmd{ + out: out, + } + + cmd := &cobra.Command{ + Use: "list [flags]", + Short: "list chart repositories", + RunE: func(cmd *cobra.Command, args []string) error { + return list.run() + }, + } + + return cmd +} + +func (a *repoListCmd) run() error { + f, err := repo.LoadRepositoriesFile(repositoriesFile()) + if err != nil { + return err + } + if len(f.Repositories) == 0 { + return errors.New("no repositories to show") + } + table := uitable.New() + table.MaxColWidth = 50 + table.AddRow("NAME", "URL") + for k, v := range f.Repositories { + table.AddRow(k, v) + } + fmt.Println(table) + return nil +} diff --git a/cmd/helm/repo_remove.go b/cmd/helm/repo_remove.go new file mode 100644 index 000000000..ab1ad0dae --- /dev/null +++ b/cmd/helm/repo_remove.go @@ -0,0 +1,97 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "io" + "io/ioutil" + "os" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" + + "k8s.io/helm/pkg/repo" +) + +type repoRemoveCmd struct { + out io.Writer + name string +} + +func newRepoRemoveCmd(out io.Writer) *cobra.Command { + remove := &repoRemoveCmd{ + out: out, + } + + cmd := &cobra.Command{ + Use: "remove [flags] [NAME]", + Aliases: []string{"rm"}, + Short: "remove a chart repository", + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkArgsLength(len(args), "name of chart repository"); err != nil { + return err + } + remove.name = args[0] + + return remove.run() + }, + } + + return cmd +} + +func (r *repoRemoveCmd) run() error { + return removeRepoLine(r.name) +} + +func removeRepoLine(name string) error { + r, err := repo.LoadRepositoriesFile(repositoriesFile()) + if err != nil { + return err + } + + _, ok := r.Repositories[name] + if ok { + delete(r.Repositories, name) + b, err := yaml.Marshal(&r.Repositories) + if err != nil { + return err + } + if err := ioutil.WriteFile(repositoriesFile(), b, 0666); err != nil { + return err + } + if err := removeRepoCache(name); err != nil { + return err + } + + } else { + return fmt.Errorf("The repository, %s, does not exist in your repositories list", name) + } + + return nil +} + +func removeRepoCache(name string) error { + if _, err := os.Stat(cacheIndexFile(name)); err == nil { + err = os.Remove(cacheIndexFile(name)) + if err != nil { + return err + } + } + return nil +} diff --git a/cmd/helm/repo_remove_test.go b/cmd/helm/repo_remove_test.go new file mode 100644 index 000000000..007c02f0a --- /dev/null +++ b/cmd/helm/repo_remove_test.go @@ -0,0 +1,60 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "os" + "testing" + + "k8s.io/helm/pkg/repo" +) + +func TestRepoRemove(t *testing.T) { + home := createTmpHome() + helmHome = home + if err := ensureHome(); err != nil { + t.Errorf("%s", err) + } + + if err := removeRepoLine(testName); err == nil { + t.Errorf("Expected error removing %s, but did not get one.", testName) + } + + if err := insertRepoLine(testName, testURL); err != nil { + t.Errorf("%s", err) + } + + mf, _ := os.Create(cacheIndexFile(testName)) + mf.Close() + + if err := removeRepoLine(testName); err != nil { + t.Errorf("Error removing %s from repositories", testName) + } + + if _, err := os.Stat(cacheIndexFile(testName)); err == nil { + t.Errorf("Error cache file was not removed for repository %s", testName) + } + + f, err := repo.LoadRepositoriesFile(repositoriesFile()) + if err != nil { + t.Errorf("%s", err) + } + + if _, ok := f.Repositories[testName]; ok { + t.Errorf("%s was not successfully removed from repositories list", testName) + } +} From ec4442373e05d9b83f1e189e007e5200a16f384d Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Sun, 18 Sep 2016 21:28:13 -0500 Subject: [PATCH 099/183] helm(repo): add repo add and index cmd test --- cmd/helm/repo_add_test.go | 29 +++++++++++++++++--- cmd/helm/repo_index_test.go | 51 ++++++++++++++++++++++++++++++++++++ cmd/helm/repo_remove_test.go | 1 + 3 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 cmd/helm/repo_index_test.go diff --git a/cmd/helm/repo_add_test.go b/cmd/helm/repo_add_test.go index 3c768fcd0..7eb8e7b78 100644 --- a/cmd/helm/repo_add_test.go +++ b/cmd/helm/repo_add_test.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "bytes" "fmt" "io/ioutil" "net/http" @@ -28,10 +29,30 @@ import ( "k8s.io/helm/pkg/repo" ) -var ( - testName = "test-name" - testURL = "test-url" -) +var testName string = "test-name" + +func TestRepoAddCmd(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + fmt.Fprintln(w, "OK") + })) + + tests := []releaseCase{ + { + name: "add a repository", + args: []string{testName, ts.URL}, + expected: testName + " has been added to your repositories", + }, + } + + for _, tt := range tests { + buf := bytes.NewBuffer(nil) + c := newRepoAddCmd(buf) + if err := c.RunE(c, tt.args); err != nil { + t.Errorf("%q: expected '%q', got '%q'", tt.name, tt.expected, err) + } + } +} func TestRepoAdd(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/cmd/helm/repo_index_test.go b/cmd/helm/repo_index_test.go new file mode 100644 index 000000000..fbf22daca --- /dev/null +++ b/cmd/helm/repo_index_test.go @@ -0,0 +1,51 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "k8s.io/helm/pkg/repo" +) + +func TestRepoIndexCmd(t *testing.T) { + + dir, _ := ioutil.TempDir("", "charts") + defer os.RemoveAll(dir) + os.Link("testdata/testcharts/compressedChart-0.1.0.tgz", filepath.Join(dir, "compressedChart-0.1.0.tgz")) + + buf := bytes.NewBuffer(nil) + c := newRepoIndexCmd(buf) + + if err := c.RunE(c, []string{dir}); err != nil { + t.Errorf("%q", err) + } + + index, err := repo.LoadIndexFile(filepath.Join(dir, "index.yaml")) + if err != nil { + t.Fatal(err) + } + + if len(index.Entries) != 1 { + t.Errorf("expected 1 entires, got %v", len(index.Entries)) + } + +} diff --git a/cmd/helm/repo_remove_test.go b/cmd/helm/repo_remove_test.go index 007c02f0a..ea4fa6b55 100644 --- a/cmd/helm/repo_remove_test.go +++ b/cmd/helm/repo_remove_test.go @@ -24,6 +24,7 @@ import ( ) func TestRepoRemove(t *testing.T) { + testURL := "https://test-url.com" home := createTmpHome() helmHome = home if err := ensureHome(); err != nil { From 30036834e8d65621590767c8bd259a14ae947266 Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Sun, 18 Sep 2016 22:42:46 -0500 Subject: [PATCH 100/183] ref(helm): make index description more clear --- cmd/helm/repo_index.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/helm/repo_index.go b/cmd/helm/repo_index.go index b49363c75..636d90090 100644 --- a/cmd/helm/repo_index.go +++ b/cmd/helm/repo_index.go @@ -38,7 +38,7 @@ func newRepoIndexCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "index [flags] [DIR]", - Short: "generate an index file for a chart repository given a directory", + Short: "generate an index file given a directory containing packaged charts", RunE: func(cmd *cobra.Command, args []string) error { if err := checkArgsLength(len(args), "path to a directory"); err != nil { return err From bad00592421585b17b77c8a9f517cd4ddaf63649 Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Sun, 18 Sep 2016 22:46:22 -0500 Subject: [PATCH 101/183] ref(helm): display repo remove confirmation text --- cmd/helm/repo_add_test.go | 2 +- cmd/helm/repo_index_test.go | 11 ++++++++--- cmd/helm/repo_remove.go | 2 ++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/cmd/helm/repo_add_test.go b/cmd/helm/repo_add_test.go index 7eb8e7b78..bd6af277e 100644 --- a/cmd/helm/repo_add_test.go +++ b/cmd/helm/repo_add_test.go @@ -29,7 +29,7 @@ import ( "k8s.io/helm/pkg/repo" ) -var testName string = "test-name" +var testName = "test-name" func TestRepoAddCmd(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/cmd/helm/repo_index_test.go b/cmd/helm/repo_index_test.go index fbf22daca..d207a8b5e 100644 --- a/cmd/helm/repo_index_test.go +++ b/cmd/helm/repo_index_test.go @@ -28,9 +28,14 @@ import ( func TestRepoIndexCmd(t *testing.T) { - dir, _ := ioutil.TempDir("", "charts") + dir, err := ioutil.TempDir("", "helm-") + if err != nil { + t.Fatal(err) + } defer os.RemoveAll(dir) - os.Link("testdata/testcharts/compressedChart-0.1.0.tgz", filepath.Join(dir, "compressedChart-0.1.0.tgz")) + if err := os.Link("testdata/testcharts/compressedchart-0.1.0.tgz", filepath.Join(dir, "compressedchart-0.1.0.tgz")); err != nil { + t.Fatal(err) + } buf := bytes.NewBuffer(nil) c := newRepoIndexCmd(buf) @@ -45,7 +50,7 @@ func TestRepoIndexCmd(t *testing.T) { } if len(index.Entries) != 1 { - t.Errorf("expected 1 entires, got %v", len(index.Entries)) + t.Errorf("expected 1 entry, got %v: %#v", len(index.Entries), index.Entries) } } diff --git a/cmd/helm/repo_remove.go b/cmd/helm/repo_remove.go index ab1ad0dae..f58f78212 100644 --- a/cmd/helm/repo_remove.go +++ b/cmd/helm/repo_remove.go @@ -83,6 +83,8 @@ func removeRepoLine(name string) error { return fmt.Errorf("The repository, %s, does not exist in your repositories list", name) } + fmt.Println(name + " has been removed from your repositories") + return nil } From a5921faf99fdbdfcf58a251ace75f93c9c2b75ee Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 13 Sep 2016 13:29:58 -0600 Subject: [PATCH 102/183] feat(chartutils): add support for requirements.yaml --- cmd/helm/dependency.go | 216 +++++++++++++ cmd/helm/dependency_test.go | 71 +++++ cmd/helm/dependency_update.go | 287 ++++++++++++++++++ cmd/helm/dependency_update_test.go | 214 +++++++++++++ cmd/helm/fetch.go | 28 +- cmd/helm/helm.go | 1 + cmd/helm/helm_test.go | 20 ++ cmd/helm/init_test.go | 2 +- cmd/helm/install.go | 2 +- cmd/helm/repo_add_test.go | 2 +- cmd/helm/resolver/resolver.go | 87 ++++++ cmd/helm/resolver/resolver_test.go | 116 +++++++ cmd/helm/search.go | 1 + .../testdata/testcharts/reqtest-0.1.0.tgz | Bin 0 -> 911 bytes .../testdata/testcharts/reqtest/.helmignore | 21 ++ .../testdata/testcharts/reqtest/Chart.yaml | 3 + .../reqtest/charts/reqsubchart/.helmignore | 21 ++ .../reqtest/charts/reqsubchart/Chart.yaml | 3 + .../reqtest/charts/reqsubchart/values.yaml | 4 + .../reqtest/charts/reqsubchart2/.helmignore | 21 ++ .../reqtest/charts/reqsubchart2/Chart.yaml | 3 + .../reqtest/charts/reqsubchart2/values.yaml | 4 + .../testcharts/reqtest/requirements.lock | 3 + .../testcharts/reqtest/requirements.yaml | 7 + .../testdata/testcharts/reqtest/values.yaml | 4 + docs/charts.md | 56 ++++ pkg/chartutil/load_test.go | 30 +- pkg/chartutil/requirements.go | 84 +++++ pkg/chartutil/requirements_test.go | 27 ++ pkg/chartutil/testdata/frobnitz-1.2.3.tgz | Bin 3831 -> 3974 bytes .../frobnitz/charts/mariner-4.3.2.tgz | Bin 941 -> 1025 bytes .../testdata/frobnitz/requirements.yaml | 7 + .../mariner/charts/albatross-0.1.0.tgz | Bin 347 -> 347 bytes .../testdata/mariner/requirements.yaml | 4 + pkg/provenance/sign.go | 16 +- pkg/provenance/sign_test.go | 26 +- pkg/repo/index.go | 72 ++++- pkg/repo/index_test.go | 32 ++ 38 files changed, 1462 insertions(+), 33 deletions(-) create mode 100644 cmd/helm/dependency.go create mode 100644 cmd/helm/dependency_test.go create mode 100644 cmd/helm/dependency_update.go create mode 100644 cmd/helm/dependency_update_test.go create mode 100644 cmd/helm/resolver/resolver.go create mode 100644 cmd/helm/resolver/resolver_test.go create mode 100644 cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz create mode 100644 cmd/helm/testdata/testcharts/reqtest/.helmignore create mode 100755 cmd/helm/testdata/testcharts/reqtest/Chart.yaml create mode 100644 cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/.helmignore create mode 100755 cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml create mode 100644 cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/values.yaml create mode 100644 cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/.helmignore create mode 100755 cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml create mode 100644 cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/values.yaml create mode 100755 cmd/helm/testdata/testcharts/reqtest/requirements.lock create mode 100644 cmd/helm/testdata/testcharts/reqtest/requirements.yaml create mode 100644 cmd/helm/testdata/testcharts/reqtest/values.yaml create mode 100644 pkg/chartutil/requirements.go create mode 100644 pkg/chartutil/requirements_test.go create mode 100644 pkg/chartutil/testdata/frobnitz/requirements.yaml create mode 100644 pkg/chartutil/testdata/mariner/requirements.yaml diff --git a/cmd/helm/dependency.go b/cmd/helm/dependency.go new file mode 100644 index 000000000..2da344052 --- /dev/null +++ b/cmd/helm/dependency.go @@ -0,0 +1,216 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/gosuri/uitable" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/chartutil" +) + +const dependencyDesc = ` +Manage the dependencies of a chart. + +Helm charts store their dependencies in 'charts/'. For chart developers, it is +often easier to manage a single dependency file ('requirements.yaml') +which declares all dependencies. + +The dependency commands operate on that file, making it easy to synchronize +between the desired dependencies and the actual dependencies stored in the +'charts/' directory. + +A 'requirements.yaml' file is a YAML file in which developers can declare chart +dependencies, along with the location of the chart and the desired version. +For example, this requirements file declares two dependencies: + + # requirements.yaml + dependencies: + - name: nginx + version: "1.2.3" + repository: "https://example.com/charts" + - name: memcached + version: "3.2.1" + repository: "https://another.example.com/charts" + +The 'name' should be the name of a chart, where that name must match the name +in that chart's 'Chart.yaml' file. + +The 'version' field should contain a semantic version or version range. + +The 'repository' URL should point to a Chart Repository. Helm expects that by +appending '/index.yaml' to the URL, it should be able to retrieve the chart +repository's index. Note: 'repository' cannot be a repository alias. It must be +a URL. +` + +const dependencyListDesc = ` +List all of the dependencies declared in a chart. + +This can take chart archives and chart directories as input. It will not alter +the contents of a chart. + +This will produce an error if the chart cannot be loaded. It will emit a warning +if it cannot find a requirements.yaml. +` + +func newDependencyCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "dependency update|list", + Aliases: []string{"dep", "dependencies"}, + Short: "manage a chart's dependencies", + Long: dependencyDesc, + } + + cmd.AddCommand(newDependencyListCmd(out)) + cmd.AddCommand(newDependencyUpdateCmd(out)) + + return cmd +} + +type dependencyListCmd struct { + out io.Writer + chartpath string +} + +func newDependencyListCmd(out io.Writer) *cobra.Command { + dlc := &dependencyListCmd{ + out: out, + } + cmd := &cobra.Command{ + Use: "list [flags] CHART", + Aliases: []string{"ls"}, + Short: "list the dependencies for the given chart", + Long: dependencyListDesc, + RunE: func(cmd *cobra.Command, args []string) error { + cp := "." + if len(args) > 0 { + cp = args[0] + } + + var err error + dlc.chartpath, err = filepath.Abs(cp) + if err != nil { + return err + } + return dlc.run() + }, + } + return cmd +} + +func (l *dependencyListCmd) run() error { + c, err := chartutil.Load(l.chartpath) + if err != nil { + return err + } + + r, err := chartutil.LoadRequirements(c) + if err != nil { + if err == chartutil.ErrRequirementsNotFound { + fmt.Fprintf(l.out, "WARNING: no requirements at %s/charts", l.chartpath) + return nil + } + return err + } + + l.printRequirements(r, l.out) + fmt.Fprintln(l.out) + l.printMissing(r, l.out) + return nil +} + +func (l *dependencyListCmd) dependencyStatus(dep *chartutil.Dependency) string { + filename := fmt.Sprintf("%s-%s.tgz", dep.Name, dep.Version) + archive := filepath.Join(l.chartpath, "charts", filename) + if _, err := os.Stat(archive); err == nil { + c, err := chartutil.Load(archive) + if err != nil { + return "corrupt" + } + if c.Metadata.Name == dep.Name && c.Metadata.Version == dep.Version { + return "ok" + } + return "mismatch" + } + + folder := filepath.Join(l.chartpath, "charts", dep.Name) + if fi, err := os.Stat(folder); err != nil { + return "missing" + } else if !fi.IsDir() { + return "mispackaged" + } + + c, err := chartutil.Load(folder) + if err != nil { + return "corrupt" + } + + if c.Metadata.Name != dep.Name { + return "misnamed" + } + + if c.Metadata.Version != dep.Version { + return "wrong version" + } + + return "unpacked" +} + +// printRequirements prints all of the requirements in the yaml file. +func (l *dependencyListCmd) printRequirements(reqs *chartutil.Requirements, out io.Writer) { + table := uitable.New() + table.MaxColWidth = 80 + table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS") + for _, row := range reqs.Dependencies { + table.AddRow(row.Name, row.Version, row.Repository, l.dependencyStatus(row)) + } + fmt.Fprintln(out, table) +} + +// printMissing prints warnings about charts that are present on disk, but are not in the requirements. +func (l *dependencyListCmd) printMissing(reqs *chartutil.Requirements, out io.Writer) { + folder := filepath.Join(l.chartpath, "charts/*") + files, err := filepath.Glob(folder) + if err != nil { + fmt.Fprintln(l.out, err) + return + } + + for _, f := range files { + c, err := chartutil.Load(f) + if err != nil { + fmt.Fprintf(l.out, "WARNING: %q is not a chart.\n", f) + continue + } + found := false + for _, d := range reqs.Dependencies { + if d.Name == c.Metadata.Name { + found = true + break + } + } + if !found { + fmt.Fprintf(l.out, "WARNING: %q is not in requirements.yaml.\n", f) + } + } + +} diff --git a/cmd/helm/dependency_test.go b/cmd/helm/dependency_test.go new file mode 100644 index 000000000..749d490cb --- /dev/null +++ b/cmd/helm/dependency_test.go @@ -0,0 +1,71 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "strings" + "testing" +) + +func TestDependencyListCmd(t *testing.T) { + + tests := []struct { + name string + args []string + expect string + err bool + }{ + { + name: "No such chart", + args: []string{"/no/such/chart"}, + err: true, + }, + { + name: "No requirements.yaml", + args: []string{"testdata/testcharts/alpine"}, + expect: "WARNING: no requirements at ", + }, + { + name: "Requirements in chart dir", + args: []string{"testdata/testcharts/reqtest"}, + expect: "NAME \tVERSION\tREPOSITORY \tSTATUS \nreqsubchart \t0.1.0 \thttps://example.com/charts\tunpacked\nreqsubchart2\t0.2.0 \thttps://example.com/charts\tunpacked\n", + }, + { + name: "Requirements in chart archive", + args: []string{"testdata/testcharts/reqtest-0.1.0.tgz"}, + expect: "NAME \tVERSION\tREPOSITORY \tSTATUS \nreqsubchart \t0.1.0 \thttps://example.com/charts\tmissing\nreqsubchart2\t0.2.0 \thttps://example.com/charts\tmissing\n", + }, + } + + for _, tt := range tests { + buf := bytes.NewBuffer(nil) + dlc := newDependencyListCmd(buf) + if err := dlc.RunE(dlc, tt.args); err != nil { + if tt.err { + continue + } + t.Errorf("Test %q: %s", tt.name, err) + continue + } + + got := buf.String() + if !strings.Contains(got, tt.expect) { + t.Errorf("Test: %q, Expected:\n%q\nGot:\n%q", tt.name, tt.expect, got) + } + } + +} diff --git a/cmd/helm/dependency_update.go b/cmd/helm/dependency_update.go new file mode 100644 index 000000000..9b9eb1d12 --- /dev/null +++ b/cmd/helm/dependency_update.go @@ -0,0 +1,287 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/ghodss/yaml" + "github.com/spf13/cobra" + + "k8s.io/helm/cmd/helm/resolver" + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/repo" +) + +const dependencyUpDesc = ` +Update the on-disk dependencies to mirror the requirements.yaml file. + +This command verifies that the required charts, as expressed in 'requirements.yaml', +are present in 'charts/' and are at an acceptable version. +` + +// dependencyUpdateCmd describes a 'helm dependency update' +type dependencyUpdateCmd struct { + out io.Writer + chartpath string + repoFile string + repopath string + helmhome string + verify bool + keyring string +} + +// newDependencyUpdateCmd creates a new dependency update command. +func newDependencyUpdateCmd(out io.Writer) *cobra.Command { + duc := &dependencyUpdateCmd{ + out: out, + } + + cmd := &cobra.Command{ + Use: "update [flags] CHART", + Aliases: []string{"up"}, + Short: "update charts/ based on the contents of requirements.yaml", + Long: dependencyUpDesc, + RunE: func(cmd *cobra.Command, args []string) error { + cp := "." + if len(args) > 0 { + cp = args[0] + } + + var err error + duc.chartpath, err = filepath.Abs(cp) + if err != nil { + return err + } + + duc.helmhome = homePath() + duc.repoFile = repositoriesFile() + duc.repopath = repositoryDirectory() + + return duc.run() + }, + } + + f := cmd.Flags() + f.BoolVar(&duc.verify, "verify", false, "Verify the package against its signature.") + f.StringVar(&duc.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.") + + return cmd +} + +// run runs the full dependency update process. +func (d *dependencyUpdateCmd) run() error { + if fi, err := os.Stat(d.chartpath); err != nil { + return fmt.Errorf("could not find %s: %s", d.chartpath, err) + } else if !fi.IsDir() { + return errors.New("only unpacked charts can be updated") + } + c, err := chartutil.LoadDir(d.chartpath) + if err != nil { + return err + } + + req, err := chartutil.LoadRequirements(c) + if err != nil { + if err == chartutil.ErrRequirementsNotFound { + fmt.Fprintf(d.out, "No requirements found in %s/charts.\n", d.chartpath) + return nil + } + return err + } + + // For each repo in the file, update the cached copy of that repo + if _, err := d.updateRepos(req.Dependencies); err != nil { + return err + } + + // Now we need to find out which version of a chart best satisfies the + // requirements the requirements.yaml + lock, err := d.resolve(req) + if err != nil { + return err + } + + // Now we need to fetch every package here into charts/ + if err := d.downloadAll(lock.Dependencies); err != nil { + return err + } + + // Finally, we need to write the lockfile. + return writeLock(d.chartpath, lock) +} + +// 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 (d *dependencyUpdateCmd) resolve(req *chartutil.Requirements) (*chartutil.RequirementsLock, error) { + res := resolver.New(d.chartpath, d.helmhome) + return res.Resolve(req) +} + +// downloadAll takes a list of dependencies and downloads them into charts/ +func (d *dependencyUpdateCmd) downloadAll(deps []*chartutil.Dependency) error { + repos, err := loadChartRepositories(d.repopath) + if err != nil { + return err + } + + fmt.Fprintf(d.out, "Saving %d charts\n", len(deps)) + for _, dep := range deps { + fmt.Fprintf(d.out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) + + target := fmt.Sprintf("%s-%s", dep.Name, dep.Version) + churl, err := findChartURL(target, dep.Repository, repos) + if err != nil { + fmt.Fprintf(d.out, "WARNING: %s (skipped)", err) + continue + } + + dest := filepath.Join(d.chartpath, "charts", target+".tgz") + data, err := downloadChart(churl, d.verify, d.keyring) + if err != nil { + fmt.Fprintf(d.out, "WARNING: Could not download %s: %s (skipped)", churl, err) + continue + } + if err := ioutil.WriteFile(dest, data.Bytes(), 0655); err != nil { + fmt.Fprintf(d.out, "WARNING: %s (skipped)", err) + continue + } + } + return nil +} + +// updateRepos updates all of the local repos to their latest. +// +// If one of the dependencies present is not in the cached repos, this will error out. The +// consequence of that is that every repository referenced in a requirements.yaml file +// must also be added with 'helm repo add'. +func (d *dependencyUpdateCmd) updateRepos(deps []*chartutil.Dependency) (*repo.RepoFile, error) { + // TODO: In the future, we could make it so that only the repositories that + // are used by this chart are updated. As it is, we're mainly doing some sanity + // checking here. + rf, err := repo.LoadRepositoriesFile(d.repoFile) + if err != nil { + return rf, err + } + repos := rf.Repositories + + // Verify that all repositories referenced in the deps are actually known + // by Helm. + missing := []string{} + for _, dd := range deps { + found := false + if dd.Repository == "" { + found = true + } else { + for _, repo := range repos { + if urlsAreEqual(repo, dd.Repository) { + found = true + } + } + } + if !found { + missing = append(missing, dd.Repository) + } + } + + if len(missing) > 0 { + return rf, fmt.Errorf("no repository definition for %s. Try 'helm repo add'", strings.Join(missing, ", ")) + } + + if len(repos) > 0 { + // This prints errors straight to out. + updateCharts(repos, flagDebug, d.out) + } + return rf, nil +} + +// urlsAreEqual normalizes two URLs and then compares for equality. +func urlsAreEqual(a, b string) bool { + au, err := url.Parse(a) + if err != nil { + return a == b + } + bu, err := url.Parse(b) + if err != nil { + return false + } + return au.String() == bu.String() +} + +// findChartURL searches the cache of repo data for a chart that has the name and the repourl specified. +// +// In this current version, name is of the form 'foo-1.2.3'. This will change when +// the repository index stucture changes. +func findChartURL(name, repourl string, repos map[string]*repo.ChartRepository) (string, error) { + for _, cr := range repos { + if urlsAreEqual(repourl, cr.URL) { + for ename, entry := range cr.IndexFile.Entries { + if ename == name { + return entry.URL, nil + } + } + } + } + return "", fmt.Errorf("chart %s not found in %s", name, repourl) +} + +// loadChartRepositories reads the repositories.yaml, and then builds a map of +// ChartRepositories. +// +// The key is the local name (which is only present in the repositories.yaml). +func loadChartRepositories(repodir string) (map[string]*repo.ChartRepository, error) { + indices := map[string]*repo.ChartRepository{} + repoyaml := repositoriesFile() + + // Load repositories.yaml file + rf, err := repo.LoadRepositoriesFile(repoyaml) + if err != nil { + return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err) + } + + // localName: chartRepo + for lname, url := range rf.Repositories { + index, err := repo.LoadIndexFile(cacheIndexFile(lname)) + if err != nil { + return indices, err + } + + cr := &repo.ChartRepository{ + URL: url, + IndexFile: index, + } + indices[lname] = cr + } + return indices, nil +} + +// writeLock writes a lockfile to disk +func writeLock(chartpath string, lock *chartutil.RequirementsLock) error { + data, err := yaml.Marshal(lock) + if err != nil { + return err + } + dest := filepath.Join(chartpath, "requirements.lock") + return ioutil.WriteFile(dest, data, 0755) +} diff --git a/cmd/helm/dependency_update_test.go b/cmd/helm/dependency_update_test.go new file mode 100644 index 000000000..b38b90a21 --- /dev/null +++ b/cmd/helm/dependency_update_test.go @@ -0,0 +1,214 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/ghodss/yaml" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/provenance" + "k8s.io/helm/pkg/repo" +) + +func TestDependencyUpdateCmd(t *testing.T) { + // Set up a testing helm home + oldhome := helmHome + hh, err := tempHelmHome() + if err != nil { + t.Fatal(err) + } + helmHome = hh // Shoot me now. + defer func() { + os.RemoveAll(hh) + helmHome = oldhome + }() + + srv := newTestingRepositoryServer(hh) + defer srv.stop() + copied, err := srv.copyCharts("testdata/testcharts/*.tgz") + t.Logf("Copied charts %s", strings.Join(copied, "\n")) + t.Logf("Listening for directory %s", srv.docroot) + + chartname := "depup" + if err := createTestingChart(hh, chartname, srv.url()); err != nil { + t.Fatal(err) + } + + out := bytes.NewBuffer(nil) + duc := &dependencyUpdateCmd{out: out} + duc.helmhome = hh + duc.chartpath = filepath.Join(hh, chartname) + duc.repoFile = filepath.Join(duc.helmhome, "repository/repositories.yaml") + duc.repopath = filepath.Join(duc.helmhome, "repository") + + if err := duc.run(); err != nil { + t.Fatal(err) + } + + output := out.String() + t.Logf("Output: %s", output) + // This is written directly to stdout, so we have to capture as is. + if !strings.Contains(output, `update from the "test" chart repository`) { + t.Errorf("Repo did not get updated\n%s", output) + } + + // Make sure the actual file got downloaded. + expect := filepath.Join(hh, chartname, "charts/reqtest-0.1.0.tgz") + if _, err := os.Stat(expect); err != nil { + t.Fatal(err) + } + + hash, err := provenance.DigestFile(expect) + if err != nil { + t.Fatal(err) + } + + i, err := repo.LoadIndexFile(cacheIndexFile("test")) + if err != nil { + t.Fatal(err) + } + + if h := i.Entries["reqtest-0.1.0"].Digest; h != hash { + t.Errorf("Failed hash match: expected %s, got %s", hash, h) + } + + t.Logf("Results: %s", out.String()) +} + +// newTestingRepositoryServer creates a repository server for testing. +// +// docroot should be a temp dir managed by the caller. +// +// This will start the server, serving files off of the docroot. +// +// Use copyCharts to move charts into the repository and then index them +// for service. +func newTestingRepositoryServer(docroot string) *testingRepositoryServer { + root, err := filepath.Abs(docroot) + if err != nil { + panic(err) + } + srv := &testingRepositoryServer{ + docroot: root, + } + srv.start() + // Add the testing repository as the only repo. + if err := setTestingRepository(docroot, "test", srv.url()); err != nil { + panic(err) + } + return srv +} + +type testingRepositoryServer struct { + docroot string + srv *httptest.Server +} + +// copyCharts takes a glob expression and copies those charts to the server root. +func (s *testingRepositoryServer) copyCharts(origin string) ([]string, error) { + files, err := filepath.Glob(origin) + if err != nil { + return []string{}, err + } + copied := make([]string, len(files)) + for i, f := range files { + base := filepath.Base(f) + newname := filepath.Join(s.docroot, base) + data, err := ioutil.ReadFile(f) + if err != nil { + return []string{}, err + } + if err := ioutil.WriteFile(newname, data, 0755); err != nil { + return []string{}, err + } + copied[i] = newname + } + + // generate the index + index, err := repo.IndexDirectory(s.docroot, s.url()) + if err != nil { + return copied, err + } + + d, err := yaml.Marshal(index.Entries) + if err != nil { + return copied, err + } + + ifile := filepath.Join(s.docroot, "index.yaml") + err = ioutil.WriteFile(ifile, d, 0755) + return copied, err +} + +func (s *testingRepositoryServer) start() { + s.srv = httptest.NewServer(http.FileServer(http.Dir(s.docroot))) +} + +func (s *testingRepositoryServer) stop() { + s.srv.Close() +} + +func (s *testingRepositoryServer) url() string { + return s.srv.URL +} + +// setTestingRepository sets up a testing repository.yaml with only the given name/URL. +func setTestingRepository(helmhome, name, url string) error { + // Oddly, there is no repo.Save function for this. + data, err := yaml.Marshal(&map[string]string{name: url}) + if err != nil { + return err + } + os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755) + dest := filepath.Join(helmhome, "repository/repositories.yaml") + return ioutil.WriteFile(dest, data, 0666) +} + +// createTestingChart creates a basic chart that depends on reqtest-0.1.0 +// +// The baseURL can be used to point to a particular repository server. +func createTestingChart(dest, name, baseURL string) error { + cfile := &chart.Metadata{ + Name: name, + Version: "1.2.3", + } + dir := filepath.Join(dest, name) + _, err := chartutil.Create(cfile, dest) + if err != nil { + return err + } + req := &chartutil.Requirements{ + Dependencies: []*chartutil.Dependency{ + {Name: "reqtest", Version: "0.1.0", Repository: baseURL}, + }, + } + data, err := yaml.Marshal(req) + if err != nil { + return err + } + + return ioutil.WriteFile(filepath.Join(dir, "requirements.yaml"), data, 0655) +} diff --git a/cmd/helm/fetch.go b/cmd/helm/fetch.go index 395fef175..e174bcdac 100644 --- a/cmd/helm/fetch.go +++ b/cmd/helm/fetch.go @@ -96,28 +96,36 @@ func (f *fetchCmd) run() error { pname += ".tgz" } - return downloadChart(pname, f.untar, f.untardir, f.verify, f.keyring) + return downloadAndSaveChart(pname, f.untar, f.untardir, f.verify, f.keyring) } -// downloadChart fetches a chart over HTTP, and then (if verify is true) verifies it. +// downloadAndSaveChart fetches a chart over HTTP, and then (if verify is true) verifies it. // // If untar is true, it also unpacks the file into untardir. -func downloadChart(pname string, untar bool, untardir string, verify bool, keyring string) error { - r, err := repo.LoadRepositoriesFile(repositoriesFile()) +func downloadAndSaveChart(pname string, untar bool, untardir string, verify bool, keyring string) error { + buf, err := downloadChart(pname, verify, keyring) if err != nil { return err } + return saveChart(pname, buf, untar, untardir) +} + +func downloadChart(pname string, verify bool, keyring string) (*bytes.Buffer, error) { + r, err := repo.LoadRepositoriesFile(repositoriesFile()) + if err != nil { + return bytes.NewBuffer(nil), err + } // get download url u, err := mapRepoArg(pname, r.Repositories) if err != nil { - return err + return bytes.NewBuffer(nil), err } href := u.String() buf, err := fetchChart(href) if err != nil { - return err + return buf, err } if verify { @@ -125,17 +133,17 @@ func downloadChart(pname string, untar bool, untardir string, verify bool, keyri sigref := href + ".prov" sig, err := fetchChart(sigref) if err != nil { - return fmt.Errorf("provenance data not downloaded from %s: %s", sigref, err) + return buf, fmt.Errorf("provenance data not downloaded from %s: %s", sigref, err) } if err := ioutil.WriteFile(basename+".prov", sig.Bytes(), 0755); err != nil { - return fmt.Errorf("provenance data not saved: %s", err) + return buf, fmt.Errorf("provenance data not saved: %s", err) } if err := verifyChart(basename, keyring); err != nil { - return err + return buf, err } } - return saveChart(pname, buf, untar, untardir) + return buf, nil } // verifyChart takes a path to a chart archive and a keyring, and verifies the chart. diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 5bb4603cf..11e0f88d6 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -100,6 +100,7 @@ func newRootCmd(out io.Writer) *cobra.Command { newUpdateCmd(out), newVersionCmd(nil, out), newRepoCmd(out), + newDependencyCmd(out), ) return cmd } diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 68c3ccc98..e0ce52c74 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -20,6 +20,7 @@ import ( "bytes" "fmt" "io" + "io/ioutil" "math/rand" "regexp" "testing" @@ -205,3 +206,22 @@ type releaseCase struct { err bool resp *release.Release } + +// tmpHelmHome sets up a Helm Home in a temp dir. +// +// This does not clean up the directory. You must do that yourself. +// You must also set helmHome yourself. +func tempHelmHome() (string, error) { + oldhome := helmHome + dir, err := ioutil.TempDir("", "helm_home-") + if err != nil { + return "n/", err + } + + helmHome = dir + if err := ensureHome(); err != nil { + return "n/", err + } + helmHome = oldhome + return dir, nil +} diff --git a/cmd/helm/init_test.go b/cmd/helm/init_test.go index 44bcafce2..df20e3786 100644 --- a/cmd/helm/init_test.go +++ b/cmd/helm/init_test.go @@ -28,7 +28,7 @@ import ( func TestEnsureHome(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") - fmt.Fprintln(w, "OK") + fmt.Fprintln(w, "") })) defaultRepositoryURL = ts.URL diff --git a/cmd/helm/install.go b/cmd/helm/install.go index ced11eb9c..3fab2c7fd 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -306,7 +306,7 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) { if filepath.Ext(name) != ".tgz" { name += ".tgz" } - if err := downloadChart(name, false, ".", verify, keyring); err == nil { + if err := downloadAndSaveChart(name, false, ".", verify, keyring); err == nil { lname, err := filepath.Abs(filepath.Base(name)) if err != nil { return lname, err diff --git a/cmd/helm/repo_add_test.go b/cmd/helm/repo_add_test.go index bd6af277e..3d6497442 100644 --- a/cmd/helm/repo_add_test.go +++ b/cmd/helm/repo_add_test.go @@ -57,7 +57,7 @@ func TestRepoAddCmd(t *testing.T) { func TestRepoAdd(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") - fmt.Fprintln(w, "OK") + fmt.Fprintln(w, "") })) helmHome, _ = ioutil.TempDir("", "helm_home") diff --git a/cmd/helm/resolver/resolver.go b/cmd/helm/resolver/resolver.go new file mode 100644 index 000000000..34854b538 --- /dev/null +++ b/cmd/helm/resolver/resolver.go @@ -0,0 +1,87 @@ +/* +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 resolver + +import ( + "bytes" + "encoding/json" + "fmt" + "time" + + "github.com/Masterminds/semver" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/provenance" +) + +// Resolver resolves dependencies from semantic version ranges to a particular version. +type Resolver struct { + chartpath string + helmhome string +} + +// New creates a new resolver for a given chart and a given helm home. +func New(chartpath string, helmhome string) *Resolver { + return &Resolver{ + chartpath: chartpath, + helmhome: helmhome, + } +} + +// Resolve resolves dependencies and returns a lock file with the resolution. +func (r *Resolver) Resolve(reqs *chartutil.Requirements) (*chartutil.RequirementsLock, error) { + d, err := hashReq(reqs) + if err != nil { + return nil, err + } + + // Now we clone the dependencies, locking as we go. + locked := make([]*chartutil.Dependency, len(reqs.Dependencies)) + for i, d := range reqs.Dependencies { + // Right now, we're just copying one entry to another. What we need to + // do here is parse the requirement as a SemVer range, and then look up + // whether a version in index.yaml satisfies this constraint. If so, + // we need to clone the dep, settinv Version appropriately. + // If not, we need to error out. + if _, err := semver.NewVersion(d.Version); err != nil { + return nil, fmt.Errorf("dependency %q has an invalid version: %s", d.Name, err) + } + locked[i] = &chartutil.Dependency{ + Name: d.Name, + Repository: d.Repository, + Version: d.Version, + } + } + + return &chartutil.RequirementsLock{ + Generated: time.Now(), + Digest: d, + Dependencies: locked, + }, nil +} + +// hashReq generates a hash of the requirements. +// +// This should be used only to compare against another hash generated by this +// function. +func hashReq(req *chartutil.Requirements) (string, error) { + data, err := json.Marshal(req) + if err != nil { + return "", err + } + s, err := provenance.Digest(bytes.NewBuffer(data)) + return "sha256:" + s, err +} diff --git a/cmd/helm/resolver/resolver_test.go b/cmd/helm/resolver/resolver_test.go new file mode 100644 index 000000000..0212757b8 --- /dev/null +++ b/cmd/helm/resolver/resolver_test.go @@ -0,0 +1,116 @@ +/* +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 resolver + +import ( + "testing" + + "k8s.io/helm/pkg/chartutil" +) + +func TestResolve(t *testing.T) { + tests := []struct { + name string + req *chartutil.Requirements + expect *chartutil.RequirementsLock + err bool + }{ + { + name: "version failure", + req: &chartutil.Requirements{ + Dependencies: []*chartutil.Dependency{ + {Name: "oedipus-rex", Repository: "http://example.com", Version: ">1"}, + }, + }, + err: true, + }, + { + name: "valid lock", + req: &chartutil.Requirements{ + Dependencies: []*chartutil.Dependency{ + {Name: "antigone", Repository: "http://example.com", Version: "1.0.0"}, + }, + }, + expect: &chartutil.RequirementsLock{ + Dependencies: []*chartutil.Dependency{ + {Name: "antigone", Repository: "http://example.com", Version: "1.0.0"}, + }, + }, + }, + } + + r := New("testdata/chartpath", "testdata/helmhome") + for _, tt := range tests { + l, err := r.Resolve(tt.req) + if err != nil { + if tt.err { + continue + } + t.Fatal(err) + } + + if tt.err { + t.Fatalf("Expected error in test %q", tt.name) + } + + if h, err := hashReq(tt.req); err != nil { + t.Fatal(err) + } else if h != l.Digest { + t.Errorf("%q: hashes don't match.", tt.name) + } + + // Check fields. + if len(l.Dependencies) != len(tt.req.Dependencies) { + t.Errorf("%s: wrong number of dependencies in lock", tt.name) + } + d0 := l.Dependencies[0] + e0 := tt.expect.Dependencies[0] + if d0.Name != e0.Name { + t.Errorf("%s: expected name %s, got %s", tt.name, e0.Name, d0.Name) + } + if d0.Repository != e0.Repository { + t.Errorf("%s: expected repo %s, got %s", tt.name, e0.Repository, d0.Repository) + } + if d0.Version != e0.Version { + t.Errorf("%s: expected version %s, got %s", tt.name, e0.Version, d0.Version) + } + } +} + +func TestHashReq(t *testing.T) { + expect := "sha256:e70e41f8922e19558a8bf62f591a8b70c8e4622e3c03e5415f09aba881f13885" + req := &chartutil.Requirements{ + Dependencies: []*chartutil.Dependency{ + {Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"}, + }, + } + h, err := hashReq(req) + if err != nil { + t.Fatal(err) + } + if expect != h { + t.Errorf("Expected %q, got %q", expect, h) + } + + req = &chartutil.Requirements{Dependencies: []*chartutil.Dependency{}} + h, err = hashReq(req) + if err != nil { + t.Fatal(err) + } + if expect == h { + t.Errorf("Expected %q != %q", expect, h) + } +} diff --git a/cmd/helm/search.go b/cmd/helm/search.go index 3c06aec47..e1a465c99 100644 --- a/cmd/helm/search.go +++ b/cmd/helm/search.go @@ -45,6 +45,7 @@ func search(cmd *cobra.Command, args []string) error { return errors.New("This command needs at least one argument (search string)") } + // TODO: This needs to be refactored to use loadChartRepositories results, err := searchCacheForPattern(cacheDirectory(), args[0]) if err != nil { return err diff --git a/cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz b/cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..356bc93030395fa1c045c8aaefb60c9d80a079a5 GIT binary patch literal 911 zcmV;A191EwiwG0|32ul0|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PI=cZ=^O5zGwc5>E)%Zy8PMzIo?i5rB?kZ<#bY2Rh5AWxB)|L zlU(}Hzr6$8=1yd@=(ZfIWc?{JtYZ)M?tGtT28n-bRN6T&nAG+itI8L%!zDyP&|eAT ztLwSp{e9o>`N6680_I=I7PLw;Nss@(cE+1~BFIpsk~f;yB8J!S9hMcOoiD&uE#ZeY zK`D?t#1gE+7~Z>!b%Rp%Q(W7#UF*=hFxVFx{@<{&MfG_EV2b~~=a2axMS5P4G z`RApkwULSQx~j;)+w)7vNN6lO=i2GpVfmJw{3D&d-E0rq>P9#p3?;O`w&?{; zSzp`gwxKp**VO8Y?*FBsZ<*wEtKj>KZ|Q-JtpCDPTQ<*-Im0;WdWsUZ;XhqlF0n$P zm0i~9^^DJ$jQ`i}i2tWPr35hJ5+28q^FPA|MTR2fsABm24=dwDbsfXcwFXW{b?*|G zSvd-nbZ%!c_^ubO+*d1a{l<%8KZw1^4qmOJv$N{fxe{Wp>4@1wK|BK+U`rpP2ObzgPV+a3dD+x~V|6%FjW9B008H7+U)=U literal 0 HcmV?d00001 diff --git a/cmd/helm/testdata/testcharts/reqtest/.helmignore b/cmd/helm/testdata/testcharts/reqtest/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/cmd/helm/testdata/testcharts/reqtest/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/cmd/helm/testdata/testcharts/reqtest/Chart.yaml b/cmd/helm/testdata/testcharts/reqtest/Chart.yaml new file mode 100755 index 000000000..e2fbe4b01 --- /dev/null +++ b/cmd/helm/testdata/testcharts/reqtest/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: reqtest +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/.helmignore b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml new file mode 100755 index 000000000..c3813bc8c --- /dev/null +++ b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: reqsubchart +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/values.yaml b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/values.yaml new file mode 100644 index 000000000..0f0b63f2a --- /dev/null +++ b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/values.yaml @@ -0,0 +1,4 @@ +# Default values for reqsubchart. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name: value diff --git a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/.helmignore b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml new file mode 100755 index 000000000..9f7c22a71 --- /dev/null +++ b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: reqsubchart2 +version: 0.2.0 diff --git a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/values.yaml b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/values.yaml new file mode 100644 index 000000000..0f0b63f2a --- /dev/null +++ b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/values.yaml @@ -0,0 +1,4 @@ +# Default values for reqsubchart. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name: value diff --git a/cmd/helm/testdata/testcharts/reqtest/requirements.lock b/cmd/helm/testdata/testcharts/reqtest/requirements.lock new file mode 100755 index 000000000..ab1ae8cc0 --- /dev/null +++ b/cmd/helm/testdata/testcharts/reqtest/requirements.lock @@ -0,0 +1,3 @@ +dependencies: [] +digest: Not implemented +generated: 2016-09-13T17:25:17.593788787-06:00 diff --git a/cmd/helm/testdata/testcharts/reqtest/requirements.yaml b/cmd/helm/testdata/testcharts/reqtest/requirements.yaml new file mode 100644 index 000000000..4b0b8c2db --- /dev/null +++ b/cmd/helm/testdata/testcharts/reqtest/requirements.yaml @@ -0,0 +1,7 @@ +dependencies: + - name: reqsubchart + version: 0.1.0 + repository: "https://example.com/charts" + - name: reqsubchart2 + version: 0.2.0 + repository: "https://example.com/charts" diff --git a/cmd/helm/testdata/testcharts/reqtest/values.yaml b/cmd/helm/testdata/testcharts/reqtest/values.yaml new file mode 100644 index 000000000..d57f76b07 --- /dev/null +++ b/cmd/helm/testdata/testcharts/reqtest/values.yaml @@ -0,0 +1,4 @@ +# Default values for reqtest. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name: value diff --git a/docs/charts.md b/docs/charts.md index 104c3ad18..7b5e1ee67 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -125,6 +125,7 @@ chart's `charts/` directory: ``` wordpress: Chart.yaml + requirements.yaml # ... charts/ apache/ @@ -142,6 +143,61 @@ directory. **TIP:** _To drop a dependency into your `charts/` directory, use the `helm fetch` command._ +### Managing Dependencies with `requirements.yaml` + +While Helm will allow you to manually manage your dependencies, the +preferred method of declaring dependencies is by using a +`requirements.yaml` file inside of your chart. + +A `requirements.yaml` file is a simple file for listing your +dependencies. + +```yaml +dependencies: + - name: apache + version: 1.2.3 + repository: http://example.com/charts + - name: mysql + version: 3.2.1 + repository: http://another.example.com/charts +``` + +- The `name` field is the name of the chart you want. +- The `version` field is the version of the chart you want. +- The `repository` field is the full URL to the chart repository. Note + that you must also use `helm repo add` to add that repo locally. + +Once you have a dependencies file, you can run `helm dependency update` +and it will use your dependency file to download all of the specified +charts into your `charts/` directory for you. + +```console +$ helm dep up foochart +Hang tight while we grab the latest from your chart repositories... +...Successfully got an update from the "local" chart repository +...Successfully got an update from the "stable" chart repository +...Successfully got an update from the "example" chart repository +...Successfully got an update from the "another" chart repository +Update Complete. Happy Helming! +Saving 2 charts +Downloading apache from repo http://example.com/charts +Downloading mysql from repo http://another.example.com/charts +``` + +When `helm dependency update` retrieves charts, it will store them as +chart archives in the `charts/` directory. So for the example above, one +would expect to see the following files in the charts directory: + +``` +charts/ + apache-1.2.3.tgz + mysql-3.2.1.tgz +``` + +Manging charts with `requirements.yaml` is a good way to easily keep +charts updated, and also share requirements information throughout a +team. + ## Templates and Values By default, Helm Chart templates are written in the Go template language, with the diff --git a/pkg/chartutil/load_test.go b/pkg/chartutil/load_test.go index 822e8d078..3b785d20b 100644 --- a/pkg/chartutil/load_test.go +++ b/pkg/chartutil/load_test.go @@ -29,6 +29,7 @@ func TestLoadDir(t *testing.T) { } verifyFrobnitz(t, c) verifyChart(t, c) + verifyRequirements(t, c) } func TestLoadFile(t *testing.T) { @@ -38,6 +39,7 @@ func TestLoadFile(t *testing.T) { } verifyFrobnitz(t, c) verifyChart(t, c) + verifyRequirements(t, c) } func verifyChart(t *testing.T, c *chart.Chart) { @@ -49,7 +51,7 @@ func verifyChart(t *testing.T, c *chart.Chart) { t.Errorf("Expected 1 template, got %d", len(c.Templates)) } - numfiles := 6 + numfiles := 7 if len(c.Files) != numfiles { t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files)) for _, n := range c.Files { @@ -88,6 +90,32 @@ func verifyChart(t *testing.T, c *chart.Chart) { } +func verifyRequirements(t *testing.T, c *chart.Chart) { + r, err := LoadRequirements(c) + if err != nil { + t.Fatal(err) + } + if len(r.Dependencies) != 2 { + t.Errorf("Expected 2 requirements, got %d", len(r.Dependencies)) + } + tests := []*Dependency{ + {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, + {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + } + for i, tt := range tests { + d := r.Dependencies[i] + if d.Name != tt.Name { + t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name) + } + if d.Version != tt.Version { + t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) + } + if d.Repository != tt.Repository { + t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) + } + } +} + func verifyFrobnitz(t *testing.T, c *chart.Chart) { verifyChartfile(t, c.Metadata) diff --git a/pkg/chartutil/requirements.go b/pkg/chartutil/requirements.go new file mode 100644 index 000000000..4a452f64c --- /dev/null +++ b/pkg/chartutil/requirements.go @@ -0,0 +1,84 @@ +/* +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 chartutil + +import ( + "errors" + "time" + + "github.com/ghodss/yaml" + + "k8s.io/helm/pkg/proto/hapi/chart" +) + +// Dependency describes a chart upon which another chart depends. +// +// Dependencies can be used to express developer intent, or to capture the state +// of a chart. +type Dependency struct { + // Name is the name of the dependency. + // + // This must mach the name in the dependency's Chart.yaml. + Name string `json:"name"` + // Version is the version (range) of this chart. + // + // A lock file will always produce a single version, while a dependency + // may contain a semantic version range. + Version string `json:"version,omitempty"` + // The URL to the repository. + // + // Appending `index.yaml` to this string should result in a URL that can be + // used to fetch the repository index. + Repository string `json:"repository"` +} + +// Requirements is a list of requirements for a chart. +// +// Requirements are charts upon which this chart depends. This expresses +// developer intent. +type Requirements struct { + Dependencies []*Dependency `json:"dependencies"` +} + +// RequirementsLock is a lock file for requirements. +// +// It represents the state that the dependencies should be in. +type RequirementsLock struct { + // Genderated is the date the lock file was last generated. + Generated time.Time `json:"generated"` + // Digest is a hash of the requirements file used to generate it. + Digest string `json:"digest"` + // Dependencies is the list of dependencies that this lock file has locked. + Dependencies []*Dependency `json:"dependencies"` +} + +// ErrRequirementsNotFound indicates that a requirements.yaml is not found. +var ErrRequirementsNotFound = errors.New("requirements.yaml not found") + +// LoadRequirements loads a requirements file from an in-memory chart. +func LoadRequirements(c *chart.Chart) (*Requirements, error) { + var data []byte + for _, f := range c.Files { + if f.TypeUrl == "requirements.yaml" { + data = f.Value + } + } + if len(data) == 0 { + return nil, ErrRequirementsNotFound + } + r := &Requirements{} + return r, yaml.Unmarshal(data, r) +} diff --git a/pkg/chartutil/requirements_test.go b/pkg/chartutil/requirements_test.go new file mode 100644 index 000000000..04f1bd4a6 --- /dev/null +++ b/pkg/chartutil/requirements_test.go @@ -0,0 +1,27 @@ +/* +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 chartutil + +import ( + "testing" +) + +func TestLoadRequirements(t *testing.T) { + c, err := Load("testdata/frobnitz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyRequirements(t, c) +} diff --git a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz index 8055a0094583776ac76723b5bd687cae2e22e36f..27bec72bd85b6a1a2df341d237ba554c6d751200 100644 GIT binary patch literal 3974 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 diff --git a/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz index 03de29d688dc070e04f6f391c0f7525ff172a2bc..408f3bd5ccb61041ca3abe9a7577b651a0e72660 100644 GIT binary patch literal 1025 zcmV+c1pfOUiwFQBI@?zO1MQc4Y!pQt$1j*vT_1mmpb2(zXqATU?R#|_tXQKYRUv_d z5H#uB-Q3;QeOzbe3g@dau>@=?v9XB>(Eh>8M<5lo7!M*ugHj(Th!GH9F*ZdTqlCs- zyt=pdun1a-*F(d6?vmNNot>H8@9+2h&Ds!~4#J_p6e0+MuByOqwR|f`O1>=sL`l~) zO;!a_27;(aq8I?`R4%GWicpBXrPz?taEe9}2%oc`*DV(Nn{f*FQELAu*aiI?6A)8A z4Q;FZb9m1~Q+e7t8K4jaTojQAHj~tJ z9Rth<3y@`lNG>u!%*CK76-L-W6cMfsVd8gEc$t^Dgqs^B800SW?J&-hV*dLolc^~K z=kNcjqN~jRD?w3zj9ODBMmBONDR&u|)c;i}ufM8FimrS5>zXVv|E~m5ltfMxIS~^P zg9D%r2e#3YFdY^GxRdlB)UotBS13O3wc^ zMPUA45oUwCPz|0l7vZs_l02D{QK^$^UmfT6pDhF>3M&t7{pDQ zNQM1cKwaCav!E0AkGR|f5es7E7nXdSfh06B0n`QI+%X~Ec&6k1OuH!zN>-GcKVOZ4 z{$;F&l%KQx-)kZ$;aX8I0BEutZZKK?3$l2o|4X9xVfkMPCT93V$C|Tr%?M@s_v1%&M-kl@A9gnVF z(6s;QZN_WQb}oKo-^%S*)GpckLi5`lbDyfO*}CrF^AByj+^p?d^YV#rb_{$Se)AK1 z_v7*PCyiC3YlX&a=g}u`ZP&Uoqx!+|MR)Gn&~PB#*Uod>I_>7Lhr&-d34{IH^G`PiP`HwIU2=oXZU|H#Ztwo}@YurP^^3+`_-@1WA7>i%@9pS1+0%;lJvSU4Y21J97f1RZ vwCBE9Kk;$yaYv_xE?rg|xaC3#%3v@U3Wm6jxDq!wXS2UM1phQ}PJKj8Eb z0lColcg{#GD$%P<%*~;`9l-D~Gcmy(|AvO<;P^K&Ha9mhH37!InW2d>gMtC|oq`EQ zc>L!j=B8RHKvMx%N@{U(QD#9&W`3TPf}?^*YEG^~GALyzq~#YWc$X%n7UiXuq!x3P zr4|)~6`AN6>ltxn#n+5=(`rpXZ46XfV zW@2D8TL04nC@Z+6rX`lqq+^!3pN%7OwaHANvU zGbdF~A)}K+1Nxni7h+SL^G%qs`$gRvTEmD9NMIiej?Gq)iw$bJV^#Nx252TV)GxBp%fC-?a zfEuL%q5N-PY=~O^8yN!OX#S@MK+fUFF+ZBsMg~A@|0m`oC6*NB7Z>Xq=o#u6=#`{b zQEmsJ{BLS(49fpz#%87lW`+j9{0~%bI-39K0pz>V2u!02J1KH*8g ztxKQt*{bbZzk&0=?prMu^=7@hznNe9AKs3om&0l9b(feQJ zKkLgkCh_i8h`w1Sx$XCkue&GNZg77swe3wgTlDV(^LCwTo7=nXET7qy<9;U3i%(_U z?>C;EYQIub@4V%6716KH?WZ!_`M=%ZnSAc%ay8kvtMeQ4pXe92 z{>e`~^FJXb{kEnYgZ$O{Nx!cC_J27i;D3JN3477^5?0oyH}!s=T9|rl#>=F)&L_q0 z-P{nHtFy`)IXDmGe`C^TkJMt&7^nyx1*2dTjDk@x3P!;w7zLwX6pVsVFbYP&C>RB! P00{s9M%sWT04M+eiqO^z diff --git a/pkg/chartutil/testdata/frobnitz/requirements.yaml b/pkg/chartutil/testdata/frobnitz/requirements.yaml new file mode 100644 index 000000000..5eb0bc98b --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/requirements.yaml @@ -0,0 +1,7 @@ +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz b/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz index 8b1b19dce8a88243fcbdfe46415d6bcf86d1b8a9..97e0b2eb844e4a2bc6571cc215a30798605a8619 100644 GIT binary patch delta 16 Xcmcc3beoA?zMF$V$?En-_7Fw@D`W(? delta 16 Xcmcc3beoA?zMF%gBk=r2_7Fw@E_?+k diff --git a/pkg/chartutil/testdata/mariner/requirements.yaml b/pkg/chartutil/testdata/mariner/requirements.yaml new file mode 100644 index 000000000..0b21d15b7 --- /dev/null +++ b/pkg/chartutil/testdata/mariner/requirements.yaml @@ -0,0 +1,4 @@ +dependencies: + - name: albatross + repository: https://example.com/mariner/charts + version: "0.1.0" diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go index ab0fa6ebe..ae4c6d2b6 100644 --- a/pkg/provenance/sign.go +++ b/pkg/provenance/sign.go @@ -204,7 +204,7 @@ func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) { ver.SignedBy = by // Second, verify the hash of the tarball. - sum, err := sumArchive(chartpath) + sum, err := DigestFile(chartpath) if err != nil { return ver, err } @@ -254,7 +254,7 @@ func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, er func messageBlock(chartpath string) (*bytes.Buffer, error) { var b *bytes.Buffer // Checksum the archive - chash, err := sumArchive(chartpath) + chash, err := DigestFile(chartpath) if err != nil { return b, err } @@ -332,20 +332,26 @@ func loadKeyRing(ringpath string) (openpgp.EntityList, error) { return openpgp.ReadKeyRing(f) } -// sumArchive calculates a SHA256 hash (like Docker) for a given file. +// DigestFile calculates a SHA256 hash (like Docker) for a given file. // // It takes the path to the archive file, and returns a string representation of // the SHA256 sum. // // The intended use of this function is to generate a sum of a chart TGZ file. -func sumArchive(filename string) (string, error) { +func DigestFile(filename string) (string, error) { f, err := os.Open(filename) if err != nil { return "", err } defer f.Close() + return Digest(f) +} +// Digest hashes a reader and returns a SHA256 digest. +// +// Helm uses SHA256 as its default hash for all non-cryptographic applications. +func Digest(in io.Reader) (string, error) { hash := crypto.SHA256.New() - io.Copy(hash, f) + io.Copy(hash, in) return hex.EncodeToString(hash.Sum(nil)), nil } diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go index 2f66748c1..00bc5aced 100644 --- a/pkg/provenance/sign_test.go +++ b/pkg/provenance/sign_test.go @@ -127,6 +127,28 @@ func TestLoadKeyRing(t *testing.T) { } } +func TestDigest(t *testing.T) { + f, err := os.Open(testChartfile) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + hash, err := Digest(f) + if err != nil { + t.Fatal(err) + } + + sig, err := readSumFile(testSumfile) + if err != nil { + t.Fatal(err) + } + + if !strings.Contains(sig, hash) { + t.Errorf("Expected %s to be in %s", hash, sig) + } +} + func TestNewFromFiles(t *testing.T) { s, err := NewFromFiles(testKeyfile, testPubfile) if err != nil { @@ -138,8 +160,8 @@ func TestNewFromFiles(t *testing.T) { } } -func TestSumArchive(t *testing.T) { - hash, err := sumArchive(testChartfile) +func TestDigestFile(t *testing.T) { + hash, err := DigestFile(testChartfile) if err != nil { t.Fatal(err) } diff --git a/pkg/repo/index.go b/pkg/repo/index.go index 5e9a08837..b7aade1dc 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -19,11 +19,14 @@ package repo import ( "io/ioutil" "net/http" + "path/filepath" "strings" "gopkg.in/yaml.v2" + "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/provenance" ) var indexPath = "index.yaml" @@ -33,14 +36,61 @@ type IndexFile struct { Entries map[string]*ChartRef } +// NewIndexFile initializes an index. +func NewIndexFile() *IndexFile { + return &IndexFile{Entries: map[string]*ChartRef{}} +} + +// Add adds a file to the index +func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { + name := strings.TrimSuffix(filename, ".tgz") + cr := &ChartRef{ + Name: name, + URL: baseURL + "/" + filename, + Chartfile: md, + Digest: digest, + // FIXME: Need to add Created + } + i.Entries[name] = cr +} + +// Need both JSON and YAML annotations until we get rid of gopkg.in/yaml.v2 + // ChartRef represents a chart entry in the IndexFile type ChartRef struct { - Name string `yaml:"name"` - URL string `yaml:"url"` - Created string `yaml:"created,omitempty"` - Removed bool `yaml:"removed,omitempty"` - Digest string `yaml:"digest,omitempty"` - Chartfile *chart.Metadata `yaml:"chartfile"` + Name string `yaml:"name" json:"name"` + URL string `yaml:"url" json:"url"` + Created string `yaml:"created,omitempty" json:"created,omitempty"` + Removed bool `yaml:"removed,omitempty" json:"removed,omitempty"` + Digest string `yaml:"digest,omitempty" json:"digest,omitempty"` + Chartfile *chart.Metadata `yaml:"chartfile" json:"chartfile"` +} + +// IndexDirectory reads a (flat) directory and generates an index. +// +// It indexes only charts that have been packaged (*.tgz). +// +// It writes the results to dir/index.yaml. +func IndexDirectory(dir, baseURL string) (*IndexFile, error) { + archives, err := filepath.Glob(filepath.Join(dir, "*.tgz")) + if err != nil { + return nil, err + } + index := NewIndexFile() + for _, arch := range archives { + fname := filepath.Base(arch) + c, err := chartutil.Load(arch) + if err != nil { + // Assume this is not a chart. + continue + } + hash, err := provenance.DigestFile(arch) + if err != nil { + return index, err + } + index.Add(c.Metadata, fname, baseURL, hash) + } + return index, nil } // DownloadIndexFile uses @@ -72,9 +122,7 @@ func DownloadIndexFile(repoName, url, indexFilePath string) error { func (i *IndexFile) UnmarshalYAML(unmarshal func(interface{}) error) error { var refs map[string]*ChartRef if err := unmarshal(&refs); err != nil { - if _, ok := err.(*yaml.TypeError); !ok { - return err - } + return err } i.Entries = refs return nil @@ -101,11 +149,11 @@ func LoadIndexFile(path string) (*IndexFile, error) { return nil, err } - var indexfile IndexFile - err = yaml.Unmarshal(b, &indexfile) + indexfile := NewIndexFile() + err = yaml.Unmarshal(b, indexfile) if err != nil { return nil, err } - return &indexfile, nil + return indexfile, nil } diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go index 4fd4c255b..3b248fb19 100644 --- a/pkg/repo/index_test.go +++ b/pkg/repo/index_test.go @@ -110,3 +110,35 @@ func TestLoadIndexFile(t *testing.T) { t.Errorf("alpine entry was not decoded properly") } } + +func TestIndexDirectory(t *testing.T) { + dir := "testdata/repository" + index, err := IndexDirectory(dir, "http://localhost:8080") + if err != nil { + t.Fatal(err) + } + + if l := len(index.Entries); l != 2 { + t.Fatalf("Expected 2 entries, got %d", l) + } + + // Other things test the entry generation more thoroughly. We just test a + // few fields. + cname := "frobnitz-1.2.3" + frob, ok := index.Entries[cname] + if !ok { + t.Fatalf("Could not read chart %s", cname) + } + if len(frob.Digest) == 0 { + t.Errorf("Missing digest of file %s.", frob.Name) + } + if frob.Chartfile == nil { + t.Fatalf("Chartfile %s not added to index.", cname) + } + if frob.URL != "http://localhost:8080/frobnitz-1.2.3.tgz" { + t.Errorf("Unexpected URL: %s", frob.URL) + } + if frob.Chartfile.Name != "frobnitz" { + t.Errorf("Expected frobnitz, got %q", frob.Chartfile.Name) + } +} From e0d02e6e5b729e6e1c395edf652ad411fb8e5bd8 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Fri, 23 Sep 2016 14:48:19 -0700 Subject: [PATCH 103/183] feat(*): add api version checks --- cmd/tiller/release_server.go | 41 +++++++++++++++++++++++++++ cmd/tiller/release_server_test.go | 37 +++++++++++++------------ pkg/helm/client.go | 5 +--- pkg/helm/option.go | 34 +++++++++++------------ pkg/version/compatible.go | 46 +++++++++++++++++++++++++++++++ pkg/version/compatible_test.go | 43 +++++++++++++++++++++++++++++ pkg/version/doc.go | 18 ++++++++++++ pkg/version/version.go | 1 - 8 files changed, 184 insertions(+), 41 deletions(-) create mode 100644 pkg/version/compatible.go create mode 100644 pkg/version/compatible_test.go create mode 100644 pkg/version/doc.go diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 3d8267715..8820afbda 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -25,6 +25,8 @@ import ( "sort" "strings" + "google.golang.org/grpc/metadata" + "github.com/ghodss/yaml" "github.com/technosophos/moniker" ctx "golang.org/x/net/context" @@ -67,6 +69,8 @@ var ( errMissingChart = errors.New("no chart provided") // errMissingRelease indicates that a release (name) was not provided. errMissingRelease = errors.New("no release provided") + // errIncompatibleVersion indicates incompatible client/server versions. + errIncompatibleVersion = errors.New("client version is incompatible") ) // ListDefaultLimit is the default limit for number of items returned in a list. @@ -76,7 +80,19 @@ type releaseServer struct { env *environment.Environment } +func getVersion(c ctx.Context) string { + if md, ok := metadata.FromContext(c); ok { + if v, ok := md["x-helm-api-client"]; ok { + return v[0] + } + } + return "" +} + func (s *releaseServer) ListReleases(req *services.ListReleasesRequest, stream services.ReleaseService_ListReleasesServer) error { + if !checkClientVersion(stream.Context()) { + return errIncompatibleVersion + } if len(req.StatusCodes) == 0 { req.StatusCodes = []release.Status_Code{release.Status_DEPLOYED} @@ -181,7 +197,16 @@ func (s *releaseServer) GetVersion(c ctx.Context, req *services.GetVersionReques return &services.GetVersionResponse{Version: v}, nil } +func checkClientVersion(c ctx.Context) bool { + v := getVersion(c) + return version.IsCompatible(v, version.Version) +} + func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetReleaseStatusRequest) (*services.GetReleaseStatusResponse, error) { + if !checkClientVersion(c) { + return nil, errIncompatibleVersion + } + if req.Name == "" { return nil, errMissingRelease } @@ -225,6 +250,10 @@ func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetRelease } func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleaseContentRequest) (*services.GetReleaseContentResponse, error) { + if !checkClientVersion(c) { + return nil, errIncompatibleVersion + } + if req.Name == "" { return nil, errMissingRelease } @@ -238,6 +267,10 @@ func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleas } func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) { + if !checkClientVersion(c) { + return nil, errIncompatibleVersion + } + currentRelease, updatedRelease, err := s.prepareUpdate(req) if err != nil { return nil, err @@ -402,6 +435,10 @@ func (s *releaseServer) engine(ch *chart.Chart) environment.Engine { } func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) { + if !checkClientVersion(c) { + return nil, errIncompatibleVersion + } + rel, err := s.prepareRelease(req) if err != nil { log.Printf("Failed install prepare step: %s", err) @@ -631,6 +668,10 @@ func (s *releaseServer) execHook(hs []*release.Hook, name, namespace, hook strin } func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) { + if !checkClientVersion(c) { + return nil, errIncompatibleVersion + } + if req.Name == "" { log.Printf("uninstall: Release not found: %s", req.Name) return nil, errMissingRelease diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 594bd865d..7db3cac83 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -30,6 +30,7 @@ import ( "google.golang.org/grpc/metadata" "k8s.io/helm/cmd/tiller/environment" + "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/services" @@ -175,7 +176,7 @@ func TestUniqName(t *testing.T) { } func TestInstallRelease(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() // TODO: Refactor this into a mock. @@ -235,7 +236,7 @@ func TestInstallRelease(t *testing.T) { } func TestInstallReleaseWithNotes(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() // TODO: Refactor this into a mock. @@ -300,7 +301,7 @@ func TestInstallReleaseWithNotes(t *testing.T) { } func TestInstallReleaseWithNotesRendered(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() // TODO: Refactor this into a mock. @@ -366,7 +367,7 @@ func TestInstallReleaseWithNotesRendered(t *testing.T) { } func TestInstallReleaseDryRun(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() req := &services.InstallReleaseRequest{ @@ -415,7 +416,7 @@ func TestInstallReleaseDryRun(t *testing.T) { } func TestInstallReleaseNoHooks(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -434,7 +435,7 @@ func TestInstallReleaseNoHooks(t *testing.T) { } func TestInstallReleaseFailedHooks(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) rs.env.KubeClient = newHookFailingKubeClient() @@ -453,7 +454,7 @@ func TestInstallReleaseFailedHooks(t *testing.T) { } func TestInstallReleaseReuseName(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rel.Info.Status.Code = release.Status_DELETED @@ -484,7 +485,7 @@ func TestInstallReleaseReuseName(t *testing.T) { } func TestUpdateRelease(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rs.env.Releases.Create(rel) @@ -554,7 +555,7 @@ func TestUpdateRelease(t *testing.T) { } func TestUpdateReleaseNoHooks(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rs.env.Releases.Create(rel) @@ -583,7 +584,7 @@ func TestUpdateReleaseNoHooks(t *testing.T) { } func TestUpdateReleaseNoChanges(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rs.env.Releases.Create(rel) @@ -601,7 +602,7 @@ func TestUpdateReleaseNoChanges(t *testing.T) { } func TestUninstallRelease(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -632,7 +633,7 @@ func TestUninstallRelease(t *testing.T) { } func TestUninstallPurgeRelease(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -664,7 +665,7 @@ func TestUninstallPurgeRelease(t *testing.T) { } func TestUninstallPurgeDeleteRelease(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -689,7 +690,7 @@ func TestUninstallPurgeDeleteRelease(t *testing.T) { } func TestUninstallReleaseNoHooks(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -710,7 +711,7 @@ func TestUninstallReleaseNoHooks(t *testing.T) { } func TestGetReleaseContent(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() if err := rs.env.Releases.Create(rel); err != nil { @@ -728,7 +729,7 @@ func TestGetReleaseContent(t *testing.T) { } func TestGetReleaseStatus(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() if err := rs.env.Releases.Create(rel); err != nil { @@ -746,7 +747,7 @@ func TestGetReleaseStatus(t *testing.T) { } func TestGetReleaseStatusDeleted(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rel.Info.Status.Code = release.Status_DELETED @@ -960,7 +961,7 @@ func (l *mockListServer) Send(res *services.ListReleasesResponse) error { return nil } -func (l *mockListServer) Context() context.Context { return context.TODO() } +func (l *mockListServer) Context() context.Context { return helm.NewContext() } func (l *mockListServer) SendMsg(v interface{}) error { return nil } func (l *mockListServer) RecvMsg(v interface{}) error { return nil } func (l *mockListServer) SendHeader(m metadata.MD) error { return nil } diff --git a/pkg/helm/client.go b/pkg/helm/client.go index bf55562a0..fc47687f7 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -17,8 +17,6 @@ limitations under the License. package helm // import "k8s.io/helm/pkg/helm" import ( - "os" - "google.golang.org/grpc" "k8s.io/helm/pkg/chartutil" @@ -59,8 +57,7 @@ func (h *Client) Option(opts ...Option) *Client { // Init initializes the helm client with default options func (h *Client) Init() *Client { - return h.Option(Host(DefaultHelmHost)). - Option(Home(os.ExpandEnv(DefaultHelmHome))) + return h } // ListReleases lists the current releases. diff --git a/pkg/helm/option.go b/pkg/helm/option.go index 269001742..c83185f12 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -18,9 +18,12 @@ package helm import ( "golang.org/x/net/context" + "google.golang.org/grpc/metadata" + cpb "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/release" rls "k8s.io/helm/pkg/proto/hapi/services" + "k8s.io/helm/pkg/version" ) // Option allows specifying various settings configurable by @@ -30,12 +33,8 @@ type Option func(*options) // options specify optional settings used by the helm client. type options struct { - // value of helm host override - home string // value of helm home override host string - // name of chart - chart string // if set dry-run helm client calls dryRun bool // if set, re-use an existing name @@ -56,13 +55,6 @@ type options struct { contentReq rls.GetReleaseContentRequest } -// Home specifies the location of helm home, (default = "$HOME/.helm"). -func Home(home string) Option { - return func(opts *options) { - opts.home = home - } -} - // Host specifies the host address of the Tiller release server, (default = ":44134"). func Host(host string) Option { return func(opts *options) { @@ -249,7 +241,7 @@ func (o *options) rpcListReleases(rlc rls.ReleaseServiceClient, opts ...ReleaseL for _, opt := range opts { opt(o) } - s, err := rlc.ListReleases(context.TODO(), &o.listReq) + s, err := rlc.ListReleases(NewContext(), &o.listReq) if err != nil { return nil, err } @@ -257,6 +249,12 @@ func (o *options) rpcListReleases(rlc rls.ReleaseServiceClient, opts ...ReleaseL return s.Recv() } +// NewContext creates a versioned context. +func NewContext() context.Context { + md := metadata.Pairs("x-helm-api-client", version.Version) + return metadata.NewContext(context.TODO(), md) +} + // Executes tiller.InstallRelease RPC. func (o *options) rpcInstallRelease(chr *cpb.Chart, rlc rls.ReleaseServiceClient, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) { // apply the install options @@ -269,7 +267,7 @@ func (o *options) rpcInstallRelease(chr *cpb.Chart, rlc rls.ReleaseServiceClient o.instReq.DisableHooks = o.disableHooks o.instReq.ReuseName = o.reuseName - return rlc.InstallRelease(context.TODO(), &o.instReq) + return rlc.InstallRelease(NewContext(), &o.instReq) } // Executes tiller.UninstallRelease RPC. @@ -289,7 +287,7 @@ func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient, o.uninstallReq.Name = rlsName o.uninstallReq.DisableHooks = o.disableHooks - return rlc.UninstallRelease(context.TODO(), &o.uninstallReq) + return rlc.UninstallRelease(NewContext(), &o.uninstallReq) } // Executes tiller.UpdateRelease RPC. @@ -302,7 +300,7 @@ func (o *options) rpcUpdateRelease(rlsName string, chr *cpb.Chart, rlc rls.Relea o.updateReq.DryRun = o.dryRun o.updateReq.Name = rlsName - return rlc.UpdateRelease(context.TODO(), &o.updateReq) + return rlc.UpdateRelease(NewContext(), &o.updateReq) } // Executes tiller.GetReleaseStatus RPC. @@ -311,7 +309,7 @@ func (o *options) rpcGetReleaseStatus(rlsName string, rlc rls.ReleaseServiceClie opt(o) } o.statusReq.Name = rlsName - return rlc.GetReleaseStatus(context.TODO(), &o.statusReq) + return rlc.GetReleaseStatus(NewContext(), &o.statusReq) } // Executes tiller.GetReleaseContent. @@ -320,11 +318,11 @@ func (o *options) rpcGetReleaseContent(rlsName string, rlc rls.ReleaseServiceCli opt(o) } o.contentReq.Name = rlsName - return rlc.GetReleaseContent(context.TODO(), &o.contentReq) + return rlc.GetReleaseContent(NewContext(), &o.contentReq) } // Executes tiller.GetVersion RPC. func (o *options) rpcGetVersion(rlc rls.ReleaseServiceClient, opts ...VersionOption) (*rls.GetVersionResponse, error) { req := &rls.GetVersionRequest{} - return rlc.GetVersion(context.TODO(), req) + return rlc.GetVersion(NewContext(), req) } diff --git a/pkg/version/compatible.go b/pkg/version/compatible.go new file mode 100644 index 000000000..4a7b0d4bc --- /dev/null +++ b/pkg/version/compatible.go @@ -0,0 +1,46 @@ +/* +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 // import "k8s.io/helm/pkg/version" + +import ( + "fmt" + + "github.com/Masterminds/semver" +) + +// IsCompatible tests if a client and server version are compatible. +func IsCompatible(client, server string) bool { + cv, err := semver.NewVersion(client) + if err != nil { + return false + } + sv, err := semver.NewVersion(server) + if err != nil { + return false + } + + constraint := fmt.Sprintf("^%d.%d.x", cv.Major(), cv.Minor()) + if cv.Prerelease() != "" || sv.Prerelease() != "" { + constraint = cv.String() + } + + c, err := semver.NewConstraint(constraint) + if err != nil { + return false + } + return c.Check(sv) +} diff --git a/pkg/version/compatible_test.go b/pkg/version/compatible_test.go new file mode 100644 index 000000000..3b92fa25f --- /dev/null +++ b/pkg/version/compatible_test.go @@ -0,0 +1,43 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package version represents the current version of the project. +package version // import "k8s.io/helm/pkg/version" + +import "testing" + +func TestIsCompatible(t *testing.T) { + tests := []struct { + client string + server string + expected bool + }{ + {"v2.0.0-alpha.4", "v2.0.0-alpha.4", true}, + {"v2.0.0-alpha.3", "v2.0.0-alpha.4", false}, + {"v2.0.0", "v2.0.0-alpha.4", false}, + {"v2.0.0-alpha.4", "v2.0.0", false}, + {"v2.0.0", "v2.0.1", true}, + {"v2.0.1", "v2.0.0", true}, + {"v2.0.0", "v2.1.1", true}, + {"v2.1.0", "v2.0.1", false}, + } + + for _, tt := range tests { + if IsCompatible(tt.client, tt.server) != tt.expected { + t.Errorf("expected client(%s) and server(%s) to be %v", tt.client, tt.server, tt.expected) + } + } +} diff --git a/pkg/version/doc.go b/pkg/version/doc.go new file mode 100644 index 000000000..23c9e500d --- /dev/null +++ b/pkg/version/doc.go @@ -0,0 +1,18 @@ +/* +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" diff --git a/pkg/version/version.go b/pkg/version/version.go index eb96bd7c8..6da49d843 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -14,7 +14,6 @@ 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 "k8s.io/helm/pkg/proto/hapi/version" From 593718d74910c6d180b05b52e97cc20c8bb18d66 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Fri, 16 Sep 2016 22:19:23 -0600 Subject: [PATCH 104/183] feat(helm): add 'helm dependency' commands This also refactors significant portions of the CLI, moving much of the shared code into a library. Also in this release, a testing repository server has been added. --- cmd/helm/dependency.go | 26 +- cmd/helm/dependency_build.go | 85 +++++ cmd/helm/dependency_build_test.go | 115 ++++++ cmd/helm/dependency_update.go | 225 +----------- cmd/helm/dependency_update_test.go | 114 +----- cmd/helm/downloader/chart_downloader.go | 196 +++++++++++ cmd/helm/downloader/chart_downloader_test.go | 149 ++++++++ cmd/helm/downloader/doc.go | 23 ++ cmd/helm/downloader/manager.go | 330 ++++++++++++++++++ .../downloader/testdata/helm-test-key.pub | Bin 0 -> 1243 bytes .../downloader/testdata/helm-test-key.secret | Bin 0 -> 2545 bytes .../cache/kubernetes-charts-index.yaml | 38 ++ .../repository/cache/local-index.yaml | 1 + .../helmhome/repository/local/index.yaml | 0 .../helmhome/repository/repositories.yaml | 1 + .../downloader/testdata/signtest-0.1.0.tgz | Bin 0 -> 471 bytes .../testdata/signtest-0.1.0.tgz.prov | 20 ++ .../downloader/testdata/signtest/.helmignore | 5 + .../downloader/testdata/signtest/Chart.yaml | 3 + .../testdata/signtest/alpine/Chart.yaml | 6 + .../testdata/signtest/alpine/README.md | 9 + .../signtest/alpine/templates/alpine-pod.yaml | 16 + .../testdata/signtest/alpine/values.yaml | 2 + .../testdata/signtest/templates/pod.yaml | 10 + .../downloader/testdata/signtest/values.yaml | 0 cmd/helm/fetch.go | 187 +++------- cmd/helm/fetch_test.go | 128 +++++-- cmd/helm/helmpath/helmhome.go | 61 ++++ cmd/helm/helmpath/helmhome_test.go | 36 ++ cmd/helm/install.go | 16 +- cmd/helm/repo_add_test.go | 2 +- cmd/helm/resolver/resolver.go | 13 +- cmd/helm/resolver/resolver_test.go | 6 +- cmd/helm/verify.go | 5 +- pkg/chartutil/expand.go | 10 + pkg/chartutil/load.go | 4 + pkg/chartutil/load_test.go | 27 +- pkg/chartutil/requirements.go | 32 +- pkg/chartutil/requirements_test.go | 8 + pkg/chartutil/testdata/frobnitz-1.2.3.tgz | Bin 3974 -> 4025 bytes .../frobnitz/charts/mariner-4.3.2.tgz | Bin 1025 -> 1029 bytes .../testdata/frobnitz/requirements.lock | 8 + .../mariner/charts/albatross-0.1.0.tgz | Bin 347 -> 347 bytes pkg/repo/index.go | 4 +- pkg/repo/repo.go | 7 +- pkg/repo/repotest/doc.go | 20 ++ pkg/repo/repotest/server.go | 130 +++++++ pkg/repo/repotest/server_test.go | 107 ++++++ .../repotest/testdata/examplechart-0.1.0.tgz | Bin 0 -> 558 bytes .../testdata/examplechart/.helmignore | 21 ++ .../repotest/testdata/examplechart/Chart.yaml | 3 + .../testdata/examplechart/values.yaml | 4 + 52 files changed, 1697 insertions(+), 516 deletions(-) create mode 100644 cmd/helm/dependency_build.go create mode 100644 cmd/helm/dependency_build_test.go create mode 100644 cmd/helm/downloader/chart_downloader.go create mode 100644 cmd/helm/downloader/chart_downloader_test.go create mode 100644 cmd/helm/downloader/doc.go create mode 100644 cmd/helm/downloader/manager.go create mode 100644 cmd/helm/downloader/testdata/helm-test-key.pub create mode 100644 cmd/helm/downloader/testdata/helm-test-key.secret create mode 100644 cmd/helm/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml create mode 120000 cmd/helm/downloader/testdata/helmhome/repository/cache/local-index.yaml create mode 100644 cmd/helm/downloader/testdata/helmhome/repository/local/index.yaml create mode 100644 cmd/helm/downloader/testdata/helmhome/repository/repositories.yaml create mode 100644 cmd/helm/downloader/testdata/signtest-0.1.0.tgz create mode 100755 cmd/helm/downloader/testdata/signtest-0.1.0.tgz.prov create mode 100644 cmd/helm/downloader/testdata/signtest/.helmignore create mode 100755 cmd/helm/downloader/testdata/signtest/Chart.yaml create mode 100755 cmd/helm/downloader/testdata/signtest/alpine/Chart.yaml create mode 100755 cmd/helm/downloader/testdata/signtest/alpine/README.md create mode 100755 cmd/helm/downloader/testdata/signtest/alpine/templates/alpine-pod.yaml create mode 100755 cmd/helm/downloader/testdata/signtest/alpine/values.yaml create mode 100644 cmd/helm/downloader/testdata/signtest/templates/pod.yaml create mode 100644 cmd/helm/downloader/testdata/signtest/values.yaml create mode 100644 cmd/helm/helmpath/helmhome.go create mode 100644 cmd/helm/helmpath/helmhome_test.go create mode 100644 pkg/chartutil/testdata/frobnitz/requirements.lock create mode 100644 pkg/repo/repotest/doc.go create mode 100644 pkg/repo/repotest/server.go create mode 100644 pkg/repo/repotest/server_test.go create mode 100644 pkg/repo/repotest/testdata/examplechart-0.1.0.tgz create mode 100644 pkg/repo/repotest/testdata/examplechart/.helmignore create mode 100755 pkg/repo/repotest/testdata/examplechart/Chart.yaml create mode 100644 pkg/repo/repotest/testdata/examplechart/values.yaml diff --git a/cmd/helm/dependency.go b/cmd/helm/dependency.go index 2da344052..ed5df720d 100644 --- a/cmd/helm/dependency.go +++ b/cmd/helm/dependency.go @@ -27,6 +27,11 @@ import ( "k8s.io/helm/pkg/chartutil" ) +const ( + reqLock = "requirements.lock" + reqYaml = "requirements.yaml" +) + const dependencyDesc = ` Manage the dependencies of a chart. @@ -74,7 +79,7 @@ if it cannot find a requirements.yaml. func newDependencyCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ - Use: "dependency update|list", + Use: "dependency update|build|list", Aliases: []string{"dep", "dependencies"}, Short: "manage a chart's dependencies", Long: dependencyDesc, @@ -82,6 +87,7 @@ func newDependencyCmd(out io.Writer) *cobra.Command { cmd.AddCommand(newDependencyListCmd(out)) cmd.AddCommand(newDependencyUpdateCmd(out)) + cmd.AddCommand(newDependencyBuildCmd(out)) return cmd } @@ -146,10 +152,14 @@ func (l *dependencyListCmd) dependencyStatus(dep *chartutil.Dependency) string { if err != nil { return "corrupt" } - if c.Metadata.Name == dep.Name && c.Metadata.Version == dep.Version { - return "ok" + if c.Metadata.Name != dep.Name { + return "misnamed" + } + + if c.Metadata.Version != dep.Version { + return "wrong version" } - return "mismatch" + return "ok" } folder := filepath.Join(l.chartpath, "charts", dep.Name) @@ -196,6 +206,14 @@ func (l *dependencyListCmd) printMissing(reqs *chartutil.Requirements, out io.Wr } for _, f := range files { + fi, err := os.Stat(f) + if err != nil { + fmt.Fprintf(l.out, "Warning: %s\n", err) + } + // Skip anything that is not a directory and not a tgz file. + if !fi.IsDir() && filepath.Ext(f) != ".tgz" { + continue + } c, err := chartutil.Load(f) if err != nil { fmt.Fprintf(l.out, "WARNING: %q is not a chart.\n", f) diff --git a/cmd/helm/dependency_build.go b/cmd/helm/dependency_build.go new file mode 100644 index 000000000..837cad784 --- /dev/null +++ b/cmd/helm/dependency_build.go @@ -0,0 +1,85 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/cmd/helm/downloader" + "k8s.io/helm/cmd/helm/helmpath" +) + +const dependencyBuildDesc = ` +Build out the charts/ directory from the requirements.lock file. + +Build is used to reconstruct a chart's dependencies to the state specified in +the lock file. This will not re-negotiate dependencies, as 'helm dependency update' +does. + +If no lock file is found, 'helm dependency build' will mirror the behavior +of 'helm dependency update'. +` + +type dependencyBuildCmd struct { + out io.Writer + chartpath string + verify bool + keyring string + helmhome helmpath.Home +} + +func newDependencyBuildCmd(out io.Writer) *cobra.Command { + dbc := &dependencyBuildCmd{ + out: out, + } + + cmd := &cobra.Command{ + Use: "build [flags] CHART", + Short: "rebuild the charts/ directory based on the requirements.lock file", + Long: dependencyBuildDesc, + RunE: func(cmd *cobra.Command, args []string) error { + dbc.helmhome = helmpath.Home(homePath()) + dbc.chartpath = "." + + if len(args) > 0 { + dbc.chartpath = args[0] + } + return dbc.run() + }, + } + + f := cmd.Flags() + f.BoolVar(&dbc.verify, "verify", false, "Verify the packages against signatures.") + f.StringVar(&dbc.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.") + + return cmd +} + +func (d *dependencyBuildCmd) run() error { + man := &downloader.Manager{ + Out: d.out, + ChartPath: d.chartpath, + HelmHome: d.helmhome, + Keyring: d.keyring, + } + if d.verify { + man.Verify = downloader.VerifyIfPossible + } + + return man.Build() +} diff --git a/cmd/helm/dependency_build_test.go b/cmd/helm/dependency_build_test.go new file mode 100644 index 000000000..d382d68bb --- /dev/null +++ b/cmd/helm/dependency_build_test.go @@ -0,0 +1,115 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + + "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/pkg/provenance" + "k8s.io/helm/pkg/repo" + "k8s.io/helm/pkg/repo/repotest" +) + +func TestDependencyBuildCmd(t *testing.T) { + oldhome := helmHome + hh, err := tempHelmHome() + if err != nil { + t.Fatal(err) + } + helmHome = hh + defer func() { + os.RemoveAll(hh) + helmHome = oldhome + }() + + srv := repotest.NewServer(hh) + defer srv.Stop() + _, err = srv.CopyCharts("testdata/testcharts/*.tgz") + if err != nil { + t.Fatal(err) + } + + chartname := "depbuild" + if err := createTestingChart(hh, chartname, srv.URL()); err != nil { + t.Fatal(err) + } + + out := bytes.NewBuffer(nil) + dbc := &dependencyBuildCmd{out: out} + dbc.helmhome = helmpath.Home(hh) + dbc.chartpath = filepath.Join(hh, chartname) + + // In the first pass, we basically want the same results as an update. + if err := dbc.run(); err != nil { + output := out.String() + t.Logf("Output: %s", output) + t.Fatal(err) + } + + output := out.String() + if !strings.Contains(output, `update from the "test" chart repository`) { + t.Errorf("Repo did not get updated\n%s", output) + } + + // Make sure the actual file got downloaded. + expect := filepath.Join(hh, chartname, "charts/reqtest-0.1.0.tgz") + if _, err := os.Stat(expect); err != nil { + t.Fatal(err) + } + + // In the second pass, we want to remove the chart's request dependency, + // then see if it restores from the lock. + lockfile := filepath.Join(hh, chartname, "requirements.lock") + if _, err := os.Stat(lockfile); err != nil { + t.Fatal(err) + } + if err := os.RemoveAll(expect); err != nil { + t.Fatal(err) + } + + if err := dbc.run(); err != nil { + output := out.String() + t.Logf("Output: %s", output) + t.Fatal(err) + } + + // Now repeat the test that the dependency exists. + expect = filepath.Join(hh, chartname, "charts/reqtest-0.1.0.tgz") + if _, err := os.Stat(expect); err != nil { + t.Fatal(err) + } + + // Make sure that build is also fetching the correct version. + hash, err := provenance.DigestFile(expect) + if err != nil { + t.Fatal(err) + } + + i, err := repo.LoadIndexFile(cacheIndexFile("test")) + if err != nil { + t.Fatal(err) + } + + if h := i.Entries["reqtest-0.1.0"].Digest; h != hash { + t.Errorf("Failed hash match: expected %s, got %s", hash, h) + } + +} diff --git a/cmd/helm/dependency_update.go b/cmd/helm/dependency_update.go index 9b9eb1d12..30e4c62c8 100644 --- a/cmd/helm/dependency_update.go +++ b/cmd/helm/dependency_update.go @@ -16,21 +16,12 @@ limitations under the License. package main import ( - "errors" - "fmt" "io" - "io/ioutil" - "net/url" - "os" "path/filepath" - "strings" - "github.com/ghodss/yaml" "github.com/spf13/cobra" - - "k8s.io/helm/cmd/helm/resolver" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/repo" + "k8s.io/helm/cmd/helm/downloader" + "k8s.io/helm/cmd/helm/helmpath" ) const dependencyUpDesc = ` @@ -38,15 +29,16 @@ Update the on-disk dependencies to mirror the requirements.yaml file. This command verifies that the required charts, as expressed in 'requirements.yaml', are present in 'charts/' and are at an acceptable version. + +On successful update, this will generate a lock file that can be used to +rebuild the requirements to an exact version. ` // dependencyUpdateCmd describes a 'helm dependency update' type dependencyUpdateCmd struct { out io.Writer chartpath string - repoFile string - repopath string - helmhome string + helmhome helmpath.Home verify bool keyring string } @@ -74,16 +66,14 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command { return err } - duc.helmhome = homePath() - duc.repoFile = repositoriesFile() - duc.repopath = repositoryDirectory() + duc.helmhome = helmpath.Home(homePath()) return duc.run() }, } f := cmd.Flags() - f.BoolVar(&duc.verify, "verify", false, "Verify the package against its signature.") + f.BoolVar(&duc.verify, "verify", false, "Verify the packages against signatures.") f.StringVar(&duc.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.") return cmd @@ -91,197 +81,14 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command { // run runs the full dependency update process. func (d *dependencyUpdateCmd) run() error { - if fi, err := os.Stat(d.chartpath); err != nil { - return fmt.Errorf("could not find %s: %s", d.chartpath, err) - } else if !fi.IsDir() { - return errors.New("only unpacked charts can be updated") - } - c, err := chartutil.LoadDir(d.chartpath) - if err != nil { - return err - } - - req, err := chartutil.LoadRequirements(c) - if err != nil { - if err == chartutil.ErrRequirementsNotFound { - fmt.Fprintf(d.out, "No requirements found in %s/charts.\n", d.chartpath) - return nil - } - return err - } - - // For each repo in the file, update the cached copy of that repo - if _, err := d.updateRepos(req.Dependencies); err != nil { - return err - } - - // Now we need to find out which version of a chart best satisfies the - // requirements the requirements.yaml - lock, err := d.resolve(req) - if err != nil { - return err - } - - // Now we need to fetch every package here into charts/ - if err := d.downloadAll(lock.Dependencies); err != nil { - return err - } - - // Finally, we need to write the lockfile. - return writeLock(d.chartpath, lock) -} - -// 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 (d *dependencyUpdateCmd) resolve(req *chartutil.Requirements) (*chartutil.RequirementsLock, error) { - res := resolver.New(d.chartpath, d.helmhome) - return res.Resolve(req) -} - -// downloadAll takes a list of dependencies and downloads them into charts/ -func (d *dependencyUpdateCmd) downloadAll(deps []*chartutil.Dependency) error { - repos, err := loadChartRepositories(d.repopath) - if err != nil { - return err - } - - fmt.Fprintf(d.out, "Saving %d charts\n", len(deps)) - for _, dep := range deps { - fmt.Fprintf(d.out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) - - target := fmt.Sprintf("%s-%s", dep.Name, dep.Version) - churl, err := findChartURL(target, dep.Repository, repos) - if err != nil { - fmt.Fprintf(d.out, "WARNING: %s (skipped)", err) - continue - } - - dest := filepath.Join(d.chartpath, "charts", target+".tgz") - data, err := downloadChart(churl, d.verify, d.keyring) - if err != nil { - fmt.Fprintf(d.out, "WARNING: Could not download %s: %s (skipped)", churl, err) - continue - } - if err := ioutil.WriteFile(dest, data.Bytes(), 0655); err != nil { - fmt.Fprintf(d.out, "WARNING: %s (skipped)", err) - continue - } - } - return nil -} - -// updateRepos updates all of the local repos to their latest. -// -// If one of the dependencies present is not in the cached repos, this will error out. The -// consequence of that is that every repository referenced in a requirements.yaml file -// must also be added with 'helm repo add'. -func (d *dependencyUpdateCmd) updateRepos(deps []*chartutil.Dependency) (*repo.RepoFile, error) { - // TODO: In the future, we could make it so that only the repositories that - // are used by this chart are updated. As it is, we're mainly doing some sanity - // checking here. - rf, err := repo.LoadRepositoriesFile(d.repoFile) - if err != nil { - return rf, err + man := &downloader.Manager{ + Out: d.out, + ChartPath: d.chartpath, + HelmHome: d.helmhome, + Keyring: d.keyring, } - repos := rf.Repositories - - // Verify that all repositories referenced in the deps are actually known - // by Helm. - missing := []string{} - for _, dd := range deps { - found := false - if dd.Repository == "" { - found = true - } else { - for _, repo := range repos { - if urlsAreEqual(repo, dd.Repository) { - found = true - } - } - } - if !found { - missing = append(missing, dd.Repository) - } - } - - if len(missing) > 0 { - return rf, fmt.Errorf("no repository definition for %s. Try 'helm repo add'", strings.Join(missing, ", ")) - } - - if len(repos) > 0 { - // This prints errors straight to out. - updateCharts(repos, flagDebug, d.out) - } - return rf, nil -} - -// urlsAreEqual normalizes two URLs and then compares for equality. -func urlsAreEqual(a, b string) bool { - au, err := url.Parse(a) - if err != nil { - return a == b - } - bu, err := url.Parse(b) - if err != nil { - return false - } - return au.String() == bu.String() -} - -// findChartURL searches the cache of repo data for a chart that has the name and the repourl specified. -// -// In this current version, name is of the form 'foo-1.2.3'. This will change when -// the repository index stucture changes. -func findChartURL(name, repourl string, repos map[string]*repo.ChartRepository) (string, error) { - for _, cr := range repos { - if urlsAreEqual(repourl, cr.URL) { - for ename, entry := range cr.IndexFile.Entries { - if ename == name { - return entry.URL, nil - } - } - } - } - return "", fmt.Errorf("chart %s not found in %s", name, repourl) -} - -// loadChartRepositories reads the repositories.yaml, and then builds a map of -// ChartRepositories. -// -// The key is the local name (which is only present in the repositories.yaml). -func loadChartRepositories(repodir string) (map[string]*repo.ChartRepository, error) { - indices := map[string]*repo.ChartRepository{} - repoyaml := repositoriesFile() - - // Load repositories.yaml file - rf, err := repo.LoadRepositoriesFile(repoyaml) - if err != nil { - return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err) - } - - // localName: chartRepo - for lname, url := range rf.Repositories { - index, err := repo.LoadIndexFile(cacheIndexFile(lname)) - if err != nil { - return indices, err - } - - cr := &repo.ChartRepository{ - URL: url, - IndexFile: index, - } - indices[lname] = cr - } - return indices, nil -} - -// writeLock writes a lockfile to disk -func writeLock(chartpath string, lock *chartutil.RequirementsLock) error { - data, err := yaml.Marshal(lock) - if err != nil { - return err + if d.verify { + man.Verify = downloader.VerifyIfPossible } - dest := filepath.Join(chartpath, "requirements.lock") - return ioutil.WriteFile(dest, data, 0755) + return man.Update() } diff --git a/cmd/helm/dependency_update_test.go b/cmd/helm/dependency_update_test.go index b38b90a21..53df11eca 100644 --- a/cmd/helm/dependency_update_test.go +++ b/cmd/helm/dependency_update_test.go @@ -18,8 +18,6 @@ package main import ( "bytes" "io/ioutil" - "net/http" - "net/http/httptest" "os" "path/filepath" "strings" @@ -27,10 +25,12 @@ import ( "github.com/ghodss/yaml" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/provenance" "k8s.io/helm/pkg/repo" + "k8s.io/helm/pkg/repo/repotest" ) func TestDependencyUpdateCmd(t *testing.T) { @@ -40,36 +40,35 @@ func TestDependencyUpdateCmd(t *testing.T) { if err != nil { t.Fatal(err) } - helmHome = hh // Shoot me now. + helmHome = hh defer func() { os.RemoveAll(hh) helmHome = oldhome }() - srv := newTestingRepositoryServer(hh) - defer srv.stop() - copied, err := srv.copyCharts("testdata/testcharts/*.tgz") - t.Logf("Copied charts %s", strings.Join(copied, "\n")) - t.Logf("Listening for directory %s", srv.docroot) + srv := repotest.NewServer(hh) + defer srv.Stop() + copied, err := srv.CopyCharts("testdata/testcharts/*.tgz") + t.Logf("Copied charts:\n%s", strings.Join(copied, "\n")) + t.Logf("Listening on directory %s", srv.Root()) chartname := "depup" - if err := createTestingChart(hh, chartname, srv.url()); err != nil { + if err := createTestingChart(hh, chartname, srv.URL()); err != nil { t.Fatal(err) } out := bytes.NewBuffer(nil) duc := &dependencyUpdateCmd{out: out} - duc.helmhome = hh + duc.helmhome = helmpath.Home(hh) duc.chartpath = filepath.Join(hh, chartname) - duc.repoFile = filepath.Join(duc.helmhome, "repository/repositories.yaml") - duc.repopath = filepath.Join(duc.helmhome, "repository") if err := duc.run(); err != nil { + output := out.String() + t.Logf("Output: %s", output) t.Fatal(err) } output := out.String() - t.Logf("Output: %s", output) // This is written directly to stdout, so we have to capture as is. if !strings.Contains(output, `update from the "test" chart repository`) { t.Errorf("Repo did not get updated\n%s", output) @@ -98,95 +97,6 @@ func TestDependencyUpdateCmd(t *testing.T) { t.Logf("Results: %s", out.String()) } -// newTestingRepositoryServer creates a repository server for testing. -// -// docroot should be a temp dir managed by the caller. -// -// This will start the server, serving files off of the docroot. -// -// Use copyCharts to move charts into the repository and then index them -// for service. -func newTestingRepositoryServer(docroot string) *testingRepositoryServer { - root, err := filepath.Abs(docroot) - if err != nil { - panic(err) - } - srv := &testingRepositoryServer{ - docroot: root, - } - srv.start() - // Add the testing repository as the only repo. - if err := setTestingRepository(docroot, "test", srv.url()); err != nil { - panic(err) - } - return srv -} - -type testingRepositoryServer struct { - docroot string - srv *httptest.Server -} - -// copyCharts takes a glob expression and copies those charts to the server root. -func (s *testingRepositoryServer) copyCharts(origin string) ([]string, error) { - files, err := filepath.Glob(origin) - if err != nil { - return []string{}, err - } - copied := make([]string, len(files)) - for i, f := range files { - base := filepath.Base(f) - newname := filepath.Join(s.docroot, base) - data, err := ioutil.ReadFile(f) - if err != nil { - return []string{}, err - } - if err := ioutil.WriteFile(newname, data, 0755); err != nil { - return []string{}, err - } - copied[i] = newname - } - - // generate the index - index, err := repo.IndexDirectory(s.docroot, s.url()) - if err != nil { - return copied, err - } - - d, err := yaml.Marshal(index.Entries) - if err != nil { - return copied, err - } - - ifile := filepath.Join(s.docroot, "index.yaml") - err = ioutil.WriteFile(ifile, d, 0755) - return copied, err -} - -func (s *testingRepositoryServer) start() { - s.srv = httptest.NewServer(http.FileServer(http.Dir(s.docroot))) -} - -func (s *testingRepositoryServer) stop() { - s.srv.Close() -} - -func (s *testingRepositoryServer) url() string { - return s.srv.URL -} - -// setTestingRepository sets up a testing repository.yaml with only the given name/URL. -func setTestingRepository(helmhome, name, url string) error { - // Oddly, there is no repo.Save function for this. - data, err := yaml.Marshal(&map[string]string{name: url}) - if err != nil { - return err - } - os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755) - dest := filepath.Join(helmhome, "repository/repositories.yaml") - return ioutil.WriteFile(dest, data, 0666) -} - // createTestingChart creates a basic chart that depends on reqtest-0.1.0 // // The baseURL can be used to point to a particular repository server. diff --git a/cmd/helm/downloader/chart_downloader.go b/cmd/helm/downloader/chart_downloader.go new file mode 100644 index 000000000..5019739d1 --- /dev/null +++ b/cmd/helm/downloader/chart_downloader.go @@ -0,0 +1,196 @@ +/* +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 downloader + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + + "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/pkg/provenance" + "k8s.io/helm/pkg/repo" +) + +// VerificationStrategy describes a strategy for determining whether to verify a chart. +type VerificationStrategy int + +const ( + // VerifyNever will skip all verification of a chart. + VerifyNever VerificationStrategy = iota + // VerifyIfPossible will attempt a verification, it will not error if verification + // data is missing. But it will not stop processing if verification fails. + VerifyIfPossible + // VerifyAlways will always attempt a verification, and will fail if the + // verification fails. + VerifyAlways +) + +// ChartDownloader handles downloading a chart. +// +// It is capable of performing verifications on charts as well. +type ChartDownloader struct { + // Out is the location to write warning and info messages. + Out io.Writer + // Verify indicates what verification strategy to use. + Verify VerificationStrategy + // Keyring is the keyring file used for verification. + Keyring string + // HelmHome is the $HELM_HOME. + HelmHome helmpath.Home +} + +// DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file. +// +// If Verify is set to VerifyNever, the verification will be nil. +// If Verify is set to VerifyIfPossible, this will return a verification (or nil on failure), and print a warning on failure. +// If Verify is set to VerifyAlways, this will return a verification or an error if the verification fails. +// +// For VerifyNever and VerifyIfPossible, the Verification may be empty. +func (c *ChartDownloader) DownloadTo(ref string, dest string) (*provenance.Verification, error) { + // resolve URL + u, err := c.ResolveChartRef(ref) + if err != nil { + return nil, err + } + data, err := download(u.String()) + if err != nil { + return nil, err + } + + name := filepath.Base(u.Path) + destfile := filepath.Join(dest, name) + if err := ioutil.WriteFile(destfile, data.Bytes(), 0655); err != nil { + return nil, err + } + + // If provenance is requested, verify it. + ver := &provenance.Verification{} + if c.Verify > VerifyNever { + + body, err := download(u.String() + ".prov") + if err != nil { + if c.Verify == VerifyAlways { + return ver, fmt.Errorf("Failed to fetch provenance %q", u.String()+".prov") + } + fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", ref, err) + return ver, nil + } + provfile := destfile + ".prov" + if err := ioutil.WriteFile(provfile, body.Bytes(), 0655); err != nil { + return nil, err + } + + ver, err = VerifyChart(destfile, c.Keyring) + if err != nil { + // Fail always in this case, since it means the verification step + // failed. + return ver, err + } + } + return ver, nil +} + +// ResolveChartRef resolves a chart reference to a URL. +// +// A reference may be an HTTP URL, a 'reponame/chartname' reference, or a local path. +func (c *ChartDownloader) ResolveChartRef(ref string) (*url.URL, error) { + // See if it's already a full URL. + u, err := url.ParseRequestURI(ref) + if err == nil { + // If it has a scheme and host and path, it's a full URL + if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 { + return u, nil + } + return u, fmt.Errorf("Invalid chart url format: %s", ref) + } + + r, err := repo.LoadRepositoriesFile(c.HelmHome.RepositoryFile()) + if err != nil { + return u, err + } + + // See if it's of the form: repo/path_to_chart + p := strings.Split(ref, "/") + if len(p) > 1 { + if baseURL, ok := r.Repositories[p[0]]; ok { + if !strings.HasSuffix(baseURL, "/") { + baseURL = baseURL + "/" + } + return url.ParseRequestURI(baseURL + strings.Join(p[1:], "/")) + } + return u, fmt.Errorf("No such repo: %s", p[0]) + } + return u, fmt.Errorf("Invalid chart url format: %s", ref) +} + +// VerifyChart takes a path to a chart archive and a keyring, and verifies the chart. +// +// It assumes that a chart archive file is accompanied by a provenance file whose +// name is the archive file name plus the ".prov" extension. +func VerifyChart(path string, keyring string) (*provenance.Verification, error) { + // For now, error out if it's not a tar file. + if fi, err := os.Stat(path); err != nil { + return nil, err + } else if fi.IsDir() { + return nil, errors.New("unpacked charts cannot be verified") + } else if !isTar(path) { + return nil, errors.New("chart must be a tgz file") + } + + provfile := path + ".prov" + if _, err := os.Stat(provfile); err != nil { + return nil, fmt.Errorf("could not load provenance file %s: %s", provfile, err) + } + + sig, err := provenance.NewFromKeyring(keyring, "") + if err != nil { + return nil, fmt.Errorf("failed to load keyring: %s", err) + } + return sig.Verify(path, provfile) +} + +// download performs a simple HTTP Get and returns the body. +func download(href string) (*bytes.Buffer, error) { + buf := bytes.NewBuffer(nil) + + resp, err := http.Get(href) + if err != nil { + return buf, err + } + if resp.StatusCode != 200 { + return buf, fmt.Errorf("Failed to fetch %s : %s", href, resp.Status) + } + + _, err = io.Copy(buf, resp.Body) + resp.Body.Close() + return buf, err +} + +// isTar tests whether the given file is a tar file. +// +// Currently, this simply checks extension, since a subsequent function will +// untar the file and validate its binary format. +func isTar(filename string) bool { + return strings.ToLower(filepath.Ext(filename)) == ".tgz" +} diff --git a/cmd/helm/downloader/chart_downloader_test.go b/cmd/helm/downloader/chart_downloader_test.go new file mode 100644 index 000000000..652776a51 --- /dev/null +++ b/cmd/helm/downloader/chart_downloader_test.go @@ -0,0 +1,149 @@ +/* +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 downloader + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/pkg/repo/repotest" +) + +func TestResolveChartRef(t *testing.T) { + tests := []struct { + name, ref, expect string + fail bool + }{ + {name: "full URL", ref: "http://example.com/foo-1.2.3.tgz", expect: "http://example.com/foo-1.2.3.tgz"}, + {name: "full URL, HTTPS", ref: "https://example.com/foo-1.2.3.tgz", expect: "https://example.com/foo-1.2.3.tgz"}, + {name: "reference, testing repo", ref: "testing/foo-1.2.3.tgz", expect: "http://example.com/foo-1.2.3.tgz"}, + {name: "full URL, file", ref: "file:///foo-1.2.3.tgz", fail: true}, + {name: "invalid", ref: "invalid-1.2.3", fail: true}, + {name: "not found", ref: "nosuchthing/invalid-1.2.3", fail: true}, + } + + c := ChartDownloader{ + HelmHome: helmpath.Home("testdata/helmhome"), + Out: os.Stderr, + } + + for _, tt := range tests { + u, err := c.ResolveChartRef(tt.ref) + if err != nil { + if tt.fail { + continue + } + t.Errorf("%s: failed with error %s", tt.name, err) + continue + } + if got := u.String(); got != tt.expect { + t.Errorf("%s: expected %s, got %s", tt.name, tt.expect, got) + } + } +} + +func TestVerifyChart(t *testing.T) { + v, err := VerifyChart("testdata/signtest-0.1.0.tgz", "testdata/helm-test-key.pub") + if err != nil { + t.Fatal(err) + } + // The verification is tested at length in the provenance package. Here, + // we just want a quick sanity check that the v is not empty. + if len(v.FileHash) == 0 { + t.Error("Digest missing") + } +} + +func TestDownload(t *testing.T) { + expect := "Call me Ishmael" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, expect) + })) + defer srv.Close() + + got, err := download(srv.URL) + if err != nil { + t.Fatal(err) + } + + if got.String() != expect { + t.Errorf("Expected %q, got %q", expect, got.String()) + } +} + +func TestIsTar(t *testing.T) { + tests := map[string]bool{ + "foo.tgz": true, + "foo/bar/baz.tgz": true, + "foo-1.2.3.4.5.tgz": true, + "foo.tar.gz": false, // for our purposes + "foo.tgz.1": false, + "footgz": false, + } + + for src, expect := range tests { + if isTar(src) != expect { + t.Errorf("%q should be %t", src, expect) + } + } +} + +func TestDownloadTo(t *testing.T) { + hh, err := ioutil.TempDir("", "helm-downloadto-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(hh) + + dest := filepath.Join(hh, "dest") + os.MkdirAll(dest, 0755) + + // Set up a fake repo + srv := repotest.NewServer(hh) + defer srv.Stop() + if _, err := srv.CopyCharts("testdata/*.tgz*"); err != nil { + t.Error(err) + return + } + + c := ChartDownloader{ + HelmHome: helmpath.Home("testdata/helmhome"), + Out: os.Stderr, + Verify: VerifyAlways, + Keyring: "testdata/helm-test-key.pub", + } + cname := "/signtest-0.1.0.tgz" + v, err := c.DownloadTo(srv.URL()+cname, dest) + if err != nil { + t.Error(err) + return + } + + if v.FileHash == "" { + t.Error("File hash was empty, but verification is required.") + } + + if _, err := os.Stat(filepath.Join(dest, cname)); err != nil { + t.Error(err) + return + } +} diff --git a/cmd/helm/downloader/doc.go b/cmd/helm/downloader/doc.go new file mode 100644 index 000000000..fb54936b8 --- /dev/null +++ b/cmd/helm/downloader/doc.go @@ -0,0 +1,23 @@ +/* +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 downloader provides a library for downloading charts. + +This package contains various tools for downloading charts from repository +servers, and then storing them in Helm-specific directory structures (like +HELM_HOME). This library contains many functions that depend on a specific +filesystem layout. +*/ +package downloader diff --git a/cmd/helm/downloader/manager.go b/cmd/helm/downloader/manager.go new file mode 100644 index 000000000..204ea240d --- /dev/null +++ b/cmd/helm/downloader/manager.go @@ -0,0 +1,330 @@ +/* +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 downloader + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/ghodss/yaml" + + "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/cmd/helm/resolver" + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/repo" +) + +// Manager handles the lifecycle of fetching, resolving, and storing dependencies. +type Manager struct { + // Out is used to print warnings and notifications. + Out io.Writer + // ChartPath is the path to the unpacked base chart upon which this operates. + ChartPath string + // HelmHome is the $HELM_HOME directory + HelmHome helmpath.Home + // Verification indicates whether the chart should be verified. + Verify VerificationStrategy + // Keyring is the key ring file. + Keyring string +} + +// Build rebuilds a local charts directory from a lockfile. +// +// If the lockfile is not present, this will run a Manager.Update() +func (m *Manager) Build() error { + c, err := m.loadChartDir() + if err != nil { + return err + } + + // If a lock file is found, run a build from that. Otherwise, just do + // an update. + lock, err := chartutil.LoadRequirementsLock(c) + if err != nil { + return m.Update() + } + + // A lock must accompany a requirements.yaml file. + req, err := chartutil.LoadRequirements(c) + if err != nil { + return fmt.Errorf("requirements.yaml cannot be opened: %s", err) + } + if sum, err := resolver.HashReq(req); err != nil || sum != lock.Digest { + return fmt.Errorf("requirements.lock is out of sync with requirements.yaml") + } + + // Check that all of the repos we're dependent on actually exist. + if err := m.hasAllRepos(lock.Dependencies); err != nil { + return err + } + + // For each repo in the file, update the cached copy of that repo + if err := m.UpdateRepositories(); err != nil { + return err + } + + // Now we need to fetch every package here into charts/ + if err := m.downloadAll(lock.Dependencies); err != nil { + return err + } + + return nil +} + +// Update updates a local charts directory. +// +// It first reads the requirements.yaml file, and then attempts to +// negotiate versions based on that. It will download the versions +// from remote chart repositories. +func (m *Manager) Update() error { + c, err := m.loadChartDir() + if err != nil { + return err + } + + // If no requirements file is found, we consider this a successful + // completion. + req, err := chartutil.LoadRequirements(c) + if err != nil { + if err == chartutil.ErrRequirementsNotFound { + fmt.Fprintf(m.Out, "No requirements found in %s/charts.\n", m.ChartPath) + return nil + } + return err + } + + // Check that all of the repos we're dependent on actually exist. + if err := m.hasAllRepos(req.Dependencies); err != nil { + return err + } + + // For each repo in the file, update the cached copy of that repo + if err := m.UpdateRepositories(); err != nil { + return err + } + + // Now we need to find out which version of a chart best satisfies the + // requirements the requirements.yaml + lock, err := m.resolve(req) + if err != nil { + return err + } + + // Now we need to fetch every package here into charts/ + if err := m.downloadAll(lock.Dependencies); err != nil { + return err + } + + // Finally, we need to write the lockfile. + return writeLock(m.ChartPath, lock) +} + +func (m *Manager) loadChartDir() (*chart.Chart, error) { + if fi, err := os.Stat(m.ChartPath); err != nil { + return nil, fmt.Errorf("could not find %s: %s", m.ChartPath, err) + } else if !fi.IsDir() { + return nil, errors.New("only unpacked charts can be updated") + } + return chartutil.LoadDir(m.ChartPath) +} + +// 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) (*chartutil.RequirementsLock, error) { + res := resolver.New(m.ChartPath, m.HelmHome) + return res.Resolve(req) +} + +// downloadAll takes a list of dependencies and downloads them into charts/ +func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { + repos, err := m.loadChartRepositories() + if err != nil { + return err + } + + dl := ChartDownloader{ + Out: m.Out, + Verify: m.Verify, + Keyring: m.Keyring, + HelmHome: m.HelmHome, + } + + fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) + for _, dep := range deps { + fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) + + target := fmt.Sprintf("%s-%s", dep.Name, dep.Version) + churl, err := findChartURL(target, dep.Repository, repos) + if err != nil { + fmt.Fprintf(m.Out, "WARNING: %s (skipped)", err) + continue + } + + dest := filepath.Join(m.ChartPath, "charts") + if _, err := dl.DownloadTo(churl, dest); err != nil { + fmt.Fprintf(m.Out, "WARNING: Could not download %s: %s (skipped)", churl, err) + continue + } + } + return nil +} + +// hasAllRepos ensures that all of the referenced deps are in the local repo cache. +func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error { + rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) + if err != nil { + return err + } + repos := rf.Repositories + // Verify that all repositories referenced in the deps are actually known + // by Helm. + missing := []string{} + for _, dd := range deps { + found := false + if dd.Repository == "" { + found = true + } else { + for _, repo := range repos { + if urlsAreEqual(repo, dd.Repository) { + found = true + } + } + } + if !found { + missing = append(missing, dd.Repository) + } + } + if len(missing) > 0 { + return fmt.Errorf("no repository definition for %s. Try 'helm repo add'", strings.Join(missing, ", ")) + } + return nil +} + +// UpdateRepositories updates all of the local repos to the latest. +func (m *Manager) UpdateRepositories() error { + rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) + if err != nil { + return err + } + repos := rf.Repositories + if len(repos) > 0 { + // This prints warnings straight to out. + m.parallelRepoUpdate(repos) + } + return nil +} + +func (m *Manager) parallelRepoUpdate(repos map[string]string) { + out := m.Out + fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") + var wg sync.WaitGroup + for name, url := range repos { + wg.Add(1) + go func(n, u string) { + err := repo.DownloadIndexFile(n, u, m.HelmHome.CacheIndex(n)) + if err != nil { + updateErr := fmt.Sprintf("...Unable to get an update from the %q chart repository: %s", n, err) + fmt.Fprintln(out, updateErr) + } else { + fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", n) + } + wg.Done() + }(name, url) + } + wg.Wait() + fmt.Fprintln(out, "Update Complete. Happy Helming!") +} + +// urlsAreEqual normalizes two URLs and then compares for equality. +func urlsAreEqual(a, b string) bool { + au, err := url.Parse(a) + if err != nil { + // If urls are paths, return true only if they are an exact match + return a == b + } + bu, err := url.Parse(b) + if err != nil { + return false + } + return au.String() == bu.String() +} + +// findChartURL searches the cache of repo data for a chart that has the name and the repourl specified. +// +// In this current version, name is of the form 'foo-1.2.3'. This will change when +// the repository index stucture changes. +func findChartURL(name, repourl string, repos map[string]*repo.ChartRepository) (string, error) { + for _, cr := range repos { + if urlsAreEqual(repourl, cr.URL) { + for ename, entry := range cr.IndexFile.Entries { + if ename == name { + return entry.URL, nil + } + } + } + } + return "", fmt.Errorf("chart %s not found in %s", name, repourl) +} + +// loadChartRepositories reads the repositories.yaml, and then builds a map of +// ChartRepositories. +// +// The key is the local name (which is only present in the repositories.yaml). +func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) { + indices := map[string]*repo.ChartRepository{} + repoyaml := m.HelmHome.RepositoryFile() + + // Load repositories.yaml file + rf, err := repo.LoadRepositoriesFile(repoyaml) + if err != nil { + return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err) + } + + // localName: chartRepo + for lname, url := range rf.Repositories { + cacheindex := m.HelmHome.CacheIndex(lname) + index, err := repo.LoadIndexFile(cacheindex) + if err != nil { + return indices, err + } + + cr := &repo.ChartRepository{ + URL: url, + IndexFile: index, + } + indices[lname] = cr + } + return indices, nil +} + +// writeLock writes a lockfile to disk +func writeLock(chartpath string, lock *chartutil.RequirementsLock) error { + data, err := yaml.Marshal(lock) + if err != nil { + return err + } + dest := filepath.Join(chartpath, "requirements.lock") + return ioutil.WriteFile(dest, data, 0644) +} diff --git a/cmd/helm/downloader/testdata/helm-test-key.pub b/cmd/helm/downloader/testdata/helm-test-key.pub new file mode 100644 index 0000000000000000000000000000000000000000..38714f25adaf701b08e11fd559a587074bbde0e4 GIT binary patch literal 1243 zcmV<11SI>J0SyFKmTjH^2mr{k15wFPQdpTAAEclY4oV^-7nTy5x$&xF;PC4il}o-Pk4@#$knlp#(|J0GE?qli_lr;7-o zyY8vBsN_GJe;#w<`JdR7riNL&RJlcS)FG+W=91;dYS6NZ2tY?kZ8Sw9{r=e4|L3E{ zRod|EPC!PWgW&pe&4qiqKQAijj;G~fyjcC^m%0p54Tn{h%YFKd0VC4n=#~SRc@BVd znj66*)Om%*SEQfX{1*Tb0RRECT}WkYZ6H)-b98BLXCNq4XlZjGYh`&Lb7*gMY-AvB zZftoVVr3w8b7f>8W^ZyJbY*jNX>MmOAVg0fPES-IR8mz_R4yqXJZNQXZ7p6uVakV zT7GGV$jaKjyjfI_a~N1!Hk?5C$0wa&4)R=i$v7t&ZMycW#RkavpF%A?>MTT2anNDzOQUm<++zEOykJ9-@&c2QXq3owqf7fek`=L@+7iF zv;IW2Q>Q&r+V@cWDF&hAUUsCKlDinerKgvJUJCl$5gjb7NhM{mBP%!M^mX-iS8xFf zuLB{@MDqvtZzF#Bxd9CXSC(y_0SExW>8~h=U8|!do4*OJj2u#!KDe3v+1T+aVzU5di=Ji2)x37y$|Z2?YXImTjH_8w>yn2@r%kznCAv zhhp0`2mpxVj5j%o&5i)?`r7iES|8dA@p2kk@+XS(tjBGN)6>tm^=gayCn`gTEC*K74Y~{I_PREk) z)PstIMx1RxB@cK8%Mey%;nVnKriAKUk2Ky?dBMG3uXItKL$3N(#3P^pQa*K$l)wUy F^>pMLK0g2e literal 0 HcmV?d00001 diff --git a/cmd/helm/downloader/testdata/helm-test-key.secret b/cmd/helm/downloader/testdata/helm-test-key.secret new file mode 100644 index 0000000000000000000000000000000000000000..a966aef93ed97d01d764f29940738df6df2d9d24 GIT binary patch literal 2545 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 N|NqlRq0#^l007yS;m800 literal 0 HcmV?d00001 diff --git a/cmd/helm/downloader/testdata/signtest-0.1.0.tgz.prov b/cmd/helm/downloader/testdata/signtest-0.1.0.tgz.prov new file mode 100755 index 000000000..94235399a --- /dev/null +++ b/cmd/helm/downloader/testdata/signtest-0.1.0.tgz.prov @@ -0,0 +1,20 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +description: A Helm chart for Kubernetes +name: signtest +version: 0.1.0 + +... +files: + signtest-0.1.0.tgz: sha256:dee72947753628425b82814516bdaa37aef49f25e8820dd2a6e15a33a007823b +-----BEGIN PGP SIGNATURE----- + +wsBcBAEBCgAQBQJXomNHCRCEO7+YH8GHYgAALywIAG1Me852Fpn1GYu8Q1GCcw4g +l2k7vOFchdDwDhdSVbkh4YyvTaIO3iE2Jtk1rxw+RIJiUr0eLO/rnIJuxZS8WKki +DR1LI9J1VD4dxN3uDETtWDWq7ScoPsRY5mJvYZXC8whrWEt/H2kfqmoA9LloRPWp +flOE0iktA4UciZOblTj6nAk3iDyjh/4HYL4a6tT0LjjKI7OTw4YyHfjHad1ywVCz +9dMUc1rPgTnl+fnRiSPSrlZIWKOt1mcQ4fVrU3nwtRUwTId2k8FtygL0G6M+Y6t0 +S6yaU7qfk9uTxkdkUF7Bf1X3ukxfe+cNBC32vf4m8LY4NkcYfSqK2fGtQsnVr6s= +=NyOM +-----END PGP SIGNATURE----- \ No newline at end of file diff --git a/cmd/helm/downloader/testdata/signtest/.helmignore b/cmd/helm/downloader/testdata/signtest/.helmignore new file mode 100644 index 000000000..435b756d8 --- /dev/null +++ b/cmd/helm/downloader/testdata/signtest/.helmignore @@ -0,0 +1,5 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +.git diff --git a/cmd/helm/downloader/testdata/signtest/Chart.yaml b/cmd/helm/downloader/testdata/signtest/Chart.yaml new file mode 100755 index 000000000..90964b44a --- /dev/null +++ b/cmd/helm/downloader/testdata/signtest/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: signtest +version: 0.1.0 diff --git a/cmd/helm/downloader/testdata/signtest/alpine/Chart.yaml b/cmd/helm/downloader/testdata/signtest/alpine/Chart.yaml new file mode 100755 index 000000000..6fbb27f18 --- /dev/null +++ b/cmd/helm/downloader/testdata/signtest/alpine/Chart.yaml @@ -0,0 +1,6 @@ +description: Deploy a basic Alpine Linux pod +home: https://k8s.io/helm +name: alpine +sources: +- https://github.com/kubernetes/helm +version: 0.1.0 diff --git a/cmd/helm/downloader/testdata/signtest/alpine/README.md b/cmd/helm/downloader/testdata/signtest/alpine/README.md new file mode 100755 index 000000000..5bd595747 --- /dev/null +++ b/cmd/helm/downloader/testdata/signtest/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.yaml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install docs/examples/alpine`. diff --git a/cmd/helm/downloader/testdata/signtest/alpine/templates/alpine-pod.yaml b/cmd/helm/downloader/testdata/signtest/alpine/templates/alpine-pod.yaml new file mode 100755 index 000000000..08cf3c2c1 --- /dev/null +++ b/cmd/helm/downloader/testdata/signtest/alpine/templates/alpine-pod.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + heritage: {{.Release.Service}} + chartName: {{.Chart.Name}} + chartVersion: {{.Chart.Version | quote}} + annotations: + "helm.sh/created": "{{.Release.Time.Seconds}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/cmd/helm/downloader/testdata/signtest/alpine/values.yaml b/cmd/helm/downloader/testdata/signtest/alpine/values.yaml new file mode 100755 index 000000000..bb6c06ae4 --- /dev/null +++ b/cmd/helm/downloader/testdata/signtest/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: my-alpine diff --git a/cmd/helm/downloader/testdata/signtest/templates/pod.yaml b/cmd/helm/downloader/testdata/signtest/templates/pod.yaml new file mode 100644 index 000000000..9b00ccaf7 --- /dev/null +++ b/cmd/helm/downloader/testdata/signtest/templates/pod.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: signtest +spec: + restartPolicy: Never + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/cmd/helm/downloader/testdata/signtest/values.yaml b/cmd/helm/downloader/testdata/signtest/values.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/helm/fetch.go b/cmd/helm/fetch.go index e174bcdac..5491f5299 100644 --- a/cmd/helm/fetch.go +++ b/cmd/helm/fetch.go @@ -17,21 +17,16 @@ limitations under the License. package main import ( - "bytes" - "errors" "fmt" "io" "io/ioutil" - "net/http" - "net/url" "os" "path/filepath" - "strings" "github.com/spf13/cobra" + "k8s.io/helm/cmd/helm/downloader" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/provenance" - "k8s.io/helm/pkg/repo" ) const fetchDesc = ` @@ -53,6 +48,7 @@ type fetchCmd struct { untar bool untardir string chartRef string + destdir string verify bool keyring string @@ -83,9 +79,10 @@ func newFetchCmd(out io.Writer) *cobra.Command { f := cmd.Flags() f.BoolVar(&fch.untar, "untar", false, "If set to true, will untar the chart after downloading it.") - f.StringVar(&fch.untardir, "untardir", ".", "If untar is specified, this flag specifies where to untar the chart.") + f.StringVar(&fch.untardir, "untardir", ".", "If untar is specified, this flag specifies the name of the directory into which the chart is expanded.") f.BoolVar(&fch.verify, "verify", false, "Verify the package against its signature.") f.StringVar(&fch.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.") + f.StringVarP(&fch.destdir, "destination", "d", ".", "The location to write the chart. If this and tardir are specified, tardir is appended to this.") return cmd } @@ -96,162 +93,60 @@ func (f *fetchCmd) run() error { pname += ".tgz" } - return downloadAndSaveChart(pname, f.untar, f.untardir, f.verify, f.keyring) -} - -// downloadAndSaveChart fetches a chart over HTTP, and then (if verify is true) verifies it. -// -// If untar is true, it also unpacks the file into untardir. -func downloadAndSaveChart(pname string, untar bool, untardir string, verify bool, keyring string) error { - buf, err := downloadChart(pname, verify, keyring) - if err != nil { - return err + c := downloader.ChartDownloader{ + HelmHome: helmpath.Home(homePath()), + Out: f.out, + Keyring: f.keyring, + Verify: downloader.VerifyNever, } - return saveChart(pname, buf, untar, untardir) -} -func downloadChart(pname string, verify bool, keyring string) (*bytes.Buffer, error) { - r, err := repo.LoadRepositoriesFile(repositoriesFile()) - if err != nil { - return bytes.NewBuffer(nil), err + if f.verify { + c.Verify = downloader.VerifyAlways } - // get download url - u, err := mapRepoArg(pname, r.Repositories) - if err != nil { - return bytes.NewBuffer(nil), err - } - - href := u.String() - buf, err := fetchChart(href) - if err != nil { - return buf, err - } - - if verify { - basename := filepath.Base(pname) - sigref := href + ".prov" - sig, err := fetchChart(sigref) + // If untar is set, we fetch to a tempdir, then untar and copy after + // verification. + dest := f.destdir + if f.untar { + var err error + dest, err = ioutil.TempDir("", "helm-") if err != nil { - return buf, fmt.Errorf("provenance data not downloaded from %s: %s", sigref, err) - } - if err := ioutil.WriteFile(basename+".prov", sig.Bytes(), 0755); err != nil { - return buf, fmt.Errorf("provenance data not saved: %s", err) - } - if err := verifyChart(basename, keyring); err != nil { - return buf, err + return fmt.Errorf("Failed to untar: %s", err) } + defer os.RemoveAll(dest) } - return buf, nil -} - -// verifyChart takes a path to a chart archive and a keyring, and verifies the chart. -// -// It assumes that a chart archive file is accompanied by a provenance file whose -// name is the archive file name plus the ".prov" extension. -func verifyChart(path string, keyring string) error { - // For now, error out if it's not a tar file. - if fi, err := os.Stat(path); err != nil { + v, err := c.DownloadTo(pname, dest) + if err != nil { return err - } else if fi.IsDir() { - return errors.New("unpacked charts cannot be verified") - } else if !isTar(path) { - return errors.New("chart must be a tgz file") } - provfile := path + ".prov" - if _, err := os.Stat(provfile); err != nil { - return fmt.Errorf("could not load provenance file %s: %s", provfile, err) + if f.verify { + fmt.Fprintf(f.out, "Verification: %v", v) } - sig, err := provenance.NewFromKeyring(keyring, "") - if err != nil { - return fmt.Errorf("failed to load keyring: %s", err) - } - ver, err := sig.Verify(path, provfile) - if flagDebug { - for name := range ver.SignedBy.Identities { - fmt.Printf("Signed by %q\n", name) + // After verification, untar the chart into the requested directory. + if f.untar { + ud := f.untardir + if !filepath.IsAbs(ud) { + ud = filepath.Join(f.destdir, ud) } + if fi, err := os.Stat(ud); err != nil { + if err := os.MkdirAll(ud, 0755); err != nil { + return fmt.Errorf("Failed to untar (mkdir): %s", err) + } + + } else if !fi.IsDir() { + return fmt.Errorf("Failed to untar: %s is not a directory", ud) + } + + from := filepath.Join(dest, filepath.Base(pname)) + return chartutil.ExpandFile(ud, from) } - return err + return nil } // defaultKeyring returns the expanded path to the default keyring. func defaultKeyring() string { return os.ExpandEnv("$HOME/.gnupg/pubring.gpg") } - -// isTar tests whether the given file is a tar file. -// -// Currently, this simply checks extension, since a subsequent function will -// untar the file and validate its binary format. -func isTar(filename string) bool { - return strings.ToLower(filepath.Ext(filename)) == ".tgz" -} - -// saveChart saves a chart locally. -func saveChart(name string, buf *bytes.Buffer, untar bool, untardir string) error { - if untar { - return chartutil.Expand(untardir, buf) - } - - p := strings.Split(name, "/") - return saveChartFile(p[len(p)-1], buf) -} - -// fetchChart retrieves a chart over HTTP. -func fetchChart(href string) (*bytes.Buffer, error) { - buf := bytes.NewBuffer(nil) - - resp, err := http.Get(href) - if err != nil { - return buf, err - } - if resp.StatusCode != 200 { - return buf, fmt.Errorf("Failed to fetch %s : %s", href, resp.Status) - } - - _, err = io.Copy(buf, resp.Body) - resp.Body.Close() - return buf, err -} - -// mapRepoArg figures out which format the argument is given, and creates a fetchable -// url from it. -func mapRepoArg(arg string, r map[string]string) (*url.URL, error) { - // See if it's already a full URL. - u, err := url.ParseRequestURI(arg) - if err == nil { - // If it has a scheme and host and path, it's a full URL - if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 { - return u, nil - } - return nil, fmt.Errorf("Invalid chart url format: %s", arg) - } - // See if it's of the form: repo/path_to_chart - p := strings.Split(arg, "/") - if len(p) > 1 { - if baseURL, ok := r[p[0]]; ok { - if !strings.HasSuffix(baseURL, "/") { - baseURL = baseURL + "/" - } - return url.ParseRequestURI(baseURL + strings.Join(p[1:], "/")) - } - return nil, fmt.Errorf("No such repo: %s", p[0]) - } - return nil, fmt.Errorf("Invalid chart url format: %s", arg) -} - -func saveChartFile(c string, r io.Reader) error { - // Grab the chart name that we'll use for the name of the file to download to. - out, err := os.Create(c) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(out, r) - return err -} diff --git a/cmd/helm/fetch_test.go b/cmd/helm/fetch_test.go index be548ee0c..3dd241a1f 100644 --- a/cmd/helm/fetch_test.go +++ b/cmd/helm/fetch_test.go @@ -17,49 +17,109 @@ limitations under the License. package main import ( - "fmt" - + "bytes" + "os" + "path/filepath" "testing" + + "k8s.io/helm/pkg/repo/repotest" ) -type testCase struct { - in string - expectedErr error - expectedOut string -} +func TestFetchCmd(t *testing.T) { + hh, err := tempHelmHome() + if err != nil { + t.Fatal(err) + } + old := homePath() + helmHome = hh + defer func() { + helmHome = old + os.RemoveAll(hh) + }() -var repos = map[string]string{ - "local": "http://localhost:8879/charts", - "someother": "http://storage.googleapis.com/mycharts", -} + // all flags will get "--home=TMDIR -d outdir" appended. + tests := []struct { + name string + chart string + flags []string + fail bool + failExpect string + expectFile string + expectDir bool + }{ + { + name: "Basic chart fetch", + chart: "test/signtest-0.1.0", + expectFile: "./signtest-0.1.0.tgz", + }, + { + name: "Fail fetching non-existent chart", + chart: "test/nosuchthing-0.1.0", + failExpect: "Failed to fetch", + fail: true, + }, + { + name: "Fetch and verify", + chart: "test/signtest-0.1.0", + flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"}, + expectFile: "./signtest-0.1.0.tgz", + }, + { + name: "Fetch and fail verify", + chart: "test/reqtest-0.1.0", + flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"}, + failExpect: "Failed to fetch provenance", + fail: true, + }, + { + name: "Fetch and untar", + chart: "test/signtest-0.1.0", + flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub", "--untar", "--untardir", "signtest"}, + expectFile: "./signtest", + expectDir: true, + }, + { + name: "Fetch, verify, untar", + chart: "test/signtest-0.1.0", + flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub", "--untar", "--untardir", "signtest"}, + expectFile: "./signtest", + expectDir: true, + }, + } -var testCases = []testCase{ - {"bad", fmt.Errorf("Invalid chart url format: bad"), ""}, - {"http://", fmt.Errorf("Invalid chart url format: http://"), ""}, - {"http://example.com", fmt.Errorf("Invalid chart url format: http://example.com"), ""}, - {"http://example.com/foo/bar", nil, "http://example.com/foo/bar"}, - {"local/nginx-2.0.0.tgz", nil, "http://localhost:8879/charts/nginx-2.0.0.tgz"}, - {"nonexistentrepo/nginx-2.0.0.tgz", fmt.Errorf("No such repo: nonexistentrepo"), ""}, -} + srv := repotest.NewServer(hh) + defer srv.Stop() -func testRunner(t *testing.T, tc testCase) { - u, err := mapRepoArg(tc.in, repos) - if (tc.expectedErr == nil && err != nil) || - (tc.expectedErr != nil && err == nil) || - (tc.expectedErr != nil && err != nil && tc.expectedErr.Error() != err.Error()) { - t.Errorf("Expected mapRepoArg to fail with input %s %v but got %v", tc.in, tc.expectedErr, err) + if _, err := srv.CopyCharts("testdata/testcharts/*.tgz*"); err != nil { + t.Fatal(err) } - if (u == nil && len(tc.expectedOut) != 0) || - (u != nil && len(tc.expectedOut) == 0) || - (u != nil && tc.expectedOut != u.String()) { - t.Errorf("Expected %s to map to fetch url %v but got %v", tc.in, tc.expectedOut, u) - } + t.Logf("HELM_HOME=%s", homePath()) -} + for _, tt := range tests { + outdir := filepath.Join(hh, "testout") + os.RemoveAll(outdir) + os.Mkdir(outdir, 0755) + + buf := bytes.NewBuffer(nil) + cmd := newFetchCmd(buf) + tt.flags = append(tt.flags, "-d", outdir) + cmd.ParseFlags(tt.flags) + if err := cmd.RunE(cmd, []string{tt.chart}); err != nil { + if tt.fail { + continue + } + t.Errorf("%q reported error: %s", tt.name, err) + continue + } -func TestMappings(t *testing.T) { - for _, tc := range testCases { - testRunner(t, tc) + ef := filepath.Join(outdir, tt.expectFile) + fi, err := os.Stat(ef) + if err != nil { + t.Errorf("%q: expected a file at %s. %s", tt.name, ef, err) + } + if fi.IsDir() != tt.expectDir { + t.Errorf("%q: expected directory=%t, but it's not.", tt.name, tt.expectDir) + } } } diff --git a/cmd/helm/helmpath/helmhome.go b/cmd/helm/helmpath/helmhome.go new file mode 100644 index 000000000..d2749ef2f --- /dev/null +++ b/cmd/helm/helmpath/helmhome.go @@ -0,0 +1,61 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helmpath + +import ( + "fmt" + "path/filepath" +) + +// Home describes the location of a CLI configuration. +// +// This helper builds paths relative to a Helm Home directory. +type Home string + +// String returns Home as a string. +// +// Implements fmt.Stringer. +func (h Home) String() string { + return string(h) +} + +// Repository returns the path to the local repository. +func (h Home) Repository() string { + return filepath.Join(string(h), "repository") +} + +// RepositoryFile returns the path to the repositories.yaml file. +func (h Home) RepositoryFile() string { + return filepath.Join(string(h), "repository/repositories.yaml") +} + +// Cache returns the path to the local cache. +func (h Home) Cache() string { + return filepath.Join(string(h), "repository/cache") +} + +// CacheIndex returns the path to an index for the given named repository. +func (h Home) CacheIndex(name string) string { + target := fmt.Sprintf("repository/cache/%s-index.yaml", name) + return filepath.Join(string(h), target) +} + +// LocalRepository returns the location to the local repo. +// +// The local repo is the one used by 'helm serve' +func (h Home) LocalRepository() string { + return filepath.Join(string(h), "repository/local") +} diff --git a/cmd/helm/helmpath/helmhome_test.go b/cmd/helm/helmpath/helmhome_test.go new file mode 100644 index 000000000..d44bcd624 --- /dev/null +++ b/cmd/helm/helmpath/helmhome_test.go @@ -0,0 +1,36 @@ +/* +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 helmpath + +import ( + "testing" +) + +func TestHelmHome(t *testing.T) { + hh := Home("/r") + isEq := func(t *testing.T, a, b string) { + if a != b { + t.Errorf("Expected %q, got %q", a, b) + } + } + + isEq(t, hh.String(), "/r") + isEq(t, hh.Repository(), "/r/repository") + isEq(t, hh.RepositoryFile(), "/r/repository/repositories.yaml") + isEq(t, hh.LocalRepository(), "/r/repository/local") + isEq(t, hh.Cache(), "/r/repository/cache") + isEq(t, hh.CacheIndex("t"), "/r/repository/cache/t-index.yaml") +} diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 3fab2c7fd..648c2b5ea 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -32,6 +32,8 @@ import ( "github.com/ghodss/yaml" "github.com/spf13/cobra" + "k8s.io/helm/cmd/helm/downloader" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/timeconv" @@ -286,7 +288,7 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) { if fi.IsDir() { return "", errors.New("cannot verify a directory") } - if err := verifyChart(abs, keyring); err != nil { + if _, err := downloader.VerifyChart(abs, keyring); err != nil { return "", err } } @@ -306,7 +308,17 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) { if filepath.Ext(name) != ".tgz" { name += ".tgz" } - if err := downloadAndSaveChart(name, false, ".", verify, keyring); err == nil { + + dl := downloader.ChartDownloader{ + HelmHome: helmpath.Home(homePath()), + Out: os.Stdout, + Keyring: keyring, + } + if verify { + dl.Verify = downloader.VerifyAlways + } + + if _, err := dl.DownloadTo(name, "."); err == nil { lname, err := filepath.Abs(filepath.Base(name)) if err != nil { return lname, err diff --git a/cmd/helm/repo_add_test.go b/cmd/helm/repo_add_test.go index 3d6497442..be6a669ef 100644 --- a/cmd/helm/repo_add_test.go +++ b/cmd/helm/repo_add_test.go @@ -34,7 +34,7 @@ var testName = "test-name" func TestRepoAddCmd(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") - fmt.Fprintln(w, "OK") + fmt.Fprintln(w, "") })) tests := []releaseCase{ diff --git a/cmd/helm/resolver/resolver.go b/cmd/helm/resolver/resolver.go index 34854b538..eb878cbb4 100644 --- a/cmd/helm/resolver/resolver.go +++ b/cmd/helm/resolver/resolver.go @@ -23,6 +23,7 @@ import ( "github.com/Masterminds/semver" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/provenance" ) @@ -30,11 +31,11 @@ import ( // Resolver resolves dependencies from semantic version ranges to a particular version. type Resolver struct { chartpath string - helmhome string + helmhome helmpath.Home } // New creates a new resolver for a given chart and a given helm home. -func New(chartpath string, helmhome string) *Resolver { +func New(chartpath string, helmhome helmpath.Home) *Resolver { return &Resolver{ chartpath: chartpath, helmhome: helmhome, @@ -43,7 +44,7 @@ func New(chartpath string, helmhome string) *Resolver { // Resolve resolves dependencies and returns a lock file with the resolution. func (r *Resolver) Resolve(reqs *chartutil.Requirements) (*chartutil.RequirementsLock, error) { - d, err := hashReq(reqs) + d, err := HashReq(reqs) if err != nil { return nil, err } @@ -54,7 +55,7 @@ func (r *Resolver) Resolve(reqs *chartutil.Requirements) (*chartutil.Requirement // Right now, we're just copying one entry to another. What we need to // do here is parse the requirement as a SemVer range, and then look up // whether a version in index.yaml satisfies this constraint. If so, - // we need to clone the dep, settinv Version appropriately. + // we need to clone the dep, setting Version appropriately. // If not, we need to error out. if _, err := semver.NewVersion(d.Version); err != nil { return nil, fmt.Errorf("dependency %q has an invalid version: %s", d.Name, err) @@ -73,11 +74,11 @@ func (r *Resolver) Resolve(reqs *chartutil.Requirements) (*chartutil.Requirement }, nil } -// hashReq generates a hash of the requirements. +// HashReq generates a hash of the requirements. // // This should be used only to compare against another hash generated by this // function. -func hashReq(req *chartutil.Requirements) (string, error) { +func HashReq(req *chartutil.Requirements) (string, error) { data, err := json.Marshal(req) if err != nil { return "", err diff --git a/cmd/helm/resolver/resolver_test.go b/cmd/helm/resolver/resolver_test.go index 0212757b8..440a58af1 100644 --- a/cmd/helm/resolver/resolver_test.go +++ b/cmd/helm/resolver/resolver_test.go @@ -66,7 +66,7 @@ func TestResolve(t *testing.T) { t.Fatalf("Expected error in test %q", tt.name) } - if h, err := hashReq(tt.req); err != nil { + if h, err := HashReq(tt.req); err != nil { t.Fatal(err) } else if h != l.Digest { t.Errorf("%q: hashes don't match.", tt.name) @@ -97,7 +97,7 @@ func TestHashReq(t *testing.T) { {Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"}, }, } - h, err := hashReq(req) + h, err := HashReq(req) if err != nil { t.Fatal(err) } @@ -106,7 +106,7 @@ func TestHashReq(t *testing.T) { } req = &chartutil.Requirements{Dependencies: []*chartutil.Dependency{}} - h, err = hashReq(req) + h, err = HashReq(req) if err != nil { t.Fatal(err) } diff --git a/cmd/helm/verify.go b/cmd/helm/verify.go index 4e342daaf..07e1c9b77 100644 --- a/cmd/helm/verify.go +++ b/cmd/helm/verify.go @@ -20,6 +20,8 @@ import ( "io" "github.com/spf13/cobra" + + "k8s.io/helm/cmd/helm/downloader" ) const verifyDesc = ` @@ -63,5 +65,6 @@ func newVerifyCmd(out io.Writer) *cobra.Command { } func (v *verifyCmd) run() error { - return verifyChart(v.chartfile, v.keyring) + _, err := downloader.VerifyChart(v.chartfile, v.keyring) + return err } diff --git a/pkg/chartutil/expand.go b/pkg/chartutil/expand.go index 45bb9e474..30600cb61 100644 --- a/pkg/chartutil/expand.go +++ b/pkg/chartutil/expand.go @@ -71,3 +71,13 @@ func Expand(dir string, r io.Reader) error { } return nil } + +// ExpandFile expands the src file into the dest directroy. +func ExpandFile(dest, src string) error { + h, err := os.Open(src) + if err != nil { + return err + } + defer h.Close() + return Expand(dest, h) +} diff --git a/pkg/chartutil/load.go b/pkg/chartutil/load.go index 20212f241..05bc1187b 100644 --- a/pkg/chartutil/load.go +++ b/pkg/chartutil/load.go @@ -120,6 +120,10 @@ func loadFiles(files []*afile) (*chart.Chart, error) { } else if strings.HasPrefix(f.name, "templates/") { c.Templates = append(c.Templates, &chart.Template{Name: f.name, Data: f.data}) } else if strings.HasPrefix(f.name, "charts/") { + if filepath.Ext(f.name) == ".prov" { + c.Files = append(c.Files, &any.Any{TypeUrl: f.name, Value: f.data}) + continue + } cname := strings.TrimPrefix(f.name, "charts/") parts := strings.SplitN(cname, "/", 2) scname := parts[0] diff --git a/pkg/chartutil/load_test.go b/pkg/chartutil/load_test.go index 3b785d20b..9586e3036 100644 --- a/pkg/chartutil/load_test.go +++ b/pkg/chartutil/load_test.go @@ -51,7 +51,7 @@ func verifyChart(t *testing.T, c *chart.Chart) { t.Errorf("Expected 1 template, got %d", len(c.Templates)) } - numfiles := 7 + numfiles := 8 if len(c.Files) != numfiles { t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files)) for _, n := range c.Files { @@ -115,6 +115,31 @@ func verifyRequirements(t *testing.T, c *chart.Chart) { } } } +func verifyRequirementsLock(t *testing.T, c *chart.Chart) { + r, err := LoadRequirementsLock(c) + if err != nil { + t.Fatal(err) + } + if len(r.Dependencies) != 2 { + t.Errorf("Expected 2 requirements, got %d", len(r.Dependencies)) + } + tests := []*Dependency{ + {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, + {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + } + for i, tt := range tests { + d := r.Dependencies[i] + if d.Name != tt.Name { + t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name) + } + if d.Version != tt.Version { + t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) + } + if d.Repository != tt.Repository { + t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) + } + } +} func verifyFrobnitz(t *testing.T, c *chart.Chart) { verifyChartfile(t, c.Metadata) diff --git a/pkg/chartutil/requirements.go b/pkg/chartutil/requirements.go index 4a452f64c..8871edbb8 100644 --- a/pkg/chartutil/requirements.go +++ b/pkg/chartutil/requirements.go @@ -24,6 +24,18 @@ import ( "k8s.io/helm/pkg/proto/hapi/chart" ) +const ( + requirementsName = "requirements.yaml" + lockfileName = "requirements.lock" +) + +var ( + // ErrRequirementsNotFound indicates that a requirements.yaml is not found. + ErrRequirementsNotFound = errors.New(requirementsName + " not found") + // ErrLockfileNotFound indicates that a requirements.lock is not found. + ErrLockfileNotFound = errors.New(lockfileName + " not found") +) + // Dependency describes a chart upon which another chart depends. // // Dependencies can be used to express developer intent, or to capture the state @@ -65,14 +77,11 @@ type RequirementsLock struct { Dependencies []*Dependency `json:"dependencies"` } -// ErrRequirementsNotFound indicates that a requirements.yaml is not found. -var ErrRequirementsNotFound = errors.New("requirements.yaml not found") - // LoadRequirements loads a requirements file from an in-memory chart. func LoadRequirements(c *chart.Chart) (*Requirements, error) { var data []byte for _, f := range c.Files { - if f.TypeUrl == "requirements.yaml" { + if f.TypeUrl == requirementsName { data = f.Value } } @@ -82,3 +91,18 @@ func LoadRequirements(c *chart.Chart) (*Requirements, error) { r := &Requirements{} return r, yaml.Unmarshal(data, r) } + +// LoadRequirementsLock loads a requirements lock file. +func LoadRequirementsLock(c *chart.Chart) (*RequirementsLock, error) { + var data []byte + for _, f := range c.Files { + if f.TypeUrl == lockfileName { + data = f.Value + } + } + if len(data) == 0 { + return nil, ErrLockfileNotFound + } + r := &RequirementsLock{} + return r, yaml.Unmarshal(data, r) +} diff --git a/pkg/chartutil/requirements_test.go b/pkg/chartutil/requirements_test.go index 04f1bd4a6..afcc75621 100644 --- a/pkg/chartutil/requirements_test.go +++ b/pkg/chartutil/requirements_test.go @@ -25,3 +25,11 @@ func TestLoadRequirements(t *testing.T) { } verifyRequirements(t, c) } + +func TestLoadRequirementsLock(t *testing.T) { + c, err := Load("testdata/frobnitz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyRequirementsLock(t, c) +} diff --git a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz index 27bec72bd85b6a1a2df341d237ba554c6d751200..e230f5b4018e6c44c5433902bda0392f641a13a0 100644 GIT binary patch literal 4025 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 literal 3974 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 delta 16 Xcmcc3beoA?zMF$V$?En-_7Fw@D`W(? diff --git a/pkg/repo/index.go b/pkg/repo/index.go index b7aade1dc..457e608b4 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -49,7 +49,7 @@ func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { URL: baseURL + "/" + filename, Chartfile: md, Digest: digest, - // FIXME: Need to add Created + Created: nowString(), } i.Entries[name] = cr } @@ -93,7 +93,7 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) { return index, nil } -// DownloadIndexFile uses +// DownloadIndexFile fetches the index from a repository. func DownloadIndexFile(repoName, url, indexFilePath string) error { var indexURL string diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go index e670bea44..75f7708d8 100644 --- a/pkg/repo/repo.go +++ b/pkg/repo/repo.go @@ -147,7 +147,7 @@ func (r *ChartRepository) Index() error { if ok && ref.Created != "" { created = ref.Created } else { - created = time.Now().UTC().String() + created = nowString() } url, _ := url.Parse(r.URL) @@ -171,6 +171,11 @@ func (r *ChartRepository) Index() error { return r.saveIndexFile() } +func nowString() string { + // FIXME: This is a different date format than we use elsewhere. + return time.Now().UTC().String() +} + func generateDigest(path string) (string, error) { f, err := os.Open(path) if err != nil { diff --git a/pkg/repo/repotest/doc.go b/pkg/repo/repotest/doc.go new file mode 100644 index 000000000..34d4bc6b0 --- /dev/null +++ b/pkg/repo/repotest/doc.go @@ -0,0 +1,20 @@ +/* +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 repotest provides utilities for testing. + +The server provides a testing server that can be set up and torn down quickly. +*/ +package repotest diff --git a/pkg/repo/repotest/server.go b/pkg/repo/repotest/server.go new file mode 100644 index 000000000..eb737290c --- /dev/null +++ b/pkg/repo/repotest/server.go @@ -0,0 +1,130 @@ +/* +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 repotest + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + + "github.com/ghodss/yaml" + + "k8s.io/helm/pkg/repo" +) + +// NewServer creates a repository server for testing. +// +// docroot should be a temp dir managed by the caller. +// +// This will start the server, serving files off of the docroot. +// +// Use CopyCharts to move charts into the repository and then index them +// for service. +func NewServer(docroot string) *Server { + root, err := filepath.Abs(docroot) + if err != nil { + panic(err) + } + srv := &Server{ + docroot: root, + } + srv.start() + // Add the testing repository as the only repo. + if err := setTestingRepository(docroot, "test", srv.URL()); err != nil { + panic(err) + } + return srv +} + +// Server is an implementaiton of a repository server for testing. +type Server struct { + docroot string + srv *httptest.Server +} + +// Root gets the docroot for the server. +func (s *Server) Root() string { + return s.docroot +} + +// CopyCharts takes a glob expression and copies those charts to the server root. +func (s *Server) CopyCharts(origin string) ([]string, error) { + files, err := filepath.Glob(origin) + if err != nil { + return []string{}, err + } + copied := make([]string, len(files)) + for i, f := range files { + base := filepath.Base(f) + newname := filepath.Join(s.docroot, base) + data, err := ioutil.ReadFile(f) + if err != nil { + return []string{}, err + } + if err := ioutil.WriteFile(newname, data, 0755); err != nil { + return []string{}, err + } + copied[i] = newname + } + + // generate the index + index, err := repo.IndexDirectory(s.docroot, s.URL()) + if err != nil { + return copied, err + } + + d, err := yaml.Marshal(index.Entries) + if err != nil { + return copied, err + } + + ifile := filepath.Join(s.docroot, "index.yaml") + err = ioutil.WriteFile(ifile, d, 0755) + return copied, err +} + +func (s *Server) start() { + s.srv = httptest.NewServer(http.FileServer(http.Dir(s.docroot))) +} + +// Stop stops the server and closes all connections. +// +// It should be called explicitly. +func (s *Server) Stop() { + s.srv.Close() +} + +// URL returns the URL of the server. +// +// Example: +// http://localhost:1776 +func (s *Server) URL() string { + return s.srv.URL +} + +// setTestingRepository sets up a testing repository.yaml with only the given name/URL. +func setTestingRepository(helmhome, name, url string) error { + // Oddly, there is no repo.Save function for this. + data, err := yaml.Marshal(&map[string]string{name: url}) + if err != nil { + return err + } + os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755) + dest := filepath.Join(helmhome, "repository/repositories.yaml") + return ioutil.WriteFile(dest, data, 0666) +} diff --git a/pkg/repo/repotest/server_test.go b/pkg/repo/repotest/server_test.go new file mode 100644 index 000000000..8437ed512 --- /dev/null +++ b/pkg/repo/repotest/server_test.go @@ -0,0 +1,107 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repotest + +import ( + "io/ioutil" + "net/http" + "os" + "path/filepath" + "testing" + + "gopkg.in/yaml.v2" + + "k8s.io/helm/pkg/repo" +) + +// Young'n, in these here parts, we test our tests. + +func TestServer(t *testing.T) { + docroot, err := ioutil.TempDir("", "helm-repotest-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(docroot) + + srv := NewServer(docroot) + defer srv.Stop() + + c, err := srv.CopyCharts("testdata/*.tgz") + if err != nil { + // Some versions of Go don't correctly fire defer on Fatal. + t.Error(err) + return + } + + if len(c) != 1 { + t.Errorf("Unexpected chart count: %d", len(c)) + } + + if filepath.Base(c[0]) != "examplechart-0.1.0.tgz" { + t.Errorf("Unexpected chart: %s", c[0]) + } + + res, err := http.Get(srv.URL() + "/examplechart-0.1.0.tgz") + if err != nil { + t.Error(err) + return + } + + if res.ContentLength < 500 { + t.Errorf("Expected at least 500 bytes of data, got %d", res.ContentLength) + } + + res, err = http.Get(srv.URL() + "/index.yaml") + if err != nil { + t.Error(err) + return + } + + data, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Error(err) + return + } + + var m map[string]*repo.ChartRef + if err := yaml.Unmarshal(data, &m); err != nil { + t.Error(err) + return + } + + if l := len(m); l != 1 { + t.Errorf("Expected 1 entry, got %d", l) + return + } + + expect := "examplechart-0.1.0" + if m[expect].Name != "examplechart-0.1.0" { + t.Errorf("Unexpected chart: %s", m[expect].Name) + } + if m[expect].Chartfile.Name != "examplechart" { + t.Errorf("Unexpected chart: %s", m[expect].Chartfile.Name) + } + + res, err = http.Get(srv.URL() + "/index.yaml-nosuchthing") + if err != nil { + t.Error(err) + return + } + if res.StatusCode != 404 { + t.Errorf("Expected 404, got %d", res.StatusCode) + } +} diff --git a/pkg/repo/repotest/testdata/examplechart-0.1.0.tgz b/pkg/repo/repotest/testdata/examplechart-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..aec86c64002af0b6d3d114e15de120306ba24baa GIT binary patch literal 558 zcmV+}0@3{+iwG0|32ul0|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PL1ui_|<6#`n6P;=Ihwt7zKpS_bxRnGqBfg^>lXByG>ManmH^ z&&-Y&es)h%R%b_KL5GorJ`AU6I5|n^`8^EY^1(=KdTxEbh>`91AkU7ef;6wH^ducV zi?Y1*h5`YMzDK==6Ha2e1Y-2fiq|Gb;=d}ZU-*A9@qZG{;6p^& zs>JH}?P1%af;tG<3e^$4%?XVHY%~u!$1YD z7b|GVV=~qWpQkt;KV$V*o2Pg;(RX Date: Fri, 23 Sep 2016 16:30:12 -0600 Subject: [PATCH 105/183] feat(helm): make long listing default for helm list This removes --long and adds --short,-q The default listing is now the long listing, which matches the behavior of all of the other listing commands. Closes #1215 --- cmd/helm/list.go | 15 +++++++-------- cmd/helm/list_test.go | 8 ++++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/cmd/helm/list.go b/cmd/helm/list.go index 64d7ff908..b4ec4498b 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -58,7 +58,7 @@ flag with the '--offset' flag allows you to page through results. type listCmd struct { filter string - long bool + short bool limit int offset string byDate bool @@ -94,7 +94,7 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { }, } f := cmd.Flags() - f.BoolVarP(&list.long, "long", "l", false, "output long listing format") + f.BoolVarP(&list.short, "short", "q", false, "output short (quiet) listing format") f.BoolVarP(&list.byDate, "date", "d", false, "sort by release date") f.BoolVarP(&list.sortDesc, "reverse", "r", false, "reverse the sort order") f.IntVarP(&list.limit, "max", "m", 256, "maximum number of releases to fetch") @@ -144,14 +144,13 @@ func (l *listCmd) run() error { rels := res.Releases - if l.long { - fmt.Fprintln(l.out, formatList(rels)) + if l.short { + for _, r := range rels { + fmt.Fprintln(l.out, r.Name) + } return nil } - for _, r := range rels { - fmt.Fprintln(l.out, r.Name) - } - + fmt.Fprintln(l.out, formatList(rels)) return nil } diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index c2ea487df..70197cf02 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -40,8 +40,8 @@ func TestListCmd(t *testing.T) { expected: "thomas-guide", }, { - name: "list --long", - args: []string{"--long"}, + name: "list", + args: []string{}, resp: []*release.Release{ releaseMock(&releaseOptions{name: "atlas"}), }, @@ -49,7 +49,7 @@ func TestListCmd(t *testing.T) { }, { name: "with a release, multiple flags", - args: []string{"--deleted", "--deployed", "--failed"}, + args: []string{"--deleted", "--deployed", "--failed", "-q"}, resp: []*release.Release{ releaseMock(&releaseOptions{name: "thomas-guide", statusCode: release.Status_DELETED}), releaseMock(&releaseOptions{name: "atlas-guide", statusCode: release.Status_DEPLOYED}), @@ -60,7 +60,7 @@ func TestListCmd(t *testing.T) { }, { name: "with a release, multiple flags", - args: []string{"--all"}, + args: []string{"--all", "-q"}, resp: []*release.Release{ releaseMock(&releaseOptions{name: "thomas-guide", statusCode: release.Status_DELETED}), releaseMock(&releaseOptions{name: "atlas-guide", statusCode: release.Status_DEPLOYED}), From a4a2f5c983485c860d18d1a794d8d7b1bf43a6fd Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Mon, 26 Sep 2016 13:14:34 -0700 Subject: [PATCH 106/183] chore(*): bump grpc to 1.0.1-GA --- glide.lock | 12 +- glide.yaml | 4 +- pkg/proto/hapi/chart/chart.pb.go | 39 +++-- pkg/proto/hapi/chart/config.pb.go | 14 +- pkg/proto/hapi/chart/metadata.pb.go | 46 +++--- pkg/proto/hapi/chart/template.pb.go | 10 +- pkg/proto/hapi/release/hook.pb.go | 48 +++--- pkg/proto/hapi/release/info.pb.go | 23 +-- pkg/proto/hapi/release/release.pb.go | 36 +++-- pkg/proto/hapi/release/status.pb.go | 38 ++--- pkg/proto/hapi/services/tiller.pb.go | 224 ++++++++++++++++----------- pkg/proto/hapi/version/version.pb.go | 21 ++- 12 files changed, 292 insertions(+), 223 deletions(-) diff --git a/glide.lock b/glide.lock index 808be278c..e30e2d481 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 05c56f2ae4c8bcbaf2c428e2e070ec00f865b284ea61dd671e2c4e117f2d6528 -updated: 2016-08-19T17:30:32.462379907-06:00 +hash: 45eadf6012afd2293f9e82032dba0d4bcff712bf56f8b6cb875c4bdf02d1b08c +updated: 2016-09-26T12:02:34.264796591-07:00 imports: - name: github.com/aokoli/goutils version: 9c37978a95bd5c709a15883b6242714ea6709e64 @@ -127,13 +127,13 @@ imports: subpackages: - lru - name: github.com/golang/protobuf - version: f0a097ddac24fb00e07d2ac17f8671423f3ea47c + version: 8616e8ee5e20a1704615e6c8d7afcdac06087a67 subpackages: - proto - ptypes/any - ptypes/timestamp - name: github.com/google/cadvisor - version: c2ea32971ae033041f0fb0f309b1dee94fd1d55f + version: 404f31ff971a5d5d9595f1a7ca6a07881ac37fed subpackages: - api - cache/memory @@ -295,7 +295,7 @@ imports: - compute/metadata - internal - name: google.golang.org/grpc - version: dec33edc378cf4971a2741cfd86ed70a644d6ba3 + version: 0032a855ba5c8a3c8e0d71c2deef354b70af1584 subpackages: - codes - credentials @@ -310,7 +310,7 @@ imports: - name: gopkg.in/yaml.v2 version: a83829b6f1293c91addabc89d0571c246397bbf4 - name: k8s.io/kubernetes - version: a570a0fef12408f1c0a7214952055bad39c160d7 + version: 17d4cf1af43a03a5997ea9142b19e5b1ac2d8747 subpackages: - federation/apis/federation - federation/apis/federation/install diff --git a/glide.yaml b/glide.yaml index 2f7567469..f0011582d 100644 --- a/glide.yaml +++ b/glide.yaml @@ -14,13 +14,13 @@ import: version: 1.1.0 - package: github.com/technosophos/moniker - package: github.com/golang/protobuf - version: f0a097ddac24fb00e07d2ac17f8671423f3ea47c + version: 8616e8ee5e20a1704615e6c8d7afcdac06087a67 subpackages: - proto - ptypes/any - ptypes/timestamp - package: google.golang.org/grpc - version: dec33edc378cf4971a2741cfd86ed70a644d6ba3 + version: v1.0.1-GA - package: k8s.io/kubernetes version: ~1.3 subpackages: diff --git a/pkg/proto/hapi/chart/chart.pb.go b/pkg/proto/hapi/chart/chart.pb.go index 261951a41..10a525cfd 100644 --- a/pkg/proto/hapi/chart/chart.pb.go +++ b/pkg/proto/hapi/chart/chart.pb.go @@ -34,7 +34,9 @@ var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. -const _ = proto.ProtoPackageIsVersion1 +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package // Chart is a helm package that contains metadata, a default config, zero or more // optionally parameterizable templates, and zero or more charts (dependencies). @@ -96,21 +98,24 @@ func init() { proto.RegisterType((*Chart)(nil), "hapi.chart.Chart") } +func init() { proto.RegisterFile("hapi/chart/chart.proto", fileDescriptor0) } + var fileDescriptor0 = []byte{ - // 239 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcb, 0x48, 0x2c, 0xc8, - 0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0x81, 0x90, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x5c, - 0x20, 0x71, 0x3d, 0xb0, 0x88, 0x94, 0x38, 0xb2, 0x9a, 0xfc, 0xbc, 0xb4, 0xcc, 0x74, 0x88, 0x22, - 0x29, 0x49, 0x24, 0x89, 0xdc, 0xd4, 0x92, 0xc4, 0x94, 0xc4, 0x92, 0x44, 0x2c, 0x52, 0x25, 0xa9, - 0xb9, 0x05, 0x39, 0x89, 0x25, 0xa9, 0x30, 0xa9, 0xf4, 0xfc, 0xfc, 0xf4, 0x9c, 0x54, 0x7d, 0x30, - 0x2f, 0xa9, 0x34, 0x4d, 0x3f, 0x31, 0xaf, 0x12, 0x22, 0xa5, 0xf4, 0x87, 0x91, 0x8b, 0xd5, 0x19, - 0xa4, 0x47, 0xc8, 0x80, 0x8b, 0x03, 0x66, 0xa2, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0xb7, 0x91, 0x88, - 0x1e, 0xc2, 0x49, 0x7a, 0xbe, 0x50, 0xb9, 0x20, 0xb8, 0x2a, 0x21, 0x23, 0x2e, 0x4e, 0x98, 0x45, - 0xc5, 0x12, 0x4c, 0x0a, 0xcc, 0xe8, 0x5a, 0x42, 0xa0, 0x92, 0x41, 0x08, 0x65, 0x42, 0xa6, 0x5c, - 0x3c, 0x29, 0xa9, 0x05, 0xa9, 0x79, 0x29, 0xa9, 0x79, 0xc9, 0x99, 0x40, 0x6d, 0xcc, 0x60, 0x6d, - 0x82, 0xc8, 0xda, 0xc0, 0xce, 0x09, 0x42, 0x51, 0x26, 0xa4, 0xc5, 0xc5, 0x56, 0x96, 0x98, 0x53, - 0x0a, 0xd4, 0xc0, 0x02, 0x76, 0x9a, 0x10, 0x8a, 0x06, 0x70, 0x08, 0x05, 0x41, 0x55, 0x00, 0xd5, - 0xb2, 0xa6, 0x65, 0xe6, 0x00, 0x95, 0xb2, 0x42, 0x9d, 0x04, 0xf1, 0xbd, 0x1e, 0xcc, 0xf7, 0x7a, - 0x8e, 0x79, 0x95, 0x41, 0x10, 0x25, 0x4e, 0xec, 0x51, 0xac, 0x60, 0x33, 0x92, 0xd8, 0xc0, 0xb2, - 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe9, 0x70, 0x34, 0x75, 0x9e, 0x01, 0x00, 0x00, + // 242 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x90, 0xb1, 0x4e, 0xc3, 0x30, + 0x10, 0x86, 0x15, 0x4a, 0x0a, 0x1c, 0x2c, 0x58, 0x08, 0x4c, 0xa7, 0x8a, 0x09, 0x75, 0x70, 0x50, + 0x11, 0x0f, 0x00, 0xcc, 0x2c, 0x16, 0x13, 0xdb, 0xb5, 0xb9, 0xa4, 0x91, 0x52, 0x3b, 0xaa, 0x5d, + 0xa4, 0xbe, 0x3b, 0x03, 0xea, 0xd9, 0xa6, 0x09, 0xea, 0x12, 0x29, 0xf7, 0x7d, 0xff, 0xe5, 0xbf, + 0xc0, 0xed, 0x0a, 0xbb, 0xa6, 0x58, 0xae, 0x70, 0xe3, 0xc3, 0x53, 0x75, 0x1b, 0xeb, 0xad, 0x80, + 0xfd, 0x5c, 0xf1, 0x64, 0x72, 0xd7, 0x77, 0xac, 0xa9, 0x9a, 0x3a, 0x48, 0x93, 0xfb, 0x1e, 0x58, + 0x93, 0xc7, 0x12, 0x3d, 0x1e, 0x41, 0x9e, 0xd6, 0x5d, 0x8b, 0x9e, 0x12, 0xaa, 0xad, 0xad, 0x5b, + 0x2a, 0xf8, 0x6d, 0xb1, 0xad, 0x0a, 0x34, 0xbb, 0x80, 0x1e, 0x7e, 0x32, 0xc8, 0xdf, 0xf7, 0x19, + 0xf1, 0x04, 0xe7, 0x69, 0xa3, 0xcc, 0xa6, 0xd9, 0xe3, 0xe5, 0xfc, 0x46, 0x1d, 0x2a, 0xa9, 0x8f, + 0xc8, 0xf4, 0x9f, 0x25, 0xe6, 0x70, 0x91, 0x3e, 0xe4, 0xe4, 0xc9, 0x74, 0xf4, 0x3f, 0xf2, 0x19, + 0xa1, 0x3e, 0x68, 0xe2, 0x05, 0xae, 0x4a, 0xea, 0xc8, 0x94, 0x64, 0x96, 0x0d, 0x39, 0x39, 0xe2, + 0xd8, 0x75, 0x3f, 0xc6, 0x75, 0xf4, 0x40, 0x13, 0x33, 0x18, 0x7f, 0x63, 0xbb, 0x25, 0x27, 0x4f, + 0xb9, 0x9a, 0x18, 0x04, 0xf8, 0x0f, 0xe9, 0x68, 0x88, 0x19, 0xe4, 0x55, 0xd3, 0x92, 0x93, 0x79, + 0xac, 0x14, 0xae, 0x57, 0xe9, 0x7a, 0xf5, 0x6a, 0x76, 0x3a, 0x28, 0x6f, 0x67, 0x5f, 0x39, 0xef, + 0x58, 0x8c, 0x99, 0x3e, 0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0xe9, 0x70, 0x34, 0x75, 0x9e, 0x01, + 0x00, 0x00, } diff --git a/pkg/proto/hapi/chart/config.pb.go b/pkg/proto/hapi/chart/config.pb.go index c53bf71af..a7b61885a 100644 --- a/pkg/proto/hapi/chart/config.pb.go +++ b/pkg/proto/hapi/chart/config.pb.go @@ -46,8 +46,10 @@ func init() { proto.RegisterType((*Value)(nil), "hapi.chart.Value") } +func init() { proto.RegisterFile("hapi/chart/config.proto", fileDescriptor1) } + var fileDescriptor1 = []byte{ - // 179 bytes of a gzipped FileDescriptorProto + // 182 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0x48, 0x2c, 0xc8, 0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0xd1, 0x4f, 0xce, 0xcf, 0x4b, 0xcb, 0x4c, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x02, 0x49, 0xe8, 0x81, 0x25, 0x94, 0x16, 0x30, 0x72, 0xb1, 0x39, @@ -55,9 +57,9 @@ var fileDescriptor1 = []byte{ 0x40, 0x4c, 0x21, 0x33, 0x2e, 0xb6, 0xb2, 0xc4, 0x9c, 0xd2, 0xd4, 0x62, 0x09, 0x26, 0x05, 0x66, 0x0d, 0x6e, 0x23, 0x39, 0x3d, 0x84, 0x4e, 0x3d, 0x88, 0x2e, 0xbd, 0x30, 0xb0, 0x02, 0xd7, 0xbc, 0x92, 0xa2, 0xca, 0x20, 0xa8, 0x6a, 0x29, 0x1f, 0x2e, 0x6e, 0x24, 0x61, 0x90, 0xc1, 0xd9, 0xa9, - 0x95, 0x30, 0x83, 0x81, 0x4c, 0x21, 0x75, 0x2e, 0x56, 0xb0, 0x52, 0xa0, 0xb9, 0x8c, 0x40, 0x73, - 0x05, 0x91, 0xcd, 0x05, 0xeb, 0x0c, 0x82, 0xc8, 0x5b, 0x31, 0x59, 0x30, 0x2a, 0xc9, 0x72, 0xb1, - 0x82, 0xc5, 0x84, 0x44, 0x60, 0xba, 0x20, 0x26, 0x41, 0x38, 0x4e, 0xec, 0x51, 0xac, 0x60, 0x8d, - 0x49, 0x6c, 0x60, 0xdf, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xe1, 0x12, 0x60, 0xda, 0xf8, - 0x00, 0x00, 0x00, + 0x95, 0x30, 0x83, 0xb3, 0x53, 0x2b, 0x85, 0xd4, 0xb9, 0x58, 0xc1, 0x4a, 0x25, 0x98, 0x14, 0x18, + 0x35, 0xb8, 0x8d, 0x04, 0x91, 0xcd, 0x05, 0xeb, 0x0c, 0x82, 0xc8, 0x5b, 0x31, 0x59, 0x30, 0x2a, + 0xc9, 0x72, 0xb1, 0x82, 0xc5, 0x84, 0x44, 0x60, 0xba, 0x20, 0x26, 0x41, 0x38, 0x4e, 0xec, 0x51, + 0xac, 0x60, 0x8d, 0x49, 0x6c, 0x60, 0xdf, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xe1, 0x12, + 0x60, 0xda, 0xf8, 0x00, 0x00, 0x00, } diff --git a/pkg/proto/hapi/chart/metadata.pb.go b/pkg/proto/hapi/chart/metadata.pb.go index e45ab4e49..f049137a5 100644 --- a/pkg/proto/hapi/chart/metadata.pb.go +++ b/pkg/proto/hapi/chart/metadata.pb.go @@ -114,27 +114,29 @@ func init() { proto.RegisterEnum("hapi.chart.Metadata_Engine", Metadata_Engine_name, Metadata_Engine_value) } +func init() { proto.RegisterFile("hapi/chart/metadata.proto", fileDescriptor2) } + var fileDescriptor2 = []byte{ - // 326 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x52, 0xcf, 0x4b, 0xc3, 0x30, - 0x14, 0x76, 0x3f, 0xda, 0x6e, 0xaf, 0x1e, 0x46, 0x90, 0x11, 0x3d, 0xc8, 0xe8, 0xc9, 0x53, 0x07, - 0x0a, 0x22, 0x1e, 0x45, 0xf1, 0xa0, 0xdb, 0x64, 0x28, 0xc2, 0x6e, 0xb1, 0x7d, 0xb8, 0xa0, 0x4d, - 0x4a, 0x12, 0x95, 0xfd, 0xc7, 0xfe, 0x19, 0x26, 0x69, 0xb7, 0x75, 0xb8, 0x43, 0xe1, 0x7d, 0xdf, - 0x97, 0x7c, 0xef, 0xbd, 0xaf, 0x81, 0xe3, 0x25, 0x2b, 0xf9, 0x38, 0x5b, 0x32, 0x65, 0xc6, 0x05, - 0x1a, 0x96, 0x33, 0xc3, 0xd2, 0x52, 0x49, 0x23, 0x09, 0x38, 0x29, 0xf5, 0x52, 0x72, 0x09, 0x30, - 0x61, 0x5c, 0x18, 0xfb, 0xa1, 0x22, 0x04, 0xba, 0x82, 0x15, 0x48, 0x5b, 0xa3, 0xd6, 0x59, 0x7f, - 0xee, 0x6b, 0x72, 0x04, 0x01, 0x16, 0x8c, 0x7f, 0xd2, 0xb6, 0x27, 0x2b, 0x90, 0x2c, 0x00, 0x6e, - 0xb1, 0x44, 0x91, 0xa3, 0xc8, 0x56, 0x7b, 0xef, 0x9d, 0x02, 0x28, 0x2c, 0xa5, 0xe6, 0x46, 0xaa, - 0x55, 0x7d, 0xb9, 0xc1, 0x10, 0x0a, 0xd1, 0x37, 0x2a, 0xcd, 0xa5, 0xa0, 0x1d, 0x2f, 0xae, 0x61, - 0xf2, 0xdb, 0x86, 0xde, 0xa4, 0x1e, 0x79, 0xaf, 0xb5, 0xe5, 0x96, 0xd2, 0x72, 0x95, 0xa9, 0xaf, - 0x9d, 0x9d, 0x96, 0x5f, 0x2a, 0x43, 0x6d, 0xed, 0x3a, 0xce, 0xae, 0x86, 0xcd, 0x46, 0xdd, 0x9d, - 0x46, 0x64, 0x04, 0x71, 0x8e, 0x3a, 0x53, 0xbc, 0x34, 0x4e, 0x0d, 0xbc, 0xda, 0xa4, 0xc8, 0x09, - 0xf4, 0x3e, 0x70, 0xf5, 0x23, 0x55, 0xae, 0x69, 0xe8, 0x6d, 0x37, 0x98, 0x5c, 0x41, 0x5c, 0x6c, - 0xa2, 0xd3, 0x34, 0xb2, 0x72, 0x7c, 0x3e, 0x4c, 0xb7, 0xe1, 0xa6, 0xdb, 0x64, 0xe7, 0xcd, 0xa3, - 0x64, 0x08, 0x21, 0x8a, 0x77, 0x5b, 0xd3, 0x9e, 0x6f, 0x59, 0x23, 0xb7, 0x17, 0xcf, 0xec, 0x20, - 0xfd, 0x6a, 0x2f, 0x57, 0x93, 0x6b, 0x38, 0xcc, 0xd7, 0x41, 0x73, 0xbb, 0x1c, 0xfc, 0x6f, 0xb3, - 0xfd, 0x11, 0xf3, 0x9d, 0xb3, 0xc9, 0x08, 0xc2, 0xbb, 0xca, 0x39, 0x86, 0xe8, 0x65, 0xfa, 0x30, - 0x9d, 0xbd, 0x4e, 0x07, 0x07, 0xa4, 0x0f, 0xc1, 0xfd, 0xec, 0xf9, 0xe9, 0x71, 0xd0, 0xba, 0x89, - 0x16, 0x81, 0xf7, 0x78, 0x0b, 0xfd, 0xd3, 0xb8, 0xf8, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xb4, 0xec, - 0x4d, 0xaf, 0x37, 0x02, 0x00, 0x00, + // 327 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x52, 0x41, 0x4b, 0xf3, 0x40, + 0x10, 0xfd, 0xda, 0x34, 0x49, 0x3b, 0xf9, 0x0e, 0x65, 0x90, 0xb2, 0x7a, 0x90, 0x90, 0x53, 0x4f, + 0x29, 0x28, 0x88, 0x78, 0x14, 0xc5, 0x83, 0xb6, 0x95, 0xa2, 0x08, 0xbd, 0xad, 0xc9, 0x60, 0x17, + 0xcd, 0x6e, 0xd8, 0x5d, 0x95, 0xfe, 0x63, 0x7f, 0x86, 0x64, 0x9b, 0x36, 0x29, 0xf6, 0x36, 0x6f, + 0xde, 0xcc, 0x7b, 0x99, 0x97, 0x85, 0xe3, 0x15, 0x2f, 0xc5, 0x24, 0x5b, 0x71, 0x6d, 0x27, 0x05, + 0x59, 0x9e, 0x73, 0xcb, 0xd3, 0x52, 0x2b, 0xab, 0x10, 0x2a, 0x2a, 0x75, 0x54, 0x72, 0x01, 0x30, + 0xe5, 0x42, 0x5a, 0x2e, 0x24, 0x69, 0x44, 0xe8, 0x49, 0x5e, 0x10, 0xeb, 0xc4, 0x9d, 0xf1, 0x60, + 0xe1, 0x6a, 0x3c, 0x02, 0x9f, 0x0a, 0x2e, 0x3e, 0x58, 0xd7, 0x35, 0x37, 0x20, 0x59, 0x02, 0xdc, + 0x50, 0x49, 0x32, 0x27, 0x99, 0xad, 0x0f, 0xee, 0x9d, 0x02, 0x68, 0x2a, 0x95, 0x11, 0x56, 0xe9, + 0x75, 0xbd, 0xdc, 0xea, 0x20, 0x83, 0xf0, 0x8b, 0xb4, 0x11, 0x4a, 0x32, 0xcf, 0x91, 0x5b, 0x98, + 0xfc, 0x74, 0xa1, 0x3f, 0xad, 0x3f, 0xf9, 0xa0, 0x34, 0x42, 0x6f, 0xa5, 0x0a, 0xaa, 0x45, 0x5d, + 0x5d, 0xc9, 0x19, 0xf5, 0xa9, 0x33, 0x32, 0xcc, 0x8b, 0xbd, 0x4a, 0xae, 0x86, 0x6d, 0xa3, 0xde, + 0x9e, 0x11, 0xc6, 0x10, 0xe5, 0x64, 0x32, 0x2d, 0x4a, 0x5b, 0xb1, 0xbe, 0x63, 0xdb, 0x2d, 0x3c, + 0x81, 0xfe, 0x3b, 0xad, 0xbf, 0x95, 0xce, 0x0d, 0x0b, 0x9c, 0xec, 0x0e, 0xe3, 0x25, 0x44, 0xc5, + 0x2e, 0x3a, 0xc3, 0xc2, 0xd8, 0x1b, 0x47, 0x67, 0xa3, 0xb4, 0x09, 0x37, 0x6d, 0x92, 0x5d, 0xb4, + 0x47, 0x71, 0x04, 0x01, 0xc9, 0x37, 0x21, 0x89, 0xf5, 0x9d, 0x65, 0x8d, 0xaa, 0xbb, 0x44, 0xa6, + 0x24, 0x1b, 0x6c, 0xee, 0xaa, 0x6a, 0xbc, 0x82, 0xff, 0xf9, 0x36, 0x68, 0x41, 0x86, 0xc1, 0x5f, + 0x9b, 0xe6, 0x47, 0x2c, 0xf6, 0x66, 0x93, 0x18, 0x82, 0xdb, 0x8d, 0x72, 0x04, 0xe1, 0xf3, 0xec, + 0x7e, 0x36, 0x7f, 0x99, 0x0d, 0xff, 0xe1, 0x00, 0xfc, 0xbb, 0xf9, 0xd3, 0xe3, 0xc3, 0xb0, 0x73, + 0x1d, 0x2e, 0x7d, 0xa7, 0xf1, 0x1a, 0xb8, 0xa7, 0x71, 0xfe, 0x1b, 0x00, 0x00, 0xff, 0xff, 0xb4, + 0xec, 0x4d, 0xaf, 0x37, 0x02, 0x00, 0x00, } diff --git a/pkg/proto/hapi/chart/template.pb.go b/pkg/proto/hapi/chart/template.pb.go index 115bc945e..2bed587b5 100644 --- a/pkg/proto/hapi/chart/template.pb.go +++ b/pkg/proto/hapi/chart/template.pb.go @@ -33,13 +33,15 @@ func init() { proto.RegisterType((*Template)(nil), "hapi.chart.Template") } +func init() { proto.RegisterFile("hapi/chart/template.proto", fileDescriptor3) } + var fileDescriptor3 = []byte{ - // 106 bytes of a gzipped FileDescriptorProto + // 107 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8, 0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0xd1, 0x2f, 0x49, 0xcd, 0x2d, 0xc8, 0x49, 0x2c, 0x49, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x02, 0x49, 0xe9, 0x81, 0xa5, 0x94, 0x8c, 0xb8, 0x38, 0x42, 0xa0, 0xb2, 0x42, 0x42, 0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, - 0x9c, 0x41, 0x60, 0x36, 0x48, 0x2c, 0x25, 0xb1, 0x24, 0x51, 0x82, 0x09, 0x28, 0xc6, 0x13, 0x04, - 0x66, 0x3b, 0xb1, 0x47, 0xb1, 0x82, 0x35, 0x27, 0xb1, 0x81, 0xcd, 0x33, 0x06, 0x04, 0x00, 0x00, - 0xff, 0xff, 0x53, 0xee, 0x0e, 0x67, 0x6c, 0x00, 0x00, 0x00, + 0x9c, 0x41, 0x60, 0x36, 0x48, 0x2c, 0x25, 0xb1, 0x24, 0x51, 0x82, 0x49, 0x81, 0x51, 0x83, 0x27, + 0x08, 0xcc, 0x76, 0x62, 0x8f, 0x62, 0x05, 0x6b, 0x4e, 0x62, 0x03, 0x9b, 0x67, 0x0c, 0x08, 0x00, + 0x00, 0xff, 0xff, 0x53, 0xee, 0x0e, 0x67, 0x6c, 0x00, 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/hook.pb.go b/pkg/proto/hapi/release/hook.pb.go index 8694579a7..0a0fecdbe 100644 --- a/pkg/proto/hapi/release/hook.pb.go +++ b/pkg/proto/hapi/release/hook.pb.go @@ -31,7 +31,9 @@ var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. -const _ = proto.ProtoPackageIsVersion1 +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package type Hook_Event int32 @@ -79,7 +81,7 @@ type Hook struct { // Manifest is the manifest contents. Manifest string `protobuf:"bytes,4,opt,name=manifest" json:"manifest,omitempty"` // Events are the events that this hook fires on. - Events []Hook_Event `protobuf:"varint,5,rep,name=events,enum=hapi.release.Hook_Event" json:"events,omitempty"` + 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"` } @@ -101,25 +103,27 @@ func init() { proto.RegisterEnum("hapi.release.Hook_Event", Hook_Event_name, Hook_Event_value) } +func init() { proto.RegisterFile("hapi/release/hook.proto", fileDescriptor0) } + var fileDescriptor0 = []byte{ - // 300 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0x5f, 0x4f, 0xc2, 0x30, - 0x14, 0xc5, 0x05, 0xc6, 0x80, 0x0b, 0xc1, 0xa5, 0x2f, 0x36, 0xbc, 0x48, 0x78, 0xe2, 0xa9, 0x33, - 0x18, 0x3f, 0x00, 0x86, 0x46, 0x8d, 0x64, 0x90, 0x32, 0x62, 0xe2, 0x0b, 0x29, 0xb1, 0xc0, 0x02, - 0x6b, 0x17, 0xd6, 0xf9, 0xe2, 0xd7, 0xf3, 0x83, 0xb9, 0x76, 0x7f, 0xe2, 0xdb, 0xed, 0xef, 0x9c, - 0x7b, 0x7a, 0x0f, 0xdc, 0x9d, 0x78, 0x12, 0xf9, 0x57, 0x71, 0x11, 0x3c, 0x15, 0xfe, 0x49, 0xa9, - 0x33, 0x49, 0xae, 0x4a, 0x2b, 0x34, 0x30, 0x02, 0x29, 0x85, 0xd1, 0xfd, 0x51, 0xa9, 0xe3, 0x45, - 0xf8, 0x56, 0xdb, 0x67, 0x07, 0x5f, 0x47, 0xb1, 0x48, 0x35, 0x8f, 0x93, 0xc2, 0x3e, 0xf9, 0x6d, - 0x82, 0xf3, 0x9a, 0x6f, 0x23, 0x04, 0x8e, 0xe4, 0xb1, 0xc0, 0x8d, 0x71, 0x63, 0xda, 0x63, 0x76, - 0x36, 0xec, 0x1c, 0xc9, 0x2f, 0xdc, 0x2c, 0x98, 0x99, 0x0d, 0x4b, 0xb8, 0x3e, 0xe1, 0x56, 0xc1, - 0xcc, 0x8c, 0x46, 0xd0, 0x8d, 0xb9, 0x8c, 0x0e, 0x79, 0x32, 0x76, 0x2c, 0xaf, 0xdf, 0xe8, 0x01, - 0x5c, 0xf1, 0x2d, 0xa4, 0x4e, 0x71, 0x7b, 0xdc, 0x9a, 0x0e, 0x67, 0x98, 0xfc, 0x3f, 0x90, 0x98, - 0xbf, 0x09, 0x35, 0x06, 0x56, 0xfa, 0xd0, 0x13, 0x74, 0x2f, 0x3c, 0xd5, 0xbb, 0x6b, 0x26, 0xb1, - 0x9b, 0xa7, 0xf5, 0x67, 0x23, 0x52, 0xd4, 0x20, 0x55, 0x0d, 0x12, 0x56, 0x35, 0x58, 0xc7, 0x78, - 0x59, 0x26, 0x27, 0x3f, 0xd0, 0xb6, 0x39, 0xa8, 0x0f, 0x9d, 0x6d, 0xf0, 0x1e, 0xac, 0x3e, 0x02, - 0xef, 0x06, 0xdd, 0x42, 0x7f, 0xcd, 0xe8, 0xee, 0x2d, 0xd8, 0x84, 0xf3, 0xe5, 0xd2, 0x6b, 0x20, - 0x0f, 0x06, 0xeb, 0xd5, 0x26, 0xac, 0x49, 0x13, 0x0d, 0x01, 0x8c, 0x65, 0x41, 0x97, 0x34, 0xa4, - 0x5e, 0xcb, 0xae, 0x18, 0x47, 0x09, 0x9c, 0x2a, 0x63, 0xbb, 0x7e, 0x61, 0xf3, 0x05, 0xf5, 0xda, - 0x75, 0x46, 0x45, 0xdc, 0xe7, 0xde, 0x67, 0xa7, 0x6c, 0xb4, 0x77, 0xed, 0x91, 0x8f, 0x7f, 0x01, - 0x00, 0x00, 0xff, 0xff, 0x16, 0x64, 0x61, 0x76, 0xa2, 0x01, 0x00, 0x00, + // 301 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0x4f, 0x4f, 0xc2, 0x30, + 0x18, 0xc6, 0x1d, 0x8c, 0x01, 0x2f, 0x04, 0x97, 0x5e, 0x6c, 0xb8, 0x48, 0x38, 0x71, 0xea, 0x0c, + 0xc6, 0x0f, 0x80, 0xa1, 0x51, 0x23, 0x19, 0xa4, 0x8c, 0x98, 0x78, 0x21, 0x25, 0x16, 0x58, 0x60, + 0xed, 0xb2, 0x75, 0x5e, 0xfc, 0x7a, 0x7e, 0x30, 0xd3, 0xee, 0x4f, 0xbc, 0xbd, 0xfb, 0xbd, 0xbf, + 0x3d, 0x7d, 0x1f, 0xb8, 0x3b, 0xf3, 0x34, 0x0e, 0x32, 0x71, 0x15, 0x3c, 0x17, 0xc1, 0x59, 0xa9, + 0x0b, 0x49, 0x33, 0xa5, 0x15, 0x1a, 0x9a, 0x05, 0xa9, 0x16, 0xe3, 0xfb, 0x93, 0x52, 0xa7, 0xab, + 0x08, 0xec, 0xee, 0x50, 0x1c, 0x03, 0x1d, 0x27, 0x22, 0xd7, 0x3c, 0x49, 0x4b, 0x7d, 0xfa, 0xdb, + 0x02, 0xf7, 0x55, 0xa9, 0x0b, 0x42, 0xe0, 0x4a, 0x9e, 0x08, 0xec, 0x4c, 0x9c, 0x59, 0x9f, 0xd9, + 0xd9, 0xb0, 0x4b, 0x2c, 0xbf, 0x70, 0xab, 0x64, 0x66, 0x36, 0x2c, 0xe5, 0xfa, 0x8c, 0xdb, 0x25, + 0x33, 0x33, 0x1a, 0x43, 0x2f, 0xe1, 0x32, 0x3e, 0x8a, 0x5c, 0x63, 0xd7, 0xf2, 0xe6, 0x1b, 0x3d, + 0x80, 0x27, 0xbe, 0x85, 0xd4, 0x39, 0xee, 0x4c, 0xda, 0xb3, 0xd1, 0x1c, 0x93, 0xff, 0x07, 0x12, + 0xf3, 0x36, 0xa1, 0x46, 0x60, 0x95, 0x87, 0x9e, 0xa0, 0x77, 0xe5, 0xb9, 0xde, 0x67, 0x85, 0xc4, + 0xde, 0xc4, 0x99, 0x0d, 0xe6, 0x63, 0x52, 0xd6, 0x20, 0x75, 0x0d, 0x12, 0xd5, 0x35, 0x58, 0xd7, + 0xb8, 0xac, 0x90, 0xd3, 0x1f, 0xe8, 0xd8, 0x1c, 0x34, 0x80, 0xee, 0x2e, 0x7c, 0x0f, 0xd7, 0x1f, + 0xa1, 0x7f, 0x83, 0x6e, 0x61, 0xb0, 0x61, 0x74, 0xff, 0x16, 0x6e, 0xa3, 0xc5, 0x6a, 0xe5, 0x3b, + 0xc8, 0x87, 0xe1, 0x66, 0xbd, 0x8d, 0x1a, 0xd2, 0x42, 0x23, 0x00, 0xa3, 0x2c, 0xe9, 0x8a, 0x46, + 0xd4, 0x6f, 0xdb, 0x5f, 0x8c, 0x51, 0x01, 0xb7, 0xce, 0xd8, 0x6d, 0x5e, 0xd8, 0x62, 0x49, 0xfd, + 0x4e, 0x93, 0x51, 0x13, 0xef, 0xb9, 0xff, 0xd9, 0xad, 0x1a, 0x1d, 0x3c, 0x7b, 0xe4, 0xe3, 0x5f, + 0x00, 0x00, 0x00, 0xff, 0xff, 0x16, 0x64, 0x61, 0x76, 0xa2, 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/info.pb.go b/pkg/proto/hapi/release/info.pb.go index c54578569..a63a039cd 100644 --- a/pkg/proto/hapi/release/info.pb.go +++ b/pkg/proto/hapi/release/info.pb.go @@ -60,19 +60,22 @@ func init() { proto.RegisterType((*Info)(nil), "hapi.release.Info") } +func init() { proto.RegisterFile("hapi/release/info.proto", fileDescriptor1) } + var fileDescriptor1 = []byte{ - // 208 bytes of a gzipped FileDescriptorProto + // 212 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0x48, 0x2c, 0xc8, 0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0xcf, 0xcc, 0x4b, 0xcb, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x01, 0x49, 0xe8, 0x41, 0x25, 0xa4, 0xe4, 0xd3, 0xf3, 0xf3, 0xd3, 0x73, 0x52, 0xf5, 0xc1, 0x72, 0x49, 0xa5, 0x69, 0xfa, 0x25, 0x99, 0xb9, 0xa9, 0xc5, 0x25, 0x89, - 0xb9, 0x05, 0x10, 0xe5, 0x52, 0x92, 0x28, 0xe6, 0x00, 0x65, 0x4a, 0x4a, 0x8b, 0x21, 0x52, 0x4a, - 0xef, 0x18, 0xb9, 0x58, 0x3c, 0x81, 0x06, 0x0b, 0xe9, 0x70, 0xb1, 0x41, 0x24, 0x24, 0x18, 0x15, - 0x18, 0x35, 0xb8, 0x8d, 0x44, 0xf4, 0x90, 0xed, 0xd0, 0x0b, 0x06, 0xcb, 0x05, 0x41, 0xd5, 0x08, - 0x39, 0x72, 0xf1, 0xa5, 0x65, 0x16, 0x15, 0x97, 0xc4, 0xa7, 0xa4, 0x16, 0xe4, 0xe4, 0x57, 0xa6, - 0xa6, 0x48, 0x30, 0x81, 0x75, 0x49, 0xe9, 0x41, 0xdc, 0xa2, 0x07, 0x73, 0x8b, 0x5e, 0x08, 0xcc, - 0x2d, 0x41, 0xbc, 0x60, 0x1d, 0x2e, 0x50, 0x0d, 0x42, 0xf6, 0x5c, 0xbc, 0x39, 0x89, 0xc8, 0x26, - 0x30, 0x13, 0x34, 0x81, 0x07, 0xa4, 0x01, 0x6e, 0x80, 0x09, 0x17, 0x7b, 0x0a, 0xd0, 0x75, 0x25, - 0x40, 0xad, 0x2c, 0x04, 0xb5, 0xc2, 0x94, 0x3a, 0x71, 0x46, 0xb1, 0x43, 0xfd, 0x94, 0xc4, 0x06, - 0x56, 0x67, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0xeb, 0x9d, 0xa1, 0xf8, 0x67, 0x01, 0x00, 0x00, + 0xb9, 0x05, 0x10, 0xe5, 0x52, 0x92, 0x28, 0xe6, 0x14, 0x97, 0x24, 0x96, 0x94, 0x16, 0x43, 0xa4, + 0x94, 0xde, 0x31, 0x72, 0xb1, 0x78, 0xe6, 0xa5, 0xe5, 0x0b, 0xe9, 0x70, 0xb1, 0x41, 0x24, 0x24, + 0x18, 0x15, 0x18, 0x35, 0xb8, 0x8d, 0x44, 0xf4, 0x90, 0xed, 0xd0, 0x0b, 0x06, 0xcb, 0x05, 0x41, + 0xd5, 0x08, 0x39, 0x72, 0xf1, 0xa5, 0x65, 0x16, 0x15, 0x97, 0xc4, 0xa7, 0xa4, 0x16, 0xe4, 0xe4, + 0x57, 0xa6, 0xa6, 0x48, 0x30, 0x81, 0x75, 0x49, 0xe9, 0x41, 0xdc, 0xa2, 0x07, 0x73, 0x8b, 0x5e, + 0x08, 0xcc, 0x2d, 0x41, 0xbc, 0x60, 0x1d, 0x2e, 0x50, 0x0d, 0x42, 0xf6, 0x5c, 0xbc, 0x39, 0x89, + 0xc8, 0x26, 0x30, 0x13, 0x34, 0x81, 0x07, 0xa4, 0x01, 0x6e, 0x80, 0x09, 0x17, 0x7b, 0x4a, 0x6a, + 0x4e, 0x6a, 0x49, 0x6a, 0x8a, 0x04, 0x0b, 0x41, 0xad, 0x30, 0xa5, 0x4e, 0x9c, 0x51, 0xec, 0x50, + 0x3f, 0x25, 0xb1, 0x81, 0xd5, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xeb, 0x9d, 0xa1, 0xf8, + 0x67, 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/release.pb.go b/pkg/proto/hapi/release/release.pb.go index f6a255d55..72255e3e2 100644 --- a/pkg/proto/hapi/release/release.pb.go +++ b/pkg/proto/hapi/release/release.pb.go @@ -74,22 +74,24 @@ func init() { proto.RegisterType((*Release)(nil), "hapi.release.Release") } +func init() { proto.RegisterFile("hapi/release/release.proto", fileDescriptor2) } + var fileDescriptor2 = []byte{ - // 254 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x90, 0x3f, 0x4f, 0xc3, 0x30, - 0x10, 0xc5, 0xd5, 0x36, 0x7f, 0x9a, 0x83, 0x85, 0x1b, 0xe0, 0x14, 0x31, 0x54, 0x0c, 0x50, 0x31, - 0xa4, 0x12, 0x7c, 0x03, 0x58, 0x60, 0xf5, 0xc8, 0x66, 0x22, 0x87, 0x58, 0x50, 0x3b, 0x8a, 0x23, - 0x3e, 0x0b, 0x1f, 0x17, 0xdb, 0xe7, 0x42, 0x0a, 0x8b, 0x13, 0xbf, 0xdf, 0xd3, 0xbb, 0xe7, 0x83, - 0xba, 0x97, 0x83, 0xde, 0x8d, 0xea, 0x43, 0x49, 0xa7, 0x0e, 0xdf, 0x66, 0x18, 0xed, 0x64, 0xf1, - 0x34, 0xb0, 0x26, 0x69, 0xf5, 0xc5, 0x91, 0xb3, 0xb7, 0xf6, 0x9d, 0x6d, 0x7f, 0x80, 0x36, 0x9d, - 0x3d, 0x02, 0x6d, 0x2f, 0xc7, 0x69, 0xd7, 0x5a, 0xd3, 0xe9, 0xb7, 0x04, 0xce, 0xe7, 0x20, 0x9c, - 0xac, 0x5f, 0x7d, 0x2d, 0xa1, 0x14, 0x9c, 0x83, 0x08, 0x99, 0x91, 0x7b, 0x45, 0x8b, 0xcd, 0x62, - 0x5b, 0x89, 0xf8, 0x8f, 0xd7, 0x90, 0x85, 0x78, 0x5a, 0x7a, 0xed, 0xe4, 0x0e, 0x9b, 0x79, 0xbf, - 0xe6, 0xd9, 0x13, 0x11, 0x39, 0xde, 0x40, 0x1e, 0x63, 0x69, 0x15, 0x8d, 0x67, 0x6c, 0xe4, 0x49, - 0x8f, 0xe1, 0x14, 0xcc, 0xf1, 0x16, 0x0a, 0x2e, 0x46, 0xd9, 0x3c, 0x32, 0x39, 0x23, 0x11, 0xc9, - 0x81, 0x35, 0xac, 0xf7, 0xd2, 0xe8, 0x4e, 0xb9, 0x89, 0xf2, 0x58, 0xea, 0xe7, 0x8e, 0x5b, 0xc8, - 0xc3, 0x42, 0x1c, 0x15, 0x9b, 0xd5, 0xff, 0x66, 0x4f, 0x1e, 0x09, 0x36, 0x20, 0x41, 0xf9, 0xa9, - 0x46, 0xa7, 0xad, 0xa1, 0xd2, 0x87, 0xe4, 0xe2, 0x70, 0xc5, 0x4b, 0xa8, 0xc2, 0x23, 0xdd, 0x20, - 0x5b, 0x45, 0xeb, 0x38, 0xe0, 0x57, 0x78, 0xa8, 0x5e, 0xca, 0x14, 0xf7, 0x5a, 0xc4, 0x65, 0xdd, - 0x7f, 0x07, 0x00, 0x00, 0xff, 0xff, 0xc8, 0x8f, 0xec, 0x97, 0xbb, 0x01, 0x00, 0x00, + // 256 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x90, 0xbf, 0x4e, 0xc3, 0x40, + 0x0c, 0xc6, 0x95, 0x36, 0x7f, 0x1a, 0xc3, 0x82, 0x07, 0xb0, 0x22, 0x86, 0x88, 0x01, 0x22, 0x86, + 0x54, 0x82, 0x37, 0x80, 0x05, 0xd6, 0x1b, 0xd9, 0x8e, 0xe8, 0x42, 0x4e, 0xa5, 0xe7, 0x28, 0x17, + 0xf1, 0x2c, 0x3c, 0x2e, 0xba, 0x3f, 0x85, 0x94, 0x2e, 0x4e, 0xec, 0xdf, 0xa7, 0xcf, 0xdf, 0x19, + 0xaa, 0x41, 0x8e, 0x7a, 0x3b, 0xa9, 0x4f, 0x25, 0xad, 0x3a, 0x7c, 0xdb, 0x71, 0xe2, 0x99, 0xf1, + 0xdc, 0xb1, 0x36, 0xce, 0xaa, 0xab, 0x23, 0xe5, 0xc0, 0xbc, 0x0b, 0xb2, 0x7f, 0x40, 0x9b, 0x9e, + 0x8f, 0x40, 0x37, 0xc8, 0x69, 0xde, 0x76, 0x6c, 0x7a, 0xfd, 0x11, 0xc1, 0xe5, 0x12, 0xb8, 0x1a, + 0xe6, 0x37, 0xdf, 0x2b, 0x28, 0x44, 0xf0, 0x41, 0x84, 0xd4, 0xc8, 0xbd, 0xa2, 0xa4, 0x4e, 0x9a, + 0x52, 0xf8, 0x7f, 0xbc, 0x85, 0xd4, 0xd9, 0xd3, 0xaa, 0x4e, 0x9a, 0xb3, 0x07, 0x6c, 0x97, 0xf9, + 0xda, 0x57, 0xd3, 0xb3, 0xf0, 0x1c, 0xef, 0x20, 0xf3, 0xb6, 0xb4, 0xf6, 0xc2, 0x8b, 0x20, 0x0c, + 0x9b, 0x9e, 0x5d, 0x15, 0x81, 0xe3, 0x3d, 0xe4, 0x21, 0x18, 0xa5, 0x4b, 0xcb, 0xa8, 0xf4, 0x44, + 0x44, 0x05, 0x56, 0xb0, 0xd9, 0x4b, 0xa3, 0x7b, 0x65, 0x67, 0xca, 0x7c, 0xa8, 0xdf, 0x1e, 0x1b, + 0xc8, 0xdc, 0x41, 0x2c, 0xe5, 0xf5, 0xfa, 0x34, 0xd9, 0x0b, 0xf3, 0x4e, 0x04, 0x01, 0x12, 0x14, + 0x5f, 0x6a, 0xb2, 0x9a, 0x0d, 0x15, 0x75, 0xd2, 0x64, 0xe2, 0xd0, 0xe2, 0x35, 0x94, 0xee, 0x91, + 0x76, 0x94, 0x9d, 0xa2, 0x8d, 0x5f, 0xf0, 0x37, 0x78, 0x2a, 0xdf, 0x8a, 0x68, 0xf7, 0x9e, 0xfb, + 0x63, 0x3d, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, 0xc8, 0x8f, 0xec, 0x97, 0xbb, 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/status.pb.go b/pkg/proto/hapi/release/status.pb.go index ee417759d..79c721e31 100644 --- a/pkg/proto/hapi/release/status.pb.go +++ b/pkg/proto/hapi/release/status.pb.go @@ -76,23 +76,25 @@ func init() { proto.RegisterEnum("hapi.release.Status_Code", Status_Code_name, Status_Code_value) } +func init() { proto.RegisterFile("hapi/release/status.proto", fileDescriptor3) } + var fileDescriptor3 = []byte{ - // 259 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0xc1, 0x4e, 0xf2, 0x40, - 0x14, 0x85, 0xff, 0x42, 0xff, 0xd6, 0x5e, 0x08, 0x21, 0x37, 0x2c, 0x5a, 0xe3, 0xc2, 0xb0, 0x72, - 0xe3, 0x6d, 0x82, 0x4f, 0x80, 0x76, 0x4c, 0xd4, 0xa6, 0x90, 0x56, 0x62, 0x74, 0x37, 0xc0, 0x88, - 0x24, 0x4d, 0x87, 0x74, 0xa6, 0x0b, 0x9e, 0xd8, 0xd7, 0x70, 0x3a, 0x85, 0xe8, 0xae, 0xa7, 0xdf, - 0x77, 0xe6, 0xcc, 0x40, 0xf4, 0xc5, 0x0f, 0xfb, 0xb8, 0x16, 0xa5, 0xe0, 0x4a, 0xc4, 0x4a, 0x73, - 0xdd, 0x28, 0x3a, 0xd4, 0x52, 0x4b, 0x1c, 0xb6, 0x88, 0x4e, 0xe8, 0x32, 0xda, 0x49, 0xb9, 0x2b, - 0x45, 0x6c, 0xd9, 0xba, 0xf9, 0x8c, 0x79, 0x75, 0xec, 0xc4, 0xe9, 0xb7, 0x03, 0x5e, 0x61, 0x9b, - 0x78, 0x0b, 0xee, 0x46, 0x6e, 0x45, 0xe8, 0x5c, 0x3b, 0x37, 0xa3, 0x59, 0x44, 0x7f, 0x8f, 0xa0, - 0xce, 0xa1, 0x07, 0x23, 0xe4, 0x56, 0x43, 0x02, 0x7f, 0x2b, 0x34, 0xdf, 0x97, 0x2a, 0xec, 0x99, - 0xc6, 0x60, 0x36, 0xa1, 0x6e, 0x86, 0xce, 0x33, 0x34, 0xaf, 0x8e, 0xf9, 0x59, 0xc2, 0x2b, 0x08, - 0x6a, 0xa1, 0x64, 0x53, 0x6f, 0x84, 0x0a, 0xfb, 0xa6, 0x11, 0xe4, 0xbf, 0x3f, 0x70, 0x02, 0xff, - 0x2b, 0xa9, 0x0d, 0x71, 0x2d, 0xe9, 0xc2, 0xf4, 0x19, 0xdc, 0x76, 0x11, 0x07, 0xe0, 0xaf, 0xb2, - 0x97, 0x6c, 0xf1, 0x96, 0x8d, 0xff, 0xe1, 0x10, 0x2e, 0x12, 0xb6, 0x4c, 0x17, 0xef, 0x2c, 0x19, - 0x3b, 0x2d, 0x4a, 0x58, 0xca, 0x5e, 0x4d, 0xe8, 0xe1, 0x08, 0xa0, 0x58, 0x2d, 0x59, 0x5e, 0xb0, - 0xc4, 0xe4, 0x3e, 0x02, 0x78, 0x8f, 0xf3, 0xa7, 0xd4, 0x7c, 0xbb, 0xf7, 0xc1, 0x87, 0x7f, 0x7a, - 0xcc, 0xda, 0xb3, 0x37, 0xbc, 0xfb, 0x09, 0x00, 0x00, 0xff, 0xff, 0xae, 0x07, 0x47, 0x1f, 0x41, - 0x01, 0x00, 0x00, + // 261 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0xc1, 0x4e, 0x83, 0x40, + 0x10, 0x86, 0xdd, 0x16, 0x41, 0xa6, 0x4d, 0x43, 0x36, 0x3d, 0x80, 0xf1, 0x40, 0x7a, 0xe2, 0xe2, + 0x92, 0xd4, 0x27, 0xa8, 0xee, 0x9a, 0xa8, 0x84, 0x36, 0x60, 0x63, 0xf4, 0x46, 0xcb, 0x58, 0x9b, + 0x10, 0xb6, 0x61, 0x97, 0x43, 0x9f, 0xd8, 0xd7, 0x30, 0x2c, 0x6d, 0xec, 0x71, 0xe6, 0xfb, 0x66, + 0xfe, 0x19, 0x08, 0x7e, 0x8a, 0xc3, 0x3e, 0x6e, 0xb0, 0xc2, 0x42, 0x61, 0xac, 0x74, 0xa1, 0x5b, + 0xc5, 0x0e, 0x8d, 0xd4, 0x92, 0x8e, 0x3b, 0xc4, 0x4e, 0xe8, 0x36, 0xd8, 0x49, 0xb9, 0xab, 0x30, + 0x36, 0x6c, 0xd3, 0x7e, 0xc7, 0x45, 0x7d, 0xec, 0xc5, 0xd9, 0x2f, 0x01, 0x3b, 0x37, 0x93, 0xf4, + 0x1e, 0xac, 0xad, 0x2c, 0xd1, 0x27, 0x21, 0x89, 0x26, 0xf3, 0x80, 0x5d, 0xae, 0x60, 0xbd, 0xc3, + 0x9e, 0x64, 0x89, 0x99, 0xd1, 0x28, 0x03, 0xa7, 0x44, 0x5d, 0xec, 0x2b, 0xe5, 0x0f, 0x42, 0x12, + 0x8d, 0xe6, 0x53, 0xd6, 0xc7, 0xb0, 0x73, 0x0c, 0x5b, 0xd4, 0xc7, 0xec, 0x2c, 0xd1, 0x3b, 0x70, + 0x1b, 0x54, 0xb2, 0x6d, 0xb6, 0xa8, 0xfc, 0x61, 0x48, 0x22, 0x37, 0xfb, 0x6f, 0xd0, 0x29, 0x5c, + 0xd7, 0x52, 0xa3, 0xf2, 0x2d, 0x43, 0xfa, 0x62, 0xf6, 0x0a, 0x56, 0x97, 0x48, 0x47, 0xe0, 0xac, + 0xd3, 0xb7, 0x74, 0xf9, 0x91, 0x7a, 0x57, 0x74, 0x0c, 0x37, 0x5c, 0xac, 0x92, 0xe5, 0xa7, 0xe0, + 0x1e, 0xe9, 0x10, 0x17, 0x89, 0x78, 0x17, 0xdc, 0x1b, 0xd0, 0x09, 0x40, 0xbe, 0x5e, 0x89, 0x2c, + 0x17, 0x5c, 0x70, 0x6f, 0x48, 0x01, 0xec, 0xe7, 0xc5, 0x4b, 0x22, 0xb8, 0x67, 0x3d, 0xba, 0x5f, + 0xce, 0xe9, 0x99, 0x8d, 0x6d, 0x2e, 0x7c, 0xf8, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xae, 0x07, 0x47, + 0x1f, 0x41, 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index ba78f2836..55d1f8ccd 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -49,7 +49,9 @@ var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. -const _ = proto.ProtoPackageIsVersion1 +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package // SortBy defines sort operations. type ListSort_SortBy int32 @@ -120,7 +122,7 @@ type ListReleasesRequest struct { Filter string `protobuf:"bytes,4,opt,name=filter" json:"filter,omitempty"` // SortOrder is the ordering directive used for sorting. SortOrder ListSort_SortOrder `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,enum=hapi.services.tiller.ListSort_SortOrder" json:"sort_order,omitempty"` - StatusCodes []hapi_release1.Status_Code `protobuf:"varint,6,rep,name=status_codes,json=statusCodes,enum=hapi.release.Status_Code" json:"status_codes,omitempty"` + StatusCodes []hapi_release1.Status_Code `protobuf:"varint,6,rep,packed,name=status_codes,json=statusCodes,enum=hapi.release.Status_Code" json:"status_codes,omitempty"` } func (m *ListReleasesRequest) Reset() { *m = ListReleasesRequest{} } @@ -379,7 +381,7 @@ func (*GetVersionRequest) ProtoMessage() {} func (*GetVersionRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{13} } type GetVersionResponse struct { - Version *hapi_version.Version `protobuf:"bytes,1,opt,name=Version,json=version" json:"Version,omitempty"` + Version *hapi_version.Version `protobuf:"bytes,1,opt,name=Version" json:"Version,omitempty"` } func (m *GetVersionResponse) Reset() { *m = GetVersionResponse{} } @@ -420,7 +422,7 @@ var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion1 +const _ = grpc.SupportPackageIsVersion3 // Client API for ReleaseService service @@ -585,76 +587,112 @@ func (x *releaseServiceListReleasesServer) Send(m *ListReleasesResponse) error { return x.ServerStream.SendMsg(m) } -func _ReleaseService_GetReleaseStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _ReleaseService_GetReleaseStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetReleaseStatusRequest) if err := dec(in); err != nil { return nil, err } - out, err := srv.(ReleaseServiceServer).GetReleaseStatus(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(ReleaseServiceServer).GetReleaseStatus(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/hapi.services.tiller.ReleaseService/GetReleaseStatus", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReleaseServiceServer).GetReleaseStatus(ctx, req.(*GetReleaseStatusRequest)) + } + return interceptor(ctx, in, info, handler) } -func _ReleaseService_GetReleaseContent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _ReleaseService_GetReleaseContent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetReleaseContentRequest) if err := dec(in); err != nil { return nil, err } - out, err := srv.(ReleaseServiceServer).GetReleaseContent(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(ReleaseServiceServer).GetReleaseContent(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/hapi.services.tiller.ReleaseService/GetReleaseContent", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReleaseServiceServer).GetReleaseContent(ctx, req.(*GetReleaseContentRequest)) + } + return interceptor(ctx, in, info, handler) } -func _ReleaseService_UpdateRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _ReleaseService_UpdateRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UpdateReleaseRequest) if err := dec(in); err != nil { return nil, err } - out, err := srv.(ReleaseServiceServer).UpdateRelease(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(ReleaseServiceServer).UpdateRelease(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/hapi.services.tiller.ReleaseService/UpdateRelease", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReleaseServiceServer).UpdateRelease(ctx, req.(*UpdateReleaseRequest)) + } + return interceptor(ctx, in, info, handler) } -func _ReleaseService_InstallRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _ReleaseService_InstallRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(InstallReleaseRequest) if err := dec(in); err != nil { return nil, err } - out, err := srv.(ReleaseServiceServer).InstallRelease(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(ReleaseServiceServer).InstallRelease(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/hapi.services.tiller.ReleaseService/InstallRelease", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReleaseServiceServer).InstallRelease(ctx, req.(*InstallReleaseRequest)) + } + return interceptor(ctx, in, info, handler) } -func _ReleaseService_UninstallRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _ReleaseService_UninstallRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UninstallReleaseRequest) if err := dec(in); err != nil { return nil, err } - out, err := srv.(ReleaseServiceServer).UninstallRelease(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(ReleaseServiceServer).UninstallRelease(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/hapi.services.tiller.ReleaseService/UninstallRelease", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReleaseServiceServer).UninstallRelease(ctx, req.(*UninstallReleaseRequest)) + } + return interceptor(ctx, in, info, handler) } -func _ReleaseService_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _ReleaseService_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetVersionRequest) if err := dec(in); err != nil { return nil, err } - out, err := srv.(ReleaseServiceServer).GetVersion(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(ReleaseServiceServer).GetVersion(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/hapi.services.tiller.ReleaseService/GetVersion", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReleaseServiceServer).GetVersion(ctx, req.(*GetVersionRequest)) + } + return interceptor(ctx, in, info, handler) } var _ReleaseService_serviceDesc = grpc.ServiceDesc{ @@ -693,64 +731,68 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ ServerStreams: true, }, }, + Metadata: fileDescriptor0, } +func init() { proto.RegisterFile("hapi/services/tiller.proto", fileDescriptor0) } + var fileDescriptor0 = []byte{ - // 893 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0x5f, 0x6f, 0xe3, 0x44, - 0x10, 0x3f, 0x27, 0xa9, 0x93, 0x4c, 0xda, 0x2a, 0xdd, 0x6b, 0x1b, 0xd7, 0x02, 0x74, 0x32, 0x82, - 0x0b, 0x07, 0x24, 0x10, 0x5e, 0x11, 0x52, 0x2e, 0x17, 0xf5, 0xaa, 0x0b, 0x39, 0x69, 0x43, 0x41, - 0xe2, 0x81, 0xc8, 0x4d, 0x36, 0x17, 0x83, 0x6b, 0x07, 0xef, 0x26, 0xa2, 0x1f, 0x81, 0xcf, 0xc0, - 0x1b, 0xdf, 0x83, 0x4f, 0xc6, 0x0b, 0xeb, 0xfd, 0xe3, 0xc6, 0xa9, 0x4d, 0x7d, 0x79, 0x89, 0xbd, - 0x3b, 0xbf, 0xfd, 0xcd, 0xcc, 0x6f, 0x76, 0xc6, 0x01, 0x7b, 0xe9, 0xae, 0xbc, 0x2e, 0x25, 0xd1, - 0xc6, 0x9b, 0x11, 0xda, 0x65, 0x9e, 0xef, 0x93, 0xa8, 0xb3, 0x8a, 0x42, 0x16, 0xa2, 0xd3, 0xd8, - 0xd6, 0xd1, 0xb6, 0x8e, 0xb4, 0xd9, 0xe7, 0xe2, 0xc4, 0x6c, 0xe9, 0x46, 0x4c, 0xfe, 0x4a, 0xb4, - 0xdd, 0xda, 0xde, 0x0f, 0x83, 0x85, 0xf7, 0x4e, 0x19, 0xa4, 0x8b, 0x88, 0xf8, 0xc4, 0xa5, 0x44, - 0x3f, 0x53, 0x87, 0xb4, 0xcd, 0x0b, 0x16, 0xa1, 0x32, 0x5c, 0xa4, 0x0c, 0x94, 0xb9, 0x6c, 0x4d, - 0x53, 0x7c, 0x1b, 0x12, 0x51, 0x2f, 0x0c, 0xf4, 0x53, 0xda, 0x9c, 0xbf, 0x4b, 0xf0, 0x74, 0xe4, - 0x51, 0x86, 0xe5, 0x41, 0x8a, 0xc9, 0xef, 0x6b, 0x42, 0x19, 0x3a, 0x85, 0x03, 0xdf, 0xbb, 0xf5, - 0x98, 0x65, 0x3c, 0x33, 0xda, 0x65, 0x2c, 0x17, 0xe8, 0x1c, 0xcc, 0x70, 0xb1, 0xa0, 0x84, 0x59, - 0x25, 0xbe, 0x5d, 0xc7, 0x6a, 0x85, 0xbe, 0x83, 0x2a, 0x0d, 0x23, 0x36, 0xbd, 0xb9, 0xb3, 0xca, - 0xdc, 0x70, 0xdc, 0xfb, 0xa4, 0x93, 0x25, 0x45, 0x27, 0xf6, 0x34, 0xe1, 0xc0, 0x4e, 0xfc, 0xf3, - 0xf2, 0x0e, 0x9b, 0x54, 0x3c, 0x63, 0xde, 0x85, 0xe7, 0x33, 0x12, 0x59, 0x15, 0xc9, 0x2b, 0x57, - 0xe8, 0x12, 0x40, 0xf0, 0x86, 0xd1, 0x9c, 0xdb, 0x0e, 0x04, 0x75, 0xbb, 0x00, 0xf5, 0xdb, 0x18, - 0x8f, 0xeb, 0x54, 0xbf, 0xa2, 0x6f, 0xe1, 0x50, 0x4a, 0x32, 0x9d, 0x85, 0x73, 0x42, 0x2d, 0xf3, - 0x59, 0x99, 0x53, 0x5d, 0x48, 0x2a, 0xad, 0xf0, 0x44, 0x8a, 0x36, 0xe0, 0x08, 0xdc, 0x90, 0xf0, - 0xf8, 0x9d, 0x3a, 0xbf, 0x40, 0x4d, 0xd3, 0x3b, 0x3d, 0x30, 0x65, 0xf0, 0xa8, 0x01, 0xd5, 0xeb, - 0xf1, 0x9b, 0xf1, 0xdb, 0x9f, 0xc6, 0xcd, 0x27, 0xa8, 0x06, 0x95, 0x71, 0xff, 0xfb, 0x61, 0xd3, - 0x40, 0x27, 0x70, 0x34, 0xea, 0x4f, 0x7e, 0x98, 0xe2, 0xe1, 0x68, 0xd8, 0x9f, 0x0c, 0x5f, 0x35, - 0x4b, 0xce, 0x47, 0x50, 0x4f, 0xa2, 0x42, 0x55, 0x28, 0xf7, 0x27, 0x03, 0x79, 0xe4, 0xd5, 0x90, - 0xbf, 0x19, 0xce, 0x9f, 0x06, 0x9c, 0xa6, 0x8b, 0x40, 0x57, 0x61, 0x40, 0x49, 0x5c, 0x85, 0x59, - 0xb8, 0x0e, 0x92, 0x2a, 0x88, 0x05, 0x42, 0x50, 0x09, 0xc8, 0x1f, 0xba, 0x06, 0xe2, 0x3d, 0x46, - 0xb2, 0x90, 0xb9, 0xbe, 0xd0, 0x9f, 0x23, 0xc5, 0x02, 0x7d, 0x0d, 0x35, 0x95, 0x1c, 0xe5, 0xca, - 0x96, 0xdb, 0x8d, 0xde, 0x59, 0x3a, 0x65, 0xe5, 0x11, 0x27, 0x30, 0xe7, 0x12, 0x5a, 0x97, 0x44, - 0x47, 0x22, 0x15, 0xd1, 0x77, 0x22, 0xf6, 0xeb, 0xde, 0x12, 0x11, 0x4c, 0xec, 0x97, 0xbf, 0x23, - 0x0b, 0xaa, 0xea, 0x42, 0x89, 0x70, 0x0e, 0xb0, 0x5e, 0x3a, 0x0c, 0xac, 0x87, 0x44, 0x2a, 0xaf, - 0x2c, 0xa6, 0x4f, 0xa1, 0x12, 0x5f, 0x67, 0x41, 0xd3, 0xe8, 0xa1, 0x74, 0x9c, 0x57, 0xdc, 0x82, - 0x85, 0x1d, 0x7d, 0x00, 0xf5, 0x18, 0x4f, 0x57, 0xee, 0x8c, 0x88, 0x6c, 0xeb, 0xf8, 0x7e, 0xc3, - 0x79, 0xbd, 0xed, 0x75, 0x10, 0x06, 0x8c, 0x04, 0x6c, 0xbf, 0xf8, 0x47, 0x70, 0x91, 0xc1, 0xa4, - 0x12, 0xe8, 0x42, 0x55, 0x85, 0x26, 0xd8, 0x72, 0x75, 0xd5, 0x28, 0xe7, 0x1f, 0x5e, 0xe2, 0xeb, - 0xd5, 0xdc, 0x65, 0x44, 0x9b, 0xfe, 0x27, 0xa8, 0xe7, 0xbc, 0xec, 0xf1, 0x58, 0x50, 0x5a, 0x9c, - 0x48, 0x6e, 0x39, 0x3b, 0x06, 0xf1, 0x2f, 0x96, 0x76, 0xf4, 0x02, 0xcc, 0x8d, 0xeb, 0x73, 0x1e, - 0x21, 0x44, 0xa2, 0x9a, 0x42, 0x8a, 0x99, 0x82, 0x15, 0x02, 0xb5, 0xa0, 0x3a, 0x8f, 0xee, 0xa6, - 0xd1, 0x3a, 0x10, 0x4d, 0x56, 0xc3, 0x26, 0x5f, 0xe2, 0x75, 0x80, 0x3e, 0x86, 0xa3, 0xb9, 0x47, - 0xdd, 0x1b, 0x9f, 0x4c, 0x97, 0x61, 0xf8, 0x1b, 0x15, 0x7d, 0x56, 0xc3, 0x87, 0x6a, 0xf3, 0x75, - 0xbc, 0xc7, 0x75, 0x3d, 0xdb, 0x09, 0x7f, 0x5f, 0x25, 0xfe, 0x35, 0xe0, 0xec, 0x2a, 0xe0, 0xed, - 0xe5, 0xfb, 0x3b, 0x52, 0x24, 0x69, 0x1b, 0x85, 0xd3, 0x2e, 0xbd, 0x4f, 0xda, 0xe5, 0x54, 0xda, - 0x5a, 0xf8, 0xca, 0x96, 0xf0, 0x45, 0xa4, 0x48, 0x5f, 0x40, 0x73, 0xe7, 0x02, 0xa2, 0x0f, 0x01, - 0x22, 0xb2, 0xa6, 0x64, 0x2a, 0xc8, 0xab, 0xe2, 0x7c, 0x5d, 0xec, 0x8c, 0xf9, 0x86, 0x73, 0x05, - 0xe7, 0xbb, 0xc9, 0xef, 0x2b, 0xe4, 0x12, 0x5a, 0xd7, 0x81, 0x97, 0xa9, 0x64, 0xd6, 0xa5, 0x7a, - 0x90, 0x5b, 0x29, 0x23, 0x37, 0x3e, 0x46, 0x56, 0xeb, 0xe8, 0x1d, 0x51, 0x5a, 0xc9, 0x85, 0xf3, - 0x06, 0xac, 0x87, 0x9e, 0xf6, 0x0d, 0xfb, 0x29, 0x9c, 0xf0, 0xbe, 0xfa, 0x51, 0x76, 0x99, 0x0a, - 0xd8, 0x19, 0x02, 0xda, 0xde, 0xbc, 0xe7, 0x56, 0x5b, 0x69, 0x6e, 0xfd, 0x09, 0xd3, 0x78, 0xdd, - 0xb3, 0xbd, 0xbf, 0x4c, 0x38, 0xd6, 0x13, 0x47, 0x7e, 0x1f, 0x90, 0x07, 0x87, 0xdb, 0xa3, 0x15, - 0x7d, 0x96, 0xff, 0xf9, 0xd8, 0xf9, 0x06, 0xda, 0x2f, 0x8a, 0x40, 0x65, 0xa8, 0xce, 0x93, 0xaf, - 0x0c, 0x44, 0xa1, 0xb9, 0x3b, 0xf1, 0xd0, 0x97, 0xd9, 0x1c, 0x39, 0x23, 0xd6, 0xee, 0x14, 0x85, - 0x6b, 0xb7, 0x68, 0x23, 0xe4, 0x4c, 0x8f, 0x29, 0xf4, 0x28, 0x4d, 0x7a, 0x32, 0xda, 0xdd, 0xc2, - 0xf8, 0xc4, 0xef, 0xaf, 0x70, 0x94, 0x1a, 0x08, 0x28, 0x47, 0xad, 0xac, 0xa1, 0x67, 0x7f, 0x5e, - 0x08, 0x9b, 0xf8, 0xba, 0x85, 0xe3, 0x74, 0xd3, 0xa0, 0x1c, 0x82, 0xcc, 0xb9, 0x62, 0x7f, 0x51, - 0x0c, 0x9c, 0xb8, 0xe3, 0x75, 0xdc, 0xbd, 0xee, 0x79, 0x75, 0xcc, 0x69, 0xc0, 0xbc, 0x3a, 0xe6, - 0x75, 0x11, 0x77, 0xea, 0x02, 0xdc, 0x77, 0x00, 0x7a, 0x9e, 0x5b, 0x90, 0x74, 0xe3, 0xd8, 0xed, - 0xc7, 0x81, 0xda, 0xc5, 0x4b, 0xf8, 0xb9, 0xa6, 0x71, 0x37, 0xa6, 0xf8, 0xfb, 0xf7, 0xcd, 0x7f, - 0x01, 0x00, 0x00, 0xff, 0xff, 0x14, 0xb2, 0x9f, 0x07, 0xcf, 0x0a, 0x00, 0x00, + // 899 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdd, 0x6e, 0xe3, 0x44, + 0x14, 0xae, 0xf3, 0xe3, 0x24, 0xa7, 0x3f, 0x4a, 0x67, 0xd3, 0xd6, 0xb5, 0x00, 0x45, 0x46, 0xb0, + 0x61, 0x81, 0x14, 0xc2, 0x2d, 0x42, 0xea, 0x66, 0xa3, 0xb6, 0xda, 0x92, 0x95, 0x26, 0x14, 0x24, + 0x2e, 0x88, 0xdc, 0x64, 0xb2, 0x35, 0x38, 0x9e, 0x30, 0x33, 0xae, 0xe8, 0x23, 0xf0, 0x0c, 0xdc, + 0xf1, 0x1e, 0x3c, 0x19, 0x37, 0xc8, 0xf3, 0xe3, 0xc6, 0xa9, 0xcd, 0x7a, 0x73, 0x63, 0x7b, 0xe6, + 0x7c, 0xf3, 0x9d, 0x73, 0xbe, 0x99, 0x73, 0xc6, 0xe0, 0xde, 0xf9, 0xab, 0xe0, 0x8c, 0x13, 0x76, + 0x1f, 0xcc, 0x08, 0x3f, 0x13, 0x41, 0x18, 0x12, 0xd6, 0x5f, 0x31, 0x2a, 0x28, 0xea, 0x24, 0xb6, + 0xbe, 0xb1, 0xf5, 0x95, 0xcd, 0x3d, 0x96, 0x2b, 0x66, 0x77, 0x3e, 0x13, 0xea, 0xa9, 0xd0, 0xee, + 0xc9, 0xfa, 0x3c, 0x8d, 0x16, 0xc1, 0x5b, 0x6d, 0x50, 0x2e, 0x18, 0x09, 0x89, 0xcf, 0x89, 0x79, + 0x67, 0x16, 0x19, 0x5b, 0x10, 0x2d, 0xa8, 0x36, 0x9c, 0x66, 0x0c, 0x5c, 0xf8, 0x22, 0xe6, 0x19, + 0xbe, 0x7b, 0xc2, 0x78, 0x40, 0x23, 0xf3, 0x56, 0x36, 0xef, 0xef, 0x0a, 0x3c, 0xbb, 0x0e, 0xb8, + 0xc0, 0x6a, 0x21, 0xc7, 0xe4, 0xf7, 0x98, 0x70, 0x81, 0x3a, 0x50, 0x0f, 0x83, 0x65, 0x20, 0x1c, + 0xab, 0x6b, 0xf5, 0xaa, 0x58, 0x0d, 0xd0, 0x31, 0xd8, 0x74, 0xb1, 0xe0, 0x44, 0x38, 0x95, 0xae, + 0xd5, 0x6b, 0x61, 0x3d, 0x42, 0xdf, 0x41, 0x83, 0x53, 0x26, 0xa6, 0xb7, 0x0f, 0x4e, 0xb5, 0x6b, + 0xf5, 0x0e, 0x06, 0x9f, 0xf4, 0xf3, 0xa4, 0xe8, 0x27, 0x9e, 0x26, 0x94, 0x89, 0x7e, 0xf2, 0x78, + 0xf9, 0x80, 0x6d, 0x2e, 0xdf, 0x09, 0xef, 0x22, 0x08, 0x05, 0x61, 0x4e, 0x4d, 0xf1, 0xaa, 0x11, + 0xba, 0x00, 0x90, 0xbc, 0x94, 0xcd, 0x09, 0x73, 0xea, 0x92, 0xba, 0x57, 0x82, 0xfa, 0x4d, 0x82, + 0xc7, 0x2d, 0x6e, 0x3e, 0xd1, 0xb7, 0xb0, 0xa7, 0x24, 0x99, 0xce, 0xe8, 0x9c, 0x70, 0xc7, 0xee, + 0x56, 0x7b, 0x07, 0x83, 0x53, 0x45, 0x65, 0x14, 0x9e, 0x28, 0xd1, 0x86, 0x74, 0x4e, 0xf0, 0xae, + 0x82, 0x27, 0xdf, 0xdc, 0xfb, 0x05, 0x9a, 0x86, 0xde, 0x1b, 0x80, 0xad, 0x82, 0x47, 0xbb, 0xd0, + 0xb8, 0x19, 0xbf, 0x1e, 0xbf, 0xf9, 0x69, 0xdc, 0xde, 0x41, 0x4d, 0xa8, 0x8d, 0xcf, 0xbf, 0x1f, + 0xb5, 0x2d, 0x74, 0x08, 0xfb, 0xd7, 0xe7, 0x93, 0x1f, 0xa6, 0x78, 0x74, 0x3d, 0x3a, 0x9f, 0x8c, + 0x5e, 0xb5, 0x2b, 0xde, 0x47, 0xd0, 0x4a, 0xa3, 0x42, 0x0d, 0xa8, 0x9e, 0x4f, 0x86, 0x6a, 0xc9, + 0xab, 0xd1, 0x64, 0xd8, 0xb6, 0xbc, 0x3f, 0x2d, 0xe8, 0x64, 0x37, 0x81, 0xaf, 0x68, 0xc4, 0x49, + 0xb2, 0x0b, 0x33, 0x1a, 0x47, 0xe9, 0x2e, 0xc8, 0x01, 0x42, 0x50, 0x8b, 0xc8, 0x1f, 0x66, 0x0f, + 0xe4, 0x77, 0x82, 0x14, 0x54, 0xf8, 0xa1, 0xd4, 0xbf, 0x8a, 0xd5, 0x00, 0x7d, 0x0d, 0x4d, 0x9d, + 0x1c, 0x77, 0x6a, 0xdd, 0x6a, 0x6f, 0x77, 0x70, 0x94, 0x4d, 0x59, 0x7b, 0xc4, 0x29, 0xcc, 0xbb, + 0x80, 0x93, 0x0b, 0x62, 0x22, 0x51, 0x8a, 0x98, 0x33, 0x91, 0xf8, 0xf5, 0x97, 0x44, 0x06, 0x93, + 0xf8, 0xf5, 0x97, 0x04, 0x39, 0xd0, 0xd0, 0x07, 0x4a, 0x86, 0x53, 0xc7, 0x66, 0xe8, 0x09, 0x70, + 0x9e, 0x12, 0xe9, 0xbc, 0xf2, 0x98, 0x3e, 0x85, 0x5a, 0x72, 0x9c, 0x25, 0xcd, 0xee, 0x00, 0x65, + 0xe3, 0xbc, 0x8a, 0x16, 0x14, 0x4b, 0x3b, 0xfa, 0x00, 0x5a, 0x09, 0x9e, 0xaf, 0xfc, 0x19, 0x91, + 0xd9, 0xb6, 0xf0, 0xe3, 0x84, 0x77, 0xb9, 0xee, 0x75, 0x48, 0x23, 0x41, 0x22, 0xb1, 0x5d, 0xfc, + 0xd7, 0x70, 0x9a, 0xc3, 0xa4, 0x13, 0x38, 0x83, 0x86, 0x0e, 0x4d, 0xb2, 0x15, 0xea, 0x6a, 0x50, + 0xde, 0x3f, 0x16, 0x74, 0x6e, 0x56, 0x73, 0x5f, 0x10, 0x63, 0xfa, 0x9f, 0xa0, 0x9e, 0x43, 0x5d, + 0xb6, 0x05, 0xad, 0xc5, 0xa1, 0xe2, 0x56, 0xbd, 0x63, 0x98, 0x3c, 0xb1, 0xb2, 0xa3, 0x17, 0x60, + 0xdf, 0xfb, 0x61, 0x4c, 0xb8, 0x14, 0x22, 0x55, 0x4d, 0x23, 0x65, 0x4f, 0xc1, 0x1a, 0x81, 0x4e, + 0xa0, 0x31, 0x67, 0x0f, 0x53, 0x16, 0x47, 0xb2, 0xc8, 0x9a, 0xd8, 0x9e, 0xb3, 0x07, 0x1c, 0x47, + 0xe8, 0x63, 0xd8, 0x9f, 0x07, 0xdc, 0xbf, 0x0d, 0xc9, 0xf4, 0x8e, 0xd2, 0xdf, 0xb8, 0xac, 0xb3, + 0x26, 0xde, 0xd3, 0x93, 0x97, 0xc9, 0x9c, 0x77, 0x09, 0x47, 0x1b, 0xe1, 0x6f, 0xab, 0xc4, 0xbf, + 0x16, 0x1c, 0x5d, 0x45, 0x5c, 0xf8, 0x61, 0xb8, 0x21, 0x45, 0x9a, 0xb6, 0x55, 0x3a, 0xed, 0xca, + 0xfb, 0xa4, 0x5d, 0xcd, 0xa4, 0x6d, 0x84, 0xaf, 0xad, 0x09, 0x5f, 0x46, 0x8a, 0xec, 0x01, 0xb4, + 0x37, 0x0e, 0x20, 0xfa, 0x10, 0x80, 0x91, 0x98, 0x93, 0xa9, 0x24, 0x6f, 0xc8, 0xf5, 0x2d, 0x39, + 0x33, 0xf6, 0x97, 0xc4, 0xbb, 0x82, 0xe3, 0xcd, 0xe4, 0xb7, 0x15, 0xf2, 0x0e, 0x4e, 0x6e, 0xa2, + 0x20, 0x57, 0xc9, 0xbc, 0x43, 0xf5, 0x24, 0xb7, 0x4a, 0x4e, 0x6e, 0x1d, 0xa8, 0xaf, 0x62, 0xf6, + 0x96, 0x68, 0xad, 0xd4, 0xc0, 0x7b, 0x0d, 0xce, 0x53, 0x4f, 0xdb, 0x86, 0xfd, 0x0c, 0x0e, 0x2f, + 0x88, 0xf8, 0x51, 0x55, 0x99, 0x0e, 0xd8, 0x1b, 0x01, 0x5a, 0x9f, 0x7c, 0xe4, 0xd6, 0x53, 0x59, + 0x6e, 0x73, 0x85, 0x19, 0xbc, 0x41, 0x0d, 0xfe, 0xb2, 0xe1, 0xc0, 0x74, 0x1c, 0x75, 0x3f, 0xa0, + 0x00, 0xf6, 0xd6, 0x5b, 0x2b, 0xfa, 0xac, 0xf8, 0xfa, 0xd8, 0xb8, 0x03, 0xdd, 0x17, 0x65, 0xa0, + 0x2a, 0x54, 0x6f, 0xe7, 0x2b, 0x0b, 0x71, 0x68, 0x6f, 0x76, 0x3c, 0xf4, 0x65, 0x3e, 0x47, 0x41, + 0x8b, 0x75, 0xfb, 0x65, 0xe1, 0xc6, 0x2d, 0xba, 0x97, 0x72, 0x66, 0xdb, 0x14, 0x7a, 0x27, 0x4d, + 0xb6, 0x33, 0xba, 0x67, 0xa5, 0xf1, 0xa9, 0xdf, 0x5f, 0x61, 0x3f, 0xd3, 0x10, 0x50, 0x81, 0x5a, + 0x79, 0x4d, 0xcf, 0xfd, 0xbc, 0x14, 0x36, 0xf5, 0xb5, 0x84, 0x83, 0x6c, 0xd1, 0xa0, 0x02, 0x82, + 0xdc, 0xbe, 0xe2, 0x7e, 0x51, 0x0e, 0x9c, 0xba, 0xe3, 0xd0, 0xde, 0x3c, 0xee, 0x45, 0xfb, 0x58, + 0x50, 0x80, 0x45, 0xfb, 0x58, 0x54, 0x45, 0xde, 0x0e, 0xf2, 0x01, 0x1e, 0x2b, 0x00, 0x3d, 0x2f, + 0xdc, 0x90, 0x6c, 0xe1, 0xb8, 0xbd, 0x77, 0x03, 0x8d, 0x8b, 0x97, 0xf0, 0x73, 0xd3, 0xe0, 0x6e, + 0x6d, 0xf9, 0xfb, 0xf7, 0xcd, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x0d, 0xc6, 0x82, 0x07, 0xcf, + 0x0a, 0x00, 0x00, } diff --git a/pkg/proto/hapi/version/version.pb.go b/pkg/proto/hapi/version/version.pb.go index 7d69bf03d..79771408e 100644 --- a/pkg/proto/hapi/version/version.pb.go +++ b/pkg/proto/hapi/version/version.pb.go @@ -24,7 +24,9 @@ var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. -const _ = proto.ProtoPackageIsVersion1 +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package type Version struct { // Sem ver string for the version @@ -42,15 +44,18 @@ func init() { proto.RegisterType((*Version)(nil), "hapi.version.Version") } +func init() { proto.RegisterFile("hapi/version/version.proto", fileDescriptor0) } + var fileDescriptor0 = []byte{ - // 144 bytes of a gzipped FileDescriptorProto + // 151 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xca, 0x48, 0x2c, 0xc8, 0xd4, 0x2f, 0x4b, 0x2d, 0x2a, 0xce, 0xcc, 0xcf, 0x83, 0xd1, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x3c, 0x20, 0x39, 0x3d, 0xa8, 0x98, 0x52, 0x3a, 0x17, 0x7b, 0x18, 0x84, 0x29, 0x24, 0xce, - 0xc5, 0x5e, 0x9c, 0x9a, 0x1b, 0x0f, 0x94, 0x91, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x62, 0x03, - 0x72, 0x81, 0x92, 0x42, 0xb2, 0x5c, 0x5c, 0xe9, 0x99, 0x25, 0xf1, 0xc9, 0xf9, 0xb9, 0xb9, 0x99, - 0x25, 0x12, 0x4c, 0x60, 0x39, 0x4e, 0xa0, 0x88, 0x33, 0x58, 0x40, 0x48, 0x85, 0x8b, 0x0f, 0x24, - 0x5d, 0x52, 0x94, 0x9a, 0x1a, 0x5f, 0x5c, 0x92, 0x58, 0x92, 0x2a, 0xc1, 0x0c, 0x56, 0xc2, 0x03, - 0x14, 0x0d, 0x01, 0x0a, 0x06, 0x83, 0xc4, 0x9c, 0x38, 0xa3, 0xd8, 0xa1, 0x76, 0x26, 0xb1, 0x81, - 0x1d, 0x62, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x20, 0xcc, 0x0e, 0x1b, 0xa6, 0x00, 0x00, 0x00, + 0xc5, 0x5e, 0x9c, 0x9a, 0x1b, 0x5f, 0x96, 0x5a, 0x24, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x19, 0xc4, + 0x56, 0x9c, 0x9a, 0x1b, 0x96, 0x5a, 0x24, 0x24, 0xcb, 0xc5, 0x95, 0x9e, 0x59, 0x12, 0x9f, 0x9c, + 0x9f, 0x9b, 0x9b, 0x59, 0x22, 0xc1, 0x04, 0x96, 0xe3, 0x4c, 0xcf, 0x2c, 0x71, 0x06, 0x0b, 0x08, + 0xa9, 0x70, 0xf1, 0x81, 0xa4, 0x4b, 0x8a, 0x52, 0x53, 0xe3, 0x8b, 0x4b, 0x12, 0x4b, 0x52, 0x25, + 0x98, 0xc1, 0x4a, 0x78, 0xd2, 0x33, 0x4b, 0x42, 0x8a, 0x52, 0x53, 0x83, 0x41, 0x62, 0x4e, 0x9c, + 0x51, 0xec, 0x50, 0x3b, 0x93, 0xd8, 0xc0, 0x0e, 0x31, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x20, + 0xcc, 0x0e, 0x1b, 0xa6, 0x00, 0x00, 0x00, } From 431cc46cad3ae5248e32df1f6c44f2f4ce5547ba Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 26 Sep 2016 14:33:54 -0600 Subject: [PATCH 107/183] feat(tiller): add toYaml template function This adds the function toYaml to the Engine template functions. Closes #1225 --- pkg/engine/engine.go | 14 ++++++++++++++ pkg/engine/engine_test.go | 13 +++++++++++++ 2 files changed, 27 insertions(+) diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index e415913a3..93927bcb5 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -24,6 +24,8 @@ import ( "text/template" "github.com/Masterminds/sprig" + "github.com/ghodss/yaml" + "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/chart" ) @@ -49,11 +51,23 @@ func New() *Engine { f := sprig.TxtFuncMap() delete(f, "env") delete(f, "expandenv") + + // Add a function to convert to YAML: + f["toYaml"] = toYaml return &Engine{ FuncMap: f, } } +func toYaml(v interface{}) string { + data, err := yaml.Marshal(v) + if err != nil { + // Swallow errors inside of a template. + return "" + } + return string(data) +} + // Render takes a chart, optional values, and value overrides, and attempts to render the Go templates. // // Render can be called repeatedly on the same engine. diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index ef71bbd0c..d9e595412 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -27,6 +27,19 @@ import ( "github.com/golang/protobuf/ptypes/any" ) +func TestToYaml(t *testing.T) { + expect := "foo: bar\n" + v := struct { + Foo string `json:"foo"` + }{ + Foo: "bar", + } + + if got := toYaml(v); got != expect { + t.Errorf("Expected %q, got %q", expect, got) + } +} + func TestEngine(t *testing.T) { e := New() From 36e6094c6293d73ae84acf1544614a97cc62eef4 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 26 Sep 2016 17:25:56 -0600 Subject: [PATCH 108/183] feat(helm): add 'helm upgrade --install' support This makes it possible to run an upgrade that will install a release if it doesn't already exist. Closes #1042 --- cmd/helm/upgrade.go | 82 ++++++++++++++++++++++++++++------------ cmd/helm/upgrade_test.go | 7 ++++ 2 files changed, 64 insertions(+), 25 deletions(-) diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index ca60c9985..b91efbbc6 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -21,10 +21,12 @@ import ( "fmt" "io" "io/ioutil" + "strings" "github.com/spf13/cobra" "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/storage/driver" ) const upgradeDesc = ` @@ -48,6 +50,8 @@ type upgradeCmd struct { values *values verify bool keyring string + install bool + namespace string } func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { @@ -83,41 +87,45 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { f.BoolVar(&upgrade.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks") f.BoolVar(&upgrade.verify, "verify", false, "verify the provenance of the chart before upgrading") f.StringVar(&upgrade.keyring, "keyring", defaultKeyring(), "the path to the keyring that contains public singing keys") + f.BoolVarP(&upgrade.install, "install", "i", false, "if a release by this name doesn't already exist, run an install") + f.StringVar(&upgrade.namespace, "namespace", "default", "the namespace to install the release into (only used if --install is set)") return cmd } -func (u *upgradeCmd) vals() ([]byte, error) { - var buffer bytes.Buffer - - // User specified a values file via -f/--values - if u.valuesFile != "" { - bytes, err := ioutil.ReadFile(u.valuesFile) - if err != nil { - return []byte{}, err - } - buffer.Write(bytes) - } - - // User specified value pairs via --set - // These override any values in the specified file - if len(u.values.pairs) > 0 { - bytes, err := u.values.yaml() - if err != nil { - return []byte{}, err - } - buffer.Write(bytes) - } - - return buffer.Bytes(), nil -} - func (u *upgradeCmd) run() error { chartPath, err := locateChartPath(u.chart, u.verify, u.keyring) if err != nil { return err } + if u.install { + // If a release does not exist, install it. If another error occurs during + // the check, ignore the error and continue with the upgrade. + // + // The returned error is a grpc.rpcError that wraps the message from the original error. + // So we're stuck doing string matching against the wrapped error, which is nested somewhere + // inside of the grpc.rpcError message. + _, err := u.client.ReleaseContent(u.release, helm.ContentReleaseVersion(1)) + if err != nil && strings.Contains(err.Error(), driver.ErrReleaseNotFound.Error()) { + fmt.Fprintf(u.out, "Release %q does not exist. Installing it now.\n", u.release) + ic := &installCmd{ + chartPath: chartPath, + client: u.client, + out: u.out, + name: u.release, + valuesFile: u.valuesFile, + dryRun: u.dryRun, + verify: u.verify, + disableHooks: u.disableHooks, + keyring: u.keyring, + values: u.values, + namespace: u.namespace, + } + return ic.run() + } + } + rawVals, err := u.vals() if err != nil { return err @@ -139,5 +147,29 @@ func (u *upgradeCmd) run() error { PrintStatus(u.out, status) return nil +} + +func (u *upgradeCmd) vals() ([]byte, error) { + var buffer bytes.Buffer + + // User specified a values file via -f/--values + if u.valuesFile != "" { + bytes, err := ioutil.ReadFile(u.valuesFile) + if err != nil { + return []byte{}, err + } + buffer.Write(bytes) + } + // User specified value pairs via --set + // These override any values in the specified file + if len(u.values.pairs) > 0 { + bytes, err := u.values.yaml() + if err != nil { + return []byte{}, err + } + buffer.Write(bytes) + } + + return buffer.Bytes(), nil } diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go index f51049e28..7eab8acde 100644 --- a/cmd/helm/upgrade_test.go +++ b/cmd/helm/upgrade_test.go @@ -69,6 +69,13 @@ func TestUpgradeCmd(t *testing.T) { resp: releaseMock(&releaseOptions{name: "funny-bunny", version: 2, chart: ch}), expected: "funny-bunny has been upgraded. Happy Helming!\n", }, + { + name: "install a release with 'upgrade --install'", + args: []string{"zany-bunny", chartPath}, + flags: []string{"-i"}, + resp: releaseMock(&releaseOptions{name: "zany-bunny", version: 1, chart: ch}), + expected: "zany-bunny has been upgraded. Happy Helming!\n", + }, } cmd := func(c *fakeReleaseClient, out io.Writer) *cobra.Command { From 85b70c48819c9f0e2fc605b2d184b7f2bd495230 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 26 Sep 2016 17:48:21 -0600 Subject: [PATCH 109/183] fix(helm): if server is old, report this in a friendly way If 'helm version' is run on an old server, it will emit an error telling you the server is too old. Closes #1223 --- cmd/helm/version.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/helm/version.go b/cmd/helm/version.go index e22d0a800..1242a4667 100644 --- a/cmd/helm/version.go +++ b/cmd/helm/version.go @@ -17,10 +17,13 @@ limitations under the License. package main import ( + "errors" "fmt" "io" "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/version" @@ -56,6 +59,9 @@ func (v *versionCmd) run() error { resp, err := v.client.GetVersion() if err != nil { + if grpc.Code(err) == codes.Unimplemented { + return errors.New("server is too old to know its version") + } return err } fmt.Fprintf(v.out, "Server: %#v\n", resp.Version) From 90d3a952e9c8e772615404c02af269f33d314021 Mon Sep 17 00:00:00 2001 From: Adnan Abdulhussein Date: Tue, 27 Sep 2016 12:38:39 +0200 Subject: [PATCH 110/183] chore(repo): Rename default charts repo to 'stable' --- cmd/helm/init.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/helm/init.go b/cmd/helm/init.go index accf65f61..d8f807691 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -34,7 +34,7 @@ Kubernetes Cluster and sets up local configuration in $HELM_HOME (default: ~/.he ` var ( - defaultRepository = "kubernetes-charts" + defaultRepository = "stable" defaultRepositoryURL = "http://storage.googleapis.com/kubernetes-charts" ) From 446d5551787646c03b27fabdeb504dde36f97af6 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Fri, 23 Sep 2016 21:42:07 -0600 Subject: [PATCH 111/183] feat(helm): update helm search Switch 'helm search' from file crawling to using the indices. Also add scorable indexing, forward porting the search code I originally wrote for Helm Classic. Closes #1226 Partially addresses #1199 --- cmd/helm/helm.go | 1 + cmd/helm/search.go | 125 +++++----- cmd/helm/search/search.go | 183 ++++++++++++++ cmd/helm/search/search_test.go | 232 ++++++++++++++++++ cmd/helm/search_test.go | 121 +++++---- .../repository/cache/testing-index.yaml | 54 ++++ .../helmhome/repository/local/index.yaml | 0 .../helmhome/repository/repositories.yaml | 1 + 8 files changed, 593 insertions(+), 124 deletions(-) create mode 100644 cmd/helm/search/search.go create mode 100644 cmd/helm/search/search_test.go create mode 100644 cmd/helm/testdata/helmhome/repository/cache/testing-index.yaml create mode 100644 cmd/helm/testdata/helmhome/repository/local/index.yaml create mode 100644 cmd/helm/testdata/helmhome/repository/repositories.yaml diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 11e0f88d6..4c076b068 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -101,6 +101,7 @@ func newRootCmd(out io.Writer) *cobra.Command { newVersionCmd(nil, out), newRepoCmd(out), newDependencyCmd(out), + newSearchCmd(out), ) return cmd } diff --git a/cmd/helm/search.go b/cmd/helm/search.go index e1a465c99..625b980bd 100644 --- a/cmd/helm/search.go +++ b/cmd/helm/search.go @@ -17,90 +17,99 @@ limitations under the License. package main import ( - "errors" "fmt" - "os" - "path/filepath" + "io" "strings" "github.com/spf13/cobra" + "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/cmd/helm/search" "k8s.io/helm/pkg/repo" ) -func init() { - RootCommand.AddCommand(searchCmd) +const searchDesc = ` +Search reads through all of the repositories configured on the system, and +looks for matches. + +Repositories are managed with 'helm repo' commands. +` + +// searchMaxScore suggests that any score higher than this is not considered a match. +const searchMaxScore = 25 + +type searchCmd struct { + out io.Writer + helmhome helmpath.Home + + regexp bool } -var searchCmd = &cobra.Command{ - Use: "search [keyword]", - Short: "search for a keyword in charts", - Long: "Searches the known repositories cache files for the specified search string, looks at name and keywords", - RunE: search, - PreRunE: requireInit, +func newSearchCmd(out io.Writer) *cobra.Command { + sc := &searchCmd{out: out, helmhome: helmpath.Home(homePath())} + + cmd := &cobra.Command{ + Use: "search [keyword]", + Short: "search for a keyword in charts", + Long: searchDesc, + RunE: func(cmd *cobra.Command, args []string) error { + return sc.run(args) + }, + PreRunE: requireInit, + } + + cmd.Flags().BoolVarP(&sc.regexp, "regexp", "r", false, "use regular expressions for searching") + + return cmd } -func search(cmd *cobra.Command, args []string) error { +func (s *searchCmd) run(args []string) error { + index, err := s.buildIndex() + if err != nil { + return err + } + if len(args) == 0 { - return errors.New("This command needs at least one argument (search string)") + s.showAllCharts(index) } - // TODO: This needs to be refactored to use loadChartRepositories - results, err := searchCacheForPattern(cacheDirectory(), args[0]) + q := strings.Join(args, " ") + res, err := index.Search(q, searchMaxScore, s.regexp) if err != nil { - return err + return nil } - if len(results) > 0 { - for _, result := range results { - fmt.Println(result) - } + search.SortScore(res) + + for _, r := range res { + fmt.Fprintln(s.out, r.Name) } + return nil } -func searchChartRefsForPattern(search string, chartRefs map[string]*repo.ChartRef) []string { - matches := []string{} - for k, c := range chartRefs { - if strings.Contains(c.Name, search) && !c.Removed { - matches = append(matches, k) - continue - } - if c.Chartfile == nil { - continue - } - for _, keyword := range c.Chartfile.Keywords { - if strings.Contains(keyword, search) { - matches = append(matches, k) - } - } +func (s *searchCmd) showAllCharts(i *search.Index) { + for name := range i.Entries() { + fmt.Fprintln(s.out, name) } - return matches } -func searchCacheForPattern(dir string, search string) ([]string, error) { - fileList := []string{} - filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { - if !f.IsDir() { - fileList = append(fileList, path) - } - return nil - }) - matches := []string{} - for _, f := range fileList { - index, err := repo.LoadIndexFile(f) +func (s *searchCmd) buildIndex() (*search.Index, error) { + // Load the repositories.yaml + rf, err := repo.LoadRepositoriesFile(s.helmhome.RepositoryFile()) + if err != nil { + return nil, err + } + + i := search.NewIndex() + for n := range rf.Repositories { + f := s.helmhome.CacheIndex(n) + ind, err := repo.LoadIndexFile(f) if err != nil { - return matches, fmt.Errorf("index %s corrupted: %s", f, err) + fmt.Fprintf(s.out, "WARNING: Repo %q is corrupt. Try 'helm update': %s", f, err) + continue } - m := searchChartRefsForPattern(search, index.Entries) - repoName := strings.TrimSuffix(filepath.Base(f), "-index.yaml") - for _, c := range m { - // TODO: Is it possible for this file to be missing? Or to have - // an extension other than .tgz? Should the actual filename be in - // the YAML? - fname := filepath.Join(repoName, c+".tgz") - matches = append(matches, fname) - } + i.AddRepo(n, ind) } - return matches, nil + return i, nil } diff --git a/cmd/helm/search/search.go b/cmd/helm/search/search.go new file mode 100644 index 000000000..0dfb72add --- /dev/null +++ b/cmd/helm/search/search.go @@ -0,0 +1,183 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/*Package search provides client-side repository searching. + +This supports building an in-memory search index based on the contents of +multiple repositories, and then using string matching or regular expressions +to find matches. +*/ +package search + +import ( + "errors" + "path/filepath" + "regexp" + "sort" + "strings" + + "k8s.io/helm/pkg/repo" +) + +// Result is a search result. +// +// Score indicates how close it is to match. The higher the score, the longer +// the distance. +type Result struct { + Name string + Score int +} + +// Index is a searchable index of chart information. +type Index struct { + lines map[string]string + charts map[string]*repo.ChartRef +} + +const sep = "\v" + +// NewIndex creats a new Index. +func NewIndex() *Index { + return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartRef{}} +} + +// AddRepo adds a repository index to the search index. +func (i *Index) AddRepo(rname string, ind *repo.IndexFile) { + for name, ref := range ind.Entries { + fname := filepath.Join(rname, name) + i.lines[fname] = indstr(rname, ref) + i.charts[fname] = ref + } +} + +// Entries returns the entries in an index. +func (i *Index) Entries() map[string]*repo.ChartRef { + return i.charts +} + +// Search searches an index for the given term. +// +// Threshold indicates the maximum score a term may have before being marked +// irrelevant. (Low score means higher relevance. Golf, not bowling.) +// +// If regexp is true, the term is treated as a regular expression. Otherwise, +// term is treated as a literal string. +func (i *Index) Search(term string, threshold int, regexp bool) ([]*Result, error) { + if regexp == true { + return i.SearchRegexp(term, threshold) + } + return i.SearchLiteral(term, threshold), nil +} + +// calcScore calculates a score for a match. +func (i *Index) calcScore(index int, matchline string) int { + + // This is currently tied to the fact that sep is a single char. + splits := []int{} + s := rune(sep[0]) + for i, ch := range matchline { + if ch == s { + splits = append(splits, i) + } + } + + for i, pos := range splits { + if index > pos { + continue + } + return i + } + return len(splits) +} + +// SearchLiteral does a literal string search (no regexp). +func (i *Index) SearchLiteral(term string, threshold int) []*Result { + term = strings.ToLower(term) + buf := []*Result{} + for k, v := range i.lines { + res := strings.Index(v, term) + if score := i.calcScore(res, v); res != -1 && score < threshold { + buf = append(buf, &Result{Name: k, Score: score}) + } + } + return buf +} + +// SearchRegexp searches using a regular expression. +func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) { + matcher, err := regexp.Compile(re) + if err != nil { + return []*Result{}, err + } + buf := []*Result{} + for k, v := range i.lines { + ind := matcher.FindStringIndex(v) + if len(ind) == 0 { + continue + } + if score := i.calcScore(ind[0], v); ind[0] >= 0 && score < threshold { + buf = append(buf, &Result{Name: k, Score: score}) + } + } + return buf, nil +} + +// Chart returns the ChartRef for a particular name. +func (i *Index) Chart(name string) (*repo.ChartRef, error) { + c, ok := i.charts[name] + if !ok { + return nil, errors.New("no such chart") + } + return c, nil +} + +// SortScore does an in-place sort of the results. +// +// Lowest scores are highest on the list. Matching scores are subsorted alphabetically. +func SortScore(r []*Result) { + sort.Sort(scoreSorter(r)) +} + +// scoreSorter sorts results by score, and subsorts by alpha Name. +type scoreSorter []*Result + +// Len returns the length of this scoreSorter. +func (s scoreSorter) Len() int { return len(s) } + +// Swap performs an in-place swap. +func (s scoreSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Less compares a to b, and returns true if a is less than b. +func (s scoreSorter) Less(a, b int) bool { + first := s[a] + second := s[b] + + if first.Score > second.Score { + return false + } + if first.Score < second.Score { + return true + } + return first.Name < second.Name +} + +func indstr(name string, ref *repo.ChartRef) string { + i := ref.Name + sep + name + "/" + ref.Name + sep + if ref.Chartfile != nil { + i += ref.Chartfile.Description + sep + strings.Join(ref.Chartfile.Keywords, sep) + } + return strings.ToLower(i) +} diff --git a/cmd/helm/search/search_test.go b/cmd/helm/search/search_test.go new file mode 100644 index 000000000..e20b8e756 --- /dev/null +++ b/cmd/helm/search/search_test.go @@ -0,0 +1,232 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package search + +import ( + "strings" + "testing" + + "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/repo" +) + +func TestSortScore(t *testing.T) { + in := []*Result{ + {Name: "bbb", Score: 0}, + {Name: "aaa", Score: 5}, + {Name: "abb", Score: 5}, + {Name: "aab", Score: 0}, + {Name: "bab", Score: 5}, + } + expect := []string{"aab", "bbb", "aaa", "abb", "bab"} + expectScore := []int{0, 0, 5, 5, 5} + SortScore(in) + + // Test Score + for i := 0; i < len(expectScore); i++ { + if expectScore[i] != in[i].Score { + t.Errorf("Sort error on index %d: expected %d, got %d", i, expectScore[i], in[i].Score) + } + } + // Test Name + for i := 0; i < len(expect); i++ { + if expect[i] != in[i].Name { + t.Errorf("Sort error: expected %s, got %s", expect[i], in[i].Name) + } + } +} + +var testCacheDir = "../testdata/" + +var indexfileEntries = map[string]*repo.ChartRef{ + "niña-0.1.0": { + Name: "niña", + URL: "http://example.com/charts/nina-0.1.0.tgz", + Chartfile: &chart.Metadata{ + Name: "niña", + Version: "0.1.0", + Description: "One boat", + }, + }, + "pinta-0.1.0": { + Name: "pinta", + URL: "http://example.com/charts/pinta-0.1.0.tgz", + Chartfile: &chart.Metadata{ + Name: "pinta", + Version: "0.1.0", + Description: "Two ship", + }, + }, + "santa-maria-1.2.3": { + Name: "santa-maria", + URL: "http://example.com/charts/santa-maria-1.2.3.tgz", + Chartfile: &chart.Metadata{ + Name: "santa-maria", + Version: "1.2.3", + Description: "Three boat", + }, + }, +} + +func loadTestIndex(t *testing.T) *Index { + i := NewIndex() + i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}) + i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]*repo.ChartRef{ + "pinta-2.0.0": { + Name: "pinta", + URL: "http://example.com/charts/pinta-2.0.0.tgz", + Chartfile: &chart.Metadata{ + Name: "pinta", + Version: "2.0.0", + Description: "Two ship, version two", + }, + }, + }}) + return i +} + +func TestSearchByName(t *testing.T) { + + tests := []struct { + name string + query string + expect []*Result + regexp bool + fail bool + failMsg string + }{ + { + name: "basic search for one result", + query: "santa-maria", + expect: []*Result{ + {Name: "testing/santa-maria-1.2.3"}, + }, + }, + { + name: "basic search for two results", + query: "pinta", + expect: []*Result{ + {Name: "testing/pinta-0.1.0"}, + {Name: "ztesting/pinta-2.0.0"}, + }, + }, + { + name: "repo-specific search for one result", + query: "ztesting/pinta", + expect: []*Result{ + {Name: "ztesting/pinta-2.0.0"}, + }, + }, + { + name: "partial name search", + query: "santa", + expect: []*Result{ + {Name: "testing/santa-maria-1.2.3"}, + }, + }, + { + name: "description search, one result", + query: "Three", + expect: []*Result{ + {Name: "testing/santa-maria-1.2.3"}, + }, + }, + { + name: "description search, two results", + query: "two", + expect: []*Result{ + {Name: "testing/pinta-0.1.0"}, + {Name: "ztesting/pinta-2.0.0"}, + }, + }, + { + name: "nothing found", + query: "mayflower", + expect: []*Result{}, + }, + { + name: "regexp, one result", + query: "th[ref]*", + expect: []*Result{ + {Name: "testing/santa-maria-1.2.3"}, + }, + regexp: true, + }, + { + name: "regexp, fail compile", + query: "th[", + expect: []*Result{}, + regexp: true, + fail: true, + failMsg: "error parsing regexp:", + }, + } + + i := loadTestIndex(t) + + for _, tt := range tests { + + charts, err := i.Search(tt.query, 100, tt.regexp) + if err != nil { + if tt.fail { + if !strings.Contains(err.Error(), tt.failMsg) { + t.Fatalf("%s: Unexpected error message: %s", tt.name, err) + } + continue + } + t.Fatalf("%s: %s", tt.name, err) + } + // Give us predictably ordered results. + SortScore(charts) + + l := len(charts) + if l != len(tt.expect) { + t.Fatalf("%s: Expected %d result, got %d", tt.name, len(tt.expect), l) + } + // For empty result sets, just keep going. + if l == 0 { + continue + } + + for i, got := range charts { + ex := tt.expect[i] + if got.Name != ex.Name { + t.Errorf("%s[%d]: Expected name %q, got %q", tt.name, i, ex.Name, got.Name) + } + } + + } +} + +func TestCalcScore(t *testing.T) { + i := NewIndex() + + fields := []string{"aaa", "bbb", "ccc", "ddd"} + matchline := strings.Join(fields, sep) + if r := i.calcScore(2, matchline); r != 0 { + t.Errorf("Expected 0, got %d", r) + } + if r := i.calcScore(5, matchline); r != 1 { + t.Errorf("Expected 1, got %d", r) + } + if r := i.calcScore(10, matchline); r != 2 { + t.Errorf("Expected 2, got %d", r) + } + if r := i.calcScore(14, matchline); r != 3 { + t.Errorf("Expected 3, got %d", r) + } +} diff --git a/cmd/helm/search_test.go b/cmd/helm/search_test.go index 0869551aa..ffd4493fe 100644 --- a/cmd/helm/search_test.go +++ b/cmd/helm/search_test.go @@ -17,79 +17,68 @@ limitations under the License. package main import ( + "bytes" + "strings" "testing" - - "k8s.io/helm/pkg/repo" ) -const testDir = "testdata/testcache" -const testFile = "testdata/testcache/local-index.yaml" - -type searchTestCase struct { - in string - expectedOut []string -} - -var searchTestCases = []searchTestCase{ - {"foo", []string{}}, - {"alpine", []string{"alpine-1.0.0"}}, - {"sumtin", []string{"alpine-1.0.0"}}, - {"web", []string{"nginx-0.1.0"}}, -} +func TestSearchCmd(t *testing.T) { + tests := []struct { + name string + args []string + flags []string + expect string + regexp bool + fail bool + }{ + { + name: "search for 'maria', expect one match", + args: []string{"maria"}, + expect: "testing/mariadb-0.3.0", + }, + { + name: "search for 'alpine', expect two matches", + args: []string{"alpine"}, + expect: "testing/alpine-0.1.0\ntesting/alpine-0.2.0", + }, + { + name: "search for 'syzygy', expect no matches", + args: []string{"syzygy"}, + expect: "", + }, + { + name: "search for 'alp[a-z]+', expect two matches", + args: []string{"alp[a-z]+"}, + flags: []string{"--regexp"}, + expect: "testing/alpine-0.1.0\ntesting/alpine-0.2.0", + regexp: true, + }, + { + name: "search for 'alp[', expect failure to compile regexp", + args: []string{"alp["}, + flags: []string{"--regexp"}, + regexp: true, + fail: true, + }, + } -var searchCacheTestCases = []searchTestCase{ - {"notthere", []string{}}, - {"odd", []string{"foobar/oddness-1.2.3.tgz"}}, - {"sumtin", []string{"local/alpine-1.0.0.tgz", "foobar/oddness-1.2.3.tgz"}}, - {"foobar", []string{"foobar/foobar-0.1.0.tgz"}}, - {"web", []string{"local/nginx-0.1.0.tgz"}}, -} + oldhome := helmHome + helmHome = "testdata/helmhome" + defer func() { helmHome = oldhome }() -func validateEntries(t *testing.T, in string, found []string, expected []string) { - if len(found) != len(expected) { - t.Errorf("Failed to search %s: Expected: %#v got: %#v", in, expected, found) - } - foundCount := 0 - for _, exp := range expected { - for _, f := range found { - if exp == f { - foundCount = foundCount + 1 + for _, tt := range tests { + buf := bytes.NewBuffer(nil) + cmd := newSearchCmd(buf) + cmd.ParseFlags(tt.flags) + if err := cmd.RunE(cmd, tt.args); err != nil { + if tt.fail { continue } + t.Fatalf("%s: unexpected error %s", tt.name, err) + } + got := strings.TrimSpace(buf.String()) + if got != tt.expect { + t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got) } - } - if foundCount != len(expected) { - t.Errorf("Failed to find expected items for %s: Expected: %#v got: %#v", in, expected, found) - } - -} - -func searchTestRunner(t *testing.T, tc searchTestCase) { - cf, err := repo.LoadIndexFile(testFile) - if err != nil { - t.Errorf("Failed to load index file : %s : %s", testFile, err) - } - - u := searchChartRefsForPattern(tc.in, cf.Entries) - validateEntries(t, tc.in, u, tc.expectedOut) -} - -func searchCacheTestRunner(t *testing.T, tc searchTestCase) { - u, err := searchCacheForPattern(testDir, tc.in) - if err != nil { - t.Errorf("searchCacheForPattern failed: %#v", err) - } - validateEntries(t, tc.in, u, tc.expectedOut) -} - -func TestSearches(t *testing.T) { - for _, tc := range searchTestCases { - searchTestRunner(t, tc) - } -} - -func TestCacheSearches(t *testing.T) { - for _, tc := range searchCacheTestCases { - searchCacheTestRunner(t, tc) } } diff --git a/cmd/helm/testdata/helmhome/repository/cache/testing-index.yaml b/cmd/helm/testdata/helmhome/repository/cache/testing-index.yaml new file mode 100644 index 000000000..67595882a --- /dev/null +++ b/cmd/helm/testdata/helmhome/repository/cache/testing-index.yaml @@ -0,0 +1,54 @@ +alpine-0.1.0: + name: alpine + url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz + created: 2016-09-06 21:58:44.211261566 +0000 UTC + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + chartfile: + name: alpine + home: https://k8s.io/helm + sources: + - https://github.com/kubernetes/helm + version: 0.1.0 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + engine: "" + icon: "" +alpine-0.2.0: + name: alpine + url: http://storage.googleapis.com/kubernetes-charts/alpine-0.2.0.tgz + created: 2016-09-06 21:58:44.211261566 +0000 UTC + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + chartfile: + name: alpine + home: https://k8s.io/helm + sources: + - https://github.com/kubernetes/helm + version: 0.2.0 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + engine: "" + icon: "" +mariadb-0.3.0: + name: mariadb + url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz + created: 2016-09-06 21:58:44.211870222 +0000 UTC + checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56 + chartfile: + name: mariadb + home: https://mariadb.org + sources: + - https://github.com/bitnami/bitnami-docker-mariadb + version: 0.3.0 + description: Chart for MariaDB + keywords: + - mariadb + - mysql + - database + - sql + maintainers: + - name: Bitnami + email: containers@bitnami.com + engine: gotpl + icon: "" diff --git a/cmd/helm/testdata/helmhome/repository/local/index.yaml b/cmd/helm/testdata/helmhome/repository/local/index.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/helm/testdata/helmhome/repository/repositories.yaml b/cmd/helm/testdata/helmhome/repository/repositories.yaml new file mode 100644 index 000000000..cd16e634a --- /dev/null +++ b/cmd/helm/testdata/helmhome/repository/repositories.yaml @@ -0,0 +1 @@ +testing: http://example.com/charts From e4c217768e9f000539b0d06287d7e061b3e81594 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 27 Sep 2016 14:15:23 -0600 Subject: [PATCH 112/183] ref(*): update to Kubernetes 1.4 This starts with #1211 and does the other updates necessary for bumping us to Kubernetes 1.4. --- Makefile | 2 +- glide.lock | 247 ++++++++++++++++++++++++++++++++-------- glide.yaml | 2 +- pkg/kube/client.go | 2 +- pkg/kube/client_test.go | 2 - pkg/kube/tunnel.go | 17 +-- 6 files changed, 213 insertions(+), 59 deletions(-) diff --git a/Makefile b/Makefile index 7de4f832b..705d2dbe8 100644 --- a/Makefile +++ b/Makefile @@ -100,7 +100,7 @@ endif ifndef HAS_GIT $(error You must install Git) endif - glide install + glide install --strip-vendor go build -o bin/protoc-gen-go ./vendor/github.com/golang/protobuf/protoc-gen-go include versioning.mk diff --git a/glide.lock b/glide.lock index e30e2d481..99a057ebd 100644 --- a/glide.lock +++ b/glide.lock @@ -1,16 +1,77 @@ -hash: 45eadf6012afd2293f9e82032dba0d4bcff712bf56f8b6cb875c4bdf02d1b08c -updated: 2016-09-26T12:02:34.264796591-07:00 +hash: a017cec6c2fa7ab82b7a2747d016f2254f6d26c2e24ae48f2d34bb51d85cc0d1 +updated: 2016-09-27T14:51:17.303868688-06:00 imports: +- name: bitbucket.org/ww/goautoneg + version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 - name: github.com/aokoli/goutils version: 9c37978a95bd5c709a15883b6242714ea6709e64 - name: github.com/asaskevich/govalidator version: 7664702784775e51966f0885f5cd27435916517b +- name: github.com/Azure/go-ansiterm + version: 70b2c90b260171e829f1ebd7c17f600c11858dbe + subpackages: + - winterm - name: github.com/beorn7/perks version: 3ac7bf7a47d159a033b107610db8a1b6575507a4 subpackages: - quantile - name: github.com/blang/semver version: 31b736133b98f26d5e078ec9eb591666edfd091f +- name: github.com/coreos/etcd + version: 9efa00d1030d4bf62eb8e5ec130023aeb1b8e2d0 + subpackages: + - alarm + - auth + - auth/authpb + - client + - clientv3 + - compactor + - discovery + - error + - etcdserver + - etcdserver/api + - etcdserver/api/v2http + - etcdserver/api/v2http/httptypes + - etcdserver/api/v3rpc + - etcdserver/api/v3rpc/rpctypes + - etcdserver/auth + - etcdserver/etcdserverpb + - etcdserver/membership + - etcdserver/stats + - integration + - lease + - lease/leasehttp + - lease/leasepb + - mvcc + - mvcc/backend + - mvcc/mvccpb + - pkg/adt + - pkg/contention + - pkg/crc + - pkg/fileutil + - pkg/httputil + - pkg/idutil + - pkg/ioutil + - pkg/logutil + - pkg/netutil + - pkg/pathutil + - pkg/pbutil + - pkg/runtime + - pkg/schedule + - pkg/testutil + - pkg/tlsutil + - pkg/transport + - pkg/types + - pkg/wait + - raft + - raft/raftpb + - rafthttp + - snap + - snap/snappb + - store + - version + - wal + - wal/walpb - name: github.com/coreos/go-oidc version: 5cf2aa52da8c574d3aa4458f471ad6ae2240fe6b subpackages: @@ -29,7 +90,7 @@ imports: - unit - util - name: github.com/coreos/pkg - version: 7f080b6c11ac2d2347c3cd7521e810207ea1a041 + version: fa29b1d70f0beaddd4c7021607cc3c3be8ce94b8 subpackages: - capnslog - dlopen @@ -46,16 +107,17 @@ imports: - digest - reference - name: github.com/docker/docker - version: 0f5c9d301b9b1cca66b3ea0f9dec3b5317d3686d + version: b9f10c951893f9a00865890a5232e85d770c1087 subpackages: + - pkg/jsonlog - pkg/jsonmessage + - pkg/longpath - pkg/mount - pkg/stdcopy - pkg/symlink + - pkg/system - pkg/term - - pkg/term/winconsole - - pkg/timeutils - - pkg/units + - pkg/term/windows - name: github.com/docker/engine-api version: dea108d3aa0c67d7162a3fd8aa65f38a430019fd subpackages: @@ -85,7 +147,7 @@ imports: subpackages: - spdy - name: github.com/emicklei/go-restful - version: 7c47e2558a0bbbaba9ecab06bc6681e73028a28a + version: 89ef8af493ab468a45a42bb0d89a06fccdd2fb22 subpackages: - log - swagger @@ -94,9 +156,10 @@ imports: - name: github.com/ghodss/yaml version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee - name: github.com/gogo/protobuf - version: 82d16f734d6d871204a3feb1a73cb220cc92574c + version: e18d7aa8f8c624c915db340349aad4c49b10d173 subpackages: - gogoproto + - plugin/compare - plugin/defaultcheck - plugin/description - plugin/embedcheck @@ -104,7 +167,6 @@ imports: - plugin/equal - plugin/face - plugin/gostring - - plugin/grpc - plugin/marshalto - plugin/oneofcheck - plugin/populate @@ -116,6 +178,7 @@ imports: - proto - protoc-gen-gogo/descriptor - protoc-gen-gogo/generator + - protoc-gen-gogo/grpc - protoc-gen-gogo/plugin - sortkeys - vanity @@ -123,20 +186,22 @@ imports: - name: github.com/golang/glog version: 44145f04b68cf362d9c4df2182967c2275eaefed - name: github.com/golang/groupcache - version: 604ed5785183e59ae2789449d89e73f3a2a77987 + version: 02826c3e79038b59d737d3b1c0a1d937f71a4433 subpackages: - lru - name: github.com/golang/protobuf version: 8616e8ee5e20a1704615e6c8d7afcdac06087a67 subpackages: + - jsonpb - proto - ptypes/any - ptypes/timestamp - name: github.com/google/cadvisor - version: 404f31ff971a5d5d9595f1a7ca6a07881ac37fed + version: 0cdf4912793fac9990de3790c273342ec31817fb subpackages: - api - cache/memory + - client/v2 - collector - container - container/common @@ -182,12 +247,18 @@ imports: subpackages: - util/strutil - util/wordwrap +- name: github.com/grpc-ecosystem/grpc-gateway + version: f52d055dc48aec25854ed7d31862f78913cf17d1 + subpackages: + - runtime + - runtime/internal + - utilities - name: github.com/imdario/mergo version: 6633656539c1639d9d78127b7d47c622b5d7b6dc - name: github.com/inconshreveable/mousetrap version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 - name: github.com/jonboulle/clockwork - version: 3f831b65b61282ba6bece21b91beea2edc4c887a + version: 72f9bd7c4e0c2a40055ab3d0f09654f730cce982 - name: github.com/juju/ratelimit version: 77ed1c8a01217656d2080ad51981f6e99adaa177 - name: github.com/Masterminds/semver @@ -200,28 +271,10 @@ imports: version: fc2b8d3a73c4867e51861bbdd5ae3c1f0869dd6a subpackages: - pbutil -- name: github.com/opencontainers/runc - version: 7ca2aa4873aea7cb4265b1726acb24b90d8726c6 - subpackages: - - libcontainer - - libcontainer/apparmor - - libcontainer/cgroups - - libcontainer/cgroups/fs - - libcontainer/cgroups/systemd - - libcontainer/configs - - libcontainer/configs/validate - - libcontainer/criurpc - - libcontainer/label - - libcontainer/seccomp - - libcontainer/selinux - - libcontainer/stacktrace - - libcontainer/system - - libcontainer/user - - libcontainer/utils - name: github.com/pborman/uuid version: ca53cad383cad2479bbba7f7a1a05797ec1386e4 - name: github.com/prometheus/client_golang - version: 3b78d7a77f51ccbc364d4bc170920153022cfd08 + version: e51041b3fa41cece0dca035740ba6411905be473 subpackages: - prometheus - name: github.com/prometheus/client_model @@ -229,13 +282,14 @@ imports: subpackages: - go - name: github.com/prometheus/common - version: a6ab08426bb262e2d190097751f5cfd1cfdfd17d + version: ffe929a3f4c4faeaa10f2b9535c2b1be3ad15650 subpackages: - expfmt - - internal/bitbucket.org/ww/goautoneg - model - name: github.com/prometheus/procfs - version: 490cc6eb5fa45bf8a8b7b73c8bc82a8160e8531d + version: 454a56f35412459b5e684fd5ec0f9211b94f002a +- name: github.com/Sirupsen/logrus + version: 51fe59aca108dc5680109e7b2051cbdcfa5a253c - name: github.com/spf13/cobra version: 6a8bd97bdb1fc0d08a83459940498ea49d3e8c93 - name: github.com/spf13/pflag @@ -243,12 +297,12 @@ imports: - name: github.com/technosophos/moniker version: 9f956786b91d9786ca11aa5be6104542fa911546 - name: github.com/ugorji/go - version: f4485b318aadd133842532f841dc205a8e339d74 + version: f1f1a805ed361a0e078bb537e4ea78cd37dcf065 subpackages: - codec - codec/codecgen - name: golang.org/x/crypto - version: c84e1f8e3a7e322d497cd16c0e8a13c7e127baf3 + version: 1f22c0103821b9390939b6776727195525381532 subpackages: - cast5 - openpgp @@ -309,14 +363,83 @@ imports: version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 - name: gopkg.in/yaml.v2 version: a83829b6f1293c91addabc89d0571c246397bbf4 +- name: k8s.io/client-go + version: 0b62e254fe853d89b1d8d3445bbdab11bcc11bc3 + subpackages: + - 1.4/pkg/api + - 1.4/pkg/api/endpoints + - 1.4/pkg/api/errors + - 1.4/pkg/api/meta + - 1.4/pkg/api/meta/metatypes + - 1.4/pkg/api/pod + - 1.4/pkg/api/resource + - 1.4/pkg/api/service + - 1.4/pkg/api/unversioned + - 1.4/pkg/api/unversioned/validation + - 1.4/pkg/api/util + - 1.4/pkg/api/v1 + - 1.4/pkg/api/validation + - 1.4/pkg/apimachinery + - 1.4/pkg/apimachinery/registered + - 1.4/pkg/apis/autoscaling + - 1.4/pkg/apis/batch + - 1.4/pkg/apis/extensions + - 1.4/pkg/auth/user + - 1.4/pkg/capabilities + - 1.4/pkg/conversion + - 1.4/pkg/conversion/queryparams + - 1.4/pkg/fields + - 1.4/pkg/labels + - 1.4/pkg/runtime + - 1.4/pkg/runtime/serializer + - 1.4/pkg/runtime/serializer/json + - 1.4/pkg/runtime/serializer/protobuf + - 1.4/pkg/runtime/serializer/recognizer + - 1.4/pkg/runtime/serializer/streaming + - 1.4/pkg/runtime/serializer/versioning + - 1.4/pkg/security/apparmor + - 1.4/pkg/selection + - 1.4/pkg/third_party/forked/golang/reflect + - 1.4/pkg/types + - 1.4/pkg/util + - 1.4/pkg/util/clock + - 1.4/pkg/util/config + - 1.4/pkg/util/crypto + - 1.4/pkg/util/errors + - 1.4/pkg/util/flowcontrol + - 1.4/pkg/util/framer + - 1.4/pkg/util/hash + - 1.4/pkg/util/integer + - 1.4/pkg/util/intstr + - 1.4/pkg/util/json + - 1.4/pkg/util/labels + - 1.4/pkg/util/net + - 1.4/pkg/util/net/sets + - 1.4/pkg/util/parsers + - 1.4/pkg/util/rand + - 1.4/pkg/util/runtime + - 1.4/pkg/util/sets + - 1.4/pkg/util/uuid + - 1.4/pkg/util/validation + - 1.4/pkg/util/validation/field + - 1.4/pkg/util/wait + - 1.4/pkg/util/yaml + - 1.4/pkg/version + - 1.4/pkg/watch + - 1.4/pkg/watch/versioned + - 1.4/rest + - 1.4/tools/clientcmd/api + - 1.4/tools/metrics + - 1.4/transport - name: k8s.io/kubernetes - version: 17d4cf1af43a03a5997ea9142b19e5b1ac2d8747 + version: 23b426563780c64ac25df63a2d141247d02aac7a subpackages: - federation/apis/federation - federation/apis/federation/install - federation/apis/federation/v1beta1 - federation/client/clientset_generated/federation_internalclientset - federation/client/clientset_generated/federation_internalclientset/typed/core/unversioned + - federation/client/clientset_generated/federation_internalclientset/typed/extensions/unversioned - federation/client/clientset_generated/federation_internalclientset/typed/federation/unversioned - pkg/api - pkg/api/annotations @@ -341,9 +464,9 @@ imports: - pkg/apis/apps - pkg/apis/apps/install - pkg/apis/apps/v1alpha1 - - pkg/apis/authentication.k8s.io - - pkg/apis/authentication.k8s.io/install - - pkg/apis/authentication.k8s.io/v1beta1 + - pkg/apis/authentication + - pkg/apis/authentication/install + - pkg/apis/authentication/v1beta1 - pkg/apis/authorization - pkg/apis/authorization/install - pkg/apis/authorization/v1beta1 @@ -354,6 +477,9 @@ imports: - pkg/apis/batch/install - pkg/apis/batch/v1 - pkg/apis/batch/v2alpha1 + - pkg/apis/certificates + - pkg/apis/certificates/install + - pkg/apis/certificates/v1alpha1 - pkg/apis/componentconfig - pkg/apis/componentconfig/install - pkg/apis/componentconfig/v1alpha1 @@ -361,26 +487,37 @@ imports: - pkg/apis/extensions/install - pkg/apis/extensions/v1beta1 - pkg/apis/extensions/validation + - pkg/apis/imagepolicy + - pkg/apis/imagepolicy/install + - pkg/apis/imagepolicy/v1alpha1 - pkg/apis/policy - pkg/apis/policy/install - pkg/apis/policy/v1alpha1 - pkg/apis/rbac - pkg/apis/rbac/install - pkg/apis/rbac/v1alpha1 + - pkg/apis/storage + - pkg/apis/storage/install + - pkg/apis/storage/v1beta1 - pkg/auth/user - pkg/capabilities - pkg/client/cache - pkg/client/clientset_generated/internalclientset + - pkg/client/clientset_generated/internalclientset/typed/authentication/unversioned + - pkg/client/clientset_generated/internalclientset/typed/authorization/unversioned - pkg/client/clientset_generated/internalclientset/typed/autoscaling/unversioned - pkg/client/clientset_generated/internalclientset/typed/batch/unversioned + - pkg/client/clientset_generated/internalclientset/typed/certificates/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/client/clientset_generated/internalclientset/typed/storage/unversioned - pkg/client/metrics - pkg/client/record - pkg/client/restclient - pkg/client/transport - pkg/client/typed/discovery + - pkg/client/typed/dynamic - pkg/client/unversioned - pkg/client/unversioned/adapters/internalclientset - pkg/client/unversioned/auth @@ -393,6 +530,7 @@ imports: - pkg/client/unversioned/remotecommand - pkg/client/unversioned/testclient - pkg/controller + - pkg/controller/deployment/util - pkg/controller/framework - pkg/conversion - pkg/conversion/queryparams @@ -404,9 +542,9 @@ imports: - pkg/kubectl/cmd/util - pkg/kubectl/resource - pkg/kubelet/qos - - pkg/kubelet/qos/util - pkg/kubelet/server/portforward - pkg/kubelet/server/remotecommand + - pkg/kubelet/types - pkg/labels - pkg/master/ports - pkg/registry/generic @@ -418,13 +556,25 @@ imports: - pkg/runtime/serializer/recognizer - pkg/runtime/serializer/streaming - pkg/runtime/serializer/versioning + - pkg/security/apparmor - pkg/security/podsecuritypolicy/util + - pkg/selection - pkg/storage + - pkg/storage/etcd + - pkg/storage/etcd/metrics + - pkg/storage/etcd/util + - pkg/storage/etcd3 + - pkg/storage/storagebackend + - pkg/storage/storagebackend/factory - pkg/types - pkg/util + - pkg/util/cache + - pkg/util/certificates + - pkg/util/clock + - pkg/util/config - pkg/util/crypto - - pkg/util/deployment - pkg/util/errors + - pkg/util/exec - pkg/util/flag - pkg/util/flowcontrol - pkg/util/framer @@ -433,6 +583,7 @@ imports: - pkg/util/httpstream - pkg/util/httpstream/spdy - pkg/util/integer + - pkg/util/interrupt - pkg/util/intstr - pkg/util/json - pkg/util/jsonpath @@ -447,6 +598,8 @@ imports: - pkg/util/sets - pkg/util/slice - pkg/util/strategicpatch + - pkg/util/term + - pkg/util/uuid - pkg/util/validation - pkg/util/validation/field - pkg/util/wait @@ -458,8 +611,8 @@ imports: - plugin/pkg/client/auth - plugin/pkg/client/auth/gcp - plugin/pkg/client/auth/oidc - - third_party/forked/json - - third_party/forked/reflect - - third_party/golang/netutil - - third_party/golang/template + - third_party/forked/golang/json + - third_party/forked/golang/netutil + - third_party/forked/golang/reflect + - third_party/forked/golang/template testImports: [] diff --git a/glide.yaml b/glide.yaml index f0011582d..e4947bdb3 100644 --- a/glide.yaml +++ b/glide.yaml @@ -22,7 +22,7 @@ import: - package: google.golang.org/grpc version: v1.0.1-GA - package: k8s.io/kubernetes - version: ~1.3 + version: ~1.4 subpackages: - pkg/api - pkg/api/meta diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 39a90aba5..5eaee1e69 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -117,7 +117,7 @@ func (c *Client) Get(namespace string, reader io.Reader) (string, error) { // an object type changes, so we can just rely on that. Problem is it doesn't seem to keep // track of tab widths buf := new(bytes.Buffer) - p := kubectl.NewHumanReadablePrinter(false, false, false, false, false, false, []string{}) + p := kubectl.NewHumanReadablePrinter(kubectl.PrintOptions{}) for t, ot := range objs { _, err = buf.WriteString("==> " + t + "\n") if err != nil { diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index bfa953481..e1b418aa8 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -26,7 +26,6 @@ import ( "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" @@ -315,7 +314,6 @@ func createFakeInfo(name string, labels map[string]string) *resource.Info { }} 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) diff --git a/pkg/kube/tunnel.go b/pkg/kube/tunnel.go index 66406d512..53e0b1a9a 100644 --- a/pkg/kube/tunnel.go +++ b/pkg/kube/tunnel.go @@ -28,14 +28,16 @@ import ( // Tunnel describes a ssh-like tunnel to a kubernetes pod type Tunnel struct { - Local int - Remote int - stopChan chan struct{} + Local int + Remote int + stopChan chan struct{} + readyChan chan struct{} } // Close disconnects a tunnel connection func (t *Tunnel) Close() { close(t.stopChan) + close(t.readyChan) } // ForwardPort opens a tunnel to a kubernetes pod @@ -69,15 +71,16 @@ func (c *Client) ForwardPort(namespace, podName string, remote int) (*Tunnel, er } t := &Tunnel{ - Local: local, - Remote: remote, - stopChan: make(chan struct{}, 1), + Local: local, + Remote: remote, + stopChan: make(chan struct{}, 1), + readyChan: make(chan struct{}, 1), } ports := []string{fmt.Sprintf("%d:%d", local, remote)} var b bytes.Buffer - pf, err := portforward.New(dialer, ports, t.stopChan, &b, &b) + pf, err := portforward.New(dialer, ports, t.stopChan, t.readyChan, &b, &b) if err != nil { return nil, err } From ac88aaf218a89aff2327937fe6f4e02871718798 Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Fri, 23 Sep 2016 06:12:10 -0600 Subject: [PATCH 113/183] feat(*): add helm rollback functionality This feature allows you to rollback release to the previous version of release. resolves #1004 --- _proto/hapi/release/hook.proto | 16 ++- _proto/hapi/services/tiller.proto | 25 +++- cmd/helm/helm_test.go | 4 + cmd/helm/rollback.go | 15 +- cmd/helm/rollback_test.go | 2 +- cmd/tiller/hooks.go | 28 ++-- cmd/tiller/release_server.go | 115 +++++++++++++++- cmd/tiller/release_server_test.go | 171 +++++++++++++++++++++++ pkg/helm/client.go | 11 ++ pkg/helm/interface.go | 1 + pkg/helm/option.go | 69 +++++++--- pkg/kube/client.go | 1 + pkg/proto/hapi/release/hook.pb.go | 76 +++++----- pkg/proto/hapi/services/tiller.pb.go | 198 ++++++++++++++++++--------- 14 files changed, 582 insertions(+), 150 deletions(-) diff --git a/_proto/hapi/release/hook.proto b/_proto/hapi/release/hook.proto index 56918230a..388c34535 100644 --- a/_proto/hapi/release/hook.proto +++ b/_proto/hapi/release/hook.proto @@ -23,13 +23,15 @@ option go_package = "release"; // Hook defines a hook object. message Hook { enum Event { - UNKNOWN = 0; - PRE_INSTALL = 1; - POST_INSTALL = 2; - PRE_DELETE = 3; - POST_DELETE = 4; - PRE_UPGRADE = 5; - POST_UPGRADE = 6; + UNKNOWN = 0; + PRE_INSTALL = 1; + POST_INSTALL = 2; + PRE_DELETE = 3; + POST_DELETE = 4; + PRE_UPGRADE = 5; + POST_UPGRADE = 6; + PRE_ROLLBACK = 7; + POST_ROLLBACK = 8; } string name = 1; // Kind is the Kubernetes kind. diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto index 46f999a07..366a32779 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -67,9 +67,14 @@ service ReleaseService { rpc UninstallRelease(UninstallReleaseRequest) returns (UninstallReleaseResponse) { } - // GetVersion returns the current version of the server. - rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) { -} + // GetVersion returns the current version of the server. + rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) { + } + + // RollbackRelease rolls back a release to a previous version. + rpc RollbackRelease(RollbackReleaseRequest) returns (RollbackReleaseResponse) { + } + } // ListReleasesRequest requests a list of releases. @@ -188,6 +193,20 @@ message UpdateReleaseResponse { hapi.release.Release release = 1; } +message RollbackReleaseRequest { + // The name of the release + string name = 1; + // dry_run, if true, will run through the release logic but no create + bool dry_run = 2; + // DisableHooks causes the server to skip running any hooks for the rollback + bool disable_hooks = 3; +} + +// RollbackReleaseResponse is the response to an update request. +message RollbackReleaseResponse { + hapi.release.Release release = 1; +} + // InstallReleaseRequest is the request for an installation of a chart. message InstallReleaseRequest { // Chart is the protobuf representation of a chart. diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index e0ce52c74..ef99484ba 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -159,6 +159,10 @@ func (c *fakeReleaseClient) UpdateRelease(rlsName string, chStr string, opts ... return nil, nil } +func (c *fakeReleaseClient) RollbackRelease(rlsName string, opts ...helm.RollbackOption) (*rls.RollbackReleaseResponse, error) { + return nil, nil +} + func (c *fakeReleaseClient) ReleaseContent(rlsName string, opts ...helm.ContentOption) (resp *rls.GetReleaseContentResponse, err error) { if len(c.rels) > 0 { resp = &rls.GetReleaseContentResponse{ diff --git a/cmd/helm/rollback.go b/cmd/helm/rollback.go index f711dacf4..d34a68c80 100644 --- a/cmd/helm/rollback.go +++ b/cmd/helm/rollback.go @@ -27,9 +27,7 @@ import ( const rollbackDesc = ` This command rolls back a release to the previous version. - -The rollback argument is the name of a release. - +The argument of the rollback command is the name of a release. ` type rollbackCmd struct { @@ -55,22 +53,25 @@ func newRollbackCmd(c helm.Interface, out io.Writer) *cobra.Command { if err := checkArgsLength(len(args), "release name"); err != nil { return err } + rollback.name = args[0] rollback.client = ensureHelmClient(rollback.client) return rollback.run() }, } f := cmd.Flags() - f.BoolVar(&rollback.dryRun, "dry-run", false, "simulate an install") + f.BoolVar(&rollback.dryRun, "dry-run", false, "simulate a rollback") f.BoolVar(&rollback.disableHooks, "no-hooks", false, "prevent hooks from running during rollback") return cmd } func (r *rollbackCmd) run() error { + _, err := r.client.RollbackRelease(r.name, helm.RollbackDryRun(r.dryRun), helm.RollbackDisableHooks(r.disableHooks)) + if err != nil { + return prettyError(err) + } - msg := "This command is under construction. Coming soon to a Helm near you!" - - fmt.Fprintf(r.out, msg) + fmt.Fprintf(r.out, "Rollback was a success! Happy Helming!\n") return nil } diff --git a/cmd/helm/rollback_test.go b/cmd/helm/rollback_test.go index 0c641b66e..7d9218b23 100644 --- a/cmd/helm/rollback_test.go +++ b/cmd/helm/rollback_test.go @@ -30,7 +30,7 @@ func TestRollbackCmd(t *testing.T) { name: "rollback a release", args: []string{"funny-honey"}, resp: nil, - expected: "This command is under construction. Coming soon to a Helm near you!", + expected: "Rollback was a success! Happy Helming!", }, } diff --git a/cmd/tiller/hooks.go b/cmd/tiller/hooks.go index c0fd5be3c..5eb3722af 100644 --- a/cmd/tiller/hooks.go +++ b/cmd/tiller/hooks.go @@ -30,21 +30,25 @@ import ( const hookAnno = "helm.sh/hook" const ( - preInstall = "pre-install" - postInstall = "post-install" - preDelete = "pre-delete" - postDelete = "post-delete" - preUpgrade = "pre-upgrade" - postUpgrade = "post-upgrade" + preInstall = "pre-install" + postInstall = "post-install" + preDelete = "pre-delete" + postDelete = "post-delete" + preUpgrade = "pre-upgrade" + postUpgrade = "post-upgrade" + preRollback = "pre-rollback" + postRollback = "post-rollback" ) var events = map[string]release.Hook_Event{ - preInstall: release.Hook_PRE_INSTALL, - postInstall: release.Hook_POST_INSTALL, - preDelete: release.Hook_PRE_DELETE, - postDelete: release.Hook_POST_DELETE, - preUpgrade: release.Hook_PRE_UPGRADE, - postUpgrade: release.Hook_POST_UPGRADE, + preInstall: release.Hook_PRE_INSTALL, + postInstall: release.Hook_POST_INSTALL, + preDelete: release.Hook_PRE_DELETE, + postDelete: release.Hook_POST_DELETE, + preUpgrade: release.Hook_PRE_UPGRADE, + postUpgrade: release.Hook_POST_UPGRADE, + preRollback: release.Hook_PRE_ROLLBACK, + postRollback: release.Hook_POST_ROLLBACK, } type simpleHead struct { diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 8820afbda..ed674aaab 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -30,6 +30,7 @@ import ( "github.com/ghodss/yaml" "github.com/technosophos/moniker" ctx "golang.org/x/net/context" + "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/helm/cmd/tiller/environment" "k8s.io/helm/pkg/chartutil" @@ -39,7 +40,6 @@ import ( "k8s.io/helm/pkg/storage/driver" "k8s.io/helm/pkg/timeconv" "k8s.io/helm/pkg/version" - "k8s.io/kubernetes/pkg/api/unversioned" ) var srv *releaseServer @@ -303,10 +303,7 @@ func (s *releaseServer) performUpdate(originalRelease, updatedRelease *release.R } } - kubeCli := s.env.KubeClient - original := bytes.NewBufferString(originalRelease.Manifest) - modified := bytes.NewBufferString(updatedRelease.Manifest) - if err := kubeCli.Update(updatedRelease.Namespace, original, modified); err != nil { + if err := s.performKubeUpdate(originalRelease, updatedRelease); err != nil { return nil, err } @@ -382,6 +379,114 @@ func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*rele return currentRelease, updatedRelease, nil } +func (s *releaseServer) RollbackRelease(c ctx.Context, req *services.RollbackReleaseRequest) (*services.RollbackReleaseResponse, error) { + + currentRelease, targetRelease, err := s.prepareRollback(req) + if err != nil { + return nil, err + } + + rel, err := s.performRollback(currentRelease, targetRelease, req) + if err != nil { + return nil, err + } + + if err := s.env.Releases.Create(targetRelease); err != nil { + return nil, err + } + + return rel, nil +} + +func (s *releaseServer) performRollback(currentRelease, targetRelease *release.Release, req *services.RollbackReleaseRequest) (*services.RollbackReleaseResponse, error) { + res := &services.RollbackReleaseResponse{Release: targetRelease} + + if req.DryRun { + log.Printf("Dry run for %s", targetRelease.Name) + return res, nil + } + + // pre-rollback hooks + if !req.DisableHooks { + if err := s.execHook(targetRelease.Hooks, targetRelease.Name, targetRelease.Namespace, preRollback); err != nil { + return res, err + } + } + + if err := s.performKubeUpdate(currentRelease, targetRelease); err != nil { + return nil, err + } + + // post-rollback hooks + if !req.DisableHooks { + if err := s.execHook(targetRelease.Hooks, targetRelease.Name, targetRelease.Namespace, postRollback); err != nil { + return res, err + } + } + + currentRelease.Info.Status.Code = release.Status_SUPERSEDED + if err := s.env.Releases.Update(currentRelease); err != nil { + return nil, fmt.Errorf("Update of %s failed: %s", currentRelease.Name, err) + } + + targetRelease.Info.Status.Code = release.Status_DEPLOYED + + return res, nil +} + +func (s *releaseServer) performKubeUpdate(currentRelease, targetRelease *release.Release) error { + kubeCli := s.env.KubeClient + current := bytes.NewBufferString(currentRelease.Manifest) + target := bytes.NewBufferString(targetRelease.Manifest) + if err := kubeCli.Update(targetRelease.Namespace, current, target); err != nil { + return err + } + return nil +} + +// prepareRollback finds the previous release and prepares a new release object with +// the previous release's configuration +func (s *releaseServer) prepareRollback(req *services.RollbackReleaseRequest) (*release.Release, *release.Release, error) { + + if req.Name == "" { + return nil, nil, errMissingRelease + } + + // finds the non-deleted release with the given name + currentRelease, err := s.env.Releases.Deployed(req.Name) + if err != nil { + return nil, nil, err + } + + previousRelease, err := s.env.Releases.Get(req.Name, currentRelease.Version-1) + if err != nil { + return nil, nil, err + } + + ts := timeconv.Now() + + // Store a new release object with previous release's configuration + targetRelease := &release.Release{ + Name: req.Name, + Namespace: currentRelease.Namespace, + Chart: previousRelease.Chart, + Config: previousRelease.Config, + Info: &release.Info{ + FirstDeployed: currentRelease.Info.FirstDeployed, + LastDeployed: ts, + Status: &release.Status{ + Code: release.Status_UNKNOWN, + Notes: previousRelease.Info.Status.Notes, + }, + }, + Version: currentRelease.Version + 1, + Manifest: previousRelease.Manifest, + Hooks: previousRelease.Hooks, + } + + return currentRelease, targetRelease, nil +} + func (s *releaseServer) uniqName(start string, reuse bool) (string, error) { // If a name is supplied, we check to see if that name is taken. If not, it diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 7db3cac83..d9d988447 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -60,6 +60,16 @@ data: name: value ` +var manifestWithRollbackHooks = `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm + annotations: + "helm.sh/hook": post-rollback,pre-rollback +data: + name: value +` + func rsFixture() *releaseServer { return &releaseServer{ env: mockEnvironment(), @@ -117,6 +127,23 @@ func namedReleaseStub(name string, status release.Status_Code) *release.Release } } +func upgradeReleaseVersion(rel *release.Release) *release.Release { + date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0} + + rel.Info.Status.Code = release.Status_SUPERSEDED + return &release.Release{ + Name: rel.Name, + Info: &release.Info{ + FirstDeployed: rel.Info.FirstDeployed, + LastDeployed: &date, + Status: &release.Status{Code: release.Status_DEPLOYED}, + }, + Chart: rel.Chart, + Config: rel.Config, + Version: rel.Version + 1, + } +} + func TestGetVersionSet(t *testing.T) { rs := rsFixture() vs, err := rs.getVersionSet() @@ -601,6 +628,150 @@ func TestUpdateReleaseNoChanges(t *testing.T) { } } +func TestRollbackReleaseNoHooks(t *testing.T) { + c := context.Background() + rs := rsFixture() + rel := releaseStub() + rel.Hooks = []*release.Hook{ + { + Name: "test-cm", + Kind: "ConfigMap", + Path: "test-cm", + Manifest: manifestWithRollbackHooks, + Events: []release.Hook_Event{ + release.Hook_PRE_ROLLBACK, + release.Hook_POST_ROLLBACK, + }, + }, + } + rs.env.Releases.Create(rel) + upgradedRel := upgradeReleaseVersion(rel) + rs.env.Releases.Update(rel) + rs.env.Releases.Create(upgradedRel) + + req := &services.RollbackReleaseRequest{ + Name: rel.Name, + DisableHooks: true, + } + + res, err := rs.RollbackRelease(c, req) + if err != nil { + t.Fatalf("Failed rollback: %s", err) + } + + if hl := res.Release.Hooks[0].LastRun; hl != nil { + t.Errorf("Expected that no hooks were run. Got %d", hl) + } +} + +func TestRollbackRelease(t *testing.T) { + c := context.Background() + rs := rsFixture() + rel := releaseStub() + rs.env.Releases.Create(rel) + upgradedRel := upgradeReleaseVersion(rel) + upgradedRel.Hooks = []*release.Hook{ + { + Name: "test-cm", + Kind: "ConfigMap", + Path: "test-cm", + Manifest: manifestWithRollbackHooks, + Events: []release.Hook_Event{ + release.Hook_PRE_ROLLBACK, + release.Hook_POST_ROLLBACK, + }, + }, + } + + upgradedRel.Manifest = "hello world" + rs.env.Releases.Update(rel) + rs.env.Releases.Create(upgradedRel) + + req := &services.RollbackReleaseRequest{ + Name: rel.Name, + } + res, err := rs.RollbackRelease(c, req) + if err != nil { + t.Fatalf("Failed rollback: %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) + } + + if res.Release.Version != 3 { + t.Errorf("Expected release version to be %v, got %v", 3, res.Release.Version) + } + + updated, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version) + 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 != manifestWithHook { + t.Errorf("Unexpected manifest: %v", updated.Hooks[0].Manifest) + } + + anotherUpgradedRelease := upgradeReleaseVersion(upgradedRel) + rs.env.Releases.Update(upgradedRel) + rs.env.Releases.Create(anotherUpgradedRelease) + + res, err = rs.RollbackRelease(c, req) + if err != nil { + t.Fatalf("Failed rollback: %s", err) + } + + updated, err = rs.env.Releases.Get(res.Release.Name, res.Release.Version) + 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 != manifestWithRollbackHooks { + t.Errorf("Unexpected manifest: %v", updated.Hooks[0].Manifest) + } + + if res.Release.Version != 4 { + t.Errorf("Expected release version to be %v, got %v", 3, res.Release.Version) + } + + if updated.Hooks[0].Events[0] != release.Hook_PRE_ROLLBACK { + t.Errorf("Expected event 0 to be pre rollback") + } + + if updated.Hooks[0].Events[1] != release.Hook_POST_ROLLBACK { + t.Errorf("Expected event 1 to be post rollback") + } + + 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, "hello world") { + t.Errorf("unexpected output: %s", rel.Manifest) + } + +} + func TestUninstallRelease(t *testing.T) { c := helm.NewContext() rs := rsFixture() diff --git a/pkg/helm/client.go b/pkg/helm/client.go index fc47687f7..b7522fd03 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -131,6 +131,17 @@ func (h *Client) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, err return h.opts.rpcGetVersion(rls.NewReleaseServiceClient(c), opts...) } +// RollbackRelease rolls back a release to the previous version +func (h *Client) RollbackRelease(rlsName string, opts ...RollbackOption) (*rls.RollbackReleaseResponse, error) { + c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) + if err != nil { + return nil, err + } + defer c.Close() + + return h.opts.rpcRollbackRelease(rlsName, rls.NewReleaseServiceClient(c), opts...) +} + // ReleaseStatus returns the given release's status. // // Note: there aren't currently any supported StatusOptions, diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go index 1510b4d56..3ec1e83b4 100644 --- a/pkg/helm/interface.go +++ b/pkg/helm/interface.go @@ -27,6 +27,7 @@ type Interface interface { DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) ReleaseStatus(rlsName string, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) UpdateRelease(rlsName, chStr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) + RollbackRelease(rlsName string, opts ...RollbackOption) (*rls.RollbackReleaseResponse, error) ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) } diff --git a/pkg/helm/option.go b/pkg/helm/option.go index c83185f12..4ff73ea9c 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -53,6 +53,8 @@ type options struct { statusReq rls.GetReleaseStatusRequest // release get content options are applied directly to the get release content request contentReq rls.GetReleaseContentRequest + // release rollback options are applied directly to the rollback release request + rollbackReq rls.RollbackReleaseRequest } // Host specifies the host address of the Tiller release server, (default = ":44134"). @@ -124,17 +126,17 @@ func ValueOverrides(raw []byte) InstallOption { } } -// UpdateValueOverrides specifies a list of values to include when upgrading -func UpdateValueOverrides(raw []byte) UpdateOption { +// ReleaseName specifies the name of the release when installing. +func ReleaseName(name string) InstallOption { return func(opts *options) { - opts.updateReq.Values = &cpb.Config{Raw: string(raw)} + opts.instReq.Name = name } } -// ReleaseName specifies the name of the release when installing. -func ReleaseName(name string) InstallOption { +// UpdateValueOverrides specifies a list of values to include when upgrading +func UpdateValueOverrides(raw []byte) UpdateOption { return func(opts *options) { - opts.instReq.Name = name + opts.updateReq.Values = &cpb.Config{Raw: string(raw)} } } @@ -159,38 +161,52 @@ func DeletePurge(purge bool) DeleteOption { } } -// UpgradeDisableHooks will disable hooks for an upgrade operation. -func UpgradeDisableHooks(disable bool) UpdateOption { +// InstallDryRun will (if true) execute an installation as a dry run. +func InstallDryRun(dry bool) InstallOption { + return func(opts *options) { + opts.dryRun = dry + } +} + +// InstallDisableHooks disables hooks during installation. +func InstallDisableHooks(disable bool) InstallOption { return func(opts *options) { opts.disableHooks = disable } } -// UpgradeDryRun will (if true) execute an upgrade as a dry run. -func UpgradeDryRun(dry bool) UpdateOption { +// InstallReuseName will (if true) instruct Tiller to re-use an existing name. +func InstallReuseName(reuse bool) InstallOption { return func(opts *options) { - opts.dryRun = dry + opts.reuseName = reuse } } -// InstallDisableHooks disables hooks during installation. -func InstallDisableHooks(disable bool) InstallOption { +// RollbackDisableHooks will disable hooks for a rollback operation +func RollbackDisableHooks(disable bool) RollbackOption { return func(opts *options) { opts.disableHooks = disable } } -// InstallDryRun will (if true) execute an installation as a dry run. -func InstallDryRun(dry bool) InstallOption { +// RollbackDryRun will (if true) execute a rollback as a dry run. +func RollbackDryRun(dry bool) RollbackOption { return func(opts *options) { opts.dryRun = dry } } -// InstallReuseName will (if true) instruct Tiller to re-use an existing name. -func InstallReuseName(reuse bool) InstallOption { +// UpgradeDisableHooks will disable hooks for an upgrade operation. +func UpgradeDisableHooks(disable bool) UpdateOption { return func(opts *options) { - opts.reuseName = reuse + opts.disableHooks = disable + } +} + +// UpgradeDryRun will (if true) execute an upgrade as a dry run. +func UpgradeDryRun(dry bool) UpdateOption { + return func(opts *options) { + opts.dryRun = dry } } @@ -230,6 +246,11 @@ type VersionOption func(*options) // the defaults used when running the `helm upgrade` command. type UpdateOption func(*options) +// RollbackOption allows specififying various settings configurable +// by the helm client user for overriding the defaults used when +// running the `helm rollback` command. +type RollbackOption func(*options) + // RPC helpers defined on `options` type. Note: These actually execute the // the corresponding tiller RPC. There is no particular reason why these // are APIs are hung off `options`, they are internal to pkg/helm to remain @@ -303,6 +324,18 @@ func (o *options) rpcUpdateRelease(rlsName string, chr *cpb.Chart, rlc rls.Relea return rlc.UpdateRelease(NewContext(), &o.updateReq) } +// Executes tiller.UpdateRelease RPC. +func (o *options) rpcRollbackRelease(rlsName string, rlc rls.ReleaseServiceClient, opts ...RollbackOption) (*rls.RollbackReleaseResponse, error) { + for _, opt := range opts { + opt(o) + } + + o.rollbackReq.DryRun = o.dryRun + o.rollbackReq.Name = rlsName + + return rlc.RollbackRelease(context.TODO(), &o.rollbackReq) +} + // Executes tiller.GetReleaseStatus RPC. func (o *options) rpcGetReleaseStatus(rlsName string, rlc rls.ReleaseServiceClient, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) { for _, opt := range opts { diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 39a90aba5..8c35f751d 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -170,6 +170,7 @@ func (c *Client) Update(namespace string, currentReader, targetReader io.Reader) updateErrors := []string{} err = target.Visit(func(info *resource.Info, err error) error { + targetInfos = append(targetInfos, info) if err != nil { return err diff --git a/pkg/proto/hapi/release/hook.pb.go b/pkg/proto/hapi/release/hook.pb.go index 0a0fecdbe..57581b14e 100644 --- a/pkg/proto/hapi/release/hook.pb.go +++ b/pkg/proto/hapi/release/hook.pb.go @@ -38,13 +38,15 @@ const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package type Hook_Event int32 const ( - Hook_UNKNOWN Hook_Event = 0 - Hook_PRE_INSTALL Hook_Event = 1 - Hook_POST_INSTALL Hook_Event = 2 - Hook_PRE_DELETE Hook_Event = 3 - Hook_POST_DELETE Hook_Event = 4 - Hook_PRE_UPGRADE Hook_Event = 5 - Hook_POST_UPGRADE Hook_Event = 6 + Hook_UNKNOWN Hook_Event = 0 + Hook_PRE_INSTALL Hook_Event = 1 + Hook_POST_INSTALL Hook_Event = 2 + Hook_PRE_DELETE Hook_Event = 3 + Hook_POST_DELETE Hook_Event = 4 + Hook_PRE_UPGRADE Hook_Event = 5 + Hook_POST_UPGRADE Hook_Event = 6 + Hook_PRE_ROLLBACK Hook_Event = 7 + Hook_POST_ROLLBACK Hook_Event = 8 ) var Hook_Event_name = map[int32]string{ @@ -55,15 +57,19 @@ var Hook_Event_name = map[int32]string{ 4: "POST_DELETE", 5: "PRE_UPGRADE", 6: "POST_UPGRADE", + 7: "PRE_ROLLBACK", + 8: "POST_ROLLBACK", } var Hook_Event_value = map[string]int32{ - "UNKNOWN": 0, - "PRE_INSTALL": 1, - "POST_INSTALL": 2, - "PRE_DELETE": 3, - "POST_DELETE": 4, - "PRE_UPGRADE": 5, - "POST_UPGRADE": 6, + "UNKNOWN": 0, + "PRE_INSTALL": 1, + "POST_INSTALL": 2, + "PRE_DELETE": 3, + "POST_DELETE": 4, + "PRE_UPGRADE": 5, + "POST_UPGRADE": 6, + "PRE_ROLLBACK": 7, + "POST_ROLLBACK": 8, } func (x Hook_Event) String() string { @@ -106,24 +112,26 @@ func init() { func init() { proto.RegisterFile("hapi/release/hook.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 301 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0x4f, 0x4f, 0xc2, 0x30, - 0x18, 0xc6, 0x1d, 0x8c, 0x01, 0x2f, 0x04, 0x97, 0x5e, 0x6c, 0xb8, 0x48, 0x38, 0x71, 0xea, 0x0c, - 0xc6, 0x0f, 0x80, 0xa1, 0x51, 0x23, 0x19, 0xa4, 0x8c, 0x98, 0x78, 0x21, 0x25, 0x16, 0x58, 0x60, - 0xed, 0xb2, 0x75, 0x5e, 0xfc, 0x7a, 0x7e, 0x30, 0xd3, 0xee, 0x4f, 0xbc, 0xbd, 0xfb, 0xbd, 0xbf, - 0x3d, 0x7d, 0x1f, 0xb8, 0x3b, 0xf3, 0x34, 0x0e, 0x32, 0x71, 0x15, 0x3c, 0x17, 0xc1, 0x59, 0xa9, - 0x0b, 0x49, 0x33, 0xa5, 0x15, 0x1a, 0x9a, 0x05, 0xa9, 0x16, 0xe3, 0xfb, 0x93, 0x52, 0xa7, 0xab, - 0x08, 0xec, 0xee, 0x50, 0x1c, 0x03, 0x1d, 0x27, 0x22, 0xd7, 0x3c, 0x49, 0x4b, 0x7d, 0xfa, 0xdb, - 0x02, 0xf7, 0x55, 0xa9, 0x0b, 0x42, 0xe0, 0x4a, 0x9e, 0x08, 0xec, 0x4c, 0x9c, 0x59, 0x9f, 0xd9, - 0xd9, 0xb0, 0x4b, 0x2c, 0xbf, 0x70, 0xab, 0x64, 0x66, 0x36, 0x2c, 0xe5, 0xfa, 0x8c, 0xdb, 0x25, - 0x33, 0x33, 0x1a, 0x43, 0x2f, 0xe1, 0x32, 0x3e, 0x8a, 0x5c, 0x63, 0xd7, 0xf2, 0xe6, 0x1b, 0x3d, - 0x80, 0x27, 0xbe, 0x85, 0xd4, 0x39, 0xee, 0x4c, 0xda, 0xb3, 0xd1, 0x1c, 0x93, 0xff, 0x07, 0x12, - 0xf3, 0x36, 0xa1, 0x46, 0x60, 0x95, 0x87, 0x9e, 0xa0, 0x77, 0xe5, 0xb9, 0xde, 0x67, 0x85, 0xc4, - 0xde, 0xc4, 0x99, 0x0d, 0xe6, 0x63, 0x52, 0xd6, 0x20, 0x75, 0x0d, 0x12, 0xd5, 0x35, 0x58, 0xd7, - 0xb8, 0xac, 0x90, 0xd3, 0x1f, 0xe8, 0xd8, 0x1c, 0x34, 0x80, 0xee, 0x2e, 0x7c, 0x0f, 0xd7, 0x1f, - 0xa1, 0x7f, 0x83, 0x6e, 0x61, 0xb0, 0x61, 0x74, 0xff, 0x16, 0x6e, 0xa3, 0xc5, 0x6a, 0xe5, 0x3b, - 0xc8, 0x87, 0xe1, 0x66, 0xbd, 0x8d, 0x1a, 0xd2, 0x42, 0x23, 0x00, 0xa3, 0x2c, 0xe9, 0x8a, 0x46, - 0xd4, 0x6f, 0xdb, 0x5f, 0x8c, 0x51, 0x01, 0xb7, 0xce, 0xd8, 0x6d, 0x5e, 0xd8, 0x62, 0x49, 0xfd, - 0x4e, 0x93, 0x51, 0x13, 0xef, 0xb9, 0xff, 0xd9, 0xad, 0x1a, 0x1d, 0x3c, 0x7b, 0xe4, 0xe3, 0x5f, - 0x00, 0x00, 0x00, 0xff, 0xff, 0x16, 0x64, 0x61, 0x76, 0xa2, 0x01, 0x00, 0x00, + // 321 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0x4d, 0x6e, 0xea, 0x30, + 0x14, 0x85, 0x5f, 0x20, 0x24, 0x70, 0xe1, 0xd1, 0xd4, 0x93, 0x5a, 0x4c, 0x8a, 0x18, 0x31, 0x72, + 0x2a, 0xaa, 0x2e, 0x00, 0x8a, 0xd5, 0x56, 0x44, 0x01, 0x99, 0xa0, 0x4a, 0x9d, 0x20, 0xa3, 0x1a, + 0x88, 0x20, 0x71, 0x44, 0x4c, 0xd7, 0xd3, 0xf5, 0x75, 0x15, 0x95, 0x9d, 0x1f, 0x75, 0x76, 0xfd, + 0xdd, 0xcf, 0xc7, 0x3e, 0x70, 0x77, 0xe4, 0x59, 0xec, 0x5f, 0xc4, 0x59, 0xf0, 0x5c, 0xf8, 0x47, + 0x29, 0x4f, 0x24, 0xbb, 0x48, 0x25, 0x51, 0x4f, 0x2f, 0x48, 0xb9, 0x18, 0xdc, 0x1f, 0xa4, 0x3c, + 0x9c, 0x85, 0x6f, 0x76, 0xbb, 0xeb, 0xde, 0x57, 0x71, 0x22, 0x72, 0xc5, 0x93, 0xac, 0xd0, 0x47, + 0x3f, 0x0d, 0xb0, 0x5f, 0xa5, 0x3c, 0x21, 0x04, 0x76, 0xca, 0x13, 0x81, 0xad, 0xa1, 0x35, 0xee, + 0x30, 0x33, 0x6b, 0x76, 0x8a, 0xd3, 0x4f, 0xdc, 0x28, 0x98, 0x9e, 0x35, 0xcb, 0xb8, 0x3a, 0xe2, + 0x66, 0xc1, 0xf4, 0x8c, 0x06, 0xd0, 0x4e, 0x78, 0x1a, 0xef, 0x45, 0xae, 0xb0, 0x6d, 0x78, 0x7d, + 0x46, 0x0f, 0xe0, 0x88, 0x2f, 0x91, 0xaa, 0x1c, 0xb7, 0x86, 0xcd, 0x71, 0x7f, 0x82, 0xc9, 0xdf, + 0x0f, 0x12, 0xfd, 0x36, 0xa1, 0x5a, 0x60, 0xa5, 0x87, 0x9e, 0xa0, 0x7d, 0xe6, 0xb9, 0xda, 0x5e, + 0xae, 0x29, 0x76, 0x86, 0xd6, 0xb8, 0x3b, 0x19, 0x90, 0xa2, 0x06, 0xa9, 0x6a, 0x90, 0xa8, 0xaa, + 0xc1, 0x5c, 0xed, 0xb2, 0x6b, 0x3a, 0xfa, 0xb6, 0xa0, 0x65, 0x82, 0x50, 0x17, 0xdc, 0x4d, 0xb8, + 0x08, 0x97, 0xef, 0xa1, 0xf7, 0x0f, 0xdd, 0x40, 0x77, 0xc5, 0xe8, 0xf6, 0x2d, 0x5c, 0x47, 0xd3, + 0x20, 0xf0, 0x2c, 0xe4, 0x41, 0x6f, 0xb5, 0x5c, 0x47, 0x35, 0x69, 0xa0, 0x3e, 0x80, 0x56, 0xe6, + 0x34, 0xa0, 0x11, 0xf5, 0x9a, 0xe6, 0x8a, 0x36, 0x4a, 0x60, 0x57, 0x19, 0x9b, 0xd5, 0x0b, 0x9b, + 0xce, 0xa9, 0xd7, 0xaa, 0x33, 0x2a, 0xe2, 0x18, 0xc2, 0xe8, 0x96, 0x2d, 0x83, 0x60, 0x36, 0x7d, + 0x5e, 0x78, 0x2e, 0xba, 0x85, 0xff, 0xc6, 0xa9, 0x51, 0x7b, 0xd6, 0xf9, 0x70, 0xcb, 0xde, 0x3b, + 0xc7, 0x54, 0x79, 0xfc, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xa4, 0x2e, 0x6f, 0xbd, 0xc8, 0x01, 0x00, + 0x00, } diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index 55d1f8ccd..f05cca029 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -18,6 +18,8 @@ It has these top-level messages: GetReleaseContentResponse UpdateReleaseRequest UpdateReleaseResponse + RollbackReleaseRequest + RollbackReleaseResponse InstallReleaseRequest InstallReleaseResponse UninstallReleaseRequest @@ -280,6 +282,37 @@ func (m *UpdateReleaseResponse) GetRelease() *hapi_release3.Release { return nil } +type RollbackReleaseRequest struct { + // The name of the release + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + // dry_run, if true, will run through the release logic but no create + DryRun bool `protobuf:"varint,2,opt,name=dry_run,json=dryRun" json:"dry_run,omitempty"` + // DisableHooks causes the server to skip running any hooks for the rollback + DisableHooks bool `protobuf:"varint,3,opt,name=disable_hooks,json=disableHooks" json:"disable_hooks,omitempty"` +} + +func (m *RollbackReleaseRequest) Reset() { *m = RollbackReleaseRequest{} } +func (m *RollbackReleaseRequest) String() string { return proto.CompactTextString(m) } +func (*RollbackReleaseRequest) ProtoMessage() {} +func (*RollbackReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{9} } + +// RollbackReleaseResponse is the response to an update request. +type RollbackReleaseResponse struct { + Release *hapi_release3.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` +} + +func (m *RollbackReleaseResponse) Reset() { *m = RollbackReleaseResponse{} } +func (m *RollbackReleaseResponse) String() string { return proto.CompactTextString(m) } +func (*RollbackReleaseResponse) ProtoMessage() {} +func (*RollbackReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} } + +func (m *RollbackReleaseResponse) GetRelease() *hapi_release3.Release { + if m != nil { + return m.Release + } + return nil +} + // InstallReleaseRequest is the request for an installation of a chart. type InstallReleaseRequest struct { // Chart is the protobuf representation of a chart. @@ -305,7 +338,7 @@ type InstallReleaseRequest struct { func (m *InstallReleaseRequest) Reset() { *m = InstallReleaseRequest{} } func (m *InstallReleaseRequest) String() string { return proto.CompactTextString(m) } func (*InstallReleaseRequest) ProtoMessage() {} -func (*InstallReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{9} } +func (*InstallReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{11} } func (m *InstallReleaseRequest) GetChart() *hapi_chart3.Chart { if m != nil { @@ -329,7 +362,7 @@ type InstallReleaseResponse struct { func (m *InstallReleaseResponse) Reset() { *m = InstallReleaseResponse{} } func (m *InstallReleaseResponse) String() string { return proto.CompactTextString(m) } func (*InstallReleaseResponse) ProtoMessage() {} -func (*InstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} } +func (*InstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{12} } func (m *InstallReleaseResponse) GetRelease() *hapi_release3.Release { if m != nil { @@ -351,7 +384,7 @@ type UninstallReleaseRequest struct { func (m *UninstallReleaseRequest) Reset() { *m = UninstallReleaseRequest{} } func (m *UninstallReleaseRequest) String() string { return proto.CompactTextString(m) } func (*UninstallReleaseRequest) ProtoMessage() {} -func (*UninstallReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{11} } +func (*UninstallReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{13} } // UninstallReleaseResponse represents a successful response to an uninstall request. type UninstallReleaseResponse struct { @@ -362,7 +395,7 @@ type UninstallReleaseResponse struct { func (m *UninstallReleaseResponse) Reset() { *m = UninstallReleaseResponse{} } func (m *UninstallReleaseResponse) String() string { return proto.CompactTextString(m) } func (*UninstallReleaseResponse) ProtoMessage() {} -func (*UninstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{12} } +func (*UninstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{14} } func (m *UninstallReleaseResponse) GetRelease() *hapi_release3.Release { if m != nil { @@ -378,7 +411,7 @@ type GetVersionRequest struct { func (m *GetVersionRequest) Reset() { *m = GetVersionRequest{} } func (m *GetVersionRequest) String() string { return proto.CompactTextString(m) } func (*GetVersionRequest) ProtoMessage() {} -func (*GetVersionRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{13} } +func (*GetVersionRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{15} } type GetVersionResponse struct { Version *hapi_version.Version `protobuf:"bytes,1,opt,name=Version" json:"Version,omitempty"` @@ -387,7 +420,7 @@ type GetVersionResponse struct { func (m *GetVersionResponse) Reset() { *m = GetVersionResponse{} } func (m *GetVersionResponse) String() string { return proto.CompactTextString(m) } func (*GetVersionResponse) ProtoMessage() {} -func (*GetVersionResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{14} } +func (*GetVersionResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{16} } func (m *GetVersionResponse) GetVersion() *hapi_version.Version { if m != nil { @@ -406,6 +439,8 @@ func init() { proto.RegisterType((*GetReleaseContentResponse)(nil), "hapi.services.tiller.GetReleaseContentResponse") proto.RegisterType((*UpdateReleaseRequest)(nil), "hapi.services.tiller.UpdateReleaseRequest") proto.RegisterType((*UpdateReleaseResponse)(nil), "hapi.services.tiller.UpdateReleaseResponse") + proto.RegisterType((*RollbackReleaseRequest)(nil), "hapi.services.tiller.RollbackReleaseRequest") + proto.RegisterType((*RollbackReleaseResponse)(nil), "hapi.services.tiller.RollbackReleaseResponse") proto.RegisterType((*InstallReleaseRequest)(nil), "hapi.services.tiller.InstallReleaseRequest") proto.RegisterType((*InstallReleaseResponse)(nil), "hapi.services.tiller.InstallReleaseResponse") proto.RegisterType((*UninstallReleaseRequest)(nil), "hapi.services.tiller.UninstallReleaseRequest") @@ -444,6 +479,8 @@ type ReleaseServiceClient interface { UninstallRelease(ctx context.Context, in *UninstallReleaseRequest, opts ...grpc.CallOption) (*UninstallReleaseResponse, error) // GetVersion returns the current version of the server. GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) + // RollbackRelease rolls back a release to a previous version. + RollbackRelease(ctx context.Context, in *RollbackReleaseRequest, opts ...grpc.CallOption) (*RollbackReleaseResponse, error) } type releaseServiceClient struct { @@ -540,6 +577,15 @@ func (c *releaseServiceClient) GetVersion(ctx context.Context, in *GetVersionReq return out, nil } +func (c *releaseServiceClient) RollbackRelease(ctx context.Context, in *RollbackReleaseRequest, opts ...grpc.CallOption) (*RollbackReleaseResponse, error) { + out := new(RollbackReleaseResponse) + err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/RollbackRelease", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // Server API for ReleaseService service type ReleaseServiceServer interface { @@ -560,6 +606,8 @@ type ReleaseServiceServer interface { UninstallRelease(context.Context, *UninstallReleaseRequest) (*UninstallReleaseResponse, error) // GetVersion returns the current version of the server. GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) + // RollbackRelease rolls back a release to a previous version. + RollbackRelease(context.Context, *RollbackReleaseRequest) (*RollbackReleaseResponse, error) } func RegisterReleaseServiceServer(s *grpc.Server, srv ReleaseServiceServer) { @@ -695,6 +743,24 @@ func _ReleaseService_GetVersion_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _ReleaseService_RollbackRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RollbackReleaseRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ReleaseServiceServer).RollbackRelease(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/hapi.services.tiller.ReleaseService/RollbackRelease", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReleaseServiceServer).RollbackRelease(ctx, req.(*RollbackReleaseRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _ReleaseService_serviceDesc = grpc.ServiceDesc{ ServiceName: "hapi.services.tiller.ReleaseService", HandlerType: (*ReleaseServiceServer)(nil), @@ -723,6 +789,10 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ MethodName: "GetVersion", Handler: _ReleaseService_GetVersion_Handler, }, + { + MethodName: "RollbackRelease", + Handler: _ReleaseService_RollbackRelease_Handler, + }, }, Streams: []grpc.StreamDesc{ { @@ -737,62 +807,64 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ func init() { proto.RegisterFile("hapi/services/tiller.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 899 bytes of a gzipped FileDescriptorProto + // 944 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdd, 0x6e, 0xe3, 0x44, - 0x14, 0xae, 0xf3, 0xe3, 0x24, 0xa7, 0x3f, 0x4a, 0x67, 0xd3, 0xd6, 0xb5, 0x00, 0x45, 0x46, 0xb0, - 0x61, 0x81, 0x14, 0xc2, 0x2d, 0x42, 0xea, 0x66, 0xa3, 0xb6, 0xda, 0x92, 0x95, 0x26, 0x14, 0x24, - 0x2e, 0x88, 0xdc, 0x64, 0xb2, 0x35, 0x38, 0x9e, 0x30, 0x33, 0xae, 0xe8, 0x23, 0xf0, 0x0c, 0xdc, - 0xf1, 0x1e, 0x3c, 0x19, 0x37, 0xc8, 0xf3, 0xe3, 0xc6, 0xa9, 0xcd, 0x7a, 0x73, 0x63, 0x7b, 0xe6, - 0x7c, 0xf3, 0x9d, 0x73, 0xbe, 0x99, 0x73, 0xc6, 0xe0, 0xde, 0xf9, 0xab, 0xe0, 0x8c, 0x13, 0x76, - 0x1f, 0xcc, 0x08, 0x3f, 0x13, 0x41, 0x18, 0x12, 0xd6, 0x5f, 0x31, 0x2a, 0x28, 0xea, 0x24, 0xb6, - 0xbe, 0xb1, 0xf5, 0x95, 0xcd, 0x3d, 0x96, 0x2b, 0x66, 0x77, 0x3e, 0x13, 0xea, 0xa9, 0xd0, 0xee, - 0xc9, 0xfa, 0x3c, 0x8d, 0x16, 0xc1, 0x5b, 0x6d, 0x50, 0x2e, 0x18, 0x09, 0x89, 0xcf, 0x89, 0x79, - 0x67, 0x16, 0x19, 0x5b, 0x10, 0x2d, 0xa8, 0x36, 0x9c, 0x66, 0x0c, 0x5c, 0xf8, 0x22, 0xe6, 0x19, - 0xbe, 0x7b, 0xc2, 0x78, 0x40, 0x23, 0xf3, 0x56, 0x36, 0xef, 0xef, 0x0a, 0x3c, 0xbb, 0x0e, 0xb8, - 0xc0, 0x6a, 0x21, 0xc7, 0xe4, 0xf7, 0x98, 0x70, 0x81, 0x3a, 0x50, 0x0f, 0x83, 0x65, 0x20, 0x1c, - 0xab, 0x6b, 0xf5, 0xaa, 0x58, 0x0d, 0xd0, 0x31, 0xd8, 0x74, 0xb1, 0xe0, 0x44, 0x38, 0x95, 0xae, - 0xd5, 0x6b, 0x61, 0x3d, 0x42, 0xdf, 0x41, 0x83, 0x53, 0x26, 0xa6, 0xb7, 0x0f, 0x4e, 0xb5, 0x6b, - 0xf5, 0x0e, 0x06, 0x9f, 0xf4, 0xf3, 0xa4, 0xe8, 0x27, 0x9e, 0x26, 0x94, 0x89, 0x7e, 0xf2, 0x78, - 0xf9, 0x80, 0x6d, 0x2e, 0xdf, 0x09, 0xef, 0x22, 0x08, 0x05, 0x61, 0x4e, 0x4d, 0xf1, 0xaa, 0x11, - 0xba, 0x00, 0x90, 0xbc, 0x94, 0xcd, 0x09, 0x73, 0xea, 0x92, 0xba, 0x57, 0x82, 0xfa, 0x4d, 0x82, - 0xc7, 0x2d, 0x6e, 0x3e, 0xd1, 0xb7, 0xb0, 0xa7, 0x24, 0x99, 0xce, 0xe8, 0x9c, 0x70, 0xc7, 0xee, - 0x56, 0x7b, 0x07, 0x83, 0x53, 0x45, 0x65, 0x14, 0x9e, 0x28, 0xd1, 0x86, 0x74, 0x4e, 0xf0, 0xae, - 0x82, 0x27, 0xdf, 0xdc, 0xfb, 0x05, 0x9a, 0x86, 0xde, 0x1b, 0x80, 0xad, 0x82, 0x47, 0xbb, 0xd0, - 0xb8, 0x19, 0xbf, 0x1e, 0xbf, 0xf9, 0x69, 0xdc, 0xde, 0x41, 0x4d, 0xa8, 0x8d, 0xcf, 0xbf, 0x1f, - 0xb5, 0x2d, 0x74, 0x08, 0xfb, 0xd7, 0xe7, 0x93, 0x1f, 0xa6, 0x78, 0x74, 0x3d, 0x3a, 0x9f, 0x8c, - 0x5e, 0xb5, 0x2b, 0xde, 0x47, 0xd0, 0x4a, 0xa3, 0x42, 0x0d, 0xa8, 0x9e, 0x4f, 0x86, 0x6a, 0xc9, - 0xab, 0xd1, 0x64, 0xd8, 0xb6, 0xbc, 0x3f, 0x2d, 0xe8, 0x64, 0x37, 0x81, 0xaf, 0x68, 0xc4, 0x49, - 0xb2, 0x0b, 0x33, 0x1a, 0x47, 0xe9, 0x2e, 0xc8, 0x01, 0x42, 0x50, 0x8b, 0xc8, 0x1f, 0x66, 0x0f, - 0xe4, 0x77, 0x82, 0x14, 0x54, 0xf8, 0xa1, 0xd4, 0xbf, 0x8a, 0xd5, 0x00, 0x7d, 0x0d, 0x4d, 0x9d, - 0x1c, 0x77, 0x6a, 0xdd, 0x6a, 0x6f, 0x77, 0x70, 0x94, 0x4d, 0x59, 0x7b, 0xc4, 0x29, 0xcc, 0xbb, - 0x80, 0x93, 0x0b, 0x62, 0x22, 0x51, 0x8a, 0x98, 0x33, 0x91, 0xf8, 0xf5, 0x97, 0x44, 0x06, 0x93, - 0xf8, 0xf5, 0x97, 0x04, 0x39, 0xd0, 0xd0, 0x07, 0x4a, 0x86, 0x53, 0xc7, 0x66, 0xe8, 0x09, 0x70, - 0x9e, 0x12, 0xe9, 0xbc, 0xf2, 0x98, 0x3e, 0x85, 0x5a, 0x72, 0x9c, 0x25, 0xcd, 0xee, 0x00, 0x65, - 0xe3, 0xbc, 0x8a, 0x16, 0x14, 0x4b, 0x3b, 0xfa, 0x00, 0x5a, 0x09, 0x9e, 0xaf, 0xfc, 0x19, 0x91, - 0xd9, 0xb6, 0xf0, 0xe3, 0x84, 0x77, 0xb9, 0xee, 0x75, 0x48, 0x23, 0x41, 0x22, 0xb1, 0x5d, 0xfc, - 0xd7, 0x70, 0x9a, 0xc3, 0xa4, 0x13, 0x38, 0x83, 0x86, 0x0e, 0x4d, 0xb2, 0x15, 0xea, 0x6a, 0x50, - 0xde, 0x3f, 0x16, 0x74, 0x6e, 0x56, 0x73, 0x5f, 0x10, 0x63, 0xfa, 0x9f, 0xa0, 0x9e, 0x43, 0x5d, - 0xb6, 0x05, 0xad, 0xc5, 0xa1, 0xe2, 0x56, 0xbd, 0x63, 0x98, 0x3c, 0xb1, 0xb2, 0xa3, 0x17, 0x60, - 0xdf, 0xfb, 0x61, 0x4c, 0xb8, 0x14, 0x22, 0x55, 0x4d, 0x23, 0x65, 0x4f, 0xc1, 0x1a, 0x81, 0x4e, - 0xa0, 0x31, 0x67, 0x0f, 0x53, 0x16, 0x47, 0xb2, 0xc8, 0x9a, 0xd8, 0x9e, 0xb3, 0x07, 0x1c, 0x47, - 0xe8, 0x63, 0xd8, 0x9f, 0x07, 0xdc, 0xbf, 0x0d, 0xc9, 0xf4, 0x8e, 0xd2, 0xdf, 0xb8, 0xac, 0xb3, - 0x26, 0xde, 0xd3, 0x93, 0x97, 0xc9, 0x9c, 0x77, 0x09, 0x47, 0x1b, 0xe1, 0x6f, 0xab, 0xc4, 0xbf, - 0x16, 0x1c, 0x5d, 0x45, 0x5c, 0xf8, 0x61, 0xb8, 0x21, 0x45, 0x9a, 0xb6, 0x55, 0x3a, 0xed, 0xca, - 0xfb, 0xa4, 0x5d, 0xcd, 0xa4, 0x6d, 0x84, 0xaf, 0xad, 0x09, 0x5f, 0x46, 0x8a, 0xec, 0x01, 0xb4, - 0x37, 0x0e, 0x20, 0xfa, 0x10, 0x80, 0x91, 0x98, 0x93, 0xa9, 0x24, 0x6f, 0xc8, 0xf5, 0x2d, 0x39, - 0x33, 0xf6, 0x97, 0xc4, 0xbb, 0x82, 0xe3, 0xcd, 0xe4, 0xb7, 0x15, 0xf2, 0x0e, 0x4e, 0x6e, 0xa2, - 0x20, 0x57, 0xc9, 0xbc, 0x43, 0xf5, 0x24, 0xb7, 0x4a, 0x4e, 0x6e, 0x1d, 0xa8, 0xaf, 0x62, 0xf6, - 0x96, 0x68, 0xad, 0xd4, 0xc0, 0x7b, 0x0d, 0xce, 0x53, 0x4f, 0xdb, 0x86, 0xfd, 0x0c, 0x0e, 0x2f, - 0x88, 0xf8, 0x51, 0x55, 0x99, 0x0e, 0xd8, 0x1b, 0x01, 0x5a, 0x9f, 0x7c, 0xe4, 0xd6, 0x53, 0x59, - 0x6e, 0x73, 0x85, 0x19, 0xbc, 0x41, 0x0d, 0xfe, 0xb2, 0xe1, 0xc0, 0x74, 0x1c, 0x75, 0x3f, 0xa0, - 0x00, 0xf6, 0xd6, 0x5b, 0x2b, 0xfa, 0xac, 0xf8, 0xfa, 0xd8, 0xb8, 0x03, 0xdd, 0x17, 0x65, 0xa0, - 0x2a, 0x54, 0x6f, 0xe7, 0x2b, 0x0b, 0x71, 0x68, 0x6f, 0x76, 0x3c, 0xf4, 0x65, 0x3e, 0x47, 0x41, - 0x8b, 0x75, 0xfb, 0x65, 0xe1, 0xc6, 0x2d, 0xba, 0x97, 0x72, 0x66, 0xdb, 0x14, 0x7a, 0x27, 0x4d, - 0xb6, 0x33, 0xba, 0x67, 0xa5, 0xf1, 0xa9, 0xdf, 0x5f, 0x61, 0x3f, 0xd3, 0x10, 0x50, 0x81, 0x5a, - 0x79, 0x4d, 0xcf, 0xfd, 0xbc, 0x14, 0x36, 0xf5, 0xb5, 0x84, 0x83, 0x6c, 0xd1, 0xa0, 0x02, 0x82, - 0xdc, 0xbe, 0xe2, 0x7e, 0x51, 0x0e, 0x9c, 0xba, 0xe3, 0xd0, 0xde, 0x3c, 0xee, 0x45, 0xfb, 0x58, - 0x50, 0x80, 0x45, 0xfb, 0x58, 0x54, 0x45, 0xde, 0x0e, 0xf2, 0x01, 0x1e, 0x2b, 0x00, 0x3d, 0x2f, - 0xdc, 0x90, 0x6c, 0xe1, 0xb8, 0xbd, 0x77, 0x03, 0x8d, 0x8b, 0x97, 0xf0, 0x73, 0xd3, 0xe0, 0x6e, - 0x6d, 0xf9, 0xfb, 0xf7, 0xcd, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x0d, 0xc6, 0x82, 0x07, 0xcf, - 0x0a, 0x00, 0x00, + 0x14, 0xae, 0xf3, 0x9f, 0xd3, 0x1f, 0xd2, 0xb3, 0x69, 0xe3, 0x5a, 0x80, 0x22, 0x23, 0xd8, 0xb0, + 0xb0, 0x29, 0x84, 0x5b, 0x84, 0xd4, 0xcd, 0x46, 0x6d, 0xd9, 0x92, 0x95, 0x26, 0x14, 0x24, 0x2e, + 0x88, 0xdc, 0x64, 0xb2, 0xf5, 0xae, 0xe3, 0x09, 0x9e, 0x49, 0x45, 0x1f, 0x81, 0x0b, 0x5e, 0x82, + 0xf7, 0xe0, 0xc9, 0xb8, 0x41, 0x9e, 0xf1, 0xb8, 0x71, 0x62, 0x6f, 0xbd, 0xb9, 0x89, 0x3d, 0x73, + 0x3e, 0x7f, 0xe7, 0x9b, 0xef, 0xcc, 0x9c, 0x09, 0x58, 0xb7, 0xce, 0xc2, 0x3d, 0xe5, 0x34, 0xb8, + 0x73, 0x27, 0x94, 0x9f, 0x0a, 0xd7, 0xf3, 0x68, 0xd0, 0x5d, 0x04, 0x4c, 0x30, 0x6c, 0x86, 0xb1, + 0xae, 0x8e, 0x75, 0x55, 0xcc, 0x3a, 0x96, 0x5f, 0x4c, 0x6e, 0x9d, 0x40, 0xa8, 0x5f, 0x85, 0xb6, + 0x5a, 0xab, 0xf3, 0xcc, 0x9f, 0xb9, 0x6f, 0xa2, 0x80, 0x4a, 0x11, 0x50, 0x8f, 0x3a, 0x9c, 0xea, + 0x67, 0xe2, 0x23, 0x1d, 0x73, 0xfd, 0x19, 0x8b, 0x02, 0x27, 0x89, 0x00, 0x17, 0x8e, 0x58, 0xf2, + 0x04, 0xdf, 0x1d, 0x0d, 0xb8, 0xcb, 0x7c, 0xfd, 0x54, 0x31, 0xfb, 0x9f, 0x02, 0x3c, 0xb9, 0x72, + 0xb9, 0x20, 0xea, 0x43, 0x4e, 0xe8, 0x1f, 0x4b, 0xca, 0x05, 0x36, 0xa1, 0xec, 0xb9, 0x73, 0x57, + 0x98, 0x46, 0xdb, 0xe8, 0x14, 0x89, 0x1a, 0xe0, 0x31, 0x54, 0xd8, 0x6c, 0xc6, 0xa9, 0x30, 0x0b, + 0x6d, 0xa3, 0x53, 0x27, 0xd1, 0x08, 0x7f, 0x80, 0x2a, 0x67, 0x81, 0x18, 0xdf, 0xdc, 0x9b, 0xc5, + 0xb6, 0xd1, 0x39, 0xe8, 0x7d, 0xde, 0x4d, 0xb3, 0xa2, 0x1b, 0x66, 0x1a, 0xb1, 0x40, 0x74, 0xc3, + 0x9f, 0x17, 0xf7, 0xa4, 0xc2, 0xe5, 0x33, 0xe4, 0x9d, 0xb9, 0x9e, 0xa0, 0x81, 0x59, 0x52, 0xbc, + 0x6a, 0x84, 0xe7, 0x00, 0x92, 0x97, 0x05, 0x53, 0x1a, 0x98, 0x65, 0x49, 0xdd, 0xc9, 0x41, 0xfd, + 0x3a, 0xc4, 0x93, 0x3a, 0xd7, 0xaf, 0xf8, 0x3d, 0xec, 0x29, 0x4b, 0xc6, 0x13, 0x36, 0xa5, 0xdc, + 0xac, 0xb4, 0x8b, 0x9d, 0x83, 0xde, 0x89, 0xa2, 0xd2, 0x0e, 0x8f, 0x94, 0x69, 0x7d, 0x36, 0xa5, + 0x64, 0x57, 0xc1, 0xc3, 0x77, 0x6e, 0xff, 0x0e, 0x35, 0x4d, 0x6f, 0xf7, 0xa0, 0xa2, 0xc4, 0xe3, + 0x2e, 0x54, 0xaf, 0x87, 0xaf, 0x86, 0xaf, 0x7f, 0x1d, 0x36, 0x76, 0xb0, 0x06, 0xa5, 0xe1, 0xd9, + 0x4f, 0x83, 0x86, 0x81, 0x87, 0xb0, 0x7f, 0x75, 0x36, 0xfa, 0x79, 0x4c, 0x06, 0x57, 0x83, 0xb3, + 0xd1, 0xe0, 0x65, 0xa3, 0x60, 0x7f, 0x0a, 0xf5, 0x58, 0x15, 0x56, 0xa1, 0x78, 0x36, 0xea, 0xab, + 0x4f, 0x5e, 0x0e, 0x46, 0xfd, 0x86, 0x61, 0xff, 0x65, 0x40, 0x33, 0x59, 0x04, 0xbe, 0x60, 0x3e, + 0xa7, 0x61, 0x15, 0x26, 0x6c, 0xe9, 0xc7, 0x55, 0x90, 0x03, 0x44, 0x28, 0xf9, 0xf4, 0x4f, 0x5d, + 0x03, 0xf9, 0x1e, 0x22, 0x05, 0x13, 0x8e, 0x27, 0xfd, 0x2f, 0x12, 0x35, 0xc0, 0x6f, 0xa1, 0x16, + 0x2d, 0x8e, 0x9b, 0xa5, 0x76, 0xb1, 0xb3, 0xdb, 0x3b, 0x4a, 0x2e, 0x39, 0xca, 0x48, 0x62, 0x98, + 0x7d, 0x0e, 0xad, 0x73, 0xaa, 0x95, 0x28, 0x47, 0xf4, 0x9e, 0x08, 0xf3, 0x3a, 0x73, 0x2a, 0xc5, + 0x84, 0x79, 0x9d, 0x39, 0x45, 0x13, 0xaa, 0xd1, 0x86, 0x92, 0x72, 0xca, 0x44, 0x0f, 0x6d, 0x01, + 0xe6, 0x26, 0x51, 0xb4, 0xae, 0x34, 0xa6, 0x2f, 0xa0, 0x14, 0x6e, 0x67, 0x49, 0xb3, 0xdb, 0xc3, + 0xa4, 0xce, 0x4b, 0x7f, 0xc6, 0x88, 0x8c, 0xe3, 0xc7, 0x50, 0x0f, 0xf1, 0x7c, 0xe1, 0x4c, 0xa8, + 0x5c, 0x6d, 0x9d, 0x3c, 0x4c, 0xd8, 0x17, 0xab, 0x59, 0xfb, 0xcc, 0x17, 0xd4, 0x17, 0xdb, 0xe9, + 0xbf, 0x82, 0x93, 0x14, 0xa6, 0x68, 0x01, 0xa7, 0x50, 0x8d, 0xa4, 0x49, 0xb6, 0x4c, 0x5f, 0x35, + 0xca, 0xfe, 0xd7, 0x80, 0xe6, 0xf5, 0x62, 0xea, 0x08, 0xaa, 0x43, 0xef, 0x11, 0xf5, 0x14, 0xca, + 0xb2, 0x2d, 0x44, 0x5e, 0x1c, 0x2a, 0x6e, 0xd5, 0x3b, 0xfa, 0xe1, 0x2f, 0x51, 0x71, 0x7c, 0x06, + 0x95, 0x3b, 0xc7, 0x5b, 0x52, 0x2e, 0x8d, 0x88, 0x5d, 0x8b, 0x90, 0xb2, 0xa7, 0x90, 0x08, 0x81, + 0x2d, 0xa8, 0x4e, 0x83, 0xfb, 0x71, 0xb0, 0xf4, 0xe5, 0x21, 0xab, 0x91, 0xca, 0x34, 0xb8, 0x27, + 0x4b, 0x1f, 0x3f, 0x83, 0xfd, 0xa9, 0xcb, 0x9d, 0x1b, 0x8f, 0x8e, 0x6f, 0x19, 0x7b, 0xc7, 0xe5, + 0x39, 0xab, 0x91, 0xbd, 0x68, 0xf2, 0x22, 0x9c, 0xb3, 0x2f, 0xe0, 0x68, 0x4d, 0xfe, 0xb6, 0x4e, + 0xbc, 0x85, 0x63, 0xc2, 0x3c, 0xef, 0xc6, 0x99, 0xbc, 0xcb, 0x61, 0xc5, 0x8a, 0xea, 0xc2, 0xfb, + 0x55, 0x17, 0x53, 0x54, 0xff, 0x08, 0xad, 0x8d, 0x5c, 0xdb, 0xea, 0xfe, 0xcf, 0x80, 0xa3, 0x4b, + 0x9f, 0x0b, 0xc7, 0xf3, 0xd6, 0x74, 0xc7, 0xe5, 0x32, 0x72, 0x97, 0xab, 0xf0, 0x21, 0xe5, 0x2a, + 0x26, 0x16, 0xae, 0x5d, 0x2a, 0xad, 0xb8, 0x94, 0xa7, 0x84, 0xc9, 0x83, 0x53, 0x59, 0x3b, 0x38, + 0xf8, 0x09, 0x40, 0x40, 0x97, 0x9c, 0x8e, 0x25, 0x79, 0x55, 0x7e, 0x5f, 0x97, 0x33, 0x43, 0x67, + 0x4e, 0xed, 0x4b, 0x38, 0x5e, 0x5f, 0xfc, 0xb6, 0x46, 0xde, 0x42, 0xeb, 0xda, 0x77, 0x53, 0x9d, + 0x4c, 0xdb, 0x01, 0x1b, 0x6b, 0x2b, 0xa4, 0xac, 0xad, 0x09, 0xe5, 0xc5, 0x32, 0x78, 0x43, 0x23, + 0xaf, 0xd4, 0xc0, 0x7e, 0x05, 0xe6, 0x66, 0xa6, 0x6d, 0x65, 0x3f, 0x81, 0xc3, 0x73, 0x2a, 0x7e, + 0x51, 0xdd, 0x21, 0x12, 0x6c, 0x0f, 0x00, 0x57, 0x27, 0x1f, 0xb8, 0xa3, 0xa9, 0x24, 0xb7, 0xbe, + 0x7a, 0x35, 0x5e, 0xa3, 0x7a, 0x7f, 0x57, 0xe1, 0x40, 0x77, 0x4a, 0x75, 0xaf, 0xa1, 0x0b, 0x7b, + 0xab, 0x57, 0x02, 0x7e, 0x99, 0x7d, 0xed, 0xad, 0xdd, 0xdd, 0xd6, 0xb3, 0x3c, 0x50, 0x25, 0xd5, + 0xde, 0xf9, 0xc6, 0x40, 0x0e, 0x8d, 0xf5, 0x4e, 0x8d, 0xcf, 0xd3, 0x39, 0x32, 0xae, 0x06, 0xab, + 0x9b, 0x17, 0xae, 0xd3, 0xe2, 0x9d, 0xb4, 0x33, 0xd9, 0x5e, 0xf1, 0x51, 0x9a, 0x64, 0x47, 0xb7, + 0x4e, 0x73, 0xe3, 0xe3, 0xbc, 0x6f, 0x61, 0x3f, 0xd1, 0xc8, 0x30, 0xc3, 0xad, 0xb4, 0x66, 0x6d, + 0x7d, 0x95, 0x0b, 0x1b, 0xe7, 0x9a, 0xc3, 0x41, 0xf2, 0xd0, 0x60, 0x06, 0x41, 0x6a, 0x5f, 0xb1, + 0xbe, 0xce, 0x07, 0x8e, 0xd3, 0x71, 0x68, 0xac, 0x6f, 0xf7, 0xac, 0x3a, 0x66, 0x1c, 0xc0, 0xac, + 0x3a, 0x66, 0x9d, 0x22, 0x7b, 0x07, 0x1d, 0x80, 0x87, 0x13, 0x80, 0x4f, 0x33, 0x0b, 0x92, 0x3c, + 0x38, 0x56, 0xe7, 0x71, 0x60, 0x9c, 0x62, 0x01, 0x1f, 0xad, 0x75, 0x71, 0xcc, 0xb0, 0x26, 0xfd, + 0x62, 0xb1, 0x9e, 0xe7, 0x44, 0xeb, 0x8c, 0x2f, 0xe0, 0xb7, 0x9a, 0x06, 0xdf, 0x54, 0xe4, 0x1f, + 0xe5, 0xef, 0xfe, 0x0f, 0x00, 0x00, 0xff, 0xff, 0x0b, 0x37, 0x7c, 0x73, 0xf9, 0x0b, 0x00, 0x00, } From 65d0c03c0f06f3334b2a48dd5ab9d439477d0fd5 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 27 Sep 2016 12:37:15 -0600 Subject: [PATCH 114/183] fix(tiller): Order the manifests before sending to k8s This orders manifests both for installs and deletes so that the manifests are ordered by kind. Closes #1228 --- cmd/tiller/hooks.go | 15 +++++-- cmd/tiller/hooks_test.go | 2 +- cmd/tiller/kind_sorter.go | 75 ++++++++++++++++++++++++++++++++++ cmd/tiller/kind_sorter_test.go | 72 ++++++++++++++++++++++++++++++++ cmd/tiller/release_server.go | 45 ++++++++++++++++---- 5 files changed, 197 insertions(+), 12 deletions(-) create mode 100644 cmd/tiller/kind_sorter.go create mode 100644 cmd/tiller/kind_sorter_test.go diff --git a/cmd/tiller/hooks.go b/cmd/tiller/hooks.go index c0fd5be3c..b291dff6b 100644 --- a/cmd/tiller/hooks.go +++ b/cmd/tiller/hooks.go @@ -71,6 +71,13 @@ func (v versionSet) Has(apiVersion string) bool { return ok } +// manifest represents a manifest file, which has a name and some content. +type manifest struct { + name string + content string + head *simpleHead +} + // sortManifests takes a map of filename/YAML contents and sorts them into hook types. // // The resulting hooks struct will be populated with all of the generated hooks. @@ -92,9 +99,9 @@ func (v versionSet) Has(apiVersion string) bool { // // Files that do not parse into the expected format are simply placed into a map and // returned. -func sortManifests(files map[string]string, apis versionSet) ([]*release.Hook, map[string]string, error) { +func sortManifests(files map[string]string, apis versionSet, sort SortOrder) ([]*release.Hook, []manifest, error) { hs := []*release.Hook{} - generic := map[string]string{} + generic := []manifest{} for n, c := range files { // Skip partials. We could return these as a separate map, but there doesn't @@ -121,13 +128,13 @@ func sortManifests(files map[string]string, apis versionSet) ([]*release.Hook, m } if sh.Metadata == nil || sh.Metadata.Annotations == nil || len(sh.Metadata.Annotations) == 0 { - generic[n] = c + generic = append(generic, manifest{name: n, content: c, head: &sh}) continue } hookTypes, ok := sh.Metadata.Annotations[hookAnno] if !ok { - generic[n] = c + generic = append(generic, manifest{name: n, content: c, head: &sh}) continue } h := &release.Hook{ diff --git a/cmd/tiller/hooks_test.go b/cmd/tiller/hooks_test.go index 14287a7e6..b43df6391 100644 --- a/cmd/tiller/hooks_test.go +++ b/cmd/tiller/hooks_test.go @@ -116,7 +116,7 @@ metadata: manifests[o.path] = o.manifest } - hs, generic, err := sortManifests(manifests, newVersionSet("v1", "v1beta1")) + hs, generic, err := sortManifests(manifests, newVersionSet("v1", "v1beta1"), InstallOrder) if err != nil { t.Fatalf("Unexpected error: %s", err) } diff --git a/cmd/tiller/kind_sorter.go b/cmd/tiller/kind_sorter.go new file mode 100644 index 000000000..3ee4797fa --- /dev/null +++ b/cmd/tiller/kind_sorter.go @@ -0,0 +1,75 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "sort" +) + +// 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", "ServiceAccount", "Service", "Pod", "ReplicationController", "Deployment", "DaemonSet", "Ingress", "Job"} + +// UninstallOrder is the order in which manifests should be uninstalled (by Kind) +var UninstallOrder SortOrder = []string{"Service", "Pod", "ReplicationController", "Deployment", "DaemonSet", "ConfigMap", "Secret", "PersistentVolume", "ServiceAccount", "Ingress", "Job", "Namespace"} + +// sortByKind does an in-place sort of manifests by Kind. +// +// Results are sorted by 'ordering' +func sortByKind(manifests []manifest, ordering SortOrder) []manifest { + ks := newKindSorter(manifests, ordering) + sort.Sort(ks) + return ks.manifests +} + +type kindSorter struct { + ordering map[string]int + manifests []manifest +} + +func newKindSorter(m []manifest, s SortOrder) *kindSorter { + o := make(map[string]int, len(s)) + for v, k := range s { + o[k] = v + } + + return &kindSorter{ + manifests: m, + ordering: o, + } +} + +func (k *kindSorter) Len() int { return len(k.manifests) } + +func (k *kindSorter) Swap(i, j int) { k.manifests[i], k.manifests[j] = k.manifests[j], k.manifests[i] } + +func (k *kindSorter) Less(i, j int) bool { + a := k.manifests[i] + b := k.manifests[j] + first, ok := k.ordering[a.head.Kind] + if !ok { + // Unknown is always last + return false + } + second, ok := k.ordering[b.head.Kind] + if !ok { + return true + } + return first < second +} diff --git a/cmd/tiller/kind_sorter_test.go b/cmd/tiller/kind_sorter_test.go new file mode 100644 index 000000000..46de376dd --- /dev/null +++ b/cmd/tiller/kind_sorter_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "testing" +) + +func TestKindSorter(t *testing.T) { + manifests := []manifest{ + { + name: "m", + content: "", + head: &simpleHead{Kind: "Deployment"}, + }, + { + name: "l", + content: "", + head: &simpleHead{Kind: "Service"}, + }, + { + name: "!", + content: "", + head: &simpleHead{Kind: "HonkyTonkSet"}, + }, + { + name: "h", + content: "", + head: &simpleHead{Kind: "Namespace"}, + }, + { + name: "e", + content: "", + head: &simpleHead{Kind: "ConfigMap"}, + }, + } + + res := sortByKind(manifests, InstallOrder) + got := "" + expect := "helm!" + for _, r := range res { + got += r.name + } + if got != expect { + t.Errorf("Expected %q, got %q", expect, got) + } + + expect = "lmeh!" + got = "" + res = sortByKind(manifests, UninstallOrder) + for _, r := range res { + got += r.name + } + if got != expect { + t.Errorf("Expected %q, got %q", expect, got) + } + +} diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 8820afbda..c161abb79 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -548,7 +548,7 @@ func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values if err != nil { return nil, nil, "", fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err) } - hooks, manifests, err := sortManifests(files, vs) + hooks, manifests, err := sortManifests(files, vs, InstallOrder) if err != nil { // By catching parse errors here, we can prevent bogus releases from going // to Kubernetes. @@ -557,9 +557,9 @@ func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values // Aggregate all valid manifests into one big doc. b := bytes.NewBuffer(nil) - for name, file := range manifests { - b.WriteString("\n---\n# Source: " + name + "\n") - b.WriteString(file) + for _, m := range manifests { + b.WriteString("\n---\n# Source: " + m.name + "\n") + b.WriteString(m.content) } return hooks, b, notes, nil @@ -707,11 +707,27 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR } } - b := bytes.NewBuffer([]byte(rel.Manifest)) - if err := s.env.KubeClient.Delete(rel.Namespace, b); err != nil { - log.Printf("uninstall: Failed deletion of %q: %s", req.Name, err) + vs, err := s.getVersionSet() + if err != nil { + return nil, fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err) + } + + manifests := splitManifests(rel.Manifest) + _, files, err := sortManifests(manifests, vs, UninstallOrder) + if err != nil { + // We could instead just delete everything in no particular order. return nil, err } + // Note: We could re-join these into one file and delete just that one. Or + // we could collect errors (instead of bailing on the first error) and try + // to delete as much as possible instead of failing at the first error. + for _, file := range files { + b := bytes.NewBufferString(file.content) + if err := s.env.KubeClient.Delete(rel.Namespace, b); err != nil { + log.Printf("uninstall: Failed deletion of %q: %s", req.Name, err) + return nil, err + } + } if !req.DisableHooks { if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, postDelete); err != nil { @@ -754,3 +770,18 @@ func (r byDate) Swap(p, q int) { func (r byDate) Less(p, q int) bool { return r[p].Info.LastDeployed.Seconds < r[q].Info.LastDeployed.Seconds } + +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 { + res[fmt.Sprintf(tpl, i)] = d + } + return res +} From 6f7de6d34d58fbfd61886893acdae1a5e3c34e0d Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 27 Sep 2016 17:46:10 -0600 Subject: [PATCH 115/183] docs(glossary): added glossary Added glossary to the index on README as well. Closes #1213 --- README.md | 1 + docs/glossary.md | 170 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 docs/glossary.md diff --git a/README.md b/README.md index a742816d2..be7be995c 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Download a [release tarball of helm for your platform](https://github.com/kubern - [Syncing your Chart Repository](docs/chart_repository_sync_example.md) - [Developers](docs/developers.md) - [History](docs/history.md) +- [Glossary](docs/glossary.md) ## Community, discussion, contribution, and support diff --git a/docs/glossary.md b/docs/glossary.md new file mode 100644 index 000000000..641dfdd59 --- /dev/null +++ b/docs/glossary.md @@ -0,0 +1,170 @@ +# Helm Glossary + +Helm uses a few special terms to describe components of the +architecture. + +## Chart + +A Helm package that contains information sufficient for installing a set +of Kubernetes resources into a Kubernetes cluster. + +Charts contain a `Chart.yaml` file as well as templates, default values +(`values.yaml`), and dependencies. + +Charts are developed in a well-defined directory structure, and then +packaged into an archive format called a _chart archive_. + +## Chart Archive + +A _chart archive_ is a tarred and gzipped (and optionally signed) chart. + +## Chart Dependency (Subcharts) + +Charts may depend upon other charts. There are two ways a dependency may +occur: + +- Soft dependency: A chart may simply not function without another chart + being installed in a cluster. Helm does not provide tooling for this + case. In this case, dependencies may be managed separately. +- Hard dependency: A chart may contain (inside of its `charts/` + directory) another chart upon which it depends. In this case, + installing the chart will install all of its dependencies. In this + case, a chart and its dependencies are managed as a collection. + +When a chart is packaged (via `helm package`) all of its hard dependencies +are bundled with it. + +## Chart Version + +Charts are versioned according to the [SemVer 2 +spec](http://semver.org). A version number is required on every chart. + +## Chart.yaml + +Information about a chart is stored in a special file called +`Chart.yaml`. Every chart must have this file. + +## Helm (and helm) + +Helm is the package manager for Kubernetes. As an operating system +package manager makes it easy to install tools on an OS, Helm makes it +easy to install applications and resources into Kubernetes clusters. + +While _Helm_ is the name of the project, the command line client is also +named `helm`. By convention, when speaking of the project, _Helm_ is +capitalized. When speaking of the client, _helm_ is in lowercase. + +## Helm Home (HELM_HOME) + +The Helm client stores information in a local directory referred to as +_helm home_. By default, this is in the `$HOME/.helm` directory. + +This directory contains configuration and cache data, and is created by +`helm init`. + +## Kube Config (KUBECONFIG) + +The Helm client learns about Kubernetes clusters by using files in the _Kube +config_ file format. By default, Helm attempts to find this file in the +place where `kubectl` creates it (`$HOME/.kube/config`). + +## Lint (Linting) + +To _lint_ a chart is to validate that it follows the conventions and +requirements of the Helm chart standard. Helm provides tools to do this, +notably the `helm lint` command. + +## Provenance (Provenance file) + +Helm charts may be accompanied by a _provenance file_ which provides +information about where the chart came from and what it contains. + +Provenance files are one part of the Helm security story. A provenance contains +a cryptographic hash of the chart archive file, the Chart.yaml data, and +a signature block (an OpenPGP "clearsign" block). When coupled with a +keychain, this provides chart users with the ability to: + +- Validate that a chart was signed by a trusted party +- Validate that the chart file has not been tampered with +- Validate the contents of a chart metadata (`Chart.yaml`) +- Quickly match a chart to its provenance data + +Provenance files have the `.prov` extension, and can be served from a +chart repository server or any other HTTP server. + +## Release + +When a chart is installed, Tiller (the Helm server) creates a _release_ +to track that installation. + +A single chart may be installed many times into the same cluster, and +create many different releases. For example, one can install three +PostgreSQL databases by running `helm install` three times with a +different release name. + +(Prior to 2.0.0-Alpha.1, releases were called _deployments_. But this +caused confusion with the Kubernetes _Deployment_ kind.) + +## Release Number (Release Version) + +A single release can be updated multiple times. A sequential counter is +used to track releases as they change. After a first `helm install`, a +release will have _release number_ 1. Each time a release is upgraded or +rolled back, the release number will be incremented. + +## Rollback + +A release can be upgraded to a newer chart or configuration. But since +release history is stored, a release can also be _rolled back_ to a +previous release number. This is done with the `helm rollback` command. + +Importantly, a rolled back release will recieve a new release number. + +Operation | Release Number +----------|--------------- +install | release 1 +upgrade | release 2 +upgrade | release 3 +rollback 1| release 4 (but running the same config as release 1) + +The above table illustrates how release numbers increment across +install, upgrade, and rollback. + +## Tiller + +Tiller is the in-cluster component of Helm. It interacts directly with +the Kubernetes API server to install, upgrade, query, and remove +Kubernetes resources. It also stores the objects that represent +releases. + +## Repository (Repo, Chart Repository) + +Helm charts may be stored on dedicated HTTP servers called _chart +repositories_ (_repositories_, or just _repos_). + +A chart repository server is a simple HTTP server that can serve an +`index.yaml` file that describes a batch of charts, and provides +information on where each chart can be downloaded from. (Many chart +repositories serve the charts as well as the `index.yaml` file.) + +A Helm client can point to zero or more chart repositories. By default, +Helm clients point the the `stable` official Kubernetes chart +repository. + +## Values (Values Files, values.yaml) + +Values provide a way to override template defaults with your own +information. + +Helm Charts are "parameterized", which means the chart developer may +expose configuration that can be overridden at installation time. For +example, a chart may expose a `username` field that allows setting a +user name for a service. + +These exposed variables are called _values_ in Helm parlance. + +Values can be set during `helm install` and `helm upgrade` operations, +either by passing them in directly, or by uploading a `values.yaml` +file. + + From 3f1101cdfb62e61a6170f1073271f294e72aa54c Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 27 Sep 2016 16:58:56 -0700 Subject: [PATCH 116/183] fix(tiller): do not create releases for a dry-run --- cmd/tiller/release_server.go | 23 +++++++++++++---------- cmd/tiller/release_server_test.go | 4 ++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index ed674aaab..b9f2bb882 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -174,8 +174,7 @@ func (s *releaseServer) ListReleases(req *services.ListReleasesRequest, stream s Total: total, Releases: rels, } - stream.Send(res) - return nil + return stream.Send(res) } func filterReleases(filter string, rels []*release.Release) ([]*release.Release, error) { @@ -281,8 +280,10 @@ func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateRelease return nil, err } - if err := s.env.Releases.Create(updatedRelease); err != nil { - return nil, err + if !req.DryRun { + if err := s.env.Releases.Create(updatedRelease); err != nil { + return nil, err + } } return res, nil @@ -380,6 +381,9 @@ func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*rele } func (s *releaseServer) RollbackRelease(c ctx.Context, req *services.RollbackReleaseRequest) (*services.RollbackReleaseResponse, error) { + if !checkClientVersion(c) { + return nil, errIncompatibleVersion + } currentRelease, targetRelease, err := s.prepareRollback(req) if err != nil { @@ -391,8 +395,10 @@ func (s *releaseServer) RollbackRelease(c ctx.Context, req *services.RollbackRel return nil, err } - if err := s.env.Releases.Create(targetRelease); err != nil { - return nil, err + if !req.DryRun { + if err := s.env.Releases.Create(targetRelease); err != nil { + return nil, err + } } return rel, nil @@ -438,10 +444,7 @@ func (s *releaseServer) performKubeUpdate(currentRelease, targetRelease *release kubeCli := s.env.KubeClient current := bytes.NewBufferString(currentRelease.Manifest) target := bytes.NewBufferString(targetRelease.Manifest) - if err := kubeCli.Update(targetRelease.Namespace, current, target); err != nil { - return err - } - return nil + return kubeCli.Update(targetRelease.Namespace, current, target) } // prepareRollback finds the previous release and prepares a new release object with diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index d9d988447..5f63c9abe 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -629,7 +629,7 @@ func TestUpdateReleaseNoChanges(t *testing.T) { } func TestRollbackReleaseNoHooks(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rel.Hooks = []*release.Hook{ @@ -665,7 +665,7 @@ func TestRollbackReleaseNoHooks(t *testing.T) { } func TestRollbackRelease(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rs.env.Releases.Create(rel) From 6da5bc5ceef5e3925bb49b15d2cf9341bba26550 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 28 Sep 2016 10:32:35 -0600 Subject: [PATCH 117/183] docs(charts): fix charts docs to reflect current template practices Closes #1241 --- docs/charts.md | 58 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/docs/charts.md b/docs/charts.md index 7b5e1ee67..c882ae81f 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -244,17 +244,18 @@ spec: serviceAccount: deis-database containers: - name: deis-database - image: {{.imageRegistry}}/postgres:{{.dockerTag}} - imagePullPolicy: {{.pullPolicy}} + image: {{.Values.imageRegistry}}/postgres:{{.Values.dockerTag}} + imagePullPolicy: {{.Values.pullPolicy}} ports: - containerPort: 5432 env: - name: DATABASE_STORAGE - value: {{default "minio" .storage}} + value: {{default "minio" .Values.storage}} ``` The above example, based loosely on [https://github.com/deis/charts](https://github.com/deis/charts), is a template for a Kubernetes replication controller. -It can use the following four template values: +It can use the following four template values (usually defined in a +`.values.yaml` file): - `imageRegistry`: The source registry for the Docker image. - `dockerTag`: The tag for the docker image. @@ -266,6 +267,10 @@ require or dictate parameters. ### Predefined Values +Values that are supplied via a `values.yaml` file (or via the `--set` +flag) are accessible from the `.Values` object in a template. But there +are other pre-defined pieces of data you can access in your templates. + The following values are pre-defined, are available to every template, and cannot be overridden. As with all values, the names are _case sensitive_. @@ -333,6 +338,39 @@ Note that only the last field was overridden. `values.yaml`. But files specified on the command line can be named anything. +Any of these values are then accessible inside of templates using the +`.Values` object: + +```yaml +apiVersion: v1 +kind: ReplicationController +metadata: + name: deis-database + namespace: deis + labels: + heritage: deis +spec: + replicas: 1 + selector: + app: deis-database + template: + metadata: + labels: + app: deis-database + spec: + serviceAccount: deis-database + containers: + - name: deis-database + image: {{.Values.imageRegistry}}/postgres:{{.Values.dockerTag}} + imagePullPolicy: {{.Values.pullPolicy}} + ports: + - containerPort: 5432 + env: + - name: DATABASE_STORAGE + value: {{default "minio" .Values.storage}} + +``` + ### Scope, Dependencies, and Values Values files can declare values for the top-level chart, as well as for @@ -355,16 +393,16 @@ apache: ``` Charts at a higher level have access to all of the variables defined -beneath. So the wordpress chart can access `.mysql.password`. But lower -level charts cannot access things in parent charts, so MySQL will not be -able to access the `title` property. Nor, for that matter, can it access -`.apache.port`. +beneath. So the wordpress chart can access the MySQL password as +`.Values.mysql.password`. But lower level charts cannot access things in +parent charts, so MySQL will not be able to access the `title` property. Nor, +for that matter, can it access `apache.port`. Values are namespaced, but namespaces are pruned. So for the Wordpress -chart, it can access the MySQL password field as `.mysql.password`. But +chart, it can access the MySQL password field as `.Values.mysql.password`. But for the MySQL chart, the scope of the values has been reduced and the namespace prefix removed, so it will see the password field simply as -`.password`. +`.Values.password`. #### Global Values From 8c0f905e9229efda88748053f13d946efdc1ee20 Mon Sep 17 00:00:00 2001 From: Rimantas Mocevicius Date: Wed, 28 Sep 2016 11:00:28 -0600 Subject: [PATCH 118/183] docs(chart_repository): added github pages example --- docs/chart_repository.md | 34 ++++++++++++++++++++++-- docs/images/create-a-gh-page-button.png | Bin 0 -> 30225 bytes docs/images/set-a-gh-page.png | Bin 0 -> 130550 bytes 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 docs/images/create-a-gh-page-button.png create mode 100644 docs/images/set-a-gh-page.png diff --git a/docs/chart_repository.md b/docs/chart_repository.md index 9e1748853..88e6e4bb8 100644 --- a/docs/chart_repository.md +++ b/docs/chart_repository.md @@ -7,7 +7,7 @@ ## Create a chart repository A _chart repository_ is an HTTP server that houses one or more packaged charts. When you're ready to share your charts, the preferred mechanism is a chart repository. You can contribute to the official helm chart repository or create your own. Here we'll talk about creating your own chart repository. -Because a chart repository can be any HTTP server that can serve YAML and tar files and can answer GET requests, you have a plethora of options when it comes down to hosting your own chart repository. For example, you can use a Google Cloud Storage(GCS) bucket, Amazon S3 bucket, or even create your own web server. +Because a chart repository can be any HTTP server that can serve YAML and tar files and can answer GET requests, you have a plethora of options when it comes down to hosting your own chart repository. For example, you can use a Google Cloud Storage (GCS) bucket, Amazon S3 bucket, Github Pages, or even create your own web server. ### The chart repository structure A chart repository consists of packaged charts and a special file called `index.yaml` which contains an index of all of the charts in the repository. A chart repository has a flat structure. Given a repository URL, you should be able to download a chart via a GET request to `URL/chartname-version.tgz`. @@ -41,7 +41,9 @@ redis-2.0.0: home: https://github.com/example-charts/redis ``` -We will go through a detailed GCS example here, but feel free to skip to the next section if you've already created a chart repository. +We will go through a detailed GCS and Github Pages examples here, but feel free to skip to the next section if you've already created a chart repository. + +##### GCS bucket example The first step will be to **create your GCS bucket**. We'll call ours `fantastic-charts`. @@ -57,6 +59,34 @@ Insert this line item to **make your bucket public**: Congratulations, now you have an empty GCS bucket ready to serve charts! +##### Github Pages example + +In a similar way you can create charts repository using Github Pages. + +The first step will be to **create your gh-pages branch**. +You can do that localy as. + +``` +$ git checkout -b [name_of_your_new_branch] +``` + +Or via web browser using **Branch** button on your Github repository: + +![Create Github Pages branch](images/create-a-gh-page-button.png) + +Next, you'll want to make sure your **gh-pages branch** is set as Github Pages, click on your repo **Settings** and scroll down to **Github pages** section and set as per below: + +![Create Github Pages branch](images/set-a-gh-page.png) + +By default **Source** usually gets set to **gh-pages branch**, if not do so select it. + +You can use a **custom domain** there if you wish so. + +And check that **Enforce HTTPS** is ticked, so the **HTTPS** will be used when charts are served. + +In such setup you can use **master branch** to store your charts code, and **gh-pages branch** as charts repository, e.g.: `https://my-repo-name.github.io/charts`. + + ## Store charts in your chart repository Now that you have a chart repository, let's upload a chart and an index file to the repository. Charts in a chart repository must be packaged (`helm package chart-name/`) and versioned correctly (following [SemVer 2](https://semver.org/) guidelines). diff --git a/docs/images/create-a-gh-page-button.png b/docs/images/create-a-gh-page-button.png new file mode 100644 index 0000000000000000000000000000000000000000..b9d43a7059b37dbb4c5cc2df3fef55cf0d5d2833 GIT binary patch literal 30225 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

iM6KS2Zo4QhTLT@5OQK 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 p|C5}jg6{xk&#douM#y1_&=T?02TlM literal 0 HcmV?d00001 From 7d8b5bbb494a5a9ba636de0e7a9c55f426b1d066 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 28 Sep 2016 13:19:22 -0600 Subject: [PATCH 119/183] docs(install, using): add installation and usage docs --- README.md | 6 +- docs/install.md | 122 ++++++++++++++ docs/using_helm.md | 392 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 518 insertions(+), 2 deletions(-) create mode 100644 docs/install.md create mode 100644 docs/using_helm.md diff --git a/README.md b/README.md index a742816d2..15454f9a2 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,12 @@ Download a [release tarball of helm for your platform](https://github.com/kubern ## Docs - [Quick Start](docs/quickstart.md) -- [Architecture](docs/architecture.md) -- [Charts](docs/charts.md) +- [Installing Helm](docs/install.md) +- [Using Helm](docs/using_helm.md) +- [Developing Charts](docs/charts.md) - [Chart Repository Guide](docs/chart_repository.md) - [Syncing your Chart Repository](docs/chart_repository_sync_example.md) +- [Architecture](docs/architecture.md) - [Developers](docs/developers.md) - [History](docs/history.md) diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 000000000..1a4578a9c --- /dev/null +++ b/docs/install.md @@ -0,0 +1,122 @@ +# Installing Helm + +There are two parts to Helm: The Helm client (`helm`) and the Helm +server (Tiller). This guide shows how to install the client, and then +proceeds to show two ways to install the server. + +## Installing the Helm Client + +The Helm client can be installed either from source, or from pre-built binary +releases. + +### From the GitHub Releases + +Every [release](https://github.com/kubernetes/helm/releases) of Helm +provides binary releases for a variety of OSes. These binary versions +can be manually downloaded and installed. + +1. Download your [desired version](https://github.com/kubernetes/helm/releases) +2. Unpack it (`tar -zxvf helm-v2.0.0-linux-amd64.tgz`) +3. Find the `helm` binary in the unpacked directory, and move it to its + desired destination (`mv linux-amd64/helm /usr/local/bin/helm`) + +From there, you should be able to run the client: `helm help`. + +### From Homebrew (Mac OSX) + +Members of the Kubernetes community have contributed a Helm cask built to +Homebrew. This formula is generally up to date. + +``` +brew cask install helm +``` + +(Note: There is also a formula for emacs-helm, which is a different +project.) + +### From Source (Linux, Mac OSX) + +Building Helm from source is slightly more work, but is the best way to +go if you want to test the latest (pre-release) Helm version. + +You must have a working Go environment. + +```console +$ cd $GOPATH +$ mkdir -p src/k8s.io +$ cd src/k8s.io +$ git clone https://github.com/kubernetes/helm.git +$ cd helm +$ make bootstrap build +``` + +The `bootstrap` target will attempt to install dependencies, rebuild the +`vendor/` tree, and validate configuration. + +The `build` target will compile `helm` and place it in `bin/helm`. +Tiller is also compiled, and is placed in `bin/tiller`. + +## Installing Tiller + +Tiller, the server portion of Helm, typically runs inside of your +Kubernetes cluster. But for development, it can also be run locally, and +configured to talk to a remote Kubernetes cluster. + +### Easy In-Cluster Installation + +The easiest way to install `tiller` into the cluster is simply to run +`helm init`. This will validate that `helm`'s local environment is set +up correctly (and set it up if necessary). Then it will connect to +whatever cluster `kubectl` connects to by default (`kubectl config +view`). Once it connects, it will install `tiller` into the +`kube-system` namespace. + +After `helm init`, you should be able to run `kubectl get po --namespace +kube-system` and see Tiller running. + +Once Tiller is installed, running `helm version` should show you both +the client and server version. (If it shows only the client version, +`helm` cannot yet connect to the server. Use `kubectl` to see if any +`tiller` pods are running.) + +### Running Tiller Locally + +For development, it is sometimes easier to work on Tiller locally, and +configure it to connect to a remote Kubernetes cluster. + +The process of building Tiller is explained above. + +Once `tiller` has been built, simply start it: + +```console +$ bin/tiller +Tiller running on :44134 +``` + +When Tiller is running locally, it will attempt to connect to the +Kubernetes cluster that is configured by `kubectl`. (Run `kubectl config +view` to see which cluster that is.) + +You must tell `helm` to connect to this new local Tiller host instead of +connecting to the one in-cluster. There are two ways to do this. The +first is to specify the `--host` option on the command line. The second +is to set the `$HELM_HOST` environment variable. + +```console +$ export HELM_HOST=localhost:44134 +$ helm version # Should connect to localhost. +Client: &version.Version{SemVer:"v2.0.0-alpha.4", GitCommit:"db...", GitTreeState:"dirty"} +Server: &version.Version{SemVer:"v2.0.0-alpha.4", GitCommit:"a5...", GitTreeState:"dirty"} +``` + +Importantly, even when running locally, Tiller will store release +configuration in ConfigMaps inside of Kubernetes. + +## Conclusion + +In most cases, installation is as simple as getting a pre-built `helm` binary +and running `helm init`. This document covers additional cases for those +who want to do more sophisticated things with Helm. + +Once you have the Helm Client and Tiller successfully installed, you can +move on to using Helm to manage charts. diff --git a/docs/using_helm.md b/docs/using_helm.md new file mode 100644 index 000000000..369d39ee3 --- /dev/null +++ b/docs/using_helm.md @@ -0,0 +1,392 @@ +# Using Helm + +This guide explains the basics of using Helm (and Tiller) to manage +packages on your Kubernetes cluster. It assumes that you have already +[installed](install.md) the Helm client and the Tiller server (typically by `helm +init`). + +If you are simply interested in running a few quick commands, you may +wish to begin with the [Quickstart Guide](quickstart.md). This chapter +covers the particulars of Helm commands, and explains how to use Helm. + +## Three Big Concepts + +A *Chart* is a Helm package. It contains all of the resource definitions +necessary to run an application, tool, or service inside of a Kubernetes +cluster. Think of it like a Homebrew formula or an `apt` or `rpm` +package for Kubernetes. + +A *Repository* is the place where charts can be collected and shared. +It's like `npm` for Kubernetes packages. + +A *Release* is an instance of a chart running in a Kubernetes cluster. +One chart can often be installed many times into the same cluster. And +each time it is installed, a new _release_ is created. Consider a MySQL +chart. If you want two databases running in your cluster, you can +install that chart twice. Each one will have its own _release_, which +will in turn have its own _release name_. + +With these concepts in mind, we can now explain Helm like this: + +Helm installs _charts_ into Kubernetes, creating a new _release_ for +each installation. And to find new charts, you can search Helm chart +_repositories_. + +## 'helm search': Finding Charts + +When you first install Helm, it is preconfigured to talk to the official +Kubernetes charts repository. This repository contains a number of +carefully currated and maintained charts. This chart repository is named +`stable` by default. + +You can see which charts are available by running `helm search`: + +``` +$ helm search +stable/drupal +stable/jenkins +stable/mariadb +... +``` + +With no filter, `helm search` shows you all of the available charts. You +can narrow down your results by searching with a filter: + +``` +$ helm search mysql +stable/mysql +stable/mariadb +``` + +Now you will only see the results that match your filter. Why is +`mariadb` in the list? Because its package description. We can use `helm +inspect chart` to see this: + +``` +$ helm inspect stable/mariadb +Fetched stable/mariadb-0.3.0.tgz to /Users/mattbutcher/Code/Go/src/k8s.io/helm/mariadb-0.3.0.tgz +description: Chart for MariaDB +engine: gotpl +home: https://mariadb.org +keywords: +- mariadb +- mysql +- database +- sql +maintainers: +- email: containers@bitnami.com + name: Bitnami +name: mariadb +sources: +- https://github.com/bitnami/bitnami-docker-mariadb +version: 0.3.0 +``` + +Search is a good way to find available packages. Once you have found a +package you want to install, you can use `helm install` to install it. + +## 'helm install': Installing a Package + +To install a new package, use the `helm install` command. At its +simplest, it takes only one argument: The name of the chart. + +``` +$ helm install stable/mariadb +Fetched stable/mariadb-0.3.0 to /Users/mattbutcher/Code/Go/src/k8s.io/helm/mariadb-0.3.0.tgz +happy-panda +Last Deployed: Wed Sep 28 12:32:28 2016 +Namespace: default +Status: DEPLOYED + +Resources: +==> extensions/Deployment +NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE +happy-panda-mariadb 1 0 0 0 1s + +==> v1/Secret +NAME TYPE DATA AGE +happy-panda-mariadb Opaque 2 1s + +==> v1/Service +NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE +happy-panda-mariadb 10.0.0.70 3306/TCP 1s + + +Notes: +MariaDB can be accessed via port 3306 on the following DNS name from within your cluster: +happy-panda-mariadb.default.svc.cluster.local + +To connect to your database run the following command: + + kubectl run happy-panda-mariadb-client --rm --tty -i --image bitnami/mariadb --command -- mysql -h happy-panda-mariadb +``` + +Now the `mariadb` chart is installed. Note that installing a chart +creates a new _release_ object. The release above is named +`happy-panda`. (If you want to use your own release name, simply use the +`--name` flag on `helm install`.) + +During installation, the `helm` client will print useful information +about which resources were created, what the state of the release is, +and also whether there are additional configuration steps you can or +should take. + +Helm does not wait until all of the resources are running before it +exits. Many charts require Docker images that are over 600M in size, and +may take a long time to install into the cluster. + +To keep track of a release's state, or to re-read configuration +information, you can use `helm status`: + +``` +$ helm status happy-panda +Last Deployed: Wed Sep 28 12:32:28 2016 +Namespace: default +Status: DEPLOYED + +Resources: +==> v1/Service +NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE +happy-panda-mariadb 10.0.0.70 3306/TCP 4m + +==> extensions/Deployment +NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE +happy-panda-mariadb 1 1 1 1 4m + +==> v1/Secret +NAME TYPE DATA AGE +happy-panda-mariadb Opaque 2 4m + + +Notes: +MariaDB can be accessed via port 3306 on the following DNS name from within your cluster: +happy-panda-mariadb.default.svc.cluster.local + +To connect to your database run the following command: + + kubectl run happy-panda-mariadb-client --rm --tty -i --image bitnami/mariadb --command -- mysql -h happy-panda-mariadb +``` + +The above shows the current state of your release. + +### Customizing the Chart Before Installing + +Installing the way we have here will only use the default configuration +options for this chart. Many times, you will want to customize the chart +to use your preferred configuration. + +To see what options are configurable on a chart, use `helm inspect +values`: + +```console +helm inspect values stable/mariadb-0.3.0.tgz +Fetched stable/mariadb-0.3.0.tgz to /Users/mattbutcher/Code/Go/src/k8s.io/helm/mariadb-0.3.0.tgz +## Bitnami MariaDB image version +## ref: https://hub.docker.com/r/bitnami/mariadb/tags/ +## +## Default: none +imageTag: 10.1.14-r3 + +## Specify a imagePullPolicy +## Default to 'Always' if imageTag is 'latest', else set to 'IfNotPresent' +## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images +## +# imagePullPolicy: + +## Specify password for root user +## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#setting-the-root-password-on-first-run +## +# mariadbRootPassword: + +## Create a database user +## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#creating-a-database-user-on-first-run +## +# mariadbUser: +# mariadbPassword: + +## Create a database +## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#creating-a-database-on-first-run +## +# mariadbDatabase: +``` + +You can then override any of these settings in a YAML formatted file, +and then pass that file during installation. + +```console +$ echo 'mariadbUser: user0` > config.yaml +$ glide install -f config.yaml stable/mariadb +``` + +The above will set the default MariaDB user to `user0`, but accept all +the rest of the defaults for that chart. + +## 'helm upgrade' and 'helm rollback': Upgrading a Release, and Recovering on Failure + +When a new version of a chart is released, or when you want to change +the configuration of your release, you can use the `helm upgrade` +command. + +An upgrade takes an existing release and upgrades it according to the +information you provide. Because Kubernetes charts can be large and +complex, Helm tries to perform the least invasive upgrade. It will only +update things that have changed since the last release. + +```console +$ helm upgrade -f panda.yaml happy-panda stable/mariadb-0.3.0.tgz 1 ↵ +Fetched stable/mariadb-0.3.0.tgz to /Users/mattbutcher/Code/Go/src/k8s.io/helm/mariadb-0.3.0.tgz +happy-panda has been upgraded. Happy Helming! +Last Deployed: Wed Sep 28 12:47:54 2016 +Namespace: default +Status: DEPLOYED +... +``` + +In the above case, the `happy-panda` release is upgraded with the same +chart, but with a new YAML file: + +```yaml +mariadbUser: user1 +``` + +We can use `helm get values` to see whether that new setting took +effect. + +```console +$ helm get values happy-panda +mariadbUser: user1 +``` + +The `helm get` command is a useful tool for looking at a release in the +cluster. And as we can see above, it shows that our new values from +`panda.yaml` were deployed to the cluster. + +Now, if something does not go as planned during a release, it is easy to +roll back to a previous release. + +```console +$ helm rollback happy-panda --version 1 +``` + +The above rolls back our happy-panda to its very first release version. +A release version is an incremental revision. Every time an install, +upgrade, or rollback happens, the revision number is incremented by 1. +The first revision number is always 1. + +## 'helm delete': Deleting a Release + +When it is time to uninstall or delete a release from the cluster, use +the `helm delete` command: + +``` +$ helm delete happy-panda +``` + +This will remove the release from the cluster. You can see all of your +currently deployed releases with the `helm list` command: + +``` +$ helm list +NAME VERSION UPDATED STATUS CHART +inky-cat 1 Wed Sep 28 12:59:46 2016 DEPLOYED alpine-0.1.0 +``` + +From the output above, we can see that the `happy-panda` release was +deleted. + +However, Helm always keeps records of what releases happened. Need to +see the deleted releases? `helm list --deleted` shows those, and `helm +list --all` shows all of the releases (deleted and currently deployed, +as well as releases that failed): + +```console +⇒ helm list --all +NAME VERSION UPDATED STATUS CHART +happy-panda 2 Wed Sep 28 12:47:54 2016 DELETED mariadb-0.3.0 +inky-cat 1 Wed Sep 28 12:59:46 2016 DEPLOYED alpine-0.1.0 +kindred-angelf 2 Tue Sep 27 16:16:10 2016 DELETED alpine-0.1.0 +``` + +Because Helm keeps records of deleted releases, a release name cannot be +re-used. (If you _really_ need to re-use a release name, you can use the +`--replace` flag, but it will simply re-use the existing release and +replace its resources.) + +Note that because releases are preserved in this way, you can rollback a +deleted resource, and have it re-activate. + +## 'helm repo': Working with Repositories + +So far, we've been installing charts only from the `stable` repository. +But you can configure `helm` to use other repositories. Helm provides +several repository tools under the `helm repo` command. + +You can see which repositories are configured using `helm repo list`: + +```console +$ helm repo list +NAME URL +stable http://storage.googleapis.com/kubernetes-charts +local http://localhost:8879/charts +mumoshu https://mumoshu.github.io/charts +``` + +And new repositories can be added with `helm repo add`: + +```console +$ helm repo add dev https://example.com/dev-charts +``` + +Because chart repositories change frequently, at any point you can make +sure your Helm client is up to date by running `helm update`. + +## Creating Your Own Charts + +The [Chart Development Guide](chart.md) explains how to develop your own +charts. But you can get started quickly by using the `helm create` +command: + +```console +$ helm create deis-workflow +Creating deis-workflow +``` + +Now there is a chart in `./deis-workflow`. You can edit it and create +your own templates. + +As you edit your chart, you can validate that it is well-formatted by +running `helm lint`. + +When it's time to package the chart up for distribution, you can run the +`helm package` command: + +```console +$ helm package deis-workflow +deis-workflow-0.1.0.tgz +``` + +And that chart can now easily be installed by `helm install`: + +```console +$ helm install ./deis-workflow-0.1.0.tgz +... +``` + +Charts that are archived can be loaded into chart repositories. See the +documentation for your chart repository server to learn how to upload. + +Note: The `stable` repository is managed on the [Kubernetes Charts +GitHub repository](https://github.com/kubernetes/charts). That project +accepts chart source code, and (after audit) packages those for you. + +## Conclusion + +This chapter has covered the basic usage patterns of the `helm` client, +including searching, installation, upgrading, and deleting. It has also +covered useful utility commands like `helm status`, `helm get`, and +`helm repo`. + +For more information on these commands, take a look at Helm's built-in +help: `helm help`. + +In the next chapter, we look at the process of developing charts. From 6eb340ed5bb66742b9c92c2425b851446f030cd2 Mon Sep 17 00:00:00 2001 From: Rimantas Mocevicius Date: Wed, 28 Sep 2016 14:30:29 -0600 Subject: [PATCH 120/183] docs(chart_repository): added github pages example --- docs/chart_repository.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/chart_repository.md b/docs/chart_repository.md index 88e6e4bb8..94992a30c 100644 --- a/docs/chart_repository.md +++ b/docs/chart_repository.md @@ -41,7 +41,7 @@ redis-2.0.0: home: https://github.com/example-charts/redis ``` -We will go through a detailed GCS and Github Pages examples here, but feel free to skip to the next section if you've already created a chart repository. +We will go through detailed GCS and Github Pages examples here, but feel free to skip to the next section if you've already created a chart repository. ##### GCS bucket example From 5a5a44ec178f9c72fbb51b9adce962fc71c4ab52 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Wed, 28 Sep 2016 14:49:53 -0700 Subject: [PATCH 121/183] feat(*): add rollback to a release version closes #1244 --- _proto/hapi/services/tiller.proto | 3 +- cmd/helm/rollback.go | 13 ++- cmd/helm/rollback_test.go | 7 +- cmd/tiller/release_server.go | 12 ++- cmd/tiller/release_server_test.go | 21 +++++ pkg/helm/option.go | 9 +- pkg/proto/hapi/services/tiller.pb.go | 123 ++++++++++++++------------- 7 files changed, 121 insertions(+), 67 deletions(-) diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto index 366a32779..2e4fee917 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -183,7 +183,6 @@ 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; } @@ -200,6 +199,8 @@ message RollbackReleaseRequest { bool dry_run = 2; // DisableHooks causes the server to skip running any hooks for the rollback bool disable_hooks = 3; + // Version is the version of the release to deploy. + int32 version = 4; } // RollbackReleaseResponse is the response to an update request. diff --git a/cmd/helm/rollback.go b/cmd/helm/rollback.go index d34a68c80..d8931c29f 100644 --- a/cmd/helm/rollback.go +++ b/cmd/helm/rollback.go @@ -26,12 +26,13 @@ import ( ) const rollbackDesc = ` -This command rolls back a release to the previous version. +This command rolls back a release to the previous revision. The argument of the rollback command is the name of a release. ` type rollbackCmd struct { name string + version int32 dryRun bool disableHooks bool out io.Writer @@ -46,7 +47,7 @@ func newRollbackCmd(c helm.Interface, out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "rollback [RELEASE]", - Short: "roll back a release to the previous version", + Short: "roll back a release to a previous revision", Long: rollbackDesc, PersistentPreRunE: setupConnection, RunE: func(cmd *cobra.Command, args []string) error { @@ -60,13 +61,19 @@ func newRollbackCmd(c helm.Interface, out io.Writer) *cobra.Command { } f := cmd.Flags() + f.Int32Var(&rollback.version, "revision", 0, "revision to deploy") f.BoolVar(&rollback.dryRun, "dry-run", false, "simulate a rollback") f.BoolVar(&rollback.disableHooks, "no-hooks", false, "prevent hooks from running during rollback") return cmd } func (r *rollbackCmd) run() error { - _, err := r.client.RollbackRelease(r.name, helm.RollbackDryRun(r.dryRun), helm.RollbackDisableHooks(r.disableHooks)) + _, err := r.client.RollbackRelease( + r.name, + helm.RollbackDryRun(r.dryRun), + helm.RollbackDisableHooks(r.disableHooks), + helm.RollbackVersion(r.version), + ) if err != nil { return prettyError(err) } diff --git a/cmd/helm/rollback_test.go b/cmd/helm/rollback_test.go index 7d9218b23..2455ec775 100644 --- a/cmd/helm/rollback_test.go +++ b/cmd/helm/rollback_test.go @@ -29,7 +29,12 @@ func TestRollbackCmd(t *testing.T) { { name: "rollback a release", args: []string{"funny-honey"}, - resp: nil, + flags: []string{"revision", "1"}, + expected: "Rollback was a success! Happy Helming!", + }, + { + name: "rollback a release without version", + args: []string{"funny-honey"}, expected: "Rollback was a success! Happy Helming!", }, } diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index d080f527c..4260bd914 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -461,7 +461,17 @@ func (s *releaseServer) prepareRollback(req *services.RollbackReleaseRequest) (* return nil, nil, err } - previousRelease, err := s.env.Releases.Get(req.Name, currentRelease.Version-1) + v := req.Version + if v == 0 { + v = currentRelease.Version - 1 + } + if v < 1 { + return nil, nil, errors.New("cannot rollback to version < 1") + } + + log.Printf("rolling back %s to version %d", req.Name, v) + + previousRelease, err := s.env.Releases.Get(req.Name, v) if err != nil { return nil, nil, err } diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 5f63c9abe..3da3a6173 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -664,6 +664,27 @@ func TestRollbackReleaseNoHooks(t *testing.T) { } } +func TestRollbackWithReleaseVersion(t *testing.T) { + c := helm.NewContext() + rs := rsFixture() + rel := releaseStub() + rs.env.Releases.Create(rel) + upgradedRel := upgradeReleaseVersion(rel) + rs.env.Releases.Update(rel) + rs.env.Releases.Create(upgradedRel) + + req := &services.RollbackReleaseRequest{ + Name: rel.Name, + DisableHooks: true, + Version: 1, + } + + _, err := rs.RollbackRelease(c, req) + if err != nil { + t.Fatalf("Failed rollback: %s", err) + } +} + func TestRollbackRelease(t *testing.T) { c := helm.NewContext() rs := rsFixture() diff --git a/pkg/helm/option.go b/pkg/helm/option.go index 4ff73ea9c..de90474e1 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -196,6 +196,13 @@ func RollbackDryRun(dry bool) RollbackOption { } } +// RollbackVersion sets the version of the release to deploy. +func RollbackVersion(ver int32) RollbackOption { + return func(opts *options) { + opts.rollbackReq.Version = ver + } +} + // UpgradeDisableHooks will disable hooks for an upgrade operation. func UpgradeDisableHooks(disable bool) UpdateOption { return func(opts *options) { @@ -333,7 +340,7 @@ func (o *options) rpcRollbackRelease(rlsName string, rlc rls.ReleaseServiceClien o.rollbackReq.DryRun = o.dryRun o.rollbackReq.Name = rlsName - return rlc.RollbackRelease(context.TODO(), &o.rollbackReq) + return rlc.RollbackRelease(NewContext(), &o.rollbackReq) } // Executes tiller.GetReleaseStatus RPC. diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index f05cca029..6b2ddb6de 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -289,6 +289,8 @@ type RollbackReleaseRequest struct { DryRun bool `protobuf:"varint,2,opt,name=dry_run,json=dryRun" json:"dry_run,omitempty"` // DisableHooks causes the server to skip running any hooks for the rollback DisableHooks bool `protobuf:"varint,3,opt,name=disable_hooks,json=disableHooks" json:"disable_hooks,omitempty"` + // Version is the version of the release to deploy. + Version int32 `protobuf:"varint,4,opt,name=version" json:"version,omitempty"` } func (m *RollbackReleaseRequest) Reset() { *m = RollbackReleaseRequest{} } @@ -807,64 +809,65 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ func init() { proto.RegisterFile("hapi/services/tiller.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 944 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdd, 0x6e, 0xe3, 0x44, - 0x14, 0xae, 0xf3, 0x9f, 0xd3, 0x1f, 0xd2, 0xb3, 0x69, 0xe3, 0x5a, 0x80, 0x22, 0x23, 0xd8, 0xb0, - 0xb0, 0x29, 0x84, 0x5b, 0x84, 0xd4, 0xcd, 0x46, 0x6d, 0xd9, 0x92, 0x95, 0x26, 0x14, 0x24, 0x2e, - 0x88, 0xdc, 0x64, 0xb2, 0xf5, 0xae, 0xe3, 0x09, 0x9e, 0x49, 0x45, 0x1f, 0x81, 0x0b, 0x5e, 0x82, - 0xf7, 0xe0, 0xc9, 0xb8, 0x41, 0x9e, 0xf1, 0xb8, 0x71, 0x62, 0x6f, 0xbd, 0xb9, 0x89, 0x3d, 0x73, - 0x3e, 0x7f, 0xe7, 0x9b, 0xef, 0xcc, 0x9c, 0x09, 0x58, 0xb7, 0xce, 0xc2, 0x3d, 0xe5, 0x34, 0xb8, - 0x73, 0x27, 0x94, 0x9f, 0x0a, 0xd7, 0xf3, 0x68, 0xd0, 0x5d, 0x04, 0x4c, 0x30, 0x6c, 0x86, 0xb1, - 0xae, 0x8e, 0x75, 0x55, 0xcc, 0x3a, 0x96, 0x5f, 0x4c, 0x6e, 0x9d, 0x40, 0xa8, 0x5f, 0x85, 0xb6, - 0x5a, 0xab, 0xf3, 0xcc, 0x9f, 0xb9, 0x6f, 0xa2, 0x80, 0x4a, 0x11, 0x50, 0x8f, 0x3a, 0x9c, 0xea, - 0x67, 0xe2, 0x23, 0x1d, 0x73, 0xfd, 0x19, 0x8b, 0x02, 0x27, 0x89, 0x00, 0x17, 0x8e, 0x58, 0xf2, - 0x04, 0xdf, 0x1d, 0x0d, 0xb8, 0xcb, 0x7c, 0xfd, 0x54, 0x31, 0xfb, 0x9f, 0x02, 0x3c, 0xb9, 0x72, - 0xb9, 0x20, 0xea, 0x43, 0x4e, 0xe8, 0x1f, 0x4b, 0xca, 0x05, 0x36, 0xa1, 0xec, 0xb9, 0x73, 0x57, - 0x98, 0x46, 0xdb, 0xe8, 0x14, 0x89, 0x1a, 0xe0, 0x31, 0x54, 0xd8, 0x6c, 0xc6, 0xa9, 0x30, 0x0b, - 0x6d, 0xa3, 0x53, 0x27, 0xd1, 0x08, 0x7f, 0x80, 0x2a, 0x67, 0x81, 0x18, 0xdf, 0xdc, 0x9b, 0xc5, - 0xb6, 0xd1, 0x39, 0xe8, 0x7d, 0xde, 0x4d, 0xb3, 0xa2, 0x1b, 0x66, 0x1a, 0xb1, 0x40, 0x74, 0xc3, - 0x9f, 0x17, 0xf7, 0xa4, 0xc2, 0xe5, 0x33, 0xe4, 0x9d, 0xb9, 0x9e, 0xa0, 0x81, 0x59, 0x52, 0xbc, - 0x6a, 0x84, 0xe7, 0x00, 0x92, 0x97, 0x05, 0x53, 0x1a, 0x98, 0x65, 0x49, 0xdd, 0xc9, 0x41, 0xfd, - 0x3a, 0xc4, 0x93, 0x3a, 0xd7, 0xaf, 0xf8, 0x3d, 0xec, 0x29, 0x4b, 0xc6, 0x13, 0x36, 0xa5, 0xdc, - 0xac, 0xb4, 0x8b, 0x9d, 0x83, 0xde, 0x89, 0xa2, 0xd2, 0x0e, 0x8f, 0x94, 0x69, 0x7d, 0x36, 0xa5, - 0x64, 0x57, 0xc1, 0xc3, 0x77, 0x6e, 0xff, 0x0e, 0x35, 0x4d, 0x6f, 0xf7, 0xa0, 0xa2, 0xc4, 0xe3, - 0x2e, 0x54, 0xaf, 0x87, 0xaf, 0x86, 0xaf, 0x7f, 0x1d, 0x36, 0x76, 0xb0, 0x06, 0xa5, 0xe1, 0xd9, - 0x4f, 0x83, 0x86, 0x81, 0x87, 0xb0, 0x7f, 0x75, 0x36, 0xfa, 0x79, 0x4c, 0x06, 0x57, 0x83, 0xb3, - 0xd1, 0xe0, 0x65, 0xa3, 0x60, 0x7f, 0x0a, 0xf5, 0x58, 0x15, 0x56, 0xa1, 0x78, 0x36, 0xea, 0xab, - 0x4f, 0x5e, 0x0e, 0x46, 0xfd, 0x86, 0x61, 0xff, 0x65, 0x40, 0x33, 0x59, 0x04, 0xbe, 0x60, 0x3e, - 0xa7, 0x61, 0x15, 0x26, 0x6c, 0xe9, 0xc7, 0x55, 0x90, 0x03, 0x44, 0x28, 0xf9, 0xf4, 0x4f, 0x5d, - 0x03, 0xf9, 0x1e, 0x22, 0x05, 0x13, 0x8e, 0x27, 0xfd, 0x2f, 0x12, 0x35, 0xc0, 0x6f, 0xa1, 0x16, - 0x2d, 0x8e, 0x9b, 0xa5, 0x76, 0xb1, 0xb3, 0xdb, 0x3b, 0x4a, 0x2e, 0x39, 0xca, 0x48, 0x62, 0x98, - 0x7d, 0x0e, 0xad, 0x73, 0xaa, 0x95, 0x28, 0x47, 0xf4, 0x9e, 0x08, 0xf3, 0x3a, 0x73, 0x2a, 0xc5, - 0x84, 0x79, 0x9d, 0x39, 0x45, 0x13, 0xaa, 0xd1, 0x86, 0x92, 0x72, 0xca, 0x44, 0x0f, 0x6d, 0x01, - 0xe6, 0x26, 0x51, 0xb4, 0xae, 0x34, 0xa6, 0x2f, 0xa0, 0x14, 0x6e, 0x67, 0x49, 0xb3, 0xdb, 0xc3, - 0xa4, 0xce, 0x4b, 0x7f, 0xc6, 0x88, 0x8c, 0xe3, 0xc7, 0x50, 0x0f, 0xf1, 0x7c, 0xe1, 0x4c, 0xa8, - 0x5c, 0x6d, 0x9d, 0x3c, 0x4c, 0xd8, 0x17, 0xab, 0x59, 0xfb, 0xcc, 0x17, 0xd4, 0x17, 0xdb, 0xe9, - 0xbf, 0x82, 0x93, 0x14, 0xa6, 0x68, 0x01, 0xa7, 0x50, 0x8d, 0xa4, 0x49, 0xb6, 0x4c, 0x5f, 0x35, - 0xca, 0xfe, 0xd7, 0x80, 0xe6, 0xf5, 0x62, 0xea, 0x08, 0xaa, 0x43, 0xef, 0x11, 0xf5, 0x14, 0xca, - 0xb2, 0x2d, 0x44, 0x5e, 0x1c, 0x2a, 0x6e, 0xd5, 0x3b, 0xfa, 0xe1, 0x2f, 0x51, 0x71, 0x7c, 0x06, - 0x95, 0x3b, 0xc7, 0x5b, 0x52, 0x2e, 0x8d, 0x88, 0x5d, 0x8b, 0x90, 0xb2, 0xa7, 0x90, 0x08, 0x81, - 0x2d, 0xa8, 0x4e, 0x83, 0xfb, 0x71, 0xb0, 0xf4, 0xe5, 0x21, 0xab, 0x91, 0xca, 0x34, 0xb8, 0x27, - 0x4b, 0x1f, 0x3f, 0x83, 0xfd, 0xa9, 0xcb, 0x9d, 0x1b, 0x8f, 0x8e, 0x6f, 0x19, 0x7b, 0xc7, 0xe5, - 0x39, 0xab, 0x91, 0xbd, 0x68, 0xf2, 0x22, 0x9c, 0xb3, 0x2f, 0xe0, 0x68, 0x4d, 0xfe, 0xb6, 0x4e, - 0xbc, 0x85, 0x63, 0xc2, 0x3c, 0xef, 0xc6, 0x99, 0xbc, 0xcb, 0x61, 0xc5, 0x8a, 0xea, 0xc2, 0xfb, - 0x55, 0x17, 0x53, 0x54, 0xff, 0x08, 0xad, 0x8d, 0x5c, 0xdb, 0xea, 0xfe, 0xcf, 0x80, 0xa3, 0x4b, - 0x9f, 0x0b, 0xc7, 0xf3, 0xd6, 0x74, 0xc7, 0xe5, 0x32, 0x72, 0x97, 0xab, 0xf0, 0x21, 0xe5, 0x2a, - 0x26, 0x16, 0xae, 0x5d, 0x2a, 0xad, 0xb8, 0x94, 0xa7, 0x84, 0xc9, 0x83, 0x53, 0x59, 0x3b, 0x38, - 0xf8, 0x09, 0x40, 0x40, 0x97, 0x9c, 0x8e, 0x25, 0x79, 0x55, 0x7e, 0x5f, 0x97, 0x33, 0x43, 0x67, - 0x4e, 0xed, 0x4b, 0x38, 0x5e, 0x5f, 0xfc, 0xb6, 0x46, 0xde, 0x42, 0xeb, 0xda, 0x77, 0x53, 0x9d, - 0x4c, 0xdb, 0x01, 0x1b, 0x6b, 0x2b, 0xa4, 0xac, 0xad, 0x09, 0xe5, 0xc5, 0x32, 0x78, 0x43, 0x23, - 0xaf, 0xd4, 0xc0, 0x7e, 0x05, 0xe6, 0x66, 0xa6, 0x6d, 0x65, 0x3f, 0x81, 0xc3, 0x73, 0x2a, 0x7e, - 0x51, 0xdd, 0x21, 0x12, 0x6c, 0x0f, 0x00, 0x57, 0x27, 0x1f, 0xb8, 0xa3, 0xa9, 0x24, 0xb7, 0xbe, - 0x7a, 0x35, 0x5e, 0xa3, 0x7a, 0x7f, 0x57, 0xe1, 0x40, 0x77, 0x4a, 0x75, 0xaf, 0xa1, 0x0b, 0x7b, - 0xab, 0x57, 0x02, 0x7e, 0x99, 0x7d, 0xed, 0xad, 0xdd, 0xdd, 0xd6, 0xb3, 0x3c, 0x50, 0x25, 0xd5, - 0xde, 0xf9, 0xc6, 0x40, 0x0e, 0x8d, 0xf5, 0x4e, 0x8d, 0xcf, 0xd3, 0x39, 0x32, 0xae, 0x06, 0xab, - 0x9b, 0x17, 0xae, 0xd3, 0xe2, 0x9d, 0xb4, 0x33, 0xd9, 0x5e, 0xf1, 0x51, 0x9a, 0x64, 0x47, 0xb7, - 0x4e, 0x73, 0xe3, 0xe3, 0xbc, 0x6f, 0x61, 0x3f, 0xd1, 0xc8, 0x30, 0xc3, 0xad, 0xb4, 0x66, 0x6d, - 0x7d, 0x95, 0x0b, 0x1b, 0xe7, 0x9a, 0xc3, 0x41, 0xf2, 0xd0, 0x60, 0x06, 0x41, 0x6a, 0x5f, 0xb1, - 0xbe, 0xce, 0x07, 0x8e, 0xd3, 0x71, 0x68, 0xac, 0x6f, 0xf7, 0xac, 0x3a, 0x66, 0x1c, 0xc0, 0xac, - 0x3a, 0x66, 0x9d, 0x22, 0x7b, 0x07, 0x1d, 0x80, 0x87, 0x13, 0x80, 0x4f, 0x33, 0x0b, 0x92, 0x3c, - 0x38, 0x56, 0xe7, 0x71, 0x60, 0x9c, 0x62, 0x01, 0x1f, 0xad, 0x75, 0x71, 0xcc, 0xb0, 0x26, 0xfd, - 0x62, 0xb1, 0x9e, 0xe7, 0x44, 0xeb, 0x8c, 0x2f, 0xe0, 0xb7, 0x9a, 0x06, 0xdf, 0x54, 0xe4, 0x1f, - 0xe5, 0xef, 0xfe, 0x0f, 0x00, 0x00, 0xff, 0xff, 0x0b, 0x37, 0x7c, 0x73, 0xf9, 0x0b, 0x00, 0x00, + // 951 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0x5f, 0x6f, 0xe3, 0x44, + 0x10, 0xaf, 0xf3, 0x3f, 0xd3, 0x3f, 0xa4, 0x73, 0x69, 0xe3, 0x5a, 0x80, 0x22, 0x23, 0xb8, 0x70, + 0x70, 0x29, 0x84, 0x57, 0x84, 0xd4, 0xcb, 0x45, 0x6d, 0xb9, 0x92, 0x93, 0x36, 0x14, 0x24, 0x1e, + 0x88, 0xdc, 0x64, 0x73, 0x35, 0xe7, 0x78, 0x83, 0x77, 0x53, 0xd1, 0x77, 0x5e, 0x78, 0xe0, 0x4b, + 0xf0, 0x3d, 0xf8, 0x64, 0xbc, 0x20, 0xef, 0x7a, 0xdd, 0x38, 0xb1, 0xaf, 0xbe, 0xbc, 0xc4, 0xde, + 0x9d, 0x9f, 0x7f, 0x33, 0xf3, 0x9b, 0x9d, 0xd9, 0x80, 0x75, 0xeb, 0x2c, 0xdc, 0x53, 0x4e, 0x83, + 0x3b, 0x77, 0x42, 0xf9, 0xa9, 0x70, 0x3d, 0x8f, 0x06, 0xdd, 0x45, 0xc0, 0x04, 0xc3, 0x66, 0x68, + 0xeb, 0x6a, 0x5b, 0x57, 0xd9, 0xac, 0x63, 0xf9, 0xc5, 0xe4, 0xd6, 0x09, 0x84, 0xfa, 0x55, 0x68, + 0xab, 0xb5, 0xba, 0xcf, 0xfc, 0x99, 0xfb, 0x26, 0x32, 0x28, 0x17, 0x01, 0xf5, 0xa8, 0xc3, 0xa9, + 0x7e, 0x26, 0x3e, 0xd2, 0x36, 0xd7, 0x9f, 0xb1, 0xc8, 0x70, 0x92, 0x30, 0x70, 0xe1, 0x88, 0x25, + 0x4f, 0xf0, 0xdd, 0xd1, 0x80, 0xbb, 0xcc, 0xd7, 0x4f, 0x65, 0xb3, 0xff, 0x29, 0xc0, 0x93, 0x2b, + 0x97, 0x0b, 0xa2, 0x3e, 0xe4, 0x84, 0xfe, 0xbe, 0xa4, 0x5c, 0x60, 0x13, 0xca, 0x9e, 0x3b, 0x77, + 0x85, 0x69, 0xb4, 0x8d, 0x4e, 0x91, 0xa8, 0x05, 0x1e, 0x43, 0x85, 0xcd, 0x66, 0x9c, 0x0a, 0xb3, + 0xd0, 0x36, 0x3a, 0x75, 0x12, 0xad, 0xf0, 0x3b, 0xa8, 0x72, 0x16, 0x88, 0xf1, 0xcd, 0xbd, 0x59, + 0x6c, 0x1b, 0x9d, 0x83, 0xde, 0xa7, 0xdd, 0x34, 0x29, 0xba, 0xa1, 0xa7, 0x11, 0x0b, 0x44, 0x37, + 0xfc, 0x79, 0x71, 0x4f, 0x2a, 0x5c, 0x3e, 0x43, 0xde, 0x99, 0xeb, 0x09, 0x1a, 0x98, 0x25, 0xc5, + 0xab, 0x56, 0x78, 0x0e, 0x20, 0x79, 0x59, 0x30, 0xa5, 0x81, 0x59, 0x96, 0xd4, 0x9d, 0x1c, 0xd4, + 0xaf, 0x43, 0x3c, 0xa9, 0x73, 0xfd, 0x8a, 0xdf, 0xc2, 0x9e, 0x92, 0x64, 0x3c, 0x61, 0x53, 0xca, + 0xcd, 0x4a, 0xbb, 0xd8, 0x39, 0xe8, 0x9d, 0x28, 0x2a, 0xad, 0xf0, 0x48, 0x89, 0xd6, 0x67, 0x53, + 0x4a, 0x76, 0x15, 0x3c, 0x7c, 0xe7, 0xf6, 0xaf, 0x50, 0xd3, 0xf4, 0x76, 0x0f, 0x2a, 0x2a, 0x78, + 0xdc, 0x85, 0xea, 0xf5, 0xf0, 0xd5, 0xf0, 0xf5, 0xcf, 0xc3, 0xc6, 0x0e, 0xd6, 0xa0, 0x34, 0x3c, + 0xfb, 0x61, 0xd0, 0x30, 0xf0, 0x10, 0xf6, 0xaf, 0xce, 0x46, 0x3f, 0x8e, 0xc9, 0xe0, 0x6a, 0x70, + 0x36, 0x1a, 0xbc, 0x6c, 0x14, 0xec, 0x8f, 0xa1, 0x1e, 0x47, 0x85, 0x55, 0x28, 0x9e, 0x8d, 0xfa, + 0xea, 0x93, 0x97, 0x83, 0x51, 0xbf, 0x61, 0xd8, 0x7f, 0x19, 0xd0, 0x4c, 0x16, 0x81, 0x2f, 0x98, + 0xcf, 0x69, 0x58, 0x85, 0x09, 0x5b, 0xfa, 0x71, 0x15, 0xe4, 0x02, 0x11, 0x4a, 0x3e, 0xfd, 0x43, + 0xd7, 0x40, 0xbe, 0x87, 0x48, 0xc1, 0x84, 0xe3, 0x49, 0xfd, 0x8b, 0x44, 0x2d, 0xf0, 0x6b, 0xa8, + 0x45, 0xc9, 0x71, 0xb3, 0xd4, 0x2e, 0x76, 0x76, 0x7b, 0x47, 0xc9, 0x94, 0x23, 0x8f, 0x24, 0x86, + 0xd9, 0xe7, 0xd0, 0x3a, 0xa7, 0x3a, 0x12, 0xa5, 0x88, 0x3e, 0x13, 0xa1, 0x5f, 0x67, 0x4e, 0x65, + 0x30, 0xa1, 0x5f, 0x67, 0x4e, 0xd1, 0x84, 0x6a, 0x74, 0xa0, 0x64, 0x38, 0x65, 0xa2, 0x97, 0xb6, + 0x00, 0x73, 0x93, 0x28, 0xca, 0x2b, 0x8d, 0xe9, 0x33, 0x28, 0x85, 0xc7, 0x59, 0xd2, 0xec, 0xf6, + 0x30, 0x19, 0xe7, 0xa5, 0x3f, 0x63, 0x44, 0xda, 0xf1, 0x43, 0xa8, 0x87, 0x78, 0xbe, 0x70, 0x26, + 0x54, 0x66, 0x5b, 0x27, 0x0f, 0x1b, 0xf6, 0xc5, 0xaa, 0xd7, 0x3e, 0xf3, 0x05, 0xf5, 0xc5, 0x76, + 0xf1, 0x5f, 0xc1, 0x49, 0x0a, 0x53, 0x94, 0xc0, 0x29, 0x54, 0xa3, 0xd0, 0x24, 0x5b, 0xa6, 0xae, + 0x1a, 0x65, 0xff, 0x6b, 0x40, 0xf3, 0x7a, 0x31, 0x75, 0x04, 0xd5, 0xa6, 0x77, 0x04, 0xf5, 0x14, + 0xca, 0x72, 0x2c, 0x44, 0x5a, 0x1c, 0x2a, 0x6e, 0x35, 0x3b, 0xfa, 0xe1, 0x2f, 0x51, 0x76, 0x7c, + 0x06, 0x95, 0x3b, 0xc7, 0x5b, 0x52, 0x2e, 0x85, 0x88, 0x55, 0x8b, 0x90, 0x72, 0xa6, 0x90, 0x08, + 0x81, 0x2d, 0xa8, 0x4e, 0x83, 0xfb, 0x71, 0xb0, 0xf4, 0x65, 0x93, 0xd5, 0x48, 0x65, 0x1a, 0xdc, + 0x93, 0xa5, 0x8f, 0x9f, 0xc0, 0xfe, 0xd4, 0xe5, 0xce, 0x8d, 0x47, 0xc7, 0xb7, 0x8c, 0xbd, 0xe5, + 0xb2, 0xcf, 0x6a, 0x64, 0x2f, 0xda, 0xbc, 0x08, 0xf7, 0xec, 0x0b, 0x38, 0x5a, 0x0b, 0x7f, 0x5b, + 0x25, 0xfe, 0x34, 0xe0, 0x98, 0x30, 0xcf, 0xbb, 0x71, 0x26, 0x6f, 0x73, 0x68, 0xb1, 0x12, 0x76, + 0xe1, 0xdd, 0x61, 0x17, 0x37, 0xc3, 0x5e, 0x2d, 0x6f, 0x29, 0x59, 0xde, 0xef, 0xa1, 0xb5, 0x11, + 0xc5, 0xb6, 0x29, 0xfd, 0x67, 0xc0, 0xd1, 0xa5, 0xcf, 0x85, 0xe3, 0x79, 0x6b, 0x19, 0xc5, 0x95, + 0x34, 0x72, 0x57, 0xb2, 0xf0, 0x3e, 0x95, 0x2c, 0x26, 0x24, 0xd1, 0xfa, 0x95, 0x56, 0xf4, 0xcb, + 0x53, 0xdd, 0x64, 0x4f, 0x55, 0xd6, 0x7a, 0x0a, 0x3f, 0x02, 0x08, 0xe8, 0x92, 0xd3, 0xb1, 0x24, + 0xaf, 0xca, 0xef, 0xeb, 0x72, 0x67, 0xe8, 0xcc, 0xa9, 0x7d, 0x09, 0xc7, 0xeb, 0xc9, 0x6f, 0x2b, + 0xe4, 0x2d, 0xb4, 0xae, 0x7d, 0x37, 0x55, 0xc9, 0xb4, 0xb3, 0xb1, 0x91, 0x5b, 0x21, 0x25, 0xb7, + 0x26, 0x94, 0x17, 0xcb, 0xe0, 0x0d, 0x8d, 0xb4, 0x52, 0x0b, 0xfb, 0x15, 0x98, 0x9b, 0x9e, 0xb6, + 0x0d, 0xfb, 0x09, 0x1c, 0x9e, 0x53, 0xf1, 0x93, 0x3a, 0x59, 0x51, 0xc0, 0xf6, 0x00, 0x70, 0x75, + 0xf3, 0x81, 0x3b, 0xda, 0x4a, 0x72, 0xeb, 0x5b, 0x59, 0xe3, 0x35, 0xaa, 0xf7, 0x77, 0x15, 0x0e, + 0xf4, 0x10, 0x55, 0x57, 0x1e, 0xba, 0xb0, 0xb7, 0x7a, 0x5b, 0xe0, 0xe7, 0xd9, 0x37, 0xe2, 0xda, + 0xb5, 0x6e, 0x3d, 0xcb, 0x03, 0x55, 0xa1, 0xda, 0x3b, 0x5f, 0x19, 0xc8, 0xa1, 0xb1, 0x3e, 0xc4, + 0xf1, 0x79, 0x3a, 0x47, 0xc6, 0xad, 0x61, 0x75, 0xf3, 0xc2, 0xb5, 0x5b, 0xbc, 0x93, 0x72, 0x26, + 0x27, 0x2f, 0x3e, 0x4a, 0x93, 0x1c, 0xf6, 0xd6, 0x69, 0x6e, 0x7c, 0xec, 0xf7, 0x37, 0xd8, 0x4f, + 0xcc, 0x38, 0xcc, 0x50, 0x2b, 0x6d, 0x8e, 0x5b, 0x5f, 0xe4, 0xc2, 0xc6, 0xbe, 0xe6, 0x70, 0x90, + 0x6c, 0x1a, 0xcc, 0x20, 0x48, 0x9d, 0x2b, 0xd6, 0x97, 0xf9, 0xc0, 0xb1, 0x3b, 0x0e, 0x8d, 0xf5, + 0xe3, 0x9e, 0x55, 0xc7, 0x8c, 0x06, 0xcc, 0xaa, 0x63, 0x56, 0x17, 0xd9, 0x3b, 0xe8, 0x00, 0x3c, + 0x74, 0x00, 0x3e, 0xcd, 0x2c, 0x48, 0xb2, 0x71, 0xac, 0xce, 0xe3, 0xc0, 0xd8, 0xc5, 0x02, 0x3e, + 0x58, 0x9b, 0xe2, 0x98, 0x21, 0x4d, 0xfa, 0x95, 0x63, 0x3d, 0xcf, 0x89, 0xd6, 0x1e, 0x5f, 0xc0, + 0x2f, 0x35, 0x0d, 0xbe, 0xa9, 0xc8, 0xff, 0xd0, 0xdf, 0xfc, 0x1f, 0x00, 0x00, 0xff, 0xff, 0x76, + 0x9b, 0x44, 0xa7, 0x14, 0x0c, 0x00, 0x00, } From 1db7bd649fac94747d6ca3de39fac3eb2e3213e3 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Wed, 28 Sep 2016 15:02:27 -0700 Subject: [PATCH 122/183] fix(helm): s/version/revision/ --- cmd/helm/get.go | 4 ++-- cmd/helm/get_test.go | 2 +- cmd/helm/list.go | 2 +- cmd/helm/list_test.go | 2 +- cmd/helm/status.go | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/helm/get.go b/cmd/helm/get.go index 3c8beb26f..9402762c5 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -73,7 +73,7 @@ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { }, } - cmd.PersistentFlags().Int32Var(&get.version, "version", 0, "get the named release with version") + cmd.PersistentFlags().Int32Var(&get.version, "revision", 0, "get the named release with revision") cmd.AddCommand(newGetValuesCmd(nil, out)) cmd.AddCommand(newGetManifestCmd(nil, out)) @@ -81,7 +81,7 @@ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { return cmd } -var getTemplate = `VERSION: {{.Release.Version}} +var getTemplate = `REVISION: {{.Release.Version}} RELEASED: {{.ReleaseDate}} CHART: {{.Release.Chart.Metadata.Name}}-{{.Release.Chart.Metadata.Version}} USER-SUPPLIED VALUES: diff --git a/cmd/helm/get_test.go b/cmd/helm/get_test.go index 95ecffeb0..77d8d4d19 100644 --- a/cmd/helm/get_test.go +++ b/cmd/helm/get_test.go @@ -29,7 +29,7 @@ func TestGetCmd(t *testing.T) { name: "get with a release", resp: releaseMock(&releaseOptions{name: "thomas-guide"}), args: []string{"thomas-guide"}, - expected: "VERSION: 1\nRELEASED: (.*)\nCHART: foo-0.1.0-beta.1\nUSER-SUPPLIED VALUES:\nname: \"value\"\nCOMPUTED VALUES:\nname: value\n\nHOOKS:\n---\n# pre-install-hook\n" + mockHookTemplate + "\nMANIFEST:", + expected: "REVISION: 1\nRELEASED: (.*)\nCHART: foo-0.1.0-beta.1\nUSER-SUPPLIED VALUES:\nname: \"value\"\nCOMPUTED VALUES:\nname: value\n\nHOOKS:\n---\n# pre-install-hook\n" + mockHookTemplate + "\nMANIFEST:", }, { name: "get requires release name arg", diff --git a/cmd/helm/list.go b/cmd/helm/list.go index b4ec4498b..461bd89de 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -191,7 +191,7 @@ func (l *listCmd) statusCodes() []release.Status_Code { func formatList(rels []*release.Release) string { table := uitable.New() table.MaxColWidth = 30 - table.AddRow("NAME", "VERSION", "UPDATED", "STATUS", "CHART") + table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART") for _, r := range rels { c := fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version) t := timeconv.String(r.Info.LastDeployed) diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index 70197cf02..06d64c1f9 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -45,7 +45,7 @@ func TestListCmd(t *testing.T) { resp: []*release.Release{ releaseMock(&releaseOptions{name: "atlas"}), }, - expected: "NAME \tVERSION\tUPDATED \tSTATUS \tCHART \natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\n", + expected: "NAME \tREVISION\tUPDATED \tSTATUS \tCHART \natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\n", }, { name: "with a release, multiple flags", diff --git a/cmd/helm/status.go b/cmd/helm/status.go index 356fdcb7d..30b572996 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -60,7 +60,7 @@ func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command { }, } - cmd.PersistentFlags().Int32Var(&status.version, "version", 0, "If set, display the status of the named release with version") + cmd.PersistentFlags().Int32Var(&status.version, "revision", 0, "If set, display the status of the named release with revision") return cmd } From 68dd4c9a66de973d2bf3d9f158ae06ab68989f18 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 28 Sep 2016 16:13:38 -0600 Subject: [PATCH 123/183] fix(proto): remove unused fields Some fields were introduced to Chart metadata after Alpha.4, but are not going to be used ever. So we made the decision to remove them before we get stuck with ugliness. This should not break compatibility, since these fields were not used. --- _proto/hapi/chart/metadata.proto | 15 --- pkg/chartutil/chartfile_test.go | 22 ---- pkg/chartutil/testdata/chartfiletest.yaml | 7 - pkg/chartutil/testdata/frobnitz-1.2.3.tgz | Bin 4025 -> 4017 bytes pkg/chartutil/testdata/frobnitz/Chart.yaml | 7 - .../frobnitz/charts/mariner-4.3.2.tgz | Bin 1029 -> 1025 bytes .../mariner/charts/albatross-0.1.0.tgz | Bin 347 -> 347 bytes pkg/proto/hapi/chart/chart.pb.go | 34 +++-- pkg/proto/hapi/chart/config.pb.go | 12 +- pkg/proto/hapi/chart/metadata.pb.go | 72 ++++------- pkg/proto/hapi/chart/template.pb.go | 8 +- pkg/proto/hapi/release/hook.pb.go | 43 +++--- pkg/proto/hapi/release/info.pb.go | 21 ++- pkg/proto/hapi/release/release.pb.go | 34 ++--- pkg/proto/hapi/release/status.pb.go | 36 +++--- pkg/proto/hapi/services/tiller.pb.go | 122 +++++++++--------- pkg/proto/hapi/version/version.pb.go | 15 +-- 17 files changed, 182 insertions(+), 266 deletions(-) diff --git a/_proto/hapi/chart/metadata.proto b/_proto/hapi/chart/metadata.proto index 979702df0..40c8b40dc 100644 --- a/_proto/hapi/chart/metadata.proto +++ b/_proto/hapi/chart/metadata.proto @@ -27,19 +27,6 @@ message Maintainer { string email = 2; } -// Dependency describes this chart's dependency on another chart. -message Dependency { - // Name is the name of the dependency, e.g. 'nginx' - string name = 1; - - // Repository is the repository URL. Appending '/index.yaml' to this should - // return the repo index. - string repository = 2; - - // Version is a SemVer 2 version. - string version = 3; -} - // Metadata for a Chart file. This models the structure of a Chart.yaml file. // // Spec: https://k8s.io/helm/blob/master/docs/design/chart_format.md#the-chart-file @@ -74,6 +61,4 @@ message Metadata { // The URL to an icon file. string icon = 9; - - repeated Dependency dependencies = 10; } diff --git a/pkg/chartutil/chartfile_test.go b/pkg/chartutil/chartfile_test.go index e24f11761..1b8e800f0 100644 --- a/pkg/chartutil/chartfile_test.go +++ b/pkg/chartutil/chartfile_test.go @@ -89,26 +89,4 @@ func verifyChartfile(t *testing.T, f *chart.Metadata) { t.Errorf("Expected %q, got %q", kk[i], k) } } - - if len(f.Dependencies) != 2 { - t.Fatalf("Expected 2 dependencies, got %d", len(f.Dependencies)) - } - - deps := []*chart.Dependency{ - {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, - {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, - } - for i, tt := range deps { - c := f.Dependencies[i] - if c.Name != tt.Name { - t.Errorf("Expected name %q, got %q", tt.Name, c.Name) - } - if c.Version != tt.Version { - t.Errorf("Expected version %q, got %q", tt.Version, c.Version) - } - if c.Repository != tt.Repository { - t.Errorf("Expected repository %q, got %q", tt.Repository, c.Repository) - } - } - } diff --git a/pkg/chartutil/testdata/chartfiletest.yaml b/pkg/chartutil/testdata/chartfiletest.yaml index 325fb27e2..b665c61be 100644 --- a/pkg/chartutil/testdata/chartfiletest.yaml +++ b/pkg/chartutil/testdata/chartfiletest.yaml @@ -14,10 +14,3 @@ sources: - https://example.com/foo/bar home: http://example.com icon: https://example.com/64x64.png -dependencies: - - name: alpine - version: "0.1.0" - repository: https://example.com/charts - - name: mariner - version: "4.3.2" - repository: https://example.com/charts diff --git a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz index e230f5b4018e6c44c5433902bda0392f641a13a0..49ec9ca8789a18c5aa67ef1a0bcc34a924f9f134 100644 GIT binary patch literal 4017 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 literal 4025 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 diff --git a/pkg/chartutil/testdata/frobnitz/Chart.yaml b/pkg/chartutil/testdata/frobnitz/Chart.yaml index 325fb27e2..b665c61be 100644 --- a/pkg/chartutil/testdata/frobnitz/Chart.yaml +++ b/pkg/chartutil/testdata/frobnitz/Chart.yaml @@ -14,10 +14,3 @@ sources: - https://example.com/foo/bar home: http://example.com icon: https://example.com/64x64.png -dependencies: - - name: alpine - version: "0.1.0" - repository: https://example.com/charts - - name: mariner - version: "4.3.2" - repository: https://example.com/charts diff --git a/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz index c2a5c2d612cfc1bed6b4785790a20cff960859ab..4928e7e7af41bd33be43a458f4c9c3a7221fc595 100644 GIT binary patch literal 1025 zcmV+c1pfOUiwFQ!I_y^f1MQc4Y!pQt$1j*vT_1mmq6v0Vv`Rzw_Iaz@V8t3GsR{`g zLeQjhcXM}J_i>$>E1a*!#1gQn#KtBjK>G&>J_4zr#dr`Q8kG7#L5!gIim@ru7$r2u z;?>=~N6JGh@$S$tpSxuCZf9p^_xt;Of3p_EhK+DAP(}nn&{P%pZ7A0Yl9Frl08!F3 zQI-`+lYk&9k|+d#TFylkND&ILyA%sj8cxwj0^xJ^bGyZ2e=|=0K8o%C1S_w9Qvzbj zr=ew*eh&9}gcRkk^%q6W(_d0#O;c3G)juREasUXWU&`5B%r<%A`DH z;QakxRWyb9e@8^l^}|e$c`dAVj!Y(0MvtA zMSx})QtS}o{s#a?NrxEJ!D$^NC`}SQ7(^?el{66_ajf9!F5+KOtPnK_6e_v%=a|a>B{k&huPU-CE1IXjB1p{t zE5R&qH;TcONkJ|G{EdD-D0prbSe!5jaK8{Nop=9z_3l~kL7^y!8Lsyii$UC=iB#CH z1=O+3dK21l|A_M@h?o!~zp&)u3?!j}37`%Ld&h*h;~BR5Gwq}>C|Z#>f36zU_?NL7 zQhLtzf47OCgkwhC0HDcIxWQEUFUaEQ{x6B{hvk1In4IC0m1n7Tad5`_e{tJi@9)}q z8h`ZP&oNd0tFr9B|H*=^ijwN)zt^ATe4gj)cbUxTSB^r#p{rZtGpWXUFItN29Ce zH}89Pi~h#*U5g&uyJG7Vb&L1B-12VcoM#$pH?KYL(jyx#H|n}qzjo~VZG+#1-~P!*WIzrut)Z3cy9YnII`sN`4=BTAsP&sQ z@>%#*N vTXSA%nEW_*J7d#=moBXf+6ZtiaDKCU}+h4a;zSOPW`ZERu!w14pO5lBTX#)Alf@Tdm0QUtBU>!D#jcggJC&hG5)cYfdR+*!~yY~%(5lZYS)x~c-d)pD&MDY-Te5G7sH zG+7ly83>{xiDCe#lX+1EQkX!OUWx??38zRTf!y=ELBz9#l9WJ1yZPgUu8Tw zF>vAjuPVCA{J$I&^mkE9%5af|Y=TQ20~7kcD&_Q7RY}oxs=uzu67&CZ5JgF3N0A*d z5Dsww)Ph_hKr;-9>tIa31HeT|2OGq3(;<)`B#A@8AX*Nsq>1>5V+GH45&w{4LR1O7 z$O|kQ{+kq(BZ!I#X|zv51pGt)AHV*SMCSixp`d>&G*hVL&Yxo<|CdzF*I!j+RaQLz z*A#*Ie_5Cb?m;n_G6~4d0DqyMpAMzm1m`PY4E4t|HWm0 zt-osLN&L}&KgLA$ugbDl|GFrPv?tAfiS}pz|CNQxjw=HCbKflvTsHj4iRL|Edc{{W z25UA<`|yeC>|Gz+JHH~s9XYq~{IHZl-d27$gk9M5g+|s>j_x9o6 zPefPFYdrYume89obS!*q-}0?j)hycma?{@S+0WEfZ(eiwrAO9ZY1DMCdhO&l+xou_ zzw@!R^U3(yQ=t_jtA&Pa$ML6cZ__$6Bl_X71$XaQSAQto+s1QS+OM0{b_*Hk-8$#_ z#%wxUcjVKqYcgQzvDO>6+7b-w!wT|G2DT>FBPWw+B|N>+FC3TJ4Sf4=RIr zp!v(y@)}gX6a3os^O?$pJsYlm{Fm&ZI|gr_dU~+FJMj6~cH#R<>-A;TGlpMvo^rko z;`TERd~dAjq4;OBZVFT$ZR`5&=;-{RwF|}^c%XjjPt!wn?{4cn)!l;jy*LyeZa8@T z7svV@wr0OlH~w+%az>^EFJDp>>-Q*E-eKu diff --git a/pkg/proto/hapi/chart/chart.pb.go b/pkg/proto/hapi/chart/chart.pb.go index 10a525cfd..db1d5ef97 100644 --- a/pkg/proto/hapi/chart/chart.pb.go +++ b/pkg/proto/hapi/chart/chart.pb.go @@ -16,7 +16,6 @@ It has these top-level messages: Config Value Maintainer - Dependency Metadata Template */ @@ -101,21 +100,20 @@ func init() { func init() { proto.RegisterFile("hapi/chart/chart.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 242 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x90, 0xb1, 0x4e, 0xc3, 0x30, - 0x10, 0x86, 0x15, 0x4a, 0x0a, 0x1c, 0x2c, 0x58, 0x08, 0x4c, 0xa7, 0x8a, 0x09, 0x75, 0x70, 0x50, - 0x11, 0x0f, 0x00, 0xcc, 0x2c, 0x16, 0x13, 0xdb, 0xb5, 0xb9, 0xa4, 0x91, 0x52, 0x3b, 0xaa, 0x5d, - 0xa4, 0xbe, 0x3b, 0x03, 0xea, 0xd9, 0xa6, 0x09, 0xea, 0x12, 0x29, 0xf7, 0x7d, 0xff, 0xe5, 0xbf, - 0xc0, 0xed, 0x0a, 0xbb, 0xa6, 0x58, 0xae, 0x70, 0xe3, 0xc3, 0x53, 0x75, 0x1b, 0xeb, 0xad, 0x80, - 0xfd, 0x5c, 0xf1, 0x64, 0x72, 0xd7, 0x77, 0xac, 0xa9, 0x9a, 0x3a, 0x48, 0x93, 0xfb, 0x1e, 0x58, - 0x93, 0xc7, 0x12, 0x3d, 0x1e, 0x41, 0x9e, 0xd6, 0x5d, 0x8b, 0x9e, 0x12, 0xaa, 0xad, 0xad, 0x5b, - 0x2a, 0xf8, 0x6d, 0xb1, 0xad, 0x0a, 0x34, 0xbb, 0x80, 0x1e, 0x7e, 0x32, 0xc8, 0xdf, 0xf7, 0x19, - 0xf1, 0x04, 0xe7, 0x69, 0xa3, 0xcc, 0xa6, 0xd9, 0xe3, 0xe5, 0xfc, 0x46, 0x1d, 0x2a, 0xa9, 0x8f, - 0xc8, 0xf4, 0x9f, 0x25, 0xe6, 0x70, 0x91, 0x3e, 0xe4, 0xe4, 0xc9, 0x74, 0xf4, 0x3f, 0xf2, 0x19, - 0xa1, 0x3e, 0x68, 0xe2, 0x05, 0xae, 0x4a, 0xea, 0xc8, 0x94, 0x64, 0x96, 0x0d, 0x39, 0x39, 0xe2, - 0xd8, 0x75, 0x3f, 0xc6, 0x75, 0xf4, 0x40, 0x13, 0x33, 0x18, 0x7f, 0x63, 0xbb, 0x25, 0x27, 0x4f, - 0xb9, 0x9a, 0x18, 0x04, 0xf8, 0x0f, 0xe9, 0x68, 0x88, 0x19, 0xe4, 0x55, 0xd3, 0x92, 0x93, 0x79, - 0xac, 0x14, 0xae, 0x57, 0xe9, 0x7a, 0xf5, 0x6a, 0x76, 0x3a, 0x28, 0x6f, 0x67, 0x5f, 0x39, 0xef, - 0x58, 0x8c, 0x99, 0x3e, 0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0xe9, 0x70, 0x34, 0x75, 0x9e, 0x01, - 0x00, 0x00, + // 239 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcb, 0x48, 0x2c, 0xc8, + 0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0x81, 0x90, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x5c, + 0x20, 0x71, 0x3d, 0xb0, 0x88, 0x94, 0x38, 0xb2, 0x9a, 0xfc, 0xbc, 0xb4, 0xcc, 0x74, 0x88, 0x22, + 0x29, 0x49, 0x24, 0x89, 0xdc, 0xd4, 0x92, 0xc4, 0x94, 0xc4, 0x92, 0x44, 0x2c, 0x52, 0x25, 0xa9, + 0xb9, 0x05, 0x39, 0x89, 0x25, 0xa9, 0x30, 0xa9, 0xf4, 0xfc, 0xfc, 0xf4, 0x9c, 0x54, 0x7d, 0x30, + 0x2f, 0xa9, 0x34, 0x4d, 0x3f, 0x31, 0xaf, 0x12, 0x22, 0xa5, 0xf4, 0x87, 0x91, 0x8b, 0xd5, 0x19, + 0xa4, 0x47, 0xc8, 0x80, 0x8b, 0x03, 0x66, 0xa2, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0xb7, 0x91, 0x88, + 0x1e, 0xc2, 0x49, 0x7a, 0xbe, 0x50, 0xb9, 0x20, 0xb8, 0x2a, 0x21, 0x23, 0x2e, 0x4e, 0x98, 0x45, + 0xc5, 0x12, 0x4c, 0x0a, 0xcc, 0xe8, 0x5a, 0x42, 0xa0, 0x92, 0x41, 0x08, 0x65, 0x42, 0xa6, 0x5c, + 0x3c, 0x29, 0xa9, 0x05, 0xa9, 0x79, 0x29, 0xa9, 0x79, 0xc9, 0x99, 0x40, 0x6d, 0xcc, 0x60, 0x6d, + 0x82, 0xc8, 0xda, 0xc0, 0xce, 0x09, 0x42, 0x51, 0x26, 0xa4, 0xc5, 0xc5, 0x56, 0x96, 0x98, 0x53, + 0x0a, 0xd4, 0xc0, 0x02, 0x76, 0x9a, 0x10, 0x8a, 0x06, 0x70, 0x08, 0x05, 0x41, 0x55, 0x00, 0xd5, + 0xb2, 0xa6, 0x65, 0xe6, 0x00, 0x95, 0xb2, 0x42, 0x9d, 0x04, 0xf1, 0xbd, 0x1e, 0xcc, 0xf7, 0x7a, + 0x8e, 0x79, 0x95, 0x41, 0x10, 0x25, 0x4e, 0xec, 0x51, 0xac, 0x60, 0x33, 0x92, 0xd8, 0xc0, 0xb2, + 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe9, 0x70, 0x34, 0x75, 0x9e, 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/chart/config.pb.go b/pkg/proto/hapi/chart/config.pb.go index a7b61885a..f1471405d 100644 --- a/pkg/proto/hapi/chart/config.pb.go +++ b/pkg/proto/hapi/chart/config.pb.go @@ -49,7 +49,7 @@ func init() { func init() { proto.RegisterFile("hapi/chart/config.proto", fileDescriptor1) } var fileDescriptor1 = []byte{ - // 182 bytes of a gzipped FileDescriptorProto + // 179 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0x48, 0x2c, 0xc8, 0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0xd1, 0x4f, 0xce, 0xcf, 0x4b, 0xcb, 0x4c, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x02, 0x49, 0xe8, 0x81, 0x25, 0x94, 0x16, 0x30, 0x72, 0xb1, 0x39, @@ -57,9 +57,9 @@ var fileDescriptor1 = []byte{ 0x40, 0x4c, 0x21, 0x33, 0x2e, 0xb6, 0xb2, 0xc4, 0x9c, 0xd2, 0xd4, 0x62, 0x09, 0x26, 0x05, 0x66, 0x0d, 0x6e, 0x23, 0x39, 0x3d, 0x84, 0x4e, 0x3d, 0x88, 0x2e, 0xbd, 0x30, 0xb0, 0x02, 0xd7, 0xbc, 0x92, 0xa2, 0xca, 0x20, 0xa8, 0x6a, 0x29, 0x1f, 0x2e, 0x6e, 0x24, 0x61, 0x90, 0xc1, 0xd9, 0xa9, - 0x95, 0x30, 0x83, 0xb3, 0x53, 0x2b, 0x85, 0xd4, 0xb9, 0x58, 0xc1, 0x4a, 0x25, 0x98, 0x14, 0x18, - 0x35, 0xb8, 0x8d, 0x04, 0x91, 0xcd, 0x05, 0xeb, 0x0c, 0x82, 0xc8, 0x5b, 0x31, 0x59, 0x30, 0x2a, - 0xc9, 0x72, 0xb1, 0x82, 0xc5, 0x84, 0x44, 0x60, 0xba, 0x20, 0x26, 0x41, 0x38, 0x4e, 0xec, 0x51, - 0xac, 0x60, 0x8d, 0x49, 0x6c, 0x60, 0xdf, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xe1, 0x12, - 0x60, 0xda, 0xf8, 0x00, 0x00, 0x00, + 0x95, 0x30, 0x83, 0x81, 0x4c, 0x21, 0x75, 0x2e, 0x56, 0xb0, 0x52, 0xa0, 0xb9, 0x8c, 0x40, 0x73, + 0x05, 0x91, 0xcd, 0x05, 0xeb, 0x0c, 0x82, 0xc8, 0x5b, 0x31, 0x59, 0x30, 0x2a, 0xc9, 0x72, 0xb1, + 0x82, 0xc5, 0x84, 0x44, 0x60, 0xba, 0x20, 0x26, 0x41, 0x38, 0x4e, 0xec, 0x51, 0xac, 0x60, 0x8d, + 0x49, 0x6c, 0x60, 0xdf, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xe1, 0x12, 0x60, 0xda, 0xf8, + 0x00, 0x00, 0x00, } diff --git a/pkg/proto/hapi/chart/metadata.pb.go b/pkg/proto/hapi/chart/metadata.pb.go index f049137a5..8cd1ac1f1 100644 --- a/pkg/proto/hapi/chart/metadata.pb.go +++ b/pkg/proto/hapi/chart/metadata.pb.go @@ -32,7 +32,7 @@ var Metadata_Engine_value = map[string]int32{ func (x Metadata_Engine) String() string { return proto.EnumName(Metadata_Engine_name, int32(x)) } -func (Metadata_Engine) EnumDescriptor() ([]byte, []int) { return fileDescriptor2, []int{2, 0} } +func (Metadata_Engine) EnumDescriptor() ([]byte, []int) { return fileDescriptor2, []int{1, 0} } // Maintainer describes a Chart maintainer. type Maintainer struct { @@ -47,22 +47,6 @@ func (m *Maintainer) String() string { return proto.CompactTextString func (*Maintainer) ProtoMessage() {} func (*Maintainer) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} } -// Dependency describes this chart's dependency on another chart. -type Dependency struct { - // Name is the name of the dependency, e.g. 'nginx' - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - // Repository is the repository URL. Appending '/index.yaml' to this should - // return the repo index. - Repository string `protobuf:"bytes,2,opt,name=repository" json:"repository,omitempty"` - // Version is a SemVer 2 version. - Version string `protobuf:"bytes,3,opt,name=version" json:"version,omitempty"` -} - -func (m *Dependency) Reset() { *m = Dependency{} } -func (m *Dependency) String() string { return proto.CompactTextString(m) } -func (*Dependency) ProtoMessage() {} -func (*Dependency) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{1} } - // Metadata for a Chart file. This models the structure of a Chart.yaml file. // // Spec: https://k8s.io/helm/blob/master/docs/design/chart_format.md#the-chart-file @@ -84,14 +68,13 @@ type Metadata struct { // The name of the template engine to use. Defaults to 'gotpl'. Engine string `protobuf:"bytes,8,opt,name=engine" json:"engine,omitempty"` // The URL to an icon file. - Icon string `protobuf:"bytes,9,opt,name=icon" json:"icon,omitempty"` - Dependencies []*Dependency `protobuf:"bytes,10,rep,name=dependencies" json:"dependencies,omitempty"` + Icon string `protobuf:"bytes,9,opt,name=icon" json:"icon,omitempty"` } func (m *Metadata) Reset() { *m = Metadata{} } func (m *Metadata) String() string { return proto.CompactTextString(m) } func (*Metadata) ProtoMessage() {} -func (*Metadata) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{2} } +func (*Metadata) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{1} } func (m *Metadata) GetMaintainers() []*Maintainer { if m != nil { @@ -100,16 +83,8 @@ func (m *Metadata) GetMaintainers() []*Maintainer { return nil } -func (m *Metadata) GetDependencies() []*Dependency { - if m != nil { - return m.Dependencies - } - return nil -} - func init() { proto.RegisterType((*Maintainer)(nil), "hapi.chart.Maintainer") - proto.RegisterType((*Dependency)(nil), "hapi.chart.Dependency") proto.RegisterType((*Metadata)(nil), "hapi.chart.Metadata") proto.RegisterEnum("hapi.chart.Metadata_Engine", Metadata_Engine_name, Metadata_Engine_value) } @@ -117,26 +92,23 @@ func init() { func init() { proto.RegisterFile("hapi/chart/metadata.proto", fileDescriptor2) } var fileDescriptor2 = []byte{ - // 327 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x52, 0x41, 0x4b, 0xf3, 0x40, - 0x10, 0xfd, 0xda, 0x34, 0x49, 0x3b, 0xf9, 0x0e, 0x65, 0x90, 0xb2, 0x7a, 0x90, 0x90, 0x53, 0x4f, - 0x29, 0x28, 0x88, 0x78, 0x14, 0xc5, 0x83, 0xb6, 0x95, 0xa2, 0x08, 0xbd, 0xad, 0xc9, 0x60, 0x17, - 0xcd, 0x6e, 0xd8, 0x5d, 0x95, 0xfe, 0x63, 0x7f, 0x86, 0x64, 0x9b, 0x36, 0x29, 0xf6, 0x36, 0x6f, - 0xde, 0xcc, 0x7b, 0x99, 0x97, 0x85, 0xe3, 0x15, 0x2f, 0xc5, 0x24, 0x5b, 0x71, 0x6d, 0x27, 0x05, - 0x59, 0x9e, 0x73, 0xcb, 0xd3, 0x52, 0x2b, 0xab, 0x10, 0x2a, 0x2a, 0x75, 0x54, 0x72, 0x01, 0x30, - 0xe5, 0x42, 0x5a, 0x2e, 0x24, 0x69, 0x44, 0xe8, 0x49, 0x5e, 0x10, 0xeb, 0xc4, 0x9d, 0xf1, 0x60, - 0xe1, 0x6a, 0x3c, 0x02, 0x9f, 0x0a, 0x2e, 0x3e, 0x58, 0xd7, 0x35, 0x37, 0x20, 0x59, 0x02, 0xdc, - 0x50, 0x49, 0x32, 0x27, 0x99, 0xad, 0x0f, 0xee, 0x9d, 0x02, 0x68, 0x2a, 0x95, 0x11, 0x56, 0xe9, - 0x75, 0xbd, 0xdc, 0xea, 0x20, 0x83, 0xf0, 0x8b, 0xb4, 0x11, 0x4a, 0x32, 0xcf, 0x91, 0x5b, 0x98, - 0xfc, 0x74, 0xa1, 0x3f, 0xad, 0x3f, 0xf9, 0xa0, 0x34, 0x42, 0x6f, 0xa5, 0x0a, 0xaa, 0x45, 0x5d, - 0x5d, 0xc9, 0x19, 0xf5, 0xa9, 0x33, 0x32, 0xcc, 0x8b, 0xbd, 0x4a, 0xae, 0x86, 0x6d, 0xa3, 0xde, - 0x9e, 0x11, 0xc6, 0x10, 0xe5, 0x64, 0x32, 0x2d, 0x4a, 0x5b, 0xb1, 0xbe, 0x63, 0xdb, 0x2d, 0x3c, - 0x81, 0xfe, 0x3b, 0xad, 0xbf, 0x95, 0xce, 0x0d, 0x0b, 0x9c, 0xec, 0x0e, 0xe3, 0x25, 0x44, 0xc5, - 0x2e, 0x3a, 0xc3, 0xc2, 0xd8, 0x1b, 0x47, 0x67, 0xa3, 0xb4, 0x09, 0x37, 0x6d, 0x92, 0x5d, 0xb4, - 0x47, 0x71, 0x04, 0x01, 0xc9, 0x37, 0x21, 0x89, 0xf5, 0x9d, 0x65, 0x8d, 0xaa, 0xbb, 0x44, 0xa6, - 0x24, 0x1b, 0x6c, 0xee, 0xaa, 0x6a, 0xbc, 0x82, 0xff, 0xf9, 0x36, 0x68, 0x41, 0x86, 0xc1, 0x5f, - 0x9b, 0xe6, 0x47, 0x2c, 0xf6, 0x66, 0x93, 0x18, 0x82, 0xdb, 0x8d, 0x72, 0x04, 0xe1, 0xf3, 0xec, - 0x7e, 0x36, 0x7f, 0x99, 0x0d, 0xff, 0xe1, 0x00, 0xfc, 0xbb, 0xf9, 0xd3, 0xe3, 0xc3, 0xb0, 0x73, - 0x1d, 0x2e, 0x7d, 0xa7, 0xf1, 0x1a, 0xb8, 0xa7, 0x71, 0xfe, 0x1b, 0x00, 0x00, 0xff, 0xff, 0xb4, - 0xec, 0x4d, 0xaf, 0x37, 0x02, 0x00, 0x00, + // 275 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x91, 0x4b, 0x4b, 0xc4, 0x30, + 0x14, 0x85, 0x9d, 0x47, 0x5f, 0xb7, 0x9b, 0xe1, 0x22, 0x43, 0x74, 0x55, 0xba, 0x72, 0xd5, 0x01, + 0x05, 0x71, 0x2d, 0x88, 0x0b, 0x9d, 0x8e, 0x0c, 0x8a, 0xe0, 0x2e, 0xb6, 0xc1, 0x06, 0x6d, 0x53, + 0x92, 0xa8, 0xf8, 0x9f, 0xfc, 0x91, 0x26, 0xb7, 0xf3, 0x5a, 0xb8, 0x28, 0x9c, 0x73, 0xbe, 0xde, + 0xdc, 0x9e, 0x06, 0x4e, 0x1a, 0xde, 0xcb, 0x45, 0xd5, 0x70, 0x6d, 0x17, 0xad, 0xb0, 0xbc, 0xe6, + 0x96, 0x17, 0xbd, 0x56, 0x56, 0x21, 0x78, 0x54, 0x10, 0xca, 0x2f, 0x01, 0x96, 0x5c, 0x76, 0xd6, + 0x3d, 0x42, 0x23, 0xc2, 0xb4, 0xe3, 0xad, 0x60, 0xa3, 0x6c, 0x74, 0x96, 0xac, 0x49, 0xe3, 0x31, + 0x04, 0xa2, 0xe5, 0xf2, 0x83, 0x8d, 0x29, 0x1c, 0x4c, 0xfe, 0x3b, 0x86, 0x78, 0xb9, 0x39, 0xf6, + 0xdf, 0x31, 0x97, 0x35, 0xca, 0x65, 0xc3, 0x14, 0x69, 0x64, 0x10, 0x19, 0xf5, 0xa9, 0x2b, 0x61, + 0xd8, 0x24, 0x9b, 0xb8, 0x78, 0x6b, 0x3d, 0xf9, 0x12, 0xda, 0x48, 0xd5, 0xb1, 0x29, 0x0d, 0x6c, + 0x2d, 0x66, 0x90, 0xd6, 0xc2, 0x54, 0x5a, 0xf6, 0xd6, 0xd3, 0x80, 0xe8, 0x61, 0x84, 0xa7, 0x10, + 0xbf, 0x8b, 0x9f, 0x6f, 0xa5, 0x6b, 0xc3, 0x42, 0x3a, 0x76, 0xe7, 0xf1, 0x0a, 0xd2, 0x76, 0x57, + 0xcf, 0xb0, 0xc8, 0xe1, 0xf4, 0x7c, 0x5e, 0xec, 0x7f, 0x40, 0xb1, 0x6f, 0xbf, 0x3e, 0x7c, 0x15, + 0xe7, 0x10, 0x8a, 0xee, 0xcd, 0x69, 0x16, 0xd3, 0xca, 0x8d, 0xf3, 0xbd, 0x64, 0xe5, 0x3e, 0x24, + 0x19, 0x7a, 0x79, 0x9d, 0x67, 0x10, 0xde, 0x0c, 0x34, 0x85, 0xe8, 0xa9, 0xbc, 0x2b, 0x57, 0xcf, + 0xe5, 0xec, 0x08, 0x13, 0x08, 0x6e, 0x57, 0x8f, 0x0f, 0xf7, 0xb3, 0xd1, 0x75, 0xf4, 0x12, 0xd0, + 0xba, 0xd7, 0x90, 0xae, 0xe0, 0xe2, 0x2f, 0x00, 0x00, 0xff, 0xff, 0x72, 0xdf, 0x74, 0xb5, 0x9f, + 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/chart/template.pb.go b/pkg/proto/hapi/chart/template.pb.go index 2bed587b5..aecab641f 100644 --- a/pkg/proto/hapi/chart/template.pb.go +++ b/pkg/proto/hapi/chart/template.pb.go @@ -36,12 +36,12 @@ func init() { func init() { proto.RegisterFile("hapi/chart/template.proto", fileDescriptor3) } var fileDescriptor3 = []byte{ - // 107 bytes of a gzipped FileDescriptorProto + // 106 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8, 0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0xd1, 0x2f, 0x49, 0xcd, 0x2d, 0xc8, 0x49, 0x2c, 0x49, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x02, 0x49, 0xe9, 0x81, 0xa5, 0x94, 0x8c, 0xb8, 0x38, 0x42, 0xa0, 0xb2, 0x42, 0x42, 0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, - 0x9c, 0x41, 0x60, 0x36, 0x48, 0x2c, 0x25, 0xb1, 0x24, 0x51, 0x82, 0x49, 0x81, 0x51, 0x83, 0x27, - 0x08, 0xcc, 0x76, 0x62, 0x8f, 0x62, 0x05, 0x6b, 0x4e, 0x62, 0x03, 0x9b, 0x67, 0x0c, 0x08, 0x00, - 0x00, 0xff, 0xff, 0x53, 0xee, 0x0e, 0x67, 0x6c, 0x00, 0x00, 0x00, + 0x9c, 0x41, 0x60, 0x36, 0x48, 0x2c, 0x25, 0xb1, 0x24, 0x51, 0x82, 0x09, 0x28, 0xc6, 0x13, 0x04, + 0x66, 0x3b, 0xb1, 0x47, 0xb1, 0x82, 0x35, 0x27, 0xb1, 0x81, 0xcd, 0x33, 0x06, 0x04, 0x00, 0x00, + 0xff, 0xff, 0x53, 0xee, 0x0e, 0x67, 0x6c, 0x00, 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/hook.pb.go b/pkg/proto/hapi/release/hook.pb.go index 57581b14e..0da69d40e 100644 --- a/pkg/proto/hapi/release/hook.pb.go +++ b/pkg/proto/hapi/release/hook.pb.go @@ -112,26 +112,25 @@ func init() { func init() { proto.RegisterFile("hapi/release/hook.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 321 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0x4d, 0x6e, 0xea, 0x30, - 0x14, 0x85, 0x5f, 0x20, 0x24, 0x70, 0xe1, 0xd1, 0xd4, 0x93, 0x5a, 0x4c, 0x8a, 0x18, 0x31, 0x72, - 0x2a, 0xaa, 0x2e, 0x00, 0x8a, 0xd5, 0x56, 0x44, 0x01, 0x99, 0xa0, 0x4a, 0x9d, 0x20, 0xa3, 0x1a, - 0x88, 0x20, 0x71, 0x44, 0x4c, 0xd7, 0xd3, 0xf5, 0x75, 0x15, 0x95, 0x9d, 0x1f, 0x75, 0x76, 0xfd, - 0xdd, 0xcf, 0xc7, 0x3e, 0x70, 0x77, 0xe4, 0x59, 0xec, 0x5f, 0xc4, 0x59, 0xf0, 0x5c, 0xf8, 0x47, - 0x29, 0x4f, 0x24, 0xbb, 0x48, 0x25, 0x51, 0x4f, 0x2f, 0x48, 0xb9, 0x18, 0xdc, 0x1f, 0xa4, 0x3c, - 0x9c, 0x85, 0x6f, 0x76, 0xbb, 0xeb, 0xde, 0x57, 0x71, 0x22, 0x72, 0xc5, 0x93, 0xac, 0xd0, 0x47, - 0x3f, 0x0d, 0xb0, 0x5f, 0xa5, 0x3c, 0x21, 0x04, 0x76, 0xca, 0x13, 0x81, 0xad, 0xa1, 0x35, 0xee, - 0x30, 0x33, 0x6b, 0x76, 0x8a, 0xd3, 0x4f, 0xdc, 0x28, 0x98, 0x9e, 0x35, 0xcb, 0xb8, 0x3a, 0xe2, - 0x66, 0xc1, 0xf4, 0x8c, 0x06, 0xd0, 0x4e, 0x78, 0x1a, 0xef, 0x45, 0xae, 0xb0, 0x6d, 0x78, 0x7d, - 0x46, 0x0f, 0xe0, 0x88, 0x2f, 0x91, 0xaa, 0x1c, 0xb7, 0x86, 0xcd, 0x71, 0x7f, 0x82, 0xc9, 0xdf, - 0x0f, 0x12, 0xfd, 0x36, 0xa1, 0x5a, 0x60, 0xa5, 0x87, 0x9e, 0xa0, 0x7d, 0xe6, 0xb9, 0xda, 0x5e, - 0xae, 0x29, 0x76, 0x86, 0xd6, 0xb8, 0x3b, 0x19, 0x90, 0xa2, 0x06, 0xa9, 0x6a, 0x90, 0xa8, 0xaa, - 0xc1, 0x5c, 0xed, 0xb2, 0x6b, 0x3a, 0xfa, 0xb6, 0xa0, 0x65, 0x82, 0x50, 0x17, 0xdc, 0x4d, 0xb8, - 0x08, 0x97, 0xef, 0xa1, 0xf7, 0x0f, 0xdd, 0x40, 0x77, 0xc5, 0xe8, 0xf6, 0x2d, 0x5c, 0x47, 0xd3, - 0x20, 0xf0, 0x2c, 0xe4, 0x41, 0x6f, 0xb5, 0x5c, 0x47, 0x35, 0x69, 0xa0, 0x3e, 0x80, 0x56, 0xe6, - 0x34, 0xa0, 0x11, 0xf5, 0x9a, 0xe6, 0x8a, 0x36, 0x4a, 0x60, 0x57, 0x19, 0x9b, 0xd5, 0x0b, 0x9b, - 0xce, 0xa9, 0xd7, 0xaa, 0x33, 0x2a, 0xe2, 0x18, 0xc2, 0xe8, 0x96, 0x2d, 0x83, 0x60, 0x36, 0x7d, - 0x5e, 0x78, 0x2e, 0xba, 0x85, 0xff, 0xc6, 0xa9, 0x51, 0x7b, 0xd6, 0xf9, 0x70, 0xcb, 0xde, 0x3b, - 0xc7, 0x54, 0x79, 0xfc, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xa4, 0x2e, 0x6f, 0xbd, 0xc8, 0x01, 0x00, - 0x00, + // 320 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0xcd, 0x4e, 0xf2, 0x40, + 0x14, 0x86, 0xbf, 0x42, 0x69, 0xe1, 0xc0, 0x87, 0x75, 0x36, 0x4e, 0xd8, 0x48, 0x58, 0xb1, 0x9a, + 0x1a, 0x8c, 0x17, 0x00, 0xd2, 0xa8, 0xa1, 0x29, 0x64, 0x28, 0x31, 0x71, 0x43, 0x86, 0x38, 0x40, + 0x03, 0xed, 0x34, 0x74, 0xf0, 0x7a, 0xbc, 0x3e, 0xaf, 0xc2, 0xce, 0xf4, 0x27, 0xee, 0x4e, 0x9f, + 0xf3, 0x9c, 0xb7, 0xf3, 0xc2, 0xdd, 0x91, 0xa5, 0x91, 0x7b, 0xe1, 0x67, 0xce, 0x32, 0xee, 0x1e, + 0x85, 0x38, 0x91, 0xf4, 0x22, 0xa4, 0x40, 0x3d, 0xb5, 0x20, 0xe5, 0x62, 0x70, 0x7f, 0x10, 0xe2, + 0x70, 0xe6, 0xae, 0xde, 0xed, 0xae, 0x7b, 0x57, 0x46, 0x31, 0xcf, 0x24, 0x8b, 0xd3, 0x42, 0x1f, + 0xfd, 0x34, 0xc0, 0x7c, 0xcd, 0xaf, 0x11, 0x02, 0x33, 0x61, 0x31, 0xc7, 0xc6, 0xd0, 0x18, 0x77, + 0xa8, 0x9e, 0x15, 0x3b, 0x45, 0xc9, 0x27, 0x6e, 0x14, 0x4c, 0xcd, 0x8a, 0xa5, 0x4c, 0x1e, 0x71, + 0xb3, 0x60, 0x6a, 0x46, 0x03, 0x68, 0xc7, 0x2c, 0x89, 0xf6, 0x79, 0x32, 0x36, 0x35, 0xaf, 0xbf, + 0xd1, 0x03, 0x58, 0xfc, 0x8b, 0x27, 0x32, 0xc3, 0xad, 0x61, 0x73, 0xdc, 0x9f, 0x60, 0xf2, 0xf7, + 0x81, 0x44, 0xfd, 0x9b, 0x78, 0x4a, 0xa0, 0xa5, 0x87, 0x9e, 0xa0, 0x7d, 0x66, 0x99, 0xdc, 0x5e, + 0xae, 0x09, 0xb6, 0xf2, 0xb4, 0xee, 0x64, 0x40, 0x8a, 0x1a, 0xa4, 0xaa, 0x41, 0xc2, 0xaa, 0x06, + 0xb5, 0x95, 0x4b, 0xaf, 0xc9, 0xe8, 0xdb, 0x80, 0x96, 0x0e, 0x42, 0x5d, 0xb0, 0x37, 0xc1, 0x22, + 0x58, 0xbe, 0x07, 0xce, 0x3f, 0x74, 0x03, 0xdd, 0x15, 0xf5, 0xb6, 0x6f, 0xc1, 0x3a, 0x9c, 0xfa, + 0xbe, 0x63, 0x20, 0x07, 0x7a, 0xab, 0xe5, 0x3a, 0xac, 0x49, 0x03, 0xf5, 0x01, 0x94, 0x32, 0xf7, + 0x7c, 0x2f, 0xf4, 0x9c, 0xa6, 0x3e, 0x51, 0x46, 0x09, 0xcc, 0x2a, 0x63, 0xb3, 0x7a, 0xa1, 0xd3, + 0xb9, 0xe7, 0xb4, 0xea, 0x8c, 0x8a, 0x58, 0x9a, 0xe4, 0x0a, 0x5d, 0xfa, 0xfe, 0x6c, 0xfa, 0xbc, + 0x70, 0x6c, 0x74, 0x0b, 0xff, 0xb5, 0x53, 0xa3, 0xf6, 0xac, 0xf3, 0x61, 0x97, 0xbd, 0x77, 0x96, + 0xae, 0xf2, 0xf8, 0x1b, 0x00, 0x00, 0xff, 0xff, 0xa4, 0x2e, 0x6f, 0xbd, 0xc8, 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/info.pb.go b/pkg/proto/hapi/release/info.pb.go index a63a039cd..d5e843df5 100644 --- a/pkg/proto/hapi/release/info.pb.go +++ b/pkg/proto/hapi/release/info.pb.go @@ -63,19 +63,18 @@ func init() { func init() { proto.RegisterFile("hapi/release/info.proto", fileDescriptor1) } var fileDescriptor1 = []byte{ - // 212 bytes of a gzipped FileDescriptorProto + // 208 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0x48, 0x2c, 0xc8, 0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0xcf, 0xcc, 0x4b, 0xcb, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x01, 0x49, 0xe8, 0x41, 0x25, 0xa4, 0xe4, 0xd3, 0xf3, 0xf3, 0xd3, 0x73, 0x52, 0xf5, 0xc1, 0x72, 0x49, 0xa5, 0x69, 0xfa, 0x25, 0x99, 0xb9, 0xa9, 0xc5, 0x25, 0x89, - 0xb9, 0x05, 0x10, 0xe5, 0x52, 0x92, 0x28, 0xe6, 0x14, 0x97, 0x24, 0x96, 0x94, 0x16, 0x43, 0xa4, - 0x94, 0xde, 0x31, 0x72, 0xb1, 0x78, 0xe6, 0xa5, 0xe5, 0x0b, 0xe9, 0x70, 0xb1, 0x41, 0x24, 0x24, - 0x18, 0x15, 0x18, 0x35, 0xb8, 0x8d, 0x44, 0xf4, 0x90, 0xed, 0xd0, 0x0b, 0x06, 0xcb, 0x05, 0x41, - 0xd5, 0x08, 0x39, 0x72, 0xf1, 0xa5, 0x65, 0x16, 0x15, 0x97, 0xc4, 0xa7, 0xa4, 0x16, 0xe4, 0xe4, - 0x57, 0xa6, 0xa6, 0x48, 0x30, 0x81, 0x75, 0x49, 0xe9, 0x41, 0xdc, 0xa2, 0x07, 0x73, 0x8b, 0x5e, - 0x08, 0xcc, 0x2d, 0x41, 0xbc, 0x60, 0x1d, 0x2e, 0x50, 0x0d, 0x42, 0xf6, 0x5c, 0xbc, 0x39, 0x89, - 0xc8, 0x26, 0x30, 0x13, 0x34, 0x81, 0x07, 0xa4, 0x01, 0x6e, 0x80, 0x09, 0x17, 0x7b, 0x4a, 0x6a, - 0x4e, 0x6a, 0x49, 0x6a, 0x8a, 0x04, 0x0b, 0x41, 0xad, 0x30, 0xa5, 0x4e, 0x9c, 0x51, 0xec, 0x50, - 0x3f, 0x25, 0xb1, 0x81, 0xd5, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xeb, 0x9d, 0xa1, 0xf8, - 0x67, 0x01, 0x00, 0x00, + 0xb9, 0x05, 0x10, 0xe5, 0x52, 0x92, 0x28, 0xe6, 0x00, 0x65, 0x4a, 0x4a, 0x8b, 0x21, 0x52, 0x4a, + 0xef, 0x18, 0xb9, 0x58, 0x3c, 0x81, 0x06, 0x0b, 0xe9, 0x70, 0xb1, 0x41, 0x24, 0x24, 0x18, 0x15, + 0x18, 0x35, 0xb8, 0x8d, 0x44, 0xf4, 0x90, 0xed, 0xd0, 0x0b, 0x06, 0xcb, 0x05, 0x41, 0xd5, 0x08, + 0x39, 0x72, 0xf1, 0xa5, 0x65, 0x16, 0x15, 0x97, 0xc4, 0xa7, 0xa4, 0x16, 0xe4, 0xe4, 0x57, 0xa6, + 0xa6, 0x48, 0x30, 0x81, 0x75, 0x49, 0xe9, 0x41, 0xdc, 0xa2, 0x07, 0x73, 0x8b, 0x5e, 0x08, 0xcc, + 0x2d, 0x41, 0xbc, 0x60, 0x1d, 0x2e, 0x50, 0x0d, 0x42, 0xf6, 0x5c, 0xbc, 0x39, 0x89, 0xc8, 0x26, + 0x30, 0x13, 0x34, 0x81, 0x07, 0xa4, 0x01, 0x6e, 0x80, 0x09, 0x17, 0x7b, 0x0a, 0xd0, 0x75, 0x25, + 0x40, 0xad, 0x2c, 0x04, 0xb5, 0xc2, 0x94, 0x3a, 0x71, 0x46, 0xb1, 0x43, 0xfd, 0x94, 0xc4, 0x06, + 0x56, 0x67, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0xeb, 0x9d, 0xa1, 0xf8, 0x67, 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/release.pb.go b/pkg/proto/hapi/release/release.pb.go index 72255e3e2..561930f1f 100644 --- a/pkg/proto/hapi/release/release.pb.go +++ b/pkg/proto/hapi/release/release.pb.go @@ -77,21 +77,21 @@ func init() { func init() { proto.RegisterFile("hapi/release/release.proto", fileDescriptor2) } var fileDescriptor2 = []byte{ - // 256 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x90, 0xbf, 0x4e, 0xc3, 0x40, - 0x0c, 0xc6, 0x95, 0x36, 0x7f, 0x1a, 0xc3, 0x82, 0x07, 0xb0, 0x22, 0x86, 0x88, 0x01, 0x22, 0x86, - 0x54, 0x82, 0x37, 0x80, 0x05, 0xd6, 0x1b, 0xd9, 0x8e, 0xe8, 0x42, 0x4e, 0xa5, 0xe7, 0x28, 0x17, - 0xf1, 0x2c, 0x3c, 0x2e, 0xba, 0x3f, 0x85, 0x94, 0x2e, 0x4e, 0xec, 0xdf, 0xa7, 0xcf, 0xdf, 0x19, - 0xaa, 0x41, 0x8e, 0x7a, 0x3b, 0xa9, 0x4f, 0x25, 0xad, 0x3a, 0x7c, 0xdb, 0x71, 0xe2, 0x99, 0xf1, - 0xdc, 0xb1, 0x36, 0xce, 0xaa, 0xab, 0x23, 0xe5, 0xc0, 0xbc, 0x0b, 0xb2, 0x7f, 0x40, 0x9b, 0x9e, - 0x8f, 0x40, 0x37, 0xc8, 0x69, 0xde, 0x76, 0x6c, 0x7a, 0xfd, 0x11, 0xc1, 0xe5, 0x12, 0xb8, 0x1a, - 0xe6, 0x37, 0xdf, 0x2b, 0x28, 0x44, 0xf0, 0x41, 0x84, 0xd4, 0xc8, 0xbd, 0xa2, 0xa4, 0x4e, 0x9a, - 0x52, 0xf8, 0x7f, 0xbc, 0x85, 0xd4, 0xd9, 0xd3, 0xaa, 0x4e, 0x9a, 0xb3, 0x07, 0x6c, 0x97, 0xf9, - 0xda, 0x57, 0xd3, 0xb3, 0xf0, 0x1c, 0xef, 0x20, 0xf3, 0xb6, 0xb4, 0xf6, 0xc2, 0x8b, 0x20, 0x0c, - 0x9b, 0x9e, 0x5d, 0x15, 0x81, 0xe3, 0x3d, 0xe4, 0x21, 0x18, 0xa5, 0x4b, 0xcb, 0xa8, 0xf4, 0x44, - 0x44, 0x05, 0x56, 0xb0, 0xd9, 0x4b, 0xa3, 0x7b, 0x65, 0x67, 0xca, 0x7c, 0xa8, 0xdf, 0x1e, 0x1b, - 0xc8, 0xdc, 0x41, 0x2c, 0xe5, 0xf5, 0xfa, 0x34, 0xd9, 0x0b, 0xf3, 0x4e, 0x04, 0x01, 0x12, 0x14, - 0x5f, 0x6a, 0xb2, 0x9a, 0x0d, 0x15, 0x75, 0xd2, 0x64, 0xe2, 0xd0, 0xe2, 0x35, 0x94, 0xee, 0x91, - 0x76, 0x94, 0x9d, 0xa2, 0x8d, 0x5f, 0xf0, 0x37, 0x78, 0x2a, 0xdf, 0x8a, 0x68, 0xf7, 0x9e, 0xfb, - 0x63, 0x3d, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, 0xc8, 0x8f, 0xec, 0x97, 0xbb, 0x01, 0x00, 0x00, + // 254 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x90, 0x3f, 0x4f, 0xc3, 0x30, + 0x10, 0xc5, 0xd5, 0x36, 0x7f, 0x9a, 0x83, 0x85, 0x1b, 0xe0, 0x14, 0x31, 0x54, 0x0c, 0x50, 0x31, + 0xa4, 0x12, 0x7c, 0x03, 0x58, 0x60, 0xf5, 0xc8, 0x66, 0x22, 0x87, 0x58, 0x50, 0x3b, 0x8a, 0x23, + 0x3e, 0x0b, 0x1f, 0x17, 0xdb, 0xe7, 0x42, 0x0a, 0x8b, 0x13, 0xbf, 0xdf, 0xd3, 0xbb, 0xe7, 0x83, + 0xba, 0x97, 0x83, 0xde, 0x8d, 0xea, 0x43, 0x49, 0xa7, 0x0e, 0xdf, 0x66, 0x18, 0xed, 0x64, 0xf1, + 0x34, 0xb0, 0x26, 0x69, 0xf5, 0xc5, 0x91, 0xb3, 0xb7, 0xf6, 0x9d, 0x6d, 0x7f, 0x80, 0x36, 0x9d, + 0x3d, 0x02, 0x6d, 0x2f, 0xc7, 0x69, 0xd7, 0x5a, 0xd3, 0xe9, 0xb7, 0x04, 0xce, 0xe7, 0x20, 0x9c, + 0xac, 0x5f, 0x7d, 0x2d, 0xa1, 0x14, 0x9c, 0x83, 0x08, 0x99, 0x91, 0x7b, 0x45, 0x8b, 0xcd, 0x62, + 0x5b, 0x89, 0xf8, 0x8f, 0xd7, 0x90, 0x85, 0x78, 0x5a, 0x7a, 0xed, 0xe4, 0x0e, 0x9b, 0x79, 0xbf, + 0xe6, 0xd9, 0x13, 0x11, 0x39, 0xde, 0x40, 0x1e, 0x63, 0x69, 0x15, 0x8d, 0x67, 0x6c, 0xe4, 0x49, + 0x8f, 0xe1, 0x14, 0xcc, 0xf1, 0x16, 0x0a, 0x2e, 0x46, 0xd9, 0x3c, 0x32, 0x39, 0x23, 0x11, 0xc9, + 0x81, 0x35, 0xac, 0xf7, 0xd2, 0xe8, 0x4e, 0xb9, 0x89, 0xf2, 0x58, 0xea, 0xe7, 0x8e, 0x5b, 0xc8, + 0xc3, 0x42, 0x1c, 0x15, 0x9b, 0xd5, 0xff, 0x66, 0x4f, 0x1e, 0x09, 0x36, 0x20, 0x41, 0xf9, 0xa9, + 0x46, 0xa7, 0xad, 0xa1, 0xd2, 0x87, 0xe4, 0xe2, 0x70, 0xc5, 0x4b, 0xa8, 0xc2, 0x23, 0xdd, 0x20, + 0x5b, 0x45, 0xeb, 0x38, 0xe0, 0x57, 0x78, 0xa8, 0x5e, 0xca, 0x14, 0xf7, 0x5a, 0xc4, 0x65, 0xdd, + 0x7f, 0x07, 0x00, 0x00, 0xff, 0xff, 0xc8, 0x8f, 0xec, 0x97, 0xbb, 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/status.pb.go b/pkg/proto/hapi/release/status.pb.go index 79c721e31..6be0f74ac 100644 --- a/pkg/proto/hapi/release/status.pb.go +++ b/pkg/proto/hapi/release/status.pb.go @@ -79,22 +79,22 @@ func init() { func init() { proto.RegisterFile("hapi/release/status.proto", fileDescriptor3) } var fileDescriptor3 = []byte{ - // 261 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0xc1, 0x4e, 0x83, 0x40, - 0x10, 0x86, 0xdd, 0x16, 0x41, 0xa6, 0x4d, 0x43, 0x36, 0x3d, 0x80, 0xf1, 0x40, 0x7a, 0xe2, 0xe2, - 0x92, 0xd4, 0x27, 0xa8, 0xee, 0x9a, 0xa8, 0x84, 0x36, 0x60, 0x63, 0xf4, 0x46, 0xcb, 0x58, 0x9b, - 0x10, 0xb6, 0x61, 0x97, 0x43, 0x9f, 0xd8, 0xd7, 0x30, 0x2c, 0x6d, 0xec, 0x71, 0xe6, 0xfb, 0x66, - 0xfe, 0x19, 0x08, 0x7e, 0x8a, 0xc3, 0x3e, 0x6e, 0xb0, 0xc2, 0x42, 0x61, 0xac, 0x74, 0xa1, 0x5b, - 0xc5, 0x0e, 0x8d, 0xd4, 0x92, 0x8e, 0x3b, 0xc4, 0x4e, 0xe8, 0x36, 0xd8, 0x49, 0xb9, 0xab, 0x30, - 0x36, 0x6c, 0xd3, 0x7e, 0xc7, 0x45, 0x7d, 0xec, 0xc5, 0xd9, 0x2f, 0x01, 0x3b, 0x37, 0x93, 0xf4, - 0x1e, 0xac, 0xad, 0x2c, 0xd1, 0x27, 0x21, 0x89, 0x26, 0xf3, 0x80, 0x5d, 0xae, 0x60, 0xbd, 0xc3, - 0x9e, 0x64, 0x89, 0x99, 0xd1, 0x28, 0x03, 0xa7, 0x44, 0x5d, 0xec, 0x2b, 0xe5, 0x0f, 0x42, 0x12, - 0x8d, 0xe6, 0x53, 0xd6, 0xc7, 0xb0, 0x73, 0x0c, 0x5b, 0xd4, 0xc7, 0xec, 0x2c, 0xd1, 0x3b, 0x70, - 0x1b, 0x54, 0xb2, 0x6d, 0xb6, 0xa8, 0xfc, 0x61, 0x48, 0x22, 0x37, 0xfb, 0x6f, 0xd0, 0x29, 0x5c, - 0xd7, 0x52, 0xa3, 0xf2, 0x2d, 0x43, 0xfa, 0x62, 0xf6, 0x0a, 0x56, 0x97, 0x48, 0x47, 0xe0, 0xac, - 0xd3, 0xb7, 0x74, 0xf9, 0x91, 0x7a, 0x57, 0x74, 0x0c, 0x37, 0x5c, 0xac, 0x92, 0xe5, 0xa7, 0xe0, - 0x1e, 0xe9, 0x10, 0x17, 0x89, 0x78, 0x17, 0xdc, 0x1b, 0xd0, 0x09, 0x40, 0xbe, 0x5e, 0x89, 0x2c, - 0x17, 0x5c, 0x70, 0x6f, 0x48, 0x01, 0xec, 0xe7, 0xc5, 0x4b, 0x22, 0xb8, 0x67, 0x3d, 0xba, 0x5f, - 0xce, 0xe9, 0x99, 0x8d, 0x6d, 0x2e, 0x7c, 0xf8, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xae, 0x07, 0x47, - 0x1f, 0x41, 0x01, 0x00, 0x00, + // 259 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0xc1, 0x4e, 0xf2, 0x40, + 0x14, 0x85, 0xff, 0x42, 0xff, 0xd6, 0x5e, 0x08, 0x21, 0x37, 0x2c, 0x5a, 0xe3, 0xc2, 0xb0, 0x72, + 0xe3, 0x6d, 0x82, 0x4f, 0x80, 0x76, 0x4c, 0xd4, 0xa6, 0x90, 0x56, 0x62, 0x74, 0x37, 0xc0, 0x88, + 0x24, 0x4d, 0x87, 0x74, 0xa6, 0x0b, 0x9e, 0xd8, 0xd7, 0x70, 0x3a, 0x85, 0xe8, 0xae, 0xa7, 0xdf, + 0x77, 0xe6, 0xcc, 0x40, 0xf4, 0xc5, 0x0f, 0xfb, 0xb8, 0x16, 0xa5, 0xe0, 0x4a, 0xc4, 0x4a, 0x73, + 0xdd, 0x28, 0x3a, 0xd4, 0x52, 0x4b, 0x1c, 0xb6, 0x88, 0x4e, 0xe8, 0x32, 0xda, 0x49, 0xb9, 0x2b, + 0x45, 0x6c, 0xd9, 0xba, 0xf9, 0x8c, 0x79, 0x75, 0xec, 0xc4, 0xe9, 0xb7, 0x03, 0x5e, 0x61, 0x9b, + 0x78, 0x0b, 0xee, 0x46, 0x6e, 0x45, 0xe8, 0x5c, 0x3b, 0x37, 0xa3, 0x59, 0x44, 0x7f, 0x8f, 0xa0, + 0xce, 0xa1, 0x07, 0x23, 0xe4, 0x56, 0x43, 0x02, 0x7f, 0x2b, 0x34, 0xdf, 0x97, 0x2a, 0xec, 0x99, + 0xc6, 0x60, 0x36, 0xa1, 0x6e, 0x86, 0xce, 0x33, 0x34, 0xaf, 0x8e, 0xf9, 0x59, 0xc2, 0x2b, 0x08, + 0x6a, 0xa1, 0x64, 0x53, 0x6f, 0x84, 0x0a, 0xfb, 0xa6, 0x11, 0xe4, 0xbf, 0x3f, 0x70, 0x02, 0xff, + 0x2b, 0xa9, 0x0d, 0x71, 0x2d, 0xe9, 0xc2, 0xf4, 0x19, 0xdc, 0x76, 0x11, 0x07, 0xe0, 0xaf, 0xb2, + 0x97, 0x6c, 0xf1, 0x96, 0x8d, 0xff, 0xe1, 0x10, 0x2e, 0x12, 0xb6, 0x4c, 0x17, 0xef, 0x2c, 0x19, + 0x3b, 0x2d, 0x4a, 0x58, 0xca, 0x5e, 0x4d, 0xe8, 0xe1, 0x08, 0xa0, 0x58, 0x2d, 0x59, 0x5e, 0xb0, + 0xc4, 0xe4, 0x3e, 0x02, 0x78, 0x8f, 0xf3, 0xa7, 0xd4, 0x7c, 0xbb, 0xf7, 0xc1, 0x87, 0x7f, 0x7a, + 0xcc, 0xda, 0xb3, 0x37, 0xbc, 0xfb, 0x09, 0x00, 0x00, 0xff, 0xff, 0xae, 0x07, 0x47, 0x1f, 0x41, + 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index 6b2ddb6de..ca556500a 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -809,65 +809,65 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ func init() { proto.RegisterFile("hapi/services/tiller.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 951 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0x5f, 0x6f, 0xe3, 0x44, - 0x10, 0xaf, 0xf3, 0x3f, 0xd3, 0x3f, 0xa4, 0x73, 0x69, 0xe3, 0x5a, 0x80, 0x22, 0x23, 0xb8, 0x70, - 0x70, 0x29, 0x84, 0x57, 0x84, 0xd4, 0xcb, 0x45, 0x6d, 0xb9, 0x92, 0x93, 0x36, 0x14, 0x24, 0x1e, - 0x88, 0xdc, 0x64, 0x73, 0x35, 0xe7, 0x78, 0x83, 0x77, 0x53, 0xd1, 0x77, 0x5e, 0x78, 0xe0, 0x4b, - 0xf0, 0x3d, 0xf8, 0x64, 0xbc, 0x20, 0xef, 0x7a, 0xdd, 0x38, 0xb1, 0xaf, 0xbe, 0xbc, 0xc4, 0xde, - 0x9d, 0x9f, 0x7f, 0x33, 0xf3, 0x9b, 0x9d, 0xd9, 0x80, 0x75, 0xeb, 0x2c, 0xdc, 0x53, 0x4e, 0x83, - 0x3b, 0x77, 0x42, 0xf9, 0xa9, 0x70, 0x3d, 0x8f, 0x06, 0xdd, 0x45, 0xc0, 0x04, 0xc3, 0x66, 0x68, - 0xeb, 0x6a, 0x5b, 0x57, 0xd9, 0xac, 0x63, 0xf9, 0xc5, 0xe4, 0xd6, 0x09, 0x84, 0xfa, 0x55, 0x68, - 0xab, 0xb5, 0xba, 0xcf, 0xfc, 0x99, 0xfb, 0x26, 0x32, 0x28, 0x17, 0x01, 0xf5, 0xa8, 0xc3, 0xa9, - 0x7e, 0x26, 0x3e, 0xd2, 0x36, 0xd7, 0x9f, 0xb1, 0xc8, 0x70, 0x92, 0x30, 0x70, 0xe1, 0x88, 0x25, - 0x4f, 0xf0, 0xdd, 0xd1, 0x80, 0xbb, 0xcc, 0xd7, 0x4f, 0x65, 0xb3, 0xff, 0x29, 0xc0, 0x93, 0x2b, - 0x97, 0x0b, 0xa2, 0x3e, 0xe4, 0x84, 0xfe, 0xbe, 0xa4, 0x5c, 0x60, 0x13, 0xca, 0x9e, 0x3b, 0x77, - 0x85, 0x69, 0xb4, 0x8d, 0x4e, 0x91, 0xa8, 0x05, 0x1e, 0x43, 0x85, 0xcd, 0x66, 0x9c, 0x0a, 0xb3, - 0xd0, 0x36, 0x3a, 0x75, 0x12, 0xad, 0xf0, 0x3b, 0xa8, 0x72, 0x16, 0x88, 0xf1, 0xcd, 0xbd, 0x59, - 0x6c, 0x1b, 0x9d, 0x83, 0xde, 0xa7, 0xdd, 0x34, 0x29, 0xba, 0xa1, 0xa7, 0x11, 0x0b, 0x44, 0x37, - 0xfc, 0x79, 0x71, 0x4f, 0x2a, 0x5c, 0x3e, 0x43, 0xde, 0x99, 0xeb, 0x09, 0x1a, 0x98, 0x25, 0xc5, - 0xab, 0x56, 0x78, 0x0e, 0x20, 0x79, 0x59, 0x30, 0xa5, 0x81, 0x59, 0x96, 0xd4, 0x9d, 0x1c, 0xd4, - 0xaf, 0x43, 0x3c, 0xa9, 0x73, 0xfd, 0x8a, 0xdf, 0xc2, 0x9e, 0x92, 0x64, 0x3c, 0x61, 0x53, 0xca, - 0xcd, 0x4a, 0xbb, 0xd8, 0x39, 0xe8, 0x9d, 0x28, 0x2a, 0xad, 0xf0, 0x48, 0x89, 0xd6, 0x67, 0x53, - 0x4a, 0x76, 0x15, 0x3c, 0x7c, 0xe7, 0xf6, 0xaf, 0x50, 0xd3, 0xf4, 0x76, 0x0f, 0x2a, 0x2a, 0x78, - 0xdc, 0x85, 0xea, 0xf5, 0xf0, 0xd5, 0xf0, 0xf5, 0xcf, 0xc3, 0xc6, 0x0e, 0xd6, 0xa0, 0x34, 0x3c, - 0xfb, 0x61, 0xd0, 0x30, 0xf0, 0x10, 0xf6, 0xaf, 0xce, 0x46, 0x3f, 0x8e, 0xc9, 0xe0, 0x6a, 0x70, - 0x36, 0x1a, 0xbc, 0x6c, 0x14, 0xec, 0x8f, 0xa1, 0x1e, 0x47, 0x85, 0x55, 0x28, 0x9e, 0x8d, 0xfa, - 0xea, 0x93, 0x97, 0x83, 0x51, 0xbf, 0x61, 0xd8, 0x7f, 0x19, 0xd0, 0x4c, 0x16, 0x81, 0x2f, 0x98, - 0xcf, 0x69, 0x58, 0x85, 0x09, 0x5b, 0xfa, 0x71, 0x15, 0xe4, 0x02, 0x11, 0x4a, 0x3e, 0xfd, 0x43, - 0xd7, 0x40, 0xbe, 0x87, 0x48, 0xc1, 0x84, 0xe3, 0x49, 0xfd, 0x8b, 0x44, 0x2d, 0xf0, 0x6b, 0xa8, - 0x45, 0xc9, 0x71, 0xb3, 0xd4, 0x2e, 0x76, 0x76, 0x7b, 0x47, 0xc9, 0x94, 0x23, 0x8f, 0x24, 0x86, - 0xd9, 0xe7, 0xd0, 0x3a, 0xa7, 0x3a, 0x12, 0xa5, 0x88, 0x3e, 0x13, 0xa1, 0x5f, 0x67, 0x4e, 0x65, - 0x30, 0xa1, 0x5f, 0x67, 0x4e, 0xd1, 0x84, 0x6a, 0x74, 0xa0, 0x64, 0x38, 0x65, 0xa2, 0x97, 0xb6, - 0x00, 0x73, 0x93, 0x28, 0xca, 0x2b, 0x8d, 0xe9, 0x33, 0x28, 0x85, 0xc7, 0x59, 0xd2, 0xec, 0xf6, - 0x30, 0x19, 0xe7, 0xa5, 0x3f, 0x63, 0x44, 0xda, 0xf1, 0x43, 0xa8, 0x87, 0x78, 0xbe, 0x70, 0x26, - 0x54, 0x66, 0x5b, 0x27, 0x0f, 0x1b, 0xf6, 0xc5, 0xaa, 0xd7, 0x3e, 0xf3, 0x05, 0xf5, 0xc5, 0x76, - 0xf1, 0x5f, 0xc1, 0x49, 0x0a, 0x53, 0x94, 0xc0, 0x29, 0x54, 0xa3, 0xd0, 0x24, 0x5b, 0xa6, 0xae, - 0x1a, 0x65, 0xff, 0x6b, 0x40, 0xf3, 0x7a, 0x31, 0x75, 0x04, 0xd5, 0xa6, 0x77, 0x04, 0xf5, 0x14, - 0xca, 0x72, 0x2c, 0x44, 0x5a, 0x1c, 0x2a, 0x6e, 0x35, 0x3b, 0xfa, 0xe1, 0x2f, 0x51, 0x76, 0x7c, - 0x06, 0x95, 0x3b, 0xc7, 0x5b, 0x52, 0x2e, 0x85, 0x88, 0x55, 0x8b, 0x90, 0x72, 0xa6, 0x90, 0x08, - 0x81, 0x2d, 0xa8, 0x4e, 0x83, 0xfb, 0x71, 0xb0, 0xf4, 0x65, 0x93, 0xd5, 0x48, 0x65, 0x1a, 0xdc, - 0x93, 0xa5, 0x8f, 0x9f, 0xc0, 0xfe, 0xd4, 0xe5, 0xce, 0x8d, 0x47, 0xc7, 0xb7, 0x8c, 0xbd, 0xe5, - 0xb2, 0xcf, 0x6a, 0x64, 0x2f, 0xda, 0xbc, 0x08, 0xf7, 0xec, 0x0b, 0x38, 0x5a, 0x0b, 0x7f, 0x5b, - 0x25, 0xfe, 0x34, 0xe0, 0x98, 0x30, 0xcf, 0xbb, 0x71, 0x26, 0x6f, 0x73, 0x68, 0xb1, 0x12, 0x76, - 0xe1, 0xdd, 0x61, 0x17, 0x37, 0xc3, 0x5e, 0x2d, 0x6f, 0x29, 0x59, 0xde, 0xef, 0xa1, 0xb5, 0x11, - 0xc5, 0xb6, 0x29, 0xfd, 0x67, 0xc0, 0xd1, 0xa5, 0xcf, 0x85, 0xe3, 0x79, 0x6b, 0x19, 0xc5, 0x95, - 0x34, 0x72, 0x57, 0xb2, 0xf0, 0x3e, 0x95, 0x2c, 0x26, 0x24, 0xd1, 0xfa, 0x95, 0x56, 0xf4, 0xcb, - 0x53, 0xdd, 0x64, 0x4f, 0x55, 0xd6, 0x7a, 0x0a, 0x3f, 0x02, 0x08, 0xe8, 0x92, 0xd3, 0xb1, 0x24, - 0xaf, 0xca, 0xef, 0xeb, 0x72, 0x67, 0xe8, 0xcc, 0xa9, 0x7d, 0x09, 0xc7, 0xeb, 0xc9, 0x6f, 0x2b, - 0xe4, 0x2d, 0xb4, 0xae, 0x7d, 0x37, 0x55, 0xc9, 0xb4, 0xb3, 0xb1, 0x91, 0x5b, 0x21, 0x25, 0xb7, - 0x26, 0x94, 0x17, 0xcb, 0xe0, 0x0d, 0x8d, 0xb4, 0x52, 0x0b, 0xfb, 0x15, 0x98, 0x9b, 0x9e, 0xb6, - 0x0d, 0xfb, 0x09, 0x1c, 0x9e, 0x53, 0xf1, 0x93, 0x3a, 0x59, 0x51, 0xc0, 0xf6, 0x00, 0x70, 0x75, - 0xf3, 0x81, 0x3b, 0xda, 0x4a, 0x72, 0xeb, 0x5b, 0x59, 0xe3, 0x35, 0xaa, 0xf7, 0x77, 0x15, 0x0e, - 0xf4, 0x10, 0x55, 0x57, 0x1e, 0xba, 0xb0, 0xb7, 0x7a, 0x5b, 0xe0, 0xe7, 0xd9, 0x37, 0xe2, 0xda, - 0xb5, 0x6e, 0x3d, 0xcb, 0x03, 0x55, 0xa1, 0xda, 0x3b, 0x5f, 0x19, 0xc8, 0xa1, 0xb1, 0x3e, 0xc4, - 0xf1, 0x79, 0x3a, 0x47, 0xc6, 0xad, 0x61, 0x75, 0xf3, 0xc2, 0xb5, 0x5b, 0xbc, 0x93, 0x72, 0x26, - 0x27, 0x2f, 0x3e, 0x4a, 0x93, 0x1c, 0xf6, 0xd6, 0x69, 0x6e, 0x7c, 0xec, 0xf7, 0x37, 0xd8, 0x4f, - 0xcc, 0x38, 0xcc, 0x50, 0x2b, 0x6d, 0x8e, 0x5b, 0x5f, 0xe4, 0xc2, 0xc6, 0xbe, 0xe6, 0x70, 0x90, - 0x6c, 0x1a, 0xcc, 0x20, 0x48, 0x9d, 0x2b, 0xd6, 0x97, 0xf9, 0xc0, 0xb1, 0x3b, 0x0e, 0x8d, 0xf5, - 0xe3, 0x9e, 0x55, 0xc7, 0x8c, 0x06, 0xcc, 0xaa, 0x63, 0x56, 0x17, 0xd9, 0x3b, 0xe8, 0x00, 0x3c, - 0x74, 0x00, 0x3e, 0xcd, 0x2c, 0x48, 0xb2, 0x71, 0xac, 0xce, 0xe3, 0xc0, 0xd8, 0xc5, 0x02, 0x3e, - 0x58, 0x9b, 0xe2, 0x98, 0x21, 0x4d, 0xfa, 0x95, 0x63, 0x3d, 0xcf, 0x89, 0xd6, 0x1e, 0x5f, 0xc0, - 0x2f, 0x35, 0x0d, 0xbe, 0xa9, 0xc8, 0xff, 0xd0, 0xdf, 0xfc, 0x1f, 0x00, 0x00, 0xff, 0xff, 0x76, - 0x9b, 0x44, 0xa7, 0x14, 0x0c, 0x00, 0x00, + // 946 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdd, 0x72, 0xdb, 0x44, + 0x14, 0xae, 0x6c, 0xc7, 0xb2, 0x4f, 0x7e, 0x48, 0xb6, 0x49, 0xac, 0x68, 0x80, 0xe9, 0x88, 0x81, + 0x86, 0x42, 0x1d, 0x30, 0xb7, 0x0c, 0x33, 0x69, 0xea, 0x49, 0x43, 0x83, 0x3b, 0xb3, 0x26, 0x30, + 0xc3, 0x05, 0x1e, 0xc5, 0x5e, 0x37, 0xa2, 0x8a, 0xd6, 0x68, 0xd7, 0x1e, 0x72, 0xcf, 0x0d, 0x17, + 0xbc, 0x04, 0xef, 0xc1, 0x93, 0x71, 0xc3, 0x6a, 0x7f, 0x14, 0x4b, 0x96, 0x5a, 0xd5, 0x37, 0xd6, + 0xee, 0x9e, 0x6f, 0xbf, 0x73, 0xce, 0x77, 0x76, 0xcf, 0x1a, 0xdc, 0x1b, 0x7f, 0x16, 0x9c, 0x30, + 0x12, 0x2f, 0x82, 0x31, 0x61, 0x27, 0x3c, 0x08, 0x43, 0x12, 0x77, 0x67, 0x31, 0xe5, 0x14, 0xed, + 0x27, 0xb6, 0xae, 0xb1, 0x75, 0x95, 0xcd, 0x3d, 0x94, 0x3b, 0xc6, 0x37, 0x7e, 0xcc, 0xd5, 0xaf, + 0x42, 0xbb, 0x9d, 0xe5, 0x75, 0x1a, 0x4d, 0x83, 0xd7, 0xda, 0xa0, 0x5c, 0xc4, 0x24, 0x24, 0x3e, + 0x23, 0xe6, 0x9b, 0xd9, 0x64, 0x6c, 0x41, 0x34, 0xa5, 0xda, 0x70, 0x94, 0x31, 0x30, 0xee, 0xf3, + 0x39, 0xcb, 0xf0, 0x2d, 0x48, 0xcc, 0x02, 0x1a, 0x99, 0xaf, 0xb2, 0x79, 0xff, 0xd4, 0xe0, 0xe1, + 0x65, 0xc0, 0x38, 0x56, 0x1b, 0x19, 0x26, 0xbf, 0xcf, 0x09, 0xe3, 0x68, 0x1f, 0x36, 0xc2, 0xe0, + 0x36, 0xe0, 0x8e, 0xf5, 0xc8, 0x3a, 0xae, 0x63, 0x35, 0x41, 0x87, 0xd0, 0xa4, 0xd3, 0x29, 0x23, + 0xdc, 0xa9, 0x89, 0xe5, 0x36, 0xd6, 0x33, 0xf4, 0x1d, 0xd8, 0x8c, 0xc6, 0x7c, 0x74, 0x7d, 0xe7, + 0xd4, 0x85, 0x61, 0xa7, 0xf7, 0x69, 0xb7, 0x48, 0x8a, 0x6e, 0xe2, 0x69, 0x28, 0x80, 0xdd, 0xe4, + 0xe7, 0xd9, 0x1d, 0x6e, 0x32, 0xf9, 0x4d, 0x78, 0xa7, 0x41, 0xc8, 0x49, 0xec, 0x34, 0x14, 0xaf, + 0x9a, 0xa1, 0x73, 0x00, 0xc9, 0x4b, 0xe3, 0x89, 0xb0, 0x6d, 0x48, 0xea, 0xe3, 0x0a, 0xd4, 0xaf, + 0x12, 0x3c, 0x6e, 0x33, 0x33, 0x44, 0xdf, 0xc2, 0x96, 0x92, 0x64, 0x34, 0xa6, 0x13, 0xc2, 0x9c, + 0xe6, 0xa3, 0xba, 0xa0, 0x3a, 0x52, 0x54, 0x46, 0xe1, 0xa1, 0x12, 0xed, 0x4c, 0x20, 0xf0, 0xa6, + 0x82, 0x27, 0x63, 0xe6, 0xfd, 0x0a, 0x2d, 0x43, 0xef, 0xf5, 0xa0, 0xa9, 0x82, 0x47, 0x9b, 0x60, + 0x5f, 0x0d, 0x5e, 0x0e, 0x5e, 0xfd, 0x3c, 0xd8, 0x7d, 0x80, 0x5a, 0xd0, 0x18, 0x9c, 0xfe, 0xd0, + 0xdf, 0xb5, 0xd0, 0x1e, 0x6c, 0x5f, 0x9e, 0x0e, 0x7f, 0x1c, 0xe1, 0xfe, 0x65, 0xff, 0x74, 0xd8, + 0x7f, 0xbe, 0x5b, 0xf3, 0x3e, 0x86, 0x76, 0x1a, 0x15, 0xb2, 0xa1, 0x7e, 0x3a, 0x3c, 0x53, 0x5b, + 0x9e, 0xf7, 0xc5, 0xc8, 0xf2, 0xfe, 0xb2, 0x60, 0x3f, 0x5b, 0x04, 0x36, 0xa3, 0x11, 0x23, 0x49, + 0x15, 0xc6, 0x74, 0x1e, 0xa5, 0x55, 0x90, 0x13, 0x84, 0xa0, 0x11, 0x91, 0x3f, 0x4c, 0x0d, 0xe4, + 0x38, 0x41, 0x72, 0xca, 0xfd, 0x50, 0xea, 0x2f, 0x90, 0x72, 0x82, 0xbe, 0x86, 0x96, 0x4e, 0x8e, + 0x09, 0x65, 0xeb, 0xc7, 0x9b, 0xbd, 0x83, 0x6c, 0xca, 0xda, 0x23, 0x4e, 0x61, 0xde, 0x39, 0x74, + 0xce, 0x89, 0x89, 0x44, 0x29, 0x62, 0xce, 0x44, 0xe2, 0xd7, 0xbf, 0x25, 0x32, 0x98, 0xc4, 0xaf, + 0x18, 0x23, 0x07, 0x6c, 0x7d, 0xa0, 0x64, 0x38, 0x1b, 0xd8, 0x4c, 0x3d, 0x0e, 0xce, 0x2a, 0x91, + 0xce, 0xab, 0x88, 0xe9, 0x33, 0x68, 0x24, 0xc7, 0x59, 0xd2, 0x6c, 0xf6, 0x50, 0x36, 0xce, 0x0b, + 0x61, 0xc1, 0xd2, 0x8e, 0x3e, 0x84, 0x76, 0x82, 0x67, 0x33, 0x7f, 0x4c, 0x64, 0xb6, 0x6d, 0x7c, + 0xbf, 0xe0, 0xbd, 0x58, 0xf6, 0x7a, 0x46, 0x23, 0x4e, 0x22, 0xbe, 0x5e, 0xfc, 0x97, 0x70, 0x54, + 0xc0, 0xa4, 0x13, 0x38, 0x01, 0x5b, 0x87, 0x26, 0xd9, 0x4a, 0x75, 0x35, 0x28, 0xef, 0x5f, 0x51, + 0xe2, 0xab, 0xd9, 0xc4, 0xe7, 0xc4, 0x98, 0xde, 0x12, 0xd4, 0x63, 0x51, 0xf6, 0xa4, 0x2d, 0x68, + 0x2d, 0xf6, 0x14, 0xb7, 0xea, 0x1d, 0x67, 0xc9, 0x2f, 0x56, 0x76, 0xf4, 0x04, 0x9a, 0x0b, 0x3f, + 0x14, 0x3c, 0x52, 0x88, 0x54, 0x35, 0x8d, 0x94, 0x3d, 0x05, 0x6b, 0x04, 0xea, 0x80, 0x3d, 0x89, + 0xef, 0x46, 0xf1, 0x3c, 0x92, 0x97, 0xac, 0x85, 0x9b, 0x62, 0x8a, 0xe7, 0x11, 0xfa, 0x04, 0xb6, + 0x27, 0x01, 0xf3, 0xaf, 0x43, 0x32, 0xba, 0xa1, 0xf4, 0x0d, 0x93, 0xf7, 0xac, 0x85, 0xb7, 0xf4, + 0xe2, 0x8b, 0x64, 0x4d, 0xe8, 0x7a, 0x90, 0x0b, 0x7f, 0x5d, 0x25, 0xfe, 0xb4, 0xe0, 0x10, 0xd3, + 0x30, 0xbc, 0xf6, 0xc7, 0x6f, 0x2a, 0x68, 0xb1, 0x14, 0x76, 0xed, 0xed, 0x61, 0xd7, 0x57, 0xc3, + 0x5e, 0x2e, 0x6f, 0x23, 0x5b, 0xde, 0xef, 0xa1, 0xb3, 0x12, 0xc5, 0xba, 0x29, 0xfd, 0x67, 0xc1, + 0xc1, 0x45, 0x24, 0x3a, 0x46, 0x18, 0xe6, 0x32, 0x4a, 0x2b, 0x69, 0x55, 0xae, 0x64, 0xed, 0x7d, + 0x2a, 0x59, 0xcf, 0x48, 0x62, 0xf4, 0x6b, 0x2c, 0xe9, 0x57, 0xa5, 0xba, 0xd9, 0x3b, 0xd5, 0xcc, + 0xdd, 0x29, 0xf4, 0x11, 0x40, 0x4c, 0xe6, 0x8c, 0x8c, 0x24, 0xb9, 0x2d, 0xf7, 0xb7, 0xe5, 0xca, + 0x40, 0x2c, 0x78, 0x17, 0x70, 0x98, 0x4f, 0x7e, 0x5d, 0x21, 0x6f, 0xa0, 0x73, 0x15, 0x05, 0x85, + 0x4a, 0x16, 0x9d, 0x8d, 0x95, 0xdc, 0x6a, 0x05, 0xb9, 0x89, 0xce, 0x38, 0x9b, 0xc7, 0xaf, 0x89, + 0xd6, 0x4a, 0x4d, 0xbc, 0x97, 0xe0, 0xac, 0x7a, 0x5a, 0x37, 0xec, 0x87, 0xb0, 0x27, 0x5a, 0xc5, + 0x4f, 0xea, 0x64, 0xe9, 0x80, 0xbd, 0x3e, 0xa0, 0xe5, 0xc5, 0x7b, 0x6e, 0xbd, 0x94, 0xe5, 0x36, + 0xaf, 0xb2, 0xc1, 0x1b, 0x54, 0xef, 0x6f, 0x1b, 0x76, 0x4c, 0x13, 0x55, 0x4f, 0x1e, 0x0a, 0x60, + 0x6b, 0xf9, 0xb5, 0x40, 0x9f, 0x97, 0xbf, 0x88, 0xb9, 0x67, 0xdd, 0x7d, 0x52, 0x05, 0xaa, 0x42, + 0xf5, 0x1e, 0x7c, 0x65, 0x21, 0x06, 0xbb, 0xf9, 0x26, 0x8e, 0x9e, 0x16, 0x73, 0x94, 0xbc, 0x1a, + 0x6e, 0xb7, 0x2a, 0xdc, 0xb8, 0x45, 0x0b, 0x29, 0x67, 0xb6, 0xf3, 0xa2, 0x77, 0xd2, 0x64, 0x9b, + 0xbd, 0x7b, 0x52, 0x19, 0x9f, 0xfa, 0xfd, 0x0d, 0xb6, 0x33, 0x3d, 0x0e, 0x95, 0xa8, 0x55, 0xd4, + 0xc7, 0xdd, 0x2f, 0x2a, 0x61, 0x53, 0x5f, 0xb7, 0xb0, 0x93, 0xbd, 0x34, 0xa8, 0x84, 0xa0, 0xb0, + 0xaf, 0xb8, 0x5f, 0x56, 0x03, 0xa7, 0xee, 0x44, 0x1d, 0xf3, 0xc7, 0xbd, 0xac, 0x8e, 0x25, 0x17, + 0xb0, 0xac, 0x8e, 0x65, 0xb7, 0x48, 0x38, 0xf5, 0x01, 0xee, 0x6f, 0x00, 0x7a, 0x5c, 0x5a, 0x90, + 0xec, 0xc5, 0x71, 0x8f, 0xdf, 0x0d, 0x4c, 0x5d, 0xcc, 0xe0, 0x83, 0x5c, 0x17, 0x47, 0x25, 0xd2, + 0x14, 0x3f, 0x39, 0xee, 0xd3, 0x8a, 0x68, 0xe3, 0xf1, 0x19, 0xfc, 0xd2, 0x32, 0xe0, 0xeb, 0xa6, + 0xfc, 0x0f, 0xfd, 0xcd, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0x76, 0x9b, 0x44, 0xa7, 0x14, 0x0c, + 0x00, 0x00, } diff --git a/pkg/proto/hapi/version/version.pb.go b/pkg/proto/hapi/version/version.pb.go index 79771408e..4677764e4 100644 --- a/pkg/proto/hapi/version/version.pb.go +++ b/pkg/proto/hapi/version/version.pb.go @@ -47,15 +47,14 @@ func init() { func init() { proto.RegisterFile("hapi/version/version.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 151 bytes of a gzipped FileDescriptorProto + // 144 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xca, 0x48, 0x2c, 0xc8, 0xd4, 0x2f, 0x4b, 0x2d, 0x2a, 0xce, 0xcc, 0xcf, 0x83, 0xd1, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x3c, 0x20, 0x39, 0x3d, 0xa8, 0x98, 0x52, 0x3a, 0x17, 0x7b, 0x18, 0x84, 0x29, 0x24, 0xce, - 0xc5, 0x5e, 0x9c, 0x9a, 0x1b, 0x5f, 0x96, 0x5a, 0x24, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x19, 0xc4, - 0x56, 0x9c, 0x9a, 0x1b, 0x96, 0x5a, 0x24, 0x24, 0xcb, 0xc5, 0x95, 0x9e, 0x59, 0x12, 0x9f, 0x9c, - 0x9f, 0x9b, 0x9b, 0x59, 0x22, 0xc1, 0x04, 0x96, 0xe3, 0x4c, 0xcf, 0x2c, 0x71, 0x06, 0x0b, 0x08, - 0xa9, 0x70, 0xf1, 0x81, 0xa4, 0x4b, 0x8a, 0x52, 0x53, 0xe3, 0x8b, 0x4b, 0x12, 0x4b, 0x52, 0x25, - 0x98, 0xc1, 0x4a, 0x78, 0xd2, 0x33, 0x4b, 0x42, 0x8a, 0x52, 0x53, 0x83, 0x41, 0x62, 0x4e, 0x9c, - 0x51, 0xec, 0x50, 0x3b, 0x93, 0xd8, 0xc0, 0x0e, 0x31, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x20, - 0xcc, 0x0e, 0x1b, 0xa6, 0x00, 0x00, 0x00, + 0xc5, 0x5e, 0x9c, 0x9a, 0x1b, 0x0f, 0x94, 0x91, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x62, 0x03, + 0x72, 0x81, 0x92, 0x42, 0xb2, 0x5c, 0x5c, 0xe9, 0x99, 0x25, 0xf1, 0xc9, 0xf9, 0xb9, 0xb9, 0x99, + 0x25, 0x12, 0x4c, 0x60, 0x39, 0x4e, 0xa0, 0x88, 0x33, 0x58, 0x40, 0x48, 0x85, 0x8b, 0x0f, 0x24, + 0x5d, 0x52, 0x94, 0x9a, 0x1a, 0x5f, 0x5c, 0x92, 0x58, 0x92, 0x2a, 0xc1, 0x0c, 0x56, 0xc2, 0x03, + 0x14, 0x0d, 0x01, 0x0a, 0x06, 0x83, 0xc4, 0x9c, 0x38, 0xa3, 0xd8, 0xa1, 0x76, 0x26, 0xb1, 0x81, + 0x1d, 0x62, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x20, 0xcc, 0x0e, 0x1b, 0xa6, 0x00, 0x00, 0x00, } From 9cfbbb34f1bdcfc10c5c302d902f5f9054f1e09c Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Thu, 29 Sep 2016 10:31:30 -0600 Subject: [PATCH 124/183] fix(1245): hook up revision flag to helm get {manifest,values,hooks} --- cmd/helm/get.go | 2 +- cmd/helm/get_hooks.go | 4 +++- cmd/helm/get_manifest.go | 5 ++++- cmd/helm/get_values.go | 5 ++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/cmd/helm/get.go b/cmd/helm/get.go index 9402762c5..83ec28b28 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -73,7 +73,7 @@ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { }, } - cmd.PersistentFlags().Int32Var(&get.version, "revision", 0, "get the named release with revision") + cmd.Flags().Int32Var(&get.version, "revision", 0, "get the named release with revision") cmd.AddCommand(newGetValuesCmd(nil, out)) cmd.AddCommand(newGetManifestCmd(nil, out)) diff --git a/cmd/helm/get_hooks.go b/cmd/helm/get_hooks.go index f880bf789..dc93fb1df 100644 --- a/cmd/helm/get_hooks.go +++ b/cmd/helm/get_hooks.go @@ -35,6 +35,7 @@ type getHooksCmd struct { release string out io.Writer client helm.Interface + version int32 } func newGetHooksCmd(client helm.Interface, out io.Writer) *cobra.Command { @@ -55,11 +56,12 @@ func newGetHooksCmd(client helm.Interface, out io.Writer) *cobra.Command { return ghc.run() }, } + cmd.Flags().Int32Var(&ghc.version, "revision", 0, "get the named release with revision") return cmd } func (g *getHooksCmd) run() error { - res, err := g.client.ReleaseContent(g.release) + res, err := g.client.ReleaseContent(g.release, helm.ContentReleaseVersion(g.version)) if err != nil { fmt.Fprintln(g.out, g.release) return prettyError(err) diff --git a/cmd/helm/get_manifest.go b/cmd/helm/get_manifest.go index f37bafbe4..85b31df06 100644 --- a/cmd/helm/get_manifest.go +++ b/cmd/helm/get_manifest.go @@ -37,6 +37,7 @@ type getManifestCmd struct { release string out io.Writer client helm.Interface + version int32 } func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command { @@ -59,12 +60,14 @@ func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command { return get.run() }, } + + cmd.Flags().Int32Var(&get.version, "revision", 0, "get the named release with revision") return cmd } // getManifest implements 'helm get manifest' func (g *getManifestCmd) run() error { - res, err := g.client.ReleaseContent(g.release) + res, err := g.client.ReleaseContent(g.release, helm.ContentReleaseVersion(g.version)) if err != nil { return prettyError(err) } diff --git a/cmd/helm/get_values.go b/cmd/helm/get_values.go index c95b6e056..bce35958b 100644 --- a/cmd/helm/get_values.go +++ b/cmd/helm/get_values.go @@ -35,6 +35,7 @@ type getValuesCmd struct { allValues bool out io.Writer client helm.Interface + version int32 } func newGetValuesCmd(client helm.Interface, out io.Writer) *cobra.Command { @@ -55,13 +56,15 @@ func newGetValuesCmd(client helm.Interface, out io.Writer) *cobra.Command { return get.run() }, } + + cmd.Flags().Int32Var(&get.version, "revision", 0, "get the named release with revision") cmd.Flags().BoolVarP(&get.allValues, "all", "a", false, "dump all (computed) values") return cmd } // getValues implements 'helm get values' func (g *getValuesCmd) run() error { - res, err := g.client.ReleaseContent(g.release) + res, err := g.client.ReleaseContent(g.release, helm.ContentReleaseVersion(g.version)) if err != nil { return prettyError(err) } From 94c30847069ae5b9f850983c2707cdd72b908fa0 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Thu, 29 Sep 2016 10:17:59 -0700 Subject: [PATCH 125/183] feat(ci): automate release builds on circleci --- circle.yml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 36f1232de..282e30e63 100644 --- a/circle.yml +++ b/circle.yml @@ -38,7 +38,27 @@ test: parallel: true deployment: - gcr: + release: + tag: /.*/ + commands: + # setup gcloud tools + - sudo /opt/google-cloud-sdk/bin/gcloud --quiet components update + - echo "${GCLOUD_SERVICE_KEY}" | base64 --decode > "${HOME}/gcloud-service-key.json" + - sudo /opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file "${HOME}/gcloud-service-key.json" + - sudo /opt/google-cloud-sdk/bin/gcloud config set project "${PROJECT_NAME}" + - docker login -e 1234@5678.com -u _json_key -p "$(cat ${HOME}/gcloud-service-key.json)" https://gcr.io + + # build canary tiller image and push + - make docker-build VERSION="${CIRCLE_TAG}" + - docker push "gcr.io/kubernetes-helm/tiller:${CIRCLE_TAG}" + - docker push gcr.io/kubernetes-helm/tiller:canary + + # build canary helm binaries and push + - make build-cross + - make dist VERSION="${CIRCLE_TAG}" + - sudo /opt/google-cloud-sdk/bin/gsutil cp ./_dist/* "gs://${PROJECT_NAME}" + + canary: branch: master commands: # setup gcloud tools From 3ec83045d44d735bfb865829c71592a4d85fa9bc Mon Sep 17 00:00:00 2001 From: Mark Petrovic Date: Fri, 30 Sep 2016 17:46:53 -0700 Subject: [PATCH 126/183] issue/1254 Fix typo in using_helm.md that refers to "glide install". The reference should be to "helm install". --- docs/using_helm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using_helm.md b/docs/using_helm.md index 369d39ee3..a0d19b911 100644 --- a/docs/using_helm.md +++ b/docs/using_helm.md @@ -215,7 +215,7 @@ and then pass that file during installation. ```console $ echo 'mariadbUser: user0` > config.yaml -$ glide install -f config.yaml stable/mariadb +$ helm install -f config.yaml stable/mariadb ``` The above will set the default MariaDB user to `user0`, but accept all From 854f3e0b51f86b005dc9e094e86ec70d9825d9e1 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Mon, 3 Oct 2016 11:34:49 -0700 Subject: [PATCH 127/183] ref(helm): refactor {home,lint,serve} commands --- cmd/helm/helm.go | 22 +++++++++++----------- cmd/helm/home.go | 26 +++++++++++++------------- cmd/helm/lint.go | 46 +++++++++++++++++++++++++++------------------- cmd/helm/serve.go | 36 ++++++++++++++++++++++-------------- 4 files changed, 73 insertions(+), 57 deletions(-) diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 4c076b068..af51494e5 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -86,31 +86,31 @@ func newRootCmd(out io.Writer) *cobra.Command { cmd.AddCommand( newCreateCmd(out), newDeleteCmd(nil, out), + newDependencyCmd(out), + newFetchCmd(out), newGetCmd(nil, out), + newHomeCmd(out), newInitCmd(out), newInspectCmd(nil, out), newInstallCmd(nil, out), + newLintCmd(out), newListCmd(nil, out), + newPackageCmd(nil, out), + newRepoCmd(out), + newRollbackCmd(nil, out), + newSearchCmd(out), + newServeCmd(out), newStatusCmd(nil, out), + newUpdateCmd(out), newUpgradeCmd(nil, out), - newRollbackCmd(nil, out), - newPackageCmd(nil, out), - newFetchCmd(out), newVerifyCmd(out), - newUpdateCmd(out), newVersionCmd(nil, out), - newRepoCmd(out), - newDependencyCmd(out), - newSearchCmd(out), ) return cmd } -// RootCommand is the top-level command for Helm. -var RootCommand = newRootCmd(os.Stdout) - func main() { - cmd := RootCommand + cmd := newRootCmd(os.Stdout) if err := cmd.Execute(); err != nil { os.Exit(1) } diff --git a/cmd/helm/home.go b/cmd/helm/home.go index 93c130d85..feaf1f77b 100644 --- a/cmd/helm/home.go +++ b/cmd/helm/home.go @@ -17,6 +17,9 @@ limitations under the License. package main import ( + "fmt" + "io" + "github.com/spf13/cobra" ) @@ -25,17 +28,14 @@ This command displays the location of HELM_HOME. This is where any helm configuration files live. ` -var homeCommand = &cobra.Command{ - Use: "home", - Short: "displays the location of HELM_HOME", - Long: longHomeHelp, - Run: home, -} - -func init() { - RootCommand.AddCommand(homeCommand) -} - -func home(cmd *cobra.Command, args []string) { - cmd.Printf(homePath() + "\n") +func newHomeCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "home", + Short: "displays the location of HELM_HOME", + Long: longHomeHelp, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(out, homePath()+"\n") + }, + } + return cmd } diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index 75a0f3082..20f1995c4 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -19,6 +19,7 @@ package main import ( "errors" "fmt" + "io" "io/ioutil" "os" "path/filepath" @@ -40,30 +41,37 @@ it will emit [ERROR] messages. If it encounters issues that break with conventio or recommendation, it will emit [WARNING] messages. ` -var lintCommand = &cobra.Command{ - Use: "lint [flags] PATH", - Short: "examines a chart for possible issues", - Long: longLintHelp, - RunE: lintCmd, +type lintCmd struct { + strict bool + paths []string + out io.Writer } -var flagStrict bool - -func init() { - lintCommand.Flags().BoolVarP(&flagStrict, "strict", "", false, "fail on lint warnings") - RootCommand.AddCommand(lintCommand) +func newLintCmd(out io.Writer) *cobra.Command { + l := &lintCmd{ + paths: []string{"."}, + out: out, + } + cmd := &cobra.Command{ + Use: "lint [flags] PATH", + Short: "examines a chart for possible issues", + Long: longLintHelp, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + l.paths = args + } + return l.run() + }, + } + cmd.Flags().BoolVar(&l.strict, "strict", false, "fail on lint warnings") + return cmd } var errLintNoChart = errors.New("No chart found for linting (missing Chart.yaml)") -func lintCmd(cmd *cobra.Command, args []string) error { - paths := []string{"."} - if len(args) > 0 { - paths = args - } - +func (l *lintCmd) run() error { var lowestTolerance int - if flagStrict { + if l.strict { lowestTolerance = support.WarningSev } else { lowestTolerance = support.ErrorSev @@ -71,7 +79,7 @@ func lintCmd(cmd *cobra.Command, args []string) error { var total int var failures int - for _, path := range paths { + for _, path := range l.paths { if linter, err := lintChart(path); err != nil { fmt.Println("==> Skipping", path) fmt.Println(err) @@ -99,7 +107,7 @@ func lintCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("%s, %d chart(s) failed", msg, failures) } - fmt.Printf("%s, no failures\n", msg) + fmt.Fprintf(l.out, "%s, no failures\n", msg) return nil } diff --git a/cmd/helm/serve.go b/cmd/helm/serve.go index 3fca0bd8a..ebb75e223 100644 --- a/cmd/helm/serve.go +++ b/cmd/helm/serve.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "io" "os" "path/filepath" @@ -25,24 +26,31 @@ import ( "k8s.io/helm/pkg/repo" ) -var serveDesc = `This command starts a local chart repository server that serves charts from a local directory.` -var repoPath string +const serveDesc = `This command starts a local chart repository server that serves charts from a local directory.` -func init() { - serveCmd.Flags().StringVar(&repoPath, "repo-path", localRepoDirectory(), "The local directory path from which to serve charts.") - RootCommand.AddCommand(serveCmd) +type serveCmd struct { + repoPath string + out io.Writer } -var serveCmd = &cobra.Command{ - Use: "serve", - Short: "start a local http web server", - Long: serveDesc, - RunE: serve, +func newServeCmd(out io.Writer) *cobra.Command { + s := &serveCmd{ + out: out, + } + cmd := &cobra.Command{ + Use: "serve", + Short: "start a local http web server", + Long: serveDesc, + RunE: func(cmd *cobra.Command, args []string) error { + return s.run() + }, + } + cmd.Flags().StringVar(&s.repoPath, "repo-path", localRepoDirectory(), "The local directory path from which to serve charts.") + return cmd } -func serve(cmd *cobra.Command, args []string) error { - - repoPath, err := filepath.Abs(repoPath) +func (s *serveCmd) run() error { + repoPath, err := filepath.Abs(s.repoPath) if err != nil { return err } @@ -50,6 +58,6 @@ func serve(cmd *cobra.Command, args []string) error { return err } - repo.StartLocalRepo(repoPath) + repo.StartLocalRepo(s.repoPath) return nil } From 3a095a47ca08dfa289ea87453e9c0833c77e8e65 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 3 Oct 2016 20:30:41 -0600 Subject: [PATCH 128/183] feat(tiller): update Sprig to 2.6.0 --- glide.lock | 8 +++++--- glide.yaml | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/glide.lock b/glide.lock index 99a057ebd..3bd4d1d39 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: a017cec6c2fa7ab82b7a2747d016f2254f6d26c2e24ae48f2d34bb51d85cc0d1 -updated: 2016-09-27T14:51:17.303868688-06:00 +hash: 041486e116f9792e4533a4a67b191ab139d29ea4449d4b6eb6e2d96b620b3cac +updated: 2016-10-03T20:28:56.472755464-06:00 imports: - name: bitbucket.org/ww/goautoneg version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 @@ -264,7 +264,7 @@ imports: - name: github.com/Masterminds/semver version: 808ed7761c233af2de3f9729a041d68c62527f3a - name: github.com/Masterminds/sprig - version: 89eb6984259918bffe1ddff9647b9ed06061e688 + version: 8f797f5b23118d8fe846c4296b0ad55044201b14 - name: github.com/mattn/go-runewidth version: d6bea18f789704b5f83375793155289da36a3c7f - name: github.com/matttproud/golang_protobuf_extensions @@ -288,6 +288,8 @@ imports: - model - name: github.com/prometheus/procfs version: 454a56f35412459b5e684fd5ec0f9211b94f002a +- name: github.com/satori/go.uuid + version: 879c5887cd475cd7864858769793b2ceb0d44feb - name: github.com/Sirupsen/logrus version: 51fe59aca108dc5680109e7b2051cbdcfa5a253c - name: github.com/spf13/cobra diff --git a/glide.yaml b/glide.yaml index e4947bdb3..5439b4341 100644 --- a/glide.yaml +++ b/glide.yaml @@ -8,7 +8,7 @@ import: - package: github.com/spf13/pflag version: 367864438f1b1a3c7db4da06a2f55b144e6784e0 - package: github.com/Masterminds/sprig - version: ^2.5 + version: ^2.6 - package: gopkg.in/yaml.v2 - package: github.com/Masterminds/semver version: 1.1.0 From e8df9c8af084dc33d6147a0408fa3cdedb3968ca Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 4 Oct 2016 10:03:25 -0700 Subject: [PATCH 129/183] chore(ci): go1.7.1 closes #1267 --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 282e30e63..dc4366df0 100644 --- a/circle.yml +++ b/circle.yml @@ -3,7 +3,7 @@ machine: - curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0 environment: - GOVERSION: "1.7" + GOVERSION: "1.7.1" GOPATH: "${HOME}/.go_workspace" WORKDIR: "${GOPATH}/src/k8s.io/helm" PROJECT_NAME: "kubernetes-helm" From e0227c75104520a56a80e3f8efe3db5bb54a2269 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 4 Oct 2016 12:13:01 -0600 Subject: [PATCH 130/183] fix(chart): Chart.yaml now has an apiVersion field. This is to future-proof charts, and also be consistent with repo and index YAML files. Closes #1264 --- _proto/hapi/chart/metadata.proto | 3 + cmd/helm/create.go | 1 + cmd/helm/create_test.go | 71 ++++++++++++++++++ pkg/chartutil/chartfile.go | 3 + pkg/chartutil/chartfile_test.go | 5 ++ pkg/chartutil/testdata/chartfiletest.yaml | 1 + pkg/chartutil/testdata/frobnitz-1.2.3.tgz | Bin 4017 -> 4025 bytes pkg/chartutil/testdata/frobnitz/Chart.yaml | 1 + .../frobnitz/charts/mariner-4.3.2.tgz | Bin 1025 -> 1031 bytes .../mariner/charts/albatross-0.1.0.tgz | Bin 347 -> 347 bytes pkg/proto/hapi/chart/metadata.pb.go | 40 +++++----- 11 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 cmd/helm/create_test.go diff --git a/_proto/hapi/chart/metadata.proto b/_proto/hapi/chart/metadata.proto index 40c8b40dc..da194e9d3 100644 --- a/_proto/hapi/chart/metadata.proto +++ b/_proto/hapi/chart/metadata.proto @@ -61,4 +61,7 @@ message Metadata { // The URL to an icon file. string icon = 9; + + // The API Version of this chart. + string apiVersion = 10; } diff --git a/cmd/helm/create.go b/cmd/helm/create.go index 9f50be5f7..317e8fd6b 100644 --- a/cmd/helm/create.go +++ b/cmd/helm/create.go @@ -85,6 +85,7 @@ func (c *createCmd) run() error { Name: chartname, Description: "A Helm chart for Kubernetes", Version: "0.1.0", + ApiVersion: chartutil.ApiVersionV1, } _, err := chartutil.Create(cfile, filepath.Dir(c.name)) diff --git a/cmd/helm/create_test.go b/cmd/helm/create_test.go new file mode 100644 index 000000000..5fb2c82a6 --- /dev/null +++ b/cmd/helm/create_test.go @@ -0,0 +1,71 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "io/ioutil" + "os" + "testing" + + "k8s.io/helm/pkg/chartutil" +) + +func TestCreateCmd(t *testing.T) { + cname := "testchart" + // Make a temp dir + tdir, err := ioutil.TempDir("", "helm-create-") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tdir) + + // CD into it + pwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(tdir); err != nil { + t.Fatal(err) + } + defer os.Chdir(pwd) + + // Run a create + cmd := newCreateCmd(os.Stdout) + if err := cmd.RunE(cmd, []string{cname}); err != nil { + t.Errorf("Failed to run create: %s", err) + return + } + + // Test that the chart is there + if fi, err := os.Stat(cname); err != nil { + t.Fatalf("no chart directory: %s", err) + } else if !fi.IsDir() { + t.Fatalf("chart is not directory") + } + + c, err := chartutil.LoadDir(cname) + if err != nil { + t.Fatal(err) + } + + if c.Metadata.Name != cname { + t.Errorf("Expected %q name, got %q", cname, c.Metadata.Name) + } + if c.Metadata.ApiVersion != chartutil.ApiVersionV1 { + t.Errorf("Wrong API version: %q", c.Metadata.ApiVersion) + } +} diff --git a/pkg/chartutil/chartfile.go b/pkg/chartutil/chartfile.go index 588670541..3087d81d4 100644 --- a/pkg/chartutil/chartfile.go +++ b/pkg/chartutil/chartfile.go @@ -24,6 +24,9 @@ import ( "k8s.io/helm/pkg/proto/hapi/chart" ) +// APIVersionV1 is the API version number for version 1. +const ApiVersionV1 = "v1" + // UnmarshalChartfile takes raw Chart.yaml data and unmarshals it. func UnmarshalChartfile(data []byte) (*chart.Metadata, error) { y := &chart.Metadata{} diff --git a/pkg/chartutil/chartfile_test.go b/pkg/chartutil/chartfile_test.go index 1b8e800f0..4ccddd3d7 100644 --- a/pkg/chartutil/chartfile_test.go +++ b/pkg/chartutil/chartfile_test.go @@ -39,6 +39,11 @@ func verifyChartfile(t *testing.T, f *chart.Metadata) { t.Fatal("Failed verifyChartfile because f is nil") } + // Api instead of API because it was generated via protobuf. + if f.ApiVersion != ApiVersionV1 { + t.Errorf("Expected API Version %q, got %q", ApiVersionV1, f.ApiVersion) + } + if f.Name != "frobnitz" { t.Errorf("Expected frobnitz, got %s", f.Name) } diff --git a/pkg/chartutil/testdata/chartfiletest.yaml b/pkg/chartutil/testdata/chartfiletest.yaml index b665c61be..7c071c27b 100644 --- a/pkg/chartutil/testdata/chartfiletest.yaml +++ b/pkg/chartutil/testdata/chartfiletest.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: frobnitz description: This is a frobnitz. version: "1.2.3" diff --git a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz index 49ec9ca8789a18c5aa67ef1a0bcc34a924f9f134..07b1113649c3fad6538ec8f284eacfd3f49524c5 100644 GIT binary patch literal 4025 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 diff --git a/pkg/chartutil/testdata/frobnitz/Chart.yaml b/pkg/chartutil/testdata/frobnitz/Chart.yaml index b665c61be..7c071c27b 100644 --- a/pkg/chartutil/testdata/frobnitz/Chart.yaml +++ b/pkg/chartutil/testdata/frobnitz/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: frobnitz description: This is a frobnitz. version: "1.2.3" diff --git a/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz index 4928e7e7af41bd33be43a458f4c9c3a7221fc595..88f255c238b9b53ea02fc4cc1266f94165c8bd41 100644 GIT binary patch literal 1031 zcmV+i1o-6ZtiaDKCZKKh4a;zSOPYc*x1AbX#W7gM<5lo7!M*ugHj(Th!GH9F*ZdSqYaI* zcy)H~k@C=DygM|^=PsGO+u5Dn{m$?EowXn~Y=nb>G9n0quByOqwOlJmO0LZVL`m00 zSym)n0)nVWq7VRTIWMX}$|VrfOR*p!;S`A^5I$!=+AS9QoAc!FquBmWu=4shB_Jkz z8d_%Q$Dq$cQnRk|yFKjukxJMf^jG2~i{P zA}_FL_-|5Bjvy)~q|si62slswpSu2&y!vPUUw#Vuw?i|9O78qQrt^PE)qMR`MOJ0S z%YRJ~BU=Fw&#bC-LAU6a2g?@fg@Z21*G-0@aej!-4;QssS>0a+ap(uzM)ccEJ z5I0C7751}$IF?y&LObqnaozwC6Jq2COKzTlBs8!Khy%jjw(HXK88-cyc2XD=t;icc zmqs=IVJt&R&)NP@n+QrcW|U3+Q)y|XnqyK)4>FQsV<=p$fu4yve79>H_L{|Tmpt|d_z|j+94+bt7{o+{byI@w; zx3h=qHeL7W6SdhpKe=a7RfaowWOGZaxOpJcZ#2)?IPv0$Hh#w~eXBp)d3;M-@0vY3 zM*lb#UA?e*-?LjoZ$96(&}ep2PPNawR1z`{&ar_&u#6zW^TvLWT=1J z{O6jp>1@NnFMF=afaQnVuOFOv>tv>9{=mII-7t7!Mb+|&-F@+8}C1$ z47)?E->j9_p~hX{x1L{4RWIq=bmil}X7}GdeACR6!;QUxuO@d0KUQ0>uc)0p`l|Dk z^Fz?>JmtWTCR-kgADw$+p!!fp&+msO7LBZ5Jn6s>8fX4IE7b7e_U^IXHnjJJk??5K zzH7feJn*nJ@0EtBk8`&(J|lSPvbw;n7fMhDgTY`h7z_r3!C)}X!JozzB5VLC004S+ B3t0dF literal 1025 zcmV+c1pfOUiwFQ!I_y^f1MQc4Y!pQt$1j*vT_1mmq6v0Vv`Rzw_Iaz@V8t3GsR{`g zLeQjhcXM}J_i>$>E1a*!#1gQn#KtBjK>G&>J_4zr#dr`Q8kG7#L5!gIim@ru7$r2u z;?>=~N6JGh@$S$tpSxuCZf9p^_xt;Of3p_EhK+DAP(}nn&{P%pZ7A0Yl9Frl08!F3 zQI-`+lYk&9k|+d#TFylkND&ILyA%sj8cxwj0^xJ^bGyZ2e=|=0K8o%C1S_w9Qvzbj zr=ew*eh&9}gcRkk^%q6W(_d0#O;c3G)juREasUXWU&`5B%r<%A`DH z;QakxRWyb9e@8^l^}|e$c`dAVj!Y(0MvtA zMSx})QtS}o{s#a?NrxEJ!D$^NC`}SQ7(^?el{66_ajf9!F5+KOtPnK_6e_v%=a|a>B{k&huPU-CE1IXjB1p{t zE5R&qH;TcONkJ|G{EdD-D0prbSe!5jaK8{Nop=9z_3l~kL7^y!8Lsyii$UC=iB#CH z1=O+3dK21l|A_M@h?o!~zp&)u3?!j}37`%Ld&h*h;~BR5Gwq}>C|Z#>f36zU_?NL7 zQhLtzf47OCgkwhC0HDcIxWQEUFUaEQ{x6B{hvk1In4IC0m1n7Tad5`_e{tJi@9)}q z8h`ZP&oNd0tFr9B|H*=^ijwN)zt^ATe4gj)cbUxTSB^r#p{rZtGpWXUFItN29Ce zH}89Pi~h#*U5g&uyJG7Vb&L1B-12VcoM#$pH?KYL(jyx#H|n}qzjo~VZG+#1-~P!*WIzrut)Z3cy9YnII`sN`4=BTAsP&sQ z@>%#*N vTXSA%nEW_*J7d#=moBXf+ Date: Tue, 4 Oct 2016 12:27:44 -0600 Subject: [PATCH 131/183] fix(helm): change 'helm update' to 'helm repo update' The old form is marked deprecated. Relates to #1196 --- cmd/helm/helm.go | 6 +++++- cmd/helm/repo.go | 5 +++-- cmd/helm/{update.go => repo_update.go} | 11 +++++++---- cmd/helm/{update_test.go => repo_update_test.go} | 2 +- docs/chart_repository.md | 4 ++-- docs/using_helm.md | 2 +- 6 files changed, 19 insertions(+), 11 deletions(-) rename cmd/helm/{update.go => repo_update.go} (90%) rename cmd/helm/{update_test.go => repo_update_test.go} (98%) diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index af51494e5..3a0f0cfa9 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -83,6 +83,9 @@ func newRootCmd(out io.Writer) *cobra.Command { p.StringVar(&tillerHost, "host", thost, "address of tiller. Overrides $HELM_HOST.") p.BoolVarP(&flagDebug, "debug", "", false, "enable verbose output") + rup := newRepoUpdateCmd(out) + rup.Deprecated = "use 'helm repo update'\n" + cmd.AddCommand( newCreateCmd(out), newDeleteCmd(nil, out), @@ -101,10 +104,11 @@ func newRootCmd(out io.Writer) *cobra.Command { newSearchCmd(out), newServeCmd(out), newStatusCmd(nil, out), - newUpdateCmd(out), newUpgradeCmd(nil, out), newVerifyCmd(out), newVersionCmd(nil, out), + // Deprecated + rup, ) return cmd } diff --git a/cmd/helm/repo.go b/cmd/helm/repo.go index 31249c7fa..fd8a0dfdb 100644 --- a/cmd/helm/repo.go +++ b/cmd/helm/repo.go @@ -36,8 +36,8 @@ type repoCmd struct { func newRepoCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ - Use: "repo [FLAGS] add|remove|list|index [ARGS]", - Short: "add, list, remove, index chart repositories", + Use: "repo [FLAGS] add|remove|list|index|update [ARGS]", + Short: "add, list, remove, update, and index chart repositories", Long: repoHelm, } @@ -45,6 +45,7 @@ func newRepoCmd(out io.Writer) *cobra.Command { cmd.AddCommand(newRepoListCmd(out)) cmd.AddCommand(newRepoRemoveCmd(out)) cmd.AddCommand(newRepoIndexCmd(out)) + cmd.AddCommand(newRepoUpdateCmd(out)) return cmd } diff --git a/cmd/helm/update.go b/cmd/helm/repo_update.go similarity index 90% rename from cmd/helm/update.go rename to cmd/helm/repo_update.go index 86f656d88..865e5eae0 100644 --- a/cmd/helm/update.go +++ b/cmd/helm/repo_update.go @@ -30,16 +30,19 @@ import ( const updateDesc = ` Update gets the latest information about charts from the respective chart repositories. Information is cached locally, where it is used by commands like 'helm search'. + +'helm update' is the deprecated form of 'helm repo update'. It will be removed in +future releases. ` -type updateCmd struct { +type repoUpdateCmd struct { repoFile string update func(map[string]string, bool, io.Writer) out io.Writer } -func newUpdateCmd(out io.Writer) *cobra.Command { - u := &updateCmd{ +func newRepoUpdateCmd(out io.Writer) *cobra.Command { + u := &repoUpdateCmd{ out: out, update: updateCharts, repoFile: repositoriesFile(), @@ -56,7 +59,7 @@ func newUpdateCmd(out io.Writer) *cobra.Command { return cmd } -func (u *updateCmd) run() error { +func (u *repoUpdateCmd) run() error { f, err := repo.LoadRepositoriesFile(u.repoFile) if err != nil { return err diff --git a/cmd/helm/update_test.go b/cmd/helm/repo_update_test.go similarity index 98% rename from cmd/helm/update_test.go rename to cmd/helm/repo_update_test.go index c8af270a9..867babd80 100644 --- a/cmd/helm/update_test.go +++ b/cmd/helm/repo_update_test.go @@ -34,7 +34,7 @@ func TestUpdateCmd(t *testing.T) { fmt.Fprintln(out, name) } } - uc := &updateCmd{ + uc := &repoUpdateCmd{ out: out, update: updater, repoFile: "testdata/repositories.yaml", diff --git a/docs/chart_repository.md b/docs/chart_repository.md index 94992a30c..56822e175 100644 --- a/docs/chart_repository.md +++ b/docs/chart_repository.md @@ -129,9 +129,9 @@ fantastic-charts https://storage.googleapis.com/fantastic-charts *Note: A repository will not be added if it does not contain a valid index.yaml.* -After that, they'll be able to search through your charts. After you've updated the repository, they can use the `helm update` command to get the latest chart information. +After that, they'll be able to search through your charts. After you've updated the repository, they can use the `helm repo update` command to get the latest chart information. -*Under the hood, the `helm repo add` and `helm update` commands are fetching the index.yaml file and storing them in the `$HELM_HOME/repository/cache/` directory. This is where the `helm search` function finds information about charts.* +*Under the hood, the `helm repo add` and `helm repo update` commands are fetching the index.yaml file and storing them in the `$HELM_HOME/repository/cache/` directory. This is where the `helm search` function finds information about charts.* ## Contributing charts to the official helm chart repository *Coming Soon* diff --git a/docs/using_helm.md b/docs/using_helm.md index a0d19b911..eed798b38 100644 --- a/docs/using_helm.md +++ b/docs/using_helm.md @@ -338,7 +338,7 @@ $ helm repo add dev https://example.com/dev-charts ``` Because chart repositories change frequently, at any point you can make -sure your Helm client is up to date by running `helm update`. +sure your Helm client is up to date by running `helm repo update`. ## Creating Your Own Charts From 4f09b05613cd1c3d7f0fe1fda0b55390a24e9449 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 28 Sep 2016 18:51:12 -0600 Subject: [PATCH 132/183] feat(helm): implement new index format This implements a new index file format for repository indices. It also implements a new format for requirements.yaml. Breaking change: This will break all previous versions of Helm, and will impact helm search, repo, serve, and fetch functions. Closes #1197 --- Makefile | 2 +- cmd/helm/dependency_build_test.go | 8 +- cmd/helm/dependency_update_test.go | 5 +- cmd/helm/downloader/chart_downloader.go | 36 ++- cmd/helm/downloader/chart_downloader_test.go | 2 +- cmd/helm/downloader/manager.go | 35 +-- .../cache/kubernetes-charts-index.yaml | 84 ++++--- .../helmhome/repository/repositories.yaml | 5 +- cmd/helm/fetch_test.go | 2 +- cmd/helm/helm_test.go | 57 ++++- cmd/helm/helmpath/helmhome.go | 7 +- cmd/helm/init.go | 56 +++-- cmd/helm/init_test.go | 35 ++- cmd/helm/package.go | 10 +- cmd/helm/package_test.go | 13 + cmd/helm/repo_add.go | 45 ++-- cmd/helm/repo_add_test.go | 65 +++-- cmd/helm/repo_list.go | 13 +- cmd/helm/repo_remove.go | 43 ++-- cmd/helm/repo_remove_test.go | 37 +-- cmd/helm/repo_update.go | 27 ++- cmd/helm/repo_update_test.go | 67 ++--- cmd/helm/search.go | 5 +- cmd/helm/search/search.go | 27 ++- cmd/helm/search/search_test.go | 90 ++++--- cmd/helm/search_test.go | 6 +- cmd/helm/serve.go | 21 +- .../repository/cache/testing-index.yaml | 100 ++++---- .../helmhome/repository/repositories.yaml | 7 +- cmd/helm/testdata/repositories.yaml | 8 +- cmd/helm/testdata/testserver/index.yaml | 1 + .../testserver/repository/repositories.yaml | 6 + docs/chart_repository.md | 59 +++-- pkg/repo/doc.go | 93 +++++++ pkg/repo/index.go | 165 +++++++++---- pkg/repo/index_test.go | 212 ++++++++++++---- pkg/repo/local.go | 21 +- pkg/repo/repo.go | 178 ++++++++------ pkg/repo/repo_test.go | 229 +++++++++++++----- pkg/repo/repotest/server.go | 26 +- pkg/repo/repotest/server_test.go | 17 +- pkg/repo/testdata/local-index.yaml | 30 ++- pkg/repo/testdata/old-repositories.yaml | 3 + pkg/repo/testdata/repositories.yaml | 11 +- 44 files changed, 1296 insertions(+), 673 deletions(-) create mode 100644 cmd/helm/testdata/testserver/index.yaml create mode 100644 cmd/helm/testdata/testserver/repository/repositories.yaml create mode 100644 pkg/repo/doc.go create mode 100644 pkg/repo/testdata/old-repositories.yaml diff --git a/Makefile b/Makefile index 705d2dbe8..9599b80a7 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ test: test-unit .PHONY: test-unit test-unit: - $(GO) test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS) + HELM_HOME=/no/such/dir $(GO) test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS) .PHONY: test-style test-style: diff --git a/cmd/helm/dependency_build_test.go b/cmd/helm/dependency_build_test.go index d382d68bb..7038e9aa1 100644 --- a/cmd/helm/dependency_build_test.go +++ b/cmd/helm/dependency_build_test.go @@ -30,7 +30,7 @@ import ( func TestDependencyBuildCmd(t *testing.T) { oldhome := helmHome - hh, err := tempHelmHome() + hh, err := tempHelmHome(t) if err != nil { t.Fatal(err) } @@ -108,8 +108,12 @@ func TestDependencyBuildCmd(t *testing.T) { t.Fatal(err) } - if h := i.Entries["reqtest-0.1.0"].Digest; h != hash { + reqver := i.Entries["reqtest"][0] + if h := reqver.Digest; h != hash { t.Errorf("Failed hash match: expected %s, got %s", hash, h) } + if v := reqver.Version; v != "0.1.0" { + t.Errorf("mismatched versions. Expected %q, got %q", "0.1.0", v) + } } diff --git a/cmd/helm/dependency_update_test.go b/cmd/helm/dependency_update_test.go index 53df11eca..becbef9a3 100644 --- a/cmd/helm/dependency_update_test.go +++ b/cmd/helm/dependency_update_test.go @@ -36,7 +36,7 @@ import ( func TestDependencyUpdateCmd(t *testing.T) { // Set up a testing helm home oldhome := helmHome - hh, err := tempHelmHome() + hh, err := tempHelmHome(t) if err != nil { t.Fatal(err) } @@ -90,7 +90,8 @@ func TestDependencyUpdateCmd(t *testing.T) { t.Fatal(err) } - if h := i.Entries["reqtest-0.1.0"].Digest; h != hash { + reqver := i.Entries["reqtest"][0] + if h := reqver.Digest; h != hash { t.Errorf("Failed hash match: expected %s, got %s", hash, h) } diff --git a/cmd/helm/downloader/chart_downloader.go b/cmd/helm/downloader/chart_downloader.go index 5019739d1..db8db771d 100644 --- a/cmd/helm/downloader/chart_downloader.go +++ b/cmd/helm/downloader/chart_downloader.go @@ -69,7 +69,7 @@ type ChartDownloader struct { // For VerifyNever and VerifyIfPossible, the Verification may be empty. func (c *ChartDownloader) DownloadTo(ref string, dest string) (*provenance.Verification, error) { // resolve URL - u, err := c.ResolveChartRef(ref) + u, err := c.ResolveChartVersion(ref) if err != nil { return nil, err } @@ -111,10 +111,10 @@ func (c *ChartDownloader) DownloadTo(ref string, dest string) (*provenance.Verif return ver, nil } -// ResolveChartRef resolves a chart reference to a URL. +// ResolveChartVersion resolves a chart reference to a URL. // // A reference may be an HTTP URL, a 'reponame/chartname' reference, or a local path. -func (c *ChartDownloader) ResolveChartRef(ref string) (*url.URL, error) { +func (c *ChartDownloader) ResolveChartVersion(ref string) (*url.URL, error) { // See if it's already a full URL. u, err := url.ParseRequestURI(ref) if err == nil { @@ -122,7 +122,7 @@ func (c *ChartDownloader) ResolveChartRef(ref string) (*url.URL, error) { if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 { return u, nil } - return u, fmt.Errorf("Invalid chart url format: %s", ref) + return u, fmt.Errorf("invalid chart url format: %s", ref) } r, err := repo.LoadRepositoriesFile(c.HelmHome.RepositoryFile()) @@ -133,15 +133,29 @@ func (c *ChartDownloader) ResolveChartRef(ref string) (*url.URL, error) { // See if it's of the form: repo/path_to_chart p := strings.Split(ref, "/") if len(p) > 1 { - if baseURL, ok := r.Repositories[p[0]]; ok { - if !strings.HasSuffix(baseURL, "/") { - baseURL = baseURL + "/" - } - return url.ParseRequestURI(baseURL + strings.Join(p[1:], "/")) + rf, err := findRepoEntry(p[0], r.Repositories) + if err != nil { + return u, err + } + if rf.URL == "" { + return u, fmt.Errorf("no URL found for repository %q", p[0]) + } + baseURL := rf.URL + if !strings.HasSuffix(baseURL, "/") { + baseURL = baseURL + "/" + } + return url.ParseRequestURI(baseURL + strings.Join(p[1:], "/")) + } + return u, fmt.Errorf("invalid chart url format: %s", ref) +} + +func findRepoEntry(name string, repos []*repo.Entry) (*repo.Entry, error) { + for _, re := range repos { + if re.Name == name { + return re, nil } - return u, fmt.Errorf("No such repo: %s", p[0]) } - return u, fmt.Errorf("Invalid chart url format: %s", ref) + return nil, fmt.Errorf("no repo named %q", name) } // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart. diff --git a/cmd/helm/downloader/chart_downloader_test.go b/cmd/helm/downloader/chart_downloader_test.go index 652776a51..c8bbfa8e6 100644 --- a/cmd/helm/downloader/chart_downloader_test.go +++ b/cmd/helm/downloader/chart_downloader_test.go @@ -47,7 +47,7 @@ func TestResolveChartRef(t *testing.T) { } for _, tt := range tests { - u, err := c.ResolveChartRef(tt.ref) + u, err := c.ResolveChartVersion(tt.ref) if err != nil { if tt.fail { continue diff --git a/cmd/helm/downloader/manager.go b/cmd/helm/downloader/manager.go index 204ea240d..17ef1efd2 100644 --- a/cmd/helm/downloader/manager.go +++ b/cmd/helm/downloader/manager.go @@ -175,8 +175,7 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { for _, dep := range deps { fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) - target := fmt.Sprintf("%s-%s", dep.Name, dep.Version) - churl, err := findChartURL(target, dep.Repository, repos) + churl, err := findChartURL(dep.Name, dep.Repository, repos) if err != nil { fmt.Fprintf(m.Out, "WARNING: %s (skipped)", err) continue @@ -207,7 +206,7 @@ func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error { found = true } else { for _, repo := range repos { - if urlsAreEqual(repo, dd.Repository) { + if urlsAreEqual(repo.URL, dd.Repository) { found = true } } @@ -236,25 +235,23 @@ func (m *Manager) UpdateRepositories() error { return nil } -func (m *Manager) parallelRepoUpdate(repos map[string]string) { +func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) { out := m.Out fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") var wg sync.WaitGroup - for name, url := range repos { + for _, re := range repos { wg.Add(1) go func(n, u string) { - err := repo.DownloadIndexFile(n, u, m.HelmHome.CacheIndex(n)) - if err != nil { - updateErr := fmt.Sprintf("...Unable to get an update from the %q chart repository: %s", n, err) - fmt.Fprintln(out, updateErr) + if err := repo.DownloadIndexFile(n, u, m.HelmHome.CacheIndex(n)); err != nil { + fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", n, u, err) } else { fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", n) } wg.Done() - }(name, url) + }(re.Name, re.URL) } wg.Wait() - fmt.Fprintln(out, "Update Complete. Happy Helming!") + fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") } // urlsAreEqual normalizes two URLs and then compares for equality. @@ -280,7 +277,15 @@ func findChartURL(name, repourl string, repos map[string]*repo.ChartRepository) if urlsAreEqual(repourl, cr.URL) { for ename, entry := range cr.IndexFile.Entries { if ename == name { - return entry.URL, nil + for _, verEntry := range entry { + if len(verEntry.URLs) == 0 { + // Not totally sure what to do here. Returning an + // error is the strictest option. Skipping it might + // be preferable. + return "", fmt.Errorf("chart %q has no download URL", name) + } + return verEntry.URLs[0], nil + } } } } @@ -302,8 +307,8 @@ func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, err return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err) } - // localName: chartRepo - for lname, url := range rf.Repositories { + for _, re := range rf.Repositories { + lname := re.Name cacheindex := m.HelmHome.CacheIndex(lname) index, err := repo.LoadIndexFile(cacheindex) if err != nil { @@ -311,7 +316,7 @@ func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, err } cr := &repo.ChartRepository{ - URL: url, + URL: re.URL, IndexFile: index, } indices[lname] = cr diff --git a/cmd/helm/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml b/cmd/helm/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml index 6b27d1b84..26ce97423 100644 --- a/cmd/helm/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml +++ b/cmd/helm/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml @@ -1,38 +1,46 @@ -alpine-0.1.0: - name: alpine - url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz - created: 2016-09-06 21:58:44.211261566 +0000 UTC - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - chartfile: - name: alpine - home: https://k8s.io/helm - sources: - - https://github.com/kubernetes/helm - version: 0.1.0 - description: Deploy a basic Alpine Linux pod - keywords: [] - maintainers: [] - engine: "" - icon: "" -mariadb-0.3.0: - name: mariadb - url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz - created: 2016-09-06 21:58:44.211870222 +0000 UTC - checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56 - chartfile: - name: mariadb - home: https://mariadb.org - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - version: 0.3.0 - description: Chart for MariaDB - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - name: Bitnami - email: containers@bitnami.com - engine: gotpl - icon: "" +apiVersion: v1 +entries: + alpine: + - name: alpine + url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + home: https://k8s.io/helm + sources: + - https://github.com/kubernetes/helm + version: 0.1.0 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + engine: "" + icon: "" + - name: alpine + url: http://storage.googleapis.com/kubernetes-charts/alpine-0.2.0.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + home: https://k8s.io/helm + sources: + - https://github.com/kubernetes/helm + version: 0.2.0 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + engine: "" + icon: "" + mariadb: + - name: mariadb + url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz + checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56 + home: https://mariadb.org + sources: + - https://github.com/bitnami/bitnami-docker-mariadb + version: 0.3.0 + description: Chart for MariaDB + keywords: + - mariadb + - mysql + - database + - sql + maintainers: + - name: Bitnami + email: containers@bitnami.com + engine: gotpl + icon: "" diff --git a/cmd/helm/downloader/testdata/helmhome/repository/repositories.yaml b/cmd/helm/downloader/testdata/helmhome/repository/repositories.yaml index 29cb2d2bf..10d83457e 100644 --- a/cmd/helm/downloader/testdata/helmhome/repository/repositories.yaml +++ b/cmd/helm/downloader/testdata/helmhome/repository/repositories.yaml @@ -1 +1,4 @@ -testing: "http://example.com" +apiVersion: v1 +repositories: + - name: testing + url: "http://example.com" diff --git a/cmd/helm/fetch_test.go b/cmd/helm/fetch_test.go index 3dd241a1f..b24d255e7 100644 --- a/cmd/helm/fetch_test.go +++ b/cmd/helm/fetch_test.go @@ -26,7 +26,7 @@ import ( ) func TestFetchCmd(t *testing.T) { - hh, err := tempHelmHome() + hh, err := tempHelmHome(t) if err != nil { t.Fatal(err) } diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index ef99484ba..94dd57122 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -22,17 +22,20 @@ import ( "io" "io/ioutil" "math/rand" + "os" "regexp" "testing" "github.com/golang/protobuf/ptypes/timestamp" "github.com/spf13/cobra" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/release" rls "k8s.io/helm/pkg/proto/hapi/services" "k8s.io/helm/pkg/proto/hapi/version" + "k8s.io/helm/pkg/repo" ) var mockHookTemplate = `apiVersion: v1 @@ -211,11 +214,11 @@ type releaseCase struct { resp *release.Release } -// tmpHelmHome sets up a Helm Home in a temp dir. +// tempHelmHome sets up a Helm Home in a temp dir. // // This does not clean up the directory. You must do that yourself. // You must also set helmHome yourself. -func tempHelmHome() (string, error) { +func tempHelmHome(t *testing.T) (string, error) { oldhome := helmHome dir, err := ioutil.TempDir("", "helm_home-") if err != nil { @@ -223,9 +226,57 @@ func tempHelmHome() (string, error) { } helmHome = dir - if err := ensureHome(); err != nil { + if err := ensureTestHome(helmpath.Home(helmHome), t); err != nil { return "n/", err } helmHome = oldhome return dir, nil } + +// ensureTestHome creates a home directory like ensureHome, but without remote references. +// +// t is used only for logging. +func ensureTestHome(home helmpath.Home, t *testing.T) error { + configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository()} + for _, p := range configDirectories { + if fi, err := os.Stat(p); err != nil { + if err := os.MkdirAll(p, 0755); err != nil { + return fmt.Errorf("Could not create %s: %s", p, err) + } + } else if !fi.IsDir() { + return fmt.Errorf("%s must be a directory", p) + } + } + + repoFile := home.RepositoryFile() + if fi, err := os.Stat(repoFile); err != nil { + rf := repo.NewRepoFile() + if err := rf.WriteFile(repoFile, 0644); err != nil { + return err + } + } else if fi.IsDir() { + return fmt.Errorf("%s must be a file, not a directory", repoFile) + } + if r, err := repo.LoadRepositoriesFile(repoFile); err == repo.ErrRepoOutOfDate { + t.Log("Updating repository file format...") + if err := r.WriteFile(repoFile, 0644); err != nil { + return err + } + } + + localRepoIndexFile := home.LocalRepository(localRepoIndexFilePath) + if fi, err := os.Stat(localRepoIndexFile); err != nil { + i := repo.NewIndexFile() + if err := i.WriteFile(localRepoIndexFile, 0644); err != nil { + return err + } + + //TODO: take this out and replace with helm update functionality + os.Symlink(localRepoIndexFile, cacheDirectory("local-index.yaml")) + } else if fi.IsDir() { + return fmt.Errorf("%s must be a file, not a directory", localRepoIndexFile) + } + + t.Logf("$HELM_HOME has been configured at %s.\n", helmHome) + return nil +} diff --git a/cmd/helm/helmpath/helmhome.go b/cmd/helm/helmpath/helmhome.go index d2749ef2f..798ab3d5f 100644 --- a/cmd/helm/helmpath/helmhome.go +++ b/cmd/helm/helmpath/helmhome.go @@ -56,6 +56,9 @@ func (h Home) CacheIndex(name string) string { // LocalRepository returns the location to the local repo. // // The local repo is the one used by 'helm serve' -func (h Home) LocalRepository() string { - return filepath.Join(string(h), "repository/local") +// +// If additional path elements are passed, they are appended to the returned path. +func (h Home) LocalRepository(paths ...string) string { + frag := append([]string{string(h), "repository/local"}, paths...) + return filepath.Join(frag...) } diff --git a/cmd/helm/init.go b/cmd/helm/init.go index d8f807691..c0e091041 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -25,7 +25,9 @@ import ( "github.com/spf13/cobra" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/cmd/helm/installer" + "k8s.io/helm/pkg/repo" ) const initDesc = ` @@ -33,15 +35,18 @@ This command installs Tiller (the helm server side component) onto your Kubernetes Cluster and sets up local configuration in $HELM_HOME (default: ~/.helm/) ` -var ( - defaultRepository = "stable" - defaultRepositoryURL = "http://storage.googleapis.com/kubernetes-charts" +const ( + stableRepository = "stable" + localRepository = "local" + stableRepositoryURL = "http://storage.googleapis.com/kubernetes-charts" + localRepositoryURL = "http://localhost:8879/charts" ) type initCmd struct { image string clientOnly bool out io.Writer + home helmpath.Home } func newInitCmd(out io.Writer) *cobra.Command { @@ -56,6 +61,7 @@ func newInitCmd(out io.Writer) *cobra.Command { if len(args) != 0 { return errors.New("This command does not accept arguments") } + i.home = helmpath.Home(homePath()) return i.run() }, } @@ -66,7 +72,7 @@ func newInitCmd(out io.Writer) *cobra.Command { // runInit initializes local config and installs tiller to Kubernetes Cluster func (i *initCmd) run() error { - if err := ensureHome(); err != nil { + if err := ensureHome(i.home, i.out); err != nil { return err } @@ -101,12 +107,11 @@ func requireHome() error { // ensureHome checks to see if $HELM_HOME exists // // If $HELM_HOME does not exist, this function will create it. -func ensureHome() error { - configDirectories := []string{homePath(), repositoryDirectory(), cacheDirectory(), localRepoDirectory()} - +func ensureHome(home helmpath.Home, out io.Writer) error { + configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository()} for _, p := range configDirectories { if fi, err := os.Stat(p); err != nil { - fmt.Printf("Creating %s \n", p) + fmt.Fprintf(out, "Creating %s \n", p) if err := os.MkdirAll(p, 0755); err != nil { return fmt.Errorf("Could not create %s: %s", p, err) } @@ -115,24 +120,41 @@ func ensureHome() error { } } - repoFile := repositoriesFile() + repoFile := home.RepositoryFile() if fi, err := os.Stat(repoFile); err != nil { - fmt.Printf("Creating %s \n", repoFile) - if _, err := os.Create(repoFile); err != nil { + fmt.Fprintf(out, "Creating %s \n", repoFile) + r := repo.NewRepoFile() + r.Add(&repo.Entry{ + Name: stableRepository, + URL: stableRepositoryURL, + Cache: "stable-index.yaml", + }, &repo.Entry{ + Name: localRepository, + URL: localRepositoryURL, + Cache: "local-index.yaml", + }) + if err := r.WriteFile(repoFile, 0644); err != nil { return err } - if err := addRepository(defaultRepository, defaultRepositoryURL); err != nil { - return err + cif := home.CacheIndex(stableRepository) + if err := repo.DownloadIndexFile(stableRepository, stableRepositoryURL, cif); err != nil { + fmt.Fprintf(out, "WARNING: Failed to download %s: %s (run 'helm update')\n", stableRepository, err) } } else if fi.IsDir() { return fmt.Errorf("%s must be a file, not a directory", repoFile) } + if r, err := repo.LoadRepositoriesFile(repoFile); err == repo.ErrRepoOutOfDate { + fmt.Fprintln(out, "Updating repository file format...") + if err := r.WriteFile(repoFile, 0644); err != nil { + return err + } + } localRepoIndexFile := localRepoDirectory(localRepoIndexFilePath) if fi, err := os.Stat(localRepoIndexFile); err != nil { - fmt.Printf("Creating %s \n", localRepoIndexFile) - _, err := os.Create(localRepoIndexFile) - if err != nil { + fmt.Fprintf(out, "Creating %s \n", localRepoIndexFile) + i := repo.NewIndexFile() + if err := i.WriteFile(localRepoIndexFile, 0644); err != nil { return err } @@ -142,6 +164,6 @@ func ensureHome() error { return fmt.Errorf("%s must be a file, not a directory", localRepoIndexFile) } - fmt.Printf("$HELM_HOME has been configured at %s.\n", helmHome) + fmt.Fprintf(out, "$HELM_HOME has been configured at %s.\n", helmHome) return nil } diff --git a/cmd/helm/init_test.go b/cmd/helm/init_test.go index df20e3786..171ef1ac5 100644 --- a/cmd/helm/init_test.go +++ b/cmd/helm/init_test.go @@ -17,28 +17,29 @@ limitations under the License. package main import ( - "fmt" + "bytes" "io/ioutil" - "net/http" - "net/http/httptest" "os" "testing" + + "k8s.io/helm/cmd/helm/helmpath" ) func TestEnsureHome(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") - fmt.Fprintln(w, "") - })) - defaultRepositoryURL = ts.URL + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.Remove(home) - home := createTmpHome() + b := bytes.NewBuffer(nil) + hh := helmpath.Home(home) helmHome = home - if err := ensureHome(); err != nil { - t.Errorf("%s", err) + if err := ensureHome(hh, b); err != nil { + t.Error(err) } - expectedDirs := []string{homePath(), repositoryDirectory(), cacheDirectory(), localRepoDirectory()} + expectedDirs := []string{hh.String(), hh.Repository(), hh.Cache(), hh.LocalRepository()} for _, dir := range expectedDirs { if fi, err := os.Stat(dir); err != nil { t.Errorf("%s", err) @@ -47,8 +48,8 @@ func TestEnsureHome(t *testing.T) { } } - if fi, err := os.Stat(repositoriesFile()); err != nil { - t.Errorf("%s", err) + if fi, err := os.Stat(hh.RepositoryFile()); err != nil { + t.Error(err) } else if fi.IsDir() { t.Errorf("%s should not be a directory", fi) } @@ -59,9 +60,3 @@ func TestEnsureHome(t *testing.T) { t.Errorf("%s should not be a directory", fi) } } - -func createTmpHome() string { - tmpHome, _ := ioutil.TempDir("", "helm_home") - defer os.Remove(tmpHome) - return tmpHome -} diff --git a/cmd/helm/package.go b/cmd/helm/package.go index fac2a2fef..df4583c62 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -26,6 +26,7 @@ import ( "github.com/spf13/cobra" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/provenance" @@ -50,6 +51,7 @@ type packageCmd struct { key string keyring string out io.Writer + home helmpath.Home } func newPackageCmd(client helm.Interface, out io.Writer) *cobra.Command { @@ -61,6 +63,7 @@ func newPackageCmd(client helm.Interface, out io.Writer) *cobra.Command { Short: "package a chart directory into a chart archive", Long: packageDesc, RunE: func(cmd *cobra.Command, args []string) error { + pkg.home = helmpath.Home(homePath()) if len(args) == 0 { return fmt.Errorf("This command needs at least one argument, the path to the chart.") } @@ -113,16 +116,17 @@ func (p *packageCmd) run(cmd *cobra.Command, args []string) error { } name, err := chartutil.Save(ch, cwd) if err == nil && flagDebug { - cmd.Printf("Saved %s to current directory\n", name) + fmt.Fprintf(p.out, "Saved %s to current directory\n", name) } // Save to $HELM_HOME/local directory. This is second, because we don't want // the case where we saved here, but didn't save to the default destination. if p.save { - if err := repo.AddChartToLocalRepo(ch, localRepoDirectory()); err != nil { + lr := p.home.LocalRepository() + if err := repo.AddChartToLocalRepo(ch, lr); err != nil { return err } else if flagDebug { - cmd.Printf("Saved %s to %s\n", name, localRepoDirectory()) + fmt.Fprintf(p.out, "Saved %s to %s\n", name, lr) } } diff --git a/cmd/helm/package_test.go b/cmd/helm/package_test.go index b724ba2ea..fc2060c16 100644 --- a/cmd/helm/package_test.go +++ b/cmd/helm/package_test.go @@ -24,6 +24,8 @@ import ( "testing" "github.com/spf13/cobra" + + "k8s.io/helm/cmd/helm/helmpath" ) func TestPackage(t *testing.T) { @@ -57,6 +59,13 @@ func TestPackage(t *testing.T) { expect: "keyring is required for signing a package", err: true, }, + { + name: "package testdata/testcharts/alpine, no save", + args: []string{"testdata/testcharts/alpine"}, + flags: map[string]string{"save": "0"}, + expect: "", + hasfile: "alpine-0.1.0.tgz", + }, { name: "package testdata/testcharts/alpine", args: []string{"testdata/testcharts/alpine"}, @@ -87,7 +96,11 @@ func TestPackage(t *testing.T) { t.Fatal(err) } + ensureTestHome(helmpath.Home(tmp), t) + oldhome := homePath() + helmHome = tmp defer func() { + helmHome = oldhome os.Chdir(origDir) os.RemoveAll(tmp) }() diff --git a/cmd/helm/repo_add.go b/cmd/helm/repo_add.go index 5ca4cc799..fd31e0d33 100644 --- a/cmd/helm/repo_add.go +++ b/cmd/helm/repo_add.go @@ -17,20 +17,20 @@ limitations under the License. package main import ( - "errors" "fmt" "io" - "io/ioutil" + "path/filepath" "github.com/spf13/cobra" - "gopkg.in/yaml.v2" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/repo" ) type repoAddCmd struct { name string url string + home helmpath.Home out io.Writer } @@ -49,6 +49,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command { add.name = args[0] add.url = args[1] + add.home = helmpath.Home(homePath()) return add.run() }, @@ -57,38 +58,36 @@ func newRepoAddCmd(out io.Writer) *cobra.Command { } func (a *repoAddCmd) run() error { - if err := addRepository(a.name, a.url); err != nil { + if err := addRepository(a.name, a.url, a.home); err != nil { return err } - - fmt.Println(a.name + " has been added to your repositories") + fmt.Fprintf(a.out, "%q has been added to your repositories\n", a.name) return nil } -func addRepository(name, url string) error { - if err := repo.DownloadIndexFile(name, url, cacheIndexFile(name)); err != nil { - return errors.New("Looks like " + url + " is not a valid chart repository or cannot be reached: " + err.Error()) +func addRepository(name, url string, home helmpath.Home) error { + cif := home.CacheIndex(name) + if err := repo.DownloadIndexFile(name, url, cif); err != nil { + return fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", url, err.Error()) } - return insertRepoLine(name, url) + return insertRepoLine(name, url, home) } -func insertRepoLine(name, url string) error { - f, err := repo.LoadRepositoriesFile(repositoriesFile()) +func insertRepoLine(name, url string, home helmpath.Home) error { + cif := home.CacheIndex(name) + f, err := repo.LoadRepositoriesFile(home.RepositoryFile()) if err != nil { return err } - _, ok := f.Repositories[name] - if ok { - return fmt.Errorf("The repository name you provided (%s) already exists. Please specify a different name.", name) - } - if f.Repositories == nil { - f.Repositories = make(map[string]string) + if f.Has(name) { + return fmt.Errorf("The repository name you provided (%s) already exists. Please specify a different name.", name) } - - f.Repositories[name] = url - - b, _ := yaml.Marshal(&f.Repositories) - return ioutil.WriteFile(repositoriesFile(), b, 0666) + f.Add(&repo.Entry{ + Name: name, + URL: url, + Cache: filepath.Base(cif), + }) + return f.WriteFile(home.RepositoryFile(), 0644) } diff --git a/cmd/helm/repo_add_test.go b/cmd/helm/repo_add_test.go index be6a669ef..218353174 100644 --- a/cmd/helm/repo_add_test.go +++ b/cmd/helm/repo_add_test.go @@ -18,29 +18,35 @@ package main import ( "bytes" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" "os" - "path/filepath" "testing" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/repo" + "k8s.io/helm/pkg/repo/repotest" ) var testName = "test-name" func TestRepoAddCmd(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") - fmt.Fprintln(w, "") - })) + srv := repotest.NewServer("testdata/testserver") + defer srv.Stop() + + thome, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + oldhome := homePath() + helmHome = thome + defer func() { + helmHome = oldhome + os.Remove(thome) + }() tests := []releaseCase{ { name: "add a repository", - args: []string{testName, ts.URL}, + args: []string{testName, srv.URL()}, expected: testName + " has been added to your repositories", }, } @@ -49,41 +55,32 @@ func TestRepoAddCmd(t *testing.T) { buf := bytes.NewBuffer(nil) c := newRepoAddCmd(buf) if err := c.RunE(c, tt.args); err != nil { - t.Errorf("%q: expected '%q', got '%q'", tt.name, tt.expected, err) + t.Errorf("%q: expected %q, got %q", tt.name, tt.expected, err) } } } func TestRepoAdd(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") - fmt.Fprintln(w, "") - })) - - helmHome, _ = ioutil.TempDir("", "helm_home") - defer os.Remove(helmHome) - os.Mkdir(filepath.Join(helmHome, repositoryDir), 0755) - os.Mkdir(cacheDirectory(), 0755) - - if err := ioutil.WriteFile(repositoriesFile(), []byte("example-repo: http://exampleurl.com"), 0666); err != nil { - t.Errorf("%#v", err) + ts := repotest.NewServer("testdata/testserver") + defer ts.Stop() + + thome, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) } + defer os.Remove(thome) + hh := helmpath.Home(thome) - if err := addRepository(testName, ts.URL); err != nil { - t.Errorf("%s", err) + if err := addRepository(testName, ts.URL(), hh); err != nil { + t.Error(err) } - f, err := repo.LoadRepositoriesFile(repositoriesFile()) + f, err := repo.LoadRepositoriesFile(hh.RepositoryFile()) if err != nil { - t.Errorf("%s", err) - } - _, ok := f.Repositories[testName] - if !ok { - t.Errorf("%s was not successfully inserted into %s", testName, repositoriesFile()) + t.Error(err) } - if err := insertRepoLine(testName, ts.URL); err == nil { - t.Errorf("Duplicate repository name was added") + if !f.Has(testName) { + t.Errorf("%s was not successfully inserted into %s", testName, hh.RepositoryFile()) } - } diff --git a/cmd/helm/repo_list.go b/cmd/helm/repo_list.go index 21696a0aa..a3816facd 100644 --- a/cmd/helm/repo_list.go +++ b/cmd/helm/repo_list.go @@ -24,11 +24,13 @@ import ( "github.com/gosuri/uitable" "github.com/spf13/cobra" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/repo" ) type repoListCmd struct { - out io.Writer + out io.Writer + home helmpath.Home } func newRepoListCmd(out io.Writer) *cobra.Command { @@ -40,6 +42,7 @@ func newRepoListCmd(out io.Writer) *cobra.Command { Use: "list [flags]", Short: "list chart repositories", RunE: func(cmd *cobra.Command, args []string) error { + list.home = helmpath.Home(homePath()) return list.run() }, } @@ -48,7 +51,7 @@ func newRepoListCmd(out io.Writer) *cobra.Command { } func (a *repoListCmd) run() error { - f, err := repo.LoadRepositoriesFile(repositoriesFile()) + f, err := repo.LoadRepositoriesFile(a.home.RepositoryFile()) if err != nil { return err } @@ -58,9 +61,9 @@ func (a *repoListCmd) run() error { table := uitable.New() table.MaxColWidth = 50 table.AddRow("NAME", "URL") - for k, v := range f.Repositories { - table.AddRow(k, v) + for _, re := range f.Repositories { + table.AddRow(re.Name, re.URL) } - fmt.Println(table) + fmt.Fprintln(a.out, table) return nil } diff --git a/cmd/helm/repo_remove.go b/cmd/helm/repo_remove.go index f58f78212..422b545b7 100644 --- a/cmd/helm/repo_remove.go +++ b/cmd/helm/repo_remove.go @@ -19,18 +19,18 @@ package main import ( "fmt" "io" - "io/ioutil" "os" "github.com/spf13/cobra" - "gopkg.in/yaml.v2" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/repo" ) type repoRemoveCmd struct { out io.Writer name string + home helmpath.Home } func newRepoRemoveCmd(out io.Writer) *cobra.Command { @@ -47,6 +47,7 @@ func newRepoRemoveCmd(out io.Writer) *cobra.Command { return err } remove.name = args[0] + remove.home = helmpath.Home(homePath()) return remove.run() }, @@ -56,41 +57,35 @@ func newRepoRemoveCmd(out io.Writer) *cobra.Command { } func (r *repoRemoveCmd) run() error { - return removeRepoLine(r.name) + return removeRepoLine(r.out, r.name, r.home) } -func removeRepoLine(name string) error { - r, err := repo.LoadRepositoriesFile(repositoriesFile()) +func removeRepoLine(out io.Writer, name string, home helmpath.Home) error { + repoFile := home.RepositoryFile() + r, err := repo.LoadRepositoriesFile(repoFile) if err != nil { return err } - _, ok := r.Repositories[name] - if ok { - delete(r.Repositories, name) - b, err := yaml.Marshal(&r.Repositories) - if err != nil { - return err - } - if err := ioutil.WriteFile(repositoriesFile(), b, 0666); err != nil { - return err - } - if err := removeRepoCache(name); err != nil { - return err - } + if !r.Remove(name) { + return fmt.Errorf("no repo named %q found", name) + } + if err := r.WriteFile(repoFile, 0644); err != nil { + return err + } - } else { - return fmt.Errorf("The repository, %s, does not exist in your repositories list", name) + if err := removeRepoCache(name, home); err != nil { + return err } - fmt.Println(name + " has been removed from your repositories") + fmt.Fprintf(out, "%q has been removed from your repositories", name) return nil } -func removeRepoCache(name string) error { - if _, err := os.Stat(cacheIndexFile(name)); err == nil { - err = os.Remove(cacheIndexFile(name)) +func removeRepoCache(name string, home helmpath.Home) error { + if _, err := os.Stat(home.CacheIndex(name)); err == nil { + err = os.Remove(home.CacheIndex(name)) if err != nil { return err } diff --git a/cmd/helm/repo_remove_test.go b/cmd/helm/repo_remove_test.go index ea4fa6b55..77b30426a 100644 --- a/cmd/helm/repo_remove_test.go +++ b/cmd/helm/repo_remove_test.go @@ -17,45 +17,52 @@ limitations under the License. package main import ( + "bytes" "os" + "strings" "testing" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/repo" ) func TestRepoRemove(t *testing.T) { testURL := "https://test-url.com" - home := createTmpHome() - helmHome = home - if err := ensureHome(); err != nil { - t.Errorf("%s", err) - } - if err := removeRepoLine(testName); err == nil { + b := bytes.NewBuffer(nil) + + home, err := tempHelmHome(t) + defer os.Remove(home) + hh := helmpath.Home(home) + + if err := removeRepoLine(b, testName, hh); err == nil { t.Errorf("Expected error removing %s, but did not get one.", testName) } - - if err := insertRepoLine(testName, testURL); err != nil { - t.Errorf("%s", err) + if err := insertRepoLine(testName, testURL, hh); err != nil { + t.Error(err) } - mf, _ := os.Create(cacheIndexFile(testName)) + mf, _ := os.Create(hh.CacheIndex(testName)) mf.Close() - if err := removeRepoLine(testName); err != nil { + b.Reset() + if err := removeRepoLine(b, testName, hh); err != nil { t.Errorf("Error removing %s from repositories", testName) } + if !strings.Contains(b.String(), "has been removed") { + t.Errorf("Unexpected output: %s", b.String()) + } - if _, err := os.Stat(cacheIndexFile(testName)); err == nil { + if _, err := os.Stat(hh.CacheIndex(testName)); err == nil { t.Errorf("Error cache file was not removed for repository %s", testName) } - f, err := repo.LoadRepositoriesFile(repositoriesFile()) + f, err := repo.LoadRepositoriesFile(hh.RepositoryFile()) if err != nil { - t.Errorf("%s", err) + t.Error(err) } - if _, ok := f.Repositories[testName]; ok { + if f.Has(testName) { t.Errorf("%s was not successfully removed from repositories list", testName) } } diff --git a/cmd/helm/repo_update.go b/cmd/helm/repo_update.go index 865e5eae0..1a7685abe 100644 --- a/cmd/helm/repo_update.go +++ b/cmd/helm/repo_update.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/cobra" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/repo" ) @@ -37,8 +38,9 @@ future releases. type repoUpdateCmd struct { repoFile string - update func(map[string]string, bool, io.Writer) + update func([]*repo.Entry, bool, io.Writer, helmpath.Home) out io.Writer + home helmpath.Home } func newRepoUpdateCmd(out io.Writer) *cobra.Command { @@ -53,6 +55,7 @@ func newRepoUpdateCmd(out io.Writer) *cobra.Command { Short: "update information on available charts in the chart repositories", Long: updateDesc, RunE: func(cmd *cobra.Command, args []string) error { + u.home = helmpath.Home(homePath()) return u.run() }, } @@ -69,29 +72,29 @@ func (u *repoUpdateCmd) run() error { return errors.New("no repositories found. You must add one before updating") } - u.update(f.Repositories, flagDebug, u.out) + u.update(f.Repositories, flagDebug, u.out, u.home) return nil } -func updateCharts(repos map[string]string, verbose bool, out io.Writer) { +func updateCharts(repos []*repo.Entry, verbose bool, out io.Writer, home helmpath.Home) { fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") var wg sync.WaitGroup - for name, url := range repos { + for _, re := range repos { wg.Add(1) go func(n, u string) { defer wg.Done() - err := repo.DownloadIndexFile(n, u, cacheIndexFile(n)) + if n == localRepository { + // We skip local because the indices are symlinked. + return + } + err := repo.DownloadIndexFile(n, u, home.CacheIndex(n)) if err != nil { - updateErr := fmt.Sprintf("...Unable to get an update from the %q chart repository", n) - if verbose { - updateErr = updateErr + ": " + err.Error() - } - fmt.Fprintln(out, updateErr) + fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", n, u, err) } else { fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", n) } - }(name, url) + }(re.Name, re.URL) } wg.Wait() - fmt.Fprintln(out, "Update Complete. Happy Helming!") + fmt.Fprintln(out, "Update Complete. ⎈ Happy Helming!⎈ ") } diff --git a/cmd/helm/repo_update_test.go b/cmd/helm/repo_update_test.go index 867babd80..6ec59c410 100644 --- a/cmd/helm/repo_update_test.go +++ b/cmd/helm/repo_update_test.go @@ -19,19 +19,34 @@ import ( "bytes" "fmt" "io" - "net/http" - "net/http/httptest" + "os" "strings" "testing" + + "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/pkg/repo" + "k8s.io/helm/pkg/repo/repotest" ) func TestUpdateCmd(t *testing.T) { + + thome, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + oldhome := homePath() + helmHome = thome + defer func() { + helmHome = oldhome + os.Remove(thome) + }() + out := bytes.NewBuffer(nil) // Instead of using the HTTP updater, we provide our own for this test. // The TestUpdateCharts test verifies the HTTP behavior independently. - updater := func(repos map[string]string, verbose bool, out io.Writer) { - for name := range repos { - fmt.Fprintln(out, name) + updater := func(repos []*repo.Entry, verbose bool, out io.Writer, home helmpath.Home) { + for _, re := range repos { + fmt.Fprintln(out, re.Name) } } uc := &repoUpdateCmd{ @@ -46,36 +61,26 @@ func TestUpdateCmd(t *testing.T) { } } -const mockRepoIndex = ` -mychart-0.1.0: - name: mychart-0.1.0 - url: localhost:8879/charts/mychart-0.1.0.tgz - chartfile: - name: "" - home: "" - sources: [] - version: "" - description: "" - keywords: [] - maintainers: [] - engine: "" - icon: "" -` - func TestUpdateCharts(t *testing.T) { - // This tests the repo in isolation. It creates a mock HTTP server that simply - // returns a static YAML file in the anticipate format. - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(mockRepoIndex)) - }) - srv := httptest.NewServer(handler) - defer srv.Close() + srv := repotest.NewServer("testdata/testserver") + defer srv.Stop() + + thome, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + oldhome := homePath() + helmHome = thome + defer func() { + helmHome = oldhome + os.Remove(thome) + }() buf := bytes.NewBuffer(nil) - repos := map[string]string{ - "charts": srv.URL, + repos := []*repo.Entry{ + {Name: "charts", URL: srv.URL()}, } - updateCharts(repos, false, buf) + updateCharts(repos, false, buf, helmpath.Home(thome)) got := buf.String() if strings.Contains(got, "Unable to get an update") { diff --git a/cmd/helm/search.go b/cmd/helm/search.go index 625b980bd..f8b02cff8 100644 --- a/cmd/helm/search.go +++ b/cmd/helm/search.go @@ -101,11 +101,12 @@ func (s *searchCmd) buildIndex() (*search.Index, error) { } i := search.NewIndex() - for n := range rf.Repositories { + for _, re := range rf.Repositories { + n := re.Name f := s.helmhome.CacheIndex(n) ind, err := repo.LoadIndexFile(f) if err != nil { - fmt.Fprintf(s.out, "WARNING: Repo %q is corrupt. Try 'helm update': %s", f, err) + fmt.Fprintf(s.out, "WARNING: Repo %q is corrupt or missing. Try 'helm update':\n\t%s\n", f, err) continue } diff --git a/cmd/helm/search/search.go b/cmd/helm/search/search.go index 0dfb72add..def6da698 100644 --- a/cmd/helm/search/search.go +++ b/cmd/helm/search/search.go @@ -44,27 +44,34 @@ type Result struct { // Index is a searchable index of chart information. type Index struct { lines map[string]string - charts map[string]*repo.ChartRef + charts map[string]*repo.ChartVersion } const sep = "\v" // NewIndex creats a new Index. func NewIndex() *Index { - return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartRef{}} + return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartVersion{}} } // AddRepo adds a repository index to the search index. func (i *Index) AddRepo(rname string, ind *repo.IndexFile) { for name, ref := range ind.Entries { + if len(ref) == 0 { + // Skip chart names that havae zero releases. + continue + } + // By convention, an index file is supposed to have the newest at the + // 0 slot, so our best bet is to grab the 0 entry and build the index + // entry off of that. fname := filepath.Join(rname, name) - i.lines[fname] = indstr(rname, ref) - i.charts[fname] = ref + i.lines[fname] = indstr(rname, ref[0]) + i.charts[fname] = ref[0] } } // Entries returns the entries in an index. -func (i *Index) Entries() map[string]*repo.ChartRef { +func (i *Index) Entries() map[string]*repo.ChartVersion { return i.charts } @@ -136,7 +143,7 @@ func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) { } // Chart returns the ChartRef for a particular name. -func (i *Index) Chart(name string) (*repo.ChartRef, error) { +func (i *Index) Chart(name string) (*repo.ChartVersion, error) { c, ok := i.charts[name] if !ok { return nil, errors.New("no such chart") @@ -174,10 +181,8 @@ func (s scoreSorter) Less(a, b int) bool { return first.Name < second.Name } -func indstr(name string, ref *repo.ChartRef) string { - i := ref.Name + sep + name + "/" + ref.Name + sep - if ref.Chartfile != nil { - i += ref.Chartfile.Description + sep + strings.Join(ref.Chartfile.Keywords, sep) - } +func indstr(name string, ref *repo.ChartVersion) string { + i := ref.Name + sep + name + "/" + ref.Name + sep + + ref.Description + sep + strings.Join(ref.Keywords, " ") return strings.ToLower(i) } diff --git a/cmd/helm/search/search_test.go b/cmd/helm/search/search_test.go index e20b8e756..9cfa1bc3e 100644 --- a/cmd/helm/search/search_test.go +++ b/cmd/helm/search/search_test.go @@ -52,32 +52,43 @@ func TestSortScore(t *testing.T) { var testCacheDir = "../testdata/" -var indexfileEntries = map[string]*repo.ChartRef{ - "niña-0.1.0": { - Name: "niña", - URL: "http://example.com/charts/nina-0.1.0.tgz", - Chartfile: &chart.Metadata{ - Name: "niña", - Version: "0.1.0", - Description: "One boat", +var indexfileEntries = map[string]repo.ChartVersions{ + "niña": { + { + URLs: []string{"http://example.com/charts/nina-0.1.0.tgz"}, + Metadata: &chart.Metadata{ + Name: "niña", + Version: "0.1.0", + Description: "One boat", + }, }, }, - "pinta-0.1.0": { - Name: "pinta", - URL: "http://example.com/charts/pinta-0.1.0.tgz", - Chartfile: &chart.Metadata{ - Name: "pinta", - Version: "0.1.0", - Description: "Two ship", + "pinta": { + { + URLs: []string{"http://example.com/charts/pinta-0.1.0.tgz"}, + Metadata: &chart.Metadata{ + Name: "pinta", + Version: "0.1.0", + Description: "Two ship", + }, }, }, - "santa-maria-1.2.3": { - Name: "santa-maria", - URL: "http://example.com/charts/santa-maria-1.2.3.tgz", - Chartfile: &chart.Metadata{ - Name: "santa-maria", - Version: "1.2.3", - Description: "Three boat", + "santa-maria": { + { + URLs: []string{"http://example.com/charts/santa-maria-1.2.3.tgz"}, + Metadata: &chart.Metadata{ + Name: "santa-maria", + Version: "1.2.3", + Description: "Three boat", + }, + }, + { + URLs: []string{"http://example.com/charts/santa-maria-1.2.2.tgz"}, + Metadata: &chart.Metadata{ + Name: "santa-maria", + Version: "1.2.2", + Description: "Three boat", + }, }, }, } @@ -85,14 +96,15 @@ var indexfileEntries = map[string]*repo.ChartRef{ func loadTestIndex(t *testing.T) *Index { i := NewIndex() i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}) - i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]*repo.ChartRef{ - "pinta-2.0.0": { - Name: "pinta", - URL: "http://example.com/charts/pinta-2.0.0.tgz", - Chartfile: &chart.Metadata{ - Name: "pinta", - Version: "2.0.0", - Description: "Two ship, version two", + i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{ + "pinta": { + { + URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"}, + Metadata: &chart.Metadata{ + Name: "pinta", + Version: "2.0.0", + Description: "Two ship, version two", + }, }, }, }}) @@ -113,44 +125,44 @@ func TestSearchByName(t *testing.T) { name: "basic search for one result", query: "santa-maria", expect: []*Result{ - {Name: "testing/santa-maria-1.2.3"}, + {Name: "testing/santa-maria"}, }, }, { name: "basic search for two results", query: "pinta", expect: []*Result{ - {Name: "testing/pinta-0.1.0"}, - {Name: "ztesting/pinta-2.0.0"}, + {Name: "testing/pinta"}, + {Name: "ztesting/pinta"}, }, }, { name: "repo-specific search for one result", query: "ztesting/pinta", expect: []*Result{ - {Name: "ztesting/pinta-2.0.0"}, + {Name: "ztesting/pinta"}, }, }, { name: "partial name search", query: "santa", expect: []*Result{ - {Name: "testing/santa-maria-1.2.3"}, + {Name: "testing/santa-maria"}, }, }, { name: "description search, one result", query: "Three", expect: []*Result{ - {Name: "testing/santa-maria-1.2.3"}, + {Name: "testing/santa-maria"}, }, }, { name: "description search, two results", query: "two", expect: []*Result{ - {Name: "testing/pinta-0.1.0"}, - {Name: "ztesting/pinta-2.0.0"}, + {Name: "testing/pinta"}, + {Name: "ztesting/pinta"}, }, }, { @@ -162,7 +174,7 @@ func TestSearchByName(t *testing.T) { name: "regexp, one result", query: "th[ref]*", expect: []*Result{ - {Name: "testing/santa-maria-1.2.3"}, + {Name: "testing/santa-maria"}, }, regexp: true, }, diff --git a/cmd/helm/search_test.go b/cmd/helm/search_test.go index ffd4493fe..c9ce62e05 100644 --- a/cmd/helm/search_test.go +++ b/cmd/helm/search_test.go @@ -34,12 +34,12 @@ func TestSearchCmd(t *testing.T) { { name: "search for 'maria', expect one match", args: []string{"maria"}, - expect: "testing/mariadb-0.3.0", + expect: "testing/mariadb", }, { name: "search for 'alpine', expect two matches", args: []string{"alpine"}, - expect: "testing/alpine-0.1.0\ntesting/alpine-0.2.0", + expect: "testing/alpine", }, { name: "search for 'syzygy', expect no matches", @@ -50,7 +50,7 @@ func TestSearchCmd(t *testing.T) { name: "search for 'alp[a-z]+', expect two matches", args: []string{"alp[a-z]+"}, flags: []string{"--regexp"}, - expect: "testing/alpine-0.1.0\ntesting/alpine-0.2.0", + expect: "testing/alpine", regexp: true, }, { diff --git a/cmd/helm/serve.go b/cmd/helm/serve.go index ebb75e223..069938fde 100644 --- a/cmd/helm/serve.go +++ b/cmd/helm/serve.go @@ -17,35 +17,40 @@ limitations under the License. package main import ( + "fmt" "io" "os" "path/filepath" "github.com/spf13/cobra" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/repo" ) const serveDesc = `This command starts a local chart repository server that serves charts from a local directory.` type serveCmd struct { - repoPath string out io.Writer + home helmpath.Home + address string + repoPath string } func newServeCmd(out io.Writer) *cobra.Command { - s := &serveCmd{ - out: out, - } + srv := &serveCmd{out: out} cmd := &cobra.Command{ Use: "serve", Short: "start a local http web server", Long: serveDesc, RunE: func(cmd *cobra.Command, args []string) error { - return s.run() + srv.home = helmpath.Home(homePath()) + return srv.run() }, } - cmd.Flags().StringVar(&s.repoPath, "repo-path", localRepoDirectory(), "The local directory path from which to serve charts.") + cmd.Flags().StringVar(&srv.repoPath, "repo-path", helmpath.Home(homePath()).LocalRepository(), "The local directory path from which to serve charts.") + cmd.Flags().StringVar(&srv.address, "address", "localhost:8879", "The address to listen on.") + return cmd } @@ -58,6 +63,6 @@ func (s *serveCmd) run() error { return err } - repo.StartLocalRepo(s.repoPath) - return nil + fmt.Fprintf(s.out, "Now serving you on %s\n", s.address) + return repo.StartLocalRepo(repoPath, s.address) } diff --git a/cmd/helm/testdata/helmhome/repository/cache/testing-index.yaml b/cmd/helm/testdata/helmhome/repository/cache/testing-index.yaml index 67595882a..26ce97423 100644 --- a/cmd/helm/testdata/helmhome/repository/cache/testing-index.yaml +++ b/cmd/helm/testdata/helmhome/repository/cache/testing-index.yaml @@ -1,54 +1,46 @@ -alpine-0.1.0: - name: alpine - url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz - created: 2016-09-06 21:58:44.211261566 +0000 UTC - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - chartfile: - name: alpine - home: https://k8s.io/helm - sources: - - https://github.com/kubernetes/helm - version: 0.1.0 - description: Deploy a basic Alpine Linux pod - keywords: [] - maintainers: [] - engine: "" - icon: "" -alpine-0.2.0: - name: alpine - url: http://storage.googleapis.com/kubernetes-charts/alpine-0.2.0.tgz - created: 2016-09-06 21:58:44.211261566 +0000 UTC - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - chartfile: - name: alpine - home: https://k8s.io/helm - sources: - - https://github.com/kubernetes/helm - version: 0.2.0 - description: Deploy a basic Alpine Linux pod - keywords: [] - maintainers: [] - engine: "" - icon: "" -mariadb-0.3.0: - name: mariadb - url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz - created: 2016-09-06 21:58:44.211870222 +0000 UTC - checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56 - chartfile: - name: mariadb - home: https://mariadb.org - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - version: 0.3.0 - description: Chart for MariaDB - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - name: Bitnami - email: containers@bitnami.com - engine: gotpl - icon: "" +apiVersion: v1 +entries: + alpine: + - name: alpine + url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + home: https://k8s.io/helm + sources: + - https://github.com/kubernetes/helm + version: 0.1.0 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + engine: "" + icon: "" + - name: alpine + url: http://storage.googleapis.com/kubernetes-charts/alpine-0.2.0.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + home: https://k8s.io/helm + sources: + - https://github.com/kubernetes/helm + version: 0.2.0 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + engine: "" + icon: "" + mariadb: + - name: mariadb + url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz + checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56 + home: https://mariadb.org + sources: + - https://github.com/bitnami/bitnami-docker-mariadb + version: 0.3.0 + description: Chart for MariaDB + keywords: + - mariadb + - mysql + - database + - sql + maintainers: + - name: Bitnami + email: containers@bitnami.com + engine: gotpl + icon: "" diff --git a/cmd/helm/testdata/helmhome/repository/repositories.yaml b/cmd/helm/testdata/helmhome/repository/repositories.yaml index cd16e634a..3835aaa5a 100644 --- a/cmd/helm/testdata/helmhome/repository/repositories.yaml +++ b/cmd/helm/testdata/helmhome/repository/repositories.yaml @@ -1 +1,6 @@ -testing: http://example.com/charts +apiVersion: v1 +generated: 2016-10-03T16:03:10.640376913-06:00 +repositories: +- cache: testing-index.yaml + name: testing + url: http://example.com/charts diff --git a/cmd/helm/testdata/repositories.yaml b/cmd/helm/testdata/repositories.yaml index 6fe931e89..0ff94a0e3 100644 --- a/cmd/helm/testdata/repositories.yaml +++ b/cmd/helm/testdata/repositories.yaml @@ -1,2 +1,6 @@ -charts: http://storage.googleapis.com/kubernetes-charts -local: http://localhost:8879/charts +apiVersion: v1 +repositories: + - name: charts + url: "http://storage.googleapis.com/kubernetes-charts" + - name: local + url: "http://localhost:8879/charts" diff --git a/cmd/helm/testdata/testserver/index.yaml b/cmd/helm/testdata/testserver/index.yaml new file mode 100644 index 000000000..9cde8e8dd --- /dev/null +++ b/cmd/helm/testdata/testserver/index.yaml @@ -0,0 +1 @@ +apiVersion: v1 diff --git a/cmd/helm/testdata/testserver/repository/repositories.yaml b/cmd/helm/testdata/testserver/repository/repositories.yaml new file mode 100644 index 000000000..271301c95 --- /dev/null +++ b/cmd/helm/testdata/testserver/repository/repositories.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +generated: 2016-10-04T13:50:02.87649685-06:00 +repositories: +- cache: "" + name: test + url: http://127.0.0.1:49216 diff --git a/docs/chart_repository.md b/docs/chart_repository.md index 56822e175..900d45d8d 100644 --- a/docs/chart_repository.md +++ b/docs/chart_repository.md @@ -19,26 +19,45 @@ The index file is a yaml file called `index.yaml`. It contains some metadata abo This is an example of an index file: ``` -alpine-0.1.0: - name: alpine - url: https://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz - created: 2016-05-26 11:23:44.086354411 +0000 UTC - digest: sha256:78e9a4282295184e8ce1496d23987993673f38e33e203c8bc18bc838a73e5864 - chartfile: - name: alpine - description: Deploy a basic Alpine Linux pod - version: 0.1.0 - home: https://github.com/example-charts/alpine -redis-2.0.0: - name: redis - url: https://storage.googleapis.com/kubernetes-charts/redis-2.0.0.tgz - created: 2016-05-26 11:23:44.087939192 +0000 UTC - digest: sha256:bde9c2949e64d059c18d8f93566a64dafc6d2e8e259a70322fb804831dfd0b5b - chartfile: - name: redis - description: Port of the replicatedservice template from kubernetes/charts - version: 2.0.0 - home: https://github.com/example-charts/redis +apiVersion: v1 +entries: + nginx: + - urls: + - http://storage.googleapis.com/kubernetes-charts/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: + - http://storage.googleapis.com/kubernetes-charts/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: + - http://storage.googleapis.com/kubernetes-charts/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" ``` We will go through detailed GCS and Github Pages examples here, but feel free to skip to the next section if you've already created a chart repository. diff --git a/pkg/repo/doc.go b/pkg/repo/doc.go new file mode 100644 index 000000000..fb8b3f4b2 --- /dev/null +++ b/pkg/repo/doc.go @@ -0,0 +1,93 @@ +/* +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 repo implements the Helm Chart Repository. + +A chart repository is an HTTP server that provides information on charts. A local +repository cache is an on-disk representation of a chart repository. + +There are two important file formats for chart repositories. + +The first is the 'index.yaml' format, which is expressed like this: + + apiVersion: v1 + entries: + frobnitz: + - created: 2016-09-29T12:14:34.830161306-06:00 + description: This is a frobniz. + digest: 587bd19a9bd9d2bc4a6d25ab91c8c8e7042c47b4ac246e37bf8e1e74386190f4 + home: http://example.com + keywords: + - frobnitz + - sprocket + - dodad + maintainers: + - email: helm@example.com + name: The Helm Team + - email: nobody@example.com + name: Someone Else + name: frobnitz + urls: + - http://example-charts.com/testdata/repository/frobnitz-1.2.3.tgz + version: 1.2.3 + sprocket: + - created: 2016-09-29T12:14:34.830507606-06:00 + description: This is a sprocket" + digest: 8505ff813c39502cc849a38e1e4a8ac24b8e6e1dcea88f4c34ad9b7439685ae6 + home: http://example.com + keywords: + - frobnitz + - sprocket + - dodad + maintainers: + - email: helm@example.com + name: The Helm Team + - email: nobody@example.com + name: Someone Else + name: sprocket + urls: + - http://example-charts.com/testdata/repository/sprocket-1.2.0.tgz + version: 1.2.0 + generated: 2016-09-29T12:14:34.829721375-06:00 + +An index.yaml file contains the necessary descriptive information about what +charts are available in a repository, and how to get them. + +The second file format is the repositories.yaml file format. This file is for +facilitating local cached copies of one or more chart repositories. + +The format of a repository.yaml file is: + + apiVersion: v1 + generated: TIMESTAMP + repositories: + - name: stable + url: http://example.com/charts + cache: stable-index.yaml + - name: incubator + url: http://example.com/incubator + cache: incubator-index.yaml + +This file maps three bits of information about a repository: + + - The name the user uses to refer to it + - The fully qualified URL to the repository (index.yaml will be appended) + - The name of the local cachefile + +The format for both files was changed after Helm v2.0.0-Alpha.4. Helm is not +backwards compatible with those earlier versions. +*/ +package repo diff --git a/pkg/repo/index.go b/pkg/repo/index.go index 457e608b4..42f751955 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -17,12 +17,17 @@ limitations under the License. package repo import ( + "errors" "io/ioutil" "net/http" + "os" "path/filepath" + "sort" "strings" + "time" - "gopkg.in/yaml.v2" + "github.com/Masterminds/semver" + "github.com/ghodss/yaml" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/chart" @@ -31,39 +36,116 @@ import ( var indexPath = "index.yaml" +// APIVersionV1 is the v1 API version for index and repository files. +const APIVersionV1 = "v1" + +// ErrNoAPIVersion indicates that an API version was not specified. +var ErrNoAPIVersion = errors.New("no API version specified") + +// ChartVersions is a list of versioned chart references. +// Implements a sorter on Version. +type ChartVersions []*ChartVersion + +// Len returns the length. +func (c ChartVersions) Len() int { return len(c) } + +// Swap swaps the position of two items in the versions slice. +func (c ChartVersions) Swap(i, j int) { c[i], c[j] = c[j], c[i] } + +// Less returns true if the version of entry a is less than the version of entry b. +func (c ChartVersions) Less(a, b int) bool { + // Failed parse pushes to the back. + i, err := semver.NewVersion(c[a].Version) + if err != nil { + return true + } + j, err := semver.NewVersion(c[b].Version) + if err != nil { + return false + } + return i.LessThan(j) +} + // IndexFile represents the index file in a chart repository type IndexFile struct { - Entries map[string]*ChartRef + APIVersion string `json:"apiVersion"` + Generated time.Time `json:"generated"` + Entries map[string]ChartVersions `json:"entries"` + PublicKeys []string `json:"publicKeys,omitempty"` } // NewIndexFile initializes an index. func NewIndexFile() *IndexFile { - return &IndexFile{Entries: map[string]*ChartRef{}} + return &IndexFile{ + APIVersion: APIVersionV1, + Generated: time.Now(), + Entries: map[string]ChartVersions{}, + PublicKeys: []string{}, + } } // Add adds a file to the index func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { - name := strings.TrimSuffix(filename, ".tgz") - cr := &ChartRef{ - Name: name, - URL: baseURL + "/" + filename, - Chartfile: md, - Digest: digest, - Created: nowString(), - } - i.Entries[name] = cr + cr := &ChartVersion{ + URLs: []string{baseURL + "/" + filename}, + Metadata: md, + Digest: digest, + Created: time.Now(), + } + if ee, ok := i.Entries[md.Name]; !ok { + i.Entries[md.Name] = ChartVersions{cr} + } else { + i.Entries[md.Name] = append(ee, cr) + } +} + +// Has returns true if the index has an entry for a chart with the given name and exact version. +func (i IndexFile) Has(name, version string) bool { + vs, ok := i.Entries[name] + if !ok { + return false + } + for _, ver := range vs { + // TODO: Do we need to normalize the version field with the SemVer lib? + if ver.Version == version { + return true + } + } + return false +} + +// SortEntries sorts the entries by version in descending order. +// +// In canonical form, the individual version records should be sorted so that +// the most recent release for every version is in the 0th slot in the +// Entries.ChartVersions array. That way, tooling can predict the newest +// version without needing to parse SemVers. +func (i IndexFile) SortEntries() { + for _, versions := range i.Entries { + sort.Sort(sort.Reverse(versions)) + } +} + +// WriteFile writes an index file to the given destination path. +// +// The mode on the file is set to 'mode'. +func (i IndexFile) WriteFile(dest string, mode os.FileMode) error { + b, err := yaml.Marshal(i) + if err != nil { + return err + } + return ioutil.WriteFile(dest, b, mode) } // Need both JSON and YAML annotations until we get rid of gopkg.in/yaml.v2 -// ChartRef represents a chart entry in the IndexFile -type ChartRef struct { - Name string `yaml:"name" json:"name"` - URL string `yaml:"url" json:"url"` - Created string `yaml:"created,omitempty" json:"created,omitempty"` - Removed bool `yaml:"removed,omitempty" json:"removed,omitempty"` - Digest string `yaml:"digest,omitempty" json:"digest,omitempty"` - Chartfile *chart.Metadata `yaml:"chartfile" json:"chartfile"` +// ChartVersion represents a chart entry in the IndexFile +type ChartVersion struct { + *chart.Metadata + URLs []string `yaml:"url" json:"urls"` + Created time.Time `yaml:"created,omitempty" json:"created,omitempty"` + Removed bool `yaml:"removed,omitempty" json:"removed,omitempty"` + Digest string `yaml:"digest,omitempty" json:"digest,omitempty"` } // IndexDirectory reads a (flat) directory and generates an index. @@ -104,42 +186,30 @@ func DownloadIndexFile(repoName, url, indexFilePath string) error { } defer resp.Body.Close() - var r IndexFile - b, err := ioutil.ReadAll(resp.Body) if err != nil { return err } - if err := yaml.Unmarshal(b, &r); err != nil { + if _, err := LoadIndex(b); err != nil { return err } return ioutil.WriteFile(indexFilePath, b, 0644) } -// UnmarshalYAML unmarshals the index file -func (i *IndexFile) UnmarshalYAML(unmarshal func(interface{}) error) error { - var refs map[string]*ChartRef - if err := unmarshal(&refs); err != nil { - return err - } - i.Entries = refs - return nil -} - -func (i *IndexFile) addEntry(name string, url string) ([]byte, error) { - if i.Entries == nil { - i.Entries = make(map[string]*ChartRef) +// LoadIndex loads an index file and does minimal validity checking. +// +// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails. +func LoadIndex(data []byte) (*IndexFile, error) { + i := &IndexFile{} + if err := yaml.Unmarshal(data, i); err != nil { + return i, err } - entry := ChartRef{Name: name, URL: url} - i.Entries[name] = &entry - out, err := yaml.Marshal(&i.Entries) - if err != nil { - return nil, err + if i.APIVersion == "" { + return i, ErrNoAPIVersion } - - return out, nil + return i, nil } // LoadIndexFile takes a file at the given path and returns an IndexFile object @@ -148,12 +218,5 @@ func LoadIndexFile(path string) (*IndexFile, error) { if err != nil { return nil, err } - - indexfile := NewIndexFile() - err = yaml.Unmarshal(b, indexfile) - if err != nil { - return nil, err - } - - return indexfile, nil + return LoadIndex(b) } diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go index 3b248fb19..a73cd0352 100644 --- a/pkg/repo/index_test.go +++ b/pkg/repo/index_test.go @@ -17,7 +17,6 @@ limitations under the License. package repo import ( - "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -25,25 +24,74 @@ import ( "path/filepath" "testing" - "gopkg.in/yaml.v2" + "k8s.io/helm/pkg/proto/hapi/chart" ) -const testfile = "testdata/local-index.yaml" - -var ( +const ( + testfile = "testdata/local-index.yaml" testRepo = "test-repo" ) +func TestIndexFile(t *testing.T) { + i := NewIndexFile() + i.Add(&chart.Metadata{Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890") + i.Add(&chart.Metadata{Name: "cutter", Version: "0.1.1"}, "cutter-0.1.1.tgz", "http://example.com/charts", "sha256:1234567890abc") + i.Add(&chart.Metadata{Name: "cutter", Version: "0.1.0"}, "cutter-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890abc") + i.Add(&chart.Metadata{Name: "cutter", Version: "0.2.0"}, "cutter-0.2.0.tgz", "http://example.com/charts", "sha256:1234567890abc") + i.SortEntries() + + if i.APIVersion != APIVersionV1 { + t.Error("Expected API version v1") + } + + if len(i.Entries) != 2 { + t.Errorf("Expected 2 charts. Got %d", len(i.Entries)) + } + + if i.Entries["clipper"][0].Name != "clipper" { + t.Errorf("Expected clipper, got %s", i.Entries["clipper"][0].Name) + } + + if len(i.Entries["cutter"]) != 3 { + t.Error("Expected two cutters.") + } + + // Test that the sort worked. 0.2 should be at the first index for Cutter. + if v := i.Entries["cutter"][0].Version; v != "0.2.0" { + t.Errorf("Unexpected first version: %s", v) + } +} + +func TestLoadIndex(t *testing.T) { + b, err := ioutil.ReadFile(testfile) + if err != nil { + t.Fatal(err) + } + i, err := LoadIndex(b) + if err != nil { + t.Fatal(err) + } + verifyLocalIndex(t, i) +} + +func TestLoadIndexFile(t *testing.T) { + i, err := LoadIndexFile(testfile) + if err != nil { + t.Fatal(err) + } + verifyLocalIndex(t, i) +} + func TestDownloadIndexFile(t *testing.T) { fileBytes, err := ioutil.ReadFile("testdata/local-index.yaml") if err != nil { - t.Errorf("%#v", err) + t.Fatal(err) } - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "binary/octet-stream") - fmt.Fprintln(w, string(fileBytes)) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(fileBytes) })) + defer srv.Close() dirName, err := ioutil.TempDir("", "tmp") if err != nil { @@ -52,7 +100,7 @@ func TestDownloadIndexFile(t *testing.T) { defer os.RemoveAll(dirName) path := filepath.Join(dirName, testRepo+"-index.yaml") - if err := DownloadIndexFile(testRepo, ts.URL, path); err != nil { + if err := DownloadIndexFile(testRepo, srv.URL, path); err != nil { t.Errorf("%#v", err) } @@ -65,50 +113,111 @@ func TestDownloadIndexFile(t *testing.T) { t.Errorf("error reading index file: %#v", err) } - var i IndexFile - if err = yaml.Unmarshal(b, &i); err != nil { - t.Errorf("error unmarshaling index file: %#v", err) + i, err := LoadIndex(b) + if err != nil { + t.Errorf("Index %q failed to parse: %s", testfile, err) + return } + verifyLocalIndex(t, i) +} + +func verifyLocalIndex(t *testing.T, i *IndexFile) { numEntries := len(i.Entries) if numEntries != 2 { - t.Errorf("Expected 2 entries in index file but got %v", numEntries) + t.Errorf("Expected 2 entries in index file but got %d", numEntries) } - os.Remove(path) -} -func TestLoadIndexFile(t *testing.T) { - cf, err := LoadIndexFile(testfile) - if err != nil { - t.Errorf("Failed to load index file: %s", err) - } - if len(cf.Entries) != 2 { - t.Errorf("Expected 2 entries in the index file, but got %d", len(cf.Entries)) - } - nginx := false - alpine := false - for k, e := range cf.Entries { - if k == "nginx-0.1.0" { - if e.Name == "nginx" { - if len(e.Chartfile.Keywords) == 3 { - nginx = true - } + alpine, ok := i.Entries["alpine"] + if !ok { + t.Errorf("'alpine' section not found.") + return + } + + if l := len(alpine); l != 1 { + t.Errorf("'alpine' should have 1 chart, got %d", l) + return + } + + nginx, ok := i.Entries["nginx"] + if !ok || len(nginx) != 2 { + t.Error("Expected 2 nginx entries") + return + } + + expects := []*ChartVersion{ + { + Metadata: &chart.Metadata{ + Name: "alpine", + Description: "string", + Version: "1.0.0", + Keywords: []string{"linux", "alpine", "small", "sumtin"}, + Home: "https://github.com/something", + }, + URLs: []string{ + "http://storage.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz", + "http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz", + }, + Digest: "sha256:1234567890abcdef", + }, + { + Metadata: &chart.Metadata{ + Name: "nginx", + Description: "string", + Version: "0.1.0", + Keywords: []string{"popular", "web server", "proxy"}, + Home: "https://github.com/something", + }, + URLs: []string{ + "http://storage.googleapis.com/kubernetes-charts/nginx-0.1.0.tgz", + }, + Digest: "sha256:1234567890abcdef", + }, + { + Metadata: &chart.Metadata{ + Name: "nginx", + Description: "string", + Version: "0.2.0", + Keywords: []string{"popular", "web server", "proxy"}, + Home: "https://github.com/something/else", + }, + URLs: []string{ + "http://storage.googleapis.com/kubernetes-charts/nginx-0.2.0.tgz", + }, + Digest: "sha256:1234567890abcdef", + }, + } + tests := []*ChartVersion{alpine[0], nginx[0], nginx[1]} + + for i, tt := range tests { + expect := expects[i] + if tt.Name != expect.Name { + t.Errorf("Expected name %q, got %q", expect.Name, tt.Name) + } + if tt.Description != expect.Description { + t.Errorf("Expected description %q, got %q", expect.Description, tt.Description) + } + if tt.Version != expect.Version { + t.Errorf("Expected version %q, got %q", expect.Version, tt.Version) + } + if tt.Digest != expect.Digest { + t.Errorf("Expected digest %q, got %q", expect.Digest, tt.Digest) + } + if tt.Home != expect.Home { + t.Errorf("Expected home %q, got %q", expect.Home, tt.Home) + } + + for i, url := range tt.URLs { + if url != expect.URLs[i] { + t.Errorf("Expected URL %q, got %q", expect.URLs[i], url) } } - if k == "alpine-1.0.0" { - if e.Name == "alpine" { - if len(e.Chartfile.Keywords) == 4 { - alpine = true - } + for i, kw := range tt.Keywords { + if kw != expect.Keywords[i] { + t.Errorf("Expected keywords %q, got %q", expect.Keywords[i], kw) } } } - if !nginx { - t.Errorf("nginx entry was not decoded properly") - } - if !alpine { - t.Errorf("alpine entry was not decoded properly") - } } func TestIndexDirectory(t *testing.T) { @@ -124,21 +233,20 @@ func TestIndexDirectory(t *testing.T) { // Other things test the entry generation more thoroughly. We just test a // few fields. - cname := "frobnitz-1.2.3" - frob, ok := index.Entries[cname] + cname := "frobnitz" + frobs, ok := index.Entries[cname] if !ok { t.Fatalf("Could not read chart %s", cname) } + + frob := frobs[0] if len(frob.Digest) == 0 { t.Errorf("Missing digest of file %s.", frob.Name) } - if frob.Chartfile == nil { - t.Fatalf("Chartfile %s not added to index.", cname) - } - if frob.URL != "http://localhost:8080/frobnitz-1.2.3.tgz" { - t.Errorf("Unexpected URL: %s", frob.URL) + if frob.URLs[0] != "http://localhost:8080/frobnitz-1.2.3.tgz" { + t.Errorf("Unexpected URLs: %v", frob.URLs) } - if frob.Chartfile.Name != "frobnitz" { - t.Errorf("Expected frobnitz, got %q", frob.Chartfile.Name) + if frob.Name != "frobnitz" { + t.Errorf("Expected frobnitz, got %q", frob.Name) } } diff --git a/pkg/repo/local.go b/pkg/repo/local.go index 3ffd72c73..b3105706a 100644 --- a/pkg/repo/local.go +++ b/pkg/repo/local.go @@ -23,21 +23,27 @@ import ( "path/filepath" "strings" + "github.com/ghodss/yaml" + "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/provenance" ) var localRepoPath string // StartLocalRepo starts a web server and serves files from the given path -func StartLocalRepo(path string) { - fmt.Println("Now serving you on localhost:8879...") +func StartLocalRepo(path, address string) error { + if address == "" { + address = ":8879" + } localRepoPath = path http.HandleFunc("/", rootHandler) http.HandleFunc("/charts/", indexHandler) - http.ListenAndServe(":8879", nil) + return http.ListenAndServe(address, nil) } func rootHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") fmt.Fprintf(w, "Welcome to the Kubernetes Package manager!\nBrowse charts on localhost:8879/charts!") } func indexHandler(w http.ResponseWriter, r *http.Request) { @@ -81,9 +87,14 @@ func Reindex(ch *chart.Chart, path string) error { } } if !found { - url := "localhost:8879/charts/" + name + ".tgz" + dig, err := provenance.DigestFile(path) + if err != nil { + return err + } + + y.Add(ch.Metadata, name+".tgz", "http://localhost:8879/charts", "sha256:"+dig) - out, err := y.addEntry(name, url) + out, err := yaml.Marshal(y) if err != nil { return err } diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go index 75f7708d8..baa66a7d9 100644 --- a/pkg/repo/repo.go +++ b/pkg/repo/repo.go @@ -17,22 +17,24 @@ limitations under the License. package repo // import "k8s.io/helm/pkg/repo" import ( - "crypto/sha256" - "encoding/hex" "errors" - "io" + "fmt" "io/ioutil" - "net/url" "os" "path/filepath" "strings" "time" - "gopkg.in/yaml.v2" + "github.com/ghodss/yaml" "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/provenance" ) +// ErrRepoOutOfDate indicates that the repository file is out of date, but +// is fixable. +var ErrRepoOutOfDate = errors.New("repository file is out of date") + // ChartRepository represents a chart repository type ChartRepository struct { RootPath string @@ -41,41 +43,109 @@ type ChartRepository struct { IndexFile *IndexFile } +// Entry represents one repo entry in a repositories listing. +type Entry struct { + Name string `json:"name"` + Cache string `json:"cache"` + URL string `json:"url"` +} + // RepoFile represents the repositories.yaml file in $HELM_HOME type RepoFile struct { - Repositories map[string]string + APIVersion string `json:"apiVersion"` + Generated time.Time `json:"generated"` + Repositories []*Entry `json:"repositories"` +} + +// NewRepoFile generates an empty repositories file. +// +// Generated and APIVersion are automatically set. +func NewRepoFile() *RepoFile { + return &RepoFile{ + APIVersion: APIVersionV1, + Generated: time.Now(), + Repositories: []*Entry{}, + } } // LoadRepositoriesFile takes a file at the given path and returns a RepoFile object +// +// If this returns ErrRepoOutOfDate, it also returns a recovered RepoFile that +// can be saved as a replacement to the out of date file. func LoadRepositoriesFile(path string) (*RepoFile, error) { b, err := ioutil.ReadFile(path) if err != nil { return nil, err } - var r RepoFile - err = yaml.Unmarshal(b, &r) + r := &RepoFile{} + err = yaml.Unmarshal(b, r) if err != nil { return nil, err } - return &r, nil + // File is either corrupt, or is from before v2.0.0-Alpha.5 + if r.APIVersion == "" { + m := map[string]string{} + if err = yaml.Unmarshal(b, &m); err != nil { + return nil, err + } + r := NewRepoFile() + for k, v := range m { + r.Add(&Entry{ + Name: k, + URL: v, + Cache: fmt.Sprintf("%s-index.yaml", k), + }) + } + return r, ErrRepoOutOfDate + } + + return r, nil } -// UnmarshalYAML unmarshals the repo file -func (rf *RepoFile) UnmarshalYAML(unmarshal func(interface{}) error) error { - var repos map[string]string - if err := unmarshal(&repos); err != nil { - if _, ok := err.(*yaml.TypeError); !ok { - return err +// Add adds one or more repo entries to a repo file. +func (r *RepoFile) Add(re ...*Entry) { + r.Repositories = append(r.Repositories, re...) +} + +// Has returns true if the given name is already a repository name. +func (r *RepoFile) Has(name string) bool { + for _, rf := range r.Repositories { + if rf.Name == name { + return true } } - rf.Repositories = repos - return nil + return false +} + +// Remove removes the entry from the list of repositories. +func (r *RepoFile) Remove(name string) bool { + cp := []*Entry{} + found := false + for _, rf := range r.Repositories { + if rf.Name == name { + found = true + continue + } + cp = append(cp, rf) + } + r.Repositories = cp + return found +} + +// WriteFile writes a repositories file to the given path. +func (r *RepoFile) WriteFile(path string, perm os.FileMode) error { + data, err := yaml.Marshal(r) + if err != nil { + return err + } + return ioutil.WriteFile(path, data, perm) } -// LoadChartRepository takes in a path to a local chart repository -// which contains packaged charts and an index.yaml file +// LoadChartRepository loads a directory of charts as if it were a repository. +// +// It requires the presence of an index.yaml file in the directory. // // This function evaluates the contents of the directory and // returns a ChartRepository @@ -86,14 +156,17 @@ func LoadChartRepository(dir, url string) (*ChartRepository, error) { } if !dirInfo.IsDir() { - return nil, errors.New(dir + "is not a directory") + return nil, fmt.Errorf("%q is not a directory", dir) } r := &ChartRepository{RootPath: dir, URL: url} + // FIXME: Why are we recursively walking directories? + // FIXME: Why are we not reading the repositories.yaml to figure out + // what repos to use? filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { if !f.IsDir() { - if strings.Contains(f.Name(), "index.yaml") { + if strings.Contains(f.Name(), "-index.yaml") { i, err := LoadIndexFile(path) if err != nil { return nil @@ -109,82 +182,35 @@ func LoadChartRepository(dir, url string) (*ChartRepository, error) { } func (r *ChartRepository) saveIndexFile() error { - index, err := yaml.Marshal(&r.IndexFile.Entries) + index, err := yaml.Marshal(r.IndexFile) if err != nil { return err } - return ioutil.WriteFile(filepath.Join(r.RootPath, indexPath), index, 0644) } -// Index generates an index for the chart repository and writes an index.yaml file +// Index generates an index for the chart repository and writes an index.yaml file. func (r *ChartRepository) Index() error { if r.IndexFile == nil { - r.IndexFile = &IndexFile{Entries: make(map[string]*ChartRef)} + r.IndexFile = NewIndexFile() } - existCharts := map[string]bool{} - for _, path := range r.ChartPaths { ch, err := chartutil.Load(path) if err != nil { return err } - chartfile := ch.Metadata - digest, err := generateDigest(path) + digest, err := provenance.DigestFile(path) if err != nil { return err } - key := chartfile.Name + "-" + chartfile.Version - if r.IndexFile.Entries == nil { - r.IndexFile.Entries = make(map[string]*ChartRef) - } - - ref, ok := r.IndexFile.Entries[key] - var created string - if ok && ref.Created != "" { - created = ref.Created - } else { - created = nowString() + if !r.IndexFile.Has(ch.Metadata.Name, ch.Metadata.Version) { + r.IndexFile.Add(ch.Metadata, path, r.URL, digest) } - - url, _ := url.Parse(r.URL) - url.Path = filepath.Join(url.Path, key+".tgz") - - entry := &ChartRef{Chartfile: chartfile, Name: chartfile.Name, URL: url.String(), Created: created, Digest: digest, Removed: false} - - r.IndexFile.Entries[key] = entry - - // chart is existing - existCharts[key] = true + // TODO: If a chart exists, but has a different Digest, should we error? } - - // update deleted charts with Removed = true - for k := range r.IndexFile.Entries { - if _, ok := existCharts[k]; !ok { - r.IndexFile.Entries[k].Removed = true - } - } - + r.IndexFile.SortEntries() return r.saveIndexFile() } - -func nowString() string { - // FIXME: This is a different date format than we use elsewhere. - return time.Now().UTC().String() -} - -func generateDigest(path string) (string, error) { - f, err := os.Open(path) - if err != nil { - return "", err - } - - h := sha256.New() - io.Copy(h, f) - - digest := h.Sum([]byte{}) - return "sha256:" + hex.EncodeToString(digest[:]), nil -} diff --git a/pkg/repo/repo_test.go b/pkg/repo/repo_test.go index 651f7fcae..0f4589dba 100644 --- a/pkg/repo/repo_test.go +++ b/pkg/repo/repo_test.go @@ -22,12 +22,111 @@ import ( "reflect" "testing" "time" + + "k8s.io/helm/pkg/proto/hapi/chart" ) const testRepositoriesFile = "testdata/repositories.yaml" const testRepository = "testdata/repository" const testURL = "http://example-charts.com" +func TestRepoFile(t *testing.T) { + rf := NewRepoFile() + rf.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", + }, + ) + + if len(rf.Repositories) != 2 { + t.Fatal("Expected 2 repositories") + } + + if rf.Has("nosuchrepo") { + t.Error("Found nonexistent repo") + } + if !rf.Has("incubator") { + t.Error("incubator repo is missing") + } + + stable := rf.Repositories[0] + if stable.Name != "stable" { + t.Error("stable is not named stable") + } + if stable.URL != "https://example.com/stable/charts" { + t.Error("Wrong URL for stable") + } + if stable.Cache != "stable-index.yaml" { + t.Error("Wrong cache name for stable") + } +} + +func TestLoadRepositoriesFile(t *testing.T) { + expects := NewRepoFile() + expects.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 := LoadRepositoriesFile(testRepositoriesFile) + if err != nil { + t.Errorf("%q could not be loaded: %s", testRepositoriesFile, err) + } + + if len(expects.Repositories) != len(repofile.Repositories) { + t.Fatalf("Unexpected repo data: %#v", repofile.Repositories) + } + + for i, expect := range expects.Repositories { + got := repofile.Repositories[i] + if expect.Name != got.Name { + t.Errorf("Expected name %q, got %q", expect.Name, got.Name) + } + if expect.URL != got.URL { + t.Errorf("Expected url %q, got %q", expect.URL, got.URL) + } + if expect.Cache != got.Cache { + t.Errorf("Expected cache %q, got %q", expect.Cache, got.Cache) + } + } +} + +func TestLoadPreV1RepositoriesFile(t *testing.T) { + r, err := LoadRepositoriesFile("testdata/old-repositories.yaml") + if err != nil && err != ErrRepoOutOfDate { + t.Fatal(err) + } + if len(r.Repositories) != 3 { + t.Fatalf("Expected 3 repos: %#v", r) + } + + // Because they are parsed as a map, we lose ordering. + found := false + for _, rr := range r.Repositories { + if rr.Name == "best-charts-ever" { + found = true + } + } + if !found { + t.Errorf("expected the best charts ever. Got %#v", r.Repositories) + } +} + func TestLoadChartRepository(t *testing.T) { cr, err := LoadChartRepository(testRepository, testURL) if err != nil { @@ -66,76 +165,94 @@ func TestIndex(t *testing.T) { if err != nil { t.Errorf("Error loading index file %v", err) } + verifyIndex(t, actual) - entries := actual.Entries - numEntries := len(entries) - if numEntries != 2 { - t.Errorf("Expected 2 charts to be listed in index file but got %v", numEntries) - } - - timestamps := make(map[string]string) - var empty time.Time - for chartName, details := range entries { - if details == nil { - t.Errorf("Chart Entry is not filled out for %s", chartName) - } - - if details.Created == empty.String() { - t.Errorf("Created timestamp under %s chart entry is nil", chartName) - } - timestamps[chartName] = details.Created - - if details.Digest == "" { - t.Errorf("Digest was not set for %s", chartName) - } - } - - if err = cr.Index(); err != nil { - t.Errorf("Error performing index the second time: %v\n", err) + // Re-index and test again. + err = cr.Index() + if err != nil { + t.Errorf("Error performing re-index: %s\n", err) } second, err := LoadIndexFile(tempIndexPath) if err != nil { - t.Errorf("Error loading index file second time: %#v\n", err) + t.Errorf("Error re-loading index file %v", err) } + verifyIndex(t, second) +} - for chart, created := range timestamps { - v, ok := second.Entries[chart] - if !ok { - t.Errorf("Expected %s chart entry in index file but did not find it", chart) - } - if v.Created != created { - t.Errorf("Expected Created timestamp to be %s, but got %s for chart %s", created, v.Created, chart) - } - // Created manually since we control the input of the test - expectedURL := testURL + "/" + chart + ".tgz" - if v.URL != expectedURL { - t.Errorf("Expected url in entry to be %s but got %s for chart: %s", expectedURL, v.URL, chart) - } +func verifyIndex(t *testing.T, actual *IndexFile) { + + var empty time.Time + if actual.Generated == empty { + t.Errorf("Generated should be greater than 0: %s", actual.Generated) } -} -func TestLoadRepositoriesFile(t *testing.T) { - rf, err := LoadRepositoriesFile(testRepositoriesFile) - if err != nil { - t.Errorf(testRepositoriesFile + " could not be loaded: " + err.Error()) + if actual.APIVersion != APIVersionV1 { + t.Error("Expected v1 API") } - expected := map[string]string{"best-charts-ever": "http://best-charts-ever.com", - "okay-charts": "http://okay-charts.org", "example123": "http://examplecharts.net/charts/123"} - numOfRepositories := len(rf.Repositories) - expectedNumOfRepositories := 3 - if numOfRepositories != expectedNumOfRepositories { - t.Errorf("Expected %v repositories but only got %v", expectedNumOfRepositories, numOfRepositories) + entries := actual.Entries + if numEntries := len(entries); numEntries != 2 { + t.Errorf("Expected 2 charts to be listed in index file but got %v", numEntries) } - for expectedRepo, expectedURL := range expected { - actual, ok := rf.Repositories[expectedRepo] + expects := map[string]ChartVersions{ + "frobnitz": { + { + Metadata: &chart.Metadata{ + Name: "frobnitz", + Version: "1.2.3", + }, + }, + }, + "sprocket": { + { + Metadata: &chart.Metadata{ + Name: "sprocket", + Version: "1.2.0", + }, + }, + }, + } + + for name, versions := range expects { + got, ok := entries[name] if !ok { - t.Errorf("Expected repository: %v but was not found", expectedRepo) + t.Errorf("Could not find %q entry", name) + continue } - - if expectedURL != actual { - t.Errorf("Expected url %s for the %s repository but got %s ", expectedURL, expectedRepo, actual) + if len(versions) != len(got) { + t.Errorf("Expected %d versions, got %d", len(versions), len(got)) + continue + } + for i, e := range versions { + g := got[i] + if e.Name != g.Name { + t.Errorf("Expected %q, got %q", e.Name, g.Name) + } + if e.Version != g.Version { + t.Errorf("Expected %q, got %q", e.Version, g.Version) + } + if len(g.Keywords) != 3 { + t.Error("Expected 3 keyrwords.") + } + if len(g.Maintainers) != 2 { + t.Error("Expected 2 maintainers.") + } + if g.Created == empty { + t.Error("Expected created to be non-empty") + } + if g.Description == "" { + t.Error("Expected description to be non-empty") + } + if g.Home == "" { + t.Error("Expected home to be non-empty") + } + if g.Digest == "" { + t.Error("Expected digest to be non-empty") + } + if len(g.URLs) != 1 { + t.Error("Expected exactly 1 URL") + } } } } diff --git a/pkg/repo/repotest/server.go b/pkg/repo/repotest/server.go index eb737290c..8023dbc5c 100644 --- a/pkg/repo/repotest/server.go +++ b/pkg/repo/repotest/server.go @@ -82,20 +82,27 @@ func (s *Server) CopyCharts(origin string) ([]string, error) { copied[i] = newname } + err = s.CreateIndex() + return copied, err +} + +// CreateIndex will read docroot and generate an index.yaml file. +func (s *Server) CreateIndex() error { // generate the index index, err := repo.IndexDirectory(s.docroot, s.URL()) if err != nil { - return copied, err + return err } - d, err := yaml.Marshal(index.Entries) + d, err := yaml.Marshal(index) if err != nil { - return copied, err + return err } + println(string(d)) + ifile := filepath.Join(s.docroot, "index.yaml") - err = ioutil.WriteFile(ifile, d, 0755) - return copied, err + return ioutil.WriteFile(ifile, d, 0755) } func (s *Server) start() { @@ -119,12 +126,9 @@ func (s *Server) URL() string { // setTestingRepository sets up a testing repository.yaml with only the given name/URL. func setTestingRepository(helmhome, name, url string) error { - // Oddly, there is no repo.Save function for this. - data, err := yaml.Marshal(&map[string]string{name: url}) - if err != nil { - return err - } + rf := repo.NewRepoFile() + rf.Add(&repo.Entry{Name: name, URL: url}) os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755) dest := filepath.Join(helmhome, "repository/repositories.yaml") - return ioutil.WriteFile(dest, data, 0666) + return rf.WriteFile(dest, 0644) } diff --git a/pkg/repo/repotest/server_test.go b/pkg/repo/repotest/server_test.go index 8437ed512..b2a9a5b00 100644 --- a/pkg/repo/repotest/server_test.go +++ b/pkg/repo/repotest/server_test.go @@ -22,7 +22,7 @@ import ( "path/filepath" "testing" - "gopkg.in/yaml.v2" + "github.com/ghodss/yaml" "k8s.io/helm/pkg/repo" ) @@ -77,23 +77,20 @@ func TestServer(t *testing.T) { return } - var m map[string]*repo.ChartRef - if err := yaml.Unmarshal(data, &m); err != nil { + m := repo.NewIndexFile() + if err := yaml.Unmarshal(data, m); err != nil { t.Error(err) return } - if l := len(m); l != 1 { + if l := len(m.Entries); l != 1 { t.Errorf("Expected 1 entry, got %d", l) return } - expect := "examplechart-0.1.0" - if m[expect].Name != "examplechart-0.1.0" { - t.Errorf("Unexpected chart: %s", m[expect].Name) - } - if m[expect].Chartfile.Name != "examplechart" { - t.Errorf("Unexpected chart: %s", m[expect].Chartfile.Name) + expect := "examplechart" + if !m.Has(expect, "0.1.0") { + t.Errorf("missing %q", expect) } res, err = http.Get(srv.URL() + "/index.yaml-nosuchthing") diff --git a/pkg/repo/testdata/local-index.yaml b/pkg/repo/testdata/local-index.yaml index 3db03faa4..ae29dfd8f 100644 --- a/pkg/repo/testdata/local-index.yaml +++ b/pkg/repo/testdata/local-index.yaml @@ -1,19 +1,32 @@ -nginx-0.1.0: - url: http://storage.googleapis.com/kubernetes-charts/nginx-0.1.0.tgz - name: nginx - chartfile: +apiVersion: v1 +entries: + nginx: + - urls: + - http://storage.googleapis.com/kubernetes-charts/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 -alpine-1.0.0: - url: http://storage.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz - name: alpine - chartfile: + - urls: + - http://storage.googleapis.com/kubernetes-charts/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: + - http://storage.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz + - http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz name: alpine description: string version: 1.0.0 @@ -23,4 +36,5 @@ alpine-1.0.0: - alpine - small - sumtin + digest: "sha256:1234567890abcdef" diff --git a/pkg/repo/testdata/old-repositories.yaml b/pkg/repo/testdata/old-repositories.yaml new file mode 100644 index 000000000..3fb55b060 --- /dev/null +++ b/pkg/repo/testdata/old-repositories.yaml @@ -0,0 +1,3 @@ +best-charts-ever: http://best-charts-ever.com +okay-charts: http://okay-charts.org +example123: http://examplecharts.net/charts/123 diff --git a/pkg/repo/testdata/repositories.yaml b/pkg/repo/testdata/repositories.yaml index 3fb55b060..a28c48eab 100644 --- a/pkg/repo/testdata/repositories.yaml +++ b/pkg/repo/testdata/repositories.yaml @@ -1,3 +1,8 @@ -best-charts-ever: http://best-charts-ever.com -okay-charts: http://okay-charts.org -example123: http://examplecharts.net/charts/123 +apiVersion: v1 +repositories: + - name: stable + url: https://example.com/stable/charts + cache: stable-index.yaml + - name: incubator + url: https://example.com/incubator + cache: incubator-index.yaml From fc160256e525dd2ab993581523f81f114acddaec Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 4 Oct 2016 14:57:52 -0600 Subject: [PATCH 133/183] fix(helm): fix tests so that they do not write data into testdata There was a bug in the repo tests that caused them to overwrite the repositories.yaml file in that directory. Now, the entire tests (server and client-side) run inside of a temp directory. --- cmd/helm/repo_add_test.go | 27 ++++++++++++++++++--------- cmd/helm/repo_update_test.go | 10 ++++++---- pkg/repo/repotest/server.go | 24 ++++++++++++++++++++++++ pkg/repo/repotest/server_test.go | 23 +++++++++++++++++++++++ 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/cmd/helm/repo_add_test.go b/cmd/helm/repo_add_test.go index 218353174..055ee0bfd 100644 --- a/cmd/helm/repo_add_test.go +++ b/cmd/helm/repo_add_test.go @@ -29,19 +29,21 @@ import ( var testName = "test-name" func TestRepoAddCmd(t *testing.T) { - srv := repotest.NewServer("testdata/testserver") - defer srv.Stop() - - thome, err := tempHelmHome(t) + srv, thome, err := repotest.NewTempServer("testdata/testserver/*.*") if err != nil { t.Fatal(err) } + oldhome := homePath() helmHome = thome defer func() { + srv.Stop() helmHome = oldhome os.Remove(thome) }() + if err := ensureTestHome(helmpath.Home(thome), t); err != nil { + t.Fatal(err) + } tests := []releaseCase{ { @@ -61,15 +63,22 @@ func TestRepoAddCmd(t *testing.T) { } func TestRepoAdd(t *testing.T) { - ts := repotest.NewServer("testdata/testserver") - defer ts.Stop() - - thome, err := tempHelmHome(t) + ts, thome, err := repotest.NewTempServer("testdata/testserver/*.*") if err != nil { t.Fatal(err) } - defer os.Remove(thome) + + oldhome := homePath() + helmHome = thome hh := helmpath.Home(thome) + defer func() { + ts.Stop() + helmHome = oldhome + os.Remove(thome) + }() + if err := ensureTestHome(hh, t); err != nil { + t.Fatal(err) + } if err := addRepository(testName, ts.URL(), hh); err != nil { t.Error(err) diff --git a/cmd/helm/repo_update_test.go b/cmd/helm/repo_update_test.go index 6ec59c410..857fbbf88 100644 --- a/cmd/helm/repo_update_test.go +++ b/cmd/helm/repo_update_test.go @@ -62,19 +62,21 @@ func TestUpdateCmd(t *testing.T) { } func TestUpdateCharts(t *testing.T) { - srv := repotest.NewServer("testdata/testserver") - defer srv.Stop() - - thome, err := tempHelmHome(t) + srv, thome, err := repotest.NewTempServer("testdata/testserver/*.*") if err != nil { t.Fatal(err) } + oldhome := homePath() helmHome = thome defer func() { + srv.Stop() helmHome = oldhome os.Remove(thome) }() + if err := ensureTestHome(helmpath.Home(thome), t); err != nil { + t.Fatal(err) + } buf := bytes.NewBuffer(nil) repos := []*repo.Entry{ diff --git a/pkg/repo/repotest/server.go b/pkg/repo/repotest/server.go index 8023dbc5c..9223ed0a3 100644 --- a/pkg/repo/repotest/server.go +++ b/pkg/repo/repotest/server.go @@ -27,6 +27,30 @@ import ( "k8s.io/helm/pkg/repo" ) +// NewTempServer creates a server inside of a temp dir. +// +// If the passed in string is not "", it will be treated as a shell glob, and files +// will be copied from that path to the server's docroot. +// +// The caller is responsible for destroying the temp directory as well as stopping +// the server. +func NewTempServer(glob string) (*Server, string, error) { + tdir, err := ioutil.TempDir("", "helm-repotest-") + if err != nil { + return nil, tdir, err + } + srv := NewServer(tdir) + + if glob != "" { + if _, err := srv.CopyCharts(glob); err != nil { + srv.Stop() + return srv, tdir, err + } + } + + return srv, tdir, nil +} + // NewServer creates a repository server for testing. // // docroot should be a temp dir managed by the caller. diff --git a/pkg/repo/repotest/server_test.go b/pkg/repo/repotest/server_test.go index b2a9a5b00..1d4c78e41 100644 --- a/pkg/repo/repotest/server_test.go +++ b/pkg/repo/repotest/server_test.go @@ -102,3 +102,26 @@ func TestServer(t *testing.T) { t.Errorf("Expected 404, got %d", res.StatusCode) } } + +func TestNewTempServer(t *testing.T) { + srv, tdir, err := NewTempServer("testdata/examplechart-0.1.0.tgz") + if err != nil { + t.Fatal(err) + } + defer func() { + srv.Stop() + os.RemoveAll(tdir) + }() + + if _, err := os.Stat(tdir); err != nil { + t.Fatal(err) + } + + res, err := http.Head(srv.URL() + "/examplechart-0.1.0.tgz") + if err != nil { + t.Error(err) + } + if res.StatusCode != 200 { + t.Errorf("Expected 200, got %d", res.StatusCode) + } +} From 41be5f598550152d7016310043a51acdcfa04912 Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Fri, 30 Sep 2016 09:40:17 -0600 Subject: [PATCH 134/183] chore(helm): add test coverage to pkg/helm --- pkg/helm/client.go | 244 ++++++++++++++++++++++++++------ pkg/helm/helm_test.go | 315 ++++++++++++++++++++++++++++++++++++++++++ pkg/helm/option.go | 117 ++-------------- 3 files changed, 529 insertions(+), 147 deletions(-) create mode 100644 pkg/helm/helm_test.go diff --git a/pkg/helm/client.go b/pkg/helm/client.go index b7522fd03..1a4160174 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -17,6 +17,7 @@ limitations under the License. package helm // import "k8s.io/helm/pkg/helm" import ( + "golang.org/x/net/context" "google.golang.org/grpc" "k8s.io/helm/pkg/chartutil" @@ -44,7 +45,8 @@ type Client struct { // NewClient creates a new client. func NewClient(opts ...Option) *Client { - return new(Client).Init().Option(opts...) + var c Client + return c.Option(opts...) } // Option configures the helm client with the provided options @@ -55,117 +57,271 @@ func (h *Client) Option(opts ...Option) *Client { return h } -// Init initializes the helm client with default options -func (h *Client) Init() *Client { - return h -} - // ListReleases lists the current releases. func (h *Client) ListReleases(opts ...ReleaseListOption) (*rls.ListReleasesResponse, error) { - c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) + for _, opt := range opts { + opt(&h.opts) + } + req := &h.opts.listReq + ctx := NewContext() + + if h.opts.before != nil { + if err := h.opts.before(ctx, req); err != nil { + return nil, err + } + } + return h.list(ctx, req) +} + +// InstallRelease installs a new chart and returns the release response. +func (h *Client) InstallRelease(chstr, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) { + // load the chart to install + chart, err := chartutil.Load(chstr) if err != nil { return nil, err } - defer c.Close() - return h.opts.rpcListReleases(rls.NewReleaseServiceClient(c), opts...) + // apply the install options + for _, opt := range opts { + opt(&h.opts) + } + req := &h.opts.instReq + req.Chart = chart + req.Namespace = ns + req.DryRun = h.opts.dryRun + req.DisableHooks = h.opts.disableHooks + req.ReuseName = h.opts.reuseName + ctx := NewContext() + + if h.opts.before != nil { + if err := h.opts.before(ctx, req); err != nil { + return nil, err + } + } + return h.install(ctx, req) } -// InstallRelease installs a new chart and returns the release response. -func (h *Client) InstallRelease(chStr, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) { +// DeleteRelease uninstalls a named release and returns the response. +func (h *Client) DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) { + if h.opts.dryRun { + // In the dry run case, just see if the release exists + r, err := h.ReleaseContent(rlsName, nil) + if err != nil { + return &rls.UninstallReleaseResponse{}, err + } + return &rls.UninstallReleaseResponse{Release: r.Release}, nil + } + + // apply the uninstall options + for _, opt := range opts { + opt(&h.opts) + } + + req := &h.opts.uninstallReq + req.Name = rlsName + req.DisableHooks = h.opts.disableHooks + ctx := NewContext() + + if h.opts.before != nil { + if err := h.opts.before(ctx, req); err != nil { + return nil, err + } + } + return h.delete(ctx, req) +} + +// UpdateRelease updates a release to a new/different chart +func (h *Client) UpdateRelease(rlsName string, chstr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { + // load the chart to update + chart, err := chartutil.Load(chstr) + if err != nil { + return nil, err + } + + // apply the update options + for _, opt := range opts { + opt(&h.opts) + } + req := &h.opts.updateReq + req.Chart = chart + req.DryRun = h.opts.dryRun + req.Name = rlsName + ctx := NewContext() + + if h.opts.before != nil { + if err := h.opts.before(ctx, req); err != nil { + return nil, err + } + } + return h.update(ctx, req) +} + +// GetVersion returns the server version +func (h *Client) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) { + for _, opt := range opts { + opt(&h.opts) + } + req := &rls.GetVersionRequest{} + ctx := NewContext() + + if h.opts.before != nil { + if err := h.opts.before(ctx, req); err != nil { + return nil, err + } + } + return h.version(ctx, req) +} + +// RollbackRelease rolls back a release to the previous version +func (h *Client) RollbackRelease(rlsName string, opts ...RollbackOption) (*rls.RollbackReleaseResponse, error) { + for _, opt := range opts { + opt(&h.opts) + } + req := &h.opts.rollbackReq + req.DisableHooks = h.opts.disableHooks + req.DryRun = h.opts.dryRun + req.Name = rlsName + ctx := NewContext() + + if h.opts.before != nil { + if err := h.opts.before(ctx, req); err != nil { + return nil, err + } + } + return h.rollback(ctx, req) +} + +// ReleaseStatus returns the given release's status. +func (h *Client) ReleaseStatus(rlsName string, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) { + for _, opt := range opts { + opt(&h.opts) + } + req := &h.opts.statusReq + req.Name = rlsName + ctx := NewContext() + + if h.opts.before != nil { + if err := h.opts.before(ctx, req); err != nil { + return nil, err + } + } + return h.status(ctx, req) +} + +// ReleaseContent returns the configuration for a given release. +func (h *Client) ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) { + for _, opt := range opts { + opt(&h.opts) + } + req := &h.opts.contentReq + req.Name = rlsName + ctx := NewContext() + + if h.opts.before != nil { + if err := h.opts.before(ctx, req); err != nil { + return nil, err + } + } + return h.content(ctx, req) +} + +// Executes tiller.ListReleases RPC. +func (h *Client) list(ctx context.Context, req *rls.ListReleasesRequest) (*rls.ListReleasesResponse, error) { c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) if err != nil { return nil, err } defer c.Close() - chart, err := chartutil.Load(chStr) + rlc := rls.NewReleaseServiceClient(c) + s, err := rlc.ListReleases(ctx, req) if err != nil { return nil, err } - return h.opts.rpcInstallRelease(chart, rls.NewReleaseServiceClient(c), ns, opts...) + return s.Recv() } -// DeleteRelease uninstalls a named release and returns the response. -// -// Note: there aren't currently any supported DeleteOptions, but they are -// kept in the API signature as a placeholder for future additions. -func (h *Client) DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) { +// Executes tiller.InstallRelease RPC. +func (h *Client) install(ctx context.Context, req *rls.InstallReleaseRequest) (*rls.InstallReleaseResponse, error) { c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) if err != nil { return nil, err } defer c.Close() - return h.opts.rpcDeleteRelease(rlsName, rls.NewReleaseServiceClient(c), opts...) + rlc := rls.NewReleaseServiceClient(c) + return rlc.InstallRelease(ctx, req) } -// UpdateRelease updates a release to a new/different chart -func (h *Client) UpdateRelease(rlsName string, chStr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { +// Executes tiller.UninstallRelease RPC. +func (h *Client) delete(ctx context.Context, req *rls.UninstallReleaseRequest) (*rls.UninstallReleaseResponse, error) { c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) if err != nil { return nil, err } defer c.Close() - chart, err := chartutil.Load(chStr) + rlc := rls.NewReleaseServiceClient(c) + return rlc.UninstallRelease(ctx, req) +} + +// Executes tiller.UpdateRelease RPC. +func (h *Client) update(ctx context.Context, req *rls.UpdateReleaseRequest) (*rls.UpdateReleaseResponse, error) { + c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) if err != nil { return nil, err } + defer c.Close() - return h.opts.rpcUpdateRelease(rlsName, chart, rls.NewReleaseServiceClient(c), opts...) + rlc := rls.NewReleaseServiceClient(c) + return rlc.UpdateRelease(ctx, req) } -// GetVersion returns the server version -// -// Note: there aren't currently any supported StatusOptions, -// but they are kept in the API signature as a placeholder for future additions. -func (h *Client) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) { +// Executes tiller.RollbackRelease RPC. +func (h *Client) rollback(ctx context.Context, req *rls.RollbackReleaseRequest) (*rls.RollbackReleaseResponse, error) { c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) if err != nil { return nil, err } defer c.Close() - return h.opts.rpcGetVersion(rls.NewReleaseServiceClient(c), opts...) + rlc := rls.NewReleaseServiceClient(c) + return rlc.RollbackRelease(ctx, req) } -// RollbackRelease rolls back a release to the previous version -func (h *Client) RollbackRelease(rlsName string, opts ...RollbackOption) (*rls.RollbackReleaseResponse, error) { +// Executes tiller.GetReleaseStatus RPC. +func (h *Client) status(ctx context.Context, req *rls.GetReleaseStatusRequest) (*rls.GetReleaseStatusResponse, error) { c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) if err != nil { return nil, err } defer c.Close() - return h.opts.rpcRollbackRelease(rlsName, rls.NewReleaseServiceClient(c), opts...) + rlc := rls.NewReleaseServiceClient(c) + return rlc.GetReleaseStatus(ctx, req) } -// ReleaseStatus returns the given release's status. -// -// Note: there aren't currently any supported StatusOptions, -// but they are kept in the API signature as a placeholder for future additions. -func (h *Client) ReleaseStatus(rlsName string, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) { +// Executes tiller.GetReleaseContent RPC. +func (h *Client) content(ctx context.Context, req *rls.GetReleaseContentRequest) (*rls.GetReleaseContentResponse, error) { c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) if err != nil { return nil, err } defer c.Close() - return h.opts.rpcGetReleaseStatus(rlsName, rls.NewReleaseServiceClient(c), opts...) + rlc := rls.NewReleaseServiceClient(c) + return rlc.GetReleaseContent(ctx, req) } -// ReleaseContent returns the configuration for a given release. -// -// Note: there aren't currently any supported ContentOptions, but -// they are kept in the API signature as a placeholder for future additions. -func (h *Client) ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) { +// Executes tiller.GetVersion RPC. +func (h *Client) version(ctx context.Context, req *rls.GetVersionRequest) (*rls.GetVersionResponse, error) { c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) if err != nil { return nil, err } defer c.Close() - return h.opts.rpcGetReleaseContent(rlsName, rls.NewReleaseServiceClient(c), opts...) + rlc := rls.NewReleaseServiceClient(c) + return rlc.GetVersion(ctx, req) } diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go new file mode 100644 index 000000000..a6289f2c8 --- /dev/null +++ b/pkg/helm/helm_test.go @@ -0,0 +1,315 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helm // import "k8s.io/helm/pkg/helm" + +import ( + "errors" + "path/filepath" + "reflect" + "testing" + + "github.com/golang/protobuf/proto" + "golang.org/x/net/context" + + "k8s.io/helm/pkg/chartutil" + cpb "k8s.io/helm/pkg/proto/hapi/chart" + rls "k8s.io/helm/pkg/proto/hapi/release" + tpb "k8s.io/helm/pkg/proto/hapi/services" +) + +// path to example charts relative to pkg/helm. +const chartsDir = "../../docs/examples/" + +// sentinel error to indicate to the helm client to not send the request to tiller. +var errSkip = errors.New("test: skip") + +// Verify ReleaseListOption's are applied to a ListReleasesRequest correctly. +func TestListReleases_VerifyOptions(t *testing.T) { + // Options testdata + var limit = 2 + var offset = "offset" + var filter = "filter" + var sortBy = int32(2) + var sortOrd = int32(1) + var codes = []rls.Status_Code{ + rls.Status_FAILED, + rls.Status_DELETED, + rls.Status_DEPLOYED, + rls.Status_SUPERSEDED, + } + + // Expected ListReleasesRequest message + exp := &tpb.ListReleasesRequest{ + Limit: int64(limit), + Offset: offset, + Filter: filter, + SortBy: tpb.ListSort_SortBy(sortBy), + SortOrder: tpb.ListSort_SortOrder(sortOrd), + StatusCodes: codes, + } + + // Options used in ListReleases + ops := []ReleaseListOption{ + ReleaseListSort(sortBy), + ReleaseListOrder(sortOrd), + ReleaseListLimit(limit), + ReleaseListOffset(offset), + ReleaseListFilter(filter), + ReleaseListStatuses(codes), + } + + // BeforeCall option to intercept helm client ListReleasesRequest + b4c := BeforeCall(func(_ context.Context, msg proto.Message) error { + switch act := msg.(type) { + case *tpb.ListReleasesRequest: + t.Logf("ListReleasesRequest: %#+v\n", act) + assert(t, exp, act) + default: + t.Fatalf("expected message of type ListReleasesRequest, got %T\n", act) + } + return errSkip + }) + + NewClient(b4c).ListReleases(ops...) +} + +// Verify InstallOption's are applied to an InstallReleaseRequest correctly. +func TestInstallRelease_VerifyOptions(t *testing.T) { + // Options testdata + var disableHooks = true + var releaseName = "test" + var namespace = "default" + var reuseName = true + var dryRun = true + var chartName = "alpine" + var overrides = []byte("key1=value1,key2=value2") + + // Expected InstallReleaseRequest message + exp := &tpb.InstallReleaseRequest{ + Chart: loadChart(t, chartName), + Values: &cpb.Config{Raw: string(overrides)}, + DryRun: dryRun, + Name: releaseName, + DisableHooks: disableHooks, + Namespace: namespace, + ReuseName: reuseName, + } + + // Options used in InstallRelease + ops := []InstallOption{ + ValueOverrides(overrides), + InstallDryRun(dryRun), + ReleaseName(releaseName), + InstallReuseName(reuseName), + InstallDisableHooks(disableHooks), + } + + // BeforeCall option to intercept helm client InstallReleaseRequest + b4c := BeforeCall(func(_ context.Context, msg proto.Message) error { + switch act := msg.(type) { + case *tpb.InstallReleaseRequest: + t.Logf("InstallReleaseRequest: %#+v\n", act) + assert(t, exp, act) + default: + t.Fatalf("expected message of type InstallReleaseRequest, got %T\n", act) + } + return errSkip + }) + + NewClient(b4c).InstallRelease(chartName, namespace, ops...) +} + +// Verify DeleteOptions's are applied to an UninstallReleaseRequest correctly. +func TestDeleteRelease_VerifyOptions(t *testing.T) { + // Options testdata + var releaseName = "test" + var disableHooks = true + var purgeFlag = true + + // Expected DeleteReleaseRequest message + exp := &tpb.UninstallReleaseRequest{ + Name: releaseName, + Purge: purgeFlag, + DisableHooks: disableHooks, + } + + // Options used in DeleteRelease + ops := []DeleteOption{ + DeletePurge(purgeFlag), + DeleteDisableHooks(disableHooks), + } + + // BeforeCall option to intercept helm client DeleteReleaseRequest + b4c := BeforeCall(func(_ context.Context, msg proto.Message) error { + switch act := msg.(type) { + case *tpb.UninstallReleaseRequest: + t.Logf("UninstallReleaseRequest: %#+v\n", act) + assert(t, exp, act) + default: + t.Fatalf("expected message of type UninstallReleaseRequest, got %T\n", act) + } + return errSkip + }) + + NewClient(b4c).DeleteRelease(releaseName, ops...) +} + +// Verify UpdateOption's are applied to an UpdateReleaseRequest correctly. +func TestUpdateRelease_VerifyOptions(t *testing.T) { + // Options testdata + var chartName = "alpine" + var releaseName = "test" + var disableHooks = true + var overrides = []byte("key1=value1,key2=value2") + var dryRun = false + + // Expected UpdateReleaseRequest message + exp := &tpb.UpdateReleaseRequest{ + Name: releaseName, + Chart: loadChart(t, chartName), + Values: &cpb.Config{Raw: string(overrides)}, + DryRun: dryRun, + DisableHooks: disableHooks, + } + + // Options used in UpdateRelease + ops := []UpdateOption{ + UpgradeDryRun(dryRun), + UpdateValueOverrides(overrides), + UpgradeDisableHooks(disableHooks), + } + + // BeforeCall option to intercept helm client UpdateReleaseRequest + b4c := BeforeCall(func(_ context.Context, msg proto.Message) error { + switch act := msg.(type) { + case *tpb.UpdateReleaseRequest: + t.Logf("UpdateReleaseRequest: %#+v\n", act) + assert(t, exp, act) + default: + t.Fatalf("expected message of type UpdateReleaseRequest, got %T\n", act) + } + return errSkip + }) + + NewClient(b4c).UpdateRelease(releaseName, chartName, ops...) +} + +// Verify RollbackOption's are applied to a RollbackReleaseRequest correctly. +func TestRollbackRelease_VerifyOptions(t *testing.T) { + // Options testdata + var disableHooks = true + var releaseName = "test" + var revision = int32(2) + var dryRun = true + + // Expected RollbackReleaseRequest message + exp := &tpb.RollbackReleaseRequest{ + Name: releaseName, + DryRun: dryRun, + Version: revision, + DisableHooks: disableHooks, + } + + // Options used in RollbackRelease + ops := []RollbackOption{ + RollbackDryRun(dryRun), + RollbackVersion(revision), + RollbackDisableHooks(disableHooks), + } + + // BeforeCall option to intercept helm client RollbackReleaseRequest + b4c := BeforeCall(func(_ context.Context, msg proto.Message) error { + switch act := msg.(type) { + case *tpb.RollbackReleaseRequest: + t.Logf("RollbackReleaseRequest: %#+v\n", act) + assert(t, exp, act) + default: + t.Fatalf("expected message of type RollbackReleaseRequest, got %T\n", act) + } + return errSkip + }) + + NewClient(b4c).RollbackRelease(releaseName, ops...) +} + +// Verify StatusOption's are applied to a GetReleaseStatusRequest correctly. +func TestReleaseStatus_VerifyOptions(t *testing.T) { + // Options testdata + var releaseName = "test" + var revision = int32(2) + + // Expected GetReleaseStatusRequest message + exp := &tpb.GetReleaseStatusRequest{ + Name: releaseName, + Version: revision, + } + + // BeforeCall option to intercept helm client GetReleaseStatusRequest + b4c := BeforeCall(func(_ context.Context, msg proto.Message) error { + switch act := msg.(type) { + case *tpb.GetReleaseStatusRequest: + t.Logf("GetReleaseStatusRequest: %#+v\n", act) + assert(t, exp, act) + default: + t.Fatalf("expected message of type GetReleaseStatusRequest, got %T\n", act) + } + return errSkip + }) + + NewClient(b4c).ReleaseStatus(releaseName, StatusReleaseVersion(revision)) +} + +// Verify ContentOption's are applied to a GetReleaseContentRequest correctly. +func TestReleaseContent_VerifyOptions(t *testing.T) { + // Options testdata + var releaseName = "test" + var revision = int32(2) + + // Expected GetReleaseContentRequest message + exp := &tpb.GetReleaseContentRequest{ + Name: releaseName, + Version: revision, + } + + // BeforeCall option to intercept helm client GetReleaseContentRequest + b4c := BeforeCall(func(_ context.Context, msg proto.Message) error { + switch act := msg.(type) { + case *tpb.GetReleaseContentRequest: + t.Logf("GetReleaseContentRequest: %#+v\n", act) + assert(t, exp, act) + default: + t.Fatalf("expected message of type GetReleaseContentRequest, got %T\n", act) + } + return errSkip + }) + + NewClient(b4c).ReleaseContent(releaseName, ContentReleaseVersion(revision)) +} + +func assert(t *testing.T, expect, actual interface{}) { + if !reflect.DeepEqual(expect, actual) { + t.Fatalf("expected %#+v, actual %#+v\n", expect, actual) + } +} + +func loadChart(t *testing.T, name string) *cpb.Chart { + c, err := chartutil.Load(filepath.Join(chartsDir, name)) + if err != nil { + t.Fatalf("failed to load test chart (%q): %s\n", name, err) + } + return c +} diff --git a/pkg/helm/option.go b/pkg/helm/option.go index de90474e1..08dc6dc73 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -17,6 +17,7 @@ limitations under the License. package helm import ( + "github.com/golang/protobuf/proto" "golang.org/x/net/context" "google.golang.org/grpc/metadata" @@ -41,6 +42,8 @@ type options struct { reuseName bool // if set, skip running hooks disableHooks bool + // name of release + releaseName string // release list options are applied directly to the list releases request listReq rls.ListReleasesRequest // release install options are applied directly to the install release request @@ -55,6 +58,8 @@ type options struct { contentReq rls.GetReleaseContentRequest // release rollback options are applied directly to the rollback release request rollbackReq rls.RollbackReleaseRequest + // before intercepts client calls before sending + before func(context.Context, proto.Message) error } // Host specifies the host address of the Tiller release server, (default = ":44134"). @@ -64,6 +69,15 @@ func Host(host string) Option { } } +// BeforeCall returns an option that allows intercepting a helm client rpc +// before being sent OTA to tiller. The intercepting function should return +// an error to indicate that the call should not proceed or nil otherwise. +func BeforeCall(fn func(context.Context, proto.Message) error) Option { + return func(opts *options) { + opts.before = fn + } +} + // ReleaseListOption allows specifying various settings // configurable by the helm client user for overriding // the defaults used when running the `helm list` command. @@ -258,111 +272,8 @@ type UpdateOption func(*options) // running the `helm rollback` command. type RollbackOption func(*options) -// RPC helpers defined on `options` type. Note: These actually execute the -// the corresponding tiller RPC. There is no particular reason why these -// are APIs are hung off `options`, they are internal to pkg/helm to remain -// malleable. - -// Executes tiller.ListReleases RPC. -func (o *options) rpcListReleases(rlc rls.ReleaseServiceClient, opts ...ReleaseListOption) (*rls.ListReleasesResponse, error) { - // apply release list options - for _, opt := range opts { - opt(o) - } - s, err := rlc.ListReleases(NewContext(), &o.listReq) - if err != nil { - return nil, err - } - - return s.Recv() -} - // NewContext creates a versioned context. func NewContext() context.Context { md := metadata.Pairs("x-helm-api-client", version.Version) return metadata.NewContext(context.TODO(), md) } - -// Executes tiller.InstallRelease RPC. -func (o *options) rpcInstallRelease(chr *cpb.Chart, rlc rls.ReleaseServiceClient, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) { - // apply the install options - for _, opt := range opts { - opt(o) - } - o.instReq.Chart = chr - o.instReq.Namespace = ns - o.instReq.DryRun = o.dryRun - o.instReq.DisableHooks = o.disableHooks - o.instReq.ReuseName = o.reuseName - - return rlc.InstallRelease(NewContext(), &o.instReq) -} - -// Executes tiller.UninstallRelease RPC. -func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) { - for _, opt := range opts { - opt(o) - } - if o.dryRun { - // In the dry run case, just see if the release exists - r, err := o.rpcGetReleaseContent(rlsName, rlc) - if err != nil { - return &rls.UninstallReleaseResponse{}, err - } - return &rls.UninstallReleaseResponse{Release: r.Release}, nil - } - - o.uninstallReq.Name = rlsName - o.uninstallReq.DisableHooks = o.disableHooks - - return rlc.UninstallRelease(NewContext(), &o.uninstallReq) -} - -// Executes tiller.UpdateRelease RPC. -func (o *options) rpcUpdateRelease(rlsName string, chr *cpb.Chart, rlc rls.ReleaseServiceClient, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { - for _, opt := range opts { - opt(o) - } - - o.updateReq.Chart = chr - o.updateReq.DryRun = o.dryRun - o.updateReq.Name = rlsName - - return rlc.UpdateRelease(NewContext(), &o.updateReq) -} - -// Executes tiller.UpdateRelease RPC. -func (o *options) rpcRollbackRelease(rlsName string, rlc rls.ReleaseServiceClient, opts ...RollbackOption) (*rls.RollbackReleaseResponse, error) { - for _, opt := range opts { - opt(o) - } - - o.rollbackReq.DryRun = o.dryRun - o.rollbackReq.Name = rlsName - - return rlc.RollbackRelease(NewContext(), &o.rollbackReq) -} - -// Executes tiller.GetReleaseStatus RPC. -func (o *options) rpcGetReleaseStatus(rlsName string, rlc rls.ReleaseServiceClient, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) { - for _, opt := range opts { - opt(o) - } - o.statusReq.Name = rlsName - return rlc.GetReleaseStatus(NewContext(), &o.statusReq) -} - -// Executes tiller.GetReleaseContent. -func (o *options) rpcGetReleaseContent(rlsName string, rlc rls.ReleaseServiceClient, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) { - for _, opt := range opts { - opt(o) - } - o.contentReq.Name = rlsName - return rlc.GetReleaseContent(NewContext(), &o.contentReq) -} - -// Executes tiller.GetVersion RPC. -func (o *options) rpcGetVersion(rlc rls.ReleaseServiceClient, opts ...VersionOption) (*rls.GetVersionResponse, error) { - req := &rls.GetVersionRequest{} - return rlc.GetVersion(NewContext(), req) -} From 6691b79f306b96a4a949865c75f3944404db9827 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 4 Oct 2016 15:48:08 -0600 Subject: [PATCH 135/183] docs(install): Add information on using canary releases This includes documentation on how to use Helm and Tiller canary releases. --- README.md | 3 +++ docs/install.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/README.md b/README.md index c03676282..6c624d098 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ Think of it like apt/yum/homebrew for Kubernetes. Download a [release tarball of helm for your platform](https://github.com/kubernetes/helm/releases). Unpack the `helm` binary and add it to your PATH and you are good to go! OS X/[Cask](https://caskroom.github.io/) users can `brew cask install helm`. +See the [installation guide](docs/install.md) for more options, +including installing pre-releases. + ## Docs - [Quick Start](docs/quickstart.md) diff --git a/docs/install.md b/docs/install.md index 1a4578a9c..43b67c1fc 100644 --- a/docs/install.md +++ b/docs/install.md @@ -34,6 +34,20 @@ brew cask install helm (Note: There is also a formula for emacs-helm, which is a different project.) +### From Canary Builds + +"Canary" builds are versions of the Helm software that are built from +the latest master branch. They are not official releases, and may not be +stable. However, they offer the opportunity to test the cutting edge +features. + +Canary Helm binaries are stored in the [Kubernetes Helm GCS bucket](http://storage.googleapis.com/kubernetes-helm). +Here are links to the common builds: + +- [Linux AMD64](http://storage.googleapis.com/kubernetes-helm/helm-canary-linux-amd64.tar.gz) +- [OSX AMD64](http://storage.googleapis.com/kubernetes-helm/helm-canary-darwin-amd64.tar.gz) +- [Experimental Windows AMD64](http://storage.googleapis.com/kubernetes-helm/helm-canary-windows-amd64.zip) + ### From Source (Linux, Mac OSX) Building Helm from source is slightly more work, but is the best way to @@ -79,6 +93,22 @@ the client and server version. (If it shows only the client version, `helm` cannot yet connect to the server. Use `kubectl` to see if any `tiller` pods are running.) +### Installing Tiller Canary Builds + +Canary images are built from the `master` branch. They may not be +stable, but they offer you the chance to test out the latest features. + +The easiest way to install a canary image is to use `helm init` with the +`--tiller-image` flag: + +```console +$ helm init -i "gcr.io/kubernetes-helm/tiller:canary" +``` + +This will use the most recently built container image. You can always +uninstall Tiller by deleting the Tiller deployment from the +`kube-system` namespace using `kubectl`. + ### Running Tiller Locally For development, it is sometimes easier to work on Tiller locally, and From 51981d62f2eaf56260e5835610b13f77b4a56dd3 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 4 Oct 2016 17:27:54 -0600 Subject: [PATCH 136/183] docs(using_helm): change reference from npm to CPAN/Fedora DB Closes #1271 --- docs/using_helm.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/using_helm.md b/docs/using_helm.md index eed798b38..155da24e5 100644 --- a/docs/using_helm.md +++ b/docs/using_helm.md @@ -17,7 +17,9 @@ cluster. Think of it like a Homebrew formula or an `apt` or `rpm` package for Kubernetes. A *Repository* is the place where charts can be collected and shared. -It's like `npm` for Kubernetes packages. +It's like Perl's [CPAN archive](http://www.cpan.org) or the +[Fedora Package Database](https://admin.fedoraproject.org/pkgdb/), but for +Kubernetes packages. A *Release* is an instance of a chart running in a Kubernetes cluster. One chart can often be installed many times into the same cluster. And From 79e5fd6b74abface96e3df2feeb17b713cfab6e0 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 4 Oct 2016 21:19:52 -0600 Subject: [PATCH 137/183] fix(helm): fix 'helm search' to use UITable Closes #1261 --- cmd/helm/search.go | 29 ++++++++++++++++++++++++----- cmd/helm/search/search.go | 5 +++-- cmd/helm/search_test.go | 8 ++++---- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/cmd/helm/search.go b/cmd/helm/search.go index f8b02cff8..264870810 100644 --- a/cmd/helm/search.go +++ b/cmd/helm/search.go @@ -21,6 +21,7 @@ import ( "io" "strings" + "github.com/gosuri/uitable" "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/helmpath" @@ -71,6 +72,7 @@ func (s *searchCmd) run(args []string) error { if len(args) == 0 { s.showAllCharts(index) + return nil } q := strings.Join(args, " ") @@ -80,17 +82,34 @@ func (s *searchCmd) run(args []string) error { } search.SortScore(res) - for _, r := range res { - fmt.Fprintln(s.out, r.Name) - } + fmt.Fprintln(s.out, s.formatSearchResults(res)) return nil } func (s *searchCmd) showAllCharts(i *search.Index) { - for name := range i.Entries() { - fmt.Fprintln(s.out, name) + e := i.Entries() + res := make([]*search.Result, len(e)) + j := 0 + for name, ch := range e { + res[j] = &search.Result{ + Name: name, + Chart: ch, + } + j++ + } + search.SortScore(res) + fmt.Fprintln(s.out, s.formatSearchResults(res)) +} + +func (s *searchCmd) formatSearchResults(res []*search.Result) string { + table := uitable.New() + table.MaxColWidth = 50 + table.AddRow("NAME", "VERSION", "DESCRIPTION") + for _, r := range res { + table.AddRow(r.Name, r.Chart.Version, r.Chart.Description) } + return table.String() } func (s *searchCmd) buildIndex() (*search.Index, error) { diff --git a/cmd/helm/search/search.go b/cmd/helm/search/search.go index def6da698..aa17fd7ee 100644 --- a/cmd/helm/search/search.go +++ b/cmd/helm/search/search.go @@ -39,6 +39,7 @@ import ( type Result struct { Name string Score int + Chart *repo.ChartVersion } // Index is a searchable index of chart information. @@ -117,7 +118,7 @@ func (i *Index) SearchLiteral(term string, threshold int) []*Result { for k, v := range i.lines { res := strings.Index(v, term) if score := i.calcScore(res, v); res != -1 && score < threshold { - buf = append(buf, &Result{Name: k, Score: score}) + buf = append(buf, &Result{Name: k, Score: score, Chart: i.charts[k]}) } } return buf @@ -136,7 +137,7 @@ func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) { continue } if score := i.calcScore(ind[0], v); ind[0] >= 0 && score < threshold { - buf = append(buf, &Result{Name: k, Score: score}) + buf = append(buf, &Result{Name: k, Score: score, Chart: i.charts[k]}) } } return buf, nil diff --git a/cmd/helm/search_test.go b/cmd/helm/search_test.go index c9ce62e05..40eb935d3 100644 --- a/cmd/helm/search_test.go +++ b/cmd/helm/search_test.go @@ -34,23 +34,23 @@ func TestSearchCmd(t *testing.T) { { name: "search for 'maria', expect one match", args: []string{"maria"}, - expect: "testing/mariadb", + expect: "NAME \tVERSION\tDESCRIPTION \ntesting/mariadb\t0.3.0 \tChart for MariaDB", }, { name: "search for 'alpine', expect two matches", args: []string{"alpine"}, - expect: "testing/alpine", + expect: "NAME \tVERSION\tDESCRIPTION \ntesting/alpine\t0.1.0 \tDeploy a basic Alpine Linux pod", }, { name: "search for 'syzygy', expect no matches", args: []string{"syzygy"}, - expect: "", + expect: "NAME\tVERSION\tDESCRIPTION", }, { name: "search for 'alp[a-z]+', expect two matches", args: []string{"alp[a-z]+"}, flags: []string{"--regexp"}, - expect: "testing/alpine", + expect: "NAME \tVERSION\tDESCRIPTION \ntesting/alpine\t0.1.0 \tDeploy a basic Alpine Linux pod", regexp: true, }, { From 3e3312cea06f7155ee8ffbecb70c20a5e450af4f Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 4 Oct 2016 17:04:46 -0600 Subject: [PATCH 138/183] feat(tiller): re-use values during upgrade When `helm install -f foo.yaml bar` is called, and then the release is upgraded with `helm upgrade happy-panda bar`, this will now re-use the values that were submitted with `-f foo.yaml`. The same is true for values specified with `--set`. Closes #1227 --- cmd/tiller/release_server.go | 13 +++++++++++++ cmd/tiller/release_server_test.go | 8 +++++++- pkg/chartutil/chartfile.go | 4 +++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 4260bd914..52e5e9b2d 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -325,6 +325,16 @@ func (s *releaseServer) performUpdate(originalRelease, updatedRelease *release.R return res, nil } +// reuseValues copies values from the current release to a new release if the new release does not have any values. +// +// If the request already has values, or if there are no values in the current release, this does nothing. +func (s *releaseServer) reuseValues(req *services.UpdateReleaseRequest, current *release.Release) { + if (req.Values == nil || req.Values.Raw == "") && current.Config != nil && current.Config.Raw != "" { + log.Printf("Copying values from %s (v%d) to new release.", current.Name, current.Version) + req.Values = current.Config + } +} + // prepareUpdate builds an updated release for an update operation. func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*release.Release, *release.Release, error) { if req.Name == "" { @@ -341,6 +351,9 @@ func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*rele return nil, nil, err } + // If new values were not supplied in the upgrade, re-use the existing values. + s.reuseValues(req, currentRelease) + ts := timeconv.Now() options := chartutil.ReleaseOptions{ Name: req.Name, diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 3da3a6173..d63a52e42 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -110,7 +110,7 @@ func namedReleaseStub(name string, status release.Status_Code) *release.Release Status: &release.Status{Code: status}, }, Chart: chartStub(), - Config: &chart.Config{Raw: `name = "value"`}, + Config: &chart.Config{Raw: `name: value`}, Version: 1, Hooks: []*release.Hook{ { @@ -568,6 +568,12 @@ func TestUpdateRelease(t *testing.T) { t.Errorf("No manifest returned: %v", res.Release) } + if res.Release.Config == nil { + t.Errorf("Got release without config: %#v", res.Release) + } else if res.Release.Config.Raw != rel.Config.Raw { + t.Errorf("Expected release values %q, got %q", rel.Config.Raw, res.Release.Config.Raw) + } + if len(updated.Manifest) == 0 { t.Errorf("Expected manifest in %v", res) } diff --git a/pkg/chartutil/chartfile.go b/pkg/chartutil/chartfile.go index 3087d81d4..27b8364df 100644 --- a/pkg/chartutil/chartfile.go +++ b/pkg/chartutil/chartfile.go @@ -24,7 +24,9 @@ import ( "k8s.io/helm/pkg/proto/hapi/chart" ) -// APIVersionV1 is the API version number for version 1. +// ApiVersionV1 is the API version number for version 1. +// +// This is ApiVersionV1 instead of APIVersionV1 to match the protobuf-generated name. const ApiVersionV1 = "v1" // UnmarshalChartfile takes raw Chart.yaml data and unmarshals it. From ea0e665f84cfe63cf89d8690ee738e1ff005536c Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 4 Oct 2016 23:11:58 -0600 Subject: [PATCH 139/183] fix(repo): auto-update index file formats This performs a relatively weak in-memory translation of index file data. It does not, in most cases, write the corrected data to disk, and it emits a warning directly to STDERR each time it loads a deprecated index. Known limitations: - It cannot recover certain bogus records that earlier alpha releases generated (notably, where all chartfile data is missing) - In some cases, it has to parse a filename to get version info. This is lossy. - Because it takes three passes through the YAML and JSON unmarshal, it is not performant. This feature is transitional and should be removed during the Beta cycle, prior to the release of 2.0.0. Closes #1265 --- cmd/helm/init.go | 2 +- cmd/helm/search.go | 2 +- pkg/repo/index.go | 101 +++++++++++++++++++---- pkg/repo/index_test.go | 20 +++++ pkg/repo/testdata/unversioned-index.yaml | 64 ++++++++++++++ 5 files changed, 172 insertions(+), 17 deletions(-) create mode 100644 pkg/repo/testdata/unversioned-index.yaml diff --git a/cmd/helm/init.go b/cmd/helm/init.go index c0e091041..1546860c6 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -138,7 +138,7 @@ func ensureHome(home helmpath.Home, out io.Writer) error { } cif := home.CacheIndex(stableRepository) if err := repo.DownloadIndexFile(stableRepository, stableRepositoryURL, cif); err != nil { - fmt.Fprintf(out, "WARNING: Failed to download %s: %s (run 'helm update')\n", stableRepository, err) + fmt.Fprintf(out, "WARNING: Failed to download %s: %s (run 'helm repo update')\n", stableRepository, err) } } else if fi.IsDir() { return fmt.Errorf("%s must be a file, not a directory", repoFile) diff --git a/cmd/helm/search.go b/cmd/helm/search.go index f8b02cff8..61ee4bb15 100644 --- a/cmd/helm/search.go +++ b/cmd/helm/search.go @@ -106,7 +106,7 @@ func (s *searchCmd) buildIndex() (*search.Index, error) { f := s.helmhome.CacheIndex(n) ind, err := repo.LoadIndexFile(f) if err != nil { - fmt.Fprintf(s.out, "WARNING: Repo %q is corrupt or missing. Try 'helm update':\n\t%s\n", f, err) + fmt.Fprintf(s.out, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update':\n\t%s\n", f, err) continue } diff --git a/pkg/repo/index.go b/pkg/repo/index.go index 42f751955..a8589aa19 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -17,7 +17,9 @@ limitations under the License. package repo import ( + "encoding/json" "errors" + "fmt" "io/ioutil" "net/http" "os" @@ -39,8 +41,14 @@ var indexPath = "index.yaml" // APIVersionV1 is the v1 API version for index and repository files. const APIVersionV1 = "v1" -// ErrNoAPIVersion indicates that an API version was not specified. -var ErrNoAPIVersion = errors.New("no API version specified") +var ( + // ErrNoAPIVersion indicates that an API version was not specified. + ErrNoAPIVersion = errors.New("no API version specified") + // ErrNoChartVersion indicates that a chart with the given version is not found. + ErrNoChartVersion = errors.New("no chart version found") + // ErrNoChartName indicates that a chart with the given name is not found. + ErrNoChartName = errors.New("no chart name found") +) // ChartVersions is a list of versioned chart references. // Implements a sorter on Version. @@ -86,8 +94,12 @@ func NewIndexFile() *IndexFile { // Add adds a file to the index func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { + u := filename + if baseURL != "" { + u = baseURL + "/" + filename + } cr := &ChartVersion{ - URLs: []string{baseURL + "/" + filename}, + URLs: []string{u}, Metadata: md, Digest: digest, Created: time.Now(), @@ -101,17 +113,8 @@ func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { // Has returns true if the index has an entry for a chart with the given name and exact version. func (i IndexFile) Has(name, version string) bool { - vs, ok := i.Entries[name] - if !ok { - return false - } - for _, ver := range vs { - // TODO: Do we need to normalize the version field with the SemVer lib? - if ver.Version == version { - return true - } - } - return false + _, err := i.Get(name, version) + return err == nil } // SortEntries sorts the entries by version in descending order. @@ -126,6 +129,26 @@ func (i IndexFile) SortEntries() { } } +// Get returns the ChartVersion for the given name. +// +// If version is empty, this will return the chart with the highest version. +func (i IndexFile) Get(name, version string) (*ChartVersion, error) { + vs, ok := i.Entries[name] + if !ok { + return nil, ErrNoChartName + } + if version == "" && len(vs) > 0 { + return vs[0], nil + } + for _, ver := range vs { + // TODO: Do we need to normalize the version field with the SemVer lib? + if ver.Version == version { + return ver, nil + } + } + return nil, ErrNoChartVersion +} + // WriteFile writes an index file to the given destination path. // // The mode on the file is set to 'mode'. @@ -207,11 +230,59 @@ func LoadIndex(data []byte) (*IndexFile, error) { return i, err } if i.APIVersion == "" { - return i, ErrNoAPIVersion + // When we leave Beta, we should remove legacy support and just + // return this error: + //return i, ErrNoAPIVersion + return loadUnversionedIndex(data) } return i, nil } +// unversionedEntry represents a deprecated pre-Alpha.5 format. +// +// This will be removed prior to v2.0.0 +type unversionedEntry struct { + Checksum string `json:"checksum"` + URL string `json:"url"` + Chartfile *chart.Metadata `json:"chartfile"` +} + +// loadUnversionedIndex loads a pre-Alpha.5 index.yaml file. +// +// This format is deprecated. This function will be removed prior to v2.0.0. +func loadUnversionedIndex(data []byte) (*IndexFile, error) { + fmt.Fprintln(os.Stderr, "WARNING: Deprecated index file format. Try 'helm repo update'") + i := map[string]unversionedEntry{} + + // This gets around an error in the YAML parser. Instead of parsing as YAML, + // we convert to JSON, and then decode again. + var err error + data, err = yaml.YAMLToJSON(data) + if err != nil { + return nil, err + } + if err := json.Unmarshal(data, &i); err != nil { + return nil, err + } + + if len(i) == 0 { + return nil, ErrNoAPIVersion + } + ni := NewIndexFile() + for n, item := range i { + if item.Chartfile == nil || item.Chartfile.Name == "" { + parts := strings.Split(n, "-") + ver := "" + if len(parts) > 1 { + ver = strings.TrimSuffix(parts[1], ".tgz") + } + item.Chartfile = &chart.Metadata{Name: parts[0], Version: ver} + } + ni.Add(item.Chartfile, item.URL, "", item.Checksum) + } + return ni, nil +} + // LoadIndexFile takes a file at the given path and returns an IndexFile object func LoadIndexFile(path string) (*IndexFile, error) { b, err := ioutil.ReadFile(path) diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go index a73cd0352..e5f5256a2 100644 --- a/pkg/repo/index_test.go +++ b/pkg/repo/index_test.go @@ -250,3 +250,23 @@ func TestIndexDirectory(t *testing.T) { t.Errorf("Expected frobnitz, got %q", frob.Name) } } + +func TestLoadUnversionedIndex(t *testing.T) { + data, err := ioutil.ReadFile("testdata/unversioned-index.yaml") + if err != nil { + t.Fatal(err) + } + + ind, err := loadUnversionedIndex(data) + if err != nil { + t.Fatal(err) + } + + if l := len(ind.Entries); l != 2 { + t.Fatalf("Expected 2 entries, got %d", l) + } + + if l := len(ind.Entries["mysql"]); l != 3 { + t.Fatalf("Expected 3 mysql versions, got %d", l) + } +} diff --git a/pkg/repo/testdata/unversioned-index.yaml b/pkg/repo/testdata/unversioned-index.yaml new file mode 100644 index 000000000..7299c66dc --- /dev/null +++ b/pkg/repo/testdata/unversioned-index.yaml @@ -0,0 +1,64 @@ +memcached-0.1.0: + name: memcached + url: https://mumoshu.github.io/charts/memcached-0.1.0.tgz + created: 2016-08-04 02:05:02.259205055 +0000 UTC + checksum: ce9b76576c4b4eb74286fa30a978c56d69e7a522 + chartfile: + name: memcached + home: http://https://hub.docker.com/_/memcached/ + sources: [] + version: 0.1.0 + description: A simple Memcached cluster + keywords: [] + maintainers: + - name: Matt Butcher + email: mbutcher@deis.com + engine: "" +mysql-0.2.0: + name: mysql + url: https://mumoshu.github.io/charts/mysql-0.2.0.tgz + created: 2016-08-04 00:42:47.517342022 +0000 UTC + checksum: aa5edd2904d639b0b6295f1c7cf4c0a8e4f77dd3 + chartfile: + name: mysql + home: https://www.mysql.com/ + sources: [] + version: 0.2.0 + description: Chart running MySQL. + keywords: [] + maintainers: + - name: Matt Fisher + email: mfisher@deis.com + engine: "" +mysql-0.2.1: + name: mysql + url: https://mumoshu.github.io/charts/mysql-0.2.1.tgz + created: 2016-08-04 02:40:29.717829534 +0000 UTC + checksum: 9d9f056171beefaaa04db75680319ca4edb6336a + chartfile: + name: mysql + home: https://www.mysql.com/ + sources: [] + version: 0.2.1 + description: Chart running MySQL. + keywords: [] + maintainers: + - name: Matt Fisher + email: mfisher@deis.com + engine: "" +mysql-0.2.2: + name: mysql + url: https://mumoshu.github.io/charts/mysql-0.2.2.tgz + created: 2016-08-04 02:40:29.71841952 +0000 UTC + checksum: 6d6810e76a5987943faf0040ec22990d9fb141c7 + chartfile: + name: mysql + home: https://www.mysql.com/ + sources: [] + version: 0.2.2 + description: Chart running MySQL. + keywords: [] + maintainers: + - name: Matt Fisher + email: mfisher@deis.com + engine: "" From d0cefeaf82504a63a47b0aa6aaaf9d8d02abaeba Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 5 Oct 2016 14:45:41 -0600 Subject: [PATCH 140/183] feat(helm): add --versions flag on search This causes search to index by name/version instead of just name, which means you can get a list of versions of a chart. The '--versions' flag enables this behavior. Partially fixes #1199 --- cmd/helm/search.go | 20 ++++-------- cmd/helm/search/search.go | 59 ++++++++++++++++++++++++++++------ cmd/helm/search/search_test.go | 34 +++++++++++++++++--- cmd/helm/search_test.go | 6 ++++ 4 files changed, 93 insertions(+), 26 deletions(-) diff --git a/cmd/helm/search.go b/cmd/helm/search.go index 541dabc98..020b6fe5f 100644 --- a/cmd/helm/search.go +++ b/cmd/helm/search.go @@ -43,7 +43,8 @@ type searchCmd struct { out io.Writer helmhome helmpath.Home - regexp bool + versions bool + regexp bool } func newSearchCmd(out io.Writer) *cobra.Command { @@ -59,7 +60,9 @@ func newSearchCmd(out io.Writer) *cobra.Command { PreRunE: requireInit, } - cmd.Flags().BoolVarP(&sc.regexp, "regexp", "r", false, "use regular expressions for searching") + f := cmd.Flags() + f.BoolVarP(&sc.regexp, "regexp", "r", false, "use regular expressions for searching") + f.BoolVarP(&sc.versions, "versions", "l", false, "show the long listing, with each version of each chart on its own line.") return cmd } @@ -88,16 +91,7 @@ func (s *searchCmd) run(args []string) error { } func (s *searchCmd) showAllCharts(i *search.Index) { - e := i.Entries() - res := make([]*search.Result, len(e)) - j := 0 - for name, ch := range e { - res[j] = &search.Result{ - Name: name, - Chart: ch, - } - j++ - } + res := i.All() search.SortScore(res) fmt.Fprintln(s.out, s.formatSearchResults(res)) } @@ -129,7 +123,7 @@ func (s *searchCmd) buildIndex() (*search.Index, error) { continue } - i.AddRepo(n, ind) + i.AddRepo(n, ind, s.versions) } return i, nil } diff --git a/cmd/helm/search/search.go b/cmd/helm/search/search.go index aa17fd7ee..e96de2bc1 100644 --- a/cmd/helm/search/search.go +++ b/cmd/helm/search/search.go @@ -29,6 +29,8 @@ import ( "sort" "strings" + "github.com/Masterminds/semver" + "k8s.io/helm/pkg/repo" ) @@ -55,25 +57,51 @@ func NewIndex() *Index { return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartVersion{}} } +// verSep is a separator for version fields in map keys. +const verSep = "$$" + // AddRepo adds a repository index to the search index. -func (i *Index) AddRepo(rname string, ind *repo.IndexFile) { +func (i *Index) AddRepo(rname string, ind *repo.IndexFile, all bool) { for name, ref := range ind.Entries { if len(ref) == 0 { - // Skip chart names that havae zero releases. + // Skip chart names that have zero releases. continue } // By convention, an index file is supposed to have the newest at the // 0 slot, so our best bet is to grab the 0 entry and build the index // entry off of that. fname := filepath.Join(rname, name) - i.lines[fname] = indstr(rname, ref[0]) - i.charts[fname] = ref[0] + if !all { + i.lines[fname] = indstr(rname, ref[0]) + i.charts[fname] = ref[0] + continue + } + + // If 'all' is set, then we go through all of the refs, and add them all + // to the index. This will generate a lot of near-duplicate entries. + for _, rr := range ref { + versionedName := fname + verSep + rr.Version + i.lines[versionedName] = indstr(rname, rr) + i.charts[versionedName] = rr + } } } -// Entries returns the entries in an index. -func (i *Index) Entries() map[string]*repo.ChartVersion { - return i.charts +// All returns all charts in the index as if they were search results. +// +// Each will be given a score of 0. +func (i *Index) All() []*Result { + res := make([]*Result, len(i.charts)) + j := 0 + for name, ch := range i.charts { + parts := strings.Split(name, verSep) + res[j] = &Result{ + Name: parts[0], + Chart: ch, + } + j++ + } + return res } // Search searches an index for the given term. @@ -118,7 +146,8 @@ func (i *Index) SearchLiteral(term string, threshold int) []*Result { for k, v := range i.lines { res := strings.Index(v, term) if score := i.calcScore(res, v); res != -1 && score < threshold { - buf = append(buf, &Result{Name: k, Score: score, Chart: i.charts[k]}) + parts := strings.Split(k, verSep) // Remove version, if it is there. + buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) } } return buf @@ -137,7 +166,8 @@ func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) { continue } if score := i.calcScore(ind[0], v); ind[0] >= 0 && score < threshold { - buf = append(buf, &Result{Name: k, Score: score, Chart: i.charts[k]}) + parts := strings.Split(k, verSep) // Remove version, if it is there. + buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) } } return buf, nil @@ -179,6 +209,17 @@ func (s scoreSorter) Less(a, b int) bool { if first.Score < second.Score { return true } + if first.Name == second.Name { + v1, err := semver.NewVersion(first.Chart.Version) + if err != nil { + return true + } + v2, err := semver.NewVersion(second.Chart.Version) + if err != nil { + return true + } + return v1.GreaterThan(v2) + } return first.Name < second.Name } diff --git a/cmd/helm/search/search_test.go b/cmd/helm/search/search_test.go index 9cfa1bc3e..7f5d29409 100644 --- a/cmd/helm/search/search_test.go +++ b/cmd/helm/search/search_test.go @@ -93,9 +93,9 @@ var indexfileEntries = map[string]repo.ChartVersions{ }, } -func loadTestIndex(t *testing.T) *Index { +func loadTestIndex(t *testing.T, all bool) *Index { i := NewIndex() - i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}) + i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all) i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{ "pinta": { { @@ -107,10 +107,24 @@ func loadTestIndex(t *testing.T) *Index { }, }, }, - }}) + }}, all) return i } +func TestAll(t *testing.T) { + i := loadTestIndex(t, false) + all := i.All() + if len(all) != 4 { + t.Errorf("Expected 4 entries, got %d", len(all)) + } + + i = loadTestIndex(t, true) + all = i.All() + if len(all) != 5 { + t.Errorf("Expected 5 entries, got %d", len(all)) + } +} + func TestSearchByName(t *testing.T) { tests := []struct { @@ -188,7 +202,7 @@ func TestSearchByName(t *testing.T) { }, } - i := loadTestIndex(t) + i := loadTestIndex(t, false) for _, tt := range tests { @@ -224,6 +238,18 @@ func TestSearchByName(t *testing.T) { } } +func TestSearchByNameAll(t *testing.T) { + // Test with the All bit turned on. + i := loadTestIndex(t, true) + cs, err := i.Search("santa-maria", 100, false) + if err != nil { + t.Fatal(err) + } + if len(cs) != 2 { + t.Errorf("expected 2 charts, got %d", len(cs)) + } +} + func TestCalcScore(t *testing.T) { i := NewIndex() diff --git a/cmd/helm/search_test.go b/cmd/helm/search_test.go index 40eb935d3..999e79414 100644 --- a/cmd/helm/search_test.go +++ b/cmd/helm/search_test.go @@ -41,6 +41,12 @@ func TestSearchCmd(t *testing.T) { args: []string{"alpine"}, expect: "NAME \tVERSION\tDESCRIPTION \ntesting/alpine\t0.1.0 \tDeploy a basic Alpine Linux pod", }, + { + name: "search for 'alpine' with versions, expect three matches", + args: []string{"alpine"}, + flags: []string{"--versions"}, + expect: "NAME \tVERSION\tDESCRIPTION \ntesting/alpine\t0.2.0 \tDeploy a basic Alpine Linux pod\ntesting/alpine\t0.1.0 \tDeploy a basic Alpine Linux pod", + }, { name: "search for 'syzygy', expect no matches", args: []string{"syzygy"}, From fd0303c86efcecc7904e995d693ff3a94c9b7943 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 5 Oct 2016 17:29:49 -0600 Subject: [PATCH 141/183] fix(*): remove references to gopkg.in/yaml.v2 This removes the last of the requests for gopkg.in/yaml.v2, and also the struct annotations. Closes #1192 Closes #1263 --- glide.lock | 4 ++-- glide.yaml | 2 +- pkg/lint/rules/template.go | 2 +- pkg/repo/index.go | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/glide.lock b/glide.lock index 3bd4d1d39..79052cc41 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 041486e116f9792e4533a4a67b191ab139d29ea4449d4b6eb6e2d96b620b3cac -updated: 2016-10-03T20:28:56.472755464-06:00 +hash: 59cc65f24ef42bcc96a5e5f660dd79cff89aa53040a561c47b629f1d55397424 +updated: 2016-10-05T17:24:13.347058671-06:00 imports: - name: bitbucket.org/ww/goautoneg version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 diff --git a/glide.yaml b/glide.yaml index 5439b4341..663c3fd47 100644 --- a/glide.yaml +++ b/glide.yaml @@ -9,7 +9,7 @@ import: version: 367864438f1b1a3c7db4da06a2f55b144e6784e0 - package: github.com/Masterminds/sprig version: ^2.6 -- package: gopkg.in/yaml.v2 +- package: github.com/ghodss/yaml - package: github.com/Masterminds/semver version: 1.1.0 - package: github.com/technosophos/moniker diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index 10badbcfc..dd36e5621 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -27,7 +27,7 @@ import ( "text/template" "github.com/Masterminds/sprig" - "gopkg.in/yaml.v2" + "github.com/ghodss/yaml" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/engine" "k8s.io/helm/pkg/lint/support" diff --git a/pkg/repo/index.go b/pkg/repo/index.go index a8589aa19..742742e92 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -165,10 +165,10 @@ func (i IndexFile) WriteFile(dest string, mode os.FileMode) error { // ChartVersion represents a chart entry in the IndexFile type ChartVersion struct { *chart.Metadata - URLs []string `yaml:"url" json:"urls"` - Created time.Time `yaml:"created,omitempty" json:"created,omitempty"` - Removed bool `yaml:"removed,omitempty" json:"removed,omitempty"` - Digest string `yaml:"digest,omitempty" json:"digest,omitempty"` + URLs []string `json:"urls"` + Created time.Time `json:"created,omitempty"` + Removed bool `json:"removed,omitempty"` + Digest string `json:"digest,omitempty"` } // IndexDirectory reads a (flat) directory and generates an index. From e9dd302a9cc7216b7ac65d037387a64d5da30b47 Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Tue, 4 Oct 2016 14:33:02 -0600 Subject: [PATCH 142/183] feat(helm/cmd): support for retrieving release history --- _proto/hapi/services/tiller.proto | 16 +++ cmd/helm/helm.go | 1 + cmd/helm/helm_test.go | 4 + cmd/helm/history.go | 107 +++++++++++++++ cmd/helm/history_test.go | 82 +++++++++++ cmd/tiller/release_history.go | 58 ++++++++ cmd/tiller/release_history_test.go | 116 ++++++++++++++++ pkg/helm/client.go | 30 ++++ pkg/helm/interface.go | 1 + pkg/helm/option.go | 14 ++ pkg/proto/hapi/services/tiller.pb.go | 196 ++++++++++++++++++--------- pkg/storage/storage.go | 12 ++ pkg/storage/storage_test.go | 31 +++++ 13 files changed, 606 insertions(+), 62 deletions(-) create mode 100644 cmd/helm/history.go create mode 100644 cmd/helm/history_test.go create mode 100644 cmd/tiller/release_history.go create mode 100644 cmd/tiller/release_history_test.go diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto index 2e4fee917..51fb1552c 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -75,6 +75,9 @@ service ReleaseService { rpc RollbackRelease(RollbackReleaseRequest) returns (RollbackReleaseResponse) { } + // ReleaseHistory retrieves a releasse's history. + rpc GetHistory(GetHistoryRequest) returns (GetHistoryResponse) { + } } // ListReleasesRequest requests a list of releases. @@ -262,3 +265,16 @@ message GetVersionRequest { message GetVersionResponse { hapi.version.Version Version = 1; } + +// GetHistoryRequest requests a release's history. +message GetHistoryRequest { + // The name of the release. + string name = 1; + // The maximum number of releases to include. + int32 max = 2; +} + +// GetHistoryResponse is received in response to a GetHistory rpc. +message GetHistoryResponse { + repeated hapi.release.Release releases = 1; +} diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 3a0f0cfa9..46d13c64d 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -93,6 +93,7 @@ func newRootCmd(out io.Writer) *cobra.Command { newFetchCmd(out), newGetCmd(nil, out), newHomeCmd(out), + newHistoryCmd(nil, out), newInitCmd(out), newInspectCmd(nil, out), newInstallCmd(nil, out), diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 94dd57122..43a1c8b64 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -175,6 +175,10 @@ func (c *fakeReleaseClient) ReleaseContent(rlsName string, opts ...helm.ContentO return resp, c.err } +func (c *fakeReleaseClient) ReleaseHistory(rlsName string, opts ...helm.HistoryOption) (*rls.GetHistoryResponse, error) { + return &rls.GetHistoryResponse{Releases: c.rels}, c.err +} + func (c *fakeReleaseClient) Option(opt ...helm.Option) helm.Interface { return c } diff --git a/cmd/helm/history.go b/cmd/helm/history.go new file mode 100644 index 000000000..44829de5a --- /dev/null +++ b/cmd/helm/history.go @@ -0,0 +1,107 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "io" + + "github.com/gosuri/uitable" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/timeconv" +) + +var historyHelp = ` +History prints historical revisions for a given release. + +A default maximum of 256 revisions will be returned. Setting '--max' +configures the maximum length of the revision list returned. + +The historical release set is printed as a formatted table, e.g: + + $ helm history angry-bird --max=4 + REVISION UPDATED STATUS CHART + 4 Mon Oct 3 10:15:13 2016 DEPLOYED alpine-0.1.0 + 3 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 + 2 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 + 1 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 +` + +type historyCmd struct { + max int32 + rls string + out io.Writer + helmc helm.Interface +} + +func newHistoryCmd(c helm.Interface, w io.Writer) *cobra.Command { + his := &historyCmd{out: w, helmc: c} + cmd := &cobra.Command{ + Use: "history [flags] RELEASE_NAME", + Long: historyHelp, + Short: "fetch release history", + Aliases: []string{"hist"}, + PersistentPreRunE: setupConnection, + RunE: func(cmd *cobra.Command, args []string) error { + switch { + case len(args) == 0: + return errReleaseRequired + case his.helmc == nil: + his.helmc = helm.NewClient(helm.Host(tillerHost)) + } + his.rls = args[0] + return his.run() + }, + } + + cmd.Flags().Int32Var(&his.max, "max", 256, "maximum number of revision to include in history") + return cmd +} + +func (cmd *historyCmd) run() error { + opts := []helm.HistoryOption{ + helm.WithMaxHistory(cmd.max), + } + + r, err := cmd.helmc.ReleaseHistory(cmd.rls, opts...) + if err != nil { + return prettyError(err) + } + if len(r.Releases) == 0 { + return nil + } + + fmt.Fprintln(cmd.out, formatHistory(r.Releases)) + return nil +} + +func formatHistory(rls []*release.Release) string { + tbl := uitable.New() + tbl.MaxColWidth = 30 + tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART") + for _, r := range rls { + c := fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version) + t := timeconv.String(r.Info.LastDeployed) + s := r.Info.Status.Code.String() + v := r.Version + tbl.AddRow(v, t, s, c) + } + return tbl.String() +} diff --git a/cmd/helm/history_test.go b/cmd/helm/history_test.go new file mode 100644 index 000000000..d8d729096 --- /dev/null +++ b/cmd/helm/history_test.go @@ -0,0 +1,82 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "regexp" + "testing" + + rpb "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestHistoryCmd(t *testing.T) { + mk := func(name string, vers int32, code rpb.Status_Code) *rpb.Release { + return releaseMock(&releaseOptions{ + name: name, + version: vers, + statusCode: code, + }) + } + + tests := []struct { + cmds string + desc string + args []string + resp []*rpb.Release + xout string + }{ + { + cmds: "helm history RELEASE_NAME", + desc: "get history for release", + args: []string{"angry-bird"}, + resp: []*rpb.Release{ + mk("angry-bird", 4, rpb.Status_DEPLOYED), + mk("angry-bird", 3, rpb.Status_SUPERSEDED), + mk("angry-bird", 2, rpb.Status_SUPERSEDED), + mk("angry-bird", 1, rpb.Status_SUPERSEDED), + }, + xout: "REVISION\tUPDATED \tSTATUS \tCHART \n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n2 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n1 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n", + }, + { + cmds: "helm history --max=MAX RELEASE_NAME", + desc: "get history with max limit set", + args: []string{"--max=2", "angry-bird"}, + resp: []*rpb.Release{ + mk("angry-bird", 4, rpb.Status_DEPLOYED), + mk("angry-bird", 3, rpb.Status_SUPERSEDED), + }, + xout: "REVISION\tUPDATED \tSTATUS \tCHART \n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n", + }, + } + + var buf bytes.Buffer + for _, tt := range tests { + frc := &fakeReleaseClient{rels: tt.resp} + cmd := newHistoryCmd(frc, &buf) + cmd.ParseFlags(tt.args) + + if err := cmd.RunE(cmd, tt.args); err != nil { + t.Fatalf("%q\n\t%s: unexpected error: %v", tt.cmds, tt.desc, err) + } + re := regexp.MustCompile(tt.xout) + if !re.Match(buf.Bytes()) { + t.Fatalf("%q\n\t%s:\nexpected\n\t%q\nactual\n\t%q", tt.cmds, tt.desc, tt.xout, buf.String()) + } + buf.Reset() + } +} diff --git a/cmd/tiller/release_history.go b/cmd/tiller/release_history.go new file mode 100644 index 000000000..70176b42e --- /dev/null +++ b/cmd/tiller/release_history.go @@ -0,0 +1,58 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "sort" + + "golang.org/x/net/context" + rpb "k8s.io/helm/pkg/proto/hapi/release" + tpb "k8s.io/helm/pkg/proto/hapi/services" +) + +func (s *releaseServer) GetHistory(ctx context.Context, req *tpb.GetHistoryRequest) (*tpb.GetHistoryResponse, error) { + if !checkClientVersion(ctx) { + return nil, errIncompatibleVersion + } + + h, err := s.env.Releases.History(req.Name) + if err != nil { + return nil, err + } + + sort.Sort(sort.Reverse(byRev(h))) + + var resp tpb.GetHistoryResponse + for i := 0; i < min(len(h), int(req.Max)); i++ { + resp.Releases = append(resp.Releases, h[i]) + } + + return &resp, nil +} + +type byRev []*rpb.Release + +func (s byRev) Len() int { return len(s) } +func (s byRev) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byRev) Less(i, j int) bool { return s[i].Version < s[j].Version } + +func min(x, y int) int { + if x < y { + return x + } + return y +} diff --git a/cmd/tiller/release_history_test.go b/cmd/tiller/release_history_test.go new file mode 100644 index 000000000..9663c6248 --- /dev/null +++ b/cmd/tiller/release_history_test.go @@ -0,0 +1,116 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "reflect" + "testing" + + "k8s.io/helm/pkg/helm" + + rpb "k8s.io/helm/pkg/proto/hapi/release" + tpb "k8s.io/helm/pkg/proto/hapi/services" +) + +func TestGetHistory_WithRevisions(t *testing.T) { + mk := func(name string, vers int32, code rpb.Status_Code) *rpb.Release { + return &rpb.Release{ + Name: name, + Version: vers, + Info: &rpb.Info{Status: &rpb.Status{Code: code}}, + } + } + + // GetReleaseHistoryTests + tests := []struct { + desc string + req *tpb.GetHistoryRequest + res *tpb.GetHistoryResponse + }{ + { + desc: "get release with history and default limit (max=256)", + req: &tpb.GetHistoryRequest{Name: "angry-bird", Max: 256}, + res: &tpb.GetHistoryResponse{Releases: []*rpb.Release{ + mk("angry-bird", 4, rpb.Status_DEPLOYED), + mk("angry-bird", 3, rpb.Status_SUPERSEDED), + mk("angry-bird", 2, rpb.Status_SUPERSEDED), + mk("angry-bird", 1, rpb.Status_SUPERSEDED), + }}, + }, + { + desc: "get release with history using result limit (max=2)", + req: &tpb.GetHistoryRequest{Name: "angry-bird", Max: 2}, + res: &tpb.GetHistoryResponse{Releases: []*rpb.Release{ + mk("angry-bird", 4, rpb.Status_DEPLOYED), + mk("angry-bird", 3, rpb.Status_SUPERSEDED), + }}, + }, + } + + // test release history for release 'angry-bird' + hist := []*rpb.Release{ + mk("angry-bird", 4, rpb.Status_DEPLOYED), + mk("angry-bird", 3, rpb.Status_SUPERSEDED), + mk("angry-bird", 2, rpb.Status_SUPERSEDED), + mk("angry-bird", 1, rpb.Status_SUPERSEDED), + } + + srv := rsFixture() + for _, rls := range hist { + if err := srv.env.Releases.Create(rls); err != nil { + t.Fatalf("Failed to create release: %s", err) + } + } + + // run tests + for _, tt := range tests { + res, err := srv.GetHistory(helm.NewContext(), tt.req) + if err != nil { + t.Fatalf("%s:\nFailed to get History of %q: %s", tt.desc, tt.req.Name, err) + } + if !reflect.DeepEqual(res, tt.res) { + t.Fatalf("%s:\nExpected:\n\t%+v\nActual\n\t%+v", tt.desc, tt.res, res) + } + } +} + +func TestGetHistory_WithNoRevisions(t *testing.T) { + tests := []struct { + desc string + req *tpb.GetHistoryRequest + }{ + { + desc: "get release with no history", + req: &tpb.GetHistoryRequest{Name: "sad-panda", Max: 256}, + }, + } + + // create release 'sad-panda' with no revision history + rls := namedReleaseStub("sad-panda", rpb.Status_DEPLOYED) + srv := rsFixture() + srv.env.Releases.Create(rls) + + for _, tt := range tests { + res, err := srv.GetHistory(helm.NewContext(), tt.req) + if err != nil { + t.Fatalf("%s:\nFailed to get History of %q: %s", tt.desc, tt.req.Name, err) + } + if len(res.Releases) > 1 { + t.Fatalf("%s:\nExpected zero items, got %d", tt.desc, len(res.Releases)) + } + } +} diff --git a/pkg/helm/client.go b/pkg/helm/client.go index 1a4160174..16fb2e118 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -225,6 +225,24 @@ func (h *Client) ReleaseContent(rlsName string, opts ...ContentOption) (*rls.Get return h.content(ctx, req) } +// ReleaseHistory returns a release's revision history. +func (h *Client) ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.GetHistoryResponse, error) { + for _, opt := range opts { + opt(&h.opts) + } + + req := &h.opts.histReq + req.Name = rlsName + ctx := NewContext() + + if h.opts.before != nil { + if err := h.opts.before(ctx, req); err != nil { + return nil, err + } + } + return h.history(ctx, req) +} + // Executes tiller.ListReleases RPC. func (h *Client) list(ctx context.Context, req *rls.ListReleasesRequest) (*rls.ListReleasesResponse, error) { c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) @@ -325,3 +343,15 @@ func (h *Client) version(ctx context.Context, req *rls.GetVersionRequest) (*rls. rlc := rls.NewReleaseServiceClient(c) return rlc.GetVersion(ctx, req) } + +// Executes tiller.GetHistory RPC. +func (h *Client) history(ctx context.Context, req *rls.GetHistoryRequest) (*rls.GetHistoryResponse, error) { + c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) + if err != nil { + return nil, err + } + defer c.Close() + + rlc := rls.NewReleaseServiceClient(c) + return rlc.GetHistory(ctx, req) +} diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go index 3ec1e83b4..5c6921afe 100644 --- a/pkg/helm/interface.go +++ b/pkg/helm/interface.go @@ -29,5 +29,6 @@ type Interface interface { UpdateRelease(rlsName, chStr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) RollbackRelease(rlsName string, opts ...RollbackOption) (*rls.RollbackReleaseResponse, error) ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) + ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.GetHistoryResponse, error) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) } diff --git a/pkg/helm/option.go b/pkg/helm/option.go index 08dc6dc73..a445e8643 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -60,6 +60,8 @@ type options struct { rollbackReq rls.RollbackReleaseRequest // before intercepts client calls before sending before func(context.Context, proto.Message) error + // release history options are applied directly to the get release history request + histReq rls.GetHistoryRequest } // Host specifies the host address of the Tiller release server, (default = ":44134"). @@ -272,6 +274,18 @@ type UpdateOption func(*options) // running the `helm rollback` command. type RollbackOption func(*options) +// HistoryOption allows configuring optional request data for +// issuing a GetHistory rpc. +type HistoryOption func(*options) + +// WithMaxHistory sets the max number of releases to return +// in a release history query. +func WithMaxHistory(max int32) HistoryOption { + return func(opts *options) { + opts.histReq.Max = max + } +} + // NewContext creates a versioned context. func NewContext() context.Context { md := metadata.Pairs("x-helm-api-client", version.Version) diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index ca556500a..b2e540a2f 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -26,6 +26,8 @@ It has these top-level messages: UninstallReleaseResponse GetVersionRequest GetVersionResponse + GetHistoryRequest + GetHistoryResponse */ package services @@ -416,7 +418,7 @@ func (*GetVersionRequest) ProtoMessage() {} func (*GetVersionRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{15} } type GetVersionResponse struct { - Version *hapi_version.Version `protobuf:"bytes,1,opt,name=Version" json:"Version,omitempty"` + Version *hapi_version.Version `protobuf:"bytes,1,opt,name=Version,json=version" json:"Version,omitempty"` } func (m *GetVersionResponse) Reset() { *m = GetVersionResponse{} } @@ -431,6 +433,36 @@ func (m *GetVersionResponse) GetVersion() *hapi_version.Version { return nil } +// GetHistoryRequest requests a release's history. +type GetHistoryRequest struct { + // The name of the release. + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + // The maximum number of releases to include. + Max int32 `protobuf:"varint,2,opt,name=max" json:"max,omitempty"` +} + +func (m *GetHistoryRequest) Reset() { *m = GetHistoryRequest{} } +func (m *GetHistoryRequest) String() string { return proto.CompactTextString(m) } +func (*GetHistoryRequest) ProtoMessage() {} +func (*GetHistoryRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{17} } + +// GetHistoryResponse is received in response to a GetHistory rpc. +type GetHistoryResponse struct { + Releases []*hapi_release3.Release `protobuf:"bytes,1,rep,name=releases" json:"releases,omitempty"` +} + +func (m *GetHistoryResponse) Reset() { *m = GetHistoryResponse{} } +func (m *GetHistoryResponse) String() string { return proto.CompactTextString(m) } +func (*GetHistoryResponse) ProtoMessage() {} +func (*GetHistoryResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{18} } + +func (m *GetHistoryResponse) GetReleases() []*hapi_release3.Release { + if m != nil { + return m.Releases + } + return nil +} + func init() { proto.RegisterType((*ListReleasesRequest)(nil), "hapi.services.tiller.ListReleasesRequest") proto.RegisterType((*ListSort)(nil), "hapi.services.tiller.ListSort") @@ -449,6 +481,8 @@ func init() { proto.RegisterType((*UninstallReleaseResponse)(nil), "hapi.services.tiller.UninstallReleaseResponse") proto.RegisterType((*GetVersionRequest)(nil), "hapi.services.tiller.GetVersionRequest") proto.RegisterType((*GetVersionResponse)(nil), "hapi.services.tiller.GetVersionResponse") + proto.RegisterType((*GetHistoryRequest)(nil), "hapi.services.tiller.GetHistoryRequest") + proto.RegisterType((*GetHistoryResponse)(nil), "hapi.services.tiller.GetHistoryResponse") proto.RegisterEnum("hapi.services.tiller.ListSort_SortBy", ListSort_SortBy_name, ListSort_SortBy_value) proto.RegisterEnum("hapi.services.tiller.ListSort_SortOrder", ListSort_SortOrder_name, ListSort_SortOrder_value) } @@ -483,6 +517,8 @@ type ReleaseServiceClient interface { GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) // RollbackRelease rolls back a release to a previous version. RollbackRelease(ctx context.Context, in *RollbackReleaseRequest, opts ...grpc.CallOption) (*RollbackReleaseResponse, error) + // ReleaseHistory retrieves a releasse's history. + GetHistory(ctx context.Context, in *GetHistoryRequest, opts ...grpc.CallOption) (*GetHistoryResponse, error) } type releaseServiceClient struct { @@ -588,6 +624,15 @@ func (c *releaseServiceClient) RollbackRelease(ctx context.Context, in *Rollback return out, nil } +func (c *releaseServiceClient) GetHistory(ctx context.Context, in *GetHistoryRequest, opts ...grpc.CallOption) (*GetHistoryResponse, error) { + out := new(GetHistoryResponse) + err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/GetHistory", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // Server API for ReleaseService service type ReleaseServiceServer interface { @@ -610,6 +655,8 @@ type ReleaseServiceServer interface { GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) // RollbackRelease rolls back a release to a previous version. RollbackRelease(context.Context, *RollbackReleaseRequest) (*RollbackReleaseResponse, error) + // ReleaseHistory retrieves a releasse's history. + GetHistory(context.Context, *GetHistoryRequest) (*GetHistoryResponse, error) } func RegisterReleaseServiceServer(s *grpc.Server, srv ReleaseServiceServer) { @@ -763,6 +810,24 @@ func _ReleaseService_RollbackRelease_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _ReleaseService_GetHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetHistoryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ReleaseServiceServer).GetHistory(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/hapi.services.tiller.ReleaseService/GetHistory", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReleaseServiceServer).GetHistory(ctx, req.(*GetHistoryRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _ReleaseService_serviceDesc = grpc.ServiceDesc{ ServiceName: "hapi.services.tiller.ReleaseService", HandlerType: (*ReleaseServiceServer)(nil), @@ -795,6 +860,10 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ MethodName: "RollbackRelease", Handler: _ReleaseService_RollbackRelease_Handler, }, + { + MethodName: "GetHistory", + Handler: _ReleaseService_GetHistory_Handler, + }, }, Streams: []grpc.StreamDesc{ { @@ -809,65 +878,68 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ func init() { proto.RegisterFile("hapi/services/tiller.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 946 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdd, 0x72, 0xdb, 0x44, - 0x14, 0xae, 0x6c, 0xc7, 0xb2, 0x4f, 0x7e, 0x48, 0xb6, 0x49, 0xac, 0x68, 0x80, 0xe9, 0x88, 0x81, - 0x86, 0x42, 0x1d, 0x30, 0xb7, 0x0c, 0x33, 0x69, 0xea, 0x49, 0x43, 0x83, 0x3b, 0xb3, 0x26, 0x30, - 0xc3, 0x05, 0x1e, 0xc5, 0x5e, 0x37, 0xa2, 0x8a, 0xd6, 0x68, 0xd7, 0x1e, 0x72, 0xcf, 0x0d, 0x17, - 0xbc, 0x04, 0xef, 0xc1, 0x93, 0x71, 0xc3, 0x6a, 0x7f, 0x14, 0x4b, 0x96, 0x5a, 0xd5, 0x37, 0xd6, - 0xee, 0x9e, 0x6f, 0xbf, 0x73, 0xce, 0x77, 0x76, 0xcf, 0x1a, 0xdc, 0x1b, 0x7f, 0x16, 0x9c, 0x30, - 0x12, 0x2f, 0x82, 0x31, 0x61, 0x27, 0x3c, 0x08, 0x43, 0x12, 0x77, 0x67, 0x31, 0xe5, 0x14, 0xed, - 0x27, 0xb6, 0xae, 0xb1, 0x75, 0x95, 0xcd, 0x3d, 0x94, 0x3b, 0xc6, 0x37, 0x7e, 0xcc, 0xd5, 0xaf, - 0x42, 0xbb, 0x9d, 0xe5, 0x75, 0x1a, 0x4d, 0x83, 0xd7, 0xda, 0xa0, 0x5c, 0xc4, 0x24, 0x24, 0x3e, - 0x23, 0xe6, 0x9b, 0xd9, 0x64, 0x6c, 0x41, 0x34, 0xa5, 0xda, 0x70, 0x94, 0x31, 0x30, 0xee, 0xf3, - 0x39, 0xcb, 0xf0, 0x2d, 0x48, 0xcc, 0x02, 0x1a, 0x99, 0xaf, 0xb2, 0x79, 0xff, 0xd4, 0xe0, 0xe1, - 0x65, 0xc0, 0x38, 0x56, 0x1b, 0x19, 0x26, 0xbf, 0xcf, 0x09, 0xe3, 0x68, 0x1f, 0x36, 0xc2, 0xe0, - 0x36, 0xe0, 0x8e, 0xf5, 0xc8, 0x3a, 0xae, 0x63, 0x35, 0x41, 0x87, 0xd0, 0xa4, 0xd3, 0x29, 0x23, - 0xdc, 0xa9, 0x89, 0xe5, 0x36, 0xd6, 0x33, 0xf4, 0x1d, 0xd8, 0x8c, 0xc6, 0x7c, 0x74, 0x7d, 0xe7, - 0xd4, 0x85, 0x61, 0xa7, 0xf7, 0x69, 0xb7, 0x48, 0x8a, 0x6e, 0xe2, 0x69, 0x28, 0x80, 0xdd, 0xe4, - 0xe7, 0xd9, 0x1d, 0x6e, 0x32, 0xf9, 0x4d, 0x78, 0xa7, 0x41, 0xc8, 0x49, 0xec, 0x34, 0x14, 0xaf, - 0x9a, 0xa1, 0x73, 0x00, 0xc9, 0x4b, 0xe3, 0x89, 0xb0, 0x6d, 0x48, 0xea, 0xe3, 0x0a, 0xd4, 0xaf, - 0x12, 0x3c, 0x6e, 0x33, 0x33, 0x44, 0xdf, 0xc2, 0x96, 0x92, 0x64, 0x34, 0xa6, 0x13, 0xc2, 0x9c, - 0xe6, 0xa3, 0xba, 0xa0, 0x3a, 0x52, 0x54, 0x46, 0xe1, 0xa1, 0x12, 0xed, 0x4c, 0x20, 0xf0, 0xa6, - 0x82, 0x27, 0x63, 0xe6, 0xfd, 0x0a, 0x2d, 0x43, 0xef, 0xf5, 0xa0, 0xa9, 0x82, 0x47, 0x9b, 0x60, - 0x5f, 0x0d, 0x5e, 0x0e, 0x5e, 0xfd, 0x3c, 0xd8, 0x7d, 0x80, 0x5a, 0xd0, 0x18, 0x9c, 0xfe, 0xd0, - 0xdf, 0xb5, 0xd0, 0x1e, 0x6c, 0x5f, 0x9e, 0x0e, 0x7f, 0x1c, 0xe1, 0xfe, 0x65, 0xff, 0x74, 0xd8, - 0x7f, 0xbe, 0x5b, 0xf3, 0x3e, 0x86, 0x76, 0x1a, 0x15, 0xb2, 0xa1, 0x7e, 0x3a, 0x3c, 0x53, 0x5b, - 0x9e, 0xf7, 0xc5, 0xc8, 0xf2, 0xfe, 0xb2, 0x60, 0x3f, 0x5b, 0x04, 0x36, 0xa3, 0x11, 0x23, 0x49, - 0x15, 0xc6, 0x74, 0x1e, 0xa5, 0x55, 0x90, 0x13, 0x84, 0xa0, 0x11, 0x91, 0x3f, 0x4c, 0x0d, 0xe4, - 0x38, 0x41, 0x72, 0xca, 0xfd, 0x50, 0xea, 0x2f, 0x90, 0x72, 0x82, 0xbe, 0x86, 0x96, 0x4e, 0x8e, - 0x09, 0x65, 0xeb, 0xc7, 0x9b, 0xbd, 0x83, 0x6c, 0xca, 0xda, 0x23, 0x4e, 0x61, 0xde, 0x39, 0x74, - 0xce, 0x89, 0x89, 0x44, 0x29, 0x62, 0xce, 0x44, 0xe2, 0xd7, 0xbf, 0x25, 0x32, 0x98, 0xc4, 0xaf, - 0x18, 0x23, 0x07, 0x6c, 0x7d, 0xa0, 0x64, 0x38, 0x1b, 0xd8, 0x4c, 0x3d, 0x0e, 0xce, 0x2a, 0x91, - 0xce, 0xab, 0x88, 0xe9, 0x33, 0x68, 0x24, 0xc7, 0x59, 0xd2, 0x6c, 0xf6, 0x50, 0x36, 0xce, 0x0b, - 0x61, 0xc1, 0xd2, 0x8e, 0x3e, 0x84, 0x76, 0x82, 0x67, 0x33, 0x7f, 0x4c, 0x64, 0xb6, 0x6d, 0x7c, - 0xbf, 0xe0, 0xbd, 0x58, 0xf6, 0x7a, 0x46, 0x23, 0x4e, 0x22, 0xbe, 0x5e, 0xfc, 0x97, 0x70, 0x54, - 0xc0, 0xa4, 0x13, 0x38, 0x01, 0x5b, 0x87, 0x26, 0xd9, 0x4a, 0x75, 0x35, 0x28, 0xef, 0x5f, 0x51, - 0xe2, 0xab, 0xd9, 0xc4, 0xe7, 0xc4, 0x98, 0xde, 0x12, 0xd4, 0x63, 0x51, 0xf6, 0xa4, 0x2d, 0x68, - 0x2d, 0xf6, 0x14, 0xb7, 0xea, 0x1d, 0x67, 0xc9, 0x2f, 0x56, 0x76, 0xf4, 0x04, 0x9a, 0x0b, 0x3f, - 0x14, 0x3c, 0x52, 0x88, 0x54, 0x35, 0x8d, 0x94, 0x3d, 0x05, 0x6b, 0x04, 0xea, 0x80, 0x3d, 0x89, - 0xef, 0x46, 0xf1, 0x3c, 0x92, 0x97, 0xac, 0x85, 0x9b, 0x62, 0x8a, 0xe7, 0x11, 0xfa, 0x04, 0xb6, - 0x27, 0x01, 0xf3, 0xaf, 0x43, 0x32, 0xba, 0xa1, 0xf4, 0x0d, 0x93, 0xf7, 0xac, 0x85, 0xb7, 0xf4, - 0xe2, 0x8b, 0x64, 0x4d, 0xe8, 0x7a, 0x90, 0x0b, 0x7f, 0x5d, 0x25, 0xfe, 0xb4, 0xe0, 0x10, 0xd3, - 0x30, 0xbc, 0xf6, 0xc7, 0x6f, 0x2a, 0x68, 0xb1, 0x14, 0x76, 0xed, 0xed, 0x61, 0xd7, 0x57, 0xc3, - 0x5e, 0x2e, 0x6f, 0x23, 0x5b, 0xde, 0xef, 0xa1, 0xb3, 0x12, 0xc5, 0xba, 0x29, 0xfd, 0x67, 0xc1, - 0xc1, 0x45, 0x24, 0x3a, 0x46, 0x18, 0xe6, 0x32, 0x4a, 0x2b, 0x69, 0x55, 0xae, 0x64, 0xed, 0x7d, - 0x2a, 0x59, 0xcf, 0x48, 0x62, 0xf4, 0x6b, 0x2c, 0xe9, 0x57, 0xa5, 0xba, 0xd9, 0x3b, 0xd5, 0xcc, - 0xdd, 0x29, 0xf4, 0x11, 0x40, 0x4c, 0xe6, 0x8c, 0x8c, 0x24, 0xb9, 0x2d, 0xf7, 0xb7, 0xe5, 0xca, - 0x40, 0x2c, 0x78, 0x17, 0x70, 0x98, 0x4f, 0x7e, 0x5d, 0x21, 0x6f, 0xa0, 0x73, 0x15, 0x05, 0x85, - 0x4a, 0x16, 0x9d, 0x8d, 0x95, 0xdc, 0x6a, 0x05, 0xb9, 0x89, 0xce, 0x38, 0x9b, 0xc7, 0xaf, 0x89, - 0xd6, 0x4a, 0x4d, 0xbc, 0x97, 0xe0, 0xac, 0x7a, 0x5a, 0x37, 0xec, 0x87, 0xb0, 0x27, 0x5a, 0xc5, - 0x4f, 0xea, 0x64, 0xe9, 0x80, 0xbd, 0x3e, 0xa0, 0xe5, 0xc5, 0x7b, 0x6e, 0xbd, 0x94, 0xe5, 0x36, - 0xaf, 0xb2, 0xc1, 0x1b, 0x54, 0xef, 0x6f, 0x1b, 0x76, 0x4c, 0x13, 0x55, 0x4f, 0x1e, 0x0a, 0x60, - 0x6b, 0xf9, 0xb5, 0x40, 0x9f, 0x97, 0xbf, 0x88, 0xb9, 0x67, 0xdd, 0x7d, 0x52, 0x05, 0xaa, 0x42, - 0xf5, 0x1e, 0x7c, 0x65, 0x21, 0x06, 0xbb, 0xf9, 0x26, 0x8e, 0x9e, 0x16, 0x73, 0x94, 0xbc, 0x1a, - 0x6e, 0xb7, 0x2a, 0xdc, 0xb8, 0x45, 0x0b, 0x29, 0x67, 0xb6, 0xf3, 0xa2, 0x77, 0xd2, 0x64, 0x9b, - 0xbd, 0x7b, 0x52, 0x19, 0x9f, 0xfa, 0xfd, 0x0d, 0xb6, 0x33, 0x3d, 0x0e, 0x95, 0xa8, 0x55, 0xd4, - 0xc7, 0xdd, 0x2f, 0x2a, 0x61, 0x53, 0x5f, 0xb7, 0xb0, 0x93, 0xbd, 0x34, 0xa8, 0x84, 0xa0, 0xb0, - 0xaf, 0xb8, 0x5f, 0x56, 0x03, 0xa7, 0xee, 0x44, 0x1d, 0xf3, 0xc7, 0xbd, 0xac, 0x8e, 0x25, 0x17, - 0xb0, 0xac, 0x8e, 0x65, 0xb7, 0x48, 0x38, 0xf5, 0x01, 0xee, 0x6f, 0x00, 0x7a, 0x5c, 0x5a, 0x90, - 0xec, 0xc5, 0x71, 0x8f, 0xdf, 0x0d, 0x4c, 0x5d, 0xcc, 0xe0, 0x83, 0x5c, 0x17, 0x47, 0x25, 0xd2, - 0x14, 0x3f, 0x39, 0xee, 0xd3, 0x8a, 0x68, 0xe3, 0xf1, 0x19, 0xfc, 0xd2, 0x32, 0xe0, 0xeb, 0xa6, - 0xfc, 0x0f, 0xfd, 0xcd, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0x76, 0x9b, 0x44, 0xa7, 0x14, 0x0c, - 0x00, 0x00, + // 997 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x57, 0x5f, 0x6f, 0xe3, 0x44, + 0x10, 0xaf, 0x93, 0x34, 0x7f, 0xa6, 0x7f, 0x68, 0xf7, 0xda, 0x26, 0xb5, 0x00, 0x9d, 0x8c, 0xe0, + 0xca, 0xc1, 0xa5, 0x10, 0x9e, 0x90, 0x10, 0x52, 0xaf, 0x17, 0xb5, 0xe5, 0x4a, 0x4e, 0xda, 0x50, + 0x90, 0x78, 0x20, 0x72, 0x93, 0xcd, 0xd5, 0x9c, 0xeb, 0x0d, 0xde, 0x4d, 0x75, 0x7d, 0xe7, 0x85, + 0xaf, 0xc1, 0xf7, 0xe0, 0x3b, 0xf1, 0xce, 0x0b, 0xeb, 0xfd, 0xe3, 0xda, 0x8e, 0x9d, 0xf3, 0xe5, + 0x25, 0xde, 0xdd, 0x99, 0xfd, 0xcd, 0xcc, 0x6f, 0x76, 0x66, 0x5a, 0xb0, 0x6f, 0xdc, 0x99, 0x77, + 0xcc, 0x48, 0x78, 0xe7, 0x8d, 0x09, 0x3b, 0xe6, 0x9e, 0xef, 0x93, 0xb0, 0x3b, 0x0b, 0x29, 0xa7, + 0x68, 0x2f, 0x92, 0x75, 0x8d, 0xac, 0xab, 0x64, 0xf6, 0x81, 0xbc, 0x31, 0xbe, 0x71, 0x43, 0xae, + 0x7e, 0x95, 0xb6, 0xdd, 0x4e, 0x9e, 0xd3, 0x60, 0xea, 0xbd, 0xd6, 0x02, 0x65, 0x22, 0x24, 0x3e, + 0x71, 0x19, 0x31, 0xdf, 0xd4, 0x25, 0x23, 0xf3, 0x82, 0x29, 0xd5, 0x82, 0xc3, 0x94, 0x80, 0x71, + 0x97, 0xcf, 0x59, 0x0a, 0xef, 0x8e, 0x84, 0xcc, 0xa3, 0x81, 0xf9, 0x2a, 0x99, 0xf3, 0x77, 0x05, + 0x1e, 0x5d, 0x7a, 0x8c, 0x63, 0x75, 0x91, 0x61, 0xf2, 0xc7, 0x9c, 0x30, 0x8e, 0xf6, 0x60, 0xdd, + 0xf7, 0x6e, 0x3d, 0xde, 0xb1, 0x1e, 0x5b, 0x47, 0x55, 0xac, 0x36, 0xe8, 0x00, 0xea, 0x74, 0x3a, + 0x65, 0x84, 0x77, 0x2a, 0xe2, 0xb8, 0x85, 0xf5, 0x0e, 0x7d, 0x0f, 0x0d, 0x46, 0x43, 0x3e, 0xba, + 0xbe, 0xef, 0x54, 0x85, 0x60, 0xbb, 0xf7, 0x69, 0x37, 0x8f, 0x8a, 0x6e, 0x64, 0x69, 0x28, 0x14, + 0xbb, 0xd1, 0xcf, 0xf3, 0x7b, 0x5c, 0x67, 0xf2, 0x1b, 0xe1, 0x4e, 0x3d, 0x9f, 0x93, 0xb0, 0x53, + 0x53, 0xb8, 0x6a, 0x87, 0xce, 0x00, 0x24, 0x2e, 0x0d, 0x27, 0x42, 0xb6, 0x2e, 0xa1, 0x8f, 0x4a, + 0x40, 0xbf, 0x8a, 0xf4, 0x71, 0x8b, 0x99, 0x25, 0xfa, 0x0e, 0x36, 0x15, 0x25, 0xa3, 0x31, 0x9d, + 0x10, 0xd6, 0xa9, 0x3f, 0xae, 0x0a, 0xa8, 0x43, 0x05, 0x65, 0x18, 0x1e, 0x2a, 0xd2, 0x4e, 0x85, + 0x06, 0xde, 0x50, 0xea, 0xd1, 0x9a, 0x39, 0xbf, 0x41, 0xd3, 0xc0, 0x3b, 0x3d, 0xa8, 0x2b, 0xe7, + 0xd1, 0x06, 0x34, 0xae, 0x06, 0x2f, 0x07, 0xaf, 0x7e, 0x19, 0xec, 0xac, 0xa1, 0x26, 0xd4, 0x06, + 0x27, 0x3f, 0xf6, 0x77, 0x2c, 0xb4, 0x0b, 0x5b, 0x97, 0x27, 0xc3, 0x9f, 0x46, 0xb8, 0x7f, 0xd9, + 0x3f, 0x19, 0xf6, 0x5f, 0xec, 0x54, 0x9c, 0x8f, 0xa1, 0x15, 0x7b, 0x85, 0x1a, 0x50, 0x3d, 0x19, + 0x9e, 0xaa, 0x2b, 0x2f, 0xfa, 0x62, 0x65, 0x39, 0x7f, 0x59, 0xb0, 0x97, 0x4e, 0x02, 0x9b, 0xd1, + 0x80, 0x91, 0x28, 0x0b, 0x63, 0x3a, 0x0f, 0xe2, 0x2c, 0xc8, 0x0d, 0x42, 0x50, 0x0b, 0xc8, 0x5b, + 0x93, 0x03, 0xb9, 0x8e, 0x34, 0x39, 0xe5, 0xae, 0x2f, 0xf9, 0x17, 0x9a, 0x72, 0x83, 0xbe, 0x86, + 0xa6, 0x0e, 0x8e, 0x09, 0x66, 0xab, 0x47, 0x1b, 0xbd, 0xfd, 0x74, 0xc8, 0xda, 0x22, 0x8e, 0xd5, + 0x9c, 0x33, 0x68, 0x9f, 0x11, 0xe3, 0x89, 0x62, 0xc4, 0xbc, 0x89, 0xc8, 0xae, 0x7b, 0x4b, 0xa4, + 0x33, 0x91, 0x5d, 0xb1, 0x46, 0x1d, 0x68, 0xe8, 0x07, 0x25, 0xdd, 0x59, 0xc7, 0x66, 0xeb, 0x70, + 0xe8, 0x2c, 0x02, 0xe9, 0xb8, 0xf2, 0x90, 0x3e, 0x83, 0x5a, 0xf4, 0x9c, 0x25, 0xcc, 0x46, 0x0f, + 0xa5, 0xfd, 0xbc, 0x10, 0x12, 0x2c, 0xe5, 0xe8, 0x43, 0x68, 0x45, 0xfa, 0x6c, 0xe6, 0x8e, 0x89, + 0x8c, 0xb6, 0x85, 0x1f, 0x0e, 0x9c, 0xf3, 0xa4, 0xd5, 0x53, 0x1a, 0x70, 0x12, 0xf0, 0xd5, 0xfc, + 0xbf, 0x84, 0xc3, 0x1c, 0x24, 0x1d, 0xc0, 0x31, 0x34, 0xb4, 0x6b, 0x12, 0xad, 0x90, 0x57, 0xa3, + 0xe5, 0xfc, 0x23, 0x52, 0x7c, 0x35, 0x9b, 0xb8, 0x9c, 0x18, 0xd1, 0x12, 0xa7, 0x9e, 0x88, 0xb4, + 0x47, 0x6d, 0x41, 0x73, 0xb1, 0xab, 0xb0, 0x55, 0xef, 0x38, 0x8d, 0x7e, 0xb1, 0x92, 0xa3, 0xa7, + 0x50, 0xbf, 0x73, 0x7d, 0x81, 0x23, 0x89, 0x88, 0x59, 0xd3, 0x9a, 0xb2, 0xa7, 0x60, 0xad, 0x81, + 0xda, 0xd0, 0x98, 0x84, 0xf7, 0xa3, 0x70, 0x1e, 0xc8, 0x22, 0x6b, 0xe2, 0xba, 0xd8, 0xe2, 0x79, + 0x80, 0x3e, 0x81, 0xad, 0x89, 0xc7, 0xdc, 0x6b, 0x9f, 0x8c, 0x6e, 0x28, 0x7d, 0xc3, 0x64, 0x9d, + 0x35, 0xf1, 0xa6, 0x3e, 0x3c, 0x8f, 0xce, 0x04, 0xaf, 0xfb, 0x19, 0xf7, 0x57, 0x65, 0xe2, 0x4f, + 0x0b, 0x0e, 0x30, 0xf5, 0xfd, 0x6b, 0x77, 0xfc, 0xa6, 0x04, 0x17, 0x09, 0xb7, 0x2b, 0xcb, 0xdd, + 0xae, 0x2e, 0xba, 0x9d, 0x4c, 0x6f, 0x2d, 0x9d, 0xde, 0x1f, 0xa0, 0xbd, 0xe0, 0xc5, 0xaa, 0x21, + 0xfd, 0x67, 0xc1, 0xfe, 0x45, 0x20, 0x3a, 0x86, 0xef, 0x67, 0x22, 0x8a, 0x33, 0x69, 0x95, 0xce, + 0x64, 0xe5, 0x7d, 0x32, 0x59, 0x4d, 0x51, 0x62, 0xf8, 0xab, 0x25, 0xf8, 0x2b, 0x93, 0xdd, 0x74, + 0x4d, 0xd5, 0x33, 0x35, 0x85, 0x3e, 0x02, 0x08, 0xc9, 0x9c, 0x91, 0x91, 0x04, 0x6f, 0xc8, 0xfb, + 0x2d, 0x79, 0x32, 0x10, 0x07, 0xce, 0x05, 0x1c, 0x64, 0x83, 0x5f, 0x95, 0xc8, 0x1b, 0x68, 0x5f, + 0x05, 0x5e, 0x2e, 0x93, 0x79, 0x6f, 0x63, 0x21, 0xb6, 0x4a, 0x4e, 0x6c, 0xa2, 0x33, 0xce, 0xe6, + 0xe1, 0x6b, 0xa2, 0xb9, 0x52, 0x1b, 0xe7, 0x25, 0x74, 0x16, 0x2d, 0xad, 0xea, 0xf6, 0x23, 0xd8, + 0x15, 0xad, 0xe2, 0x67, 0xf5, 0xb2, 0xb4, 0xc3, 0x4e, 0x1f, 0x50, 0xf2, 0xf0, 0x01, 0x5b, 0x1f, + 0xa5, 0xb1, 0xcd, 0x54, 0x36, 0xfa, 0xf1, 0x3b, 0xfd, 0x56, 0x62, 0x9f, 0x8b, 0xe9, 0x40, 0x45, + 0x92, 0x97, 0x90, 0xb1, 0x03, 0xd5, 0x5b, 0xf7, 0xad, 0xee, 0x62, 0xd1, 0x52, 0xb4, 0x72, 0x94, + 0xbc, 0xaa, 0x3d, 0x48, 0xce, 0x04, 0xab, 0xd4, 0x4c, 0xe8, 0xfd, 0xdb, 0x80, 0x6d, 0xd3, 0xc8, + 0xd5, 0xd8, 0x45, 0x1e, 0x6c, 0x26, 0x27, 0x16, 0xfa, 0xbc, 0x78, 0x2a, 0x67, 0xfe, 0xb4, 0xb0, + 0x9f, 0x96, 0x51, 0x55, 0xce, 0x3a, 0x6b, 0x5f, 0x59, 0x88, 0xc1, 0x4e, 0x76, 0x90, 0xa0, 0x67, + 0xf9, 0x18, 0x05, 0x93, 0xcb, 0xee, 0x96, 0x55, 0x37, 0x66, 0xd1, 0x9d, 0xa4, 0x3d, 0xdd, 0xfd, + 0xd1, 0x3b, 0x61, 0xd2, 0x03, 0xc7, 0x3e, 0x2e, 0xad, 0x1f, 0xdb, 0xfd, 0x1d, 0xb6, 0x52, 0x7d, + 0x16, 0x15, 0xb0, 0x95, 0x37, 0x4b, 0xec, 0x2f, 0x4a, 0xe9, 0xc6, 0xb6, 0x6e, 0x61, 0x3b, 0x5d, + 0xb8, 0xa8, 0x00, 0x20, 0xb7, 0xb7, 0xd9, 0x5f, 0x96, 0x53, 0x8e, 0xcd, 0x89, 0x3c, 0x66, 0x4b, + 0xae, 0x28, 0x8f, 0x05, 0x4d, 0xa0, 0x28, 0x8f, 0x45, 0x95, 0x2c, 0x8c, 0xba, 0x00, 0x0f, 0x55, + 0x88, 0x9e, 0x14, 0x26, 0x24, 0x5d, 0xbc, 0xf6, 0xd1, 0xbb, 0x15, 0x63, 0x13, 0x33, 0xf8, 0x20, + 0x33, 0x49, 0x50, 0x01, 0x35, 0xf9, 0x63, 0xcf, 0x7e, 0x56, 0x52, 0x3b, 0x13, 0x94, 0x2e, 0xec, + 0x25, 0x41, 0xa5, 0xbb, 0xc6, 0x92, 0xa0, 0x32, 0x3d, 0xc2, 0x59, 0x7b, 0x0e, 0xbf, 0x36, 0x8d, + 0xde, 0x75, 0x5d, 0xfe, 0xab, 0xf0, 0xcd, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0xdb, 0x8a, 0x4d, + 0xa4, 0xfb, 0x0c, 0x00, 0x00, } diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index ba55062a9..a7ee01fd5 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -126,6 +126,18 @@ func (s *Storage) Deployed(name string) (*rspb.Release, error) { } } +// History returns the revision history for the release with the provided name, or +// returns ErrReleaseNotFound if no such release name exists. +func (s *Storage) History(name string) ([]*rspb.Release, error) { + log.Printf("Getting release history for '%s'\n", name) + + l, err := s.Driver.Query(map[string]string{"NAME": name, "OWNER": "TILLER"}) + if err != nil { + return nil, err + } + return l, nil +} + // makeKey concatenates a release name and version into // a string with format ```#v```. // This key is used to uniquely identify storage objects. diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go index 42151b418..5935b4e88 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/storage/storage_test.go @@ -186,6 +186,37 @@ func TestStorageDeployed(t *testing.T) { } } +func TestStorageHistory(t *testing.T) { + storage := Init(driver.NewMemory()) + + const name = "angry-bird" + + // setup storage with test releases + setup := func() { + // release records + rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.Status_SUPERSEDED}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.Status_SUPERSEDED}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.Status_SUPERSEDED}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.Status_DEPLOYED}.ToRelease() + + // create the release records in the storage + assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") + assertErrNil(t.Fatal, storage.Create(rls1), "Storing release 'angry-bird' (v2)") + assertErrNil(t.Fatal, storage.Create(rls2), "Storing release 'angry-bird' (v3)") + assertErrNil(t.Fatal, storage.Create(rls3), "Storing release 'angry-bird' (v4)") + } + + setup() + + h, err := storage.History(name) + if err != nil { + t.Fatalf("Failed to query for release history (%q): %s\n", name, err) + } + if len(h) != 4 { + t.Fatalf("Release history (%q) is empty\n", name) + } +} + type ReleaseTestData struct { Name string Version int32 From 0ab6c4f9c6c44267221ebc77138383bfc37c0e5a Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Wed, 5 Oct 2016 17:40:02 -0600 Subject: [PATCH 143/183] fix(1246): pull most recent release from history --- cmd/tiller/release_server.go | 54 ++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 52e5e9b2d..a3bfb03e6 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -69,6 +69,8 @@ var ( errMissingChart = errors.New("no chart provided") // errMissingRelease indicates that a release (name) was not provided. errMissingRelease = errors.New("no release provided") + // errInvalidRevision indicates that an invalid release revision number was provided. + errInvalidRevision = errors.New("invalid release revision") // errIncompatibleVersion indicates incompatible client/server versions. errIncompatibleVersion = errors.New("client version is incompatible") ) @@ -463,54 +465,58 @@ func (s *releaseServer) performKubeUpdate(currentRelease, targetRelease *release // prepareRollback finds the previous release and prepares a new release object with // the previous release's configuration func (s *releaseServer) prepareRollback(req *services.RollbackReleaseRequest) (*release.Release, *release.Release, error) { - - if req.Name == "" { + switch { + case req.Name == "": return nil, nil, errMissingRelease + case req.Version < 0: + return nil, nil, errInvalidRevision } // finds the non-deleted release with the given name - currentRelease, err := s.env.Releases.Deployed(req.Name) + h, err := s.env.Releases.History(req.Name) if err != nil { return nil, nil, err } - - v := req.Version - if v == 0 { - v = currentRelease.Version - 1 + if len(h) <= 1 { + return nil, nil, errors.New("no revision to rollback") } - if v < 1 { - return nil, nil, errors.New("cannot rollback to version < 1") + + sort.Sort(sort.Reverse(byRev(h))) + crls := h[0] + + rbv := req.Version + if req.Version == 0 { + rbv = crls.Version - 1 } - log.Printf("rolling back %s to version %d", req.Name, v) + log.Printf("rolling back %s (current: v%d, target: v%d)", req.Name, crls.Version, rbv) - previousRelease, err := s.env.Releases.Get(req.Name, v) + prls, err := s.env.Releases.Get(req.Name, rbv) if err != nil { return nil, nil, err } - ts := timeconv.Now() - // Store a new release object with previous release's configuration - targetRelease := &release.Release{ + // Store a new release object with previous release's configuration + target := &release.Release{ Name: req.Name, - Namespace: currentRelease.Namespace, - Chart: previousRelease.Chart, - Config: previousRelease.Config, + Namespace: crls.Namespace, + Chart: prls.Chart, + Config: prls.Config, Info: &release.Info{ - FirstDeployed: currentRelease.Info.FirstDeployed, - LastDeployed: ts, + FirstDeployed: crls.Info.FirstDeployed, + LastDeployed: timeconv.Now(), Status: &release.Status{ Code: release.Status_UNKNOWN, - Notes: previousRelease.Info.Status.Notes, + Notes: prls.Info.Status.Notes, }, }, - Version: currentRelease.Version + 1, - Manifest: previousRelease.Manifest, - Hooks: previousRelease.Hooks, + Version: crls.Version + 1, + Manifest: prls.Manifest, + Hooks: prls.Hooks, } - return currentRelease, targetRelease, nil + return crls, target, nil } func (s *releaseServer) uniqName(start string, reuse bool) (string, error) { From 00938d2a6d1546dd5f6efd0b3a522f32963e99b8 Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Thu, 6 Oct 2016 11:24:38 -0600 Subject: [PATCH 144/183] fix(cmd/hist): print revision history with unix synergy --- cmd/helm/history.go | 9 +++++---- cmd/helm/history_test.go | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/helm/history.go b/cmd/helm/history.go index 44829de5a..e2c9eef43 100644 --- a/cmd/helm/history.go +++ b/cmd/helm/history.go @@ -38,10 +38,10 @@ The historical release set is printed as a formatted table, e.g: $ helm history angry-bird --max=4 REVISION UPDATED STATUS CHART - 4 Mon Oct 3 10:15:13 2016 DEPLOYED alpine-0.1.0 - 3 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 - 2 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 1 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 + 2 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 + 3 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 + 4 Mon Oct 3 10:15:13 2016 DEPLOYED alpine-0.1.0 ` type historyCmd struct { @@ -96,7 +96,8 @@ func formatHistory(rls []*release.Release) string { tbl := uitable.New() tbl.MaxColWidth = 30 tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART") - for _, r := range rls { + for i := len(rls) - 1; i >= 0; i-- { + r := rls[i] c := fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version) t := timeconv.String(r.Info.LastDeployed) s := r.Info.Status.Code.String() diff --git a/cmd/helm/history_test.go b/cmd/helm/history_test.go index d8d729096..6b3fab51e 100644 --- a/cmd/helm/history_test.go +++ b/cmd/helm/history_test.go @@ -50,7 +50,7 @@ func TestHistoryCmd(t *testing.T) { mk("angry-bird", 2, rpb.Status_SUPERSEDED), mk("angry-bird", 1, rpb.Status_SUPERSEDED), }, - xout: "REVISION\tUPDATED \tSTATUS \tCHART \n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n2 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n1 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n", + xout: "REVISION\tUPDATED \tSTATUS \tCHART \n1 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n2 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\n", }, { cmds: "helm history --max=MAX RELEASE_NAME", @@ -60,7 +60,7 @@ func TestHistoryCmd(t *testing.T) { mk("angry-bird", 4, rpb.Status_DEPLOYED), mk("angry-bird", 3, rpb.Status_SUPERSEDED), }, - xout: "REVISION\tUPDATED \tSTATUS \tCHART \n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n", + xout: "REVISION\tUPDATED \tSTATUS \tCHART \n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\n", }, } From 3830a06b1449bf144e9a8d718395d489bff0def3 Mon Sep 17 00:00:00 2001 From: "Keerthan Reddy Mala (kmala)" Date: Thu, 6 Oct 2016 14:09:18 -0600 Subject: [PATCH 145/183] fix(index): Append just the filename instead of full path to the url Signed-off-by: Keerthan Reddy Mala (kmala) --- pkg/repo/index.go | 3 ++- pkg/repo/index_test.go | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pkg/repo/index.go b/pkg/repo/index.go index a8589aa19..0c8a9436f 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -96,7 +96,8 @@ func NewIndexFile() *IndexFile { func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { u := filename if baseURL != "" { - u = baseURL + "/" + filename + _, file := filepath.Split(filename) + u = strings.TrimSuffix(baseURL, "/") + "/" + file } cr := &ChartVersion{ URLs: []string{u}, diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go index e5f5256a2..33203c495 100644 --- a/pkg/repo/index_test.go +++ b/pkg/repo/index_test.go @@ -270,3 +270,25 @@ func TestLoadUnversionedIndex(t *testing.T) { t.Fatalf("Expected 3 mysql versions, got %d", l) } } + +func TestIndexAdd(t *testing.T) { + + i := NewIndexFile() + i.Add(&chart.Metadata{Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890") + + if i.Entries["clipper"][0].URLs[0] != "http://example.com/charts/clipper-0.1.0.tgz" { + t.Errorf("Expected http://example.com/charts/clipper-0.1.0.tgz, got %s", i.Entries["clipper"][0].URLs[0]) + } + + i.Add(&chart.Metadata{Name: "alpine", Version: "0.1.0"}, "/home/charts/alpine-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890") + + if i.Entries["alpine"][0].URLs[0] != "http://example.com/charts/alpine-0.1.0.tgz" { + t.Errorf("Expected http://example.com/charts/alpine-0.1.0.tgz, got %s", i.Entries["alpine"][0].URLs[0]) + } + + i.Add(&chart.Metadata{Name: "deis", Version: "0.1.0"}, "/home/charts/deis-0.1.0.tgz", "http://example.com/charts/", "sha256:1234567890") + + if i.Entries["deis"][0].URLs[0] != "http://example.com/charts/deis-0.1.0.tgz" { + t.Errorf("Expected http://example.com/charts/deis-0.1.0.tgz, got %s", i.Entries["deis"][0].URLs[0]) + } +} From e6a9110ce078369203bf907b016be707d5c2ddd6 Mon Sep 17 00:00:00 2001 From: Kent Rancourt Date: Wed, 5 Oct 2016 18:52:39 -0400 Subject: [PATCH 146/183] docs(spelling): Fix some random misspellings --- docs/architecture.md | 2 +- docs/developers.md | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 272431eb7..1d5134f12 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -43,7 +43,7 @@ is responsible for the following domains: Helm client, and interfaces with the Kubernetes API server. The server is responsible for the following: -- Listing for incomming requests from the Helm client +- Listening for incoming requests from the Helm client - Combining a chart and configuration to build a release - Installing charts into Kubernetes, and then tracking the subsequent release diff --git a/docs/developers.md b/docs/developers.md index 5ebdc58fe..5f41ce045 100644 --- a/docs/developers.md +++ b/docs/developers.md @@ -30,7 +30,7 @@ To run Helm and Tiller locally, you can run `bin/helm` or `bin/tiller`. - Helm and Tiller are known to run on Mac OSX and most Linuxes, including Alpine. - Tiller must have access to a Kubernets cluster. It learns about the - cluster by examining the Kube config files that `kubectl` uese. + cluster by examining the Kube config files that `kubectl` uses. ## gRPC and Protobuf @@ -68,7 +68,7 @@ GCR registry. For development, we highly recommend using the [Kubernetes Minikube](https://github.com/kubernetes/minikube) -developer-oriented distribution. Once this is installed, you can use +developer-oriented distribution. Once this is installed, you can use `helm init` to install into the cluster. For developing on Tiller, it is sometimes more expedient to run Tiller locally @@ -186,4 +186,3 @@ Conventions: messages. - Deprecated RPCs, messages, and fields are marked deprecated in the comments (`// UpdateFoo DEPRECATED updates a foo.`). - From 5600b129efa290a35fade6ce6766108f277b0f7b Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Thu, 6 Oct 2016 16:07:22 -0600 Subject: [PATCH 147/183] fix(helm): resolve URLs and SemVers correctly The original dependency resolution did not correctly resolve version or URL of a dependency. Version was tracked by filename, and URL was assumed to be absolute. This fixes both of those. Closes #1277 --- cmd/helm/downloader/manager.go | 57 ++++++++++-- cmd/helm/downloader/manager_test.go | 87 +++++++++++++++++++ .../cache/kubernetes-charts-index.yaml | 9 +- .../repository/cache/testing-index.yaml | 1 + .../helmhome/repository/repositories.yaml | 2 + 5 files changed, 144 insertions(+), 12 deletions(-) create mode 100644 cmd/helm/downloader/manager_test.go create mode 100644 cmd/helm/downloader/testdata/helmhome/repository/cache/testing-index.yaml diff --git a/cmd/helm/downloader/manager.go b/cmd/helm/downloader/manager.go index 17ef1efd2..373714cee 100644 --- a/cmd/helm/downloader/manager.go +++ b/cmd/helm/downloader/manager.go @@ -22,10 +22,12 @@ import ( "io/ioutil" "net/url" "os" + "path" "path/filepath" "strings" "sync" + "github.com/Masterminds/semver" "github.com/ghodss/yaml" "k8s.io/helm/cmd/helm/helmpath" @@ -175,7 +177,7 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { for _, dep := range deps { fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) - churl, err := findChartURL(dep.Name, dep.Repository, repos) + churl, err := findChartURL(dep.Name, dep.Version, dep.Repository, repos) if err != nil { fmt.Fprintf(m.Out, "WARNING: %s (skipped)", err) continue @@ -270,21 +272,28 @@ func urlsAreEqual(a, b string) bool { // findChartURL searches the cache of repo data for a chart that has the name and the repourl specified. // -// In this current version, name is of the form 'foo-1.2.3'. This will change when -// the repository index stucture changes. -func findChartURL(name, repourl string, repos map[string]*repo.ChartRepository) (string, error) { +// 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the +// newest version will be returned. +// +// repourl is the repository to search +// +// If it finds a URL that is "relative", it will prepend the repourl. +func findChartURL(name, version, repourl string, repos map[string]*repo.ChartRepository) (string, error) { for _, cr := range repos { if urlsAreEqual(repourl, cr.URL) { for ename, entry := range cr.IndexFile.Entries { if ename == name { for _, verEntry := range entry { if len(verEntry.URLs) == 0 { - // Not totally sure what to do here. Returning an - // error is the strictest option. Skipping it might - // be preferable. - return "", fmt.Errorf("chart %q has no download URL", name) + // Not a legit entry. + continue } - return verEntry.URLs[0], nil + + if version == "" || versionEquals(version, verEntry.Version) { + return normalizeURL(repourl, verEntry.URLs[0]) + } + + return normalizeURL(repourl, verEntry.URLs[0]) } } } @@ -293,6 +302,36 @@ func findChartURL(name, repourl string, repos map[string]*repo.ChartRepository) return "", fmt.Errorf("chart %s not found in %s", name, repourl) } +func versionEquals(v1, v2 string) bool { + sv1, err := semver.NewVersion(v1) + if err != nil { + // Fallback to string comparison. + return v1 == v2 + } + sv2, err := semver.NewVersion(v2) + if err != nil { + return false + } + return sv1.Equal(sv2) +} + +func normalizeURL(baseURL, urlOrPath string) (string, error) { + u, err := url.Parse(urlOrPath) + if err != nil { + return urlOrPath, err + } + if u.IsAbs() { + return u.String(), nil + } + u2, err := url.Parse(baseURL) + if err != nil { + return urlOrPath, fmt.Errorf("Base URL failed to parse: %s", err) + } + + u2.Path = path.Join(u2.Path, urlOrPath) + return u2.String(), nil +} + // loadChartRepositories reads the repositories.yaml, and then builds a map of // ChartRepositories. // diff --git a/cmd/helm/downloader/manager_test.go b/cmd/helm/downloader/manager_test.go new file mode 100644 index 000000000..3910d1684 --- /dev/null +++ b/cmd/helm/downloader/manager_test.go @@ -0,0 +1,87 @@ +/* +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 downloader + +import ( + "bytes" + "testing" + + "k8s.io/helm/cmd/helm/helmpath" + //"k8s.io/helm/pkg/repo" +) + +func TestVersionEquals(t *testing.T) { + tests := []struct { + name, v1, v2 string + expect bool + }{ + {name: "semver match", v1: "1.2.3-beta.11", v2: "1.2.3-beta.11", expect: true}, + {name: "semver match, build info", v1: "1.2.3-beta.11+a", v2: "1.2.3-beta.11+b", expect: true}, + {name: "string match", v1: "abcdef123", v2: "abcdef123", expect: true}, + {name: "semver mismatch", v1: "1.2.3-beta.11", v2: "1.2.3-beta.22", expect: false}, + {name: "semver mismatch, invalid semver", v1: "1.2.3-beta.11", v2: "stinkycheese", expect: false}, + } + + for _, tt := range tests { + if versionEquals(tt.v1, tt.v2) != tt.expect { + t.Errorf("%s: failed comparison of %q and %q (expect equal: %t)", tt.name, tt.v1, tt.v2, tt.expect) + } + } +} + +func TestNormalizeURL(t *testing.T) { + tests := []struct { + name, base, path, expect string + }{ + {name: "basic URL", base: "https://example.com", path: "http://helm.sh/foo", expect: "http://helm.sh/foo"}, + {name: "relative path", base: "https://helm.sh/charts", path: "foo", expect: "https://helm.sh/charts/foo"}, + } + + for _, tt := range tests { + got, err := normalizeURL(tt.base, tt.path) + if err != nil { + t.Errorf("%s: error %s", tt.name, err) + continue + } else if got != tt.expect { + t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got) + } + } +} + +func TestFindChartURL(t *testing.T) { + b := bytes.NewBuffer(nil) + m := &Manager{ + Out: b, + HelmHome: helmpath.Home("testdata/helmhome"), + } + repos, err := m.loadChartRepositories() + if err != nil { + t.Fatal(err) + } + + name := "alpine" + version := "0.1.0" + repoURL := "http://example.com/charts" + + churl, err := findChartURL(name, version, repoURL, repos) + if err != nil { + t.Fatal(err) + } + if churl != "http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz" { + t.Errorf("Unexpected URL %q", churl) + } + +} diff --git a/cmd/helm/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml b/cmd/helm/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml index 26ce97423..ec7283685 100644 --- a/cmd/helm/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml +++ b/cmd/helm/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml @@ -2,7 +2,8 @@ apiVersion: v1 entries: alpine: - name: alpine - url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz + urls: + - http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d home: https://k8s.io/helm sources: @@ -14,7 +15,8 @@ entries: engine: "" icon: "" - name: alpine - url: http://storage.googleapis.com/kubernetes-charts/alpine-0.2.0.tgz + urls: + - http://storage.googleapis.com/kubernetes-charts/alpine-0.2.0.tgz checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d home: https://k8s.io/helm sources: @@ -27,7 +29,8 @@ entries: icon: "" mariadb: - name: mariadb - url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz + urls: + - http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56 home: https://mariadb.org sources: diff --git a/cmd/helm/downloader/testdata/helmhome/repository/cache/testing-index.yaml b/cmd/helm/downloader/testdata/helmhome/repository/cache/testing-index.yaml new file mode 100644 index 000000000..9cde8e8dd --- /dev/null +++ b/cmd/helm/downloader/testdata/helmhome/repository/cache/testing-index.yaml @@ -0,0 +1 @@ +apiVersion: v1 diff --git a/cmd/helm/downloader/testdata/helmhome/repository/repositories.yaml b/cmd/helm/downloader/testdata/helmhome/repository/repositories.yaml index 10d83457e..c7ddf316a 100644 --- a/cmd/helm/downloader/testdata/helmhome/repository/repositories.yaml +++ b/cmd/helm/downloader/testdata/helmhome/repository/repositories.yaml @@ -2,3 +2,5 @@ apiVersion: v1 repositories: - name: testing url: "http://example.com" + - name: kubernetes-charts + url: "http://example.com/charts" From eda3910361a25e41eaeb1c19e0405dbf1bea0cc9 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Thu, 6 Oct 2016 19:59:10 -0700 Subject: [PATCH 148/183] feat(ci): upload checksum with binary releases closes #1214 --- Makefile | 9 +++++++++ circle.yml | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 9599b80a7..fbed23716 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,15 @@ dist: $(DIST_DIRS) zip -r helm-${VERSION}-{}.zip {} \; \ ) +.PHONY: checksum +checksum: + ( \ + cd _dist && \ + for f in ./*.{tar.gz,zip} ; do \ + shasum -a 256 "$${f}" | awk '{print $$1}' > "$${f}.sha256" ; \ + done \ + ) + .PHONY: check-docker check-docker: @if [ -z $$(which docker) ]; then \ diff --git a/circle.yml b/circle.yml index dc4366df0..4434754d8 100644 --- a/circle.yml +++ b/circle.yml @@ -55,7 +55,7 @@ deployment: # build canary helm binaries and push - make build-cross - - make dist VERSION="${CIRCLE_TAG}" + - make dist checksum VERSION="${CIRCLE_TAG}" - sudo /opt/google-cloud-sdk/bin/gsutil cp ./_dist/* "gs://${PROJECT_NAME}" canary: @@ -74,5 +74,5 @@ deployment: # build canary helm binaries and push - make build-cross - - make dist VERSION=canary + - make dist checksum VERSION=canary - sudo /opt/google-cloud-sdk/bin/gsutil cp ./_dist/* "gs://${PROJECT_NAME}" From e8f1bf47700dda267761bbeb9b8585d1525773d6 Mon Sep 17 00:00:00 2001 From: Javier Cuevas Date: Fri, 7 Oct 2016 19:04:42 +0200 Subject: [PATCH 149/183] Fix typo in chart docs The name for the default values file is `values.yaml` and not `.values.yaml`. --- docs/charts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/charts.md b/docs/charts.md index c882ae81f..a9ae6fd5a 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -255,7 +255,7 @@ spec: The above example, based loosely on [https://github.com/deis/charts](https://github.com/deis/charts), is a template for a Kubernetes replication controller. It can use the following four template values (usually defined in a -`.values.yaml` file): +`values.yaml` file): - `imageRegistry`: The source registry for the Docker image. - `dockerTag`: The tag for the docker image. From d516631d67ad3b2a4c9280fdbc953945fc39541f Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Fri, 7 Oct 2016 11:13:14 -0700 Subject: [PATCH 150/183] feat(ci): cache glide directory --- circle.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/circle.yml b/circle.yml index 4434754d8..6baee102a 100644 --- a/circle.yml +++ b/circle.yml @@ -12,6 +12,9 @@ machine: - docker dependencies: + cache_directories: + - "${HOME}/.glide" + pre: # remove old go files - sudo rm -rf /usr/local/go From 1c6fc9c0e857c1876f310bbbd6678134a00566ef Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 5 Oct 2016 12:43:06 -0600 Subject: [PATCH 151/183] feat(helm): remove the requirement that fetch/install need version This removes the requirement that a fetch or install command must explicitly state the version number to install. Instead, this goes to the strategy used by OS package managers: Install the latest until told to do otherwise. Closes #1198 --- README.md | 2 + cmd/helm/downloader/chart_downloader.go | 97 ++++++++++++++----- cmd/helm/downloader/chart_downloader_test.go | 37 ++++++- cmd/helm/downloader/manager.go | 69 ++++++++----- cmd/helm/downloader/manager_test.go | 1 - .../repository/cache/testing-index.yaml | 29 ++++++ cmd/helm/fetch.go | 11 +-- cmd/helm/fetch_test.go | 30 ++++-- cmd/helm/inspect.go | 16 ++- cmd/helm/install.go | 52 +++++++--- cmd/helm/search.go | 5 +- cmd/helm/search_test.go | 2 +- cmd/helm/upgrade.go | 8 +- docs/quickstart.md | 23 +++-- docs/using_helm.md | 4 +- pkg/provenance/sign.go | 3 + pkg/provenance/sign_test.go | 3 + pkg/repo/index.go | 7 +- pkg/repo/repotest/server.go | 14 ++- 19 files changed, 307 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 6c624d098..b9589dcbc 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ Think of it like apt/yum/homebrew for Kubernetes. Download a [release tarball of helm for your platform](https://github.com/kubernetes/helm/releases). Unpack the `helm` binary and add it to your PATH and you are good to go! OS X/[Cask](https://caskroom.github.io/) users can `brew cask install helm`. +To rapidly get Helm up and running, start with the [Quick Start Guide](docs/quickstart.md). + See the [installation guide](docs/install.md) for more options, including installing pre-releases. diff --git a/cmd/helm/downloader/chart_downloader.go b/cmd/helm/downloader/chart_downloader.go index db8db771d..2871f1dea 100644 --- a/cmd/helm/downloader/chart_downloader.go +++ b/cmd/helm/downloader/chart_downloader.go @@ -24,6 +24,7 @@ import ( "net/http" "net/url" "os" + "path" "path/filepath" "strings" @@ -67,21 +68,24 @@ type ChartDownloader struct { // If Verify is set to VerifyAlways, this will return a verification or an error if the verification fails. // // For VerifyNever and VerifyIfPossible, the Verification may be empty. -func (c *ChartDownloader) DownloadTo(ref string, dest string) (*provenance.Verification, error) { +// +// Returns a string path to the location where the file was downloaded and a verification +// (if provenance was verified), or an error if something bad happened. +func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) { // resolve URL - u, err := c.ResolveChartVersion(ref) + u, err := c.ResolveChartVersion(ref, version) if err != nil { - return nil, err + return "", nil, err } data, err := download(u.String()) if err != nil { - return nil, err + return "", nil, err } name := filepath.Base(u.Path) destfile := filepath.Join(dest, name) if err := ioutil.WriteFile(destfile, data.Bytes(), 0655); err != nil { - return nil, err + return destfile, nil, err } // If provenance is requested, verify it. @@ -91,31 +95,40 @@ func (c *ChartDownloader) DownloadTo(ref string, dest string) (*provenance.Verif body, err := download(u.String() + ".prov") if err != nil { if c.Verify == VerifyAlways { - return ver, fmt.Errorf("Failed to fetch provenance %q", u.String()+".prov") + return destfile, ver, fmt.Errorf("Failed to fetch provenance %q", u.String()+".prov") } fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", ref, err) - return ver, nil + return destfile, ver, nil } provfile := destfile + ".prov" if err := ioutil.WriteFile(provfile, body.Bytes(), 0655); err != nil { - return nil, err + return destfile, nil, err } ver, err = VerifyChart(destfile, c.Keyring) if err != nil { // Fail always in this case, since it means the verification step // failed. - return ver, err + return destfile, ver, err } } - return ver, nil + return destfile, ver, nil } // ResolveChartVersion resolves a chart reference to a URL. // // A reference may be an HTTP URL, a 'reponame/chartname' reference, or a local path. -func (c *ChartDownloader) ResolveChartVersion(ref string) (*url.URL, error) { +// +// A version is a SemVer string (1.2.3-beta.1+f334a6789). +// +// - For fully qualified URLs, the version will be ignored (since URLs aren't versioned) +// - For a chart reference +// * If version is non-empty, this will return the URL for that version +// * If version is empty, this will return the URL for the latest version +// * If no version can be found, an error is returned +func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, error) { // See if it's already a full URL. + // FIXME: Why do we use url.ParseRequestURI instead of url.Parse? u, err := url.ParseRequestURI(ref) if err == nil { // If it has a scheme and host and path, it's a full URL @@ -131,22 +144,54 @@ func (c *ChartDownloader) ResolveChartVersion(ref string) (*url.URL, error) { } // See if it's of the form: repo/path_to_chart - p := strings.Split(ref, "/") - if len(p) > 1 { - rf, err := findRepoEntry(p[0], r.Repositories) - if err != nil { - return u, err - } - if rf.URL == "" { - return u, fmt.Errorf("no URL found for repository %q", p[0]) - } - baseURL := rf.URL - if !strings.HasSuffix(baseURL, "/") { - baseURL = baseURL + "/" - } - return url.ParseRequestURI(baseURL + strings.Join(p[1:], "/")) + p := strings.SplitN(ref, "/", 2) + if len(p) < 2 { + return u, fmt.Errorf("invalid chart url format: %s", ref) + } + + repoName := p[0] + chartName := p[1] + rf, err := findRepoEntry(repoName, r.Repositories) + if err != nil { + return u, err + } + if rf.URL == "" { + return u, fmt.Errorf("no URL found for repository %q", repoName) + } + + // Next, we need to load the index, and actually look up the chart. + i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(repoName)) + if err != nil { + return u, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err) + } + + cv, err := i.Get(chartName, version) + if err != nil { + return u, fmt.Errorf("chart %q not found in %s index. (try 'helm repo update'). %s", chartName, repoName, err) + } + + if len(cv.URLs) == 0 { + return u, fmt.Errorf("chart %q has no downloadable URLs", ref) + } + return url.Parse(cv.URLs[0]) +} + +// urlJoin joins a base URL to one or more path components. +// +// It's like filepath.Join for URLs. If the baseURL is pathish, this will still +// perform a join. +// +// If the URL is unparsable, this returns an error. +func urlJoin(baseURL string, paths ...string) (string, error) { + u, err := url.Parse(baseURL) + if err != nil { + return "", err } - return u, fmt.Errorf("invalid chart url format: %s", ref) + // We want path instead of filepath because path always uses /. + all := []string{u.Path} + all = append(all, paths...) + u.Path = path.Join(all...) + return u.String(), nil } func findRepoEntry(name string, repos []*repo.Entry) (*repo.Entry, error) { diff --git a/cmd/helm/downloader/chart_downloader_test.go b/cmd/helm/downloader/chart_downloader_test.go index c8bbfa8e6..545943304 100644 --- a/cmd/helm/downloader/chart_downloader_test.go +++ b/cmd/helm/downloader/chart_downloader_test.go @@ -30,12 +30,14 @@ import ( func TestResolveChartRef(t *testing.T) { tests := []struct { - name, ref, expect string - fail bool + name, ref, expect, version string + fail bool }{ {name: "full URL", ref: "http://example.com/foo-1.2.3.tgz", expect: "http://example.com/foo-1.2.3.tgz"}, {name: "full URL, HTTPS", ref: "https://example.com/foo-1.2.3.tgz", expect: "https://example.com/foo-1.2.3.tgz"}, - {name: "reference, testing repo", ref: "testing/foo-1.2.3.tgz", expect: "http://example.com/foo-1.2.3.tgz"}, + {name: "full URL, HTTPS, irrelevant version", ref: "https://example.com/foo-1.2.3.tgz", version: "0.1.0", expect: "https://example.com/foo-1.2.3.tgz"}, + {name: "reference, testing repo", ref: "testing/alpine", expect: "http://example.com/alpine-1.2.3.tgz"}, + {name: "reference, version, testing repo", ref: "testing/alpine", version: "0.2.0", expect: "http://example.com/alpine-0.2.0.tgz"}, {name: "full URL, file", ref: "file:///foo-1.2.3.tgz", fail: true}, {name: "invalid", ref: "invalid-1.2.3", fail: true}, {name: "not found", ref: "nosuchthing/invalid-1.2.3", fail: true}, @@ -47,7 +49,7 @@ func TestResolveChartRef(t *testing.T) { } for _, tt := range tests { - u, err := c.ResolveChartVersion(tt.ref) + u, err := c.ResolveChartVersion(tt.ref, tt.version) if err != nil { if tt.fail { continue @@ -132,12 +134,16 @@ func TestDownloadTo(t *testing.T) { Keyring: "testdata/helm-test-key.pub", } cname := "/signtest-0.1.0.tgz" - v, err := c.DownloadTo(srv.URL()+cname, dest) + where, v, err := c.DownloadTo(srv.URL()+cname, "", dest) if err != nil { t.Error(err) return } + if expect := filepath.Join(dest, cname); where != expect { + t.Errorf("Expected download to %s, got %s", expect, where) + } + if v.FileHash == "" { t.Error("File hash was empty, but verification is required.") } @@ -147,3 +153,24 @@ func TestDownloadTo(t *testing.T) { return } } + +func TestUrlJoin(t *testing.T) { + tests := []struct { + name, url, expect string + paths []string + }{ + {name: "URL, one path", url: "http://example.com", paths: []string{"hello"}, expect: "http://example.com/hello"}, + {name: "Long URL, one path", url: "http://example.com/but/first", paths: []string{"slurm"}, expect: "http://example.com/but/first/slurm"}, + {name: "URL, two paths", url: "http://example.com", paths: []string{"hello", "world"}, expect: "http://example.com/hello/world"}, + {name: "URL, no paths", url: "http://example.com", paths: []string{}, expect: "http://example.com"}, + {name: "basepath, two paths", url: "../example.com", paths: []string{"hello", "world"}, expect: "../example.com/hello/world"}, + } + + for _, tt := range tests { + if got, err := urlJoin(tt.url, tt.paths...); err != nil { + t.Errorf("%s: error %q", tt.name, err) + } else if got != tt.expect { + t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got) + } + } +} diff --git a/cmd/helm/downloader/manager.go b/cmd/helm/downloader/manager.go index 373714cee..c140504fc 100644 --- a/cmd/helm/downloader/manager.go +++ b/cmd/helm/downloader/manager.go @@ -184,7 +184,7 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { } dest := filepath.Join(m.ChartPath, "charts") - if _, err := dl.DownloadTo(churl, dest); err != nil { + if _, _, err := dl.DownloadTo(churl, "", dest); err != nil { fmt.Fprintf(m.Out, "WARNING: Could not download %s: %s (skipped)", churl, err) continue } @@ -270,36 +270,61 @@ func urlsAreEqual(a, b string) bool { return au.String() == bu.String() } -// findChartURL searches the cache of repo data for a chart that has the name and the repourl specified. +// findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified. // // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the // newest version will be returned. // -// repourl is the repository to search +// repoURL is the repository to search // -// If it finds a URL that is "relative", it will prepend the repourl. -func findChartURL(name, version, repourl string, repos map[string]*repo.ChartRepository) (string, error) { +// If it finds a URL that is "relative", it will prepend the repoURL. +func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (string, error) { for _, cr := range repos { - if urlsAreEqual(repourl, cr.URL) { - for ename, entry := range cr.IndexFile.Entries { - if ename == name { - for _, verEntry := range entry { - if len(verEntry.URLs) == 0 { - // Not a legit entry. - continue - } - - if version == "" || versionEquals(version, verEntry.Version) { - return normalizeURL(repourl, verEntry.URLs[0]) - } - - return normalizeURL(repourl, verEntry.URLs[0]) - } - } + if urlsAreEqual(repoURL, cr.URL) { + entry, err := findEntryByName(name, cr) + if err != nil { + return "", err + } + ve, err := findVersionedEntry(version, entry) + if err != nil { + return "", err } + + return normalizeURL(repoURL, ve.URLs[0]) } } - return "", fmt.Errorf("chart %s not found in %s", name, repourl) + return "", fmt.Errorf("chart %s not found in %s", name, repoURL) +} + +// findEntryByName finds an entry in the chart repository whose name matches the given name. +// +// It returns the ChartVersions for that entry. +func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) { + for ename, entry := range cr.IndexFile.Entries { + if ename == name { + return entry, nil + } + } + return nil, errors.New("entry not found") +} + +// findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints. +// +// If version is empty, the first chart found is returned. +func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) { + for _, verEntry := range vers { + if len(verEntry.URLs) == 0 { + // Not a legit entry. + continue + } + + if version == "" || versionEquals(version, verEntry.Version) { + return verEntry, nil + } + + return verEntry, nil + } + return nil, errors.New("no matching version") } func versionEquals(v1, v2 string) bool { diff --git a/cmd/helm/downloader/manager_test.go b/cmd/helm/downloader/manager_test.go index 3910d1684..b8b32e7d6 100644 --- a/cmd/helm/downloader/manager_test.go +++ b/cmd/helm/downloader/manager_test.go @@ -20,7 +20,6 @@ import ( "testing" "k8s.io/helm/cmd/helm/helmpath" - //"k8s.io/helm/pkg/repo" ) func TestVersionEquals(t *testing.T) { diff --git a/cmd/helm/downloader/testdata/helmhome/repository/cache/testing-index.yaml b/cmd/helm/downloader/testdata/helmhome/repository/cache/testing-index.yaml index 9cde8e8dd..4a46c7b8b 100644 --- a/cmd/helm/downloader/testdata/helmhome/repository/cache/testing-index.yaml +++ b/cmd/helm/downloader/testdata/helmhome/repository/cache/testing-index.yaml @@ -1 +1,30 @@ apiVersion: v1 +entries: + alpine: + - name: alpine + urls: + - http://example.com/alpine-1.2.3.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + home: https://k8s.io/helm + sources: + - https://github.com/kubernetes/helm + version: 1.2.3 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + engine: "" + icon: "" + - name: alpine + urls: + - http://example.com/alpine-0.2.0.tgz + - http://storage.googleapis.com/kubernetes-charts/alpine-0.2.0.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + home: https://k8s.io/helm + sources: + - https://github.com/kubernetes/helm + version: 0.2.0 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + engine: "" + icon: "" diff --git a/cmd/helm/fetch.go b/cmd/helm/fetch.go index 5491f5299..84acea792 100644 --- a/cmd/helm/fetch.go +++ b/cmd/helm/fetch.go @@ -49,6 +49,7 @@ type fetchCmd struct { untardir string chartRef string destdir string + version string verify bool keyring string @@ -81,6 +82,7 @@ func newFetchCmd(out io.Writer) *cobra.Command { f.BoolVar(&fch.untar, "untar", false, "If set to true, will untar the chart after downloading it.") f.StringVar(&fch.untardir, "untardir", ".", "If untar is specified, this flag specifies the name of the directory into which the chart is expanded.") f.BoolVar(&fch.verify, "verify", false, "Verify the package against its signature.") + f.StringVar(&fch.version, "version", "", "The specific version of a chart. Without this, the latest version is fetched.") f.StringVar(&fch.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.") f.StringVarP(&fch.destdir, "destination", "d", ".", "The location to write the chart. If this and tardir are specified, tardir is appended to this.") @@ -89,10 +91,6 @@ func newFetchCmd(out io.Writer) *cobra.Command { func (f *fetchCmd) run() error { pname := f.chartRef - if filepath.Ext(pname) != ".tgz" { - pname += ".tgz" - } - c := downloader.ChartDownloader{ HelmHome: helmpath.Home(homePath()), Out: f.out, @@ -116,7 +114,7 @@ func (f *fetchCmd) run() error { defer os.RemoveAll(dest) } - v, err := c.DownloadTo(pname, dest) + saved, v, err := c.DownloadTo(pname, f.version, dest) if err != nil { return err } @@ -140,8 +138,7 @@ func (f *fetchCmd) run() error { return fmt.Errorf("Failed to untar: %s is not a directory", ud) } - from := filepath.Join(dest, filepath.Base(pname)) - return chartutil.ExpandFile(ud, from) + return chartutil.ExpandFile(ud, saved) } return nil } diff --git a/cmd/helm/fetch_test.go b/cmd/helm/fetch_test.go index b24d255e7..286537a7d 100644 --- a/cmd/helm/fetch_test.go +++ b/cmd/helm/fetch_test.go @@ -49,38 +49,51 @@ func TestFetchCmd(t *testing.T) { }{ { name: "Basic chart fetch", - chart: "test/signtest-0.1.0", + chart: "test/signtest", expectFile: "./signtest-0.1.0.tgz", }, + { + name: "Chart fetch with version", + chart: "test/signtest", + flags: []string{"--version", "0.1.0"}, + expectFile: "./signtest-0.1.0.tgz", + }, + { + name: "Fail chart fetch with non-existent version", + chart: "test/signtest", + flags: []string{"--version", "99.1.0"}, + fail: true, + failExpect: "no such chart", + }, { name: "Fail fetching non-existent chart", - chart: "test/nosuchthing-0.1.0", + chart: "test/nosuchthing", failExpect: "Failed to fetch", fail: true, }, { name: "Fetch and verify", - chart: "test/signtest-0.1.0", + chart: "test/signtest", flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"}, expectFile: "./signtest-0.1.0.tgz", }, { name: "Fetch and fail verify", - chart: "test/reqtest-0.1.0", + chart: "test/reqtest", flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"}, failExpect: "Failed to fetch provenance", fail: true, }, { name: "Fetch and untar", - chart: "test/signtest-0.1.0", + chart: "test/signtest", flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub", "--untar", "--untardir", "signtest"}, expectFile: "./signtest", expectDir: true, }, { name: "Fetch, verify, untar", - chart: "test/signtest-0.1.0", + chart: "test/signtest", flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub", "--untar", "--untardir", "signtest"}, expectFile: "./signtest", expectDir: true, @@ -93,8 +106,9 @@ func TestFetchCmd(t *testing.T) { if _, err := srv.CopyCharts("testdata/testcharts/*.tgz*"); err != nil { t.Fatal(err) } - - t.Logf("HELM_HOME=%s", homePath()) + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } for _, tt := range tests { outdir := filepath.Join(hh, "testout") diff --git a/cmd/helm/inspect.go b/cmd/helm/inspect.go index 8386fd1d0..dd15b7dc2 100644 --- a/cmd/helm/inspect.go +++ b/cmd/helm/inspect.go @@ -28,7 +28,8 @@ import ( ) const inspectDesc = ` -This command inspects a chart (directory, file, or URL) and displays information. +This command inspects a chart and displays information. It takes a chart reference +('stable/drupal'), a full path to a directory or packaged chart, or a URL. Inspect prints the contents of the Chart.yaml file and the values.yaml file. ` @@ -50,6 +51,7 @@ type inspectCmd struct { keyring string out io.Writer client helm.Interface + version string } const ( @@ -73,7 +75,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command { if err := checkArgsLength(len(args), "chart name"); err != nil { return err } - cp, err := locateChartPath(args[0], insp.verify, insp.keyring) + cp, err := locateChartPath(args[0], insp.version, insp.verify, insp.keyring) if err != nil { return err } @@ -88,7 +90,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command { Long: inspectValuesDesc, RunE: func(cmd *cobra.Command, args []string) error { insp.output = valuesOnly - cp, err := locateChartPath(args[0], insp.verify, insp.keyring) + cp, err := locateChartPath(args[0], insp.version, insp.verify, insp.keyring) if err != nil { return err } @@ -103,7 +105,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command { Long: inspectChartDesc, RunE: func(cmd *cobra.Command, args []string) error { insp.output = chartOnly - cp, err := locateChartPath(args[0], insp.verify, insp.keyring) + cp, err := locateChartPath(args[0], insp.version, insp.verify, insp.keyring) if err != nil { return err } @@ -125,6 +127,12 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command { valuesSubCmd.Flags().StringVar(&insp.keyring, kflag, kdefault, kdesc) chartSubCmd.Flags().StringVar(&insp.keyring, kflag, kdefault, kdesc) + verflag := "version" + verdesc := "the version of the chart. By default, the newest chart is shown." + inspectCommand.Flags().StringVar(&insp.version, verflag, "", verdesc) + valuesSubCmd.Flags().StringVar(&insp.version, verflag, "", verdesc) + chartSubCmd.Flags().StringVar(&insp.version, verflag, "", verdesc) + inspectCommand.AddCommand(valuesSubCmd) inspectCommand.AddCommand(chartSubCmd) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 648c2b5ea..d53dd7bd2 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -48,11 +48,11 @@ name of a chart in the current working directory. To override values in a chart, use either the '--values' flag and pass in a file or use the '--set' flag and pass configuration from the command line. - $ helm install -f myvalues.yaml redis + $ helm install -f myvalues.yaml ./redis or - $ helm install --set name=prod redis + $ helm install --set name=prod ./redis To check the generated manifests of a release without installing the chart, the '--debug' and '--dry-run' flags can be combined. This will still require a @@ -60,6 +60,26 @@ round-trip to the Tiller server. If --verify is set, the chart MUST have a provenance file, and the provenenace fall MUST pass all verification steps. + +There are four different ways you can express the chart you want to install: + +1. By chart reference: helm install stable/mariadb +2. By path to a packaged chart: helm install ./nginx-1.2.3.tgz +3. By path to an unpacked chart directory: helm install ./nginx +4. By absolute URL: helm install https://example.com/charts/nginx-1.2.3.tgz + +CHART REFERENCES + +A chart reference is a convenient way of reference a chart in a chart repository. + +When you use a chart reference ('stable/mariadb'), Helm will look in the local +configuration for a chart repository named 'stable', and will then look for a +chart in that repository whose name is 'mariadb'. It will install the latest +version of that chart unless you also supply a version number with the +'--version' flag. + +To see the list of chart repositories, use 'helm repo list'. To search for +charts in a repository, use 'helm search'. ` type installCmd struct { @@ -76,6 +96,7 @@ type installCmd struct { client helm.Interface values *values nameTemplate string + version string } func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { @@ -94,7 +115,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { if err := checkArgsLength(len(args), "chart name"); err != nil { return err } - cp, err := locateChartPath(args[0], inst.verify, inst.keyring) + cp, err := locateChartPath(args[0], inst.version, inst.verify, inst.keyring) if err != nil { return err } @@ -116,6 +137,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { f.StringVar(&inst.nameTemplate, "name-template", "", "specify template used to name the release") f.BoolVar(&inst.verify, "verify", false, "verify the package before installing it") f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "location of public keys used for verification") + f.StringVar(&inst.version, "version", "", "specify the exact chart version to install. If this is not specified, the latest version is installed.") return cmd } @@ -276,9 +298,12 @@ func splitPair(item string) (name string, value interface{}) { // - current working directory // - if path is absolute or begins with '.', error out here // - chart repos in $HELM_HOME +// - URL // // If 'verify' is true, this will attempt to also verify the chart. -func locateChartPath(name string, verify bool, keyring string) (string, error) { +func locateChartPath(name, version string, verify bool, keyring string) (string, error) { + name = strings.TrimSpace(name) + version = strings.TrimSpace(version) if fi, err := os.Stat(name); err == nil { abs, err := filepath.Abs(name) if err != nil { @@ -303,12 +328,6 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) { return filepath.Abs(crepo) } - // Try fetching the chart from a remote repo into a tmpdir - origname := name - if filepath.Ext(name) != ".tgz" { - name += ".tgz" - } - dl := downloader.ChartDownloader{ HelmHome: helmpath.Home(homePath()), Out: os.Stdout, @@ -318,16 +337,19 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) { dl.Verify = downloader.VerifyAlways } - if _, err := dl.DownloadTo(name, "."); err == nil { - lname, err := filepath.Abs(filepath.Base(name)) + filename, _, err := dl.DownloadTo(name, version, ".") + if err == nil { + lname, err := filepath.Abs(filename) if err != nil { - return lname, err + return filename, err } - fmt.Printf("Fetched %s to %s\n", origname, lname) + fmt.Printf("Fetched %s to %s\n", name, filename) return lname, nil + } else if flagDebug { + return filename, err } - return name, fmt.Errorf("file %q not found", origname) + return filename, fmt.Errorf("file %q not found", name) } func generateName(nameTemplate string) (string, error) { diff --git a/cmd/helm/search.go b/cmd/helm/search.go index 020b6fe5f..6028aa6c1 100644 --- a/cmd/helm/search.go +++ b/cmd/helm/search.go @@ -97,6 +97,9 @@ func (s *searchCmd) showAllCharts(i *search.Index) { } func (s *searchCmd) formatSearchResults(res []*search.Result) string { + if len(res) == 0 { + return "No results found" + } table := uitable.New() table.MaxColWidth = 50 table.AddRow("NAME", "VERSION", "DESCRIPTION") @@ -119,7 +122,7 @@ func (s *searchCmd) buildIndex() (*search.Index, error) { f := s.helmhome.CacheIndex(n) ind, err := repo.LoadIndexFile(f) if err != nil { - fmt.Fprintf(s.out, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update':\n\t%s\n", f, err) + fmt.Fprintf(s.out, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n) continue } diff --git a/cmd/helm/search_test.go b/cmd/helm/search_test.go index 999e79414..b81a3536d 100644 --- a/cmd/helm/search_test.go +++ b/cmd/helm/search_test.go @@ -50,7 +50,7 @@ func TestSearchCmd(t *testing.T) { { name: "search for 'syzygy', expect no matches", args: []string{"syzygy"}, - expect: "NAME\tVERSION\tDESCRIPTION", + expect: "No results found", }, { name: "search for 'alp[a-z]+', expect two matches", diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index b91efbbc6..d85bf8cfe 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -33,7 +33,9 @@ const upgradeDesc = ` This command upgrades a release to a new version of a chart. The upgrade arguments must be a release and a chart. The chart -argument can be a relative path to a packaged or unpackaged chart. +argument can a chart reference ('stable/mariadb'), a path to a chart directory +or packaged chart, or a fully qualified URL. For chart references, the latest +version will be specified unless the '--version' flag is set. To override values in a chart, use either the '--values' flag and pass in a file or use the '--set' flag and pass configuration from the command line. @@ -52,6 +54,7 @@ type upgradeCmd struct { keyring string install bool namespace string + version string } func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { @@ -89,12 +92,13 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { f.StringVar(&upgrade.keyring, "keyring", defaultKeyring(), "the path to the keyring that contains public singing keys") f.BoolVarP(&upgrade.install, "install", "i", false, "if a release by this name doesn't already exist, run an install") f.StringVar(&upgrade.namespace, "namespace", "default", "the namespace to install the release into (only used if --install is set)") + f.StringVar(&upgrade.version, "version", "", "specify the exact chart version to use. If this is not specified, the latest version is used.") return cmd } func (u *upgradeCmd) run() error { - chartPath, err := locateChartPath(u.chart, u.verify, u.keyring) + chartPath, err := locateChartPath(u.chart, u.version, u.verify, u.keyring) if err != nil { return err } diff --git a/docs/quickstart.md b/docs/quickstart.md index b9ecf846a..3266f9151 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -7,6 +7,11 @@ This guide covers how you can quickly get started using Helm. - You must have Kubernetes installed, and have a local configured copy of `kubectl`. +Helm will figure out where to install Tiller by reading your Kubernetes +configuration file (usually `$HOME/.kube/config`). This is the same file +that `kubectl` uses, so to find out which cluster Tiller would install +to, you can run `kubectl cluster-info`. + ## Install Helm Download a binary release of the Helm client from @@ -27,20 +32,19 @@ $ helm init ## Install an Example Chart -To install a chart, you can run the `helm install` command. -Let's use an example chart from this repository. +To install a chart, you can run the `helm install` command. +Let's use an example chart from this repository. Make sure you are in the root directory of this repo. ```console -$ helm install docs/examples/alpine +$ helm install stable/mysql Released smiling-penguin ``` -In the example above, the `alpine` chart was released, and the name of -our new release is `smiling-penguin`. You can view the details of the chart we just -installed by taking a look at the nginx chart in -[docs/examples/alpine/Chart.yaml](examples/alpine/Chart.yaml). +In the example above, the `stable/mysql` chart was released, and the name of +our new release is `smiling-penguin`. You get a simple idea of this +MySQL chart by running `helm inspect stable/mysql`. ## Change a Default Chart Value @@ -48,7 +52,7 @@ A nice feature of helm is the ability to change certain values of the package fo Let's install the `nginx` example from this repository but change the `replicaCount` to 7. ```console -$ helm install --set replicaCount=7 docs/examples/nginx +$ helm install --set replicaCount=7 ./docs/examples/nginx happy-panda ``` @@ -65,6 +69,9 @@ $ helm status smiling-penguin Status: DEPLOYED ``` +The `status` command will display information about a release in your +cluster. + ## Uninstall a Release To uninstall a release, use the `helm delete` command: diff --git a/docs/using_helm.md b/docs/using_helm.md index 155da24e5..5665bc5e4 100644 --- a/docs/using_helm.md +++ b/docs/using_helm.md @@ -181,7 +181,7 @@ To see what options are configurable on a chart, use `helm inspect values`: ```console -helm inspect values stable/mariadb-0.3.0.tgz +helm inspect values stable/mariadb Fetched stable/mariadb-0.3.0.tgz to /Users/mattbutcher/Code/Go/src/k8s.io/helm/mariadb-0.3.0.tgz ## Bitnami MariaDB image version ## ref: https://hub.docker.com/r/bitnami/mariadb/tags/ @@ -235,7 +235,7 @@ complex, Helm tries to perform the least invasive upgrade. It will only update things that have changed since the last release. ```console -$ helm upgrade -f panda.yaml happy-panda stable/mariadb-0.3.0.tgz 1 ↵ +$ helm upgrade -f panda.yaml happy-panda stable/mariadb Fetched stable/mariadb-0.3.0.tgz to /Users/mattbutcher/Code/Go/src/k8s.io/helm/mariadb-0.3.0.tgz happy-panda has been upgraded. Happy Helming! Last Deployed: Wed Sep 28 12:47:54 2016 diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go index ae4c6d2b6..90b14cdc5 100644 --- a/pkg/provenance/sign.go +++ b/pkg/provenance/sign.go @@ -60,6 +60,8 @@ type Verification struct { SignedBy *openpgp.Entity // FileHash is the hash, prepended with the scheme, for the file that was verified. FileHash string + // FileName is the name of the file that FileHash verifies. + FileName string } // Signatory signs things. @@ -221,6 +223,7 @@ func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) { return ver, fmt.Errorf("sha256 sum does not match for %s: %q != %q", basename, sha, sum) } ver.FileHash = sum + ver.FileName = basename // TODO: when image signing is added, verify that here. diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go index 00bc5aced..747a9376a 100644 --- a/pkg/provenance/sign_test.go +++ b/pkg/provenance/sign_test.go @@ -18,6 +18,7 @@ package provenance import ( "io/ioutil" "os" + "path/filepath" "strings" "testing" @@ -246,6 +247,8 @@ func TestVerify(t *testing.T) { t.Error("Verification is missing hash.") } else if ver.SignedBy == nil { t.Error("No SignedBy field") + } else if ver.FileName != filepath.Base(testChartfile) { + t.Errorf("FileName is unexpectedly %q", ver.FileName) } if _, err = signer.Verify(testChartfile, testTamperedSigBlock); err == nil { diff --git a/pkg/repo/index.go b/pkg/repo/index.go index 9fe08b7e0..5b591f7e5 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -138,7 +138,10 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) { if !ok { return nil, ErrNoChartName } - if version == "" && len(vs) > 0 { + if len(vs) == 0 { + return nil, ErrNoChartVersion + } + if len(version) == 0 { return vs[0], nil } for _, ver := range vs { @@ -147,7 +150,7 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) { return ver, nil } } - return nil, ErrNoChartVersion + return nil, fmt.Errorf("No chart version found for %s-%s", name, version) } // WriteFile writes an index file to the given destination path. diff --git a/pkg/repo/repotest/server.go b/pkg/repo/repotest/server.go index 9223ed0a3..8094a245c 100644 --- a/pkg/repo/repotest/server.go +++ b/pkg/repo/repotest/server.go @@ -123,8 +123,6 @@ func (s *Server) CreateIndex() error { return err } - println(string(d)) - ifile := filepath.Join(s.docroot, "index.yaml") return ioutil.WriteFile(ifile, d, 0755) } @@ -148,11 +146,23 @@ func (s *Server) URL() string { return s.srv.URL } +// LinkIndices links the index created with CreateIndex and makes a symboic link to the repositories/cache directory. +// +// This makes it possible to simulate a local cache of a repository. +func (s *Server) LinkIndices() error { + destfile := "test-index.yaml" + // Link the index.yaml file to the + lstart := filepath.Join(s.docroot, "index.yaml") + ldest := filepath.Join(s.docroot, "repository/cache", destfile) + return os.Symlink(lstart, ldest) +} + // setTestingRepository sets up a testing repository.yaml with only the given name/URL. func setTestingRepository(helmhome, name, url string) error { rf := repo.NewRepoFile() rf.Add(&repo.Entry{Name: name, URL: url}) os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755) dest := filepath.Join(helmhome, "repository/repositories.yaml") + return rf.WriteFile(dest, 0644) } From 50249923767ad008bde0bec611129468afa0b4f8 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Fri, 7 Oct 2016 11:36:05 -0700 Subject: [PATCH 152/183] fix(ci): glide cache path more money, more problems --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 6baee102a..b4d2500e8 100644 --- a/circle.yml +++ b/circle.yml @@ -13,7 +13,7 @@ machine: dependencies: cache_directories: - - "${HOME}/.glide" + - "~/.glide" pre: # remove old go files From 4de5c4aa578ed9ca173aff472f00d2167ec59a88 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Fri, 7 Oct 2016 12:28:50 -0700 Subject: [PATCH 153/183] chore(*): bump to v2.0.0-alpha.5 --- pkg/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/version/version.go b/pkg/version/version.go index 6da49d843..c6a934119 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -26,7 +26,7 @@ var ( // Increment major number for new feature additions and behavioral changes. // Increment minor number for bug fixes and performance enhancements. // Increment patch number for critical fixes to existing releases. - Version = "v2.0.0-alpha.4" + Version = "v2.0.0-alpha.5" // BuildMetadata is extra build time data BuildMetadata = "" From 7fa5f3155a1bcd63ffae807116da2e3a2834032e Mon Sep 17 00:00:00 2001 From: Javier Cuevas Date: Fri, 7 Oct 2016 23:17:03 +0200 Subject: [PATCH 154/183] Fix docs regarding .Files.GetString and .Files.GetBytes After #1021 ` .Files.GetString` does not exist any more. --- docs/charts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/charts.md b/docs/charts.md index c882ae81f..da09b3552 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -287,8 +287,8 @@ sensitive_. - `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. + or using the `{{.Files.Get name}}` or `{{.Files.GetString name}}` functions. You can + also access the contents of the file as `[]byte` using `{{.Files.GetBytes}}` **NOTE:** Any unknown Chart.yaml fields will be dropped. They will not be accessible inside of the `Chart` object. Thus, Chart.yaml cannot be From 150fb33c2e0d7ca86f1cda21bb37ab077a4c389d Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Fri, 7 Oct 2016 15:51:19 -0700 Subject: [PATCH 155/183] fix(*): prevent testing package from being imported Prevent testing package from being imported into main application --- pkg/storage/driver/{testing.go => mock_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pkg/storage/driver/{testing.go => mock_test.go} (100%) diff --git a/pkg/storage/driver/testing.go b/pkg/storage/driver/mock_test.go similarity index 100% rename from pkg/storage/driver/testing.go rename to pkg/storage/driver/mock_test.go From ac6477ccb9f0641e1ea33d0c97583f3b82f19de0 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Sat, 8 Oct 2016 10:11:14 -0700 Subject: [PATCH 156/183] fix(ci): fix ci builds reporting dirty repo ``` % ./helm version Client: &version.Version{SemVer:"v2.0.0-alpha.5", GitCommit:"4de...", GitTreeState:"dirty"} Server: &version.Version{SemVer:"v2.0.0-alpha.5", GitCommit:"4de...", GitTreeState:"dirty"} ``` --- circle.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/circle.yml b/circle.yml index b4d2500e8..db4bec459 100644 --- a/circle.yml +++ b/circle.yml @@ -22,8 +22,8 @@ dependencies: override: # install go - - wget "https://storage.googleapis.com/golang/go${GOVERSION}.linux-amd64.tar.gz" - - sudo tar -C /usr/local -xzf "go${GOVERSION}.linux-amd64.tar.gz" + - wget "https://storage.googleapis.com/golang/go${GOVERSION}.linux-amd64.tar.gz" -O "${HOME}/go${GOVERSION}.tar.gz" + - sudo tar -C /usr/local -xzf "${HOME}/go${GOVERSION}.tar.gz" # move repository to the canonical import path - mkdir -p "$(dirname ${WORKDIR})" From f333bc37a02961f070bb09026b27b8387ca08ae4 Mon Sep 17 00:00:00 2001 From: Ken Wronkiewicz Date: Sun, 9 Oct 2016 11:38:52 -0700 Subject: [PATCH 157/183] docs(install): Add reference to glide in install docs The user needs to get all of the way down to developers docs to realize they need glide, but if you follow a user who doesn't necessarily want to develop but wants to build from source, you'd follow this path: README -> Quick Start -> Releases -> . o O (Source doesn't work) -> Install And then you'd realize that you need Glide. This one-liner patch fixes that. :) --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index 43b67c1fc..6830c7b03 100644 --- a/docs/install.md +++ b/docs/install.md @@ -53,7 +53,7 @@ Here are links to the common builds: Building Helm from source is slightly more work, but is the best way to go if you want to test the latest (pre-release) Helm version. -You must have a working Go environment. +You must have a working Go environment with (https://github.com/Masterminds/glide)[glide] installed. ```console $ cd $GOPATH From 3c27a071ae7b587aeb561e11124653498b4b5af9 Mon Sep 17 00:00:00 2001 From: Ken Wronkiewicz Date: Sun, 9 Oct 2016 11:57:08 -0700 Subject: [PATCH 158/183] docs(install): Add reference to hg in install docs Also, hg is required. --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index 6830c7b03..5403d2802 100644 --- a/docs/install.md +++ b/docs/install.md @@ -53,7 +53,7 @@ Here are links to the common builds: Building Helm from source is slightly more work, but is the best way to go if you want to test the latest (pre-release) Helm version. -You must have a working Go environment with (https://github.com/Masterminds/glide)[glide] installed. +You must have a working Go environment with (https://github.com/Masterminds/glide)[glide]and Mercurial installed. ```console $ cd $GOPATH From 729f9b816fc5cf27d005021f011d1879037ecb98 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Mon, 10 Oct 2016 10:34:22 -0700 Subject: [PATCH 159/183] chore(ci): limit cross build targets Limit cross builds to linux/amd64 linux/386 darwin/amd64 --- Makefile | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index fbed23716..3a9e7e341 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ DOCKER_REGISTRY ?= gcr.io IMAGE_PREFIX ?= kubernetes-helm SHORT_NAME ?= tiller +TARGETS = darwin/amd64 linux/amd64 linux/386 +DIST_DIRS = find * -type d -exec # go option GO ?= go @@ -12,7 +14,6 @@ LDFLAGS := GOFLAGS := BINDIR := $(CURDIR)/bin BINARIES := helm tiller -DIST_DIRS := find * -type d -exec .PHONY: all all: build @@ -24,7 +25,7 @@ build: # usage: make build-cross dist VERSION=v2.0.0-alpha.3 .PHONY: build-cross build-cross: - gox -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -os="darwin linux windows" -arch="amd64 386" $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/helm + gox -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/helm .PHONY: dist dist: @@ -32,18 +33,15 @@ dist: cd _dist && \ $(DIST_DIRS) cp ../LICENSE {} \; && \ $(DIST_DIRS) cp ../README.md {} \; && \ - $(DIST_DIRS) tar -zcf helm-${VERSION}-{}.tar.gz {} \; && \ - $(DIST_DIRS) zip -r helm-${VERSION}-{}.zip {} \; \ + $(DIST_DIRS) tar -zcf helm-${GIT_TAG}-{}.tar.gz {} \; && \ + $(DIST_DIRS) zip -r helm-${GIT_TAG}-{}.zip {} \; \ ) .PHONY: checksum checksum: - ( \ - cd _dist && \ - for f in ./*.{tar.gz,zip} ; do \ - shasum -a 256 "$${f}" | awk '{print $$1}' > "$${f}.sha256" ; \ - done \ - ) + for f in _dist/*.{gz,zip} ; do \ + shasum -a 256 "$${f}" | awk '{print $$1}' > "$${f}.sha256" ; \ + done .PHONY: check-docker check-docker: From a85c37f2a4ad9f72450c8b5c95b97c82a6e9171d Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 10 Oct 2016 12:04:11 -0600 Subject: [PATCH 160/183] fix(tiller): correct sort manifests by type Closes #1313 --- cmd/tiller/hooks.go | 2 +- cmd/tiller/hooks_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/cmd/tiller/hooks.go b/cmd/tiller/hooks.go index 95e24a203..fab903749 100644 --- a/cmd/tiller/hooks.go +++ b/cmd/tiller/hooks.go @@ -165,5 +165,5 @@ func sortManifests(files map[string]string, apis versionSet, sort SortOrder) ([] } hs = append(hs, h) } - return hs, generic, nil + return hs, sortByKind(generic, sort), nil } diff --git a/cmd/tiller/hooks_test.go b/cmd/tiller/hooks_test.go index b43df6391..9ff4bf533 100644 --- a/cmd/tiller/hooks_test.go +++ b/cmd/tiller/hooks_test.go @@ -19,6 +19,8 @@ package main import ( "testing" + "github.com/ghodss/yaml" + "k8s.io/helm/pkg/proto/hapi/release" ) @@ -156,6 +158,28 @@ metadata: } } + // Verify the sort order + sorted := make([]manifest, len(data)) + for i, s := range data { + var sh simpleHead + err := yaml.Unmarshal([]byte(s.manifest), &sh) + if err != nil { + // This is expected for manifests that are corrupt or empty. + t.Log(err) + } + sorted[i] = manifest{ + content: s.manifest, + name: s.name, + head: &sh, + } + } + sorted = sortByKind(sorted, InstallOrder) + for i, m := range generic { + if m.content != sorted[i].content { + t.Errorf("Expected %q, got %q", m.content, sorted[i].content) + } + } + } func TestVersionSet(t *testing.T) { From b2b5dba355d1f8bad849a8b1f5db6c77515401e4 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 10 Oct 2016 12:59:06 -0600 Subject: [PATCH 161/183] docs(README): update download information --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b9589dcbc..6d9acdd32 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,20 @@ Think of it like apt/yum/homebrew for Kubernetes. ## Install -Download a [release tarball of helm for your platform](https://github.com/kubernetes/helm/releases). Unpack the `helm` binary and add it to your PATH and you are good to go! OS X/[Cask](https://caskroom.github.io/) users can `brew cask install helm`. +Binary downloads of the Alpha.5 Helm client can be found at the following links: + +- [OSX](http://storage.googleapis.com/kubernetes-helm/helm-v2.0.0-alpha.5-darwin-amd64.tar.gz) +- [Linux](http://storage.googleapis.com/kubernetes-helm/helm-v2.0.0-alpha.5-linux-amd64.tar.gz) +- [Linux 32-bit](http://storage.googleapis.com/kubernetes-helm/helm-v2.0.0-alpha.5-linux-386.tar.gz) + +Unpack the `helm` binary and add it to your PATH and you are good to go! OS X/[Cask](https://caskroom.github.io/) users can `brew cask install helm`. To rapidly get Helm up and running, start with the [Quick Start Guide](docs/quickstart.md). See the [installation guide](docs/install.md) for more options, including installing pre-releases. + ## Docs - [Quick Start](docs/quickstart.md) From 4979aa08274e49db825f661d58acf9e5f2d67590 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 10 Oct 2016 13:50:01 -0600 Subject: [PATCH 162/183] ref(repo): move urlJoin to the right package I wrote urlJoin to fix URL joining, but I put it in the wrong place and never used it. This moves it to the right place, and replaces the hacky previous solution. --- cmd/helm/downloader/chart_downloader.go | 19 -------------- cmd/helm/downloader/chart_downloader_test.go | 21 ---------------- pkg/repo/index.go | 26 +++++++++++++++++++- pkg/repo/index_test.go | 21 ++++++++++++++++ 4 files changed, 46 insertions(+), 41 deletions(-) diff --git a/cmd/helm/downloader/chart_downloader.go b/cmd/helm/downloader/chart_downloader.go index 2871f1dea..84def6cd0 100644 --- a/cmd/helm/downloader/chart_downloader.go +++ b/cmd/helm/downloader/chart_downloader.go @@ -24,7 +24,6 @@ import ( "net/http" "net/url" "os" - "path" "path/filepath" "strings" @@ -176,24 +175,6 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er return url.Parse(cv.URLs[0]) } -// urlJoin joins a base URL to one or more path components. -// -// It's like filepath.Join for URLs. If the baseURL is pathish, this will still -// perform a join. -// -// If the URL is unparsable, this returns an error. -func urlJoin(baseURL string, paths ...string) (string, error) { - u, err := url.Parse(baseURL) - if err != nil { - return "", err - } - // We want path instead of filepath because path always uses /. - all := []string{u.Path} - all = append(all, paths...) - u.Path = path.Join(all...) - return u.String(), nil -} - func findRepoEntry(name string, repos []*repo.Entry) (*repo.Entry, error) { for _, re := range repos { if re.Name == name { diff --git a/cmd/helm/downloader/chart_downloader_test.go b/cmd/helm/downloader/chart_downloader_test.go index 545943304..d2cf191f9 100644 --- a/cmd/helm/downloader/chart_downloader_test.go +++ b/cmd/helm/downloader/chart_downloader_test.go @@ -153,24 +153,3 @@ func TestDownloadTo(t *testing.T) { return } } - -func TestUrlJoin(t *testing.T) { - tests := []struct { - name, url, expect string - paths []string - }{ - {name: "URL, one path", url: "http://example.com", paths: []string{"hello"}, expect: "http://example.com/hello"}, - {name: "Long URL, one path", url: "http://example.com/but/first", paths: []string{"slurm"}, expect: "http://example.com/but/first/slurm"}, - {name: "URL, two paths", url: "http://example.com", paths: []string{"hello", "world"}, expect: "http://example.com/hello/world"}, - {name: "URL, no paths", url: "http://example.com", paths: []string{}, expect: "http://example.com"}, - {name: "basepath, two paths", url: "../example.com", paths: []string{"hello", "world"}, expect: "../example.com/hello/world"}, - } - - for _, tt := range tests { - if got, err := urlJoin(tt.url, tt.paths...); err != nil { - t.Errorf("%s: error %q", tt.name, err) - } else if got != tt.expect { - t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got) - } - } -} diff --git a/pkg/repo/index.go b/pkg/repo/index.go index 5b591f7e5..d9d04762c 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -22,7 +22,9 @@ import ( "fmt" "io/ioutil" "net/http" + "net/url" "os" + "path" "path/filepath" "sort" "strings" @@ -96,8 +98,12 @@ func NewIndexFile() *IndexFile { func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { u := filename if baseURL != "" { + var err error _, file := filepath.Split(filename) - u = strings.TrimSuffix(baseURL, "/") + "/" + file + u, err = urlJoin(baseURL, file) + if err != nil { + u = filepath.Join(baseURL, file) + } } cr := &ChartVersion{ URLs: []string{u}, @@ -295,3 +301,21 @@ func LoadIndexFile(path string) (*IndexFile, error) { } return LoadIndex(b) } + +// urlJoin joins a base URL to one or more path components. +// +// It's like filepath.Join for URLs. If the baseURL is pathish, this will still +// perform a join. +// +// If the URL is unparsable, this returns an error. +func urlJoin(baseURL string, paths ...string) (string, error) { + u, err := url.Parse(baseURL) + if err != nil { + return "", err + } + // We want path instead of filepath because path always uses /. + all := []string{u.Path} + all = append(all, paths...) + u.Path = path.Join(all...) + return u.String(), nil +} diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go index 33203c495..080023f24 100644 --- a/pkg/repo/index_test.go +++ b/pkg/repo/index_test.go @@ -292,3 +292,24 @@ func TestIndexAdd(t *testing.T) { t.Errorf("Expected http://example.com/charts/deis-0.1.0.tgz, got %s", i.Entries["deis"][0].URLs[0]) } } + +func TestUrlJoin(t *testing.T) { + tests := []struct { + name, url, expect string + paths []string + }{ + {name: "URL, one path", url: "http://example.com", paths: []string{"hello"}, expect: "http://example.com/hello"}, + {name: "Long URL, one path", url: "http://example.com/but/first", paths: []string{"slurm"}, expect: "http://example.com/but/first/slurm"}, + {name: "URL, two paths", url: "http://example.com", paths: []string{"hello", "world"}, expect: "http://example.com/hello/world"}, + {name: "URL, no paths", url: "http://example.com", paths: []string{}, expect: "http://example.com"}, + {name: "basepath, two paths", url: "../example.com", paths: []string{"hello", "world"}, expect: "../example.com/hello/world"}, + } + + for _, tt := range tests { + if got, err := urlJoin(tt.url, tt.paths...); err != nil { + t.Errorf("%s: error %q", tt.name, err) + } else if got != tt.expect { + t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got) + } + } +} From 87ab6673e0fc9eaeafc47f1f9508640c4619c4dd Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 10 Oct 2016 14:10:00 -0600 Subject: [PATCH 163/183] fix(tiller): when delete fails, mark release as deleted When a deletion fails to remove a manifest file, the release should still be marked as deleted. This changes the error handling to try to delete all manifests, and then mark the release as deleted, then return the errors. Closes #1305 --- cmd/tiller/release_server.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index a3bfb03e6..2b3cb2bd1 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -855,20 +855,20 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR // We could instead just delete everything in no particular order. return nil, err } - // Note: We could re-join these into one file and delete just that one. Or - // we could collect errors (instead of bailing on the first error) and try - // to delete as much as possible instead of failing at the first error. + + // Collect the errors, and return them later. + es := []string{} for _, file := range files { b := bytes.NewBufferString(file.content) if err := s.env.KubeClient.Delete(rel.Namespace, b); err != nil { log.Printf("uninstall: Failed deletion of %q: %s", req.Name, err) - return nil, err + es = append(es, err.Error()) } } if !req.DisableHooks { if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, postDelete); err != nil { - return res, err + es = append(es, err.Error()) } } @@ -882,7 +882,12 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR } } - return res, nil + var errs error + if len(es) > 0 { + errs = fmt.Errorf("deletion error count %d: %s", len(es), strings.Join(es, "; ")) + } + + return res, errs } // byName implements the sort.Interface for []*release.Release. From b9fb8abdbc97b0b65a8829c2e2ef745e6fb0a134 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 10 Oct 2016 13:31:26 -0600 Subject: [PATCH 164/183] ref(helm): remove old structure functions This replaces the old structure functions with the functions in cmd/helm/helmpath. Closes #1318 --- cmd/helm/dependency_build_test.go | 2 +- cmd/helm/dependency_update_test.go | 2 +- cmd/helm/helm.go | 20 +++++------ cmd/helm/helm_test.go | 11 +++++- cmd/helm/init.go | 16 ++------- cmd/helm/init_test.go | 2 +- cmd/helm/install.go | 2 +- cmd/helm/repo_update.go | 14 ++++---- cmd/helm/repo_update_test.go | 13 +++---- cmd/helm/search.go | 1 - cmd/helm/structure.go | 57 ------------------------------ 11 files changed, 37 insertions(+), 103 deletions(-) delete mode 100644 cmd/helm/structure.go diff --git a/cmd/helm/dependency_build_test.go b/cmd/helm/dependency_build_test.go index 7038e9aa1..361d3ed6c 100644 --- a/cmd/helm/dependency_build_test.go +++ b/cmd/helm/dependency_build_test.go @@ -103,7 +103,7 @@ func TestDependencyBuildCmd(t *testing.T) { t.Fatal(err) } - i, err := repo.LoadIndexFile(cacheIndexFile("test")) + i, err := repo.LoadIndexFile(dbc.helmhome.CacheIndex("test")) if err != nil { t.Fatal(err) } diff --git a/cmd/helm/dependency_update_test.go b/cmd/helm/dependency_update_test.go index becbef9a3..465b2afc1 100644 --- a/cmd/helm/dependency_update_test.go +++ b/cmd/helm/dependency_update_test.go @@ -85,7 +85,7 @@ func TestDependencyUpdateCmd(t *testing.T) { t.Fatal(err) } - i, err := repo.LoadIndexFile(cacheIndexFile("test")) + i, err := repo.LoadIndexFile(duc.helmhome.CacheIndex("test")) if err != nil { t.Fatal(err) } diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 46d13c64d..d1e5e8faf 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -28,9 +28,10 @@ import ( ) const ( - homeEnvVar = "HELM_HOME" - hostEnvVar = "HELM_HOST" - tillerNamespace = "kube-system" + localRepoIndexFilePath = "index.yaml" + homeEnvVar = "HELM_HOME" + hostEnvVar = "HELM_HOST" + tillerNamespace = "kube-system" ) var ( @@ -159,15 +160,6 @@ func checkArgsLength(argsReceived int, requiredArgs ...string) error { return nil } -// requireInit is a PreRunE implementation for validating that $HELM_HOME is configured. -func requireInit(cmd *cobra.Command, args []string) error { - err := requireHome() - if err != nil { - return fmt.Errorf("%s (try running 'helm init')", err) - } - return nil -} - // prettyError unwraps or rewrites certain errors to make them more user-friendly. func prettyError(err error) error { if err == nil { @@ -178,3 +170,7 @@ func prettyError(err error) error { // the desc. Instead, we have to pass ALL errors through this. return errors.New(grpc.ErrorDesc(err)) } + +func homePath() string { + return os.ExpandEnv(helmHome) +} diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 43a1c8b64..e3bba8777 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -255,6 +255,15 @@ func ensureTestHome(home helmpath.Home, t *testing.T) error { repoFile := home.RepositoryFile() if fi, err := os.Stat(repoFile); err != nil { rf := repo.NewRepoFile() + rf.Add(&repo.Entry{ + Name: "charts", + URL: "http://example.com/foo", + Cache: "charts-index.yaml", + }, &repo.Entry{ + Name: "local", + URL: "http://localhost.com:7743/foo", + Cache: "local-index.yaml", + }) if err := rf.WriteFile(repoFile, 0644); err != nil { return err } @@ -276,7 +285,7 @@ func ensureTestHome(home helmpath.Home, t *testing.T) error { } //TODO: take this out and replace with helm update functionality - os.Symlink(localRepoIndexFile, cacheDirectory("local-index.yaml")) + os.Symlink(localRepoIndexFile, home.CacheIndex("local")) } else if fi.IsDir() { return fmt.Errorf("%s must be a file, not a directory", localRepoIndexFile) } diff --git a/cmd/helm/init.go b/cmd/helm/init.go index 1546860c6..8e7d11ee1 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -92,18 +92,6 @@ func (i *initCmd) run() error { return nil } -func requireHome() error { - dirs := []string{homePath(), repositoryDirectory(), cacheDirectory(), localRepoDirectory()} - for _, d := range dirs { - if fi, err := os.Stat(d); err != nil { - return fmt.Errorf("directory %q is not configured", d) - } else if !fi.IsDir() { - return fmt.Errorf("expected %q to be a directory", d) - } - } - return nil -} - // ensureHome checks to see if $HELM_HOME exists // // If $HELM_HOME does not exist, this function will create it. @@ -150,7 +138,7 @@ func ensureHome(home helmpath.Home, out io.Writer) error { } } - localRepoIndexFile := localRepoDirectory(localRepoIndexFilePath) + localRepoIndexFile := home.LocalRepository(localRepoIndexFilePath) if fi, err := os.Stat(localRepoIndexFile); err != nil { fmt.Fprintf(out, "Creating %s \n", localRepoIndexFile) i := repo.NewIndexFile() @@ -159,7 +147,7 @@ func ensureHome(home helmpath.Home, out io.Writer) error { } //TODO: take this out and replace with helm update functionality - os.Symlink(localRepoIndexFile, cacheDirectory("local-index.yaml")) + os.Symlink(localRepoIndexFile, home.CacheIndex("local")) } else if fi.IsDir() { return fmt.Errorf("%s must be a file, not a directory", localRepoIndexFile) } diff --git a/cmd/helm/init_test.go b/cmd/helm/init_test.go index 171ef1ac5..bbea3b608 100644 --- a/cmd/helm/init_test.go +++ b/cmd/helm/init_test.go @@ -54,7 +54,7 @@ func TestEnsureHome(t *testing.T) { t.Errorf("%s should not be a directory", fi) } - if fi, err := os.Stat(localRepoDirectory(localRepoIndexFilePath)); err != nil { + if fi, err := os.Stat(hh.LocalRepository(localRepoIndexFilePath)); err != nil { t.Errorf("%s", err) } else if fi.IsDir() { t.Errorf("%s should not be a directory", fi) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index d53dd7bd2..38cb01767 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -323,7 +323,7 @@ func locateChartPath(name, version string, verify bool, keyring string) (string, return name, fmt.Errorf("path %q not found", name) } - crepo := filepath.Join(repositoryDirectory(), name) + crepo := filepath.Join(helmpath.Home(homePath()).Repository(), name) if _, err := os.Stat(crepo); err == nil { return filepath.Abs(crepo) } diff --git a/cmd/helm/repo_update.go b/cmd/helm/repo_update.go index 1a7685abe..7e04b5e27 100644 --- a/cmd/helm/repo_update.go +++ b/cmd/helm/repo_update.go @@ -37,17 +37,15 @@ future releases. ` type repoUpdateCmd struct { - repoFile string - update func([]*repo.Entry, bool, io.Writer, helmpath.Home) - out io.Writer - home helmpath.Home + update func([]*repo.Entry, bool, io.Writer, helmpath.Home) + out io.Writer + home helmpath.Home } func newRepoUpdateCmd(out io.Writer) *cobra.Command { u := &repoUpdateCmd{ - out: out, - update: updateCharts, - repoFile: repositoriesFile(), + out: out, + update: updateCharts, } cmd := &cobra.Command{ Use: "update", @@ -63,7 +61,7 @@ func newRepoUpdateCmd(out io.Writer) *cobra.Command { } func (u *repoUpdateCmd) run() error { - f, err := repo.LoadRepositoriesFile(u.repoFile) + f, err := repo.LoadRepositoriesFile(u.home.RepositoryFile()) if err != nil { return err } diff --git a/cmd/helm/repo_update_test.go b/cmd/helm/repo_update_test.go index 857fbbf88..5906bd62d 100644 --- a/cmd/helm/repo_update_test.go +++ b/cmd/helm/repo_update_test.go @@ -29,7 +29,6 @@ import ( ) func TestUpdateCmd(t *testing.T) { - thome, err := tempHelmHome(t) if err != nil { t.Fatal(err) @@ -50,14 +49,16 @@ func TestUpdateCmd(t *testing.T) { } } uc := &repoUpdateCmd{ - out: out, - update: updater, - repoFile: "testdata/repositories.yaml", + out: out, + update: updater, + home: helmpath.Home(thome), + } + if err := uc.run(); err != nil { + t.Fatal(err) } - uc.run() if got := out.String(); !strings.Contains(got, "charts") || !strings.Contains(got, "local") { - t.Errorf("Expected 'charts' and 'local' (in any order) got %s", got) + t.Errorf("Expected 'charts' and 'local' (in any order) got %q", got) } } diff --git a/cmd/helm/search.go b/cmd/helm/search.go index 6028aa6c1..c17c17d5d 100644 --- a/cmd/helm/search.go +++ b/cmd/helm/search.go @@ -57,7 +57,6 @@ func newSearchCmd(out io.Writer) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return sc.run(args) }, - PreRunE: requireInit, } f := cmd.Flags() diff --git a/cmd/helm/structure.go b/cmd/helm/structure.go deleted file mode 100644 index f1d40040a..000000000 --- a/cmd/helm/structure.go +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - "os" - "path/filepath" -) - -const ( - repositoryDir string = "repository" - repositoriesFilePath string = "repositories.yaml" - cachePath string = "cache" - localRepoPath string = "local" - localRepoIndexFilePath string = "index.yaml" -) - -func homePath() string { - return os.ExpandEnv(helmHome) -} - -// All other directories go under the $HELM_HOME/repository. -func repositoryDirectory() string { - return homePath() + "/" + repositoryDir -} - -func cacheDirectory(paths ...string) string { - fragments := append([]string{repositoryDirectory(), cachePath}, paths...) - return filepath.Join(fragments...) -} - -func cacheIndexFile(repoName string) string { - return cacheDirectory(repoName + "-index.yaml") -} - -func localRepoDirectory(paths ...string) string { - fragments := append([]string{repositoryDirectory(), localRepoPath}, paths...) - return filepath.Join(fragments...) -} - -func repositoriesFile() string { - return filepath.Join(repositoryDirectory(), repositoriesFilePath) -} From 59229eefeed75405aafb99edac52113854795abf Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Mon, 10 Oct 2016 16:48:40 -0400 Subject: [PATCH 165/183] ref(helm): add new line after repo remove msg I think this got lost in a refactor. Was just bothering me. --- cmd/helm/repo_remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/helm/repo_remove.go b/cmd/helm/repo_remove.go index 422b545b7..f7671cab0 100644 --- a/cmd/helm/repo_remove.go +++ b/cmd/helm/repo_remove.go @@ -78,7 +78,7 @@ func removeRepoLine(out io.Writer, name string, home helmpath.Home) error { return err } - fmt.Fprintf(out, "%q has been removed from your repositories", name) + fmt.Fprintf(out, "%q has been removed from your repositories\n", name) return nil } From 7c096fb2c2c0abffe50cdceedf39162846962c46 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Mon, 10 Oct 2016 14:55:00 -0600 Subject: [PATCH 166/183] fix(helm): make 'helm help' text punctuation consistent Closes #1301 --- cmd/helm/helm.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 46d13c64d..b35e97a3a 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -58,9 +58,9 @@ Common actions from this point include: - helm list: list releases of charts Environment: - $HELM_HOME Set an alternative location for Helm files. By default, these are stored in ~/.helm - $HELM_HOST Set an alternative Tiller host. The format is host:port. - $KUBECONFIG Set an alternate Kubernetes configuration file (default: "~/.kube/config"). + $HELM_HOME set an alternative location for Helm files. By default, these are stored in ~/.helm + $HELM_HOST set an alternative Tiller host. The format is host:port + $KUBECONFIG set an alternate Kubernetes configuration file (default "~/.kube/config") ` func newRootCmd(out io.Writer) *cobra.Command { @@ -79,8 +79,8 @@ func newRootCmd(out io.Writer) *cobra.Command { } thost := os.Getenv(hostEnvVar) p := cmd.PersistentFlags() - p.StringVar(&helmHome, "home", home, "location of your Helm config. Overrides $HELM_HOME.") - p.StringVar(&tillerHost, "host", thost, "address of tiller. Overrides $HELM_HOST.") + p.StringVar(&helmHome, "home", home, "location of your Helm config. Overrides $HELM_HOME") + p.StringVar(&tillerHost, "host", thost, "address of tiller. Overrides $HELM_HOST") p.BoolVarP(&flagDebug, "debug", "", false, "enable verbose output") rup := newRepoUpdateCmd(out) From 72dd427d07a731c9c1f402b7fbc5210e5e4b0c6c Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Mon, 10 Oct 2016 11:07:33 -0700 Subject: [PATCH 167/183] ref(scripts): use gometalinter for linters --- scripts/validate-go.sh | 65 +++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/scripts/validate-go.sh b/scripts/validate-go.sh index 8c68bac69..ccdfd2fef 100755 --- a/scripts/validate-go.sh +++ b/scripts/validate-go.sh @@ -15,42 +15,37 @@ # limitations under the License. set -euo pipefail -readonly reset=$(tput sgr0) -readonly red=$(tput bold; tput setaf 1) -readonly green=$(tput bold; tput setaf 2) -readonly yellow=$(tput bold; tput setaf 3) - exit_code=0 -find_go_files() { - find . -type f -name "*.go" | grep -v vendor -} - -hash golint 2>/dev/null || go get -u github.com/golang/lint/golint -hash godir 2>/dev/null || go get -u github.com/Masterminds/godir - -echo "==> Running golint..." -for pkg in $(godir pkgs | grep -v proto); do - golint_out=$(golint "$pkg" 2>&1) - if [[ -n "$golint_out" ]]; then - echo "${yellow}${golint_out}${reset}" - fi -done - -echo "==> Running go vet..." -echo -n "$red" -go vet $(godir pkgs) 2>&1 | grep -v "^exit status " || exit_code=${PIPESTATUS[0]} -echo -n "$reset" - -echo "==> Running gofmt..." -failed_fmt=$(find_go_files | xargs gofmt -s -l) -if [[ -n "${failed_fmt}" ]]; then - echo -n "${red}" - echo "gofmt check failed:" - echo "$failed_fmt" - gofmt -s -d "${failed_fmt}" - echo -n "${reset}" - exit_code=1 +if ! hash gometalinter 2>/dev/null ; then + go get github.com/alecthomas/gometalinter + gometalinter --install fi -exit ${exit_code} +echo +echo "==> Running static validations <==" +# Run linters that should return errors +gometalinter \ + --disable-all \ + --enable deadcode \ + --severity deadcode:error \ + --enable gofmt \ + --enable gosimple \ + --enable ineffassign \ + --enable misspell \ + --enable vet \ + --tests \ + --vendor \ + ./... || exit_code=1 + +echo +echo "==> Running linters <==" +# Run linters that should return warnings +gometalinter \ + --disable-all \ + --enable golint \ + --vendor \ + --skip proto \ + ./... || : + +exit $exit_code From f71230ccd33e34e6dde4c73d8cb2e07d654faaee Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Mon, 10 Oct 2016 14:58:33 -0700 Subject: [PATCH 168/183] fix(*): resolve go linter issues --- _proto/hapi/services/tiller.proto | 2 +- cmd/helm/dependency.go | 5 - cmd/helm/repo.go | 4 - cmd/helm/search/search.go | 2 +- cmd/tiller/hooks_test.go | 2 +- cmd/tiller/release_server.go | 7 -- glide.lock | 9 +- glide.yaml | 2 +- pkg/chartutil/values_test.go | 3 + pkg/ignore/rules.go | 5 +- pkg/kube/client.go | 7 +- pkg/lint/rules/chartfile.go | 10 +- pkg/lint/rules/chartfile_test.go | 21 ----- pkg/lint/rules/template.go | 29 +----- pkg/lint/rules/template_test.go | 32 ------- pkg/proto/hapi/chart/chart.pb.go | 33 +++---- pkg/proto/hapi/chart/config.pb.go | 12 +-- pkg/proto/hapi/chart/metadata.pb.go | 39 ++++---- pkg/proto/hapi/chart/template.pb.go | 8 +- pkg/proto/hapi/release/hook.pb.go | 43 ++++----- pkg/proto/hapi/release/info.pb.go | 21 +++-- pkg/proto/hapi/release/release.pb.go | 34 +++---- pkg/proto/hapi/release/status.pb.go | 36 +++---- pkg/proto/hapi/services/tiller.pb.go | 134 +++++++++++++-------------- pkg/proto/hapi/version/version.pb.go | 15 +-- 25 files changed, 208 insertions(+), 307 deletions(-) diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto index 51fb1552c..d3fd05337 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -51,7 +51,7 @@ service ReleaseService { rpc GetReleaseStatus(GetReleaseStatusRequest) returns (GetReleaseStatusResponse) { } - // GetReleaseContent retrieves the release content (chart + value) for the specifed release. + // GetReleaseContent retrieves the release content (chart + value) for the specified release. rpc GetReleaseContent(GetReleaseContentRequest) returns (GetReleaseContentResponse) { } diff --git a/cmd/helm/dependency.go b/cmd/helm/dependency.go index ed5df720d..b22c3926e 100644 --- a/cmd/helm/dependency.go +++ b/cmd/helm/dependency.go @@ -27,11 +27,6 @@ import ( "k8s.io/helm/pkg/chartutil" ) -const ( - reqLock = "requirements.lock" - reqYaml = "requirements.yaml" -) - const dependencyDesc = ` Manage the dependencies of a chart. diff --git a/cmd/helm/repo.go b/cmd/helm/repo.go index fd8a0dfdb..8acc762e2 100644 --- a/cmd/helm/repo.go +++ b/cmd/helm/repo.go @@ -30,10 +30,6 @@ Example usage: $ helm repo add [NAME] [REPO_URL] ` -type repoCmd struct { - out io.Writer -} - func newRepoCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "repo [FLAGS] add|remove|list|index|update [ARGS]", diff --git a/cmd/helm/search/search.go b/cmd/helm/search/search.go index e96de2bc1..bc4a8eaf1 100644 --- a/cmd/helm/search/search.go +++ b/cmd/helm/search/search.go @@ -112,7 +112,7 @@ func (i *Index) All() []*Result { // If regexp is true, the term is treated as a regular expression. Otherwise, // term is treated as a literal string. func (i *Index) Search(term string, threshold int, regexp bool) ([]*Result, error) { - if regexp == true { + if regexp { return i.SearchRegexp(term, threshold) } return i.SearchLiteral(term, threshold), nil diff --git a/cmd/tiller/hooks_test.go b/cmd/tiller/hooks_test.go index 9ff4bf533..af25468ad 100644 --- a/cmd/tiller/hooks_test.go +++ b/cmd/tiller/hooks_test.go @@ -43,7 +43,7 @@ kind: Job metadata: name: first labels: - doesnt: matter + doesnot: matter annotations: "helm.sh/hook": pre-install `, diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index a3bfb03e6..850fedd72 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -27,7 +27,6 @@ import ( "google.golang.org/grpc/metadata" - "github.com/ghodss/yaml" "github.com/technosophos/moniker" ctx "golang.org/x/net/context" "k8s.io/kubernetes/pkg/api/unversioned" @@ -702,12 +701,6 @@ func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values return hooks, b, notes, nil } -// validateYAML checks to see if YAML is well-formed. -func validateYAML(data string) error { - b := map[string]interface{}{} - return yaml.Unmarshal([]byte(data), b) -} - func (s *releaseServer) recordRelease(r *release.Release, reuse bool) { if reuse { if err := s.env.Releases.Update(r); err != nil { diff --git a/glide.lock b/glide.lock index 79052cc41..7d023b61a 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 59cc65f24ef42bcc96a5e5f660dd79cff89aa53040a561c47b629f1d55397424 -updated: 2016-10-05T17:24:13.347058671-06:00 +hash: d9c66d934b1b6906646654d536190df6bc2d8c9dfa6b414a91b893846fb5d86e +updated: 2016-10-10T11:23:48.277967613-07:00 imports: - name: bitbucket.org/ww/goautoneg version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 @@ -190,7 +190,7 @@ imports: subpackages: - lru - name: github.com/golang/protobuf - version: 8616e8ee5e20a1704615e6c8d7afcdac06087a67 + version: df1d3ca07d2d07bba352d5b73c4313b4e2a6203e subpackages: - jsonpb - proto @@ -434,7 +434,7 @@ imports: - 1.4/tools/metrics - 1.4/transport - name: k8s.io/kubernetes - version: 23b426563780c64ac25df63a2d141247d02aac7a + version: bd85192c2673153b3b470a2f158ddc840d295768 subpackages: - federation/apis/federation - federation/apis/federation/install @@ -533,7 +533,6 @@ imports: - pkg/client/unversioned/testclient - pkg/controller - pkg/controller/deployment/util - - pkg/controller/framework - pkg/conversion - pkg/conversion/queryparams - pkg/credentialprovider diff --git a/glide.yaml b/glide.yaml index 663c3fd47..4feaea68c 100644 --- a/glide.yaml +++ b/glide.yaml @@ -14,7 +14,7 @@ import: version: 1.1.0 - package: github.com/technosophos/moniker - package: github.com/golang/protobuf - version: 8616e8ee5e20a1704615e6c8d7afcdac06087a67 + version: df1d3ca07d2d07bba352d5b73c4313b4e2a6203e subpackages: - proto - ptypes/any diff --git a/pkg/chartutil/values_test.go b/pkg/chartutil/values_test.go index e5a11600f..b48d4e943 100644 --- a/pkg/chartutil/values_test.go +++ b/pkg/chartutil/values_test.go @@ -270,6 +270,9 @@ func TestCoalesceValues(t *testing.T) { tvals := &chart.Config{Raw: testCoalesceValuesYaml} v, err := CoalesceValues(c, tvals) + if err != nil { + t.Fatal(err) + } j, _ := json.MarshalIndent(v, "", " ") t.Logf("Coalesced Values: %s", string(j)) diff --git a/pkg/ignore/rules.go b/pkg/ignore/rules.go index 0e102a2e7..450e91889 100644 --- a/pkg/ignore/rules.go +++ b/pkg/ignore/rules.go @@ -69,10 +69,7 @@ func Parse(file io.Reader) (*Rules, error) { return r, err } } - if err := s.Err(); err != nil { - return r, err - } - return r, nil + return r, s.Err() } // Len returns the number of patterns in this rule set. diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 2bfd2e48a..5e46bf44e 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -354,11 +354,8 @@ func updateResource(target *resource.Info, currentObj runtime.Object) error { // send patch to server helper := resource.NewHelper(target.Client, target.Mapping) - if _, err = helper.Patch(target.Namespace, target.Name, api.StrategicMergePatchType, patch); err != nil { - return err - } - - return nil + _, err = helper.Patch(target.Namespace, target.Name, api.StrategicMergePatchType, patch) + return err } func watchUntilReady(info *resource.Info) error { diff --git a/pkg/lint/rules/chartfile.go b/pkg/lint/rules/chartfile.go index 8763af0de..f319dca9d 100644 --- a/pkg/lint/rules/chartfile.go +++ b/pkg/lint/rules/chartfile.go @@ -98,6 +98,9 @@ func validateChartVersion(cf *chart.Metadata) error { } c, err := semver.NewConstraint("> 0") + if err != nil { + return err + } valid, msg := c.Validate(version) if !valid && len(msg) > 0 { @@ -149,10 +152,3 @@ func validateChartSources(cf *chart.Metadata) error { } return nil } - -func validateChartHome(cf *chart.Metadata) error { - if cf.Home != "" && !govalidator.IsRequestURL(cf.Home) { - return fmt.Errorf("invalid home URL '%s'", cf.Home) - } - return nil -} diff --git a/pkg/lint/rules/chartfile_test.go b/pkg/lint/rules/chartfile_test.go index e1295a7cf..3bec2f1a5 100644 --- a/pkg/lint/rules/chartfile_test.go +++ b/pkg/lint/rules/chartfile_test.go @@ -194,27 +194,6 @@ func TestValidateChartSources(t *testing.T) { } } -func TestValidateChartHome(t *testing.T) { - var failTest = []string{"RiverRun", "john@winterfell", "riverrun.io"} - var successTest = []string{"", "http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish"} - - for _, test := range failTest { - badChart.Home = test - err := validateChartHome(badChart) - if err == nil || !strings.Contains(err.Error(), "invalid home URL") { - t.Errorf("validateChartHome(%s) to return \"invalid home URL\", got no error", test) - } - } - - for _, test := range successTest { - badChart.Home = test - err := validateChartHome(badChart) - if err != nil { - t.Errorf("validateChartHome(%s) to return no error, got %s", test, err.Error()) - } - } -} - func TestChartfile(t *testing.T) { linter := support.Linter{ChartDir: badChartDir} Chartfile(&linter) diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index dd36e5621..a4d66a629 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -23,7 +23,6 @@ import ( "os" "path/filepath" "regexp" - "strings" "text/template" "github.com/Masterminds/sprig" @@ -121,32 +120,6 @@ func validateTemplatesDir(templatesPath string) error { return nil } -// Validates that go template tags include the quote pipelined function -// i.e {{ .Foo.bar }} -> {{ .Foo.bar | quote }} -// {{ .Foo.bar }}-{{ .Foo.baz }} -> "{{ .Foo.bar }}-{{ .Foo.baz }}" -func validateQuotes(templateContent string) error { - // {{ .Foo.bar }} - r, _ := regexp.Compile(`(?m)(:|-)\s+{{[\w|\.|\s|\']+}}\s*$`) - functions := r.FindAllString(templateContent, -1) - - for _, str := range functions { - if match, _ := regexp.MatchString("quote", str); !match { - result := strings.Replace(str, "}}", " | quote }}", -1) - return fmt.Errorf("wrap substitution functions in quotes or use the sprig \"quote\" function: %s -> %s", str, result) - } - } - - // {{ .Foo.bar }}-{{ .Foo.baz }} -> "{{ .Foo.bar }}-{{ .Foo.baz }}" - r, _ = regexp.Compile(`(?m)({{(\w|\.|\s|\')+}}(\s|-)*)+\s*$`) - functions = r.FindAllString(templateContent, -1) - - for _, str := range functions { - result := strings.Replace(str, str, fmt.Sprintf("\"%s\"", str), -1) - return fmt.Errorf("wrap substitution functions in quotes: %s -> %s", str, result) - } - return nil -} - func validateAllowedExtension(fileName string) error { ext := filepath.Ext(fileName) validExtensions := []string{".yaml", ".tpl", ".txt"} @@ -179,7 +152,7 @@ func validateNoMissingValues(templatesPath string, chartValues chartutil.Values, var buf bytes.Buffer var emptyValues []string - // 2 - Extract every function and execute them agains the loaded values + // 2 - Extract every function and execute them against the loaded values // Supported {{ .Chart.Name }}, {{ .Chart.Name | quote }} r, _ := regexp.Compile(`{{[\w|\.|\s|\|\"|\']+}}`) functions := r.FindAllString(string(templateContent), -1) diff --git a/pkg/lint/rules/template_test.go b/pkg/lint/rules/template_test.go index fdd113023..5a56e22c9 100644 --- a/pkg/lint/rules/template_test.go +++ b/pkg/lint/rules/template_test.go @@ -44,38 +44,6 @@ func TestValidateAllowedExtension(t *testing.T) { } } -func TestValidateQuotes(t *testing.T) { - // add `| quote` lint error - var failTest = []string{"foo: {{.Release.Service }}", "foo: {{.Release.Service }}", "- {{.Release.Service }}", "foo: {{default 'Never' .restart_policy}}", "- {{.Release.Service }} "} - - for _, test := range failTest { - err := validateQuotes(test) - if err == nil || !strings.Contains(err.Error(), "use the sprig \"quote\" function") { - t.Errorf("validateQuotes('%s') to return \"use the sprig \"quote\" function:\", got no error.", test) - } - } - - var successTest = []string{"foo: {{.Release.Service | quote }}", "foo: {{.Release.Service | quote }}", "- {{.Release.Service | quote }}", "foo: {{default 'Never' .restart_policy | quote }}", "foo: \"{{ .Release.Service }}\"", "foo: \"{{ .Release.Service }} {{ .Foo.Bar }}\"", "foo: \"{{ default 'Never' .Release.Service }} {{ .Foo.Bar }}\"", "foo: {{.Release.Service | squote }}"} - - for _, test := range successTest { - err := validateQuotes(test) - if err != nil { - t.Errorf("validateQuotes('%s') to return not error and got \"%s\"", test, err.Error()) - } - } - - // Surrounding quotes - failTest = []string{"foo: {{.Release.Service }}-{{ .Release.Bar }}", "foo: {{.Release.Service }} {{ .Release.Bar }}", "- {{.Release.Service }}-{{ .Release.Bar }}", "- {{.Release.Service }}-{{ .Release.Bar }} {{ .Release.Baz }}", "foo: {{.Release.Service | default }}-{{ .Release.Bar }}"} - - for _, test := range failTest { - err := validateQuotes(test) - if err == nil || !strings.Contains(err.Error(), "wrap substitution functions in quotes") { - t.Errorf("validateQuotes('%s') to return \"wrap substitution functions in quotes\", got no error", test) - } - } - -} - func TestTemplateParsing(t *testing.T) { linter := support.Linter{ChartDir: templateTestBasedir} Templates(&linter) diff --git a/pkg/proto/hapi/chart/chart.pb.go b/pkg/proto/hapi/chart/chart.pb.go index db1d5ef97..c3afc3f44 100644 --- a/pkg/proto/hapi/chart/chart.pb.go +++ b/pkg/proto/hapi/chart/chart.pb.go @@ -100,20 +100,21 @@ func init() { func init() { proto.RegisterFile("hapi/chart/chart.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 239 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcb, 0x48, 0x2c, 0xc8, - 0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0x81, 0x90, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x5c, - 0x20, 0x71, 0x3d, 0xb0, 0x88, 0x94, 0x38, 0xb2, 0x9a, 0xfc, 0xbc, 0xb4, 0xcc, 0x74, 0x88, 0x22, - 0x29, 0x49, 0x24, 0x89, 0xdc, 0xd4, 0x92, 0xc4, 0x94, 0xc4, 0x92, 0x44, 0x2c, 0x52, 0x25, 0xa9, - 0xb9, 0x05, 0x39, 0x89, 0x25, 0xa9, 0x30, 0xa9, 0xf4, 0xfc, 0xfc, 0xf4, 0x9c, 0x54, 0x7d, 0x30, - 0x2f, 0xa9, 0x34, 0x4d, 0x3f, 0x31, 0xaf, 0x12, 0x22, 0xa5, 0xf4, 0x87, 0x91, 0x8b, 0xd5, 0x19, - 0xa4, 0x47, 0xc8, 0x80, 0x8b, 0x03, 0x66, 0xa2, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0xb7, 0x91, 0x88, - 0x1e, 0xc2, 0x49, 0x7a, 0xbe, 0x50, 0xb9, 0x20, 0xb8, 0x2a, 0x21, 0x23, 0x2e, 0x4e, 0x98, 0x45, - 0xc5, 0x12, 0x4c, 0x0a, 0xcc, 0xe8, 0x5a, 0x42, 0xa0, 0x92, 0x41, 0x08, 0x65, 0x42, 0xa6, 0x5c, - 0x3c, 0x29, 0xa9, 0x05, 0xa9, 0x79, 0x29, 0xa9, 0x79, 0xc9, 0x99, 0x40, 0x6d, 0xcc, 0x60, 0x6d, - 0x82, 0xc8, 0xda, 0xc0, 0xce, 0x09, 0x42, 0x51, 0x26, 0xa4, 0xc5, 0xc5, 0x56, 0x96, 0x98, 0x53, - 0x0a, 0xd4, 0xc0, 0x02, 0x76, 0x9a, 0x10, 0x8a, 0x06, 0x70, 0x08, 0x05, 0x41, 0x55, 0x00, 0xd5, - 0xb2, 0xa6, 0x65, 0xe6, 0x00, 0x95, 0xb2, 0x42, 0x9d, 0x04, 0xf1, 0xbd, 0x1e, 0xcc, 0xf7, 0x7a, - 0x8e, 0x79, 0x95, 0x41, 0x10, 0x25, 0x4e, 0xec, 0x51, 0xac, 0x60, 0x33, 0x92, 0xd8, 0xc0, 0xb2, - 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe9, 0x70, 0x34, 0x75, 0x9e, 0x01, 0x00, 0x00, + // 242 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x90, 0xb1, 0x4e, 0xc3, 0x30, + 0x10, 0x86, 0x15, 0x4a, 0x0a, 0x1c, 0x2c, 0x58, 0x08, 0x4c, 0xa7, 0x8a, 0x09, 0x75, 0x70, 0x50, + 0x11, 0x0f, 0x00, 0xcc, 0x2c, 0x16, 0x13, 0xdb, 0xb5, 0xb9, 0xa4, 0x91, 0x52, 0x3b, 0xaa, 0x5d, + 0xa4, 0xbe, 0x3b, 0x03, 0xea, 0xd9, 0xa6, 0x09, 0xea, 0x12, 0x29, 0xf7, 0x7d, 0xff, 0xe5, 0xbf, + 0xc0, 0xed, 0x0a, 0xbb, 0xa6, 0x58, 0xae, 0x70, 0xe3, 0xc3, 0x53, 0x75, 0x1b, 0xeb, 0xad, 0x80, + 0xfd, 0x5c, 0xf1, 0x64, 0x72, 0xd7, 0x77, 0xac, 0xa9, 0x9a, 0x3a, 0x48, 0x93, 0xfb, 0x1e, 0x58, + 0x93, 0xc7, 0x12, 0x3d, 0x1e, 0x41, 0x9e, 0xd6, 0x5d, 0x8b, 0x9e, 0x12, 0xaa, 0xad, 0xad, 0x5b, + 0x2a, 0xf8, 0x6d, 0xb1, 0xad, 0x0a, 0x34, 0xbb, 0x80, 0x1e, 0x7e, 0x32, 0xc8, 0xdf, 0xf7, 0x19, + 0xf1, 0x04, 0xe7, 0x69, 0xa3, 0xcc, 0xa6, 0xd9, 0xe3, 0xe5, 0xfc, 0x46, 0x1d, 0x2a, 0xa9, 0x8f, + 0xc8, 0xf4, 0x9f, 0x25, 0xe6, 0x70, 0x91, 0x3e, 0xe4, 0xe4, 0xc9, 0x74, 0xf4, 0x3f, 0xf2, 0x19, + 0xa1, 0x3e, 0x68, 0xe2, 0x05, 0xae, 0x4a, 0xea, 0xc8, 0x94, 0x64, 0x96, 0x0d, 0x39, 0x39, 0xe2, + 0xd8, 0x75, 0x3f, 0xc6, 0x75, 0xf4, 0x40, 0x13, 0x33, 0x18, 0x7f, 0x63, 0xbb, 0x25, 0x27, 0x4f, + 0xb9, 0x9a, 0x18, 0x04, 0xf8, 0x0f, 0xe9, 0x68, 0x88, 0x19, 0xe4, 0x55, 0xd3, 0x92, 0x93, 0x79, + 0xac, 0x14, 0xae, 0x57, 0xe9, 0x7a, 0xf5, 0x6a, 0x76, 0x3a, 0x28, 0x6f, 0x67, 0x5f, 0x39, 0xef, + 0x58, 0x8c, 0x99, 0x3e, 0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0xe9, 0x70, 0x34, 0x75, 0x9e, 0x01, + 0x00, 0x00, } diff --git a/pkg/proto/hapi/chart/config.pb.go b/pkg/proto/hapi/chart/config.pb.go index f1471405d..a7b61885a 100644 --- a/pkg/proto/hapi/chart/config.pb.go +++ b/pkg/proto/hapi/chart/config.pb.go @@ -49,7 +49,7 @@ func init() { func init() { proto.RegisterFile("hapi/chart/config.proto", fileDescriptor1) } var fileDescriptor1 = []byte{ - // 179 bytes of a gzipped FileDescriptorProto + // 182 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0x48, 0x2c, 0xc8, 0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0xd1, 0x4f, 0xce, 0xcf, 0x4b, 0xcb, 0x4c, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x02, 0x49, 0xe8, 0x81, 0x25, 0x94, 0x16, 0x30, 0x72, 0xb1, 0x39, @@ -57,9 +57,9 @@ var fileDescriptor1 = []byte{ 0x40, 0x4c, 0x21, 0x33, 0x2e, 0xb6, 0xb2, 0xc4, 0x9c, 0xd2, 0xd4, 0x62, 0x09, 0x26, 0x05, 0x66, 0x0d, 0x6e, 0x23, 0x39, 0x3d, 0x84, 0x4e, 0x3d, 0x88, 0x2e, 0xbd, 0x30, 0xb0, 0x02, 0xd7, 0xbc, 0x92, 0xa2, 0xca, 0x20, 0xa8, 0x6a, 0x29, 0x1f, 0x2e, 0x6e, 0x24, 0x61, 0x90, 0xc1, 0xd9, 0xa9, - 0x95, 0x30, 0x83, 0x81, 0x4c, 0x21, 0x75, 0x2e, 0x56, 0xb0, 0x52, 0xa0, 0xb9, 0x8c, 0x40, 0x73, - 0x05, 0x91, 0xcd, 0x05, 0xeb, 0x0c, 0x82, 0xc8, 0x5b, 0x31, 0x59, 0x30, 0x2a, 0xc9, 0x72, 0xb1, - 0x82, 0xc5, 0x84, 0x44, 0x60, 0xba, 0x20, 0x26, 0x41, 0x38, 0x4e, 0xec, 0x51, 0xac, 0x60, 0x8d, - 0x49, 0x6c, 0x60, 0xdf, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xe1, 0x12, 0x60, 0xda, 0xf8, - 0x00, 0x00, 0x00, + 0x95, 0x30, 0x83, 0xb3, 0x53, 0x2b, 0x85, 0xd4, 0xb9, 0x58, 0xc1, 0x4a, 0x25, 0x98, 0x14, 0x18, + 0x35, 0xb8, 0x8d, 0x04, 0x91, 0xcd, 0x05, 0xeb, 0x0c, 0x82, 0xc8, 0x5b, 0x31, 0x59, 0x30, 0x2a, + 0xc9, 0x72, 0xb1, 0x82, 0xc5, 0x84, 0x44, 0x60, 0xba, 0x20, 0x26, 0x41, 0x38, 0x4e, 0xec, 0x51, + 0xac, 0x60, 0x8d, 0x49, 0x6c, 0x60, 0xdf, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xe1, 0x12, + 0x60, 0xda, 0xf8, 0x00, 0x00, 0x00, } diff --git a/pkg/proto/hapi/chart/metadata.pb.go b/pkg/proto/hapi/chart/metadata.pb.go index 220902ba7..41e0999f4 100644 --- a/pkg/proto/hapi/chart/metadata.pb.go +++ b/pkg/proto/hapi/chart/metadata.pb.go @@ -94,23 +94,24 @@ func init() { func init() { proto.RegisterFile("hapi/chart/metadata.proto", fileDescriptor2) } var fileDescriptor2 = []byte{ - // 287 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x51, 0x4b, 0x4b, 0xc4, 0x30, - 0x10, 0x76, 0x1f, 0x6d, 0xb7, 0xd3, 0xcb, 0x32, 0xc8, 0x12, 0x3d, 0x48, 0xe9, 0xc9, 0x53, 0x17, - 0x14, 0xc4, 0xb3, 0x20, 0x1e, 0x74, 0xbb, 0xb2, 0xf8, 0x00, 0x6f, 0xb1, 0x0d, 0x36, 0x68, 0x9b, - 0x92, 0x44, 0xc5, 0xff, 0xe8, 0x8f, 0x32, 0x9d, 0x76, 0x77, 0x7b, 0xf0, 0x50, 0xf8, 0x1e, 0xfd, - 0x26, 0xf3, 0x25, 0x70, 0x54, 0xf2, 0x46, 0x2e, 0xf3, 0x92, 0x6b, 0xbb, 0xac, 0x84, 0xe5, 0x05, - 0xb7, 0x3c, 0x6d, 0xb4, 0xb2, 0x0a, 0xa1, 0xb5, 0x52, 0xb2, 0x92, 0x0b, 0x80, 0x15, 0x97, 0xb5, - 0x75, 0x9f, 0xd0, 0x88, 0x30, 0xad, 0x79, 0x25, 0xd8, 0x28, 0x1e, 0x9d, 0x86, 0x1b, 0xc2, 0x78, - 0x08, 0x9e, 0xa8, 0xb8, 0xfc, 0x60, 0x63, 0x12, 0x3b, 0x92, 0xfc, 0x8e, 0x61, 0xb6, 0xea, 0xc7, - 0xfe, 0x1b, 0x73, 0x5a, 0xa9, 0x9c, 0xd6, 0xa5, 0x08, 0x23, 0x83, 0xc0, 0xa8, 0x4f, 0x9d, 0x0b, - 0xc3, 0x26, 0xf1, 0xc4, 0xc9, 0x5b, 0xda, 0x3a, 0x5f, 0x42, 0x1b, 0xa9, 0x6a, 0x36, 0xa5, 0xc0, - 0x96, 0x62, 0x0c, 0x51, 0x21, 0x4c, 0xae, 0x65, 0x63, 0x5b, 0xd7, 0x23, 0x77, 0x28, 0xe1, 0x31, - 0xcc, 0xde, 0xc5, 0xcf, 0xb7, 0xd2, 0x85, 0x61, 0x3e, 0x8d, 0xdd, 0x71, 0xbc, 0x84, 0xa8, 0xda, - 0xd5, 0x33, 0x2c, 0x70, 0x76, 0x74, 0xb6, 0x48, 0xf7, 0x17, 0x90, 0xee, 0xdb, 0x6f, 0x86, 0xbf, - 0xe2, 0x02, 0x7c, 0x51, 0xbf, 0x39, 0xcc, 0x66, 0x74, 0x64, 0xcf, 0xda, 0x5e, 0x32, 0x77, 0x8b, - 0x84, 0x5d, 0xaf, 0x16, 0xe3, 0x09, 0x80, 0x1b, 0xf8, 0xd4, 0x17, 0x00, 0x72, 0x06, 0x4a, 0x12, - 0x83, 0x7f, 0xdd, 0xa5, 0x23, 0x08, 0x1e, 0xb3, 0xdb, 0x6c, 0xfd, 0x9c, 0xcd, 0x0f, 0x30, 0x04, - 0xef, 0x66, 0xfd, 0x70, 0x7f, 0x37, 0x1f, 0x5d, 0x05, 0x2f, 0x1e, 0xad, 0xf3, 0xea, 0xd3, 0x13, - 0x9d, 0xff, 0x05, 0x00, 0x00, 0xff, 0xff, 0x65, 0x86, 0x8b, 0xda, 0xbf, 0x01, 0x00, 0x00, + // 290 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x91, 0x4d, 0x4b, 0xf4, 0x30, + 0x14, 0x85, 0xdf, 0x4e, 0xbf, 0x6f, 0x37, 0xc3, 0xe5, 0x65, 0x88, 0x2e, 0xa4, 0x74, 0xd5, 0x55, + 0x07, 0x14, 0xc4, 0xb5, 0x20, 0x2e, 0x74, 0x3a, 0x52, 0xfc, 0x00, 0x77, 0xb1, 0x0d, 0x36, 0x68, + 0x93, 0x92, 0x44, 0xc5, 0xff, 0xe8, 0x8f, 0x92, 0xa6, 0x9d, 0x99, 0x2e, 0xdc, 0xdd, 0x73, 0x9e, + 0x9e, 0xdb, 0x9c, 0x04, 0x8e, 0x5a, 0xda, 0xf3, 0x75, 0xdd, 0x52, 0x65, 0xd6, 0x1d, 0x33, 0xb4, + 0xa1, 0x86, 0x16, 0xbd, 0x92, 0x46, 0x22, 0x0c, 0xa8, 0xb0, 0x28, 0x3b, 0x07, 0xd8, 0x50, 0x2e, + 0x0c, 0xe5, 0x82, 0x29, 0x44, 0xf0, 0x04, 0xed, 0x18, 0x71, 0x52, 0x27, 0x8f, 0x2b, 0x3b, 0xe3, + 0x7f, 0xf0, 0x59, 0x47, 0xf9, 0x3b, 0x59, 0x58, 0x73, 0x14, 0xd9, 0xcf, 0x02, 0xa2, 0xcd, 0xb4, + 0xf6, 0xcf, 0x18, 0x82, 0xd7, 0xca, 0x8e, 0x4d, 0x29, 0x3b, 0x23, 0x81, 0x50, 0xcb, 0x0f, 0x55, + 0x33, 0x4d, 0xdc, 0xd4, 0xcd, 0xe3, 0x6a, 0x27, 0x07, 0xf2, 0xc9, 0x94, 0xe6, 0x52, 0x10, 0xcf, + 0x06, 0x76, 0x12, 0x53, 0x48, 0x1a, 0xa6, 0x6b, 0xc5, 0x7b, 0x33, 0x50, 0xdf, 0xd2, 0xb9, 0x85, + 0xc7, 0x10, 0xbd, 0xb1, 0xef, 0x2f, 0xa9, 0x1a, 0x4d, 0x02, 0xbb, 0x76, 0xaf, 0xf1, 0x02, 0x92, + 0x6e, 0x5f, 0x4f, 0x93, 0x30, 0x75, 0xf3, 0xe4, 0x74, 0x55, 0x1c, 0x2e, 0xa0, 0x38, 0xb4, 0xaf, + 0xe6, 0x9f, 0xe2, 0x0a, 0x02, 0x26, 0x5e, 0xb9, 0x60, 0x24, 0xb2, 0xbf, 0x9c, 0xd4, 0xd0, 0x8b, + 0xd7, 0x52, 0x90, 0x78, 0xec, 0x35, 0xcc, 0x78, 0x02, 0x40, 0x7b, 0xfe, 0x38, 0x15, 0x00, 0x4b, + 0x66, 0x4e, 0x96, 0x42, 0x70, 0x35, 0xa6, 0x13, 0x08, 0x1f, 0xca, 0x9b, 0x72, 0xfb, 0x54, 0x2e, + 0xff, 0x61, 0x0c, 0xfe, 0xf5, 0xf6, 0xfe, 0xee, 0x76, 0xe9, 0x5c, 0x86, 0xcf, 0xbe, 0x3d, 0xce, + 0x4b, 0x60, 0x9f, 0xe8, 0xec, 0x37, 0x00, 0x00, 0xff, 0xff, 0x65, 0x86, 0x8b, 0xda, 0xbf, 0x01, + 0x00, 0x00, } diff --git a/pkg/proto/hapi/chart/template.pb.go b/pkg/proto/hapi/chart/template.pb.go index aecab641f..2bed587b5 100644 --- a/pkg/proto/hapi/chart/template.pb.go +++ b/pkg/proto/hapi/chart/template.pb.go @@ -36,12 +36,12 @@ func init() { func init() { proto.RegisterFile("hapi/chart/template.proto", fileDescriptor3) } var fileDescriptor3 = []byte{ - // 106 bytes of a gzipped FileDescriptorProto + // 107 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8, 0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0xd1, 0x2f, 0x49, 0xcd, 0x2d, 0xc8, 0x49, 0x2c, 0x49, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x02, 0x49, 0xe9, 0x81, 0xa5, 0x94, 0x8c, 0xb8, 0x38, 0x42, 0xa0, 0xb2, 0x42, 0x42, 0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, - 0x9c, 0x41, 0x60, 0x36, 0x48, 0x2c, 0x25, 0xb1, 0x24, 0x51, 0x82, 0x09, 0x28, 0xc6, 0x13, 0x04, - 0x66, 0x3b, 0xb1, 0x47, 0xb1, 0x82, 0x35, 0x27, 0xb1, 0x81, 0xcd, 0x33, 0x06, 0x04, 0x00, 0x00, - 0xff, 0xff, 0x53, 0xee, 0x0e, 0x67, 0x6c, 0x00, 0x00, 0x00, + 0x9c, 0x41, 0x60, 0x36, 0x48, 0x2c, 0x25, 0xb1, 0x24, 0x51, 0x82, 0x49, 0x81, 0x51, 0x83, 0x27, + 0x08, 0xcc, 0x76, 0x62, 0x8f, 0x62, 0x05, 0x6b, 0x4e, 0x62, 0x03, 0x9b, 0x67, 0x0c, 0x08, 0x00, + 0x00, 0xff, 0xff, 0x53, 0xee, 0x0e, 0x67, 0x6c, 0x00, 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/hook.pb.go b/pkg/proto/hapi/release/hook.pb.go index 0da69d40e..57581b14e 100644 --- a/pkg/proto/hapi/release/hook.pb.go +++ b/pkg/proto/hapi/release/hook.pb.go @@ -112,25 +112,26 @@ func init() { func init() { proto.RegisterFile("hapi/release/hook.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 320 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0xcd, 0x4e, 0xf2, 0x40, - 0x14, 0x86, 0xbf, 0x42, 0x69, 0xe1, 0xc0, 0x87, 0x75, 0x36, 0x4e, 0xd8, 0x48, 0x58, 0xb1, 0x9a, - 0x1a, 0x8c, 0x17, 0x00, 0xd2, 0xa8, 0xa1, 0x29, 0x64, 0x28, 0x31, 0x71, 0x43, 0x86, 0x38, 0x40, - 0x03, 0xed, 0x34, 0x74, 0xf0, 0x7a, 0xbc, 0x3e, 0xaf, 0xc2, 0xce, 0xf4, 0x27, 0xee, 0x4e, 0x9f, - 0xf3, 0x9c, 0xb7, 0xf3, 0xc2, 0xdd, 0x91, 0xa5, 0x91, 0x7b, 0xe1, 0x67, 0xce, 0x32, 0xee, 0x1e, - 0x85, 0x38, 0x91, 0xf4, 0x22, 0xa4, 0x40, 0x3d, 0xb5, 0x20, 0xe5, 0x62, 0x70, 0x7f, 0x10, 0xe2, - 0x70, 0xe6, 0xae, 0xde, 0xed, 0xae, 0x7b, 0x57, 0x46, 0x31, 0xcf, 0x24, 0x8b, 0xd3, 0x42, 0x1f, - 0xfd, 0x34, 0xc0, 0x7c, 0xcd, 0xaf, 0x11, 0x02, 0x33, 0x61, 0x31, 0xc7, 0xc6, 0xd0, 0x18, 0x77, - 0xa8, 0x9e, 0x15, 0x3b, 0x45, 0xc9, 0x27, 0x6e, 0x14, 0x4c, 0xcd, 0x8a, 0xa5, 0x4c, 0x1e, 0x71, - 0xb3, 0x60, 0x6a, 0x46, 0x03, 0x68, 0xc7, 0x2c, 0x89, 0xf6, 0x79, 0x32, 0x36, 0x35, 0xaf, 0xbf, - 0xd1, 0x03, 0x58, 0xfc, 0x8b, 0x27, 0x32, 0xc3, 0xad, 0x61, 0x73, 0xdc, 0x9f, 0x60, 0xf2, 0xf7, - 0x81, 0x44, 0xfd, 0x9b, 0x78, 0x4a, 0xa0, 0xa5, 0x87, 0x9e, 0xa0, 0x7d, 0x66, 0x99, 0xdc, 0x5e, - 0xae, 0x09, 0xb6, 0xf2, 0xb4, 0xee, 0x64, 0x40, 0x8a, 0x1a, 0xa4, 0xaa, 0x41, 0xc2, 0xaa, 0x06, - 0xb5, 0x95, 0x4b, 0xaf, 0xc9, 0xe8, 0xdb, 0x80, 0x96, 0x0e, 0x42, 0x5d, 0xb0, 0x37, 0xc1, 0x22, - 0x58, 0xbe, 0x07, 0xce, 0x3f, 0x74, 0x03, 0xdd, 0x15, 0xf5, 0xb6, 0x6f, 0xc1, 0x3a, 0x9c, 0xfa, - 0xbe, 0x63, 0x20, 0x07, 0x7a, 0xab, 0xe5, 0x3a, 0xac, 0x49, 0x03, 0xf5, 0x01, 0x94, 0x32, 0xf7, - 0x7c, 0x2f, 0xf4, 0x9c, 0xa6, 0x3e, 0x51, 0x46, 0x09, 0xcc, 0x2a, 0x63, 0xb3, 0x7a, 0xa1, 0xd3, - 0xb9, 0xe7, 0xb4, 0xea, 0x8c, 0x8a, 0x58, 0x9a, 0xe4, 0x0a, 0x5d, 0xfa, 0xfe, 0x6c, 0xfa, 0xbc, - 0x70, 0x6c, 0x74, 0x0b, 0xff, 0xb5, 0x53, 0xa3, 0xf6, 0xac, 0xf3, 0x61, 0x97, 0xbd, 0x77, 0x96, - 0xae, 0xf2, 0xf8, 0x1b, 0x00, 0x00, 0xff, 0xff, 0xa4, 0x2e, 0x6f, 0xbd, 0xc8, 0x01, 0x00, 0x00, + // 321 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0x4d, 0x6e, 0xea, 0x30, + 0x14, 0x85, 0x5f, 0x20, 0x24, 0x70, 0xe1, 0xd1, 0xd4, 0x93, 0x5a, 0x4c, 0x8a, 0x18, 0x31, 0x72, + 0x2a, 0xaa, 0x2e, 0x00, 0x8a, 0xd5, 0x56, 0x44, 0x01, 0x99, 0xa0, 0x4a, 0x9d, 0x20, 0xa3, 0x1a, + 0x88, 0x20, 0x71, 0x44, 0x4c, 0xd7, 0xd3, 0xf5, 0x75, 0x15, 0x95, 0x9d, 0x1f, 0x75, 0x76, 0xfd, + 0xdd, 0xcf, 0xc7, 0x3e, 0x70, 0x77, 0xe4, 0x59, 0xec, 0x5f, 0xc4, 0x59, 0xf0, 0x5c, 0xf8, 0x47, + 0x29, 0x4f, 0x24, 0xbb, 0x48, 0x25, 0x51, 0x4f, 0x2f, 0x48, 0xb9, 0x18, 0xdc, 0x1f, 0xa4, 0x3c, + 0x9c, 0x85, 0x6f, 0x76, 0xbb, 0xeb, 0xde, 0x57, 0x71, 0x22, 0x72, 0xc5, 0x93, 0xac, 0xd0, 0x47, + 0x3f, 0x0d, 0xb0, 0x5f, 0xa5, 0x3c, 0x21, 0x04, 0x76, 0xca, 0x13, 0x81, 0xad, 0xa1, 0x35, 0xee, + 0x30, 0x33, 0x6b, 0x76, 0x8a, 0xd3, 0x4f, 0xdc, 0x28, 0x98, 0x9e, 0x35, 0xcb, 0xb8, 0x3a, 0xe2, + 0x66, 0xc1, 0xf4, 0x8c, 0x06, 0xd0, 0x4e, 0x78, 0x1a, 0xef, 0x45, 0xae, 0xb0, 0x6d, 0x78, 0x7d, + 0x46, 0x0f, 0xe0, 0x88, 0x2f, 0x91, 0xaa, 0x1c, 0xb7, 0x86, 0xcd, 0x71, 0x7f, 0x82, 0xc9, 0xdf, + 0x0f, 0x12, 0xfd, 0x36, 0xa1, 0x5a, 0x60, 0xa5, 0x87, 0x9e, 0xa0, 0x7d, 0xe6, 0xb9, 0xda, 0x5e, + 0xae, 0x29, 0x76, 0x86, 0xd6, 0xb8, 0x3b, 0x19, 0x90, 0xa2, 0x06, 0xa9, 0x6a, 0x90, 0xa8, 0xaa, + 0xc1, 0x5c, 0xed, 0xb2, 0x6b, 0x3a, 0xfa, 0xb6, 0xa0, 0x65, 0x82, 0x50, 0x17, 0xdc, 0x4d, 0xb8, + 0x08, 0x97, 0xef, 0xa1, 0xf7, 0x0f, 0xdd, 0x40, 0x77, 0xc5, 0xe8, 0xf6, 0x2d, 0x5c, 0x47, 0xd3, + 0x20, 0xf0, 0x2c, 0xe4, 0x41, 0x6f, 0xb5, 0x5c, 0x47, 0x35, 0x69, 0xa0, 0x3e, 0x80, 0x56, 0xe6, + 0x34, 0xa0, 0x11, 0xf5, 0x9a, 0xe6, 0x8a, 0x36, 0x4a, 0x60, 0x57, 0x19, 0x9b, 0xd5, 0x0b, 0x9b, + 0xce, 0xa9, 0xd7, 0xaa, 0x33, 0x2a, 0xe2, 0x18, 0xc2, 0xe8, 0x96, 0x2d, 0x83, 0x60, 0x36, 0x7d, + 0x5e, 0x78, 0x2e, 0xba, 0x85, 0xff, 0xc6, 0xa9, 0x51, 0x7b, 0xd6, 0xf9, 0x70, 0xcb, 0xde, 0x3b, + 0xc7, 0x54, 0x79, 0xfc, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xa4, 0x2e, 0x6f, 0xbd, 0xc8, 0x01, 0x00, + 0x00, } diff --git a/pkg/proto/hapi/release/info.pb.go b/pkg/proto/hapi/release/info.pb.go index d5e843df5..a63a039cd 100644 --- a/pkg/proto/hapi/release/info.pb.go +++ b/pkg/proto/hapi/release/info.pb.go @@ -63,18 +63,19 @@ func init() { func init() { proto.RegisterFile("hapi/release/info.proto", fileDescriptor1) } var fileDescriptor1 = []byte{ - // 208 bytes of a gzipped FileDescriptorProto + // 212 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0x48, 0x2c, 0xc8, 0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0xcf, 0xcc, 0x4b, 0xcb, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x01, 0x49, 0xe8, 0x41, 0x25, 0xa4, 0xe4, 0xd3, 0xf3, 0xf3, 0xd3, 0x73, 0x52, 0xf5, 0xc1, 0x72, 0x49, 0xa5, 0x69, 0xfa, 0x25, 0x99, 0xb9, 0xa9, 0xc5, 0x25, 0x89, - 0xb9, 0x05, 0x10, 0xe5, 0x52, 0x92, 0x28, 0xe6, 0x00, 0x65, 0x4a, 0x4a, 0x8b, 0x21, 0x52, 0x4a, - 0xef, 0x18, 0xb9, 0x58, 0x3c, 0x81, 0x06, 0x0b, 0xe9, 0x70, 0xb1, 0x41, 0x24, 0x24, 0x18, 0x15, - 0x18, 0x35, 0xb8, 0x8d, 0x44, 0xf4, 0x90, 0xed, 0xd0, 0x0b, 0x06, 0xcb, 0x05, 0x41, 0xd5, 0x08, - 0x39, 0x72, 0xf1, 0xa5, 0x65, 0x16, 0x15, 0x97, 0xc4, 0xa7, 0xa4, 0x16, 0xe4, 0xe4, 0x57, 0xa6, - 0xa6, 0x48, 0x30, 0x81, 0x75, 0x49, 0xe9, 0x41, 0xdc, 0xa2, 0x07, 0x73, 0x8b, 0x5e, 0x08, 0xcc, - 0x2d, 0x41, 0xbc, 0x60, 0x1d, 0x2e, 0x50, 0x0d, 0x42, 0xf6, 0x5c, 0xbc, 0x39, 0x89, 0xc8, 0x26, - 0x30, 0x13, 0x34, 0x81, 0x07, 0xa4, 0x01, 0x6e, 0x80, 0x09, 0x17, 0x7b, 0x0a, 0xd0, 0x75, 0x25, - 0x40, 0xad, 0x2c, 0x04, 0xb5, 0xc2, 0x94, 0x3a, 0x71, 0x46, 0xb1, 0x43, 0xfd, 0x94, 0xc4, 0x06, - 0x56, 0x67, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0xeb, 0x9d, 0xa1, 0xf8, 0x67, 0x01, 0x00, 0x00, + 0xb9, 0x05, 0x10, 0xe5, 0x52, 0x92, 0x28, 0xe6, 0x14, 0x97, 0x24, 0x96, 0x94, 0x16, 0x43, 0xa4, + 0x94, 0xde, 0x31, 0x72, 0xb1, 0x78, 0xe6, 0xa5, 0xe5, 0x0b, 0xe9, 0x70, 0xb1, 0x41, 0x24, 0x24, + 0x18, 0x15, 0x18, 0x35, 0xb8, 0x8d, 0x44, 0xf4, 0x90, 0xed, 0xd0, 0x0b, 0x06, 0xcb, 0x05, 0x41, + 0xd5, 0x08, 0x39, 0x72, 0xf1, 0xa5, 0x65, 0x16, 0x15, 0x97, 0xc4, 0xa7, 0xa4, 0x16, 0xe4, 0xe4, + 0x57, 0xa6, 0xa6, 0x48, 0x30, 0x81, 0x75, 0x49, 0xe9, 0x41, 0xdc, 0xa2, 0x07, 0x73, 0x8b, 0x5e, + 0x08, 0xcc, 0x2d, 0x41, 0xbc, 0x60, 0x1d, 0x2e, 0x50, 0x0d, 0x42, 0xf6, 0x5c, 0xbc, 0x39, 0x89, + 0xc8, 0x26, 0x30, 0x13, 0x34, 0x81, 0x07, 0xa4, 0x01, 0x6e, 0x80, 0x09, 0x17, 0x7b, 0x4a, 0x6a, + 0x4e, 0x6a, 0x49, 0x6a, 0x8a, 0x04, 0x0b, 0x41, 0xad, 0x30, 0xa5, 0x4e, 0x9c, 0x51, 0xec, 0x50, + 0x3f, 0x25, 0xb1, 0x81, 0xd5, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xeb, 0x9d, 0xa1, 0xf8, + 0x67, 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/release.pb.go b/pkg/proto/hapi/release/release.pb.go index 561930f1f..72255e3e2 100644 --- a/pkg/proto/hapi/release/release.pb.go +++ b/pkg/proto/hapi/release/release.pb.go @@ -77,21 +77,21 @@ func init() { func init() { proto.RegisterFile("hapi/release/release.proto", fileDescriptor2) } var fileDescriptor2 = []byte{ - // 254 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x90, 0x3f, 0x4f, 0xc3, 0x30, - 0x10, 0xc5, 0xd5, 0x36, 0x7f, 0x9a, 0x83, 0x85, 0x1b, 0xe0, 0x14, 0x31, 0x54, 0x0c, 0x50, 0x31, - 0xa4, 0x12, 0x7c, 0x03, 0x58, 0x60, 0xf5, 0xc8, 0x66, 0x22, 0x87, 0x58, 0x50, 0x3b, 0x8a, 0x23, - 0x3e, 0x0b, 0x1f, 0x17, 0xdb, 0xe7, 0x42, 0x0a, 0x8b, 0x13, 0xbf, 0xdf, 0xd3, 0xbb, 0xe7, 0x83, - 0xba, 0x97, 0x83, 0xde, 0x8d, 0xea, 0x43, 0x49, 0xa7, 0x0e, 0xdf, 0x66, 0x18, 0xed, 0x64, 0xf1, - 0x34, 0xb0, 0x26, 0x69, 0xf5, 0xc5, 0x91, 0xb3, 0xb7, 0xf6, 0x9d, 0x6d, 0x7f, 0x80, 0x36, 0x9d, - 0x3d, 0x02, 0x6d, 0x2f, 0xc7, 0x69, 0xd7, 0x5a, 0xd3, 0xe9, 0xb7, 0x04, 0xce, 0xe7, 0x20, 0x9c, - 0xac, 0x5f, 0x7d, 0x2d, 0xa1, 0x14, 0x9c, 0x83, 0x08, 0x99, 0x91, 0x7b, 0x45, 0x8b, 0xcd, 0x62, - 0x5b, 0x89, 0xf8, 0x8f, 0xd7, 0x90, 0x85, 0x78, 0x5a, 0x7a, 0xed, 0xe4, 0x0e, 0x9b, 0x79, 0xbf, - 0xe6, 0xd9, 0x13, 0x11, 0x39, 0xde, 0x40, 0x1e, 0x63, 0x69, 0x15, 0x8d, 0x67, 0x6c, 0xe4, 0x49, - 0x8f, 0xe1, 0x14, 0xcc, 0xf1, 0x16, 0x0a, 0x2e, 0x46, 0xd9, 0x3c, 0x32, 0x39, 0x23, 0x11, 0xc9, - 0x81, 0x35, 0xac, 0xf7, 0xd2, 0xe8, 0x4e, 0xb9, 0x89, 0xf2, 0x58, 0xea, 0xe7, 0x8e, 0x5b, 0xc8, - 0xc3, 0x42, 0x1c, 0x15, 0x9b, 0xd5, 0xff, 0x66, 0x4f, 0x1e, 0x09, 0x36, 0x20, 0x41, 0xf9, 0xa9, - 0x46, 0xa7, 0xad, 0xa1, 0xd2, 0x87, 0xe4, 0xe2, 0x70, 0xc5, 0x4b, 0xa8, 0xc2, 0x23, 0xdd, 0x20, - 0x5b, 0x45, 0xeb, 0x38, 0xe0, 0x57, 0x78, 0xa8, 0x5e, 0xca, 0x14, 0xf7, 0x5a, 0xc4, 0x65, 0xdd, - 0x7f, 0x07, 0x00, 0x00, 0xff, 0xff, 0xc8, 0x8f, 0xec, 0x97, 0xbb, 0x01, 0x00, 0x00, + // 256 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x90, 0xbf, 0x4e, 0xc3, 0x40, + 0x0c, 0xc6, 0x95, 0x36, 0x7f, 0x1a, 0xc3, 0x82, 0x07, 0xb0, 0x22, 0x86, 0x88, 0x01, 0x22, 0x86, + 0x54, 0x82, 0x37, 0x80, 0x05, 0xd6, 0x1b, 0xd9, 0x8e, 0xe8, 0x42, 0x4e, 0xa5, 0xe7, 0x28, 0x17, + 0xf1, 0x2c, 0x3c, 0x2e, 0xba, 0x3f, 0x85, 0x94, 0x2e, 0x4e, 0xec, 0xdf, 0xa7, 0xcf, 0xdf, 0x19, + 0xaa, 0x41, 0x8e, 0x7a, 0x3b, 0xa9, 0x4f, 0x25, 0xad, 0x3a, 0x7c, 0xdb, 0x71, 0xe2, 0x99, 0xf1, + 0xdc, 0xb1, 0x36, 0xce, 0xaa, 0xab, 0x23, 0xe5, 0xc0, 0xbc, 0x0b, 0xb2, 0x7f, 0x40, 0x9b, 0x9e, + 0x8f, 0x40, 0x37, 0xc8, 0x69, 0xde, 0x76, 0x6c, 0x7a, 0xfd, 0x11, 0xc1, 0xe5, 0x12, 0xb8, 0x1a, + 0xe6, 0x37, 0xdf, 0x2b, 0x28, 0x44, 0xf0, 0x41, 0x84, 0xd4, 0xc8, 0xbd, 0xa2, 0xa4, 0x4e, 0x9a, + 0x52, 0xf8, 0x7f, 0xbc, 0x85, 0xd4, 0xd9, 0xd3, 0xaa, 0x4e, 0x9a, 0xb3, 0x07, 0x6c, 0x97, 0xf9, + 0xda, 0x57, 0xd3, 0xb3, 0xf0, 0x1c, 0xef, 0x20, 0xf3, 0xb6, 0xb4, 0xf6, 0xc2, 0x8b, 0x20, 0x0c, + 0x9b, 0x9e, 0x5d, 0x15, 0x81, 0xe3, 0x3d, 0xe4, 0x21, 0x18, 0xa5, 0x4b, 0xcb, 0xa8, 0xf4, 0x44, + 0x44, 0x05, 0x56, 0xb0, 0xd9, 0x4b, 0xa3, 0x7b, 0x65, 0x67, 0xca, 0x7c, 0xa8, 0xdf, 0x1e, 0x1b, + 0xc8, 0xdc, 0x41, 0x2c, 0xe5, 0xf5, 0xfa, 0x34, 0xd9, 0x0b, 0xf3, 0x4e, 0x04, 0x01, 0x12, 0x14, + 0x5f, 0x6a, 0xb2, 0x9a, 0x0d, 0x15, 0x75, 0xd2, 0x64, 0xe2, 0xd0, 0xe2, 0x35, 0x94, 0xee, 0x91, + 0x76, 0x94, 0x9d, 0xa2, 0x8d, 0x5f, 0xf0, 0x37, 0x78, 0x2a, 0xdf, 0x8a, 0x68, 0xf7, 0x9e, 0xfb, + 0x63, 0x3d, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, 0xc8, 0x8f, 0xec, 0x97, 0xbb, 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/status.pb.go b/pkg/proto/hapi/release/status.pb.go index 6be0f74ac..79c721e31 100644 --- a/pkg/proto/hapi/release/status.pb.go +++ b/pkg/proto/hapi/release/status.pb.go @@ -79,22 +79,22 @@ func init() { func init() { proto.RegisterFile("hapi/release/status.proto", fileDescriptor3) } var fileDescriptor3 = []byte{ - // 259 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0xc1, 0x4e, 0xf2, 0x40, - 0x14, 0x85, 0xff, 0x42, 0xff, 0xd6, 0x5e, 0x08, 0x21, 0x37, 0x2c, 0x5a, 0xe3, 0xc2, 0xb0, 0x72, - 0xe3, 0x6d, 0x82, 0x4f, 0x80, 0x76, 0x4c, 0xd4, 0xa6, 0x90, 0x56, 0x62, 0x74, 0x37, 0xc0, 0x88, - 0x24, 0x4d, 0x87, 0x74, 0xa6, 0x0b, 0x9e, 0xd8, 0xd7, 0x70, 0x3a, 0x85, 0xe8, 0xae, 0xa7, 0xdf, - 0x77, 0xe6, 0xcc, 0x40, 0xf4, 0xc5, 0x0f, 0xfb, 0xb8, 0x16, 0xa5, 0xe0, 0x4a, 0xc4, 0x4a, 0x73, - 0xdd, 0x28, 0x3a, 0xd4, 0x52, 0x4b, 0x1c, 0xb6, 0x88, 0x4e, 0xe8, 0x32, 0xda, 0x49, 0xb9, 0x2b, - 0x45, 0x6c, 0xd9, 0xba, 0xf9, 0x8c, 0x79, 0x75, 0xec, 0xc4, 0xe9, 0xb7, 0x03, 0x5e, 0x61, 0x9b, - 0x78, 0x0b, 0xee, 0x46, 0x6e, 0x45, 0xe8, 0x5c, 0x3b, 0x37, 0xa3, 0x59, 0x44, 0x7f, 0x8f, 0xa0, - 0xce, 0xa1, 0x07, 0x23, 0xe4, 0x56, 0x43, 0x02, 0x7f, 0x2b, 0x34, 0xdf, 0x97, 0x2a, 0xec, 0x99, - 0xc6, 0x60, 0x36, 0xa1, 0x6e, 0x86, 0xce, 0x33, 0x34, 0xaf, 0x8e, 0xf9, 0x59, 0xc2, 0x2b, 0x08, - 0x6a, 0xa1, 0x64, 0x53, 0x6f, 0x84, 0x0a, 0xfb, 0xa6, 0x11, 0xe4, 0xbf, 0x3f, 0x70, 0x02, 0xff, - 0x2b, 0xa9, 0x0d, 0x71, 0x2d, 0xe9, 0xc2, 0xf4, 0x19, 0xdc, 0x76, 0x11, 0x07, 0xe0, 0xaf, 0xb2, - 0x97, 0x6c, 0xf1, 0x96, 0x8d, 0xff, 0xe1, 0x10, 0x2e, 0x12, 0xb6, 0x4c, 0x17, 0xef, 0x2c, 0x19, - 0x3b, 0x2d, 0x4a, 0x58, 0xca, 0x5e, 0x4d, 0xe8, 0xe1, 0x08, 0xa0, 0x58, 0x2d, 0x59, 0x5e, 0xb0, - 0xc4, 0xe4, 0x3e, 0x02, 0x78, 0x8f, 0xf3, 0xa7, 0xd4, 0x7c, 0xbb, 0xf7, 0xc1, 0x87, 0x7f, 0x7a, - 0xcc, 0xda, 0xb3, 0x37, 0xbc, 0xfb, 0x09, 0x00, 0x00, 0xff, 0xff, 0xae, 0x07, 0x47, 0x1f, 0x41, - 0x01, 0x00, 0x00, + // 261 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0xc1, 0x4e, 0x83, 0x40, + 0x10, 0x86, 0xdd, 0x16, 0x41, 0xa6, 0x4d, 0x43, 0x36, 0x3d, 0x80, 0xf1, 0x40, 0x7a, 0xe2, 0xe2, + 0x92, 0xd4, 0x27, 0xa8, 0xee, 0x9a, 0xa8, 0x84, 0x36, 0x60, 0x63, 0xf4, 0x46, 0xcb, 0x58, 0x9b, + 0x10, 0xb6, 0x61, 0x97, 0x43, 0x9f, 0xd8, 0xd7, 0x30, 0x2c, 0x6d, 0xec, 0x71, 0xe6, 0xfb, 0x66, + 0xfe, 0x19, 0x08, 0x7e, 0x8a, 0xc3, 0x3e, 0x6e, 0xb0, 0xc2, 0x42, 0x61, 0xac, 0x74, 0xa1, 0x5b, + 0xc5, 0x0e, 0x8d, 0xd4, 0x92, 0x8e, 0x3b, 0xc4, 0x4e, 0xe8, 0x36, 0xd8, 0x49, 0xb9, 0xab, 0x30, + 0x36, 0x6c, 0xd3, 0x7e, 0xc7, 0x45, 0x7d, 0xec, 0xc5, 0xd9, 0x2f, 0x01, 0x3b, 0x37, 0x93, 0xf4, + 0x1e, 0xac, 0xad, 0x2c, 0xd1, 0x27, 0x21, 0x89, 0x26, 0xf3, 0x80, 0x5d, 0xae, 0x60, 0xbd, 0xc3, + 0x9e, 0x64, 0x89, 0x99, 0xd1, 0x28, 0x03, 0xa7, 0x44, 0x5d, 0xec, 0x2b, 0xe5, 0x0f, 0x42, 0x12, + 0x8d, 0xe6, 0x53, 0xd6, 0xc7, 0xb0, 0x73, 0x0c, 0x5b, 0xd4, 0xc7, 0xec, 0x2c, 0xd1, 0x3b, 0x70, + 0x1b, 0x54, 0xb2, 0x6d, 0xb6, 0xa8, 0xfc, 0x61, 0x48, 0x22, 0x37, 0xfb, 0x6f, 0xd0, 0x29, 0x5c, + 0xd7, 0x52, 0xa3, 0xf2, 0x2d, 0x43, 0xfa, 0x62, 0xf6, 0x0a, 0x56, 0x97, 0x48, 0x47, 0xe0, 0xac, + 0xd3, 0xb7, 0x74, 0xf9, 0x91, 0x7a, 0x57, 0x74, 0x0c, 0x37, 0x5c, 0xac, 0x92, 0xe5, 0xa7, 0xe0, + 0x1e, 0xe9, 0x10, 0x17, 0x89, 0x78, 0x17, 0xdc, 0x1b, 0xd0, 0x09, 0x40, 0xbe, 0x5e, 0x89, 0x2c, + 0x17, 0x5c, 0x70, 0x6f, 0x48, 0x01, 0xec, 0xe7, 0xc5, 0x4b, 0x22, 0xb8, 0x67, 0x3d, 0xba, 0x5f, + 0xce, 0xe9, 0x99, 0x8d, 0x6d, 0x2e, 0x7c, 0xf8, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xae, 0x07, 0x47, + 0x1f, 0x41, 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index b2e540a2f..074418600 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -418,7 +418,7 @@ func (*GetVersionRequest) ProtoMessage() {} func (*GetVersionRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{15} } type GetVersionResponse struct { - Version *hapi_version.Version `protobuf:"bytes,1,opt,name=Version,json=version" json:"Version,omitempty"` + Version *hapi_version.Version `protobuf:"bytes,1,opt,name=Version" json:"Version,omitempty"` } func (m *GetVersionResponse) Reset() { *m = GetVersionResponse{} } @@ -505,7 +505,7 @@ type ReleaseServiceClient interface { ListReleases(ctx context.Context, in *ListReleasesRequest, opts ...grpc.CallOption) (ReleaseService_ListReleasesClient, error) // GetReleasesStatus retrieves status information for the specified release. GetReleaseStatus(ctx context.Context, in *GetReleaseStatusRequest, opts ...grpc.CallOption) (*GetReleaseStatusResponse, error) - // GetReleaseContent retrieves the release content (chart + value) for the specifed release. + // GetReleaseContent retrieves the release content (chart + value) for the specified release. GetReleaseContent(ctx context.Context, in *GetReleaseContentRequest, opts ...grpc.CallOption) (*GetReleaseContentResponse, error) // UpdateRelease updates release content. UpdateRelease(ctx context.Context, in *UpdateReleaseRequest, opts ...grpc.CallOption) (*UpdateReleaseResponse, error) @@ -643,7 +643,7 @@ type ReleaseServiceServer interface { ListReleases(*ListReleasesRequest, ReleaseService_ListReleasesServer) error // GetReleasesStatus retrieves status information for the specified release. GetReleaseStatus(context.Context, *GetReleaseStatusRequest) (*GetReleaseStatusResponse, error) - // GetReleaseContent retrieves the release content (chart + value) for the specifed release. + // GetReleaseContent retrieves the release content (chart + value) for the specified release. GetReleaseContent(context.Context, *GetReleaseContentRequest) (*GetReleaseContentResponse, error) // UpdateRelease updates release content. UpdateRelease(context.Context, *UpdateReleaseRequest) (*UpdateReleaseResponse, error) @@ -878,68 +878,68 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ func init() { proto.RegisterFile("hapi/services/tiller.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 997 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x57, 0x5f, 0x6f, 0xe3, 0x44, - 0x10, 0xaf, 0x93, 0x34, 0x7f, 0xa6, 0x7f, 0x68, 0xf7, 0xda, 0x26, 0xb5, 0x00, 0x9d, 0x8c, 0xe0, - 0xca, 0xc1, 0xa5, 0x10, 0x9e, 0x90, 0x10, 0x52, 0xaf, 0x17, 0xb5, 0xe5, 0x4a, 0x4e, 0xda, 0x50, - 0x90, 0x78, 0x20, 0x72, 0x93, 0xcd, 0xd5, 0x9c, 0xeb, 0x0d, 0xde, 0x4d, 0x75, 0x7d, 0xe7, 0x85, - 0xaf, 0xc1, 0xf7, 0xe0, 0x3b, 0xf1, 0xce, 0x0b, 0xeb, 0xfd, 0xe3, 0xda, 0x8e, 0x9d, 0xf3, 0xe5, - 0x25, 0xde, 0xdd, 0x99, 0xfd, 0xcd, 0xcc, 0x6f, 0x76, 0x66, 0x5a, 0xb0, 0x6f, 0xdc, 0x99, 0x77, - 0xcc, 0x48, 0x78, 0xe7, 0x8d, 0x09, 0x3b, 0xe6, 0x9e, 0xef, 0x93, 0xb0, 0x3b, 0x0b, 0x29, 0xa7, - 0x68, 0x2f, 0x92, 0x75, 0x8d, 0xac, 0xab, 0x64, 0xf6, 0x81, 0xbc, 0x31, 0xbe, 0x71, 0x43, 0xae, - 0x7e, 0x95, 0xb6, 0xdd, 0x4e, 0x9e, 0xd3, 0x60, 0xea, 0xbd, 0xd6, 0x02, 0x65, 0x22, 0x24, 0x3e, - 0x71, 0x19, 0x31, 0xdf, 0xd4, 0x25, 0x23, 0xf3, 0x82, 0x29, 0xd5, 0x82, 0xc3, 0x94, 0x80, 0x71, - 0x97, 0xcf, 0x59, 0x0a, 0xef, 0x8e, 0x84, 0xcc, 0xa3, 0x81, 0xf9, 0x2a, 0x99, 0xf3, 0x77, 0x05, - 0x1e, 0x5d, 0x7a, 0x8c, 0x63, 0x75, 0x91, 0x61, 0xf2, 0xc7, 0x9c, 0x30, 0x8e, 0xf6, 0x60, 0xdd, - 0xf7, 0x6e, 0x3d, 0xde, 0xb1, 0x1e, 0x5b, 0x47, 0x55, 0xac, 0x36, 0xe8, 0x00, 0xea, 0x74, 0x3a, - 0x65, 0x84, 0x77, 0x2a, 0xe2, 0xb8, 0x85, 0xf5, 0x0e, 0x7d, 0x0f, 0x0d, 0x46, 0x43, 0x3e, 0xba, - 0xbe, 0xef, 0x54, 0x85, 0x60, 0xbb, 0xf7, 0x69, 0x37, 0x8f, 0x8a, 0x6e, 0x64, 0x69, 0x28, 0x14, - 0xbb, 0xd1, 0xcf, 0xf3, 0x7b, 0x5c, 0x67, 0xf2, 0x1b, 0xe1, 0x4e, 0x3d, 0x9f, 0x93, 0xb0, 0x53, - 0x53, 0xb8, 0x6a, 0x87, 0xce, 0x00, 0x24, 0x2e, 0x0d, 0x27, 0x42, 0xb6, 0x2e, 0xa1, 0x8f, 0x4a, - 0x40, 0xbf, 0x8a, 0xf4, 0x71, 0x8b, 0x99, 0x25, 0xfa, 0x0e, 0x36, 0x15, 0x25, 0xa3, 0x31, 0x9d, - 0x10, 0xd6, 0xa9, 0x3f, 0xae, 0x0a, 0xa8, 0x43, 0x05, 0x65, 0x18, 0x1e, 0x2a, 0xd2, 0x4e, 0x85, - 0x06, 0xde, 0x50, 0xea, 0xd1, 0x9a, 0x39, 0xbf, 0x41, 0xd3, 0xc0, 0x3b, 0x3d, 0xa8, 0x2b, 0xe7, - 0xd1, 0x06, 0x34, 0xae, 0x06, 0x2f, 0x07, 0xaf, 0x7e, 0x19, 0xec, 0xac, 0xa1, 0x26, 0xd4, 0x06, - 0x27, 0x3f, 0xf6, 0x77, 0x2c, 0xb4, 0x0b, 0x5b, 0x97, 0x27, 0xc3, 0x9f, 0x46, 0xb8, 0x7f, 0xd9, - 0x3f, 0x19, 0xf6, 0x5f, 0xec, 0x54, 0x9c, 0x8f, 0xa1, 0x15, 0x7b, 0x85, 0x1a, 0x50, 0x3d, 0x19, - 0x9e, 0xaa, 0x2b, 0x2f, 0xfa, 0x62, 0x65, 0x39, 0x7f, 0x59, 0xb0, 0x97, 0x4e, 0x02, 0x9b, 0xd1, - 0x80, 0x91, 0x28, 0x0b, 0x63, 0x3a, 0x0f, 0xe2, 0x2c, 0xc8, 0x0d, 0x42, 0x50, 0x0b, 0xc8, 0x5b, - 0x93, 0x03, 0xb9, 0x8e, 0x34, 0x39, 0xe5, 0xae, 0x2f, 0xf9, 0x17, 0x9a, 0x72, 0x83, 0xbe, 0x86, - 0xa6, 0x0e, 0x8e, 0x09, 0x66, 0xab, 0x47, 0x1b, 0xbd, 0xfd, 0x74, 0xc8, 0xda, 0x22, 0x8e, 0xd5, - 0x9c, 0x33, 0x68, 0x9f, 0x11, 0xe3, 0x89, 0x62, 0xc4, 0xbc, 0x89, 0xc8, 0xae, 0x7b, 0x4b, 0xa4, - 0x33, 0x91, 0x5d, 0xb1, 0x46, 0x1d, 0x68, 0xe8, 0x07, 0x25, 0xdd, 0x59, 0xc7, 0x66, 0xeb, 0x70, - 0xe8, 0x2c, 0x02, 0xe9, 0xb8, 0xf2, 0x90, 0x3e, 0x83, 0x5a, 0xf4, 0x9c, 0x25, 0xcc, 0x46, 0x0f, - 0xa5, 0xfd, 0xbc, 0x10, 0x12, 0x2c, 0xe5, 0xe8, 0x43, 0x68, 0x45, 0xfa, 0x6c, 0xe6, 0x8e, 0x89, - 0x8c, 0xb6, 0x85, 0x1f, 0x0e, 0x9c, 0xf3, 0xa4, 0xd5, 0x53, 0x1a, 0x70, 0x12, 0xf0, 0xd5, 0xfc, - 0xbf, 0x84, 0xc3, 0x1c, 0x24, 0x1d, 0xc0, 0x31, 0x34, 0xb4, 0x6b, 0x12, 0xad, 0x90, 0x57, 0xa3, - 0xe5, 0xfc, 0x23, 0x52, 0x7c, 0x35, 0x9b, 0xb8, 0x9c, 0x18, 0xd1, 0x12, 0xa7, 0x9e, 0x88, 0xb4, - 0x47, 0x6d, 0x41, 0x73, 0xb1, 0xab, 0xb0, 0x55, 0xef, 0x38, 0x8d, 0x7e, 0xb1, 0x92, 0xa3, 0xa7, - 0x50, 0xbf, 0x73, 0x7d, 0x81, 0x23, 0x89, 0x88, 0x59, 0xd3, 0x9a, 0xb2, 0xa7, 0x60, 0xad, 0x81, - 0xda, 0xd0, 0x98, 0x84, 0xf7, 0xa3, 0x70, 0x1e, 0xc8, 0x22, 0x6b, 0xe2, 0xba, 0xd8, 0xe2, 0x79, - 0x80, 0x3e, 0x81, 0xad, 0x89, 0xc7, 0xdc, 0x6b, 0x9f, 0x8c, 0x6e, 0x28, 0x7d, 0xc3, 0x64, 0x9d, - 0x35, 0xf1, 0xa6, 0x3e, 0x3c, 0x8f, 0xce, 0x04, 0xaf, 0xfb, 0x19, 0xf7, 0x57, 0x65, 0xe2, 0x4f, - 0x0b, 0x0e, 0x30, 0xf5, 0xfd, 0x6b, 0x77, 0xfc, 0xa6, 0x04, 0x17, 0x09, 0xb7, 0x2b, 0xcb, 0xdd, - 0xae, 0x2e, 0xba, 0x9d, 0x4c, 0x6f, 0x2d, 0x9d, 0xde, 0x1f, 0xa0, 0xbd, 0xe0, 0xc5, 0xaa, 0x21, - 0xfd, 0x67, 0xc1, 0xfe, 0x45, 0x20, 0x3a, 0x86, 0xef, 0x67, 0x22, 0x8a, 0x33, 0x69, 0x95, 0xce, - 0x64, 0xe5, 0x7d, 0x32, 0x59, 0x4d, 0x51, 0x62, 0xf8, 0xab, 0x25, 0xf8, 0x2b, 0x93, 0xdd, 0x74, - 0x4d, 0xd5, 0x33, 0x35, 0x85, 0x3e, 0x02, 0x08, 0xc9, 0x9c, 0x91, 0x91, 0x04, 0x6f, 0xc8, 0xfb, - 0x2d, 0x79, 0x32, 0x10, 0x07, 0xce, 0x05, 0x1c, 0x64, 0x83, 0x5f, 0x95, 0xc8, 0x1b, 0x68, 0x5f, - 0x05, 0x5e, 0x2e, 0x93, 0x79, 0x6f, 0x63, 0x21, 0xb6, 0x4a, 0x4e, 0x6c, 0xa2, 0x33, 0xce, 0xe6, - 0xe1, 0x6b, 0xa2, 0xb9, 0x52, 0x1b, 0xe7, 0x25, 0x74, 0x16, 0x2d, 0xad, 0xea, 0xf6, 0x23, 0xd8, - 0x15, 0xad, 0xe2, 0x67, 0xf5, 0xb2, 0xb4, 0xc3, 0x4e, 0x1f, 0x50, 0xf2, 0xf0, 0x01, 0x5b, 0x1f, - 0xa5, 0xb1, 0xcd, 0x54, 0x36, 0xfa, 0xf1, 0x3b, 0xfd, 0x56, 0x62, 0x9f, 0x8b, 0xe9, 0x40, 0x45, - 0x92, 0x97, 0x90, 0xb1, 0x03, 0xd5, 0x5b, 0xf7, 0xad, 0xee, 0x62, 0xd1, 0x52, 0xb4, 0x72, 0x94, - 0xbc, 0xaa, 0x3d, 0x48, 0xce, 0x04, 0xab, 0xd4, 0x4c, 0xe8, 0xfd, 0xdb, 0x80, 0x6d, 0xd3, 0xc8, - 0xd5, 0xd8, 0x45, 0x1e, 0x6c, 0x26, 0x27, 0x16, 0xfa, 0xbc, 0x78, 0x2a, 0x67, 0xfe, 0xb4, 0xb0, - 0x9f, 0x96, 0x51, 0x55, 0xce, 0x3a, 0x6b, 0x5f, 0x59, 0x88, 0xc1, 0x4e, 0x76, 0x90, 0xa0, 0x67, - 0xf9, 0x18, 0x05, 0x93, 0xcb, 0xee, 0x96, 0x55, 0x37, 0x66, 0xd1, 0x9d, 0xa4, 0x3d, 0xdd, 0xfd, - 0xd1, 0x3b, 0x61, 0xd2, 0x03, 0xc7, 0x3e, 0x2e, 0xad, 0x1f, 0xdb, 0xfd, 0x1d, 0xb6, 0x52, 0x7d, - 0x16, 0x15, 0xb0, 0x95, 0x37, 0x4b, 0xec, 0x2f, 0x4a, 0xe9, 0xc6, 0xb6, 0x6e, 0x61, 0x3b, 0x5d, - 0xb8, 0xa8, 0x00, 0x20, 0xb7, 0xb7, 0xd9, 0x5f, 0x96, 0x53, 0x8e, 0xcd, 0x89, 0x3c, 0x66, 0x4b, - 0xae, 0x28, 0x8f, 0x05, 0x4d, 0xa0, 0x28, 0x8f, 0x45, 0x95, 0x2c, 0x8c, 0xba, 0x00, 0x0f, 0x55, - 0x88, 0x9e, 0x14, 0x26, 0x24, 0x5d, 0xbc, 0xf6, 0xd1, 0xbb, 0x15, 0x63, 0x13, 0x33, 0xf8, 0x20, - 0x33, 0x49, 0x50, 0x01, 0x35, 0xf9, 0x63, 0xcf, 0x7e, 0x56, 0x52, 0x3b, 0x13, 0x94, 0x2e, 0xec, - 0x25, 0x41, 0xa5, 0xbb, 0xc6, 0x92, 0xa0, 0x32, 0x3d, 0xc2, 0x59, 0x7b, 0x0e, 0xbf, 0x36, 0x8d, - 0xde, 0x75, 0x5d, 0xfe, 0xab, 0xf0, 0xcd, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0xdb, 0x8a, 0x4d, - 0xa4, 0xfb, 0x0c, 0x00, 0x00, + // 1004 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x57, 0xdf, 0x6f, 0xe3, 0xc4, + 0x13, 0xaf, 0x93, 0x34, 0x3f, 0xa6, 0x3f, 0xbe, 0xe9, 0x5e, 0xda, 0xb8, 0xd6, 0x17, 0x14, 0x19, + 0xc1, 0x85, 0x83, 0x4b, 0x21, 0x3c, 0x21, 0x21, 0xa4, 0x5e, 0x2e, 0x4a, 0xcb, 0x95, 0x9c, 0xb4, + 0xa1, 0x20, 0xf1, 0x40, 0xe4, 0x26, 0x9b, 0xab, 0x39, 0xc7, 0x1b, 0xbc, 0x9b, 0xea, 0xf2, 0xce, + 0x0b, 0xff, 0x06, 0xff, 0x07, 0xff, 0x13, 0xef, 0xbc, 0x20, 0xef, 0x0f, 0x37, 0x76, 0xec, 0x9c, + 0xc9, 0x8b, 0xed, 0xdd, 0x99, 0xfd, 0xcc, 0xcc, 0x67, 0x76, 0x66, 0x12, 0xb0, 0xee, 0x9d, 0x85, + 0x7b, 0xc1, 0x48, 0xf0, 0xe0, 0x4e, 0x08, 0xbb, 0xe0, 0xae, 0xe7, 0x91, 0xa0, 0xb3, 0x08, 0x28, + 0xa7, 0xa8, 0x11, 0xca, 0x3a, 0x5a, 0xd6, 0x91, 0x32, 0xeb, 0x4c, 0x9c, 0x98, 0xdc, 0x3b, 0x01, + 0x97, 0x4f, 0xa9, 0x6d, 0x35, 0xd7, 0xf7, 0xa9, 0x3f, 0x73, 0xdf, 0x28, 0x81, 0x34, 0x11, 0x10, + 0x8f, 0x38, 0x8c, 0xe8, 0x77, 0xec, 0x90, 0x96, 0xb9, 0xfe, 0x8c, 0x2a, 0xc1, 0x79, 0x4c, 0xc0, + 0xb8, 0xc3, 0x97, 0x2c, 0x86, 0xf7, 0x40, 0x02, 0xe6, 0x52, 0x5f, 0xbf, 0xa5, 0xcc, 0xfe, 0xb3, + 0x00, 0x4f, 0x6e, 0x5c, 0xc6, 0xb1, 0x3c, 0xc8, 0x30, 0xf9, 0x6d, 0x49, 0x18, 0x47, 0x0d, 0xd8, + 0xf7, 0xdc, 0xb9, 0xcb, 0x4d, 0xa3, 0x65, 0xb4, 0x8b, 0x58, 0x2e, 0xd0, 0x19, 0x94, 0xe9, 0x6c, + 0xc6, 0x08, 0x37, 0x0b, 0x2d, 0xa3, 0x5d, 0xc3, 0x6a, 0x85, 0xbe, 0x85, 0x0a, 0xa3, 0x01, 0x1f, + 0xdf, 0xad, 0xcc, 0x62, 0xcb, 0x68, 0x1f, 0x77, 0x3f, 0xee, 0xa4, 0x51, 0xd1, 0x09, 0x2d, 0x8d, + 0x68, 0xc0, 0x3b, 0xe1, 0xe3, 0xc5, 0x0a, 0x97, 0x99, 0x78, 0x87, 0xb8, 0x33, 0xd7, 0xe3, 0x24, + 0x30, 0x4b, 0x12, 0x57, 0xae, 0xd0, 0x00, 0x40, 0xe0, 0xd2, 0x60, 0x4a, 0x02, 0x73, 0x5f, 0x40, + 0xb7, 0x73, 0x40, 0xbf, 0x0e, 0xf5, 0x71, 0x8d, 0xe9, 0x4f, 0xf4, 0x0d, 0x1c, 0x4a, 0x4a, 0xc6, + 0x13, 0x3a, 0x25, 0xcc, 0x2c, 0xb7, 0x8a, 0xed, 0xe3, 0xee, 0xb9, 0x84, 0xd2, 0x0c, 0x8f, 0x24, + 0x69, 0x3d, 0x3a, 0x25, 0xf8, 0x40, 0xaa, 0x87, 0xdf, 0xcc, 0xfe, 0x05, 0xaa, 0x1a, 0xde, 0xee, + 0x42, 0x59, 0x3a, 0x8f, 0x0e, 0xa0, 0x72, 0x3b, 0x7c, 0x35, 0x7c, 0xfd, 0xd3, 0xb0, 0xbe, 0x87, + 0xaa, 0x50, 0x1a, 0x5e, 0x7e, 0xdf, 0xaf, 0x1b, 0xe8, 0x04, 0x8e, 0x6e, 0x2e, 0x47, 0x3f, 0x8c, + 0x71, 0xff, 0xa6, 0x7f, 0x39, 0xea, 0xbf, 0xac, 0x17, 0xec, 0x0f, 0xa1, 0x16, 0x79, 0x85, 0x2a, + 0x50, 0xbc, 0x1c, 0xf5, 0xe4, 0x91, 0x97, 0xfd, 0x51, 0xaf, 0x6e, 0xd8, 0x7f, 0x18, 0xd0, 0x88, + 0x27, 0x81, 0x2d, 0xa8, 0xcf, 0x48, 0x98, 0x85, 0x09, 0x5d, 0xfa, 0x51, 0x16, 0xc4, 0x02, 0x21, + 0x28, 0xf9, 0xe4, 0x9d, 0xce, 0x81, 0xf8, 0x0e, 0x35, 0x39, 0xe5, 0x8e, 0x27, 0xf8, 0x2f, 0x62, + 0xb9, 0x40, 0x5f, 0x42, 0x55, 0x05, 0xc7, 0xcc, 0x52, 0xab, 0xd8, 0x3e, 0xe8, 0x9e, 0xc6, 0x43, + 0x56, 0x16, 0x71, 0xa4, 0x66, 0x0f, 0xa0, 0x39, 0x20, 0xda, 0x13, 0xc9, 0x88, 0xbe, 0x13, 0xa1, + 0x5d, 0x67, 0x4e, 0x84, 0x33, 0xa1, 0x5d, 0x67, 0x4e, 0x90, 0x09, 0x15, 0x75, 0xa1, 0x84, 0x3b, + 0xfb, 0x58, 0x2f, 0x6d, 0x0e, 0xe6, 0x26, 0x90, 0x8a, 0x2b, 0x0d, 0xe9, 0x13, 0x28, 0x85, 0xd7, + 0x59, 0xc0, 0x1c, 0x74, 0x51, 0xdc, 0xcf, 0x6b, 0x7f, 0x46, 0xb1, 0x90, 0xa3, 0xff, 0x43, 0x2d, + 0xd4, 0x67, 0x0b, 0x67, 0x42, 0x44, 0xb4, 0x35, 0xfc, 0xb8, 0x61, 0x5f, 0xad, 0x5b, 0xed, 0x51, + 0x9f, 0x13, 0x9f, 0xef, 0xe6, 0xff, 0x0d, 0x9c, 0xa7, 0x20, 0xa9, 0x00, 0x2e, 0xa0, 0xa2, 0x5c, + 0x13, 0x68, 0x99, 0xbc, 0x6a, 0x2d, 0xfb, 0x2f, 0x03, 0x1a, 0xb7, 0x8b, 0xa9, 0xc3, 0x89, 0x16, + 0x6d, 0x71, 0xea, 0x29, 0xec, 0x8b, 0xb6, 0xa0, 0xb8, 0x38, 0x91, 0xd8, 0xb2, 0x77, 0xf4, 0xc2, + 0x27, 0x96, 0x72, 0xf4, 0x0c, 0xca, 0x0f, 0x8e, 0xb7, 0x24, 0x4c, 0x10, 0x11, 0xb1, 0xa6, 0x34, + 0x45, 0x4f, 0xc1, 0x4a, 0x03, 0x35, 0xa1, 0x32, 0x0d, 0x56, 0xe3, 0x60, 0xe9, 0x8b, 0x22, 0xab, + 0xe2, 0xf2, 0x34, 0x58, 0xe1, 0xa5, 0x8f, 0x3e, 0x82, 0xa3, 0xa9, 0xcb, 0x9c, 0x3b, 0x8f, 0x8c, + 0xef, 0x29, 0x7d, 0xcb, 0x44, 0x9d, 0x55, 0xf1, 0xa1, 0xda, 0xbc, 0x0a, 0xf7, 0xec, 0x2b, 0x38, + 0x4d, 0xb8, 0xbf, 0x2b, 0x13, 0xbf, 0x1b, 0x70, 0x86, 0xa9, 0xe7, 0xdd, 0x39, 0x93, 0xb7, 0x39, + 0xb8, 0x58, 0x73, 0xbb, 0xb0, 0xdd, 0xed, 0xe2, 0xa6, 0xdb, 0xeb, 0xe9, 0x2d, 0xc5, 0xd3, 0xfb, + 0x1d, 0x34, 0x37, 0xbc, 0xd8, 0x35, 0xa4, 0x7f, 0x0c, 0x38, 0xbd, 0xf6, 0x19, 0x77, 0x3c, 0x2f, + 0x11, 0x51, 0x94, 0x49, 0x23, 0x77, 0x26, 0x0b, 0xff, 0x25, 0x93, 0xc5, 0x18, 0x25, 0x9a, 0xbf, + 0xd2, 0x1a, 0x7f, 0x79, 0xb2, 0x1b, 0xaf, 0xa9, 0x72, 0xa2, 0xa6, 0xd0, 0x07, 0x00, 0x01, 0x59, + 0x32, 0x32, 0x16, 0xe0, 0x15, 0x71, 0xbe, 0x26, 0x76, 0x86, 0xce, 0x9c, 0xd8, 0xd7, 0x70, 0x96, + 0x0c, 0x7e, 0x57, 0x22, 0xef, 0xa1, 0x79, 0xeb, 0xbb, 0xa9, 0x4c, 0xa6, 0xdd, 0x8d, 0x8d, 0xd8, + 0x0a, 0x29, 0xb1, 0x35, 0x60, 0x7f, 0xb1, 0x0c, 0xde, 0x10, 0xc5, 0x95, 0x5c, 0xd8, 0xaf, 0xc0, + 0xdc, 0xb4, 0xb4, 0xab, 0xdb, 0x4f, 0xe0, 0x64, 0x40, 0xf8, 0x8f, 0xf2, 0x66, 0x29, 0x87, 0xed, + 0x3e, 0xa0, 0xf5, 0xcd, 0x47, 0x6c, 0xb5, 0x15, 0xc7, 0xd6, 0x53, 0x59, 0xeb, 0x6b, 0x2d, 0xfb, + 0x6b, 0x81, 0x7d, 0xe5, 0x32, 0x4e, 0x83, 0xd5, 0x36, 0x32, 0xea, 0x50, 0x9c, 0x3b, 0xef, 0x54, + 0x17, 0x0b, 0x3f, 0xed, 0x81, 0xf0, 0x20, 0x3a, 0xaa, 0x3c, 0x58, 0x9f, 0x09, 0x46, 0xae, 0x99, + 0xd0, 0xfd, 0xbb, 0x02, 0xc7, 0xba, 0x91, 0xcb, 0xb1, 0x8b, 0x5c, 0x38, 0x5c, 0x9f, 0x58, 0xe8, + 0xd3, 0xec, 0xa9, 0x9c, 0xf8, 0x69, 0x61, 0x3d, 0xcb, 0xa3, 0x2a, 0x9d, 0xb5, 0xf7, 0xbe, 0x30, + 0x10, 0x83, 0x7a, 0x72, 0x90, 0xa0, 0xe7, 0xe9, 0x18, 0x19, 0x93, 0xcb, 0xea, 0xe4, 0x55, 0xd7, + 0x66, 0xd1, 0x83, 0xa0, 0x3d, 0xde, 0xfd, 0xd1, 0x7b, 0x61, 0xe2, 0x03, 0xc7, 0xba, 0xc8, 0xad, + 0x1f, 0xd9, 0xfd, 0x15, 0x8e, 0x62, 0x7d, 0x16, 0x65, 0xb0, 0x95, 0x36, 0x4b, 0xac, 0xcf, 0x72, + 0xe9, 0x46, 0xb6, 0xe6, 0x70, 0x1c, 0x2f, 0x5c, 0x94, 0x01, 0x90, 0xda, 0xdb, 0xac, 0xcf, 0xf3, + 0x29, 0x47, 0xe6, 0x18, 0xd4, 0x93, 0x25, 0x97, 0x95, 0xc7, 0x8c, 0x26, 0x90, 0x95, 0xc7, 0xac, + 0x4a, 0xb6, 0xf7, 0x90, 0x03, 0xf0, 0x58, 0x85, 0xe8, 0x69, 0x66, 0x42, 0xe2, 0xc5, 0x6b, 0xb5, + 0xdf, 0xaf, 0x18, 0x99, 0x58, 0xc0, 0xff, 0x12, 0x93, 0x04, 0x65, 0x50, 0x93, 0x3e, 0xf6, 0xac, + 0xe7, 0x39, 0xb5, 0x13, 0x41, 0xa9, 0xc2, 0xde, 0x12, 0x54, 0xbc, 0x6b, 0x6c, 0x09, 0x2a, 0xd1, + 0x23, 0xec, 0xbd, 0x17, 0xf0, 0x73, 0x55, 0xeb, 0xdd, 0x95, 0xc5, 0x5f, 0x85, 0xaf, 0xfe, 0x0d, + 0x00, 0x00, 0xff, 0xff, 0x9a, 0xb0, 0xe9, 0x29, 0xfb, 0x0c, 0x00, 0x00, } diff --git a/pkg/proto/hapi/version/version.pb.go b/pkg/proto/hapi/version/version.pb.go index 4677764e4..79771408e 100644 --- a/pkg/proto/hapi/version/version.pb.go +++ b/pkg/proto/hapi/version/version.pb.go @@ -47,14 +47,15 @@ func init() { func init() { proto.RegisterFile("hapi/version/version.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 144 bytes of a gzipped FileDescriptorProto + // 151 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xca, 0x48, 0x2c, 0xc8, 0xd4, 0x2f, 0x4b, 0x2d, 0x2a, 0xce, 0xcc, 0xcf, 0x83, 0xd1, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x3c, 0x20, 0x39, 0x3d, 0xa8, 0x98, 0x52, 0x3a, 0x17, 0x7b, 0x18, 0x84, 0x29, 0x24, 0xce, - 0xc5, 0x5e, 0x9c, 0x9a, 0x1b, 0x0f, 0x94, 0x91, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x62, 0x03, - 0x72, 0x81, 0x92, 0x42, 0xb2, 0x5c, 0x5c, 0xe9, 0x99, 0x25, 0xf1, 0xc9, 0xf9, 0xb9, 0xb9, 0x99, - 0x25, 0x12, 0x4c, 0x60, 0x39, 0x4e, 0xa0, 0x88, 0x33, 0x58, 0x40, 0x48, 0x85, 0x8b, 0x0f, 0x24, - 0x5d, 0x52, 0x94, 0x9a, 0x1a, 0x5f, 0x5c, 0x92, 0x58, 0x92, 0x2a, 0xc1, 0x0c, 0x56, 0xc2, 0x03, - 0x14, 0x0d, 0x01, 0x0a, 0x06, 0x83, 0xc4, 0x9c, 0x38, 0xa3, 0xd8, 0xa1, 0x76, 0x26, 0xb1, 0x81, - 0x1d, 0x62, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x20, 0xcc, 0x0e, 0x1b, 0xa6, 0x00, 0x00, 0x00, + 0xc5, 0x5e, 0x9c, 0x9a, 0x1b, 0x5f, 0x96, 0x5a, 0x24, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x19, 0xc4, + 0x56, 0x9c, 0x9a, 0x1b, 0x96, 0x5a, 0x24, 0x24, 0xcb, 0xc5, 0x95, 0x9e, 0x59, 0x12, 0x9f, 0x9c, + 0x9f, 0x9b, 0x9b, 0x59, 0x22, 0xc1, 0x04, 0x96, 0xe3, 0x4c, 0xcf, 0x2c, 0x71, 0x06, 0x0b, 0x08, + 0xa9, 0x70, 0xf1, 0x81, 0xa4, 0x4b, 0x8a, 0x52, 0x53, 0xe3, 0x8b, 0x4b, 0x12, 0x4b, 0x52, 0x25, + 0x98, 0xc1, 0x4a, 0x78, 0xd2, 0x33, 0x4b, 0x42, 0x8a, 0x52, 0x53, 0x83, 0x41, 0x62, 0x4e, 0x9c, + 0x51, 0xec, 0x50, 0x3b, 0x93, 0xd8, 0xc0, 0x0e, 0x31, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x20, + 0xcc, 0x0e, 0x1b, 0xa6, 0x00, 0x00, 0x00, } From 9b91996c8ad4604eb07bb9a5808774ce79811bea Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Mon, 10 Oct 2016 17:08:26 -0700 Subject: [PATCH 169/183] fix(Makefile): explicitly set bash shell in Makefile fixes #1302 --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 3a9e7e341..87d9f7c12 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,9 @@ GOFLAGS := BINDIR := $(CURDIR)/bin BINARIES := helm tiller +# Required for globs to work correctly +SHELL=/bin/bash + .PHONY: all all: build From 995f7569c9a8274568168a153f799028f26b0ab2 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 11 Oct 2016 11:00:35 -0700 Subject: [PATCH 170/183] feat(tiller): add optional grpc tracing --- cmd/tiller/tiller.go | 23 +++++++++++------ cmd/tiller/trace.go | 60 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 cmd/tiller/trace.go diff --git a/cmd/tiller/tiller.go b/cmd/tiller/tiller.go index 2c0e1dd9a..b00cddba0 100644 --- a/cmd/tiller/tiller.go +++ b/cmd/tiller/tiller.go @@ -46,9 +46,11 @@ var rootServer = grpc.NewServer() var env = environment.New() var ( - addr = ":44134" - probe = ":44135" - store = storageConfigMap + grpcAddr = ":44134" + probeAddr = ":44135" + traceAddr = ":44136" + enableTracing = false + store = storageConfigMap ) const globalUsage = `The Kubernetes Helm server. @@ -67,8 +69,9 @@ var rootCommand = &cobra.Command{ func main() { pf := rootCommand.PersistentFlags() - pf.StringVarP(&addr, "listen", "l", ":44134", "The address:port to listen on") + pf.StringVarP(&grpcAddr, "listen", "l", ":44134", "The address:port to listen on") pf.StringVar(&store, "storage", storageConfigMap, "The storage driver to use. One of 'configmap' or 'memory'") + pf.BoolVar(&enableTracing, "trace", false, "Enable rpc tracing") rootCommand.Execute() } @@ -84,16 +87,20 @@ func start(c *cobra.Command, args []string) { env.Releases = storage.Init(driver.NewConfigMaps(c.ConfigMaps(environment.TillerNamespace))) } - lstn, err := net.Listen("tcp", addr) + lstn, err := net.Listen("tcp", grpcAddr) if err != nil { fmt.Fprintf(os.Stderr, "Server died: %s\n", err) os.Exit(1) } - fmt.Printf("Tiller is running on %s\n", addr) - fmt.Printf("Tiller probes server is running on %s\n", probe) + fmt.Printf("Tiller is listening on %s\n", grpcAddr) + fmt.Printf("Probes server is listening on %s\n", probeAddr) fmt.Printf("Storage driver is %s\n", env.Releases.Name()) + if enableTracing { + startTracing(traceAddr) + } + srvErrCh := make(chan error) probeErrCh := make(chan error) go func() { @@ -104,7 +111,7 @@ func start(c *cobra.Command, args []string) { go func() { mux := newProbesMux() - if err := http.ListenAndServe(probe, mux); err != nil { + if err := http.ListenAndServe(probeAddr, mux); err != nil { probeErrCh <- err } }() diff --git a/cmd/tiller/trace.go b/cmd/tiller/trace.go new file mode 100644 index 000000000..b9e0583f2 --- /dev/null +++ b/cmd/tiller/trace.go @@ -0,0 +1,60 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main // import "k8s.io/helm/cmd/tiller" + +import ( + "fmt" + "log" + "net/http" + + _ "net/http/pprof" + + "google.golang.org/grpc" +) + +func startTracing(addr string) { + fmt.Printf("Tracing server is listening on %s\n", addr) + grpc.EnableTracing = true + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(traceIndexHTML)) + }) + + go func() { + if err := http.ListenAndServe(addr, nil); err != nil { + log.Printf("tracing error: %s", err) + } + }() +} + +const traceIndexHTML = ` + + +

+ + +` From a2e577814d440ddd4d680bdf4633ad423b77fe47 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 11 Oct 2016 11:33:47 -0700 Subject: [PATCH 171/183] feat(helm): add kubeconfig context switching closes: #1127 --- cmd/helm/helm.go | 8 +++++--- cmd/helm/tunnel.go | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 540eb3871..678314441 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -35,8 +35,9 @@ const ( ) var ( - helmHome string - tillerHost string + helmHome string + tillerHost string + kubeContext string ) // flagDebug is a signal that the user wants additional output. @@ -82,6 +83,7 @@ func newRootCmd(out io.Writer) *cobra.Command { p := cmd.PersistentFlags() p.StringVar(&helmHome, "home", home, "location of your Helm config. Overrides $HELM_HOME") p.StringVar(&tillerHost, "host", thost, "address of tiller. Overrides $HELM_HOST") + p.StringVar(&kubeContext, "kube-context", "", "name of the kubeconfig context to use") p.BoolVarP(&flagDebug, "debug", "", false, "enable verbose output") rup := newRepoUpdateCmd(out) @@ -124,7 +126,7 @@ func main() { func setupConnection(c *cobra.Command, args []string) error { if tillerHost == "" { - tunnel, err := newTillerPortForwarder(tillerNamespace) + tunnel, err := newTillerPortForwarder(tillerNamespace, kubeContext) if err != nil { return err } diff --git a/cmd/helm/tunnel.go b/cmd/helm/tunnel.go index f5be0cfff..b6b7fbebd 100644 --- a/cmd/helm/tunnel.go +++ b/cmd/helm/tunnel.go @@ -21,6 +21,7 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" "k8s.io/kubernetes/pkg/labels" "k8s.io/helm/pkg/kube" @@ -29,8 +30,17 @@ import ( // TODO refactor out this global var var tunnel *kube.Tunnel -func newTillerPortForwarder(namespace string) (*kube.Tunnel, error) { - kc := kube.New(nil) +func getKubeConfig(context string) clientcmd.ClientConfig { + rules := clientcmd.NewDefaultClientConfigLoadingRules() + overrides := &clientcmd.ConfigOverrides{} + if context != "" { + overrides.CurrentContext = context + } + return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides) +} + +func newTillerPortForwarder(namespace, context string) (*kube.Tunnel, error) { + kc := kube.New(getKubeConfig(context)) client, err := kc.Client() if err != nil { return nil, err From a0d3e43b5333bb563419ce73c5464ab3d9b4ddab Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 11 Oct 2016 12:37:22 -0700 Subject: [PATCH 172/183] fix(Makefile): allow overriding the version for canary builds --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 87d9f7c12..741c49cdb 100644 --- a/Makefile +++ b/Makefile @@ -36,8 +36,8 @@ dist: cd _dist && \ $(DIST_DIRS) cp ../LICENSE {} \; && \ $(DIST_DIRS) cp ../README.md {} \; && \ - $(DIST_DIRS) tar -zcf helm-${GIT_TAG}-{}.tar.gz {} \; && \ - $(DIST_DIRS) zip -r helm-${GIT_TAG}-{}.zip {} \; \ + $(DIST_DIRS) tar -zcf helm-${VERSION}-{}.tar.gz {} \; && \ + $(DIST_DIRS) zip -r helm-${VERSION}-{}.zip {} \; \ ) .PHONY: checksum From 5accd5a3ab988fe5650f3317e1e6629e4dcde690 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 11 Oct 2016 13:50:18 -0600 Subject: [PATCH 173/183] fix(k8s): Update to latest Kubernetes release --- glide.lock | 6 +++--- glide.yaml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/glide.lock b/glide.lock index 7d023b61a..bab587bd7 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: d9c66d934b1b6906646654d536190df6bc2d8c9dfa6b414a91b893846fb5d86e -updated: 2016-10-10T11:23:48.277967613-07:00 +hash: 0b56505a7d2b0bde1a8aba9c4ac52ef18ea1eae6d46157db598e5a1051b64cf5 +updated: 2016-10-11T12:54:05.869559929-06:00 imports: - name: bitbucket.org/ww/goautoneg version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 @@ -434,7 +434,7 @@ imports: - 1.4/tools/metrics - 1.4/transport - name: k8s.io/kubernetes - version: bd85192c2673153b3b470a2f158ddc840d295768 + version: ef16c3f8079df0654c8336741134ba142846ec13 subpackages: - federation/apis/federation - federation/apis/federation/install diff --git a/glide.yaml b/glide.yaml index 4feaea68c..e7a599b4d 100644 --- a/glide.yaml +++ b/glide.yaml @@ -22,7 +22,7 @@ import: - package: google.golang.org/grpc version: v1.0.1-GA - package: k8s.io/kubernetes - version: ~1.4 + version: ~1.4.1 subpackages: - pkg/api - pkg/api/meta From 11d4a67a808edc7ba1b38e71512caac1e2c2c6f2 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 11 Oct 2016 17:27:10 -0600 Subject: [PATCH 174/183] docs(repo): update repo index docs to show new flags This adds the `--url` flag to examples. Closes #1343 --- docs/chart_repository.md | 2 +- docs/chart_repository_sync_example.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/chart_repository.md b/docs/chart_repository.md index 900d45d8d..bd00f9b12 100644 --- a/docs/chart_repository.md +++ b/docs/chart_repository.md @@ -124,7 +124,7 @@ $ mv alpine-0.1.0.tgz fantastic-charts/ Outside of your directory, run the `helm repo index [DIR] [URL]` command. This command takes the path of the local directory that you just created and the URL of your remote chart repository and composes an index.yaml file inside the given directory path. ```console -$ helm repo index fantastic-charts https://storage.googleapis.com/fantastic-charts +$ helm repo index fantastic-charts --url https://storage.googleapis.com/fantastic-charts ``` Now, you can upload the chart and the index file to your chart repository using a sync tool or manually. If you're using Google Cloud Storage, check out this [example workflow](chart_repository_sync_example.md) using the gsutil client. diff --git a/docs/chart_repository_sync_example.md b/docs/chart_repository_sync_example.md index 41e5523f4..9ca87a3ea 100644 --- a/docs/chart_repository_sync_example.md +++ b/docs/chart_repository_sync_example.md @@ -19,7 +19,7 @@ $ mv alpine-0.1.0.tgz fantastic-charts/ Use helm to generate an updated index.yaml file by passing in the directory path and the url of the remote repository to the `helm repo index` command like this: ```console -$ helm repo index fantastic-charts/ https://storage.googleapis.com/fantastic-charts +$ helm repo index fantastic-charts/ --url https://storage.googleapis.com/fantastic-charts ``` This will generate an updated index.yaml file and place in the `fantastic-charts/` directory. From 2388e71528f782c430f876a0858a19d7a2896a54 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 11 Oct 2016 18:59:45 -0600 Subject: [PATCH 175/183] fix(helm): ignore dotfiles in charts/ directories This causes 'helm dep [up|install]' to ignore files in charts/ that start with either a dot or an underscore. It also changes the chartloader to ignore those files. Also, if a 'helm dep up' does not find a charts/ directory, it creates one. Closes #1342 --- cmd/helm/downloader/manager.go | 16 ++++++++++++++-- docs/charts.md | 6 +++++- pkg/chartutil/load.go | 8 +++++++- pkg/chartutil/testdata/frobnitz-1.2.3.tgz | Bin 4025 -> 4105 bytes .../testdata/frobnitz/charts/_ignore_me | 1 + .../frobnitz/charts/mariner-4.3.2.tgz | Bin 1031 -> 1034 bytes .../mariner/charts/albatross-0.1.0.tgz | Bin 347 -> 347 bytes 7 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 pkg/chartutil/testdata/frobnitz/charts/_ignore_me diff --git a/cmd/helm/downloader/manager.go b/cmd/helm/downloader/manager.go index c140504fc..4e4ddebf2 100644 --- a/cmd/helm/downloader/manager.go +++ b/cmd/helm/downloader/manager.go @@ -173,6 +173,17 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { HelmHome: m.HelmHome, } + destPath := filepath.Join(m.ChartPath, "charts") + + // Create 'charts' directory if it doesn't already exist. + if fi, err := os.Stat(destPath); err != nil { + if err := os.MkdirAll(destPath, 0755); err != nil { + return err + } + } else if !fi.IsDir() { + return fmt.Errorf("%q is not a directory", destPath) + } + fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) for _, dep := range deps { fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) @@ -183,8 +194,7 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { continue } - dest := filepath.Join(m.ChartPath, "charts") - if _, _, err := dl.DownloadTo(churl, "", dest); err != nil { + if _, _, err := dl.DownloadTo(churl, "", destPath); err != nil { fmt.Fprintf(m.Out, "WARNING: Could not download %s: %s (skipped)", churl, err) continue } @@ -257,6 +267,8 @@ func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) { } // urlsAreEqual normalizes two URLs and then compares for equality. +// +// TODO: This and the urlJoin functions should really be moved to a 'urlutil' package. func urlsAreEqual(a, b string) bool { au, err := url.Parse(a) if err != nil { diff --git a/docs/charts.md b/docs/charts.md index 1ee1cebe9..f94fdb22d 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -115,6 +115,10 @@ In Helm, one chart may depend on any number of other charts. These dependencies are expressed explicitly by copying the dependency charts into the `charts/` directory. +A dependency can be either a chart archive (`foo-1.2.3.tgz`) or an +unpacked chart directory. But its name cannot start with `_` or `.`. +Such files are ignored by the chart loader. + **Note:** The `dependencies:` section of the `Chart.yaml` from Helm Classic has been completely removed. @@ -141,7 +145,7 @@ on Apache and MySQL by including those charts inside of its `charts/` directory. **TIP:** _To drop a dependency into your `charts/` directory, use the -`helm fetch` command._ +`helm fetch` command or use a `requirements.yaml` file_ ### Managing Dependencies with `requirements.yaml` diff --git a/pkg/chartutil/load.go b/pkg/chartutil/load.go index 05bc1187b..dba1100e1 100644 --- a/pkg/chartutil/load.go +++ b/pkg/chartutil/load.go @@ -125,6 +125,10 @@ func loadFiles(files []*afile) (*chart.Chart, error) { continue } cname := strings.TrimPrefix(f.name, "charts/") + if strings.IndexAny(cname, "._") == 0 { + // Ignore charts/ that start with . or _. + continue + } parts := strings.SplitN(cname, "/", 2) scname := parts[0] subcharts[scname] = append(subcharts[scname], &afile{name: cname, data: f.data}) @@ -141,7 +145,9 @@ func loadFiles(files []*afile) (*chart.Chart, error) { for n, files := range subcharts { var sc *chart.Chart var err error - if filepath.Ext(n) == ".tgz" { + if strings.IndexAny(n, "_.") == 0 { + continue + } else if filepath.Ext(n) == ".tgz" { file := files[0] if file.name != n { return c, fmt.Errorf("error unpacking tar in %s: expected %s, got %s", c.Metadata.Name, n, file.name) diff --git a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz index 07b1113649c3fad6538ec8f284eacfd3f49524c5..aaf443dbaadcca47c145e7194ac8243e3975c89f 100644 GIT binary patch literal 4105 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 literal 4025 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 K!&6THC;$N4ss1zo delta 986 zcmV<0110>52!{v^ABzY8+wAjK00ZrpkqjVzVhEaa?r!dG>prftbA|KOm{^rrNTLt`YB?{eK*}W$(@U`> zAR*xti6js{XFu947Wf4`Koxybdmp@l*q zKMS}hawFJC62q}WU;$W&ObbN3nE_%B2FtjFOIP5C^A2AVEmd4F!W}CA5+z;v(cWXHvO4)QWzAi z$QwVGMm7FnEJI4q+5S(P2ue6+luiJWEQK3PSO0=6p6>sWNI$IpE5XzXpQ=17wTpu@ z-v5is{(67a&Xf3~fB$}r>FQsV<=p$fu4yve79>H_L{|Tmpt|d_z|j+94+bt7{o+{b zyI@w;x3h=qHeL7W6SdhpKe=a7RfaowWOGZaxOpJcZ#2)?IPv0$Hh#w~eXBp)d3;M- z@0vY3M*lb#UA?e*-?LjoZ$96(jx$m-?ei?64 z&19&5+x+L6v*~QZ!7qER%7EpE+piy-cDzSWg+d9*Q5Gdt;#bP)E=2hb9({tY19ozz-T{{yZzx@Zt9EvEDYc z_l1%0Xw$xHzdk(hur=?MhN+Kpw=+H?cvnE(I) diff --git a/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz b/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz index 5fb374a789567b5f1518f1b00c3d4b75c29f7e4a..0b66d27fe1bb83cde374420591ce39b5ee58543f 100644 GIT binary patch delta 16 Xcmcc3beoA?zMF$#Ufth~>>-Q*F$x8m delta 16 Xcmcc3beoA?zMF&L_M6We*+UosH5Udu From 1b5c99385396b84d46a196a9a1c7e2f20fdf9ade Mon Sep 17 00:00:00 2001 From: Michal Rostecki Date: Wed, 12 Oct 2016 14:40:59 +0200 Subject: [PATCH 176/183] fix(helm): remove unused consts from client Const variables defined in client.go weren't used anywhere. Ref #953 --- pkg/helm/client.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/pkg/helm/client.go b/pkg/helm/client.go index 16fb2e118..bdf687933 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -24,20 +24,6 @@ import ( rls "k8s.io/helm/pkg/proto/hapi/services" ) -const ( - // HelmHostEnvVar is the $HELM_HOST envvar - HelmHostEnvVar = "HELM_HOST" - - // HelmHomeEnvVar is the $HELM_HOME envvar - HelmHomeEnvVar = "HELM_HOME" - - // DefaultHelmHost is the default tiller server host address. - DefaultHelmHost = ":44134" - - // DefaultHelmHome is the default $HELM_HOME envvar value - DefaultHelmHome = "$HOME/.helm" -) - // Client manages client side of the helm-tiller protocol type Client struct { opts options From 24fe8643ded4520dcc005cd7a4014f3f5895fac8 Mon Sep 17 00:00:00 2001 From: fibonacci1729 Date: Tue, 11 Oct 2016 13:06:20 -0600 Subject: [PATCH 177/183] ref(*): add sorter/filter utilties to releaseutil --- cmd/tiller/release_history.go | 12 +--- cmd/tiller/release_server.go | 33 ++--------- pkg/{storage => releaseutil}/filter.go | 15 ++++- pkg/releaseutil/filter_test.go | 58 ++++++++++++++++++ pkg/releaseutil/sorter.go | 77 ++++++++++++++++++++++++ pkg/releaseutil/sorter_test.go | 81 ++++++++++++++++++++++++++ pkg/storage/doc.go | 22 ------- pkg/storage/storage.go | 13 +++-- 8 files changed, 243 insertions(+), 68 deletions(-) rename pkg/{storage => releaseutil}/filter.go (81%) create mode 100644 pkg/releaseutil/filter_test.go create mode 100644 pkg/releaseutil/sorter.go create mode 100644 pkg/releaseutil/sorter_test.go delete mode 100644 pkg/storage/doc.go diff --git a/cmd/tiller/release_history.go b/cmd/tiller/release_history.go index 70176b42e..99bb3bb1a 100644 --- a/cmd/tiller/release_history.go +++ b/cmd/tiller/release_history.go @@ -17,11 +17,9 @@ limitations under the License. package main import ( - "sort" - "golang.org/x/net/context" - rpb "k8s.io/helm/pkg/proto/hapi/release" tpb "k8s.io/helm/pkg/proto/hapi/services" + relutil "k8s.io/helm/pkg/releaseutil" ) func (s *releaseServer) GetHistory(ctx context.Context, req *tpb.GetHistoryRequest) (*tpb.GetHistoryResponse, error) { @@ -34,7 +32,7 @@ func (s *releaseServer) GetHistory(ctx context.Context, req *tpb.GetHistoryReque return nil, err } - sort.Sort(sort.Reverse(byRev(h))) + relutil.Reverse(h, relutil.SortByRevision) var resp tpb.GetHistoryResponse for i := 0; i < min(len(h), int(req.Max)); i++ { @@ -44,12 +42,6 @@ func (s *releaseServer) GetHistory(ctx context.Context, req *tpb.GetHistoryReque return &resp, nil } -type byRev []*rpb.Release - -func (s byRev) Len() int { return len(s) } -func (s byRev) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (s byRev) Less(i, j int) bool { return s[i].Version < s[j].Version } - func min(x, y int) int { if x < y { return x diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 850fedd72..60a245459 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -22,7 +22,6 @@ import ( "fmt" "log" "regexp" - "sort" "strings" "google.golang.org/grpc/metadata" @@ -36,6 +35,7 @@ import ( "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/services" + relutil "k8s.io/helm/pkg/releaseutil" "k8s.io/helm/pkg/storage/driver" "k8s.io/helm/pkg/timeconv" "k8s.io/helm/pkg/version" @@ -123,9 +123,9 @@ func (s *releaseServer) ListReleases(req *services.ListReleasesRequest, stream s switch req.SortBy { case services.ListSort_NAME: - sort.Sort(byName(rels)) + relutil.SortByName(rels) case services.ListSort_LAST_RELEASED: - sort.Sort(byDate(rels)) + relutil.SortByDate(rels) } if req.SortOrder == services.ListSort_DESC { @@ -480,8 +480,8 @@ func (s *releaseServer) prepareRollback(req *services.RollbackReleaseRequest) (* return nil, nil, errors.New("no revision to rollback") } - sort.Sort(sort.Reverse(byRev(h))) - crls := h[0] + relutil.SortByRevision(h) + crls := h[len(h)-1] rbv := req.Version if req.Version == 0 { @@ -878,29 +878,6 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR return res, nil } -// byName implements the sort.Interface for []*release.Release. -type byName []*release.Release - -func (r byName) Len() int { - return len(r) -} -func (r byName) Swap(p, q int) { - r[p], r[q] = r[q], r[p] -} -func (r byName) Less(i, j int) bool { - return r[i].Name < r[j].Name -} - -type byDate []*release.Release - -func (r byDate) Len() int { return len(r) } -func (r byDate) Swap(p, q int) { - r[p], r[q] = r[q], r[p] -} -func (r byDate) Less(p, q int) bool { - return r[p].Info.LastDeployed.Seconds < r[q].Info.LastDeployed.Seconds -} - 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 diff --git a/pkg/storage/filter.go b/pkg/releaseutil/filter.go similarity index 81% rename from pkg/storage/filter.go rename to pkg/releaseutil/filter.go index c23f8c94d..fdd2cc381 100644 --- a/pkg/storage/filter.go +++ b/pkg/releaseutil/filter.go @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package storage // import "k8s.io/helm/pkg/storage" +package releaseutil // import "k8s.io/helm/pkg/releaseutil" import rspb "k8s.io/helm/pkg/proto/hapi/release" // FilterFunc returns true if the release object satisfies -// the predicate of the underlying func. +// the predicate of the underlying filter func. type FilterFunc func(*rspb.Release) bool // Check applies the FilterFunc to the release object. @@ -30,6 +30,17 @@ func (fn FilterFunc) Check(rls *rspb.Release) bool { return fn(rls) } +// Filter applies the filter(s) to the list of provided releases +// returning the list that satisfies the filtering predicate. +func (fn FilterFunc) Filter(rels []*rspb.Release) (rets []*rspb.Release) { + for _, rel := range rels { + if fn.Check(rel) { + rets = append(rets, rel) + } + } + return +} + // Any returns a FilterFunc that filters a list of releases // determined by the predicate 'f0 || f1 || ... || fn'. func Any(filters ...FilterFunc) FilterFunc { diff --git a/pkg/releaseutil/filter_test.go b/pkg/releaseutil/filter_test.go new file mode 100644 index 000000000..88cf88aa9 --- /dev/null +++ b/pkg/releaseutil/filter_test.go @@ -0,0 +1,58 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package releaseutil // import "k8s.io/helm/pkg/releaseutil" + +import ( + rspb "k8s.io/helm/pkg/proto/hapi/release" + "testing" +) + +func TestFilterAny(t *testing.T) { + ls := Any(StatusFilter(rspb.Status_DELETED)).Filter(releases) + if len(ls) != 2 { + t.Fatalf("expected 2 results, got '%d'", len(ls)) + } + + r0, r1 := ls[0], ls[1] + switch { + case r0.Info.Status.Code != rspb.Status_DELETED: + t.Fatalf("expected DELETED result, got '%s'", r1.Info.Status.Code) + case r1.Info.Status.Code != rspb.Status_DELETED: + t.Fatalf("expected DELETED result, got '%s'", r1.Info.Status.Code) + } +} + +func TestFilterAll(t *testing.T) { + fn := FilterFunc(func(rls *rspb.Release) bool { + // true if not deleted and version < 4 + v0 := !StatusFilter(rspb.Status_DELETED).Check(rls) + v1 := rls.Version < 4 + return v0 && v1 + }) + + ls := All(fn).Filter(releases) + if len(ls) != 1 { + t.Fatalf("expected 1 result, got '%d'", len(ls)) + } + + switch r0 := ls[0]; { + case r0.Version == 4: + t.Fatal("got release with status revision 4") + case r0.Info.Status.Code == rspb.Status_DELETED: + t.Fatal("got release with status DELTED") + } +} diff --git a/pkg/releaseutil/sorter.go b/pkg/releaseutil/sorter.go new file mode 100644 index 000000000..1b744d72c --- /dev/null +++ b/pkg/releaseutil/sorter.go @@ -0,0 +1,77 @@ +/* +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 releaseutil // import "k8s.io/helm/pkg/releaseutil" + +import ( + "sort" + + rspb "k8s.io/helm/pkg/proto/hapi/release" +) + +type sorter struct { + list []*rspb.Release + less func(int, int) bool +} + +func (s *sorter) Len() int { return len(s.list) } +func (s *sorter) Less(i, j int) bool { return s.less(i, j) } +func (s *sorter) Swap(i, j int) { s.list[i], s.list[j] = s.list[j], s.list[i] } + +// Reverse reverses the list of releases sorted by the sort func. +func Reverse(list []*rspb.Release, sortFn func([]*rspb.Release)) { + sortFn(list) + for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 { + list[i], list[j] = list[j], list[i] + } +} + +// SortByName returns the list of releases sorted +// in lexicographical order. +func SortByName(list []*rspb.Release) { + s := &sorter{list: list} + s.less = func(i, j int) bool { + ni := s.list[i].Name + nj := s.list[j].Name + return ni < nj + } + sort.Sort(s) +} + +// SortByDate returns the list of releases sorted by a +// release's last deployed time (in seconds). +func SortByDate(list []*rspb.Release) { + s := &sorter{list: list} + + s.less = func(i, j int) bool { + ti := s.list[i].Info.LastDeployed.Seconds + tj := s.list[j].Info.LastDeployed.Seconds + return ti < tj + } + sort.Sort(s) +} + +// SortByRevision returns the list of releases sorted by a +// release's revision number (release.Version). +func SortByRevision(list []*rspb.Release) { + s := &sorter{list: list} + s.less = func(i, j int) bool { + vi := s.list[i].Version + vj := s.list[j].Version + return vi < vj + } + sort.Sort(s) +} diff --git a/pkg/releaseutil/sorter_test.go b/pkg/releaseutil/sorter_test.go new file mode 100644 index 000000000..526108107 --- /dev/null +++ b/pkg/releaseutil/sorter_test.go @@ -0,0 +1,81 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package releaseutil // import "k8s.io/helm/pkg/releaseutil" + +import ( + rspb "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/timeconv" + "testing" + "time" +) + +// note: this test data is shared with filter_test.go. + +var releases = []*rspb.Release{ + tsRelease("quiet-bear", 2, 2000, rspb.Status_SUPERSEDED), + tsRelease("angry-bird", 4, 3000, rspb.Status_DEPLOYED), + tsRelease("happy-cats", 1, 4000, rspb.Status_DELETED), + tsRelease("vocal-dogs", 3, 6000, rspb.Status_DELETED), +} + +func tsRelease(name string, vers int32, dur time.Duration, code rspb.Status_Code) *rspb.Release { + tmsp := timeconv.Timestamp(time.Now().Add(time.Duration(dur))) + info := &rspb.Info{Status: &rspb.Status{Code: code}, LastDeployed: tmsp} + return &rspb.Release{ + Name: name, + Version: vers, + Info: info, + } +} + +func check(t *testing.T, by string, fn func(int, int) bool) { + for i := len(releases) - 1; i > 0; i-- { + if fn(i, i-1) { + t.Errorf("release at positions '(%d,%d)' not sorted by %s", i-1, i, by) + } + } +} + +func TestSortByName(t *testing.T) { + SortByName(releases) + + check(t, "ByName", func(i, j int) bool { + ni := releases[i].Name + nj := releases[j].Name + return ni < nj + }) +} + +func TestSortByDate(t *testing.T) { + SortByDate(releases) + + check(t, "ByDate", func(i, j int) bool { + ti := releases[i].Info.LastDeployed.Seconds + tj := releases[j].Info.LastDeployed.Seconds + return ti < tj + }) +} + +func TestSortByRevision(t *testing.T) { + SortByRevision(releases) + + check(t, "ByRevision", func(i, j int) bool { + vi := releases[i].Version + vj := releases[j].Version + return vi < vj + }) +} diff --git a/pkg/storage/doc.go b/pkg/storage/doc.go deleted file mode 100644 index 231e30bb9..000000000 --- a/pkg/storage/doc.go +++ /dev/null @@ -1,22 +0,0 @@ -/* -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 storage implements storage for Tiller objects.The backend storage -mechanism may be implemented with different backends. This package and its -subpackages provide storage layers for Tiller objects. -*/ -package storage // import "k8s.io/helm/pkg/storage" diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index a7ee01fd5..25ab6dae4 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -21,6 +21,7 @@ import ( "log" rspb "k8s.io/helm/pkg/proto/hapi/release" + relutil "k8s.io/helm/pkg/releaseutil" "k8s.io/helm/pkg/storage/driver" ) @@ -73,7 +74,7 @@ func (s *Storage) ListReleases() ([]*rspb.Release, error) { func (s *Storage) ListDeleted() ([]*rspb.Release, error) { log.Println("List deleted releases in storage") return s.Driver.List(func(rls *rspb.Release) bool { - return StatusFilter(rspb.Status_DELETED).Check(rls) + return relutil.StatusFilter(rspb.Status_DELETED).Check(rls) }) } @@ -82,27 +83,27 @@ func (s *Storage) ListDeleted() ([]*rspb.Release, error) { func (s *Storage) ListDeployed() ([]*rspb.Release, error) { log.Println("Listing all deployed releases in storage") return s.Driver.List(func(rls *rspb.Release) bool { - return StatusFilter(rspb.Status_DEPLOYED).Check(rls) + return relutil.StatusFilter(rspb.Status_DEPLOYED).Check(rls) }) } // ListFilterAll returns the set of releases satisfying satisfying the predicate // (filter0 && filter1 && ... && filterN), i.e. a Release is included in the results // if and only if all filters return true. -func (s *Storage) ListFilterAll(filters ...FilterFunc) ([]*rspb.Release, error) { +func (s *Storage) ListFilterAll(fns ...relutil.FilterFunc) ([]*rspb.Release, error) { log.Println("Listing all releases with filter") return s.Driver.List(func(rls *rspb.Release) bool { - return All(filters...).Check(rls) + return relutil.All(fns...).Check(rls) }) } // ListFilterAny returns the set of releases satisfying satisfying the predicate // (filter0 || filter1 || ... || filterN), i.e. a Release is included in the results // if at least one of the filters returns true. -func (s *Storage) ListFilterAny(filters ...FilterFunc) ([]*rspb.Release, error) { +func (s *Storage) ListFilterAny(fns ...relutil.FilterFunc) ([]*rspb.Release, error) { log.Println("Listing any releases with filter") return s.Driver.List(func(rls *rspb.Release) bool { - return Any(filters...).Check(rls) + return relutil.Any(fns...).Check(rls) }) } From 864d2783396cc6a888c774004e2181d3410478e0 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 12 Oct 2016 12:58:19 -0600 Subject: [PATCH 178/183] fix(helm): prevent 'helm history' from segfaulting An release that does not contain chart metadata cannot print its chart name/version. This fixes a bug found in the wild where a release did not (for reasons yet unknown) contain a chart. Closes #1348 --- cmd/helm/history.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/cmd/helm/history.go b/cmd/helm/history.go index e2c9eef43..da3cd663d 100644 --- a/cmd/helm/history.go +++ b/cmd/helm/history.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/timeconv" ) @@ -98,7 +99,7 @@ func formatHistory(rls []*release.Release) string { tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART") for i := len(rls) - 1; i >= 0; i-- { r := rls[i] - c := fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version) + c := formatChartname(r.Chart) t := timeconv.String(r.Info.LastDeployed) s := r.Info.Status.Code.String() v := r.Version @@ -106,3 +107,12 @@ func formatHistory(rls []*release.Release) string { } return tbl.String() } + +func formatChartname(c *chart.Chart) string { + if c == nil || c.Metadata == nil { + // This is an edge case that has happened in prod, though we don't + // know how: https://github.com/kubernetes/helm/issues/1347 + return "MISSING" + } + return fmt.Sprintf("%s-%s", c.Metadata.Name, c.Metadata.Version) +} From d379eda7852d9ba978078231aa6598dad207d084 Mon Sep 17 00:00:00 2001 From: Erik Christensen Date: Wed, 12 Oct 2016 15:07:26 -0400 Subject: [PATCH 179/183] Fixed broken link from "chart.md" to "charts.md" --- docs/using_helm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using_helm.md b/docs/using_helm.md index 5665bc5e4..41ae8b444 100644 --- a/docs/using_helm.md +++ b/docs/using_helm.md @@ -344,7 +344,7 @@ sure your Helm client is up to date by running `helm repo update`. ## Creating Your Own Charts -The [Chart Development Guide](chart.md) explains how to develop your own +The [Chart Development Guide](charts.md) explains how to develop your own charts. But you can get started quickly by using the `helm create` command: From f5c07ea0cfd6d681f074202773d4c146facd5e5f Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Wed, 12 Oct 2016 16:55:27 -0700 Subject: [PATCH 180/183] docs(developers): require glide 0.12.0 ref: #1310 --- docs/developers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developers.md b/docs/developers.md index 5f41ce045..a88abcb4e 100644 --- a/docs/developers.md +++ b/docs/developers.md @@ -6,7 +6,7 @@ Helm and Tiller. ## Prerequisites - Go 1.6.0 or later -- Glide 0.10.2 or later +- Glide 0.12.0 or later - kubectl 1.2 or later - A Kubernetes cluster (optional) - The gRPC toolchain From 57b2e6429e9888f84977e0fa4aebc307f57cbd4a Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Tue, 11 Oct 2016 12:30:54 -0400 Subject: [PATCH 181/183] bug(tiller): set status correctly in performUpdate Handling release status updates on errors better resolves #1137 --- cmd/tiller/release_server.go | 15 +++++---- cmd/tiller/release_server_test.go | 51 +++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 850fedd72..5f05a5783 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -278,12 +278,12 @@ func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateRelease res, err := s.performUpdate(currentRelease, updatedRelease, req) if err != nil { - return nil, err + return res, err } if !req.DryRun { if err := s.env.Releases.Create(updatedRelease); err != nil { - return nil, err + return res, err } } @@ -306,7 +306,12 @@ func (s *releaseServer) performUpdate(originalRelease, updatedRelease *release.R } if err := s.performKubeUpdate(originalRelease, updatedRelease); err != nil { - return nil, err + log.Printf("warning: Release Upgrade %q failed: %s", updatedRelease.Name, err) + originalRelease.Info.Status.Code = release.Status_SUPERSEDED + updatedRelease.Info.Status.Code = release.Status_FAILED + s.recordRelease(originalRelease, true) + s.recordRelease(updatedRelease, false) + return res, err } // post-upgrade hooks @@ -317,9 +322,7 @@ func (s *releaseServer) performUpdate(originalRelease, updatedRelease *release.R } originalRelease.Info.Status.Code = release.Status_SUPERSEDED - if err := s.env.Releases.Update(originalRelease); err != nil { - return nil, fmt.Errorf("Update of %s failed: %s", originalRelease.Name, err) - } + s.recordRelease(originalRelease, true) updatedRelease.Info.Status.Code = release.Status_DEPLOYED diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index d63a52e42..72b054c20 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -587,6 +587,42 @@ func TestUpdateRelease(t *testing.T) { } } +func TestUpdateReleaseFailure(t *testing.T) { + c := helm.NewContext() + rs := rsFixture() + rel := releaseStub() + rs.env.Releases.Create(rel) + rs.env.KubeClient = newUpdateFailingKubeClient() + + req := &services.UpdateReleaseRequest{ + Name: rel.Name, + DisableHooks: true, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{Name: "hello"}, + Templates: []*chart.Template{ + {Name: "something", Data: []byte("hello: world")}, + }, + }, + } + + res, err := rs.UpdateRelease(c, req) + if err == nil { + t.Error("Expected failed update") + } + + if updatedStatus := res.Release.Info.Status.Code; updatedStatus != release.Status_FAILED { + t.Errorf("Expected FAILED release. Got %d", updatedStatus) + } + + oldRelease, err := rs.env.Releases.Get(rel.Name, rel.Version) + if err != nil { + t.Errorf("Expected to be able to get previous release") + } + if oldStatus := oldRelease.Info.Status.Code; oldStatus != release.Status_SUPERSEDED { + t.Errorf("Expected SUPERSEDED status on previous Release version. Got %v", oldStatus) + } +} + func TestUpdateReleaseNoHooks(t *testing.T) { c := helm.NewContext() rs := rsFixture() @@ -1136,6 +1172,21 @@ func mockEnvironment() *environment.Environment { return e } +func newUpdateFailingKubeClient() *updateFailingKubeClient { + return &updateFailingKubeClient{ + PrintingKubeClient: environment.PrintingKubeClient{Out: os.Stdout}, + } + +} + +type updateFailingKubeClient struct { + environment.PrintingKubeClient +} + +func (u *updateFailingKubeClient) Update(namespace string, originalReader, modifiedReader io.Reader) error { + return errors.New("Failed update in kube client") +} + func newHookFailingKubeClient() *hookFailingKubeClient { return &hookFailingKubeClient{ PrintingKubeClient: environment.PrintingKubeClient{Out: os.Stdout}, From 1ad84745f8ccd23cb7cd3a2afc67d18a7145fde6 Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Thu, 13 Oct 2016 09:38:06 -0400 Subject: [PATCH 182/183] bug(tiller): correct release statuses on rollback Correcting similar issue as the one with UpdateRelease (#1137) for RollbackRelase --- cmd/tiller/release_server.go | 19 ++++++++++-------- cmd/tiller/release_server_test.go | 33 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 0897853c9..a9e6cafb5 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -407,18 +407,18 @@ func (s *releaseServer) RollbackRelease(c ctx.Context, req *services.RollbackRel return nil, err } - rel, err := s.performRollback(currentRelease, targetRelease, req) + res, err := s.performRollback(currentRelease, targetRelease, req) if err != nil { - return nil, err + return res, err } if !req.DryRun { if err := s.env.Releases.Create(targetRelease); err != nil { - return nil, err + return res, err } } - return rel, nil + return res, nil } func (s *releaseServer) performRollback(currentRelease, targetRelease *release.Release, req *services.RollbackReleaseRequest) (*services.RollbackReleaseResponse, error) { @@ -437,7 +437,12 @@ func (s *releaseServer) performRollback(currentRelease, targetRelease *release.R } if err := s.performKubeUpdate(currentRelease, targetRelease); err != nil { - return nil, err + log.Printf("warning: Release Rollback %q failed: %s", targetRelease.Name, err) + currentRelease.Info.Status.Code = release.Status_SUPERSEDED + targetRelease.Info.Status.Code = release.Status_FAILED + s.recordRelease(currentRelease, true) + s.recordRelease(targetRelease, false) + return res, err } // post-rollback hooks @@ -448,9 +453,7 @@ func (s *releaseServer) performRollback(currentRelease, targetRelease *release.R } currentRelease.Info.Status.Code = release.Status_SUPERSEDED - if err := s.env.Releases.Update(currentRelease); err != nil { - return nil, fmt.Errorf("Update of %s failed: %s", currentRelease.Name, err) - } + s.recordRelease(currentRelease, true) targetRelease.Info.Status.Code = release.Status_DEPLOYED diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 72b054c20..e3073c189 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -623,6 +623,39 @@ func TestUpdateReleaseFailure(t *testing.T) { } } +func TestRollbackReleaseFailure(t *testing.T) { + c := helm.NewContext() + rs := rsFixture() + rel := releaseStub() + rs.env.Releases.Create(rel) + upgradedRel := upgradeReleaseVersion(rel) + rs.env.Releases.Update(rel) + rs.env.Releases.Create(upgradedRel) + + req := &services.RollbackReleaseRequest{ + Name: rel.Name, + DisableHooks: true, + } + + rs.env.KubeClient = newUpdateFailingKubeClient() + res, err := rs.RollbackRelease(c, req) + if err == nil { + t.Error("Expected failed rollback") + } + + if targetStatus := res.Release.Info.Status.Code; targetStatus != release.Status_FAILED { + t.Errorf("Expected FAILED release. Got %v", targetStatus) + } + + oldRelease, err := rs.env.Releases.Get(rel.Name, rel.Version) + if err != nil { + t.Errorf("Expected to be able to get previous release") + } + if oldStatus := oldRelease.Info.Status.Code; oldStatus != release.Status_SUPERSEDED { + t.Errorf("Expected SUPERSEDED status on previous Release version. Got %v", oldStatus) + } +} + func TestUpdateReleaseNoHooks(t *testing.T) { c := helm.NewContext() rs := rsFixture() From 491dc325521976046a7738250802efcc32e5c400 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Thu, 13 Oct 2016 10:21:23 -0600 Subject: [PATCH 183/183] docs(chart_repositories): document correct index example The previous index.yaml example was missing a few fields, notably the 'created' timestamp. Closes #1348 --- docs/chart_repository.md | 69 +++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/docs/chart_repository.md b/docs/chart_repository.md index bd00f9b12..1c65348b3 100644 --- a/docs/chart_repository.md +++ b/docs/chart_repository.md @@ -18,46 +18,43 @@ For example, if a repository lives at the URL: `https://helm-charts.com`, the `a The index file is a yaml file called `index.yaml`. It contains some metadata about the package as well as a dump of the Chart.yaml file of a packaged chart. A valid chart repository must have an index file. The index file contains information about each chart in the chart repository. The `helm repo index` command will generate an index file based on a given local directory that contains packaged charts. This is an example of an index file: + ``` apiVersion: v1 entries: - nginx: - - urls: - - http://storage.googleapis.com/kubernetes-charts/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: - - http://storage.googleapis.com/kubernetes-charts/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: - - http://storage.googleapis.com/kubernetes-charts/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" + - created: 2016-10-06T16:23:20.499814565-06:00 + description: Deploy a basic Alpine Linux pod + digest: 99c76e403d752c84ead610644d4b1c2f2b453a74b921f422b9dcb8a7c8b559cd + home: https://k8s.io/helm + name: alpine + sources: + - https://github.com/kubernetes/helm + urls: + - https://technosophos.github.io/tscharts/alpine-0.2.0.tgz + version: 0.2.0 + - created: 2016-10-06T16:23:20.499543808-06:00 + description: Deploy a basic Alpine Linux pod + digest: 515c58e5f79d8b2913a10cb400ebb6fa9c77fe813287afbacf1a0b897cd78727 + home: https://k8s.io/helm + name: alpine + sources: + - https://github.com/kubernetes/helm + urls: + - https://technosophos.github.io/tscharts/alpine-0.1.0.tgz + version: 0.1.0 + nginx: + - created: 2016-10-06T16:23:20.499543808-06:00 + description: Create a basic nginx HTTP server + digest: aaff4545f79d8b2913a10cb400ebb6fa9c77fe813287afbacf1a0b897cdffffff + home: https://k8s.io/helm + name: nginx + sources: + - https://github.com/kubernetes/charts + urls: + - https://technosophos.github.io/tscharts/nginx-1.1.0.tgz + version: 1.1.0 +generated: 2016-10-06T16:23:20.499029981-06:00 ``` We will go through detailed GCS and Github Pages examples here, but feel free to skip to the next section if you've already created a chart repository.