| OLD | NEW |
| (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 } |
| OLD | NEW |