diff --git a/cmd/deploy.go b/cmd/deploy.go new file mode 100644 index 000000000..8126ca4e4 --- /dev/null +++ b/cmd/deploy.go @@ -0,0 +1,24 @@ +package main + +import ( + "errors" + + dep "github.com/deis/helm-dm/deploy" + "github.com/deis/helm-dm/format" +) + +func deploy(cfg *dep.Deployment, dry bool) error { + if dry { + format.Error("Not implemented: --dry-run") + } + 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.Commit(); err != nil { + format.Error("Failed to commit deployment: %s", err) + return err + } + + return nil +} diff --git a/cmd/helm.go b/cmd/helm.go index d69ebd667..d0edde4bb 100644 --- a/cmd/helm.go +++ b/cmd/helm.go @@ -4,6 +4,7 @@ import ( "os" "github.com/codegangsta/cli" + dep "github.com/deis/helm-dm/deploy" "github.com/deis/helm-dm/format" ) @@ -22,7 +23,7 @@ func main() { func commands() []cli.Command { return []cli.Command{ { - Name: "install", + Name: "init", Usage: "Initialize the client and install DM on Kubernetes.", Description: ``, Flags: []cli.Flag{ @@ -33,7 +34,7 @@ func commands() []cli.Command { }, Action: func(c *cli.Context) { if err := install(c.Bool("dry-run")); err != nil { - format.Error(err.Error()) + format.Error("%s (Run 'helm doctor' for more information)", err) os.Exit(1) } }, @@ -44,7 +45,7 @@ func commands() []cli.Command { ArgsUsage: "", Action: func(c *cli.Context) { if err := target(c.Bool("dry-run")); err != nil { - format.Error(err.Error()) + format.Error("%s (Is the cluster running?)", err) os.Exit(1) } }, @@ -59,7 +60,66 @@ func commands() []cli.Command { Name: "doctor", }, { - Name: "deploy", + 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.Bool("dry-run")); err != nil { + format.Error("%s (Try running 'helm doctor')", err) + os.Exit(1) + } + }, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "dry-run", + Usage: "Only display the underlying kubectl commands.", + }, + cli.BoolFlag{ + Name: "stdin,i", + Usage: "Read a configuration from STDIN.", + }, + cli.StringFlag{ + Name: "name", + Usage: "Name of deployment, used for deploy and update commands (defaults to template name)", + }, + // TODO: I think there is a Generic flag type that we can implement parsing with. + cli.StringFlag{ + Name: "properties,p", + Usage: "A comma-separated list of key=value pairs: 'foo=bar,foo2=baz'.", + }, + cli.StringFlag{ + // FIXME: This is not right. It's sort of a half-baked forward + // port of dm.go. + Name: "repository", + Usage: "The default repository", + Value: "kubernetes/application-dm-templates", + }, + }, }, { Name: "search", diff --git a/cmd/properties.go b/cmd/properties.go new file mode 100644 index 000000000..9632c95f6 --- /dev/null +++ b/cmd/properties.go @@ -0,0 +1,40 @@ +package main + +import ( + "errors" + "strconv" + "strings" +) + +// TODO: The concept of property here is really simple. We could definitely get +// better about the values we allow. Also, we need some validation on the names. + +var errInvalidProperty = errors.New("property is not in name=value format") + +// parseProperties is a utility for parsing a comma-separated key=value string. +func parseProperties(kvstr string) (map[string]interface{}, error) { + properties := map[string]interface{}{} + + if len(kvstr) == 0 { + return properties, nil + } + + pairs := strings.Split(kvstr, ",") + for _, p := range pairs { + // Allow for "k=v, k=v" + p = strings.TrimSpace(p) + pair := strings.Split(p, "=") + if len(pair) == 1 { + return properties, errInvalidProperty + } + + // If the value looks int-like, convert it. + if i, err := strconv.Atoi(pair[1]); err == nil { + properties[pair[0]] = pair[1] + } else { + properties[pair[0]] = i + } + } + + return properties, nil +} diff --git a/deploy/deploy.go b/deploy/deploy.go new file mode 100644 index 000000000..038c7eafa --- /dev/null +++ b/deploy/deploy.go @@ -0,0 +1,148 @@ +package deploy + +import ( + "archive/tar" + "errors" + "fmt" + "os" + "strings" + + "github.com/ghodss/yaml" + "github.com/kubernetes/deployment-manager/common" + "github.com/kubernetes/deployment-manager/expandybird/expander" + "github.com/kubernetes/deployment-manager/registry" +) + +type Deployer interface { + Commit() error +} + +// Deployment describes a deployment of a package. +type Deployment struct { + // Name is the Deployment name. Autogenerated if empty. + Name string + // Filename is the filename for the base deployment. + Filename string + // Imports is a list of imported files. + Imports []string + // Properties to pass into the template. + Properties map[string]interface{} + // Input is a file containing templates. It may be os.Stdin. + Input *os.File + // Repository is the location of the templates. + Repository string +} + +// Commit prepares the Deployment and then commits it to the remote processor. +func (d *Deployment) Commit() error { + tpl, err := d.resolveTemplate() + if err != nil { + return err + } + + // If a deployment Name is specified, set that explicitly. + if d.Name != "" { + tpl.Name = d.Name + } + + 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. + if d.Input != nil { + // Assume this is a tar archive. + tpl, err := expander.NewTemplateFromArchive(d.Filename, d.Input, d.Imports) + if err == nil { + return tpl, err + } else if err != tar.ErrHeader { + return nil, err + } + + // If we get here, the file is not a tar archive. + if _, err := os.Stdin.Seek(0, 0); err != nil { + return nil, err + } + return expander.NewTemplateFromReader(d.Filename, d.Input, d.Imports) + } + + // Non-Stdin case + if len(d.Imports) > 0 { + if t, err := registryType(d.Filename); err != nil { + return expander.NewTemplateFromRootTemplate(d.Filename) + } else { + return buildTemplateFromType(t, d.Repository, d.Properties) + } + } + return expander.NewTemplateFromFileNames(d.Filename, d.Imports) +} + +// registryType is a placeholder until registry.ParseType() is merged. +func registryType(name string) (*registry.Type, error) { + tList := strings.Split(name, ":") + if len(tList) != 2 { + return nil, errors.New("No version") + } + + tt := registry.Type{Version: tList[1]} + + cList := strings.Split(tList[0], "/") + + if len(cList) == 1 { + tt.Name = tList[0] + } else { + tt.Collection = cList[0] + tt.Name = cList[1] + } + return &tt, nil +} + +// buildTemplateFromType is a straight lift-n-shift from dm.go. +func buildTemplateFromType(t *registry.Type, reg string, props map[string]interface{}) (*common.Template, error) { + // Name the deployment after the type name. + name := fmt.Sprintf("%s:%s", t.Name, t.Version) + git, err := getGitRegistry(reg) + if err != nil { + return nil, err + } + gurls, err := git.GetURLs(*t) + if err != nil { + return nil, err + } + + config := common.Configuration{Resources: []*common.Resource{&common.Resource{ + Name: name, + Type: gurls[0], + Properties: props, + }}} + + y, err := yaml.Marshal(config) + if err != nil { + return nil, fmt.Errorf("error: %s\ncannot create configuration for deployment: %v\n", err, config) + } + + return &common.Template{ + Name: name, + Content: string(y), + // No imports, as this is a single type from repository. + }, nil +} + +func getGitRegistry(reg string) (registry.Registry, error) { + s := strings.SplitN(reg, "/", 3) + if len(s) < 2 { + return nil, fmt.Errorf("invalid template registry: %s", reg) + } + + path := "" + if len(s) > 2 { + path = s[3] + } + + if s[0] == "helm" { + return registry.NewGithubPackageRegistry(s[0], s[1]), nil + } else { + return registry.NewGithubRegistry(s[0], s[1], path), nil + } +}