diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index b1729f2f19c..9227d8a9eaf 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -1241,6 +1241,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "k8s.io/kube-scheduler/config/v1.ScoringStrategy": schema_k8sio_kube_scheduler_config_v1_ScoringStrategy(ref), "k8s.io/kube-scheduler/config/v1.UtilizationShapePoint": schema_k8sio_kube_scheduler_config_v1_UtilizationShapePoint(ref), "k8s.io/kube-scheduler/config/v1.VolumeBindingArgs": schema_k8sio_kube_scheduler_config_v1_VolumeBindingArgs(ref), + "k8s.io/kubectl/pkg/config/v1alpha1.AliasOverride": schema_kubectl_pkg_config_v1alpha1_AliasOverride(ref), + "k8s.io/kubectl/pkg/config/v1alpha1.CommandOverride": schema_kubectl_pkg_config_v1alpha1_CommandOverride(ref), + "k8s.io/kubectl/pkg/config/v1alpha1.CommandOverrideFlag": schema_kubectl_pkg_config_v1alpha1_CommandOverrideFlag(ref), + "k8s.io/kubectl/pkg/config/v1alpha1.Preference": schema_kubectl_pkg_config_v1alpha1_Preference(ref), "k8s.io/kubelet/config/v1.CredentialProvider": schema_k8sio_kubelet_config_v1_CredentialProvider(ref), "k8s.io/kubelet/config/v1.CredentialProviderConfig": schema_k8sio_kubelet_config_v1_CredentialProviderConfig(ref), "k8s.io/kubelet/config/v1.ExecEnvVar": schema_k8sio_kubelet_config_v1_ExecEnvVar(ref), @@ -63549,6 +63553,238 @@ func schema_k8sio_kube_scheduler_config_v1_VolumeBindingArgs(ref common.Referenc } } +func schema_kubectl_pkg_config_v1alpha1_AliasOverride(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "AliasOverride stores the alias definitions.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "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.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "command": { + SchemaProps: spec.SchemaProps{ + Description: "Command is the single or set of commands to execute, such as \"set env\" or \"create\"", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "prependArgs": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "PrependArgs stores the arguments such as resource names, etc. These arguments are inserted after the alias name.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "appendArgs": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "AppendArgs stores the arguments such as resource names, etc. These arguments are appended to the USER_ARGS.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "flags": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "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.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/kubectl/pkg/config/v1alpha1.CommandOverrideFlag"), + }, + }, + }, + }, + }, + }, + Required: []string{"name", "command"}, + }, + }, + Dependencies: []string{ + "k8s.io/kubectl/pkg/config/v1alpha1.CommandOverrideFlag"}, + } +} + +func schema_kubectl_pkg_config_v1alpha1_CommandOverride(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "CommandOverride stores the commands and their associated flag's default values.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "command": { + SchemaProps: spec.SchemaProps{ + Description: "Command refers to a command whose flag's default value is changed.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "flags": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Flags is a list of flags storing different default values.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/kubectl/pkg/config/v1alpha1.CommandOverrideFlag"), + }, + }, + }, + }, + }, + }, + Required: []string{"command", "flags"}, + }, + }, + Dependencies: []string{ + "k8s.io/kubectl/pkg/config/v1alpha1.CommandOverrideFlag"}, + } +} + +func schema_kubectl_pkg_config_v1alpha1_CommandOverrideFlag(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "CommandOverrideFlag stores the name and the specified default value of the flag.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Flag name (long form, without dashes).", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "default": { + SchemaProps: spec.SchemaProps{ + Description: "In a string format of a default value. It will be parsed by kubectl to the compatible value of the flag.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "default"}, + }, + }, + } +} + +func schema_kubectl_pkg_config_v1alpha1_Preference(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Preference stores elements of KubeRC configuration file", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "overrides": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "overrides allows changing default flag values of commands. This is especially useful, when user doesn't want to explicitly set flags each time.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/kubectl/pkg/config/v1alpha1.CommandOverride"), + }, + }, + }, + }, + }, + "aliases": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "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\n [KUBERC_PREPEND_ARGS]\n [USER_FLAGS]\n [KUBERC_FLAGS] # rest of the flags that are not passed by user in [USER_FLAGS]\n [USER_EXPLICIT_ARGS]\n [KUBERC_APPEND_ARGS]\ne.g. - name: runx\n command: run\n flags:\n - name: image\n default: nginx\n appendArgs:\n - --\n - custom-arg1\nFor example, if user invokes \"kubectl runx test-pod\" command, this will be expanded to \"kubectl run --image=nginx test-pod -- custom-arg1\" - name: getn\n command: get\n flags:\n - name: output\n default: wide\n prependArgs:\n - node\n\"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\"", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/kubectl/pkg/config/v1alpha1.AliasOverride"), + }, + }, + }, + }, + }, + }, + Required: []string{"overrides", "aliases"}, + }, + }, + Dependencies: []string{ + "k8s.io/kubectl/pkg/config/v1alpha1.AliasOverride", "k8s.io/kubectl/pkg/config/v1alpha1.CommandOverride"}, + } +} + func schema_k8sio_kubelet_config_v1_CredentialProvider(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go b/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go index a3bac47fee0..6e11c843094 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go @@ -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 } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go index 5ad4269e9dd..50eb2a636e7 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go @@ -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. diff --git a/staging/src/k8s.io/kubectl/pkg/config/OWNERS b/staging/src/k8s.io/kubectl/pkg/config/OWNERS new file mode 100644 index 00000000000..1ca7fae4d05 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/config/OWNERS @@ -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 diff --git a/staging/src/k8s.io/kubectl/pkg/config/doc.go b/staging/src/k8s.io/kubectl/pkg/config/doc.go new file mode 100644 index 00000000000..010e2e5b658 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/config/doc.go @@ -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" diff --git a/staging/src/k8s.io/kubectl/pkg/config/install/install.go b/staging/src/k8s.io/kubectl/pkg/config/install/install.go new file mode 100644 index 00000000000..49dd50f6049 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/config/install/install.go @@ -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)) +} diff --git a/staging/src/k8s.io/kubectl/pkg/config/register.go b/staging/src/k8s.io/kubectl/pkg/config/register.go new file mode 100644 index 00000000000..a7b13ebd094 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/config/register.go @@ -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 +} diff --git a/staging/src/k8s.io/kubectl/pkg/config/types.go b/staging/src/k8s.io/kubectl/pkg/config/types.go new file mode 100644 index 00000000000..1f1423a39fe --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/config/types.go @@ -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"` +} diff --git a/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/doc.go b/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/doc.go new file mode 100644 index 00000000000..d53157c3152 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/doc.go @@ -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" diff --git a/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/register.go b/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/register.go new file mode 100644 index 00000000000..38461a8e954 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/register.go @@ -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 +} diff --git a/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/types.go b/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/types.go new file mode 100644 index 00000000000..81bf55efb68 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/types.go @@ -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"` +} diff --git a/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/zz_generated.conversion.go b/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/zz_generated.conversion.go new file mode 100644 index 00000000000..92b588ae950 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/zz_generated.conversion.go @@ -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) +} diff --git a/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/zz_generated.deepcopy.go b/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..e799c7c968b --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/zz_generated.deepcopy.go @@ -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 +} diff --git a/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/zz_generated.defaults.go b/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/zz_generated.defaults.go new file mode 100644 index 00000000000..5070cb91b90 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/zz_generated.defaults.go @@ -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 +} diff --git a/staging/src/k8s.io/kubectl/pkg/config/zz_generated.deepcopy.go b/staging/src/k8s.io/kubectl/pkg/config/zz_generated.deepcopy.go new file mode 100644 index 00000000000..7954bebb695 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/config/zz_generated.deepcopy.go @@ -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 +} diff --git a/staging/src/k8s.io/kubectl/pkg/kuberc/kuberc.go b/staging/src/k8s.io/kubectl/pkg/kuberc/kuberc.go new file mode 100644 index 00000000000..03c6a2ef9b9 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/kuberc/kuberc.go @@ -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 +} diff --git a/staging/src/k8s.io/kubectl/pkg/kuberc/kuberc_test.go b/staging/src/k8s.io/kubectl/pkg/kuberc/kuberc_test.go new file mode 100644 index 00000000000..90a2805e481 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/kuberc/kuberc_test.go @@ -0,0 +1,2723 @@ +/* +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 ( + "bytes" + "fmt" + "io" + "strconv" + "testing" + + "github.com/spf13/cobra" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/config" +) + +type fakeCmds[T supportedTypes] struct { + name string + flags []fakeFlag[T] +} + +type supportedTypes interface { + string | bool +} + +type fakeFlag[T supportedTypes] struct { + name string + value T + shorthand string +} + +type testApplyOverride[T supportedTypes] struct { + name string + nestedCmds []fakeCmds[T] + args []string + getPreferencesFunc func(kuberc string, errOut io.Writer) (*config.Preference, error) + expectedFLags []fakeFlag[T] + expectedErr error +} + +type testApplyAlias[T supportedTypes] struct { + name string + nestedCmds []fakeCmds[T] + args []string + getPreferencesFunc func(kuberc string, errOut io.Writer) (*config.Preference, error) + expectedFLags []fakeFlag[T] + expectedCmd string + expectedArgs []string + expectedErr error +} + +func TestApplyOverride(t *testing.T) { + tests := []testApplyOverride[string]{ + { + name: "command override", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "changed", + }, + }, + }, + { + name: "subcommand override", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: nil, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "command2", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1 command2", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "changed", + }, + }, + }, + { + name: "subcommand override with prefix incorrectly matches", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: nil, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "first", + value: "test", + }, + { + name: "firstflag", + value: "test2", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "command2", + "--firstflag", + "explicit", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1 command2", + Flags: []config.CommandOverrideFlag{ + { + Name: "first", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "first", + value: "changed", + }, + { + name: "firstflag", + value: "explicit", + }, + }, + }, + { + name: "use explicit kuberc, subcommand explicit takes precedence", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: nil, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "command2", + "--kuberc", + "test-custom-kuberc-path", + "--firstflag=explicit", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + if kuberc != "test-custom-kuberc-path" { + return nil, fmt.Errorf("unexpected kuberc: %s", kuberc) + } + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1 command2", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + }, + }, + { + name: "use explicit kuberc, subcommand explicit takes precedence kuberc flag first", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: nil, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "--kuberc=test-custom-kuberc-path", + "command1", + "command2", + "--firstflag=explicit", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + if kuberc != "test-custom-kuberc-path" { + return nil, fmt.Errorf("unexpected kuberc: %s", kuberc) + } + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1 command2", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + }, + }, + { + name: "use explicit kuberc equal, subcommand explicit takes precedence", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: nil, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "command2", + "--kuberc=test-custom-kuberc-path", + "--firstflag=explicit", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + if kuberc != "test-custom-kuberc-path" { + return nil, fmt.Errorf("unexpected kuberc: %s", kuberc) + } + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1 command2", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + }, + }, + { + name: "use explicit kuberc equal, subcommand explicit takes precedence multi spaces", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: nil, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "command2", + "--kuberc=test-custom-kuberc-path", + "--firstflag=explicit", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + if kuberc != "test-custom-kuberc-path" { + return nil, fmt.Errorf("unexpected kuberc: %s", kuberc) + } + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: " command1 command2 ", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + }, + }, + { + name: "use explicit kuberc equal at the end, subcommand explicit takes precedence", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: nil, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "command2", + "--firstflag=explicit", + "--kuberc=test-custom-kuberc-path", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + if kuberc != "test-custom-kuberc-path" { + return nil, fmt.Errorf("unexpected kuberc: %s", kuberc) + } + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1 command2", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + }, + }, + { + name: "subcommand explicit takes precedence", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: nil, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "command2", + "--firstflag=explicit", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1 command2", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + }, + }, + { + name: "subcommand explicit takes precedence with space", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: nil, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "command2", + "--firstflag", + "explicit", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1 command2", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + }, + }, + { + name: "subcommand explicit takes precedence with space and with shorthand", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: nil, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + shorthand: "r", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "command2", + "-r", + "explicit", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1 command2", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + }, + }, + { + name: "subcommand explicit takes precedence with space and with shorthand and equal sign", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: nil, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + shorthand: "r", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "command2", + "-r=explicit", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1 command2", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + }, + }, + { + name: "subcommand check the not overridden flag", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: nil, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + { + name: "secondflag", + value: "secondflagvalue", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "command2", + "--firstflag", + "explicit", + "--secondflag=changed", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1 command2", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + { + name: "secondflag", + value: "changed", + }, + }, + }, + { + name: "command1 also has same flag", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "shouldnuse", + }, + { + name: "secondflag", + value: "shouldnuse", + }, + }, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "command2", + "--firstflag", + "explicit", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1 command2", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + }, + }, + { + name: "alias ignores command override", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "alias", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + Aliases: []config.AliasOverride{ + { + Name: "alias", + Command: "command1", + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + { + name: "alias command override", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "testalias", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "testalias", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + Aliases: []config.AliasOverride{ + { + Name: "testalias", + Command: "command1", + }, + }, + }, nil + }, + expectedErr: fmt.Errorf("alias testalias can not be overridden"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmdtesting.WithAlphaEnvs([]util.FeatureGate{util.KubeRC}, t, func(t *testing.T) { + rootCmd := &cobra.Command{ + Use: "root", + } + prefHandler := NewPreferences() + prefHandler.AddFlags(rootCmd.PersistentFlags()) + pref, ok := prefHandler.(*Preferences) + if !ok { + t.Fatal("unexpected type. Expected *Preferences") + } + addCommands(rootCmd, test.nestedCmds) + pref.getPreferencesFunc = test.getPreferencesFunc + errWriter := &bytes.Buffer{} + _, err := pref.Apply(rootCmd, test.args, errWriter) + if test.expectedErr == nil && err != nil { + t.Fatalf("unexpected error %v\n", err) + } + if test.expectedErr != nil { + if test.expectedErr.Error() != err.Error() { + t.Fatalf("error %s expected but actual is %s", test.expectedErr, err) + } + return + } + + actualCmd, _, err := rootCmd.Find(test.args[1:]) + if err != nil { + t.Fatalf("unable to find the command %v\n", err) + } + + err = actualCmd.ParseFlags(test.args[1:]) + if err != nil { + t.Fatalf("unexpected error %v\n", err) + } + + if errWriter.String() != "" { + t.Fatalf("unexpected error message %s\n", errWriter.String()) + } + + for _, expectedFlag := range test.expectedFLags { + actualFlag := actualCmd.Flag(expectedFlag.name) + if actualFlag.Value.String() != expectedFlag.value { + t.Fatalf("unexpected flag value expected %s actual %s", expectedFlag.value, actualFlag.Value.String()) + } + } + }) + }) + } +} + +func TestApplOverrideBool(t *testing.T) { + tests := []testApplyOverride[bool]{ + { + name: "command override", + nestedCmds: []fakeCmds[bool]{ + { + name: "command1", + flags: []fakeFlag[bool]{ + { + name: "firstflag", + value: true, + }, + }, + }, + }, + args: []string{ + "root", + "command1", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "false", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[bool]{ + { + name: "firstflag", + value: false, + }, + }, + }, + { + name: "command override explicit pass", + nestedCmds: []fakeCmds[bool]{ + { + name: "command1", + flags: []fakeFlag[bool]{ + { + name: "firstflag", + value: true, + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "--firstflag", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "false", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[bool]{ + { + name: "firstflag", + value: true, + }, + }, + }, + { + name: "command override explicit pass with shorthand", + nestedCmds: []fakeCmds[bool]{ + { + name: "command1", + flags: []fakeFlag[bool]{ + { + name: "firstflag", + value: true, + shorthand: "f", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "-f", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "false", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[bool]{ + { + name: "firstflag", + value: true, + }, + }, + }, + { + name: "command override explicit pass with combined multiple shorthand", + nestedCmds: []fakeCmds[bool]{ + { + name: "command1", + flags: []fakeFlag[bool]{ + { + name: "firstflag", + value: false, + shorthand: "f", + }, + { + name: "secondflag", + value: false, + shorthand: "v", + }, + { + name: "thirdflag", + value: true, + shorthand: "d", + }, + }, + }, + }, + args: []string{ + "root", + "command1", + "-dfv", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Overrides: []config.CommandOverride{ + { + Command: "command1", + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "false", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[bool]{ + { + name: "firstflag", + value: true, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmdtesting.WithAlphaEnvs([]util.FeatureGate{util.KubeRC}, t, func(t *testing.T) { + rootCmd := &cobra.Command{ + Use: "root", + } + prefHandler := NewPreferences() + prefHandler.AddFlags(rootCmd.PersistentFlags()) + pref, ok := prefHandler.(*Preferences) + if !ok { + t.Fatal("unexpected type. Expected *Preferences") + } + addCommands(rootCmd, test.nestedCmds) + pref.getPreferencesFunc = test.getPreferencesFunc + errWriter := &bytes.Buffer{} + _, err := pref.Apply(rootCmd, test.args, errWriter) + if err != nil { + t.Fatalf("unexpected error %v\n", err) + } + actualCmd, _, err := rootCmd.Find(test.args[1:]) + if err != nil { + t.Fatalf("unable to find the command %v\n", err) + } + + err = actualCmd.ParseFlags(test.args[1:]) + if err != nil { + t.Fatalf("unexpected error %v\n", err) + } + + if errWriter.String() != "" { + t.Fatalf("unexpected error message %s\n", errWriter.String()) + } + + for _, expectedFlag := range test.expectedFLags { + actualFlag := actualCmd.Flag(expectedFlag.name) + actualValue, err := strconv.ParseBool(actualFlag.Value.String()) + if err != nil { + t.Fatalf("unexpected error %v\n", err) + } + if actualValue != expectedFlag.value { + t.Fatalf("unexpected flag value expected %t actual %s", expectedFlag.value, actualFlag.Value.String()) + } + } + }) + }) + } +} + +func TestApplyAliasBool(t *testing.T) { + tests := []testApplyAlias[bool]{ + { + name: "command override", + nestedCmds: []fakeCmds[bool]{ + { + name: "command1", + flags: []fakeFlag[bool]{ + { + name: "firstflag", + value: false, + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "true", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[bool]{ + { + name: "firstflag", + value: true, + }, + }, + expectedCmd: "getcmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + { + name: "command override explicit pass", + nestedCmds: []fakeCmds[bool]{ + { + name: "command1", + flags: []fakeFlag[bool]{ + { + name: "firstflag", + value: false, + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + "--firstflag", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "false", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[bool]{ + { + name: "firstflag", + value: true, + }, + }, + expectedCmd: "getcmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + { + name: "command override explicit pass with shorthand", + nestedCmds: []fakeCmds[bool]{ + { + name: "command1", + flags: []fakeFlag[bool]{ + { + name: "firstflag", + value: false, + shorthand: "f", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + "-f", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "false", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[bool]{ + { + name: "firstflag", + value: true, + }, + }, + expectedCmd: "getcmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + { + name: "command override explicit pass with combination of multiple shorthand", + nestedCmds: []fakeCmds[bool]{ + { + name: "command1", + flags: []fakeFlag[bool]{ + { + name: "firstflag", + value: false, + shorthand: "f", + }, + { + name: "secondflag", + value: true, + shorthand: "v", + }, + { + name: "thirdflag", + value: false, + shorthand: "d", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + "-vfd", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "false", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[bool]{ + { + name: "firstflag", + value: true, + }, + { + name: "secondflag", + value: true, + }, + { + name: "thirdflag", + value: true, + }, + }, + expectedCmd: "getcmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmdtesting.WithAlphaEnvs([]util.FeatureGate{util.KubeRC}, t, func(t *testing.T) { + rootCmd := &cobra.Command{ + Use: "root", + } + prefHandler := NewPreferences() + prefHandler.AddFlags(rootCmd.PersistentFlags()) + pref, ok := prefHandler.(*Preferences) + if !ok { + t.Fatal("unexpected type. Expected *Preferences") + } + addCommands(rootCmd, test.nestedCmds) + pref.getPreferencesFunc = test.getPreferencesFunc + errWriter := &bytes.Buffer{} + lastArgs, err := pref.Apply(rootCmd, test.args, errWriter) + if test.expectedErr == nil && err != nil { + t.Fatalf("unexpected error %v\n", err) + } + if test.expectedErr != nil { + if test.expectedErr.Error() != err.Error() { + t.Fatalf("error %s expected but actual is %s", test.expectedErr, err) + } + return + } + + actualCmd, _, err := rootCmd.Find(lastArgs[1:]) + if err != nil { + t.Fatal(err) + } + + err = actualCmd.ParseFlags(lastArgs) + if err != nil { + t.Fatalf("unexpected error %v\n", err) + } + + if errWriter.String() != "" { + t.Fatalf("unexpected error message %s\n", errWriter.String()) + } + + if test.expectedCmd != actualCmd.Name() { + t.Fatalf("unexpected command expected %s actual %s", test.expectedCmd, actualCmd.Name()) + } + + for _, expectedFlag := range test.expectedFLags { + actualFlag := actualCmd.Flag(expectedFlag.name) + actualValue, err := strconv.ParseBool(actualFlag.Value.String()) + if err != nil { + t.Fatalf("unexpected error %v\n", err) + } + if actualValue != expectedFlag.value { + t.Fatalf("unexpected flag value expected %t actual %s", expectedFlag.value, actualFlag.Value.String()) + } + } + + for _, expectedArg := range test.expectedArgs { + found := false + for _, actualArg := range lastArgs { + if actualArg == expectedArg { + found = true + break + } + } + if !found { + t.Fatalf("expected arg %s can not be found", expectedArg) + } + } + }) + }) + } +} + +func TestApplyAlias(t *testing.T) { + tests := []testApplyAlias[string]{ + { + name: "command override", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "changed", + }, + }, + expectedCmd: "getcmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + { + name: "command override prependArgs", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + PrependArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "changed", + }, + }, + expectedCmd: "getcmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + { + name: "command override prependArgs with args", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + "arg1", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + PrependArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "changed", + }, + }, + expectedCmd: "getcmd", + expectedArgs: []string{ + "resources", + "nodes", + "arg1", + }, + }, + { + name: "command override prependArgs with appendArgs", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + PrependArgs: []string{ + "resources", + "nodes", + }, + AppendArgs: []string{ + "arg1", + "arg2", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "changed", + }, + }, + expectedCmd: "getcmd", + expectedArgs: []string{ + "resources", + "nodes", + "arg1", + "arg2", + }, + }, + { + name: "command override prependArgs with appendArgs with args", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + "arg1", + "arg2", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + PrependArgs: []string{ + "resources", + "nodes", + }, + AppendArgs: []string{ + "arg3", + "arg4", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "changed", + }, + }, + expectedCmd: "getcmd", + expectedArgs: []string{ + "resources", + "nodes", + "arg1", + "arg2", + "arg3", + "arg4", + }, + }, + { + name: "command override prependArgs with appendArgs with args with flagas", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + "arg1", + "--firstflag", + "explicit", + "arg2", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + PrependArgs: []string{ + "resources", + "nodes", + }, + AppendArgs: []string{ + "arg3", + "arg4", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + }, + expectedCmd: "getcmd", + expectedArgs: []string{ + "resources", + "nodes", + "arg1", + "arg2", + "arg3", + "arg4", + }, + }, + { + name: "invalid duplicate aliasname", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + { + Name: "getcmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedErr: fmt.Errorf("duplicate alias name getcmd"), + }, + { + name: "alias name with flags having dashes as prefix ", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "--firstflag", + Default: "changed", + }, + }, + }, + { + Name: "getcmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedErr: fmt.Errorf("flag name --firstflag should be in long form without dashes"), + }, + { + name: "invalid aliasname", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd!!", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedErr: fmt.Errorf("invalid alias name, can only include alphabetical characters"), + }, + { + name: "invalid aliasname with spaces", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd subalias", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedErr: fmt.Errorf("invalid alias name, can only include alphabetical characters"), + }, + { + name: "command override", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + "--firstflag=explicit", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + }, + expectedCmd: "getcmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + { + name: "command override with shorthand", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + shorthand: "r", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + "-r=explicit", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + }, + expectedCmd: "getcmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + { + name: "command override with shorthand and space", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + shorthand: "r", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + "-r", + "explicit", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + }, + expectedCmd: "getcmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + { + name: "command override", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + { + name: "secondflag", + value: "secondflagvalue", + }, + }, + }, + }, + args: []string{ + "root", + "getcmd", + "--firstflag=explicit", + "--secondflag=changed", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "getcmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "explicit", + }, + { + name: "secondflag", + value: "changed", + }, + }, + expectedCmd: "getcmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + { + name: "simple aliasing", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "aliascmd", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "aliascmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "changed", + }, + }, + expectedCmd: "aliascmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + { + name: "simple aliasing with kuberc flag first", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "--kuberc=kuberc", + "aliascmd", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "aliascmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "changed", + }, + }, + expectedCmd: "aliascmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + { + name: "simple aliasing with kuberc flag after", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "aliascmd", + "--kuberc=kuberc", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "aliascmd", + Command: "command1", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "changed", + }, + }, + expectedCmd: "aliascmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + { + name: "subcommand aliasing", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "shouldntuse", + }, + { + name: "secondflag", + value: "shouldntuse", + }, + }, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "aliascmd", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "aliascmd", + Command: "command1 command2", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed2", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "changed2", + }, + }, + expectedCmd: "aliascmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + { + name: "subcommand aliasing with spaces", + nestedCmds: []fakeCmds[string]{ + { + name: "command1", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "shouldntuse", + }, + { + name: "secondflag", + value: "shouldntuse", + }, + }, + }, + { + name: "command2", + flags: []fakeFlag[string]{ + { + name: "firstflag", + value: "test", + }, + }, + }, + }, + args: []string{ + "root", + "aliascmd", + }, + getPreferencesFunc: func(kuberc string, errOut io.Writer) (*config.Preference, error) { + return &config.Preference{ + TypeMeta: metav1.TypeMeta{ + Kind: "Preference", + APIVersion: "kubectl.config.k8s.io/v1alpha1", + }, + Aliases: []config.AliasOverride{ + { + Name: "aliascmd", + Command: " command1 command2 ", + AppendArgs: []string{ + "resources", + "nodes", + }, + Flags: []config.CommandOverrideFlag{ + { + Name: "firstflag", + Default: "changed2", + }, + }, + }, + }, + }, nil + }, + expectedFLags: []fakeFlag[string]{ + { + name: "firstflag", + value: "changed2", + }, + }, + expectedCmd: "aliascmd", + expectedArgs: []string{ + "resources", + "nodes", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmdtesting.WithAlphaEnvs([]util.FeatureGate{util.KubeRC}, t, func(t *testing.T) { + rootCmd := &cobra.Command{ + Use: "root", + } + prefHandler := NewPreferences() + prefHandler.AddFlags(rootCmd.PersistentFlags()) + pref, ok := prefHandler.(*Preferences) + if !ok { + t.Fatal("unexpected type. Expected *Preferences") + } + addCommands(rootCmd, test.nestedCmds) + pref.getPreferencesFunc = test.getPreferencesFunc + errWriter := &bytes.Buffer{} + lastArgs, err := pref.Apply(rootCmd, test.args, errWriter) + if test.expectedErr == nil && err != nil { + t.Fatalf("unexpected error %v\n", err) + } + if test.expectedErr != nil { + if test.expectedErr.Error() != err.Error() { + t.Fatalf("error %s expected but actual is %s", test.expectedErr, err) + } + return + } + + actualCmd, _, err := rootCmd.Find(lastArgs[1:]) + if err != nil { + t.Fatal(err) + } + + err = actualCmd.ParseFlags(lastArgs) + if err != nil { + t.Fatalf("unexpected error %v\n", err) + } + + if errWriter.String() != "" { + t.Fatalf("unexpected error message %s\n", errWriter.String()) + } + + if test.expectedCmd != actualCmd.Name() { + t.Fatalf("unexpected command expected %s actual %s", test.expectedCmd, actualCmd.Name()) + } + + for _, expectedFlag := range test.expectedFLags { + actualFlag := actualCmd.Flag(expectedFlag.name) + if actualFlag.Value.String() != expectedFlag.value { + t.Fatalf("unexpected flag value expected %s actual %s", expectedFlag.value, actualFlag.Value.String()) + } + } + + for _, expectedArg := range test.expectedArgs { + found := false + for _, actualArg := range lastArgs { + if actualArg == expectedArg { + found = true + break + } + } + if !found { + t.Fatalf("expected arg %s can not be found", expectedArg) + } + } + }) + }) + } +} + +func TestGetExplicitKuberc(t *testing.T) { + tests := []struct { + args []string + expected string + expectedErr error + }{ + { + args: []string{"kubectl", "get", "--kuberc", "/tmp/filepath"}, + expected: "/tmp/filepath", + }, + { + args: []string{"kubectl", "get", "--kuberc=/tmp/filepath"}, + expected: "/tmp/filepath", + }, + { + args: []string{"kubectl", "get", "--kuberc=/tmp/filepath", "--", "/bin/bash", "--kuberc", "anotherpath"}, + expected: "/tmp/filepath", + }, + { + args: []string{"kubectl", "get", "--kuberc", "/tmp/filepath", "--", "/bin/bash", "--kuberc", "anotherpath"}, + expected: "/tmp/filepath", + }, + { + args: []string{"kubectl", "get", "--kuberc="}, + expectedErr: fmt.Errorf("kuberc file is not found"), + }, + { + args: []string{"kubectl", "get", "--kuberc"}, + expectedErr: fmt.Errorf("kuberc file is not found"), + }, + { + args: []string{"kubectl", "get", "--", "/bin/bash", "--kuberc", "anotherpath"}, + expected: "", + }, + } + for _, test := range tests { + t.Run("", func(t *testing.T) { + actual, err := getExplicitKuberc(test.args) + if err != nil { + if err.Error() != test.expectedErr.Error() { + t.Fatalf("unexpected error %v\n", err) + } + } + if test.expected != actual { + t.Fatalf("unexpected value %s expected %s", actual, test.expected) + } + }) + } +} + +// Add list of commands in nested way. +// First iteration adds command into rootCmd, +// Second iteration adds command into the previous one. +func addCommands[T supportedTypes](rootCmd *cobra.Command, commands []fakeCmds[T]) { + if len(commands) == 0 { + return + } + + subCmd := &cobra.Command{ + Use: commands[0].name, + } + + for _, flg := range commands[0].flags { + switch v := any(flg.value).(type) { + case string: + if flg.shorthand != "" { + subCmd.Flags().StringP(flg.name, flg.shorthand, v, "") + } else { + subCmd.Flags().String(flg.name, v, "") + } + case bool: + if flg.shorthand != "" { + subCmd.Flags().BoolP(flg.name, flg.shorthand, v, "") + } else { + subCmd.Flags().Bool(flg.name, v, "") + } + } + + } + rootCmd.AddCommand(subCmd) + + addCommands[T](subCmd, commands[1:]) +} diff --git a/staging/src/k8s.io/kubectl/pkg/kuberc/marshal.go b/staging/src/k8s.io/kubectl/pkg/kuberc/marshal.go new file mode 100644 index 00000000000..06f40d53452 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/kuberc/marshal.go @@ -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 +} diff --git a/test/cmd/kuberc.sh b/test/cmd/kuberc.sh new file mode 100755 index 00000000000..a9702dca7ce --- /dev/null +++ b/test/cmd/kuberc.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash + +# 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. + +set -o errexit +set -o nounset +set -o pipefail + +run_kuberc_tests() { + set -o nounset + set -o errexit + + create_and_use_new_namespace + kube::log::status "Testing kuberc" + + # Enable KUBERC feature + export KUBECTL_KUBERC=true + + cat > "${TMPDIR:-/tmp}"/kuberc_file << EOF +apiVersion: kubectl.config.k8s.io/v1alpha1 +kind: Preference +aliases: +- name: crns + command: create namespace + appendArgs: + - test-kuberc-ns +- name: getn + command: get + prependArgs: + - namespace + flags: + - name: output + default: wide +- name: crole + command: create role + flags: + - name: verb + default: get,watch +- name: getrole + command: get + flags: + - name: output + default: json +- name: runx + command: run + flags: + - name: image + default: nginx + - name: labels + default: app=test,env=test + - name: env + default: DNS_DOMAIN=test + - name: namespace + default: test-kuberc-ns + appendArgs: + - test-pod-2 + - -- + - custom-arg1 + - custom-arg2 +- name: setx + command: set image + appendArgs: + - pod/test-pod-2 + - test-pod-2=busybox +overrides: +- command: apply + flags: + - name: server-side + default: "true" + - name: dry-run + default: "server" + - name: validate + default: "strict" +- command: delete + flags: + - name: interactive + default: "true" +- command: get + flags: + - name: namespace + default: "test-kuberc-ns" + - name: output + default: "json" +EOF + + # Pre-condition: the test-kuberc-ns namespace does not exist + kube::test::get_object_assert 'namespaces' "{{range.items}}{{ if eq ${id_field:?} \"test-kuberc-ns\" }}found{{end}}{{end}}:" ':' + # Alias command crns successfully creates namespace + kubectl crns --kuberc="${TMPDIR:-/tmp}"/kuberc_file + # Post-condition: namespace 'test-kuberc-ns' is created. + kube::test::get_object_assert 'namespaces/test-kuberc-ns' "{{$id_field}}" 'test-kuberc-ns' + + # Alias command crns successfully creates namespace + kubectl getn --kuberc="${TMPDIR:-/tmp}"/kuberc_file test-kuberc-ns + # Post-condition: namespace 'test-kuberc-ns' is created. + kube::test::get_object_assert 'namespaces/test-kuberc-ns' "{{$id_field}}" 'test-kuberc-ns' + + # Alias command crns successfully creates namespace + kubectl getn test-kuberc-ns --output=json --kuberc="${TMPDIR:-/tmp}"/kuberc_file + # Post-condition: namespace 'test-kuberc-ns' is created. + kube::test::get_object_assert 'namespaces/test-kuberc-ns' "{{$id_field}}" 'test-kuberc-ns' + + # check array flags are appended after implicit defaults + kubectl crole testkubercrole --verb=list --namespace test-kuberc-ns --resource=pods --kuberc="${TMPDIR:-/tmp}"/kuberc_file + output_message=$(kubectl getrole role/testkubercrole -n test-kuberc-ns -oyaml --kuberc="${TMPDIR:-/tmp}"/kuberc_file) + kube::test::if_has_string "${output_message}" 'list' + kube::test::if_has_not_string "${output_message}" 'watch' 'get' + # Post-condition: remove role + kubectl delete role testkubercrole --namespace=test-kuberc-ns + + # Alias run command creates a pod with the given configurations + kubectl runx --kuberc "${TMPDIR:-/tmp}"/kuberc_file + # Post-Condition: assertion object exists + kube::test::get_object_assert 'pod/test-pod-2 --namespace=test-kuberc-ns' "{{$id_field}}" 'test-pod-2' + # Not explicitly pass namespace to assure that default flag value is used + output_message=$(kubectl get pod/test-pod-2 2>&1 "${kube_flags[@]:?}" --kuberc="${TMPDIR:-/tmp}"/kuberc_file) + kube::test::if_has_string "${output_message}" 'nginx' 'app=test' 'env=test' 'DNS_DOMAIN=test' 'custom-arg1' + # output flag is defaulted to json and assure that it is correct format + kube::test::if_has_string "${output_message}" '{' + + # pass explicit invalid namespace to assure that it takes precedence over the value in kuberc + output_message=$(! kubectl get pod/test-pod-2 -n kube-system 2>&1 "${kube_flags[@]:?}" --kuberc="${TMPDIR:-/tmp}"/kuberc_file) + kube::test::if_has_string "${output_message}" 'pods "test-pod-2" not found' + + # Alias set env command sets new env var + kubectl setx --kuberc="${TMPDIR:-/tmp}"/kuberc_file -n test-kuberc-ns + # explicitly pass same namespace also defined in kuberc + output_message=$(kubectl get pod/test-pod-2 -n test-kuberc-ns 2>&1 "${kube_flags[@]:?}" --kuberc="${TMPDIR:-/tmp}"/kuberc_file) + kube::test::if_has_string "${output_message}" 'busybox' + kube::test::if_has_not_string "${output_message}" 'nginx' + + # default overrides should prevent actual apply as they are all dry-run=server + # also assure that explicit flags are also passed + output_message=$(kubectl apply -n test-kuberc-ns -f hack/testdata/pod.yaml --kuberc="${TMPDIR:-/tmp}"/kuberc_file) + kube::test::if_has_string "${output_message}" 'serverside-applied (server dry run)' + + # interactive flag is defaulted to true and prompted as no + output_message=$(kubectl delete pod/test-pod-2 -n test-kuberc-ns <<< $'n\n' --kuberc="${TMPDIR:-/tmp}"/kuberc_file) + kube::test::if_has_string "${output_message}" 'pod/test-pod-2' + # assure that it is not deleted + output_message=$(kubectl get pod/test-pod-2 2>&1 "${kube_flags[@]:?}" --kuberc="${TMPDIR:-/tmp}"/kuberc_file) + kube::test::if_has_string "${output_message}" "test-pod-2" + + cat > "${TMPDIR:-/tmp}"/kuberc_file_multi << EOF +--- +apiVersion: kubectl.config.k8s.io/v1alpha1 +kind: Preference +overrides: +- command: get + flags: + - name: namespace + default: "test-kuberc-ns" + - name: output + default: "json" +unknown: invalid +--- +apiVersion: kubectl.config.k8s.io/notexist +kind: Preference +overrides: +- command: get + flags: + - name: namespace + default: "test-kuberc-ns" + - name: output + default: "json" +EOF + + # assure that it is not deleted + output_message=$(kubectl get pod/test-pod-2 2>&1 "${kube_flags[@]:?}" --kuberc="${TMPDIR:-/tmp}"/kuberc_file_multi) + # assure that correct kuberc is found and printed in output_message + kube::test::if_has_string "${output_message}" "test-pod-2" + # assure that warning message is also printed for the notexist kuberc version + kube::test::if_has_string "${output_message}" "strict decoding error" "unknown" + + # explicitly overwriting the value that is also defaulted in kuberc and + # assure that explicit value supersedes + output_message=$(kubectl delete namespace/test-kuberc-ns --interactive=false --kuberc="${TMPDIR:-/tmp}"/kuberc_file) + kube::test::if_has_string "${output_message}" 'namespace "test-kuberc-ns" deleted' + + unset KUBECTL_KUBERC + + set +o nounset + set +o errexit +} diff --git a/test/cmd/legacy-script.sh b/test/cmd/legacy-script.sh index 70e97481051..4f9177a5612 100755 --- a/test/cmd/legacy-script.sh +++ b/test/cmd/legacy-script.sh @@ -48,6 +48,7 @@ source "${KUBE_ROOT}/test/cmd/generic-resources.sh" source "${KUBE_ROOT}/test/cmd/get.sh" source "${KUBE_ROOT}/test/cmd/help.sh" source "${KUBE_ROOT}/test/cmd/kubeconfig.sh" +source "${KUBE_ROOT}/test/cmd/kuberc.sh" source "${KUBE_ROOT}/test/cmd/node-management.sh" source "${KUBE_ROOT}/test/cmd/plugins.sh" source "${KUBE_ROOT}/test/cmd/proxy.sh" @@ -1056,5 +1057,11 @@ runTests() { record_command run_kubectl_debug_netadmin_node_tests fi + ####################### + # kuberc # + ####################### + + record_command run_kuberc_tests + cleanup_tests }