mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 12:43:23 +00:00
Kubectl command headers in requests: KEP 859
This commit is contained in:
parent
2f263b24a7
commit
211fc12b67
@ -8,6 +8,7 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
|
||||
github.com/evanphx/json-patch v4.9.0+incompatible
|
||||
github.com/google/uuid v1.1.2
|
||||
github.com/googleapis/gnostic v0.4.1
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de
|
||||
github.com/mailru/easyjson v0.7.0 // indirect
|
||||
|
1
staging/src/k8s.io/cli-runtime/go.sum
generated
1
staging/src/k8s.io/cli-runtime/go.sum
generated
@ -148,6 +148,7 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright 2021 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 genericclioptions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
kubectlCommandHeader = "X-Kubectl-Command"
|
||||
kubectlSessionHeader = "X-Kubectl-Session"
|
||||
)
|
||||
|
||||
// CommandHeaderRoundTripper adds a layer around the standard
|
||||
// round tripper to add Request headers before delegation. Implements
|
||||
// the go standard library "http.RoundTripper" interface.
|
||||
type CommandHeaderRoundTripper struct {
|
||||
Delegate http.RoundTripper
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
// CommandHeaderRoundTripper adds Request headers before delegating to standard
|
||||
// round tripper. These headers are kubectl command headers which
|
||||
// detail the kubectl command. See SIG CLI KEP 859:
|
||||
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/859-kubectl-headers
|
||||
func (c *CommandHeaderRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
for header, value := range c.Headers {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
return c.Delegate.RoundTrip(req)
|
||||
}
|
||||
|
||||
// ParseCommandHeaders fills in a map of X-Headers into the CommandHeaderRoundTripper. These
|
||||
// headers are then filled into each request. For details on X-Headers see:
|
||||
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/859-kubectl-headers
|
||||
// Each call overwrites the previously parsed command headers (not additive).
|
||||
// TODO(seans3): Parse/add flags removing PII from flag values.
|
||||
func (c *CommandHeaderRoundTripper) ParseCommandHeaders(cmd *cobra.Command, args []string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
// Overwrites previously parsed command headers (headers not additive).
|
||||
c.Headers = map[string]string{}
|
||||
// Session identifier to aggregate multiple Requests from single kubectl command.
|
||||
uid := uuid.New().String()
|
||||
c.Headers[kubectlSessionHeader] = uid
|
||||
// Iterate up the hierarchy of commands from the leaf command to create
|
||||
// the full command string. Example: kubectl create secret generic
|
||||
cmdStrs := []string{}
|
||||
for cmd.HasParent() {
|
||||
parent := cmd.Parent()
|
||||
currName := strings.TrimSpace(cmd.Name())
|
||||
cmdStrs = append([]string{currName}, cmdStrs...)
|
||||
cmd = parent
|
||||
}
|
||||
currName := strings.TrimSpace(cmd.Name())
|
||||
cmdStrs = append([]string{currName}, cmdStrs...)
|
||||
if len(cmdStrs) > 0 {
|
||||
c.Headers[kubectlCommandHeader] = strings.Join(cmdStrs, " ")
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
Copyright 2021 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 genericclioptions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var kubectlCmd = &cobra.Command{Use: "kubectl"}
|
||||
var applyCmd = &cobra.Command{Use: "apply"}
|
||||
var createCmd = &cobra.Command{Use: "create"}
|
||||
var secretCmd = &cobra.Command{Use: "secret"}
|
||||
var genericCmd = &cobra.Command{Use: "generic"}
|
||||
var authCmd = &cobra.Command{Use: "auth"}
|
||||
var reconcileCmd = &cobra.Command{Use: "reconcile"}
|
||||
|
||||
func TestParseCommandHeaders(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
// Ordering is important; each subsequent command is added as a subcommand
|
||||
// of the previous command.
|
||||
commands []*cobra.Command
|
||||
// Headers which should be present; but other headers may exist
|
||||
expectedHeaders map[string]string
|
||||
}{
|
||||
"Single kubectl command example": {
|
||||
commands: []*cobra.Command{kubectlCmd},
|
||||
expectedHeaders: map[string]string{
|
||||
kubectlCommandHeader: "kubectl",
|
||||
},
|
||||
},
|
||||
"Simple kubectl apply example": {
|
||||
commands: []*cobra.Command{kubectlCmd, applyCmd},
|
||||
expectedHeaders: map[string]string{
|
||||
kubectlCommandHeader: "kubectl apply",
|
||||
},
|
||||
},
|
||||
"Kubectl auth reconcile example": {
|
||||
commands: []*cobra.Command{kubectlCmd, authCmd, reconcileCmd},
|
||||
expectedHeaders: map[string]string{
|
||||
kubectlCommandHeader: "kubectl auth reconcile",
|
||||
},
|
||||
},
|
||||
"Long kubectl create secret generic example": {
|
||||
commands: []*cobra.Command{kubectlCmd, createCmd, secretCmd, genericCmd},
|
||||
expectedHeaders: map[string]string{
|
||||
kubectlCommandHeader: "kubectl create secret generic",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
rootCmd := buildCommandChain(tc.commands)
|
||||
ch := &CommandHeaderRoundTripper{}
|
||||
ch.ParseCommandHeaders(rootCmd, []string{})
|
||||
// Unique session ID header should always be present.
|
||||
if _, found := ch.Headers[kubectlSessionHeader]; !found {
|
||||
t.Errorf("expected kubectl session header (%s) is missing", kubectlSessionHeader)
|
||||
}
|
||||
// All expected headers must be present; but there may be extras.
|
||||
for key, expectedValue := range tc.expectedHeaders {
|
||||
actualValue, found := ch.Headers[key]
|
||||
if found {
|
||||
if expectedValue != actualValue {
|
||||
t.Errorf("expected header value (%s), got (%s)", expectedValue, actualValue)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("expected header (%s) not found", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Builds a hierarchy of commands in order from the passed slice of commands,
|
||||
// by adding each subsequent command as a child of the previous command,
|
||||
// returning the last leaf command.
|
||||
func buildCommandChain(commands []*cobra.Command) *cobra.Command {
|
||||
var currCmd *cobra.Command
|
||||
if len(commands) > 0 {
|
||||
currCmd = commands[0]
|
||||
}
|
||||
for i := 1; i < len(commands); i++ {
|
||||
cmd := commands[i]
|
||||
currCmd.AddCommand(cmd)
|
||||
currCmd = cmd
|
||||
}
|
||||
return currCmd
|
||||
}
|
@ -98,6 +98,9 @@ type ConfigFlags struct {
|
||||
Username *string
|
||||
Password *string
|
||||
Timeout *string
|
||||
// If non-nil, wrap config function can transform the Config
|
||||
// before it is returned in ToRESTConfig function.
|
||||
WrapConfigFn func(*rest.Config) *rest.Config
|
||||
|
||||
clientConfig clientcmd.ClientConfig
|
||||
lock sync.Mutex
|
||||
@ -113,9 +116,17 @@ type ConfigFlags struct {
|
||||
// ToRESTConfig implements RESTClientGetter.
|
||||
// Returns a REST client configuration based on a provided path
|
||||
// to a .kubeconfig file, loading rules, and config flag overrides.
|
||||
// Expects the AddFlags method to have been called.
|
||||
// Expects the AddFlags method to have been called. If WrapConfigFn
|
||||
// is non-nil this function can transform config before return.
|
||||
func (f *ConfigFlags) ToRESTConfig() (*rest.Config, error) {
|
||||
return f.ToRawKubeConfigLoader().ClientConfig()
|
||||
c, err := f.ToRawKubeConfigLoader().ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f.WrapConfigFn != nil {
|
||||
return f.WrapConfigFn(c), nil
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ToRawKubeConfigLoader binds config flag values to config overrides
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
@ -322,6 +323,8 @@ __kubectl_custom_func() {
|
||||
`
|
||||
)
|
||||
|
||||
const kubectlCmdHeaders = "KUBECTL_COMMAND_HEADERS"
|
||||
|
||||
var (
|
||||
bashCompletionFlags = map[string]string{
|
||||
"namespace": "__kubectl_get_resource_namespace",
|
||||
@ -469,7 +472,6 @@ func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string) error {
|
||||
func NewKubectlCommand(in io.Reader, out, err io.Writer) *cobra.Command {
|
||||
warningHandler := rest.NewWarningWriter(err, rest.WarningWriterOptions{Deduplicate: true, Color: term.AllowsColorOutput(err)})
|
||||
warningsAsErrors := false
|
||||
|
||||
// Parent command to which all subcommands are added.
|
||||
cmds := &cobra.Command{
|
||||
Use: "kubectl",
|
||||
@ -521,6 +523,8 @@ func NewKubectlCommand(in io.Reader, out, err io.Writer) *cobra.Command {
|
||||
kubeConfigFlags.AddFlags(flags)
|
||||
matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags)
|
||||
matchVersionKubeConfigFlags.AddFlags(cmds.PersistentFlags())
|
||||
// Updates hooks to add kubectl command headers: SIG CLI KEP 859.
|
||||
addCmdHeaderHooks(cmds, kubeConfigFlags)
|
||||
|
||||
cmds.PersistentFlags().AddGoFlagSet(flag.CommandLine)
|
||||
|
||||
@ -538,6 +542,12 @@ func NewKubectlCommand(in io.Reader, out, err io.Writer) *cobra.Command {
|
||||
|
||||
ioStreams := genericclioptions.IOStreams{In: in, Out: out, ErrOut: err}
|
||||
|
||||
// Proxy command is incompatible with CommandHeaderRoundTripper, so
|
||||
// clear the WrapConfigFn before running proxy command.
|
||||
proxyCmd := proxy.NewCmdProxy(f, ioStreams)
|
||||
proxyCmd.PreRun = func(cmd *cobra.Command, args []string) {
|
||||
kubeConfigFlags.WrapConfigFn = nil
|
||||
}
|
||||
groups := templates.CommandGroups{
|
||||
{
|
||||
Message: "Basic Commands (Beginner):",
|
||||
@ -585,7 +595,7 @@ func NewKubectlCommand(in io.Reader, out, err io.Writer) *cobra.Command {
|
||||
attach.NewCmdAttach(f, ioStreams),
|
||||
cmdexec.NewCmdExec(f, ioStreams),
|
||||
portforward.NewCmdPortForward(f, ioStreams),
|
||||
proxy.NewCmdProxy(f, ioStreams),
|
||||
proxyCmd,
|
||||
cp.NewCmdCp(f, ioStreams),
|
||||
auth.NewCmdAuth(f, ioStreams),
|
||||
debug.NewCmdDebug(f, ioStreams),
|
||||
@ -646,6 +656,38 @@ func NewKubectlCommand(in io.Reader, out, err io.Writer) *cobra.Command {
|
||||
return cmds
|
||||
}
|
||||
|
||||
// addCmdHeaderHooks performs updates on two hooks:
|
||||
// 1) Modifies the passed "cmds" persistent pre-run function to parse command headers.
|
||||
// These headers will be subsequently added as X-headers to every
|
||||
// REST call.
|
||||
// 2) Adds CommandHeaderRoundTripper as a wrapper around the standard
|
||||
// RoundTripper. CommandHeaderRoundTripper adds X-Headers then delegates
|
||||
// to standard RoundTripper.
|
||||
// For alpha, these hooks are only updated if the KUBECTL_COMMAND_HEADERS
|
||||
// environment variable is set.
|
||||
// See SIG CLI KEP 859 for more information:
|
||||
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/859-kubectl-headers
|
||||
func addCmdHeaderHooks(cmds *cobra.Command, kubeConfigFlags *genericclioptions.ConfigFlags) {
|
||||
if _, exists := os.LookupEnv(kubectlCmdHeaders); !exists {
|
||||
return
|
||||
}
|
||||
crt := &genericclioptions.CommandHeaderRoundTripper{}
|
||||
existingPreRunE := cmds.PersistentPreRunE
|
||||
// Add command parsing to the existing persistent pre-run function.
|
||||
cmds.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
crt.ParseCommandHeaders(cmd, args)
|
||||
return existingPreRunE(cmd, args)
|
||||
}
|
||||
// Wraps CommandHeaderRoundTripper around standard RoundTripper.
|
||||
kubeConfigFlags.WrapConfigFn = func(c *rest.Config) *rest.Config {
|
||||
c.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||
crt.Delegate = rt
|
||||
return crt
|
||||
})
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
func runHelp(cmd *cobra.Command, args []string) {
|
||||
cmd.Help()
|
||||
}
|
||||
|
1
staging/src/k8s.io/sample-cli-plugin/go.sum
generated
1
staging/src/k8s.io/sample-cli-plugin/go.sum
generated
@ -148,6 +148,7 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
|
Loading…
Reference in New Issue
Block a user