func/cmd/build.go

347 lines
12 KiB
Go

package cmd
import (
"errors"
"fmt"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/ory/viper"
"github.com/spf13/cobra"
"knative.dev/func/pkg/builders"
"knative.dev/func/pkg/builders/buildpacks"
"knative.dev/func/pkg/builders/s2i"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
)
func NewBuildCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "build",
Short: "Build a function container",
Long: `
NAME
{{rootCmdUse}} build - Build a function container locally withoud deploying
SYNOPSIS
{{rootCmdUse}} build [-r|--registry] [--builder] [--builder-image] [--push]
[--platform] [-p|--path] [-c|--confirm] [-v|--verbose]
DESCRIPTION
Builds a function's container image and optionally pushes it to the
configured container registry.
By default building is handled automatically when deploying (see the deploy
subcommand). However, sometimes it is useful to build a function container
outside of this normal deployment process, for example for testing or during
composition when integrating with other systems. Additionally, the container
can be pushed to the configured registry using the --push option.
When building a function for the first time, either a registry or explicit
image name is required. Subsequent builds will reuse these option values.
EXAMPLES
o Build a function container using the given registry.
The full image name will be calculated using the registry and function name.
$ {{rootCmdUse}} build --registry registry.example.com/alice
o Build a function container using an explicit image name, ignoring registry
and function name.
$ {{rootCmdUse}} build --image registry.example.com/alice/f:latest
o Rebuild a function using prior values to determine container name.
$ {{rootCmdUse}} build
o Build a function specifying the Source-to-Image (S2I) builder
$ {{rootCmdUse}} build --builder=s2i
o Build a function specifying the Pack builder with a custom Buildpack
builder image.
$ {{rootCmdUse}} 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", "verbose"),
RunE: func(cmd *cobra.Command, args []string) error {
return runBuild(cmd, args, 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) // defined values on f take precedence over cfg defaults
}
// Flags
//
// NOTE on flag defaults:
// Use the config value when available, as this will include global static
// defaults, user settings and the value from the function with context.
// Use the function struct for flag flags which are not globally configurable
//
// Globally-Configurable Flags:
// Options whose value may be defined globally may also exist on the
// contextually relevant function; sets are flattened above via cfg.Apply(f)
cmd.Flags().StringP("builder", "b", cfg.Builder,
fmt.Sprintf("Builder to use when creating the function's container. Currently supported builders are %s. (Env: $FUNC_BUILDER)", 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. (Env: $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().StringP("builder-image", "", builderImage,
"Specify a custom builder image for use by the builder other than its default. (Env: $FUNC_BUILDER_IMAGE)")
cmd.Flags().StringP("image", "i", f.Image,
"Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE)")
// Static Flags:
// Options which have static defaults only (not globally configurable nor
// persisted with the function)
cmd.Flags().BoolP("push", "u", false,
"Attempt to push the function image to the configured registry after being successfully built")
cmd.Flags().StringP("platform", "", "",
"Optionally specify a target platform, for example \"linux/amd64\" when using the s2i build strategy")
// 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 runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err error) {
var (
cfg buildConfig
f fn.Function
)
if err = config.CreatePaths(); err != nil { // for possible auth.json usage
return
}
if cfg, err = newBuildConfig().Prompt(); err != nil {
return
}
if err = cfg.Validate(); err != nil {
return
}
if f, err = fn.NewFunction(cfg.Path); err != nil {
return
}
f = cfg.Configure(f) // Updates f at path to include build request values
// TODO: this logic is duplicated with runDeploy. Shouild be in buildConfig
// constructor.
// Checks if there is a difference between defined registry and its value used as a prefix in the image tag
// In case of a mismatch a new image tag is created and used for build
// Do not react if image tag has been changed outside configuration
if f.Registry != "" && !cmd.Flags().Changed("image") && strings.Index(f.Image, "/") > 0 && !strings.HasPrefix(f.Image, f.Registry) {
prfx := f.Registry
if prfx[len(prfx)-1:] != "/" {
prfx = prfx + "/"
}
sps := strings.Split(f.Image, "/")
updImg := prfx + sps[len(sps)-1]
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: function has current image '%s' which has a different registry than the currently configured registry '%s'. The new image tag will be '%s'. To use an explicit image, use --image.\n", f.Image, f.Registry, updImg)
f.Image = updImg
}
// Client
// Concrete implementations (ex builder) vary based on final effective config
var builder fn.Builder
if f.Build.Builder == builders.Pack {
builder = buildpacks.NewBuilder(
buildpacks.WithName(builders.Pack),
buildpacks.WithVerbose(cfg.Verbose))
} else if f.Build.Builder == builders.S2I {
builder = s2i.NewBuilder(
s2i.WithName(builders.S2I),
s2i.WithPlatform(cfg.Platform),
s2i.WithVerbose(cfg.Verbose))
} else {
return builders.ErrUnknownBuilder{Name: f.Build.Builder, Known: KnownBuilders()}
}
client, done := newClient(ClientConfig{Verbose: cfg.Verbose},
fn.WithRegistry(cfg.Registry),
fn.WithBuilder(builder))
defer done()
// TODO(lkingland): this write will be unnecessary when the client API is
// updated to accept function structs rather than a path as argument.
if err = f.Write(); err != nil {
return
}
// Build and (optionally) push
if err = client.Build(cmd.Context(), cfg.Path); err != nil {
return
}
if cfg.Push {
err = client.Push(cmd.Context(), cfg.Path)
}
// TODO(lkingland): when the above Build and Push calls are refactored to not
// write the function but instead take and return a function struct, use
// `return f.Write()` below and remove from above such that function on disk
// is only written on success and thus is always in a known valid state unless
// manually edited.
// return f.Write()
return
}
type buildConfig struct {
// Globals (builder, confirm, registry, verbose)
config.Global
// BuilderImage is the image (name or mapping) to use for building. Usually
// set automatically.
BuilderImage string
// 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
// Platform ofr resultant image (s2i builder only)
Platform string
// Push the resulting image to the registry after building.
Push bool
}
// newBuildConfig gathers options into a single build request.
func newBuildConfig() buildConfig {
return buildConfig{
Global: config.Global{
Builder: viper.GetString("builder"),
Confirm: viper.GetBool("confirm"),
Registry: registry(), // deferred defaulting
Verbose: viper.GetBool("verbose"),
},
BuilderImage: viper.GetString("builder-image"),
Image: viper.GetString("image"),
Path: viper.GetString("path"),
Platform: viper.GetString("platform"),
Push: viper.GetBool("push"),
}
}
// Configure the given function. Updates a function struct with all
// configurable values. Note that buildConfig already includes function's
// current values, as they were passed through via flag defaults, so overwriting
// is a noop.
func (c buildConfig) Configure(f fn.Function) fn.Function {
f = c.Global.Configure(f)
if f.Build.Builder != "" && c.BuilderImage != "" {
f.Build.BuilderImages[f.Build.Builder] = c.BuilderImage
}
f.Image = c.Image
// Path, Platform and Push are not part of a function's state.
return f
}
// Prompt the user with value of config members, allowing for interactive 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() {
return c, nil
}
// If there is no registry nor explicit image name defined, the
// Registry prompt is shown whether or not we are in confirm mode.
// Otherwise, it is only showin if in confirm mode
// NOTE: the default in this latter situation will ignore the current function
// value and will always use the value from the config (flag or env variable).
// This is not strictly correct and will be fixed when Global Config: Function
// Context is available (PR#1416)
f, err := fn.NewFunction(c.Path)
if err != nil {
return c, err
}
if (f.Registry == "" && c.Registry == "" && c.Image == "") || c.Confirm {
fmt.Println("A registry for function images is required. For example, 'docker.io/tigerteam'.")
err := survey.AskOne(
&survey.Input{Message: "Registry for function images:", Default: c.Registry},
&c.Registry,
survey.WithValidator(NewRegistryValidator(c.Path)))
if err != nil {
return c, fn.ErrRegistryRequired
}
fmt.Println("Note: building a function the first time will take longer than subsequent builds")
}
// Remainder of prompts are optional and only shown if in --confirm mode
if !c.Confirm {
return c, nil
}
// Image Name Override
// Calculate a better image name message which shows the value of the final
// image name as it will be calculated if an explicit image name is not used.
var imagePromptMessageSuffix string
if name := deriveImage(c.Image, c.Registry, c.Path); name != "" {
imagePromptMessageSuffix = fmt.Sprintf(". if not specified, the default '%v' will be used')", name)
}
qs := []*survey.Question{
{
Name: "image",
Prompt: &survey.Input{
Message: fmt.Sprintf("Image name to use (e.g. quay.io/boson/node-sample)%v:", imagePromptMessageSuffix),
},
},
{
Name: "path",
Prompt: &survey.Input{
Message: "Project path:",
Default: c.Path,
},
},
}
//
// TODO(lkingland): add confirmation prompts for other config members here
//
err = survey.Ask(qs, &c)
return c, err
}
// Validate the config passes an initial consistency check
func (c buildConfig) Validate() (err error) {
// Builder value must refer to a known builder short name
if err = ValidateBuilder(c.Builder); err != nil {
return
}
// Platform is only supported with the S2I builder at this time
if c.Platform != "" && c.Builder != builders.S2I {
err = errors.New("Only S2I builds currently support specifying platform")
return
}
return
}