Skip to content
Snippets Groups Projects
Commit 0762fdd0 authored by Alex Clemmer's avatar Alex Clemmer Committed by GitHub
Browse files

Merge pull request #154 from hausdorff/proto-preview

Implement 'prototype preview'
parents 5ac3a915 64f78b42
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"
...@@ -30,6 +34,7 @@ func init() { ...@@ -30,6 +34,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{
...@@ -184,13 +189,13 @@ var prototypeSearchCmd = &cobra.Command{ ...@@ -184,13 +189,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]
...@@ -200,13 +205,7 @@ var prototypeUseCmd = &cobra.Command{ ...@@ -200,13 +205,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)
...@@ -227,82 +226,214 @@ var prototypeUseCmd = &cobra.Command{ ...@@ -227,82 +226,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,7 +20,6 @@ import ( ...@@ -20,7 +20,6 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
...@@ -77,7 +76,7 @@ func (m *manager) createEnvironment(name, uri string, extensionsLibData, k8sLibD ...@@ -77,7 +76,7 @@ func (m *manager) createEnvironment(name, uri string, extensionsLibData, k8sLibD
// ensure environment name does not contain punctuation // ensure environment name does not contain punctuation
if !isValidName(name) { if !isValidName(name) {
return fmt.Errorf("Environment '%s' is not valid; must not contain punctuation or trailing slashes", name) return fmt.Errorf("Environment name '%s' is not valid; must not contain punctuation, spaces, or begin or end with a slash", name)
} }
log.Infof("Creating environment '%s' with uri '%s'", name, uri) log.Infof("Creating environment '%s' with uri '%s'", name, uri)
...@@ -249,7 +248,7 @@ func (m *manager) SetEnvironment(name string, desired *Environment) error { ...@@ -249,7 +248,7 @@ func (m *manager) SetEnvironment(name string, desired *Environment) error {
// ensure new environment name does not contain punctuation // ensure new environment name does not contain punctuation
if !isValidName(desired.Name) { if !isValidName(desired.Name) {
return fmt.Errorf("Environment '%s' is not valid; must not contain punctuation or trailing slashes", desired.Name) return fmt.Errorf("Environment name '%s' is not valid; must not contain punctuation, spaces, or begin or end with a slash", name)
} }
// If the name has changed, the directory location needs to be moved to // If the name has changed, the directory location needs to be moved to
...@@ -349,10 +348,3 @@ func (m *manager) environmentExists(name string) (bool, error) { ...@@ -349,10 +348,3 @@ func (m *manager) environmentExists(name string) (bool, error) {
return envExists, nil return envExists, nil
} }
// regex matcher to ensure environment name does not contain punctuation
func isValidName(envName string) bool {
hasPunctuation := regexp.MustCompile(`[,;.':!()?"{}\[\]*&%@$]+`).MatchString
hasTrailingSlashes := regexp.MustCompile(`/+$`).MatchString
return !hasPunctuation(envName) && !hasTrailingSlashes(envName)
}
...@@ -17,7 +17,10 @@ package metadata ...@@ -17,7 +17,10 @@ package metadata
import ( import (
"os" "os"
"regexp"
"strings"
"github.com/ksonnet/kubecfg/prototype"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
...@@ -39,6 +42,7 @@ type AbsPaths []string ...@@ -39,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
...@@ -85,6 +89,23 @@ func ParseClusterSpec(specFlag string) (ClusterSpec, error) { ...@@ -85,6 +89,23 @@ func ParseClusterSpec(specFlag string) (ClusterSpec, error) {
return parseClusterSpec(specFlag, appFS) return parseClusterSpec(specFlag, appFS)
} }
// isValidName returns true if a name (e.g., for an environment) is valid.
// Broadly, this means it does not contain punctuation, whitespace, leading or
// trailing slashes.
func isValidName(name string) bool {
// No unicode whitespace is allowed. `Fields` doesn't handle trailing or
// leading whitespace.
fields := strings.Fields(name)
if len(fields) > 1 || len(strings.TrimSpace(name)) != len(name) {
return false
}
hasPunctuation := regexp.MustCompile(`[\\,;':!()?"{}\[\]*&%@$]+`).MatchString
hasTrailingSlashes := regexp.MustCompile(`/+$`).MatchString
hasLeadingSlashes := regexp.MustCompile(`^/+`).MatchString
return len(name) != 0 && !hasPunctuation(name) && !hasTrailingSlashes(name) && !hasLeadingSlashes(name)
}
func init() { func init() {
appFS = afero.NewOsFs() appFS = afero.NewOsFs()
} }
...@@ -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