diff --git a/cmd/create.go b/cmd/create.go index b1ebf83b7..a9730b81a 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -12,6 +12,16 @@ import ( "github.com/boson-project/faas/kubectl" ) +// 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 ", + Short: "Create a Service Function", + SuggestFor: []string{"init", "new"}, + RunE: create, +} + func init() { // Add the `create` command as a subcommand to root. root.AddCommand(createCmd) @@ -34,71 +44,105 @@ func init() { } -// 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 ", - Short: "Create a Service Function", - SuggestFor: []string{"init", "new"}, - RunE: create, +// 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 + + // Language is the first argument, and specifies the resultant Function + // implementation language. + Language 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 language parameter was provided if len(args) == 0 { return errors.New("'faas create' requires a language argument.") } - // Assemble parameters for use in client's 'create' invocation. - var ( - language = args[0] // language is the first argument - path = "" // path is defaulted to current working directory. - local = viper.GetBool("local") // Only perform local creation steps - internal = viper.GetBool("internal") // Do not expose publicly (internal route only) - name = viper.GetString("name") // Explicit name override (by default path-derives) - verbose = viper.GetBool("verbose") // Verbose logging - registry = viper.GetString("registry") // Registry (ex: docker.io) - namespace = viper.GetString("namespace") // namespace at registry (user or org name) - ) + // 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"), + Language: args[0], + Path: ".", // will be expanded to process current working dir. + } // If path is provided if len(args) == 2 { - path = args[1] + config.Path = args[1] } // Namespace can not be defaulted. - if namespace == "" { + 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 isInteractive() { + config = gatherFromUser(config) + } + // Initializer creates a deployable noop function implementation in the // configured path. initializer := appsody.NewInitializer() - initializer.Verbose = verbose + initializer.Verbose = config.Verbose // Builder creates images from function source. - builder := appsody.NewBuilder(registry, namespace) - builder.Verbose = verbose + builder := appsody.NewBuilder(config.Registry, config.Namespace) + builder.Verbose = config.Verbose // Pusher of images pusher := docker.NewPusher() - pusher.Verbose = verbose + pusher.Verbose = config.Verbose // Deployer of built images. deployer := kubectl.NewDeployer() - deployer.Verbose = verbose + deployer.Verbose = 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(verbose), + faas.WithVerbose(config.Verbose), faas.WithInitializer(initializer), faas.WithBuilder(builder), faas.WithPusher(pusher), faas.WithDeployer(deployer), - faas.WithLocal(local), // local template only (no cluster deployment) - faas.WithInternal(internal), // if deployed, no publicly accessible route. + faas.WithLocal(config.Local), // local template only (no cluster deployment) + faas.WithInternal(config.Internal), // if deployed, no publicly accessible route. ) if err != nil { return @@ -108,5 +152,10 @@ func create(cmd *cobra.Command, args []string) (err error) { // 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(language, name, path) + return client.Create(config.Language, config.Name, config.Path) +} + +func gatherFromUser(config createConfig) createConfig { + // TODO + return config } diff --git a/cmd/root.go b/cmd/root.go index fd16cd1f9..3e8665fb8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -62,3 +62,11 @@ func Execute() { os.Exit(1) } } + +// isInteractive returns whether or not the currently attached process terminal +// is interactive. Used for determining whether or not to interactively prompt +// the user to confirm default choices, etc. +func isInteractive() bool { + fi, err := os.Stdin.Stat() + return err == nil && ((fi.Mode() & os.ModeCharDevice) != 0) +}