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

Side by Side Diff: appengine/gaeconfig/ds_test.go

Issue 2576923003: Implement config service cache on top of datastore (Closed)
Patch Set: Created 4 years 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
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 "fmt"
9 "net/http"
10 "net/http/httptest"
11 "sort"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/luci/luci-go/appengine/datastorecache"
17 "github.com/luci/luci-go/common/clock/testclock"
18 commonConfig "github.com/luci/luci-go/common/config"
19 memConfig "github.com/luci/luci-go/common/config/impl/memory"
20 "github.com/luci/luci-go/common/errors"
21 configPB "github.com/luci/luci-go/common/proto/config"
22 "github.com/luci/luci-go/server/auth"
23 "github.com/luci/luci-go/server/auth/authtest"
24 "github.com/luci/luci-go/server/config"
25 "github.com/luci/luci-go/server/config/caching"
26 "github.com/luci/luci-go/server/config/testconfig"
27 "github.com/luci/luci-go/server/config/textproto"
28 "github.com/luci/luci-go/server/router"
29 "github.com/luci/luci-go/server/settings"
30
31 "github.com/luci/gae/filter/count"
32 "github.com/luci/gae/impl/memory"
33 "github.com/luci/gae/service/datastore"
34
35 "github.com/golang/protobuf/proto"
36 "golang.org/x/net/context"
37
38 . "github.com/luci/luci-go/common/testing/assertions"
39 . "github.com/smartystreets/goconvey/convey"
40 )
41
42 // testCache is a generic Cache testing layer.
43 type testCache interface {
44 dsCacheBackend
45
46 setCacheErr(err error)
47 setProjectDNE(project string)
48 addConfig(configSet, path, content string) *config.Item
49 addProjectConfig(name commonConfig.ProjectName, access string)
50 addConfigSets(path string, configSets ...string) []string
51 addConfigSetURL(configSet string) string
52 }
53
54 func projectConfigWithAccess(name commonConfig.ProjectName, access ...string) *c onfigPB.ProjectCfg {
55 return &configPB.ProjectCfg{
56 Name: proto.String(string(name)),
57 Access: access,
58 }
59 }
60
61 // fakeCache is a pure in-memory testCache implementation. It is very simple,
62 // storing only raw cache key/value pairs.
63 type fakeCache struct {
64 d map[string]datastorecache.Value
65 err error
66 }
67
68 func mkFakeCache() *fakeCache {
69 return &fakeCache{
70 d: make(map[string]datastorecache.Value),
71 }
72 }
73
74 func (fc *fakeCache) Get(c context.Context, key []byte) (v datastorecache.Value, err error) {
75 if err = fc.err; err != nil {
76 return
77 }
78
79 var k caching.Key
80 caching.Decode(key, &k)
81
82 var ok bool
83 if v, ok = fc.d[string(key)]; ok {
84 return
85 }
86
87 err = datastorecache.ErrCacheExpired
88 return
89 }
90
91 func (fc *fakeCache) setCacheData(key caching.Key, d []byte) {
92 }
93
94 func (fc *fakeCache) set(key caching.Key, v *caching.Value) {
95 encKey, err := caching.Encode(&key)
96 if err != nil {
97 panic(fmt.Errorf("failed to encode key: %s", err))
98 }
99
100 if v == nil {
101 delete(fc.d, string(encKey))
102 return
103 }
104
105 encValue, err := v.Encode()
106 if err != nil {
107 panic(fmt.Errorf("failed to encode cache value: %s", err))
108 }
109
110 fc.d[string(encKey)] = datastorecache.Value{
111 Schema: dsCacheSchema,
112 Data: encValue,
113 Description: key.String(),
114 }
115 }
116
117 func (fc *fakeCache) setCacheErr(err error) { fc.err = err }
118
119 func (fc *fakeCache) setProjectDNE(project string) {
120 // Get for this project config will fail.
121 fc.set(caching.Key{
122 Schema: caching.Schema,
123 Op: caching.OpGet,
124 ConfigSet: config.ProjectConfigSet(commonConfig.ProjectName(proj ect)),
125 Path: config.ProjectConfigPath,
126 }, nil)
127 }
128
129 func (fc *fakeCache) addConfigImpl(configSet, path, format, formatData, content string) *config.Item {
130 var (
131 item *config.Item
132 cv caching.Value
133 )
134 if content != "" {
135 item = &config.Item{
136 Meta: config.Meta{
137 ConfigSet: configSet,
138 Path: path,
139 ContentHash: "hash",
140 },
141 Content: content,
142 Format: format,
143 FormatData: formatData,
144 }
145 cv.LoadItems(item)
146 }
147
148 fc.set(caching.Key{
149 Schema: caching.Schema,
150 Authority: config.AsService,
151 Op: caching.OpGet,
152 ConfigSet: configSet,
153 Path: path,
154 Content: true,
155 Format: format,
156 FormatData: formatData,
157 }, &cv)
158
159 return item
160 }
161
162 func (fc *fakeCache) addConfig(configSet, path, content string) *config.Item {
163 return fc.addConfigImpl(configSet, path, "", "", content)
164 }
165
166 // addProjectConfig caches a "project.cfg" file for the specified project with
167 // the specified access string.
168 func (fc *fakeCache) addProjectConfig(name commonConfig.ProjectName, access stri ng) {
169 // We're loading the resolved version of this cache item.
170 pcfg := projectConfigWithAccess(name, access)
171 pcfgName := proto.MessageName(pcfg)
172
173 f := textproto.Formatter{}
174 formattedData, err := f.FormatItem(proto.MarshalTextString(pcfg), pcfgNa me)
175 if err != nil {
176 panic(err)
177 }
178
179 fc.addConfigImpl(config.ProjectConfigSet(name), config.ProjectConfigPath ,
180 textproto.BinaryFormat, pcfgName, formattedData)
181 }
182
183 func (fc *fakeCache) addConfigSets(path string, configSets ...string) []string {
184 items := make([]*config.Item, len(configSets))
185 contents := make([]string, len(configSets))
186 for i, cs := range configSets {
187 contents[i] = cs
188 items[i] = &config.Item{
189 Meta: config.Meta{
190 ConfigSet: cs,
191 Path: path,
192 ContentHash: "hash",
193 },
194 Content: contents[i],
195 Format: "",
196 FormatData: "",
197 }
198 }
199
200 for _, t := range []config.GetAllType{config.Project, config.Ref} {
201 var cv caching.Value
202 cv.LoadItems(items...)
203
204 fc.set(caching.Key{
205 Schema: caching.Schema,
206 Authority: config.AsService,
207 Op: caching.OpGetAll,
208 Content: true,
209 Path: path,
210 GetAllType: t,
211 Format: "",
212 FormatData: "",
213 }, &cv)
214 }
215 return contents
216 }
217
218 func (fc *fakeCache) addConfigSetURL(configSet string) string {
219 u := fmt.Sprintf("https://exmaple.com/config-sets/%s", configSet)
220 fc.set(caching.Key{
221 Schema: caching.Schema,
222 Authority: config.AsService,
223 Op: caching.OpConfigSetURL,
224 ConfigSet: configSet,
225 }, &caching.Value{
226 URL: u,
227 })
228 return u
229 }
230
231 // fullStackCache is a testCache implementation built on top of an in-memory
232 // base config.Interface with the datastore Cache layer on top of it.
233 type fullStackCache struct {
234 cache *datastorecache.Cache
235 err error
236
237 data map[string]memConfig.ConfigSet
238 backend config.Backend
239 junkIdx int
240 }
241
242 func (fsc *fullStackCache) Get(c context.Context, key []byte) (datastorecache.Va lue, error) {
243 if err := fsc.err; err != nil {
244 return datastorecache.Value{}, err
245 }
246 return fsc.cache.Get(c, key)
247 }
248
249 func (fsc *fullStackCache) setCacheErr(err error) { fsc.err = err }
250
251 func (fsc *fullStackCache) setProjectDNE(project string) {
252 key := "projects/" + project
253 for k := range fsc.data {
254 if k == key || strings.HasPrefix(k, key+"/") {
255 delete(fsc.data, k)
256 }
257 }
258 }
259
260 func (fsc *fullStackCache) addConfig(configSet, path, content string) *config.It em {
261 cset := fsc.data[configSet]
262 if cset == nil {
263 cset = memConfig.ConfigSet{}
264 fsc.data[configSet] = cset
265 }
266 if content == "" {
267 delete(cset, path)
268 return nil
269 }
270 cset[path] = content
271
272 // Pull the config right back out of the base service.
273 item, err := fsc.backend.Get(context.Background(), configSet, path, conf ig.Params{
274 Authority: config.AsService,
275 })
276 if err != nil {
277 panic(err)
278 }
279 return item
280 }
281
282 // addProjectConfig caches a "project.cfg" file for the specified project with
283 // the specified access string.
284 func (fsc *fullStackCache) addProjectConfig(name commonConfig.ProjectName, acces s string) {
285 fsc.addConfig(config.ProjectConfigSet(name), config.ProjectConfigPath,
286 proto.MarshalTextString(projectConfigWithAccess(name, access)))
287 }
288
289 func (fsc *fullStackCache) addConfigSets(path string, configSets ...string) []st ring {
290 sort.Strings(configSets)
291 items := make([]*config.Item, len(configSets))
292 for i, cs := range configSets {
293 items[i] = fsc.addConfig(cs, path, cs)
294 }
295 return configSets
296 }
297
298 func (fsc *fullStackCache) addConfigSetURL(configSet string) string {
299 if _, ok := fsc.data[configSet]; !ok {
300 fsc.data[configSet] = memConfig.ConfigSet{}
301 }
302
303 // We're pretty rigid here. Whatever our backend returns is all we can
304 // return. We will just assert that anything more flexible has to confor m to
305 // this.
306 v, err := fsc.backend.ConfigSetURL(context.Background(), config.AsServic e, configSet)
307 if err != nil {
308 panic(err)
309 }
310 return v.String()
311 }
312
313 // stripMeta strips cache-specific identifying information from a set of Metas.
314 func stripMeta(metas []*config.Meta) []*config.Meta {
315 for _, meta := range metas {
316 meta.ContentHash = ""
317 meta.Revision = ""
318 }
319 return metas
320 }
321
322 func testDatastoreCacheImpl(c context.Context, backend config.Backend, cache tes tCache) {
323 // Install fake auth state.
324 var authState authtest.FakeState
325 c = auth.WithState(c, &authState)
326 authState.Identity = "user:person@example.com"
327 authState.IdentityGroups = []string{"users"}
328
329 dsc := datastoreCache{
330 refreshInterval: 1 * time.Hour,
331 failOpen: false,
332 cache: cache,
333 }
334 c = config.WithBackend(c, dsc.getBackend(backend))
335
336 testErr := errors.New("test error")
337
338 Convey(`Test Get`, func() {
339 var v string
340
341 Convey(`Config missing`, func() {
342 cache.addConfig("projects/test", "foo", "")
343
344 So(config.Get(c, config.AsService, "projects/test", "foo ", config.String(&v), nil),
345 ShouldEqual, config.ErrNoConfig)
346 })
347
348 Convey(`Config is present`, func() {
349 cache.addConfig("projects/test", "foo", "bar")
350 cache.addProjectConfig("test", "group:privileged")
351
352 Convey(`As service`, func() {
353 So(config.Get(c, config.AsService, "projects/tes t", "foo", config.String(&v), nil), ShouldBeNil)
354 So(v, ShouldEqual, "bar")
355 })
356
357 Convey(`As user, when not a project group member, fails with ErrNoConfig`, func() {
358 So(config.Get(c, config.AsUser, "projects/test", "foo", config.String(&v), nil),
359 ShouldEqual, config.ErrNoConfig)
360 })
361
362 Convey(`As user, when a project group member, succeeds.` , func() {
363 authState.IdentityGroups = append(authState.Iden tityGroups, "privileged")
364 So(config.Get(c, config.AsUser, "projects/test", "foo", config.String(&v), nil), ShouldBeNil)
365 So(v, ShouldEqual, "bar")
366 })
367
368 Convey(`As anonymous, fails with ErrNoConfig`, func() {
369 So(config.Get(c, config.AsAnonymous, "projects/t est", "foo", config.String(&v), nil),
370 ShouldEqual, config.ErrNoConfig)
371 })
372 })
373 })
374
375 Convey(`Test GetAll with projects`, func() {
376 var v []string
377 var meta []*config.Meta
378
379 Convey(`When cache returns an error`, func() {
380 cache.setCacheErr(testErr)
381
382 So(config.GetAll(c, config.AsService, config.Project, "t est.cfg", config.StringSlice(&v), nil),
383 ShouldUnwrapTo, testErr)
384 So(config.GetAll(c, config.AsUser, config.Project, "test .cfg", config.StringSlice(&v), nil),
385 ShouldUnwrapTo, testErr)
386 So(config.GetAll(c, config.AsAnonymous, config.Project, "test.cfg", config.StringSlice(&v), nil),
387 ShouldUnwrapTo, testErr)
388 })
389
390 Convey(`With project configs installed`, func() {
391 allConfigs := cache.addConfigSets("test.cfg",
392 "projects/bar",
393 "projects/baz",
394 "projects/foo")
395
396 Convey(`As service, retrieves all configs.`, func() {
397 So(config.GetAll(c, config.AsService, config.Pro ject, "test.cfg", config.StringSlice(&v), &meta), ShouldBeNil)
398 So(v, ShouldResemble, allConfigs)
399 So(stripMeta(meta), ShouldResemble, []*config.Me ta{
400 {ConfigSet: "projects/bar", Path: "test. cfg"},
401 {ConfigSet: "projects/baz", Path: "test. cfg"},
402 {ConfigSet: "projects/foo", Path: "test. cfg"},
403 })
404 })
405
406 Convey(`As user`, func() {
407 Convey(`Not a member of any projects, receives e mpty slice.`, func() {
408 cache.addProjectConfig("foo", "group:som eone")
409
410 So(config.GetAll(c, config.AsUser, confi g.Project, "test.cfg", config.StringSlice(&v), &meta), ShouldBeNil)
411 So(v, ShouldResemble, []string(nil))
412 So(stripMeta(meta), ShouldResemble, []*c onfig.Meta{})
413 })
414
415 Convey(`Member of "foo", gets only "foo".`, func () {
416 cache.addProjectConfig("foo", "group:use rs")
417
418 So(config.GetAll(c, config.AsUser, confi g.Project, "test.cfg", config.StringSlice(&v), &meta), ShouldBeNil)
419 So(v, ShouldResemble, allConfigs[2:3])
420 So(stripMeta(meta), ShouldResemble, []*c onfig.Meta{
421 {ConfigSet: "projects/foo", Path : "test.cfg"},
422 })
423 })
424
425 Convey(`Member of all projects, gets all project s.`, func() {
426 cache.addProjectConfig("foo", "group:use rs")
427 cache.addProjectConfig("bar", "group:use rs")
428 cache.addProjectConfig("baz", "group:use rs")
429
430 So(config.GetAll(c, config.AsUser, confi g.Project, "test.cfg", config.StringSlice(&v), &meta), ShouldBeNil)
431 So(v, ShouldResemble, allConfigs)
432 So(stripMeta(meta), ShouldResemble, []*c onfig.Meta{
433 {ConfigSet: "projects/bar", Path : "test.cfg"},
434 {ConfigSet: "projects/baz", Path : "test.cfg"},
435 {ConfigSet: "projects/foo", Path : "test.cfg"},
436 })
437 })
438 })
439 })
440 })
441
442 Convey(`Test GetAll with refs`, func() {
443 var v []string
444 var meta []*config.Meta
445
446 Convey(`When cache returns an error`, func() {
447 cache.setCacheErr(testErr)
448
449 So(config.GetAll(c, config.AsService, config.Ref, "test. cfg", config.StringSlice(&v), nil),
450 ShouldUnwrapTo, testErr)
451 So(config.GetAll(c, config.AsUser, config.Ref, "test.cfg ", config.StringSlice(&v), nil),
452 ShouldUnwrapTo, testErr)
453 So(config.GetAll(c, config.AsAnonymous, config.Ref, "tes t.cfg", config.StringSlice(&v), nil),
454 ShouldUnwrapTo, testErr)
455 })
456
457 Convey(`With ref configs installed`, func() {
458 allConfigs := cache.addConfigSets("test.cfg",
459 "projects/bar/refs/branches/mybranch",
460 "projects/bar/refs/heads/master",
461 "projects/foo/refs/branches/mybranch",
462 "projects/foo/refs/heads/master")
463
464 Convey(`As service, retrieves all configs.`, func() {
465 So(config.GetAll(c, config.AsService, config.Ref , "test.cfg", config.StringSlice(&v), &meta), ShouldBeNil)
466 So(v, ShouldResemble, allConfigs)
467 So(stripMeta(meta), ShouldResemble, []*config.Me ta{
468 {ConfigSet: "projects/bar/refs/branches/ mybranch", Path: "test.cfg"},
469 {ConfigSet: "projects/bar/refs/heads/mas ter", Path: "test.cfg"},
470 {ConfigSet: "projects/foo/refs/branches/ mybranch", Path: "test.cfg"},
471 {ConfigSet: "projects/foo/refs/heads/mas ter", Path: "test.cfg"},
472 })
473 })
474
475 Convey(`As user`, func() {
476 Convey(`Not a member of any projects, receives e mpty slice.`, func() {
477 cache.addProjectConfig("foo", "group:som eone")
478
479 So(config.GetAll(c, config.AsUser, confi g.Ref, "test.cfg", config.StringSlice(&v), &meta), ShouldBeNil)
480 So(v, ShouldResemble, []string(nil))
481 So(stripMeta(meta), ShouldResemble, []*c onfig.Meta{})
482 })
483
484 Convey(`Member of "foo", gets only "foo".`, func () {
485 cache.addProjectConfig("foo", "group:use rs")
486
487 So(config.GetAll(c, config.AsUser, confi g.Ref, "test.cfg", config.StringSlice(&v), &meta), ShouldBeNil)
488 So(v, ShouldResemble, allConfigs[2:4])
489 So(stripMeta(meta), ShouldResemble, []*c onfig.Meta{
490 {ConfigSet: "projects/foo/refs/b ranches/mybranch", Path: "test.cfg"},
491 {ConfigSet: "projects/foo/refs/h eads/master", Path: "test.cfg"},
492 })
493 })
494
495 Convey(`Member of all projects, gets all project s.`, func() {
496 cache.addProjectConfig("foo", "group:use rs")
497 cache.addProjectConfig("bar", "group:use rs")
498
499 So(config.GetAll(c, config.AsUser, confi g.Ref, "test.cfg", config.StringSlice(&v), &meta), ShouldBeNil)
500 So(v, ShouldResemble, allConfigs)
501 So(stripMeta(meta), ShouldResemble, []*c onfig.Meta{
502 {ConfigSet: "projects/bar/refs/b ranches/mybranch", Path: "test.cfg"},
503 {ConfigSet: "projects/bar/refs/h eads/master", Path: "test.cfg"},
504 {ConfigSet: "projects/foo/refs/b ranches/mybranch", Path: "test.cfg"},
505 {ConfigSet: "projects/foo/refs/h eads/master", Path: "test.cfg"},
506 })
507 })
508 })
509 })
510 })
511
512 Convey(`Test ConfigSetURL`, func() {
513 cache.addProjectConfig("foo", "group:someone")
514 csURL := cache.addConfigSetURL("projects/foo")
515
516 Convey(`AsService`, func() {
517 u, err := config.GetConfigSetURL(c, config.AsService, "p rojects/foo")
518 So(err, ShouldBeNil)
519 So(u.String(), ShouldEqual, csURL)
520 })
521
522 Convey(`AsUser`, func() {
523
524 Convey(`When not a member of the group.`, func() {
525 _, err := config.GetConfigSetURL(c, config.AsUse r, "projects/foo")
526 So(err, ShouldEqual, config.ErrNoConfig)
527 })
528
529 Convey(`When a member of the group.`, func() {
530 authState.IdentityGroups = append(authState.Iden tityGroups, "someone")
531
532 u, err := config.GetConfigSetURL(c, config.AsUse r, "projects/foo")
533 So(err, ShouldBeNil)
534 So(u.String(), ShouldEqual, csURL)
535 })
536 })
537
538 Convey(`AsAnonymous`, func() {
539 // Log in just to show that, while AsUser could access, AsAnonymous
540 // cannot.
541 authState.IdentityGroups = append(authState.IdentityGrou ps, "someone")
542 _, err := config.GetConfigSetURL(c, config.AsUser, "proj ects/foo")
543 So(err, ShouldBeNil)
544
545 _, err = config.GetConfigSetURL(c, config.AsAnonymous, " projects/foo")
546 So(err, ShouldEqual, config.ErrNoConfig)
547 })
548 })
549 }
550
551 func TestDatastoreCache(t *testing.T) {
552 t.Parallel()
553
554 Convey(`Testing with in-memory stub cache`, t, func() {
555 c := context.Background()
556 fc := mkFakeCache()
557
558 var backend config.Backend
559 backend = &config.ClientBackend{
560 Provider: &testconfig.LocalClientProvider{
561 Base: memConfig.New(nil),
562 },
563 }
564
565 fr := defaultFormatterRegistry()
566 backend = &config.FormatBackend{
567 Backend: backend,
568 GetRegistry: func(context.Context) *config.FormatterRegi stry { return fr },
569 }
570
571 Convey(`Standard datastore tests`, func() {
572 testDatastoreCacheImpl(c, backend, fc)
573 })
574
575 Convey(`A testing setup built around the fake cache`, func() {
576 dsc := datastoreCache{
577 refreshInterval: 1 * time.Hour,
578 failOpen: false,
579 cache: fc,
580 }
581 c = config.WithBackend(c, dsc.getBackend(backend))
582
583 Convey(`Errors with different schema.`, func() {
584 fc.addConfig("foo", "bar", "value")
585 for k, v := range fc.d {
586 v.Schema = "unknown"
587 fc.d[k] = v
588 }
589
590 var v string
591 So(config.Get(c, config.AsService, "foo", "bar", config.String(&v), nil),
592 ShouldErrLike, `response schema ("unknow n") doesn't match current`)
593 })
594 })
595 })
596 }
597
598 func TestDatastoreCacheFullStack(t *testing.T) {
599 t.Parallel()
600
601 Convey(`Testing full-stack datastore cache`, t, func() {
602 c := memory.Use(context.Background())
603
604 data := map[string]memConfig.ConfigSet{}
605
606 var backend config.Backend
607 backend = &config.ClientBackend{
608 Provider: &testconfig.LocalClientProvider{
609 Base: memConfig.New(data),
610 },
611 }
612
613 fr := defaultFormatterRegistry()
614 backend = &config.FormatBackend{
615 Backend: backend,
616 GetRegistry: func(context.Context) *config.FormatterRegi stry { return fr },
617 }
618
619 fsc := fullStackCache{
620 cache: &dsCache,
621 data: data,
622 backend: backend,
623 }
624 testDatastoreCacheImpl(c, backend, &fsc)
625 })
626 }
627
628 func TestDatastoreCacheIntegration(t *testing.T) {
629 t.Parallel()
630
631 Convey(`A testing environment with fake cache and config service`, t, fu nc() {
632 c, clk := testclock.UseTime(context.Background(), datastore.Roun dTime(testclock.TestTimeUTC))
633
634 c = memory.Use(c)
635
636 // Install fake auth state.
637 var authState authtest.FakeState
638 c = auth.WithState(c, &authState)
639 authState.Identity = "user:person@example.com"
640 authState.IdentityGroups = []string{"all", "users"}
641
642 // Use a memory-backed Config service instance.
643 baseMap := map[string]memConfig.ConfigSet{
644 "projects/open": map[string]string{
645 "project.cfg": proto.MarshalTextString(projectCo nfigWithAccess("foo", "group:all")),
646 "test.cfg": "Test Config Content",
647 },
648 "projects/foo": map[string]string{
649 "project.cfg": proto.MarshalTextString(projectCo nfigWithAccess("foo", "group:restricted")),
650 },
651 "projects/noconfig": map[string]string{},
652 "projects/invalidconfig": map[string]string{
653 "project.cfg": "!!! not a valid config !!!",
654 },
655 "projects/foo/refs/heads/master": map[string]string{
656 "ref.cfg": "foo",
657 },
658 "projects/foo/refs/branches/bar": map[string]string{
659 "ref.cfg": "foo/bar",
660 },
661 "projects/bar/refs/heads/master": map[string]string{
662 "ref.cfg": "bar",
663 },
664 }
665 base := memConfig.New(baseMap)
666 backend := &config.ClientBackend{
667 Provider: &testconfig.LocalClientProvider{
668 Base: base,
669 },
670 }
671
672 // Install our settings into a memory-backed Settings storage.
673 memSettings := settings.MemoryStorage{}
674 c = settings.Use(c, settings.New(&memSettings))
675 s := Settings{
676 CacheExpirationSec: 10,
677 DatastoreCacheMode: dsCacheStrict,
678 }
679 putSettings := func() {
680 if err := settings.GetSettings(c).Set(c, settingsKey, &s , "test harness", "initial settings"); err != nil {
681 panic(err)
682 }
683 }
684 putSettings()
685
686 // Install our middleware chain into our Router.
687 baseMW := router.NewMiddlewareChain(func(ctx *router.Context, ne xt router.Handler) {
688 ctx.Context = c
689 next(ctx)
690 })
691 rtr := router.New()
692 server := httptest.NewServer(rtr)
693 defer server.Close()
694
695 // Install our Manager cron handlers.
696 installCacheCronHandlerImpl(rtr, baseMW, backend)
697
698 runCron := func() int {
699 datastore.GetTestable(c).CatchupIndexes()
700 resp, err := http.Get(fmt.Sprintf("%s/admin/config/cache /manager", server.URL))
701 if err != nil {
702 panic(fmt.Errorf("failed to GET: %s", err))
703 }
704 return resp.StatusCode
705 }
706 So(runCron(), ShouldEqual, http.StatusOK)
707
708 // Called when all parameters are in place to install our config service
709 // layers into "c".
710 installConfig := func(c context.Context) context.Context { retur n useImpl(c, backend) }
711
712 loadProjectConfigs := func(c context.Context, a config.Authority ) (projs []string, meta []*config.Meta, err error) {
713 err = config.GetAll(c, a, config.Project, "project.cfg",
714 config.StringSlice(&projs), &meta)
715 return
716 }
717
718 Convey(`Cache modes`, func() {
719 // Load all of our project configs and metadata. Load di rectly from our
720 // non-datastore Backend, since this data is not part of the test.
721 allProjs, _, err := loadProjectConfigs(config.WithBacken d(c, backend), config.AsService)
722 if err != nil {
723 panic(err)
724 }
725
726 Convey(`Disabled, skips datastore cache.`, func() {
727 s.DatastoreCacheMode = dsCacheDisabled
728 putSettings()
729
730 c = installConfig(c)
731 c, cnt := count.FilterRDS(c)
732
733 projs, _, err := loadProjectConfigs(c, config.As Service)
734 So(err, ShouldBeNil)
735 So(projs, ShouldResemble, allProjs)
736
737 // No datastore operations should have been perf ormed.
738 So(cnt.GetMulti.Total(), ShouldEqual, 0)
739 So(cnt.PutMulti.Total(), ShouldEqual, 0)
740 })
741
742 Convey(`Enabled`, func() {
743 s.DatastoreCacheMode = dsCacheEnabled
744 putSettings()
745
746 c = installConfig(c)
747 c, cnt := count.FilterRDS(c)
748 projs, _, err := loadProjectConfigs(c, config.As Service)
749 So(err, ShouldBeNil)
750 So(projs, ShouldResemble, allProjs)
751
752 So(cnt.GetMulti.Total(), ShouldEqual, 1)
753 So(cnt.PutMulti.Total(), ShouldEqual, 1)
754
755 // Break our backing store. This forces lookups to load from cache.
756 memConfig.SetError(base, errors.New("config is b roken"))
757
758 projs, _, err = loadProjectConfigs(c, config.AsS ervice)
759 So(err, ShouldBeNil)
760 So(projs, ShouldResemble, allProjs)
761
762 So(cnt.GetMulti.Total(), ShouldEqual, 2)
763 So(cnt.PutMulti.Total(), ShouldEqual, 1)
764
765 // Expire the cache.
766 clk.Add(time.Hour)
767
768 projs, _, err = loadProjectConfigs(c, config.AsS ervice)
769 So(err, ShouldBeNil)
770 So(projs, ShouldResemble, allProjs)
771
772 So(cnt.GetMulti.Total(), ShouldEqual, 3)
773 So(cnt.PutMulti.Total(), ShouldEqual, 1)
774 })
775
776 Convey(`Strict`, func() {
777 s.DatastoreCacheMode = dsCacheStrict
778 putSettings()
779
780 c = installConfig(c)
781 c, cnt := count.FilterRDS(c)
782 projs, _, err := loadProjectConfigs(c, config.As Service)
783 So(err, ShouldBeNil)
784 So(projs, ShouldResemble, allProjs)
785
786 So(cnt.GetMulti.Total(), ShouldEqual, 1)
787 So(cnt.PutMulti.Total(), ShouldEqual, 1)
788
789 // Break our backing store. This forces lookups to load from cache.
790 memConfig.SetError(base, errors.New("config is b roken"))
791
792 projs, _, err = loadProjectConfigs(c, config.AsS ervice)
793 So(err, ShouldBeNil)
794 So(projs, ShouldResemble, allProjs)
795
796 So(cnt.GetMulti.Total(), ShouldEqual, 2)
797 So(cnt.PutMulti.Total(), ShouldEqual, 1)
798
799 // Expire the cache. Now that the entries are ex pired, we will
800 // fail-closed to the backing store and return " config is broken".
801 clk.Add(time.Hour)
802
803 projs, _, err = loadProjectConfigs(c, config.AsS ervice)
804 So(err, ShouldUnwrapTo, datastorecache.ErrCacheE xpired)
805
806 So(cnt.GetMulti.Total(), ShouldEqual, 3)
807 So(cnt.PutMulti.Total(), ShouldEqual, 1)
808 })
809 })
810
811 Convey(`A user, denied access to a project, gains access once pr oject config refreshes.`, func() {
812 c = installConfig(c)
813
814 _, metas, err := loadProjectConfigs(c, config.AsUser)
815 So(err, ShouldBeNil)
816 So(stripMeta(metas), ShouldResemble, []*config.Meta{
817 {ConfigSet: "projects/open", Path: "project.cfg" },
818 })
819
820 // Update "foo" project ACLs to include the "users" grou p.
821 baseMap["projects/foo"]["project.cfg"] = proto.MarshalTe xtString(projectConfigWithAccess("foo", "group:users"))
822
823 // Still denied access (cached).
824 c = installConfig(c)
825 _, metas, err = loadProjectConfigs(c, config.AsUser)
826 So(err, ShouldBeNil)
827 So(stripMeta(metas), ShouldResemble, []*config.Meta{
828 {ConfigSet: "projects/open", Path: "project.cfg" },
829 })
830
831 // Expire cache.
832 clk.Add(time.Hour)
833 _, _, err = loadProjectConfigs(c, config.AsUser)
834 So(err, ShouldUnwrapTo, datastorecache.ErrCacheExpired)
835
836 // Update our cache entries.
837 So(runCron(), ShouldEqual, http.StatusOK)
838
839 // Granted access!
840 c = installConfig(c)
841 _, metas, err = loadProjectConfigs(c, config.AsUser)
842 So(err, ShouldBeNil)
843 So(stripMeta(metas), ShouldResemble, []*config.Meta{
844 {ConfigSet: "projects/foo", Path: "project.cfg"} ,
845 {ConfigSet: "projects/open", Path: "project.cfg" },
846 })
847 })
848
849 Convey(`GetConfig iterative updates`, func() {
850 c = installConfig(c)
851
852 get := func(c context.Context) (v string, meta config.Me ta, err error) {
853 err = config.Get(c, config.AsService, "projects/ open", "test.cfg", config.String(&v), &meta)
854 return
855 }
856
857 baseContentHash := func(configSet, config string) string {
858 cfg, err := base.GetConfig(c, configSet, config, true)
859 if err != nil {
860 panic(err)
861 }
862 return cfg.ContentHash
863 }
864
865 _, meta, err := get(c)
866 So(err, ShouldBeNil)
867 So(meta.ContentHash, ShouldEqual, baseContentHash("proje cts/open", "test.cfg"))
868
869 Convey(`When the content changes, the hash is updated.`, func() {
870 // Change content, hash should not have changed.
871 baseMap["projects/open"]["test.cfg"] = "New Cont ent!"
872 _, meta2, err := get(c)
873 So(err, ShouldBeNil)
874 So(meta2.ContentHash, ShouldEqual, meta.ContentH ash)
875
876 // Expire the cache entry and refresh.
877 clk.Add(time.Hour)
878 So(runCron(), ShouldEqual, http.StatusOK)
879
880 // Re-get the config. The content hash should ha ve changed.
881 _, meta, err = get(c)
882 So(err, ShouldBeNil)
883 So(meta.ContentHash, ShouldEqual, baseContentHas h("projects/open", "test.cfg"))
884 So(meta.ContentHash, ShouldNotEqual, meta2.Conte ntHash)
885 })
886
887 Convey(`When the content is deleted, returns ErrNoConfig .`, func() {
888 // Change content, hash should not have changed.
889 delete(baseMap["projects/open"], "test.cfg")
890 _, meta2, err := get(c)
891 So(err, ShouldBeNil)
892 So(meta2.ContentHash, ShouldEqual, meta.ContentH ash)
893
894 // Expire the cache entry and refresh.
895 clk.Add(time.Hour)
896 So(runCron(), ShouldEqual, http.StatusOK)
897
898 _, _, err = get(c)
899 So(err, ShouldEqual, config.ErrNoConfig)
900 })
901 })
902
903 // Load project config, return a slice of hashes.
904 getHashes := func(c context.Context, t config.GetAllType, path s tring) ([]string, error) {
905 var (
906 content []string
907 metas []*config.Meta
908 )
909 err := config.GetAll(c, config.AsService, t, path, confi g.StringSlice(&content), &metas)
910 if err != nil {
911 return nil, err
912 }
913
914 hashes := make([]string, len(metas))
915 for i, meta := range metas {
916 hashes[i] = meta.ContentHash
917 }
918 return hashes, nil
919 }
920
921 // Return project config hashes from backing memory config store .
922 baseContentHashes := func(t config.GetAllType, path string) []st ring {
923 var fn func(context.Context, string, bool) ([]commonConf ig.Config, error)
924 switch t {
925 case config.Project:
926 fn = base.GetProjectConfigs
927 case config.Ref:
928 fn = base.GetRefConfigs
929 }
930 cfgs, err := fn(c, path, true)
931 if err != nil {
932 panic(err)
933 }
934 hashes := make([]string, len(cfgs))
935 for i := range cfgs {
936 hashes[i] = cfgs[i].ContentHash
937 }
938 return hashes
939 }
940
941 Convey(`GetAll for project iterative updates`, func() {
942 c = installConfig(c)
943
944 hashes, err := getHashes(c, config.Project, "project.cfg ")
945 So(err, ShouldBeNil)
946 So(hashes, ShouldResemble, baseContentHashes(config.Proj ect, "project.cfg"))
947
948 Convey(`When the project list doesn't change, is not re- fetched.`, func() {
949 // Expire the cache entry and refresh.
950 clk.Add(time.Hour)
951 So(runCron(), ShouldEqual, http.StatusOK)
952
953 hashes2, err := getHashes(c, config.Project, "pr oject.cfg")
954 So(err, ShouldBeNil)
955 So(hashes2, ShouldResemble, hashes)
956 })
957
958 Convey(`When the project changes, is re-fetched.`, func( ) {
959 delete(baseMap["projects/invalidconfig"], "proje ct.cfg")
960
961 // Expire the cache entry and refresh.
962 clk.Add(time.Hour)
963 So(runCron(), ShouldEqual, http.StatusOK)
964
965 hashes2, err := getHashes(c, config.Project, "pr oject.cfg")
966 So(err, ShouldBeNil)
967 So(hashes2, ShouldResemble, baseContentHashes(co nfig.Project, "project.cfg"))
968 So(hashes2, ShouldNotResemble, hashes)
969 })
970 })
971
972 Convey(`GetAll for ref iterative updates`, func() {
973 c = installConfig(c)
974
975 hashes, err := getHashes(c, config.Ref, "ref.cfg")
976 So(err, ShouldBeNil)
977 So(hashes, ShouldResemble, baseContentHashes(config.Ref, "ref.cfg"))
978
979 Convey(`When the ref list doesn't change, is not re-fetc hed.`, func() {
980 // Expire the cache entry and refresh.
981 clk.Add(time.Hour)
982 So(runCron(), ShouldEqual, http.StatusOK)
983
984 hashes2, err := getHashes(c, config.Ref, "ref.cf g")
985 So(err, ShouldBeNil)
986 So(hashes2, ShouldResemble, hashes)
987 })
988
989 Convey(`When the ref changes, is re-fetched.`, func() {
990 delete(baseMap["projects/foo/refs/branches/bar"] , "ref.cfg")
991
992 // Expire the cache entry and refresh.
993 clk.Add(time.Hour)
994 So(runCron(), ShouldEqual, http.StatusOK)
995
996 hashes2, err := getHashes(c, config.Ref, "ref.cf g")
997 So(err, ShouldBeNil)
998 So(hashes2, ShouldResemble, baseContentHashes(co nfig.Ref, "ref.cfg"))
999 So(hashes2, ShouldNotResemble, hashes)
1000 })
1001 })
1002
1003 Convey(`Test GetConfigSetURL user fetch`, func() {
1004 c = installConfig(c)
1005
1006 // Project does not exist, missing for user and service.
1007 _, err := config.GetConfigSetURL(c, config.AsUser, "proj ects/urltest")
1008 So(err, ShouldEqual, config.ErrNoConfig)
1009 _, err = config.GetConfigSetURL(c, config.AsService, "pr ojects/urltest")
1010 So(err, ShouldEqual, config.ErrNoConfig)
1011
1012 // Add the project, still missing b/c of cache.
1013 baseMap["projects/urltest"] = memConfig.ConfigSet{
1014 "project.cfg": proto.MarshalTextString(projectCo nfigWithAccess("foo", "group:exclusive")),
1015 }
1016
1017 _, err = config.GetConfigSetURL(c, config.AsUser, "proje cts/urltest")
1018 So(err, ShouldEqual, config.ErrNoConfig)
1019 _, err = config.GetConfigSetURL(c, config.AsService, "pr ojects/urltest")
1020 So(err, ShouldEqual, config.ErrNoConfig)
1021
1022 // Expire the cache entry and refresh. Next project load will pick up
1023 // the new project.
1024 clk.Add(time.Hour)
1025 So(runCron(), ShouldEqual, http.StatusOK)
1026
1027 // User still cannot access (not member of group).
1028 _, err = config.GetConfigSetURL(c, config.AsUser, "proje cts/urltest")
1029 So(err, ShouldEqual, config.ErrNoConfig)
1030
1031 // Service can access.
1032 u, err := config.GetConfigSetURL(c, config.AsService, "p rojects/urltest")
1033 So(err, ShouldBeNil)
1034 So(u.String(), ShouldEqual, "https://example.com/fake-co nfig/projects/urltest")
1035
1036 // User joins group, immediately gets access b/c the cac hed entry is the
1037 // service response, and so was soft-forbidden before.
1038 authState.IdentityGroups = append(authState.IdentityGrou ps, "exclusive")
1039
1040 _, err = config.GetConfigSetURL(c, config.AsUser, "proje cts/urltest")
1041 So(err, ShouldBeNil)
1042 So(u.String(), ShouldEqual, "https://example.com/fake-co nfig/projects/urltest")
1043 })
1044 })
1045 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698