Merge pull request #107880 from liggitt/kubectl-auth-token

Add command to request a bound service account token
This commit is contained in:
Kubernetes Prow Robot 2022-02-09 14:10:01 -08:00 committed by GitHub
commit e74c42aaf2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 758 additions and 21 deletions

View File

@ -28,9 +28,11 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/authentication/authenticator"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/warning"
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
authenticationvalidation "k8s.io/kubernetes/pkg/apis/authentication/validation"
api "k8s.io/kubernetes/pkg/apis/core"
@ -62,30 +64,67 @@ var gvk = schema.GroupVersionKind{
}
func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
if createValidation != nil {
if err := createValidation(ctx, obj.DeepCopyObject()); err != nil {
return nil, err
}
req := obj.(*authenticationapi.TokenRequest)
// Get the namespace from the context (populated from the URL).
namespace, ok := genericapirequest.NamespaceFrom(ctx)
if !ok {
return nil, errors.NewBadRequest("namespace is required")
}
out := obj.(*authenticationapi.TokenRequest)
if errs := authenticationvalidation.ValidateTokenRequest(out); len(errs) != 0 {
return nil, errors.NewInvalid(gvk.GroupKind(), "", errs)
// require name/namespace in the body to match URL if specified
if len(req.Name) > 0 && req.Name != name {
errs := field.ErrorList{field.Invalid(field.NewPath("metadata").Child("name"), req.Name, "must match the service account name if specified")}
return nil, errors.NewInvalid(gvk.GroupKind(), name, errs)
}
if len(req.Namespace) > 0 && req.Namespace != namespace {
errs := field.ErrorList{field.Invalid(field.NewPath("metadata").Child("namespace"), req.Namespace, "must match the service account namespace if specified")}
return nil, errors.NewInvalid(gvk.GroupKind(), name, errs)
}
// Lookup service account
svcacctObj, err := r.svcaccts.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, err
}
svcacct := svcacctObj.(*api.ServiceAccount)
// Default unset spec audiences to API server audiences based on server config
if len(req.Spec.Audiences) == 0 {
req.Spec.Audiences = r.auds
}
// Populate metadata fields if not set
if len(req.Name) == 0 {
req.Name = svcacct.Name
}
if len(req.Namespace) == 0 {
req.Namespace = svcacct.Namespace
}
// Save current time before building the token, to make sure the expiration
// returned in TokenRequestStatus would be <= the exp field in token.
nowTime := time.Now()
req.CreationTimestamp = metav1.NewTime(nowTime)
// Clear status
req.Status = authenticationapi.TokenRequestStatus{}
// call static validation, then validating admission
if errs := authenticationvalidation.ValidateTokenRequest(req); len(errs) != 0 {
return nil, errors.NewInvalid(gvk.GroupKind(), "", errs)
}
if createValidation != nil {
if err := createValidation(ctx, obj.DeepCopyObject()); err != nil {
return nil, err
}
}
var (
pod *api.Pod
secret *api.Secret
)
if ref := out.Spec.BoundObjectRef; ref != nil {
if ref := req.Spec.BoundObjectRef; ref != nil {
var uid types.UID
gvk := schema.FromAPIVersionAndKind(ref.APIVersion, ref.Kind)
@ -116,13 +155,11 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object,
return nil, errors.NewConflict(schema.GroupResource{Group: gvk.Group, Resource: gvk.Kind}, ref.Name, fmt.Errorf("the UID in the bound object reference (%s) does not match the UID in record. The object might have been deleted and then recreated", ref.UID))
}
}
if len(out.Spec.Audiences) == 0 {
out.Spec.Audiences = r.auds
}
if r.maxExpirationSeconds > 0 && out.Spec.ExpirationSeconds > r.maxExpirationSeconds {
if r.maxExpirationSeconds > 0 && req.Spec.ExpirationSeconds > r.maxExpirationSeconds {
//only positive value is valid
out.Spec.ExpirationSeconds = r.maxExpirationSeconds
warning.AddWarning(ctx, "", fmt.Sprintf("requested expiration of %d seconds shortened to %d seconds", req.Spec.ExpirationSeconds, r.maxExpirationSeconds))
req.Spec.ExpirationSeconds = r.maxExpirationSeconds
}
// Tweak expiration for safe transition of projected service account token.
@ -130,21 +167,20 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object,
// Fail after hard-coded extended expiration time.
// Only perform the extension when token is pod-bound.
var warnAfter int64
exp := out.Spec.ExpirationSeconds
if r.extendExpiration && pod != nil && out.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds && r.isKubeAudiences(out.Spec.Audiences) {
exp := req.Spec.ExpirationSeconds
if r.extendExpiration && pod != nil && req.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds && r.isKubeAudiences(req.Spec.Audiences) {
warnAfter = exp
exp = token.ExpirationExtensionSeconds
}
// Save current time before building the token, to make sure the expiration
// returned in TokenRequestStatus would be earlier than exp field in token.
nowTime := time.Now()
sc, pc := token.Claims(*svcacct, pod, secret, exp, warnAfter, out.Spec.Audiences)
sc, pc := token.Claims(*svcacct, pod, secret, exp, warnAfter, req.Spec.Audiences)
tokdata, err := r.issuer.GenerateToken(sc, pc)
if err != nil {
return nil, fmt.Errorf("failed to generate token: %v", err)
}
// populate status
out := req.DeepCopy()
out.Status = authenticationapi.TokenRequestStatus{
Token: tokdata,
ExpirationTimestamp: metav1.Time{Time: nowTime.Add(time.Duration(out.Spec.ExpirationSeconds) * time.Second)},

View File

@ -286,6 +286,7 @@ func ClusterRoles() []rbacv1.ClusterRole {
rbacv1helpers.NewRule(Write...).Groups(legacyGroup).Resources("pods", "pods/attach", "pods/proxy", "pods/exec", "pods/portforward").RuleOrDie(),
rbacv1helpers.NewRule(Write...).Groups(legacyGroup).Resources("replicationcontrollers", "replicationcontrollers/scale", "serviceaccounts",
"services", "services/proxy", "persistentvolumeclaims", "configmaps", "secrets", "events").RuleOrDie(),
rbacv1helpers.NewRule("create").Groups(legacyGroup).Resources("serviceaccounts/token").RuleOrDie(),
rbacv1helpers.NewRule(Write...).Groups(appsGroup).Resources(
"statefulsets", "statefulsets/scale",

View File

@ -142,6 +142,12 @@ items:
- deletecollection
- patch
- update
- apiGroups:
- ""
resources:
- serviceaccounts/token
verbs:
- create
- apiGroups:
- apps
resources:

View File

@ -41,6 +41,7 @@ require (
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65
k8s.io/metrics v0.0.0
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/kyaml v0.13.0
sigs.k8s.io/yaml v1.2.0

View File

@ -153,6 +153,7 @@ func NewCmdCreate(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cob
cmd.AddCommand(NewCmdCreateJob(f, ioStreams))
cmd.AddCommand(NewCmdCreateCronJob(f, ioStreams))
cmd.AddCommand(NewCmdCreateIngress(f, ioStreams))
cmd.AddCommand(NewCmdCreateToken(f, ioStreams))
return cmd
}

View 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
}

View 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))
}
})
}
}

View File

@ -29,6 +29,7 @@ import (
"reflect"
"strconv"
"strings"
"sync"
"testing"
"time"
@ -126,7 +127,11 @@ func TestServiceAccountTokenCreate(t *testing.T) {
instanceConfig, _, closeFn := framework.RunAnAPIServer(controlPlaneConfig)
defer closeFn()
cs, err := clientset.NewForConfig(instanceConfig.GenericAPIServer.LoopbackClientConfig)
warningHandler := &recordingWarningHandler{}
configWithWarningHandler := rest.CopyConfig(instanceConfig.GenericAPIServer.LoopbackClientConfig)
configWithWarningHandler.WarningHandler = warningHandler
cs, err := clientset.NewForConfig(configWithWarningHandler)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -182,16 +187,42 @@ func TestServiceAccountTokenCreate(t *testing.T) {
},
}
warningHandler.clear()
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err == nil {
t.Fatalf("expected err creating token for nonexistant svcacct but got: %#v", resp)
}
warningHandler.assertEqual(t, nil)
sa, delSvcAcct := createDeleteSvcAcct(t, cs, sa)
defer delSvcAcct()
treqWithBadName := treq.DeepCopy()
treqWithBadName.Name = "invalid-name"
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treqWithBadName, metav1.CreateOptions{}); err == nil || !strings.Contains(err.Error(), "must match the service account name") {
t.Fatalf("expected err creating token with mismatched name but got: %#v", resp)
}
treqWithBadNamespace := treq.DeepCopy()
treqWithBadNamespace.Namespace = "invalid-namespace"
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treqWithBadNamespace, metav1.CreateOptions{}); err == nil || !strings.Contains(err.Error(), "must match the service account namespace") {
t.Fatalf("expected err creating token with mismatched namespace but got: %#v", resp)
}
warningHandler.clear()
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
if err != nil {
t.Fatalf("err: %v", err)
}
warningHandler.assertEqual(t, nil)
if treq.Name != sa.Name {
t.Errorf("expected name=%s, got %s", sa.Name, treq.Name)
}
if treq.Namespace != sa.Namespace {
t.Errorf("expected namespace=%s, got %s", sa.Namespace, treq.Namespace)
}
if treq.CreationTimestamp.IsZero() {
t.Errorf("expected non-zero creation timestamp")
}
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
@ -220,34 +251,44 @@ func TestServiceAccountTokenCreate(t *testing.T) {
},
}
warningHandler.clear()
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err == nil {
t.Fatalf("expected err creating token for nonexistant svcacct but got: %#v", resp)
}
warningHandler.assertEqual(t, nil)
sa, del := createDeleteSvcAcct(t, cs, sa)
defer del()
warningHandler.clear()
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err == nil {
t.Fatalf("expected err creating token bound to nonexistant pod but got: %#v", resp)
}
warningHandler.assertEqual(t, nil)
pod, delPod := createDeletePod(t, cs, pod)
defer delPod()
// right uid
treq.Spec.BoundObjectRef.UID = pod.UID
warningHandler.clear()
if _, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err != nil {
t.Fatalf("err: %v", err)
}
warningHandler.assertEqual(t, nil)
// wrong uid
treq.Spec.BoundObjectRef.UID = wrongUID
warningHandler.clear()
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err == nil {
t.Fatalf("expected err creating token bound to pod with wrong uid but got: %#v", resp)
}
warningHandler.assertEqual(t, nil)
// no uid
treq.Spec.BoundObjectRef.UID = noUID
warningHandler.clear()
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
if err != nil {
t.Fatalf("err: %v", err)
}
warningHandler.assertEqual(t, nil)
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
@ -283,34 +324,44 @@ func TestServiceAccountTokenCreate(t *testing.T) {
},
}
warningHandler.clear()
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err == nil {
t.Fatalf("expected err creating token for nonexistant svcacct but got: %#v", resp)
}
warningHandler.assertEqual(t, nil)
sa, del := createDeleteSvcAcct(t, cs, sa)
defer del()
warningHandler.clear()
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err == nil {
t.Fatalf("expected err creating token bound to nonexistant secret but got: %#v", resp)
}
warningHandler.assertEqual(t, nil)
secret, delSecret := createDeleteSecret(t, cs, secret)
defer delSecret()
// right uid
treq.Spec.BoundObjectRef.UID = secret.UID
warningHandler.clear()
if _, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err != nil {
t.Fatalf("err: %v", err)
}
warningHandler.assertEqual(t, nil)
// wrong uid
treq.Spec.BoundObjectRef.UID = wrongUID
warningHandler.clear()
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err == nil {
t.Fatalf("expected err creating token bound to secret with wrong uid but got: %#v", resp)
}
warningHandler.assertEqual(t, nil)
// no uid
treq.Spec.BoundObjectRef.UID = noUID
warningHandler.clear()
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
if err != nil {
t.Fatalf("err: %v", err)
}
warningHandler.assertEqual(t, nil)
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
@ -341,9 +392,11 @@ func TestServiceAccountTokenCreate(t *testing.T) {
_, del = createDeletePod(t, cs, otherpod)
defer del()
warningHandler.clear()
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err == nil {
t.Fatalf("expected err but got: %#v", resp)
}
warningHandler.assertEqual(t, nil)
})
t.Run("expired token", func(t *testing.T) {
@ -356,10 +409,12 @@ func TestServiceAccountTokenCreate(t *testing.T) {
sa, del := createDeleteSvcAcct(t, cs, sa)
defer del()
warningHandler.clear()
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
if err != nil {
t.Fatalf("err: %v", err)
}
warningHandler.assertEqual(t, nil)
doTokenReview(t, cs, treq, false)
@ -405,10 +460,12 @@ func TestServiceAccountTokenCreate(t *testing.T) {
defer delPod()
treq.Spec.BoundObjectRef.UID = pod.UID
warningHandler.clear()
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
if err != nil {
t.Fatalf("err: %v", err)
}
warningHandler.assertEqual(t, nil)
doTokenReview(t, cs, treq, false)
@ -459,10 +516,12 @@ func TestServiceAccountTokenCreate(t *testing.T) {
defer delPod()
treq.Spec.BoundObjectRef.UID = pod.UID
warningHandler.clear()
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
if err != nil {
t.Fatalf("err: %v", err)
}
warningHandler.assertEqual(t, nil)
// Give some tolerance to avoid flakiness since we are using real time.
var leeway int64 = 10
@ -499,10 +558,12 @@ func TestServiceAccountTokenCreate(t *testing.T) {
sa, del := createDeleteSvcAcct(t, cs, sa)
defer del()
warningHandler.clear()
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
if err != nil {
t.Fatalf("err: %v", err)
}
warningHandler.assertEqual(t, nil)
doTokenReview(t, cs, treq, true)
})
@ -515,10 +576,12 @@ func TestServiceAccountTokenCreate(t *testing.T) {
sa, del := createDeleteSvcAcct(t, cs, sa)
defer del()
warningHandler.clear()
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
if err != nil {
t.Fatalf("err: %v", err)
}
warningHandler.assertEqual(t, nil)
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
@ -543,9 +606,11 @@ func TestServiceAccountTokenCreate(t *testing.T) {
defer originalDelPod()
treq.Spec.BoundObjectRef.UID = originalPod.UID
warningHandler.clear()
if treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err != nil {
t.Fatalf("err: %v", err)
}
warningHandler.assertEqual(t, nil)
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
@ -584,9 +649,11 @@ func TestServiceAccountTokenCreate(t *testing.T) {
defer originalDelSecret()
treq.Spec.BoundObjectRef.UID = originalSecret.UID
warningHandler.clear()
if treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err != nil {
t.Fatalf("err: %v", err)
}
warningHandler.assertEqual(t, nil)
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
@ -627,9 +694,11 @@ func TestServiceAccountTokenCreate(t *testing.T) {
defer originalDelSecret()
treq.Spec.BoundObjectRef.UID = originalSecret.UID
warningHandler.clear()
if treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err != nil {
t.Fatalf("err: %v", err)
}
warningHandler.assertEqual(t, nil)
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
@ -671,9 +740,11 @@ func TestServiceAccountTokenCreate(t *testing.T) {
defer originalDelSecret()
treq.Spec.BoundObjectRef.UID = originalSecret.UID
warningHandler.clear()
if treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err != nil {
t.Fatalf("err: %v", err)
}
warningHandler.assertEqual(t, []string{fmt.Sprintf("requested expiration of %d seconds shortened to %d seconds", tooLongExpirationTime, maxExpirationSeconds)})
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
@ -698,6 +769,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
defer del()
t.Log("get token")
warningHandler.clear()
tokenRequest, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(
context.TODO(),
sa.Name,
@ -709,6 +781,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error creating token: %v", err)
}
warningHandler.assertEqual(t, nil)
token := tokenRequest.Status.Token
if token == "" {
t.Fatal("no token")
@ -972,3 +1045,29 @@ func (f *fakeIndexer) GetByKey(key string) (interface{}, bool, error) {
obj, err := f.get(namespace, name)
return obj, err == nil, err
}
type recordingWarningHandler struct {
warnings []string
sync.Mutex
}
func (r *recordingWarningHandler) HandleWarningHeader(code int, agent string, message string) {
r.Lock()
defer r.Unlock()
r.warnings = append(r.warnings, message)
}
func (r *recordingWarningHandler) clear() {
r.Lock()
defer r.Unlock()
r.warnings = nil
}
func (r *recordingWarningHandler) assertEqual(t *testing.T, expected []string) {
t.Helper()
r.Lock()
defer r.Unlock()
if !reflect.DeepEqual(r.warnings, expected) {
t.Errorf("expected\n\t%v\ngot\n\t%v", expected, r.warnings)
}
}