Code refactoring for bpa operator
[icn.git] / cmd / bpa-operator / vendor / k8s.io / client-go / plugin / pkg / client / auth / gcp / gcp.go
1 /*
2 Copyright 2016 The Kubernetes Authors.
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8     http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15 */
16
17 package gcp
18
19 import (
20         "bytes"
21         "context"
22         "encoding/json"
23         "fmt"
24         "net/http"
25         "os/exec"
26         "strings"
27         "sync"
28         "time"
29
30         "golang.org/x/oauth2"
31         "golang.org/x/oauth2/google"
32         "k8s.io/apimachinery/pkg/util/net"
33         "k8s.io/apimachinery/pkg/util/yaml"
34         restclient "k8s.io/client-go/rest"
35         "k8s.io/client-go/util/jsonpath"
36         "k8s.io/klog"
37 )
38
39 func init() {
40         if err := restclient.RegisterAuthProviderPlugin("gcp", newGCPAuthProvider); err != nil {
41                 klog.Fatalf("Failed to register gcp auth plugin: %v", err)
42         }
43 }
44
45 var (
46         // Stubbable for testing
47         execCommand = exec.Command
48
49         // defaultScopes:
50         // - cloud-platform is the base scope to authenticate to GCP.
51         // - userinfo.email is used to authenticate to GKE APIs with gserviceaccount
52         //   email instead of numeric uniqueID.
53         defaultScopes = []string{
54                 "https://www.googleapis.com/auth/cloud-platform",
55                 "https://www.googleapis.com/auth/userinfo.email"}
56 )
57
58 // gcpAuthProvider is an auth provider plugin that uses GCP credentials to provide
59 // tokens for kubectl to authenticate itself to the apiserver. A sample json config
60 // is provided below with all recognized options described.
61 //
62 // {
63 //   'auth-provider': {
64 //     # Required
65 //     "name": "gcp",
66 //
67 //     'config': {
68 //       # Authentication options
69 //       # These options are used while getting a token.
70 //
71 //       # comma-separated list of GCP API scopes. default value of this field
72 //       # is "https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/userinfo.email".
73 //               # to override the API scopes, specify this field explicitly.
74 //       "scopes": "https://www.googleapis.com/auth/cloud-platform"
75 //
76 //       # Caching options
77 //
78 //       # Raw string data representing cached access token.
79 //       "access-token": "ya29.CjWdA4GiBPTt",
80 //       # RFC3339Nano expiration timestamp for cached access token.
81 //       "expiry": "2016-10-31 22:31:9.123",
82 //
83 //       # Command execution options
84 //       # These options direct the plugin to execute a specified command and parse
85 //       # token and expiry time from the output of the command.
86 //
87 //       # Command to execute for access token. Command output will be parsed as JSON.
88 //       # If "cmd-args" is not present, this value will be split on whitespace, with
89 //       # the first element interpreted as the command, remaining elements as args.
90 //       "cmd-path": "/usr/bin/gcloud",
91 //
92 //       # Arguments to pass to command to execute for access token.
93 //       "cmd-args": "config config-helper --output=json"
94 //
95 //       # JSONPath to the string field that represents the access token in
96 //       # command output. If omitted, defaults to "{.access_token}".
97 //       "token-key": "{.credential.access_token}",
98 //
99 //       # JSONPath to the string field that represents expiration timestamp
100 //       # of the access token in the command output. If omitted, defaults to
101 //       # "{.token_expiry}"
102 //       "expiry-key": ""{.credential.token_expiry}",
103 //
104 //       # golang reference time in the format that the expiration timestamp uses.
105 //       # If omitted, defaults to time.RFC3339Nano
106 //       "time-fmt": "2006-01-02 15:04:05.999999999"
107 //     }
108 //   }
109 // }
110 //
111 type gcpAuthProvider struct {
112         tokenSource oauth2.TokenSource
113         persister   restclient.AuthProviderConfigPersister
114 }
115
116 func newGCPAuthProvider(_ string, gcpConfig map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) {
117         ts, err := tokenSource(isCmdTokenSource(gcpConfig), gcpConfig)
118         if err != nil {
119                 return nil, err
120         }
121         cts, err := newCachedTokenSource(gcpConfig["access-token"], gcpConfig["expiry"], persister, ts, gcpConfig)
122         if err != nil {
123                 return nil, err
124         }
125         return &gcpAuthProvider{cts, persister}, nil
126 }
127
128 func isCmdTokenSource(gcpConfig map[string]string) bool {
129         _, ok := gcpConfig["cmd-path"]
130         return ok
131 }
132
133 func tokenSource(isCmd bool, gcpConfig map[string]string) (oauth2.TokenSource, error) {
134         // Command-based token source
135         if isCmd {
136                 cmd := gcpConfig["cmd-path"]
137                 if len(cmd) == 0 {
138                         return nil, fmt.Errorf("missing access token cmd")
139                 }
140                 if gcpConfig["scopes"] != "" {
141                         return nil, fmt.Errorf("scopes can only be used when kubectl is using a gcp service account key")
142                 }
143                 var args []string
144                 if cmdArgs, ok := gcpConfig["cmd-args"]; ok {
145                         args = strings.Fields(cmdArgs)
146                 } else {
147                         fields := strings.Fields(cmd)
148                         cmd = fields[0]
149                         args = fields[1:]
150                 }
151                 return newCmdTokenSource(cmd, args, gcpConfig["token-key"], gcpConfig["expiry-key"], gcpConfig["time-fmt"]), nil
152         }
153
154         // Google Application Credentials-based token source
155         scopes := parseScopes(gcpConfig)
156         ts, err := google.DefaultTokenSource(context.Background(), scopes...)
157         if err != nil {
158                 return nil, fmt.Errorf("cannot construct google default token source: %v", err)
159         }
160         return ts, nil
161 }
162
163 // parseScopes constructs a list of scopes that should be included in token source
164 // from the config map.
165 func parseScopes(gcpConfig map[string]string) []string {
166         scopes, ok := gcpConfig["scopes"]
167         if !ok {
168                 return defaultScopes
169         }
170         if scopes == "" {
171                 return []string{}
172         }
173         return strings.Split(gcpConfig["scopes"], ",")
174 }
175
176 func (g *gcpAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper {
177         var resetCache map[string]string
178         if cts, ok := g.tokenSource.(*cachedTokenSource); ok {
179                 resetCache = cts.baseCache()
180         } else {
181                 resetCache = make(map[string]string)
182         }
183         return &conditionalTransport{&oauth2.Transport{Source: g.tokenSource, Base: rt}, g.persister, resetCache}
184 }
185
186 func (g *gcpAuthProvider) Login() error { return nil }
187
188 type cachedTokenSource struct {
189         lk          sync.Mutex
190         source      oauth2.TokenSource
191         accessToken string
192         expiry      time.Time
193         persister   restclient.AuthProviderConfigPersister
194         cache       map[string]string
195 }
196
197 func newCachedTokenSource(accessToken, expiry string, persister restclient.AuthProviderConfigPersister, ts oauth2.TokenSource, cache map[string]string) (*cachedTokenSource, error) {
198         var expiryTime time.Time
199         if parsedTime, err := time.Parse(time.RFC3339Nano, expiry); err == nil {
200                 expiryTime = parsedTime
201         }
202         if cache == nil {
203                 cache = make(map[string]string)
204         }
205         return &cachedTokenSource{
206                 source:      ts,
207                 accessToken: accessToken,
208                 expiry:      expiryTime,
209                 persister:   persister,
210                 cache:       cache,
211         }, nil
212 }
213
214 func (t *cachedTokenSource) Token() (*oauth2.Token, error) {
215         tok := t.cachedToken()
216         if tok.Valid() && !tok.Expiry.IsZero() {
217                 return tok, nil
218         }
219         tok, err := t.source.Token()
220         if err != nil {
221                 return nil, err
222         }
223         cache := t.update(tok)
224         if t.persister != nil {
225                 if err := t.persister.Persist(cache); err != nil {
226                         klog.V(4).Infof("Failed to persist token: %v", err)
227                 }
228         }
229         return tok, nil
230 }
231
232 func (t *cachedTokenSource) cachedToken() *oauth2.Token {
233         t.lk.Lock()
234         defer t.lk.Unlock()
235         return &oauth2.Token{
236                 AccessToken: t.accessToken,
237                 TokenType:   "Bearer",
238                 Expiry:      t.expiry,
239         }
240 }
241
242 func (t *cachedTokenSource) update(tok *oauth2.Token) map[string]string {
243         t.lk.Lock()
244         defer t.lk.Unlock()
245         t.accessToken = tok.AccessToken
246         t.expiry = tok.Expiry
247         ret := map[string]string{}
248         for k, v := range t.cache {
249                 ret[k] = v
250         }
251         ret["access-token"] = t.accessToken
252         ret["expiry"] = t.expiry.Format(time.RFC3339Nano)
253         return ret
254 }
255
256 // baseCache is the base configuration value for this TokenSource, without any cached ephemeral tokens.
257 func (t *cachedTokenSource) baseCache() map[string]string {
258         t.lk.Lock()
259         defer t.lk.Unlock()
260         ret := map[string]string{}
261         for k, v := range t.cache {
262                 ret[k] = v
263         }
264         delete(ret, "access-token")
265         delete(ret, "expiry")
266         return ret
267 }
268
269 type commandTokenSource struct {
270         cmd       string
271         args      []string
272         tokenKey  string
273         expiryKey string
274         timeFmt   string
275 }
276
277 func newCmdTokenSource(cmd string, args []string, tokenKey, expiryKey, timeFmt string) *commandTokenSource {
278         if len(timeFmt) == 0 {
279                 timeFmt = time.RFC3339Nano
280         }
281         if len(tokenKey) == 0 {
282                 tokenKey = "{.access_token}"
283         }
284         if len(expiryKey) == 0 {
285                 expiryKey = "{.token_expiry}"
286         }
287         return &commandTokenSource{
288                 cmd:       cmd,
289                 args:      args,
290                 tokenKey:  tokenKey,
291                 expiryKey: expiryKey,
292                 timeFmt:   timeFmt,
293         }
294 }
295
296 func (c *commandTokenSource) Token() (*oauth2.Token, error) {
297         fullCmd := strings.Join(append([]string{c.cmd}, c.args...), " ")
298         cmd := execCommand(c.cmd, c.args...)
299         var stderr bytes.Buffer
300         cmd.Stderr = &stderr
301         output, err := cmd.Output()
302         if err != nil {
303                 return nil, fmt.Errorf("error executing access token command %q: err=%v output=%s stderr=%s", fullCmd, err, output, string(stderr.Bytes()))
304         }
305         token, err := c.parseTokenCmdOutput(output)
306         if err != nil {
307                 return nil, fmt.Errorf("error parsing output for access token command %q: %v", fullCmd, err)
308         }
309         return token, nil
310 }
311
312 func (c *commandTokenSource) parseTokenCmdOutput(output []byte) (*oauth2.Token, error) {
313         output, err := yaml.ToJSON(output)
314         if err != nil {
315                 return nil, err
316         }
317         var data interface{}
318         if err := json.Unmarshal(output, &data); err != nil {
319                 return nil, err
320         }
321
322         accessToken, err := parseJSONPath(data, "token-key", c.tokenKey)
323         if err != nil {
324                 return nil, fmt.Errorf("error parsing token-key %q from %q: %v", c.tokenKey, string(output), err)
325         }
326         expiryStr, err := parseJSONPath(data, "expiry-key", c.expiryKey)
327         if err != nil {
328                 return nil, fmt.Errorf("error parsing expiry-key %q from %q: %v", c.expiryKey, string(output), err)
329         }
330         var expiry time.Time
331         if t, err := time.Parse(c.timeFmt, expiryStr); err != nil {
332                 klog.V(4).Infof("Failed to parse token expiry from %s (fmt=%s): %v", expiryStr, c.timeFmt, err)
333         } else {
334                 expiry = t
335         }
336
337         return &oauth2.Token{
338                 AccessToken: accessToken,
339                 TokenType:   "Bearer",
340                 Expiry:      expiry,
341         }, nil
342 }
343
344 func parseJSONPath(input interface{}, name, template string) (string, error) {
345         j := jsonpath.New(name)
346         buf := new(bytes.Buffer)
347         if err := j.Parse(template); err != nil {
348                 return "", err
349         }
350         if err := j.Execute(buf, input); err != nil {
351                 return "", err
352         }
353         return buf.String(), nil
354 }
355
356 type conditionalTransport struct {
357         oauthTransport *oauth2.Transport
358         persister      restclient.AuthProviderConfigPersister
359         resetCache     map[string]string
360 }
361
362 var _ net.RoundTripperWrapper = &conditionalTransport{}
363
364 func (t *conditionalTransport) RoundTrip(req *http.Request) (*http.Response, error) {
365         if len(req.Header.Get("Authorization")) != 0 {
366                 return t.oauthTransport.Base.RoundTrip(req)
367         }
368
369         res, err := t.oauthTransport.RoundTrip(req)
370
371         if err != nil {
372                 return nil, err
373         }
374
375         if res.StatusCode == 401 {
376                 klog.V(4).Infof("The credentials that were supplied are invalid for the target cluster")
377                 t.persister.Persist(t.resetCache)
378         }
379
380         return res, nil
381 }
382
383 func (t *conditionalTransport) WrappedRoundTripper() http.RoundTripper { return t.oauthTransport.Base }