feat(helm, tiller): implement k8s portion of install

This commit finally ties `helm install` together with the Kubernetes
client library to make an end-to-end trip.

There were several small fixes to go on both client and server side,
along with some changes to proto to support better error reporting.

The alpine chart has been updated to exhibit how the new Helm works.
This commit is contained in:
Matt Butcher 2016-05-04 17:27:00 -06:00
parent eba4c59a84
commit 6db7c39b84
11 changed files with 199 additions and 46 deletions

View File

@ -12,13 +12,16 @@ option go_package = "release";
//
message Status {
enum Code {
// Status_UNKNOWN indicates that a release is in an uncertain state.
UNKNOWN = 0;
// Status_DEPLOYED indicates that the release has been pushed to Kubernetes.
DEPLOYED = 1;
// Status_DELETED indicates that a release has been deleted from Kubermetes.
DELETED = 2;
// Status_SUPERSEDED indicates that this release object is outdated and a newer one exists.
SUPERSEDED = 3;
// Status_FAILED indicates that the release was not successfully deployed.
FAILED = 4;
}
Code code = 1;

View File

@ -3,11 +3,13 @@ package main
import (
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"github.com/kubernetes/helm/pkg/helm"
"github.com/kubernetes/helm/pkg/proto/hapi/release"
"github.com/kubernetes/helm/pkg/timeconv"
)
const installDesc = `
@ -25,9 +27,14 @@ const (
// install flags & args
var (
installArg string // name or relative path of the chart to install
tillerHost string // override TILLER_HOST envVar
verbose bool // enable verbose install
// installArg is the name or relative path of the chart to install
installArg string
// tillerHost overrides TILLER_HOST envVar
tillerHost string
// verbose enables verbose output
verbose bool
// installDryRun performs a dry-run install
installDryRun bool
)
var installCmd = &cobra.Command{
@ -40,7 +47,7 @@ var installCmd = &cobra.Command{
func runInstall(cmd *cobra.Command, args []string) error {
setupInstallEnv(args)
res, err := helm.InstallRelease(installArg)
res, err := helm.InstallRelease(installArg, installDryRun)
if err != nil {
return err
}
@ -59,8 +66,9 @@ func printRelease(rel *release.Release) {
}
fmt.Printf("release.name: %s\n", rel.Name)
if verbose {
fmt.Printf("release.info: %s\n", rel.GetInfo())
fmt.Printf("release.chart: %s\n", rel.GetChart())
fmt.Printf("release.info: %s %s\n", timeconv.Format(rel.Info.LastDeployed, time.ANSIC), rel.Info.Status)
fmt.Printf("release.chart: %s %s\n", rel.Chart.Metadata.Name, rel.Chart.Metadata.Version)
fmt.Printf("release.manifest: %s\n", rel.Manifest)
}
}
@ -92,6 +100,7 @@ func fatalf(format string, args ...interface{}) {
func init() {
installCmd.Flags().StringVar(&tillerHost, "host", defaultHost, "address of tiller server")
installCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose install")
installCmd.Flags().BoolVar(&installDryRun, "dry-run", false, "simulate an install")
RootCommand.AddCommand(installCmd)
}

View File

@ -1,7 +1,16 @@
/*Package environment describes the operating environment for Tiller.
Tiller's environment encapsulates all of the service dependencies Tiller has.
These dependencies are expressed as interfaces so that alternate implementations
(mocks, etc.) can be easily generated.
*/
package environment
import (
"io"
"github.com/kubernetes/helm/pkg/engine"
"github.com/kubernetes/helm/pkg/kube"
"github.com/kubernetes/helm/pkg/proto/hapi/chart"
"github.com/kubernetes/helm/pkg/proto/hapi/release"
"github.com/kubernetes/helm/pkg/storage"
@ -10,6 +19,9 @@ import (
// GoTplEngine is the name of the Go template engine, as registered in the EngineYard.
const GoTplEngine = "gotpl"
// DefaultNamespace is the default namespace for Tiller.
const DefaultNamespace = "helm"
// 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.
@ -52,7 +64,11 @@ func (y EngineYard) Default() Engine {
// 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)
// Render renders a chart.
//
// It receives a chart, a config, and a map of overrides to the config.
// Overrides are assumed to be passed from the system, not the user.
Render(*chart.Chart, *chart.Config, map[string]interface{}) (map[string]string, error)
}
// ReleaseStorage represents a storage engine for a Release.
@ -106,19 +122,37 @@ type ReleaseStorage interface {
//
// 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.
// Create creates one or more resources.
//
// TODO: Can these be in YAML or JSON, or must they be in one particular
// format?
Install(manifests map[string]string) error
// namespace must contain a valid existing namespace.
//
// reader must contain a YAML stream (one or more YAML documents separated
// by "\n---\n").
//
// config is optional. If nil, the client will use its existing configuration.
// If set, the client will override its default configuration with the
// passed in one.
Create(namespace string, reader io.Reader) error
}
// PrintingKubeClient implements KubeClient, but simply prints the reader to
// the given output.
type PrintingKubeClient struct {
Out io.Writer
}
// Create prints the values of what would be created with a real KubeClient.
func (p *PrintingKubeClient) Create(ns string, r io.Reader) error {
_, err := io.Copy(p.Out, r)
return err
}
// Environment provides the context for executing a client request.
//
// All services in a context are concurrency safe.
type Environment struct {
// The default namespace
Namespace string
// EngineYard provides access to the known template engines.
EngineYard EngineYard
// Releases stores records of releases.
@ -136,7 +170,9 @@ func New() *Environment {
GoTplEngine: e,
}
return &Environment{
Namespace: DefaultNamespace,
EngineYard: ey,
Releases: storage.NewMemory(),
KubeClient: kube.New(nil), //&PrintingKubeClient{Out: os.Stdout},
}
}

View File

@ -1,6 +1,8 @@
package environment
import (
"bytes"
"io"
"testing"
"github.com/kubernetes/helm/pkg/proto/hapi/chart"
@ -11,7 +13,7 @@ type mockEngine struct {
out map[string]string
}
func (e *mockEngine) Render(chrt *chart.Chart, v *chart.Config) (map[string]string, error) {
func (e *mockEngine) Render(chrt *chart.Chart, v *chart.Config, o map[string]interface{}) (map[string]string, error) {
return e.out, nil
}
@ -48,13 +50,14 @@ func (r *mockReleaseStorage) Query(labels map[string]string) ([]*release.Release
type mockKubeClient struct {
}
func (k *mockKubeClient) Install(manifests map[string]string) error {
func (k *mockKubeClient) Create(ns string, r io.Reader) error {
return nil
}
var _ Engine = &mockEngine{}
var _ ReleaseStorage = &mockReleaseStorage{}
var _ KubeClient = &mockKubeClient{}
var _ KubeClient = &PrintingKubeClient{}
func TestEngine(t *testing.T) {
eng := &mockEngine{out: map[string]string{"albatross": "test"}}
@ -64,7 +67,7 @@ func TestEngine(t *testing.T) {
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 {
} else if out, err := engine.Render(&chart.Chart{}, &chart.Config{}, map[string]interface{}{}); err != nil {
t.Errorf("unexpected template error: %s", err)
} else if out["albatross"] != "test" {
t.Errorf("expected 'test', got %q", out["albatross"])
@ -102,9 +105,18 @@ func TestKubeClient(t *testing.T) {
env := New()
env.KubeClient = kc
manifests := map[string]string{}
manifests := map[string]string{
"foo": "name: value\n",
"bar": "name: value\n",
}
if err := env.KubeClient.Install(manifests); err != nil {
b := bytes.NewBuffer(nil)
for _, content := range manifests {
b.WriteString("\n---\n")
b.WriteString(content)
}
if err := env.KubeClient.Create("sharry-bobbins", b); err != nil {
t.Errorf("Kubeclient failed: %s", err)
}
}

View File

@ -9,6 +9,7 @@ import (
"github.com/kubernetes/helm/cmd/tiller/environment"
"github.com/kubernetes/helm/pkg/proto/hapi/release"
"github.com/kubernetes/helm/pkg/proto/hapi/services"
"github.com/kubernetes/helm/pkg/storage"
"github.com/kubernetes/helm/pkg/timeconv"
"github.com/technosophos/moniker"
ctx "golang.org/x/net/context"
@ -99,19 +100,44 @@ func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateRelease
return nil, errNotImplemented
}
func (s *releaseServer) uniqName() (string, error) {
maxTries := 5
for i := 0; i < maxTries; i++ {
namer := moniker.New()
name := namer.NameSep("-")
if _, err := s.env.Releases.Read(name); err == storage.ErrNotFound {
return name, nil
}
log.Printf("info: Name %q is taken. Searching again.", name)
}
log.Printf("warning: No available release names found after %d tries", maxTries)
return "ERROR", errors.New("no available release name found")
}
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()
name, err := s.uniqName()
if err != nil {
return nil, err
}
overrides := map[string]interface{}{
"Release": map[string]interface{}{
"Name": name,
"Time": ts,
"Namespace": s.env.Namespace,
"Service": "Tiller",
},
"Chart": req.Chart.Metadata,
}
// Render the templates
files, err := s.env.EngineYard.Default().Render(req.Chart, req.Values)
// TODO: Fix based on whether chart has `engine: SOMETHING` set.
files, err := s.env.EngineYard.Default().Render(req.Chart, req.Values, overrides)
if err != nil {
return nil, err
}
@ -139,16 +165,30 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea
Manifest: b.String(),
}
res := &services.InstallReleaseResponse{Release: r}
if req.DryRun {
log.Printf("Dry run for %s", name)
return &services.InstallReleaseResponse{Release: r}, nil
return res, nil
}
if err := s.env.KubeClient.Create(s.env.Namespace, b); err != nil {
r.Info.Status.Code = release.Status_FAILED
log.Printf("warning: Release %q failed: %s", name, err)
return res, fmt.Errorf("release %s failed: %s", name, err)
}
// This is a tricky case. The release has been created, but the result
// cannot be recorded. The truest thing to tell the user is that the
// release was created. However, the user will not be able to do anything
// further with this release.
//
// One possible strategy would be to do a timed retry to see if we can get
// this stored in the future.
if err := s.env.Releases.Create(r); err != nil {
return nil, err
log.Printf("warning: Failed to record release %q: %s", name, err)
}
return &services.InstallReleaseResponse{Release: r}, nil
return res, nil
}
func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) {

View File

@ -2,6 +2,7 @@ package main
import (
"fmt"
"os"
"strings"
"testing"
@ -219,6 +220,7 @@ func TestListReleases(t *testing.T) {
func mockEnvironment() *environment.Environment {
e := environment.New()
e.Releases = storage.NewMemory()
e.KubeClient = &environment.PrintingKubeClient{Out: os.Stdout}
return e
}

View File

@ -1,9 +1,13 @@
apiVersion: v1
kind: Pod
metadata:
name: {{default "alpine" .name}}
name: {{.Release.Name}}-{{.Chart.Name}}
labels:
heritage: helm
heritage: {{.Release.Service}}
chartName: {{.Chart.Name}}
chartVersion: {{.Chart.Version}}
annotations:
"helm.sh/created": {{.Release.Time.Seconds}}
spec:
restartPolicy: {{default "Never" .restart_policy}}
containers:

View File

@ -59,7 +59,7 @@ func New() *Engine {
// - 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) {
func (e *Engine) Render(chrt *chart.Chart, vals *chart.Config, overrides map[string]interface{}) (map[string]string, error) {
var cvals chartutil.Values
// Parse values if not nil. We merge these at the top level because
@ -69,6 +69,12 @@ func (e *Engine) Render(chrt *chart.Chart, vals *chart.Config) (map[string]strin
if err != nil {
return map[string]string{}, err
}
// Override the top-level values. Overrides are NEVER merged deeply.
// The assumption is that an override is intended to set an explicit
// and exact value.
for k, v := range overrides {
evals[k] = v
}
cvals = coalesceValues(chrt, evals)
}

View File

@ -22,7 +22,38 @@ func TestEngine(t *testing.T) {
}
func TestRender(t *testing.T) {
t.Skip()
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "moby",
Version: "1.2.3",
},
Templates: []*chart.Template{
{Name: "test1", Data: []byte("{{.outer | title }} {{.inner | title}}")},
},
Values: &chart.Config{
Raw: `outer = "DEFAULT"\ninner= "DEFAULT"\n`,
},
}
vals := &chart.Config{
Raw: `outer = "BAD"
inner= "inn"`,
}
overrides := map[string]interface{}{
"outer": "spouter",
}
e := New()
out, err := e.Render(c, vals, overrides)
if err != nil {
t.Errorf("Failed to render templates: %s", err)
}
expect := "Spouter Inn"
if out["test1"] != expect {
t.Errorf("Expected %q, got %q", expect, out["test1"])
}
}
func TestRenderInternals(t *testing.T) {
@ -129,7 +160,7 @@ func TestRenderDependency(t *testing.T) {
},
}
out, err := e.Render(ch, nil)
out, err := e.Render(ch, nil, map[string]interface{}{})
if err != nil {
t.Fatalf("failed to render chart: %s", err)
@ -190,7 +221,7 @@ func TestRenderNestedValues(t *testing.T) {
what = "flower"`,
}
out, err := e.Render(outer, &inject)
out, err := e.Render(outer, &inject, map[string]interface{}{})
if err != nil {
t.Fatalf("failed to render templates: %s", err)
}

View File

@ -71,7 +71,7 @@ func UninstallRelease(name string) (*services.UninstallReleaseResponse, error) {
}
// InstallRelease installs a new chart and returns the release response.
func InstallRelease(chStr string) (*services.InstallReleaseResponse, error) {
func InstallRelease(chStr string, dryRun bool) (*services.InstallReleaseResponse, error) {
chfi, err := chartutil.LoadChart(chStr)
if err != nil {
return nil, err
@ -90,5 +90,6 @@ func InstallRelease(chStr string) (*services.InstallReleaseResponse, error) {
return Config.client().install(&services.InstallReleaseRequest{
Chart: chpb,
Values: vals,
DryRun: dryRun,
})
}

View File

@ -17,10 +17,16 @@ var _ = math.Inf
type Status_Code int32
const (
Status_UNKNOWN Status_Code = 0
Status_DEPLOYED Status_Code = 1
Status_DELETED Status_Code = 2
// Status_UNKNOWN indicates that a release is in an uncertain state.
Status_UNKNOWN Status_Code = 0
// Status_DEPLOYED indicates that the release has been pushed to Kubernetes.
Status_DEPLOYED Status_Code = 1
// Status_DELETED indicates that a release has been deleted from Kubermetes.
Status_DELETED Status_Code = 2
// Status_SUPERSEDED indicates that this release object is outdated and a newer one exists.
Status_SUPERSEDED Status_Code = 3
// Status_FAILED indicates that the release was not successfully deployed.
Status_FAILED Status_Code = 4
)
var Status_Code_name = map[int32]string{
@ -28,12 +34,14 @@ var Status_Code_name = map[int32]string{
1: "DEPLOYED",
2: "DELETED",
3: "SUPERSEDED",
4: "FAILED",
}
var Status_Code_value = map[string]int32{
"UNKNOWN": 0,
"DEPLOYED": 1,
"DELETED": 2,
"SUPERSEDED": 3,
"FAILED": 4,
}
func (x Status_Code) String() string {
@ -68,19 +76,20 @@ func init() {
}
var fileDescriptor2 = []byte{
// 215 bytes of a gzipped FileDescriptorProto
// 226 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8,
0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0x2f, 0x2e, 0x49, 0x2c, 0x29, 0x2d, 0xd6,
0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x01, 0x49, 0xe9, 0x41, 0xa5, 0xa4, 0x24, 0xd3, 0xf3,
0xf3, 0xd3, 0x73, 0x52, 0xf5, 0xc1, 0x72, 0x49, 0xa5, 0x69, 0xfa, 0x89, 0x79, 0x95, 0x10, 0x85,
0x4a, 0xcb, 0x19, 0xb9, 0xd8, 0x82, 0xc1, 0x3a, 0x85, 0x74, 0xb9, 0x58, 0x92, 0xf3, 0x53, 0x52,
0x4a, 0x9b, 0x19, 0xb9, 0xd8, 0x82, 0xc1, 0x3a, 0x85, 0x74, 0xb9, 0x58, 0x92, 0xf3, 0x53, 0x52,
0x25, 0x18, 0x15, 0x18, 0x35, 0xf8, 0x8c, 0x24, 0xf5, 0x90, 0x8d, 0xd0, 0x83, 0xa8, 0xd1, 0x73,
0x06, 0x2a, 0x08, 0x02, 0x2b, 0x13, 0xd2, 0xe3, 0x62, 0x4f, 0x49, 0x2d, 0x49, 0xcc, 0xcc, 0x29,
0x96, 0x60, 0x02, 0xea, 0xe0, 0x36, 0x12, 0xd1, 0x83, 0x58, 0xa3, 0x07, 0xb3, 0x46, 0xcf, 0x31,
0xaf, 0x32, 0x08, 0xa6, 0x48, 0xc9, 0x8e, 0x8b, 0x05, 0xa4, 0x5b, 0x88, 0x9b, 0x8b, 0x3d, 0xd4,
0xaf, 0x32, 0x08, 0xa6, 0x48, 0xc9, 0x8b, 0x8b, 0x05, 0xa4, 0x5b, 0x88, 0x9b, 0x8b, 0x3d, 0xd4,
0xcf, 0xdb, 0xcf, 0x3f, 0xdc, 0x4f, 0x80, 0x41, 0x88, 0x87, 0x8b, 0xc3, 0xc5, 0x35, 0xc0, 0xc7,
0x3f, 0xd2, 0xd5, 0x45, 0x80, 0x11, 0x24, 0xe5, 0xe2, 0xea, 0xe3, 0x1a, 0x02, 0xe4, 0x30, 0x09,
0xf1, 0x71, 0x71, 0x05, 0x87, 0x06, 0xb8, 0x06, 0x05, 0xbb, 0xba, 0x00, 0xf9, 0xcc, 0x4e, 0x9c,
0x51, 0xec, 0x50, 0xc7, 0x24, 0xb1, 0x81, 0x6d, 0x30, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x0d,
0xcd, 0xe7, 0x6f, 0x01, 0x01, 0x00, 0x00,
0xf1, 0x71, 0x71, 0x05, 0x87, 0x06, 0xb8, 0x06, 0x05, 0xbb, 0xba, 0x00, 0xf9, 0xcc, 0x42, 0x5c,
0x5c, 0x6c, 0x6e, 0x8e, 0x9e, 0x3e, 0x40, 0x36, 0x8b, 0x13, 0x67, 0x14, 0x3b, 0xd4, 0x61, 0x49,
0x6c, 0x60, 0xdb, 0x8c, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0x8c, 0x99, 0x9a, 0x3b, 0x0d, 0x01,
0x00, 0x00,
}