mirror of https://github.com/knative/func.git
307 lines
10 KiB
Go
307 lines
10 KiB
Go
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
|
|
}
|