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" ) 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"), } cmd.Flags().StringP("builder", "b", "pack", "build strategy to use when creating the underlying image. Currently supported build strategies are 'pack' and 's2i'.") 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 based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)") cmd.Flags().BoolP("push", "u", false, "Attempt to push the function image after being successfully built") setPathFlag(cmd) if err := cmd.RegisterFlagCompletionFunc("builder", CompleteBuildStrategyList); 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 ValidNamespaceAndRegistry(path string) survey.Validator { return func(val interface{}) error { // if the value passed in is the zero value of the appropriate type if len(val.(string)) == 0 { return errors.New("Value is required") } _, err := fn.DerivedImage(path, val.(string)) //image can be derived without any error if err != nil { return fmt.Errorf("invalid registry [%q]: %w", val.(string), err) } return nil } } func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err error) { config, err := newBuildConfig().Prompt() if err != nil { if errors.Is(err, terminal.InterruptErr) { return nil } return } function, err := functionWithOverrides(config.Path, functionOverrides{BuilderImage: config.BuilderImage, Image: config.Image}) if err != nil { return } // Check if the Function has been initialized if !function.Initialized() { return fmt.Errorf("the given path '%v' does not contain an initialized function. Please create one at this path before deploying", config.Path) } // If a registry name was provided as a command line flag, it should be validated if config.Registry != "" { err = ValidNamespaceAndRegistry(config.Path)(config.Registry) if err != nil { return } } // If the Function does not yet have an image name and one was not provided on the command line if function.Image == "" { // AND a --registry was not provided, then we need to // prompt for a registry from which we can derive an image name. if config.Registry == "" { fmt.Println("A registry for Function images is required. For example, 'docker.io/tigerteam'.") err = survey.AskOne( &survey.Input{Message: "Registry for Function images:"}, &config.Registry, survey.WithValidator(ValidNamespaceAndRegistry(config.Path))) if err != nil { if errors.Is(err, terminal.InterruptErr) { return nil } return } fmt.Println("Note: building a Function the first time will take longer than subsequent builds") } // We have the registry, so let's use it to derive the Function image name config.Image = deriveImage(config.Image, config.Registry, config.Path) function.Image = config.Image } // All set, let's write changes in the config to the disk err = function.Write() if err != nil { return } // if registry was not changed via command line flag meaning it's empty // keep the same registry by setting the config.registry to empty otherwise // trust viper to override the env variable with the given flag if both are specified if regFlag, _ := cmd.Flags().GetString("registry"); regFlag == "" { config.Registry = "" } // Choose a builder based on the value of the --builder flag var builder fn.Builder if config.Builder == "pack" { builder = buildpacks.NewBuilder(config.Verbose) } else if config.Builder == "s2i" { builder = s2i.NewBuilder(s2i.WithVerbose(config.Verbose)) } else { err = errors.New("unrecognized builder: valid values are: s2i, pack") return } // Create a client using the registry defined in config plus any additional // options provided (such as mocks for testing) client, done := newClient(ClientConfig{Verbose: config.Verbose}, fn.WithRegistry(config.Registry), fn.WithBuilder(builder)) defer done() err = client.Build(cmd.Context(), config.Path) if err == nil && 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 } func newBuildConfig() buildConfig { return buildConfig{ Image: viper.GetString("image"), Path: viper.GetString("path"), 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"), } } // 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) { imageName := deriveImage(c.Image, c.Registry, c.Path) if !interactiveTerminal() || !c.Confirm { return c, nil } bc := buildConfig{Verbose: c.Verbose} var qs = []*survey.Question{ { Name: "path", Prompt: &survey.Input{ Message: "Project path:", Default: c.Path, }, Validate: survey.Required, }, { Name: "image", Prompt: &survey.Input{ Message: "Full image name (e.g. quay.io/boson/node-sample):", Default: imageName, }, Validate: survey.Required, }, } err := survey.Ask(qs, &bc) return bc, err }