From 64f78b42df2e8e8408a520e8e241ecb1e2ec4969 Mon Sep 17 00:00:00 2001 From: Alex Clemmer <clemmer.alexander@gmail.com> Date: Thu, 21 Sep 2017 16:40:54 -0700 Subject: [PATCH] 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. --- cmd/prototype.go | 255 ++++++++++++++++++++++++++++++++---------- metadata/interface.go | 2 + metadata/manager.go | 31 +++++ 3 files changed, 226 insertions(+), 62 deletions(-) diff --git a/cmd/prototype.go b/cmd/prototype.go index f7076459..508d6c23 100644 --- a/cmd/prototype.go +++ b/cmd/prototype.go @@ -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" @@ -29,6 +33,7 @@ func init() { prototypeCmd.AddCommand(prototypeDescribeCmd) prototypeCmd.AddCommand(prototypeSearchCmd) prototypeCmd.AddCommand(prototypeUseCmd) + prototypeCmd.AddCommand(prototypePreviewCmd) } var prototypeCmd = &cobra.Command{ @@ -160,13 +165,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] @@ -176,13 +181,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) @@ -203,82 +202,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{}) diff --git a/metadata/interface.go b/metadata/interface.go index 4d13ed72..afdb46df 100644 --- a/metadata/interface.go +++ b/metadata/interface.go @@ -20,6 +20,7 @@ import ( "regexp" "strings" + "github.com/ksonnet/kubecfg/prototype" "github.com/spf13/afero" ) @@ -41,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 diff --git a/metadata/manager.go b/metadata/manager.go index 2950905c..55022151 100644 --- a/metadata/manager.go +++ b/metadata/manager.go @@ -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) } -- GitLab