func/cmd/run.go

364 lines
12 KiB
Go

package cmd
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"time"
"github.com/ory/viper"
"github.com/spf13/cobra"
"knative.dev/func/pkg/config"
"knative.dev/func/pkg/docker"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/oci"
)
func NewRunCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "run",
Short: "Run the function locally",
Long: `
NAME
{{rootCmdUse}} run - Run a function locally
SYNOPSIS
{{rootCmdUse}} run [-t|--container] [-r|--registry] [-i|--image] [-e|--env]
[--build] [-b|--builder] [--builder-image] [-c|--confirm]
[-v|--verbose]
DESCRIPTION
Run the function locally.
Values provided for flags are not persisted to the function's metadata.
Containerized Runs
The --container flag indicates that the function's container should be
run rather than running the source code directly. This may require that
the function's container first be rebuilt. Building the container on or
off can be altered using the --build flag. The value --build=auto
can be used to indicate the function should be run in a container, with
the container automatically built if necessary.
The --container flag defaults to true if the builder defined for the
function is a containerized builder such as Pack or S2I, and in the case
where the function's runtime requires containerized builds (is not yet
supported by the Host builder.
Process Scaffolding
This is an Experimental Feature currently available only to Go projects.
When running a function with --container=false (host-based runs), the
function is first wrapped code which presents it as a process.
This "scaffolding" is transient, written for each build or run, and should
in most cases be transparent to a function author. However, to customize,
or even completely replace this scafolding code, see the 'scaffold'
subcommand.
EXAMPLES
o Run the function locally from within its container.
$ {{rootCmdUse}} run
o Run the function locally from within its container, forcing a rebuild
of the container even if no filesysem changes are detected
$ {{rootCmdUse}} run --build
o Run the function locally on the host with no containerization (Go only).
$ {{rootCmdUse}} run --container=false
`,
SuggestFor: []string{"rnu"},
PreRunE: bindEnv("build", "builder", "builder-image", "confirm", "container", "env", "image", "path", "registry", "start-timeout", "verbose"),
RunE: func(cmd *cobra.Command, _ []string) error {
return runRun(cmd, newClient)
},
}
// Global Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
// Function Context
f, _ := fn.NewFunction(effectivePath())
if f.Initialized() {
cfg = cfg.Apply(f)
}
// Flags
//
// Globally-Configurable Flags:
cmd.Flags().StringP("builder", "b", cfg.Builder,
fmt.Sprintf("Builder to use when creating the function's container. Currently supported builders are %s.", KnownBuilders()))
cmd.Flags().StringP("registry", "r", cfg.Registry,
"Container registry + registry namespace. (ex 'ghcr.io/myuser'). The full image name is automatically determined using this along with function name. ($FUNC_REGISTRY)")
// Function-Context Flags:
// Options whose value is available on the function with context only
// (persisted but not globally configurable)
builderImage := f.Build.BuilderImages[f.Build.Builder]
cmd.Flags().String("builder-image", builderImage,
"Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)")
cmd.Flags().StringP("image", "i", f.Image,
"Full image name in the form [registry]/[namespace]/[name]:[tag]. This option takes precedence over --registry. Specifying tag is optional. ($FUNC_IMAGE)")
cmd.Flags().StringArrayP("env", "e", []string{},
"Environment variable to set in the form NAME=VALUE. "+
"You may provide this flag multiple times for setting multiple environment variables. "+
"To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).")
cmd.Flags().Duration("start-timeout", f.Run.StartTimeout, fmt.Sprintf("time this function needs in order to start. If not provided, the client default %v will be in effect. ($FUNC_START_TIMEOUT)", fn.DefaultStartTimeout))
cmd.Flags().BoolP("container", "t", runContainerizedByDefault(f),
"Run the function in a container. ($FUNC_CONTAINER)")
// TODO: Without the "Host" builder enabled, this code-path is unreachable,
// so remove hidden flag when either the Host builder path is available,
// or when containerized runs support start-timeout (and ideally both).
// Also remember to add it to the command help text's synopsis section.
_ = cmd.Flags().MarkHidden("start-timeout")
// Static Flags:
// Options which have static defaults only
// (not globally configurable nor persisted as function metadata)
cmd.Flags().String("build", "auto",
"Build the function. [auto|true|false]. ($FUNC_BUILD)")
cmd.Flags().Lookup("build").NoOptDefVal = "true" // register `--build` as equivalient to `--build=true`
// Oft-shared flags:
addConfirmFlag(cmd, cfg.Confirm)
addPathFlag(cmd)
addVerboseFlag(cmd, cfg.Verbose)
// Tab Completion
if err := cmd.RegisterFlagCompletionFunc("builder", CompleteBuilderList); err != nil {
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
}
if err := cmd.RegisterFlagCompletionFunc("builder-image", CompleteBuilderImageList); err != nil {
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
}
return cmd
}
func runContainerizedByDefault(f fn.Function) bool {
return f.Build.Builder == "pack" || f.Build.Builder == "s2i" || !oci.IsSupported(f.Runtime)
}
func runRun(cmd *cobra.Command, newClient ClientFactory) (err error) {
var (
cfg runConfig
f fn.Function
)
cfg = newRunConfig(cmd) // Will add Prompt on upcoming UX refactor
if f, err = fn.NewFunction(cfg.Path); err != nil {
return
}
if err = cfg.Validate(cmd, f); err != nil {
return
}
if !f.Initialized() {
return fn.NewErrNotInitialized(f.Root)
}
if f, err = cfg.Configure(f); err != nil { // Updates f with deploy cfg
return
}
// Client
clientOptions, err := cfg.clientOptions()
if err != nil {
return
}
if cfg.Container {
clientOptions = append(clientOptions, fn.WithRunner(docker.NewRunner(cfg.Verbose, os.Stdout, os.Stderr)))
}
if cfg.StartTimeout != 0 {
clientOptions = append(clientOptions, fn.WithStartTimeout(cfg.StartTimeout))
}
client, done := newClient(ClientConfig{Verbose: cfg.Verbose}, clientOptions...)
defer done()
// Build
//
// If requesting to run via the container, build the container if it is
// either out-of-date or a build was explicitly requested.
if cfg.Container {
var digested bool
buildOptions, err := cfg.buildOptions()
if err != nil {
return err
}
// if image was specified, check if its digested and do basic validation
if cfg.Image != "" {
digested, err = isDigested(cfg.Image)
if err != nil {
return err
}
if !digested {
// assign valid undigested image
f.Build.Image = cfg.Image
}
}
if digested {
// run cmd takes f.Build.Image - see newContainerConfig in docker/runner.go
// it doesnt get saved, just runtime image
f.Build.Image = cfg.Image
} else {
if f, _, err = build(cmd, cfg.Build, f, client, buildOptions); err != nil {
return err
}
}
} else {
// dont run digested image without a container
if cfg.Image != "" {
digested, err := isDigested(cfg.Image)
if err != nil {
return err
}
if digested {
return fmt.Errorf("cannot use digested image with --container=false")
}
}
}
// Run
//
// Runs the code either via a container or the default host-based runner.
// For the former, build is required and a container runtime. For the
// latter, scaffolding is first applied and the local host must be
// configured to build/run the language of the function.
job, err := client.Run(cmd.Context(), f)
if err != nil {
return
}
defer func() {
if err = job.Stop(); err != nil {
fmt.Fprintf(cmd.OutOrStderr(), "Job stop error. %v", err)
}
}()
fmt.Fprintf(cmd.OutOrStderr(), "Running on host port %v\n", job.Port)
select {
case <-cmd.Context().Done():
if !errors.Is(cmd.Context().Err(), context.Canceled) {
err = cmd.Context().Err()
}
case err = <-job.Errors:
return
// Bubble up runtime errors on the optional channel used for async job
// such as docker containers.
}
// NOTE: we do not f.Write() here unlike deploy (and build).
// running is ephemeral: a run is not affecting the function itself,
// as opposed to deploy commands, which are actually mutating the current
// state of the function as it exists on the network.
// Another way to think of this is that runs are development-centric tests,
// and thus most likely values changed such as environment variables,
// builder, etc. would not be expected to persist and affect the next deploy.
// Run is ephemeral, deploy is persistent.
return
}
type runConfig struct {
buildConfig // further embeds config.Global
// Built instructs building to happen or not
// Can be 'auto' or a truthy value.
Build string
// Container indicates the function should be run in a container.
// Requires the container be built.
Container bool
// Env variables. may include removals using a "-"
Env []string
// StartTimeout optionally adjusts the startup timeout from the client's
// default of fn.DefaultStartTimeout.
StartTimeout time.Duration
}
func newRunConfig(cmd *cobra.Command) (c runConfig) {
c = runConfig{
buildConfig: newBuildConfig(),
Build: viper.GetString("build"),
Env: viper.GetStringSlice("env"),
Container: viper.GetBool("container"),
StartTimeout: viper.GetDuration("start-timeout"),
}
// NOTE: .Env should be viper.GetStringSlice, but this returns unparsed
// results and appears to be an open issue since 2017:
// https://github.com/spf13/viper/issues/380
var err error
if c.Env, err = cmd.Flags().GetStringArray("env"); err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error reading envs: %v", err)
}
return
}
// Configure the given function. Updates a function struct with all
// configurable values. Note that the config alrady includes function's
// current state, as they were passed through via flag defaults.
func (c runConfig) Configure(f fn.Function) (fn.Function, error) {
var err error
f = c.buildConfig.Configure(f)
f.Run.StartTimeout = c.StartTimeout
f.Run.Envs, err = applyEnvs(f.Run.Envs, c.Env)
// The other members; build, path, and container; are not part of function
// state, so are not mentioned here in Configure.
return f, err
}
func (c runConfig) Prompt() (runConfig, error) {
var err error
if c.buildConfig, err = c.buildConfig.Prompt(); err != nil {
return c, err
}
if !interactiveTerminal() || !c.Confirm {
return c, nil
}
// TODO: prompt for additional settings here
return c, nil
}
func (c runConfig) Validate(cmd *cobra.Command, f fn.Function) (err error) {
// Bubble
if err = c.buildConfig.Validate(); err != nil {
return
}
// --build can be "auto"|true|false
if c.Build != "auto" {
if _, err := strconv.ParseBool(c.Build); err != nil {
return fmt.Errorf("unrecognized value for --build '%v'. Accepts 'auto', 'true' or 'false' (or similarly truthy value)", c.Build)
}
}
if !c.Container && !oci.IsSupported(f.Runtime) {
return fmt.Errorf("the %q runtime currently requires being run in a container", f.Runtime)
}
// When the docker runner respects the StartTimeout, this validation check
// can be removed
if c.StartTimeout != 0 && c.Container {
return errors.New("the ability to specify the startup timeout for containerized runs is coming soon")
}
return
}