mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 19:01:49 +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/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
"k8s.io/apiserver/pkg/warning"
|
||||||
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
|
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
|
||||||
authenticationvalidation "k8s.io/kubernetes/pkg/apis/authentication/validation"
|
authenticationvalidation "k8s.io/kubernetes/pkg/apis/authentication/validation"
|
||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
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) {
|
func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||||
if createValidation != nil {
|
req := obj.(*authenticationapi.TokenRequest)
|
||||||
if err := createValidation(ctx, obj.DeepCopyObject()); err != nil {
|
|
||||||
return nil, err
|
// 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)
|
// require name/namespace in the body to match URL if specified
|
||||||
|
if len(req.Name) > 0 && req.Name != name {
|
||||||
if errs := authenticationvalidation.ValidateTokenRequest(out); len(errs) != 0 {
|
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(), "", errs)
|
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{})
|
svcacctObj, err := r.svcaccts.Get(ctx, name, &metav1.GetOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
svcacct := svcacctObj.(*api.ServiceAccount)
|
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 (
|
var (
|
||||||
pod *api.Pod
|
pod *api.Pod
|
||||||
secret *api.Secret
|
secret *api.Secret
|
||||||
)
|
)
|
||||||
|
|
||||||
if ref := out.Spec.BoundObjectRef; ref != nil {
|
if ref := req.Spec.BoundObjectRef; ref != nil {
|
||||||
var uid types.UID
|
var uid types.UID
|
||||||
|
|
||||||
gvk := schema.FromAPIVersionAndKind(ref.APIVersion, ref.Kind)
|
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))
|
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
|
//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.
|
// 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.
|
// Fail after hard-coded extended expiration time.
|
||||||
// Only perform the extension when token is pod-bound.
|
// Only perform the extension when token is pod-bound.
|
||||||
var warnAfter int64
|
var warnAfter int64
|
||||||
exp := out.Spec.ExpirationSeconds
|
exp := req.Spec.ExpirationSeconds
|
||||||
if r.extendExpiration && pod != nil && out.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds && r.isKubeAudiences(out.Spec.Audiences) {
|
if r.extendExpiration && pod != nil && req.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds && r.isKubeAudiences(req.Spec.Audiences) {
|
||||||
warnAfter = exp
|
warnAfter = exp
|
||||||
exp = token.ExpirationExtensionSeconds
|
exp = token.ExpirationExtensionSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current time before building the token, to make sure the expiration
|
sc, pc := token.Claims(*svcacct, pod, secret, exp, warnAfter, req.Spec.Audiences)
|
||||||
// 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)
|
|
||||||
tokdata, err := r.issuer.GenerateToken(sc, pc)
|
tokdata, err := r.issuer.GenerateToken(sc, pc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to generate token: %v", err)
|
return nil, fmt.Errorf("failed to generate token: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// populate status
|
||||||
|
out := req.DeepCopy()
|
||||||
out.Status = authenticationapi.TokenRequestStatus{
|
out.Status = authenticationapi.TokenRequestStatus{
|
||||||
Token: tokdata,
|
Token: tokdata,
|
||||||
ExpirationTimestamp: metav1.Time{Time: nowTime.Add(time.Duration(out.Spec.ExpirationSeconds) * time.Second)},
|
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("pods", "pods/attach", "pods/proxy", "pods/exec", "pods/portforward").RuleOrDie(),
|
||||||
rbacv1helpers.NewRule(Write...).Groups(legacyGroup).Resources("replicationcontrollers", "replicationcontrollers/scale", "serviceaccounts",
|
rbacv1helpers.NewRule(Write...).Groups(legacyGroup).Resources("replicationcontrollers", "replicationcontrollers/scale", "serviceaccounts",
|
||||||
"services", "services/proxy", "persistentvolumeclaims", "configmaps", "secrets", "events").RuleOrDie(),
|
"services", "services/proxy", "persistentvolumeclaims", "configmaps", "secrets", "events").RuleOrDie(),
|
||||||
|
rbacv1helpers.NewRule("create").Groups(legacyGroup).Resources("serviceaccounts/token").RuleOrDie(),
|
||||||
|
|
||||||
rbacv1helpers.NewRule(Write...).Groups(appsGroup).Resources(
|
rbacv1helpers.NewRule(Write...).Groups(appsGroup).Resources(
|
||||||
"statefulsets", "statefulsets/scale",
|
"statefulsets", "statefulsets/scale",
|
||||||
|
@ -142,6 +142,12 @@ items:
|
|||||||
- deletecollection
|
- deletecollection
|
||||||
- patch
|
- patch
|
||||||
- update
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- serviceaccounts/token
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- apps
|
- apps
|
||||||
resources:
|
resources:
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -126,7 +127,11 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
|||||||
instanceConfig, _, closeFn := framework.RunAnAPIServer(controlPlaneConfig)
|
instanceConfig, _, closeFn := framework.RunAnAPIServer(controlPlaneConfig)
|
||||||
defer closeFn()
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
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 {
|
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)
|
t.Fatalf("expected err creating token for nonexistant svcacct but got: %#v", resp)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
sa, delSvcAcct := createDeleteSvcAcct(t, cs, sa)
|
sa, delSvcAcct := createDeleteSvcAcct(t, cs, sa)
|
||||||
defer delSvcAcct()
|
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{})
|
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
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, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
||||||
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
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 {
|
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)
|
t.Fatalf("expected err creating token for nonexistant svcacct but got: %#v", resp)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
sa, del := createDeleteSvcAcct(t, cs, sa)
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
||||||
defer del()
|
defer del()
|
||||||
|
|
||||||
|
warningHandler.clear()
|
||||||
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err == nil {
|
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)
|
t.Fatalf("expected err creating token bound to nonexistant pod but got: %#v", resp)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
pod, delPod := createDeletePod(t, cs, pod)
|
pod, delPod := createDeletePod(t, cs, pod)
|
||||||
defer delPod()
|
defer delPod()
|
||||||
|
|
||||||
// right uid
|
// right uid
|
||||||
treq.Spec.BoundObjectRef.UID = pod.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 {
|
if _, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
// wrong uid
|
// wrong uid
|
||||||
treq.Spec.BoundObjectRef.UID = wrongUID
|
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 {
|
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)
|
t.Fatalf("expected err creating token bound to pod with wrong uid but got: %#v", resp)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
// no uid
|
// no uid
|
||||||
treq.Spec.BoundObjectRef.UID = noUID
|
treq.Spec.BoundObjectRef.UID = noUID
|
||||||
|
warningHandler.clear()
|
||||||
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
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, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
||||||
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
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 {
|
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)
|
t.Fatalf("expected err creating token for nonexistant svcacct but got: %#v", resp)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
sa, del := createDeleteSvcAcct(t, cs, sa)
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
||||||
defer del()
|
defer del()
|
||||||
|
|
||||||
|
warningHandler.clear()
|
||||||
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err == nil {
|
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)
|
t.Fatalf("expected err creating token bound to nonexistant secret but got: %#v", resp)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
secret, delSecret := createDeleteSecret(t, cs, secret)
|
secret, delSecret := createDeleteSecret(t, cs, secret)
|
||||||
defer delSecret()
|
defer delSecret()
|
||||||
|
|
||||||
// right uid
|
// right uid
|
||||||
treq.Spec.BoundObjectRef.UID = secret.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 {
|
if _, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
// wrong uid
|
// wrong uid
|
||||||
treq.Spec.BoundObjectRef.UID = wrongUID
|
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 {
|
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)
|
t.Fatalf("expected err creating token bound to secret with wrong uid but got: %#v", resp)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
// no uid
|
// no uid
|
||||||
treq.Spec.BoundObjectRef.UID = noUID
|
treq.Spec.BoundObjectRef.UID = noUID
|
||||||
|
warningHandler.clear()
|
||||||
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
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, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
||||||
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
||||||
@ -341,9 +392,11 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
|||||||
_, del = createDeletePod(t, cs, otherpod)
|
_, del = createDeletePod(t, cs, otherpod)
|
||||||
defer del()
|
defer del()
|
||||||
|
|
||||||
|
warningHandler.clear()
|
||||||
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err == nil {
|
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)
|
t.Fatalf("expected err but got: %#v", resp)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("expired token", func(t *testing.T) {
|
t.Run("expired token", func(t *testing.T) {
|
||||||
@ -356,10 +409,12 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
|||||||
sa, del := createDeleteSvcAcct(t, cs, sa)
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
||||||
defer del()
|
defer del()
|
||||||
|
|
||||||
|
warningHandler.clear()
|
||||||
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
|
|
||||||
doTokenReview(t, cs, treq, false)
|
doTokenReview(t, cs, treq, false)
|
||||||
|
|
||||||
@ -405,10 +460,12 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
|||||||
defer delPod()
|
defer delPod()
|
||||||
treq.Spec.BoundObjectRef.UID = pod.UID
|
treq.Spec.BoundObjectRef.UID = pod.UID
|
||||||
|
|
||||||
|
warningHandler.clear()
|
||||||
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
|
|
||||||
doTokenReview(t, cs, treq, false)
|
doTokenReview(t, cs, treq, false)
|
||||||
|
|
||||||
@ -459,10 +516,12 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
|||||||
defer delPod()
|
defer delPod()
|
||||||
treq.Spec.BoundObjectRef.UID = pod.UID
|
treq.Spec.BoundObjectRef.UID = pod.UID
|
||||||
|
|
||||||
|
warningHandler.clear()
|
||||||
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
|
|
||||||
// Give some tolerance to avoid flakiness since we are using real time.
|
// Give some tolerance to avoid flakiness since we are using real time.
|
||||||
var leeway int64 = 10
|
var leeway int64 = 10
|
||||||
@ -499,10 +558,12 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
|||||||
sa, del := createDeleteSvcAcct(t, cs, sa)
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
||||||
defer del()
|
defer del()
|
||||||
|
|
||||||
|
warningHandler.clear()
|
||||||
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
|
|
||||||
doTokenReview(t, cs, treq, true)
|
doTokenReview(t, cs, treq, true)
|
||||||
})
|
})
|
||||||
@ -515,10 +576,12 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
|||||||
sa, del := createDeleteSvcAcct(t, cs, sa)
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
||||||
defer del()
|
defer del()
|
||||||
|
|
||||||
|
warningHandler.clear()
|
||||||
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
|
|
||||||
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
||||||
|
|
||||||
@ -543,9 +606,11 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
|||||||
defer originalDelPod()
|
defer originalDelPod()
|
||||||
|
|
||||||
treq.Spec.BoundObjectRef.UID = originalPod.UID
|
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 {
|
if treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
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, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
||||||
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
||||||
@ -584,9 +649,11 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
|||||||
defer originalDelSecret()
|
defer originalDelSecret()
|
||||||
|
|
||||||
treq.Spec.BoundObjectRef.UID = originalSecret.UID
|
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 {
|
if treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
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, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
||||||
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
||||||
@ -627,9 +694,11 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
|||||||
defer originalDelSecret()
|
defer originalDelSecret()
|
||||||
|
|
||||||
treq.Spec.BoundObjectRef.UID = originalSecret.UID
|
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 {
|
if treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
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, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
||||||
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
||||||
@ -671,9 +740,11 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
|||||||
defer originalDelSecret()
|
defer originalDelSecret()
|
||||||
|
|
||||||
treq.Spec.BoundObjectRef.UID = originalSecret.UID
|
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 {
|
if treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{}); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
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, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
||||||
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
||||||
@ -698,6 +769,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
|||||||
defer del()
|
defer del()
|
||||||
|
|
||||||
t.Log("get token")
|
t.Log("get token")
|
||||||
|
warningHandler.clear()
|
||||||
tokenRequest, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(
|
tokenRequest, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(
|
||||||
context.TODO(),
|
context.TODO(),
|
||||||
sa.Name,
|
sa.Name,
|
||||||
@ -709,6 +781,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error creating token: %v", err)
|
t.Fatalf("unexpected error creating token: %v", err)
|
||||||
}
|
}
|
||||||
|
warningHandler.assertEqual(t, nil)
|
||||||
token := tokenRequest.Status.Token
|
token := tokenRequest.Status.Token
|
||||||
if token == "" {
|
if token == "" {
|
||||||
t.Fatal("no token")
|
t.Fatal("no token")
|
||||||
@ -972,3 +1045,29 @@ func (f *fakeIndexer) GetByKey(key string) (interface{}, bool, error) {
|
|||||||
obj, err := f.get(namespace, name)
|
obj, err := f.get(namespace, name)
|
||||||
return obj, err == nil, err
|
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