go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/buildbucket/fake/fake.go (about)

     1  // Copyright 2022 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 bbfake
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"math"
    22  	"sync"
    23  	"time"
    24  
    25  	"cloud.google.com/go/pubsub"
    26  	"google.golang.org/protobuf/proto"
    27  	"google.golang.org/protobuf/types/known/timestamppb"
    28  
    29  	bbpb "go.chromium.org/luci/buildbucket/proto"
    30  	bbutil "go.chromium.org/luci/buildbucket/protoutil"
    31  	"go.chromium.org/luci/common/clock"
    32  	"go.chromium.org/luci/common/data/stringset"
    33  	"go.chromium.org/luci/common/errors"
    34  
    35  	cfgpb "go.chromium.org/luci/cv/api/config/v2"
    36  	"go.chromium.org/luci/cv/internal/buildbucket"
    37  )
    38  
    39  const requestDeduplicationWindow = 1 * time.Minute
    40  
    41  type fakeApp struct {
    42  	hostname      string
    43  	nextBuildID   int64 // for generating monotonically decreasing build ID
    44  	requestCache  timedMap
    45  	pubsubTopic   *pubsub.Topic
    46  	buildStoreMu  sync.RWMutex
    47  	buildStore    map[int64]*bbpb.Build // build ID -> build
    48  	configStoreMu sync.RWMutex
    49  	configStore   map[string]*bbpb.BuildbucketCfg // project name -> config
    50  }
    51  
    52  type Fake struct {
    53  	hostsMu sync.RWMutex
    54  	hosts   map[string]*fakeApp // hostname -> fakeApp
    55  }
    56  
    57  // NewClientFactory returns a factory that creates a client for this buildbucket
    58  // fake.
    59  func (f *Fake) NewClientFactory() buildbucket.ClientFactory {
    60  	return clientFactory{
    61  		fake: f,
    62  	}
    63  }
    64  
    65  // MustNewClient is a shorthand of `fake.NewClientFactory().MakeClient(...)`.
    66  //
    67  // Panics if fails to create new client.
    68  func (f *Fake) MustNewClient(ctx context.Context, host, luciProject string) *Client {
    69  	factory := clientFactory{
    70  		fake: f,
    71  	}
    72  	client, err := factory.MakeClient(ctx, host, luciProject)
    73  	if err != nil {
    74  		panic(errors.Annotate(err, "failed to create new buildbucket client").Err())
    75  	}
    76  	return client.(*Client)
    77  }
    78  
    79  // RegisterPubsubTopic registers a pubsub topic for the given host.
    80  //
    81  // If a build is updated to the terminal status, a message will be sent to
    82  // the topic for this build.
    83  func (f *Fake) RegisterPubsubTopic(host string, topic *pubsub.Topic) {
    84  	fa := f.ensureApp(host)
    85  	fa.pubsubTopic = topic
    86  }
    87  
    88  // AddBuilder adds a new builder configuration to fake Buildbucket host.
    89  //
    90  // Overwrites the existing builder if the same builder already exists.
    91  // `properties` should be marshallable by `encoding/json`.
    92  func (f *Fake) AddBuilder(host string, builder *bbpb.BuilderID, properties any) *Fake {
    93  	fa := f.ensureApp(host)
    94  	fa.configStoreMu.Lock()
    95  	defer fa.configStoreMu.Unlock()
    96  	if _, ok := fa.configStore[builder.GetProject()]; !ok {
    97  		fa.configStore[builder.GetProject()] = &bbpb.BuildbucketCfg{}
    98  	}
    99  	cfg := fa.configStore[builder.GetProject()]
   100  	var bucket *bbpb.Bucket
   101  	for _, b := range cfg.GetBuckets() {
   102  		if b.Name == builder.GetBucket() {
   103  			bucket = b
   104  			break
   105  		}
   106  	}
   107  	if bucket == nil {
   108  		bucket = &bbpb.Bucket{
   109  			Name:     builder.GetBucket(),
   110  			Swarming: &bbpb.Swarming{},
   111  		}
   112  		cfg.Buckets = append(cfg.GetBuckets(), bucket)
   113  	}
   114  
   115  	builderCfg := &bbpb.BuilderConfig{
   116  		Name: builder.GetBuilder(),
   117  	}
   118  	if properties != nil {
   119  		bProperties, err := json.Marshal(properties)
   120  		if err != nil {
   121  			panic(err)
   122  		}
   123  		builderCfg.Properties = string(bProperties)
   124  	}
   125  	for i, b := range bucket.GetSwarming().GetBuilders() {
   126  		if b.Name == builder.GetBuilder() {
   127  			bucket.GetSwarming().GetBuilders()[i] = builderCfg
   128  			return f
   129  		}
   130  	}
   131  	bucket.GetSwarming().Builders = append(bucket.GetSwarming().GetBuilders(), builderCfg)
   132  	return f
   133  }
   134  
   135  // EnsureBuilders ensures all builders defined in the Project config are added
   136  // to the Buildbucket fake.
   137  func (f *Fake) EnsureBuilders(cfg *cfgpb.Config) {
   138  	added := stringset.New(1)
   139  	for _, cg := range cfg.GetConfigGroups() {
   140  		for _, b := range cg.GetVerifiers().GetTryjob().GetBuilders() {
   141  			if added.Has(fmt.Sprintf("%s/%s", b.GetHost(), b.GetName())) {
   142  				continue
   143  			}
   144  			builder, err := bbutil.ParseBuilderID(b.GetName())
   145  			if err != nil {
   146  				panic(err)
   147  			}
   148  			f.AddBuilder(b.GetHost(), builder, nil)
   149  			added.Add(fmt.Sprintf("%s/%s", b.GetHost(), b.GetName()))
   150  		}
   151  	}
   152  }
   153  
   154  // MutateBuild mutates the provided build.
   155  //
   156  // Panics if the provided build is not found.
   157  func (f *Fake) MutateBuild(ctx context.Context, host string, id int64, mutateFn func(*bbpb.Build)) *bbpb.Build {
   158  	f.hostsMu.RLock()
   159  	fakeApp, ok := f.hosts[host]
   160  	f.hostsMu.RUnlock()
   161  	if !ok {
   162  		panic(errors.Reason("unknown host %q", host))
   163  	}
   164  	return fakeApp.updateBuild(ctx, id, mutateFn)
   165  }
   166  
   167  func (f *Fake) ensureApp(host string) *fakeApp {
   168  	f.hostsMu.Lock()
   169  	defer f.hostsMu.Unlock()
   170  	if _, ok := f.hosts[host]; !ok {
   171  		if f.hosts == nil {
   172  			f.hosts = make(map[string]*fakeApp)
   173  		}
   174  		f.hosts[host] = &fakeApp{
   175  			hostname:    host,
   176  			nextBuildID: math.MaxInt64 - 1,
   177  			buildStore:  make(map[int64]*bbpb.Build),
   178  			configStore: make(map[string]*bbpb.BuildbucketCfg),
   179  		}
   180  	}
   181  	return f.hosts[host]
   182  }
   183  
   184  func (fa *fakeApp) getBuild(id int64) *bbpb.Build {
   185  	fa.buildStoreMu.RLock()
   186  	defer fa.buildStoreMu.RUnlock()
   187  	if build, ok := fa.buildStore[id]; ok {
   188  		return proto.Clone(build).(*bbpb.Build)
   189  	}
   190  	return nil
   191  }
   192  
   193  func (fa *fakeApp) iterBuildStore(cb func(*bbpb.Build)) {
   194  	fa.buildStoreMu.RLock()
   195  	defer fa.buildStoreMu.RUnlock()
   196  	for _, build := range fa.buildStore {
   197  		cb(proto.Clone(build).(*bbpb.Build))
   198  	}
   199  }
   200  
   201  func (fa *fakeApp) updateBuild(ctx context.Context, id int64, cb func(*bbpb.Build)) *bbpb.Build {
   202  	fa.buildStoreMu.Lock()
   203  	defer fa.buildStoreMu.Unlock()
   204  	if build, ok := fa.buildStore[id]; ok {
   205  		cb(build)
   206  		build.UpdateTime = timestamppb.New(clock.Now(ctx).UTC())
   207  		// store a copy to avoid cb keeps the reference to the build and mutate it
   208  		// later.
   209  		fa.buildStore[id] = proto.Clone(build).(*bbpb.Build)
   210  		fa.publishToTopicIfNecessary(ctx, build)
   211  		return build
   212  	}
   213  	panic(errors.Reason("unknown build %d", id).Err())
   214  }
   215  
   216  // insertBuild also generates a monotonically decreasing build ID.
   217  //
   218  // Caches the build for `requestDeduplicationWindow` to deduplicate request
   219  // with same request ID later.
   220  func (fa *fakeApp) insertBuild(ctx context.Context, build *bbpb.Build, requestID string) *bbpb.Build {
   221  	fa.buildStoreMu.Lock()
   222  	defer fa.buildStoreMu.Unlock()
   223  	build.Id = fa.nextBuildID
   224  	fa.nextBuildID--
   225  	if _, ok := fa.buildStore[build.Id]; ok {
   226  		panic(fmt.Sprintf("build %d already exists", build.Id))
   227  	}
   228  	cloned := proto.Clone(build).(*bbpb.Build)
   229  	fa.buildStore[build.Id] = cloned
   230  	if requestID != "" {
   231  		fa.requestCache.set(ctx, requestID, cloned, requestDeduplicationWindow)
   232  	}
   233  	fa.publishToTopicIfNecessary(ctx, build)
   234  	return build
   235  }
   236  
   237  func (fa *fakeApp) publishToTopicIfNecessary(ctx context.Context, build *bbpb.Build) {
   238  	topic := fa.pubsubTopic
   239  	if topic == nil || !bbutil.IsEnded(build.GetStatus()) {
   240  		return
   241  	}
   242  	data, err := json.Marshal(buildbucket.PubsubMessage{
   243  		Build: buildbucket.PubsubBuildMessage{
   244  			ID: build.GetId(),
   245  		},
   246  		Hostname: fa.hostname,
   247  	})
   248  	if err != nil {
   249  		panic(errors.Annotate(err, "failed to marshal pubsub message").Err())
   250  	}
   251  	res := topic.Publish(ctx, &pubsub.Message{
   252  		Data: data,
   253  	})
   254  	// Publish will batch messages. However, in the test, we want buildbucket
   255  	// fake to publish messages asap. Therefore, flush immediately after publish
   256  	// the messages
   257  	topic.Flush()
   258  	select {
   259  	case <-res.Ready():
   260  		if _, err := res.Get(ctx); err != nil {
   261  			panic(errors.Annotate(err, "failed to publish the pubsub message").Err())
   262  		}
   263  	case <-time.After(10 * time.Second):
   264  		panic(errors.Reason("took too long to publish the pubsub message").Err())
   265  	}
   266  }
   267  
   268  func (fa *fakeApp) findDupRequest(ctx context.Context, requestID string) *bbpb.Build {
   269  	if requestID == "" {
   270  		return nil
   271  	}
   272  	if b, ok := fa.requestCache.get(ctx, requestID); ok {
   273  		return proto.Clone(b.(*bbpb.Build)).(*bbpb.Build)
   274  	}
   275  	return nil
   276  }
   277  
   278  func (fa *fakeApp) loadBuilderCfg(builderID *bbpb.BuilderID) *bbpb.BuilderConfig {
   279  	fa.configStoreMu.RLock()
   280  	defer fa.configStoreMu.RUnlock()
   281  	cfg, ok := fa.configStore[builderID.GetProject()]
   282  	if !ok {
   283  		return nil
   284  	}
   285  	for _, bucket := range cfg.GetBuckets() {
   286  		if bucket.GetName() != builderID.GetBucket() {
   287  			continue
   288  		}
   289  		for _, builder := range bucket.GetSwarming().GetBuilders() {
   290  			if builder.GetName() == builderID.GetBuilder() {
   291  				return proto.Clone(builder).(*bbpb.BuilderConfig)
   292  			}
   293  		}
   294  	}
   295  	return nil
   296  }