Introduce kuberc as new flag to customize defaulting and define aliases in kubectl (#125230)

Kubernetes-commit: c7a90b670c40a315bea3667921302675008bc39c
This commit is contained in:
Arda Güçlü 2025-02-11 23:05:58 +03:00 committed by Kubernetes Publisher
parent 2fd8e3617a
commit 2e38fc2204
19 changed files with 4178 additions and 12 deletions

10
go.mod
View File

@ -31,11 +31,11 @@ require (
github.com/stretchr/testify v1.9.0
golang.org/x/sys v0.28.0
gopkg.in/evanphx/json-patch.v4 v4.12.0
k8s.io/api v0.0.0-20250205124818-68351e3d8f2c
k8s.io/apimachinery v0.0.0-20250130161731-a2cb7d3ca743
k8s.io/api v0.0.0-20250211114750-4629116ef3ab
k8s.io/apimachinery v0.0.0-20250211114440-46c230ea8d65
k8s.io/cli-runtime v0.0.0-20250115210038-303c7e6c2210
k8s.io/client-go v0.0.0-20250130002447-362c5e8de9fa
k8s.io/component-base v0.0.0-20250130203310-264c1fd30132
k8s.io/client-go v0.0.0-20250211115216-8683d2da3be9
k8s.io/component-base v0.0.0-20250206205508-05a58ccfe08d
k8s.io/component-helpers v0.0.0-20250206005633-32b49ece5108
k8s.io/klog/v2 v2.130.1
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7
@ -94,3 +94,5 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/kustomize/api v0.19.0 // indirect
)
replace k8s.io/code-generator => k8s.io/code-generator v0.0.0-20250211120344-47286fcaaaaa

16
go.sum
View File

@ -198,16 +198,16 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.0.0-20250205124818-68351e3d8f2c h1:G1EScEUAUdkN0CMqZmKa1Tdi+Y3RpzsgHiuwnXimChU=
k8s.io/api v0.0.0-20250205124818-68351e3d8f2c/go.mod h1:ZLCbRmcWnRFuucF3pJbT54THedF3cuZ9RlLvspU+RPA=
k8s.io/apimachinery v0.0.0-20250130161731-a2cb7d3ca743 h1:E5AZGEsMCbCRL7z58Mhpx50+b1gYF5GrlXLCdbsmk+M=
k8s.io/apimachinery v0.0.0-20250130161731-a2cb7d3ca743/go.mod h1:h8DnJz4KNjkQsP8iFir+s3sSBEK3Iy43bfB2gFjSR+A=
k8s.io/api v0.0.0-20250211114750-4629116ef3ab h1:bBsQSUPkp7s90RsTrNPfVKWOeX1jqXxYDgnoc1bjs8Y=
k8s.io/api v0.0.0-20250211114750-4629116ef3ab/go.mod h1:9+9XPWTbyV1YwAc5YizzbMHBe4gp7BY2PPZ3+DxXjxw=
k8s.io/apimachinery v0.0.0-20250211114440-46c230ea8d65 h1:RADrjyqn52TmFg79piA2+zmjMJYBRxeR65d4YnqNhQE=
k8s.io/apimachinery v0.0.0-20250211114440-46c230ea8d65/go.mod h1:pvurfgWU15pkR11HFlMI9tdxY59XU+Wzo22Rx2iSD+g=
k8s.io/cli-runtime v0.0.0-20250115210038-303c7e6c2210 h1:+jWb1uCQT9Ziw2BA71oeFJivnMvElWl1Yp7pCkqAiwQ=
k8s.io/cli-runtime v0.0.0-20250115210038-303c7e6c2210/go.mod h1:s6HFZo5PA2FRMxuw11017JV0ga6qUULOWsDoBpiahfE=
k8s.io/client-go v0.0.0-20250130002447-362c5e8de9fa h1:HWmGBG30KYT4wp83FuUB6k3R1Tz8Er1ayhvIZ3r7Wyk=
k8s.io/client-go v0.0.0-20250130002447-362c5e8de9fa/go.mod h1:ovoXYEZn1G9PxrTjiz9avs6YwrgHYXWXd8EkrVc0EE8=
k8s.io/component-base v0.0.0-20250130203310-264c1fd30132 h1:qUClVn8+kthpVfr2Gqpvrh49GdQXujoavR9j8bSfw40=
k8s.io/component-base v0.0.0-20250130203310-264c1fd30132/go.mod h1:/PTOs1kJmSNpQU3qdnQNS+803zGLltnv5h9Fu8EZDaI=
k8s.io/client-go v0.0.0-20250211115216-8683d2da3be9 h1:4qQCNM+BGSmABHbooN1JAc0j6LEyMCuIqSixdBcs5V4=
k8s.io/client-go v0.0.0-20250211115216-8683d2da3be9/go.mod h1:69142mPf6rG98xzKZ6K7fhlccQVwPbCp5QbavQEqViU=
k8s.io/component-base v0.0.0-20250206205508-05a58ccfe08d h1:ucGaCLCdQDECgSOvEVYGRNTkUPamA+Of3SfwVWovZUE=
k8s.io/component-base v0.0.0-20250206205508-05a58ccfe08d/go.mod h1:m0Zr1J4qm4/+KLOEn4YxoHUVMkKFWyrm1TRORDgE9sY=
k8s.io/component-helpers v0.0.0-20250206005633-32b49ece5108 h1:TAPA2Jpn1Fy14tSRs3KazKMNah+CawqqcrOlQe6MRCM=
k8s.io/component-helpers v0.0.0-20250206005633-32b49ece5108/go.mod h1:kugcd/6pG5WAfaANP3syNMgk+/SmulwdVgKecKrMnBI=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=

View File

@ -73,6 +73,7 @@ import (
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/cmd/version"
"k8s.io/kubectl/pkg/cmd/wait"
"k8s.io/kubectl/pkg/kuberc"
utilcomp "k8s.io/kubectl/pkg/util/completion"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
@ -361,6 +362,11 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
flags.BoolVar(&warningsAsErrors, "warnings-as-errors", warningsAsErrors, "Treat warnings received from the server as errors and exit with a non-zero exit code")
pref := kuberc.NewPreferences()
if cmdutil.KubeRC.IsEnabled() {
pref.AddFlags(flags)
}
kubeConfigFlags := o.ConfigFlags
if kubeConfigFlags == nil {
kubeConfigFlags = defaultConfigFlags().WithWarningPrinter(o.IOStreams)
@ -490,6 +496,15 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
// Stop warning about normalization of flags. That makes it possible to
// add the klog flags later.
cmds.SetGlobalNormalizationFunc(cliflag.WordSepNormalizeFunc)
if cmdutil.KubeRC.IsEnabled() {
_, err := pref.Apply(cmds, o.Arguments, o.IOStreams.ErrOut)
if err != nil {
fmt.Fprintf(o.IOStreams.ErrOut, "error occurred while applying preferences %v\n", err)
os.Exit(1)
}
}
return cmds
}

View File

@ -432,6 +432,7 @@ const (
PortForwardWebsockets FeatureGate = "KUBECTL_PORT_FORWARD_WEBSOCKETS"
// DebugCustomProfile should be dropped in 1.34
DebugCustomProfile FeatureGate = "KUBECTL_DEBUG_CUSTOM_PROFILE"
KubeRC FeatureGate = "KUBECTL_KUBERC"
)
// IsEnabled returns true iff environment variable is set to true.

12
pkg/config/OWNERS Normal file
View File

@ -0,0 +1,12 @@
# See the OWNERS docs at https://go.k8s.io/owners
# Disable inheritance as this is an api owners file
options:
no_parent_owners: true
approvers:
- api-approvers
reviewers:
- api-reviewers
- sig-cli-reviewers
labels:
- kind/api-change

20
pkg/config/doc.go Normal file
View File

@ -0,0 +1,20 @@
/*
Copyright 2024 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.
*/
// +k8s:deepcopy-gen=package
// +groupName=kubectl.config.k8s.io
package config // Package config import "k8s.io/kubectl/pkg/config"

View File

@ -0,0 +1,32 @@
/*
Copyright 2024 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 install installs the experimental API group, making it available as
// an option to all of the API encoding/decoding machinery.
package install
import (
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/kubectl/pkg/config"
"k8s.io/kubectl/pkg/config/v1alpha1"
)
// Install registers the API group and adds types to a scheme
func Install(scheme *runtime.Scheme) {
utilruntime.Must(config.AddToScheme(scheme))
utilruntime.Must(v1alpha1.AddToScheme(scheme))
}

44
pkg/config/register.go Normal file
View File

@ -0,0 +1,44 @@
/*
Copyright 2024 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 config
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GroupName is the group name used in this package
const GroupName = "kubectl.config.k8s.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
var (
// SchemeBuilder is the scheme builder with scheme init functions to run for this API package
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
// AddToScheme is a global function that registers this API group & version to a scheme
AddToScheme = SchemeBuilder.AddToScheme
)
// addKnownTypes registers known types to the given scheme
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Preference{},
)
return nil
}

103
pkg/config/types.go Normal file
View File

@ -0,0 +1,103 @@
/*
Copyright 2024 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 config
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Preference stores elements of KubeRC configuration file
type Preference struct {
metav1.TypeMeta
// overrides allows changing default flag values of commands.
// This is especially useful, when user doesn't want to explicitly
// set flags each time.
// +optional
Overrides []CommandOverride
// aliases allows defining command aliases for existing kubectl commands, with optional default flag values.
// If the alias name collides with a built-in command, built-in command always takes precedence.
// Flag overrides defined in the overrides section do NOT apply to aliases for the same command.
// kubectl [ALIAS NAME] [USER_FLAGS] [USER_EXPLICIT_ARGS] expands to
// kubectl [COMMAND] # built-in command alias points to
// [KUBERC_PREPEND_ARGS]
// [USER_FLAGS]
// [KUBERC_FLAGS] # rest of the flags that are not passed by user in [USER_FLAGS]
// [USER_EXPLICIT_ARGS]
// [KUBERC_APPEND_ARGS]
// e.g.
// - name: runx
// command: run
// flags:
// - name: image
// default: nginx
// appendArgs:
// - --
// - custom-arg1
// For example, if user invokes "kubectl runx test-pod" command,
// this will be expanded to "kubectl run --image=nginx test-pod -- custom-arg1"
// - name: getn
// command: get
// flags:
// - name: output
// default: wide
// prependArgs:
// - node
// "kubectl getn control-plane-1" expands to "kubectl get node control-plane-1 --output=wide"
// "kubectl getn control-plane-1 --output=json" expands to "kubectl get node --output=json control-plane-1"
// +optional
Aliases []AliasOverride
}
// AliasOverride stores the alias definitions.
type AliasOverride struct {
// Name is the name of alias that can only include alphabetical characters
// If the alias name conflicts with the built-in command,
// built-in command will be used.
Name string
// Command is the single or set of commands to execute, such as "set env" or "create"
Command string
// PrependArgs stores the arguments such as resource names, etc.
// These arguments are inserted after the alias name.
PrependArgs []string
// AppendArgs stores the arguments such as resource names, etc.
// These arguments are appended to the USER_ARGS.
AppendArgs []string
// Flag is allocated to store the flag definitions of alias
Flags []CommandOverrideFlag
}
// CommandOverride stores the commands and their associated flag's
// default values.
type CommandOverride struct {
// Command refers to a command whose flag's default value is changed.
Command string
// Flags is a list of flags storing different default values.
Flags []CommandOverrideFlag
}
// CommandOverrideFlag stores the name and the specified default
// value of the flag.
type CommandOverrideFlag struct {
// Flag name (long form, without dashes).
Name string `json:"name"`
// In a string format of a default value. It will be parsed
// by kubectl to the compatible value of the flag.
Default string `json:"default"`
}

View File

@ -0,0 +1,23 @@
/*
Copyright 2024 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.
*/
// +k8s:deepcopy-gen=package
// +k8s:openapi-gen=true
// +groupName=kubectl.config.k8s.io
// +k8s:conversion-gen=k8s.io/kubectl/pkg/config
// +k8s:defaulter-gen=TypeMeta
package v1alpha1 // Package v1alpha1 import "k8s.io/kubectl/pkg/config/v1alpha1"

View File

@ -0,0 +1,50 @@
/*
Copyright 2024 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 v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GroupName is the group name used in this package
const GroupName = "kubectl.config.k8s.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
var (
SchemeBuilder runtime.SchemeBuilder
localSchemeBuilder = &SchemeBuilder
AddToScheme = localSchemeBuilder.AddToScheme
)
func init() {
// We only register manually written functions here. The registration of the
// generated functions takes place in the generated files. The separation
// makes the code compile even when the generated files are missing.
localSchemeBuilder.Register(addKnownTypes)
}
// addKnownTypes registers known types to the given scheme
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Preference{},
)
return nil
}

View File

@ -0,0 +1,109 @@
/*
Copyright 2024 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 v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Preference stores elements of KubeRC configuration file
type Preference struct {
metav1.TypeMeta `json:",inline"`
// overrides allows changing default flag values of commands.
// This is especially useful, when user doesn't want to explicitly
// set flags each time.
// +listType=atomic
Overrides []CommandOverride `json:"overrides"`
// aliases allows defining command aliases for existing kubectl commands, with optional default flag values.
// If the alias name collides with a built-in command, built-in command always takes precedence.
// Flag overrides defined in the overrides section do NOT apply to aliases for the same command.
// kubectl [ALIAS NAME] [USER_FLAGS] [USER_EXPLICIT_ARGS] expands to
// kubectl [COMMAND] # built-in command alias points to
// [KUBERC_PREPEND_ARGS]
// [USER_FLAGS]
// [KUBERC_FLAGS] # rest of the flags that are not passed by user in [USER_FLAGS]
// [USER_EXPLICIT_ARGS]
// [KUBERC_APPEND_ARGS]
// e.g.
// - name: runx
// command: run
// flags:
// - name: image
// default: nginx
// appendArgs:
// - --
// - custom-arg1
// For example, if user invokes "kubectl runx test-pod" command,
// this will be expanded to "kubectl run --image=nginx test-pod -- custom-arg1"
// - name: getn
// command: get
// flags:
// - name: output
// default: wide
// prependArgs:
// - node
// "kubectl getn control-plane-1" expands to "kubectl get node control-plane-1 --output=wide"
// "kubectl getn control-plane-1 --output=json" expands to "kubectl get node --output=json control-plane-1"
// +listType=atomic
Aliases []AliasOverride `json:"aliases"`
}
// AliasOverride stores the alias definitions.
type AliasOverride struct {
// Name is the name of alias that can only include alphabetical characters
// If the alias name conflicts with the built-in command,
// built-in command will be used.
Name string `json:"name"`
// Command is the single or set of commands to execute, such as "set env" or "create"
Command string `json:"command"`
// PrependArgs stores the arguments such as resource names, etc.
// These arguments are inserted after the alias name.
// +listType=atomic
PrependArgs []string `json:"prependArgs,omitempty"`
// AppendArgs stores the arguments such as resource names, etc.
// These arguments are appended to the USER_ARGS.
// +listType=atomic
AppendArgs []string `json:"appendArgs,omitempty"`
// Flag is allocated to store the flag definitions of alias.
// Flag only modifies the default value of the flag and if
// user explicitly passes a value, explicit one is used.
// +listType=atomic
Flags []CommandOverrideFlag `json:"flags,omitempty"`
}
// CommandOverride stores the commands and their associated flag's
// default values.
type CommandOverride struct {
// Command refers to a command whose flag's default value is changed.
Command string `json:"command"`
// Flags is a list of flags storing different default values.
// +listType=atomic
Flags []CommandOverrideFlag `json:"flags"`
}
// CommandOverrideFlag stores the name and the specified default
// value of the flag.
type CommandOverrideFlag struct {
// Flag name (long form, without dashes).
Name string `json:"name"`
// In a string format of a default value. It will be parsed
// by kubectl to the compatible value of the flag.
Default string `json:"default"`
}

View File

@ -0,0 +1,174 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 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.
*/
// Code generated by conversion-gen. DO NOT EDIT.
package v1alpha1
import (
unsafe "unsafe"
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
config "k8s.io/kubectl/pkg/config"
)
func init() {
localSchemeBuilder.Register(RegisterConversions)
}
// RegisterConversions adds conversion functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterConversions(s *runtime.Scheme) error {
if err := s.AddGeneratedConversionFunc((*AliasOverride)(nil), (*config.AliasOverride)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_AliasOverride_To_config_AliasOverride(a.(*AliasOverride), b.(*config.AliasOverride), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*config.AliasOverride)(nil), (*AliasOverride)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_config_AliasOverride_To_v1alpha1_AliasOverride(a.(*config.AliasOverride), b.(*AliasOverride), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*CommandOverride)(nil), (*config.CommandOverride)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_CommandOverride_To_config_CommandOverride(a.(*CommandOverride), b.(*config.CommandOverride), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*config.CommandOverride)(nil), (*CommandOverride)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_config_CommandOverride_To_v1alpha1_CommandOverride(a.(*config.CommandOverride), b.(*CommandOverride), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*CommandOverrideFlag)(nil), (*config.CommandOverrideFlag)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_CommandOverrideFlag_To_config_CommandOverrideFlag(a.(*CommandOverrideFlag), b.(*config.CommandOverrideFlag), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*config.CommandOverrideFlag)(nil), (*CommandOverrideFlag)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_config_CommandOverrideFlag_To_v1alpha1_CommandOverrideFlag(a.(*config.CommandOverrideFlag), b.(*CommandOverrideFlag), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*Preference)(nil), (*config.Preference)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_Preference_To_config_Preference(a.(*Preference), b.(*config.Preference), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*config.Preference)(nil), (*Preference)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_config_Preference_To_v1alpha1_Preference(a.(*config.Preference), b.(*Preference), scope)
}); err != nil {
return err
}
return nil
}
func autoConvert_v1alpha1_AliasOverride_To_config_AliasOverride(in *AliasOverride, out *config.AliasOverride, s conversion.Scope) error {
out.Name = in.Name
out.Command = in.Command
out.PrependArgs = *(*[]string)(unsafe.Pointer(&in.PrependArgs))
out.AppendArgs = *(*[]string)(unsafe.Pointer(&in.AppendArgs))
out.Flags = *(*[]config.CommandOverrideFlag)(unsafe.Pointer(&in.Flags))
return nil
}
// Convert_v1alpha1_AliasOverride_To_config_AliasOverride is an autogenerated conversion function.
func Convert_v1alpha1_AliasOverride_To_config_AliasOverride(in *AliasOverride, out *config.AliasOverride, s conversion.Scope) error {
return autoConvert_v1alpha1_AliasOverride_To_config_AliasOverride(in, out, s)
}
func autoConvert_config_AliasOverride_To_v1alpha1_AliasOverride(in *config.AliasOverride, out *AliasOverride, s conversion.Scope) error {
out.Name = in.Name
out.Command = in.Command
out.PrependArgs = *(*[]string)(unsafe.Pointer(&in.PrependArgs))
out.AppendArgs = *(*[]string)(unsafe.Pointer(&in.AppendArgs))
out.Flags = *(*[]CommandOverrideFlag)(unsafe.Pointer(&in.Flags))
return nil
}
// Convert_config_AliasOverride_To_v1alpha1_AliasOverride is an autogenerated conversion function.
func Convert_config_AliasOverride_To_v1alpha1_AliasOverride(in *config.AliasOverride, out *AliasOverride, s conversion.Scope) error {
return autoConvert_config_AliasOverride_To_v1alpha1_AliasOverride(in, out, s)
}
func autoConvert_v1alpha1_CommandOverride_To_config_CommandOverride(in *CommandOverride, out *config.CommandOverride, s conversion.Scope) error {
out.Command = in.Command
out.Flags = *(*[]config.CommandOverrideFlag)(unsafe.Pointer(&in.Flags))
return nil
}
// Convert_v1alpha1_CommandOverride_To_config_CommandOverride is an autogenerated conversion function.
func Convert_v1alpha1_CommandOverride_To_config_CommandOverride(in *CommandOverride, out *config.CommandOverride, s conversion.Scope) error {
return autoConvert_v1alpha1_CommandOverride_To_config_CommandOverride(in, out, s)
}
func autoConvert_config_CommandOverride_To_v1alpha1_CommandOverride(in *config.CommandOverride, out *CommandOverride, s conversion.Scope) error {
out.Command = in.Command
out.Flags = *(*[]CommandOverrideFlag)(unsafe.Pointer(&in.Flags))
return nil
}
// Convert_config_CommandOverride_To_v1alpha1_CommandOverride is an autogenerated conversion function.
func Convert_config_CommandOverride_To_v1alpha1_CommandOverride(in *config.CommandOverride, out *CommandOverride, s conversion.Scope) error {
return autoConvert_config_CommandOverride_To_v1alpha1_CommandOverride(in, out, s)
}
func autoConvert_v1alpha1_CommandOverrideFlag_To_config_CommandOverrideFlag(in *CommandOverrideFlag, out *config.CommandOverrideFlag, s conversion.Scope) error {
out.Name = in.Name
out.Default = in.Default
return nil
}
// Convert_v1alpha1_CommandOverrideFlag_To_config_CommandOverrideFlag is an autogenerated conversion function.
func Convert_v1alpha1_CommandOverrideFlag_To_config_CommandOverrideFlag(in *CommandOverrideFlag, out *config.CommandOverrideFlag, s conversion.Scope) error {
return autoConvert_v1alpha1_CommandOverrideFlag_To_config_CommandOverrideFlag(in, out, s)
}
func autoConvert_config_CommandOverrideFlag_To_v1alpha1_CommandOverrideFlag(in *config.CommandOverrideFlag, out *CommandOverrideFlag, s conversion.Scope) error {
out.Name = in.Name
out.Default = in.Default
return nil
}
// Convert_config_CommandOverrideFlag_To_v1alpha1_CommandOverrideFlag is an autogenerated conversion function.
func Convert_config_CommandOverrideFlag_To_v1alpha1_CommandOverrideFlag(in *config.CommandOverrideFlag, out *CommandOverrideFlag, s conversion.Scope) error {
return autoConvert_config_CommandOverrideFlag_To_v1alpha1_CommandOverrideFlag(in, out, s)
}
func autoConvert_v1alpha1_Preference_To_config_Preference(in *Preference, out *config.Preference, s conversion.Scope) error {
out.Overrides = *(*[]config.CommandOverride)(unsafe.Pointer(&in.Overrides))
out.Aliases = *(*[]config.AliasOverride)(unsafe.Pointer(&in.Aliases))
return nil
}
// Convert_v1alpha1_Preference_To_config_Preference is an autogenerated conversion function.
func Convert_v1alpha1_Preference_To_config_Preference(in *Preference, out *config.Preference, s conversion.Scope) error {
return autoConvert_v1alpha1_Preference_To_config_Preference(in, out, s)
}
func autoConvert_config_Preference_To_v1alpha1_Preference(in *config.Preference, out *Preference, s conversion.Scope) error {
out.Overrides = *(*[]CommandOverride)(unsafe.Pointer(&in.Overrides))
out.Aliases = *(*[]AliasOverride)(unsafe.Pointer(&in.Aliases))
return nil
}
// Convert_config_Preference_To_v1alpha1_Preference is an autogenerated conversion function.
func Convert_config_Preference_To_v1alpha1_Preference(in *config.Preference, out *Preference, s conversion.Scope) error {
return autoConvert_config_Preference_To_v1alpha1_Preference(in, out, s)
}

View File

@ -0,0 +1,133 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 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.
*/
// Code generated by deepcopy-gen. DO NOT EDIT.
package v1alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AliasOverride) DeepCopyInto(out *AliasOverride) {
*out = *in
if in.PrependArgs != nil {
in, out := &in.PrependArgs, &out.PrependArgs
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.AppendArgs != nil {
in, out := &in.AppendArgs, &out.AppendArgs
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Flags != nil {
in, out := &in.Flags, &out.Flags
*out = make([]CommandOverrideFlag, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AliasOverride.
func (in *AliasOverride) DeepCopy() *AliasOverride {
if in == nil {
return nil
}
out := new(AliasOverride)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CommandOverride) DeepCopyInto(out *CommandOverride) {
*out = *in
if in.Flags != nil {
in, out := &in.Flags, &out.Flags
*out = make([]CommandOverrideFlag, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommandOverride.
func (in *CommandOverride) DeepCopy() *CommandOverride {
if in == nil {
return nil
}
out := new(CommandOverride)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CommandOverrideFlag) DeepCopyInto(out *CommandOverrideFlag) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommandOverrideFlag.
func (in *CommandOverrideFlag) DeepCopy() *CommandOverrideFlag {
if in == nil {
return nil
}
out := new(CommandOverrideFlag)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Preference) DeepCopyInto(out *Preference) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.Overrides != nil {
in, out := &in.Overrides, &out.Overrides
*out = make([]CommandOverride, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Aliases != nil {
in, out := &in.Aliases, &out.Aliases
*out = make([]AliasOverride, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Preference.
func (in *Preference) DeepCopy() *Preference {
if in == nil {
return nil
}
out := new(Preference)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Preference) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}

View File

@ -0,0 +1,33 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 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.
*/
// Code generated by defaulter-gen. DO NOT EDIT.
package v1alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// RegisterDefaults adds defaulters functions to the given scheme.
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
return nil
}

View File

@ -0,0 +1,133 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 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.
*/
// Code generated by deepcopy-gen. DO NOT EDIT.
package config
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AliasOverride) DeepCopyInto(out *AliasOverride) {
*out = *in
if in.PrependArgs != nil {
in, out := &in.PrependArgs, &out.PrependArgs
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.AppendArgs != nil {
in, out := &in.AppendArgs, &out.AppendArgs
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Flags != nil {
in, out := &in.Flags, &out.Flags
*out = make([]CommandOverrideFlag, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AliasOverride.
func (in *AliasOverride) DeepCopy() *AliasOverride {
if in == nil {
return nil
}
out := new(AliasOverride)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CommandOverride) DeepCopyInto(out *CommandOverride) {
*out = *in
if in.Flags != nil {
in, out := &in.Flags, &out.Flags
*out = make([]CommandOverrideFlag, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommandOverride.
func (in *CommandOverride) DeepCopy() *CommandOverride {
if in == nil {
return nil
}
out := new(CommandOverride)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CommandOverrideFlag) DeepCopyInto(out *CommandOverrideFlag) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommandOverrideFlag.
func (in *CommandOverrideFlag) DeepCopy() *CommandOverrideFlag {
if in == nil {
return nil
}
out := new(CommandOverrideFlag)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Preference) DeepCopyInto(out *Preference) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.Overrides != nil {
in, out := &in.Overrides, &out.Overrides
*out = make([]CommandOverride, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Aliases != nil {
in, out := &in.Aliases, &out.Aliases
*out = make([]AliasOverride, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Preference.
func (in *Preference) DeepCopy() *Preference {
if in == nil {
return nil
}
out := new(Preference)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Preference) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}

458
pkg/kuberc/kuberc.go Normal file
View File

@ -0,0 +1,458 @@
/*
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, "-") {
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
}

2723
pkg/kuberc/kuberc_test.go Normal file

File diff suppressed because it is too large Load Diff

101
pkg/kuberc/marshal.go Normal file
View File

@ -0,0 +1,101 @@
/*
Copyright 2024 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 (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"k8s.io/klog/v2"
"k8s.io/apimachinery/pkg/runtime/schema"
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/kubectl/pkg/config"
)
// decodePreference iterates over the yamls in kuberc file to find the supported kuberc version.
// Once it finds, it returns the compatible kuberc object as well as accumulated errors during the iteration.
func decodePreference(kubercFile string) (*config.Preference, error) {
kubercBytes, err := os.ReadFile(kubercFile)
if err != nil {
return nil, err
}
attemptedItems := 0
reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewBuffer(kubercBytes)))
for {
doc, readErr := reader.Read()
if errors.Is(readErr, io.EOF) {
// no more entries, expected when we reach the end of the file
break
}
if readErr != nil {
// other errors are fatal
return nil, readErr
}
if len(bytes.TrimSpace(doc)) == 0 {
// empty item, ignore
continue
}
// remember we attempted
attemptedItems++
pref, gvk, strictDecodeErr := strictCodecs.UniversalDecoder().Decode(doc, nil, nil)
if strictDecodeErr != nil {
var lenientDecodeErr error
pref, gvk, lenientDecodeErr = lenientCodecs.UniversalDecoder().Decode(doc, nil, nil)
if lenientDecodeErr != nil {
// both strict and lenient failed
// verbose log the error with the most information about this item and continue
klog.V(5).Infof("kuberc: strict decoding error for entry %d in %s: %v", attemptedItems, kubercFile, strictDecodeErr)
continue
}
}
// check expected GVK, if bad, verbose log and continue
expectedGK := schema.GroupKind{
Group: config.SchemeGroupVersion.Group,
Kind: "Preference",
}
if gvk.GroupKind() != expectedGK {
klog.V(5).Infof("kuberc: unexpected GroupVersionKind for entry %d in %s: %v", attemptedItems, kubercFile, gvk)
continue
}
// check expected go type, if bad, verbose log and continue
preferences, ok := pref.(*config.Preference)
if !ok {
klog.V(5).Infof("kuberc: unexpected object type %T for entry %d in %s", pref, attemptedItems, kubercFile)
continue
}
// we have a usable preferences to return
klog.V(5).Infof("kuberc: successfully decoded entry %d in %s", attemptedItems, kubercFile)
return preferences, strictDecodeErr
}
if attemptedItems > 0 {
return nil, fmt.Errorf("no valid preferences found in %s, use --v=5 to see details", kubercFile)
}
// empty doc
klog.V(5).Infof("kuberc: no preferences found in %s", kubercFile)
return nil, nil
}