|
|
|
package dm
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/kubernetes/deployment-manager/common"
|
|
|
|
)
|
|
|
|
|
|
|
|
// The default HTTP timeout
|
|
|
|
var DefaultHTTPTimeout = time.Second * 10
|
|
|
|
|
|
|
|
// The default HTTP Protocol
|
|
|
|
var DefaultHTTPProtocol = "http"
|
|
|
|
|
|
|
|
// Client is a DM client.
|
|
|
|
type Client struct {
|
|
|
|
// Timeout on HTTP connections.
|
|
|
|
HTTPTimeout time.Duration
|
|
|
|
// The remote host
|
|
|
|
Host string
|
|
|
|
// The protocol. Currently only http and https are supported.
|
|
|
|
Protocol string
|
|
|
|
// Transport
|
|
|
|
Transport http.RoundTripper
|
|
|
|
// Debug enables http logging
|
|
|
|
Debug bool
|
|
|
|
|
|
|
|
// Base URL for remote service
|
|
|
|
baseURL *url.URL
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewClient creates a new DM client. Host name is required.
|
|
|
|
func NewClient(host string) *Client {
|
|
|
|
url, _ := DefaultServerURL(host)
|
|
|
|
|
|
|
|
return &Client{
|
|
|
|
HTTPTimeout: DefaultHTTPTimeout,
|
|
|
|
baseURL: url,
|
|
|
|
Transport: http.DefaultTransport,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetDebug enables debug mode which logs http
|
|
|
|
func (c *Client) SetDebug(enable bool) *Client {
|
|
|
|
c.Debug = enable
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// transport wraps client transport if debug is enabled
|
|
|
|
func (c *Client) transport() http.RoundTripper {
|
|
|
|
if c.Debug {
|
|
|
|
return NewDebugTransport(c.Transport)
|
|
|
|
}
|
|
|
|
return c.Transport
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetTransport sets a custom Transport. Defaults to http.DefaultTransport
|
|
|
|
func (c *Client) SetTransport(tr http.RoundTripper) *Client {
|
|
|
|
c.Transport = tr
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetTimeout sets a timeout for http connections
|
|
|
|
func (c *Client) SetTimeout(seconds int) *Client {
|
|
|
|
c.HTTPTimeout = time.Duration(time.Duration(seconds) * time.Second)
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// url constructs the URL.
|
|
|
|
func (c *Client) url(rawurl string) (string, error) {
|
|
|
|
u, err := url.Parse(rawurl)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return c.baseURL.ResolveReference(u).String(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CallService is a low-level function for making an API call.
|
|
|
|
//
|
|
|
|
// This calls the service and then unmarshals the returned data into dest.
|
|
|
|
func (c *Client) CallService(path, method, action string, dest interface{}, reader io.ReadCloser) error {
|
|
|
|
u, err := c.url(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := c.callHTTP(u, method, action, reader)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := json.Unmarshal([]byte(resp), dest); err != nil {
|
|
|
|
return fmt.Errorf("Failed to parse JSON response from service: %s", resp)
|
|
|
|
}
|
|
|
|
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) {
|
|
|
|
request, err := http.NewRequest(method, path, reader)
|
|
|
|
|
|
|
|
// TODO: dynamically set version
|
|
|
|
request.Header.Set("User-Agent", "helm/0.0.1")
|
|
|
|
request.Header.Add("Content-Type", "application/json")
|
|
|
|
|
|
|
|
client := http.Client{
|
|
|
|
Timeout: time.Duration(time.Duration(DefaultHTTPTimeout) * time.Second),
|
|
|
|
Transport: c.transport(),
|
|
|
|
}
|
|
|
|
|
|
|
|
response, err := client.Do(request)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("cannot %s: %s\n", action, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer response.Body.Close()
|
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("cannot %s: %s\n", action, 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)
|
|
|
|
}
|
|
|
|
|
|
|
|
return string(body), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// DefaultServerURL converts a host, host:port, or URL string to the default base server API path
|
|
|
|
// to use with a Client
|
|
|
|
func DefaultServerURL(host string) (*url.URL, error) {
|
|
|
|
if host == "" {
|
|
|
|
return nil, fmt.Errorf("host must be a URL or a host:port pair")
|
|
|
|
}
|
|
|
|
base := host
|
|
|
|
hostURL, err := url.Parse(base)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if hostURL.Scheme == "" {
|
|
|
|
hostURL, err = url.Parse(DefaultHTTPProtocol + "://" + base)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(hostURL.Path) > 0 && !strings.HasSuffix(hostURL.Path, "/") {
|
|
|
|
hostURL.Path = hostURL.Path + "/"
|
|
|
|
}
|
|
|
|
|
|
|
|
return hostURL, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ListDeployments lists the deployments in DM.
|
|
|
|
func (c *Client) ListDeployments() ([]string, error) {
|
|
|
|
var l []string
|
|
|
|
if err := c.CallService("deployments", "GET", "list deployments", &l, nil); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return l, 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
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return deployment, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteDeployment deletes the supplied deployment
|
|
|
|
func (c *Client) DeleteDeployment(name string) (*common.Deployment, error) {
|
|
|
|
var deployment *common.Deployment
|
|
|
|
if err := c.CallService(filepath.Join("deployments", name), "DELETE", "delete deployment", &deployment, nil); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return deployment, nil
|
|
|
|
}
|