diff --git a/metadata/environment.go b/metadata/environment.go index 2bf4b6fd01bc5ea0a68c295892661eaae6c835fd..9ce084c0ec6bd46b68a0fa24ec7bf1ad1b35d383 100644 --- a/metadata/environment.go +++ b/metadata/environment.go @@ -29,6 +29,7 @@ import ( "github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet" "github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec" + "github.com/ksonnet/ksonnet/metadata/snippet" ) const ( @@ -342,6 +343,36 @@ func (m *manager) SetEnvironment(name string, desired *Environment) error { return nil } +func (m *manager) SetEnvironmentParams(env, component string, params map[string]string) error { + exists, err := m.environmentExists(env) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("Environment '%s' does not exist", env) + } + + path := appendToAbsPath(m.environmentsPath, env, paramsFileName) + + text, err := afero.ReadFile(m.appFS, string(path)) + if err != nil { + return err + } + + appended, err := snippet.SetEnvironmentParams(component, string(text), params) + if err != nil { + return err + } + + err = afero.WriteFile(m.appFS, string(path), []byte(appended), defaultFilePermissions) + if err != nil { + return err + } + + log.Debugf("Successfully set parameters for component '%s' at environment '%s'", component, env) + return nil +} + func (m *manager) generateKsonnetLibData(spec ClusterSpec) ([]byte, []byte, []byte, error) { // Get cluster specification data, possibly from the network. text, err := spec.data() diff --git a/metadata/interface.go b/metadata/interface.go index d4f5495b16a3bd6e47c098ccd0eb5d23b7d6097a..8aa52749f7be7504c869e5b4c97c5b473d285173 100644 --- a/metadata/interface.go +++ b/metadata/interface.go @@ -51,6 +51,7 @@ type Manager interface { GetEnvironments() ([]*Environment, error) GetEnvironment(name string) (*Environment, error) SetEnvironment(name string, desired *Environment) error + SetEnvironmentParams(env, component string, params map[string]string) error // // TODO: Fill in methods as we need them. @@ -58,7 +59,6 @@ type Manager interface { // GetPrototype(id string) Protoype // SearchPrototypes(query string) []Protoype // VendorLibrary(uri, version string) error - // SetEnvironmentParams(component, env string, params map[string]string) error // GetEnvironmentParams(env string) (map[string]map[string]string, error) } diff --git a/metadata/snippet/interface.go b/metadata/snippet/interface.go index 7d6175cc8d33148502f7b5abe5759d8b8dea8ce7..0bff1d6ad218fca7793330eb43ea6c848c7f09a5 100644 --- a/metadata/snippet/interface.go +++ b/metadata/snippet/interface.go @@ -49,3 +49,14 @@ func GetComponentParams(component, snippet string) (map[string]string, error) { func SetComponentParams(component, snippet string, params map[string]string) (string, error) { return setComponentParams(component, snippet, params) } + +// SetEnvironmentParams takes +// +// component: the name of the new component to be modified. +// snippet: a jsonnet snippet resembling the current environment parameters (not expanded). +// params: the parameters to be set for 'component'. +// +// and returns the jsonnet snippet with the modified set of environment parameters. +func SetEnvironmentParams(component, snippet string, params map[string]string) (string, error) { + return setEnvironmentParams(component, snippet, params) +} diff --git a/metadata/snippet/params.go b/metadata/snippet/params.go index c63917eb83d2eb32916738d93b85e96f952cbf35..94e530f7f85d2741179eaad2f3efa85ed4842408 100644 --- a/metadata/snippet/params.go +++ b/metadata/snippet/params.go @@ -30,30 +30,16 @@ const ( componentsID = "components" ) -func visitComponentsObj(component, snippet string) (*ast.Node, error) { +func astRoot(component, snippet string) (ast.Node, error) { tokens, err := parser.Lex(component, snippet) if err != nil { return nil, err } - root, err := parser.Parse(tokens) - if err != nil { - return nil, err - } - - switch n := root.(type) { - case *ast.Object: - for _, field := range n.Fields { - if field.Id != nil && *field.Id == componentsID { - return &field.Expr2, nil - } - } - } - // If this point has been reached, it means we weren't able to find a top-level components object. - return nil, fmt.Errorf("Invalid format; expected to find a top-level components object") + return parser.Parse(tokens) } -func visitComponentParams(component ast.Node) (map[string]string, *ast.LocationRange, error) { +func visitParams(component ast.Node) (map[string]string, *ast.LocationRange, error) { params := make(map[string]string) var loc *ast.LocationRange @@ -88,11 +74,11 @@ func visitParamValue(param ast.Node) (string, error) { case *ast.LiteralString: return fmt.Sprintf(`"%s"`, n.Value), nil default: - return "", fmt.Errorf("Found an unsupported param value type: %T", n) + return "", fmt.Errorf("Found an unsupported param AST node type: %T", n) } } -func writeParams(params map[string]string) string { +func writeParams(indent int, params map[string]string) string { // keys maintains an alphabetically sorted list of the param keys keys := make([]string, 0, len(params)) for key := range params { @@ -100,10 +86,15 @@ func writeParams(params map[string]string) string { } sort.Strings(keys) + var indentBuffer bytes.Buffer + for i := 0; i < indent; i++ { + indentBuffer.WriteByte(' ') + } + var buffer bytes.Buffer buffer.WriteString("\n") for i, key := range keys { - buffer.WriteString(fmt.Sprintf(" %s: %s,", key, params[key])) + buffer.WriteString(fmt.Sprintf("%s%s: %s,", indentBuffer.String(), key, params[key])) if i < len(keys)-1 { buffer.WriteString("\n") } @@ -112,6 +103,27 @@ func writeParams(params map[string]string) string { return buffer.String() } +// --------------------------------------------------------------------------- +// Component Parameter-specific functionality + +func visitComponentsObj(component, snippet string) (*ast.Node, error) { + root, err := astRoot(component, snippet) + if err != nil { + return nil, err + } + + switch n := root.(type) { + case *ast.Object: + for _, field := range n.Fields { + if field.Id != nil && *field.Id == componentsID { + return &field.Expr2, nil + } + } + } + // If this point has been reached, it means we weren't able to find a top-level components object. + return nil, fmt.Errorf("Invalid format; expected to find a top-level components object") +} + func appendComponent(component, snippet string, params map[string]string) (string, error) { componentsNode, err := visitComponentsObj(component, snippet) if err != nil { @@ -136,7 +148,7 @@ func appendComponent(component, snippet string, params map[string]string) (strin // Create the jsonnet resembling the component params var buffer bytes.Buffer buffer.WriteString(" " + component + ": {") - buffer.WriteString(writeParams(params)) + buffer.WriteString(writeParams(6, params)) buffer.WriteString(" },") // Insert the new component to the end of the list of components @@ -158,7 +170,7 @@ func getComponentParams(component, snippet string) (map[string]string, *ast.Loca case *ast.Object: for _, field := range n.Fields { if field.Id != nil && string(*field.Id) == component { - return visitComponentParams(field.Expr2) + return visitParams(field.Expr2) } } default: @@ -182,10 +194,90 @@ func setComponentParams(component, snippet string, params map[string]string) (st // Replace the component param fields lines := strings.Split(snippet, "\n") - paramsSnippet := writeParams(params) + paramsSnippet := writeParams(6, params) + newSnippet := strings.Join(lines[:loc.Begin.Line], "\n") + paramsSnippet + strings.Join(lines[loc.End.Line-1:], "\n") + + return newSnippet, nil +} + +// --------------------------------------------------------------------------- +// Environment Parameter-specific functionality + +func findEnvComponentsObj(node ast.Node) (ast.Node, error) { + switch n := node.(type) { + case *ast.Local: + return findEnvComponentsObj(n.Body) + case *ast.Binary: + return findEnvComponentsObj(n.Right) + case *ast.Object: + for _, f := range n.Fields { + if *f.Id == "components" { + return f.Expr2, nil + } + } + return nil, fmt.Errorf("Invalid params schema -- found %T that is not 'components'", n) + } + return nil, fmt.Errorf("Invalid params schema -- did not expect type: %T", node) +} + +func getEnvironmentParams(component, snippet string) (map[string]string, *ast.LocationRange, bool, error) { + root, err := astRoot(component, snippet) + if err != nil { + return nil, nil, false, err + } + + componentsNode, err := findEnvComponentsObj(root) + if err != nil { + return nil, nil, false, err + } + + switch n := componentsNode.(type) { + case *ast.Object: + for _, f := range n.Fields { + if f.Id != nil && string(*f.Id) == component { + params, loc, err := visitParams(f.Expr2) + return params, loc, true, err + } + } + // If this point has been reached, it's because we don't have the + // component in the list of params, return the location after the + // last field of the components obj + loc := ast.LocationRange{ + Begin: ast.Location{Line: n.Loc().End.Line - 1, Column: n.Loc().End.Column}, + End: ast.Location{Line: n.Loc().End.Line, Column: n.Loc().End.Column}, + } + + return make(map[string]string), &loc, false, nil + } + + return nil, nil, false, fmt.Errorf("Could not find component identifier '%s' when attempting to set params", component) +} + +func setEnvironmentParams(component, snippet string, params map[string]string) (string, error) { + currentParams, loc, hasComponent, err := getEnvironmentParams(component, snippet) + if err != nil { + return "", err + } + + for k, v := range currentParams { + if _, ok := params[k]; !ok { + params[k] = v + } + } + + // Replace the component param fields + var paramsSnippet string + lines := strings.Split(snippet, "\n") + if !hasComponent { + var buffer bytes.Buffer + buffer.WriteString(fmt.Sprintf("\n %s +: {", component)) + buffer.WriteString(writeParams(6, params)) + buffer.WriteString(" },\n") + paramsSnippet = buffer.String() + } else { + paramsSnippet = writeParams(6, params) + } newSnippet := strings.Join(lines[:loc.Begin.Line], "\n") + paramsSnippet + strings.Join(lines[loc.End.Line-1:], "\n") - //newSnippet := append(lines[:loc.Begin.Line], paramsSnippet) - //newSnippet = append(newSnippet, strings.Join(lines[loc.End.Line-1:], "\n")) return newSnippet, nil } diff --git a/metadata/snippet/params_test.go b/metadata/snippet/params_test.go index 88765d70fcd7eed01cb20426fdeb39a4955b5b6d..e3a38156a1bbbed25e4938820228931f4dc29590 100644 --- a/metadata/snippet/params_test.go +++ b/metadata/snippet/params_test.go @@ -402,3 +402,127 @@ func TestSetComponentParams(t *testing.T) { } } } + +func TestSetEnvironmentParams(t *testing.T) { + tests := []struct { + componentName string + jsonnet string + params map[string]string + expected string + }{ + // Test environment param case + { + "foo", + ` +local params = import "/fake/path"; +params + { + components +: { + foo +: { + name: "foo", + replicas: 1, + }, + }, +}`, + map[string]string{"replicas": "5"}, + ` +local params = import "/fake/path"; +params + { + components +: { + foo +: { + name: "foo", + replicas: 5, + }, + }, +}`, + }, + // Test environment param case with multiple components + { + "foo", + ` +local params = import "/fake/path"; +params + { + components +: { + bar +: { + name: "bar", + replicas: 1, + }, + foo +: { + name: "foo", + replicas: 1, + }, + }, +}`, + map[string]string{"name": `"foobar"`, "replicas": "5"}, + ` +local params = import "/fake/path"; +params + { + components +: { + bar +: { + name: "bar", + replicas: 1, + }, + foo +: { + name: "foobar", + replicas: 5, + }, + }, +}`, + }, + // Test setting environment param case where component isn't in the snippet + { + "foo", + ` +local params = import "/fake/path"; +params + { + components +: { + }, +}`, + map[string]string{"replicas": "5"}, + ` +local params = import "/fake/path"; +params + { + components +: { + foo +: { + replicas: 5, + }, + }, +}`, + }, + } + + errors := []struct { + componentName string + jsonnet string + params map[string]string + }{ + // Test bad schema + { + "foo", + ` +local params = import "/fake/path"; +params + { + badobj +: { + }, +}`, + map[string]string{"replicas": "5"}, + }, + } + + for _, s := range tests { + parsed, err := SetEnvironmentParams(s.componentName, s.jsonnet, s.params) + if err != nil { + t.Errorf("Unexpected error\n input: %v\n error: %v", s.jsonnet, err) + } + + if parsed != s.expected { + t.Errorf("Wrong conversion\n expected:%v\n got:%v", s.expected, parsed) + } + } + + for _, e := range errors { + parsed, err := SetEnvironmentParams(e.componentName, e.jsonnet, e.params) + if err == nil { + t.Errorf("Expected error but not found\n input: %v got: %v", e, parsed) + } + } +}