Merge branch 'master' into bug/clean-fails-missing-tiller-dir

pull/1360/head
Naveen Srinivasan 9 years ago
commit 8dd79eccee

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

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

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

@ -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:

@ -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;
}

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

@ -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;
}

@ -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;
}

@ -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;
}

@ -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}"

@ -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))

@ -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)
}
}

@ -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)

@ -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{},

@ -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)
}
}
}

@ -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()
}

@ -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)
}
}

@ -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)
}
}
}

@ -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()
}

@ -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)
}

@ -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"
}

@ -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
}
}

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

@ -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)
}

@ -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)
}
}

Binary file not shown.

@ -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: ""

@ -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: ""

@ -0,0 +1,6 @@
apiVersion: v1
repositories:
- name: testing
url: "http://example.com"
- name: kubernetes-charts
url: "http://example.com/charts"

Binary file not shown.

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

@ -0,0 +1,5 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
.git

@ -0,0 +1,3 @@
description: A Helm chart for Kubernetes
name: signtest
version: 0.1.0

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

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

@ -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"]

@ -0,0 +1,2 @@
# The pod name
name: my-alpine

@ -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"]

@ -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")
}

@ -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)
}
}
}

@ -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)
}

@ -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)

@ -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)
}

@ -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",

@ -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)
}

@ -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)
}

@ -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
}

@ -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...)
}

@ -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")
}

@ -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)
}

@ -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()
}
}

@ -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
}

@ -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
}

@ -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
}

@ -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)

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

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

@ -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"}),
},
}
}

@ -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
}

@ -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)

@ -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)

@ -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)
}

@ -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)
}
}

@ -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
}

@ -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)
}

@ -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())
}
}

@ -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()
}

@ -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)
}
}

@ -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
}

@ -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
}

@ -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)
}
}

@ -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)
}
}

@ -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!⎈ ")
}

@ -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")
}
}

@ -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
}

@ -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)
}
}

@ -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
}

@ -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)
}

@ -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
}

@ -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)
}

@ -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)
}
}

@ -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)
}
}

@ -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)
}

@ -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
}

@ -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)
}

Binary file not shown.

Binary file not shown.

@ -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: ""

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

@ -0,0 +1,6 @@
apiVersion: v1
repositories:
- name: charts
url: "http://storage.googleapis.com/kubernetes-charts"
- name: local
url: "http://localhost:8879/charts"

Binary file not shown.

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

@ -0,0 +1,3 @@
description: A Helm chart for Kubernetes
name: reqtest
version: 0.1.0

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

@ -0,0 +1,3 @@
description: A Helm chart for Kubernetes
name: reqsubchart
version: 0.1.0

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save