func/cmd/root.go

448 lines
14 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 ""
}