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