mirror of https://github.com/knative/func.git
245 lines
9.1 KiB
Go
245 lines
9.1 KiB
Go
package cmd
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
|
|
"github.com/mitchellh/go-homedir"
|
|
"github.com/ory/viper"
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/boson-project/faas"
|
|
"github.com/boson-project/faas/buildpacks"
|
|
"github.com/boson-project/faas/docker"
|
|
"github.com/boson-project/faas/embedded"
|
|
"github.com/boson-project/faas/kubectl"
|
|
"github.com/boson-project/faas/progress"
|
|
"github.com/boson-project/faas/prompt"
|
|
)
|
|
|
|
func init() {
|
|
// Add the `create` command as a subcommand to root.
|
|
root.AddCommand(createCmd)
|
|
createCmd.Flags().BoolP("local", "l", false, "create the service function locally only.")
|
|
createCmd.Flags().BoolP("internal", "i", false, "Create a cluster-local service without a publicly accessible route. $FAAS_INTERNAL")
|
|
createCmd.Flags().StringP("name", "n", "", "optionally specify an explicit name for the serive, overriding path-derivation. $FAAS_NAME")
|
|
createCmd.Flags().StringP("registry", "r", "quay.io", "image registry (ex: quay.io). $FAAS_REGISTRY")
|
|
createCmd.Flags().StringP("namespace", "s", "", "namespace at image registry (usually username or org name). $FAAS_NAMESPACE")
|
|
createCmd.Flags().StringP("template", "t", embedded.DefaultTemplate, "Function template (ex: 'http','events'). $FAAS_TEMPLATE")
|
|
createCmd.Flags().StringP("templates", "", filepath.Join(configPath(), "faas", "templates"), "Extensible templates path. $FAAS_TEMPLATES")
|
|
createCmd.RegisterFlagCompletionFunc("registry", CompleteRegistryList)
|
|
}
|
|
|
|
// The create command invokes the Service Funciton Client to create a new,
|
|
// functional, deployed service function with a noop implementation. It
|
|
// can be optionally created only locally (no deploy) using --local.
|
|
var createCmd = &cobra.Command{
|
|
Use: "create <runtime>",
|
|
Short: "Create a Service Function",
|
|
SuggestFor: []string{"init", "new"},
|
|
ValidArgsFunction: CompleteRuntimeList,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: create,
|
|
PreRun: func(cmd *cobra.Command, args []string) {
|
|
viper.BindPFlag("local", cmd.Flags().Lookup("local"))
|
|
viper.BindPFlag("internal", cmd.Flags().Lookup("internal"))
|
|
viper.BindPFlag("name", cmd.Flags().Lookup("name"))
|
|
viper.BindPFlag("registry", cmd.Flags().Lookup("registry"))
|
|
viper.BindPFlag("namespace", cmd.Flags().Lookup("namespace"))
|
|
viper.BindPFlag("template", cmd.Flags().Lookup("template"))
|
|
viper.BindPFlag("templates", cmd.Flags().Lookup("templates"))
|
|
},
|
|
}
|
|
|
|
// The create command expects several parameters, most of which can be
|
|
// defaulted. When an interactive terminal is detected, these parameters,
|
|
// which are gathered into this config object, are passed through the shell
|
|
// allowing the user to interactively confirm and optionally modify values.
|
|
type createConfig struct {
|
|
// Verbose mode instructs the system to output detailed logs as the command
|
|
// progresses.
|
|
Verbose bool
|
|
|
|
// Local mode flag only builds a function locally, with no deployed
|
|
// counterpart.
|
|
Local bool
|
|
|
|
// Internal only flag. A public route will not be allocated and the domain
|
|
// suffix will be a .local (depending on underlying cluster configuration)
|
|
Internal bool
|
|
|
|
// Name of the service in DNS-compatible format (ex myfunc.example.com)
|
|
Name string
|
|
|
|
// Registry of containers (ex. quay.io or hub.docker.com)
|
|
Registry string
|
|
|
|
// Namespace within the container registry within which interstitial built
|
|
// images will be stored by their canonical name.
|
|
Namespace string
|
|
|
|
// Template is the form of the resultant function, i.e. the function signature
|
|
// and contextually avaialable resources. For example 'http' for a funciton
|
|
// expected to be invoked via straight HTTP requests, or 'events' for a
|
|
// function which will be invoked with CloudEvents.
|
|
Template string
|
|
|
|
// Templates is an optional path that, if it exists, will be used as a source
|
|
// for additional templates not included in the binary. If not provided
|
|
// explicitly as a flag (--templates) or env (FAAS_TEMPLATES), the default
|
|
// location is $XDG_CONFIG_HOME/templates ($HOME/.config/faas/templates)
|
|
Templates string
|
|
|
|
// Runtime is the first argument, and specifies the resultant Function
|
|
// implementation runtime.
|
|
Runtime string
|
|
|
|
// Path of the Function implementation on local disk. Defaults to current
|
|
// working directory of the process.
|
|
Path string
|
|
}
|
|
|
|
// create a new service function using the client about config.
|
|
func create(cmd *cobra.Command, args []string) (err error) {
|
|
// Assert a runtime parameter was provided
|
|
if len(args) == 0 {
|
|
return errors.New("'faas create' requires a runtime argument.")
|
|
}
|
|
|
|
// Create a deafult configuration populated first with environment variables,
|
|
// followed by overrides by flags.
|
|
var config = createConfig{
|
|
Verbose: viper.GetBool("verbose"),
|
|
Local: viper.GetBool("local"),
|
|
Internal: viper.GetBool("internal"),
|
|
Name: viper.GetString("name"),
|
|
Registry: viper.GetString("registry"),
|
|
Namespace: viper.GetString("namespace"),
|
|
Template: viper.GetString("template"), // to use
|
|
Templates: viper.GetString("templates"), // extendex repos
|
|
Runtime: args[0],
|
|
Path: ".", // will be expanded to current working dir.
|
|
}
|
|
|
|
// If path is provided
|
|
if len(args) == 2 {
|
|
config.Path = args[1]
|
|
}
|
|
|
|
// Namespace can not be defaulted.
|
|
if config.Namespace == "" {
|
|
return errors.New("image registry namespace (--namespace or FAAS_NAMESPACE is required)")
|
|
}
|
|
|
|
// If we are running as an interactive terminal, allow the user
|
|
// to mutate default config prior to execution.
|
|
if interactiveTerminal() {
|
|
config, err = gatherFromUser(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
//
|
|
|
|
// Initializer creates a deployable noop function implementation in the
|
|
// configured path.
|
|
initializer := embedded.NewInitializer(config.Templates)
|
|
initializer.Verbose = config.Verbose
|
|
|
|
// Builder creates images from function source.
|
|
builder := buildpacks.NewBuilder(config.Registry, config.Namespace)
|
|
builder.Verbose = config.Verbose
|
|
|
|
// Pusher of images
|
|
pusher := docker.NewPusher()
|
|
pusher.Verbose = config.Verbose
|
|
|
|
// Deployer of built images.
|
|
deployer := kubectl.NewDeployer()
|
|
deployer.Verbose = config.Verbose
|
|
|
|
// Progress bar
|
|
listener := progress.New(progress.WithVerbose(config.Verbose))
|
|
|
|
// Instantiate a client, specifying concrete implementations for
|
|
// Initializer and Deployer, as well as setting the optional verbosity param.
|
|
client, err := faas.New(
|
|
faas.WithVerbose(config.Verbose),
|
|
faas.WithInitializer(initializer),
|
|
faas.WithBuilder(builder),
|
|
faas.WithPusher(pusher),
|
|
faas.WithDeployer(deployer),
|
|
faas.WithLocal(config.Local), // local template only (no cluster deployment)
|
|
faas.WithInternal(config.Internal), // if deployed, no publicly accessible route.
|
|
faas.WithProgressListener(listener),
|
|
)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Invoke the creation of the new Service Function locally.
|
|
// Returns the final address.
|
|
// Name can be empty string (path-dervation will be attempted)
|
|
// Path can be empty, defaulting to current working directory.
|
|
return client.Create(config.Runtime, config.Template, config.Name, config.Path)
|
|
}
|
|
|
|
func gatherFromUser(config createConfig) (c createConfig, err error) {
|
|
config.Path = prompt.ForString("Local source path", config.Path)
|
|
config.Name, err = promptForName("name of service function", config)
|
|
if err != nil {
|
|
return config, err
|
|
}
|
|
config.Local = prompt.ForBool("Local; no remote deployment", config.Local)
|
|
config.Internal = prompt.ForBool("Internal; no public route", config.Internal)
|
|
config.Registry = prompt.ForString("Image registry", config.Registry)
|
|
config.Namespace = prompt.ForString("Namespace at registry", config.Namespace)
|
|
config.Runtime = prompt.ForString("Runtime of source", config.Runtime)
|
|
config.Template = prompt.ForString("Function Template", config.Template)
|
|
return config, nil
|
|
}
|
|
|
|
// Prompting for Service Name with Default
|
|
// Early calclation of service function name is required to provide a sensible
|
|
// default. If the user did not provide a --name parameter or FAAS_NAME,
|
|
// this funciton sets the default to the value that the client would have done
|
|
// on its own if non-interactive: by creating a new function rooted at config.Path
|
|
// and then calculate from that path.
|
|
func promptForName(label string, config createConfig) (string, error) {
|
|
// Pre-calculate the function name derived from path
|
|
if config.Name == "" {
|
|
f, err := faas.NewFunction(config.Path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
maxRecursion := 5 // TODO synchronize with that used in actual initialize step.
|
|
return prompt.ForString("Name of service function", f.DerivedName(maxRecursion), prompt.WithRequired(true)), nil
|
|
}
|
|
|
|
// The user provided a --name or FAAS_NAME; just confirm it.
|
|
return prompt.ForString("Name of service function", config.Name, prompt.WithRequired(true)), nil
|
|
}
|
|
|
|
// acceptable answers: y,yes,Y,YES,1
|
|
var confirmExp = regexp.MustCompile("(?i)y(?:es)?|1")
|
|
|
|
func fromYN(s string) bool {
|
|
return confirmExp.MatchString(s)
|
|
}
|
|
|
|
func configPath() (path string) {
|
|
if path = os.Getenv("XDG_CONFIG_HOME"); path != "" {
|
|
return
|
|
}
|
|
|
|
path, err := homedir.Expand("~/.config")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "could not derive home directory for use as default templates path: %v", err)
|
|
}
|
|
return
|
|
}
|