mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-31 07:20:13 +00:00
Add command to request a bound service account token
This commit is contained in:
parent
42c93b058e
commit
fca9b1d9fc
@ -41,6 +41,7 @@ require (
|
|||||||
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65
|
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65
|
||||||
k8s.io/metrics v0.0.0
|
k8s.io/metrics v0.0.0
|
||||||
k8s.io/utils v0.0.0-20211208161948-7d6a63dca704
|
k8s.io/utils v0.0.0-20211208161948-7d6a63dca704
|
||||||
|
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2
|
||||||
sigs.k8s.io/kustomize/kustomize/v4 v4.4.1
|
sigs.k8s.io/kustomize/kustomize/v4 v4.4.1
|
||||||
sigs.k8s.io/kustomize/kyaml v0.13.0
|
sigs.k8s.io/kustomize/kyaml v0.13.0
|
||||||
sigs.k8s.io/yaml v1.2.0
|
sigs.k8s.io/yaml v1.2.0
|
||||||
|
@ -153,6 +153,7 @@ func NewCmdCreate(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cob
|
|||||||
cmd.AddCommand(NewCmdCreateJob(f, ioStreams))
|
cmd.AddCommand(NewCmdCreateJob(f, ioStreams))
|
||||||
cmd.AddCommand(NewCmdCreateCronJob(f, ioStreams))
|
cmd.AddCommand(NewCmdCreateCronJob(f, ioStreams))
|
||||||
cmd.AddCommand(NewCmdCreateIngress(f, ioStreams))
|
cmd.AddCommand(NewCmdCreateIngress(f, ioStreams))
|
||||||
|
cmd.AddCommand(NewCmdCreateToken(f, ioStreams))
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
263
staging/src/k8s.io/kubectl/pkg/cmd/create/create_token.go
Normal file
263
staging/src/k8s.io/kubectl/pkg/cmd/create/create_token.go
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 create
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
authenticationv1 "k8s.io/api/authentication/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
"k8s.io/kubectl/pkg/util"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
"k8s.io/kubectl/pkg/util/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenOptions is the data required to perform a token request operation.
|
||||||
|
type TokenOptions struct {
|
||||||
|
// PrintFlags holds options necessary for obtaining a printer
|
||||||
|
PrintFlags *genericclioptions.PrintFlags
|
||||||
|
PrintObj func(obj runtime.Object) error
|
||||||
|
|
||||||
|
// Name and namespace of service account to create a token for
|
||||||
|
Name string
|
||||||
|
Namespace string
|
||||||
|
|
||||||
|
// BoundObjectKind is the kind of object to bind the token to. Optional. Can be Pod or Secret.
|
||||||
|
BoundObjectKind string
|
||||||
|
// BoundObjectName is the name of the object to bind the token to. Required if BoundObjectKind is set.
|
||||||
|
BoundObjectName string
|
||||||
|
// BoundObjectUID is the uid of the object to bind the token to. If unset, defaults to the current uid of the bound object.
|
||||||
|
BoundObjectUID string
|
||||||
|
|
||||||
|
// Audiences indicate the valid audiences for the requested token. If unset, defaults to the Kubernetes API server audiences.
|
||||||
|
Audiences []string
|
||||||
|
|
||||||
|
// ExpirationSeconds is the requested token lifetime. Optional.
|
||||||
|
ExpirationSeconds int64
|
||||||
|
|
||||||
|
// CoreClient is the API client used to request the token. Required.
|
||||||
|
CoreClient corev1client.CoreV1Interface
|
||||||
|
|
||||||
|
// IOStreams are the output streams for the operation. Required.
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
tokenLong = templates.LongDesc(`Request a service account token.`)
|
||||||
|
|
||||||
|
tokenExample = templates.Examples(`
|
||||||
|
# Request a token to authenticate to the kube-apiserver as the service account "myapp" in the current namespace
|
||||||
|
kubectl create token myapp
|
||||||
|
|
||||||
|
# Request a token for a service account in a custom namespace
|
||||||
|
kubectl create token myapp --namespace myns
|
||||||
|
|
||||||
|
# Request a token with a custom expiration
|
||||||
|
kubectl create token myapp --expiration-seconds 600
|
||||||
|
|
||||||
|
# Request a token with a custom audience
|
||||||
|
kubectl create token myapp --audience https://example.com
|
||||||
|
|
||||||
|
# Request a token bound to an instance of a Secret object
|
||||||
|
kubectl create token myapp --bound-object-kind Secret --bound-object-name mysecret
|
||||||
|
|
||||||
|
# Request a token bound to an instance of a Secret object with a specific uid
|
||||||
|
kubectl create token myapp --bound-object-kind Secret --bound-object-name mysecret --bound-object-uid 0d4691ed-659b-4935-a832-355f77ee47cc
|
||||||
|
`)
|
||||||
|
|
||||||
|
boundObjectKindToAPIVersion = map[string]string{
|
||||||
|
"Pod": "v1",
|
||||||
|
"Secret": "v1",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewTokenOpts(ioStreams genericclioptions.IOStreams) *TokenOptions {
|
||||||
|
return &TokenOptions{
|
||||||
|
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||||
|
IOStreams: ioStreams,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCmdCreateToken returns an initialized Command for 'create token' sub command
|
||||||
|
func NewCmdCreateToken(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
o := NewTokenOpts(ioStreams)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "token SERVICE_ACCOUNT_NAME",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: "Request a service account token",
|
||||||
|
Long: tokenLong,
|
||||||
|
Example: tokenExample,
|
||||||
|
ValidArgsFunction: util.ResourceNameCompletionFunc(f, "serviceaccount"),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := o.Complete(f, cmd, args); err != nil {
|
||||||
|
cmdutil.CheckErr(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := o.Validate(); err != nil {
|
||||||
|
cmdutil.CheckErr(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := o.Run(); err != nil {
|
||||||
|
cmdutil.CheckErr(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
o.PrintFlags.AddFlags(cmd)
|
||||||
|
|
||||||
|
cmd.Flags().StringArrayVar(&o.Audiences, "audience", o.Audiences, "Audience of the requested token. If unset, defaults to requesting a token for use with the Kubernetes API server. May be repeated to request a token valid for multiple audiences.")
|
||||||
|
|
||||||
|
cmd.Flags().Int64Var(&o.ExpirationSeconds, "expiration-seconds", o.ExpirationSeconds, "Requested lifetime of the issued token. The server may return a token with a longer or shorter lifetime.")
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&o.BoundObjectKind, "bound-object-kind", o.BoundObjectKind, "Kind of an object to bind the token to. "+
|
||||||
|
"Supported kinds are "+strings.Join(sets.StringKeySet(boundObjectKindToAPIVersion).List(), ", ")+". "+
|
||||||
|
"If set, --bound-object-name must be provided.")
|
||||||
|
cmd.Flags().StringVar(&o.BoundObjectName, "bound-object-name", o.BoundObjectName, "Name of an object to bind the token to. "+
|
||||||
|
"The token will expire when the object is deleted. "+
|
||||||
|
"Requires --bound-object-kind.")
|
||||||
|
cmd.Flags().StringVar(&o.BoundObjectUID, "bound-object-uid", o.BoundObjectUID, "UID of an object to bind the token to. "+
|
||||||
|
"Requires --bound-object-kind and --bound-object-name. "+
|
||||||
|
"If unset, the UID of the existing object is used.")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete completes all the required options
|
||||||
|
func (o *TokenOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
o.Name, err = NameFromCommandArgs(cmd, args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := f.KubernetesClientSet()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.CoreClient = client.CoreV1()
|
||||||
|
|
||||||
|
printer, err := o.PrintFlags.ToPrinter()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.PrintObj = func(obj runtime.Object) error {
|
||||||
|
return printer.PrintObj(obj, o.Out)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate makes sure provided values for TokenOptions are valid
|
||||||
|
func (o *TokenOptions) Validate() error {
|
||||||
|
if o.CoreClient == nil {
|
||||||
|
return fmt.Errorf("no client provided")
|
||||||
|
}
|
||||||
|
if len(o.Name) == 0 {
|
||||||
|
return fmt.Errorf("service account name is required")
|
||||||
|
}
|
||||||
|
if len(o.Namespace) == 0 {
|
||||||
|
return fmt.Errorf("--namespace is required")
|
||||||
|
}
|
||||||
|
if o.ExpirationSeconds < 0 {
|
||||||
|
return fmt.Errorf("--expiration-seconds must be positive")
|
||||||
|
}
|
||||||
|
for _, aud := range o.Audiences {
|
||||||
|
if len(aud) == 0 {
|
||||||
|
return fmt.Errorf("--audience must not be an empty string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(o.BoundObjectKind) == 0 {
|
||||||
|
if len(o.BoundObjectName) > 0 {
|
||||||
|
return fmt.Errorf("--bound-object-name can only be set if --bound-object-kind is provided")
|
||||||
|
}
|
||||||
|
if len(o.BoundObjectUID) > 0 {
|
||||||
|
return fmt.Errorf("--bound-object-uid can only be set if --bound-object-kind is provided")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, ok := boundObjectKindToAPIVersion[o.BoundObjectKind]; !ok {
|
||||||
|
return fmt.Errorf("supported --bound-object-kind values are %s", strings.Join(sets.StringKeySet(boundObjectKindToAPIVersion).List(), ", "))
|
||||||
|
}
|
||||||
|
if len(o.BoundObjectName) == 0 {
|
||||||
|
return fmt.Errorf("--bound-object-name is required if --bound-object-kind is provided")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run requests a token
|
||||||
|
func (o *TokenOptions) Run() error {
|
||||||
|
request := &authenticationv1.TokenRequest{
|
||||||
|
Spec: authenticationv1.TokenRequestSpec{
|
||||||
|
Audiences: o.Audiences,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if o.ExpirationSeconds > 0 {
|
||||||
|
request.Spec.ExpirationSeconds = &o.ExpirationSeconds
|
||||||
|
}
|
||||||
|
if len(o.BoundObjectKind) > 0 {
|
||||||
|
request.Spec.BoundObjectRef = &authenticationv1.BoundObjectReference{
|
||||||
|
Kind: o.BoundObjectKind,
|
||||||
|
APIVersion: boundObjectKindToAPIVersion[o.BoundObjectKind],
|
||||||
|
Name: o.BoundObjectName,
|
||||||
|
UID: types.UID(o.BoundObjectUID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := o.CoreClient.ServiceAccounts(o.Namespace).CreateToken(context.TODO(), o.Name, request, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create token: %v", err)
|
||||||
|
}
|
||||||
|
if len(response.Status.Token) == 0 {
|
||||||
|
return fmt.Errorf("failed to create token: no token in server response")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.PrintFlags.OutputFlagSpecified() {
|
||||||
|
return o.PrintObj(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
if term.IsTerminal(o.Out) {
|
||||||
|
// include a newline when printing interactively
|
||||||
|
fmt.Fprintf(o.Out, "%s\n", response.Status.Token)
|
||||||
|
} else {
|
||||||
|
// otherwise just print the token
|
||||||
|
fmt.Fprintf(o.Out, "%s", response.Status.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
330
staging/src/k8s.io/kubectl/pkg/cmd/create/create_token_test.go
Normal file
330
staging/src/k8s.io/kubectl/pkg/cmd/create/create_token_test.go
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 create
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"k8s.io/utils/pointer"
|
||||||
|
kjson "sigs.k8s.io/json"
|
||||||
|
|
||||||
|
authenticationv1 "k8s.io/api/authentication/v1"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/client-go/rest/fake"
|
||||||
|
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateToken(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
test string
|
||||||
|
|
||||||
|
name string
|
||||||
|
namespace string
|
||||||
|
output string
|
||||||
|
boundObjectKind string
|
||||||
|
boundObjectName string
|
||||||
|
boundObjectUID string
|
||||||
|
audiences []string
|
||||||
|
expirationSeconds int
|
||||||
|
|
||||||
|
serverResponseToken string
|
||||||
|
serverResponseError string
|
||||||
|
|
||||||
|
expectRequestPath string
|
||||||
|
expectTokenRequest *authenticationv1.TokenRequest
|
||||||
|
|
||||||
|
expectStdout string
|
||||||
|
expectStderr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
test: "simple",
|
||||||
|
name: "mysa",
|
||||||
|
|
||||||
|
expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
|
||||||
|
expectTokenRequest: &authenticationv1.TokenRequest{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
|
||||||
|
},
|
||||||
|
serverResponseToken: "abc",
|
||||||
|
expectStdout: "abc",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
test: "custom namespace",
|
||||||
|
name: "custom-sa",
|
||||||
|
namespace: "custom-ns",
|
||||||
|
|
||||||
|
expectRequestPath: "/api/v1/namespaces/custom-ns/serviceaccounts/custom-sa/token",
|
||||||
|
expectTokenRequest: &authenticationv1.TokenRequest{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
|
||||||
|
},
|
||||||
|
serverResponseToken: "abc",
|
||||||
|
expectStdout: "abc",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
test: "yaml",
|
||||||
|
name: "mysa",
|
||||||
|
output: "yaml",
|
||||||
|
|
||||||
|
expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
|
||||||
|
expectTokenRequest: &authenticationv1.TokenRequest{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
|
||||||
|
},
|
||||||
|
serverResponseToken: "abc",
|
||||||
|
expectStdout: `apiVersion: authentication.k8s.io/v1
|
||||||
|
kind: TokenRequest
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
spec:
|
||||||
|
audiences: null
|
||||||
|
boundObjectRef: null
|
||||||
|
expirationSeconds: null
|
||||||
|
status:
|
||||||
|
expirationTimestamp: null
|
||||||
|
token: abc
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
test: "bad bound object kind",
|
||||||
|
name: "mysa",
|
||||||
|
boundObjectKind: "Foo",
|
||||||
|
expectStderr: `error: supported --bound-object-kind values are Pod, Secret`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "missing bound object name",
|
||||||
|
name: "mysa",
|
||||||
|
boundObjectKind: "Pod",
|
||||||
|
expectStderr: `error: --bound-object-name is required if --bound-object-kind is provided`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "invalid bound object name",
|
||||||
|
name: "mysa",
|
||||||
|
boundObjectName: "mypod",
|
||||||
|
expectStderr: `error: --bound-object-name can only be set if --bound-object-kind is provided`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "invalid bound object uid",
|
||||||
|
name: "mysa",
|
||||||
|
boundObjectUID: "myuid",
|
||||||
|
expectStderr: `error: --bound-object-uid can only be set if --bound-object-kind is provided`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "valid bound object",
|
||||||
|
name: "mysa",
|
||||||
|
|
||||||
|
boundObjectKind: "Pod",
|
||||||
|
boundObjectName: "mypod",
|
||||||
|
boundObjectUID: "myuid",
|
||||||
|
|
||||||
|
expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
|
||||||
|
expectTokenRequest: &authenticationv1.TokenRequest{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
|
||||||
|
Spec: authenticationv1.TokenRequestSpec{
|
||||||
|
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
||||||
|
Kind: "Pod",
|
||||||
|
APIVersion: "v1",
|
||||||
|
Name: "mypod",
|
||||||
|
UID: "myuid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serverResponseToken: "abc",
|
||||||
|
expectStdout: "abc",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
test: "invalid audience",
|
||||||
|
name: "mysa",
|
||||||
|
audiences: []string{"test", "", "test2"},
|
||||||
|
expectStderr: `error: --audience must not be an empty string`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "valid audiences",
|
||||||
|
name: "mysa",
|
||||||
|
|
||||||
|
audiences: []string{"test,value1", "test,value2"},
|
||||||
|
|
||||||
|
expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
|
||||||
|
expectTokenRequest: &authenticationv1.TokenRequest{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
|
||||||
|
Spec: authenticationv1.TokenRequestSpec{
|
||||||
|
Audiences: []string{"test,value1", "test,value2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serverResponseToken: "abc",
|
||||||
|
expectStdout: "abc",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
test: "invalid expiration",
|
||||||
|
name: "mysa",
|
||||||
|
expirationSeconds: -1,
|
||||||
|
expectStderr: `error: --expiration-seconds must be positive`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "valid expiration",
|
||||||
|
name: "mysa",
|
||||||
|
|
||||||
|
expirationSeconds: 1000,
|
||||||
|
|
||||||
|
expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
|
||||||
|
expectTokenRequest: &authenticationv1.TokenRequest{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
|
||||||
|
Spec: authenticationv1.TokenRequestSpec{
|
||||||
|
ExpirationSeconds: pointer.Int64(1000),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serverResponseToken: "abc",
|
||||||
|
expectStdout: "abc",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
test: "server error",
|
||||||
|
name: "mysa",
|
||||||
|
|
||||||
|
expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
|
||||||
|
expectTokenRequest: &authenticationv1.TokenRequest{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
|
||||||
|
},
|
||||||
|
serverResponseError: "bad bad request",
|
||||||
|
expectStderr: `error: failed to create token: "bad bad request" is invalid`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "server missing token",
|
||||||
|
name: "mysa",
|
||||||
|
|
||||||
|
expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
|
||||||
|
expectTokenRequest: &authenticationv1.TokenRequest{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
|
||||||
|
},
|
||||||
|
serverResponseToken: "",
|
||||||
|
expectStderr: `error: failed to create token: no token in server response`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.test, func(t *testing.T) {
|
||||||
|
defer cmdutil.DefaultBehaviorOnFatal()
|
||||||
|
sawError := ""
|
||||||
|
cmdutil.BehaviorOnFatal(func(str string, code int) {
|
||||||
|
sawError = str
|
||||||
|
})
|
||||||
|
|
||||||
|
namespace := "test"
|
||||||
|
if test.namespace != "" {
|
||||||
|
namespace = test.namespace
|
||||||
|
}
|
||||||
|
tf := cmdtesting.NewTestFactory().WithNamespace(namespace)
|
||||||
|
defer tf.Cleanup()
|
||||||
|
|
||||||
|
tf.Client = &fake.RESTClient{}
|
||||||
|
|
||||||
|
var code int
|
||||||
|
var body []byte
|
||||||
|
if len(test.serverResponseError) > 0 {
|
||||||
|
code = 422
|
||||||
|
response := apierrors.NewInvalid(schema.GroupKind{Group: "", Kind: ""}, test.serverResponseError, nil)
|
||||||
|
response.ErrStatus.APIVersion = "v1"
|
||||||
|
response.ErrStatus.Kind = "Status"
|
||||||
|
body, _ = json.Marshal(response.ErrStatus)
|
||||||
|
} else {
|
||||||
|
code = 200
|
||||||
|
response := authenticationv1.TokenRequest{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
APIVersion: "authentication.k8s.io/v1",
|
||||||
|
Kind: "TokenRequest",
|
||||||
|
},
|
||||||
|
Status: authenticationv1.TokenRequestStatus{Token: test.serverResponseToken},
|
||||||
|
}
|
||||||
|
body, _ = json.Marshal(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := scheme.Codecs.WithoutConversion()
|
||||||
|
var tokenRequest *authenticationv1.TokenRequest
|
||||||
|
tf.Client = &fake.RESTClient{
|
||||||
|
NegotiatedSerializer: ns,
|
||||||
|
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||||
|
if req.URL.Path != test.expectRequestPath {
|
||||||
|
t.Fatalf("expected %q, got %q", test.expectRequestPath, req.URL.Path)
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tokenRequest = &authenticationv1.TokenRequest{}
|
||||||
|
if strictErrs, err := kjson.UnmarshalStrict(data, tokenRequest); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if len(strictErrs) > 0 {
|
||||||
|
t.Fatal(strictErrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: code,
|
||||||
|
Body: ioutil.NopCloser(bytes.NewBuffer(body)),
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||||
|
|
||||||
|
ioStreams, _, stdout, _ := genericclioptions.NewTestIOStreams()
|
||||||
|
cmd := NewCmdCreateToken(tf, ioStreams)
|
||||||
|
if test.output != "" {
|
||||||
|
cmd.Flags().Set("output", test.output)
|
||||||
|
}
|
||||||
|
if test.boundObjectKind != "" {
|
||||||
|
cmd.Flags().Set("bound-object-kind", test.boundObjectKind)
|
||||||
|
}
|
||||||
|
if test.boundObjectName != "" {
|
||||||
|
cmd.Flags().Set("bound-object-name", test.boundObjectName)
|
||||||
|
}
|
||||||
|
if test.boundObjectUID != "" {
|
||||||
|
cmd.Flags().Set("bound-object-uid", test.boundObjectUID)
|
||||||
|
}
|
||||||
|
for _, aud := range test.audiences {
|
||||||
|
cmd.Flags().Set("audience", aud)
|
||||||
|
}
|
||||||
|
if test.expirationSeconds != 0 {
|
||||||
|
cmd.Flags().Set("expiration-seconds", strconv.Itoa(test.expirationSeconds))
|
||||||
|
}
|
||||||
|
cmd.Run(cmd, []string{test.name})
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(tokenRequest, test.expectTokenRequest) {
|
||||||
|
t.Fatalf("unexpected request:\n%s", cmp.Diff(test.expectTokenRequest, tokenRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
if stdout.String() != test.expectStdout {
|
||||||
|
t.Errorf("unexpected stdout:\n%s", cmp.Diff(test.expectStdout, stdout.String()))
|
||||||
|
}
|
||||||
|
if sawError != test.expectStderr {
|
||||||
|
t.Errorf("unexpected stderr:\n%s", cmp.Diff(test.expectStderr, sawError))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user