Merge pull request #462 from jackgr/manager

WIP: Fourth round of refactoring for Manager
pull/477/head
Jack Greenfield 9 years ago
commit b51c3a8ee2

@ -111,12 +111,12 @@ func newManager(c *router.Context) manager.Manager {
cfg := c.Config cfg := c.Config
service := repo.NewInmemRepoService() service := repo.NewInmemRepoService()
cp := c.CredentialProvider cp := c.CredentialProvider
repoProvider := repo.NewRepoProvider(service, repo.NewGCSRepoProvider(cp), cp) rp := repo.NewRepoProvider(service, repo.NewGCSRepoProvider(cp), cp)
expander := manager.NewExpander(getServiceURL(cfg.ExpanderURL, cfg.ExpanderName, expanderPort)) expander := manager.NewExpander(getServiceURL(cfg.ExpanderURL, cfg.ExpanderName, expanderPort), rp)
deployer := manager.NewDeployer(getServiceURL(cfg.DeployerURL, cfg.DeployerName, deployerPort)) deployer := manager.NewDeployer(getServiceURL(cfg.DeployerURL, cfg.DeployerName, deployerPort))
address := strings.TrimPrefix(getServiceURL(cfg.MongoAddress, cfg.MongoName, cfg.MongoPort), "http://") address := strings.TrimPrefix(getServiceURL(cfg.MongoAddress, cfg.MongoName, cfg.MongoPort), "http://")
repository := createRepository(address) repository := createRepository(address)
return manager.NewManager(expander, deployer, repository, repoProvider, service, c.CredentialProvider) return manager.NewManager(expander, deployer, repository, rp, service, c.CredentialProvider)
} }
func createRepository(address string) repository.Repository { func createRepository(address string) repository.Repository {
@ -202,9 +202,9 @@ func createDeploymentHandlerFunc(w http.ResponseWriter, r *http.Request, c *rout
handler := "manager: create deployment" handler := "manager: create deployment"
util.LogHandlerEntry(handler, r) util.LogHandlerEntry(handler, r)
defer r.Body.Close() defer r.Body.Close()
t := getTemplate(w, r, handler) depReq := getDeploymentRequest(w, r, handler)
if t != nil { if depReq != nil {
d, err := c.Manager.CreateDeployment(t) d, err := c.Manager.CreateDeployment(depReq)
if err != nil { if err != nil {
httputil.BadRequest(w, r, err) httputil.BadRequest(w, r, err)
return nil return nil
@ -212,6 +212,7 @@ func createDeploymentHandlerFunc(w http.ResponseWriter, r *http.Request, c *rout
util.LogHandlerExitWithJSON(handler, w, d, http.StatusCreated) util.LogHandlerExitWithJSON(handler, w, d, http.StatusCreated)
} }
return nil return nil
} }
@ -242,9 +243,9 @@ func putDeploymentHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.
return err return err
} }
t := getTemplate(w, r, handler) depReq := getDeploymentRequest(w, r, handler)
if t != nil { if depReq != nil {
d, err := c.Manager.PutDeployment(name, t) d, err := c.Manager.PutDeployment(name, depReq)
if err != nil { if err != nil {
httputil.BadRequest(w, r, err) httputil.BadRequest(w, r, err)
return nil return nil
@ -252,6 +253,7 @@ func putDeploymentHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.
util.LogHandlerExitWithJSON(handler, w, d, http.StatusCreated) util.LogHandlerExitWithJSON(handler, w, d, http.StatusCreated)
} }
return nil return nil
} }
@ -289,14 +291,15 @@ func getPathVariable(w http.ResponseWriter, r *http.Request, variable, handler s
return unescaped, nil return unescaped, nil
} }
func getTemplate(w http.ResponseWriter, r *http.Request, handler string) *common.Template { func getDeploymentRequest(w http.ResponseWriter, r *http.Request, handler string) *common.DeploymentRequest {
util.LogHandlerEntry(handler, r) util.LogHandlerEntry(handler, r)
t := &common.Template{} depReq := &common.DeploymentRequest{}
if err := httputil.Decode(w, r, t); err != nil { if err := httputil.Decode(w, r, depReq); err != nil {
httputil.BadRequest(w, r, err) httputil.BadRequest(w, r, err)
return nil return nil
} }
return t
return depReq
} }
func listManifestsHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error { func listManifestsHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context) error {
@ -348,9 +351,9 @@ func expandHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context
handler := "manager: expand config" handler := "manager: expand config"
util.LogHandlerEntry(handler, r) util.LogHandlerEntry(handler, r)
defer r.Body.Close() defer r.Body.Close()
t := getTemplate(w, r, handler) depReq := getDeploymentRequest(w, r, handler)
if t != nil { if depReq != nil {
c, err := c.Manager.Expand(t) c, err := c.Manager.Expand(depReq)
if err != nil { if err != nil {
httputil.BadRequest(w, r, err) httputil.BadRequest(w, r, err)
return nil return nil
@ -358,6 +361,7 @@ func expandHandlerFunc(w http.ResponseWriter, r *http.Request, c *router.Context
util.LogHandlerExitWithJSON(handler, w, c, http.StatusCreated) util.LogHandlerExitWithJSON(handler, w, c, http.StatusCreated)
} }
return nil return nil
} }

@ -60,12 +60,12 @@ func TestHealthz(t *testing.T) {
func TestCreateDeployments(t *testing.T) { func TestCreateDeployments(t *testing.T) {
c := stubContext() c := stubContext()
tpl := &common.Template{Name: "foo"} depReq := &common.DeploymentRequest{Name: "foo"}
s := httpHarness(c, "POST /deployments", createDeploymentHandlerFunc) s := httpHarness(c, "POST /deployments", createDeploymentHandlerFunc)
defer s.Close() defer s.Close()
var b bytes.Buffer var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(tpl); err != nil { if err := json.NewEncoder(&b).Encode(depReq); err != nil {
t.Fatal(err) t.Fatal(err)
} }

@ -17,190 +17,138 @@ limitations under the License.
package manager package manager
import ( import (
"github.com/kubernetes/helm/pkg/common"
"github.com/kubernetes/helm/pkg/expansion"
"github.com/kubernetes/helm/pkg/repo"
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"github.com/ghodss/yaml"
"github.com/kubernetes/helm/pkg/common"
) )
/*
const ( const (
// TODO (iantw): Align this with a character not allowed to show up in resource names. // TODO (iantw): Align this with a character not allowed to show up in resource names.
layoutNodeKeySeparator = "#" layoutNodeKeySeparator = "#"
) )
*/
// ExpandedTemplate is the structure returned by the expansion service. // ExpandedConfiguration is the structure returned by the expansion service.
type ExpandedTemplate struct { type ExpandedConfiguration struct {
Config *common.Configuration `json:"config"` Config *common.Configuration `json:"config"`
Layout *common.Layout `json:"layout"` Layout *common.Layout `json:"layout"`
} }
// Expander abstracts interactions with the expander and deployer services. // Expander abstracts interactions with the expander and deployer services.
type Expander interface { type Expander interface {
ExpandTemplate(t *common.Template) (*ExpandedTemplate, error) ExpandConfiguration(conf *common.Configuration) (*ExpandedConfiguration, error)
}
// TODO: Remove mockResolver when type resolver is completely excised
type mockResolver struct {
} }
func (r *mockResolver) ResolveTypes(c *common.Configuration, i []*common.ImportFile) ([]*common.ImportFile, error) { // NewExpander returns a new initialized Expander.
return nil, nil func NewExpander(URL string, rp repo.IRepoProvider) Expander {
if rp == nil {
rp = repo.NewRepoProvider(nil, nil, nil)
} }
// NewExpander returns a new initialized Expander. return &expander{expanderURL: URL, repoProvider: rp}
func NewExpander(url string) Expander {
tr := &mockResolver{}
return &expander{url, tr}
} }
type expander struct { type expander struct {
repoProvider repo.IRepoProvider
expanderURL string expanderURL string
typeResolver TypeResolver
}
// TypeResolver finds Types in a Configuration which aren't yet reduceable to an import file
// or primitive, and attempts to replace them with a template from a URL.
type TypeResolver interface {
ResolveTypes(config *common.Configuration, imports []*common.ImportFile) ([]*common.ImportFile, error)
} }
func (e *expander) getBaseURL() string { func (e *expander) getBaseURL() string {
return fmt.Sprintf("%s/expand", e.expanderURL) return fmt.Sprintf("%s/expand", e.expanderURL)
} }
func expanderError(t *common.Template, err error) error { // ExpandConfiguration expands the supplied configuration and returns
return fmt.Errorf("cannot expand template named %s (%s):\n%s", t.Name, err, t.Content) // an expanded configuration.
func (e *expander) ExpandConfiguration(conf *common.Configuration) (*ExpandedConfiguration, error) {
expConf, err := e.expandConfiguration(conf)
if err != nil {
return nil, fmt.Errorf("cannot expand configuration:%s\n%v\n", err, conf)
} }
// ExpanderResponse gives back a layout, which has nested structure return expConf, nil
// Resource0
// ResourceDefinition
// Resource0, 0
// ResourceDefinition
// Resource0, 0, 0
// ResourceDefinition
// Resource0, 0, 1
// ResourceDefinition
// Resource0, 1
// ResourceDefinition
//
// All the leaf nodes in this tree are either primitives or a currently unexpandable type.
// Next we will resolve all the unexpandable types and re-enter expansion, at which point
// all primitives are untouched and returned as root siblings with no children in the
// resulting layout. The previously unexpandable nodes will become sibling root nodes,
// but with children. We want to replace the leaf nodes that were formerly unexpandable
// with their respective newly created trees.
//
// So, do as follows:
// 1) Do a walk of the tree and find each leaf. Check its Type and place a pointer to it
// into a map with the resource name and type as key if it is non-primitive.
// 2) Re-expand the template with the new imports.
// 3) For each root level sibling, check if its name exists in the hash map from (1)
// 4) Replace the Layout of the node in the hash map with the current node if applicable.
// 5) Return to (1)
// TODO (iantw): There may be a tricky corner case here where a known template could be
// masked by an unknown template, which on the subsequent expansion could allow a collision
// between the name#template key to exist in the layout given a particular choice of naming.
// In practice, it would be nearly impossible to hit, but consider including properties/name/type
// into a hash of sorts to make this robust...
func walkLayout(l *common.Layout, imports []*common.ImportFile, toReplace map[string]*common.LayoutResource) map[string]*common.LayoutResource {
ret := map[string]*common.LayoutResource{}
toVisit := l.Resources
for len(toVisit) > 0 {
lr := toVisit[0]
nodeKey := lr.Resource.Name + layoutNodeKeySeparator + lr.Resource.Type
if len(lr.Layout.Resources) == 0 && isTemplate(lr.Resource.Type, imports) {
ret[nodeKey] = lr
} else if toReplace[nodeKey] != nil {
toReplace[nodeKey].Resources = lr.Resources
}
toVisit = append(toVisit, lr.Resources...)
toVisit = toVisit[1:]
} }
return ret func (e *expander) expandConfiguration(conf *common.Configuration) (*ExpandedConfiguration, error) {
} resources := []*common.Resource{}
layouts := []*common.LayoutResource{}
// isTemplate returns whether a given type is a template. // Iterate over all of the resources in the unexpanded configuration
func isTemplate(t string, imports []*common.ImportFile) bool { for _, resource := range conf.Resources {
for _, imp := range imports { // A primitive layout resource captures only the name and type
if imp.Name == t { layout := &common.LayoutResource{
return true Resource: common.Resource{
} Name: resource.Name, Type: resource.Type,
},
} }
return false // If the type is not a chart reference, then it must be primitive
} if !repo.IsChartReference(resource.Type) {
// Add it to the flat list of exapnded resources
resources = append(resources, resource)
// ExpandTemplate expands the supplied template, and returns a configuration. // Add its layout to the list of layouts at this level
// It will also update the imports in the provided template if any were added layouts = append(layouts, layout)
// during type resolution. continue
func (e *expander) ExpandTemplate(t *common.Template) (*ExpandedTemplate, error) {
// We have a fencepost problem here.
// 1. Start by trying to resolve any missing templates
// 2. Expand the configuration using all the of the imports available to us at this point
// 3. Expansion may yield additional templates, so we run the type resolution again
// 4. If type resolution resulted in new imports being available, return to 2.
config := &common.Configuration{}
if err := yaml.Unmarshal([]byte(t.Content), config); err != nil {
e := fmt.Errorf("Unable to unmarshal configuration (%s): %s", err, t.Content)
return nil, e
} }
var finalLayout *common.Layout // It is a chart, so go fetch it, decompress it and unpack it
needResolve := map[string]*common.LayoutResource{} cbr, _, err := e.repoProvider.GetChartByReference(resource.Type)
// Start things off by attempting to resolve the templates in a first pass.
newImp, err := e.typeResolver.ResolveTypes(config, t.Imports)
if err != nil { if err != nil {
e := fmt.Errorf("type resolution failed: %s", err) return nil, err
return nil, expanderError(t, e)
} }
t.Imports = append(t.Imports, newImp...) defer cbr.Close()
for { // Now, load the charts contents into strings that we can pass to exapnsion
// Now expand with everything imported. content, err := cbr.LoadContent()
result, err := e.expandTemplate(t)
if err != nil { if err != nil {
e := fmt.Errorf("template expansion: %s", err) return nil, err
return nil, expanderError(t, e)
} }
// Once we set this layout, we're operating on the "needResolve" *LayoutResources, // Build a request to the expansion service and call it to do the expansion
// which are pointers into the original layout structure. After each expansion we svcReq := &expansion.ServiceRequest{
// lose the templates in the previous expansion, so we have to keep the first one ChartInvocation: resource,
// around and keep appending to the pointers in it as we get more layers of expansion. Chart: content,
if finalLayout == nil {
finalLayout = result.Layout
} }
needResolve = walkLayout(result.Layout, t.Imports, needResolve)
newImp, err = e.typeResolver.ResolveTypes(result.Config, t.Imports) svcResp, err := e.callService(svcReq)
if err != nil { if err != nil {
e := fmt.Errorf("type resolution failed: %s", err) return nil, err
return nil, expanderError(t, e)
} }
// If the new imports contain nothing, we are done. Everything is fully expanded. // Call ourselves recursively with the list of resources returned by expansion
if len(newImp) == 0 { expConf, err := e.expandConfiguration(svcResp)
result.Layout = finalLayout if err != nil {
return result, nil return nil, err
} }
// Update imports with any new imports from type resolution. // Append the reources returned by the recursion to the flat list of resources
t.Imports = append(t.Imports, newImp...) resources = append(resources, expConf.Config.Resources...)
// This was not a primitive resource, so add its properties to the layout
layout.Properties = resource.Properties
// Now add the all of the layout resources returned by the recursion to the layout
layout.Resources = expConf.Layout.Resources
layouts = append(layouts, layout)
} }
// All done with this level, so return the espanded configuration
return &ExpandedConfiguration{
Config: &common.Configuration{Resources: resources},
Layout: &common.Layout{Resources: layouts},
}, nil
} }
func (e *expander) expandTemplate(t *common.Template) (*ExpandedTemplate, error) { func (e *expander) callService(svcReq *expansion.ServiceRequest) (*common.Configuration, error) {
j, err := json.Marshal(t) j, err := json.Marshal(svcReq)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -232,37 +180,11 @@ func (e *expander) expandTemplate(t *common.Template) (*ExpandedTemplate, error)
return nil, err return nil, err
} }
er := &ExpansionResponse{} svcResp := &common.Configuration{}
if err := json.Unmarshal(body, er); err != nil { if err := json.Unmarshal(body, svcResp); err != nil {
e := fmt.Errorf("cannot unmarshal response body (%s):%s", err, body) e := fmt.Errorf("cannot unmarshal response body (%s):%s", err, body)
return nil, e return nil, e
} }
template, err := er.Unmarshal() return svcResp, nil
if err != nil {
e := fmt.Errorf("cannot unmarshal response yaml (%s):%v", err, er)
return nil, e
}
return template, nil
}
// ExpansionResponse describes the results of marshaling an ExpandedTemplate.
type ExpansionResponse struct {
Config string `json:"config"`
Layout string `json:"layout"`
}
// Unmarshal creates and returns an ExpandedTemplate from an ExpansionResponse.
func (er *ExpansionResponse) Unmarshal() (*ExpandedTemplate, error) {
template := &ExpandedTemplate{}
if err := yaml.Unmarshal([]byte(er.Config), &template.Config); err != nil {
return nil, fmt.Errorf("cannot unmarshal config (%s):\n%s", err, er.Config)
}
if err := yaml.Unmarshal([]byte(er.Layout), &template.Layout); err != nil {
return nil, fmt.Errorf("cannot unmarshal layout (%s):\n%s", err, er.Layout)
}
return template, nil
} }

@ -26,44 +26,28 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/ghodss/yaml"
"github.com/kubernetes/helm/pkg/chart"
"github.com/kubernetes/helm/pkg/common" "github.com/kubernetes/helm/pkg/common"
"github.com/kubernetes/helm/pkg/expansion"
"github.com/kubernetes/helm/pkg/repo"
"github.com/kubernetes/helm/pkg/util" "github.com/kubernetes/helm/pkg/util"
"github.com/ghodss/yaml"
) )
var validTemplateTestCaseData = common.Template{ var (
Name: "TestTemplate", TestRepoBucket = "kubernetes-charts-testing"
Content: string(validContentTestCaseData), TestRepoURL = "gs://" + TestRepoBucket
Imports: validImportFilesTestCaseData, TestChartName = "frobnitz"
} TestChartVersion = "0.0.1"
TestArchiveName = TestChartName + "-" + TestChartVersion + ".tgz"
var validContentTestCaseData = []byte(` TestResourceType = TestRepoURL + "/" + TestArchiveName
imports: TestRepoType = string(repo.GCSRepoType)
- path: test-type.py TestRepoFormat = string(repo.GCSRepoFormat)
resources: TestRepoCredentialName = "default"
- name: test TestRepoName = TestRepoBucket
type: test-type.py )
properties:
test-property: test-value
`)
var validImportFilesTestCaseData = []*common.ImportFile{
{
Name: "test-type.py",
Content: "test-type.py validTemplateTestCaseData content",
},
{
Name: "test.py",
Content: "test.py validTemplateTestCaseData content",
},
{
Name: "test2.py",
Content: "test2.py validTemplateTestCaseData content",
},
}
var validConfigTestCaseData = []byte(` var validResponseTestCaseData = []byte(`
resources: resources:
- name: test-service - name: test-service
properties: properties:
@ -93,6 +77,26 @@ resources:
var validLayoutTestCaseData = []byte(` var validLayoutTestCaseData = []byte(`
resources: resources:
- name: test_invocation
resources:
- name: test-service
type: Service
- name: test-rc
type: ReplicationController
- name: test3-service
type: Service
- name: test3-rc
type: ReplicationController
- name: test4-service
type: Service
- name: test4-rc
type: ReplicationController
type: gs://kubernetes-charts-testing/frobnitz-0.0.1.tgz
`)
/*
[]byte(`
resources:
- name: test - name: test
properties: properties:
test-property: test-value test-property: test-value
@ -126,13 +130,7 @@ resources:
type: test2.jinja type: test2.jinja
`) `)
var validResponseTestCaseData = ExpansionResponse{
Config: string(validConfigTestCaseData),
Layout: string(validLayoutTestCaseData),
}
var roundTripContent = ` var roundTripContent = `
config:
resources: resources:
- name: test - name: test
type: test.py type: test.py
@ -207,35 +205,58 @@ layout:
test: test test: test
` `
var roundTripTemplate = common.Template{ var roundTripResponse = &ExpandedConfiguration{
Name: "TestTemplate", Config: roundTripExpanded,
Content: roundTripContent, }
Imports: nil,
var roundTripResponse2 = &ExpandedConfiguration{
Config: roundTripExpanded2,
}
var roundTripResponses = []*ExpandedConfiguration{
roundTripResponse,
roundTripResponse2,
}
*/
type mockRepoProvider struct {
}
func (m *mockRepoProvider) GetChartByReference(reference string) (*chart.Chart, repo.IChartRepo, error) {
return &chart.Chart{}, nil, nil
}
func (m *mockRepoProvider) GetRepoByChartURL(URL string) (repo.IChartRepo, error) {
return nil, nil
}
func (m *mockRepoProvider) GetRepoByURL(URL string) (repo.IChartRepo, error) {
return nil, nil
} }
type ExpanderTestCase struct { type ExpanderTestCase struct {
Description string Description string
Error string Error string
Handler func(w http.ResponseWriter, r *http.Request) Handler func(w http.ResponseWriter, r *http.Request)
ValidResponse *ExpandedTemplate ValidResponse *ExpandedConfiguration
} }
func TestExpandTemplate(t *testing.T) { func TestExpandTemplate(t *testing.T) {
roundTripResponse := &ExpandedTemplate{} // roundTripResponse := &ExpandedConfiguration{}
if err := yaml.Unmarshal([]byte(finalExpanded), roundTripResponse); err != nil { // if err := yaml.Unmarshal([]byte(finalExpanded), roundTripResponse); err != nil {
panic(err) // panic(err)
} // }
tests := []ExpanderTestCase{ tests := []ExpanderTestCase{
{ {
"expect success for ExpandTemplate", "expect success for ExpandConfiguration",
"", "",
expanderSuccessHandler, expanderSuccessHandler,
getValidResponse(t, "expect success for ExpandTemplate"), getValidExpandedConfiguration(),
}, },
{ {
"expect error for ExpandTemplate", "expect error for ExpandConfiguration",
"cannot expand template", "simulated failure",
expanderErrorHandler, expanderErrorHandler,
nil, nil,
}, },
@ -245,12 +266,23 @@ func TestExpandTemplate(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(etc.Handler)) ts := httptest.NewServer(http.HandlerFunc(etc.Handler))
defer ts.Close() defer ts.Close()
expander := NewExpander(ts.URL) expander := NewExpander(ts.URL, getTestRepoProvider(t))
actualResponse, err := expander.ExpandTemplate(&validTemplateTestCaseData) resource := &common.Resource{
Name: "test_invocation",
Type: TestResourceType,
}
conf := &common.Configuration{
Resources: []*common.Resource{
resource,
},
}
actualResponse, err := expander.ExpandConfiguration(conf)
if err != nil { if err != nil {
message := err.Error() message := err.Error()
if etc.Error == "" { if etc.Error == "" {
t.Errorf("Error in test case %s when there should not be.", etc.Description) t.Errorf("unexpected error in test case %s: %s", etc.Description, err)
} }
if !strings.Contains(message, etc.Error) { if !strings.Contains(message, etc.Error) {
t.Errorf("error in test case:%s:%s\n", etc.Description, message) t.Errorf("error in test case:%s:%s\n", etc.Description, message)
@ -270,42 +302,45 @@ func TestExpandTemplate(t *testing.T) {
} }
} }
func getValidResponse(t *testing.T, description string) *ExpandedTemplate { func getValidServiceResponse() *common.Configuration {
response, err := validResponseTestCaseData.Unmarshal() conf := &common.Configuration{}
if err != nil { if err := yaml.Unmarshal(validResponseTestCaseData, conf); err != nil {
t.Errorf("cannot unmarshal valid response for test case '%s': %s\n", description, err) panic(fmt.Errorf("cannot unmarshal valid response: %s\n", err))
} }
return response return conf
} }
func expanderErrorHandler(w http.ResponseWriter, r *http.Request) { func getValidExpandedConfiguration() *ExpandedConfiguration {
defer r.Body.Close() conf := getValidServiceResponse()
http.Error(w, "something failed", http.StatusInternalServerError) layout := &common.Layout{}
if err := yaml.Unmarshal(validLayoutTestCaseData, layout); err != nil {
panic(fmt.Errorf("cannot unmarshal valid response: %s\n", err))
} }
var roundTripResponse = ExpansionResponse{ return &ExpandedConfiguration{Config: conf, Layout: layout}
Config: roundTripExpanded,
Layout: roundTripLayout,
}
var roundTripResponse2 = ExpansionResponse{
Config: roundTripExpanded2,
Layout: roundTripLayout2,
} }
var roundTripResponses = []ExpansionResponse{ func expanderErrorHandler(w http.ResponseWriter, r *http.Request) {
roundTripResponse, defer r.Body.Close()
roundTripResponse2, http.Error(w, "simulated failure", http.StatusInternalServerError)
} }
/*
func roundTripHandler(w http.ResponseWriter, r *http.Request) { func roundTripHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() defer r.Body.Close()
handler := "expandybird: expand" handler := "expandybird: expand"
util.LogHandlerEntry(handler, r) util.LogHandlerEntry(handler, r)
if len(roundTripResponses) < 1 {
http.Error(w, "Too many calls to round trip handler", http.StatusInternalServerError)
return
}
util.LogHandlerExitWithJSON(handler, w, roundTripResponses[0], http.StatusOK) util.LogHandlerExitWithJSON(handler, w, roundTripResponses[0], http.StatusOK)
roundTripResponses = roundTripResponses[1:] roundTripResponses = roundTripResponses[1:]
} }
*/
func expanderSuccessHandler(w http.ResponseWriter, r *http.Request) { func expanderSuccessHandler(w http.ResponseWriter, r *http.Request) {
handler := "expandybird: expand" handler := "expandybird: expand"
@ -318,19 +353,37 @@ func expanderSuccessHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
template := &common.Template{} svcReq := &expansion.ServiceRequest{}
if err := json.Unmarshal(body, template); err != nil { if err := json.Unmarshal(body, svcReq); err != nil {
status := fmt.Sprintf("cannot unmarshal request body:%s\n%s\n", err, body) status := fmt.Sprintf("cannot unmarshal request body:%s\n%s\n", err, body)
http.Error(w, status, http.StatusInternalServerError) http.Error(w, status, http.StatusInternalServerError)
return return
} }
if !reflect.DeepEqual(validTemplateTestCaseData, *template) { /*
if !reflect.DeepEqual(validRequestTestCaseData, *svcReq) {
status := fmt.Sprintf("error in http handler:\nwant:%s\nhave:%s\n", status := fmt.Sprintf("error in http handler:\nwant:%s\nhave:%s\n",
util.ToJSONOrError(validTemplateTestCaseData), util.ToJSONOrError(template)) util.ToJSONOrError(validRequestTestCaseData), util.ToJSONOrError(template))
http.Error(w, status, http.StatusInternalServerError) http.Error(w, status, http.StatusInternalServerError)
return return
} }
*/
svcResp := getValidServiceResponse()
util.LogHandlerExitWithJSON(handler, w, svcResp, http.StatusOK)
}
func getTestRepoProvider(t *testing.T) repo.IRepoProvider {
rs := repo.NewInmemRepoService()
rp := repo.NewRepoProvider(rs, nil, nil)
tr, err := repo.NewRepo(TestRepoURL, TestRepoCredentialName, TestRepoName, TestRepoFormat, TestRepoType)
if err != nil {
t.Fatalf("cannot create test repository: %s", err)
}
if err := rs.CreateRepo(tr); err != nil {
t.Fatalf("cannot initialize repository service: %s", err)
}
util.LogHandlerExitWithJSON(handler, w, validResponseTestCaseData, http.StatusOK) return rp
} }

@ -33,14 +33,14 @@ type Manager interface {
// Deployments // Deployments
ListDeployments() ([]common.Deployment, error) ListDeployments() ([]common.Deployment, error)
GetDeployment(name string) (*common.Deployment, error) GetDeployment(name string) (*common.Deployment, error)
CreateDeployment(t *common.Template) (*common.Deployment, error) CreateDeployment(depReq *common.DeploymentRequest) (*common.Deployment, error)
DeleteDeployment(name string, forget bool) (*common.Deployment, error) DeleteDeployment(name string, forget bool) (*common.Deployment, error)
PutDeployment(name string, t *common.Template) (*common.Deployment, error) PutDeployment(name string, depReq *common.DeploymentRequest) (*common.Deployment, error)
// Manifests // Manifests
ListManifests(deploymentName string) (map[string]*common.Manifest, error) ListManifests(deploymentName string) (map[string]*common.Manifest, error)
GetManifest(deploymentName string, manifest string) (*common.Manifest, error) GetManifest(deploymentName string, manifest string) (*common.Manifest, error)
Expand(t *common.Template) (*common.Manifest, error) Expand(t *common.DeploymentRequest) (*common.Manifest, error)
// Charts // Charts
ListCharts() ([]string, error) ListCharts() ([]string, error)
@ -125,27 +125,27 @@ func (m *manager) GetManifest(deploymentName string, manifestName string) (*comm
return d, nil return d, nil
} }
// CreateDeployment expands the supplied template, creates the resulting // CreateDeployment expands the supplied configuration, creates the resulting
// configuration in the cluster, creates a new deployment that tracks it, // resources in the cluster, creates a new deployment that tracks it, stores the
// and stores the deployment in the repository. Returns the deployment. // deployment in the repository and returns the deployment.
func (m *manager) CreateDeployment(t *common.Template) (*common.Deployment, error) { func (m *manager) CreateDeployment(depReq *common.DeploymentRequest) (*common.Deployment, error) {
log.Printf("Creating deployment: %s", t.Name) log.Printf("Creating deployment: %s", depReq.Name)
_, err := m.repository.CreateDeployment(t.Name) _, err := m.repository.CreateDeployment(depReq.Name)
if err != nil { if err != nil {
log.Printf("CreateDeployment failed %v", err) log.Printf("CreateDeployment failed %v", err)
return nil, err return nil, err
} }
manifest, err := m.createManifest(t) manifest, err := m.Expand(depReq)
if err != nil { if err != nil {
log.Printf("Manifest creation failed: %v", err) log.Printf("Manifest creation failed: %v", err)
m.repository.SetDeploymentState(t.Name, failState(err)) m.repository.SetDeploymentState(depReq.Name, failState(err))
return nil, err return nil, err
} }
if err := m.repository.AddManifest(manifest); err != nil { if err := m.repository.AddManifest(manifest); err != nil {
log.Printf("AddManifest failed %v", err) log.Printf("AddManifest failed %v", err)
m.repository.SetDeploymentState(t.Name, failState(err)) m.repository.SetDeploymentState(depReq.Name, failState(err))
return nil, err return nil, err
} }
@ -153,7 +153,7 @@ func (m *manager) CreateDeployment(t *common.Template) (*common.Deployment, erro
if err != nil { if err != nil {
// Deployment failed, mark as failed // Deployment failed, mark as failed
log.Printf("CreateConfiguration failed: %v", err) log.Printf("CreateConfiguration failed: %v", err)
m.repository.SetDeploymentState(t.Name, failState(err)) m.repository.SetDeploymentState(depReq.Name, failState(err))
// If we failed before being able to create some of the resources, then // If we failed before being able to create some of the resources, then
// return the failure as such. Otherwise, we're going to add the manifest // return the failure as such. Otherwise, we're going to add the manifest
@ -166,40 +166,24 @@ func (m *manager) CreateDeployment(t *common.Template) (*common.Deployment, erro
errs := getResourceErrors(actualConfig) errs := getResourceErrors(actualConfig)
if len(errs) > 0 { if len(errs) > 0 {
e := fmt.Errorf("Found resource errors during deployment: %v", errs) e := fmt.Errorf("Found resource errors during deployment: %v", errs)
m.repository.SetDeploymentState(t.Name, failState(e)) m.repository.SetDeploymentState(depReq.Name, failState(e))
return nil, e return nil, e
} }
m.repository.SetDeploymentState(t.Name, &common.DeploymentState{Status: common.DeployedStatus}) m.repository.SetDeploymentState(depReq.Name, &common.DeploymentState{Status: common.DeployedStatus})
} }
// Update the manifest with the actual state of the reified resources // Update the manifest with the actual state of the reified resources
manifest.ExpandedConfig = actualConfig manifest.ExpandedConfig = actualConfig
if err := m.repository.SetManifest(manifest); err != nil { if err := m.repository.SetManifest(manifest); err != nil {
log.Printf("SetManifest failed %v", err) log.Printf("SetManifest failed %v", err)
m.repository.SetDeploymentState(t.Name, failState(err)) m.repository.SetDeploymentState(depReq.Name, failState(err))
return nil, err return nil, err
} }
// Finally update the type instances for this deployment. // Finally update the type instances for this deployment.
m.setChartInstances(t.Name, manifest.Name, manifest.Layout) m.setChartInstances(depReq.Name, manifest.Name, manifest.Layout)
return m.repository.GetValidDeployment(t.Name) return m.repository.GetValidDeployment(depReq.Name)
}
func (m *manager) createManifest(t *common.Template) (*common.Manifest, error) {
et, err := m.expander.ExpandTemplate(t)
if err != nil {
log.Printf("Expansion failed %v", err)
return nil, err
}
return &common.Manifest{
Name: generateManifestName(),
Deployment: t.Name,
InputConfig: t,
ExpandedConfig: et.Config,
Layout: et.Layout,
}, nil
} }
func (m *manager) setChartInstances(deploymentName string, manifestName string, layout *common.Layout) { func (m *manager) setChartInstances(deploymentName string, manifestName string, layout *common.Layout) {
@ -284,13 +268,13 @@ func (m *manager) DeleteDeployment(name string, forget bool) (*common.Deployment
// PutDeployment replaces the configuration of the deployment with // PutDeployment replaces the configuration of the deployment with
// the supplied identifier in the cluster, and returns the deployment. // the supplied identifier in the cluster, and returns the deployment.
func (m *manager) PutDeployment(name string, t *common.Template) (*common.Deployment, error) { func (m *manager) PutDeployment(name string, depReq *common.DeploymentRequest) (*common.Deployment, error) {
_, err := m.repository.GetValidDeployment(name) _, err := m.repository.GetValidDeployment(name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
manifest, err := m.createManifest(t) manifest, err := m.Expand(depReq)
if err != nil { if err != nil {
log.Printf("Manifest creation failed: %v", err) log.Printf("Manifest creation failed: %v", err)
m.repository.SetDeploymentState(name, failState(err)) m.repository.SetDeploymentState(name, failState(err))
@ -311,20 +295,23 @@ func (m *manager) PutDeployment(name string, t *common.Template) (*common.Deploy
} }
// Finally update the type instances for this deployment. // Finally update the type instances for this deployment.
m.setChartInstances(t.Name, manifest.Name, manifest.Layout) m.setChartInstances(depReq.Name, manifest.Name, manifest.Layout)
return m.repository.GetValidDeployment(t.Name) return m.repository.GetValidDeployment(depReq.Name)
} }
func (m *manager) Expand(t *common.Template) (*common.Manifest, error) { func (m *manager) Expand(depReq *common.DeploymentRequest) (*common.Manifest, error) {
et, err := m.expander.ExpandTemplate(t) expConf, err := m.expander.ExpandConfiguration(&depReq.Configuration)
if err != nil { if err != nil {
log.Printf("Expansion failed %v", err) log.Printf("Expansion failed %v", err)
return nil, err return nil, err
} }
return &common.Manifest{ return &common.Manifest{
ExpandedConfig: et.Config, Name: generateManifestName(),
Layout: et.Layout, Deployment: depReq.Name,
InputConfig: &depReq.Configuration,
ExpandedConfig: expConf.Config,
Layout: expConf.Layout,
}, nil }, nil
} }
@ -386,7 +373,7 @@ func (m *manager) RemoveRepo(repoName string) error {
return m.service.DeleteRepo(repoURL) return m.service.DeleteRepo(repoURL)
} }
// GetRepo returns the repository with the given URL // GetRepo returns the repository with the given name
func (m *manager) GetRepo(repoName string) (repo.IRepo, error) { func (m *manager) GetRepo(repoName string) (repo.IRepo, error) {
repoURL, err := m.service.GetRepoURLByName(repoName) repoURL, err := m.service.GetRepoURLByName(repoName)
if err != nil { if err != nil {

@ -26,8 +26,6 @@ import (
"testing" "testing"
) )
var template = common.Template{Name: "test", Content: "test"}
var layout = common.Layout{ var layout = common.Layout{
Resources: []*common.LayoutResource{{Resource: common.Resource{Name: "test", Type: "test"}}}, Resources: []*common.LayoutResource{{Resource: common.Resource{Name: "test", Type: "test"}}},
} }
@ -47,7 +45,9 @@ var resourcesWithFailureState = common.Configuration{
}, },
}}, }},
} }
var expandedConfig = ExpandedTemplate{ var deploymentRequest = common.DeploymentRequest{Name: "test", Configuration: configuration}
var expandedConfig = ExpandedConfiguration{
Config: &configuration, Config: &configuration,
Layout: &layout, Layout: &layout,
} }
@ -70,8 +70,8 @@ var errTest = errors.New("test error")
type expanderStub struct{} type expanderStub struct{}
func (expander *expanderStub) ExpandTemplate(t *common.Template) (*ExpandedTemplate, error) { func (expander *expanderStub) ExpandConfiguration(conf *common.Configuration) (*ExpandedConfiguration, error) {
if reflect.DeepEqual(*t, template) { if reflect.DeepEqual(conf, &configuration) {
return &expandedConfig, nil return &expandedConfig, nil
} }
@ -333,25 +333,25 @@ func TestGetManifest(t *testing.T) {
func TestCreateDeployment(t *testing.T) { func TestCreateDeployment(t *testing.T) {
testRepository.reset() testRepository.reset()
testDeployer.reset() testDeployer.reset()
d, err := testManager.CreateDeployment(&template) d, err := testManager.CreateDeployment(&deploymentRequest)
if !reflect.DeepEqual(d, &deployment) || err != nil { if !reflect.DeepEqual(d, &deployment) || err != nil {
t.Fatalf("Expected a different set of response values from invoking CreateDeployment."+ t.Fatalf("Expected a different set of response values from invoking CreateDeployment."+
"Received: %v, %s. Expected: %#v, %s.", d, err, &deployment, "nil") "Received: %v, %s. Expected: %#v, %s.", d, err, &deployment, "nil")
} }
if testRepository.Created[0] != template.Name { if testRepository.Created[0] != deploymentRequest.Name {
t.Fatalf("Repository CreateDeployment was called with %s but expected %s.", t.Fatalf("Repository CreateDeployment was called with %s but expected %s.",
testRepository.Created[0], template.Name) testRepository.Created[0], deploymentRequest.Name)
} }
if !strings.HasPrefix(testRepository.ManifestAdd[template.Name].Name, "manifest-") { if !strings.HasPrefix(testRepository.ManifestAdd[deploymentRequest.Name].Name, "manifest-") {
t.Fatalf("Repository AddManifest was called with %s but expected manifest name"+ t.Fatalf("Repository AddManifest was called with %s but expected manifest name"+
"to begin with manifest-.", testRepository.ManifestAdd[template.Name].Name) "to begin with manifest-.", testRepository.ManifestAdd[deploymentRequest.Name].Name)
} }
if !strings.HasPrefix(testRepository.ManifestSet[template.Name].Name, "manifest-") { if !strings.HasPrefix(testRepository.ManifestSet[deploymentRequest.Name].Name, "manifest-") {
t.Fatalf("Repository SetManifest was called with %s but expected manifest name"+ t.Fatalf("Repository SetManifest was called with %s but expected manifest name"+
"to begin with manifest-.", testRepository.ManifestSet[template.Name].Name) "to begin with manifest-.", testRepository.ManifestSet[deploymentRequest.Name].Name)
} }
if !reflect.DeepEqual(*testDeployer.Created[0], configuration) || err != nil { if !reflect.DeepEqual(*testDeployer.Created[0], configuration) || err != nil {
@ -376,11 +376,11 @@ func TestCreateDeploymentCreationFailure(t *testing.T) {
testRepository.reset() testRepository.reset()
testDeployer.reset() testDeployer.reset()
testDeployer.FailCreate = true testDeployer.FailCreate = true
d, err := testManager.CreateDeployment(&template) d, err := testManager.CreateDeployment(&deploymentRequest)
if testRepository.Created[0] != template.Name { if testRepository.Created[0] != deploymentRequest.Name {
t.Fatalf("Repository CreateDeployment was called with %s but expected %s.", t.Fatalf("Repository CreateDeployment was called with %s but expected %s.",
testRepository.Created[0], template.Name) testRepository.Created[0], deploymentRequest.Name)
} }
if len(testRepository.Deleted) != 0 { if len(testRepository.Deleted) != 0 {
@ -406,11 +406,11 @@ func TestCreateDeploymentCreationResourceFailure(t *testing.T) {
testRepository.reset() testRepository.reset()
testDeployer.reset() testDeployer.reset()
testDeployer.FailCreateResource = true testDeployer.FailCreateResource = true
d, err := testManager.CreateDeployment(&template) d, err := testManager.CreateDeployment(&deploymentRequest)
if testRepository.Created[0] != template.Name { if testRepository.Created[0] != deploymentRequest.Name {
t.Fatalf("Repository CreateDeployment was called with %s but expected %s.", t.Fatalf("Repository CreateDeployment was called with %s but expected %s.",
testRepository.Created[0], template.Name) testRepository.Created[0], deploymentRequest.Name)
} }
if len(testRepository.Deleted) != 0 { if len(testRepository.Deleted) != 0 {
@ -422,14 +422,18 @@ func TestCreateDeploymentCreationResourceFailure(t *testing.T) {
t.Fatal("CreateDeployment failure did not mark deployment as failed") t.Fatal("CreateDeployment failure did not mark deployment as failed")
} }
if !strings.HasPrefix(testRepository.ManifestAdd[template.Name].Name, "manifest-") { if manifest, ok := testRepository.ManifestAdd[deploymentRequest.Name]; ok {
if !strings.HasPrefix(manifest.Name, "manifest-") {
t.Fatalf("Repository AddManifest was called with %s but expected manifest name"+ t.Fatalf("Repository AddManifest was called with %s but expected manifest name"+
"to begin with manifest-.", testRepository.ManifestAdd[template.Name].Name) "to begin with manifest-.", manifest.Name)
}
} }
if !strings.HasPrefix(testRepository.ManifestSet[template.Name].Name, "manifest-") { if manifest, ok := testRepository.ManifestSet[deploymentRequest.Name]; ok {
t.Fatalf("Repository SetManifest was called with %s but expected manifest name"+ if !strings.HasPrefix(manifest.Name, "manifest-") {
"to begin with manifest-.", testRepository.ManifestSet[template.Name].Name) t.Fatalf("Repository AddManifest was called with %s but expected manifest name"+
"to begin with manifest-.", manifest.Name)
}
} }
if err != nil || !reflect.DeepEqual(d, &deployment) { if err != nil || !reflect.DeepEqual(d, &deployment) {
@ -445,25 +449,25 @@ func TestCreateDeploymentCreationResourceFailure(t *testing.T) {
func TestDeleteDeploymentForget(t *testing.T) { func TestDeleteDeploymentForget(t *testing.T) {
testRepository.reset() testRepository.reset()
testDeployer.reset() testDeployer.reset()
d, err := testManager.CreateDeployment(&template) d, err := testManager.CreateDeployment(&deploymentRequest)
if !reflect.DeepEqual(d, &deployment) || err != nil { if !reflect.DeepEqual(d, &deployment) || err != nil {
t.Fatalf("Expected a different set of response values from invoking CreateDeployment."+ t.Fatalf("Expected a different set of response values from invoking CreateDeployment."+
"Received: %v, %s. Expected: %#v, %s.", d, err, &deployment, "nil") "Received: %v, %s. Expected: %#v, %s.", d, err, &deployment, "nil")
} }
if testRepository.Created[0] != template.Name { if testRepository.Created[0] != deploymentRequest.Name {
t.Fatalf("Repository CreateDeployment was called with %s but expected %s.", t.Fatalf("Repository CreateDeployment was called with %s but expected %s.",
testRepository.Created[0], template.Name) testRepository.Created[0], deploymentRequest.Name)
} }
if !strings.HasPrefix(testRepository.ManifestAdd[template.Name].Name, "manifest-") { if !strings.HasPrefix(testRepository.ManifestAdd[deploymentRequest.Name].Name, "manifest-") {
t.Fatalf("Repository AddManifest was called with %s but expected manifest name"+ t.Fatalf("Repository AddManifest was called with %s but expected manifest name"+
"to begin with manifest-.", testRepository.ManifestAdd[template.Name].Name) "to begin with manifest-.", testRepository.ManifestAdd[deploymentRequest.Name].Name)
} }
if !strings.HasPrefix(testRepository.ManifestSet[template.Name].Name, "manifest-") { if !strings.HasPrefix(testRepository.ManifestSet[deploymentRequest.Name].Name, "manifest-") {
t.Fatalf("Repository SetManifest was called with %s but expected manifest name"+ t.Fatalf("Repository SetManifest was called with %s but expected manifest name"+
"to begin with manifest-.", testRepository.ManifestSet[template.Name].Name) "to begin with manifest-.", testRepository.ManifestSet[deploymentRequest.Name].Name)
} }
if !reflect.DeepEqual(*testDeployer.Created[0], configuration) || err != nil { if !reflect.DeepEqual(*testDeployer.Created[0], configuration) || err != nil {
@ -492,21 +496,9 @@ func TestDeleteDeploymentForget(t *testing.T) {
} }
func TestExpand(t *testing.T) { func TestExpand(t *testing.T) {
m, err := testManager.Expand(&template) m, err := testManager.Expand(&deploymentRequest)
if err != nil { if err != nil {
t.Fatal("Failed to expand template into manifest.") t.Fatal("Failed to expand deployment request into manifest.")
}
if m.Name != "" {
t.Fatalf("Name was not empty: %v", *m)
}
if m.Deployment != "" {
t.Fatalf("Deployment was not empty: %v", *m)
}
if m.InputConfig != nil {
t.Fatalf("Input config not nil: %v", *m)
} }
if !reflect.DeepEqual(*m.ExpandedConfig, configuration) { if !reflect.DeepEqual(*m.ExpandedConfig, configuration) {

@ -83,7 +83,7 @@ func (m *mockManager) GetDeployment(name string) (*common.Deployment, error) {
return nil, errors.New("mock error: No such deployment") return nil, errors.New("mock error: No such deployment")
} }
func (m *mockManager) CreateDeployment(t *common.Template) (*common.Deployment, error) { func (m *mockManager) CreateDeployment(depReq *common.DeploymentRequest) (*common.Deployment, error) {
return &common.Deployment{}, nil return &common.Deployment{}, nil
} }
@ -91,7 +91,7 @@ func (m *mockManager) DeleteDeployment(name string, forget bool) (*common.Deploy
return &common.Deployment{}, nil return &common.Deployment{}, nil
} }
func (m *mockManager) PutDeployment(name string, t *common.Template) (*common.Deployment, error) { func (m *mockManager) PutDeployment(name string, depReq *common.DeploymentRequest) (*common.Deployment, error) {
return &common.Deployment{}, nil return &common.Deployment{}, nil
} }
@ -103,7 +103,7 @@ func (m *mockManager) GetManifest(deploymentName string, manifest string) (*comm
return &common.Manifest{}, nil return &common.Manifest{}, nil
} }
func (m *mockManager) Expand(t *common.Template) (*common.Manifest, error) { func (m *mockManager) Expand(depReq *common.DeploymentRequest) (*common.Manifest, error) {
return &common.Manifest{}, nil return &common.Manifest{}, nil
} }

@ -110,8 +110,9 @@ func (c *Client) DescribeDeployment(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(res *common.Resource) error { func (c *Client) PostDeployment(res *common.Resource) error {
// 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.CreateDeploymentRequest{ t := common.DeploymentRequest{
ChartInvocation: res, Configuration: common.Configuration{Resources: []*common.Resource{res}},
Name: res.Name,
} }
data, err := json.Marshal(t) data, err := json.Marshal(t)

@ -70,14 +70,15 @@ func (s DeploymentStatus) String() string {
type Manifest struct { type Manifest struct {
Deployment string `json:"deployment,omitempty"` Deployment string `json:"deployment,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
InputConfig *Template `json:"inputConfig,omitempty"` InputConfig *Configuration `json:"inputConfig,omitempty"`
ExpandedConfig *Configuration `json:"expandedConfig,omitempty"` ExpandedConfig *Configuration `json:"expandedConfig,omitempty"`
Layout *Layout `json:"layout,omitempty"` Layout *Layout `json:"layout,omitempty"`
} }
// CreateDeploymentRequest defines the manager API to create deployments. // DeploymentRequest defines the manager API to create deployments.
type CreateDeploymentRequest struct { type DeploymentRequest struct {
ChartInvocation *Resource `json:"chart_invocation"` Configuration
Name string `json:"name"`
} }
// ChartInstance defines the metadata for an instantiation of a chart. // ChartInstance defines the metadata for an instantiation of a chart.

Loading…
Cancel
Save