mirror of https://github.com/knative/func.git
583 lines
18 KiB
Go
583 lines
18 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"golang.org/x/term"
|
|
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/AlecAivazis/survey/v2/terminal"
|
|
"github.com/ory/viper"
|
|
"github.com/spf13/cobra"
|
|
"knative.dev/client/pkg/util"
|
|
|
|
fn "knative.dev/kn-plugin-func"
|
|
"knative.dev/kn-plugin-func/builders"
|
|
"knative.dev/kn-plugin-func/buildpacks"
|
|
"knative.dev/kn-plugin-func/docker"
|
|
"knative.dev/kn-plugin-func/docker/creds"
|
|
"knative.dev/kn-plugin-func/k8s"
|
|
"knative.dev/kn-plugin-func/s2i"
|
|
)
|
|
|
|
func NewDeployCmd(newClient ClientFactory) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "deploy",
|
|
Short: "Deploy a function",
|
|
Long: `Deploy a function
|
|
|
|
Builds a container image for the function and deploys it to the connected Knative enabled cluster.
|
|
The function is picked up from the project in the current directory or from the path provided
|
|
with --path.
|
|
If not already configured, either --registry or --image has to be provided and is then stored
|
|
in the configuration file.
|
|
|
|
If the function is already deployed, it is updated with a new container image
|
|
that is pushed to an image registry, and finally the function's Knative service is updated.
|
|
`,
|
|
Example: `
|
|
# Build and deploy the function from the current directory's project. The image will be
|
|
# pushed to "quay.io/myuser/<function name>" and deployed as Knative service with the
|
|
# same name as the function to the currently connected cluster.
|
|
{{.Name}} deploy --registry quay.io/myuser
|
|
|
|
# Same as above but using a full image name, that will create a Knative service "myfunc" in
|
|
# the namespace "myns"
|
|
{{.Name}} deploy --image quay.io/myuser/myfunc -n myns
|
|
`,
|
|
SuggestFor: []string{"delpoy", "deplyo"},
|
|
PreRunE: bindEnv("image", "path", "registry", "confirm", "build", "push", "git-url", "git-branch", "git-dir", "builder", "builder-image", "platform"),
|
|
}
|
|
|
|
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
|
|
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().StringP("git-url", "g", "", "Repo url to push the code to be built (Env: $FUNC_GIT_URL)")
|
|
cmd.Flags().StringP("git-branch", "t", "", "Git branch to be used for remote builds (Env: $FUNC_GIT_BRANCH)")
|
|
cmd.Flags().StringP("git-dir", "d", "", "Directory in the repo where the function is located (Env: $FUNC_GIT_DIR)")
|
|
cmd.Flags().StringP("build", "b", fn.DefaultBuildType, fmt.Sprintf("Build specifies the way the function should be built. Supported types are %s (Env: $FUNC_BUILD)", fn.SupportedBuildTypes(true)))
|
|
// Flags shared with Build specifically related to building:
|
|
cmd.Flags().StringP("builder", "", 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().StringP("image", "i", "", "Full image name in the form [registry]/[namespace]/[name]:[tag]@[digest]. This option takes precedence over --registry. Specifying digest is optional, but if it is given, 'build' and 'push' phases are disabled. (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", true, "Attempt to push the function image to registry before deploying (Env: $FUNC_PUSH)")
|
|
cmd.Flags().StringP("platform", "", "", "Target platform to build (e.g. linux/amd64).")
|
|
setPathFlag(cmd)
|
|
|
|
if err := cmd.RegisterFlagCompletionFunc("build", CompleteDeployBuildType); err != nil {
|
|
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
|
|
}
|
|
|
|
if err := cmd.RegisterFlagCompletionFunc("builder", CompleteBuildersList); 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 runDeploy(cmd, args, newClient)
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func runDeploy(cmd *cobra.Command, _ []string, newClient ClientFactory) (err error) {
|
|
config, err := newDeployConfig(cmd)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
config, err = config.Prompt()
|
|
if err != nil {
|
|
if err == terminal.InterruptErr {
|
|
return nil
|
|
}
|
|
return
|
|
}
|
|
|
|
//if --image contains '@', validate image digest and disable build and push if not set, otherwise return an error
|
|
imageSplit := strings.Split(config.Image, "@")
|
|
imageDigestProvided := false
|
|
|
|
if len(imageSplit) == 2 {
|
|
if config, err = parseImageDigest(imageSplit, config, cmd); err != nil {
|
|
return
|
|
}
|
|
imageDigestProvided = true
|
|
}
|
|
|
|
function, err := functionWithOverrides(config.Path, functionOverrides{Namespace: config.Namespace, Image: config.Image})
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// save image digest if provided in --image
|
|
if imageDigestProvided {
|
|
function.ImageDigest = imageSplit[1]
|
|
}
|
|
|
|
// add ns to func.yaml on first deploy and warn if current context differs from func.yaml
|
|
function.Namespace, err = checkNamespaceDeploy(function.Namespace, config.Namespace)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
function.Envs, _, err = mergeEnvs(function.Envs, config.EnvToUpdate, config.EnvToRemove)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
currentBuildType := config.BuildType
|
|
|
|
// if build type has been explicitly set as flag, validate it and override function config
|
|
if config.BuildType != "" {
|
|
err = validateBuildType(config.BuildType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
currentBuildType = function.BuildType
|
|
}
|
|
|
|
// 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 the function does not yet have an image name and one was not provided on the command line
|
|
if function.Image == "" && currentBuildType != "disabled" {
|
|
// 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(survey.Required))
|
|
if err != nil {
|
|
if err == terminal.InterruptErr {
|
|
return nil
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Choose a builder based on the value of the --builder flag
|
|
var builder fn.Builder
|
|
if function.Builder == "" || cmd.Flags().Changed("builder") {
|
|
function.Builder = config.Builder
|
|
} else {
|
|
config.Builder = function.Builder
|
|
}
|
|
if err = ValidateBuilder(config.Builder); err != nil {
|
|
return err
|
|
}
|
|
if config.Builder == builders.Pack {
|
|
if config.Platform != "" {
|
|
err = fmt.Errorf("the --platform flag works only with s2i build")
|
|
return
|
|
}
|
|
builder = buildpacks.NewBuilder(buildpacks.WithVerbose(config.Verbose))
|
|
} else if config.Builder == builders.S2I {
|
|
builder = s2i.NewBuilder(s2i.WithVerbose(config.Verbose), s2i.WithPlatform(config.Platform))
|
|
}
|
|
|
|
// All set, let's write changes in the config to the disk
|
|
err = function.Write()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Default config namespace is the function's namespace
|
|
if config.Namespace == "" {
|
|
config.Namespace = function.Namespace
|
|
}
|
|
|
|
// 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 = ""
|
|
}
|
|
|
|
// Use the user-provided builder image, if supplied
|
|
if config.BuilderImage != "" {
|
|
function.BuilderImages[config.Builder] = config.BuilderImage
|
|
}
|
|
|
|
client, done := newClient(ClientConfig{Namespace: config.Namespace, Verbose: config.Verbose},
|
|
fn.WithRegistry(config.Registry),
|
|
fn.WithBuilder(builder))
|
|
defer done()
|
|
|
|
switch currentBuildType {
|
|
case fn.BuildTypeLocal, "":
|
|
if config.GitURL != "" || config.GitDir != "" || config.GitBranch != "" {
|
|
return fmt.Errorf("remote git arguments require the --build=git flag")
|
|
}
|
|
if err := client.Build(cmd.Context(), config.Path); err != nil {
|
|
return err
|
|
}
|
|
case fn.BuildTypeGit:
|
|
git := function.Git
|
|
|
|
if config.GitURL != "" {
|
|
git.URL = &config.GitURL
|
|
if strings.Contains(config.GitURL, "#") {
|
|
parts := strings.Split(config.GitURL, "#")
|
|
git.URL = &parts[0]
|
|
git.Revision = &parts[1]
|
|
}
|
|
}
|
|
|
|
if config.GitBranch != "" {
|
|
git.Revision = &config.GitBranch
|
|
}
|
|
|
|
if config.GitDir != "" {
|
|
git.ContextDir = &config.GitDir
|
|
}
|
|
|
|
return client.RunPipeline(cmd.Context(), config.Path, git)
|
|
case fn.BuildTypeDisabled:
|
|
// nothing needed to be done for `build=disabled`
|
|
default:
|
|
return ErrInvalidBuildType(fmt.Errorf("unknown build type: %s", currentBuildType))
|
|
}
|
|
|
|
if config.Push {
|
|
if err := client.Push(cmd.Context(), config.Path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return client.Deploy(cmd.Context(), config.Path)
|
|
}
|
|
|
|
func newPromptForCredentials(in io.Reader, out, errOut io.Writer) func(registry string) (docker.Credentials, error) {
|
|
firstTime := true
|
|
return func(registry string) (docker.Credentials, error) {
|
|
var result docker.Credentials
|
|
|
|
if firstTime {
|
|
firstTime = false
|
|
fmt.Fprintf(out, "Please provide credentials for image registry (%s).\n", registry)
|
|
} else {
|
|
fmt.Fprintln(out, "Incorrect credentials, please try again.")
|
|
}
|
|
|
|
var qs = []*survey.Question{
|
|
{
|
|
Name: "username",
|
|
Prompt: &survey.Input{
|
|
Message: "Username:",
|
|
},
|
|
Validate: survey.Required,
|
|
},
|
|
{
|
|
Name: "password",
|
|
Prompt: &survey.Password{
|
|
Message: "Password:",
|
|
},
|
|
Validate: survey.Required,
|
|
},
|
|
}
|
|
|
|
var (
|
|
fr terminal.FileReader
|
|
ok bool
|
|
)
|
|
|
|
isTerm := false
|
|
if fr, ok = in.(terminal.FileReader); ok {
|
|
isTerm = term.IsTerminal(int(fr.Fd()))
|
|
}
|
|
|
|
if isTerm {
|
|
err := survey.Ask(qs, &result, survey.WithStdio(fr, out.(terminal.FileWriter), errOut))
|
|
if err != nil {
|
|
return docker.Credentials{}, err
|
|
}
|
|
} else {
|
|
reader := bufio.NewReader(in)
|
|
|
|
fmt.Fprintf(out, "Username: ")
|
|
u, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return docker.Credentials{}, err
|
|
}
|
|
u = strings.Trim(u, "\r\n")
|
|
|
|
fmt.Fprintf(out, "Password: ")
|
|
p, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return docker.Credentials{}, err
|
|
}
|
|
p = strings.Trim(p, "\r\n")
|
|
|
|
result = docker.Credentials{Username: u, Password: p}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
func newPromptForCredentialStore() creds.ChooseCredentialHelperCallback {
|
|
return func(availableHelpers []string) (string, error) {
|
|
if len(availableHelpers) < 1 {
|
|
fmt.Fprintf(os.Stderr, `Credentials will not be saved.
|
|
If you would like to save your credentials in the future,
|
|
you can install docker credential helper https://github.com/docker/docker-credential-helpers.
|
|
`)
|
|
return "", nil
|
|
}
|
|
|
|
isTerm := term.IsTerminal(int(os.Stdin.Fd()))
|
|
|
|
var resp string
|
|
|
|
if isTerm {
|
|
err := survey.AskOne(&survey.Select{
|
|
Message: "Choose credentials helper",
|
|
Options: append(availableHelpers, "None"),
|
|
}, &resp, survey.WithValidator(survey.Required))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if resp == "None" {
|
|
fmt.Fprintf(os.Stderr, "No helper selected. Credentials will not be saved.\n")
|
|
return "", nil
|
|
}
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "Available credential helpers:\n")
|
|
for _, helper := range availableHelpers {
|
|
fmt.Fprintf(os.Stderr, "%s\n", helper)
|
|
}
|
|
fmt.Fprintf(os.Stderr, "Choose credentials helper: ")
|
|
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
var err error
|
|
resp, err = reader.ReadString('\n')
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
resp = strings.Trim(resp, "\r\n")
|
|
if resp == "" {
|
|
fmt.Fprintf(os.Stderr, "No helper selected. Credentials will not be saved.\n")
|
|
}
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
}
|
|
|
|
type deployConfig struct {
|
|
buildConfig
|
|
|
|
// Namespace override for the deployed function. If provided, the
|
|
// underlying platform will be instructed to deploy the function to the given
|
|
// namespace (if such a setting is applicable; such as for Kubernetes
|
|
// clusters). If not provided, the currently configured namespace will be
|
|
// used. For instance, that which would be used by default by `kubectl`
|
|
// (~/.kube/config) in the case of Kubernetes.
|
|
Namespace string
|
|
|
|
// Path of the function implementation on local disk. Defaults to current
|
|
// working directory of the process.
|
|
Path 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
|
|
|
|
// Build the associated function before deploying.
|
|
BuildType string
|
|
|
|
// Push function image to the registry before deploying.
|
|
Push bool
|
|
|
|
// Envs passed via cmd to be added/updated
|
|
EnvToUpdate *util.OrderedMap
|
|
|
|
// Envs passed via cmd to removed
|
|
EnvToRemove []string
|
|
|
|
// Git repo url for remote builds
|
|
GitURL string
|
|
|
|
// Git branch for remote builds
|
|
GitBranch string
|
|
|
|
// Directory in the git repo where the function is located
|
|
GitDir string
|
|
}
|
|
|
|
// newDeployConfig creates a buildConfig populated from command flags and
|
|
// environment variables; in that precedence.
|
|
func newDeployConfig(cmd *cobra.Command) (deployConfig, error) {
|
|
envToUpdate, envToRemove, err := envFromCmd(cmd)
|
|
if err != nil {
|
|
return deployConfig{}, err
|
|
}
|
|
|
|
// We need to know whether the `build`` flag had been explicitly set,
|
|
// to distinguish between unset and default value.
|
|
var buildType string
|
|
if viper.IsSet("build") {
|
|
buildType = viper.GetString("build")
|
|
}
|
|
|
|
return deployConfig{
|
|
buildConfig: newBuildConfig(),
|
|
Namespace: viper.GetString("namespace"),
|
|
Path: getPathFlag(),
|
|
Verbose: viper.GetBool("verbose"), // defined on root
|
|
Confirm: viper.GetBool("confirm"),
|
|
BuildType: buildType,
|
|
Push: viper.GetBool("push"),
|
|
EnvToUpdate: envToUpdate,
|
|
EnvToRemove: envToRemove,
|
|
GitURL: viper.GetString("git-url"),
|
|
GitBranch: viper.GetString("git-branch"),
|
|
GitDir: viper.GetString("git-dir"),
|
|
}, nil
|
|
}
|
|
|
|
// Prompt the user with value of config members, allowing for interaractive changes.
|
|
// Skipped if not in an interactive terminal (non-TTY), or if --yes (agree to
|
|
// all prompts) was explicitly set.
|
|
func (c deployConfig) Prompt() (deployConfig, error) {
|
|
if !interactiveTerminal() || !c.Confirm {
|
|
return c, nil
|
|
}
|
|
|
|
var qs = []*survey.Question{
|
|
{
|
|
Name: "registry",
|
|
Prompt: &survey.Input{
|
|
Message: "Registry for function images:",
|
|
Default: c.buildConfig.Registry,
|
|
},
|
|
Validate: survey.Required,
|
|
},
|
|
{
|
|
Name: "namespace",
|
|
Prompt: &survey.Input{
|
|
Message: "Namespace:",
|
|
Default: c.Namespace,
|
|
},
|
|
},
|
|
{
|
|
Name: "path",
|
|
Prompt: &survey.Input{
|
|
Message: "Project path:",
|
|
Default: c.Path,
|
|
},
|
|
Validate: survey.Required,
|
|
},
|
|
}
|
|
answers := struct {
|
|
Registry string
|
|
Namespace string
|
|
Path string
|
|
}{}
|
|
err := survey.Ask(qs, &answers)
|
|
if err != nil {
|
|
return deployConfig{}, err
|
|
}
|
|
|
|
dc := deployConfig{
|
|
buildConfig: buildConfig{
|
|
Registry: answers.Registry,
|
|
},
|
|
Namespace: answers.Namespace,
|
|
Path: answers.Path,
|
|
Verbose: c.Verbose,
|
|
}
|
|
|
|
dc.Image = deriveImage(dc.Image, dc.Registry, dc.Path)
|
|
|
|
return dc, nil
|
|
}
|
|
|
|
// ErrInvalidBuildType indicates that the passed build type was invalid.
|
|
type ErrInvalidBuildType error
|
|
|
|
// ValidateBuildType validatest that the input Build type is valid for deploy command
|
|
func validateBuildType(buildType string) error {
|
|
if errs := fn.ValidateBuildType(buildType, false, true); len(errs) > 0 {
|
|
return ErrInvalidBuildType(errors.New(strings.Join(errs, "")))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseImageDigest(imageSplit []string, config deployConfig, cmd *cobra.Command) (deployConfig, error) {
|
|
|
|
if !strings.HasPrefix(imageSplit[1], "sha256:") {
|
|
return config, fmt.Errorf("value '%s' in --image has invalid prefix syntax for digest (should be 'sha256:')", config.Image)
|
|
}
|
|
|
|
if len(imageSplit[1][7:]) != 64 {
|
|
return config, fmt.Errorf("sha256 hash in '%s' from --image has the wrong length (%d), should be 64", imageSplit[1], len(imageSplit[1][7:]))
|
|
}
|
|
|
|
// if --build was set but not as 'disabled', return an error
|
|
if cmd.Flags().Changed("build") && config.BuildType != "disabled" {
|
|
return config, fmt.Errorf("the --build flag '%s' is not valid when using --image with digest", config.BuildType)
|
|
}
|
|
|
|
// if the --push flag was set by a user to 'true', return an error
|
|
if cmd.Flags().Changed("push") && config.Push {
|
|
return config, fmt.Errorf("the --push flag '%v' is not valid when using --image with digest", config.Push)
|
|
}
|
|
|
|
fmt.Printf("Deploying existing image with digest %s. Build and push are disabled.\n", imageSplit[1])
|
|
|
|
config.BuildType = "disabled"
|
|
config.Push = false
|
|
config.Image = imageSplit[0]
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// checkNamespaceDeploy checks current namespace against func.yaml and warns if its different
|
|
// or sets namespace to be written in func.yaml if its the first deployment
|
|
func checkNamespaceDeploy(funcNamespace string, confNamespace string) (string, error) {
|
|
currNamespace, err := k8s.GetNamespace("")
|
|
if err != nil {
|
|
return funcNamespace, err
|
|
}
|
|
|
|
// If ns exists in func.yaml & NOT given via CLI (--namespace flag) & current ns does NOT match func.yaml ns
|
|
if funcNamespace != "" && confNamespace == "" && (currNamespace != funcNamespace) {
|
|
fmt.Fprintf(os.Stderr, "Warning: Current namespace '%s' does not match namespace '%s' in func.yaml. Function is deployed at '%s' namespace\n", currNamespace, funcNamespace, funcNamespace)
|
|
}
|
|
|
|
// Add current namespace to func.yaml if it is NOT set yet & NOT given via --namespace.
|
|
if funcNamespace == "" {
|
|
funcNamespace = currNamespace
|
|
}
|
|
|
|
return funcNamespace, nil
|
|
}
|