diff --git a/_proto/hapi/release/status.proto b/_proto/hapi/release/status.proto index 9ec021005..c75033e8a 100644 --- a/_proto/hapi/release/status.proto +++ b/_proto/hapi/release/status.proto @@ -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; diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 3f7d701f1..1c878b0ea 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -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) } diff --git a/cmd/tiller/environment/environment.go b/cmd/tiller/environment/environment.go index e9140bf1f..34b39fed2 100644 --- a/cmd/tiller/environment/environment.go +++ b/cmd/tiller/environment/environment.go @@ -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. + // + // namespace must contain a valid existing namespace. // - // TODO: Can these be in YAML or JSON, or must they be in one particular - // format? - Install(manifests map[string]string) error + // 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}, } } diff --git a/cmd/tiller/environment/environment_test.go b/cmd/tiller/environment/environment_test.go index abdb015bc..78e7b8cda 100644 --- a/cmd/tiller/environment/environment_test.go +++ b/cmd/tiller/environment/environment_test.go @@ -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", + } + + b := bytes.NewBuffer(nil) + for _, content := range manifests { + b.WriteString("\n---\n") + b.WriteString(content) + } - if err := env.KubeClient.Install(manifests); err != nil { + if err := env.KubeClient.Create("sharry-bobbins", b); err != nil { t.Errorf("Kubeclient failed: %s", err) } } diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 2333b3c2b..aee3da927 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -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.Releases.Create(r); err != nil { - return nil, err + 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) } - return &services.InstallReleaseResponse{Release: r}, nil + // 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 { + log.Printf("warning: Failed to record release %q: %s", name, err) + } + return res, nil } func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) { diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index f76e3d91e..61b5935bf 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -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 } diff --git a/docs/examples/alpine/templates/alpine-pod.yaml b/docs/examples/alpine/templates/alpine-pod.yaml index 000f106e3..81e712723 100644 --- a/docs/examples/alpine/templates/alpine-pod.yaml +++ b/docs/examples/alpine/templates/alpine-pod.yaml @@ -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: diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index cabd40656..f185c752a 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -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) } diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 305c44769..4783bbd88 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -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) } diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 4b366e85f..85d65b5d7 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -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, }) } diff --git a/pkg/proto/hapi/release/status.pb.go b/pkg/proto/hapi/release/status.pb.go index 178402c8e..51ed1bf86 100644 --- a/pkg/proto/hapi/release/status.pb.go +++ b/pkg/proto/hapi/release/status.pb.go @@ -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, }