go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/internal/service/find_test.go (about)

     1  // Copyright 2023 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 service
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"testing"
    21  	"time"
    22  
    23  	"go.chromium.org/luci/common/clock"
    24  	"go.chromium.org/luci/common/clock/testclock"
    25  	"go.chromium.org/luci/common/logging"
    26  	"go.chromium.org/luci/common/logging/memlogger"
    27  	cfgcommonpb "go.chromium.org/luci/common/proto/config"
    28  	"go.chromium.org/luci/config"
    29  	"go.chromium.org/luci/gae/service/datastore"
    30  
    31  	"go.chromium.org/luci/config_service/internal/model"
    32  	"go.chromium.org/luci/config_service/testutil"
    33  
    34  	. "github.com/smartystreets/goconvey/convey"
    35  )
    36  
    37  func TestFinder(t *testing.T) {
    38  	t.Parallel()
    39  
    40  	Convey("Finder", t, func() {
    41  		ctx := testutil.SetupContext()
    42  		configSet := config.MustProjectSet("my-project")
    43  
    44  		Convey("Single service", func() {
    45  			const serviceName = "my-service"
    46  			updateService := func(updateFn func(*model.Service)) {
    47  				srv := &model.Service{
    48  					Name: serviceName,
    49  					Info: &cfgcommonpb.Service{
    50  						Id: serviceName,
    51  					},
    52  				}
    53  				updateFn(srv)
    54  				So(datastore.Put(ctx, srv), ShouldBeNil)
    55  			}
    56  
    57  			Convey("Exact match", func() {
    58  				for _, prefix := range []string{"exact:", "text:", ""} {
    59  					Convey(fmt.Sprintf("With prefix %q", prefix), func() {
    60  						updateService(func(srv *model.Service) {
    61  							srv.Metadata = &cfgcommonpb.ServiceMetadata{
    62  								ConfigPatterns: []*cfgcommonpb.ConfigPattern{
    63  									{
    64  										ConfigSet: prefix + string(configSet),
    65  										Path:      prefix + "foo.cfg",
    66  									},
    67  								},
    68  							}
    69  						})
    70  						finder, err := NewFinder(ctx)
    71  						So(err, ShouldBeNil)
    72  						services := finder.FindInterestedServices(ctx, configSet, "foo.cfg")
    73  						So(convertToServiceNames(services), ShouldResemble, []string{serviceName})
    74  						So(finder.FindInterestedServices(ctx, configSet, "boo.cfg"), ShouldBeEmpty)
    75  					})
    76  				}
    77  			})
    78  
    79  			Convey("Regexp match", func() {
    80  				Convey("Anchored", func() {
    81  					updateService(func(srv *model.Service) {
    82  						srv.Metadata = &cfgcommonpb.ServiceMetadata{
    83  							ConfigPatterns: []*cfgcommonpb.ConfigPattern{
    84  								{
    85  									ConfigSet: `regex:^projects/.+$`,
    86  									Path:      `regex:^bucket\-[a-z]+\.cfg$`,
    87  								},
    88  							},
    89  						}
    90  					})
    91  					finder, err := NewFinder(ctx)
    92  					So(err, ShouldBeNil)
    93  					services := finder.FindInterestedServices(ctx, configSet, "bucket-foo.cfg")
    94  					So(convertToServiceNames(services), ShouldResemble, []string{serviceName})
    95  					So(finder.FindInterestedServices(ctx, configSet, "bucket-foo1.cfg"), ShouldBeEmpty)
    96  				})
    97  
    98  				Convey("Auto anchor", func() {
    99  					updateService(func(srv *model.Service) {
   100  						srv.Metadata = &cfgcommonpb.ServiceMetadata{
   101  							ConfigPatterns: []*cfgcommonpb.ConfigPattern{
   102  								{
   103  									ConfigSet: `regex:projects/.+`,
   104  									Path:      `regex:bucket\-[a-z]+\.cfg`,
   105  								},
   106  							},
   107  						}
   108  					})
   109  					finder, err := NewFinder(ctx)
   110  					So(err, ShouldBeNil)
   111  					services := finder.FindInterestedServices(ctx, configSet, "bucket-foo.cfg")
   112  					So(convertToServiceNames(services), ShouldResemble, []string{serviceName})
   113  					So(finder.FindInterestedServices(ctx, "abc"+configSet, "bucket-foo.cfg"), ShouldBeEmpty)
   114  					So(finder.FindInterestedServices(ctx, configSet, "bucket-foo.cfg.gz"), ShouldBeEmpty)
   115  				})
   116  			})
   117  
   118  			Convey("Log warning if a service is interested in the config of another service", func() {
   119  				updateService(func(srv *model.Service) {
   120  					srv.Metadata = &cfgcommonpb.ServiceMetadata{
   121  						ConfigPatterns: []*cfgcommonpb.ConfigPattern{
   122  							{
   123  								ConfigSet: `regex:^services/.+$`,
   124  								Path:      `regex:^settings\-[a-z]+\.cfg$`,
   125  							},
   126  						},
   127  					}
   128  				})
   129  				ctx = memlogger.Use(ctx)
   130  				logs := logging.Get(ctx).(*memlogger.MemLogger)
   131  				finder, err := NewFinder(ctx)
   132  				So(err, ShouldBeNil)
   133  				services := finder.FindInterestedServices(ctx, config.MustServiceSet("not-my-service"), "settings-foo.cfg")
   134  				So(services, ShouldNotBeEmpty)
   135  				So(logs, memlogger.ShouldHaveLog, logging.Warning, fmt.Sprintf("crbug/1466976 - service %q declares it is interested in the config \"settings-foo.cfg\" of another service \"not-my-service\"", serviceName))
   136  			})
   137  		})
   138  
   139  		Convey("Multiple services", func() {
   140  			services := []*model.Service{
   141  				{
   142  					Name: "buildbucket",
   143  					Metadata: &cfgcommonpb.ServiceMetadata{
   144  						ConfigPatterns: []*cfgcommonpb.ConfigPattern{
   145  							{
   146  								ConfigSet: `regex:projects/.+`,
   147  								Path:      "exact:buildbucket.cfg",
   148  							},
   149  							{
   150  								ConfigSet: `regex:projects/.+`,
   151  								Path:      `regex:bucket-\w+.cfg`,
   152  							},
   153  						},
   154  					},
   155  				},
   156  				{
   157  					Name: "buildbucket-shadow",
   158  					Metadata: &cfgcommonpb.ServiceMetadata{
   159  						ConfigPatterns: []*cfgcommonpb.ConfigPattern{
   160  							{
   161  								ConfigSet: `regex:projects/.+`,
   162  								Path:      "exact:buildbucket.cfg",
   163  							},
   164  							{
   165  								ConfigSet: `regex:projects/.+`,
   166  								Path:      `regex:bucket-\w+.cfg`,
   167  							},
   168  						},
   169  					},
   170  				},
   171  				{
   172  					Name: "luci-change-verifier",
   173  					Metadata: &cfgcommonpb.ServiceMetadata{
   174  						ConfigPatterns: []*cfgcommonpb.ConfigPattern{
   175  							{
   176  								ConfigSet: `regex:projects/.+`,
   177  								Path:      "text:commit-queue.cfg",
   178  							},
   179  						},
   180  					},
   181  				},
   182  				{
   183  					Name: "swarming",
   184  					LegacyMetadata: &cfgcommonpb.ServiceDynamicMetadata{
   185  						Validation: &cfgcommonpb.Validator{
   186  							Patterns: []*cfgcommonpb.ConfigPattern{
   187  								{
   188  									ConfigSet: `regex:projects/.+`,
   189  									Path:      "swarming.cfg",
   190  								},
   191  							},
   192  						},
   193  					},
   194  				},
   195  				{
   196  					Name: "buildbucket-project-foo-specific",
   197  					Metadata: &cfgcommonpb.ServiceMetadata{
   198  						ConfigPatterns: []*cfgcommonpb.ConfigPattern{
   199  							{
   200  								ConfigSet: "exact:projects/foo",
   201  								Path:      "exact:buildbucket.cfg",
   202  							},
   203  						},
   204  					},
   205  				},
   206  				{
   207  					Name: "no-metadata",
   208  				},
   209  			}
   210  			So(datastore.Put(ctx, services), ShouldBeNil)
   211  
   212  			finder, err := NewFinder(ctx)
   213  			So(err, ShouldBeNil)
   214  			So(convertToServiceNames(finder.FindInterestedServices(ctx, config.MustProjectSet("my-proj"), "bucket-abc.cfg")), ShouldResemble, []string{"buildbucket", "buildbucket-shadow"})
   215  			So(convertToServiceNames(finder.FindInterestedServices(ctx, config.MustProjectSet("my-proj"), "buildbucket.cfg")), ShouldResemble, []string{"buildbucket", "buildbucket-shadow"})
   216  			So(convertToServiceNames(finder.FindInterestedServices(ctx, config.MustProjectSet("my-proj"), "commit-queue.cfg")), ShouldResemble, []string{"luci-change-verifier"})
   217  			So(convertToServiceNames(finder.FindInterestedServices(ctx, config.MustProjectSet("my-proj"), "swarming.cfg")), ShouldResemble, []string{"swarming"})
   218  			So(convertToServiceNames(finder.FindInterestedServices(ctx, config.MustProjectSet("foo"), "buildbucket.cfg")), ShouldResemble, []string{"buildbucket", "buildbucket-project-foo-specific", "buildbucket-shadow"})
   219  		})
   220  
   221  		Convey("Refresh", func() {
   222  			cs := config.MustProjectSet("my-proj")
   223  			srv := &model.Service{
   224  				Name: "foo",
   225  				Metadata: &cfgcommonpb.ServiceMetadata{
   226  					ConfigPatterns: []*cfgcommonpb.ConfigPattern{
   227  						{
   228  							ConfigSet: string(cs),
   229  							Path:      "old.cfg",
   230  						},
   231  					},
   232  				},
   233  			}
   234  			So(datastore.Put(ctx, srv), ShouldBeNil)
   235  			finder, err := NewFinder(ctx)
   236  			So(err, ShouldBeNil)
   237  			cctx, cancel := context.WithCancel(ctx)
   238  			defer cancel()
   239  			go func() {
   240  				finder.RefreshPeriodically(cctx)
   241  			}()
   242  			So(convertToServiceNames(finder.FindInterestedServices(ctx, cs, "old.cfg")), ShouldResemble, []string{"foo"})
   243  			So(convertToServiceNames(finder.FindInterestedServices(ctx, cs, "new.cfg")), ShouldBeEmpty)
   244  			srv.Metadata.ConfigPatterns[0].Path = "new.cfg"
   245  			So(datastore.Put(ctx, srv), ShouldBeNil)
   246  			tc := clock.Get(ctx).(testclock.TestClock)
   247  			start := time.Now()
   248  			for {
   249  				tc.Add(1 * time.Minute) // Should trigger a refresh
   250  				if len(finder.FindInterestedServices(ctx, cs, "new.cfg")) > 0 {
   251  					break
   252  				}
   253  				if time.Since(start) > 30*time.Second {
   254  					t.Fatal("finder doesn't appear to be refreshed")
   255  				}
   256  			}
   257  			So(convertToServiceNames(finder.FindInterestedServices(ctx, cs, "old.cfg")), ShouldBeEmpty)
   258  			So(convertToServiceNames(finder.FindInterestedServices(ctx, cs, "new.cfg")), ShouldResemble, []string{"foo"})
   259  		})
   260  	})
   261  }
   262  
   263  func convertToServiceNames(services []*model.Service) []string {
   264  	if services == nil {
   265  		return nil
   266  	}
   267  	ret := make([]string, len(services))
   268  	for i, srv := range services {
   269  		ret[i] = srv.Name
   270  	}
   271  	return ret
   272  }