go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/appengine/coordinator/coordinatorTest/context.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package coordinatorTest 16 17 import ( 18 "context" 19 "time" 20 21 "google.golang.org/protobuf/types/known/durationpb" 22 23 "go.chromium.org/luci/auth/identity" 24 "go.chromium.org/luci/common/clock" 25 "go.chromium.org/luci/common/clock/testclock" 26 "go.chromium.org/luci/common/data/caching/cacheContext" 27 "go.chromium.org/luci/common/data/rand/cryptorand" 28 "go.chromium.org/luci/common/gcloud/gs" 29 "go.chromium.org/luci/common/logging" 30 "go.chromium.org/luci/common/logging/gologger" 31 "go.chromium.org/luci/common/logging/memlogger" 32 "go.chromium.org/luci/config" 33 "go.chromium.org/luci/config/cfgclient" 34 "go.chromium.org/luci/config/impl/memory" 35 "go.chromium.org/luci/config/impl/resolving" 36 "go.chromium.org/luci/config/vars" 37 "go.chromium.org/luci/logdog/api/config/svcconfig" 38 "go.chromium.org/luci/logdog/appengine/coordinator" 39 "go.chromium.org/luci/logdog/appengine/coordinator/flex" 40 "go.chromium.org/luci/logdog/common/storage/archive" 41 "go.chromium.org/luci/logdog/common/storage/bigtable" 42 logdogcfg "go.chromium.org/luci/logdog/server/config" 43 "go.chromium.org/luci/server/auth" 44 "go.chromium.org/luci/server/auth/authtest" 45 "go.chromium.org/luci/server/auth/realms" 46 "go.chromium.org/luci/server/caching" 47 48 gaeMemory "go.chromium.org/luci/gae/impl/memory" 49 ds "go.chromium.org/luci/gae/service/datastore" 50 51 "github.com/golang/protobuf/proto" 52 ) 53 54 // Environment contains all of the testing facilities that are installed into 55 // the Context. 56 type Environment struct { 57 // ServiceID is LogDog's service ID for tests. 58 ServiceID string 59 60 // Clock is the installed test clock instance. 61 Clock testclock.TestClock 62 63 // AuthState is the fake authentication state. 64 AuthState authtest.FakeState 65 66 // Services is the set of installed Coordinator services. 67 Services Services 68 69 // BigTable in-memory testing instance. 70 BigTable bigtable.Testing 71 // GSClient is the test GSClient instance installed (by default) into 72 // Services. 73 GSClient GSClient 74 75 // StorageCache is the default storage cache instance. 76 StorageCache StorageCache 77 78 // config is the luci-config configuration map that is installed. 79 config map[config.Set]memory.Files 80 // syncConfig moves configs in `config` into the datastore. 81 syncConfig func() 82 } 83 84 // ActAsAnon mocks the auth state to indicate an anonymous caller. 85 // 86 // It has no access to any project. 87 func (e *Environment) ActAsAnon() { 88 e.AuthState.Identity = identity.AnonymousIdentity 89 e.AuthState.IdentityGroups = nil 90 e.AuthState.IdentityPermissions = nil 91 } 92 93 // ActAsNobody mocks the auth state to indicate it's some unknown user calling. 94 // 95 // It has no access to any project. 96 func (e *Environment) ActAsNobody() { 97 e.AuthState.Identity = "user:nodoby@example.com" 98 e.AuthState.IdentityGroups = nil 99 e.AuthState.IdentityPermissions = nil 100 } 101 102 // ActAsService mocks the auth state to indicate it's a service calling. 103 func (e *Environment) ActAsService() { 104 e.AuthState.Identity = "user:services@example.com" 105 e.AuthState.IdentityGroups = []string{"services"} 106 e.AuthState.IdentityPermissions = nil 107 } 108 109 // ActAsWriter mocks the auth state to indicate it's a prefix writer calling. 110 func (e *Environment) ActAsWriter(project, realm string) { 111 e.AuthState.Identity = "user:client@example.com" 112 e.AuthState.IdentityGroups = nil 113 e.AuthState.IdentityPermissions = []authtest.RealmPermission{ 114 { 115 Realm: realms.Join(project, realm), 116 Permission: coordinator.PermLogsGet, 117 }, 118 { 119 Realm: realms.Join(project, realm), 120 Permission: coordinator.PermLogsList, 121 }, 122 { 123 Realm: realms.Join(project, realm), 124 Permission: coordinator.PermLogsCreate, 125 }, 126 } 127 } 128 129 // ActAsReader mocks the auth state to indicate it's a prefix reader calling. 130 func (e *Environment) ActAsReader(project, realm string) { 131 e.AuthState.Identity = "user:client@example.com" 132 e.AuthState.IdentityGroups = nil 133 e.AuthState.IdentityPermissions = []authtest.RealmPermission{ 134 { 135 Realm: realms.Join(project, realm), 136 Permission: coordinator.PermLogsGet, 137 }, 138 { 139 Realm: realms.Join(project, realm), 140 Permission: coordinator.PermLogsList, 141 }, 142 } 143 } 144 145 // JoinAdmins adds the current caller to the administrators group. 146 func (e *Environment) JoinAdmins() { 147 e.AuthState.IdentityGroups = append(e.AuthState.IdentityGroups, "admin") 148 } 149 150 // JoinServices adds the current caller to the services group. 151 func (e *Environment) JoinServices() { 152 e.AuthState.IdentityGroups = append(e.AuthState.IdentityGroups, "services") 153 } 154 155 // ModServiceConfig loads the current service configuration, invokes the 156 // callback with its contents, and writes the result back to config. 157 func (e *Environment) ModServiceConfig(c context.Context, fn func(*svcconfig.Config)) { 158 var cfg svcconfig.Config 159 e.modTextProtobuf(c, config.MustServiceSet(e.ServiceID), "services.cfg", &cfg, func() { 160 fn(&cfg) 161 }) 162 } 163 164 // ModProjectConfig loads the current configuration for the named project, 165 // invokes the callback with its contents, and writes the result back to config. 166 func (e *Environment) ModProjectConfig(c context.Context, project string, fn func(*svcconfig.ProjectConfig)) { 167 var pcfg svcconfig.ProjectConfig 168 e.modTextProtobuf(c, config.MustProjectSet(project), e.ServiceID+".cfg", &pcfg, func() { 169 fn(&pcfg) 170 }) 171 } 172 173 // AddProject ensures there's a config for the given project. 174 func (e *Environment) AddProject(c context.Context, project string) { 175 e.ModProjectConfig(c, project, func(*svcconfig.ProjectConfig) {}) 176 } 177 178 func (e *Environment) modTextProtobuf(c context.Context, configSet config.Set, path string, 179 msg proto.Message, fn func()) { 180 existing := e.config[configSet][path] 181 if existing != "" { 182 if err := proto.UnmarshalText(existing, msg); err != nil { 183 panic(err) 184 } 185 } 186 fn() 187 e.addConfigEntry(configSet, path, proto.MarshalTextString(msg)) 188 } 189 190 func (e *Environment) addConfigEntry(configSet config.Set, path, content string) { 191 cset := e.config[configSet] 192 if cset == nil { 193 cset = make(memory.Files) 194 e.config[configSet] = cset 195 } 196 cset[path] = content 197 e.syncConfig() 198 } 199 200 // Install creates a testing Context and installs common test facilities into 201 // it, returning the Environment to which they're bound. 202 func Install() (context.Context, *Environment) { 203 e := Environment{ 204 ServiceID: "logdog-app-id", 205 GSClient: GSClient{}, 206 StorageCache: StorageCache{ 207 Base: &flex.StorageCache{}, 208 }, 209 config: make(map[config.Set]memory.Files), 210 } 211 212 // Get our starting context. 213 c := gaeMemory.UseWithAppID(memlogger.Use(context.Background()), e.ServiceID) 214 c, _ = testclock.UseTime(c, testclock.TestTimeUTC.Round(time.Millisecond)) 215 c = cryptorand.MockForTest(c, 765589025) // as chosen by fair dice roll 216 ds.GetTestable(c).Consistent(true) 217 218 c = caching.WithEmptyProcessCache(c) 219 if *testGoLogger { 220 c = logging.SetLevel(gologger.StdConfig.Use(c), logging.Debug) 221 } 222 223 // Create/install our BigTable memory instance. 224 e.BigTable = bigtable.NewMemoryInstance(&e.StorageCache) 225 226 // Setup clock. 227 e.Clock = clock.Get(c).(testclock.TestClock) 228 229 // Setup luci-config configuration. 230 varz := vars.VarSet{} 231 varz.Register("appid", func(context.Context) (string, error) { 232 return e.ServiceID, nil 233 }) 234 c = cfgclient.Use(c, resolving.New(&varz, memory.New(e.config))) 235 236 // Capture the context while it doesn't have a lot of other stuff to use it 237 // for Sync. We do it to simulate a sync done from the cron. The context 238 // doesn't have a lot of stuff there. 239 syncCtx := c 240 e.syncConfig = func() { logdogcfg.Sync(syncCtx) } 241 242 c = logdogcfg.WithStore(c, &logdogcfg.Store{NoCache: true}) 243 244 // Add a project without a LogDog project config. 245 e.addConfigEntry("projects/proj-unconfigured", "not-logdog.cfg", "junk") 246 // Add a project with malformed configs. 247 e.addConfigEntry(config.MustProjectSet("proj-malformed"), e.ServiceID+".cfg", "!!! not a text protobuf !!!") 248 249 // luci-config: Coordinator Defaults 250 e.ModServiceConfig(c, func(cfg *svcconfig.Config) { 251 cfg.Transport = &svcconfig.Transport{ 252 Type: &svcconfig.Transport_Pubsub{ 253 Pubsub: &svcconfig.Transport_PubSub{ 254 Project: e.ServiceID, 255 Topic: "test-topic", 256 }, 257 }, 258 } 259 cfg.Coordinator = &svcconfig.Coordinator{ 260 AdminAuthGroup: "admin", 261 ServiceAuthGroup: "services", 262 PrefixExpiration: durationpb.New(24 * time.Hour), 263 } 264 }) 265 266 // Install authentication state. 267 c = auth.WithState(c, &e.AuthState) 268 e.ActAsAnon() 269 270 // Setup our default Coordinator services. 271 e.Services = Services{ 272 ST: func(lst *coordinator.LogStreamState) (coordinator.SigningStorage, error) { 273 // If we're not archived, return our BigTable storage instance. 274 if !lst.ArchivalState().Archived() { 275 return &BigTableStorage{ 276 Testing: e.BigTable, 277 }, nil 278 } 279 280 opts := archive.Options{ 281 Index: gs.Path(lst.ArchiveIndexURL), 282 Stream: gs.Path(lst.ArchiveStreamURL), 283 Client: &e.GSClient, 284 Cache: &e.StorageCache, 285 } 286 287 base, err := archive.New(opts) 288 if err != nil { 289 return nil, err 290 } 291 return &ArchivalStorage{ 292 Storage: base, 293 Opts: opts, 294 }, nil 295 }, 296 } 297 c = flex.WithServices(c, &e.Services) 298 299 return cacheContext.Wrap(c), &e 300 } 301 302 // WithProjectNamespace runs f in project's namespace. 303 func WithProjectNamespace(c context.Context, project string, f func(context.Context)) { 304 if err := coordinator.WithProjectNamespace(&c, project); err != nil { 305 panic(err) 306 } 307 f(c) 308 }