Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(525)

Side by Side Diff: luci_config/appengine/gaeconfig/default_test.go

Issue 2576923003: Implement config service cache on top of datastore (Closed)
Patch Set: Relocated, fix, split integration test, rebase. Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « luci_config/appengine/gaeconfig/default.go ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2016 The LUCI Authors. All rights reserved.
2 // Use of this source code is governed under the Apache License, Version 2.0
3 // that can be found in the LICENSE file.
4
5 package gaeconfig
6
7 import (
8 "errors"
9 "fmt"
10 "net/http"
11 "net/http/httptest"
12 "testing"
13 "time"
14
15 "github.com/luci/luci-go/appengine/datastorecache"
16 "github.com/luci/luci-go/common/clock/testclock"
17 commonConfig "github.com/luci/luci-go/common/config"
18 memConfig "github.com/luci/luci-go/common/config/impl/memory"
19 configPB "github.com/luci/luci-go/common/proto/config"
20 "github.com/luci/luci-go/luci_config/common/cfgtypes"
21 "github.com/luci/luci-go/luci_config/server/cfgclient"
22 "github.com/luci/luci-go/luci_config/server/cfgclient/backend"
23 "github.com/luci/luci-go/luci_config/server/cfgclient/backend/client"
24 "github.com/luci/luci-go/luci_config/server/cfgclient/backend/testconfig "
25 "github.com/luci/luci-go/server/auth"
26 "github.com/luci/luci-go/server/auth/authtest"
27 "github.com/luci/luci-go/server/router"
28 "github.com/luci/luci-go/server/settings"
29
30 "github.com/luci/gae/filter/count"
31 "github.com/luci/gae/impl/memory"
32 ds "github.com/luci/gae/service/datastore"
33
34 "github.com/golang/protobuf/proto"
35 "golang.org/x/net/context"
36
37 . "github.com/luci/luci-go/common/testing/assertions"
38 . "github.com/smartystreets/goconvey/convey"
39 )
40
41 func projectConfigWithAccess(name cfgtypes.ProjectName, access ...string) *confi gPB.ProjectCfg {
42 return &configPB.ProjectCfg{
43 Name: proto.String(string(name)),
44 Access: access,
45 }
46 }
47
48 // stripMeta strips cache-specific identifying information from a set of Metas.
49 func stripMeta(metas []*cfgclient.Meta) []*cfgclient.Meta {
50 for _, meta := range metas {
51 meta.ContentHash = ""
52 meta.Revision = ""
53 }
54 return metas
55 }
56
57 func TestDatastoreCacheIntegration(t *testing.T) {
58 t.Parallel()
59
60 Convey(`A testing environment with fake cache and config service`, t, fu nc() {
61 c, clk := testclock.UseTime(context.Background(), ds.RoundTime(t estclock.TestTimeUTC))
62
63 c = memory.Use(c)
64
65 // Install fake auth state.
66 var authState authtest.FakeState
67 c = auth.WithState(c, &authState)
68 authState.Identity = "user:person@example.com"
69 authState.IdentityGroups = []string{"all", "users"}
70
71 // Use a memory-backed Config service instance.
72 baseMap := map[string]memConfig.ConfigSet{
73 "projects/open": map[string]string{
74 "project.cfg": proto.MarshalTextString(projectCo nfigWithAccess("foo", "group:all")),
75 "test.cfg": "Test Config Content",
76 },
77 "projects/foo": map[string]string{
78 "project.cfg": proto.MarshalTextString(projectCo nfigWithAccess("foo", "group:restricted")),
79 },
80 "projects/noconfig": map[string]string{},
81 "projects/invalidconfig": map[string]string{
82 "project.cfg": "!!! not a valid config !!!",
83 },
84 "projects/foo/refs/heads/master": map[string]string{
85 "ref.cfg": "foo",
86 },
87 "projects/foo/refs/branches/bar": map[string]string{
88 "ref.cfg": "foo/bar",
89 },
90 "projects/bar/refs/heads/master": map[string]string{
91 "ref.cfg": "bar",
92 },
93 }
94 base := memConfig.New(baseMap)
95 be := &client.Backend{
96 Provider: &testconfig.Provider{
97 Base: base,
98 },
99 }
100
101 // Install our settings into a memory-backed Settings storage.
102 memSettings := settings.MemoryStorage{}
103 c = settings.Use(c, settings.New(&memSettings))
104 s := Settings{
105 CacheExpirationSec: 10,
106 DatastoreCacheMode: dsCacheStrict,
107 }
108 putSettings := func() {
109 if err := settings.GetSettings(c).Set(c, settingsKey, &s , "test harness", "initial settings"); err != nil {
110 panic(err)
111 }
112 }
113 putSettings()
114
115 // Install our middleware chain into our Router.
116 baseMW := router.NewMiddlewareChain(func(ctx *router.Context, ne xt router.Handler) {
117 ctx.Context = c
118 next(ctx)
119 })
120 rtr := router.New()
121 server := httptest.NewServer(rtr)
122 defer server.Close()
123
124 // Install our Manager cron handlers.
125 installCacheCronHandlerImpl(rtr, baseMW, be)
126
127 runCron := func() int {
128 ds.GetTestable(c).CatchupIndexes()
129 resp, err := http.Get(fmt.Sprintf("%s/admin/config/cache /manager", server.URL))
130 if err != nil {
131 panic(fmt.Errorf("failed to GET: %s", err))
132 }
133 return resp.StatusCode
134 }
135 So(runCron(), ShouldEqual, http.StatusOK)
136
137 // Called when all parameters are in place to install our config service
138 // layers into "c".
139 installConfig := func(c context.Context) context.Context { retur n useImpl(c, be) }
140
141 loadProjectConfigs := func(c context.Context, a cfgclient.Author ity) (projs []string, meta []*cfgclient.Meta, err error) {
142 err = cfgclient.Projects(c, a, "project.cfg", cfgclient. StringSlice(&projs), &meta)
143 return
144 }
145
146 Convey(`Cache modes`, func() {
147 // Load all of our project configs and metadata. Load di rectly from our
148 // non-datastore Backend, since this data is not part of the test.
149 allProjs, _, err := loadProjectConfigs(backend.WithBacke nd(c, be), cfgclient.AsService)
150 if err != nil {
151 panic(err)
152 }
153
154 Convey(`Disabled, skips datastore cache.`, func() {
155 s.DatastoreCacheMode = dsCacheDisabled
156 putSettings()
157
158 c = installConfig(c)
159 c, cnt := count.FilterRDS(c)
160
161 projs, _, err := loadProjectConfigs(c, cfgclient .AsService)
162 So(err, ShouldBeNil)
163 So(projs, ShouldResemble, allProjs)
164
165 // No datastore operations should have been perf ormed.
166 So(cnt.GetMulti.Total(), ShouldEqual, 0)
167 So(cnt.PutMulti.Total(), ShouldEqual, 0)
168 })
169
170 Convey(`Enabled`, func() {
171 s.DatastoreCacheMode = dsCacheEnabled
172 putSettings()
173
174 c = installConfig(c)
175 c, cnt := count.FilterRDS(c)
176 projs, _, err := loadProjectConfigs(c, cfgclient .AsService)
177 So(err, ShouldBeNil)
178 So(projs, ShouldResemble, allProjs)
179
180 So(cnt.GetMulti.Total(), ShouldEqual, 1)
181 So(cnt.PutMulti.Total(), ShouldEqual, 1)
182
183 // Break our backing store. This forces lookups to load from cache.
184 memConfig.SetError(base, errors.New("config is b roken"))
185
186 projs, _, err = loadProjectConfigs(c, cfgclient. AsService)
187 So(err, ShouldBeNil)
188 So(projs, ShouldResemble, allProjs)
189
190 So(cnt.GetMulti.Total(), ShouldEqual, 2)
191 So(cnt.PutMulti.Total(), ShouldEqual, 1)
192
193 // Expire the cache.
194 clk.Add(time.Hour)
195
196 projs, _, err = loadProjectConfigs(c, cfgclient. AsService)
197 So(err, ShouldBeNil)
198 So(projs, ShouldResemble, allProjs)
199
200 So(cnt.GetMulti.Total(), ShouldEqual, 3)
201 So(cnt.PutMulti.Total(), ShouldEqual, 1)
202 })
203
204 Convey(`Strict`, func() {
205 s.DatastoreCacheMode = dsCacheStrict
206 putSettings()
207
208 c = installConfig(c)
209 c, cnt := count.FilterRDS(c)
210 projs, _, err := loadProjectConfigs(c, cfgclient .AsService)
211 So(err, ShouldBeNil)
212 So(projs, ShouldResemble, allProjs)
213
214 So(cnt.GetMulti.Total(), ShouldEqual, 1)
215 So(cnt.PutMulti.Total(), ShouldEqual, 1)
216
217 // Break our backing store. This forces lookups to load from cache.
218 memConfig.SetError(base, errors.New("config is b roken"))
219
220 projs, _, err = loadProjectConfigs(c, cfgclient. AsService)
221 So(err, ShouldBeNil)
222 So(projs, ShouldResemble, allProjs)
223
224 So(cnt.GetMulti.Total(), ShouldEqual, 2)
225 So(cnt.PutMulti.Total(), ShouldEqual, 1)
226
227 // Expire the cache. Now that the entries are ex pired, we will
228 // fail-closed to the backing store and return " config is broken".
229 clk.Add(time.Hour)
230
231 projs, _, err = loadProjectConfigs(c, cfgclient. AsService)
232 So(err, ShouldUnwrapTo, datastorecache.ErrCacheE xpired)
233
234 So(cnt.GetMulti.Total(), ShouldEqual, 3)
235 So(cnt.PutMulti.Total(), ShouldEqual, 1)
236 })
237 })
238
239 Convey(`A user, denied access to a project, gains access once pr oject config refreshes.`, func() {
240 c = installConfig(c)
241
242 _, metas, err := loadProjectConfigs(c, cfgclient.AsUser)
243 So(err, ShouldBeNil)
244 So(stripMeta(metas), ShouldResemble, []*cfgclient.Meta{
245 {ConfigSet: "projects/open", Path: "project.cfg" },
246 })
247
248 // Update "foo" project ACLs to include the "users" grou p.
249 baseMap["projects/foo"]["project.cfg"] = proto.MarshalTe xtString(projectConfigWithAccess("foo", "group:users"))
250
251 // Still denied access (cached).
252 c = installConfig(c)
253 _, metas, err = loadProjectConfigs(c, cfgclient.AsUser)
254 So(err, ShouldBeNil)
255 So(stripMeta(metas), ShouldResemble, []*cfgclient.Meta{
256 {ConfigSet: "projects/open", Path: "project.cfg" },
257 })
258
259 // Expire cache.
260 clk.Add(time.Hour)
261 _, _, err = loadProjectConfigs(c, cfgclient.AsUser)
262 So(err, ShouldUnwrapTo, datastorecache.ErrCacheExpired)
263
264 // Update our cache entries.
265 So(runCron(), ShouldEqual, http.StatusOK)
266
267 // Granted access!
268 c = installConfig(c)
269 _, metas, err = loadProjectConfigs(c, cfgclient.AsUser)
270 So(err, ShouldBeNil)
271 So(stripMeta(metas), ShouldResemble, []*cfgclient.Meta{
272 {ConfigSet: "projects/foo", Path: "project.cfg"} ,
273 {ConfigSet: "projects/open", Path: "project.cfg" },
274 })
275 })
276
277 Convey(`GetConfig iterative updates`, func() {
278 c = installConfig(c)
279
280 get := func(c context.Context) (v string, meta cfgclient .Meta, err error) {
281 err = cfgclient.Get(c, cfgclient.AsService, "pro jects/open", "test.cfg", cfgclient.String(&v), &meta)
282 return
283 }
284
285 baseContentHash := func(configSet, config string) string {
286 cfg, err := base.GetConfig(c, configSet, config, true)
287 if err != nil {
288 panic(err)
289 }
290 return cfg.ContentHash
291 }
292
293 _, meta, err := get(c)
294 So(err, ShouldBeNil)
295 So(meta.ContentHash, ShouldEqual, baseContentHash("proje cts/open", "test.cfg"))
296
297 Convey(`When the content changes, the hash is updated.`, func() {
298 // Change content, hash should not have changed.
299 baseMap["projects/open"]["test.cfg"] = "New Cont ent!"
300 _, meta2, err := get(c)
301 So(err, ShouldBeNil)
302 So(meta2.ContentHash, ShouldEqual, meta.ContentH ash)
303
304 // Expire the cache entry and refresh.
305 clk.Add(time.Hour)
306 So(runCron(), ShouldEqual, http.StatusOK)
307
308 // Re-get the config. The content hash should ha ve changed.
309 _, meta, err = get(c)
310 So(err, ShouldBeNil)
311 So(meta.ContentHash, ShouldEqual, baseContentHas h("projects/open", "test.cfg"))
312 So(meta.ContentHash, ShouldNotEqual, meta2.Conte ntHash)
313 })
314
315 Convey(`When the content is deleted, returns ErrNoConfig .`, func() {
316 // Change content, hash should not have changed.
317 delete(baseMap["projects/open"], "test.cfg")
318 _, meta2, err := get(c)
319 So(err, ShouldBeNil)
320 So(meta2.ContentHash, ShouldEqual, meta.ContentH ash)
321
322 // Expire the cache entry and refresh.
323 clk.Add(time.Hour)
324 So(runCron(), ShouldEqual, http.StatusOK)
325
326 _, _, err = get(c)
327 So(err, ShouldEqual, cfgclient.ErrNoConfig)
328 })
329 })
330
331 // Load project config, return a slice of hashes.
332 type getAllFn func(context.Context, cfgclient.Authority, string, cfgclient.MultiResolver, *[]*cfgclient.Meta) error
333 getHashes := func(c context.Context, gaFn getAllFn, path string) ([]string, error) {
334 var (
335 content []string
336 metas []*cfgclient.Meta
337 )
338 err := gaFn(c, cfgclient.AsService, path, cfgclient.Stri ngSlice(&content), &metas)
339 if err != nil {
340 return nil, err
341 }
342
343 hashes := make([]string, len(metas))
344 for i, meta := range metas {
345 hashes[i] = meta.ContentHash
346 }
347 return hashes, nil
348 }
349
350 // Return project config hashes from backing memory config store .
351 type baseConfigsFn func(context.Context, string, bool) ([]common Config.Config, error)
352 baseContentHashes := func(fn baseConfigsFn, path string) []strin g {
353 cfgs, err := fn(c, path, true)
354 if err != nil {
355 panic(err)
356 }
357 hashes := make([]string, len(cfgs))
358 for i := range cfgs {
359 hashes[i] = cfgs[i].ContentHash
360 }
361 return hashes
362 }
363
364 Convey(`Projects for iterative updates`, func() {
365 c = installConfig(c)
366
367 hashes, err := getHashes(c, cfgclient.Projects, "project .cfg")
368 So(err, ShouldBeNil)
369 So(hashes, ShouldResemble, baseContentHashes(base.GetPro jectConfigs, "project.cfg"))
370
371 Convey(`When the project list doesn't change, is not re- fetched.`, func() {
372 // Expire the cache entry and refresh.
373 clk.Add(time.Hour)
374 So(runCron(), ShouldEqual, http.StatusOK)
375
376 hashes2, err := getHashes(c, cfgclient.Projects, "project.cfg")
377 So(err, ShouldBeNil)
378 So(hashes2, ShouldResemble, hashes)
379 })
380
381 Convey(`When the project changes, is re-fetched.`, func( ) {
382 delete(baseMap["projects/invalidconfig"], "proje ct.cfg")
383
384 // Expire the cache entry and refresh.
385 clk.Add(time.Hour)
386 So(runCron(), ShouldEqual, http.StatusOK)
387
388 hashes2, err := getHashes(c, cfgclient.Projects, "project.cfg")
389 So(err, ShouldBeNil)
390 So(hashes2, ShouldResemble, baseContentHashes(ba se.GetProjectConfigs, "project.cfg"))
391 So(hashes2, ShouldNotResemble, hashes)
392 })
393 })
394
395 Convey(`Ref for iterative updates`, func() {
396 c = installConfig(c)
397
398 hashes, err := getHashes(c, cfgclient.Refs, "ref.cfg")
399 So(err, ShouldBeNil)
400 So(hashes, ShouldResemble, baseContentHashes(base.GetRef Configs, "ref.cfg"))
401
402 Convey(`When the ref list doesn't change, is not re-fetc hed.`, func() {
403 // Expire the cache entry and refresh.
404 clk.Add(time.Hour)
405 So(runCron(), ShouldEqual, http.StatusOK)
406
407 hashes2, err := getHashes(c, cfgclient.Refs, "re f.cfg")
408 So(err, ShouldBeNil)
409 So(hashes2, ShouldResemble, hashes)
410 })
411
412 Convey(`When the ref changes, is re-fetched.`, func() {
413 delete(baseMap["projects/foo/refs/branches/bar"] , "ref.cfg")
414
415 // Expire the cache entry and refresh.
416 clk.Add(time.Hour)
417 So(runCron(), ShouldEqual, http.StatusOK)
418
419 hashes2, err := getHashes(c, cfgclient.Refs, "re f.cfg")
420 So(err, ShouldBeNil)
421 So(hashes2, ShouldResemble, baseContentHashes(ba se.GetRefConfigs, "ref.cfg"))
422 So(hashes2, ShouldNotResemble, hashes)
423 })
424 })
425
426 Convey(`Test GetConfigSetURL user fetch`, func() {
427 c = installConfig(c)
428
429 // Project does not exist, missing for user and service.
430 _, err := cfgclient.GetConfigSetURL(c, cfgclient.AsUser, "projects/urltest")
431 So(err, ShouldEqual, cfgclient.ErrNoConfig)
432 _, err = cfgclient.GetConfigSetURL(c, cfgclient.AsServic e, "projects/urltest")
433 So(err, ShouldEqual, cfgclient.ErrNoConfig)
434
435 // Add the project, still missing b/c of cache.
436 baseMap["projects/urltest"] = memConfig.ConfigSet{
437 "project.cfg": proto.MarshalTextString(projectCo nfigWithAccess("foo", "group:exclusive")),
438 }
439
440 _, err = cfgclient.GetConfigSetURL(c, cfgclient.AsUser, "projects/urltest")
441 So(err, ShouldEqual, cfgclient.ErrNoConfig)
442 _, err = cfgclient.GetConfigSetURL(c, cfgclient.AsServic e, "projects/urltest")
443 So(err, ShouldEqual, cfgclient.ErrNoConfig)
444
445 // Expire the cache entry and refresh. Next project load will pick up
446 // the new project.
447 clk.Add(time.Hour)
448 So(runCron(), ShouldEqual, http.StatusOK)
449
450 // User still cannot access (not member of group).
451 _, err = cfgclient.GetConfigSetURL(c, cfgclient.AsUser, "projects/urltest")
452 So(err, ShouldEqual, cfgclient.ErrNoConfig)
453
454 // Service can access.
455 u, err := cfgclient.GetConfigSetURL(c, cfgclient.AsServi ce, "projects/urltest")
456 So(err, ShouldBeNil)
457 So(u.String(), ShouldEqual, "https://example.com/fake-co nfig/projects/urltest")
458
459 // User joins group, immediately gets access b/c the cac hed entry is the
460 // service response, and so was soft-forbidden before.
461 authState.IdentityGroups = append(authState.IdentityGrou ps, "exclusive")
462
463 _, err = cfgclient.GetConfigSetURL(c, cfgclient.AsUser, "projects/urltest")
464 So(err, ShouldBeNil)
465 So(u.String(), ShouldEqual, "https://example.com/fake-co nfig/projects/urltest")
466 })
467 })
468 }
OLDNEW
« no previous file with comments | « luci_config/appengine/gaeconfig/default.go ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698