OLD | NEW |
1 // Copyright 2015 The LUCI Authors. All rights reserved. | 1 // Copyright 2015 The LUCI Authors. All rights reserved. |
2 // Use of this source code is governed under the Apache License, Version 2.0 | 2 // Use of this source code is governed under the Apache License, Version 2.0 |
3 // that can be found in the LICENSE file. | 3 // that can be found in the LICENSE file. |
4 | 4 |
5 package prod | 5 package prod |
6 | 6 |
7 import ( | 7 import ( |
8 "fmt" | 8 "fmt" |
9 "net/http" | 9 "net/http" |
10 "net/http/cookiejar" | 10 "net/http/cookiejar" |
11 "net/url" | 11 "net/url" |
12 "strings" | 12 "strings" |
13 | 13 |
14 "github.com/luci/gae/service/info" | |
15 "github.com/luci/gae/service/urlfetch" | 14 "github.com/luci/gae/service/urlfetch" |
16 "golang.org/x/net/context" | 15 "golang.org/x/net/context" |
17 gOAuth "golang.org/x/oauth2/google" | 16 gOAuth "golang.org/x/oauth2/google" |
18 "google.golang.org/appengine" | 17 "google.golang.org/appengine" |
19 "google.golang.org/appengine/remote_api" | 18 "google.golang.org/appengine/remote_api" |
20 ) | 19 ) |
21 | 20 |
22 // RemoteAPIScopes is the set of OAuth2 scopes needed for Remote API access. | 21 // RemoteAPIScopes is the set of OAuth2 scopes needed for Remote API access. |
23 var RemoteAPIScopes = []string{ | 22 var RemoteAPIScopes = []string{ |
24 "https://www.googleapis.com/auth/appengine.apis", | 23 "https://www.googleapis.com/auth/appengine.apis", |
25 "https://www.googleapis.com/auth/userinfo.email", | 24 "https://www.googleapis.com/auth/userinfo.email", |
26 "https://www.googleapis.com/auth/cloud.platform", | 25 "https://www.googleapis.com/auth/cloud.platform", |
27 } | 26 } |
28 | 27 |
29 type key int | 28 type key int |
30 | 29 |
31 var ( | 30 var ( |
32 » prodContextKey key | 31 » prodStateKey = "contains the current *prodState" |
33 » prodContextNoTxnKey key = 1 | 32 » probeCacheKey = "contains the current *infoProbeCache" |
34 » probeCacheKey key = 2 | |
35 ) | 33 ) |
36 | 34 |
37 // AEContext retrieves the raw "google.golang.org/appengine" compatible Context. | 35 // AEContext retrieves the raw "google.golang.org/appengine" compatible Context. |
38 // | 36 // |
39 // It also transfers deadline of `c` to AE context, since deadline is used for | 37 // It also transfers deadline of `c` to AE context, since deadline is used for |
40 // RPCs. Doesn't transfer cancelation ability though (since it's ignored by GAE | 38 // RPCs. Doesn't transfer cancelation ability though (since it's ignored by GAE |
41 // anyway). | 39 // anyway). |
42 func AEContext(c context.Context) context.Context { | 40 func AEContext(c context.Context) context.Context { |
43 » aeCtx, _ := c.Value(prodContextKey).(context.Context) | 41 » ps := getProdState(c) |
44 » if aeCtx == nil { | 42 » return ps.context(c) |
45 » » return nil | |
46 » } | |
47 » if deadline, ok := c.Deadline(); ok { | |
48 » » aeCtx, _ = context.WithDeadline(aeCtx, deadline) | |
49 » } | |
50 » return aeCtx | |
51 } | |
52 | |
53 // AEContextNoTxn retrieves the raw "google.golang.org/appengine" compatible | |
54 // Context that's not part of a transaction. | |
55 func AEContextNoTxn(c context.Context) context.Context { | |
56 » aeCtx, _ := c.Value(prodContextNoTxnKey).(context.Context) | |
57 » if aeCtx == nil { | |
58 » » return nil | |
59 » } | |
60 | |
61 » if ns, has := info.Get(c).GetNamespace(); has { | |
62 » » var err error | |
63 » » aeCtx, err = appengine.Namespace(aeCtx, ns) | |
64 » » if err != nil { | |
65 » » » panic(err) | |
66 » » } | |
67 » } | |
68 » if deadline, ok := c.Deadline(); ok { | |
69 » » aeCtx, _ = context.WithDeadline(aeCtx, deadline) | |
70 » } | |
71 » return aeCtx | |
72 } | 43 } |
73 | 44 |
74 func setupAECtx(c, aeCtx context.Context) context.Context { | 45 func setupAECtx(c, aeCtx context.Context) context.Context { |
75 » c = context.WithValue(c, prodContextKey, aeCtx) | 46 » c = withProdState(c, prodState{ |
76 » c = context.WithValue(c, prodContextNoTxnKey, aeCtx) | 47 » » ctx: aeCtx, |
| 48 » » noTxnCtx: aeCtx, |
| 49 » }) |
77 return useModule(useMail(useUser(useURLFetch(useRDS(useMC(useTQ(useGI(us
eLogging(c))))))))) | 50 return useModule(useMail(useUser(useURLFetch(useRDS(useMC(useTQ(useGI(us
eLogging(c))))))))) |
78 } | 51 } |
79 | 52 |
80 // Use adds production implementations for all the gae services to the | 53 // Use adds production implementations for all the gae services to the |
81 // context. | 54 // context. |
82 // | 55 // |
83 // The services added are: | 56 // The services added are: |
84 // - github.com/luci-go/common/logging | 57 // - github.com/luci-go/common/logging |
85 // - github.com/luci/gae/service/datastore | 58 // - github.com/luci/gae/service/datastore |
86 // - github.com/luci/gae/service/info | 59 // - github.com/luci/gae/service/info |
(...skipping 29 matching lines...) Expand all Loading... |
116 // * If host starts with "localhost", this will create a regular http.Client | 89 // * If host starts with "localhost", this will create a regular http.Client |
117 // with a cookiejar, and call the _ah/login API to log in as an admin with | 90 // with a cookiejar, and call the _ah/login API to log in as an admin with |
118 // the user "admin@example.com". | 91 // the user "admin@example.com". |
119 // | 92 // |
120 // * Otherwise, it will create a Google OAuth2 client with the following scope
s: | 93 // * Otherwise, it will create a Google OAuth2 client with the following scope
s: |
121 // - "https://www.googleapis.com/auth/appengine.apis" | 94 // - "https://www.googleapis.com/auth/appengine.apis" |
122 // - "https://www.googleapis.com/auth/userinfo.email" | 95 // - "https://www.googleapis.com/auth/userinfo.email" |
123 // - "https://www.googleapis.com/auth/cloud.platform" | 96 // - "https://www.googleapis.com/auth/cloud.platform" |
124 func UseRemote(inOutCtx *context.Context, host string, client *http.Client) (err
error) { | 97 func UseRemote(inOutCtx *context.Context, host string, client *http.Client) (err
error) { |
125 if client == nil { | 98 if client == nil { |
| 99 aeCtx := AEContext(*inOutCtx) |
| 100 |
126 if strings.HasPrefix(host, "localhost") { | 101 if strings.HasPrefix(host, "localhost") { |
127 transp := http.DefaultTransport | 102 transp := http.DefaultTransport |
128 » » » if aeCtx := AEContextNoTxn(*inOutCtx); aeCtx != nil { | 103 » » » if aeCtx != nil { |
129 transp = urlfetch.Get(*inOutCtx) | 104 transp = urlfetch.Get(*inOutCtx) |
130 } | 105 } |
131 | 106 |
132 client = &http.Client{Transport: transp} | 107 client = &http.Client{Transport: transp} |
133 client.Jar, err = cookiejar.New(nil) | 108 client.Jar, err = cookiejar.New(nil) |
134 if err != nil { | 109 if err != nil { |
135 return | 110 return |
136 } | 111 } |
137 u := fmt.Sprintf("http://%s/_ah/login?%s", host, url.Val
ues{ | 112 u := fmt.Sprintf("http://%s/_ah/login?%s", host, url.Val
ues{ |
138 "email": {"admin@example.com"}, | 113 "email": {"admin@example.com"}, |
139 "admin": {"True"}, | 114 "admin": {"True"}, |
140 "action": {"Login"}, | 115 "action": {"Login"}, |
141 }.Encode()) | 116 }.Encode()) |
142 | 117 |
143 var rsp *http.Response | 118 var rsp *http.Response |
144 rsp, err = client.Get(u) | 119 rsp, err = client.Get(u) |
145 if err != nil { | 120 if err != nil { |
146 return | 121 return |
147 } | 122 } |
148 defer rsp.Body.Close() | 123 defer rsp.Body.Close() |
149 } else { | 124 } else { |
150 aeCtx := AEContextNoTxn(*inOutCtx) | |
151 if aeCtx == nil { | 125 if aeCtx == nil { |
152 aeCtx = context.Background() | 126 aeCtx = context.Background() |
153 } | 127 } |
154 client, err = gOAuth.DefaultClient(aeCtx, RemoteAPIScope
s...) | 128 client, err = gOAuth.DefaultClient(aeCtx, RemoteAPIScope
s...) |
155 if err != nil { | 129 if err != nil { |
156 return | 130 return |
157 } | 131 } |
158 } | 132 } |
159 } | 133 } |
160 | 134 |
161 aeCtx, err := remote_api.NewRemoteContext(host, client) | 135 aeCtx, err := remote_api.NewRemoteContext(host, client) |
162 if err != nil { | 136 if err != nil { |
163 return | 137 return |
164 } | 138 } |
165 *inOutCtx = setupAECtx(*inOutCtx, aeCtx) | 139 *inOutCtx = setupAECtx(*inOutCtx, aeCtx) |
166 return nil | 140 return nil |
167 } | 141 } |
| 142 |
| 143 // prodState is the current production state. |
| 144 type prodState struct { |
| 145 // ctx is the current derived GAE context. |
| 146 ctx context.Context |
| 147 |
| 148 // noTxnCtx is a Context maintained alongside ctx. When a transaction is |
| 149 // entered, ctx will be updated, but noTxnCtx will not, allowing extra- |
| 150 // transactional Context access. |
| 151 noTxnCtx context.Context |
| 152 |
| 153 // inTxn if true if this is in a transaction, false otherwise. |
| 154 inTxn bool |
| 155 } |
| 156 |
| 157 func getProdState(c context.Context) prodState { |
| 158 if v := c.Value(&prodStateKey).(*prodState); v != nil { |
| 159 return *v |
| 160 } |
| 161 return prodState{} |
| 162 } |
| 163 |
| 164 func withProdState(c context.Context, ps prodState) context.Context { |
| 165 return context.WithValue(c, &prodStateKey, &ps) |
| 166 } |
| 167 |
| 168 // context returns the current AppEngine-bound Context. Prior to returning, |
| 169 // the deadline from "c" (if any) is applied. |
| 170 // |
| 171 // Note that this does not (currently) apply any other Done state or propagate |
| 172 // cancellation from "c". |
| 173 // |
| 174 // Tracking at: |
| 175 // https://github.com/luci/gae/issues/59 |
| 176 func (ps *prodState) context(c context.Context) context.Context { |
| 177 aeCtx := ps.ctx |
| 178 if aeCtx == nil { |
| 179 return nil |
| 180 } |
| 181 |
| 182 if deadline, ok := c.Deadline(); ok { |
| 183 aeCtx, _ = context.WithDeadline(aeCtx, deadline) |
| 184 } |
| 185 return aeCtx |
| 186 } |
OLD | NEW |