Skip to content
Snippets Groups Projects
Commit 64f78b42 authored by Alex Clemmer's avatar Alex Clemmer
Browse files

Implement 'prototype preview'

Currently the command 'prototype use' expands a prototype and prints to
stdout. This is useful, but most of the time, users want to simply dump
the result in 'components/'

This command implements this print-to-stdout behavior in a new command,
'prototype-preview', and reimplements 'prototype use' to drop the
expanded prototype into 'components/'.

The new form of this command is:

  ksonnet prototype use <prototype-name> <component-name> [type] [flags]

So, for example, a command like:

  ksonnet prototype use deployment nginx-depl [...]

would expand the 'deployment' prototype, and place it in
'components/nginx-depl.jsonnet' (since Jsonnet is the default template
expansion). Alternatively, something like this:

  ksonnet prototype use deployment nginx-depl yaml [...]

would expand the prototype and place it in 'components/nginx-depl.yaml'
(assuming that there is a YAML version of this template.
parent 4c3488eb
No related branches found
No related tags found
No related merge requests found
...@@ -17,8 +17,12 @@ package cmd ...@@ -17,8 +17,12 @@ package cmd
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/spf13/pflag"
"github.com/ksonnet/kubecfg/metadata"
"github.com/ksonnet/kubecfg/prototype" "github.com/ksonnet/kubecfg/prototype"
"github.com/ksonnet/kubecfg/prototype/snippet" "github.com/ksonnet/kubecfg/prototype/snippet"
"github.com/spf13/cobra" "github.com/spf13/cobra"
...@@ -29,6 +33,7 @@ func init() { ...@@ -29,6 +33,7 @@ func init() {
prototypeCmd.AddCommand(prototypeDescribeCmd) prototypeCmd.AddCommand(prototypeDescribeCmd)
prototypeCmd.AddCommand(prototypeSearchCmd) prototypeCmd.AddCommand(prototypeSearchCmd)
prototypeCmd.AddCommand(prototypeUseCmd) prototypeCmd.AddCommand(prototypeUseCmd)
prototypeCmd.AddCommand(prototypePreviewCmd)
} }
var prototypeCmd = &cobra.Command{ var prototypeCmd = &cobra.Command{
...@@ -160,13 +165,13 @@ var prototypeSearchCmd = &cobra.Command{ ...@@ -160,13 +165,13 @@ var prototypeSearchCmd = &cobra.Command{
ksonnet prototype search deployment`, ksonnet prototype search deployment`,
} }
var prototypeUseCmd = &cobra.Command{ var prototypePreviewCmd = &cobra.Command{
Use: "use <prototype-name> [type] [parameter-flags]", Use: "preview <prototype-name> [type] [parameter-flags]",
Short: `Instantiate prototype, emitting the generated code to stdout.`, Short: `Expand prototype, emitting the generated code to stdout`,
DisableFlagParsing: true, DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, rawArgs []string) error { RunE: func(cmd *cobra.Command, rawArgs []string) error {
if len(rawArgs) < 1 { if len(rawArgs) < 1 {
return fmt.Errorf("Command 'prototype use' requires a prototype name\n\n%s", cmd.UsageString()) return fmt.Errorf("Command 'prototype preview' requires a prototype name\n\n%s", cmd.UsageString())
} }
query := rawArgs[0] query := rawArgs[0]
...@@ -176,13 +181,7 @@ var prototypeUseCmd = &cobra.Command{ ...@@ -176,13 +181,7 @@ var prototypeUseCmd = &cobra.Command{
return err return err
} }
for _, param := range proto.RequiredParams() { bindPrototypeFlags(cmd, proto)
cmd.PersistentFlags().String(param.Name, "", param.Description)
}
for _, param := range proto.OptionalParams() {
cmd.PersistentFlags().String(param.Name, *param.Default, param.Description)
}
cmd.DisableFlagParsing = false cmd.DisableFlagParsing = false
err = cmd.ParseFlags(rawArgs) err = cmd.ParseFlags(rawArgs)
...@@ -203,82 +202,214 @@ var prototypeUseCmd = &cobra.Command{ ...@@ -203,82 +202,214 @@ var prototypeUseCmd = &cobra.Command{
return err return err
} }
} else { } else {
return fmt.Errorf("Incorrect number of arguments supplied to 'prototype use'\n\n%s", cmd.UsageString()) return fmt.Errorf("Incorrect number of arguments supplied to 'prototype preview'\n\n%s", cmd.UsageString())
} }
missingReqd := prototype.ParamSchemas{} text, err := expandPrototype(proto, flags, templateType)
values := map[string]string{} if err != nil {
for _, param := range proto.RequiredParams() { return err
val, err := flags.GetString(param.Name) }
if err != nil {
return 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)
}
quoted, err := param.Quote(val) fmt.Println(text)
if err != nil { return nil
return err },
} Long: `Expand prototype uniquely identified by (possibly partial)
values[param.Name] = quoted 'prototype-name', filling in parameters from flags, and emitting the generated
code to stdout.
Note also that 'prototype-name' need only contain enough of the suffix of a name
to uniquely disambiguate it among known names. For example, 'deployment' may
resolve ambiguously, in which case 'use' will fail, while 'deployment' might be
unique enough to resolve to 'io.ksonnet.pkg.single-port-deployment'.`,
Example: ` # Preview prototype 'io.ksonnet.pkg.single-port-deployment', using the
# 'nginx' image, and port 80 exposed.
ksonnet prototype preview io.ksonnet.pkg.prototype.simple-deployment \
--name=nginx \
--image=nginx
# Preview prototype using a unique suffix of an identifier. See
# introduction of help message for more information on how this works.
ksonnet prototype preview simple-deployment \
--name=nginx \
--image=nginx
# Preview prototype 'io.ksonnet.pkg.single-port-deployment' as YAML,
# placing the result in 'components/nginx-depl.yaml. Note that some templates
# do not have a YAML or JSON versions.
ksonnet prototype preview deployment nginx-depl yaml \
--name=nginx \
--image=nginx`,
}
var prototypeUseCmd = &cobra.Command{
Use: "use <prototype-name> <componentName> [type] [parameter-flags]",
Short: `Expand prototype, place in components/ directory of ksonnet app`,
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, rawArgs []string) error {
cwd, err := os.Getwd()
if err != nil {
return err
}
manager, err := metadata.Find(metadata.AbsPath(cwd))
if err != nil {
return fmt.Errorf("'prototype use' can only be run in a ksonnet application directory:\n\n%v", err)
} }
if len(missingReqd) > 0 { if len(rawArgs) < 1 {
return fmt.Errorf("Failed to instantiate prototype '%s'. The following required parameters are missing:\n%s", proto.Name, missingReqd.PrettyString("")) return fmt.Errorf("Command 'prototype preview' requires a prototype name\n\n%s", cmd.UsageString())
} }
for _, param := range proto.OptionalParams() { query := rawArgs[0]
val, err := flags.GetString(param.Name)
if err != nil {
return err
} else if _, ok := values[param.Name]; ok {
return fmt.Errorf("Prototype '%s' has multiple parameters with name '%s'", proto.Name, param.Name)
}
quoted, err := param.Quote(val) proto, err := fundUniquePrototype(query)
if err != nil { if err != nil {
return err return err
}
values[param.Name] = quoted
} }
template, err := proto.Template.Body(templateType) bindPrototypeFlags(cmd, proto)
cmd.DisableFlagParsing = false
err = cmd.ParseFlags(rawArgs)
if err != nil { if err != nil {
return err return err
} }
flags := cmd.Flags()
tm := snippet.Parse(strings.Join(template, "\n")) // Try to find the template type (if it is supplied) after the args are
text, err := tm.Evaluate(values) // parsed. Note that the case that `len(args) == 0` is handled at the
// beginning of this command.
var componentName string
var templateType prototype.TemplateType
if args := flags.Args(); len(args) == 1 {
return fmt.Errorf("'prototype use' is missing argument 'componentName'\n\n%s", cmd.UsageString())
} else if len(args) == 2 {
componentName = args[1]
templateType = prototype.Jsonnet
} else if len(args) == 3 {
componentName = args[1]
templateType, err = prototype.ParseTemplateType(args[1])
if err != nil {
return err
}
} else {
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)
if err != nil { if err != nil {
return err return err
} }
fmt.Println(text)
return nil return manager.CreateComponent(componentName, text, templateType)
}, },
Long: `Instantiate prototype uniquely identified by (possibly partial) Long: `Expand prototype uniquely identified by (possibly partial) 'prototype-name',
'prototype-name', filling in parameters from flags, and emitting the generated filling in parameters from flags, and placing it into the file
code to stdout. 'components/componentName', with the appropriate extension set. For example, the
following command will expand template 'io.ksonnet.pkg.single-port-deployment'
and place it in the file 'components/nginx-depl.jsonnet' (since by default
ksonnet will expand templates as Jsonnet).
ksonnet prototype use io.ksonnet.pkg.single-port-deployment nginx-depl \
--name=nginx \
--image=nginx
'prototype-name' need only contain enough of the suffix of a name to uniquely Note that if we were to specify to expand the template as JSON or YAML, we would
disambiguate it among known names. For example, 'deployment' may resolve generate a file with a '.json' or '.yaml' extension, respectively. See examples
ambiguously, in which case 'use' will fail, while 'simple-deployment' might be below for an example of how to do this.
unique enough to resolve to 'io.ksonnet.pkg.prototype.simple-deployment'.`,
Example: ` # Instantiate prototype 'io.ksonnet.pkg.prototype.simple-deployment', using Note also that 'prototype-name' need only contain enough of the suffix of a name
# the 'nginx' image, and port 80 exposed. to uniquely disambiguate it among known names. For example, 'deployment' may
ksonnet prototype use io.ksonnet.pkg.prototype.simple-deployment \ resolve ambiguously, in which case 'use' will fail, while 'deployment' might be
--name=nginx \ unique enough to resolve to 'io.ksonnet.pkg.single-port-deployment'.`,
Example: ` # Instantiate prototype 'io.ksonnet.pkg.single-port-deployment', using the
# 'nginx' image. The expanded prototype is placed in
# 'components/nginx-depl.jsonnet'.
ksonnet prototype use io.ksonnet.pkg.prototype.simple-deployment nginx-depl \
--name=nginx \
--image=nginx --image=nginx
# Instantiate prototype using a unique suffix of an identifier. See # Instantiate prototype 'io.ksonnet.pkg.single-port-deployment' using the
# introduction of help message for more information on how this works. # unique suffix, 'deployment'. The expanded prototype is again placed in
ksonnet prototype use simple-deployment \ # 'components/nginx-depl.jsonnet'. See introduction of help message for more
--name=nginx \ # information on how this works. Note that if you have imported another
# prototype with this suffix, this may resolve ambiguously for you.
ksonnet prototype use deployment nginx-depl \
--name=nginx \
--image=nginx
# Instantiate prototype 'io.ksonnet.pkg.single-port-deployment' as YAML,
# placing the result in 'components/nginx-depl.yaml. Note that some templates
# do not have a YAML or JSON versions.
ksonnet prototype use deployment nginx-depl yaml \
--name=nginx \
--image=nginx`, --image=nginx`,
} }
func bindPrototypeFlags(cmd *cobra.Command, proto *prototype.SpecificationSchema) {
for _, param := range proto.RequiredParams() {
cmd.PersistentFlags().String(param.Name, "", param.Description)
}
for _, param := range proto.OptionalParams() {
cmd.PersistentFlags().String(param.Name, *param.Default, param.Description)
}
}
func expandPrototype(proto *prototype.SpecificationSchema, flags *pflag.FlagSet, templateType prototype.TemplateType) (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
} 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)
}
quoted, err := param.Quote(val)
if err != nil {
return "", 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(""))
}
for _, param := range proto.OptionalParams() {
val, err := flags.GetString(param.Name)
if err != nil {
return "", err
} else if _, ok := values[param.Name]; ok {
return "", fmt.Errorf("Prototype '%s' has multiple parameters with name '%s'", proto.Name, param.Name)
}
quoted, err := param.Quote(val)
if err != nil {
return "", err
}
values[param.Name] = quoted
}
template, err := proto.Template.Body(templateType)
if err != nil {
return "", err
}
tm := snippet.Parse(strings.Join(template, "\n"))
text, err := tm.Evaluate(values)
if err != nil {
return "", err
}
return text, nil
}
func fundUniquePrototype(query string) (*prototype.SpecificationSchema, error) { func fundUniquePrototype(query string) (*prototype.SpecificationSchema, error) {
index := prototype.NewIndex([]*prototype.SpecificationSchema{}) index := prototype.NewIndex([]*prototype.SpecificationSchema{})
......
...@@ -20,6 +20,7 @@ import ( ...@@ -20,6 +20,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/ksonnet/kubecfg/prototype"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
...@@ -41,6 +42,7 @@ type AbsPaths []string ...@@ -41,6 +42,7 @@ type AbsPaths []string
type Manager interface { type Manager interface {
Root() AbsPath Root() AbsPath
ComponentPaths() (AbsPaths, error) ComponentPaths() (AbsPaths, error)
CreateComponent(name string, text string, templateType prototype.TemplateType) error
LibPaths(envName string) (libPath, envLibPath AbsPath) LibPaths(envName string) (libPath, envLibPath AbsPath)
CreateEnvironment(name, uri string, spec ClusterSpec) error CreateEnvironment(name, uri string, spec ClusterSpec) error
DeleteEnvironment(name string) error DeleteEnvironment(name string) error
......
...@@ -20,7 +20,10 @@ import ( ...@@ -20,7 +20,10 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strings"
"github.com/ksonnet/kubecfg/prototype"
log "github.com/sirupsen/logrus"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
...@@ -138,6 +141,34 @@ func (m *manager) ComponentPaths() (AbsPaths, error) { ...@@ -138,6 +141,34 @@ func (m *manager) ComponentPaths() (AbsPaths, error) {
return paths, nil return paths, nil
} }
func (m *manager) CreateComponent(name string, text 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)
}
componentPath := string(appendToAbsPath(m.componentsPath, name))
switch templateType {
case prototype.YAML:
componentPath = componentPath + ".yaml"
case prototype.JSON:
componentPath = componentPath + ".json"
case prototype.Jsonnet:
componentPath = componentPath + ".jsonnet"
default:
return fmt.Errorf("Unrecognized prototype template type '%s'", templateType)
}
if exists, err := afero.Exists(m.appFS, componentPath); exists {
return fmt.Errorf("Component with name '%s' already exists", name)
} else if err != nil {
return fmt.Errorf("Could not check whether component '%s' exists:\n\n%v", name, err)
}
log.Infof("Writing component at '%s/%s'", componentsDir, name)
return afero.WriteFile(m.appFS, componentPath, []byte(text), defaultFilePermissions)
}
func (m *manager) LibPaths(envName string) (libPath, envLibPath AbsPath) { func (m *manager) LibPaths(envName string) (libPath, envLibPath AbsPath) {
return m.libPath, appendToAbsPath(m.environmentsPath, envName) return m.libPath, appendToAbsPath(m.environmentsPath, envName)
} }
......
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