mirror of https://github.com/helm/helm
commit
8dd79eccee
@ -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
|
@ -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;
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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.
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 @@
|
||||
repository/local/index.yaml
|
@ -0,0 +1,30 @@
|
||||
apiVersion: v1
|
||||
entries:
|
||||
alpine:
|
||||
- name: alpine
|
||||
urls:
|
||||
- http://example.com/alpine-1.2.3.tgz
|
||||
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
|
||||
home: https://k8s.io/helm
|
||||
sources:
|
||||
- https://github.com/kubernetes/helm
|
||||
version: 1.2.3
|
||||
description: Deploy a basic Alpine Linux pod
|
||||
keywords: []
|
||||
maintainers: []
|
||||
engine: ""
|
||||
icon: ""
|
||||
- name: alpine
|
||||
urls:
|
||||
- http://example.com/alpine-0.2.0.tgz
|
||||
- http://storage.googleapis.com/kubernetes-charts/alpine-0.2.0.tgz
|
||||
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
|
||||
home: https://k8s.io/helm
|
||||
sources:
|
||||
- https://github.com/kubernetes/helm
|
||||
version: 0.2.0
|
||||
description: Deploy a basic Alpine Linux pod
|
||||
keywords: []
|
||||
maintainers: []
|
||||
engine: ""
|
||||
icon: ""
|
@ -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"]
|
@ -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()
|
||||
}
|
||||
}
|
@ -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"}),
|
||||
},
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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…
Reference in new issue