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
import (
"fmt"
"os"
"strings"
"github.com/spf13/pflag"
"github.com/ksonnet/kubecfg/metadata"
"github.com/ksonnet/kubecfg/prototype"
"github.com/ksonnet/kubecfg/prototype/snippet"
"github.com/spf13/cobra"
......@@ -30,6 +34,7 @@ func init() {
prototypeCmd.AddCommand(prototypeDescribeCmd)
prototypeCmd.AddCommand(prototypeSearchCmd)
prototypeCmd.AddCommand(prototypeUseCmd)
prototypeCmd.AddCommand(prototypePreviewCmd)
}
var prototypeCmd = &cobra.Command{
......@@ -184,13 +189,13 @@ var prototypeSearchCmd = &cobra.Command{
ksonnet prototype search deployment`,
}
var prototypeUseCmd = &cobra.Command{
Use: "use <prototype-name> [type] [parameter-flags]",
Short: `Instantiate prototype, emitting the generated code to stdout.`,
var prototypePreviewCmd = &cobra.Command{
Use: "preview <prototype-name> [type] [parameter-flags]",
Short: `Expand prototype, emitting the generated code to stdout`,
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, rawArgs []string) error {
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]
......@@ -200,13 +205,7 @@ var prototypeUseCmd = &cobra.Command{
return err
}
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)
}
bindPrototypeFlags(cmd, proto)
cmd.DisableFlagParsing = false
err = cmd.ParseFlags(rawArgs)
......@@ -227,82 +226,214 @@ var prototypeUseCmd = &cobra.Command{
return err
}
} 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{}
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)
}
text, err := expandPrototype(proto, flags, templateType)
if err != nil {
return err
}
quoted, err := param.Quote(val)
if err != nil {
return err
}
values[param.Name] = quoted
fmt.Println(text)
return nil
},
Long: `Expand prototype uniquely identified by (possibly partial)
'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 {
return fmt.Errorf("Failed to instantiate prototype '%s'. The following required parameters are missing:\n%s", proto.Name, missingReqd.PrettyString(""))
if len(rawArgs) < 1 {
return fmt.Errorf("Command 'prototype preview' requires a prototype name\n\n%s", cmd.UsageString())
}
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)
}
query := rawArgs[0]
quoted, err := param.Quote(val)
if err != nil {
return err
}
values[param.Name] = quoted
proto, err := fundUniquePrototype(query)
if err != nil {
return err
}
template, err := proto.Template.Body(templateType)
bindPrototypeFlags(cmd, proto)
cmd.DisableFlagParsing = false
err = cmd.ParseFlags(rawArgs)
if err != nil {
return err
}
flags := cmd.Flags()
tm := snippet.Parse(strings.Join(template, "\n"))
text, err := tm.Evaluate(values)
// Try to find the template type (if it is supplied) after the args are
// 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 {
return err
}
fmt.Println(text)
return nil
return manager.CreateComponent(componentName, text, templateType)
},
Long: `Instantiate prototype uniquely identified by (possibly partial)
'prototype-name', filling in parameters from flags, and emitting the generated
code to stdout.
Long: `Expand prototype uniquely identified by (possibly partial) 'prototype-name',
filling in parameters from flags, and placing it into the file
'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
disambiguate it among known names. For example, 'deployment' may resolve
ambiguously, in which case 'use' will fail, while 'simple-deployment' might be
unique enough to resolve to 'io.ksonnet.pkg.prototype.simple-deployment'.`,
Note that if we were to specify to expand the template as JSON or YAML, we would
generate a file with a '.json' or '.yaml' extension, respectively. See examples
below for an example of how to do this.
Example: ` # Instantiate prototype 'io.ksonnet.pkg.prototype.simple-deployment', using
# the 'nginx' image, and port 80 exposed.
ksonnet prototype use io.ksonnet.pkg.prototype.simple-deployment \
--name=nginx \
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: ` # 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
# Instantiate prototype using a unique suffix of an identifier. See
# introduction of help message for more information on how this works.
ksonnet prototype use simple-deployment \
--name=nginx \
# Instantiate prototype 'io.ksonnet.pkg.single-port-deployment' using the
# unique suffix, 'deployment'. The expanded prototype is again placed in
# 'components/nginx-depl.jsonnet'. See introduction of help message for more
# 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`,
}
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) {
index := prototype.NewIndex([]*prototype.SpecificationSchema{})
......
......@@ -20,7 +20,6 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
log "github.com/sirupsen/logrus"
......@@ -77,7 +76,7 @@ func (m *manager) createEnvironment(name, uri string, extensionsLibData, k8sLibD
// ensure environment name does not contain punctuation
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)
......@@ -249,7 +248,7 @@ func (m *manager) SetEnvironment(name string, desired *Environment) error {
// ensure new environment name does not contain punctuation
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
......@@ -349,10 +348,3 @@ func (m *manager) environmentExists(name string) (bool, error) {
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
import (
"os"
"regexp"
"strings"
"github.com/ksonnet/kubecfg/prototype"
"github.com/spf13/afero"
)
......@@ -39,6 +42,7 @@ type AbsPaths []string
type Manager interface {
Root() AbsPath
ComponentPaths() (AbsPaths, error)
CreateComponent(name string, text string, templateType prototype.TemplateType) error
LibPaths(envName string) (libPath, envLibPath AbsPath)
CreateEnvironment(name, uri string, spec ClusterSpec) error
DeleteEnvironment(name string) error
......@@ -85,6 +89,23 @@ func ParseClusterSpec(specFlag string) (ClusterSpec, error) {
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() {
appFS = afero.NewOsFs()
}
......@@ -20,7 +20,10 @@ import (
"os"
"path"
"path/filepath"
"strings"
"github.com/ksonnet/kubecfg/prototype"
log "github.com/sirupsen/logrus"
"github.com/spf13/afero"
)
......@@ -138,6 +141,34 @@ func (m *manager) ComponentPaths() (AbsPaths, error) {
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) {
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