mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-19 18:02:01 +00:00
Merge pull request #107880 from liggitt/kubectl-auth-token
Add command to request a bound service account token
This commit is contained in:
commit
e74c42aaf2
@ -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)},
|
||||
|
@ -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",
|
||||
|
@ -142,6 +142,12 @@ items:
|
||||
- deletecollection
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- serviceaccounts/token
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user