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