mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-27 05:27:21 +00:00
Merge pull request #41653 from jlowdermilk/gcp-auth-plugin
Automatic merge from submit-queue (batch tested with PRs 42080, 41653, 42598, 42555) Support whitespace in command path for gcp auth plugin ``` External command option on gcp client auth plugin supports whitespace in command path. ``` Splitting on whitespace to get cmd+args breaks when the path the executable contains spaces. Resolve by adding a new "cmd-args" field to config to allow the full string of "cmd-path" to be interpreted as path to executable. This change is backwards compatible with existing behavior.
This commit is contained in:
commit
73c5d6cd2f
@ -41,6 +41,9 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stubbable for testing
|
||||||
|
var execCommand = exec.Command
|
||||||
|
|
||||||
// gcpAuthProvider is an auth provider plugin that uses GCP credentials to provide
|
// gcpAuthProvider is an auth provider plugin that uses GCP credentials to provide
|
||||||
// tokens for kubectl to authenticate itself to the apiserver. A sample json config
|
// tokens for kubectl to authenticate itself to the apiserver. A sample json config
|
||||||
// is provided below with all recognized options described.
|
// is provided below with all recognized options described.
|
||||||
@ -62,10 +65,13 @@ func init() {
|
|||||||
// # These options direct the plugin to execute a specified command and parse
|
// # These options direct the plugin to execute a specified command and parse
|
||||||
// # token and expiry time from the output of the command.
|
// # token and expiry time from the output of the command.
|
||||||
//
|
//
|
||||||
// # Command to execute for access token. String is split on whitespace
|
// # Command to execute for access token. Command output will be parsed as JSON.
|
||||||
// # with first field treated as the executable, remaining fields as args.
|
// # If "cmd-args" is not present, this value will be split on whitespace, with
|
||||||
// # Command output will be parsed as JSON.
|
// # the first element interpreted as the command, remaining elements as args.
|
||||||
// "cmd-path": "/usr/bin/gcloud config config-helper --output=json",
|
// "cmd-path": "/usr/bin/gcloud",
|
||||||
|
//
|
||||||
|
// # Arguments to pass to command to execute for access token.
|
||||||
|
// "cmd-args": "config config-helper --output=json"
|
||||||
//
|
//
|
||||||
// # JSONPath to the string field that represents the access token in
|
// # JSONPath to the string field that represents the access token in
|
||||||
// # command output. If omitted, defaults to "{.access_token}".
|
// # command output. If omitted, defaults to "{.access_token}".
|
||||||
@ -89,11 +95,21 @@ type gcpAuthProvider struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newGCPAuthProvider(_ string, gcpConfig map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) {
|
func newGCPAuthProvider(_ string, gcpConfig map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) {
|
||||||
cmd, useCmd := gcpConfig["cmd-path"]
|
|
||||||
var ts oauth2.TokenSource
|
var ts oauth2.TokenSource
|
||||||
var err error
|
var err error
|
||||||
if useCmd {
|
if cmd, useCmd := gcpConfig["cmd-path"]; useCmd {
|
||||||
ts, err = newCmdTokenSource(cmd, gcpConfig["token-key"], gcpConfig["expiry-key"], gcpConfig["time-fmt"])
|
if len(cmd) == 0 {
|
||||||
|
return nil, fmt.Errorf("missing access token cmd")
|
||||||
|
}
|
||||||
|
var args []string
|
||||||
|
if cmdArgs, ok := gcpConfig["cmd-args"]; ok {
|
||||||
|
args = strings.Fields(cmdArgs)
|
||||||
|
} else {
|
||||||
|
fields := strings.Fields(cmd)
|
||||||
|
cmd = fields[0]
|
||||||
|
args = fields[1:]
|
||||||
|
}
|
||||||
|
ts = newCmdTokenSource(cmd, args, gcpConfig["token-key"], gcpConfig["expiry-key"], gcpConfig["time-fmt"])
|
||||||
} else {
|
} else {
|
||||||
ts, err = google.DefaultTokenSource(context.Background(), "https://www.googleapis.com/auth/cloud-platform")
|
ts, err = google.DefaultTokenSource(context.Background(), "https://www.googleapis.com/auth/cloud-platform")
|
||||||
}
|
}
|
||||||
@ -192,7 +208,7 @@ type commandTokenSource struct {
|
|||||||
timeFmt string
|
timeFmt string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCmdTokenSource(cmd, tokenKey, expiryKey, timeFmt string) (*commandTokenSource, error) {
|
func newCmdTokenSource(cmd string, args []string, tokenKey, expiryKey, timeFmt string) *commandTokenSource {
|
||||||
if len(timeFmt) == 0 {
|
if len(timeFmt) == 0 {
|
||||||
timeFmt = time.RFC3339Nano
|
timeFmt = time.RFC3339Nano
|
||||||
}
|
}
|
||||||
@ -202,25 +218,21 @@ func newCmdTokenSource(cmd, tokenKey, expiryKey, timeFmt string) (*commandTokenS
|
|||||||
if len(expiryKey) == 0 {
|
if len(expiryKey) == 0 {
|
||||||
expiryKey = "{.token_expiry}"
|
expiryKey = "{.token_expiry}"
|
||||||
}
|
}
|
||||||
fields := strings.Fields(cmd)
|
|
||||||
if len(fields) == 0 {
|
|
||||||
return nil, fmt.Errorf("missing access token cmd")
|
|
||||||
}
|
|
||||||
return &commandTokenSource{
|
return &commandTokenSource{
|
||||||
cmd: fields[0],
|
cmd: cmd,
|
||||||
args: fields[1:],
|
args: args,
|
||||||
tokenKey: tokenKey,
|
tokenKey: tokenKey,
|
||||||
expiryKey: expiryKey,
|
expiryKey: expiryKey,
|
||||||
timeFmt: timeFmt,
|
timeFmt: timeFmt,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *commandTokenSource) Token() (*oauth2.Token, error) {
|
func (c *commandTokenSource) Token() (*oauth2.Token, error) {
|
||||||
fullCmd := fmt.Sprintf("%s %s", c.cmd, strings.Join(c.args, " "))
|
fullCmd := strings.Join(append([]string{c.cmd}, c.args...), " ")
|
||||||
cmd := exec.Command(c.cmd, c.args...)
|
cmd := execCommand(c.cmd, c.args...)
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error executing access token command %q: %v", fullCmd, err)
|
return nil, fmt.Errorf("error executing access token command %q: err=%v output=%s", fullCmd, err, output)
|
||||||
}
|
}
|
||||||
token, err := c.parseTokenCmdOutput(output)
|
token, err := c.parseTokenCmdOutput(output)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -241,11 +253,11 @@ func (c *commandTokenSource) parseTokenCmdOutput(output []byte) (*oauth2.Token,
|
|||||||
|
|
||||||
accessToken, err := parseJSONPath(data, "token-key", c.tokenKey)
|
accessToken, err := parseJSONPath(data, "token-key", c.tokenKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing token-key %q: %v", c.tokenKey, err)
|
return nil, fmt.Errorf("error parsing token-key %q from %q: %v", c.tokenKey, string(output), err)
|
||||||
}
|
}
|
||||||
expiryStr, err := parseJSONPath(data, "expiry-key", c.expiryKey)
|
expiryStr, err := parseJSONPath(data, "expiry-key", c.expiryKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing expiry-key %q: %v", c.expiryKey, err)
|
return nil, fmt.Errorf("error parsing expiry-key %q from %q: %v", c.expiryKey, string(output), err)
|
||||||
}
|
}
|
||||||
var expiry time.Time
|
var expiry time.Time
|
||||||
if t, err := time.Parse(c.timeFmt, expiryStr); err != nil {
|
if t, err := time.Parse(c.timeFmt, expiryStr); err != nil {
|
||||||
|
@ -18,6 +18,8 @@ package gcp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -27,118 +29,229 @@ import (
|
|||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCmdTokenSource(t *testing.T) {
|
type fakeOutput struct {
|
||||||
fakeExpiry := time.Date(2016, 10, 31, 22, 31, 9, 123000000, time.UTC)
|
args []string
|
||||||
customFmt := "2006-01-02 15:04:05.999999999"
|
output string
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
var (
|
||||||
name string
|
wantCmd []string
|
||||||
output []byte
|
// Output for fakeExec, keyed by command
|
||||||
cmd, tokenKey, expiryKey, timeFmt string
|
execOutputs = map[string]fakeOutput{
|
||||||
tok *oauth2.Token
|
"/default/no/args": {
|
||||||
expectErr error
|
args: []string{},
|
||||||
}{
|
output: `{
|
||||||
{
|
|
||||||
"defaults",
|
|
||||||
[]byte(`{
|
|
||||||
"access_token": "faketoken",
|
"access_token": "faketoken",
|
||||||
"token_expiry": "2016-10-31T22:31:09.123000000Z"
|
"token_expiry": "2016-10-31T22:31:09.123000000Z"
|
||||||
}`),
|
}`},
|
||||||
"/fake/cmd/path", "", "", "",
|
"/default/legacy/args": {
|
||||||
&oauth2.Token{
|
args: []string{"arg1", "arg2", "arg3"},
|
||||||
AccessToken: "faketoken",
|
output: `{
|
||||||
TokenType: "Bearer",
|
"access_token": "faketoken",
|
||||||
Expiry: fakeExpiry,
|
"token_expiry": "2016-10-31T22:31:09.123000000Z"
|
||||||
},
|
}`},
|
||||||
nil,
|
"/space in path/customkeys": {
|
||||||
},
|
args: []string{"can", "haz", "auth"},
|
||||||
{
|
output: `{
|
||||||
"custom keys",
|
|
||||||
[]byte(`{
|
|
||||||
"token": "faketoken",
|
"token": "faketoken",
|
||||||
"token_expiry": {
|
"token_expiry": {
|
||||||
"datetime": "2016-10-31 22:31:09.123"
|
"datetime": "2016-10-31 22:31:09.123"
|
||||||
}
|
}
|
||||||
}`),
|
}`},
|
||||||
"/fake/cmd/path", "{.token}", "{.token_expiry.datetime}", customFmt,
|
"missing/tokenkey/noargs": {
|
||||||
|
args: []string{},
|
||||||
|
output: `{
|
||||||
|
"broken": "faketoken",
|
||||||
|
"token_expiry": {
|
||||||
|
"datetime": "2016-10-31 22:31:09.123000000Z"
|
||||||
|
}
|
||||||
|
}`},
|
||||||
|
"missing/expirykey/legacyargs": {
|
||||||
|
args: []string{"split", "on", "whitespace"},
|
||||||
|
output: `{
|
||||||
|
"access_token": "faketoken",
|
||||||
|
"expires": "2016-10-31T22:31:09.123000000Z"
|
||||||
|
}`},
|
||||||
|
"invalid expiry/timestamp": {
|
||||||
|
args: []string{"foo", "--bar", "--baz=abc,def"},
|
||||||
|
output: `{
|
||||||
|
"access_token": "faketoken",
|
||||||
|
"token_expiry": "sometime soon, idk"
|
||||||
|
}`},
|
||||||
|
"badjson": {
|
||||||
|
args: []string{},
|
||||||
|
output: `{
|
||||||
|
"access_token": "faketoken",
|
||||||
|
"token_expiry": "sometime soon, idk"
|
||||||
|
------
|
||||||
|
`},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func fakeExec(command string, args ...string) *exec.Cmd {
|
||||||
|
cs := []string{"-test.run=TestHelperProcess", "--", command}
|
||||||
|
cs = append(cs, args...)
|
||||||
|
cmd := exec.Command(os.Args[0], cs...)
|
||||||
|
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelperProcess(t *testing.T) {
|
||||||
|
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Strip out the leading args used to exec into this function.
|
||||||
|
gotCmd := os.Args[3]
|
||||||
|
gotArgs := os.Args[4:]
|
||||||
|
output, ok := execOutputs[gotCmd]
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintf(os.Stdout, "unexpected call cmd=%q args=%v\n", gotCmd, gotArgs)
|
||||||
|
os.Exit(1)
|
||||||
|
} else if !reflect.DeepEqual(output.args, gotArgs) {
|
||||||
|
fmt.Fprintf(os.Stdout, "call cmd=%q got args %v, want: %v\n", gotCmd, gotArgs, output.args)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stdout, output.output)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errEquiv(got, want error) bool {
|
||||||
|
if got == want {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if got != nil && want != nil {
|
||||||
|
return strings.Contains(got.Error(), want.Error())
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdTokenSource(t *testing.T) {
|
||||||
|
execCommand = fakeExec
|
||||||
|
fakeExpiry := time.Date(2016, 10, 31, 22, 31, 9, 123000000, time.UTC)
|
||||||
|
customFmt := "2006-01-02 15:04:05.999999999"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
gcpConfig map[string]string
|
||||||
|
tok *oauth2.Token
|
||||||
|
newErr, tokenErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"default",
|
||||||
|
map[string]string{
|
||||||
|
"cmd-path": "/default/no/args",
|
||||||
|
},
|
||||||
&oauth2.Token{
|
&oauth2.Token{
|
||||||
AccessToken: "faketoken",
|
AccessToken: "faketoken",
|
||||||
TokenType: "Bearer",
|
TokenType: "Bearer",
|
||||||
Expiry: fakeExpiry,
|
Expiry: fakeExpiry,
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default legacy args",
|
||||||
|
map[string]string{
|
||||||
|
"cmd-path": "/default/legacy/args arg1 arg2 arg3",
|
||||||
|
},
|
||||||
|
&oauth2.Token{
|
||||||
|
AccessToken: "faketoken",
|
||||||
|
TokenType: "Bearer",
|
||||||
|
Expiry: fakeExpiry,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"custom keys",
|
||||||
|
map[string]string{
|
||||||
|
"cmd-path": "/space in path/customkeys",
|
||||||
|
"cmd-args": "can haz auth",
|
||||||
|
"token-key": "{.token}",
|
||||||
|
"expiry-key": "{.token_expiry.datetime}",
|
||||||
|
"time-fmt": customFmt,
|
||||||
|
},
|
||||||
|
&oauth2.Token{
|
||||||
|
AccessToken: "faketoken",
|
||||||
|
TokenType: "Bearer",
|
||||||
|
Expiry: fakeExpiry,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"missing cmd",
|
"missing cmd",
|
||||||
nil,
|
map[string]string{
|
||||||
"", "", "", "",
|
"cmd-path": "",
|
||||||
|
},
|
||||||
nil,
|
nil,
|
||||||
fmt.Errorf("missing access token cmd"),
|
fmt.Errorf("missing access token cmd"),
|
||||||
|
nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"missing token-key",
|
"missing token-key",
|
||||||
[]byte(`{
|
map[string]string{
|
||||||
"broken": "faketoken",
|
"cmd-path": "missing/tokenkey/noargs",
|
||||||
"token_expiry": {
|
"token-key": "{.token}",
|
||||||
"datetime": "2016-10-31 22:31:09.123000000Z"
|
},
|
||||||
}
|
nil,
|
||||||
}`),
|
|
||||||
"/fake/cmd/path", "{.token}", "", "",
|
|
||||||
nil,
|
nil,
|
||||||
fmt.Errorf("error parsing token-key %q", "{.token}"),
|
fmt.Errorf("error parsing token-key %q", "{.token}"),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"missing expiry-key",
|
"missing expiry-key",
|
||||||
[]byte(`{
|
map[string]string{
|
||||||
"access_token": "faketoken",
|
"cmd-path": "missing/expirykey/legacyargs split on whitespace",
|
||||||
"expires": "2016-10-31T22:31:09.123000000Z"
|
"expiry-key": "{.expiry}",
|
||||||
}`),
|
},
|
||||||
"/fake/cmd/path", "", "{.expiry}", "",
|
nil,
|
||||||
nil,
|
nil,
|
||||||
fmt.Errorf("error parsing expiry-key %q", "{.expiry}"),
|
fmt.Errorf("error parsing expiry-key %q", "{.expiry}"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"invalid expiry timestamp",
|
"invalid expiry timestamp",
|
||||||
[]byte(`{
|
map[string]string{
|
||||||
"access_token": "faketoken",
|
"cmd-path": "invalid expiry/timestamp",
|
||||||
"token_expiry": "sometime soon, idk"
|
"cmd-args": "foo --bar --baz=abc,def",
|
||||||
}`),
|
},
|
||||||
"/fake/cmd/path", "", "", "",
|
|
||||||
&oauth2.Token{
|
&oauth2.Token{
|
||||||
AccessToken: "faketoken",
|
AccessToken: "faketoken",
|
||||||
TokenType: "Bearer",
|
TokenType: "Bearer",
|
||||||
Expiry: time.Time{},
|
Expiry: time.Time{},
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bad JSON",
|
"bad JSON",
|
||||||
[]byte(`{
|
map[string]string{
|
||||||
"access_token": "faketoken",
|
"cmd-path": "badjson",
|
||||||
"token_expiry": "sometime soon, idk"
|
},
|
||||||
------
|
nil,
|
||||||
`),
|
|
||||||
"/fake/cmd", "", "", "",
|
|
||||||
nil,
|
nil,
|
||||||
fmt.Errorf("invalid character '-' after object key:value pair"),
|
fmt.Errorf("invalid character '-' after object key:value pair"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
ts, err := newCmdTokenSource(tc.cmd, tc.tokenKey, tc.expiryKey, tc.timeFmt)
|
provider, err := newGCPAuthProvider("", tc.gcpConfig, nil /* persister */)
|
||||||
if err != nil {
|
if !errEquiv(err, tc.newErr) {
|
||||||
if !strings.Contains(err.Error(), tc.expectErr.Error()) {
|
t.Errorf("%q newGCPAuthProvider error: got %v, want %v", tc.name, err, tc.newErr)
|
||||||
t.Errorf("%s newCmdTokenSource error: %v, want %v", tc.name, err, tc.expectErr)
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tok, err := ts.parseTokenCmdOutput(tc.output)
|
if err != nil {
|
||||||
|
continue
|
||||||
if err != tc.expectErr && !strings.Contains(err.Error(), tc.expectErr.Error()) {
|
}
|
||||||
t.Errorf("%s parseCmdTokenSource error: %v, want %v", tc.name, err, tc.expectErr)
|
ts := provider.(*gcpAuthProvider).tokenSource.(*cachedTokenSource).source.(*commandTokenSource)
|
||||||
|
wantCmd = append([]string{ts.cmd}, ts.args...)
|
||||||
|
tok, err := ts.Token()
|
||||||
|
if !errEquiv(err, tc.tokenErr) {
|
||||||
|
t.Errorf("%q Token() error: got %v, want %v", tc.name, err, tc.tokenErr)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(tok, tc.tok) {
|
if !reflect.DeepEqual(tok, tc.tok) {
|
||||||
t.Errorf("%s got token %v, want %v", tc.name, tok, tc.tok)
|
t.Errorf("%q Token() got %v, want %v", tc.name, tok, tc.tok)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,6 +274,7 @@ func (f *fakePersister) Persist(cache map[string]string) error {
|
|||||||
func (f *fakePersister) read() map[string]string {
|
func (f *fakePersister) read() map[string]string {
|
||||||
ret := map[string]string{}
|
ret := map[string]string{}
|
||||||
f.lk.Lock()
|
f.lk.Lock()
|
||||||
|
defer f.lk.Unlock()
|
||||||
for k, v := range f.cache {
|
for k, v := range f.cache {
|
||||||
ret[k] = v
|
ret[k] = v
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user