mirror of https://github.com/knative/func.git
448 lines
14 KiB
Go
448 lines
14 KiB
Go
package cmd
|
||
|
||
import (
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"github.com/Masterminds/semver"
|
||
"github.com/ory/viper"
|
||
"github.com/spf13/cobra"
|
||
"github.com/spf13/pflag"
|
||
"golang.org/x/term"
|
||
"k8s.io/apimachinery/pkg/util/sets"
|
||
"knative.dev/client/pkg/util"
|
||
|
||
"knative.dev/func/cmd/templates"
|
||
"knative.dev/func/pkg/config"
|
||
fn "knative.dev/func/pkg/functions"
|
||
"knative.dev/func/pkg/k8s"
|
||
)
|
||
|
||
// DefaultVersion when building source directly (bypassing the Makefile)
|
||
const DefaultVersion = "v0.0.0+source"
|
||
|
||
// DefaultNamespace is the global static default namespace, and is equivalent
|
||
// to the Kubernetes default namespace.
|
||
const DefaultNamespace = "default"
|
||
|
||
type RootCommandConfig struct {
|
||
Name string // usually `func` or `kn func`
|
||
Version
|
||
NewClient ClientFactory
|
||
}
|
||
|
||
// NewRootCmd creates the root of the command tree defines the command name, description, globally
|
||
// available flags, etc. It has no action of its own, such that running the
|
||
// resultant binary with no arguments prints the help/usage text.
|
||
func NewRootCmd(cfg RootCommandConfig) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: cfg.Name,
|
||
Short: fmt.Sprintf("%s manages Knative Functions", cfg.Name),
|
||
Long: fmt.Sprintf(`%s is the command line interface for managing Knative Function resources
|
||
|
||
Create a new Node.js function in the current directory:
|
||
{{.Use}} create --language node myfunction
|
||
|
||
Deploy the function using Docker hub to host the image:
|
||
{{.Use}} deploy --registry docker.io/alice
|
||
|
||
Learn more about Functions: https://knative.dev/docs/functions/
|
||
Learn more about Knative at: https://knative.dev`, cfg.Name),
|
||
|
||
DisableAutoGenTag: true, // no docs header
|
||
SilenceUsage: true, // no usage dump on error
|
||
SilenceErrors: true, // we explicitly handle errors in Execute()
|
||
}
|
||
|
||
// Environment Variables
|
||
// Evaluated first after static defaults, set all flags to be associated with
|
||
// a version prefixed by "FUNC_"
|
||
viper.AutomaticEnv() // read in environment variables for FUNC_<flag>
|
||
viper.SetEnvPrefix("func") // ensure that all have the prefix
|
||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||
|
||
// check if permissions for FUNC HOME are sufficient; warn if otherwise
|
||
cp := config.File()
|
||
if _, err := os.ReadFile(cp); os.IsPermission(err) {
|
||
fmt.Fprintf(os.Stderr, "Warning: Insufficient permissions to read config file at '%s' - continuing without it\n", cp)
|
||
}
|
||
// Client
|
||
// Use the provided ClientFactory or default to NewClient
|
||
newClient := cfg.NewClient
|
||
if newClient == nil {
|
||
newClient = NewClient
|
||
}
|
||
|
||
// Grouped commands
|
||
groups := templates.CommandGroups{
|
||
{
|
||
Header: "Primary Commands:",
|
||
Commands: []*cobra.Command{
|
||
NewCreateCmd(newClient),
|
||
NewDescribeCmd(newClient),
|
||
NewDeployCmd(newClient),
|
||
NewDeleteCmd(newClient),
|
||
NewListCmd(newClient),
|
||
NewSubscribeCmd(),
|
||
},
|
||
},
|
||
{
|
||
Header: "Development Commands:",
|
||
Commands: []*cobra.Command{
|
||
NewRunCmd(newClient),
|
||
NewInvokeCmd(newClient),
|
||
NewBuildCmd(newClient),
|
||
},
|
||
},
|
||
{
|
||
Header: "System Commands:",
|
||
Commands: []*cobra.Command{
|
||
NewConfigCmd(defaultLoaderSaver, newClient),
|
||
NewLanguagesCmd(newClient),
|
||
NewTemplatesCmd(newClient),
|
||
NewRepositoryCmd(newClient),
|
||
NewEnvironmentCmd(newClient, &cfg.Version),
|
||
},
|
||
},
|
||
{
|
||
Header: "Other Commands:",
|
||
Commands: []*cobra.Command{
|
||
NewCompletionCmd(),
|
||
NewVersionCmd(cfg.Version),
|
||
NewTektonClusterTasksCmd(),
|
||
},
|
||
},
|
||
}
|
||
|
||
// Add all commands to the root command, and initialize
|
||
groups.AddTo(cmd)
|
||
groups.SetRootUsage(cmd, nil)
|
||
|
||
return cmd
|
||
}
|
||
|
||
// Helpers
|
||
// ------------------------------------------
|
||
|
||
// registry to use is that provided as --registry or FUNC_REGISTRY.
|
||
// If not provided, global configuration determines the default to use.
|
||
func registry() string {
|
||
if r := viper.GetString("registry"); r != "" {
|
||
return r
|
||
}
|
||
cfg, _ := config.NewDefault()
|
||
return cfg.RegistryDefault()
|
||
}
|
||
|
||
// effectivePath to use is that which was provided by --path or FUNC_PATH.
|
||
// Manually parses flags such that this can be used during (cobra/viper) flag
|
||
// definition (prior to parsing).
|
||
func effectivePath() (path string) {
|
||
var (
|
||
env = os.Getenv("FUNC_PATH")
|
||
fs = pflag.NewFlagSet("", pflag.ContinueOnError)
|
||
p = fs.StringP("path", "p", "", "")
|
||
)
|
||
fs.SetOutput(io.Discard)
|
||
fs.ParseErrorsWhitelist.UnknownFlags = true // wokeignore:rule=whitelist
|
||
// Preparsing flags intentionally ignores errors because this is intended
|
||
// to be an opportunistic parse of the path flags, with actual validation of
|
||
// flags taking place later in the instantiation process by the cobra pkg.
|
||
_ = fs.Parse(os.Args[1:])
|
||
if env != "" {
|
||
path = env
|
||
}
|
||
if *p != "" {
|
||
path = *p
|
||
}
|
||
return path
|
||
}
|
||
|
||
// defaultNamespace to use when none is provided explicitly.
|
||
// This requires a bit more logic than normal flag defaults, which rely
|
||
// on the order of precedence Static Config -> Global Config -> Current Func ->
|
||
// -> Environment Variables -> Flags. This default calculation adds the
|
||
// step of using the active Kubernetes namespace after Static Config and before
|
||
// the optional Global Config setting. The static default is "default"
|
||
func defaultNamespace(f fn.Function, verbose bool) string {
|
||
// Specifically-requested
|
||
if f.Namespace != "" {
|
||
return f.Namespace
|
||
}
|
||
|
||
// Last deployed
|
||
if f.Deploy.Namespace != "" {
|
||
return f.Deploy.Namespace
|
||
}
|
||
|
||
// Active K8S namespace
|
||
namespace, err := k8s.GetDefaultNamespace()
|
||
if err != nil {
|
||
if verbose {
|
||
fmt.Fprintf(os.Stderr, "Unable to get current active kubernetes namespace. Defaults will be used. %v", err)
|
||
}
|
||
} else if namespace != "" {
|
||
return namespace
|
||
}
|
||
|
||
// Globally-defined default in ~/.config/func/config.yaml is next
|
||
cfg, err := config.NewDefault()
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "error loading global config at '%v'. %v\n", config.File(), err)
|
||
} else if cfg.Namespace != "" {
|
||
return cfg.Namespace
|
||
}
|
||
|
||
// Static Default is the standard Kubernetes default "default"
|
||
return DefaultNamespace
|
||
}
|
||
|
||
// interactiveTerminal returns whether or not the currently attached process
|
||
// terminal is interactive. Used for determining whether or not to
|
||
// interactively prompt the user to confirm default choices, etc.
|
||
func interactiveTerminal() bool {
|
||
return term.IsTerminal(int(os.Stdin.Fd()))
|
||
}
|
||
|
||
// bindFunc which conforms to the cobra PreRunE method signature
|
||
type bindFunc func(*cobra.Command, []string) error
|
||
|
||
// bindEnv returns a bindFunc that binds env vars to the named flags.
|
||
func bindEnv(flags ...string) bindFunc {
|
||
return func(cmd *cobra.Command, args []string) (err error) {
|
||
for _, flag := range flags {
|
||
if err = viper.BindPFlag(flag, cmd.Flags().Lookup(flag)); err != nil {
|
||
return
|
||
}
|
||
}
|
||
viper.AutomaticEnv() // read in environment variables for FUNC_<flag>
|
||
viper.SetEnvPrefix("func") // ensure that all have the prefix
|
||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||
return
|
||
}
|
||
}
|
||
|
||
// deriveName returns the explicit value (if provided) or attempts to derive
|
||
// from the given path. Path is defaulted to current working directory, where
|
||
// a function configuration, if it exists and contains a name, is used.
|
||
func deriveName(explicitName string, path string) string {
|
||
// If the name was explicitly provided, use it.
|
||
if explicitName != "" {
|
||
return explicitName
|
||
}
|
||
|
||
// If the directory at path contains an initialized function, use the name therein
|
||
f, err := fn.NewFunction(path)
|
||
if err == nil && f.Name != "" {
|
||
return f.Name
|
||
}
|
||
|
||
return ""
|
||
}
|
||
|
||
// deriveNameAndAbsolutePathFromPath returns resolved function name and absolute path
|
||
// to the function project root. The input parameter path could be one of:
|
||
// 'relative/path/to/foo', '/absolute/path/to/foo', 'foo' or ”.
|
||
func deriveNameAndAbsolutePathFromPath(path string) (string, string) {
|
||
var absPath string
|
||
|
||
// If path is not specified, we would like to use current working dir
|
||
if path == "" {
|
||
path = cwd()
|
||
}
|
||
|
||
// Expand the passed function name to its absolute path
|
||
absPath, err := filepath.Abs(path)
|
||
if err != nil {
|
||
return "", ""
|
||
}
|
||
|
||
// Get the name of the function, which equals to name of the current directory
|
||
pathParts := strings.Split(strings.TrimRight(path, string(os.PathSeparator)), string(os.PathSeparator))
|
||
return pathParts[len(pathParts)-1], absPath
|
||
}
|
||
|
||
func mergeEnvs(envs []fn.Env, envToUpdate *util.OrderedMap, envToRemove []string) ([]fn.Env, int, error) {
|
||
updated := sets.NewString()
|
||
|
||
var counter int
|
||
|
||
for i := range envs {
|
||
if envs[i].Name != nil {
|
||
value, present := envToUpdate.GetString(*envs[i].Name)
|
||
if present {
|
||
envs[i].Value = &value
|
||
updated.Insert(*envs[i].Name)
|
||
counter++
|
||
}
|
||
}
|
||
}
|
||
|
||
it := envToUpdate.Iterator()
|
||
for name, value, ok := it.NextString(); ok; name, value, ok = it.NextString() {
|
||
if !updated.Has(name) {
|
||
n := name
|
||
v := value
|
||
envs = append(envs, fn.Env{Name: &n, Value: &v})
|
||
counter++
|
||
}
|
||
}
|
||
|
||
for _, name := range envToRemove {
|
||
for i, envVar := range envs {
|
||
if *envVar.Name == name {
|
||
envs = append(envs[:i], envs[i+1:]...)
|
||
counter++
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
errMsg := fn.ValidateEnvs(envs)
|
||
if len(errMsg) > 0 {
|
||
return []fn.Env{}, 0, fmt.Errorf(strings.Join(errMsg, "\n"))
|
||
}
|
||
|
||
return envs, counter, nil
|
||
}
|
||
|
||
// addConfirmFlag ensures common text/wording when the --path flag is used
|
||
func addConfirmFlag(cmd *cobra.Command, dflt bool) {
|
||
cmd.Flags().BoolP("confirm", "c", dflt, "Prompt to confirm options interactively ($FUNC_CONFIRM)")
|
||
}
|
||
|
||
// addPathFlag ensures common text/wording when the --path flag is used
|
||
func addPathFlag(cmd *cobra.Command) {
|
||
cmd.Flags().StringP("path", "p", "", "Path to the function. Default is current directory ($FUNC_PATH)")
|
||
}
|
||
|
||
// addVerboseFlag ensures common text/wording when the --path flag is used
|
||
func addVerboseFlag(cmd *cobra.Command, dflt bool) {
|
||
cmd.Flags().BoolP("verbose", "v", dflt, "Print verbose logs ($FUNC_VERBOSE)")
|
||
}
|
||
|
||
// cwd returns the current working directory or exits 1 printing the error.
|
||
func cwd() (cwd string) {
|
||
cwd, err := os.Getwd()
|
||
if err != nil {
|
||
panic(fmt.Sprintf("Unable to determine current working directory: %v", err))
|
||
}
|
||
return cwd
|
||
}
|
||
|
||
// Version information populated on build.
|
||
type Version struct {
|
||
// Version tag of the git commit, or 'tip' if no tag.
|
||
Vers string
|
||
// Kver is the version of knative in which func was most recently
|
||
// If the build is not tagged as being released with a specific Knative
|
||
// build, this is the most recent version of knative along with a suffix
|
||
// consisting of the number of commits which have been added since it was
|
||
// included in Knative.
|
||
Kver string
|
||
// Hash of the currently active git commit on build.
|
||
Hash string
|
||
// Verbose printing enabled for the string representation.
|
||
Verbose bool
|
||
}
|
||
|
||
// Return the stringification of the Version struct.
|
||
func (v Version) String() string {
|
||
// Initialize the default value to the zero semver with a descriptive
|
||
// metadta tag indicating this must have been built from source if
|
||
// undefined:
|
||
if v.Vers == "" {
|
||
v.Vers = DefaultVersion
|
||
}
|
||
if v.Verbose {
|
||
return v.StringVerbose()
|
||
}
|
||
_ = semver.MustParse(v.Vers)
|
||
return v.Vers
|
||
}
|
||
|
||
// StringVerbose returns the version along with extended version metadata.
|
||
func (v Version) StringVerbose() string {
|
||
var (
|
||
vers = v.Vers
|
||
kver = v.Kver
|
||
hash = v.Hash
|
||
)
|
||
if strings.HasPrefix(kver, "knative-") {
|
||
kver = strings.Split(kver, "-")[1]
|
||
}
|
||
return fmt.Sprintf(
|
||
"Version: %s\n"+
|
||
"Knative: %s\n"+
|
||
"Commit: %s\n"+
|
||
"SocatImage: %s\n"+
|
||
"TarImage: %s\n",
|
||
vers,
|
||
kver,
|
||
hash,
|
||
k8s.SocatImage,
|
||
k8s.TarImage)
|
||
}
|
||
|
||
// surveySelectDefault returns 'value' if defined and exists in 'options'.
|
||
// Otherwise, options[0] is returned if it exists. Empty string otherwise.
|
||
//
|
||
// Usage Example:
|
||
//
|
||
// languages := []string{ "go", "node", "rust" },
|
||
// survey.Select{
|
||
// Options: options,
|
||
// Default: surveySelectDefaut(cfg.Language, languages),
|
||
// }
|
||
//
|
||
// Summary:
|
||
//
|
||
// This protects against an incorrectly initialized survey.Select when the user
|
||
// has provided a nonexistant option (validation is handled elsewhere) or
|
||
// when a value is required but there exists no defaults (no default value on
|
||
// the associated flag).
|
||
//
|
||
// Explanation:
|
||
//
|
||
// The above example chooses the default for the Survey (--confirm) question
|
||
// in a way that works with user-provided flag and environment variable values.
|
||
//
|
||
// `cfg.Language` is the current value set in the config struct, which is
|
||
// populated from (in ascending order of precedence):
|
||
// static flag default, associated environment variable, or command flag.
|
||
// `languages` are the options which are being used by the survey select.
|
||
//
|
||
// This cascade allows for the Survey questions to be properly pre-initialzed
|
||
// with their associated environment variables or flags. For example,
|
||
// A user whose default language is set to 'node' using the global environment
|
||
// variable FUNC_LANGUAGE will have that option pre-selected when running
|
||
// `func create -c`.
|
||
//
|
||
// The 'survey' package expects the value of the Default member to exist
|
||
// in the 'Options' member. This is not possible when user-provided data is
|
||
// allowed for the default, hence this logic is necessary.
|
||
//
|
||
// For example, when the user is using prompts (--confirm) to select from a set
|
||
// of options, but the associated flag either has an unrecognized value, or no
|
||
// value at all, without this logic the resulting select prompt would be
|
||
// initialized with this as the default value, and the act of what appears to
|
||
// be choose the first option displayed does not overwrite the invalid default.
|
||
// It could perhaps be argued this is a shortcoming in the survey package, but
|
||
// it is also clearly an error to provide invalid data for a default.
|
||
func surveySelectDefault(value string, options []string) string {
|
||
for _, v := range options {
|
||
if value == v {
|
||
return v // The provided value is acceptable
|
||
}
|
||
}
|
||
if len(options) > 0 {
|
||
return options[0] // Sync with the option which will be shown by the UX
|
||
}
|
||
// Either the value is not an option or there are no options. Either of
|
||
// which should fail proper validation
|
||
return ""
|
||
}
|