Skip to content
Snippets Groups Projects
Commit 173e0082 authored by Jessica Yuen's avatar Jessica Yuen
Browse files

Handle writing of environment params

This commit will add an interface `SetEnvironmentParams` to
metadata.Manager that allows setting of env params.

Also implements the logic for parsing the jsonnet snippet to
append / modify params in simple env param schemas. It will:

1. Update the params if the component exists in the jsonnet
snippet.
2. Add the component if it does not exist.
parent ba201148
No related branches found
No related tags found
No related merge requests found
......@@ -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()
......
......@@ -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)
}
......
......@@ -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)
}
......@@ -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
}
......@@ -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)
}
}
}
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