Commit 117679c1 authored by Matt Butcher's avatar Matt Butcher

Merge pull request #16 from technosophos/feat/deploy

feat(deploy): deploy charts
parents ec4e9f30 0cdd0771
......@@ -2,8 +2,12 @@ package main
import (
"errors"
"fmt"
"os"
"regexp"
"strings"
"github.com/aokoli/goutils"
"github.com/codegangsta/cli"
dep "github.com/deis/helm-dm/deploy"
"github.com/deis/helm-dm/format"
......@@ -74,10 +78,10 @@ func deploy(c *cli.Context) error {
d.Input = os.Stdin
}
return doDeploy(d, c.GlobalString("host"), c.Bool("dry-run"))
return doDeploy(d, c)
}
func doDeploy(cfg *dep.Deployment, host string, dry bool) error {
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.")
}
......@@ -93,7 +97,12 @@ func doDeploy(cfg *dep.Deployment, host string, dry bool) error {
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 .")
......@@ -107,18 +116,36 @@ func doDeploy(cfg *dep.Deployment, host string, dry bool) error {
return err
}
cfg.Filename = tfile
} else if cfg.Name == "" {
n, _, e := parseTarName(cfg.Filename)
if e != nil {
return e
}
cfg.Name = n
}
if !dry {
if err := uploadTar(cfg.Filename); err != nil {
return err
}
if cxt.Bool("dry-run") {
format.Info("Prepared deploy %q using file %q", cfg.Name, cfg.Filename)
return nil
}
return nil
c := client(cxt)
return c.DeployChart(cfg.Filename, cfg.Name)
}
func uploadTar(filename string) error {
return nil
func genName(pname string) string {
s, _ := goutils.RandomAlphaNumeric(8)
return fmt.Sprintf("%s-%s", pname, s)
}
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)
}
return v[1], v[2], nil
}
......@@ -29,7 +29,7 @@ func main() {
Name: "host,u",
Usage: "The URL of the DM server.",
EnvVar: "HELM_HOST",
Value: "https://localhost:8181/FIXME_NOT_RIGHT",
Value: "https://localhost:8000/",
},
cli.IntFlag{
Name: "timeout",
......
package deploy
import (
//"archive/tar"
//"errors"
//"fmt"
"os"
//"strings"
//"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"
)
// Deployer is capable of deploying an object to a back-end.
type Deployer interface {
// Prepare prepares the local side of a deployment.
Prepare() error
// Commit pushes a deployment and checks that it is completed.
//
// This sends the data to the given host.
Commit(host string) error
}
// Deployment describes a deployment of a package.
type Deployment struct {
// Name is the Deployment name. Autogenerated if empty.
......@@ -42,162 +23,4 @@ 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.
//
// This will generate the Template based on other information.
func (d *Deployment) Prepare() error {
// Is Filename a local dir, a local file, or a remote URL?
fi, err := os.Stat(d.Filename)
if err != nil {
return err
}
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
}
}
// 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.
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")
}
semver, err := registry.ParseSemVer(tList[1])
if err != nil {
return nil, err
}
tt := registry.Type{Version: semver}
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.GetDownloadURLs(*t)
if err != nil {
return nil, err
}
config := common.Configuration{Resources: []*common.Resource{&common.Resource{
Name: name,
Type: gurls[0].Host,
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
}
// getGitRegistry returns a registry object for a name.
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" {
r, err := registry.NewGithubPackageRegistry(s[0], s[1], nil, nil)
if err != nil {
return nil, err
}
return r, nil
} else {
r, err := registry.NewGithubTemplateRegistry(s[0], s[1], nil, nil)
if err != nil {
return nil, err
}
return r, nil
}
}
*/
......@@ -8,6 +8,7 @@ import (
"net/http"
"net/url"
"os"
fancypath "path"
"path/filepath"
"strings"
"time"
......@@ -84,6 +85,10 @@ func (c *Client) url(rawurl string) (string, error) {
return c.baseURL.ResolveReference(u).String(), nil
}
func (c *Client) agent() string {
return fmt.Sprintf("helm/%s", "0.0.1")
}
// CallService is a low-level function for making an API call.
//
// This calls the service and then unmarshals the returned data into dest.
......@@ -108,29 +113,28 @@ func (c *Client) callHTTP(path, method, action string, reader io.ReadCloser) (st
request, err := http.NewRequest(method, path, reader)
// TODO: dynamically set version
request.Header.Set("User-Agent", "helm/0.0.1")
request.Header.Set("User-Agent", c.agent())
request.Header.Add("Content-Type", "application/json")
client := http.Client{
Timeout: time.Duration(time.Duration(DefaultHTTPTimeout) * time.Second),
client := &http.Client{
Timeout: c.HTTPTimeout,
Transport: c.transport(),
}
response, err := client.Do(request)
if err != nil {
return "", fmt.Errorf("cannot %s: %s\n", action, err)
return "", err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", fmt.Errorf("cannot %s: %s\n", action, err)
return "", err
}
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("cannot %s: %s\n", action, message)
s := response.StatusCode
if s < http.StatusOK || s >= http.StatusMultipleChoices {
return "", fmt.Errorf("request '%s %s' failed with %d: %s\n", action, path, s, body)
}
return string(body), nil
......@@ -176,9 +180,13 @@ func (c *Client) DeployChart(filename, deployname string) error {
if err != nil {
return err
}
defer f.Close()
request, err := http.NewRequest("POST", "/v2/deployments/", f)
u, err := c.url("/v2/deployments")
request, err := http.NewRequest("POST", u, f)
if err != nil {
f.Close()
return err
}
// 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.
......@@ -189,10 +197,11 @@ func (c *Client) DeployChart(filename, deployname string) error {
request.Header.Add("Content-Encoding", "gzip")
request.Header.Add("X-Deployment-Name", deployname)
request.Header.Add("X-Chart-Name", filepath.Base(filename))
request.Header.Set("User-Agent", c.agent())
client := http.Client{
Timeout: time.Duration(time.Duration(DefaultHTTPTimeout) * time.Second),
Transport: c.Transport,
client := &http.Client{
Timeout: c.HTTPTimeout,
Transport: c.transport(),
}
response, err := client.Do(request)
......@@ -200,17 +209,14 @@ func (c *Client) DeployChart(filename, deployname string) error {
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)
// We only want 201 CREATED. Admittedly, we could accept 200 and 202.
if response.StatusCode < http.StatusCreated {
body, err := ioutil.ReadAll(response.Body)
response.Body.Close()
if err != nil {
return err
}
return fmt.Errorf("failed to post: %d %s - %s", response.StatusCode, response.Status, body)
}
return nil
......@@ -219,7 +225,7 @@ func (c *Client) DeployChart(filename, deployname string) error {
// GetDeployment retrieves the supplied deployment
func (c *Client) GetDeployment(name string) (*common.Deployment, error) {
var deployment *common.Deployment
if err := c.CallService(filepath.Join("deployments", name), "GET", "get deployment", &deployment, nil); err != nil {
if err := c.CallService(fancypath.Join("deployments", name), "GET", "get deployment", &deployment, nil); err != nil {
return nil, err
}
return deployment, nil
......
package dm
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
......@@ -130,3 +132,40 @@ func TestGetDeployment(t *testing.T) {
t.Fatalf("expected deployment status 'Deployed', got '%s'", d.State.Status)
}
}
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)
}
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)
}),
}
defer fc.teardown()
if err := fc.setup().DeployChart(testfile, testname); err != nil {
t.Fatal(err)
}
}
hash: 71e9a5a2d1f27a0ecfa70595154f87fee9f5519ba6bdea4d01834bab5aa29074
updated: 2016-02-06T17:26:31.257079434-08:00
hash: c54a5f678965132636637023cd71e4d4065360b229242718f7f6e7674ac8d1c1
updated: 2016-02-09T16:35:44.56035745-07:00
imports:
- name: github.com/aokoli/goutils
version: 9c37978a95bd5c709a15883b6242714ea6709e64
- name: github.com/codegangsta/cli
version: cf1f63a7274872768d4037305d572b70b1199397
- name: github.com/emicklei/go-restful
version: b86acf97a74ed7603ac78d012f5535b4d587b156
version: 8cea2901d4b2c28b97001e67a7d2d60e227f3da6
- name: github.com/ghodss/yaml
version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee
- name: github.com/golang/glog
version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998
- name: github.com/golang/protobuf
version: 45bba206dd5270d96bac4942dcfe515726613249
- name: github.com/google/go-github
version: b8b4ac742977310ff6e75140a403a38dab109977
- name: github.com/google/go-querystring
version: 2a60fc2ba6c19de80291203597d752e9ba58e4c0
- name: github.com/gorilla/context
version: 1c83b3eabd45b6d76072b66b746c20815fb2872d
- name: github.com/gorilla/handlers
version: b3aff83722cb2ae031a70cae984650e3a16cd20e
- name: github.com/gorilla/mux
version: 26a6070f849969ba72b72256e9f14cf519751690
- name: github.com/gorilla/schema
version: 14c555599c2a4f493c1e13fd1ea6fdf721739028
- name: github.com/kubernetes/deployment-manager
version: ""
version: 4fe37ea6342991e4d0519e48d1fd6fca061baf5c
subpackages:
- /common
- chart
- common
- log
- name: github.com/Masterminds/semver
version: c4f7ef0702f269161a60489ccbbc9f1241ad1265
- name: github.com/mjibson/appstats
version: 0542d5f0e87ea3a8fa4174322b9532f5d04f9fa8
- name: golang.org/x/crypto
version: 1f22c0103821b9390939b6776727195525381532
- name: golang.org/x/net
version: 6c581b96a7d38dd755f986fcf4f29665597694c0
- name: golang.org/x/oauth2
version: 8a57ed94ffd43444c0879fe75701732a38afc985
- name: golang.org/x/text
version: 5aaa1a807bf8a2f763540b140e7805973476eb88
- name: google.golang.com/appengine/datastore
version: ""
repo: https://google.golang.com/appengine/datastore
- name: google.golang.com/appengine/memcache
version: ""
repo: https://google.golang.com/appengine/memcache
- name: google.golang.com/appengine/user
version: ""
repo: https://google.golang.com/appengine/user
- name: google.golang.org/api
version: 8fa1015948e6fc21c025050624e4c4e2f4f405c4
- name: google.golang.org/appengine
version: 6bde959377a90acb53366051d7d587bfd7171354
- name: google.golang.org/cloud
version: 5a3b06f8b5da3b7c3a93da43163b872c86c509ef
- name: google.golang.org/grpc
version: 5d64098b94ee9dbbea8ddc130208696bcd199ba4
- name: gopkg.in/yaml.v2
version: f7716cbe52baa25d2e9b0d0da546fcf909fc16b4
devImports: []
......@@ -8,3 +8,4 @@ import:
- /common
- package: github.com/ghodss/yaml
- package: github.com/Masterminds/semver
- package: github.com/aokoli/goutils
The testdata directory here holds charts that match the specification.
The `fromnitz/` directory contains a chart that matches the chart
specification.
The `frobnitz-0.0.1.tgz` file is an archive of the `frobnitz` directory.
The `ill` chart and directory is a chart that is not 100% compatible,
but which should still be parseable.
name: frobnitz
description: This is a frobniz.
version: 1.2.3-alpha.1+12345
keywords:
- frobnitz
- sprocket
- dodad
maintainers:
- name: The Helm Team
email: helm@example.com
- name: Someone Else
email: nobody@example.com
source:
- https://example.com/foo/bar
home: http://example.com
dependencies:
- name: thingerbob
version: ^3
location: https://example.com/charts/thingerbob-3.2.1.tgz
environment:
- name: Kubernetes
version: ~1.1
extensions:
- extensions/v1beta1
- extensions/v1beta1/daemonset
apiGroups:
- 3rdParty
THIS IS PLACEHOLDER TEXT.
# Frobnitz
This is an example chart.
## Usage
This is an example. It has no usage.
## Development
For developer info, see the top-level repository.
This is a placeholder for documentation.
<?xml version="1.0"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.0" width="256" height="256" id="test">
<desc>Example icon</desc>
<rect id="first" x="2" y="2" width="40" height="60" fill="navy"/>
<rect id="second" x="15" y="4" width="40" height="60" fill="red"/>
</svg>
# Google Cloud Deployment Manager template
resources:
- name: nfs-disk
type: compute.v1.disk
properties:
zone: us-central1-b
sizeGb: 200
- name: mysql-disk
type: compute.v1.disk
properties:
zone: us-central1-b
sizeGb: 200
#helm:generate dm_template
{% set PROPERTIES = properties or {} %}
{% set PROJECT = PROPERTIES['project'] or 'dm-k8s-testing' %}
{% set NFS_SERVER = PROPERTIES['nfs-server'] or {} %}
{% set NFS_SERVER_IP = NFS_SERVER['ip'] or '10.0.253.247' %}
{% set NFS_SERVER_PORT = NFS_SERVER['port'] or 2049 %}
{% set NFS_SERVER_DISK = NFS_SERVER['disk'] or 'nfs-disk' %}
{% set NFS_SERVER_DISK_FSTYPE = NFS_SERVER['fstype'] or 'ext4' %}
{% set NGINX = PROPERTIES['nginx'] or {} %}
{% set NGINX_PORT = 80 %}
{% set NGINX_REPLICAS = NGINX['replicas'] or 2 %}
{% set WORDPRESS_PHP = PROPERTIES['wordpress-php'] or {} %}
{% set WORDPRESS_PHP_REPLICAS = WORDPRESS_PHP['replicas'] or 2 %}
{% set WORDPRESS_PHP_PORT = WORDPRESS_PHP['port'] or 9000 %}
{% set MYSQL = PROPERTIES['mysql'] or {} %}
{% set MYSQL_PORT = MYSQL['port'] or 3306 %}
{% set MYSQL_PASSWORD = MYSQL['password'] or 'mysql-password' %}
{% set MYSQL_DISK = MYSQL['disk'] or 'mysql-disk' %}
{% set MYSQL_DISK_FSTYPE = MYSQL['fstype'] or 'ext4' %}
resources:
- name: nfs
type: github.com/kubernetes/application-dm-templates/storage/nfs:v1
properties:
ip: {{ NFS_SERVER_IP }}
port: {{ NFS_SERVER_PORT }}
disk: {{ NFS_SERVER_DISK }}
fstype: {{NFS_SERVER_DISK_FSTYPE }}
- name: nginx
type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2
properties:
service_port: {{ NGINX_PORT }}
container_port: {{ NGINX_PORT }}
replicas: {{ NGINX_REPLICAS }}
external_service: true
image: gcr.io/{{ PROJECT }}/nginx:latest
volumes:
- mount_path: /var/www/html
persistentVolumeClaim:
claimName: nfs
- name: mysql
type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2
properties:
service_port: {{ MYSQL_PORT }}
container_port: {{ MYSQL_PORT }}
replicas: 1
image: mysql:5.6
env:
- name: MYSQL_ROOT_PASSWORD
value: {{ MYSQL_PASSWORD }}
volumes:
- mount_path: /var/lib/mysql
gcePersistentDisk:
pdName: {{ MYSQL_DISK }}
fsType: {{ MYSQL_DISK_FSTYPE }}
- name: wordpress-php
type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2
properties:
service_name: wordpress-php
service_port: {{ WORDPRESS_PHP_PORT }}
container_port: {{ WORDPRESS_PHP_PORT }}
replicas: 2
image: wordpress:fpm
env:
- name: WORDPRESS_DB_PASSWORD
value: {{ MYSQL_PASSWORD }}
- name: WORDPRESS_DB_HOST
value: mysql-service
volumes:
- mount_path: /var/www/html
persistentVolumeClaim:
claimName: nfs
info:
title: Wordpress
description: |
Defines a Wordpress website by defining four replicated services: an NFS service, an nginx service, a wordpress-php service, and a MySQL service.
The nginx service and the Wordpress-php service both use NFS to share files.
properties:
project:
type: string
default: dm-k8s-testing
description: Project location to load the images from.
nfs-service:
type: object
properties:
ip:
type: string
default: 10.0.253.247
description: The IP of the NFS service.
port:
type: int
default: 2049
description: The port of the NFS service.
disk:
type: string
default: nfs-disk
description: The name of the persistent disk the NFS service uses.
fstype:
type: string
default: ext4
description: The filesystem the disk of the NFS service uses.
nginx:
type: object
properties:
replicas:
type: int
default: 2
description: The number of replicas for the nginx service.
wordpress-php:
type: object
properties:
replicas:
type: int
default: 2
description: The number of replicas for the wordpress-php service.
port:
type: int
default: 9000
description: The port the wordpress-php service runs on.
mysql:
type: object
properties:
port:
type: int
default: 3306
description: The port the MySQL service runs on.
password:
type: string
default: mysql-password
description: The root password of the MySQL service.
disk:
type: string
default: mysql-disk
description: The name of the persistent disk the MySQL service uses.
fstype:
type: string
default: ext4
description: The filesystem the disk of the MySQL service uses.
imports:
- path: wordpress.jinja
resources:
- name: wordpress
type: wordpress.jinja
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