Skip to content

Commit 9f47edd

Browse files
Merge pull request #134870 from pmengelbert/pmengelbert/kuberc/4
Add client-go credential plugin to kuberc Kubernetes-commit: 183892b2c96aa962118ccfc2ee1c971caf72b16b
2 parents 46c22a3 + 52b571c commit 9f47edd

15 files changed

Lines changed: 593 additions & 52 deletions

go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ require (
3030
golang.org/x/sys v0.37.0
3131
golang.org/x/text v0.29.0
3232
gopkg.in/evanphx/json-patch.v4 v4.13.0
33-
k8s.io/api v0.0.0-20251106202824-18e16b5aa26d
34-
k8s.io/apimachinery v0.0.0-20251104194212-729c13d7df38
35-
k8s.io/cli-runtime v0.0.0-20251104214421-bd0840656b55
36-
k8s.io/client-go v0.0.0-20251106123256-0e6fc04326d2
37-
k8s.io/component-base v0.0.0-20251105043606-09c454e1f74b
33+
k8s.io/api v0.0.0-20251107002836-f1737241c064
34+
k8s.io/apimachinery v0.0.0-20251106231852-6f8949260573
35+
k8s.io/cli-runtime v0.0.0-20251110050429-aaf392a9dbb5
36+
k8s.io/client-go v0.0.0-20251110043236-6ce2c0f8c3b9
37+
k8s.io/component-base v0.0.0-20251110044304-c1ad4134391c
3838
k8s.io/component-helpers v0.0.0-20251106124553-0e2bf40485ce
3939
k8s.io/klog/v2 v2.130.1
4040
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912

go.sum

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -192,16 +192,16 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
192192
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
193193
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
194194
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
195-
k8s.io/api v0.0.0-20251106202824-18e16b5aa26d h1:3SQPUUHMeygNWukXQYPtNoQfiqgQkDxv7T+uicVmYGI=
196-
k8s.io/api v0.0.0-20251106202824-18e16b5aa26d/go.mod h1:WAtHXlm214kKcYg1bP2G5eefhLoi/NCF6sxUeTjUXMs=
197-
k8s.io/apimachinery v0.0.0-20251104194212-729c13d7df38 h1:5jRAlNQwmLaPNhf9mfhacvZKFF8fE4nfULiBul0PrGM=
198-
k8s.io/apimachinery v0.0.0-20251104194212-729c13d7df38/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak=
199-
k8s.io/cli-runtime v0.0.0-20251104214421-bd0840656b55 h1:3MvAzXG9vXwjhuBqktZiW/qzTXktD2THOt8+xuvAnaM=
200-
k8s.io/cli-runtime v0.0.0-20251104214421-bd0840656b55/go.mod h1:1S/aYJqjbAEheEwb9zogUIfQB3mcd+Vh+SKzX4hWY5A=
201-
k8s.io/client-go v0.0.0-20251106123256-0e6fc04326d2 h1:6wyLSk4mUY9VUUYqsFyDvqEQD9+QoRRBu22loMh9Tw8=
202-
k8s.io/client-go v0.0.0-20251106123256-0e6fc04326d2/go.mod h1:Xw1AYXa8yFMl6qNHHV3E0uhL7RAq+X5I5me/G08wwYY=
203-
k8s.io/component-base v0.0.0-20251105043606-09c454e1f74b h1:iZ59BHeRkNAM5RSutSryNx9qtrgT7YPSM5HanS+GXVw=
204-
k8s.io/component-base v0.0.0-20251105043606-09c454e1f74b/go.mod h1:ccVXlbwyM8koWd6+OylPWi8m3aunkoPzcQcSbVvTHrk=
195+
k8s.io/api v0.0.0-20251107002836-f1737241c064 h1:n8Q6kd+Mwr2ce6QpIGlMKM31hhb0XkeNKKYc8mfv/pk=
196+
k8s.io/api v0.0.0-20251107002836-f1737241c064/go.mod h1:KmeiqHqfbEx7y5cjOafexrDA1x0/NCS0EvYefr1M9eQ=
197+
k8s.io/apimachinery v0.0.0-20251106231852-6f8949260573 h1:gNmsE5h0ynVpA5XKQBVSWKTogBPL4ecYdzF5get+L4A=
198+
k8s.io/apimachinery v0.0.0-20251106231852-6f8949260573/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak=
199+
k8s.io/cli-runtime v0.0.0-20251110050429-aaf392a9dbb5 h1:TNpIHORxdvVjHG6sc9bTof9O+Au0HLL2Im5Ecxx0GW4=
200+
k8s.io/cli-runtime v0.0.0-20251110050429-aaf392a9dbb5/go.mod h1:gxjLvb8qJv8gyqFX9RUsHmVBcU4r++7GLwubu1PCdeI=
201+
k8s.io/client-go v0.0.0-20251110043236-6ce2c0f8c3b9 h1:w3avFIgMRha3Tp2Y8GtSbA/CP4Pjn+MoEFk2L6660Z0=
202+
k8s.io/client-go v0.0.0-20251110043236-6ce2c0f8c3b9/go.mod h1:QM7Zy4bRkvPZ8IwCto7QVHLGkeASaoOsSDtpTRqY55s=
203+
k8s.io/component-base v0.0.0-20251110044304-c1ad4134391c h1:D6Qzh3e/OInWBo4eA2W+11N1Iyp+b3AJbQ08PBpgP3A=
204+
k8s.io/component-base v0.0.0-20251110044304-c1ad4134391c/go.mod h1:N9qBoyM5YJbtzos7/NnVUVt93sD3VrxVQShY1FQK+NI=
205205
k8s.io/component-helpers v0.0.0-20251106124553-0e2bf40485ce h1:lSLeDsacpYQVPtJnd4/S3iciKZ/b08CiCxhjwBbm+ec=
206206
k8s.io/component-helpers v0.0.0-20251106124553-0e2bf40485ce/go.mod h1:hBkkxYVO7wXIh0RjzhUECKBc6LIsL1wzNIyKYGDW5q8=
207207
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=

pkg/cmd/cmd.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"net/http"
2222
"os"
2323
"strings"
24+
"sync/atomic"
2425

2526
"github.com/spf13/cobra"
2627

@@ -233,15 +234,17 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
233234
matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags)
234235
matchVersionKubeConfigFlags.AddFlags(flags)
235236
// Updates hooks to add kubectl command headers: SIG CLI KEP 859.
236-
addCmdHeaderHooks(cmds, kubeConfigFlags)
237+
var isProxyCmd atomic.Bool
238+
addCmdHeaderHooks(cmds, kubeConfigFlags, &isProxyCmd)
237239

238240
f := cmdutil.NewFactory(matchVersionKubeConfigFlags)
239241

240-
// Proxy command is incompatible with CommandHeaderRoundTripper, so
241-
// clear the WrapConfigFn before running proxy command.
242+
// Proxy command is incompatible with the headers set by
243+
// CommandHeaderRoundTripper, so the RoundTripper hooks set in
244+
// `addCmdHeaderHooks` needs to be aware that the subcommand is `proxy`
242245
proxyCmd := proxy.NewCmdProxy(f, o.IOStreams)
243246
proxyCmd.PreRun = func(cmd *cobra.Command, args []string) {
244-
kubeConfigFlags.WrapConfigFn = nil
247+
isProxyCmd.Store(true)
245248
}
246249

247250
// Avoid import cycle by setting ValidArgsFunction here instead of in NewCmdGet()
@@ -366,7 +369,7 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
366369
}
367370
return existingPreRunE(cmd, args)
368371
}
369-
_, err := pref.Apply(cmds, o.Arguments, o.IOStreams.ErrOut)
372+
_, err := pref.Apply(cmds, kubeConfigFlags, o.Arguments, o.IOStreams.ErrOut)
370373
if err != nil {
371374
fmt.Fprintf(o.IOStreams.ErrOut, "error occurred while applying preferences %v\n", err)
372375
os.Exit(1)
@@ -387,7 +390,7 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
387390
// See SIG CLI KEP 859 for more information:
388391
//
389392
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/859-kubectl-headers
390-
func addCmdHeaderHooks(cmds *cobra.Command, kubeConfigFlags *genericclioptions.ConfigFlags) {
393+
func addCmdHeaderHooks(cmds *cobra.Command, kubeConfigFlags *genericclioptions.ConfigFlags, isProxyCmd *atomic.Bool) {
391394
crt := &genericclioptions.CommandHeaderRoundTripper{}
392395
existingPreRunE := cmds.PersistentPreRunE
393396
// Add command parsing to the existing persistent pre-run function.
@@ -397,20 +400,21 @@ func addCmdHeaderHooks(cmds *cobra.Command, kubeConfigFlags *genericclioptions.C
397400
}
398401
wrapConfigFn := kubeConfigFlags.WrapConfigFn
399402
// Wraps CommandHeaderRoundTripper around standard RoundTripper.
400-
kubeConfigFlags.WrapConfigFn = func(c *rest.Config) *rest.Config {
403+
kubeConfigFlags.WithWrapConfigFn(func(c *rest.Config) *rest.Config {
401404
if wrapConfigFn != nil {
402405
c = wrapConfigFn(c)
403406
}
404407
c.Wrap(func(rt http.RoundTripper) http.RoundTripper {
405408
// Must be separate RoundTripper; not "crt" closure.
406409
// Fixes: https://github.com/kubernetes/kubectl/issues/1098
407410
return &genericclioptions.CommandHeaderRoundTripper{
408-
Delegate: rt,
409-
Headers: crt.Headers,
411+
Delegate: rt,
412+
Headers: crt.Headers,
413+
SkipHeaders: isProxyCmd, // proxy command is incompatible with these headers
410414
}
411415
})
412416
return c
413-
}
417+
})
414418
}
415419

416420
func runHelp(cmd *cobra.Command, args []string) {

pkg/config/scheme/scheme_test.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,28 @@ package scheme
1919
import (
2020
"testing"
2121

22+
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
2223
"k8s.io/apimachinery/pkg/api/apitesting/roundtrip"
23-
"k8s.io/kubectl/pkg/config/fuzzer"
24+
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
25+
"k8s.io/kubectl/pkg/config"
26+
kubectlfuzzer "k8s.io/kubectl/pkg/config/fuzzer"
27+
"sigs.k8s.io/randfill"
2428
)
2529

2630
func TestRoundTripTypes(t *testing.T) {
27-
roundtrip.RoundTripTestForScheme(t, Scheme, fuzzer.Funcs)
31+
// Because v1alpha1 does not have fields of these types, the fuzzing will
32+
// be incorrect, so we need to manually intervene here
33+
customFuzzerFuncs := func(codecs runtimeserializer.CodecFactory) []interface{} {
34+
return []interface{}{
35+
func(s *config.CredentialPluginPolicy, c randfill.Continue) {
36+
*s = ""
37+
},
38+
func(s *[]config.AllowlistEntry, c randfill.Continue) {
39+
*s = nil
40+
},
41+
}
42+
}
43+
44+
funcs := fuzzer.MergeFuzzerFuncs(kubectlfuzzer.Funcs, customFuzzerFuncs)
45+
roundtrip.RoundTripTestForScheme(t, Scheme, funcs)
2846
}

pkg/config/types.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ limitations under the License.
1616

1717
package config
1818

19-
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+
import (
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
)
2022

2123
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
2224

@@ -62,6 +64,63 @@ type Preference struct {
6264
// "kubectl getn control-plane-1 --output=json" expands to "kubectl get node --output=json control-plane-1"
6365
// +optional
6466
Aliases []AliasOverride
67+
68+
// credentialPluginPolicy specifies the policy governing which, if any, client-go
69+
// credential plugins may be executed. It MUST be one of { "", "AllowAll", "DenyAll", "Allowlist" }.
70+
// If the policy is "", then it falls back to "AllowAll" (this is required
71+
// to maintain backward compatibility). If the policy is DenyAll, no
72+
// credential plugins may run. If the policy is Allowlist, only those
73+
// plugins meeting the criteria specified in the `credentialPluginAllowlist`
74+
// field may run.
75+
// +optional
76+
CredentialPluginPolicy CredentialPluginPolicy
77+
78+
// Allowlist is a slice of allowlist entries. If any of them is a match,
79+
// then the executable in question may execute. That is, the result is the
80+
// logical OR of all entries in the allowlist. This list MUST NOT be
81+
// supplied if the policy is not "Allowlist".
82+
//
83+
// e.g.
84+
// credentialPluginAllowlist:
85+
// - name: cloud-provider-plugin
86+
// - name: /usr/local/bin/my-plugin
87+
// In the above example, the user allows the credential plugins
88+
// `cloud-provider-plugin` (found somewhere in PATH), and the plugin found
89+
// at the explicit path `/usr/local/bin/my-plugin`.
90+
// +optional
91+
CredentialPluginAllowlist []AllowlistEntry
92+
}
93+
94+
// CredentialPluginPolicy specifies the policy governing which, if any, client-go
95+
// credential plugins may be executed. It MUST be one of { "", "AllowAll", "DenyAll", "Allowlist" }.
96+
// If the policy is "", then it falls back to "AllowAll" (this is required
97+
// to maintain backward compatibility). If the policy is DenyAll, no
98+
// credential plugins may run. If the policy is Allowlist, only those
99+
// plugins meeting the criteria specified in the `credentialPluginAllowlist`
100+
// field may run. If the policy is not `Allowlist` but one is provided, it
101+
// is considered a configuration error.
102+
type CredentialPluginPolicy string
103+
104+
const (
105+
PluginPolicyAllowAll CredentialPluginPolicy = "AllowAll"
106+
PluginPolicyDenyAll CredentialPluginPolicy = "DenyAll"
107+
PluginPolicyAllowlist CredentialPluginPolicy = "Allowlist"
108+
)
109+
110+
// AllowlistEntry is an entry in the allowlist. For each allowlist item, at
111+
// least one field must be nonempty. A struct with all empty fields is
112+
// considered a misconfiguration error. Each field is a criterion for
113+
// execution. If multiple fields are specified, then the criteria of all
114+
// specified fields must be met. That is, the result of an individual entry is
115+
// the logical AND of all checks corresponding to the specified fields within
116+
// the entry.
117+
type AllowlistEntry struct {
118+
// Name matching is performed by first resolving the absolute path of both
119+
// the plugin and the name in the allowlist entry using `exec.LookPath`. It
120+
// will be called on both, and the resulting strings must be equal. If
121+
// either call to `exec.LookPath` results in an error, the `Name` check
122+
// will be considered a failure.
123+
Name string
65124
}
66125

67126
// AliasOverride stores the alias definitions.

pkg/config/v1alpha1/conversion.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha1
18+
19+
import (
20+
conversion "k8s.io/apimachinery/pkg/conversion"
21+
config "k8s.io/kubectl/pkg/config"
22+
)
23+
24+
// v1alpha1 Preference does not have `CredentialPluginPolicy` or `CredentialPluginAllowlist` fields. They can be left blank, so the autoConvert functions will suffice.
25+
func Convert_config_Preference_To_v1alpha1_Preference(in *config.Preference, out *Preference, s conversion.Scope) error {
26+
return autoConvert_config_Preference_To_v1alpha1_Preference(in, out, s)
27+
}
28+
29+
func Convert_v1alpha1_Preference_To_config_Preference(in *Preference, out *config.Preference, s conversion.Scope) error {
30+
return autoConvert_v1alpha1_Preference_To_config_Preference(in, out, s)
31+
}

pkg/config/v1alpha1/zz_generated.conversion.go

Lines changed: 6 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/config/v1beta1/register.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func init() {
3737
// We only register manually written functions here. The registration of the
3838
// generated functions takes place in the generated files. The separation
3939
// makes the code compile even when the generated files are missing.
40-
localSchemeBuilder.Register(addKnownTypes)
40+
localSchemeBuilder.Register(addKnownTypes, RegisterDefaults)
4141
}
4242

4343
// addKnownTypes registers known types to the given scheme

pkg/config/v1beta1/types.go

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ limitations under the License.
1616

1717
package v1beta1
1818

19-
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+
import (
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
)
2022

2123
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
2224

@@ -62,6 +64,64 @@ type Preference struct {
6264
// "kubectl getn control-plane-1 --output=json" expands to "kubectl get node --output=json control-plane-1"
6365
// +listType=atomic
6466
Aliases []AliasOverride `json:"aliases"`
67+
68+
// credentialPluginPolicy specifies the policy governing which, if any, client-go
69+
// credential plugins may be executed. It MUST be one of { "", "AllowAll", "DenyAll", "Allowlist" }.
70+
// If the policy is "", then it falls back to "AllowAll" (this is required
71+
// to maintain backward compatibility). If the policy is DenyAll, no
72+
// credential plugins may run. If the policy is Allowlist, only those
73+
// plugins meeting the criteria specified in the `credentialPluginAllowlist`
74+
// field may run.
75+
// +optional
76+
CredentialPluginPolicy CredentialPluginPolicy `json:"credentialPluginPolicy,omitempty"`
77+
78+
// Allowlist is a slice of allowlist entries. If any of them is a match,
79+
// then the executable in question may execute. That is, the result is the
80+
// logical OR of all entries in the allowlist. This list MUST NOT be
81+
// supplied if the policy is not "Allowlist".
82+
//
83+
// e.g.
84+
// credentialPluginAllowlist:
85+
// - name: cloud-provider-plugin
86+
// - name: /usr/local/bin/my-plugin
87+
// In the above example, the user allows the credential plugins
88+
// `cloud-provider-plugin` (found somewhere in PATH), and the plugin found
89+
// at the explicit path `/usr/local/bin/my-plugin`.
90+
// +optional
91+
// +listType=atomic
92+
CredentialPluginAllowlist []AllowlistEntry `json:"credentialPluginAllowlist,omitempty"`
93+
}
94+
95+
// CredentialPluginPolicy specifies the policy governing which, if any, client-go
96+
// credential plugins may be executed. It MUST be one of { "", "AllowAll", "DenyAll", "Allowlist" }.
97+
// If the policy is "", then it falls back to "AllowAll" (this is required
98+
// to maintain backward compatibility). If the policy is DenyAll, no
99+
// credential plugins may run. If the policy is Allowlist, only those
100+
// plugins meeting the criteria specified in the `credentialPluginAllowlist`
101+
// field may run. If the policy is not `Allowlist` but one is provided, it
102+
// is considered a configuration error.
103+
type CredentialPluginPolicy string
104+
105+
const (
106+
PluginPolicyAllowAll CredentialPluginPolicy = "AllowAll"
107+
PluginPolicyDenyAll CredentialPluginPolicy = "DenyAll"
108+
PluginPolicyAllowlist CredentialPluginPolicy = "Allowlist"
109+
)
110+
111+
// AllowlistEntry is an entry in the allowlist. For each allowlist item, at
112+
// least one field must be nonempty. A struct with all empty fields is
113+
// considered a misconfiguration error. Each field is a criterion for
114+
// execution. If multiple fields are specified, then the criteria of all
115+
// specified fields must be met. That is, the result of an individual entry is
116+
// the logical AND of all checks corresponding to the specified fields within
117+
// the entry.
118+
type AllowlistEntry struct {
119+
// Name matching is performed by first resolving the absolute path of both
120+
// the plugin and the name in the allowlist entry using `exec.LookPath`. It
121+
// will be called on both, and the resulting strings must be equal. If
122+
// either call to `exec.LookPath` results in an error, the `Name` check
123+
// will be considered a failure.
124+
Name string `json:"name"`
65125
}
66126

67127
// AliasOverride stores the alias definitions.

0 commit comments

Comments
 (0)