diff --git a/Makefile b/Makefile index 94307ab20..badddd90c 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ endif BIN_DIR := bin DIST_DIR := _dist -GO_PACKAGES := cmd/helm dm +GO_PACKAGES := cmd/helm dm deploy format kubectl MAIN_GO := github.com/deis/helm-dm/cmd/helm HELM_BIN := helm-dm PATH_WITH_HELM = PATH="$(shell pwd)/$(BIN_DIR)/helm:$(PATH)" @@ -47,7 +47,7 @@ quicktest: $(PATH_WITH_HELM) go test -short $(addprefix ./,$(GO_PACKAGES)) test: test-style - $(PATH_WITH_HELM) go test -v ./ $(addprefix ./,$(GO_PACKAGES)) + $(PATH_WITH_HELM) go test -v $(addprefix ./,$(GO_PACKAGES)) test-style: @if [ $(shell gofmt -e -l -s *.go $(GO_PACKAGES)) ]; then \ diff --git a/cmd/helm/create.go b/cmd/helm/create.go new file mode 100644 index 000000000..177f5e25b --- /dev/null +++ b/cmd/helm/create.go @@ -0,0 +1,24 @@ +package main + +import ( + "errors" + + "github.com/codegangsta/cli" + "github.com/kubernetes/deployment-manager/chart" +) + +func create(c *cli.Context) error { + args := c.Args() + if len(args) < 1 { + return errors.New("'helm create' requires a chart name as an argument") + } + + cf := &chart.Chartfile{ + Name: args[0], + Description: "Created by Helm", + Version: "0.1.0", + } + + _, err := chart.Create(cf, ".") + return err +} diff --git a/cmd/helm/deploy.go b/cmd/helm/deploy.go index 80faeea6d..12c5f2c97 100644 --- a/cmd/helm/deploy.go +++ b/cmd/helm/deploy.go @@ -1,38 +1,85 @@ package main import ( - "encoding/json" "errors" + "os" + "github.com/codegangsta/cli" dep "github.com/deis/helm-dm/deploy" "github.com/deis/helm-dm/format" + "github.com/kubernetes/deployment-manager/chart" ) -func deploy(cfg *dep.Deployment, host string, dry bool) error { +func deploy(c *cli.Context) error { + args := c.Args() + if len(args) < 1 { + format.Err("First argument, filename, is required. Try 'helm deploy --help'") + os.Exit(1) + } + + props, err := parseProperties(c.String("properties")) + if err != nil { + format.Err("Failed to parse properties: %s", err) + os.Exit(1) + } + + d := &dep.Deployment{ + Name: c.String("Name"), + Properties: props, + Filename: args[0], + Imports: args[1:], + Repository: c.String("repository"), + } + + if c.Bool("stdin") { + d.Input = os.Stdin + } + + return doDeploy(d, c.GlobalString("host"), c.Bool("dry-run")) +} + +func doDeploy(cfg *dep.Deployment, host string, dry bool) error { if cfg.Filename == "" { return errors.New("A filename must be specified. For a tar archive, this is the name of the root template in the archive.") } - if err := cfg.Prepare(); err != nil { - format.Error("Failed to prepare deployment: %s", err) + fi, err := os.Stat(cfg.Filename) + if err != nil { return err } - // For a dry run, print the template and exit. - if dry { - format.Info("Template prepared for %s", cfg.Template.Name) - data, err := json.MarshalIndent(cfg.Template, "", "\t") + if fi.IsDir() { + format.Info("Chart is directory") + c, err := chart.LoadDir(cfg.Filename) if err != nil { return err } - format.Msg(string(data)) - return nil + + //tdir, err := ioutil.TempDir("", "helm-") + //if err != nil { + //format.Warn("Could not create temporary directory. Using .") + //tdir = "." + //} else { + //defer os.RemoveAll(tdir) + //} + tdir := "." + tfile, err := chart.Save(c, tdir) + if err != nil { + return err + } + cfg.Filename = tfile + } - if err := cfg.Commit(host); err != nil { - format.Error("Failed to commit deployment: %s", err) - return err + if !dry { + if err := uploadTar(cfg.Filename); err != nil { + return err + } } return nil } + +func uploadTar(filename string) error { + return nil +} diff --git a/cmd/helm/dm.go b/cmd/helm/dm.go index a1e60e3a5..b331e62c7 100644 --- a/cmd/helm/dm.go +++ b/cmd/helm/dm.go @@ -8,14 +8,15 @@ import ( "github.com/deis/helm-dm/kubectl" ) -var ErrAlreadyInstalled error = errors.New("Already Installed") +// ErrAlreadyInstalled indicates that DM is already installed. +var ErrAlreadyInstalled = errors.New("Already Installed") func install(dryRun bool) error { runner := getKubectlRunner(dryRun) out, err := dm.Install(runner) if err != nil { - format.Error("Error installing: %s %s", out, err) + format.Err("Error installing: %s %s", out, err) } format.Msg(out) return nil @@ -26,7 +27,7 @@ func uninstall(dryRun bool) error { out, err := dm.Uninstall(runner) if err != nil { - format.Error("Error uninstalling: %s %s", out, err) + format.Err("Error uninstalling: %s %s", out, err) } format.Msg(out) return nil diff --git a/cmd/helm/doctor.go b/cmd/helm/doctor.go index ebf66099a..15f068a25 100644 --- a/cmd/helm/doctor.go +++ b/cmd/helm/doctor.go @@ -2,6 +2,7 @@ package main import ( "github.com/deis/helm-dm/dm" + "github.com/deis/helm-dm/format" "github.com/deis/helm-dm/kubectl" ) diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index bbae7554b..a08514c2a 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -4,7 +4,6 @@ import ( "os" "github.com/codegangsta/cli" - dep "github.com/deis/helm-dm/deploy" "github.com/deis/helm-dm/format" ) @@ -48,7 +47,7 @@ func commands() []cli.Command { }, Action: func(c *cli.Context) { if err := install(c.Bool("dry-run")); err != nil { - format.Error("%s (Run 'helm doctor' for more information)", err) + format.Err("%s (Run 'helm doctor' for more information)", err) os.Exit(1) } }, @@ -65,7 +64,7 @@ func commands() []cli.Command { }, Action: func(c *cli.Context) { if err := uninstall(c.Bool("dry-run")); err != nil { - format.Error("%s (Run 'helm doctor' for more information)", err) + format.Err("%s (Run 'helm doctor' for more information)", err) os.Exit(1) } }, @@ -74,7 +73,7 @@ func commands() []cli.Command { Name: "status", Usage: "Show status of DM.", Action: func(c *cli.Context) { - format.Error("Not yet implemented") + format.Err("Not yet implemented") os.Exit(1) }, }, @@ -84,7 +83,7 @@ func commands() []cli.Command { ArgsUsage: "", Action: func(c *cli.Context) { if err := target(c.Bool("dry-run")); err != nil { - format.Error("%s (Is the cluster running?)", err) + format.Err("%s (Is the cluster running?)", err) os.Exit(1) } }, @@ -109,7 +108,7 @@ func commands() []cli.Command { }, Action: func(c *cli.Context) { if err := install(c.Bool("dry-run")); err != nil { - format.Error("%s (Run 'helm doctor' for more information)", err) + format.Err("%s (Run 'helm doctor' for more information)", err) os.Exit(1) } }, @@ -120,46 +119,27 @@ func commands() []cli.Command { ArgsUsage: "", Action: func(c *cli.Context) { if err := doctor(); err != nil { - format.Error("%s", err) + format.Err("%s", err) os.Exit(1) } }, }, + { + Name: "create", + Usage: "Create a new local chart for editing.", + Action: func(c *cli.Context) { run(c, create) }, + }, + { + Name: "package", + Aliases: []string{"pack"}, + Usage: "Given a chart directory, package it into a release.", + Action: func(c *cli.Context) { run(c, pack) }, + }, { Name: "deploy", Aliases: []string{"install"}, Usage: "Deploy a chart into the cluster.", - Action: func(c *cli.Context) { - - args := c.Args() - if len(args) < 1 { - format.Error("First argument, filename, is required. Try 'helm deploy --help'") - os.Exit(1) - } - - props, err := parseProperties(c.String("properties")) - if err != nil { - format.Error("Failed to parse properties: %s", err) - os.Exit(1) - } - - d := &dep.Deployment{ - Name: c.String("Name"), - Properties: props, - Filename: args[0], - Imports: args[1:], - Repository: c.String("repository"), - } - - if c.Bool("stdin") { - d.Input = os.Stdin - } - - if err := deploy(d, c.GlobalString("host"), c.Bool("dry-run")); err != nil { - format.Error("%s (Try running 'helm doctor')", err) - os.Exit(1) - } - }, + Action: func(c *cli.Context) { run(c, deploy) }, Flags: []cli.Flag{ cli.BoolFlag{ Name: "dry-run", @@ -193,3 +173,10 @@ func commands() []cli.Command { listCmd(), } } + +func run(c *cli.Context, f func(c *cli.Context) error) { + if err := f(c); err != nil { + os.Stderr.Write([]byte(err.Error())) + os.Exit(1) + } +} diff --git a/cmd/helm/list.go b/cmd/helm/list.go index b5eb7446d..eadb25757 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -14,7 +14,7 @@ func listCmd() cli.Command { Usage: "Lists the deployments in the cluster", Action: func(c *cli.Context) { if err := list(c.GlobalString("host")); err != nil { - format.Error("%s (Is the cluster running?)", err) + format.Err("%s (Is the cluster running?)", err) os.Exit(1) } }, diff --git a/cmd/helm/pack.go b/cmd/helm/pack.go new file mode 100644 index 000000000..04ab391cf --- /dev/null +++ b/cmd/helm/pack.go @@ -0,0 +1,34 @@ +package main + +import ( + "errors" + "fmt" + "os" + + "github.com/codegangsta/cli" + "github.com/deis/helm-dm/format" + "github.com/kubernetes/deployment-manager/chart" +) + +func pack(cxt *cli.Context) error { + args := cxt.Args() + if len(args) < 1 { + return errors.New("'helm package' requires a path to a chart directory as an argument") + } + + dir := args[0] + if fi, err := os.Stat(dir); err != nil { + return fmt.Errorf("Could not find directory %s: %s", dir, err) + } else if !fi.IsDir() { + return fmt.Errorf("Not a directory: %s", dir) + } + + c, err := chart.LoadDir(dir) + if err != nil { + return fmt.Errorf("Failed to load %s: %s", dir, err) + } + + fname, err := chart.Save(c, ".") + format.Msg(fname) + return nil +} diff --git a/deploy/deploy.go b/deploy/deploy.go index 1ba09e394..41a9188d9 100644 --- a/deploy/deploy.go +++ b/deploy/deploy.go @@ -1,16 +1,17 @@ package deploy import ( - "archive/tar" - "errors" - "fmt" + //"archive/tar" + //"errors" + //"fmt" "os" - "strings" + //"strings" - "github.com/ghodss/yaml" + //"github.com/ghodss/yaml" + "github.com/kubernetes/deployment-manager/chart" "github.com/kubernetes/deployment-manager/common" - "github.com/kubernetes/deployment-manager/expandybird/expander" - "github.com/kubernetes/deployment-manager/registry" + //"github.com/kubernetes/deployment-manager/expandybird/expander" + //"github.com/kubernetes/deployment-manager/registry" ) // Deployer is capable of deploying an object to a back-end. @@ -41,6 +42,8 @@ type Deployment struct { // The template, typically generated by the Deployment. Template *common.Template + + lchart *chart.Chart } // Prepare loads templates and checks for client-side errors. @@ -48,26 +51,44 @@ type Deployment struct { // This will generate the Template based on other information. func (d *Deployment) Prepare() error { - tpl, err := d.resolveTemplate() + // Is Filename a local dir, a local file, or a remote URL? + fi, err := os.Stat(d.Filename) if err != nil { return err } - // If a deployment Name is specified, set that explicitly. - if d.Name != "" { - tpl.Name = d.Name + var c *chart.Chart + if fi.IsDir() { + c, err = chart.LoadDir(d.Filename) + if err != nil { + return err + } + } else { + c, err = chart.Load(d.Filename) + if err != nil { + return err + } } - d.Template = tpl + // Override name if we need to + + // Properties + d.lchart = c return nil } +// Chart retrieves the chart from teh deployment. +func (d *Deployment) Chart() *chart.Chart { + return d.lchart +} + // Commit prepares the Deployment and then commits it to the remote processor. func (d *Deployment) Commit(host string) error { return nil } +/* // resolveTemplate resolves what kind of template is being loaded, and then returns the template. func (d *Deployment) resolveTemplate() (*common.Template, error) { // If some input has been specified, read it. @@ -179,3 +200,4 @@ func getGitRegistry(reg string) (registry.Registry, error) { return r, nil } } +*/ diff --git a/dm/client.go b/dm/client.go index 27a01c203..301d1f564 100644 --- a/dm/client.go +++ b/dm/client.go @@ -6,13 +6,15 @@ import ( "io" "io/ioutil" "net/http" + "os" + "path/filepath" "time" "github.com/ghodss/yaml" ) // The default HTTP timeout -var DefaultHTTPTimeout time.Duration = time.Second * 10 +var DefaultHTTPTimeout = time.Second * 10 // Client is a DM client. type Client struct { @@ -48,7 +50,7 @@ func (c *Client) url(path string) string { func (c *Client) CallService(path, method, action string, dest interface{}, reader io.ReadCloser) error { u := c.url(path) - resp, err := c.callHttp(u, method, action, reader) + resp, err := c.callHTTP(u, method, action, reader) if err != nil { return err } @@ -67,8 +69,8 @@ func (c *Client) CallService(path, method, action string, dest interface{}, read return nil } -// callHttp is a low-level primative for executing HTTP operations. -func (c *Client) callHttp(path, method, action string, reader io.ReadCloser) (string, error) { +// callHTTP is a low-level primative for executing HTTP operations. +func (c *Client) callHTTP(path, method, action string, reader io.ReadCloser) (string, error) { request, err := http.NewRequest(method, path, reader) request.Header.Add("Content-Type", "application/json") @@ -97,6 +99,7 @@ func (c *Client) callHttp(path, method, action string, reader io.ReadCloser) (st return string(body), nil } +// ListDeployments lists the deployments in DM. func (c *Client) ListDeployments() error { var d interface{} if err := c.CallService("deployments", "GET", "foo", &d, nil); err != nil { @@ -106,3 +109,49 @@ func (c *Client) ListDeployments() error { fmt.Printf("%#v\n", d) return nil } + +// DeployChart sends a chart to DM for deploying. +func (c *Client) DeployChart(filename, deployname string) error { + f, err := os.Open(filename) + if err != nil { + return err + } + defer f.Close() + + request, err := http.NewRequest("POST", "/v2/deployments/", f) + + // There is an argument to be made for using the legacy x-octet-stream for + // this. But since we control both sides, we should use the standard one. + // Also, gzip (x-compress) is usually treated as a content encoding. In this + // case it probably is not, but it makes more sense to follow the standard, + // even though we don't assume the remote server will strip it off. + request.Header.Add("Content-Type", "application/x-tar") + request.Header.Add("Content-Encoding", "gzip") + request.Header.Add("X-Deployment-Name", deployname) + request.Header.Add("X-Chart-Name", filepath.Base(filename)) + + client := http.Client{ + Timeout: time.Duration(time.Duration(DefaultHTTPTimeout) * time.Second), + Transport: c.Transport, + } + + response, err := client.Do(request) + if err != nil { + return err + } + + body, err := ioutil.ReadAll(response.Body) + response.Body.Close() + if err != nil { + return err + } + + // FIXME: We only want 200 OK or 204(?) CREATED + if response.StatusCode < http.StatusOK || + response.StatusCode >= http.StatusMultipleChoices { + message := fmt.Sprintf("status code: %d status: %s : %s", response.StatusCode, response.Status, body) + return fmt.Errorf("Failed to post: %s", message) + } + + return nil +} diff --git a/dm/install.go b/dm/install.go index ad2aabb75..f2a70163d 100644 --- a/dm/install.go +++ b/dm/install.go @@ -20,7 +20,7 @@ func IsInstalled(runner kubectl.Runner) bool { // we know that we have both the namespace and the manager API server. out, err := runner.GetByKind("rc", "manager-rc", "dm") if err != nil { - format.Error("Installation not found: %s %s", out, err) + format.Err("Installation not found: %s %s", out, err) return false } return true diff --git a/dm/transport.go b/dm/transport.go index 588f7f96d..345cbfa61 100644 --- a/dm/transport.go +++ b/dm/transport.go @@ -15,6 +15,7 @@ type debugTransport struct { http.RoundTripper } +// NewDebugTransport returns a debugging implementation of a RoundTripper. func NewDebugTransport(rt http.RoundTripper) http.RoundTripper { return debugTransport{ RoundTripper: rt, diff --git a/dm/uninstall.go b/dm/uninstall.go index 277ff1c10..675ebdb73 100644 --- a/dm/uninstall.go +++ b/dm/uninstall.go @@ -4,7 +4,7 @@ import ( "github.com/deis/helm-dm/kubectl" ) -// uninstall uses kubectl to uninstall the base DM. +// Uninstall uses kubectl to uninstall the base DM. // // Returns the string output received from the operation, and an error if the // command failed. diff --git a/format/messages.go b/format/messages.go index dcf26736a..b1c63d510 100644 --- a/format/messages.go +++ b/format/messages.go @@ -7,25 +7,30 @@ import ( // This is all just placeholder. -func Error(msg string, v ...interface{}) { +// Err prints an error message to Stderr. +func Err(msg string, v ...interface{}) { msg = "[ERROR] " + msg + "\n" fmt.Fprintf(os.Stderr, msg, v...) } +// Info prints an informational message to Stdout. func Info(msg string, v ...interface{}) { msg = "[INFO] " + msg + "\n" fmt.Fprintf(os.Stdout, msg, v...) } +// Msg prints a raw message to Stdout. func Msg(msg string, v ...interface{}) { fmt.Fprintf(os.Stdout, msg, v...) } +// Success is an achievement marked by pretty output. func Success(msg string, v ...interface{}) { msg = "[Success] " + msg + "\n" fmt.Fprintf(os.Stdout, msg, v...) } +// Warning emits a warning message. func Warning(msg string, v ...interface{}) { msg = "[Warning] " + msg + "\n" fmt.Fprintf(os.Stdout, msg, v...) diff --git a/glide.lock b/glide.lock index ddd774cc4..d34eae154 100644 --- a/glide.lock +++ b/glide.lock @@ -1,53 +1,56 @@ -hash: 4cc1aba06a344d43c0c1005d71dc0659ada5d90f0b2235b1d8e8c7352d1251a7 -updated: 2016-01-06T14:30:55.041267875-08:00 +hash: fce0581223b80f7a04fbb4ad4bd7ff8fa3d12e879dba894ba448770933731887 +updated: 2016-02-02T17:30:13.283644703-07:00 imports: - name: github.com/codegangsta/cli - version: c31a7975863e7810c92e2e288a9ab074f9a88f29 + version: cf1f63a7274872768d4037305d572b70b1199397 - name: github.com/emicklei/go-restful - version: ce94a9f819d7dd2b5599ff0c017b1124595a64fb + version: b86acf97a74ed7603ac78d012f5535b4d587b156 - name: github.com/ghodss/yaml version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee - name: github.com/golang/glog - version: fca8c8854093a154ff1eb580aae10276ad6b1b5f + version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998 - name: github.com/golang/protobuf - version: 2402d76f3d41f928c7902a765dfc872356dd3aad + version: 45bba206dd5270d96bac4942dcfe515726613249 - name: github.com/google/go-github - version: 63fbbb283ce4913a5ac1b6de7abae50dbf594a04 + version: b8b4ac742977310ff6e75140a403a38dab109977 + subpackages: + - /github - name: github.com/google/go-querystring version: 2a60fc2ba6c19de80291203597d752e9ba58e4c0 - name: github.com/gorilla/context version: 1c83b3eabd45b6d76072b66b746c20815fb2872d - name: github.com/gorilla/handlers - version: 1af6d56d7cd39d982856bc0cee11142baf392c52 + version: b3aff83722cb2ae031a70cae984650e3a16cd20e - name: github.com/gorilla/mux version: 26a6070f849969ba72b72256e9f14cf519751690 - name: github.com/gorilla/schema version: 14c555599c2a4f493c1e13fd1ea6fdf721739028 - name: github.com/kubernetes/deployment-manager - version: 62f19486073edd020a11922304130f0c5c1dff20 + version: "" + repo: https://github.com/technosophos/deployment-manager + vcs: git subpackages: - /common +- name: github.com/Masterminds/semver + version: c4f7ef0702f269161a60489ccbbc9f1241ad1265 - name: github.com/mjibson/appstats version: 0542d5f0e87ea3a8fa4174322b9532f5d04f9fa8 - name: golang.org/x/crypto - version: 552e9d568fde9701ea1944fb01c8aadaceaa7353 + version: 1f22c0103821b9390939b6776727195525381532 - name: golang.org/x/net - version: 1ade16a5450925b7496e1031938175d1f5d30d31 + version: 6c581b96a7d38dd755f986fcf4f29665597694c0 - name: golang.org/x/oauth2 - version: 2baa8a1b9338cf13d9eeb27696d761155fa480be + version: 8a57ed94ffd43444c0879fe75701732a38afc985 - name: golang.org/x/text - version: cf4986612c83df6c55578ba198316d1684a9a287 -- name: google.golang.com/appengine - version: "" - repo: https://google.golang.com/appengine + version: 5aaa1a807bf8a2f763540b140e7805973476eb88 - name: google.golang.org/api - version: f5b7ec483f357a211c03c6722a840444c2d395dc + version: 8fa1015948e6fc21c025050624e4c4e2f4f405c4 - name: google.golang.org/appengine - version: 54bf9150c922186bfc45a00bf9dfcb91a5063275 + version: 6bde959377a90acb53366051d7d587bfd7171354 - name: google.golang.org/cloud - version: 1bff51b8fae8d33cb3dab8f7858c266ce001ee3e + version: 5a3b06f8b5da3b7c3a93da43163b872c86c509ef - name: google.golang.org/grpc - version: 78905999da08d7f87d5dd11608fa79ff8700daa8 + version: 5d64098b94ee9dbbea8ddc130208696bcd199ba4 - name: gopkg.in/yaml.v2 version: f7716cbe52baa25d2e9b0d0da546fcf909fc16b4 devImports: [] diff --git a/glide.yaml b/glide.yaml index 35922b0fc..dd229737b 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,7 +1,13 @@ package: github.com/deis/helm-dm +ignore: +- google.golang.com/appengine import: - package: github.com/codegangsta/cli - package: github.com/kubernetes/deployment-manager + version: feat/chartfile + repo: https://github.com/technosophos/deployment-manager + vcs: git subpackages: - /common - package: github.com/ghodss/yaml +- package: github.com/Masterminds/semver diff --git a/kubectl/get.go b/kubectl/get.go index 6bdc0e433..d80dd385d 100644 --- a/kubectl/get.go +++ b/kubectl/get.go @@ -13,6 +13,7 @@ func (r RealRunner) Get(stdin []byte, ns string) ([]byte, error) { return cmd.CombinedOutput() } +// GetByKind gets a named thing by kind. func (r RealRunner) GetByKind(kind, name, ns string) (string, error) { args := []string{"get", kind, name} @@ -37,6 +38,7 @@ func (r PrintRunner) Get(stdin []byte, ns string) ([]byte, error) { return []byte(cmd.String()), nil } +// GetByKind gets a named thing by kind. func (r PrintRunner) GetByKind(kind, name, ns string) (string, error) { args := []string{"get", kind, name}