Commit 0bbde9ad authored by Matt Butcher's avatar Matt Butcher

Merge pull request #23 from technosophos/feat/deploy

Implement client side of `helm deploy`
parents a0acb3d5 436b7f03
......@@ -50,8 +50,8 @@ test: test-style
$(PATH_WITH_HELM) go test -v -cover $(addprefix ./,$(GO_PACKAGES))
test-style:
@if [ $(shell gofmt -e -l -s *.go $(GO_PACKAGES)) ]; then \
echo "gofmt check failed:"; gofmt -e -l -s *.go $(GO_PACKAGES); exit 1; \
@if [ $(shell gofmt -e -l -s $(GO_PACKAGES)) ]; then \
echo "gofmt check failed:"; gofmt -e -d -s $(GO_PACKAGES); exit 1; \
fi
@for i in . $(GO_PACKAGES); do \
golint $$i; \
......
package main
import (
"errors"
"fmt"
"os"
"regexp"
"strings"
"io/ioutil"
"github.com/aokoli/goutils"
"github.com/codegangsta/cli"
dep "github.com/deis/helm-dm/deploy"
"github.com/deis/helm-dm/format"
"github.com/kubernetes/deployment-manager/chart"
"github.com/kubernetes/deployment-manager/common"
"gopkg.in/yaml.v2"
)
func init() {
......@@ -29,9 +23,9 @@ func deployCmd() cli.Command {
Name: "dry-run",
Usage: "Only display the underlying kubectl commands.",
},
cli.BoolFlag{
Name: "stdin,i",
Usage: "Read a configuration from STDIN.",
cli.StringFlag{
Name: "config,c",
Usage: "The configuration YAML file for this deployment.",
},
cli.StringFlag{
Name: "name",
......@@ -42,110 +36,57 @@ func deployCmd() cli.Command {
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",
},
},
}
}
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 there is a configuration file, use it.
cfg := &common.Configuration{}
if c.String("config") != "" {
if err := loadConfig(cfg, c.String("config")); err != nil {
return err
}
} else {
cfg.Resources = []*common.Resource{
{
Properties: map[string]interface{}{},
},
}
}
if c.Bool("stdin") {
d.Input = os.Stdin
// If there is a chart specified on the commandline, override the config
// file with it.
args := c.Args()
if len(args) > 0 {
cfg.Resources[0].Type = args[0]
}
return doDeploy(d, c)
}
func doDeploy(cfg *dep.Deployment, cxt *cli.Context) 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.")
// Override the name if one is passed in.
if name := c.String("name"); len(name) > 0 {
cfg.Resources[0].Name = name
}
fi, err := os.Stat(cfg.Filename)
if err != nil {
if props, err := parseProperties(c.String("properties")); err != nil {
return err
}
if fi.IsDir() {
format.Info("Chart is directory")
c, err := chart.LoadDir(cfg.Filename)
if err != nil {
return err
}
if cfg.Name == "" {
cfg.Name = genName(c.Chartfile().Name)
}
// TODO: Is it better to generate the file in temp dir like this, or
// just put it in the CWD?
//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
} else if len(props) > 0 {
// Coalesce the properties into the first props. We have no way of
// knowing which resource the properties are supposed to be part
// of.
for n, v := range props {
cfg.Resources[0].Properties[n] = v
}
cfg.Filename = tfile
} else if cfg.Name == "" {
n, _, e := parseTarName(cfg.Filename)
if e != nil {
return e
}
cfg.Name = n
}
if cxt.Bool("dry-run") {
format.Info("Prepared deploy %q using file %q", cfg.Name, cfg.Filename)
return nil
}
c := client(cxt)
return c.DeployChart(cfg.Filename, cfg.Name)
}
func genName(pname string) string {
s, _ := goutils.RandomAlphaNumeric(8)
return fmt.Sprintf("%s-%s", pname, s)
return client(c).PostDeployment(cfg)
}
func parseTarName(name string) (string, string, error) {
tnregexp := regexp.MustCompile(chart.TarNameRegex)
if strings.HasSuffix(name, ".tgz") {
name = strings.TrimSuffix(name, ".tgz")
}
v := tnregexp.FindStringSubmatch(name)
if v == nil {
return name, "", fmt.Errorf("invalid name %s", name)
// loadConfig loads a file into a common.Configuration.
func loadConfig(c *common.Configuration, filename string) error {
data, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
return v[1], v[2], nil
return yaml.Unmarshal(data, c)
}
......@@ -174,14 +174,14 @@ func (c *Client) ListDeployments() ([]string, error) {
return l, nil
}
// DeployChart sends a chart to DM for deploying.
func (c *Client) DeployChart(filename, deployname string) error {
// UploadChart sends a chart to DM for deploying.
func (c *Client) PostChart(filename, deployname string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
u, err := c.url("/v2/deployments")
u, err := c.url("/v2/charts")
request, err := http.NewRequest("POST", u, f)
if err != nil {
f.Close()
......@@ -239,3 +239,7 @@ func (c *Client) DeleteDeployment(name string) (*common.Deployment, error) {
}
return deployment, nil
}
func (c *Client) PostDeployment(cfg *common.Configuration) error {
return c.CallService("/deployments", "POST", "post deployment", cfg, nil)
}
package dm
import (
"io/ioutil"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
......@@ -133,39 +132,28 @@ func TestGetDeployment(t *testing.T) {
}
}
func TestDeployChart(t *testing.T) {
testfile := "../testdata/charts/frobnitz-0.0.1.tgz"
testname := "sparkles"
fi, err := os.Stat(testfile)
if err != nil {
t.Fatalf("could not stat file %s: %s", testfile, err)
func TestPostDeployment(t *testing.T) {
cfg := &common.Configuration{
[]*common.Resource{
{
Name: "foo",
Type: "helm:example.com/foo/bar",
Properties: map[string]interface{}{
"port": ":8080",
},
},
},
}
expectedSize := int(fi.Size())
fc := &fakeClient{
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
data, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Errorf("Failed to read data off of request: %s", err)
}
if len(data) != expectedSize {
t.Errorf("Expected content length %d, got %d", expectedSize, len(data))
}
if cn := r.Header.Get("x-chart-name"); cn != "frobnitz-0.0.1.tgz" {
t.Errorf("Expected frobnitz-0.0.1.tgz, got %q", cn)
}
if dn := r.Header.Get("x-deployment-name"); dn != "sparkles" {
t.Errorf("Expected sparkles, got %q", dn)
}
w.WriteHeader(201)
w.WriteHeader(http.StatusCreated)
fmt.Fprintln(w, "{}")
}),
}
defer fc.teardown()
if err := fc.setup().DeployChart(testfile, testname); err != nil {
t.Fatal(err)
if err := fc.setup().PostDeployment(cfg); err != nil {
t.Fatalf("failed to post deployment: %s", err)
}
}
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