blob: 8595df2716945bb7fd9e72f63b96c9f68ff55193 [file] [log] [blame]
Zack Williamse940c7a2019-08-21 14:25:39 -07001/*
2Copyright 2018 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package transport
18
19import (
20 "fmt"
21 "io/ioutil"
22 "net/http"
23 "strings"
24 "sync"
25 "time"
26
27 "golang.org/x/oauth2"
28 "k8s.io/klog"
29)
30
31// TokenSourceWrapTransport returns a WrapTransport that injects bearer tokens
32// authentication from an oauth2.TokenSource.
33func TokenSourceWrapTransport(ts oauth2.TokenSource) func(http.RoundTripper) http.RoundTripper {
34 return func(rt http.RoundTripper) http.RoundTripper {
35 return &tokenSourceTransport{
36 base: rt,
37 ort: &oauth2.Transport{
38 Source: ts,
39 Base: rt,
40 },
41 }
42 }
43}
44
45// NewCachedFileTokenSource returns a oauth2.TokenSource reads a token from a
46// file at a specified path and periodically reloads it.
47func NewCachedFileTokenSource(path string) oauth2.TokenSource {
48 return &cachingTokenSource{
49 now: time.Now,
50 leeway: 10 * time.Second,
51 base: &fileTokenSource{
52 path: path,
53 // This period was picked because it is half of the duration between when the kubelet
54 // refreshes a projected service account token and when the original token expires.
55 // Default token lifetime is 10 minutes, and the kubelet starts refreshing at 80% of lifetime.
56 // This should induce re-reading at a frequency that works with the token volume source.
57 period: time.Minute,
58 },
59 }
60}
61
62type tokenSourceTransport struct {
63 base http.RoundTripper
64 ort http.RoundTripper
65}
66
67func (tst *tokenSourceTransport) RoundTrip(req *http.Request) (*http.Response, error) {
68 // This is to allow --token to override other bearer token providers.
69 if req.Header.Get("Authorization") != "" {
70 return tst.base.RoundTrip(req)
71 }
72 return tst.ort.RoundTrip(req)
73}
74
75type fileTokenSource struct {
76 path string
77 period time.Duration
78}
79
80var _ = oauth2.TokenSource(&fileTokenSource{})
81
82func (ts *fileTokenSource) Token() (*oauth2.Token, error) {
83 tokb, err := ioutil.ReadFile(ts.path)
84 if err != nil {
85 return nil, fmt.Errorf("failed to read token file %q: %v", ts.path, err)
86 }
87 tok := strings.TrimSpace(string(tokb))
88 if len(tok) == 0 {
89 return nil, fmt.Errorf("read empty token from file %q", ts.path)
90 }
91
92 return &oauth2.Token{
93 AccessToken: tok,
94 Expiry: time.Now().Add(ts.period),
95 }, nil
96}
97
98type cachingTokenSource struct {
99 base oauth2.TokenSource
100 leeway time.Duration
101
102 sync.RWMutex
103 tok *oauth2.Token
104
105 // for testing
106 now func() time.Time
107}
108
109var _ = oauth2.TokenSource(&cachingTokenSource{})
110
111func (ts *cachingTokenSource) Token() (*oauth2.Token, error) {
112 now := ts.now()
113 // fast path
114 ts.RLock()
115 tok := ts.tok
116 ts.RUnlock()
117
118 if tok != nil && tok.Expiry.Add(-1*ts.leeway).After(now) {
119 return tok, nil
120 }
121
122 // slow path
123 ts.Lock()
124 defer ts.Unlock()
125 if tok := ts.tok; tok != nil && tok.Expiry.Add(-1*ts.leeway).After(now) {
126 return tok, nil
127 }
128
129 tok, err := ts.base.Token()
130 if err != nil {
131 if ts.tok == nil {
132 return nil, err
133 }
134 klog.Errorf("Unable to rotate token: %v", err)
135 return ts.tok, nil
136 }
137
138 ts.tok = tok
139 return tok, nil
140}