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  }