1
16
17 package create
18
19 import (
20 "context"
21 "fmt"
22 "os"
23 "strings"
24 "time"
25
26 "github.com/spf13/cobra"
27 "github.com/spf13/pflag"
28
29 authenticationv1 "k8s.io/api/authentication/v1"
30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 "k8s.io/apimachinery/pkg/runtime"
32 "k8s.io/apimachinery/pkg/types"
33 "k8s.io/apimachinery/pkg/util/sets"
34 "k8s.io/cli-runtime/pkg/genericclioptions"
35 "k8s.io/cli-runtime/pkg/genericiooptions"
36 corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
37 cmdutil "k8s.io/kubectl/pkg/cmd/util"
38 "k8s.io/kubectl/pkg/scheme"
39 "k8s.io/kubectl/pkg/util/completion"
40 "k8s.io/kubectl/pkg/util/templates"
41 "k8s.io/kubectl/pkg/util/term"
42 "k8s.io/utils/pointer"
43 )
44
45
46 type TokenOptions struct {
47
48 PrintFlags *genericclioptions.PrintFlags
49 PrintObj func(obj runtime.Object) error
50
51
52 Flags *pflag.FlagSet
53
54
55 Name string
56 Namespace string
57
58
59 BoundObjectKind string
60
61 BoundObjectName string
62
63 BoundObjectUID string
64
65
66 Audiences []string
67
68
69 Duration time.Duration
70
71
72 CoreClient corev1client.CoreV1Interface
73
74
75 genericiooptions.IOStreams
76 }
77
78 var (
79 tokenLong = templates.LongDesc(`Request a service account token.`)
80
81 tokenExample = templates.Examples(`
82 # Request a token to authenticate to the kube-apiserver as the service account "myapp" in the current namespace
83 kubectl create token myapp
84
85 # Request a token for a service account in a custom namespace
86 kubectl create token myapp --namespace myns
87
88 # Request a token with a custom expiration
89 kubectl create token myapp --duration 10m
90
91 # Request a token with a custom audience
92 kubectl create token myapp --audience https://example.com
93
94 # Request a token bound to an instance of a Secret object
95 kubectl create token myapp --bound-object-kind Secret --bound-object-name mysecret
96
97 # Request a token bound to an instance of a Secret object with a specific UID
98 kubectl create token myapp --bound-object-kind Secret --bound-object-name mysecret --bound-object-uid 0d4691ed-659b-4935-a832-355f77ee47cc
99 `)
100 )
101
102 func boundObjectKindToAPIVersions() map[string]string {
103 kinds := map[string]string{
104 "Pod": "v1",
105 "Secret": "v1",
106 }
107 if os.Getenv("KUBECTL_NODE_BOUND_TOKENS") == "true" {
108 kinds["Node"] = "v1"
109 }
110 return kinds
111 }
112
113 func NewTokenOpts(ioStreams genericiooptions.IOStreams) *TokenOptions {
114 return &TokenOptions{
115 PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
116 IOStreams: ioStreams,
117 }
118 }
119
120
121 func NewCmdCreateToken(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command {
122 o := NewTokenOpts(ioStreams)
123
124 cmd := &cobra.Command{
125 Use: "token SERVICE_ACCOUNT_NAME",
126 DisableFlagsInUseLine: true,
127 Short: "Request a service account token",
128 Long: tokenLong,
129 Example: tokenExample,
130 ValidArgsFunction: completion.ResourceNameCompletionFunc(f, "serviceaccount"),
131 Run: func(cmd *cobra.Command, args []string) {
132 if err := o.Complete(f, cmd, args); err != nil {
133 cmdutil.CheckErr(err)
134 return
135 }
136 if err := o.Validate(); err != nil {
137 cmdutil.CheckErr(err)
138 return
139 }
140 if err := o.Run(); err != nil {
141 cmdutil.CheckErr(err)
142 return
143 }
144 },
145 }
146
147 o.PrintFlags.AddFlags(cmd)
148
149 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.")
150
151 cmd.Flags().DurationVar(&o.Duration, "duration", o.Duration, "Requested lifetime of the issued token. If not set or if set to 0, the lifetime will be determined by the server automatically. The server may return a token with a longer or shorter lifetime.")
152
153 cmd.Flags().StringVar(&o.BoundObjectKind, "bound-object-kind", o.BoundObjectKind, "Kind of an object to bind the token to. "+
154 "Supported kinds are "+strings.Join(sets.StringKeySet(boundObjectKindToAPIVersions()).List(), ", ")+". "+
155 "If set, --bound-object-name must be provided.")
156 cmd.Flags().StringVar(&o.BoundObjectName, "bound-object-name", o.BoundObjectName, "Name of an object to bind the token to. "+
157 "The token will expire when the object is deleted. "+
158 "Requires --bound-object-kind.")
159 cmd.Flags().StringVar(&o.BoundObjectUID, "bound-object-uid", o.BoundObjectUID, "UID of an object to bind the token to. "+
160 "Requires --bound-object-kind and --bound-object-name. "+
161 "If unset, the UID of the existing object is used.")
162
163 o.Flags = cmd.Flags()
164
165 return cmd
166 }
167
168
169 func (o *TokenOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
170 var err error
171
172 o.Name, err = NameFromCommandArgs(cmd, args)
173 if err != nil {
174 return err
175 }
176
177 o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
178 if err != nil {
179 return err
180 }
181
182 client, err := f.KubernetesClientSet()
183 if err != nil {
184 return err
185 }
186 o.CoreClient = client.CoreV1()
187
188 printer, err := o.PrintFlags.ToPrinter()
189 if err != nil {
190 return err
191 }
192
193 o.PrintObj = func(obj runtime.Object) error {
194 return printer.PrintObj(obj, o.Out)
195 }
196
197 return nil
198 }
199
200
201 func (o *TokenOptions) Validate() error {
202 if o.CoreClient == nil {
203 return fmt.Errorf("no client provided")
204 }
205 if len(o.Name) == 0 {
206 return fmt.Errorf("service account name is required")
207 }
208 if len(o.Namespace) == 0 {
209 return fmt.Errorf("--namespace is required")
210 }
211 if o.Duration < 0 {
212 return fmt.Errorf("--duration must be greater than or equal to 0")
213 }
214 if o.Duration%time.Second != 0 {
215 return fmt.Errorf("--duration cannot be expressed in units less than seconds")
216 }
217 for _, aud := range o.Audiences {
218 if len(aud) == 0 {
219 return fmt.Errorf("--audience must not be an empty string")
220 }
221 }
222
223 if len(o.BoundObjectKind) == 0 {
224 if len(o.BoundObjectName) > 0 {
225 return fmt.Errorf("--bound-object-name can only be set if --bound-object-kind is provided")
226 }
227 if len(o.BoundObjectUID) > 0 {
228 return fmt.Errorf("--bound-object-uid can only be set if --bound-object-kind is provided")
229 }
230 } else {
231 if _, ok := boundObjectKindToAPIVersions()[o.BoundObjectKind]; !ok {
232 return fmt.Errorf("supported --bound-object-kind values are %s", strings.Join(sets.StringKeySet(boundObjectKindToAPIVersions()).List(), ", "))
233 }
234 if len(o.BoundObjectName) == 0 {
235 return fmt.Errorf("--bound-object-name is required if --bound-object-kind is provided")
236 }
237 }
238
239 return nil
240 }
241
242
243 func (o *TokenOptions) Run() error {
244 request := &authenticationv1.TokenRequest{
245 Spec: authenticationv1.TokenRequestSpec{
246 Audiences: o.Audiences,
247 },
248 }
249 if o.Duration > 0 {
250 request.Spec.ExpirationSeconds = pointer.Int64(int64(o.Duration / time.Second))
251 }
252 if len(o.BoundObjectKind) > 0 {
253 request.Spec.BoundObjectRef = &authenticationv1.BoundObjectReference{
254 Kind: o.BoundObjectKind,
255 APIVersion: boundObjectKindToAPIVersions()[o.BoundObjectKind],
256 Name: o.BoundObjectName,
257 UID: types.UID(o.BoundObjectUID),
258 }
259 }
260
261 response, err := o.CoreClient.ServiceAccounts(o.Namespace).CreateToken(context.TODO(), o.Name, request, metav1.CreateOptions{})
262 if err != nil {
263 return fmt.Errorf("failed to create token: %v", err)
264 }
265 if len(response.Status.Token) == 0 {
266 return fmt.Errorf("failed to create token: no token in server response")
267 }
268
269 if o.PrintFlags.OutputFlagSpecified() {
270 return o.PrintObj(response)
271 }
272
273 if term.IsTerminal(o.Out) {
274
275 fmt.Fprintf(o.Out, "%s\n", response.Status.Token)
276 } else {
277
278 fmt.Fprintf(o.Out, "%s", response.Status.Token)
279 }
280
281 return nil
282 }
283
View as plain text