diff --git a/Makefile b/Makefile index bc8e7f6b6..d9582abe4 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 @@ -13,6 +15,9 @@ GOFLAGS := BINDIR := $(CURDIR)/bin BINARIES := helm tiller +# Required for globs to work correctly +SHELL=/bin/bash + .PHONY: all all: build @@ -20,9 +25,26 @@ 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" -arch="amd64 386" $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/... + gox -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/helm + +.PHONY: dist +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 {} \; \ + ) + +.PHONY: checksum +checksum: + for f in _dist/*.{gz,zip} ; do \ + shasum -a 256 "$${f}" | awk '{print $$1}' > "$${f}.sha256" ; \ + done .PHONY: check-docker check-docker: @@ -50,7 +72,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: @@ -63,7 +85,7 @@ protoc: .PHONY: clean clean: - @rm -rf $(BINDIR) + @rm -rf $(BINDIR) ./rootfs/tiller ./_dist @rm -rf ./rootfs/tiller .PHONY: coverage @@ -89,7 +111,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/Makefile.orig b/Makefile.orig new file mode 100644 index 000000000..e1758b177 --- /dev/null +++ b/Makefile.orig @@ -0,0 +1,121 @@ +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 +PKG := $(shell glide novendor) +TAGS := +TESTS := . +TESTFLAGS := +LDFLAGS := +GOFLAGS := +BINDIR := $(CURDIR)/bin +BINARIES := helm tiller + +# Required for globs to work correctly +SHELL=/bin/bash + +.PHONY: all +all: build + +.PHONY: 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}}" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/helm + +.PHONY: dist +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 {} \; \ + ) + +.PHONY: checksum +checksum: + for f in _dist/*.{gz,zip} ; do \ + shasum -a 256 "$${f}" | awk '{print $$1}' > "$${f}.sha256" ; \ + done + +.PHONY: check-docker +check-docker: + @if [ -z $$(which docker) ]; then \ + echo "Missing \`docker\` client which is required for development"; \ + exit 2; \ + fi + +.PHONY: docker-binary +docker-binary: BINDIR = ./rootfs +docker-binary: GOFLAGS += -a -installsuffix cgo +docker-binary: + GOOS=linux GOARCH=amd64 CGO_ENABLED=0 $(GO) build -o $(BINDIR)/tiller $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/tiller + +.PHONY: docker-build +docker-build: check-docker docker-binary + docker build --rm -t ${IMAGE} rootfs + docker tag ${IMAGE} ${MUTABLE_IMAGE} + +.PHONY: test +test: build +test: TESTFLAGS += -race -v +test: test-style +test: test-unit + +.PHONY: test-unit +test-unit: + HELM_HOME=/no/such/dir $(GO) test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS) + +.PHONY: test-style +test-style: + @scripts/validate-go.sh + @scripts/validate-license.sh + +.PHONY: protoc +protoc: + $(MAKE) -C _proto/ all + +.PHONY: clean +clean: +<<<<<<< HEAD + @rm -rf $(BINDIR) + @rm -rf ./rootfs/tiller +======= + @rm -rf $(BINDIR) ./rootfs/tiller ./_dist +>>>>>>> master + +.PHONY: coverage +coverage: + @scripts/coverage.sh + +HAS_GLIDE := $(shell command -v glide;) +HAS_GOX := $(shell command -v gox;) +HAS_HG := $(shell command -v hg;) +HAS_GIT := $(shell command -v git;) + +.PHONY: bootstrap +bootstrap: +ifndef HAS_GLIDE + go get -u github.com/Masterminds/glide +endif +ifndef HAS_GOX + go get -u github.com/mitchellh/gox +endif +ifndef HAS_HG + $(error You must install Mercurial (hg)) +endif +ifndef HAS_GIT + $(error You must install Git) +endif + 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/README.md b/README.md index ecf4bf6b7..6d9acdd32 100644 --- a/README.md +++ b/README.md @@ -28,75 +28,43 @@ 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: +## Install -```console -$ helm init # Initialize Helm as well as the Tiller server -$ 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 -``` +Binary downloads of the Alpha.5 Helm client can be found at the following links: -## Install +- [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) +- [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) +- [Glossary](docs/glossary.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 + +### Code of conduct -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! - -### 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. - -## 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. +Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). 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/chart/metadata.proto b/_proto/hapi/chart/metadata.proto index 884f458db..da194e9d3 100644 --- a/_proto/hapi/chart/metadata.proto +++ b/_proto/hapi/chart/metadata.proto @@ -58,4 +58,10 @@ 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; + + // The API Version of this chart. + string apiVersion = 10; } 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/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 ba3252e3c..d3fd05337 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -20,6 +20,8 @@ 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"; +import "hapi/version/version.proto"; option go_package = "services"; @@ -49,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) { } @@ -64,6 +66,18 @@ 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) { + } + + // RollbackRelease rolls back a release to a previous version. + rpc RollbackRelease(RollbackReleaseRequest) returns (RollbackReleaseResponse) { + } + + // ReleaseHistory retrieves a releasse's history. + rpc GetHistory(GetHistoryRequest) returns (GetHistoryResponse) { + } } // ListReleasesRequest requests a list of releases. @@ -90,7 +104,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. @@ -129,6 +146,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. @@ -138,12 +157,17 @@ 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. 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. @@ -162,7 +186,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; } @@ -172,6 +195,22 @@ 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; + // Version is the version of the release to deploy. + int32 version = 4; +} + +// 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. @@ -209,6 +248,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. @@ -216,3 +257,24 @@ 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; +} + +// 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/_proto/hapi/version/version.proto b/_proto/hapi/version/version.proto new file mode 100644 index 000000000..0ae0985b7 --- /dev/null +++ b/_proto/hapi/version/version.proto @@ -0,0 +1,26 @@ +// 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 { + // Sem ver string for the version + string sem_ver = 1; + string git_commit = 2; + string git_tree_state = 3; +} diff --git a/circle.yml b/circle.yml index 09db231ba..db4bec459 100644 --- a/circle.yml +++ b/circle.yml @@ -3,22 +3,27 @@ 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.1" GOPATH: "${HOME}/.go_workspace" WORKDIR: "${GOPATH}/src/k8s.io/helm" + PROJECT_NAME: "kubernetes-helm" services: - docker dependencies: + cache_directories: + - "~/.glide" + pre: + # remove old go files - sudo rm -rf /usr/local/go - rm -rf "$GOPATH" 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})" @@ -36,10 +41,41 @@ 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 checksum VERSION="${CIRCLE_TAG}" + - sudo /opt/google-cloud-sdk/bin/gsutil cp ./_dist/* "gs://${PROJECT_NAME}" + + canary: 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}/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 - docker push gcr.io/kubernetes-helm/tiller:canary + + # build canary helm binaries and push + - make build-cross + - make dist checksum VERSION=canary + - sudo /opt/google-cloud-sdk/bin/gsutil cp ./_dist/* "gs://${PROJECT_NAME}" 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/cmd/helm/delete.go b/cmd/helm/delete.go index 0d42032c1..bd0ffc919 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 @@ -49,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", @@ -59,14 +60,21 @@ 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() 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 +83,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/helm/dependency.go b/cmd/helm/dependency.go new file mode 100644 index 000000000..b22c3926e --- /dev/null +++ b/cmd/helm/dependency.go @@ -0,0 +1,229 @@ +/* +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|build|list", + Aliases: []string{"dep", "dependencies"}, + Short: "manage a chart's dependencies", + Long: dependencyDesc, + } + + cmd.AddCommand(newDependencyListCmd(out)) + cmd.AddCommand(newDependencyUpdateCmd(out)) + cmd.AddCommand(newDependencyBuildCmd(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 { + return "misnamed" + } + + if c.Metadata.Version != dep.Version { + return "wrong version" + } + return "ok" + } + + 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 { + 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) + 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_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..361d3ed6c --- /dev/null +++ b/cmd/helm/dependency_build_test.go @@ -0,0 +1,119 @@ +/* +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(t) + 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(dbc.helmhome.CacheIndex("test")) + if err != nil { + t.Fatal(err) + } + + 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_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..30e4c62c8 --- /dev/null +++ b/cmd/helm/dependency_update.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 ( + "io" + "path/filepath" + + "github.com/spf13/cobra" + "k8s.io/helm/cmd/helm/downloader" + "k8s.io/helm/cmd/helm/helmpath" +) + +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. + +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 + helmhome helmpath.Home + 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 = helmpath.Home(homePath()) + + return duc.run() + }, + } + + f := cmd.Flags() + f.BoolVar(&duc.verify, "verify", false, "Verify the packages against signatures.") + 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 { + man := &downloader.Manager{ + Out: d.out, + ChartPath: d.chartpath, + HelmHome: d.helmhome, + Keyring: d.keyring, + } + if d.verify { + man.Verify = downloader.VerifyIfPossible + } + return man.Update() +} diff --git a/cmd/helm/dependency_update_test.go b/cmd/helm/dependency_update_test.go new file mode 100644 index 000000000..465b2afc1 --- /dev/null +++ b/cmd/helm/dependency_update_test.go @@ -0,0 +1,125 @@ +/* +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" + "strings" + "testing" + + "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) { + // Set up a testing helm home + oldhome := helmHome + hh, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + helmHome = hh + defer func() { + os.RemoveAll(hh) + helmHome = oldhome + }() + + 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 { + t.Fatal(err) + } + + out := bytes.NewBuffer(nil) + duc := &dependencyUpdateCmd{out: out} + duc.helmhome = helmpath.Home(hh) + duc.chartpath = filepath.Join(hh, chartname) + + if err := duc.run(); err != nil { + output := out.String() + t.Logf("Output: %s", output) + t.Fatal(err) + } + + output := out.String() + // 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(duc.helmhome.CacheIndex("test")) + if err != nil { + t.Fatal(err) + } + + reqver := i.Entries["reqtest"][0] + if h := reqver.Digest; h != hash { + t.Errorf("Failed hash match: expected %s, got %s", hash, h) + } + + t.Logf("Results: %s", out.String()) +} + +// 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/downloader/chart_downloader.go b/cmd/helm/downloader/chart_downloader.go new file mode 100644 index 000000000..84def6cd0 --- /dev/null +++ b/cmd/helm/downloader/chart_downloader.go @@ -0,0 +1,236 @@ +/* +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. +// +// 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, version) + 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 destfile, 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 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 destfile, ver, nil + } + provfile := destfile + ".prov" + if err := ioutil.WriteFile(provfile, body.Bytes(), 0655); err != nil { + 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 destfile, ver, err + } + } + 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. +// +// 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 + 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.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]) +} + +func findRepoEntry(name string, repos []*repo.Entry) (*repo.Entry, error) { + for _, re := range repos { + if re.Name == name { + return re, nil + } + } + return nil, fmt.Errorf("no repo named %q", name) +} + +// 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..d2cf191f9 --- /dev/null +++ b/cmd/helm/downloader/chart_downloader_test.go @@ -0,0 +1,155 @@ +/* +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, 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: "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}, + } + + c := ChartDownloader{ + HelmHome: helmpath.Home("testdata/helmhome"), + Out: os.Stderr, + } + + for _, tt := range tests { + u, err := c.ResolveChartVersion(tt.ref, tt.version) + 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" + 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.") + } + + 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..4e4ddebf2 --- /dev/null +++ b/cmd/helm/downloader/manager.go @@ -0,0 +1,411 @@ +/* +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" + "path/filepath" + "strings" + "sync" + + "github.com/Masterminds/semver" + "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, + } + + 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) + + churl, err := findChartURL(dep.Name, dep.Version, dep.Repository, repos) + if err != nil { + fmt.Fprintf(m.Out, "WARNING: %s (skipped)", err) + continue + } + + if _, _, err := dl.DownloadTo(churl, "", destPath); 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.URL, 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 []*repo.Entry) { + out := m.Out + fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") + var wg sync.WaitGroup + for _, re := range repos { + wg.Add(1) + go func(n, u string) { + 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() + }(re.Name, re.URL) + } + wg.Wait() + fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") +} + +// 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 { + // 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. +// +// '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) { + 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) +} + +// 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 { + 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. +// +// 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) + } + + for _, re := range rf.Repositories { + lname := re.Name + cacheindex := m.HelmHome.CacheIndex(lname) + index, err := repo.LoadIndexFile(cacheindex) + if err != nil { + return indices, err + } + + cr := &repo.ChartRepository{ + URL: re.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/manager_test.go b/cmd/helm/downloader/manager_test.go new file mode 100644 index 000000000..b8b32e7d6 --- /dev/null +++ b/cmd/helm/downloader/manager_test.go @@ -0,0 +1,86 @@ +/* +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" +) + +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/helm-test-key.pub b/cmd/helm/downloader/testdata/helm-test-key.pub new file mode 100644 index 000000000..38714f25a Binary files /dev/null and b/cmd/helm/downloader/testdata/helm-test-key.pub differ 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 000000000..a966aef93 Binary files /dev/null and b/cmd/helm/downloader/testdata/helm-test-key.secret differ 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 new file mode 100644 index 000000000..ec7283685 --- /dev/null +++ b/cmd/helm/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml @@ -0,0 +1,49 @@ +apiVersion: v1 +entries: + alpine: + - name: alpine + urls: + - 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 + urls: + - 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 + urls: + - 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/cache/local-index.yaml b/cmd/helm/downloader/testdata/helmhome/repository/cache/local-index.yaml new file mode 120000 index 000000000..ed068e99e --- /dev/null +++ b/cmd/helm/downloader/testdata/helmhome/repository/cache/local-index.yaml @@ -0,0 +1 @@ +repository/local/index.yaml \ No newline at end of file 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..4a46c7b8b --- /dev/null +++ b/cmd/helm/downloader/testdata/helmhome/repository/cache/testing-index.yaml @@ -0,0 +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/downloader/testdata/helmhome/repository/local/index.yaml b/cmd/helm/downloader/testdata/helmhome/repository/local/index.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/helm/downloader/testdata/helmhome/repository/repositories.yaml b/cmd/helm/downloader/testdata/helmhome/repository/repositories.yaml new file mode 100644 index 000000000..c7ddf316a --- /dev/null +++ b/cmd/helm/downloader/testdata/helmhome/repository/repositories.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +repositories: + - name: testing + url: "http://example.com" + - name: kubernetes-charts + url: "http://example.com/charts" diff --git a/cmd/helm/downloader/testdata/signtest-0.1.0.tgz b/cmd/helm/downloader/testdata/signtest-0.1.0.tgz new file mode 100644 index 000000000..6de9d988d Binary files /dev/null and b/cmd/helm/downloader/testdata/signtest-0.1.0.tgz differ 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 2aad66f37..84acea792 100644 --- a/cmd/helm/fetch.go +++ b/cmd/helm/fetch.go @@ -19,109 +19,131 @@ package main import ( "fmt" "io" - "net/http" - "net/url" + "io/ioutil" "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/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 + destdir string + version 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} - pname := args[0] - if filepath.Ext(pname) != ".tgz" { - pname += ".tgz" + cmd := &cobra.Command{ + 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.") + } + for i := 0; i < len(args); i++ { + fch.chartRef = args[i] + if err := fch.run(); err != nil { + return err + } + } + return nil + }, } - return fetchChart(pname) + 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 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.") + + return cmd } -func fetchChart(pname string) error { +func (f *fetchCmd) run() error { + pname := f.chartRef + c := downloader.ChartDownloader{ + HelmHome: helmpath.Home(homePath()), + Out: f.out, + Keyring: f.keyring, + Verify: downloader.VerifyNever, + } - f, err := repo.LoadRepositoriesFile(repositoriesFile()) - if err != nil { - return err + if f.verify { + c.Verify = downloader.VerifyAlways } - // get download url - u, err := mapRepoArg(pname, f.Repositories) - if err != nil { - return err + // 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 fmt.Errorf("Failed to untar: %s", err) + } + defer os.RemoveAll(dest) } - resp, err := http.Get(u.String()) + saved, v, err := c.DownloadTo(pname, f.version, dest) if err != nil { return err } - if resp.StatusCode != 200 { - return fmt.Errorf("Failed to fetch %s : %s", u.String(), resp.Status) - } - defer resp.Body.Close() - if untarFile { - return chartutil.Expand(untarDir, resp.Body) + if f.verify { + fmt.Fprintf(f.out, "Verification: %v", v) } - p := strings.Split(u.String(), "/") - return saveChartFile(p[len(p)-1], resp.Body) -} -// 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 + // 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) } - 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 + "/" + 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) } - return url.ParseRequestURI(baseURL + strings.Join(p[1:], "/")) + + } else if !fi.IsDir() { + return fmt.Errorf("Failed to untar: %s is not a directory", ud) } - 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 + return chartutil.ExpandFile(ud, saved) } - defer out.Close() + return nil +} - _, err = io.Copy(out, r) - return err +// defaultKeyring returns the expanded path to the default keyring. +func defaultKeyring() string { + return os.ExpandEnv("$HOME/.gnupg/pubring.gpg") } diff --git a/cmd/helm/fetch_test.go b/cmd/helm/fetch_test.go index be548ee0c..286537a7d 100644 --- a/cmd/helm/fetch_test.go +++ b/cmd/helm/fetch_test.go @@ -17,49 +17,123 @@ 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(t) + 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", + 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", + failExpect: "Failed to fetch", + fail: true, + }, + { + name: "Fetch and verify", + 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", + flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"}, + failExpect: "Failed to fetch provenance", + fail: true, + }, + { + name: "Fetch and untar", + 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", + 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) + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) } -} + 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/get.go b/cmd/helm/get.go index f1ad5334c..83ec28b28 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,13 +72,16 @@ func newGetCmd(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.AddCommand(newGetValuesCmd(nil, out)) cmd.AddCommand(newGetManifestCmd(nil, out)) cmd.AddCommand(newGetHooksCmd(nil, out)) 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: @@ -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/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_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/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) } diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index ad1049267..678314441 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -28,14 +28,16 @@ import ( ) const ( - homeEnvVar = "HELM_HOME" - hostEnvVar = "HELM_HOST" - tillerNamespace = "kube-system" + localRepoIndexFilePath = "index.yaml" + homeEnvVar = "HELM_HOME" + hostEnvVar = "HELM_HOST" + tillerNamespace = "kube-system" ) var ( - helmHome string - tillerHost string + helmHome string + tillerHost string + kubeContext string ) // flagDebug is a signal that the user wants additional output. @@ -58,8 +60,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 (default ":44134"). + $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 { @@ -78,29 +81,44 @@ 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.StringVar(&kubeContext, "kube-context", "", "name of the kubeconfig context to use") 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), + newDependencyCmd(out), + newFetchCmd(out), newGetCmd(nil, out), + newHomeCmd(out), + newHistoryCmd(nil, 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), newUpgradeCmd(nil, out), + newVerifyCmd(out), + newVersionCmd(nil, out), + // Deprecated + rup, ) 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) } @@ -108,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 } @@ -132,8 +150,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" @@ -143,15 +162,6 @@ func checkArgsLength(expectedNum, actualNum 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 { @@ -162,3 +172,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 5af98960d..e3bba8777 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -18,18 +18,24 @@ package main import ( "bytes" + "fmt" "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 @@ -46,9 +52,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 +84,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"`}, @@ -128,13 +140,32 @@ 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) GetVersion(opts ...helm.VersionOption) (*rls.GetVersionResponse, error) { + return &rls.GetVersionResponse{ + Version: &version.Version{ + SemVer: "1.2.3-fakeclient+testonly", + }, + }, nil } func (c *fakeReleaseClient) UpdateRelease(rlsName string, chStr string, opts ...helm.UpdateOption) (*rls.UpdateReleaseResponse, error) { 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{ @@ -144,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 } @@ -182,3 +217,79 @@ type releaseCase struct { err bool resp *release.Release } + +// 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(t *testing.T) (string, error) { + oldhome := helmHome + dir, err := ioutil.TempDir("", "helm_home-") + if err != nil { + return "n/", err + } + + helmHome = dir + 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() + 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 + } + } 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, home.CacheIndex("local")) + } 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 new file mode 100644 index 000000000..798ab3d5f --- /dev/null +++ b/cmd/helm/helmpath/helmhome.go @@ -0,0 +1,64 @@ +/* +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' +// +// 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/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/history.go b/cmd/helm/history.go new file mode 100644 index 000000000..da3cd663d --- /dev/null +++ b/cmd/helm/history.go @@ -0,0 +1,118 @@ +/* +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/chart" + "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 + 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 { + 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 i := len(rls) - 1; i >= 0; i-- { + r := rls[i] + c := formatChartname(r.Chart) + t := timeconv.String(r.Info.LastDeployed) + s := r.Info.Status.Code.String() + v := r.Version + tbl.AddRow(v, t, s, c) + } + 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) +} diff --git a/cmd/helm/history_test.go b/cmd/helm/history_test.go new file mode 100644 index 000000000..6b3fab51e --- /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 \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", + 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 \n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n4 \t(.*)\tDEPLOYED \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/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/init.go b/cmd/helm/init.go index 81c7388c9..8e7d11ee1 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -25,7 +25,9 @@ import ( "github.com/spf13/cobra" - "k8s.io/helm/pkg/client" + "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 = "kubernetes-charts" - 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,16 +72,16 @@ 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 } 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) } - 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.") } @@ -86,27 +92,14 @@ 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. -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,33 +108,50 @@ 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 repo 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) + localRepoIndexFile := home.LocalRepository(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 } //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) } - 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 44bcafce2..bbea3b608 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, "OK") - })) - 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,21 +48,15 @@ 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) } - 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) } } - -func createTmpHome() string { - tmpHome, _ := ioutil.TempDir("", "helm_home") - defer os.Remove(tmpHome) - return tmpHome -} diff --git a/cmd/helm/inspect.go b/cmd/helm/inspect.go index 8d9948b6f..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. ` @@ -46,8 +47,11 @@ of the Charts.yaml file type inspectCmd struct { chartpath string output string + verify bool + keyring string out io.Writer client helm.Interface + version string } const ( @@ -68,10 +72,10 @@ 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]) + cp, err := locateChartPath(args[0], insp.version, insp.verify, insp.keyring) if err != nil { return err } @@ -86,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]) + cp, err := locateChartPath(args[0], insp.version, insp.verify, insp.keyring) if err != nil { return err } @@ -101,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]) + cp, err := locateChartPath(args[0], insp.version, insp.verify, insp.keyring) if err != nil { return err } @@ -110,6 +114,25 @@ 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) + + 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 b526ea090..38cb01767 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -18,6 +18,7 @@ package main import ( "bytes" + "errors" "fmt" "io" "io/ioutil" @@ -31,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" @@ -45,15 +48,38 @@ 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 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 { @@ -64,10 +90,13 @@ type installCmd struct { dryRun bool disableHooks bool replace bool + verify bool + keyring string out io.Writer client helm.Interface values *values nameTemplate string + version string } func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { @@ -83,10 +112,10 @@ 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]) + cp, err := locateChartPath(args[0], inst.version, inst.verify, inst.keyring) if err != nil { return err } @@ -106,12 +135,15 @@ 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") + f.StringVar(&inst.version, "version", "", "specify the exact chart version to install. If this is not specified, the latest version is installed.") return cmd } 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() @@ -126,7 +158,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( @@ -141,21 +173,52 @@ func (i *installCmd) run() error { return prettyError(err) } - i.printRelease(res.GetRelease()) + rel := res.GetRelease() + if rel == nil { + return nil + } + 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 { + return prettyError(err) + } + PrintStatus(i.out, status) return nil } 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 } +// printRelease prints info about a release if the flagDebug is true. func (i *installCmd) printRelease(rel *release.Release) { if rel == nil { return @@ -216,7 +279,6 @@ func (v *values) Set(data string) error { } } } - fmt.Print(v.pairs) return nil } @@ -236,34 +298,58 @@ 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) +// - URL +// +// If 'verify' is true, this will attempt to also verify the chart. +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 { + return abs, err + } + if verify { + if fi.IsDir() { + return "", errors.New("cannot verify a directory") + } + if _, err := downloader.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) } - crepo := filepath.Join(repositoryDirectory(), name) + crepo := filepath.Join(helmpath.Home(homePath()).Repository(), name) if _, err := os.Stat(crepo); err == nil { 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, + Keyring: keyring, + } + if verify { + dl.Verify = downloader.VerifyAlways } - if err := fetchChart(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/install_test.go b/cmd/helm/install_test.go index 7043eee6d..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 { @@ -121,6 +139,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" + updatedYAML := `good: true +port: + destination: basrah + source: baghdad +sailor: pisti +` + newOut, err := vobj.yaml() + if err != nil { + t.Fatal(err) + } + if string(newOut) != updatedYAML { + t.Errorf("Expected YAML to be \n%s\nGot\n%s\n", updatedYAML, newOut) + } + } type nameTemplateTestCase struct { diff --git a/cmd/helm/installer/install.go b/cmd/helm/installer/install.go new file mode 100644 index 000000000..0bfd197e8 --- /dev/null +++ b/cmd/helm/installer/install.go @@ -0,0 +1,136 @@ +/* +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 installer // import "k8s.io/helm/cmd/helm/installer" + +import ( + "fmt" + "strings" + + "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 +// command failed. +// +// If verbose is true, this will print the manifest to stdout. +func Install(namespace, image string, verbose bool) error { + kc := kube.New(nil) + + if namespace == "" { + ns, _, err := kc.DefaultNamespace() + if err != nil { + return err + } + namespace = ns + } + + c, err := kc.Client() + if err != nil { + return err + } + + 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) + } + + 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 +} + +func generateNamespace(namespace string) *api.Namespace { + return &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: namespace, + Labels: generateLabels(map[string]string{"name": "helm-namespace"}), + }, + } +} 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/list.go b/cmd/helm/list.go index 058932766..461bd89de 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 + short 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 { @@ -86,11 +94,17 @@ 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") 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 { @@ -127,21 +144,54 @@ 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 +} + +// 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) } - return nil + // 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 - 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 84fe65a47..06d64c1f9 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,12 +40,33 @@ func TestListCmd(t *testing.T) { expected: "thomas-guide", }, { - name: "list --long", - flags: map[string]string{"long": "1"}, + name: "list", + args: []string{}, 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", + 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}), + }, + // 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", "-q"}, + 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", }, } @@ -56,9 +76,7 @@ func TestListCmd(t *testing.T) { rels: tt.resp, } cmd := newListCmd(c, &buf) - for flag, value := range tt.flags { - cmd.Flags().Set(flag, value) - } + 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 beff30e31..df4583c62 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -17,12 +17,19 @@ limitations under the License. package main import ( + "errors" "fmt" + "io" + "io/ioutil" "os" "path/filepath" "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" "k8s.io/helm/pkg/repo" ) @@ -37,30 +44,58 @@ 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 - -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 + home helmpath.Home } -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 [flags] [CHART_PATH] [...]", + 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.") + } + 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") + } + } + for i := 0; i < len(args); i++ { + pkg.path = args[i] + if err := pkg.run(cmd, args); err != nil { + return err + } + } + return nil + }, + } -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", defaultKeyring(), "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 } @@ -81,18 +116,42 @@ func runPackage(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 save { - if err := repo.AddChartToLocalRepo(ch, localRepoDirectory()); err != nil { + if p.save { + 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) } } + 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..fc2060c16 --- /dev/null +++ b/cmd/helm/package_test.go @@ -0,0 +1,161 @@ +/* +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" + + "k8s.io/helm/cmd/helm/helmpath" +) + +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, 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"}, + 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) + } + + ensureTestHome(helmpath.Home(tmp), t) + oldhome := homePath() + helmHome = tmp + defer func() { + helmHome = oldhome + 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/repo.go b/cmd/helm/repo.go index 73c6b0235..8acc762e2 100644 --- a/cmd/helm/repo.go +++ b/cmd/helm/repo.go @@ -17,165 +17,31 @@ limitations under the License. package main import ( - "errors" - "fmt" - "io/ioutil" - "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 repoHelm = ` +This command consists of multiple subcommands to interact with 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(2, 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 - } - - fmt.Println(name + " has been added to your repositories") - return nil -} - -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 -} - -func runRepoRemove(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(1, len(args), "name of chart repository"); err != nil { - return err - } - return removeRepoLine(args[0]) -} - -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 - } - - path, err := filepath.Abs(args[0]) - if err != nil { - return err - } - - return index(path, args[1]) -} - -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, cacheDirectory(name+"-index.yaml")); 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 - } - - } else { - return fmt.Errorf("The repository, %s, does not exist in your repositories list", name) - } - - 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) - } +It can be used to add, remove, list, and index chart repositories. +Example usage: + $ helm repo add [NAME] [REPO_URL] +` - 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|update [ARGS]", + Short: "add, list, remove, update, and 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)) + cmd.AddCommand(newRepoUpdateCmd(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..fd31e0d33 --- /dev/null +++ b/cmd/helm/repo_add.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 main + +import ( + "fmt" + "io" + "path/filepath" + + "github.com/spf13/cobra" + + "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/pkg/repo" +) + +type repoAddCmd struct { + name string + url string + home helmpath.Home + 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] + add.home = helmpath.Home(homePath()) + + return add.run() + }, + } + return cmd +} + +func (a *repoAddCmd) run() error { + if err := addRepository(a.name, a.url, a.home); err != nil { + return err + } + fmt.Fprintf(a.out, "%q has been added to your repositories\n", a.name) + return nil +} + +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, home) +} + +func insertRepoLine(name, url string, home helmpath.Home) error { + cif := home.CacheIndex(name) + f, err := repo.LoadRepositoriesFile(home.RepositoryFile()) + if err != nil { + return err + } + + if f.Has(name) { + return fmt.Errorf("The repository name you provided (%s) already exists. Please specify a different name.", name) + } + 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 new file mode 100644 index 000000000..055ee0bfd --- /dev/null +++ b/cmd/helm/repo_add_test.go @@ -0,0 +1,95 @@ +/* +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" + "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) { + 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{ + { + name: "add a repository", + args: []string{testName, srv.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, thome, err := repotest.NewTempServer("testdata/testserver/*.*") + if err != nil { + t.Fatal(err) + } + + 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) + } + + f, err := repo.LoadRepositoriesFile(hh.RepositoryFile()) + if err != nil { + t.Error(err) + } + + if !f.Has(testName) { + t.Errorf("%s was not successfully inserted into %s", testName, hh.RepositoryFile()) + } +} diff --git a/cmd/helm/repo_index.go b/cmd/helm/repo_index.go new file mode 100644 index 000000000..636d90090 --- /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 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 + } + + 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_index_test.go b/cmd/helm/repo_index_test.go new file mode 100644 index 000000000..d207a8b5e --- /dev/null +++ b/cmd/helm/repo_index_test.go @@ -0,0 +1,56 @@ +/* +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, err := ioutil.TempDir("", "helm-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + 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) + + 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 entry, got %v: %#v", len(index.Entries), index.Entries) + } + +} diff --git a/cmd/helm/repo_list.go b/cmd/helm/repo_list.go new file mode 100644 index 000000000..a3816facd --- /dev/null +++ b/cmd/helm/repo_list.go @@ -0,0 +1,69 @@ +/* +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/cmd/helm/helmpath" + "k8s.io/helm/pkg/repo" +) + +type repoListCmd struct { + out io.Writer + home helmpath.Home +} + +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 { + list.home = helmpath.Home(homePath()) + return list.run() + }, + } + + return cmd +} + +func (a *repoListCmd) run() error { + f, err := repo.LoadRepositoriesFile(a.home.RepositoryFile()) + 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 _, re := range f.Repositories { + table.AddRow(re.Name, re.URL) + } + fmt.Fprintln(a.out, table) + return nil +} diff --git a/cmd/helm/repo_remove.go b/cmd/helm/repo_remove.go new file mode 100644 index 000000000..f7671cab0 --- /dev/null +++ b/cmd/helm/repo_remove.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 ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + + "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 { + 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] + remove.home = helmpath.Home(homePath()) + + return remove.run() + }, + } + + return cmd +} + +func (r *repoRemoveCmd) run() error { + return removeRepoLine(r.out, r.name, r.home) +} + +func removeRepoLine(out io.Writer, name string, home helmpath.Home) error { + repoFile := home.RepositoryFile() + r, err := repo.LoadRepositoriesFile(repoFile) + if 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 + } + + if err := removeRepoCache(name, home); err != nil { + return err + } + + fmt.Fprintf(out, "%q has been removed from your repositories\n", name) + + return nil +} + +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 + } + } + return nil +} diff --git a/cmd/helm/repo_remove_test.go b/cmd/helm/repo_remove_test.go new file mode 100644 index 000000000..77b30426a --- /dev/null +++ b/cmd/helm/repo_remove_test.go @@ -0,0 +1,68 @@ +/* +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" + "strings" + "testing" + + "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/pkg/repo" +) + +func TestRepoRemove(t *testing.T) { + testURL := "https://test-url.com" + + 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, hh); err != nil { + t.Error(err) + } + + mf, _ := os.Create(hh.CacheIndex(testName)) + mf.Close() + + 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(hh.CacheIndex(testName)); err == nil { + t.Errorf("Error cache file was not removed for repository %s", testName) + } + + f, err := repo.LoadRepositoriesFile(hh.RepositoryFile()) + if err != nil { + t.Error(err) + } + + if f.Has(testName) { + t.Errorf("%s was not successfully removed from repositories list", testName) + } +} diff --git a/cmd/helm/repo_test.go b/cmd/helm/repo_test.go deleted file mode 100644 index 2f70484d5..000000000 --- a/cmd/helm/repo_test.go +++ /dev/null @@ -1,97 +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 ( - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - - "k8s.io/helm/pkg/repo" -) - -var ( - testName = "test-name" - testURL = "test-url" -) - -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") - })) - - 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) - } - - if err := addRepository(testName, ts.URL); err != nil { - t.Errorf("%s", err) - } - - f, err := repo.LoadRepositoriesFile(repositoriesFile()) - if err != nil { - t.Errorf("%s", err) - } - _, ok := f.Repositories[testName] - if !ok { - t.Errorf("%s was not successfully inserted into %s", testName, repositoriesFile()) - } - - if err := insertRepoLine(testName, ts.URL); err == nil { - t.Errorf("Duplicate repository name was added") - } - -} - -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) - } - - if err := removeRepoLine(testName); err != nil { - t.Errorf("Error removing %s from repositories", 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_update.go b/cmd/helm/repo_update.go new file mode 100644 index 000000000..7e04b5e27 --- /dev/null +++ b/cmd/helm/repo_update.go @@ -0,0 +1,98 @@ +/* +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" + "sync" + + "github.com/spf13/cobra" + + "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/pkg/repo" +) + +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 repoUpdateCmd struct { + 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, + } + 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 { + u.home = helmpath.Home(homePath()) + return u.run() + }, + } + return cmd +} + +func (u *repoUpdateCmd) run() error { + f, err := repo.LoadRepositoriesFile(u.home.RepositoryFile()) + if err != nil { + return err + } + + if len(f.Repositories) == 0 { + return errors.New("no repositories found. You must add one before updating") + } + + u.update(f.Repositories, flagDebug, u.out, u.home) + return nil +} + +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 _, re := range repos { + wg.Add(1) + go func(n, u string) { + defer wg.Done() + if n == localRepository { + // We skip local because the indices are symlinked. + return + } + err := repo.DownloadIndexFile(n, u, home.CacheIndex(n)) + if 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) + } + }(re.Name, re.URL) + } + wg.Wait() + fmt.Fprintln(out, "Update Complete. ⎈ Happy Helming!⎈ ") +} diff --git a/cmd/helm/repo_update_test.go b/cmd/helm/repo_update_test.go new file mode 100644 index 000000000..5906bd62d --- /dev/null +++ b/cmd/helm/repo_update_test.go @@ -0,0 +1,95 @@ +/* +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" + "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 []*repo.Entry, verbose bool, out io.Writer, home helmpath.Home) { + for _, re := range repos { + fmt.Fprintln(out, re.Name) + } + } + uc := &repoUpdateCmd{ + out: out, + update: updater, + home: helmpath.Home(thome), + } + if err := uc.run(); err != nil { + t.Fatal(err) + } + + if got := out.String(); !strings.Contains(got, "charts") || !strings.Contains(got, "local") { + t.Errorf("Expected 'charts' and 'local' (in any order) got %q", got) + } +} + +func TestUpdateCharts(t *testing.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{ + {Name: "charts", URL: srv.URL()}, + } + updateCharts(repos, false, buf, helmpath.Home(thome)) + + 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") + } +} diff --git a/cmd/helm/resolver/resolver.go b/cmd/helm/resolver/resolver.go new file mode 100644 index 000000000..eb878cbb4 --- /dev/null +++ b/cmd/helm/resolver/resolver.go @@ -0,0 +1,88 @@ +/* +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/cmd/helm/helmpath" + "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 helmpath.Home +} + +// New creates a new resolver for a given chart and a given helm home. +func New(chartpath string, helmhome helmpath.Home) *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, 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) + } + 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..440a58af1 --- /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/rollback.go b/cmd/helm/rollback.go new file mode 100644 index 000000000..d8931c29f --- /dev/null +++ b/cmd/helm/rollback.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 main + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" +) + +const rollbackDesc = ` +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 + 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 a previous revision", + Long: rollbackDesc, + PersistentPreRunE: setupConnection, + RunE: func(cmd *cobra.Command, args []string) error { + 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.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), + helm.RollbackVersion(r.version), + ) + if err != nil { + return prettyError(err) + } + + 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 new file mode 100644 index 000000000..2455ec775 --- /dev/null +++ b/cmd/helm/rollback_test.go @@ -0,0 +1,48 @@ +/* +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"}, + 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!", + }, + } + + cmd := func(c *fakeReleaseClient, out io.Writer) *cobra.Command { + return newRollbackCmd(c, out) + } + + runReleaseCases(t, tests, cmd) + +} diff --git a/cmd/helm/search.go b/cmd/helm/search.go index 3c06aec47..c17c17d5d 100644 --- a/cmd/helm/search.go +++ b/cmd/helm/search.go @@ -17,89 +17,115 @@ limitations under the License. package main import ( - "errors" "fmt" - "os" - "path/filepath" + "io" "strings" + "github.com/gosuri/uitable" "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 -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, +type searchCmd struct { + out io.Writer + helmhome helmpath.Home + + versions bool + regexp bool } -func search(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errors.New("This command needs at least one argument (search string)") +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) + }, } - results, err := searchCacheForPattern(cacheDirectory(), args[0]) + 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 +} + +func (s *searchCmd) run(args []string) error { + index, err := s.buildIndex() if err != nil { return err } - if len(results) > 0 { - for _, result := range results { - fmt.Println(result) - } + + if len(args) == 0 { + s.showAllCharts(index) + return nil + } + + q := strings.Join(args, " ") + res, err := index.Search(q, searchMaxScore, s.regexp) + if err != nil { + return nil } + search.SortScore(res) + + fmt.Fprintln(s.out, s.formatSearchResults(res)) + 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) { + res := i.All() + search.SortScore(res) + fmt.Fprintln(s.out, s.formatSearchResults(res)) +} + +func (s *searchCmd) formatSearchResults(res []*search.Result) string { + if len(res) == 0 { + return "No results found" } - return matches + 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 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 _, re := range rf.Repositories { + n := re.Name + 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 or missing. Try 'helm repo update'.", n) + 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, s.versions) } - 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..bc4a8eaf1 --- /dev/null +++ b/cmd/helm/search/search.go @@ -0,0 +1,230 @@ +/* +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" + + "github.com/Masterminds/semver" + + "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 + Chart *repo.ChartVersion +} + +// Index is a searchable index of chart information. +type Index struct { + lines map[string]string + 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.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, all bool) { + for name, ref := range ind.Entries { + if len(ref) == 0 { + // 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) + 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 + } + } +} + +// 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. +// +// 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 { + 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 { + 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 +} + +// 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 { + 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 +} + +// Chart returns the ChartRef for a particular name. +func (i *Index) Chart(name string) (*repo.ChartVersion, 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 + } + 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 +} + +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 new file mode 100644 index 000000000..7f5d29409 --- /dev/null +++ b/cmd/helm/search/search_test.go @@ -0,0 +1,270 @@ +/* +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.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": { + { + 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": { + { + 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", + }, + }, + }, +} + +func loadTestIndex(t *testing.T, all bool) *Index { + i := NewIndex() + i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all) + 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", + }, + }, + }, + }}, 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 { + 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"}, + }, + }, + { + name: "basic search for two results", + query: "pinta", + expect: []*Result{ + {Name: "testing/pinta"}, + {Name: "ztesting/pinta"}, + }, + }, + { + name: "repo-specific search for one result", + query: "ztesting/pinta", + expect: []*Result{ + {Name: "ztesting/pinta"}, + }, + }, + { + name: "partial name search", + query: "santa", + expect: []*Result{ + {Name: "testing/santa-maria"}, + }, + }, + { + name: "description search, one result", + query: "Three", + expect: []*Result{ + {Name: "testing/santa-maria"}, + }, + }, + { + name: "description search, two results", + query: "two", + expect: []*Result{ + {Name: "testing/pinta"}, + {Name: "ztesting/pinta"}, + }, + }, + { + name: "nothing found", + query: "mayflower", + expect: []*Result{}, + }, + { + name: "regexp, one result", + query: "th[ref]*", + expect: []*Result{ + {Name: "testing/santa-maria"}, + }, + regexp: true, + }, + { + name: "regexp, fail compile", + query: "th[", + expect: []*Result{}, + regexp: true, + fail: true, + failMsg: "error parsing regexp:", + }, + } + + i := loadTestIndex(t, false) + + 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 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() + + 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..b81a3536d 100644 --- a/cmd/helm/search_test.go +++ b/cmd/helm/search_test.go @@ -17,79 +17,74 @@ 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: "NAME \tVERSION\tDESCRIPTION \ntesting/mariadb\t0.3.0 \tChart for MariaDB", + }, + { + name: "search for 'alpine', expect two matches", + 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"}, + expect: "No results found", + }, + { + name: "search for 'alp[a-z]+', expect two matches", + args: []string{"alp[a-z]+"}, + flags: []string{"--regexp"}, + expect: "NAME \tVERSION\tDESCRIPTION \ntesting/alpine\t0.1.0 \tDeploy a basic Alpine Linux pod", + 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/serve.go b/cmd/helm/serve.go index 3fca0bd8a..069938fde 100644 --- a/cmd/helm/serve.go +++ b/cmd/helm/serve.go @@ -17,32 +17,45 @@ 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" ) -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 { + out io.Writer + home helmpath.Home + address string + repoPath string } -var serveCmd = &cobra.Command{ - Use: "serve", - Short: "start a local http web server", - Long: serveDesc, - RunE: serve, -} +func newServeCmd(out io.Writer) *cobra.Command { + 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 { + srv.home = helmpath.Home(homePath()) + return srv.run() + }, + } + 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.") -func serve(cmd *cobra.Command, args []string) error { + return cmd +} - repoPath, err := filepath.Abs(repoPath) +func (s *serveCmd) run() error { + repoPath, err := filepath.Abs(s.repoPath) if err != nil { return err } @@ -50,6 +63,6 @@ func serve(cmd *cobra.Command, args []string) error { return err } - repo.StartLocalRepo(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/status.go b/cmd/helm/status.go index 436445642..30b572996 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" ) @@ -34,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 { @@ -57,20 +59,38 @@ func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command { return status.run() }, } + + cmd.PersistentFlags().Int32Var(&status.version, "revision", 0, "If set, display the status of the named release with revision") + 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) } - fmt.Fprintf(s.out, "Last Deployed: %s\n", timeconv.String(res.Info.LastDeployed)) - fmt.Fprintf(s.out, "Status: %s\n", res.Info.Status.Code) - fmt.Fprintf(s.out, "Resources:\n%s\n", res.Info.Status.Resources) + PrintStatus(s.out, res) + 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) { + 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 { - fmt.Fprintf(s.out, "Details: %s\n", res.Info.Status.Details) + fmt.Fprintf(out, "Details: %s\n", res.Info.Status.Details) + } + fmt.Fprintf(out, "\n") + 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) } - return nil } diff --git a/cmd/helm/structure.go b/cmd/helm/structure.go deleted file mode 100644 index fce4838f4..000000000 --- a/cmd/helm/structure.go +++ /dev/null @@ -1,53 +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 localRepoDirectory(paths ...string) string { - fragments := append([]string{repositoryDirectory(), localRepoPath}, paths...) - return filepath.Join(fragments...) -} - -func repositoriesFile() string { - return filepath.Join(repositoryDirectory(), repositoriesFilePath) -} diff --git a/cmd/helm/testdata/helm-test-key.pub b/cmd/helm/testdata/helm-test-key.pub new file mode 100644 index 000000000..38714f25a Binary files /dev/null and b/cmd/helm/testdata/helm-test-key.pub differ diff --git a/cmd/helm/testdata/helm-test-key.secret b/cmd/helm/testdata/helm-test-key.secret new file mode 100644 index 000000000..a966aef93 Binary files /dev/null and b/cmd/helm/testdata/helm-test-key.secret differ 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..26ce97423 --- /dev/null +++ b/cmd/helm/testdata/helmhome/repository/cache/testing-index.yaml @@ -0,0 +1,46 @@ +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/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..3835aaa5a --- /dev/null +++ b/cmd/helm/testdata/helmhome/repository/repositories.yaml @@ -0,0 +1,6 @@ +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 new file mode 100644 index 000000000..0ff94a0e3 --- /dev/null +++ b/cmd/helm/testdata/repositories.yaml @@ -0,0 +1,6 @@ +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/testcharts/reqtest-0.1.0.tgz b/cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz new file mode 100644 index 000000000..356bc9303 Binary files /dev/null and b/cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz differ 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/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz b/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz new file mode 100644 index 000000000..6de9d988d Binary files /dev/null and b/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz differ 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/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/cmd/helm/tunnel.go b/cmd/helm/tunnel.go index 061eb6915..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 @@ -68,5 +78,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) diff --git a/cmd/helm/update.go b/cmd/helm/update.go deleted file mode 100644 index ecd4710da..000000000 --- a/cmd/helm/update.go +++ /dev/null @@ -1,80 +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 ( - "errors" - "fmt" - "sync" - - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/repo" -) - -var verboseUpdate bool - -var updateCommand = &cobra.Command{ - Use: "update", - Aliases: []string{"up"}, - Short: "update information on available charts in the chart repositories", - RunE: runUpdate, -} - -func init() { - updateCommand.Flags().BoolVar(&verboseUpdate, "verbose", false, "verbose error messages") - RootCommand.AddCommand(updateCommand) -} - -func runUpdate(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 found. You must add one before updating") - } - - updateCharts(f.Repositories, verboseUpdate) - return nil -} - -func updateCharts(repos map[string]string, verbose bool) { - fmt.Println("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) { - defer wg.Done() - 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" - if verbose { - updateErr = updateErr + ": " + err.Error() - } - fmt.Println(updateErr) - } else { - fmt.Println("...Successfully got an update from the " + n + " chart repository") - } - }(name, url) - } - wg.Wait() - fmt.Println("Update Complete. Happy Helming!") -} diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 07eeae03b..d85bf8cfe 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -17,20 +17,28 @@ limitations under the License. package main import ( + "bytes" "fmt" "io" "io/ioutil" + "strings" "github.com/spf13/cobra" "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/storage/driver" ) 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. ` type upgradeCmd struct { @@ -41,6 +49,12 @@ type upgradeCmd struct { dryRun bool disableHooks bool valuesFile string + values *values + verify bool + keyring string + install bool + namespace string + version string } func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { @@ -48,6 +62,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { upgrade := &upgradeCmd{ out: out, client: client, + values: new(values), } cmd := &cobra.Command{ @@ -56,7 +71,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 } @@ -71,33 +86,94 @@ 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") + 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)") + 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) + chartPath, err := locateChartPath(u.chart, u.version, u.verify, u.keyring) if err != nil { return err } - rawVals := []byte{} - if u.valuesFile != "" { - rawVals, err = ioutil.ReadFile(u.valuesFile) - 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 + } + _, 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" 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 +} + +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 { diff --git a/cmd/helm/verify.go b/cmd/helm/verify.go new file mode 100644 index 000000000..07e1c9b77 --- /dev/null +++ b/cmd/helm/verify.go @@ -0,0 +1,70 @@ +/* +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" + + "k8s.io/helm/cmd/helm/downloader" +) + +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 { + _, err := downloader.VerifyChart(v.chartfile, v.keyring) + return err +} 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/cmd/helm/version.go b/cmd/helm/version.go index d66221491..1242a4667 100644 --- a/cmd/helm/version.go +++ b/cmd/helm/version.go @@ -17,21 +17,53 @@ 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" ) -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. + cv := version.GetVersionProto() + fmt.Fprintf(v.out, "Client: %#v\n", cv) + + 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) + return nil } diff --git a/cmd/tiller/environment/environment.go b/cmd/tiller/environment/environment.go index 12690a84f..72fd510be 100644 --- a/cmd/tiller/environment/environment.go +++ b/cmd/tiller/environment/environment.go @@ -31,11 +31,10 @@ 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" + "k8s.io/kubernetes/pkg/client/unversioned/testclient" ) -// 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 testclient.NewSimpleFake(), nil +} + // 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..68cc5d63c 100644 --- a/cmd/tiller/environment/environment_test.go +++ b/cmd/tiller/environment/environment_test.go @@ -23,9 +23,8 @@ 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" ) type mockEngine struct { @@ -36,49 +35,11 @@ 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) 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 { } -type mockKubeClient struct { +func (k *mockKubeClient) APIClient() (unversionedclient.Interface, error) { + return testclient.NewSimpleFake(), nil } func (k *mockKubeClient) Create(ns string, r io.Reader) error { @@ -116,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() diff --git a/cmd/tiller/hooks.go b/cmd/tiller/hooks.go index e102699c7..fab903749 100644 --- a/cmd/tiller/hooks.go +++ b/cmd/tiller/hooks.go @@ -30,24 +30,29 @@ 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 { + Version string `json:"apiVersion"` Kind string `json:"kind,omitempty"` Metadata *struct { Name string `json:"name"` @@ -55,7 +60,29 @@ 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 +} + +// 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. // Any file that does not declare one of the hook types will be placed in the @@ -64,6 +91,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,9 +103,9 @@ 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, 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 @@ -99,14 +127,18 @@ 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 + 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{ @@ -133,5 +165,5 @@ func sortHooks(files map[string]string) ([]*release.Hook, map[string]string, err } 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 4d054ec90..af25468ad 100644 --- a/cmd/tiller/hooks_test.go +++ b/cmd/tiller/hooks_test.go @@ -19,10 +19,12 @@ package main import ( "testing" + "github.com/ghodss/yaml" + "k8s.io/helm/pkg/proto/hapi/release" ) -func TestSortHooks(t *testing.T) { +func TestSortManifests(t *testing.T) { data := []struct { name string @@ -41,7 +43,7 @@ kind: Job metadata: name: first labels: - doesnt: matter + doesnot: matter annotations: "helm.sh/hook": pre-install `, @@ -52,6 +54,7 @@ metadata: kind: "ReplicaSet", hooks: []release.Hook_Event{release.Hook_POST_INSTALL}, manifest: `kind: ReplicaSet +apiVersion: v1beta1 metadata: name: second annotations: @@ -63,6 +66,7 @@ metadata: kind: "ReplicaSet", hooks: []release.Hook_Event{}, manifest: `kind: ReplicaSet +apiVersion: v1beta1 metadata: name: third annotations: @@ -74,6 +78,7 @@ metadata: kind: "Pod", hooks: []release.Hook_Event{}, manifest: `kind: Pod +apiVersion: v1 metadata: name: fourth annotations: @@ -85,6 +90,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 +118,7 @@ metadata: manifests[o.path] = o.manifest } - hs, generic, err := sortHooks(manifests) + hs, generic, err := sortManifests(manifests, newVersionSet("v1", "v1beta1"), InstallOrder) if err != nil { t.Fatalf("Unexpected error: %s", err) } @@ -152,4 +158,42 @@ 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) { + 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/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_history.go b/cmd/tiller/release_history.go new file mode 100644 index 000000000..99bb3bb1a --- /dev/null +++ b/cmd/tiller/release_history.go @@ -0,0 +1,50 @@ +/* +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 ( + "golang.org/x/net/context" + 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) { + if !checkClientVersion(ctx) { + return nil, errIncompatibleVersion + } + + h, err := s.env.Releases.History(req.Name) + if err != nil { + return nil, err + } + + relutil.Reverse(h, relutil.SortByRevision) + + 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 +} + +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/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 2021d2eec..a9e6cafb5 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -22,23 +22,40 @@ import ( "fmt" "log" "regexp" - "sort" + "strings" + + "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" "k8s.io/helm/cmd/tiller/environment" "k8s.io/helm/pkg/chartutil" "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" ) var srv *releaseServer +// releaseNameMaxLen is the maximum length of a release 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 + +// 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 notesFileSuffix = "NOTES.txt" + func init() { srv = &releaseServer{ env: env, @@ -51,6 +68,10 @@ 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") ) // ListDefaultLimit is the default limit for number of items returned in a list. @@ -60,8 +81,33 @@ 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 { - rels, err := s.env.Releases.ListDeployed() + if !checkClientVersion(stream.Context()) { + return errIncompatibleVersion + } + + 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 } @@ -77,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 { @@ -129,8 +175,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) { @@ -147,40 +192,85 @@ 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 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 } - rel, err := s.env.Releases.Get(req.Name) - if err != nil { - return nil, err + + var rel *release.Release + var err error + + if req.Version <= 0 { + if rel, err = s.env.Releases.Deployed(req.Name); err != nil { + return nil, fmt.Errorf("getting deployed release '%s': %s", req.Name, 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 { return nil, errors.New("release info is missing") } + if rel.Chart == nil { + 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}, nil + return statusResp, nil } 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 } - rel, err := s.env.Releases.Get(req.Name) + if req.Version <= 0 { + rel, err := s.env.Releases.Deployed(req.Name) + 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) { + if !checkClientVersion(c) { + return nil, errIncompatibleVersion + } + currentRelease, updatedRelease, err := s.prepareUpdate(req) if err != nil { return nil, err @@ -188,11 +278,13 @@ 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 err := s.env.Releases.Update(updatedRelease); err != nil { - return nil, err + if !req.DryRun { + if err := s.env.Releases.Create(updatedRelease); err != nil { + return res, err + } } return res, nil @@ -213,11 +305,13 @@ 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 { - return nil, fmt.Errorf("Update of %s failed: %s", updatedRelease.Name, err) + if err := s.performKubeUpdate(originalRelease, updatedRelease); err != nil { + 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 @@ -227,11 +321,24 @@ func (s *releaseServer) performUpdate(originalRelease, updatedRelease *release.R } } + originalRelease.Info.Status.Code = release.Status_SUPERSEDED + s.recordRelease(originalRelease, true) + updatedRelease.Info.Status.Code = release.Status_DEPLOYED 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 == "" { @@ -243,11 +350,14 @@ 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 } + // 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, @@ -260,7 +370,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 } @@ -281,16 +391,151 @@ func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*rele Hooks: hooks, } + if len(notesTxt) > 0 { + updatedRelease.Info.Status.Notes = notesTxt + } return currentRelease, updatedRelease, nil } +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 { + return nil, err + } + + res, err := s.performRollback(currentRelease, targetRelease, req) + if err != nil { + return res, err + } + + if !req.DryRun { + if err := s.env.Releases.Create(targetRelease); err != nil { + return res, err + } + } + + return res, 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 { + 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 + 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 + s.recordRelease(currentRelease, true) + + 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) + return kubeCli.Update(targetRelease.Namespace, current, target) +} + +// 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) { + 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 + h, err := s.env.Releases.History(req.Name) + if err != nil { + return nil, nil, err + } + if len(h) <= 1 { + return nil, nil, errors.New("no revision to rollback") + } + + relutil.SortByRevision(h) + crls := h[len(h)-1] + + rbv := req.Version + if req.Version == 0 { + rbv = crls.Version - 1 + } + + log.Printf("rolling back %s (current: v%d, target: v%d)", req.Name, crls.Version, rbv) + + prls, err := s.env.Releases.Get(req.Name, rbv) + if err != nil { + return nil, nil, err + } + + // Store a new release object with previous release's configuration + // Store a new release object with previous release's configuration + target := &release.Release{ + Name: req.Name, + Namespace: crls.Namespace, + Chart: prls.Chart, + Config: prls.Config, + Info: &release.Info{ + FirstDeployed: crls.Info.FirstDeployed, + LastDeployed: timeconv.Now(), + Status: &release.Status{ + Code: release.Status_UNKNOWN, + Notes: prls.Info.Status.Notes, + }, + }, + Version: crls.Version + 1, + Manifest: prls.Manifest, + Hooks: prls.Hooks, + } + + return crls, target, 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 // 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 rel, err := s.env.Releases.Get(start); err == driver.ErrReleaseNotFound { + + 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, 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. @@ -307,7 +552,10 @@ func (s *releaseServer) uniqName(start string, reuse bool) (string, error) { for i := 0; i < maxTries; i++ { namer := moniker.New() name := namer.NameSep("-") - if _, err := s.env.Releases.Get(name); err == driver.ErrReleaseNotFound { + if len(name) > releaseNameMaxLen { + name = name[:releaseNameMaxLen] + } + if _, err := s.env.Releases.Get(name, 1); err == driver.ErrReleaseNotFound { return name, nil } log.Printf("info: Name %q is taken. Searching again.", name) @@ -329,6 +577,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) @@ -360,7 +612,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 } @@ -380,40 +632,89 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re Hooks: hooks, Version: 1, } + if len(notesTxt) > 0 { + rel.Info.Status.Notes = notesTxt + } return rel, nil } -func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values) ([]*release.Hook, *bytes.Buffer, error) { +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, 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, notesFileSuffix) { + notes = v + delete(files, k) + } } // 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, InstallOrder) 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. 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, nil + 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 { + 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. @@ -436,17 +737,18 @@ 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) - if err := s.env.Releases.Create(r); err != nil { - log.Printf("warning: Failed to record release %q: %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) } // 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 } } @@ -459,9 +761,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 } @@ -487,7 +787,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()? @@ -504,12 +804,16 @@ 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 } - 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 @@ -518,6 +822,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, rel.Version); 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) } @@ -532,44 +843,63 @@ 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 } + // 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) + 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()) } } - 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, rel.Version); err != nil { + log.Printf("uninstall: Failed to purge the release: %s", err) + } } - return res, nil -} - -// byName implements the sort.Interface for []*release.Release. -type byName []*release.Release + var errs error + if len(es) > 0 { + errs = fmt.Errorf("deletion error count %d: %s", len(es), strings.Join(es, "; ")) + } -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 + return res, errs } -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 + // 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 } diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index f2ba5c69a..e3073c189 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" @@ -28,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" @@ -35,6 +38,8 @@ import ( "k8s.io/helm/pkg/storage/driver" ) +const notesText = "my notes here" + var manifestWithHook = `apiVersion: v1 kind: ConfigMap metadata: @@ -55,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(), @@ -82,16 +97,20 @@ 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"`}, + Config: &chart.Config{Raw: `name: value`}, Version: 1, Hooks: []*release.Hook{ { @@ -108,6 +127,37 @@ func releaseStub() *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() + 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() @@ -130,6 +180,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 { @@ -152,7 +203,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. @@ -168,7 +219,133 @@ 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.") + } + 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, res.Release.Version) + 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.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 TestInstallReleaseWithNotes(t *testing.T) { + c := helm.NewContext() + 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(notesText)}, + }, + }, + } + res, err := rs.InstallRelease(c, req) + if err != nil { + t.Fatalf("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, res.Release.Version) + 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 != notesText { + t.Fatalf("Expected '%s', got '%s'", notesText, 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 := helm.NewContext() + 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(notesText + " {{.Release.Name}}")}, + }, + }, + } + res, err := rs.InstallRelease(c, req) + if err != nil { + t.Fatalf("Failed install: %s", err) } if res.Release.Name == "" { t.Errorf("Expected release name.") @@ -177,7 +354,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) } @@ -191,6 +368,11 @@ func TestInstallRelease(t *testing.T) { t.Errorf("Unexpected manifest: %v", rel.Hooks[0].Manifest) } + 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) + } + if rel.Hooks[0].Events[0] != release.Hook_POST_INSTALL { t.Errorf("Expected event 0 is post install") } @@ -212,7 +394,7 @@ func TestInstallRelease(t *testing.T) { } func TestInstallReleaseDryRun(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() req := &services.InstallReleaseRequest{ @@ -247,7 +429,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.") } @@ -261,7 +443,7 @@ func TestInstallReleaseDryRun(t *testing.T) { } func TestInstallReleaseNoHooks(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -279,8 +461,27 @@ func TestInstallReleaseNoHooks(t *testing.T) { } } +func TestInstallReleaseFailedHooks(t *testing.T) { + c := helm.NewContext() + 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() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rel.Info.Status.Code = release.Status_DELETED @@ -293,16 +494,25 @@ 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, Version: 1} + 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) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rs.env.Releases.Create(rel) @@ -319,7 +529,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 == "" { @@ -334,7 +544,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) } @@ -358,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) } @@ -371,8 +587,77 @@ 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 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 := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rs.env.Releases.Create(rel) @@ -391,7 +676,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 { @@ -400,8 +685,191 @@ func TestUpdateReleaseNoHooks(t *testing.T) { } +func TestUpdateReleaseNoChanges(t *testing.T) { + c := helm.NewContext() + 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 TestRollbackReleaseNoHooks(t *testing.T) { + c := helm.NewContext() + 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 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() + 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 := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -411,7 +879,39 @@ 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" { + 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) + } +} + +func TestUninstallPurgeRelease(t *testing.T) { + c := helm.NewContext() + 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.Fatalf("Failed uninstall: %s", err) } if res.Release.Name != "angry-panda" { @@ -429,17 +929,35 @@ 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) } +} + +func TestUninstallPurgeDeleteRelease(t *testing.T) { + c := helm.NewContext() + rs := rsFixture() + rs.env.Releases.Create(releaseStub()) + + req := &services.UninstallReleaseRequest{ + Name: "angry-panda", + } + + _, err := rs.UninstallRelease(c, req) + if err != nil { + t.Fatalf("Failed uninstall: %s", err) + } + + req2 := &services.UninstallReleaseRequest{ + Name: "angry-panda", + Purge: true, + } - // 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) + _, err2 := rs.UninstallRelease(c, req2) + if err2 != nil && err2.Error() != "'angry-panda' has no deployed releases" { + t.Errorf("Failed uninstall: %s", err2) } } func TestUninstallReleaseNoHooks(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -460,14 +978,14 @@ 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 { 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) } @@ -478,14 +996,14 @@ 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 { 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) } @@ -495,6 +1013,25 @@ func TestGetReleaseStatus(t *testing.T) { } } +func TestGetReleaseStatusDeleted(t *testing.T) { + c := helm.NewContext() + 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, Version: 1}) + if err != nil { + t.Fatalf("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 @@ -516,6 +1053,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() @@ -603,6 +1205,35 @@ 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}, + } +} + +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 } @@ -612,7 +1243,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/cmd/tiller/tiller.go b/cmd/tiller/tiller.go index 9dfb2c648..b00cddba0 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,13 @@ 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 ( + grpcAddr = ":44134" + probeAddr = ":44135" + traceAddr = ":44136" + enableTracing = false + store = storageConfigMap +) const globalUsage = `The Kubernetes Helm server. @@ -57,19 +69,37 @@ 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() } func start(c *cobra.Command, args []string) { - lstn, err := net.Listen("tcp", addr) + 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", 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) @@ -81,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 = ` + + + + + +` 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/ 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/chart_repository.md b/docs/chart_repository.md index 3400206b0..1c65348b3 100644 --- a/docs/chart_repository.md +++ b/docs/chart_repository.md @@ -7,41 +7,59 @@ ## 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`. +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. 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 - created: 2016-05-26 11:23:44.086354411 +0000 UTC - checksum: a61575c2d3160e5e39abf2a5ec984d6119404b18 - chartfile: +apiVersion: v1 +entries: + alpine: + - 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 - 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 - created: 2016-05-26 11:23:44.087939192 +0000 UTC - checksum: 2cea3048cf85d588204e1b1cc0674472b4517919 - chartfile: - name: redis - description: Port of the replicatedservice template from kubernetes/charts - version: 2.0.0 - home: https://github.com/example-charts/redis + 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 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 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,9 +75,37 @@ 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](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 +121,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 --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. @@ -87,21 +133,21 @@ 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.* -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/chart_repository_sync_example.md b/docs/chart_repository_sync_example.md index e38e90082..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/ http://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. diff --git a/docs/charts.md b/docs/charts.md index 03eecba04..f94fdb22d 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -22,13 +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 - 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. @@ -50,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 @@ -88,12 +90,35 @@ 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 `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 +`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 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. @@ -104,6 +129,7 @@ chart's `charts/` directory: ``` wordpress: Chart.yaml + requirements.yaml # ... charts/ apache/ @@ -119,7 +145,62 @@ 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` + +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 @@ -167,17 +248,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. @@ -189,6 +271,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_. @@ -205,8 +291,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 @@ -256,6 +342,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 @@ -278,16 +397,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 @@ -483,7 +602,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/developers.md b/docs/developers.md index 5ebdc58fe..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 @@ -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.`). - 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}}"] 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. + + 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. 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 000000000..b9d43a705 Binary files /dev/null and b/docs/images/create-a-gh-page-button.png differ diff --git a/docs/images/set-a-gh-page.png b/docs/images/set-a-gh-page.png new file mode 100644 index 000000000..ee9dccd69 Binary files /dev/null and b/docs/images/set-a-gh-page.png differ diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 000000000..5403d2802 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,152 @@ +# 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 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 +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]and Mercurial installed. + +```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.) + +### 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 +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/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/docs/quickstart.md b/docs/quickstart.md index 8d2c505a8..3266f9151 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -7,10 +7,15 @@ 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 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 +30,35 @@ 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 stable/mysql 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 `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 + +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](examples/nginx/Chart.yaml) and the default values in +[docs/examples/nginx/values.yaml](examples/nginx/values.yaml). ## Learn About The Release @@ -48,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 new file mode 100644 index 000000000..41ae8b444 --- /dev/null +++ b/docs/using_helm.md @@ -0,0 +1,394 @@ +# 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 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 +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 +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 +$ helm 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 +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 repo update`. + +## Creating Your Own Charts + +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: + +```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. diff --git a/glide.lock b/glide.lock index 724dbb06e..bab587bd7 100644 --- a/glide.lock +++ b/glide.lock @@ -1,16 +1,77 @@ -hash: b2a8d0f3f558b4c1026e519c7bdcb5dd675e66424edcf1440ec10706afd6b345 -updated: 2016-08-10T15:23:57.611441314-06:00 +hash: 0b56505a7d2b0bde1a8aba9c4ac52ef18ea1eae6d46157db598e5a1051b64cf5 +updated: 2016-10-11T12:54:05.869559929-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: f0a097ddac24fb00e07d2ac17f8671423f3ea47c + version: df1d3ca07d2d07bba352d5b73c4313b4e2a6203e subpackages: + - jsonpb - proto - ptypes/any - ptypes/timestamp - name: github.com/google/cadvisor - version: c2ea32971ae033041f0fb0f309b1dee94fd1d55f + version: 0cdf4912793fac9990de3790c273342ec31817fb subpackages: - api - cache/memory + - client/v2 - collector - container - container/common @@ -182,46 +247,34 @@ 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 version: 808ed7761c233af2de3f9729a041d68c62527f3a - name: github.com/Masterminds/sprig - version: abe09979bcb1ec0a50b2d7bbf67e6aaa11787417 + version: 8f797f5b23118d8fe846c4296b0ad55044201b14 - name: github.com/mattn/go-runewidth version: d6bea18f789704b5f83375793155289da36a3c7f - name: github.com/matttproud/golang_protobuf_extensions 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,16 @@ 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/satori/go.uuid + version: 879c5887cd475cd7864858769793b2ceb0d44feb +- name: github.com/Sirupsen/logrus + version: 51fe59aca108dc5680109e7b2051cbdcfa5a253c - name: github.com/spf13/cobra version: 6a8bd97bdb1fc0d08a83459940498ea49d3e8c93 - name: github.com/spf13/pflag @@ -243,10 +299,21 @@ 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: 1f22c0103821b9390939b6776727195525381532 + subpackages: + - cast5 + - openpgp + - openpgp/armor + - openpgp/clearsign + - openpgp/elgamal + - openpgp/errors + - openpgp/packet + - openpgp/s2k - name: golang.org/x/net version: fb93926129b8ec0056f2f458b1f519654814edf0 subpackages: @@ -284,7 +351,7 @@ imports: - compute/metadata - internal - name: google.golang.org/grpc - version: dec33edc378cf4971a2741cfd86ed70a644d6ba3 + version: 0032a855ba5c8a3c8e0d71c2deef354b70af1584 subpackages: - codes - credentials @@ -298,14 +365,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: a0bfee0d3899d2b91530552a536405c1fe1a1d2a + version: ef16c3f8079df0654c8336741134ba142846ec13 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 @@ -330,9 +466,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 @@ -343,6 +479,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 @@ -350,26 +489,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 @@ -382,7 +532,7 @@ imports: - pkg/client/unversioned/remotecommand - pkg/client/unversioned/testclient - pkg/controller - - pkg/controller/framework + - pkg/controller/deployment/util - pkg/conversion - pkg/conversion/queryparams - pkg/credentialprovider @@ -393,9 +543,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 @@ -407,13 +557,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 @@ -422,6 +584,7 @@ imports: - pkg/util/httpstream - pkg/util/httpstream/spdy - pkg/util/integer + - pkg/util/interrupt - pkg/util/intstr - pkg/util/json - pkg/util/jsonpath @@ -436,6 +599,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 @@ -447,12 +612,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 -- name: speter.net/go/exp/math/dec/inf - version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 - repo: https://github.com/go-inf/inf.git - vcs: git + - 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 1cd123e71..e7a599b4d 100644 --- a/glide.yaml +++ b/glide.yaml @@ -8,21 +8,21 @@ import: - package: github.com/spf13/pflag version: 367864438f1b1a3c7db4da06a2f55b144e6784e0 - package: github.com/Masterminds/sprig - version: ^2.3 -- package: gopkg.in/yaml.v2 + version: ^2.6 +- package: github.com/ghodss/yaml - package: github.com/Masterminds/semver version: 1.1.0 - package: github.com/technosophos/moniker - package: github.com/golang/protobuf - version: f0a097ddac24fb00e07d2ac17f8671423f3ea47c + version: df1d3ca07d2d07bba352d5b73c4313b4e2a6203e 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 + version: ~1.4.1 subpackages: - pkg/api - pkg/api/meta @@ -45,12 +45,11 @@ 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 vcs: git repo: https://code.googlesource.com/gocloud +- package: golang.org/x/crypto + subpackages: + - openpgp diff --git a/pkg/chartutil/chartfile.go b/pkg/chartutil/chartfile.go index 588670541..27b8364df 100644 --- a/pkg/chartutil/chartfile.go +++ b/pkg/chartutil/chartfile.go @@ -24,6 +24,11 @@ import ( "k8s.io/helm/pkg/proto/hapi/chart" ) +// 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. 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 b43b8a5da..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) } @@ -75,6 +80,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") } @@ -85,5 +94,4 @@ func verifyChartfile(t *testing.T, f *chart.Metadata) { t.Errorf("Expected %q, got %q", kk[i], k) } } - } 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 911d883d2..dba1100e1 100644 --- a/pkg/chartutil/load.go +++ b/pkg/chartutil/load.go @@ -120,7 +120,15 @@ 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/") + 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}) @@ -137,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) @@ -208,6 +218,7 @@ func LoadDir(dir string) (*chart.Chart, error) { } rules = r } + rules.AddDefaults() files := []*afile{} topdir += string(filepath.Separator) diff --git a/pkg/chartutil/load_test.go b/pkg/chartutil/load_test.go index 822e8d078..9586e3036 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 := 8 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,57 @@ 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 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 new file mode 100644 index 000000000..8871edbb8 --- /dev/null +++ b/pkg/chartutil/requirements.go @@ -0,0 +1,108 @@ +/* +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" +) + +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 +// 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"` +} + +// 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 == requirementsName { + data = f.Value + } + } + if len(data) == 0 { + return nil, ErrRequirementsNotFound + } + 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 new file mode 100644 index 000000000..afcc75621 --- /dev/null +++ b/pkg/chartutil/requirements_test.go @@ -0,0 +1,35 @@ +/* +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) +} + +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/chartfiletest.yaml b/pkg/chartutil/testdata/chartfiletest.yaml index 9f255b9bd..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" @@ -13,3 +14,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 50d1ef014..aaf443dba 100644 Binary files a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz and b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz/Chart.yaml b/pkg/chartutil/testdata/frobnitz/Chart.yaml index 9f255b9bd..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" @@ -13,3 +14,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/charts/_ignore_me b/pkg/chartutil/testdata/frobnitz/charts/_ignore_me new file mode 100644 index 000000000..2cecca682 --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. 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 27bd51f65..3af333e76 100644 Binary files a/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz and b/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz/requirements.lock b/pkg/chartutil/testdata/frobnitz/requirements.lock new file mode 100644 index 000000000..6fcc2ed9f --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/requirements.lock @@ -0,0 +1,8 @@ +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts +digest: invalid 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 c35e1b970..0b66d27fe 100644 Binary files a/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz and b/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz differ 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/chartutil/values_test.go b/pkg/chartutil/values_test.go index 33f0854a8..b48d4e943 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) @@ -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/client/install.go b/pkg/client/install.go deleted file mode 100644 index 0b7a3b1fc..000000000 --- a/pkg/client/install.go +++ /dev/null @@ -1,101 +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 client // import "k8s.io/helm/pkg/client" - -import ( - "bytes" - "fmt" - "text/template" - - "github.com/Masterminds/sprig" - - "k8s.io/helm/pkg/kube" -) - -// Install uses kubernetes client to install tiller -// -// Returns the string output received from the operation, and an error if the -// command failed. -// -// If verbose is true, this will print the manifest to stdout. -func Install(namespace, image string, verbose bool) error { - kc := kube.New(nil) - - if namespace == "" { - ns, _, err := kc.DefaultNamespace() - if err != nil { - return err - } - 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 { - return err - } - - if verbose { - fmt.Println(b.String()) - } - - return kc.Create(namespace, &b) -} - -// 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 -` diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 73b8dabad..93927bcb5 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -19,12 +19,13 @@ package engine import ( "bytes" "fmt" - "log" "path" "strings" "text/template" "github.com/Masterminds/sprig" + "github.com/ghodss/yaml" + "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/chart" ) @@ -50,12 +51,24 @@ 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, } } -// Render takes a chart, optional values, and value overrids, and attempts to render the Go templates. +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. // @@ -132,7 +145,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) diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index ec19f8ded..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() @@ -290,20 +303,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]) } } diff --git a/pkg/helm/client.go b/pkg/helm/client.go index 3c9cdfe71..bdf687933 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -17,28 +17,13 @@ limitations under the License. package helm // import "k8s.io/helm/pkg/helm" import ( - "os" - + "golang.org/x/net/context" "google.golang.org/grpc" "k8s.io/helm/pkg/chartutil" 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 @@ -46,7 +31,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 @@ -57,93 +43,301 @@ func (h *Client) Option(opts ...Option) *Client { return h } -// Init initializes the helm client with default options -func (h *Client) Init() *Client { - return h.Option(Host(DefaultHelmHost)). - Option(Home(os.ExpandEnv(DefaultHelmHome))) -} - // ListReleases lists the current releases. func (h *Client) ListReleases(opts ...ReleaseListOption) (*rls.ListReleasesResponse, error) { + 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 + } + + // 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) +} + +// 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) +} + +// 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()) if err != nil { return nil, err } defer c.Close() - return h.opts.rpcListReleases(rls.NewReleaseServiceClient(c), opts...) + rlc := rls.NewReleaseServiceClient(c) + s, err := rlc.ListReleases(ctx, req) + if err != nil { + return nil, err + } + + return s.Recv() } -// InstallRelease installs a new chart and returns the release response. -func (h *Client) InstallRelease(chStr, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, 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() - chart, err := chartutil.Load(chStr) + rlc := rls.NewReleaseServiceClient(c) + return rlc.InstallRelease(ctx, req) +} + +// 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() - return h.opts.rpcInstallRelease(chart, rls.NewReleaseServiceClient(c), ns, opts...) + rlc := rls.NewReleaseServiceClient(c) + return rlc.UninstallRelease(ctx, req) } -// 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.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.rpcDeleteRelease(rlsName, rls.NewReleaseServiceClient(c), opts...) + rlc := rls.NewReleaseServiceClient(c) + return rlc.UpdateRelease(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.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() - chart, err := chartutil.Load(chStr) + rlc := rls.NewReleaseServiceClient(c) + return rlc.RollbackRelease(ctx, req) +} + +// 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.rpcUpdateRelease(rlsName, chart, 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() + + 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() - return h.opts.rpcGetReleaseContent(rlsName, rls.NewReleaseServiceClient(c), opts...) + rlc := rls.NewReleaseServiceClient(c) + return rlc.GetHistory(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/interface.go b/pkg/helm/interface.go index 528af8908..5c6921afe 100644 --- a/pkg/helm/interface.go +++ b/pkg/helm/interface.go @@ -27,5 +27,8 @@ 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) + 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 2af4742da..a445e8643 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -17,9 +17,14 @@ limitations under the License. package helm import ( + "github.com/golang/protobuf/proto" "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 @@ -29,37 +34,49 @@ 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 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 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 + // 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 + // 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 + // release history options are applied directly to the get release history request + histReq rls.GetHistoryRequest } -// Home specifies the location of helm home, (default = "$HOME/.helm"). -func Home(home string) Option { +// Host specifies the host address of the Tiller release server, (default = ":44134"). +func Host(host string) Option { return func(opts *options) { - opts.home = home + opts.host = host } } -// Host specifies the host address of the Tiller release server, (default = ":44134"). -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.host = host + opts.before = fn } } @@ -103,6 +120,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. @@ -115,17 +142,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)} } } @@ -143,15 +170,15 @@ func DeleteDryRun(dry bool) DeleteOption { } } -// UpgradeDisableHooks will disable hooks for an upgrade operation. -func UpgradeDisableHooks(disable bool) UpdateOption { +// 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.disableHooks = disable + opts.uninstallReq.Purge = purge } } -// UpgradeDryRun will (if true) execute an upgrade as a dry run. -func UpgradeDryRun(dry 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 } @@ -164,13 +191,6 @@ func InstallDisableHooks(disable bool) InstallOption { } } -// InstallDryRun will (if true) execute an installation as a dry run. -func InstallDryRun(dry bool) InstallOption { - return func(opts *options) { - opts.dryRun = dry - } -} - // InstallReuseName will (if true) instruct Tiller to re-use an existing name. func InstallReuseName(reuse bool) InstallOption { return func(opts *options) { @@ -178,91 +198,96 @@ func InstallReuseName(reuse bool) InstallOption { } } -// ContentOption -- TODO -type ContentOption func(*options) - -// StatusOption -- TODO -type StatusOption func(*options) - -// DeleteOption -- TODO -type DeleteOption func(*options) - -// UpdateOption allows specifying various settings -// configurable by the helm client user for overriding -// the defaults used when running the `helm upgrade` command. -type UpdateOption 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(context.TODO(), &o.listReq) - if err != nil { - return nil, err +// RollbackDisableHooks will disable hooks for a rollback operation +func RollbackDisableHooks(disable bool) RollbackOption { + return func(opts *options) { + opts.disableHooks = disable } - - return s.Recv() } -// 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) +// RollbackDryRun will (if true) execute a rollback as a dry run. +func RollbackDryRun(dry bool) RollbackOption { + return func(opts *options) { + opts.dryRun = dry } - 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(context.TODO(), &o.instReq) +// RollbackVersion sets the version of the release to deploy. +func RollbackVersion(ver int32) RollbackOption { + return func(opts *options) { + opts.rollbackReq.Version = ver + } } -// Executes tiller.UninstallRelease RPC. -func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) { - for _, opt := range opts { - opt(o) +// UpgradeDisableHooks will disable hooks for an upgrade operation. +func UpgradeDisableHooks(disable bool) UpdateOption { + return func(opts *options) { + opts.disableHooks = disable } - 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 +} + +// UpgradeDryRun will (if true) execute an upgrade as a dry run. +func UpgradeDryRun(dry bool) UpdateOption { + return func(opts *options) { + opts.dryRun = dry } - return rlc.UninstallRelease(context.TODO(), &rls.UninstallReleaseRequest{Name: rlsName, DisableHooks: o.disableHooks}) } -// 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) +// ContentOption allows setting optional attributes when +// performing a GetReleaseContent tiller rpc. +type ContentOption func(*options) + +// 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 } +} - o.updateReq.Chart = chr - o.updateReq.DryRun = o.dryRun - o.updateReq.Name = rlsName +// StatusOption allows setting optional attributes when +// performing a GetReleaseStatus tiller rpc. +type StatusOption func(*options) - return rlc.UpdateRelease(context.TODO(), &o.updateReq) +// StatusReleaseVersion will instruct Tiller to retrieve the status +// of a particular version of a release. +func StatusReleaseVersion(version int32) StatusOption { + return func(opts *options) { + opts.statusReq.Version = version + } } -// 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) +// DeleteOption allows setting optional attributes when +// 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. +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) + +// 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 + } } -// 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) +// NewContext creates a versioned context. +func NewContext() context.Context { + md := metadata.Pairs("x-helm-api-client", version.Version) + return metadata.NewContext(context.TODO(), md) } diff --git a/pkg/ignore/rules.go b/pkg/ignore/rules.go index f5b08a4ee..450e91889 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) @@ -62,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/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 diff --git a/pkg/kube/client.go b/pkg/kube/client.go index cb7e4590b..5e46bf44e 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" @@ -29,6 +30,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 +56,25 @@ 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 +// 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 @@ -96,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 { @@ -117,13 +138,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). @@ -132,11 +153,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() @@ -145,10 +166,12 @@ func (c *Client) Update(namespace string, currentReader, modifiedReader io.Reade return err } - modifiedInfos := []*resource.Info{} + targetInfos := []*resource.Info{} + updateErrors := []string{} + + err = target.Visit(func(info *resource.Info, err error) error { - modified.Visit(func(info *resource.Info, err error) error { - modifiedInfos = append(modifiedInfos, info) + targetInfos = append(targetInfos, info) if err != nil { return err } @@ -176,16 +199,28 @@ 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 + 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 err + return nil }) - deleteUnwantedResources(currentInfos, modifiedInfos) + deleteUnwantedResources(currentInfos, targetInfos) + + 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 @@ -194,19 +229,33 @@ func (c *Client) Update(namespace string, currentReader, modifiedReader io.Reade 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) - return reaper.Stop(info.Namespace, info.Name, 0, nil) + err = reaper.Stop(info.Namespace, info.Name, 0, nil) + 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 @@ -271,7 +320,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) @@ -279,7 +328,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 } @@ -295,7 +344,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 ErrAlreadyExists{target.Name} } patch, err := strategicpatch.CreateStrategicMergePatch(originalJS, editedJS, currentObj) @@ -304,12 +353,9 @@ 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 { - return err - } - - return nil + helper := resource.NewHelper(target.Client, target.Mapping) + _, err = helper.Patch(target.Namespace, target.Name, api.StrategicMergePatchType, patch) + return err } func watchUntilReady(info *resource.Info) error { @@ -390,10 +436,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 } diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index c59cc37b0..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" @@ -126,11 +125,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 @@ -278,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 } 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 ace4acf8c..a4d66a629 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -23,11 +23,10 @@ import ( "os" "path/filepath" "regexp" - "strings" "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" @@ -92,7 +91,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 @@ -120,35 +120,9 @@ 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"} + validExtensions := []string{".yaml", ".tpl", ".txt"} for _, b := range validExtensions { if b == ext { @@ -156,7 +130,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 "") @@ -178,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 c60152794..5a56e22c9 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 { @@ -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 37a013a4a..c3afc3f44 100644 --- a/pkg/proto/hapi/chart/chart.pb.go +++ b/pkg/proto/hapi/chart/chart.pb.go @@ -33,7 +33,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). @@ -95,21 +97,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 b753f8608..41e0999f4 100644 --- a/pkg/proto/hapi/chart/metadata.pb.go +++ b/pkg/proto/hapi/chart/metadata.pb.go @@ -67,6 +67,10 @@ 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"` + // The API Version of this chart. + ApiVersion string `protobuf:"bytes,10,opt,name=apiVersion" json:"apiVersion,omitempty"` } func (m *Metadata) Reset() { *m = Metadata{} } @@ -87,23 +91,27 @@ 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{ - // 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, + // 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 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..57581b14e 100644 --- a/pkg/proto/hapi/release/hook.pb.go +++ b/pkg/proto/hapi/release/hook.pb.go @@ -31,18 +31,22 @@ 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 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{ @@ -53,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 { @@ -79,7 +87,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 +109,29 @@ 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, + // 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 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 33f418aac..79c721e31 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{} } @@ -74,22 +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{ - // 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, + // 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 838e83647..074418600 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -18,10 +18,16 @@ It has these top-level messages: GetReleaseContentResponse UpdateReleaseRequest UpdateReleaseResponse + RollbackReleaseRequest + RollbackReleaseResponse InstallReleaseRequest InstallReleaseResponse UninstallReleaseRequest UninstallReleaseResponse + GetVersionRequest + GetVersionResponse + GetHistoryRequest + GetHistoryResponse */ package services @@ -32,6 +38,8 @@ 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 hapi_version "k8s.io/helm/pkg/proto/hapi/version" import ( context "golang.org/x/net/context" @@ -45,7 +53,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 @@ -113,8 +123,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,packed,name=status_codes,json=statusCodes,enum=hapi.release.Status_Code" json:"status_codes,omitempty"` } func (m *ListReleasesRequest) Reset() { *m = ListReleasesRequest{} } @@ -160,6 +172,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{} } @@ -173,6 +187,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{} } @@ -191,6 +207,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{} } @@ -266,6 +284,39 @@ 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"` + // 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{} } +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. @@ -291,7 +342,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 { @@ -315,7 +366,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 { @@ -330,12 +381,14 @@ 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"` + // 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"` } 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 { @@ -346,7 +399,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 { @@ -355,6 +408,61 @@ 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{15} } + +type GetVersionResponse struct { + Version *hapi_version.Version `protobuf:"bytes,1,opt,name=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{16} } + +func (m *GetVersionResponse) GetVersion() *hapi_version.Version { + if m != nil { + return m.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") @@ -365,10 +473,16 @@ 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") 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) } @@ -379,7 +493,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 @@ -391,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) @@ -399,6 +513,12 @@ 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) + // 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 { @@ -486,6 +606,33 @@ 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 +} + +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 +} + +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 { @@ -496,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) @@ -504,6 +651,12 @@ 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) + // 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) { @@ -531,64 +684,148 @@ 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 { + if interceptor == nil { + return srv.(ReleaseServiceServer).UninstallRelease(ctx, in) + } + 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, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetVersionRequest) + if err := dec(in); err != nil { return nil, err } - return out, nil + if interceptor == nil { + return srv.(ReleaseServiceServer).GetVersion(ctx, in) + } + 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) +} + +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) +} + +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{ @@ -615,6 +852,18 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ MethodName: "UninstallRelease", Handler: _ReleaseService_UninstallRelease_Handler, }, + { + MethodName: "GetVersion", + Handler: _ReleaseService_GetVersion_Handler, + }, + { + MethodName: "RollbackRelease", + Handler: _ReleaseService_RollbackRelease_Handler, + }, + { + MethodName: "GetHistory", + Handler: _ReleaseService_GetHistory_Handler, + }, }, Streams: []grpc.StreamDesc{ { @@ -623,57 +872,74 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ ServerStreams: true, }, }, + Metadata: fileDescriptor0, } +func init() { proto.RegisterFile("hapi/services/tiller.proto", fileDescriptor0) } + 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, + // 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 new file mode 100644 index 000000000..79771408e --- /dev/null +++ b/pkg/proto/hapi/version/version.pb.go @@ -0,0 +1,61 @@ +// 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. +// 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 + SemVer string `protobuf:"bytes,1,opt,name=sem_ver,json=semVer" json:"sem_ver,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{} } +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") +} + +func init() { proto.RegisterFile("hapi/version/version.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 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, 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, +} 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..90b14cdc5 --- /dev/null +++ b/pkg/provenance/sign.go @@ -0,0 +1,360 @@ +/* +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" + "os" + "path/filepath" + "strings" + + "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 collection 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"` +} + +// 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 + // FileName is the name of the file that FileHash verifies. + FileName string +} + +// 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 +} + +// NewFromKeyring reads a keyring file and creates a Signatory. +// +// If id is not the empty string, this will also try to find an Entity in the +// keyring whose name matches, and set that as the signing entity. It will return +// an error if the id is not empty and also not found. +func NewFromKeyring(keyringfile, id string) (*Signatory, error) { + ring, err := loadKeyRing(keyringfile) + if err != nil { + return nil, err + } + + s := &Signatory{KeyRing: ring} + + // If the ID is empty, we can return now. + if id == "" { + return s, nil + } + + // We're gonna go all GnuPG on this and look for a string that _contains_. If + // two or more keys contain the string and none are a direct match, we error + // out. + var candidate *openpgp.Entity + vague := false + for _, e := range ring { + for n := range e.Identities { + if n == id { + s.Entity = e + return s, nil + } + if strings.Contains(n, id) { + if candidate != nil { + vague = true + } + candidate = e + } + } + } + if vague { + return s, fmt.Errorf("more than one key contain the id %q", id) + } + s.Entity = candidate + return s, nil +} + +// ClearSign 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 == nil || 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 +} + +// Verify checks a signature and verifies that it is legit for a chart. +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 ver, err + } else if fi.IsDir() { + return ver, fmt.Errorf("%s cannot be a directory", fname) + } + } + + // First verify the signature + sig, err := s.decodeSignature(sigpath) + if err != nil { + return ver, fmt.Errorf("failed to decode signature: %s", err) + } + + by, err := s.verifySignature(sig) + if err != nil { + return ver, err + } + ver.SignedBy = by + + // Second, verify the hash of the tarball. + sum, err := DigestFile(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 + ver.FileName = basename + + // TODO: when image signing is added, verify that here. + + return ver, 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 := DigestFile(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) +} + +// 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 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, in) + 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..747a9376a --- /dev/null +++ b/pkg/provenance/sign_test.go @@ -0,0 +1,276 @@ +/* +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" + "path/filepath" + "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 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 { + t.Fatal(err) + } + + if _, ok := s.Entity.Identities[testKeyName]; !ok { + t.Errorf("Expected to load a key for user %q", testKeyName) + } +} + +func TestDigestFile(t *testing.T) { + hash, err := DigestFile(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) + } + + 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") + } else if ver.FileName != filepath.Base(testChartfile) { + t.Errorf("FileName is unexpectedly %q", ver.FileName) + } + + if _, err = signer.Verify(testChartfile, testTamperedSigBlock); err == nil { + 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 000000000..1e89b524f Binary files /dev/null and b/pkg/provenance/testdata/hashtest-1.2.3.tgz differ diff --git a/pkg/provenance/testdata/hashtest.sha256 b/pkg/provenance/testdata/hashtest.sha256 new file mode 100644 index 000000000..829031f9d --- /dev/null +++ b/pkg/provenance/testdata/hashtest.sha256 @@ -0,0 +1 @@ +8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75 hashtest-1.2.3.tgz diff --git a/pkg/provenance/testdata/hashtest/.helmignore b/pkg/provenance/testdata/hashtest/.helmignore new file mode 100644 index 000000000..435b756d8 --- /dev/null +++ b/pkg/provenance/testdata/hashtest/.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/pkg/provenance/testdata/hashtest/Chart.yaml b/pkg/provenance/testdata/hashtest/Chart.yaml new file mode 100755 index 000000000..342631ef8 --- /dev/null +++ b/pkg/provenance/testdata/hashtest/Chart.yaml @@ -0,0 +1,3 @@ +description: Test chart versioning +name: hashtest +version: 1.2.3 diff --git a/pkg/provenance/testdata/hashtest/values.yaml b/pkg/provenance/testdata/hashtest/values.yaml new file mode 100644 index 000000000..0827a01fb --- /dev/null +++ b/pkg/provenance/testdata/hashtest/values.yaml @@ -0,0 +1,4 @@ +# Default values for hashtest. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name: value diff --git a/pkg/provenance/testdata/helm-test-key.pub b/pkg/provenance/testdata/helm-test-key.pub new file mode 100644 index 000000000..38714f25a Binary files /dev/null and b/pkg/provenance/testdata/helm-test-key.pub differ diff --git a/pkg/provenance/testdata/helm-test-key.secret b/pkg/provenance/testdata/helm-test-key.secret new file mode 100644 index 000000000..a966aef93 Binary files /dev/null and b/pkg/provenance/testdata/helm-test-key.secret differ diff --git a/pkg/provenance/testdata/msgblock.yaml b/pkg/provenance/testdata/msgblock.yaml new file mode 100644 index 000000000..0fdbda8ce --- /dev/null +++ b/pkg/provenance/testdata/msgblock.yaml @@ -0,0 +1,7 @@ +description: Test chart versioning +name: hashtest +version: 1.2.3 + +... +files: + hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75 diff --git a/pkg/provenance/testdata/msgblock.yaml.asc b/pkg/provenance/testdata/msgblock.yaml.asc new file mode 100644 index 000000000..5a34d6c52 --- /dev/null +++ b/pkg/provenance/testdata/msgblock.yaml.asc @@ -0,0 +1,21 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +description: Test chart versioning +name: hashtest +version: 1.2.3 + +... +files: + hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75 +-----BEGIN PGP SIGNATURE----- +Comment: GPGTools - https://gpgtools.org + +iQEcBAEBCgAGBQJXlp8KAAoJEIQ7v5gfwYdiE7sIAJYDiza+asekeooSXLvQiK+G +PKnveqQpx49EZ6L7Y7UlW25SyH8EjXXHeJysDywCXF3w4luxN9n56ffU0KEW11IY +F+JSjmgIWLS6ti7ZAGEi6JInQ/30rOAIpTEBRBL2IueW3m63mezrGK6XkBlGqpor +C9WKeqLi+DWlMoBtsEy3Uk0XP6pn/qBFICYAbLQQU0sCCUT8CBA8f8aidxi7aw9t +i404yYF+Dvc6i4JlSG77SV0ZJBWllUvsWoCd9Jli0NAuaMqmE7mzcEt/dE+Fm2Ql +Bx3tr1WS4xTRiFQdcOttOl93H+OaHTh+Y0qqLTzzpCvqmttG0HfI6lMeCs7LeyA= +=vEK+ +-----END PGP SIGNATURE----- diff --git a/pkg/provenance/testdata/msgblock.yaml.tampered b/pkg/provenance/testdata/msgblock.yaml.tampered new file mode 100644 index 000000000..f15811bb2 --- /dev/null +++ b/pkg/provenance/testdata/msgblock.yaml.tampered @@ -0,0 +1,21 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +description: Test chart versioning +name: hashtest +version: 1.2.3+tampered + +... +files: + hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75 +-----BEGIN PGP SIGNATURE----- +Comment: GPGTools - https://gpgtools.org + +iQEcBAEBCgAGBQJXlp8KAAoJEIQ7v5gfwYdiE7sIAJYDiza+asekeooSXLvQiK+G +PKnveqQpx49EZ6L7Y7UlW25SyH8EjXXHeJysDywCXF3w4luxN9n56ffU0KEW11IY +F+JSjmgIWLS6ti7ZAGEi6JInQ/30rOAIpTEBRBL2IueW3m63mezrGK6XkBlGqpor +C9WKeqLi+DWlMoBtsEy3Uk0XP6pn/qBFICYAbLQQU0sCCUT8CBA8f8aidxi7aw9t +i404yYF+Dvc6i4JlSG77SV0ZJBWllUvsWoCd9Jli0NAuaMqmE7mzcEt/dE+Fm2Ql +Bx3tr1WS4xTRiFQdcOttOl93H+OaHTh+Y0qqLTzzpCvqmttG0HfI6lMeCs7LeyA= +=vEK+ +-----END PGP SIGNATURE----- diff --git a/pkg/provenance/testdata/regen-hashtest.sh b/pkg/provenance/testdata/regen-hashtest.sh new file mode 100755 index 000000000..4381fd0b1 --- /dev/null +++ b/pkg/provenance/testdata/regen-hashtest.sh @@ -0,0 +1,3 @@ +#!/bin/sh +helm package hashtest +shasum -a 256 hashtest-1.2.3.tgz > hashtest.sha256 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 91846b006..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 +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/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 c0187fc69..d9d04762c 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -17,33 +17,198 @@ limitations under the License. package repo import ( + "encoding/json" + "errors" + "fmt" "io/ioutil" "net/http" + "net/url" + "os" + "path" + "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" + "k8s.io/helm/pkg/provenance" ) var indexPath = "index.yaml" +// APIVersionV1 is the v1 API version for index and repository files. +const APIVersionV1 = "v1" + +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. +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{ + 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) { + u := filename + if baseURL != "" { + var err error + _, file := filepath.Split(filename) + u, err = urlJoin(baseURL, file) + if err != nil { + u = filepath.Join(baseURL, file) + } + } + cr := &ChartVersion{ + URLs: []string{u}, + 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 { + _, err := i.Get(name, version) + return err == nil +} + +// 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)) + } +} + +// 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 len(vs) == 0 { + return nil, ErrNoChartVersion + } + if len(version) == 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, fmt.Errorf("No chart version found for %s-%s", name, version) +} + +// 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 + +// ChartVersion represents a chart entry in the IndexFile +type ChartVersion struct { + *chart.Metadata + URLs []string `json:"urls"` + Created time.Time `json:"created,omitempty"` + Removed bool `json:"removed,omitempty"` + Digest string `json:"digest,omitempty"` } -// 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"` - Checksum string `yaml:"checksum,omitempty"` - Chartfile *chart.Metadata `yaml:"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 +// DownloadIndexFile fetches the index from a repository. func DownloadIndexFile(repoName, url, indexFilePath string) error { var indexURL string @@ -54,44 +219,78 @@ 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 { - if _, ok := err.(*yaml.TypeError); !ok { - return err - } +// 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 } - i.Entries = refs - return nil + if i.APIVersion == "" { + // When we leave Beta, we should remove legacy support and just + // return this error: + //return i, ErrNoAPIVersion + return loadUnversionedIndex(data) + } + return i, nil } -func (i *IndexFile) addEntry(name string, url string) ([]byte, error) { - if i.Entries == nil { - i.Entries = make(map[string]*ChartRef) - } - entry := ChartRef{Name: name, URL: url} - i.Entries[name] = &entry - out, err := yaml.Marshal(&i.Entries) +// 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 + } - return out, nil + 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 @@ -100,12 +299,23 @@ func LoadIndexFile(path string) (*IndexFile, error) { if err != nil { return nil, err } + return LoadIndex(b) +} - var indexfile IndexFile - err = yaml.Unmarshal(b, &indexfile) +// 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 nil, err + return "", err } - - return &indexfile, nil + // 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 4fd4c255b..080023f24 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,48 +113,203 @@ 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") +} + +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" + frobs, ok := index.Entries[cname] + if !ok { + t.Fatalf("Could not read chart %s", cname) } - if !alpine { - t.Errorf("alpine entry was not decoded properly") + + frob := frobs[0] + if len(frob.Digest) == 0 { + t.Errorf("Missing digest of file %s.", frob.Name) + } + if frob.URLs[0] != "http://localhost:8080/frobnitz-1.2.3.tgz" { + t.Errorf("Unexpected URLs: %v", frob.URLs) + } + if frob.Name != "frobnitz" { + 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) + } +} + +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]) + } +} + +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/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 c4ed13c52..baa66a7d9 100644 --- a/pkg/repo/repo.go +++ b/pkg/repo/repo.go @@ -17,21 +17,24 @@ limitations under the License. package repo // import "k8s.io/helm/pkg/repo" import ( - "crypto/sha1" "errors" "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 @@ -40,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 + } + } + 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) } - rf.Repositories = repos - return nil + r.Repositories = cp + return found } -// LoadChartRepository takes in a path to a local chart repository -// which contains packaged charts and an index.yaml file +// 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 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 @@ -85,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 @@ -108,80 +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 - hash, err := generateChecksum(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 = time.Now().UTC().String() + 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, Checksum: hash, 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 generateChecksum(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) - - return fmt.Sprintf("%x", result), nil -} diff --git a/pkg/repo/repo_test.go b/pkg/repo/repo_test.go index 9b88e238a..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.Checksum == "" { - t.Errorf("Checksum 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/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..8094a245c --- /dev/null +++ b/pkg/repo/repotest/server.go @@ -0,0 +1,168 @@ +/* +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" +) + +// 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. +// +// 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 + } + + 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 err + } + + d, err := yaml.Marshal(index) + if err != nil { + return err + } + + ifile := filepath.Join(s.docroot, "index.yaml") + return ioutil.WriteFile(ifile, d, 0755) +} + +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 +} + +// 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) +} diff --git a/pkg/repo/repotest/server_test.go b/pkg/repo/repotest/server_test.go new file mode 100644 index 000000000..1d4c78e41 --- /dev/null +++ b/pkg/repo/repotest/server_test.go @@ -0,0 +1,127 @@ +/* +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" + + "github.com/ghodss/yaml" + + "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 + } + + m := repo.NewIndexFile() + if err := yaml.Unmarshal(data, m); err != nil { + t.Error(err) + return + } + + if l := len(m.Entries); l != 1 { + t.Errorf("Expected 1 entry, got %d", l) + return + } + + expect := "examplechart" + if !m.Has(expect, "0.1.0") { + t.Errorf("missing %q", expect) + } + + 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) + } +} + +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) + } +} 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 000000000..aec86c640 Binary files /dev/null and b/pkg/repo/repotest/testdata/examplechart-0.1.0.tgz differ diff --git a/pkg/repo/repotest/testdata/examplechart/.helmignore b/pkg/repo/repotest/testdata/examplechart/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/pkg/repo/repotest/testdata/examplechart/.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/pkg/repo/repotest/testdata/examplechart/Chart.yaml b/pkg/repo/repotest/testdata/examplechart/Chart.yaml new file mode 100755 index 000000000..8e06de648 --- /dev/null +++ b/pkg/repo/repotest/testdata/examplechart/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: examplechart +version: 0.1.0 diff --git a/pkg/repo/repotest/testdata/examplechart/values.yaml b/pkg/repo/repotest/testdata/examplechart/values.yaml new file mode 100644 index 000000000..5170c61e3 --- /dev/null +++ b/pkg/repo/repotest/testdata/examplechart/values.yaml @@ -0,0 +1,4 @@ +# Default values for examplechart. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name: value 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 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: "" diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index 0925ebc26..f33142729 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -30,17 +30,15 @@ import ( "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 b64 = base64.StdEncoding +var _ Driver = (*ConfigMaps)(nil) -// labels is a map of key value pairs to be included as metadata in a configmap object. -type labels map[string]string +// ConfigMapsDriverName is the string name of the driver. +const ConfigMapsDriverName = "ConfigMap" -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 } +var b64 = base64.StdEncoding // ConfigMaps is a wrapper around an implementation of a kubernetes // ConfigMapsInterface. @@ -54,6 +52,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) { @@ -81,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 @@ -104,9 +110,41 @@ func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Releas 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 { +func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { // set labels for configmaps object meta data var lbs labels @@ -114,7 +152,7 @@ func (cfgmaps *ConfigMaps) Create(rls *rspb.Release) error { lbs.set("CREATED_AT", strconv.Itoa(int(time.Now().Unix()))) // create a new configmap to hold the release - obj, err := newConfigMapsObject(rls, lbs) + obj, err := newConfigMapsObject(key, rls, lbs) if err != nil { logerrf(err, "create: failed to encode release %q", rls.Name) return err @@ -133,7 +171,7 @@ func (cfgmaps *ConfigMaps) Create(rls *rspb.Release) error { // 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 { +func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { // set labels for configmaps object meta data var lbs labels @@ -141,7 +179,7 @@ func (cfgmaps *ConfigMaps) Update(rls *rspb.Release) error { lbs.set("MODIFIED_AT", strconv.Itoa(int(time.Now().Unix()))) // create a new configmap object to hold the release - obj, err := newConfigMapsObject(rls, lbs) + obj, err := newConfigMapsObject(key, rls, lbs) if err != nil { logerrf(err, "update: failed to encode release %q", rls.Name) return err @@ -186,7 +224,7 @@ 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) { +func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*api.ConfigMap, error) { const owner = "TILLER" // encode the release @@ -208,7 +246,7 @@ func newConfigMapsObject(rls *rspb.Release, lbs labels) (*api.ConfigMap, error) // create and return configmap object return &api.ConfigMap{ ObjectMeta: api.ObjectMeta{ - Name: rls.Name, + Name: key, Labels: lbs.toMap(), }, Data: map[string]string{"release": s}, diff --git a/pkg/storage/driver/cfgmaps_test.go b/pkg/storage/driver/cfgmaps_test.go index 95638057d..f89e26b36 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. @@ -21,17 +18,22 @@ import ( "testing" 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 TestConfigMapName(t *testing.T) { + 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) + vers := int32(1) + name := "smug-pigeon" + key := testKey(name, vers) + rel := releaseStub(name, vers, rspb.Status_DEPLOYED) - cfgmaps := newTestFixture(t, []*rspb.Release{rel}...) + cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) // get release with key got, err := cfgmaps.Get(key) @@ -45,13 +47,13 @@ func TestConfigMapGet(t *testing.T) { } 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), + 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 @@ -92,13 +94,15 @@ func TestConfigMapList(t *testing.T) { } func TestConfigMapCreate(t *testing.T) { - cfgmaps := newTestFixture(t) + cfgmaps := newTestFixtureCfgMaps(t) - key := "key-1" - rel := newTestRelease(key, 1, rspb.Status_DEPLOYED) + 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(rel); err != nil { + if err := cfgmaps.Create(key, rel); err != nil { t.Fatalf("Failed to create release with key %q: %s", key, err) } @@ -115,16 +119,18 @@ func TestConfigMapCreate(t *testing.T) { } func TestConfigMapUpdate(t *testing.T) { - key := "key-1" - rel := newTestRelease(key, 1, rspb.Status_DEPLOYED) + vers := int32(1) + name := "smug-pigeon" + key := testKey(name, vers) + rel := releaseStub(name, vers, rspb.Status_DEPLOYED) - cfgmaps := newTestFixture(t, []*rspb.Release{rel}...) + cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) - // modify release status code & version - rel = newTestRelease(key, 2, rspb.Status_SUPERSEDED) + // modify release status code + rel.Info.Status.Code = rspb.Status_SUPERSEDED // perform the update - if err := cfgmaps.Update(rel); err != nil { + if err := cfgmaps.Update(key, rel); err != nil { t.Fatalf("Failed to update release: %s", err) } @@ -135,85 +141,7 @@ func TestConfigMapUpdate(t *testing.T) { } // check release has actually been updated by comparing modified fields - switch { - case rel.Info.Status.Code != got.Info.Status.Code: + 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) - 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 } diff --git a/pkg/storage/driver/driver.go b/pkg/storage/driver/driver.go index 0cf51d6f6..45b442cd7 100644 --- a/pkg/storage/driver/driver.go +++ b/pkg/storage/driver/driver.go @@ -27,6 +27,8 @@ var ( 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. @@ -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) + Query(labels map[string]string) ([]*rspb.Release, error) } // Driver is the interface composed of Creator, Updator, Deletor, Queryor @@ -73,4 +78,5 @@ type Driver interface { 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..23538d214 --- /dev/null +++ b/pkg/storage/driver/labels.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 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..af0bd24e5 --- /dev/null +++ b/pkg/storage/driver/labels_test.go @@ -0,0 +1,49 @@ +/* +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" +) + +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 bea495133..e3b5bb222 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -14,65 +14,119 @@ 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 ( + "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 + 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 } // 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 + 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 } - return nil, ErrReleaseNotFound } // 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]) - } + 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 releases, nil + return ls, nil } // Create creates a new release or returns ErrReleaseExists. -func (mem *Memory) Create(rls *rspb.Release) error { +func (mem *Memory) Create(key string, rls *rspb.Release) error { defer unlock(mem.wlock()) - if _, ok := mem.cache[rls.Name]; ok { - return ErrReleaseExists + 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] = rls + 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 { +func (mem *Memory) Update(key string, rls *rspb.Release) error { defer unlock(mem.wlock()) - if _, ok := mem.cache[rls.Name]; ok { - mem.cache[rls.Name] = rls + if rs, ok := mem.cache[rls.Name]; ok && rs.Exists(key) { + rs.Replace(key, newRecord(key, rls)) return nil } return ErrReleaseNotFound @@ -82,27 +136,55 @@ func (mem *Memory) Update(rls *rspb.Release) error { func (mem *Memory) Delete(key string) (*rspb.Release, error) { defer unlock(mem.wlock()) - if old, ok := mem.cache[key]; ok { - delete(mem.cache, key) - return old, nil + 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 + }) + } + + _, err := w.Write(b.Bytes()) + return err } // wlock locks mem for writing func (mem *Memory) wlock() func() { mem.Lock() - return func() { - mem.Unlock() - } + return func() { mem.Unlock() } } // rlock locks mem for reading func (mem *Memory) rlock() func() { mem.RLock() - return func() { - mem.RUnlock() - } + 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 b02f8350b..8407e588c 100644 --- a/pkg/storage/driver/memory_test.go +++ b/pkg/storage/driver/memory_test.go @@ -14,7 +14,7 @@ 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" @@ -23,70 +23,146 @@ import ( rspb "k8s.io/helm/pkg/proto/hapi/release" ) -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) +func TestMemoryName(t *testing.T) { + if mem := NewMemory(); mem.Name() != MemoryDriverName { + t.Errorf("Expected name to be %q, got %q", MemoryDriverName, mem.Name()) } +} - res, err := mem.Get(key) - if err != nil { - t.Errorf("Could not get %s: %s", key, err) +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, + }, } - if res.Name != key { - t.Errorf("Expected %s, got %s", key, res.Name) + + 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 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) +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}, } - if mem.cache[key].Name != key { - t.Errorf("Unexpected release name: %s", mem.cache[key].Name) + + 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 TestMemoryUpdate(t *testing.T) { - key := "test-1" - rls := &rspb.Release{Name: key} +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) + } - mem := NewMemory() - if err := mem.Create(rls); err != nil { - t.Fatalf("Failed create: %s", err) + if tt.xlen != len(l) { + t.Fatalf("Expected %d results, actual %d\n", tt.xlen, len(l)) + } } - if err := mem.Update(rls); err != nil { - t.Fatalf("Failed update: %s", err) +} + +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, + }, } - if mem.cache[key].Name != key { - t.Errorf("Unexpected release name: %s", mem.cache[key].Name) + + 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) + 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}, } - 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) + 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/mock_test.go b/pkg/storage/driver/mock_test.go new file mode 100644 index 000000000..d21cd0f2f --- /dev/null +++ b/pkg/storage/driver/mock_test.go @@ -0,0 +1,141 @@ +/* +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" + + 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}}, + } +} + +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 +} + +// 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{} + + 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 + } +} + +// Get returns the ConfigMap by name. +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 +} + +// 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 { + list.Items = append(list.Items, *cfgmap) + } + 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 { + return object, kberrs.NewAlreadyExists(api.Resource("tests"), name) + } + mock.objects[name] = cfgmap + 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 { + return nil, kberrs.NewNotFound(api.Resource("tests"), name) + } + mock.objects[name] = cfgmap + 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) + } + delete(mock.objects, name) + return nil +} diff --git a/pkg/storage/driver/records.go b/pkg/storage/driver/records.go new file mode 100644 index 000000000..e625bb5e1 --- /dev/null +++ b/pkg/storage/driver/records.go @@ -0,0 +1,133 @@ +/* +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" + + 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("OWNER", "TILLER") + 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..99f8b2d0d --- /dev/null +++ b/pkg/storage/driver/records_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 driver // import "k8s.io/helm/pkg/storage/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-existent 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/storage.go b/pkg/storage/storage.go index 85b204fcc..25ab6dae4 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -17,9 +17,11 @@ limitations under the License. package storage // import "k8s.io/helm/pkg/storage" import ( + "fmt" "log" rspb "k8s.io/helm/pkg/proto/hapi/release" + relutil "k8s.io/helm/pkg/releaseutil" "k8s.io/helm/pkg/storage/driver" ) @@ -30,34 +32,34 @@ type Storage struct { // 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 @@ -72,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) }) } @@ -81,30 +83,69 @@ 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) }) } +// 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, + "OWNER": "TILLER", + "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 + } +} + +// 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. +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 { diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go index 25975bf1b..5935b4e88 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/storage/storage_test.go @@ -30,11 +30,15 @@ func TestStorageCreate(t *testing.T) { storage := Init(driver.NewMemory()) // create fake release - rls := ReleaseTestData{Name: "angry-beaver"}.ToRelease() + rls := ReleaseTestData{ + Name: "angry-beaver", + Version: 1, + }.ToRelease() + assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") // fetch the release - res, err := storage.Get(rls.Name) + res, err := storage.Get(rls.Name, rls.Version) assertErrNil(t.Fatal, err, "QueryRelease") // verify the fetched and created release are the same @@ -48,16 +52,20 @@ func TestStorageUpdate(t *testing.T) { storage := Init(driver.NewMemory()) // create fake release - rls := ReleaseTestData{Name: "angry-beaver"}.ToRelease() + rls := ReleaseTestData{ + Name: "angry-beaver", + Version: 1, + Status: rspb.Status_DEPLOYED, + }.ToRelease() + assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") // modify the release - rls.Version = 2 - rls.Manifest = "new-manifest" + rls.Info.Status.Code = rspb.Status_DELETED assertErrNil(t.Fatal, storage.Update(rls), "UpdateRelease") // retrieve the updated release - res, err := storage.Get(rls.Name) + res, err := storage.Get(rls.Name, rls.Version) assertErrNil(t.Fatal, err, "QueryRelease") // verify updated and fetched releases are the same. @@ -71,11 +79,15 @@ func TestStorageDelete(t *testing.T) { storage := Init(driver.NewMemory()) // create fake release - rls := ReleaseTestData{Name: "angry-beaver"}.ToRelease() + rls := ReleaseTestData{ + Name: "angry-beaver", + Version: 1, + }.ToRelease() + assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") // delete the release - res, err := storage.Delete(rls.Name) + res, err := storage.Delete(rls.Name, rls.Version) assertErrNil(t.Fatal, err, "DeleteRelease") // verify updated and fetched releases are the same. @@ -134,6 +146,77 @@ func TestStorageList(t *testing.T) { } } +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) + } +} + +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 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/storage/doc.go b/pkg/version/doc.go similarity index 69% rename from pkg/storage/doc.go rename to pkg/version/doc.go index 231e30bb9..23c9e500d 100644 --- a/pkg/storage/doc.go +++ b/pkg/version/doc.go @@ -14,9 +14,5 @@ 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" +// 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 128b9a468..c6a934119 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -14,13 +14,41 @@ 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" -// Version is the current version of the Helm. -// Update this whenever making a new release. -// The version is of the format Major.Minor.Patch -// 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" +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.5" + + // BuildMetadata is extra build time data + BuildMetadata = "" + // GitCommit is the git sha1 + GitCommit = "" + // GitTreeState is the state of the git tree + GitTreeState = "" +) + +// GetVersion returns the semver string of the version +func GetVersion() string { + if BuildMetadata == "" { + return Version + } + return Version + "+" + BuildMetadata +} + +// GetVersionProto returns protobuf representing the version +func GetVersionProto() *version.Version { + return &version.Version{ + SemVer: GetVersion(), + GitCommit: GitCommit, + GitTreeState: GitTreeState, + } +} 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 diff --git a/versioning.mk b/versioning.mk index 3b8645392..4825c4e72 100644 --- a/versioning.mk +++ b/versioning.mk @@ -1,7 +1,9 @@ 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) @@ -9,12 +11,14 @@ 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.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)