459 lines
14 KiB
Go
459 lines
14 KiB
Go
/*
|
|
Copyright 2025 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package kuberc
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"k8s.io/kubectl/pkg/config"
|
|
kuberc "k8s.io/kubectl/pkg/config/install"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
"k8s.io/client-go/util/homedir"
|
|
)
|
|
|
|
const RecommendedKubeRCFileName = "kuberc"
|
|
|
|
var (
|
|
RecommendedConfigDir = filepath.Join(homedir.HomeDir(), clientcmd.RecommendedHomeDir)
|
|
RecommendedKubeRCFile = filepath.Join(RecommendedConfigDir, RecommendedKubeRCFileName)
|
|
|
|
aliasNameRegex = regexp.MustCompile("^[a-zA-Z]+$")
|
|
shortHandRegex = regexp.MustCompile("^-[a-zA-Z]+$")
|
|
|
|
scheme = runtime.NewScheme()
|
|
strictCodecs = serializer.NewCodecFactory(scheme, serializer.EnableStrict)
|
|
lenientCodecs = serializer.NewCodecFactory(scheme, serializer.DisableStrict)
|
|
)
|
|
|
|
func init() {
|
|
kuberc.Install(scheme)
|
|
}
|
|
|
|
// PreferencesHandler is responsible for setting default flags
|
|
// arguments based on user's kuberc configuration.
|
|
type PreferencesHandler interface {
|
|
AddFlags(flags *pflag.FlagSet)
|
|
Apply(rootCmd *cobra.Command, args []string, errOut io.Writer) ([]string, error)
|
|
}
|
|
|
|
// Preferences stores the kuberc file coming either from environment variable
|
|
// or file from set in flag or the default kuberc path.
|
|
type Preferences struct {
|
|
getPreferencesFunc func(kuberc string, errOut io.Writer) (*config.Preference, error)
|
|
|
|
aliases map[string]struct{}
|
|
}
|
|
|
|
// NewPreferences returns initialized Prefrences object.
|
|
func NewPreferences() PreferencesHandler {
|
|
return &Preferences{
|
|
getPreferencesFunc: DefaultGetPreferences,
|
|
aliases: make(map[string]struct{}),
|
|
}
|
|
}
|
|
|
|
type aliasing struct {
|
|
appendArgs []string
|
|
prependArgs []string
|
|
flags []config.CommandOverrideFlag
|
|
command *cobra.Command
|
|
}
|
|
|
|
// AddFlags adds kuberc related flags into the command.
|
|
func (p *Preferences) AddFlags(flags *pflag.FlagSet) {
|
|
flags.String("kuberc", "", "Path to the kuberc file to use for preferences. This can be disabled by exporting KUBECTL_KUBERC=false.")
|
|
}
|
|
|
|
// Apply firstly applies the aliases in the preferences file and secondly overrides
|
|
// the default values of flags.
|
|
func (p *Preferences) Apply(rootCmd *cobra.Command, args []string, errOut io.Writer) ([]string, error) {
|
|
if len(args) <= 1 {
|
|
return args, nil
|
|
}
|
|
|
|
kubercPath, err := getExplicitKuberc(args)
|
|
if err != nil {
|
|
return args, err
|
|
}
|
|
kuberc, err := p.getPreferencesFunc(kubercPath, errOut)
|
|
if err != nil {
|
|
return args, fmt.Errorf("kuberc error %w", err)
|
|
}
|
|
|
|
if kuberc == nil {
|
|
return args, nil
|
|
}
|
|
|
|
err = validate(kuberc)
|
|
if err != nil {
|
|
return args, err
|
|
}
|
|
|
|
args, err = p.applyAliases(rootCmd, kuberc, args, errOut)
|
|
if err != nil {
|
|
return args, err
|
|
}
|
|
err = p.applyOverrides(rootCmd, kuberc, args, errOut)
|
|
if err != nil {
|
|
return args, err
|
|
}
|
|
return args, nil
|
|
}
|
|
|
|
// applyOverrides finds the command and sets the defaulted flag values in kuberc.
|
|
func (p *Preferences) applyOverrides(rootCmd *cobra.Command, kuberc *config.Preference, args []string, errOut io.Writer) error {
|
|
args = args[1:]
|
|
cmd, _, err := rootCmd.Find(args)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
for _, c := range kuberc.Overrides {
|
|
parsedCmds := strings.Fields(c.Command)
|
|
overrideCmd, _, err := rootCmd.Find(parsedCmds)
|
|
if err != nil {
|
|
fmt.Fprintf(errOut, "Warning: command %q not found to set kuberc override\n", c.Command)
|
|
continue
|
|
}
|
|
if overrideCmd.Name() != cmd.Name() {
|
|
continue
|
|
}
|
|
|
|
if _, ok := p.aliases[cmd.Name()]; ok {
|
|
return fmt.Errorf("alias %s can not be overridden", cmd.Name())
|
|
}
|
|
|
|
// This function triggers merging the persistent flags in the parent commands.
|
|
_ = cmd.InheritedFlags()
|
|
|
|
allShorthands := make(map[string]struct{})
|
|
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
|
|
if flag.Shorthand != "" {
|
|
allShorthands[flag.Shorthand] = struct{}{}
|
|
}
|
|
})
|
|
|
|
for _, fl := range c.Flags {
|
|
existingFlag := cmd.Flag(fl.Name)
|
|
if existingFlag == nil {
|
|
return fmt.Errorf("invalid flag %s for command %s", fl.Name, c.Command)
|
|
}
|
|
if searchInArgs(existingFlag.Name, existingFlag.Shorthand, allShorthands, args) {
|
|
// Don't modify the value implicitly, if it is passed in args explicitly
|
|
continue
|
|
}
|
|
err = cmd.Flags().Set(fl.Name, fl.Default)
|
|
if err != nil {
|
|
return fmt.Errorf("could not apply override value %s to flag %s in command %s err: %w", fl.Default, fl.Name, c.Command, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// applyAliases firstly appends all defined aliases in kuberc file to the root command.
|
|
// Since there may be several alias definitions belonging to the same command, it extracts the
|
|
// alias that is currently executed from args. After that it sets the flag definitions in alias as default values
|
|
// of the command. Lastly, others parameters (e.g. resources, etc.) that are passed as arguments in kuberc
|
|
// is appended into the command args.
|
|
func (p *Preferences) applyAliases(rootCmd *cobra.Command, kuberc *config.Preference, args []string, errOut io.Writer) ([]string, error) {
|
|
_, _, err := rootCmd.Find(args[1:])
|
|
if err == nil {
|
|
// Command is found, no need to continue for aliasing
|
|
return args, nil
|
|
}
|
|
|
|
var aliasArgs *aliasing
|
|
|
|
var commandName string // first "non-flag" arguments
|
|
var commandIndex int
|
|
for index, arg := range args[1:] {
|
|
if !strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, cobra.ShellCompRequestCmd) {
|
|
commandName = arg
|
|
commandIndex = index + 1
|
|
break
|
|
}
|
|
}
|
|
|
|
for _, alias := range kuberc.Aliases {
|
|
p.aliases[alias.Name] = struct{}{}
|
|
if alias.Name != commandName {
|
|
continue
|
|
}
|
|
|
|
// do not allow shadowing built-ins
|
|
if _, _, err := rootCmd.Find([]string{alias.Name}); err == nil {
|
|
fmt.Fprintf(errOut, "Warning: Setting alias %q to a built-in command is not supported\n", alias.Name)
|
|
break
|
|
}
|
|
|
|
commands := strings.Fields(alias.Command)
|
|
existingCmd, flags, err := rootCmd.Find(commands)
|
|
if err != nil {
|
|
return args, fmt.Errorf("command %q not found to set alias %q: %v", alias.Command, alias.Name, flags)
|
|
}
|
|
|
|
newCmd := *existingCmd
|
|
newCmd.Use = alias.Name
|
|
newCmd.Aliases = []string{}
|
|
aliasCmd := &newCmd
|
|
|
|
aliasArgs = &aliasing{
|
|
prependArgs: alias.PrependArgs,
|
|
appendArgs: alias.AppendArgs,
|
|
flags: alias.Flags,
|
|
command: aliasCmd,
|
|
}
|
|
break
|
|
}
|
|
|
|
if aliasArgs == nil {
|
|
// pursue with the current behavior.
|
|
// This might be a built-in command, external plugin, etc.
|
|
return args, nil
|
|
}
|
|
|
|
rootCmd.AddCommand(aliasArgs.command)
|
|
|
|
foundAliasCmd, _, err := rootCmd.Find([]string{commandName})
|
|
if err != nil {
|
|
return args, nil
|
|
}
|
|
|
|
// This function triggers merging the persistent flags in the parent commands.
|
|
_ = foundAliasCmd.InheritedFlags()
|
|
|
|
allShorthands := make(map[string]struct{})
|
|
foundAliasCmd.Flags().VisitAll(func(flag *pflag.Flag) {
|
|
if flag.Shorthand != "" {
|
|
allShorthands[flag.Shorthand] = struct{}{}
|
|
}
|
|
})
|
|
|
|
for _, fl := range aliasArgs.flags {
|
|
existingFlag := foundAliasCmd.Flag(fl.Name)
|
|
if existingFlag == nil {
|
|
return args, fmt.Errorf("invalid alias flag %s in alias %s", fl.Name, args[0])
|
|
}
|
|
if searchInArgs(existingFlag.Name, existingFlag.Shorthand, allShorthands, args) {
|
|
// Don't modify the value implicitly, if it is passed in args explicitly
|
|
continue
|
|
}
|
|
err = foundAliasCmd.Flags().Set(fl.Name, fl.Default)
|
|
if err != nil {
|
|
return args, fmt.Errorf("could not apply value %s to flag %s in alias %s err: %w", fl.Default, fl.Name, args[0], err)
|
|
}
|
|
}
|
|
|
|
if len(aliasArgs.prependArgs) > 0 {
|
|
// prependArgs defined in kuberc should be inserted after the alias name.
|
|
if commandIndex+1 >= len(args) {
|
|
// command is the last item, we simply append just like appendArgs
|
|
args = append(args, aliasArgs.prependArgs...)
|
|
} else {
|
|
args = append(args[:commandIndex+1], append(aliasArgs.prependArgs, args[commandIndex+1:]...)...)
|
|
}
|
|
}
|
|
if len(aliasArgs.appendArgs) > 0 {
|
|
// appendArgs defined in kuberc should be appended to actual args.
|
|
args = append(args, aliasArgs.appendArgs...)
|
|
}
|
|
// Cobra (command.go#L1078) appends only root command's args into the actual args and ignores the others.
|
|
// We are appending the additional args defined in kuberc in here and
|
|
// expect that it will be passed along to the actual command.
|
|
rootCmd.SetArgs(args[1:])
|
|
return args, nil
|
|
}
|
|
|
|
// DefaultGetPreferences returns KubeRCConfiguration.
|
|
// If users sets kuberc file explicitly in --kuberc flag, it has the highest
|
|
// priority. If not specified, it looks for in KUBERC environment variable.
|
|
// If KUBERC is also not set, it falls back to default .kuberc file at the same location
|
|
// where kubeconfig's defaults are residing in.
|
|
// If KUBERC is set to "off", kuberc will be turned off and original behaviors in kubectl will be applied.
|
|
func DefaultGetPreferences(kuberc string, errOut io.Writer) (*config.Preference, error) {
|
|
if val := os.Getenv("KUBERC"); val == "off" {
|
|
if kuberc != "" {
|
|
return nil, fmt.Errorf("disabling kuberc via KUBERC=off and passing kuberc flag are mutually exclusive")
|
|
}
|
|
return nil, nil
|
|
}
|
|
kubeRCFile := RecommendedKubeRCFile
|
|
explicitly := false
|
|
if kuberc != "" {
|
|
kubeRCFile = kuberc
|
|
explicitly = true
|
|
}
|
|
|
|
if kubeRCFile == "" && os.Getenv("KUBERC") != "" {
|
|
kubeRCFile = os.Getenv("KUBERC")
|
|
explicitly = true
|
|
}
|
|
|
|
preference, err := decodePreference(kubeRCFile)
|
|
switch {
|
|
case explicitly && preference != nil && runtime.IsStrictDecodingError(err):
|
|
// if explicitly requested, just warn about strict decoding errors if we got a usable Preference object back
|
|
fmt.Fprintf(errOut, "kuberc: ignoring strict decoding error in %s: %v", kubeRCFile, err)
|
|
return preference, nil
|
|
|
|
case explicitly && err != nil:
|
|
// if explicitly requested, error on any error other than a StrictDecodingError
|
|
return nil, fmt.Errorf("kuberc: %w", err)
|
|
|
|
case !explicitly && os.IsNotExist(err):
|
|
// if not explicitly requested, silently ignore missing kuberc
|
|
return nil, nil
|
|
|
|
case !explicitly && err != nil:
|
|
// if not explicitly requested, only warn on any other error
|
|
fmt.Fprintf(errOut, "kuberc: no preferences loaded from %s: %v", kubeRCFile, err)
|
|
return nil, nil
|
|
|
|
default:
|
|
return preference, nil
|
|
}
|
|
}
|
|
|
|
// Normally, we should extract this value directly from kuberc flag.
|
|
// However, flag values are set during the command execution and
|
|
// we are in very early stages to prepare commands prior to execute them.
|
|
// Besides, we only need kuberc flag value in this stage.
|
|
func getExplicitKuberc(args []string) (string, error) {
|
|
var kubercPath string
|
|
for i, arg := range args {
|
|
if arg == "--" {
|
|
// flags after "--" does not represent any flag of
|
|
// the command. We should short cut the iteration in here.
|
|
break
|
|
}
|
|
if arg == "--kuberc" {
|
|
if i+1 < len(args) {
|
|
kubercPath = args[i+1]
|
|
break
|
|
}
|
|
return "", fmt.Errorf("kuberc file is not found")
|
|
} else if strings.Contains(arg, "--kuberc=") {
|
|
parg := strings.Split(arg, "=")
|
|
if len(parg) > 1 && parg[1] != "" {
|
|
kubercPath = parg[1]
|
|
break
|
|
}
|
|
return "", fmt.Errorf("kuberc file is not found")
|
|
}
|
|
}
|
|
|
|
if kubercPath == "" {
|
|
return "", nil
|
|
}
|
|
|
|
return kubercPath, nil
|
|
}
|
|
|
|
// searchInArgs searches the given key in the args and returns
|
|
// true, if it finds. Otherwise, it returns false.
|
|
func searchInArgs(flagName string, shorthand string, allShorthands map[string]struct{}, args []string) bool {
|
|
for _, arg := range args {
|
|
// if flag is set in args in "--flag value" or "--flag=value" format,
|
|
// we should return it as found
|
|
if fmt.Sprintf("--%s", flagName) == arg || strings.HasPrefix(arg, fmt.Sprintf("--%s=", flagName)) {
|
|
return true
|
|
}
|
|
if shorthand == "" {
|
|
continue
|
|
}
|
|
// shorthand can be in "-n value" or "-nvalue" format
|
|
// it is guaranteed that shorthand is one letter. So that
|
|
// checking just the prefix -oyaml also finds --output.
|
|
if strings.HasPrefix(arg, fmt.Sprintf("-%s", shorthand)) {
|
|
return true
|
|
}
|
|
|
|
if !shortHandRegex.MatchString(arg) {
|
|
continue
|
|
}
|
|
|
|
// remove prefix "-"
|
|
arg = arg[1:]
|
|
// short hands can be in a combined "-abc" format.
|
|
// First we need to ensure that all the values are shorthand to safely search ours.
|
|
// Because we know that "-abcvalue" is not valid. So that we need to be sure that if we find
|
|
// "b" it correctly refers to the shorthand "b" not arbitrary value "-cargb".
|
|
arbitraryFound := false
|
|
for _, runeValue := range shorthand {
|
|
if _, ok := allShorthands[string(runeValue)]; !ok {
|
|
arbitraryFound = true
|
|
break
|
|
}
|
|
}
|
|
if arbitraryFound {
|
|
continue
|
|
}
|
|
// verified that all values are short hand. Now search ours
|
|
if strings.Contains(arg, shorthand) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func validate(plugin *config.Preference) error {
|
|
validateFlag := func(flags []config.CommandOverrideFlag) error {
|
|
for _, flag := range flags {
|
|
if strings.HasPrefix(flag.Name, "-") {
|
|
return fmt.Errorf("flag name %s should be in long form without dashes", flag.Name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
aliases := make(map[string]struct{})
|
|
for _, alias := range plugin.Aliases {
|
|
if !aliasNameRegex.MatchString(alias.Name) {
|
|
return fmt.Errorf("invalid alias name, can only include alphabetical characters")
|
|
}
|
|
|
|
if err := validateFlag(alias.Flags); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, ok := aliases[alias.Name]; ok {
|
|
return fmt.Errorf("duplicate alias name %s", alias.Name)
|
|
}
|
|
aliases[alias.Name] = struct{}{}
|
|
}
|
|
|
|
for _, override := range plugin.Overrides {
|
|
if err := validateFlag(override.Flags); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|