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

Append component params on generate

This commit will append both mandatory and optional  prototype
parameters to the component params.libsonnet file on `ks gen foo ...`.

Default values will be used for optional params where the user does not
specify flags to `ks gen foo ...`.

Because we are trying to append to jsonnet, we will have to traverse the
AST to first identify the location of where to insert the new component
params. New components will be inserted at the bottom of the components
object, with the params ordered alphabetically.
parent 995f7496
No related branches found
No related tags found
No related merge requests found
......@@ -229,7 +229,12 @@ var prototypePreviewCmd = &cobra.Command{
return fmt.Errorf("Incorrect number of arguments supplied to 'prototype preview'\n\n%s", cmd.UsageString())
}
text, err := expandPrototype(proto, flags, templateType, "preview")
params, err := getParameters(proto, flags)
if err != nil {
return err
}
text, err := expandPrototype(proto, templateType, params, "preview")
if err != nil {
return err
}
......@@ -320,12 +325,17 @@ var prototypeUseCmd = &cobra.Command{
return fmt.Errorf("'prototype use' has too many arguments (takes a prototype name and a component name)\n\n%s", cmd.UsageString())
}
text, err := expandPrototype(proto, flags, templateType, componentName)
params, err := getParameters(proto, flags)
if err != nil {
return err
}
return manager.CreateComponent(componentName, text, templateType)
text, err := expandPrototype(proto, templateType, params, componentName)
if err != nil {
return err
}
return manager.CreateComponent(componentName, text, params, templateType)
},
Long: `Expand prototype uniquely identified by (possibly partial) 'prototype-name',
filling in parameters from flags, and placing it into the file
......@@ -381,58 +391,58 @@ func bindPrototypeFlags(cmd *cobra.Command, proto *prototype.SpecificationSchema
}
}
func expandPrototype(proto *prototype.SpecificationSchema, flags *pflag.FlagSet, templateType prototype.TemplateType, componentName string) (string, error) {
func expandPrototype(proto *prototype.SpecificationSchema, templateType prototype.TemplateType, params map[string]string, componentName string) (string, error) {
template, err := proto.Template.Body(templateType)
if err != nil {
return "", err
}
if templateType == prototype.Jsonnet {
template = append([]string{`local params = std.extVar("` + metadata.ParamsExtCodeKey + `").components.` + componentName + ";"}, template...)
}
return jsonnet.Parse(componentName, strings.Join(template, "\n"))
}
func getParameters(proto *prototype.SpecificationSchema, flags *pflag.FlagSet) (map[string]string, error) {
missingReqd := prototype.ParamSchemas{}
values := map[string]string{}
for _, param := range proto.RequiredParams() {
val, err := flags.GetString(param.Name)
if err != nil {
return "", err
return nil, err
} else if val == "" {
missingReqd = append(missingReqd, param)
} else if _, ok := values[param.Name]; ok {
return "", fmt.Errorf("Prototype '%s' has multiple parameters with name '%s'", proto.Name, param.Name)
return nil, fmt.Errorf("Prototype '%s' has multiple parameters with name '%s'", proto.Name, param.Name)
}
quoted, err := param.Quote(val)
if err != nil {
return "", err
return nil, err
}
values[param.Name] = quoted
}
if len(missingReqd) > 0 {
return "", fmt.Errorf("Failed to instantiate prototype '%s'. The following required parameters are missing:\n%s", proto.Name, missingReqd.PrettyString(""))
return nil, fmt.Errorf("Failed to instantiate prototype '%s'. The following required parameters are missing:\n%s", proto.Name, missingReqd.PrettyString(""))
}
for _, param := range proto.OptionalParams() {
val, err := flags.GetString(param.Name)
if err != nil {
return "", err
return nil, err
} else if _, ok := values[param.Name]; ok {
return "", fmt.Errorf("Prototype '%s' has multiple parameters with name '%s'", proto.Name, param.Name)
return nil, fmt.Errorf("Prototype '%s' has multiple parameters with name '%s'", proto.Name, param.Name)
}
quoted, err := param.Quote(val)
if err != nil {
return "", err
return nil, err
}
values[param.Name] = quoted
}
template, err := proto.Template.Body(templateType)
if err != nil {
return "", err
}
template = append([]string{`local params = std.extVar("__ksonnet/params").components.` + componentName + ";"}, template...)
tm, err := jsonnet.Parse(componentName, strings.Join(template, "\n"))
text, err := tm.Evaluate(values)
if err != nil {
return "", err
}
return text, nil
return values, nil
}
func fundUniquePrototype(query string) (*prototype.SpecificationSchema, error) {
......
......@@ -42,7 +42,7 @@ type AbsPaths []string
type Manager interface {
Root() AbsPath
ComponentPaths() (AbsPaths, error)
CreateComponent(name string, text string, templateType prototype.TemplateType) error
CreateComponent(name string, text string, params map[string]string, templateType prototype.TemplateType) error
LibPaths(envName string) (libPath, envLibPath, envComponentPath, envParamsPath AbsPath)
CreateEnvironment(name, uri, namespace string, spec ClusterSpec) error
DeleteEnvironment(name string) error
......
......@@ -22,6 +22,7 @@ import (
"path/filepath"
"strings"
"github.com/ksonnet/ksonnet/metadata/snippet"
"github.com/ksonnet/ksonnet/prototype"
log "github.com/sirupsen/logrus"
"github.com/spf13/afero"
......@@ -154,7 +155,7 @@ func (m *manager) ComponentPaths() (AbsPaths, error) {
return paths, nil
}
func (m *manager) CreateComponent(name string, text string, templateType prototype.TemplateType) error {
func (m *manager) CreateComponent(name string, text string, params map[string]string, templateType prototype.TemplateType) error {
if !isValidName(name) || strings.Contains(name, "/") {
return fmt.Errorf("Component name '%s' is not valid; must not contain punctuation, spaces, or begin or end with a slash", name)
}
......@@ -178,8 +179,13 @@ func (m *manager) CreateComponent(name string, text string, templateType prototy
}
log.Infof("Writing component at '%s/%s'", componentsDir, name)
err := afero.WriteFile(m.appFS, componentPath, []byte(text), defaultFilePermissions)
if err != nil {
return err
}
return afero.WriteFile(m.appFS, componentPath, []byte(text), defaultFilePermissions)
log.Debugf("Writing component parameters at '%s/%s", componentsDir, name)
return m.writeComponentParams(name, params)
}
func (m *manager) LibPaths(envName string) (libPath, envLibPath, envComponentPath, envParamsPath AbsPath) {
......@@ -234,6 +240,20 @@ func (m *manager) createAppDirTree() error {
return nil
}
func (m *manager) writeComponentParams(componentName string, params map[string]string) error {
text, err := afero.ReadFile(m.appFS, string(m.componentParamsPath))
if err != nil {
return err
}
appended, err := snippet.AppendComponent(componentName, string(text), params)
if err != nil {
return err
}
return afero.WriteFile(m.appFS, string(m.componentParamsPath), []byte(appended), defaultFilePermissions)
}
func genComponentParamsContent() []byte {
return []byte(`{
global: {
......
// Copyright 2017 The kubecfg authors
//
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package snippet
// AppendComponent takes the following params
//
// component: the name of the new component to be added.
// snippet: a jsonnet snippet resembling the current component parameters.
// params: the parameters for the new component.
//
// and returns the jsonnet snippet with the appended component and parameters.
func AppendComponent(component, snippet string, params map[string]string) (string, error) {
return appendComponent(component, snippet, params)
}
// Copyright 2017 The kubecfg authors
//
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package snippet
import (
"bytes"
"fmt"
"sort"
"strings"
"github.com/google/go-jsonnet/ast"
"github.com/google/go-jsonnet/parser"
)
const (
componentsID = "components"
)
func visitComponentsObj(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
}
}
default:
return nil, fmt.Errorf("Expected node type to be object")
}
// 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 writeParams(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 {
keys = append(keys, key)
}
sort.Strings(keys)
var buffer bytes.Buffer
buffer.WriteString("\n")
for i, key := range keys {
buffer.WriteString(fmt.Sprintf(" %s: %s,", key, params[key]))
if i < len(keys)-1 {
buffer.WriteString("\n")
}
}
buffer.WriteString("\n")
return buffer.String()
}
func appendComponent(component, snippet string, params map[string]string) (string, error) {
componentsNode, err := visitComponentsObj(component, snippet)
if err != nil {
return "", err
}
// Find the location to append the next component
switch n := (*componentsNode).(type) {
case *ast.Object:
// Ensure that the component we are trying to create params for does not already exist.
for _, field := range n.Fields {
if field.Id != nil && string(*field.Id) == component {
return "", fmt.Errorf("Component parameters for '%s' already exists", component)
}
}
default:
return "", fmt.Errorf("Expected components node type to be object")
}
lines := strings.Split(snippet, "\n")
// Get an alphabetically sorted list of the param keys
keys := make([]string, 0, len(params))
for key := range params {
keys = append(keys, key)
}
sort.Strings(keys)
// Create the jsonnet resembling the component params
var buffer bytes.Buffer
buffer.WriteString(" " + component + ": {")
buffer.WriteString(writeParams(params))
buffer.WriteString(" },")
// Insert the new component to the end of the list of components
insertLine := (*componentsNode).Loc().End.Line - 1
lines = append(lines, "")
copy(lines[insertLine+1:], lines[insertLine:])
lines[insertLine] = buffer.String()
return strings.Join(lines, "\n"), nil
}
// Copyright 2017 The kubecfg authors
//
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package snippet
import (
"testing"
)
func TestAppendComponentParams(t *testing.T) {
tests := []struct {
componentName string
jsonnet string
params map[string]string
expected string
}{
// Test case with existing components
{
"baz",
`
{
global: {
// User-defined global parameters; accessible to all component and environments, Ex:
// replicas: 4,
},
components: {
// Component-level parameters, defined initially from 'ks prototype use ...'
// Each object below should correspond to a component in the components/ directory
foo: {
name: "foo",
replicas: 1,
},
bar: {
name: "bar",
},
},
}`,
map[string]string{"replicas": "5", "name": `"baz"`},
`
{
global: {
// User-defined global parameters; accessible to all component and environments, Ex:
// replicas: 4,
},
components: {
// Component-level parameters, defined initially from 'ks prototype use ...'
// Each object below should correspond to a component in the components/ directory
foo: {
name: "foo",
replicas: 1,
},
bar: {
name: "bar",
},
baz: {
name: "baz",
replicas: 5,
},
},
}`,
},
// Test case with no existing components
{
"baz",
`
{
global: {
// User-defined global parameters; accessible to all component and environments, Ex:
// replicas: 4,
},
components: {
// Component-level parameters, defined initially from 'ks prototype use ...'
// Each object below should correspond to a component in the components/ directory
},
}`,
map[string]string{"replicas": "5", "name": `"baz"`},
`
{
global: {
// User-defined global parameters; accessible to all component and environments, Ex:
// replicas: 4,
},
components: {
// Component-level parameters, defined initially from 'ks prototype use ...'
// Each object below should correspond to a component in the components/ directory
baz: {
name: "baz",
replicas: 5,
},
},
}`,
},
}
errors := []struct {
componentName string
jsonnet string
params map[string]string
}{
// Test case where there isn't a components object
{
"baz",
`
{
global: {
// User-defined global parameters; accessible to all component and environments, Ex:
// replicas: 4,
},
}`,
map[string]string{"replicas": "5", "name": `"baz"`},
},
// Test case where components isn't a top level object
{
"baz",
`
{
global: {
// User-defined global parameters; accessible to all component and environments, Ex:
// replicas: 4,
components: {},
},
}`,
map[string]string{"replicas": "5", "name": `"baz"`},
},
// Test case where component already exists
{
"baz",
`
{
global: {
// User-defined global parameters; accessible to all component and environments, Ex:
// replicas: 4,
},
components: {
// Component-level parameters, defined initially from 'ks prototype use ...'
// Each object below should correspond to a component in the components/ directory
baz: {
name: "baz",
replicas: 5,
},
},
}`,
map[string]string{"replicas": "5", "name": `"baz"`},
},
}
for _, s := range tests {
parsed, err := AppendComponent(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 := AppendComponent(e.componentName, e.jsonnet, e.params)
if err == nil {
t.Errorf("Expected error but not found\n input: %v got: %v", e, parsed)
}
}
}
......@@ -22,8 +22,6 @@ import (
"github.com/google/go-jsonnet/ast"
"github.com/google/go-jsonnet/parser"
"github.com/ksonnet/ksonnet/prototype/snippet"
)
const (
......@@ -31,15 +29,14 @@ const (
paramReplacementPrefix = "params."
)
// Parse rewrites the imports in a Jsonnet file before returning the parsed
// TextMate snippet.
func Parse(fn string, jsonnet string) (snippet.Template, error) {
// Parse rewrites the imports in a Jsonnet file before returning the snippet.
func Parse(fn string, jsonnet string) (string, error) {
s, err := parse(fn, jsonnet)
if err != nil {
return nil, err
return "", err
}
return snippet.Parse(s), nil
return s, nil
}
func parse(fn string, jsonnet string) (string, error) {
......
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