Commit af38d1b5 authored by Matt Butcher's avatar Matt Butcher

Merge branch 'master' of github.com:deis/tiller into reset

parents d58cfc46 d3de044f
.coverage/
bin/
rootfs/helm
rootfs/tiller
vendor/
_proto/*.pb.go
# Contributing Guidelines
The Kubernetes Helm project accepts contributions via GitHub pull requests. This document outlines the process to help get your contribution accepted.
## Contributor License Agreements
We'd love to accept your patches! Before we can take them, we have to jump a couple of legal hurdles.
Please fill out either the individual or corporate Contributor License Agreement (CLA).
* If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA](http://code.google.com/legal/individual-cla-v1.0.html).
* If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA](http://code.google.com/legal/corporate-cla-v1.0.html).
Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to accept your pull requests.
***NOTE***: Only original source code from you and other people that have signed the CLA can be accepted into the main repository.
## How to Contribute A Patch
1. If you haven't already done so, sign a Contributor License Agreement (see details above).
1. Fork the desired repo, develop and test your code changes.
1. Submit a pull request.
### Merge Approval
Helm collaborators may add "LGTM" (Looks Good To Me) or an equivalent comment to indicate that a PR is acceptable. Any change requires at least one LGTM. No pull requests can be merged until at least one Helm collaborator signs off with an LGTM.
If the PR is from a Helm collaborator, then he or she should be the one to merge and close it. This keeps the commit stream clean and gives the collaborator the benefit of revisiting the PR before deciding whether or not to merge the changes.
## Support Channels
Whether you are a user or contributor, official support channels include:
- GitHub issues: https://github.com/kubenetes/helm/issues/new
- Slack: #Helm room in the [Kubernetes Slack](http://slack.kubernetes.io/)
Before opening a new issue or submitting a new pull request, it's helpful to search the project - it's likely that another user has already reported the issue you're facing, or it's a known issue that we're already aware of.
This diff is collapsed.
DOCKER_REGISTRY ?= gcr.io
IMAGE_PREFIX ?= deis-sandbox
SHORT_NAME ?= tiller
# go option
GO ?= go
GOARCH ?= $(shell go env GOARCH)
GOOS ?= $(shell go env GOOS)
PKG := $(shell glide novendor)
TAGS :=
TESTS := .
TESTFLAGS :=
LDFLAGS :=
GOFLAGS :=
BINDIR := ./bin
BINARIES := helm tiller
include versioning.mk
.PHONY: all
all: build
.PHONY: build
build: GOFLAGS += -a -installsuffix cgo
build:
@for i in $(BINARIES); do \
CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GO) build -o $(BINDIR)/$$i $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' ./cmd/$$i || exit 1; \
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: GOOS = linux
docker-binary: GOARCH = amd64
docker-binary: BINDIR = ./rootfs
docker-binary: build
.PHONY: docker-build
docker-build: check-docker docker-binary
docker build --rm -t ${IMAGE} rootfs
docker tag -f ${IMAGE} ${MUTABLE_IMAGE}
.PHONY: test
test: build
test: TESTFLAGS += -race -v
test: test-style
test: test-unit
.PHONY: test-unit
test-unit:
$(GO) test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS)
.PHONY: test-style
test-style:
@scripts/validate-go.sh
.PHONY: clean
clean:
@rm -rf $(BINDIR)
.PHONY: coverage
coverage:
@scripts/coverage.sh
.PHONY: bootstrap
bootstrap:
glide install
# Kubernetes Helm
Helm is a tool for managing Kubernetes charts. Charts are packages of
pre-configured Kubernetes resources.
## Install
Helm is in its early stages of development. At this time there are no
releases.
To install Helm from source, follow this process:
Make sure you have the prerequisites:
- Go 1.6
- A running Kubernetes cluster
- `kubectl` properly configured to talk to your cluster
- Glide 0.10 or greater
1. Clone (or otherwise download) this repository
2. Run `make boostrap build`
You will now have two binaries built:
- `bin/helm` is the client
- `bin/tiller` is the server
You can locally run Tiller, or you build a Docker image (`make
docker-build`) and then deploy it (`helm init -i IMAGE_NAME`).
The [documentation](docs) folder contains more information about the
architecture and usage of Helm/Tiller.
## The History of the Project
Kubernetes Helm is the merged result of [Helm
Classic](https://github.com/helm/helm) and the Kubernetes port of GCS Deployment
Manager. The project was jointly started by Google and Deis, though it
is now part of the CNCF.
space := $(empty) $(empty)
comma := ,
empty :=
import_path = github.com/deis/tiller/pkg/proto/hapi
dst = ../pkg/proto
target = go
plugins = grpc
chart_ias = $(subst $(space),$(comma),$(addsuffix =$(import_path)/$(chart_pkg),$(addprefix M,$(chart_pbs))))
chart_pbs = $(wildcard hapi/chart/*.proto)
chart_pkg = chart
release_ias = $(subst $(space),$(comma),$(addsuffix =$(import_path)/$(release_pkg),$(addprefix M,$(release_pbs))))
release_pbs = $(wildcard hapi/release/*.proto)
release_pkg = release
services_ias = $(subst $(space),$(comma),$(addsuffix =$(import_path)/$(services_pkg),$(addprefix M,$(services_pbs))))
services_pbs = $(wildcard hapi/services/*.proto)
services_pkg = services
google_deps = Mgoogle/protobuf/timestamp.proto=github.com/golang/protobuf/ptypes/timestamp,Mgoogle/protobuf/any.proto=github.com/golang/protobuf/ptypes/any
all: chart release services
chart:
protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias):$(dst) $(chart_pbs)
release:
protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias):$(dst) $(release_pbs)
services:
protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias),$(release_ias):$(dst) $(services_pbs)
clean:
@rm -rf $(dst)/hapi 2>/dev/null
syntax = "proto3";
package hapi.chart;
import "hapi/chart/config.proto";
import "hapi/chart/metadata.proto";
import "hapi/chart/template.proto";
option go_package = "chart";
//
// Chart:
// A chart is a helm package that contains metadata, a default config, zero or more
// optionally parameterizable templates, and zero or more charts (dependencies).
//
message Chart {
// Contents of the Chartfile.
hapi.chart.Metadata metadata = 1;
// Templates for this chart.
repeated hapi.chart.Template templates = 2;
// Charts that this chart depends on.
repeated Chart dependencies = 3;
// Default config for this template.
hapi.chart.Config values = 4;
}
syntax = "proto3";
package hapi.chart;
option go_package = "chart";
//
// Config:
//
// A config supplies values to the parametrizable templates of a chart.
//
message Config {
string raw = 1;
map<string,Value> values = 2;
}
//
// Value:
//
// TODO
//
message Value {
string value = 1;
}
syntax = "proto3";
package hapi.chart;
option go_package = "chart";
//
// Maintainer:
//
// A descriptor of the Chart maintainer(s).
//
message Maintainer {
// Name is a user name or organization name
string name = 1;
// Email is an optional email address to contact the named maintainer
string email = 2;
}
//
// Metadata:
//
// Metadata for a Chart file. This models the structure
// of a Chart.yaml file.
//
// Spec: https://github.com/kubernetes/helm/blob/master/docs/design/chart_format.md#the-chart-file
//
message Metadata {
// The name of the chart
string name = 1;
// The URL to a relecant project page, git repo, or contact person
string home = 2;
// Source is the URL to the source code of this chart
repeated string sources = 3;
// A SemVer 2 conformant version string of the chart
string version = 4;
// A one-sentence description of the chart
string description = 5;
// A list of string keywords
repeated string keywords = 6;
// A list of name and URL/email address combinations for the maintainer(s)
repeated Maintainer maintainers = 7;
}
syntax = "proto3";
package hapi.chart;
option go_package = "chart";
// Template represents a template as a name/value pair.
//
// By convention, name is a relative path within the scope of the chart's
// base directory.
message Template {
// Name is the path-like name of the template.
string name = 1;
// Data is the template as byte data.
bytes data = 2;
}
syntax = "proto3";
package hapi.release;
import "google/protobuf/timestamp.proto";
import "hapi/release/status.proto";
option go_package = "release";
//
// Info:
//
//
message Info {
Status status = 1;
google.protobuf.Timestamp first_deployed = 2;
google.protobuf.Timestamp last_deployed = 3;
// Deleted tracks when this object was deleted.
google.protobuf.Timestamp deleted = 4;
}
syntax = "proto3";
package hapi.release;
import "hapi/release/info.proto";
import "hapi/chart/config.proto";
import "hapi/chart/chart.proto";
option go_package = "release";
//
// Release:
//
// A release describes a deployment of a chart, together with the chart
// and the variables used to deploy that chart.
//
message Release {
// Name is the name of the release
string name = 1;
// Info provides information about a release
hapi.release.Info info = 2;
// Chart is the chart that was released.
hapi.chart.Chart chart = 3;
// Config is the set of extra Values added to the chart.
// These values override the default values inside of the chart.
hapi.chart.Config config = 4;
// Manifest is the string representation of the rendered template.
string manifest = 5;
}
syntax = "proto3";
package hapi.release;
import "google/protobuf/any.proto";
option go_package = "release";
//
// Status:
//
//
message Status {
enum Code {
UNKNOWN = 0;
DEPLOYED = 1;
DELETED = 2;
SUPERSEDED = 3;
}
Code code = 1;
google.protobuf.Any details = 2;
}
syntax = "proto3";
package hapi.services.probe;
option go_package = "services";
service ProbeService {
rpc Ready(ReadyRequest) returns (ReadyResponse) {
}
}
message ReadyRequest {
}
message ReadyResponse {
}
syntax = "proto3";
package hapi.services.tiller;
import "hapi/chart/chart.proto";
import "hapi/chart/config.proto";
import "hapi/release/release.proto";
import "hapi/release/info.proto";
option go_package = "services";
//
// ReleaseService:
//
// The service that a helm application uses to mutate,
// query, and manage releases.
//
// Release: A named installation composed of a chart and
// config. At any given time a release has one
// chart and one config.
//
// Config: A config is a TOML file that supplies values
// to the parametrizable templates of a chart.
//
// Chart: A chart is a helm package that contains
// metadata, a default config, zero or more
// optionally parameterizable templates, and
// zero or more charts (dependencies).
//
//
service ReleaseService {
//
// Retrieve release history. TODO: Allow filtering the set of releases by
// release status. By default, ListAllReleases returns the releases who
// current status is "Active".
//
rpc ListReleases(ListReleasesRequest) returns (stream ListReleasesResponse) {
}
//
// Retrieve status information for the specified release.
//
rpc GetReleaseStatus(GetReleaseStatusRequest) returns (GetReleaseStatusResponse) {
}
//
// Retrieve the release content (chart + value) for the specifed release.
//
rpc GetReleaseContent(GetReleaseContentRequest) returns (GetReleaseContentResponse) {
}
//
// Update release content.
//
rpc UpdateRelease(UpdateReleaseRequest) returns (UpdateReleaseResponse) {
}
//
// Request release install.
//
rpc InstallRelease(InstallReleaseRequest) returns (InstallReleaseResponse) {
}
//
// Request release deletion.
//
rpc UninstallRelease(UninstallReleaseRequest) returns (UninstallReleaseResponse) {
}
}
//
// ListReleasesRequest:
//
// TODO
//
message ListReleasesRequest {
// The maximum number of releases to be returned
int64 limit = 1;
// The zero-based offset at which the returned release list begins
int64 offset = 2;
}
//
// ListReleasesResponse:
//
// TODO
//
message ListReleasesResponse {
// The expected total number of releases to be returned
int64 count = 1;
// The zero-based offset at which the list is positioned
int64 offset = 2;
// The total number of queryable releases
int64 total = 3;
// The resulting releases
repeated hapi.release.Release releases = 4;
}
// GetReleaseStatusRequest is a request to get the status of a release.
message GetReleaseStatusRequest {
// Name is the name of the release
string name = 1;
}
// GetReleaseStatusResponse is the response indicating the status of the named release.
message GetReleaseStatusResponse {
// Name is the name of the release.
string name = 1;
// Info contains information about the release.
hapi.release.Info info = 2;
}
// GetReleaseContentRequest is a request to get the contents of a release.
message GetReleaseContentRequest {
// The name of the release
string name = 1;
}
// GetReleaseContentResponse is a response containing the contents of a release.
message GetReleaseContentResponse {
// The release content
hapi.release.Release release = 1;
}
//
// UpdateReleaseRequest:
//
// TODO
//
message UpdateReleaseRequest {
}
//
// UpdateReleaseResponse:
//
// TODO
//
message UpdateReleaseResponse {
}
//
// InstallReleaseRequest:
//
// TODO
//
message InstallReleaseRequest {
// Chart is the protobuf representation of a chart.
hapi.chart.Chart chart = 1;
// Values is a string containing (unparsed) TOML values.
hapi.chart.Config values = 2;
// DryRun, if true, will run through the release logic, but neither create
// a release object nor deploy to Kubernetes. The release object returned
// in the response will be fake.
bool dry_run = 3;
}
//
// InstallReleaseResponse:
//
// TODO
//
message InstallReleaseResponse {
hapi.release.Release release = 1;
}
// UninstallReleaseRequest represents a request to uninstall a named release.
message UninstallReleaseRequest {
// Name is the name of the release to delete.
string name = 1;
}
// UninstallReleaseResponse represents a successful response to an uninstall request.
message UninstallReleaseResponse {
// Release is the release that was marked deleted.
hapi.release.Release release = 1;
}
machine:
environment:
GLIDE_VERSION: "0.10.1"
GO15VENDOREXPERIMENT: 1
GOPATH: /usr/local/go_workspace
HOME: /home/ubuntu
IMPORT_PATH: "github.com/deis/tiller"
PATH: $HOME/go/bin:$PATH
GOROOT: $HOME/go
dependencies:
override:
- mkdir -p $HOME/go
- wget "https://storage.googleapis.com/golang/go1.6.linux-amd64.tar.gz"
- tar -C $HOME -xzf go1.6.linux-amd64.tar.gz
- go version
- go env
- sudo chown -R $(whoami):staff /usr/local
- cd $GOPATH
- mkdir -p $GOPATH/src/$IMPORT_PATH
- cd $HOME/tiller
- rsync -az --delete ./ "$GOPATH/src/$IMPORT_PATH/"
- wget "https://github.com/Masterminds/glide/releases/download/$GLIDE_VERSION/glide-$GLIDE_VERSION-linux-amd64.tar.gz"
- mkdir -p $HOME/bin
- tar -vxz -C $HOME/bin --strip=1 -f glide-$GLIDE_VERSION-linux-amd64.tar.gz
- export PATH="$HOME/bin:$PATH" GLIDE_HOME="$HOME/.glide"
- cd $GOPATH/src/$IMPORT_PATH
test:
override:
- cd $GOPATH/src/$IMPORT_PATH && make bootstrap test
package main
import (
"errors"
"path/filepath"
"github.com/deis/tiller/pkg/chart"
"github.com/spf13/cobra"
)
const createDesc = `
This command creates a chart directory along with the common files and
directories used in a chart.
For example, 'helm create foo' will create a directory structure that looks
something like this:
foo/
|- Chart.yaml # Information about your chart
|
|- values.toml # The default values for your templates
|
|- charts/ # Charts that this chart depends on
|
|- templates/ # The template files
'helm create' takes a path for an argument. If directories in the given path
do not exist, Helm will attempt to create them as it goes. If the given
destination exists and there are files in that directory, conflicting files
will be overwritten, but other files will be left alone.
`
func init() {
RootCommand.AddCommand(createCmd)
}
var createCmd = &cobra.Command{
Use: "create [PATH]",
Short: "Create a new chart at the location specified.",
Long: createDesc,
RunE: runCreate,
}
func runCreate(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("the name of the new chart is required")
}
cname := args[0]
cmd.Printf("Creating %s\n", cname)
chartname := filepath.Base(cname)
cfile := chart.Chartfile{
Name: chartname,
Description: "A Helm chart for Kubernetes",
Version: "0.1.0",
}
if _, err := chart.Create(&cfile, filepath.Dir(cname)); err != nil {
return err
}
return nil
}
package main
import (
"fmt"
"io"
"net/http"
"os"
"github.com/spf13/cobra"
)
func init() {
RootCommand.AddCommand(fetchCmd)
}
var fetchCmd = &cobra.Command{
Use: "fetch",
Short: "Download a chart from a repository and unpack it in local directory.",
Long: "",
RunE: fetch,
}
func fetch(cmd *cobra.Command, args []string) error {
// parse args
// get download url
// call download url
out, err := os.Create("nginx-2.0.0.tgz")
if err != nil {
return err
}
defer out.Close()
resp, err := http.Get("http://localhost:8879/charts/nginx-2.0.0.tgz")
fmt.Println("after req")
// unpack file
if err != nil {
return err
}
defer resp.Body.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
return err
}
return nil
}
package main
import (
"errors"
"fmt"
"github.com/deis/tiller/pkg/helm"
"github.com/spf13/cobra"
)
var getHelp = `
This command shows the details of a named release.
It can be used to get extended information about the release, including:
- The values used to generate the release
- The chart used to generate the release
- The generated manifest file
By default, this prints a human readable collection of information about the
chart, the supplied values, and the generated manifest file.
`
var errReleaseRequired = errors.New("release name is required")
var getCommand = &cobra.Command{
Use: "get [flags] RELEASE_NAME",
Short: "Download a named release",
Long: getHelp,
RunE: getCmd,
}
func init() {
RootCommand.AddCommand(getCommand)
}
func getCmd(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errReleaseRequired
}
res, err := helm.GetReleaseContent(args[0])
if err != nil {
return err
}
fmt.Printf("Chart/Version: %s %s\n", res.Release.Chart.Metadata.Name, res.Release.Chart.Metadata.Version)
fmt.Println("Config:")
fmt.Println(res.Release.Config)
fmt.Println("\nManifest:")
fmt.Println(res.Release.Manifest)
return nil
}
package main
import (
"os"
"github.com/spf13/cobra"
)
var stdout = os.Stdout
var helmHome string
var globalUsage = `The Kubernetes package manager
To begin working with Helm, run the 'helm init' command:
$ helm init
This will install Tiller to your running Kubernetes cluster.
It will also set up any necessary local configuration.
Commond actions from this point on include:
- helm search: search for charts
- helm fetch: download a chart to your local directory to view
- helm install: upload the chart to Kubernetes
- helm list: list releases of charts
ENVIRONMENT:
$HELM_HOME: Set an alternative location for Helm files.
By default, these are stored in ~/.helm
`
// RootCommand is the top-level command for Helm.
var RootCommand = &cobra.Command{
Use: "helm",
Short: "The Helm package manager for Kubernetes.",
Long: globalUsage,
}
func init() {
RootCommand.PersistentFlags().StringVar(&helmHome, "home", "$HOME/.helm", "location of you Helm files [$HELM_HOME]")
}
func main() {
RootCommand.Execute()
}
package main
import (
"github.com/spf13/cobra"
)
var longHomeHelp = `
This command displays the location of HELM_HOME. This is where
any helm configuration files live.
`
var homeCommand = &cobra.Command{
Use: "home",
Short: "Displays the location of HELM_HOME",
Long: longHomeHelp,
Run: home,
}
func init() {
RootCommand.AddCommand(homeCommand)
}
func home(cmd *cobra.Command, args []string) {
cmd.Printf(homePath() + "\n")
}
package main
import (
"errors"
"fmt"
"io/ioutil"
"os"
"github.com/deis/tiller/pkg/client"
"github.com/deis/tiller/pkg/kubectl"
"github.com/spf13/cobra"
)
const initDesc = `
This command installs Tiller (the helm server side component) onto your
Kubernetes Cluster and sets up local configuration in $HELM_HOME (default: ~/.helm/)
`
var (
tillerImg string
defaultRepo = map[string]string{"default-name": "default-url"}
)
func init() {
initCmd.Flags().StringVarP(&tillerImg, "tiller-image", "i", "", "override tiller image")
RootCommand.AddCommand(initCmd)
}
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize Helm on both client and server.",
Long: installDesc,
RunE: runInit,
}
// runInit initializes local config and installs tiller to Kubernetes Cluster
func runInit(cmd *cobra.Command, args []string) error {
if len(args) != 0 {
return errors.New("This command does not accept arguments. \n")
}
if err := ensureHome(); err != nil {
return err
}
if err := installTiller(); err != nil {
return err
}
fmt.Println("Happy Helming!")
return nil
}
func installTiller() error {
// TODO: take value of global flag kubectl and pass that in
runner := buildKubectlRunner("")
i := client.NewInstaller()
i.Tiller["Image"] = tillerImg
out, err := i.Install(runner)
if err != nil {
return fmt.Errorf("error installing %s %s", string(out), err)
}
fmt.Println("\nTiller (the helm server side component) has been installed into your Kubernetes Cluster.")
return nil
}
func buildKubectlRunner(kubectlPath string) kubectl.Runner {
if kubectlPath != "" {
kubectl.Path = kubectlPath
}
return &kubectl.RealRunner{}
}
// ensureHome checks to see if $HELM_HOME exists
//
// If $HELM_HOME does not exist, this function will create it.
func ensureHome() error {
configDirectories := []string{homePath(), cacheDirectory(), localRepoDirectory()}
for _, p := range configDirectories {
if fi, err := os.Stat(p); err != nil {
fmt.Printf("Creating %s \n", p)
if err := os.MkdirAll(p, 0755); err != nil {
return fmt.Errorf("Could not create %s: %s", p, err)
}
} else if !fi.IsDir() {
return fmt.Errorf("%s must be a directory", p)
}
}
repoFile := repositoriesFile()
if fi, err := os.Stat(repoFile); err != nil {
fmt.Printf("Creating %s \n", repoFile)
if err := ioutil.WriteFile(repoFile, []byte("local: localhost:8879/charts\n"), 0644); err != nil {
return err
}
} else if fi.IsDir() {
return fmt.Errorf("%s must be a file, not a directory", repoFile)
}
localRepoCacheFile := localRepoDirectory(localRepoCacheFilePath)
if fi, err := os.Stat(localRepoCacheFile); err != nil {
fmt.Printf("Creating %s \n", localRepoCacheFile)
_, err := os.Create(localRepoCacheFile)
if err != nil {
return err
}
//TODO: take this out and replace with helm update functionality
os.Symlink(localRepoCacheFile, cacheDirectory("local-cache.yaml"))
} else if fi.IsDir() {
return fmt.Errorf("%s must be a file, not a directory", localRepoCacheFile)
}
fmt.Printf("$HELM_HOME has been configured at %s.\n", helmHome)
return nil
}
package main
import (
"io/ioutil"
"os"
"testing"
)
func TestEnsureHome(t *testing.T) {
home := createTmpHome()
helmHome = home
if err := ensureHome(); err != nil {
t.Errorf("%s", err)
}
expectedDirs := []string{homePath(), cacheDirectory(), localRepoDirectory()}
for _, dir := range expectedDirs {
if fi, err := os.Stat(dir); err != nil {
t.Errorf("%s", err)
} else if !fi.IsDir() {
t.Errorf("%s is not a directory", fi)
}
}
if fi, err := os.Stat(repositoriesFile()); err != nil {
t.Errorf("%s", err)
} else if fi.IsDir() {
t.Errorf("%s should not be a directory", fi)
}
if fi, err := os.Stat(localRepoDirectory(localRepoCacheFilePath)); err != nil {
t.Errorf("%s", err)
} else if fi.IsDir() {
t.Errorf("%s should not be a directory", fi)
}
}
func createTmpHome() string {
tmpHome, _ := ioutil.TempDir("", "helm_home")
defer os.Remove(tmpHome)
return tmpHome
}
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
"github.com/deis/tiller/pkg/chart"
"github.com/deis/tiller/pkg/helm"
)
const installDesc = `
This command installs a chart archive.
`
func init() {
RootCommand.Flags()
RootCommand.AddCommand(installCmd)
}
var installCmd = &cobra.Command{
Use: "install [CHART]",
Short: "install a chart archive.",
Long: installDesc,
RunE: runInstall,
}
func runInstall(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("This command needs at least one argument, the name of the chart.")
}
ch, err := loadChart(args[0])
if err != nil {
return err
}
res, err := helm.InstallRelease(ch)
if err != nil {
return err
}
fmt.Printf("release.name: %s\n", res.Release.Name)
fmt.Printf("release.chart: %s\n", res.Release.Chart.Metadata.Name)
fmt.Printf("release.status: %s\n", res.Release.Info.Status.Code)
return nil
}
func loadChart(path string) (*chart.Chart, error) {
path, err := filepath.Abs(path)
if err != nil {
return nil, err
}
if fi, err := os.Stat(path); err != nil {
return nil, err
} else if fi.IsDir() {
return chart.LoadDir(path)
}
return chart.Load(path)
}
package main
import (
"fmt"
"github.com/deis/tiller/pkg/lint"
"github.com/spf13/cobra"
)
var longLintHelp = `
This command takes a path to a chart and runs a series of tests to verify that
the chart is well-formed.
If the linter encounters things that will cause the chart to fail installation,
it will emit [ERROR] messages. If it encounters issues that break with convention
or recommendation, it will emit [WARNING] messages.
`
var lintCommand = &cobra.Command{
Use: "lint [flags] PATH",
Short: "Examines a chart for possible issues",
Long: longLintHelp,
Run: lintCmd,
}
func init() {
RootCommand.AddCommand(lintCommand)
}
func lintCmd(cmd *cobra.Command, args []string) {
path := "."
if len(args) > 0 {
path = args[0]
}
issues := lint.All(path)
for _, i := range issues {
fmt.Printf("%s\n", i)
}
}
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/deis/tiller/pkg/chart"
"github.com/deis/tiller/pkg/repo"
"github.com/spf13/cobra"
)
const packageDesc = `
This command packages a chart into a versioned chart archive file. If a path
is given, this will look at that path for a chart (which must contain a
Chart.yaml file) and then package that directory.
If no path is given, this will look in the present working directory for a
Chart.yaml file, and (if found) build the current directory into a chart.
Versioned chart archives are used by Helm package repositories.
`
var save bool
func init() {
packageCmd.Flags().BoolVar(&save, "save", true, "save packaged chart to local chart repository")
RootCommand.AddCommand(packageCmd)
}
var packageCmd = &cobra.Command{
Use: "package [CHART_PATH]",
Short: "Package a chart directory into a chart archive.",
Long: packageDesc,
RunE: runPackage,
}
func runPackage(cmd *cobra.Command, args []string) error {
path := "."
if len(args) > 0 {
path = args[0]
} else {
return fmt.Errorf("This command needs at least one argument, the path to the chart.")
}
path, err := filepath.Abs(path)
if err != nil {
return err
}
ch, err := chart.LoadDir(path)
if err != nil {
return err
}
// Save to $HELM_HOME/local directory.
if save {
if err := repo.AddChartToLocalRepo(ch, localRepoDirectory()); err != nil {
return err
}
}
// Save to the current working directory.
cwd, err := os.Getwd()
if err != nil {
return err
}
name, err := chart.Save(ch, cwd)
if err == nil {
cmd.Printf("Saved %s to current directory\n", name)
}
return err
}
package main
import (
"errors"
"fmt"
"github.com/deis/tiller/pkg/helm"
"github.com/spf13/cobra"
)
const removeDesc = `
This command takes a release name, and then deletes the release from Kubernetes.
It removes all of the resources associated with the last release of the chart.
Use the '--dry-run' flag to see which releases will be deleted without actually
deleting them.
`
var removeDryRun bool
var removeCommand = &cobra.Command{
Use: "remove [flags] RELEASE_NAME",
Aliases: []string{"rm"},
SuggestFor: []string{"delete", "del"},
Short: "Given a release name, remove the release from Kubernetes",
Long: removeDesc,
RunE: rmRelease,
}
func init() {
RootCommand.AddCommand(removeCommand)
removeCommand.Flags().BoolVar(&removeDryRun, "dry-run", false, "Simulate action, but don't actually do it.")
}
func rmRelease(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("Command 'remove' requires a release name.")
}
// TODO: Handle dry run use case.
if removeDryRun {
fmt.Printf("Deleting %s\n", args[0])
return nil
}
_, err := helm.UninstallRelease(args[0])
if err != nil {
return err
}
return nil
}
package main
import (
"fmt"
"os"
"github.com/deis/tiller/pkg/repo"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
)
func init() {
repoCmd.AddCommand(repoAddCmd)
repoCmd.AddCommand(repoListCmd)
RootCommand.AddCommand(repoCmd)
}
var repoCmd = &cobra.Command{
Use: "repo add|remove|list [ARG]",
Short: "add, list, or remove chart repositories",
}
var repoAddCmd = &cobra.Command{
Use: "add [flags] [NAME] [URL]",
Short: "add a chart repository",
RunE: runRepoAdd,
}
var repoListCmd = &cobra.Command{
Use: "list [flags]",
Short: "list chart repositories",
RunE: runRepoList,
}
func runRepoAdd(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return fmt.Errorf("This command needs two argument, a name for the chart repository and the url of the chart repository")
}
err := insertRepoLine(args[0], args[1])
if err != nil {
return err
}
fmt.Println(args[0] + " has been added to your repositories")
return nil
}
func runRepoList(cmd *cobra.Command, args []string) error {
f, err := repo.LoadRepositoriesFile(repositoriesFile())
if err != nil {
return err
}
if len(f.Repositories) == 0 {
fmt.Println("No repositories to show")
return nil
}
table := uitable.New()
table.MaxColWidth = 50
table.AddRow("NAME", "URL")
for k, v := range f.Repositories {
table.AddRow(k, v)
}
fmt.Println(table)
return nil
}
func insertRepoLine(name, url string) error {
err := checkUniqueName(name)
if err != nil {
return err
}
b, _ := yaml.Marshal(map[string]string{name: url})
f, err := os.OpenFile(repositoriesFile(), os.O_APPEND|os.O_WRONLY, 0666)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(b)
if err != nil {
return err
}
return nil
}
func checkUniqueName(name string) error {
file, err := repo.LoadRepositoriesFile(repositoriesFile())
if err != nil {
return err
}
_, ok := file.Repositories[name]
if ok {
return fmt.Errorf("The repository name you provided (%s) already exists. Please specifiy a different name.", name)
}
return nil
}
package main
import (
"testing"
"github.com/deis/tiller/pkg/repo"
)
func TestRepoAdd(t *testing.T) {
home := createTmpHome()
helmHome = home
if err := ensureHome(); err != nil {
t.Errorf("%s", err)
}
testName := "test-name"
testURL := "test-url"
if err := insertRepoLine(testName, testURL); 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, testURL); err == nil {
t.Errorf("Duplicate repository name was added")
}
}
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/deis/tiller/pkg/repo"
"github.com/spf13/cobra"
)
func init() {
RootCommand.AddCommand(searchCmd)
}
var searchCmd = &cobra.Command{
Use: "search [CHART]",
Short: "Search for charts",
Long: "", //TODO: add search command description
RunE: search,
}
func search(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("This command needs at least one argument")
}
results, err := searchCacheForPattern(args[0])
if err != nil {
return err
}
cmd.Println("Charts:")
for _, result := range results {
fmt.Println(result)
}
return nil
}
func searchCacheForPattern(name string) ([]string, error) {
fileList := []string{}
filepath.Walk(cacheDirectory(), func(path string, f os.FileInfo, err error) error {
if !f.IsDir() {
fileList = append(fileList, path)
}
return nil
})
matches := []string{}
for _, f := range fileList {
cache, _ := repo.LoadCacheFile(f)
repoName := filepath.Base(strings.TrimRight(f, "-cache.txt"))
for k := range cache.Entries {
if strings.Contains(k, name) {
matches = append(matches, repoName+"/"+k)
}
}
}
return matches, nil
}
package main
import (
"github.com/deis/tiller/pkg/repo"
"github.com/spf13/cobra"
)
var serveDesc = `This command starts a local chart repository server that serves the charts saved in your $HELM_HOME/local/ directory.`
//TODO: add repoPath flag to be passed in in case you want
// to serve charts from a different local dir
func init() {
RootCommand.AddCommand(serveCmd)
}
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start a local http web server",
Long: serveDesc,
Run: serve,
}
func serve(cmd *cobra.Command, args []string) {
repo.StartLocalRepo(localRepoDirectory())
}
package main
import (
"fmt"
"time"
"github.com/deis/tiller/pkg/helm"
"github.com/deis/tiller/pkg/timeconv"
"github.com/spf13/cobra"
)
var statusHelp = `
This command shows the status of a named release.
`
var statusCommand = &cobra.Command{
Use: "status [flags] RELEASE_NAME",
Short: "Displays the status of the named release",
Long: statusHelp,
RunE: status,
}
func init() {
RootCommand.AddCommand(statusCommand)
}
func status(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errReleaseRequired
}
res, err := helm.GetReleaseStatus(args[0])
if err != nil {
return err
}
fmt.Printf("Last Deployed: %s\n", timeconv.Format(res.Info.LastDeployed, time.ANSIC))
fmt.Printf("Status: %s\n", res.Info.Status.Code)
if res.Info.Status.Details != nil {
fmt.Printf("Details: %s\n", res.Info.Status.Details)
}
return nil
}
package main
import (
"os"
"path/filepath"
)
const (
repositoriesFilePath string = "repositories.yaml"
cachePath string = "cache"
localRepoPath string = "local"
localRepoCacheFilePath string = "cache.yaml"
)
func homePath() string {
return os.ExpandEnv(helmHome)
}
func cacheDirectory(paths ...string) string {
fragments := append([]string{homePath(), cachePath}, paths...)
return filepath.Join(fragments...)
}
func localRepoDirectory(paths ...string) string {
fragments := append([]string{homePath(), localRepoPath}, paths...)
return filepath.Join(fragments...)
}
func repositoriesFile() string {
return filepath.Join(homePath(), repositoriesFilePath)
}
package environment
import (
"github.com/deis/tiller/pkg/engine"
"github.com/deis/tiller/pkg/proto/hapi/chart"
"github.com/deis/tiller/pkg/proto/hapi/release"
"github.com/deis/tiller/pkg/storage"
)
// GoTplEngine is the name of the Go template engine, as registered in the EngineYard.
const GoTplEngine = "gotpl"
// DefaultEngine points to the engine that the EngineYard should treat as the
// default. A chart that does not specify an engine may be run through the
// default engine.
var DefaultEngine = GoTplEngine
// EngineYard maps engine names to engine implementations.
type EngineYard map[string]Engine
// Get retrieves a template engine by name.
//
// If no matching template engine is found, the second return value will
// be false.
func (y EngineYard) Get(k string) (Engine, bool) {
e, ok := y[k]
return e, ok
}
// Default returns the default template engine.
//
// The default is specified by DefaultEngine.
//
// If the default template engine cannot be found, this panics.
func (y EngineYard) Default() Engine {
d, ok := y[DefaultEngine]
if !ok {
// This is a developer error!
panic("Default template engine does not exist")
}
return d
}
// Engine represents a template engine that can render templates.
//
// For some engines, "rendering" includes both compiling and executing. (Other
// engines do not distinguish between phases.)
//
// The engine returns a map where the key is the named output entity (usually
// a file name) and the value is the rendered content of the template.
//
// An Engine must be capable of executing multiple concurrent requests, but
// without tainting one request's environment with data from another request.
type Engine interface {
Render(*chart.Chart, *chart.Config) (map[string]string, error)
}
// ReleaseStorage represents a storage engine for a Release.
//
// Release storage must be concurrency safe.
type ReleaseStorage interface {
// Create stores a release in the storage.
//
// If a release with the same name exists, this returns an error.
//
// It may return other errors in cases where it cannot write to storage.
Create(*release.Release) error
// Read takes a name and returns a release that has that name.
//
// It will only return releases that are not deleted and not superseded.
//
// It will return an error if no relevant release can be found, or if storage
// is not properly functioning.
Read(name string) (*release.Release, error)
// Update looks for a release with the same name and updates it with the
// present release contents.
//
// For immutable storage backends, this may result in a new release record
// being created, and the previous release being marked as superseded.
//
// It will return an error if a previous release is not found. It may also
// return an error if the storage backend encounters an error.
Update(*release.Release) error
// Delete marks a Release as deleted.
//
// It returns the deleted record. If the record is not found or if the
// underlying storage encounters an error, this will return an error.
Delete(name string) (*release.Release, error)
// List lists all active (non-deleted, non-superseded) releases.
//
// To get deleted or superseded releases, use Query.
List() ([]*release.Release, error)
// Query takes a map of labels and returns any releases that match.
//
// Query will search all releases, including deleted and superseded ones.
// The provided map will be used to filter results.
Query(map[string]string) ([]*release.Release, error)
}
// KubeClient represents a client capable of communicating with the Kubernetes API.
//
// A KubeClient must be concurrency safe.
type KubeClient interface {
// Install takes a map where the key is a "file name" (read: unique relational
// id) and the value is a Kubernetes manifest containing one or more resource
// definitions.
//
// TODO: Can these be in YAML or JSON, or must they be in one particular
// format?
Install(manifests map[string]string) error
}
// Environment provides the context for executing a client request.
//
// All services in a context are concurrency safe.
type Environment struct {
// EngineYard provides access to the known template engines.
EngineYard EngineYard
// Releases stores records of releases.
Releases ReleaseStorage
// KubeClient is a Kubernetes API client.
KubeClient KubeClient
}
// New returns an environment initialized with the defaults.
func New() *Environment {
e := engine.New()
var ey EngineYard = map[string]Engine{
// Currently, the only template engine we support is the GoTpl one. But
// we can easily add some here.
GoTplEngine: e,
}
return &Environment{
EngineYard: ey,
Releases: storage.NewMemory(),
}
}
package environment
import (
"testing"
"github.com/deis/tiller/pkg/proto/hapi/chart"
"github.com/deis/tiller/pkg/proto/hapi/release"
)
type mockEngine struct {
out map[string]string
}
func (e *mockEngine) Render(chrt *chart.Chart, v *chart.Config) (map[string]string, error) {
return e.out, nil
}
type mockReleaseStorage struct {
rel *release.Release
}
func (r *mockReleaseStorage) Create(v *release.Release) error {
r.rel = v
return nil
}
func (r *mockReleaseStorage) Read(k string) (*release.Release, error) {
return r.rel, nil
}
func (r *mockReleaseStorage) Update(v *release.Release) error {
r.rel = v
return nil
}
func (r *mockReleaseStorage) Delete(k string) (*release.Release, error) {
return r.rel, nil
}
func (r *mockReleaseStorage) List() ([]*release.Release, error) {
return []*release.Release{}, nil
}
func (r *mockReleaseStorage) Query(labels map[string]string) ([]*release.Release, error) {
return []*release.Release{}, nil
}
type mockKubeClient struct {
}
func (k *mockKubeClient) Install(manifests map[string]string) error {
return nil
}
var _ Engine = &mockEngine{}
var _ ReleaseStorage = &mockReleaseStorage{}
var _ KubeClient = &mockKubeClient{}
func TestEngine(t *testing.T) {
eng := &mockEngine{out: map[string]string{"albatross": "test"}}
env := New()
env.EngineYard = EngineYard(map[string]Engine{"test": eng})
if engine, ok := env.EngineYard.Get("test"); !ok {
t.Errorf("failed to get engine from EngineYard")
} else if out, err := engine.Render(&chart.Chart{}, &chart.Config{}); err != nil {
t.Errorf("unexpected template error: %s", err)
} else if out["albatross"] != "test" {
t.Errorf("expected 'test', got %q", out["albatross"])
}
}
func TestReleaseStorage(t *testing.T) {
rs := &mockReleaseStorage{}
env := New()
env.Releases = rs
release := &release.Release{Name: "mariner"}
if err := env.Releases.Create(release); err != nil {
t.Fatalf("failed to store release: %s", err)
}
if err := env.Releases.Update(release); err != nil {
t.Fatalf("failed to update release: %s", err)
}
if v, err := env.Releases.Read("albatross"); err != nil {
t.Errorf("Error fetching release: %s", err)
} else if v.Name != "mariner" {
t.Errorf("Expected mariner, got %q", v.Name)
}
if _, err := env.Releases.Delete("albatross"); err != nil {
t.Fatalf("failed to delete release: %s", err)
}
}
func TestKubeClient(t *testing.T) {
kc := &mockKubeClient{}
env := New()
env.KubeClient = kc
manifests := map[string]string{}
if err := env.KubeClient.Install(manifests); err != nil {
t.Errorf("Kubeclient failed: %s", err)
}
}
package main
import (
"bytes"
"errors"
"log"
"github.com/deis/tiller/cmd/tiller/environment"
"github.com/deis/tiller/pkg/proto/hapi/release"
"github.com/deis/tiller/pkg/proto/hapi/services"
"github.com/deis/tiller/pkg/timeconv"
"github.com/technosophos/moniker"
ctx "golang.org/x/net/context"
)
func init() {
srv := &releaseServer{
env: env,
}
services.RegisterReleaseServiceServer(rootServer, srv)
}
type releaseServer struct {
env *environment.Environment
}
var (
// errNotImplemented is a temporary error for uninmplemented callbacks.
errNotImplemented = errors.New("not implemented")
// errMissingChart indicates that a chart was not provided.
errMissingChart = errors.New("no chart provided")
// errMissingRelease indicates that a release (name) was not provided.
errMissingRelease = errors.New("no release provided")
)
func (s *releaseServer) ListReleases(req *services.ListReleasesRequest, stream services.ReleaseService_ListReleasesServer) error {
return errNotImplemented
}
func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetReleaseStatusRequest) (*services.GetReleaseStatusResponse, error) {
if req.Name == "" {
return nil, errMissingRelease
}
rel, err := s.env.Releases.Read(req.Name)
if err != nil {
return nil, err
}
if rel.Info == nil {
return nil, errors.New("release info is missing")
}
return &services.GetReleaseStatusResponse{Info: rel.Info}, nil
}
func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleaseContentRequest) (*services.GetReleaseContentResponse, error) {
if req.Name == "" {
return nil, errMissingRelease
}
rel, err := s.env.Releases.Read(req.Name)
return &services.GetReleaseContentResponse{Release: rel}, err
}
func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) {
return nil, errNotImplemented
}
func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) {
if req.Chart == nil {
return nil, errMissingChart
}
// We should probably make a name generator part of the Environment.
namer := moniker.New()
// TODO: Make sure this is unique.
name := namer.NameSep("-")
ts := timeconv.Now()
// Render the templates
files, err := s.env.EngineYard.Default().Render(req.Chart, req.Values)
if err != nil {
return nil, err
}
b := bytes.NewBuffer(nil)
for name, file := range files {
// Ignore empty documents because the Kubernetes library can't handle
// them.
if len(file) > 0 {
b.WriteString("\n---\n# Source: " + name + "\n")
b.WriteString(file)
}
}
// Store a release.
r := &release.Release{
Name: name,
Chart: req.Chart,
Config: req.Values,
Info: &release.Info{
FirstDeployed: ts,
LastDeployed: ts,
Status: &release.Status{Code: release.Status_UNKNOWN},
},
Manifest: b.String(),
}
if req.DryRun {
log.Printf("Dry run for %s", name)
return &services.InstallReleaseResponse{Release: r}, nil
}
if err := s.env.Releases.Create(r); err != nil {
return nil, err
}
return &services.InstallReleaseResponse{Release: r}, nil
}
func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) {
if req.Name == "" {
log.Printf("uninstall: Release not found: %s", req.Name)
return nil, errMissingRelease
}
rel, err := s.env.Releases.Read(req.Name)
if err != nil {
log.Printf("uninstall: Release not loaded: %s", req.Name)
return nil, err
}
log.Printf("uninstall: Deleting %s", req.Name)
rel.Info.Status.Code = release.Status_DELETED
rel.Info.Deleted = timeconv.Now()
// TODO: Once KubeClient is ready, delete the resources.
log.Println("WARNING: Currently not deleting resources from k8s")
if err := s.env.Releases.Update(rel); err != nil {
log.Printf("uninstall: Failed to store updated release: %s", err)
}
res := services.UninstallReleaseResponse{Release: rel}
return &res, nil
}
package main
import (
"strings"
"testing"
"github.com/deis/tiller/cmd/tiller/environment"
"github.com/deis/tiller/pkg/proto/hapi/chart"
"github.com/deis/tiller/pkg/proto/hapi/release"
"github.com/deis/tiller/pkg/proto/hapi/services"
"github.com/deis/tiller/pkg/storage"
"github.com/deis/tiller/pkg/timeconv"
"github.com/golang/protobuf/ptypes/timestamp"
"golang.org/x/net/context"
)
func rsFixture() *releaseServer {
return &releaseServer{
env: mockEnvironment(),
}
}
func releaseMock() *release.Release {
date := timestamp.Timestamp{242085845, 0}
return &release.Release{
Name: "angry-panda",
Info: &release.Info{
FirstDeployed: &date,
LastDeployed: &date,
Status: &release.Status{Code: release.Status_DEPLOYED},
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "foo",
Version: "0.1.0-beta.1",
},
Templates: []*chart.Template{
{Name: "foo.tpl", Data: []byte("Hello")},
},
},
Config: &chart.Config{Raw: `name = "value"`},
}
}
func TestInstallRelease(t *testing.T) {
c := context.Background()
rs := rsFixture()
req := &services.InstallReleaseRequest{
Chart: &chart.Chart{
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "hello", Data: []byte("hello: world")},
},
},
}
res, err := rs.InstallRelease(c, req)
if err != nil {
t.Errorf("Failed install: %s", err)
}
if res.Release.Name == "" {
t.Errorf("Expected release name.")
}
rel, err := rs.env.Releases.Read(res.Release.Name)
if err != nil {
t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases)
}
t.Logf("rel: %v", rel)
if len(res.Release.Manifest) == 0 {
t.Errorf("No manifest returned: %v", res.Release)
}
if len(rel.Manifest) == 0 {
t.Errorf("Expected manifest in %v", res)
}
if !strings.Contains(rel.Manifest, "---\n# Source: hello\nhello: world") {
t.Errorf("unexpected output: %s", rel.Manifest)
}
}
func TestInstallReleaseDryRun(t *testing.T) {
c := context.Background()
rs := rsFixture()
req := &services.InstallReleaseRequest{
Chart: &chart.Chart{
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "hello", Data: []byte("hello: world")},
{Name: "goodbye", Data: []byte("goodbye: world")},
{Name: "empty", Data: []byte("")},
},
},
DryRun: true,
}
res, err := rs.InstallRelease(c, req)
if err != nil {
t.Errorf("Failed install: %s", err)
}
if res.Release.Name == "" {
t.Errorf("Expected release name.")
}
if !strings.Contains(res.Release.Manifest, "---\n# Source: hello\nhello: world") {
t.Errorf("unexpected output: %s", res.Release.Manifest)
}
if !strings.Contains(res.Release.Manifest, "---\n# Source: goodbye\ngoodbye: world") {
t.Errorf("unexpected output: %s", res.Release.Manifest)
}
if strings.Contains(res.Release.Manifest, "empty") {
t.Errorf("Should not contain template data for an empty file. %s", res.Release.Manifest)
}
if _, err := rs.env.Releases.Read(res.Release.Name); err == nil {
t.Errorf("Expected no stored release.")
}
}
func TestUninstallRelease(t *testing.T) {
c := context.Background()
rs := rsFixture()
rs.env.Releases.Create(&release.Release{
Name: "angry-panda",
Info: &release.Info{
FirstDeployed: timeconv.Now(),
Status: &release.Status{
Code: release.Status_DEPLOYED,
},
},
})
req := &services.UninstallReleaseRequest{
Name: "angry-panda",
}
res, err := rs.UninstallRelease(c, req)
if err != nil {
t.Errorf("Failed uninstall: %s", err)
}
if res.Release.Name != "angry-panda" {
t.Errorf("Expected angry-panda, got %q", res.Release.Name)
}
if res.Release.Info.Status.Code != release.Status_DELETED {
t.Errorf("Expected status code to be DELETED, got %d", res.Release.Info.Status.Code)
}
if res.Release.Info.Deleted.Seconds <= 0 {
t.Errorf("Expected valid UNIX date, got %d", res.Release.Info.Deleted.Seconds)
}
}
func TestGetReleaseContent(t *testing.T) {
c := context.Background()
rs := rsFixture()
rel := releaseMock()
if err := rs.env.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err)
}
res, err := rs.GetReleaseContent(c, &services.GetReleaseContentRequest{Name: rel.Name})
if err != nil {
t.Errorf("Error getting release content: %s", err)
}
if res.Release.Chart.Metadata.Name != rel.Chart.Metadata.Name {
t.Errorf("Expected %q, got %q", rel.Chart.Metadata.Name, res.Release.Chart.Metadata.Name)
}
}
func TestGetReleaseStatus(t *testing.T) {
c := context.Background()
rs := rsFixture()
rel := releaseMock()
if err := rs.env.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err)
}
res, err := rs.GetReleaseStatus(c, &services.GetReleaseStatusRequest{Name: rel.Name})
if err != nil {
t.Errorf("Error getting release content: %s", err)
}
if res.Info.Status.Code != release.Status_DEPLOYED {
t.Errorf("Expected %d, got %d", release.Status_DEPLOYED, res.Info.Status.Code)
}
}
func mockEnvironment() *environment.Environment {
e := environment.New()
e.Releases = storage.NewMemory()
return e
}
package main
import (
"fmt"
"net"
"os"
"github.com/deis/tiller/cmd/tiller/environment"
"github.com/spf13/cobra"
"google.golang.org/grpc"
)
// rootServer is the root gRPC server.
//
// Each gRPC service registers itself to this server during init().
var rootServer = grpc.NewServer()
var env = environment.New()
const globalUsage = `The Kubernetes Helm server.
Tiller is the server for Helm. It provides in-cluster resource management.
By default, Tiller listens for gRPC connections on port 44134.
`
var rootCommand = &cobra.Command{
Use: "tiller",
Short: "The Kubernetes Helm server.",
Long: globalUsage,
Run: start,
}
func main() {
rootCommand.Execute()
}
func start(c *cobra.Command, args []string) {
addr := ":44134"
lstn, err := net.Listen("tcp", addr)
if err != nil {
fmt.Fprintf(os.Stderr, "Server died: %s\n", err)
os.Exit(1)
}
fmt.Printf("Tiller is running on %s\n", addr)
if err := rootServer.Serve(lstn); err != nil {
fmt.Fprintf(os.Stderr, "Server died: %s\n", err)
os.Exit(1)
}
}
package main
import (
"testing"
"github.com/deis/tiller/cmd/tiller/environment"
"github.com/deis/tiller/pkg/engine"
"github.com/deis/tiller/pkg/storage"
)
// These are canary tests to make sure that the default server actually
// fulfills its requirements.
var _ environment.Engine = &engine.Engine{}
var _ environment.ReleaseStorage = storage.NewMemory()
func TestInit(t *testing.T) {
defer func() {
if recover() != nil {
t.Fatalf("Panic trapped. Check EngineYard.Default()")
}
}()
// This will panic if it is not correct.
env.EngineYard.Default()
e, ok := env.EngineYard.Get(environment.GoTplEngine)
if !ok {
t.Fatalf("Could not find GoTplEngine")
}
if e == nil {
t.Fatalf("Template engine GoTplEngine returned nil.")
}
}
# The Kubernetes Helm Architecture
This document describes the Helm architecture at a high level.
## The Purpose of Helm
Helm is a tool for managing Kubernetes packages called _charts_. Helm
can do the following:
- Create new charts from scratch
- Package charts into chart archive (tgz) files
- Interact with chart repositories where charts are stored
- Install and uninstall charts into an existing Kubernetes cluster
- Manage the releases of charts that have been installed with Helm
For Helm, there are three important concepts:
1. The _chart_ is a bundle of information necessary to create an
instance of a Kubernetes application.
2. The _config_ contains configuration information that can be merged
into a packaged chart to create a releasable object.
3. A _release_ is a running instance of a _chart_, combined with a
specific _config_.
Following the formula made famous by the 12 Factor App, _chart + config
= release_.
## Components
Helm has two major components:
**The Helm Client** is a command-line client for end users. The client
is responsible for the following domains:
- Local chart development
- Managing repositories
- Interacting with the Tiller server
- Sending charts to be installed
- Asking for information about releases
- Requesting upgrading or uninstalling of existing releases
**The Tiller Server** is an in-cluster server that interacts with the
Helm client, and interfaces with the Kubernetes API server. The server
is responsible for the following:
- Listing for incomming requests from the Helm client
- Combining a chart and configuration to build a release
- Installing charts into Kubernetes, and then tracking the subsequent
release
- Upgrading and uninstalling charts by interacting with Kubernetes
In a nutshell, the client is responsible for managing charts, and the
server is responsible for managing releases.
## Implementation
The Helm client is written in the Go programming language, and uses the
gRPC protocol suite to interact with the Tiller server.
The Tiller server is also written in Go. It provides a gRPC server to
connect with the client, and it uses the Kubernetes client library to
communicate with Kubernetes. Currently, that library uses REST+JSON.
The Tiller server stores information in ConfigMaps located inside of
Kubernetes. It does not need its own database.
### Structure of the Code
The individual programs are located in `cmd/`. Shared libraries are
stored in `pkg/`. The raw ProtoBuf files are stored in `_proto/hapi`
(where `hapi` stands for the Helm Application Programming Interface).
The Go files generated from the `proto` definitions are stored in
`pkg/proto`.
Docker images are built by cross-compiling Linux binaries and then
building a Docker image from the files in `rootfs`.
The `scripts/` directory contains a number of utility scripts, including
`local-cluster.sh`, which can start a full Kubernetes instance inside of
a Docker container.
Go dependencies are managed with
[Glide](https://github.com/Masterminds/glide) and stored in the
`vendor/` directory.
# Charts
Helm uses a packaging format called _charts_. A chart is a collection of files
that collectively describe a set of Kubernetes resources.
## The Chart File Structure
A chart is organized as a collection of files inside of a directory. The
directory name is the name of the chart (without versioning information). Thus,
a chart describing Wordpress would be stored in the `wordpress/` directory.
Inside of this directory, Helm will expect a structure that matches this:
```
wordpress/
Chart.yaml # A YAML file containing information about the chart
LICENSE # A plain text file containing the license for the chart
README.md # A human-readable README file
values.toml # The default configuration values for this chart
charts/ # A directory containing any charts upon which this chart depends.
templates/ # A directory of templates that, when combined with values,
# will generate valid Kubernetes manifest files.
```
## The Chart.yaml File
The Chart.yaml file is required for a chart. It contains the following fields:
```yaml
name: The name of the chart (required)
version: A SemVer 2 version (required)
description: A single-sentence description of this project (optional)
keywords:
- A list of keywords about this project (optional)
home: The URL of this project's home page (optional)
sources:
- A list of URLs to source code for this project (optional)
maintainers: # (optional)
- name: The maintainer's name (required for each maintainer)
email: The maintainer's email (optional for each maintainer)
```
If you are familiar with the Chart.yaml file format for Helm Classic, you will
notice that fields specifying dependencies have been removed. That is because
the new Chart format expresses dependencies using the `charts/` directory.
## Chart Dependencies
In Helm, one chart may depend on any number of other charts. These
dependencies are expressed explicitly by copying the dependency charts
into the `charts/` directory.
For example, if the Wordpress chart depends on the Apache chart, the
Apache chart (of the correct version) is supplied in the Wordpress
chart's `charts/` directory:
```
wordpress:
Chart.yaml
# ...
charts/
apache/
Chart.yaml
# ...
mysql/
Chart.yaml
# ...
```
The example above shows how the Wordpress chart expresses its dependency
on Apache and MySQL by including those charts inside of its `charts/`
directory.
## Templates and Values
In Helm Charts, templates are written in the Go template language, with the
addition of 50 or so add-on template functions.
All template files are stored in a chart's `templates/` folder. When
Helm renders the charts, it will pass every file in that directory
through the template engine.
Values for the templates are supplied two ways:
- Chart developers may supply a file called `values.toml` inside of a
chart. This file can contain default values.
- Chart users may supply a TOML file that contains values. This can be
provided on the command line with `helm install`.
When a user supplies custom values, these values will override the
values in the chart's `values.toml` file.
### Template Files
Template files follow the standard conventions for writing Go templates.
An example template file might look something like this:
```yaml
apiVersion: v1
kind: ReplicationController
metadata:
name: deis-database
namespace: deis
labels:
heritage: deis
spec:
replicas: 1
selector:
app: deis-database
template:
metadata:
labels:
app: deis-database
spec:
serviceAccount: deis-database
containers:
- name: deis-database
image: {{.imageRegistry}}/postgres:{{.dockerTag}}
imagePullPolicy: {{.pullPolicy}}
ports:
- containerPort: 5432
env:
- name: DATABASE_STORAGE
value: {{default "minio" .storage}}
```
The above example, based loosely on [https://github.com/deis/charts](the
chart for Deis), is a template for a Kubernetes replication controller.
It can use the following four template values:
- `imageRegistry`: The source registry for the Docker image.
- `dockerTag`: The tag for the docker image.
- `pullPolicy`: The Kubernetes pull policy.
- `storage`: The storage backend, whose default is set to `"minio"`
All of these values are defined by the template author. Helm does not
require or dictate parameters.
### Values files
Considering the template in the previous section, a `values.toml` file
that supplies the necessary values would look like this:
```toml
imageRegistry = "quay.io/deis"
dockerTag = "latest"
pullPolicy = "alwaysPull"
storage = "s3"
```
When a chart includes dependency charts, values can be supplied to those
charts using TOML tables:
```toml
imageRegistry = "quay.io/deis"
dockerTag = "latest"
pullPolicy = "alwaysPull"
storage = "s3"
[router]
hostname = "example.com"
```
In the above example, the value of `hostname` will be passed to a chart
named `router` (if it exists) in the `charts/` directory.
### References
- [Go templates](https://godoc.org/text/template)
- [Extra template functions](https://godoc.org/github.com/Masterminds/sprig)
- [The TOML format](https://github.com/toml-lang/toml)
## Using Helm to Manage Charts
The `helm` tool has several commands for working with charts.
It can create a new chart for you:
```console
$ helm create mychart
Created mychart/
```
Once you have edited a chart, `helm` can package it into a chart archive
for you:
```console
$ helm package mychart
Archived mychart-0.1.-.tgz
```
You can also use `helm` to help you find issues with your chart's
formatting or information:
```console
$ helm lint mychart
No issues found
```
# Developers Guide
This guide explains how to set up your environment for developing on
Helm and Tiller.
## Prerequisites
- Go 1.6.0 or later
- Glide 0.10.2 or later
- kubectl 1.2 or later
- A Kubernetes cluster (optional)
- The gRPC toolchain
## Building Helm/Tiller
We use Make to build our programs. The simplest way to get started is:
```console
$ make boostrap build
```
This will build both Helm and Tiller.
To run all of the tests (without running the tests for `vendor/`), run
`make test`.
To run Helm and Tiller locally, you can run `bin/helm` or `bin/tiller`.
- Helm and Tiller are known to run on Mac OSX and most Linuxes, including
Alpine.
- Tiller must have access to a Kubernets cluster. It learns about the
cluster by examining the Kube config files that `kubectl` uese.
## gRPC and Protobuf
Tiller uses gRPC. To get started with gRPC, you will need to...
- Install `protoc` for compiling protobuf files. Releases are
[here](https://github.com/google/protobuf/releases)
- Install the protoc Go plugin: `go get -u github.com/golang/protobuf/protoc-gen-go`
Note that you need to be on protobuf 3.x (`protoc --version`) and use the latest Go plugin.
### The Helm API (HAPI)
We use gRPC as an API layer. See `pkg/proto/hapi` for the generated Go code,
and `_proto` for the protocol buffer definitions.
To regenerate the Go files from the protobuf source, `cd _proto &&
make`.
## Docker Images
To build Docker images, use `make docker-build`
## Running a Local Cluster
You can run tests locally using the `scripts/local-cluster.sh` script to
start Kubernetes inside of a Docker container. For OS X, you will need
to be running `docker-machine`.
## Contribution Guidelines
We welcome contributions. This project has set up some guidelines in
order to ensure that (a) code quality remains high, (b) the project
remains consistent, and (c) contributions follow the open source legal
requirements. Our intent is not to burden contributors, but to build
elegant and high-quality open source code so that our users will benefit.
We follow the coding standards and guidelines outlined by the Deis
project:
https://github.com/deis/workflow/blob/master/CONTRIBUTING.md
https://github.com/deis/workflow/blob/master/src/contributing/submitting-a-pull-request.md
Adidtionally, contributors must have a CLA with CNCF/Google before we can
accept contributions.
# Helm Examples
This directory contains example charts to help you get started with
chart development.
name: alpine
description: Deploy a basic Alpine Linux pod
version: 0.1.0
home: "https://github.com/deis/tiller"
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.toml` file contains the default values for the
`alpine-pod.yaml` template.
You can install this example using `helm install docs/examples/alpine`.
apiVersion: v1
kind: Pod
metadata:
name: {{default "alpine" .name}}
labels:
heritage: helm
spec:
restartPolicy: {{default "Never" .restart_policy}}
containers:
- name: waiter
image: "alpine:3.3"
command: ["/bin/sleep","9000"]
# The pod name
name = "my-alpine"
# Quickstart Guide
This guide covers how you can quickly get started using Helm.
## Prerequisites
- You must have Kubernetes installed, and have a local configured copy
of `kubectl`.
## Install Helm
Download a binary release of the Helm client from the official project
page.
Alternately, you can clone the GitHub project and build your own
client from source. The quickest route to installing from source is to
run `make boostrap build`, and then use `bin/helm`.
## Initialize Helm and Install Tiller
Once you have Helm ready, you can initialize the local CLI and also
install Tiller into your Kubernetes cluster in one step:
```console
$ helm init
```
## Install an Existing Chart
To install an existing chart, you can run the `helm install` command:
_TODO:_ Update this to the correct URL.
```console
$ helm install https://helm.sh/charts/nginx-0.1.0.tgz
Released smiling-penguin
```
In the example above, the `nginx` chart was released, and the name of
our new release is `smiling-penguin`
## Learn About The Release
To find out about our release, run `helm status`:
```console
$ helm status smiling-penguin
Status: DEPLOYED
```
## Uninstall a Release
To remove a release, use the `helm remove` command:
```console
$ helm remove smiling-penguin
Removed smiling-penguin
```
This will uninstall `smiling-penguin` from Kubernetes, but you will
still be able to request information about that release:
```console
$ helm status smiling-penguin
Status: DELETED
```
## Reading the Help Text
To learn more about the available Helm commands, use `helm help` or type
a command followed by the `-h` flag:
```console
$ helm get -h
```
hash: 998d87445fec0bd715fa5ccbcc227cb4997e56ceff58dc8eb53ea2e0cc84abfd
updated: 2016-04-27T16:11:47.531200165-06:00
imports:
- name: bitbucket.org/ww/goautoneg
version: 75cd24fc2f2c
- name: github.com/aokoli/goutils
version: 9c37978a95bd5c709a15883b6242714ea6709e64
- name: github.com/beorn7/perks
version: b965b613227fddccbfffe13eae360ed3fa822f8d
subpackages:
- quantile
- name: github.com/blang/semver
version: 31b736133b98f26d5e078ec9eb591666edfd091f
- name: github.com/BurntSushi/toml
version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f
- name: github.com/davecgh/go-spew
version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
subpackages:
- spew
- name: github.com/docker/distribution
version: 55f1b7651f6242617133312ff8af5c2e4e3628ea
subpackages:
- digest
- reference
- name: github.com/docker/docker
version: 0f5c9d301b9b1cca66b3ea0f9dec3b5317d3686d
subpackages:
- pkg/jsonmessage
- pkg/mount
- pkg/stdcopy
- pkg/symlink
- pkg/term
- pkg/timeutils
- pkg/units
- name: github.com/docker/engine-api
version: 26cdffeca716ae4df98070051a852b3198d7d153
subpackages:
- client
- types
- types/container
- types/filters
- types/network
- types/registry
- types/blkiodev
- types/strslice
- name: github.com/docker/go-connections
version: f549a9393d05688dff0992ef3efd8bbe6c628aeb
subpackages:
- nat
- sockets
- tlsconfig
- name: github.com/docker/go-units
version: 0bbddae09c5a5419a8c6dcdd7ff90da3d450393b
- name: github.com/emicklei/go-restful
version: 496d495156da218b9912f03dfa7df7f80fbd8cc3
subpackages:
- swagger
- log
- name: github.com/evanphx/json-patch
version: 7dd4489c2eb6073e5a9d7746c3274c5b5f0387df
- name: github.com/ghodss/yaml
version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee
- name: github.com/gogo/protobuf
version: 82d16f734d6d871204a3feb1a73cb220cc92574c
subpackages:
- gogoproto
- plugin/defaultcheck
- plugin/description
- plugin/embedcheck
- plugin/enumstringer
- plugin/equal
- plugin/face
- plugin/gostring
- plugin/grpc
- plugin/marshalto
- plugin/oneofcheck
- plugin/populate
- plugin/size
- plugin/stringer
- plugin/testgen
- plugin/union
- plugin/unmarshal
- proto
- protoc-gen-gogo/descriptor
- protoc-gen-gogo/generator
- protoc-gen-gogo/plugin
- sortkeys
- vanity
- name: github.com/golang/glog
version: 44145f04b68cf362d9c4df2182967c2275eaefed
- name: github.com/golang/groupcache
version: 604ed5785183e59ae2789449d89e73f3a2a77987
subpackages:
- lru
- name: github.com/golang/protobuf
version: f0a097ddac24fb00e07d2ac17f8671423f3ea47c
subpackages:
- proto
- ptypes/any
- ptypes/timestamp
- name: github.com/google/cadvisor
version: 546a3771589bdb356777c646c6eca24914fdd48b
subpackages:
- api
- cache/memory
- collector
- container
- events
- fs
- healthz
- http
- info/v1
- info/v2
- manager
- metrics
- pages
- storage
- summary
- utils
- validate
- version
- name: github.com/google/gofuzz
version: bbcb9da2d746f8bdbd6a936686a0a6067ada0ec5
- name: github.com/gosuri/uitable
version: 36ee7e946282a3fb1cfecd476ddc9b35d8847e42
subpackages:
- util/strutil
- util/wordwrap
- name: github.com/imdario/mergo
version: 6633656539c1639d9d78127b7d47c622b5d7b6dc
- name: github.com/inconshreveable/mousetrap
version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
- name: github.com/juju/ratelimit
version: 77ed1c8a01217656d2080ad51981f6e99adaa177
- name: github.com/Masterminds/semver
version: 808ed7761c233af2de3f9729a041d68c62527f3a
- name: github.com/Masterminds/sprig
version: e6494bc7e81206ba6db404d2fd96500ffc453407
- name: github.com/mattn/go-runewidth
version: d6bea18f789704b5f83375793155289da36a3c7f
- name: github.com/matttproud/golang_protobuf_extensions
version: fc2b8d3a73c4867e51861bbdd5ae3c1f0869dd6a
subpackages:
- pbutil
- name: github.com/opencontainers/runc
version: 7ca2aa4873aea7cb4265b1726acb24b90d8726c6
subpackages:
- libcontainer
- libcontainer/cgroups/fs
- libcontainer/configs
- libcontainer/cgroups
- libcontainer/system
- name: github.com/pborman/uuid
version: ca53cad383cad2479bbba7f7a1a05797ec1386e4
- name: github.com/prometheus/client_golang
version: 3b78d7a77f51ccbc364d4bc170920153022cfd08
subpackages:
- prometheus
- name: github.com/prometheus/client_model
version: fa8ad6fec33561be4280a8f0514318c79d7f6cb6
subpackages:
- go
- name: github.com/prometheus/common
version: ef7a9a5fb138aa5d3a19988537606226869a0390
subpackages:
- expfmt
- model
- name: github.com/prometheus/procfs
version: 490cc6eb5fa45bf8a8b7b73c8bc82a8160e8531d
- name: github.com/spf13/cobra
version: e14e47b7a916ed178f4559ebd7e625cf16410181
subpackages:
- cobra
- name: github.com/spf13/pflag
version: cb88ea77998c3f024757528e3305022ab50b43be
- name: github.com/technosophos/moniker
version: 9f956786b91d9786ca11aa5be6104542fa911546
- name: github.com/ugorji/go
version: f4485b318aadd133842532f841dc205a8e339d74
subpackages:
- codec
- name: golang.org/x/net
version: fb93926129b8ec0056f2f458b1f519654814edf0
subpackages:
- context
- http2
- trace
- http2/hpack
- internal/timeseries
- context/ctxhttp
- name: golang.org/x/oauth2
version: b5adcc2dcdf009d0391547edc6ecbaff889f5bb9
subpackages:
- google
- internal
- jwt
- jws
- name: google.golang.org/appengine
version: 12d5545dc1cfa6047a286d5e853841b6471f4c19
subpackages:
- internal
- internal/app_identity
- internal/base
- internal/datastore
- internal/log
- internal/modules
- internal/remote_api
- urlfetch
- internal/urlfetch
- name: google.golang.org/cloud
version: eb47ba841d53d93506cfbfbc03927daf9cc48f88
subpackages:
- compute/metadata
- internal
- name: google.golang.org/grpc
version: dec33edc378cf4971a2741cfd86ed70a644d6ba3
subpackages:
- codes
- credentials
- grpclog
- internal
- metadata
- naming
- transport
- peer
- name: gopkg.in/yaml.v2
version: a83829b6f1293c91addabc89d0571c246397bbf4
- name: k8s.io/heapster
version: 0991ac528ea24aae194e45d6dcf01896cb42cbea
subpackages:
- api/v1/types
- name: k8s.io/kubernetes
version: 95f2ca2ff65a03342746a2a49b8f360428dd94a2
subpackages:
- pkg/client/unversioned/clientcmd
- pkg/kubectl/cmd/util
- pkg/kubectl/resource
- pkg/api
- pkg/api/unversioned
- pkg/client/restclient
- pkg/client/unversioned/auth
- pkg/client/unversioned/clientcmd/api
- pkg/client/unversioned/clientcmd/api/latest
- pkg/runtime
- pkg/util/errors
- pkg/util/homedir
- pkg/util/validation
- pkg/api/errors
- pkg/api/meta
- pkg/api/validation
- pkg/apimachinery
- pkg/apimachinery/registered
- pkg/apis/apps
- pkg/apis/autoscaling
- pkg/apis/batch
- pkg/apis/extensions
- pkg/apis/metrics
- pkg/client/typed/discovery
- pkg/client/unversioned
- pkg/client/unversioned/adapters/internalclientset
- pkg/kubectl
- pkg/labels
- pkg/registry/thirdpartyresourcedata
- pkg/runtime/serializer/json
- pkg/util/flag
- pkg/util/strategicpatch
- pkg/util/sets
- pkg/util/yaml
- pkg/watch
- pkg/api/resource
- pkg/auth/user
- pkg/conversion
- pkg/fields
- pkg/runtime/serializer
- pkg/types
- pkg/util
- pkg/util/intstr
- pkg/util/rand
- pkg/api/v1
- pkg/client/metrics
- pkg/client/transport
- pkg/util/crypto
- pkg/util/flowcontrol
- pkg/util/net
- pkg/version
- pkg/watch/json
- pkg/client/unversioned/clientcmd/api/v1
- pkg/runtime/serializer/versioning
- pkg/conversion/queryparams
- pkg/util/json
- pkg/util/validation/field
- pkg/api/endpoints
- pkg/api/pod
- pkg/api/service
- pkg/api/util
- pkg/capabilities
- pkg/api/install
- pkg/apis/apps/install
- pkg/apis/authorization/install
- pkg/apis/autoscaling/install
- pkg/apis/batch/install
- pkg/apis/componentconfig/install
- pkg/apis/extensions/install
- pkg/apis/metrics/install
- pkg/util/wait
- plugin/pkg/client/auth
- pkg/client/clientset_generated/internalclientset
- pkg/client/clientset_generated/internalclientset/typed/core/unversioned
- pkg/client/clientset_generated/internalclientset/typed/extensions/unversioned
- pkg/apis/batch/v1
- pkg/credentialprovider
- pkg/fieldpath
- pkg/kubelet/qos/util
- pkg/util/deployment
- pkg/util/integer
- pkg/util/jsonpath
- pkg/api/rest
- pkg/apis/extensions/v1beta1
- pkg/apis/extensions/validation
- pkg/registry/generic
- pkg/util/framer
- third_party/forked/json
- pkg/util/runtime
- third_party/forked/reflect
- pkg/runtime/serializer/protobuf
- pkg/runtime/serializer/recognizer
- pkg/util/parsers
- pkg/watch/versioned
- pkg/util/hash
- pkg/util/net/sets
- pkg/apis/apps/v1alpha1
- pkg/apis/authorization
- pkg/apis/authorization/v1beta1
- pkg/apis/autoscaling/v1
- pkg/apis/componentconfig
- pkg/apis/componentconfig/v1alpha1
- pkg/apis/metrics/v1alpha1
- plugin/pkg/client/auth/gcp
- pkg/client/clientset_generated/internalclientset/typed/batch/unversioned
- pkg/controller
- pkg/util/labels
- pkg/util/pod
- third_party/golang/template
- pkg/api/unversioned/validation
- pkg/controller/podautoscaler
- pkg/storage
- pkg/kubelet/qos
- pkg/master/ports
- pkg/client/cache
- pkg/client/record
- pkg/controller/framework
- pkg/controller/podautoscaler/metrics
- name: speter.net/go/exp/math/dec/inf
version: 42ca6cd68aa922bc3f32f1e056e61b65945d9ad7
devImports: []
package: github.com/deis/tiller
import:
- package: golang.org/x/net
version: fb93926129b8ec0056f2f458b1f519654814edf0
subpackages:
- context
- package: github.com/spf13/cobra
subpackages:
- cobra
- package: github.com/Masterminds/sprig
version: ^2.1
- package: gopkg.in/yaml.v2
- package: github.com/Masterminds/semver
version: 1.1.0
- package: github.com/BurntSushi/toml
version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f
- package: github.com/technosophos/moniker
- package: github.com/golang/protobuf
version: f0a097ddac24fb00e07d2ac17f8671423f3ea47c
subpackages:
- proto
- ptypes/any
- ptypes/timestamp
- package: google.golang.org/grpc
version: dec33edc378cf4971a2741cfd86ed70a644d6ba3
- package: k8s.io/kubernetes
version: ^1.2
subpackages:
- pkg/client/unversioned/clientcmd
- pkg/kubectl/cmd/util
- pkg/kubectl/resource
- package: github.com/gosuri/uitable
This diff is collapsed.
/*
Copyright 2015 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 chart
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
)
const (
testfile = "testdata/frobnitz/Chart.yaml"
testdir = "testdata/frobnitz/"
testarchive = "testdata/frobnitz-0.0.1.tgz"
testmember = "templates/template.tpl"
)
// Type canaries. If these fail, they will fail at compile time.
var _ chartLoader = &dirChart{}
var _ chartLoader = &tarChart{}
func TestLoadDir(t *testing.T) {
c, err := LoadDir(testdir)
if err != nil {
t.Errorf("Failed to load chart: %s", err)
}
if c.Chartfile().Name != "frobnitz" {
t.Errorf("Expected chart name to be 'frobnitz'. Got '%s'.", c.Chartfile().Name)
}
}
func TestCreate(t *testing.T) {
tdir, err := ioutil.TempDir("", "helm-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tdir)
cf := &Chartfile{Name: "foo"}
c, err := Create(cf, tdir)
if err != nil {
t.Fatal(err)
}
dir := filepath.Join(tdir, "foo")
if c.Chartfile().Name != "foo" {
t.Errorf("Expected name to be 'foo', got %q", c.Chartfile().Name)
}
for _, d := range []string{preTemplates, preCharts} {
if fi, err := os.Stat(filepath.Join(dir, d)); err != nil {
t.Errorf("Expected %s dir: %s", d, err)
} else if !fi.IsDir() {
t.Errorf("Expected %s to be a directory.", d)
}
}
for _, f := range []string{ChartfileName, preValues} {
if fi, err := os.Stat(filepath.Join(dir, f)); err != nil {
t.Errorf("Expected %s file: %s", f, err)
} else if fi.IsDir() {
t.Errorf("Expected %s to be a fle.", f)
}
}
}
func TestLoad(t *testing.T) {
c, err := Load(testarchive)
if err != nil {
t.Errorf("Failed to load chart: %s", err)
return
}
defer c.Close()
if c.Chartfile() == nil {
t.Error("No chartfile was loaded.")
return
}
if c.Chartfile().Name != "frobnitz" {
t.Errorf("Expected name to be frobnitz, got %q", c.Chartfile().Name)
}
}
func TestLoadData(t *testing.T) {
data, err := ioutil.ReadFile(testarchive)
if err != nil {
t.Errorf("Failed to read testarchive file: %s", err)
return
}
c, err := LoadData(data)
if err != nil {
t.Errorf("Failed to load chart: %s", err)
return
}
if c.Chartfile() == nil {
t.Error("No chartfile was loaded.")
return
}
if c.Chartfile().Name != "frobnitz" {
t.Errorf("Expected name to be frobnitz, got %q", c.Chartfile().Name)
}
}
func TestChart(t *testing.T) {
c, err := LoadDir(testdir)
if err != nil {
t.Errorf("Failed to load chart: %s", err)
}
defer c.Close()
if c.Dir() != c.loader.dir() {
t.Errorf("Unexpected location for directory: %s", c.Dir())
}
if c.Chartfile().Name != c.loader.chartfile().Name {
t.Errorf("Unexpected chart file name: %s", c.Chartfile().Name)
}
dir := c.Dir()
d := c.ChartsDir()
if d != filepath.Join(dir, preCharts) {
t.Errorf("Unexpectedly, charts are in %s", d)
}
d = c.TemplatesDir()
if d != filepath.Join(dir, preTemplates) {
t.Errorf("Unexpectedly, templates are in %s", d)
}
}
func TestLoadTemplates(t *testing.T) {
c, err := LoadDir(testdir)
if err != nil {
t.Errorf("Failed to load chart: %s", err)
}
members, err := c.LoadTemplates()
if members == nil {
t.Fatalf("Cannot load templates: unknown error")
}
if err != nil {
t.Fatalf("Cannot load templates: %s", err)
}
dir := c.TemplatesDir()
files, err := ioutil.ReadDir(dir)
if err != nil {
t.Fatalf("Cannot read template directory: %s", err)
}
if len(members) != len(files) {
t.Fatalf("Expected %d templates, got %d", len(files), len(members))
}
root := c.loader.dir()
for _, file := range files {
path := filepath.Join(preTemplates, file.Name())
if err := findMember(root, path, members); err != nil {
t.Fatal(err)
}
}
}
func findMember(root, path string, members []*Member) error {
for _, member := range members {
if member.Path == path {
filename := filepath.Join(root, path)
if err := compareContent(filename, member.Content); err != nil {
return err
}
return nil
}
}
return fmt.Errorf("Template not found: %s", path)
}
func TestLoadMember(t *testing.T) {
c, err := LoadDir(testdir)
if err != nil {
t.Errorf("Failed to load chart: %s", err)
}
member, err := c.LoadMember(testmember)
if member == nil {
t.Fatalf("Cannot load member %s: unknown error", testmember)
}
if err != nil {
t.Fatalf("Cannot load member %s: %s", testmember, err)
}
if member.Path != testmember {
t.Errorf("Expected member path %s, got %s", testmember, member.Path)
}
filename := filepath.Join(c.loader.dir(), testmember)
if err := compareContent(filename, member.Content); err != nil {
t.Fatal(err)
}
}
func TestLoadContent(t *testing.T) {
c, err := LoadDir(testdir)
if err != nil {
t.Errorf("Failed to load chart: %s", err)
}
content, err := c.LoadContent()
if err != nil {
t.Errorf("Failed to load chart content: %s", err)
}
want := c.Chartfile()
have := content.Chartfile
if !reflect.DeepEqual(want, have) {
t.Errorf("Unexpected chart file\nwant:\n%v\nhave:\n%v\n", want, have)
}
for _, member := range content.Members {
have := member.Content
wantMember, err := c.LoadMember(member.Path)
if err != nil {
t.Errorf("Failed to load chart member: %s", err)
}
t.Logf("%s:\n%s\n\n", member.Path, member.Content)
want := wantMember.Content
if !reflect.DeepEqual(want, have) {
t.Errorf("Unexpected chart member %s\nwant:\n%v\nhave:\n%v\n", member.Path, want, have)
}
}
}
func compareContent(filename string, content []byte) error {
compare, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("Cannot read test file %s: %s", filename, err)
}
if !reflect.DeepEqual(compare, content) {
return fmt.Errorf("Expected member content\n%v\ngot\n%v", compare, content)
}
return nil
}
/*
Copyright 2015 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 chart
import (
"io/ioutil"
"gopkg.in/yaml.v2"
)
// Chartfile describes a Helm Chart (e.g. Chart.yaml)
type Chartfile struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Version string `yaml:"version"`
Keywords []string `yaml:"keywords,omitempty"`
Maintainers []*Maintainer `yaml:"maintainers,omitempty"`
Source []string `yaml:"source,omitempty"`
Home string `yaml:"home"`
}
// Maintainer describes a chart maintainer.
type Maintainer struct {
Name string `yaml:"name"`
Email string `yaml:"email,omitempty"`
}
// LoadChartfile loads a Chart.yaml file into a *Chart.
func LoadChartfile(filename string) (*Chartfile, error) {
b, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
var y Chartfile
return &y, yaml.Unmarshal(b, &y)
}
// Save saves a Chart.yaml file
func (c *Chartfile) Save(filename string) error {
b, err := c.Marshal()
if err != nil {
return err
}
return ioutil.WriteFile(filename, b, 0644)
}
// Marshal encodes the chart file into YAML.
func (c *Chartfile) Marshal() ([]byte, error) {
return yaml.Marshal(c)
}
/*
Copyright 2015 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 chart
import (
"testing"
)
func TestLoadChartfile(t *testing.T) {
f, err := LoadChartfile(testfile)
if err != nil {
t.Errorf("Failed to open %s: %s", testfile, err)
return
}
if f.Name != "frobnitz" {
t.Errorf("Expected frobnitz, got %s", f.Name)
}
if len(f.Maintainers) != 2 {
t.Errorf("Expected 2 maintainers, got %d", len(f.Maintainers))
}
if f.Source[0] != "https://example.com/foo/bar" {
t.Errorf("Expected https://example.com/foo/bar, got %s", f.Source)
}
}
/*
Copyright 2015 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 chart implements the Chart format.
This package provides tools for working with the Chart format, including the
Chartfile (chart.yaml) and compressed chart archives.
*/
package chart
/*
Copyright 2015 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 chart
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
)
// Save creates an archived chart to the given directory.
//
// This takes an existing chart and a destination directory.
//
// If the directory is /foo, and the chart is named bar, with version 1.0.0, this
// will generate /foo/bar-1.0.0.tgz.
//
// This returns the absolute path to the chart archive file.
func Save(c *Chart, outDir string) (string, error) {
// Create archive
if fi, err := os.Stat(outDir); err != nil {
return "", err
} else if !fi.IsDir() {
return "", fmt.Errorf("location %s is not a directory", outDir)
}
cfile := c.Chartfile()
dir := c.Dir()
pdir := filepath.Dir(dir)
filename := fmt.Sprintf("%s-%s.tgz", fname(cfile.Name), cfile.Version)
filename = filepath.Join(outDir, filename)
// Fail early if the YAML is borked.
if err := cfile.Save(filepath.Join(dir, ChartfileName)); err != nil {
return "", err
}
// Create file.
f, err := os.Create(filename)
if err != nil {
return "", err
}
// Wrap in gzip writer
zipper := gzip.NewWriter(f)
zipper.Header.Extra = headerBytes
zipper.Header.Comment = "Helm"
// Wrap in tar writer
twriter := tar.NewWriter(zipper)
rollback := false
defer func() {
twriter.Close()
zipper.Close()
f.Close()
if rollback {
os.Remove(filename)
}
}()
err = filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
hdr, err := tar.FileInfoHeader(fi, ".")
if err != nil {
return err
}
relpath, err := filepath.Rel(pdir, path)
if err != nil {
return err
}
hdr.Name = relpath
twriter.WriteHeader(hdr)
// Skip directories.
if fi.IsDir() {
return nil
}
in, err := os.Open(path)
if err != nil {
return err
}
_, err = io.Copy(twriter, in)
in.Close()
if err != nil {
return err
}
return nil
})
if err != nil {
rollback = true
return filename, err
}
return filename, nil
}
/*
Copyright 2015 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 chart
import (
"archive/tar"
"compress/gzip"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"testing"
)
const sprocketdir = "testdata/sprocket"
func TestSave(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "helm-")
if err != nil {
t.Fatal("Could not create temp directory")
}
t.Logf("Temp: %s", tmpdir)
// Because of the defer, don't call t.Fatal in the remainder of this
// function.
defer os.RemoveAll(tmpdir)
c, err := LoadDir(sprocketdir)
if err != nil {
t.Errorf("Failed to load %s: %s", sprocketdir, err)
return
}
tfile, err := Save(c, tmpdir)
if err != nil {
t.Errorf("Failed to save %s to %s: %s", c.Chartfile().Name, tmpdir, err)
return
}
b := filepath.Base(tfile)
expectname := "sprocket-1.2.3-alpha.1+12345.tgz"
if b != expectname {
t.Errorf("Expected %q, got %q", expectname, b)
}
files, err := getAllFiles(tfile)
if err != nil {
t.Errorf("Could not extract files: %s", err)
}
// Files should come back in order.
expect := []string{
"sprocket",
"sprocket/Chart.yaml",
"sprocket/values.toml",
"sprocket/templates",
"sprocket/templates/template.tpl",
}
if len(expect) != len(files) {
t.Errorf("Expected %d files, found %d", len(expect), len(files))
return
}
sort.Strings(files)
sort.Strings(expect)
for i := 0; i < len(expect); i++ {
if expect[i] != files[i] {
t.Errorf("Expected file %q, got %q", expect[i], files[i])
}
}
}
func getAllFiles(tfile string) ([]string, error) {
f1, err := os.Open(tfile)
if err != nil {
return []string{}, err
}
f2, err := gzip.NewReader(f1)
if err != nil {
f1.Close()
return []string{}, err
}
if f2.Header.Comment != "Helm" {
return []string{}, fmt.Errorf("Expected header Helm. Got %s", f2.Header.Comment)
}
if string(f2.Header.Extra) != string(headerBytes) {
return []string{}, fmt.Errorf("Expected header signature. Got %v", f2.Header.Extra)
}
f3 := tar.NewReader(f2)
files := []string{}
var e error
var hdr *tar.Header
for e == nil {
hdr, e = f3.Next()
if e == nil {
files = append(files, hdr.Name)
}
}
f2.Close()
f1.Close()
return files, nil
}
This directory houses charts used in testing.
poet = "Coleridge"
title = "Rime of the Ancient Mariner"
stanza = ["at", "length", "did", "cross", "an", "Albatross"]
[mariner]
with = "crossbow"
shot = "ALBATROSS"
[water.water]
where = "everywhere"
nor = "any drop to drink"
name = "frobnitz"
description = "This is a frobniz."
version = "1.2.3-alpha.1+12345"
keywords = ["frobnitz", "sprocket", "dodad"]
home = "http://example.com"
source = [
"https://example.com/foo/bar",
"https://github.com/example/foo"
]
[[maintainer]]
name = "The Helm Team"
email = "helm@example.com"
[[maintainer]]
name = "Someone Else"
email = "nobody@example.com"
name: frobnitz
description: This is a frobniz.
version: "1.2.3-alpha.1+12345"
keywords:
- frobnitz
- sprocket
- dodad
maintainers:
- name: The Helm Team
email: helm@example.com
- name: Someone Else
email: nobody@example.com
source:
- https://example.com/foo/bar
home: http://example.com
This is an install document. The client may display this.
# Frobnitz
This is an example chart.
## Usage
This is an example. It has no usage.
## Development
For developer info, see the top-level repository.
This is a placeholder for documentation.
<?xml version="1.0"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.0" width="256" height="256" id="test">
<desc>Example icon</desc>
<rect id="first" x="2" y="2" width="40" height="60" fill="navy"/>
<rect id="second" x="15" y="4" width="40" height="60" fill="red"/>
</svg>
# A values file contains configuration.
name = "Some Name"
[section]
name = "Name in a section"
name: sprocket
description: This is a sprocket"
version: 1.2.3-alpha.1+12345
keywords:
- frobnitz
- sprocket
- dodad
maintainers:
- name: The Helm Team
email: helm@example.com
- name: Someone Else
email: nobody@example.com
source:
- https://example.com/foo/bar
home: http://example.com
# A values file contains configuration.
name = "Some Name"
[section]
name = "Name in a section"
package chart
import (
"errors"
"io/ioutil"
"strings"
"github.com/BurntSushi/toml"
)
// ErrNoTable indicates that a chart does not have a matching table.
var ErrNoTable = errors.New("no table")
// Values represents a collection of chart values.
type Values map[string]interface{}
// Table gets a table (TOML subsection) from a Values object.
//
// The table is returned as a Values.
//
// Compound table names may be specified with dots:
//
// foo.bar
//
// The above will be evaluated as "The table bar inside the table
// foo".
//
// An ErrNoTable is returned if the table does not exist.
func (v Values) Table(name string) (Values, error) {
names := strings.Split(name, ".")
table := v
var err error
for _, n := range names {
table, err = tableLookup(table, n)
if err != nil {
return table, err
}
}
return table, err
}
func tableLookup(v Values, simple string) (Values, error) {
v2, ok := v[simple]
if !ok {
return v, ErrNoTable
}
vv, ok := v2.(map[string]interface{})
if !ok {
return vv, ErrNoTable
}
return vv, nil
}
// ReadValues will parse TOML byte data into a Values.
func ReadValues(data []byte) (Values, error) {
out := map[string]interface{}{}
err := toml.Unmarshal(data, out)
return out, err
}
// ReadValuesFile will parse a TOML file into a Values.
func ReadValuesFile(filename string) (Values, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return map[string]interface{}{}, err
}
return ReadValues(data)
}
package chart
import (
"bytes"
"fmt"
"testing"
"text/template"
)
func TestReadValues(t *testing.T) {
doc := `# Test TOML parse
poet = "Coleridge"
title = "Rime of the Ancient Mariner"
stanza = ["at", "length", "did", "cross", "an", "Albatross"]
[mariner]
with = "crossbow"
shot = "ALBATROSS"
[water.water]
where = "everywhere"
nor = "any drop to drink"
`
data, err := ReadValues([]byte(doc))
if err != nil {
t.Fatalf("Error parsing bytes: %s", err)
}
matchValues(t, data)
}
func TestReadValuesFile(t *testing.T) {
data, err := ReadValuesFile("./testdata/coleridge.toml")
if err != nil {
t.Fatalf("Error reading TOML file: %s", err)
}
matchValues(t, data)
}
func ExampleValues() {
doc := `title="Moby Dick"
[chapter.one]
title = "Loomings"
[chapter.two]
title = "The Carpet-Bag"
[chapter.three]
title = "The Spouter Inn"
`
d, err := ReadValues([]byte(doc))
if err != nil {
panic(err)
}
ch1, err := d.Table("chapter.one")
if err != nil {
panic("could not find chapter one")
}
fmt.Print(ch1["title"])
// Output:
// Loomings
}
func TestTable(t *testing.T) {
doc := `title="Moby Dick"
[chapter.one]
title = "Loomings"
[chapter.two]
title = "The Carpet-Bag"
[chapter.three]
title = "The Spouter Inn"
`
d, err := ReadValues([]byte(doc))
if err != nil {
t.Fatalf("Failed to parse the White Whale: %s", err)
}
if _, err := d.Table("title"); err == nil {
t.Fatalf("Title is not a table.")
}
if _, err := d.Table("chapter"); err != nil {
t.Fatalf("Failed to get the chapter table: %s\n%v", err, d)
}
if v, err := d.Table("chapter.one"); err != nil {
t.Errorf("Failed to get chapter.one: %s", err)
} else if v["title"] != "Loomings" {
t.Errorf("Unexpected title: %s", v["title"])
}
if _, err := d.Table("chapter.three"); err != nil {
t.Errorf("Chapter three is missing: %s\n%v", err, d)
}
if _, err := d.Table("chapter.OneHundredThirtySix"); err == nil {
t.Errorf("I think you mean 'Epilogue'")
}
}
func matchValues(t *testing.T, data map[string]interface{}) {
if data["poet"] != "Coleridge" {
t.Errorf("Unexpected poet: %s", data["poet"])
}
if o, err := ttpl("{{len .stanza}}", data); err != nil {
t.Errorf("len stanza: %s", err)
} else if o != "6" {
t.Errorf("Expected 6, got %s", o)
}
if o, err := ttpl("{{.mariner.shot}}", data); err != nil {
t.Errorf(".mariner.shot: %s", err)
} else if o != "ALBATROSS" {
t.Errorf("Expected that mariner shot ALBATROSS")
}
if o, err := ttpl("{{.water.water.where}}", data); err != nil {
t.Errorf(".water.water.where: %s", err)
} else if o != "everywhere" {
t.Errorf("Expected water water everywhere")
}
}
func ttpl(tpl string, v map[string]interface{}) (string, error) {
var b bytes.Buffer
tt := template.Must(template.New("t").Parse(tpl))
if err := tt.Execute(&b, v); err != nil {
return "", err
}
return b.String(), nil
}
package client
import (
"bytes"
"text/template"
"github.com/Masterminds/sprig"
"github.com/deis/tiller/pkg/kubectl"
)
// Installer installs tiller into Kubernetes
//
// See InstallYAML.
type Installer struct {
// Metadata holds any global metadata attributes for the resources
Metadata map[string]interface{}
// Tiller specific metadata
Tiller map[string]interface{}
}
// NewInstaller creates a new Installer
func NewInstaller() *Installer {
return &Installer{
Metadata: map[string]interface{}{},
Tiller: map[string]interface{}{},
}
}
// Install uses kubectl to install tiller
//
// Returns the string output received from the operation, and an error if the
// command failed.
func (i *Installer) Install(runner kubectl.Runner) (string, error) {
b, err := i.expand()
if err != nil {
return "", err
}
o, err := runner.Create(b)
return string(o), err
}
func (i *Installer) expand() ([]byte, error) {
var b bytes.Buffer
t := template.Must(template.New("manifest").Funcs(sprig.TxtFuncMap()).Parse(InstallYAML))
err := t.Execute(&b, i)
return b.Bytes(), err
}
// InstallYAML is the installation YAML for DM.
const InstallYAML = `
---
apiVersion: v1
kind: Namespace
metadata:
labels:
app: helm
name: helm-namespace
name: helm
---
apiVersion: v1
kind: ReplicationController
metadata:
labels:
app: helm
name: tiller
name: tiller-rc
namespace: helm
spec:
replicas: 1
selector:
app: helm
name: tiller
template:
metadata:
labels:
app: helm
name: tiller
spec:
containers:
- env: []
image: {{default "gcr.io/deis-sandbox/tiller:canary" .Tiller.Image}}
name: tiller
ports:
- containerPort: 8080
name: tiller
imagePullPolicy: Always
---
`
/*Package engine implements the Go template engine as a Tiller Engine.
Tiller provides a simple interface for taking a Chart and rendering its templates.
The 'engine' package implements this interface using Go's built-in 'text/template'
package.
*/
package engine
package engine
import (
"bytes"
"fmt"
"log"
"text/template"
"github.com/Masterminds/sprig"
chartutil "github.com/deis/tiller/pkg/chart"
"github.com/deis/tiller/pkg/proto/hapi/chart"
)
// Engine is an implementation of 'cmd/tiller/environment'.Engine that uses Go templates.
type Engine struct {
// FuncMap contains the template functions that will be passed to each
// render call. This may only be modified before the first call to Render.
FuncMap template.FuncMap
}
// New creates a new Go template Engine instance.
//
// The FuncMap is initialized here. You may modify the FuncMap _prior to_ the
// first invocation of Render.
//
// The FuncMap sets all of the Sprig functions except for those that provide
// access to the underlying OS (env, expandenv).
func New() *Engine {
f := sprig.TxtFuncMap()
delete(f, "env")
delete(f, "expandenv")
return &Engine{
FuncMap: f,
}
}
// Render takes a chart, optional values, and attempts to render the Go templates.
//
// Render can be called repeatedly on the same engine.
//
// This will look in the chart's 'templates' data (e.g. the 'templates/' directory)
// and attempt to render the templates there using the values passed in.
//
// Values are scoped to their templates. A dependency template will not have
// access to the values set for its parent. If chart "foo" includes chart "bar",
// "bar" will not have access to the values for "foo".
//
// Values are passed through the templates according to scope. If the top layer
// chart includes the chart foo, which includes the chart bar, the values map
// will be examined for a table called "foo". If "foo" is found in vals,
// that section of the values will be passed into the "foo" chart. And if that
// section contains a value named "bar", that value will be passed on to the
// bar chart during render time.
//
// Values are coalesced together using the fillowing rules:
//
// - Values in a higher level chart always override values in a lower-level
// dependency chart
// - Scalar values and arrays are replaced, maps are merged
// - A chart has access to all of the variables for it, as well as all of
// the values destined for its dependencies.
func (e *Engine) Render(chrt *chart.Chart, vals *chart.Config) (map[string]string, error) {
var cvals chartutil.Values
// Parse values if not nil. We merge these at the top level because
// the passed-in values are in the same namespace as the parent chart.
if vals != nil {
evals, err := chartutil.ReadValues([]byte(vals.Raw))
if err != nil {
return map[string]string{}, err
}
cvals = coalesceValues(chrt, evals)
}
// Render the charts
tmap := allTemplates(chrt, cvals)
return e.render(tmap)
}
// renderable is an object that can be rendered.
type renderable struct {
// tpl is the current template.
tpl string
// vals are the values to be supplied to the template.
vals chartutil.Values
}
// render takes a map of templates/values and renders them.
func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) {
// Basically, what we do here is start with an empty parent template and then
// build up a list of templates -- one for each file. Once all of the templates
// have been parsed, we loop through again and execute every template.
//
// The idea with this process is to make it possible for more complex templates
// to share common blocks, but to make the entire thing feel like a file-based
// template engine.
t := template.New("gotpl")
files := []string{}
for fname, r := range tpls {
t = t.New(fname).Funcs(e.FuncMap)
if _, err := t.Parse(r.tpl); err != nil {
return map[string]string{}, fmt.Errorf("parse error in %q: %s", fname, err)
}
files = append(files, fname)
}
rendered := make(map[string]string, len(files))
var buf bytes.Buffer
for _, file := range files {
// log.Printf("Exec %s with %v (%s)", file, tpls[file].vals, tpls[file].tpl)
if err := t.ExecuteTemplate(&buf, file, tpls[file].vals); err != nil {
return map[string]string{}, fmt.Errorf("render error in %q: %s", file, err)
}
rendered[file] = buf.String()
buf.Reset()
}
return rendered, nil
}
// allTemplates returns all templates for a chart and its dependencies.
//
// As it goes, it also prepares the values in a scope-sensitive manner.
func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable {
templates := map[string]renderable{}
recAllTpls(c, templates, vals, true)
return templates
}
// recAllTpls recurses through the templates in a chart.
//
// As it recurses, it also sets the values to be appropriate for the template
// scope.
func recAllTpls(c *chart.Chart, templates map[string]renderable, parentVals chartutil.Values, top bool) {
var pvals chartutil.Values
if top {
// If this is the top of the rendering tree, assume that parentVals
// is already resolved to the authoritative values.
pvals = parentVals
} else if c.Metadata != nil && c.Metadata.Name != "" {
// An error indicates that the table doesn't exist. So we leave it as
// an empty map.
tmp, err := parentVals.Table(c.Metadata.Name)
if err == nil {
pvals = tmp
}
}
cvals := coalesceValues(c, pvals)
//log.Printf("racAllTpls values: %v", cvals)
for _, child := range c.Dependencies {
recAllTpls(child, templates, cvals, false)
}
for _, t := range c.Templates {
templates[t.Name] = renderable{
tpl: string(t.Data),
vals: cvals,
}
}
}
// coalesceValues builds up a values map for a particular chart.
//
// Values in v will override the values in the chart.
func coalesceValues(c *chart.Chart, v chartutil.Values) chartutil.Values {
// If there are no values in the chart, we just return the given values
if c.Values == nil {
return v
}
nv, err := chartutil.ReadValues([]byte(c.Values.Raw))
if err != nil {
// On error, we return just the overridden values.
// FIXME: We should log this error. It indicates that the TOML data
// did not parse.
log.Printf("error reading default values: %s", err)
return v
}
for k, val := range v {
// NOTE: We could block coalesce on cases where nv does not explicitly
// declare a value. But that forces the chart author to explicitly
// set a default for every template param. We want to preserve the
// possibility of "hidden" parameters.
if istable(val) {
if inmap, ok := nv[k]; ok && istable(inmap) {
coalesceTables(inmap.(map[string]interface{}), val.(map[string]interface{}))
} else if ok {
log.Printf("Cannot copy table into non-table value for %s (%v)", k, inmap)
} else {
// The parent table does not have a key entry for this item,
// so we can safely set it. This is necessary for nested charts.
log.Printf("Copying %s into map %v", k, nv)
nv[k] = val
}
} else {
nv[k] = val
}
}
return nv
}
// coalesceTables merges a source map into a destination map.
func coalesceTables(dst, src map[string]interface{}) {
for key, val := range src {
if istable(val) {
if innerdst, ok := dst[key]; !ok {
dst[key] = val
} else if istable(innerdst) {
coalesceTables(innerdst.(map[string]interface{}), val.(map[string]interface{}))
} else {
log.Printf("Cannot overwrite table with non table for %s (%v)", key, val)
}
continue
} else if dv, ok := dst[key]; ok && istable(dv) {
log.Printf("Destination for %s is a table. Ignoring non-table value %v", key, val)
continue
}
dst[key] = val
}
}
// istable is a special-purpose function to see if the present thing matches the definition of a TOML table.
func istable(v interface{}) bool {
_, ok := v.(map[string]interface{})
return ok
}
This diff is collapsed.
This diff is collapsed.
package helm
import (
"google.golang.org/grpc"
)
type config struct {
ServAddr string
Insecure bool
}
func (cfg *config) DialOpts() (opts []grpc.DialOption) {
if cfg.Insecure {
opts = append(opts, grpc.WithInsecure())
} else {
// TODO: handle transport credentials
}
return
}
func (cfg *config) CallOpts() (opts []grpc.CallOption) {
return
}
func (cfg *config) client() *client {
return &client{cfg: cfg}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
metadata:
name: {{.name | default "foo" | title}}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment