Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kubectl command headers in requests: KEP 859 #98952

Merged
merged 1 commit into from
Mar 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions staging/src/k8s.io/cli-runtime/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions staging/src/k8s.io/cli-runtime/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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, " ")
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
46 changes: 44 additions & 2 deletions staging/src/k8s.io/kubectl/pkg/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"flag"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"runtime"
Expand Down Expand Up @@ -322,6 +323,8 @@ __kubectl_custom_func() {
`
)

const kubectlCmdHeaders = "KUBECTL_COMMAND_HEADERS"

var (
bashCompletionFlags = map[string]string{
"namespace": "__kubectl_get_resource_namespace",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)

Expand All @@ -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):",
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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 change: 1 addition & 0 deletions staging/src/k8s.io/sample-cli-plugin/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.