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 }