Commit 7c73cd88 authored by Dave Cunningham's avatar Dave Cunningham

chartify Expansion API & expandybird service

chartify create deployment API
modify CLI to match
parent d04691cb
...@@ -18,110 +18,80 @@ package expander ...@@ -18,110 +18,80 @@ package expander
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"github.com/ghodss/yaml"
"log" "log"
"os"
"os/exec" "os/exec"
"github.com/ghodss/yaml"
"github.com/kubernetes/helm/pkg/common" "github.com/kubernetes/helm/pkg/common"
) )
// Expander abstracts interactions with the expander and deployer services.
type Expander interface {
ExpandTemplate(template *common.Template) (string, error)
}
type expander struct { type expander struct {
ExpansionBinary string ExpansionBinary string
} }
// NewExpander returns a new initialized Expander. // NewExpander returns an ExpandyBird expander.
func NewExpander(binary string) Expander { func NewExpander(binary string) common.Expander {
return &expander{binary} return &expander{binary}
} }
// ExpansionResult describes the unmarshalled output of ExpandTemplate. type expandyBirdConfigOutput struct {
type ExpansionResult struct { Resources []interface{} `yaml:"resources,omitempty"`
Config map[string]interface{}
Layout map[string]interface{}
} }
// NewExpansionResult creates and returns a new expansion result from type expandyBirdOutput struct {
// the raw output of ExpandTemplate. Config *expandyBirdConfigOutput `yaml:"config,omitempty"`
func NewExpansionResult(output string) (*ExpansionResult, error) { Layout interface{} `yaml:"layout,omitempty"`
eResponse := &ExpansionResult{}
if err := yaml.Unmarshal([]byte(output), eResponse); err != nil {
return nil, fmt.Errorf("cannot unmarshal expansion result (%s):\n%s", err, output)
}
return eResponse, nil
} }
// Marshal creates and returns an ExpansionResponse from an ExpansionResult. // ExpandChart passes the given configuration to the expander and returns the
func (eResult *ExpansionResult) Marshal() (*ExpansionResponse, error) { // expanded configuration as a string on success.
configYaml, err := yaml.Marshal(eResult.Config) func (e *expander) ExpandChart(request *common.ExpansionRequest) (*common.ExpansionResponse, error) {
if err != nil { if request.ChartInvocation == nil {
return nil, fmt.Errorf("cannot marshal manifest template (%s):\n%s", err, eResult.Config) return nil, fmt.Errorf("Request does not have invocation field")
} }
if request.Chart == nil {
layoutYaml, err := yaml.Marshal(eResult.Layout) return nil, fmt.Errorf("Request does not have chart field")
if err != nil {
return nil, fmt.Errorf("cannot marshal manifest layout (%s):\n%s", err, eResult.Layout)
} }
return &ExpansionResponse{ chartInv := request.ChartInvocation
Config: string(configYaml), chartFile := request.Chart.Chartfile
Layout: string(layoutYaml), chartMembers := request.Chart.Members
}, nil schemaName := chartInv.Type + ".schema"
}
// ExpansionResponse describes the results of marshaling an ExpansionResult. if chartFile.Expander == nil {
type ExpansionResponse struct { message := fmt.Sprintf("Chart JSON does not have expander field")
Config string `json:"config"` return nil, fmt.Errorf("%s: %s", chartInv.Name, message)
Layout string `json:"layout"`
}
// NewExpansionResponse creates and returns a new expansion response from
// the raw output of ExpandTemplate.
func NewExpansionResponse(output string) (*ExpansionResponse, error) {
eResult, err := NewExpansionResult(output)
if err != nil {
return nil, err
} }
eResponse, err := eResult.Marshal() if chartFile.Expander.Name != "ExpandyBird" {
if err != nil { message := fmt.Sprintf("ExpandyBird cannot do this kind of expansion: ", chartFile.Expander.Name)
return nil, err return nil, fmt.Errorf("%s: %s", chartInv.Name, message)
} }
return eResponse, nil if e.ExpansionBinary == "" {
} message := fmt.Sprintf("expansion binary cannot be empty")
return nil, fmt.Errorf("%s: %s", chartInv.Name, message)
// Unmarshal creates and returns an ExpansionResult from an ExpansionResponse.
func (eResponse *ExpansionResponse) Unmarshal() (*ExpansionResult, error) {
var config map[string]interface{}
if err := yaml.Unmarshal([]byte(eResponse.Config), &config); err != nil {
return nil, fmt.Errorf("cannot unmarshal config (%s):\n%s", err, eResponse.Config)
} }
var layout map[string]interface{} entrypointIndex := -1
if err := yaml.Unmarshal([]byte(eResponse.Layout), &layout); err != nil { schemaIndex := -1
return nil, fmt.Errorf("cannot unmarshal layout (%s):\n%s", err, eResponse.Layout) for i, f := range chartMembers {
if f.Path == chartFile.Expander.Entrypoint {
entrypointIndex = i
}
if f.Path == chartFile.Schema {
schemaIndex = i
}
} }
if entrypointIndex == -1 {
return &ExpansionResult{ message := fmt.Sprintf("The entrypoint in the chart.yaml cannot be found: %s", chartFile.Expander.Entrypoint)
Config: config, return nil, fmt.Errorf("%s: %s", chartInv.Name, message)
Layout: layout, }
}, nil if schemaIndex == -1 {
} message := fmt.Sprintf("The schema in the chart.yaml cannot be found: %s", chartFile.Schema)
return nil, fmt.Errorf("%s: %s", chartInv.Name, message)
// ExpandTemplate passes the given configuration to the expander and returns the
// expanded configuration as a string on success.
func (e *expander) ExpandTemplate(template *common.Template) (string, error) {
if e.ExpansionBinary == "" {
message := fmt.Sprintf("expansion binary cannot be empty")
return "", fmt.Errorf("error expanding template %s: %s", template.Name, message)
} }
// Those are automatically increasing buffers, so writing arbitrary large // Those are automatically increasing buffers, so writing arbitrary large
...@@ -129,24 +99,42 @@ func (e *expander) ExpandTemplate(template *common.Template) (string, error) { ...@@ -129,24 +99,42 @@ func (e *expander) ExpandTemplate(template *common.Template) (string, error) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
// Now we convert the new chart representation into the form that classic ExpandyBird takes.
chartInvJSON, err := json.Marshal(chartInv)
if err != nil {
return nil, fmt.Errorf("error marshalling chart invocation %s: %s", chartInv.Name, err)
}
content := "{ \"resources\": [" + string(chartInvJSON) + "] }"
cmd := &exec.Cmd{ cmd := &exec.Cmd{
Path: e.ExpansionBinary, Path: e.ExpansionBinary,
// Note, that binary name still has to be passed argv[0]. // Note, that binary name still has to be passed argv[0].
Args: []string{e.ExpansionBinary, template.Content}, Args: []string{e.ExpansionBinary, content},
// TODO(vagababov): figure out whether do we even need "PROJECT" and
// "DEPLOYMENT_NAME" variables here.
Env: append(os.Environ(), "PROJECT="+template.Name, "DEPLOYMENT_NAME="+template.Name),
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
} }
for _, imp := range template.Imports { if chartFile.Schema != "" {
cmd.Args = append(cmd.Args, imp.Name, imp.Path, imp.Content) cmd.Env = []string{"VALIDATE_SCHEMA=1"}
}
for i, f := range chartMembers {
name := f.Path
path := f.Path
if i == entrypointIndex {
// This is how expandyBird identifies the entrypoint.
name = chartInv.Type
} else if i == schemaIndex {
// Doesn't matter what it was originally called, expandyBird expects to find it here.
name = schemaName
}
cmd.Args = append(cmd.Args, name, path, string(f.Content))
} }
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
log.Printf("error starting expansion process: %s", err) log.Printf("error starting expansion process: %s", err)
return "", err return nil, err
} }
cmd.Wait() cmd.Wait()
...@@ -154,8 +142,13 @@ func (e *expander) ExpandTemplate(template *common.Template) (string, error) { ...@@ -154,8 +142,13 @@ func (e *expander) ExpandTemplate(template *common.Template) (string, error) {
log.Printf("Expansion process: pid: %d SysTime: %v UserTime: %v", cmd.ProcessState.Pid(), log.Printf("Expansion process: pid: %d SysTime: %v UserTime: %v", cmd.ProcessState.Pid(),
cmd.ProcessState.SystemTime(), cmd.ProcessState.UserTime()) cmd.ProcessState.SystemTime(), cmd.ProcessState.UserTime())
if stderr.String() != "" { if stderr.String() != "" {
return "", fmt.Errorf("error expanding template %s: %s", template.Name, stderr.String()) return nil, fmt.Errorf("%s: %s", chartInv.Name, stderr.String())
}
output := &expandyBirdOutput{}
if err := yaml.Unmarshal(stdout.Bytes(), output); err != nil {
return nil, fmt.Errorf("cannot unmarshal expansion result (%s):\n%s", err, output)
} }
return stdout.String(), nil return &common.ExpansionResponse{Resources: output.Config.Resources}, nil
} }
...@@ -16,6 +16,7 @@ limitations under the License. ...@@ -16,6 +16,7 @@ limitations under the License.
package expander package expander
/*
import ( import (
"fmt" "fmt"
"io" "io"
...@@ -179,3 +180,4 @@ func TestExpandTemplate(t *testing.T) { ...@@ -179,3 +180,4 @@ func TestExpandTemplate(t *testing.T) {
} }
} }
} }
*/
...@@ -17,7 +17,6 @@ limitations under the License. ...@@ -17,7 +17,6 @@ limitations under the License.
package service package service
import ( import (
"github.com/kubernetes/helm/cmd/expandybird/expander"
"github.com/kubernetes/helm/pkg/common" "github.com/kubernetes/helm/pkg/common"
"github.com/kubernetes/helm/pkg/util" "github.com/kubernetes/helm/pkg/util"
...@@ -44,8 +43,8 @@ func NewService(handler restful.RouteFunction) *Service { ...@@ -44,8 +43,8 @@ func NewService(handler restful.RouteFunction) *Service {
webService.Produces(restful.MIME_JSON, restful.MIME_XML) webService.Produces(restful.MIME_JSON, restful.MIME_XML)
webService.Route(webService.POST("/expand").To(handler). webService.Route(webService.POST("/expand").To(handler).
Doc("Expand a template."). Doc("Expand a template.").
Reads(&common.Template{}). Reads(&common.ExpansionRequest{}).
Writes(&expander.ExpansionResponse{})) Writes(&common.ExpansionResponse{}))
return &Service{webService} return &Service{webService}
} }
...@@ -62,31 +61,24 @@ func (s *Service) Register(container *restful.Container) { ...@@ -62,31 +61,24 @@ func (s *Service) Register(container *restful.Container) {
// NewExpansionHandler returns a route function that handles an incoming // NewExpansionHandler returns a route function that handles an incoming
// template expansion request, bound to the supplied expander. // template expansion request, bound to the supplied expander.
func NewExpansionHandler(backend expander.Expander) restful.RouteFunction { func NewExpansionHandler(backend common.Expander) restful.RouteFunction {
return func(req *restful.Request, resp *restful.Response) { return func(req *restful.Request, resp *restful.Response) {
util.LogHandlerEntry("expandybird: expand", req.Request) util.LogHandlerEntry("expandybird: expand", req.Request)
template := &common.Template{} request := &common.ExpansionRequest{}
if err := req.ReadEntity(&template); err != nil { if err := req.ReadEntity(&request); err != nil {
logAndReturnErrorFromHandler(http.StatusBadRequest, err.Error(), resp) logAndReturnErrorFromHandler(http.StatusBadRequest, err.Error(), resp)
return return
} }
output, err := backend.ExpandTemplate(template) response, err := backend.ExpandChart(request)
if err != nil { if err != nil {
message := fmt.Sprintf("error expanding template: %s", err) message := fmt.Sprintf("error expanding chart: %s", err)
logAndReturnErrorFromHandler(http.StatusBadRequest, message, resp)
return
}
response, err := expander.NewExpansionResponse(output)
if err != nil {
message := fmt.Sprintf("error marshaling output: %s", err)
logAndReturnErrorFromHandler(http.StatusBadRequest, message, resp) logAndReturnErrorFromHandler(http.StatusBadRequest, message, resp)
return return
} }
util.LogHandlerExit("expandybird", http.StatusOK, "OK", resp.ResponseWriter) util.LogHandlerExit("expandybird", http.StatusOK, "OK", resp.ResponseWriter)
message := fmt.Sprintf("\nConfig:\n%s\nLayout:\n%s\n", response.Config, response.Layout) message := fmt.Sprintf("\nResources:\n%s\n", response.Resources)
util.LogHandlerText("expandybird", message) util.LogHandlerText("expandybird", message)
resp.WriteEntity(response) resp.WriteEntity(response)
} }
......
...@@ -16,6 +16,7 @@ limitations under the License. ...@@ -16,6 +16,7 @@ limitations under the License.
package service package service
/*
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
...@@ -223,3 +224,4 @@ func expandOutputOrDie(t *testing.T, output, description string) *expander.Expan ...@@ -223,3 +224,4 @@ func expandOutputOrDie(t *testing.T, output, description string) *expander.Expan
return result return result
} }
*/
...@@ -17,6 +17,7 @@ limitations under the License. ...@@ -17,6 +17,7 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
...@@ -55,40 +56,29 @@ func deployCmd() cli.Command { ...@@ -55,40 +56,29 @@ func deployCmd() cli.Command {
func deploy(c *cli.Context) error { func deploy(c *cli.Context) error {
// If there is a configuration file, use it. res := &common.Resource{
cfg := &common.Configuration{} // By default
Properties: map[string]interface{}{},
}
if c.String("config") != "" { if c.String("config") != "" {
if err := loadConfig(cfg, c.String("config")); err != nil { // If there is a configuration file, use it.
err := loadConfig(c.String("config"), &res.Properties)
if err != nil {
return err return err
} }
} else {
cfg.Resources = []*common.Resource{
{
Properties: map[string]interface{}{},
},
}
} }
// If there is a chart specified on the commandline, override the config
// file with it.
args := c.Args() args := c.Args()
if len(args) > 0 { if len(args) == 0 {
cname := args[0] return fmt.Errorf("Need chart name on commandline")
if isLocalChart(cname) {
// If we get here, we need to first package then upload the chart.
loc, err := doUpload(cname, "", c)
if err != nil {
return err
}
cfg.Resources[0].Name = loc
} else {
cfg.Resources[0].Type = cname
}
} }
res.Type = args[0]
// Override the name if one is passed in.
if name := c.String("name"); len(name) > 0 { if name := c.String("name"); len(name) > 0 {
cfg.Resources[0].Name = name res.Name = name
} else {
return fmt.Errorf("Need deployed name on commandline")
} }
if props, err := parseProperties(c.String("properties")); err != nil { if props, err := parseProperties(c.String("properties")); err != nil {
...@@ -98,11 +88,11 @@ func deploy(c *cli.Context) error { ...@@ -98,11 +88,11 @@ func deploy(c *cli.Context) error {
// knowing which resource the properties are supposed to be part // knowing which resource the properties are supposed to be part
// of. // of.
for n, v := range props { for n, v := range props {
cfg.Resources[0].Properties[n] = v res.Properties[n] = v
} }
} }
return NewClient(c).PostDeployment(cfg.Resources[0].Name, cfg) return NewClient(c).PostDeployment(res)
} }
// isLocalChart returns true if the given path can be statted. // isLocalChart returns true if the given path can be statted.
...@@ -111,11 +101,11 @@ func isLocalChart(path string) bool { ...@@ -111,11 +101,11 @@ func isLocalChart(path string) bool {
return err == nil return err == nil
} }
// loadConfig loads a file into a common.Configuration. // loadConfig loads chart arguments into c
func loadConfig(c *common.Configuration, filename string) error { func loadConfig(filename string, dest *map[string]interface{}) error {
data, err := ioutil.ReadFile(filename) data, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return err return err
} }
return yaml.Unmarshal(data, c) return yaml.Unmarshal(data, dest)
} }
...@@ -380,8 +380,8 @@ def main(): ...@@ -380,8 +380,8 @@ def main():
idx += 3 idx += 3
env = {} env = {}
env['deployment'] = os.environ['DEPLOYMENT_NAME'] # env['deployment'] = os.environ['DEPLOYMENT_NAME']
env['project'] = os.environ['PROJECT'] # env['project'] = os.environ['PROJECT']
validate_schema = 'VALIDATE_SCHEMA' in os.environ validate_schema = 'VALIDATE_SCHEMA' in os.environ
......
...@@ -427,20 +427,20 @@ func (c *Chart) loadMember(filename string) (*Member, error) { ...@@ -427,20 +427,20 @@ func (c *Chart) loadMember(filename string) (*Member, error) {
return result, nil return result, nil
} }
//chartContent is abstraction for the contents of a chart // ChartContent is abstraction for the contents of a chart.
type chartContent struct { type ChartContent struct {
Chartfile *Chartfile `json:"chartfile"` Chartfile *Chartfile `json:"chartfile"`
Members []*Member `json:"members"` Members []*Member `json:"members"`
} }
// loadContent loads contents of a chart directory into chartContent // loadContent loads contents of a chart directory into ChartContent
func (c *Chart) loadContent() (*chartContent, error) { func (c *Chart) loadContent() (*ChartContent, error) {
ms, err := c.loadDirectory(c.Dir()) ms, err := c.loadDirectory(c.Dir())
if err != nil { if err != nil {
return nil, err return nil, err
} }
cc := &chartContent{ cc := &ChartContent{
Chartfile: c.Chartfile(), Chartfile: c.Chartfile(),
Members: ms, Members: ms,
} }
......
...@@ -24,7 +24,6 @@ import ( ...@@ -24,7 +24,6 @@ import (
fancypath "path" fancypath "path"
"path/filepath" "path/filepath"
"github.com/ghodss/yaml"
"github.com/kubernetes/helm/pkg/common" "github.com/kubernetes/helm/pkg/common"
) )
...@@ -102,15 +101,10 @@ func (c *Client) DeleteDeployment(name string) (*common.Deployment, error) { ...@@ -102,15 +101,10 @@ func (c *Client) DeleteDeployment(name string) (*common.Deployment, error) {
} }
// PostDeployment posts a deployment object to the manager service. // PostDeployment posts a deployment object to the manager service.
func (c *Client) PostDeployment(name string, cfg *common.Configuration) error { func (c *Client) PostDeployment(res *common.Resource) error {
d, err := yaml.Marshal(cfg)
if err != nil {
return err
}
// This is a stop-gap until we get this API cleaned up. // This is a stop-gap until we get this API cleaned up.
t := common.Template{ t := common.CreateDeploymentRequest{
Name: name, ChartInvocation: res,
Content: string(d),
} }
data, err := json.Marshal(t) data, err := json.Marshal(t)
......
...@@ -65,15 +65,11 @@ func TestGetDeployment(t *testing.T) { ...@@ -65,15 +65,11 @@ func TestGetDeployment(t *testing.T) {
} }
func TestPostDeployment(t *testing.T) { func TestPostDeployment(t *testing.T) {
cfg := &common.Configuration{ chartInvocation := &common.Resource{
Resources: []*common.Resource{ Name: "foo",
{ Type: "helm:example.com/foo/bar",
Name: "foo", Properties: map[string]interface{}{
Type: "helm:example.com/foo/bar", "port": ":8080",
Properties: map[string]interface{}{
"port": ":8080",
},
},
}, },
} }
...@@ -85,7 +81,7 @@ func TestPostDeployment(t *testing.T) { ...@@ -85,7 +81,7 @@ func TestPostDeployment(t *testing.T) {
} }
defer fc.teardown() defer fc.teardown()
if err := fc.setup().PostDeployment("foo", cfg); err != nil { if err := fc.setup().PostDeployment(chartInvocation); err != nil {
t.Fatalf("failed to post deployment: %s", err) t.Fatalf("failed to post deployment: %s", err)
} }
} }
...@@ -17,6 +17,7 @@ limitations under the License. ...@@ -17,6 +17,7 @@ limitations under the License.
package common package common
import ( import (
"github.com/kubernetes/helm/pkg/chart"
"time" "time"
) )
...@@ -97,6 +98,27 @@ type Manifest struct { ...@@ -97,6 +98,27 @@ type Manifest struct {
Layout *Layout `json:"layout,omitempty"` Layout *Layout `json:"layout,omitempty"`
} }
// CreateDeploymentRequest defines the manager API to create deployments.
type CreateDeploymentRequest struct {
ChartInvocation *Resource `json:"chart_invocation"`
}
// ExpansionRequest defines the API to expander.
type ExpansionRequest struct {
ChartInvocation *Resource `json:"chart_invocation"`
Chart *chart.ChartContent `json:"chart"`
}
// ExpansionResponse defines the API to expander.
type ExpansionResponse struct {
Resources []interface{} `json:"resources"`
}
// Expander abstracts interactions with the expander and deployer services.
type Expander interface {
ExpandChart(request *ExpansionRequest) (*ExpansionResponse, error)
}
// Template describes a set of resources to be deployed. // Template describes a set of resources to be deployed.
// Manager expands a Template into a Configuration, which // Manager expands a Template into a Configuration, which
// describes the set in a form that can be instantiated. // describes the set in a form that can be instantiated.
......
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