package cmd import ( "errors" "fmt" "github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2/terminal" "github.com/ory/viper" "github.com/spf13/cobra" "knative.dev/kn-plugin-func/buildpacks" "knative.dev/kn-plugin-func/s2i" fn "knative.dev/kn-plugin-func" "knative.dev/kn-plugin-func/builders" ) func NewBuildCmd(newClient ClientFactory) *cobra.Command { cmd := &cobra.Command{ Use: "build", Short: "Build a function project as a container image", Long: `Build a function project as a container image This command builds the function project in the current directory or in the directory specified by --path. The result will be a container image that is pushed to a registry. The func.yaml file is read to determine the image name and registry. If the project has not already been built, either --registry or --image must be provided and the image name is stored in the configuration file. `, Example: ` # Build from the local directory, using the given registry as target. # The full image name will be determined automatically based on the # project directory name {{.Name}} build --registry quay.io/myuser # Build from the local directory, specifying the full image name {{.Name}} build --image quay.io/myuser/myfunc # Re-build, picking up a previously supplied image name from a local func.yml {{.Name}} build # Build using s2i instead of Buildpacks {{.Name}} build --builder=s2i # Build with a custom buildpack builder {{.Name}} build --builder=pack --builder-image cnbs/sample-builder:bionic `, SuggestFor: []string{"biuld", "buidl", "built"}, PreRunE: bindEnv("image", "path", "builder", "registry", "confirm", "push", "builder-image", "platform"), } cmd.Flags().StringP("builder", "b", builders.Default, fmt.Sprintf("build strategy to use when creating the underlying image. Currently supported build strategies are %s.", KnownBuilders())) cmd.Flags().StringP("builder-image", "", "", "builder image, either an as a an image name or a mapping name.\nSpecified value is stored in func.yaml (as 'builder' field) for subsequent builds. ($FUNC_BUILDER_IMAGE)") cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)") cmd.Flags().StringP("image", "i", "", "Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE)") cmd.Flags().StringP("registry", "r", GetDefaultRegistry(), "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined (Env: $FUNC_REGISTRY)") cmd.Flags().BoolP("push", "u", false, "Attempt to push the function image after being successfully built") cmd.Flags().Lookup("push").NoOptDefVal = "true" // --push == --push=true cmd.Flags().StringP("platform", "", "", "Target platform to build (e.g. linux/amd64).") setPathFlag(cmd) 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) } cmd.SetHelpFunc(defaultTemplatedHelp) cmd.RunE = func(cmd *cobra.Command, args []string) error { return runBuild(cmd, args, newClient) } return cmd } func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err error) { // Populate a command config from environment variables, flags and potentially // interactive user prompts if in confirm mode. config, err := newBuildConfig().Prompt() if err != nil { if errors.Is(err, terminal.InterruptErr) { return nil } } // Validate the config if err = config.Validate(); err != nil { return } // Load the Function at path, and if it is initialized, update it with // pertinent values from the config. // // NOTE: the checks for .Changed and altered conditionals for defaults will // be removed when Global Config is integreated, because the config object // will at that time contain the final value for the attribute, taking into // account whether or not the value was altered via flags or env variables. // This condition is also only necessary for config members whose default // value deviates from the zero value. f, err := fn.NewFunction(config.Path) if err != nil { return } if !f.Initialized() { return fmt.Errorf("'%v' does not contain an initialized function", config.Path) } if f.Registry == "" || cmd.Flags().Changed("registry") { // Sets default AND accepts any user-provided overrides f.Registry = config.Registry } if f.Builder == "" || cmd.Flags().Changed("builder") { // Sets default AND accepts any user-provided overrides f.Builder = config.Builder } if config.Image != "" { f.Image = config.Image } if config.Builder != "" { f.Builder = config.Builder } if config.BuilderImage != "" { f.BuilderImages[config.Builder] = config.BuilderImage } // Validate that a builder short-name was obtained, whether that be from // the function's prior state, or the value of flags/environment. if err = ValidateBuilder(f.Builder); err != nil { return } // Choose a builder based on the value of the --builder flag var builder fn.Builder if f.Builder == builders.Pack { builder = buildpacks.NewBuilder( buildpacks.WithName(builders.Pack), buildpacks.WithVerbose(config.Verbose)) } else if f.Builder == builders.S2I { builder = s2i.NewBuilder( s2i.WithName(builders.S2I), s2i.WithPlatform(config.Platform), s2i.WithVerbose(config.Verbose)) } else { err = fmt.Errorf("builder '%v' is not recognized", f.Builder) return } client, done := newClient(ClientConfig{Verbose: config.Verbose}, fn.WithRegistry(config.Registry), fn.WithBuilder(builder)) defer done() // Default Client Registry, Function Registry or explicit Image is required if client.Registry() == "" && f.Registry == "" && f.Image == "" { // It is not necessary that we validate here, since the client API has // its own validation, but it does give us the opportunity to show a very // cli-specific and detailed error message and (at least for now) default // to an interactive prompt. if interactiveTerminal() { fmt.Println("A registry for function images is required. For example, 'docker.io/tigerteam'.") if err = survey.AskOne( &survey.Input{Message: "Registry for function images:"}, &config.Registry, survey.WithValidator( NewRegistryValidator(config.Path))); err != nil { return ErrRegistryRequired } done() client, done = newClient(ClientConfig{Verbose: config.Verbose}, fn.WithRegistry(config.Registry), fn.WithBuilder(builder)) defer done() fmt.Println("Note: building a function the first time will take longer than subsequent builds") } else { return ErrRegistryRequired } } // This preemptive write call will be unnecessary when the API is updated // to use Function instances rather than file paths. For now it must write // even if the command fails later. Not ideal. if err = f.Write(); err != nil { return } if err = client.Build(cmd.Context(), config.Path); err != nil { return } if config.Push { err = client.Push(cmd.Context(), config.Path) } return } type buildConfig struct { // Image name in full, including registry, repo and tag (overrides // image name derivation based on registry and function name) Image string // Path of the function implementation on local disk. Defaults to current // working directory of the process. Path string // Push the resulting image to the registry after building. Push bool // Registry at which interstitial build artifacts should be kept. // This setting is ignored if Image is specified, which includes the full Registry string // Verbose logging. Verbose bool // Confirm: confirm values arrived upon from environment plus flags plus defaults, // with interactive prompting (only applicable when attached to a TTY). Confirm bool // Builder is the name of the subsystem that will complete the underlying // build (Pack, s2i, remote pipeline, etc). Currently ad-hoc rather than // an enumerated field. See the Client constructory for logic. Builder string // BuilderImage is the image (name or mapping) to use for building. Usually // set automatically. BuilderImage string Platform string } func newBuildConfig() buildConfig { return buildConfig{ Image: viper.GetString("image"), Path: getPathFlag(), Registry: viper.GetString("registry"), Verbose: viper.GetBool("verbose"), // defined on root Confirm: viper.GetBool("confirm"), Builder: viper.GetString("builder"), BuilderImage: viper.GetString("builder-image"), Push: viper.GetBool("push"), Platform: viper.GetString("platform"), } } // Prompt the user with value of config members, allowing for interaractive changes. // Skipped if not in an interactive terminal (non-TTY), or if --confirm false (agree to // all prompts) was set (default). func (c buildConfig) Prompt() (buildConfig, error) { if !interactiveTerminal() || !c.Confirm { return c, nil } imageName := deriveImage(c.Image, c.Registry, c.Path) var qs = []*survey.Question{ { Name: "path", Prompt: &survey.Input{ Message: "Project path:", Default: c.Path, }, }, { Name: "image", Prompt: &survey.Input{ Message: "Full image name (e.g. quay.io/boson/node-sample):", Default: imageName, }, }, } err := survey.Ask(qs, &c) if err != nil { return c, err } // if the result of imageName is empty (unset, underivable), // and the value of c.Image is empty (none provided explicitly by flag, env // variable or prompt), then try one more time to get enough to to derive an // image name: explicitly ask for registry. if imageName == "" && c.Image == "" { qs = []*survey.Question{ { Name: "registry", Prompt: &survey.Input{ Message: "Registry for function image:", Default: c.Registry, // This should be innefectual }, }, } } err = survey.Ask(qs, &c) return c, err } // Validate the config passes an initial consistency check func (c buildConfig) Validate() (err error) { if c.Platform != "" && c.Builder != builders.S2I { err = errors.New("Only S2I builds currently support specifying platform") return } return }