go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/internal/importer/importer_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 importer 16 17 import ( 18 "archive/tar" 19 "bytes" 20 "context" 21 "crypto/sha256" 22 "encoding/hex" 23 "fmt" 24 "math/rand" 25 "net/http" 26 "net/http/httptest" 27 "sort" 28 "strings" 29 "testing" 30 31 "cloud.google.com/go/storage" 32 "github.com/golang/mock/gomock" 33 "github.com/julienschmidt/httprouter" 34 "github.com/klauspost/compress/gzip" 35 "google.golang.org/protobuf/proto" 36 "google.golang.org/protobuf/types/known/timestamppb" 37 38 "go.chromium.org/luci/auth/identity" 39 "go.chromium.org/luci/common/clock" 40 "go.chromium.org/luci/common/errors" 41 "go.chromium.org/luci/common/gcloud/gs" 42 protoutil "go.chromium.org/luci/common/proto" 43 cfgcommonpb "go.chromium.org/luci/common/proto/config" 44 "go.chromium.org/luci/common/proto/git" 45 gitilespb "go.chromium.org/luci/common/proto/gitiles" 46 "go.chromium.org/luci/common/proto/gitiles/mock_gitiles" 47 "go.chromium.org/luci/common/tsmon" 48 "go.chromium.org/luci/config" 49 "go.chromium.org/luci/gae/service/datastore" 50 "go.chromium.org/luci/server/auth" 51 "go.chromium.org/luci/server/auth/authtest" 52 "go.chromium.org/luci/server/router" 53 "go.chromium.org/luci/server/tq" 54 "go.chromium.org/luci/server/tq/tqtesting" 55 56 "go.chromium.org/luci/config_service/internal/clients" 57 "go.chromium.org/luci/config_service/internal/common" 58 "go.chromium.org/luci/config_service/internal/metrics" 59 "go.chromium.org/luci/config_service/internal/model" 60 "go.chromium.org/luci/config_service/internal/settings" 61 "go.chromium.org/luci/config_service/internal/taskpb" 62 "go.chromium.org/luci/config_service/internal/validation" 63 "go.chromium.org/luci/config_service/testutil" 64 65 . "github.com/smartystreets/goconvey/convey" 66 . "go.chromium.org/luci/common/testing/assertions" 67 ) 68 69 func TestImportAllConfigs(t *testing.T) { 70 t.Parallel() 71 72 Convey("import all configs", t, func() { 73 ctx := testutil.SetupContext() 74 disp := &tq.Dispatcher{} 75 ctx, sch := tq.TestingContext(ctx, disp) 76 ctx = settings.WithGlobalConfigLoc(ctx, &cfgcommonpb.GitilesLocation{ 77 Repo: "https://a.googlesource.com/infradata/config", 78 Ref: "refs/heads/main", 79 Path: "dev-configs", 80 }) 81 82 ctl := gomock.NewController(t) 83 defer ctl.Finish() 84 mockClient := mock_gitiles.NewMockGitilesClient(ctl) 85 ctx = context.WithValue(ctx, &clients.MockGitilesClientKey, mockClient) 86 importer := &Importer{} 87 importer.registerTQTask(disp) 88 89 Convey("ok", func() { 90 mockClient.EXPECT().ListFiles(gomock.Any(), protoutil.MatcherEqual( 91 &gitilespb.ListFilesRequest{ 92 Project: "infradata/config", 93 Committish: "refs/heads/main", 94 Path: "dev-configs", 95 }, 96 )).Return(&gitilespb.ListFilesResponse{ 97 Files: []*git.File{ 98 { 99 Id: "hash1", 100 Path: "service1", 101 Type: git.File_TREE, 102 }, 103 { 104 Id: "hash2", 105 Path: "service2", 106 Type: git.File_TREE, 107 }, 108 { 109 Id: "hash3", 110 Path: "file1", 111 Type: git.File_BLOB, 112 }, 113 }, 114 }, nil) 115 116 Convey("projects.cfg File entity not exist", func() { 117 err := importAllConfigs(ctx, disp) 118 So(err, ShouldBeNil) 119 cfgSetsInQueue := getCfgSetsInTaskQueue(sch) 120 So(cfgSetsInQueue, ShouldResemble, []string{"services/service1", "services/service2"}) 121 }) 122 123 Convey("projects.cfg File entity exist", func() { 124 testutil.InjectSelfConfigs(ctx, map[string]proto.Message{ 125 "projects.cfg": &cfgcommonpb.ProjectsCfg{ 126 Projects: []*cfgcommonpb.Project{ 127 {Id: "proj1"}, 128 }, 129 }, 130 }) 131 err := importAllConfigs(ctx, disp) 132 So(err, ShouldBeNil) 133 cfgSetsInQueue := getCfgSetsInTaskQueue(sch) 134 So(cfgSetsInQueue, ShouldResemble, []string{"projects/proj1", "services/service1", "services/service2"}) 135 }) 136 137 Convey("delete stale config set", func() { 138 stale := &model.ConfigSet{ID: config.MustServiceSet("stale")} 139 So(datastore.Put(ctx, stale), ShouldBeNil) 140 err := importAllConfigs(ctx, disp) 141 So(err, ShouldBeNil) 142 So(datastore.Get(ctx, stale), ShouldEqual, datastore.ErrNoSuchEntity) 143 }) 144 }) 145 146 Convey("error", func() { 147 Convey("gitiles", func() { 148 mockClient.EXPECT().ListFiles(gomock.Any(), gomock.Any()).Return(nil, errors.New("gitiles error")) 149 err := importAllConfigs(ctx, disp) 150 So(err, ShouldErrLike, "failed to load service config sets: failed to call Gitiles to list files: gitiles error") 151 }) 152 153 Convey("bad projects.cfg content", func() { 154 mockClient.EXPECT().ListFiles(gomock.Any(), gomock.Any()).Return(&gitilespb.ListFilesResponse{}, nil) 155 testutil.InjectSelfConfigs(ctx, map[string]proto.Message{ 156 "projects.cfg": &cfgcommonpb.ServicesCfg{ 157 Services: []*cfgcommonpb.Service{ 158 {Id: "my-service"}, 159 }}, // bad type 160 }) 161 So(importAllConfigs(ctx, disp), ShouldErrLike, `failed to load project config sets: failed to unmarshal file "projects.cfg": proto`) 162 }) 163 }) 164 }) 165 } 166 167 type mockValidator struct { 168 result *cfgcommonpb.ValidationResult 169 err error 170 } 171 172 func (mv *mockValidator) Validate(context.Context, config.Set, []validation.File) (*cfgcommonpb.ValidationResult, error) { 173 return mv.result, mv.err 174 } 175 176 func TestImportConfigSet(t *testing.T) { 177 t.Parallel() 178 179 Convey("import single ConfigSet", t, func() { 180 ctx := testutil.SetupContext() 181 ctx = settings.WithGlobalConfigLoc(ctx, &cfgcommonpb.GitilesLocation{ 182 Repo: "https://a.googlesource.com/infradata/config", 183 Ref: "refs/heads/main", 184 Path: "dev-configs", 185 }) 186 ctx, _ = tsmon.WithDummyInMemory(ctx) 187 ctl := gomock.NewController(t) 188 mockGtClient := mock_gitiles.NewMockGitilesClient(ctl) 189 ctx = context.WithValue(ctx, &clients.MockGitilesClientKey, mockGtClient) 190 mockGsClient := clients.NewMockGsClient(ctl) 191 ctx = clients.WithGsClient(ctx, mockGsClient) 192 const testGSBucket = "test-bucket" 193 cs := config.MustServiceSet("my-service") 194 latestCommit := &git.Commit{ 195 Id: "latest revision", 196 Committer: &git.Commit_User{ 197 Name: "user", 198 Email: "user@gmail.com", 199 Time: timestamppb.New(datastore.RoundTime(clock.Now(ctx).UTC())), 200 }, 201 Author: &git.Commit_User{ 202 Email: "author@gmail.com", 203 }, 204 } 205 expectedLatestRevInfo := model.RevisionInfo{ 206 ID: latestCommit.Id, 207 CommitTime: latestCommit.Committer.Time.AsTime(), 208 CommitterEmail: latestCommit.Committer.Email, 209 AuthorEmail: latestCommit.Author.Email, 210 } 211 mockValidator := &mockValidator{} 212 importer := &Importer{ 213 Validator: mockValidator, 214 GSBucket: testGSBucket, 215 } 216 217 Convey("happy path", func() { 218 Convey("success import", func() { 219 mockGtClient.EXPECT().Log(gomock.Any(), protoutil.MatcherEqual( 220 &gitilespb.LogRequest{ 221 Project: "infradata/config", 222 Committish: "refs/heads/main", 223 Path: "dev-configs/myservice", 224 PageSize: 1, 225 }, 226 )).Return(&gitilespb.LogResponse{ 227 Log: []*git.Commit{latestCommit}, 228 }, nil) 229 tarGzContent, err := buildTarGz(map[string]any{"file1": "file1 content", "sub_dir/file2": "file2 content", "sub_dir/": "", "empty_file": ""}) 230 So(err, ShouldBeNil) 231 mockGtClient.EXPECT().Archive(gomock.Any(), protoutil.MatcherEqual( 232 &gitilespb.ArchiveRequest{ 233 Project: "infradata/config", 234 Ref: latestCommit.Id, 235 Path: "dev-configs/myservice", 236 Format: gitilespb.ArchiveRequest_GZIP, 237 }, 238 )).Return(&gitilespb.ArchiveResponse{ 239 Contents: tarGzContent, 240 }, nil) 241 var recordedGSPaths []gs.Path 242 mockGsClient.EXPECT().UploadIfMissing( 243 gomock.Any(), gomock.Eq(testGSBucket), 244 gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn( 245 func(ctx context.Context, bucket, object string, data []byte, attrsModifyFn func(*storage.ObjectAttrs)) (bool, error) { 246 recordedGSPaths = append(recordedGSPaths, gs.MakePath(bucket, object)) 247 return true, nil 248 }, 249 ) 250 251 err = importer.ImportConfigSet(ctx, config.MustServiceSet("myservice")) 252 So(err, ShouldBeNil) 253 cfgSet := &model.ConfigSet{ 254 ID: config.MustServiceSet("myservice"), 255 } 256 attempt := &model.ImportAttempt{ 257 ConfigSet: datastore.KeyForObj(ctx, cfgSet), 258 } 259 revKey := datastore.MakeKey(ctx, model.ConfigSetKind, "services/myservice", model.RevisionKind, latestCommit.Id) 260 var files []*model.File 261 So(datastore.Get(ctx, cfgSet, attempt), ShouldBeNil) 262 So(datastore.GetAll(ctx, datastore.NewQuery(model.FileKind).Ancestor(revKey), &files), ShouldBeNil) 263 264 So(cfgSet.Location, ShouldResembleProto, &cfgcommonpb.Location{ 265 Location: &cfgcommonpb.Location_GitilesLocation{ 266 GitilesLocation: &cfgcommonpb.GitilesLocation{ 267 Repo: "https://a.googlesource.com/infradata/config", 268 Ref: "refs/heads/main", 269 Path: "dev-configs/myservice", 270 }, 271 }, 272 }) 273 So(cfgSet.LatestRevision.Location, ShouldResembleProto, &cfgcommonpb.Location{ 274 Location: &cfgcommonpb.Location_GitilesLocation{ 275 GitilesLocation: &cfgcommonpb.GitilesLocation{ 276 Repo: "https://a.googlesource.com/infradata/config", 277 Ref: latestCommit.Id, 278 Path: "dev-configs/myservice", 279 }, 280 }, 281 }) 282 // Drop the `Location` as model ConfigSet has to use ShouldResemble which 283 // will not work if it contains proto. Same for other tests. 284 cfgSet.Location = nil 285 cfgSet.LatestRevision.Location = nil 286 So(cfgSet, ShouldResemble, &model.ConfigSet{ 287 ID: "services/myservice", 288 LatestRevision: expectedLatestRevInfo, 289 Version: model.CurrentCfgSetVersion, 290 }) 291 292 So(files, ShouldHaveLength, 3) 293 sort.Slice(files, func(i, j int) bool { 294 return strings.Compare(files[i].Path, files[j].Path) < 0 295 }) 296 So(files[0].Location, ShouldResembleProto, &cfgcommonpb.Location{ 297 Location: &cfgcommonpb.Location_GitilesLocation{ 298 GitilesLocation: &cfgcommonpb.GitilesLocation{ 299 Repo: "https://a.googlesource.com/infradata/config", 300 Ref: latestCommit.Id, 301 Path: "dev-configs/myservice/empty_file", 302 }, 303 }, 304 }) 305 files[0].Location = nil 306 expectedSha256, expectedContent0, err := hashAndCompressConfig(bytes.NewBuffer([]byte(""))) 307 So(err, ShouldBeNil) 308 So(files[0], ShouldResemble, &model.File{ 309 Path: "empty_file", 310 Revision: revKey, 311 CreateTime: datastore.RoundTime(clock.Now(ctx).UTC()), 312 Content: expectedContent0, 313 ContentSHA256: expectedSha256, 314 GcsURI: gs.MakePath(testGSBucket, fmt.Sprintf("%s/sha256/%s", common.GSProdCfgFolder, expectedSha256)), 315 }) 316 So(files[1].Location, ShouldResembleProto, &cfgcommonpb.Location{ 317 Location: &cfgcommonpb.Location_GitilesLocation{ 318 GitilesLocation: &cfgcommonpb.GitilesLocation{ 319 Repo: "https://a.googlesource.com/infradata/config", 320 Ref: latestCommit.Id, 321 Path: "dev-configs/myservice/file1", 322 }, 323 }, 324 }) 325 files[1].Location = nil 326 expectedSha256, expectedContent1, err := hashAndCompressConfig(bytes.NewBuffer([]byte("file1 content"))) 327 So(err, ShouldBeNil) 328 So(files[1], ShouldResemble, &model.File{ 329 Path: "file1", 330 Revision: revKey, 331 CreateTime: datastore.RoundTime(clock.Now(ctx).UTC()), 332 Content: expectedContent1, 333 ContentSHA256: expectedSha256, 334 Size: int64(len("file1 content")), 335 GcsURI: gs.MakePath(testGSBucket, fmt.Sprintf("%s/sha256/%s", common.GSProdCfgFolder, expectedSha256)), 336 }) 337 So(files[2].Location, ShouldResembleProto, &cfgcommonpb.Location{ 338 Location: &cfgcommonpb.Location_GitilesLocation{ 339 GitilesLocation: &cfgcommonpb.GitilesLocation{ 340 Repo: "https://a.googlesource.com/infradata/config", 341 Ref: latestCommit.Id, 342 Path: "dev-configs/myservice/sub_dir/file2", 343 }, 344 }, 345 }) 346 files[2].Location = nil 347 expectedSha256, expectedContent2, err := hashAndCompressConfig(bytes.NewBuffer([]byte("file2 content"))) 348 So(err, ShouldBeNil) 349 So(files[2], ShouldResemble, &model.File{ 350 Path: "sub_dir/file2", 351 Revision: revKey, 352 CreateTime: datastore.RoundTime(clock.Now(ctx).UTC()), 353 Content: expectedContent2, 354 ContentSHA256: expectedSha256, 355 Size: int64(len("file2 content")), 356 GcsURI: gs.MakePath(testGSBucket, fmt.Sprintf("%s/sha256/%s", common.GSProdCfgFolder, expectedSha256)), 357 }) 358 359 So(recordedGSPaths, ShouldHaveLength, len(files)) 360 sort.Slice(recordedGSPaths, func(i, j int) bool { 361 return strings.Compare(string(recordedGSPaths[i]), string(recordedGSPaths[j])) < 0 362 }) 363 expectedGSPaths := make([]gs.Path, len(files)) 364 for i, f := range files { 365 expectedGSPaths[i] = f.GcsURI 366 } 367 sort.Slice(expectedGSPaths, func(i, j int) bool { 368 return strings.Compare(string(expectedGSPaths[i]), string(expectedGSPaths[j])) < 0 369 }) 370 So(recordedGSPaths, ShouldResemble, expectedGSPaths) 371 372 So(attempt.Revision.Location, ShouldResembleProto, &cfgcommonpb.Location{ 373 Location: &cfgcommonpb.Location_GitilesLocation{ 374 GitilesLocation: &cfgcommonpb.GitilesLocation{ 375 Repo: "https://a.googlesource.com/infradata/config", 376 Ref: latestCommit.Id, 377 Path: "dev-configs/myservice", 378 }, 379 }, 380 }) 381 attempt.Revision.Location = nil 382 So(attempt, ShouldResemble, &model.ImportAttempt{ 383 ConfigSet: datastore.KeyForObj(ctx, cfgSet), 384 Revision: expectedLatestRevInfo, 385 Success: true, 386 Message: "Imported", 387 }) 388 }) 389 390 Convey("same git revision", func() { 391 loc := &cfgcommonpb.Location{ 392 Location: &cfgcommonpb.Location_GitilesLocation{ 393 GitilesLocation: &cfgcommonpb.GitilesLocation{ 394 Repo: "https://a.googlesource.com/infradata/config", 395 Ref: "refs/heads/main", 396 Path: "dev-configs/myservice", 397 }, 398 }, 399 } 400 cfgSetBeforeImport := &model.ConfigSet{ 401 ID: config.MustServiceSet("myservice"), 402 Location: loc, 403 LatestRevision: model.RevisionInfo{ID: latestCommit.Id}, 404 } 405 406 So(datastore.Put(ctx, cfgSetBeforeImport), ShouldBeNil) 407 mockGtClient.EXPECT().Log(gomock.Any(), gomock.Any()).Return(&gitilespb.LogResponse{ 408 Log: []*git.Commit{latestCommit}, 409 }, nil) 410 411 Convey("last attempt succeeded", func() { 412 lastAttempt := &model.ImportAttempt{ 413 ConfigSet: datastore.KeyForObj(ctx, cfgSetBeforeImport), 414 Success: true, 415 Message: "imported", 416 } 417 So(datastore.Put(ctx, cfgSetBeforeImport, lastAttempt), ShouldBeNil) 418 419 err := importer.ImportConfigSet(ctx, config.MustServiceSet("myservice")) 420 421 So(err, ShouldBeNil) 422 cfgSetAfterImport := &model.ConfigSet{ 423 ID: config.MustServiceSet("myservice"), 424 } 425 So(datastore.Get(ctx, cfgSetAfterImport), ShouldBeNil) 426 So(cfgSetAfterImport.Location, ShouldResembleProto, loc) 427 cfgSetAfterImport.Location = nil 428 cfgSetBeforeImport.Location = nil 429 So(cfgSetAfterImport, ShouldResemble, cfgSetBeforeImport) 430 attempt := &model.ImportAttempt{ConfigSet: datastore.KeyForObj(ctx, cfgSetAfterImport)} 431 So(datastore.Get(ctx, attempt), ShouldBeNil) 432 So(attempt.Revision.Location, ShouldResembleProto, &cfgcommonpb.Location{ 433 Location: &cfgcommonpb.Location_GitilesLocation{ 434 GitilesLocation: &cfgcommonpb.GitilesLocation{ 435 Repo: "https://a.googlesource.com/infradata/config", 436 Ref: latestCommit.Id, 437 Path: "dev-configs/myservice", 438 }, 439 }, 440 }) 441 attempt.Revision.Location = nil 442 So(attempt, ShouldResemble, &model.ImportAttempt{ 443 ConfigSet: datastore.KeyForObj(ctx, cfgSetAfterImport), 444 Success: true, 445 Message: "Up-to-date", 446 Revision: model.RevisionInfo{ 447 ID: latestCommit.Id, 448 CommitTime: latestCommit.Committer.Time.AsTime(), 449 CommitterEmail: latestCommit.Committer.Email, 450 AuthorEmail: latestCommit.Author.Email, 451 }, 452 }) 453 }) 454 455 Convey("last attempt succeeded but with validation msg", func() { 456 lastAttempt := &model.ImportAttempt{ 457 ConfigSet: datastore.KeyForObj(ctx, cfgSetBeforeImport), 458 Success: true, 459 Message: "imported with warnings", 460 ValidationResult: &cfgcommonpb.ValidationResult{ 461 Messages: []*cfgcommonpb.ValidationResult_Message{ 462 { 463 Path: "foo.cfg", 464 Severity: cfgcommonpb.ValidationResult_WARNING, 465 Text: "there is a warning", 466 }, 467 }, 468 }, 469 } 470 So(datastore.Put(ctx, lastAttempt), ShouldBeNil) 471 472 tarGzContent, err := buildTarGz(map[string]any{"foo.cfg": "content"}) 473 So(err, ShouldBeNil) 474 mockGtClient.EXPECT().Archive(gomock.Any(), gomock.Any()).Return(&gitilespb.ArchiveResponse{ 475 Contents: tarGzContent, 476 }, nil) 477 mockGsClient.EXPECT().UploadIfMissing( 478 gomock.Any(), gomock.Eq(testGSBucket), 479 gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) 480 mockValidator.result = lastAttempt.ValidationResult 481 482 err = importer.ImportConfigSet(ctx, config.MustServiceSet("myservice")) 483 484 So(err, ShouldBeNil) 485 currentAttempt := &model.ImportAttempt{ConfigSet: datastore.KeyForObj(ctx, cfgSetBeforeImport)} 486 So(datastore.Get(ctx, currentAttempt), ShouldBeNil) 487 So(currentAttempt.ValidationResult, ShouldResembleProto, lastAttempt.ValidationResult) 488 So(currentAttempt.Success, ShouldBeTrue) 489 }) 490 491 Convey("last attempt not succeeded", func() { 492 lastAttempt := &model.ImportAttempt{ 493 ConfigSet: datastore.KeyForObj(ctx, cfgSetBeforeImport), 494 Success: false, 495 Message: "transient gitilies error", 496 } 497 So(datastore.Put(ctx, lastAttempt), ShouldBeNil) 498 499 tarGzContent, err := buildTarGz(map[string]any{"foo.cfg": "content"}) 500 So(err, ShouldBeNil) 501 mockGtClient.EXPECT().Archive(gomock.Any(), gomock.Any()).Return(&gitilespb.ArchiveResponse{ 502 Contents: tarGzContent, 503 }, nil) 504 mockGsClient.EXPECT().UploadIfMissing( 505 gomock.Any(), gomock.Eq(testGSBucket), 506 gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) 507 508 err = importer.ImportConfigSet(ctx, config.MustServiceSet("myservice")) 509 510 So(err, ShouldBeNil) 511 currentAttempt := &model.ImportAttempt{ConfigSet: datastore.KeyForObj(ctx, cfgSetBeforeImport)} 512 So(datastore.Get(ctx, currentAttempt), ShouldBeNil) 513 So(currentAttempt.Success, ShouldBeTrue) 514 }) 515 }) 516 517 Convey("empty archive", func() { 518 mockGtClient.EXPECT().Log(gomock.Any(), protoutil.MatcherEqual( 519 &gitilespb.LogRequest{ 520 Project: "infradata/config", 521 Committish: "refs/heads/main", 522 Path: "dev-configs/myservice", 523 PageSize: 1, 524 }, 525 )).Return(&gitilespb.LogResponse{ 526 Log: []*git.Commit{latestCommit}, 527 }, nil) 528 mockGtClient.EXPECT().Archive(gomock.Any(), gomock.Any()).Return(&gitilespb.ArchiveResponse{}, nil) 529 530 err := importer.ImportConfigSet(ctx, config.MustServiceSet("myservice")) 531 532 So(err, ShouldBeNil) 533 cfgSet := &model.ConfigSet{ 534 ID: config.MustServiceSet("myservice"), 535 } 536 attempt := &model.ImportAttempt{ 537 ConfigSet: datastore.KeyForObj(ctx, cfgSet), 538 } 539 revKey := datastore.MakeKey(ctx, model.ConfigSetKind, "services/myservice", model.RevisionKind, latestCommit.Id) 540 So(datastore.Get(ctx, cfgSet, attempt), ShouldBeNil) 541 var files []*model.File 542 So(datastore.Run(ctx, datastore.NewQuery(model.FileKind).Ancestor(revKey), func(f *model.File) { 543 files = append(files, f) 544 }), ShouldBeNil) 545 So(files, ShouldHaveLength, 0) 546 547 So(cfgSet.Location, ShouldResembleProto, &cfgcommonpb.Location{ 548 Location: &cfgcommonpb.Location_GitilesLocation{ 549 GitilesLocation: &cfgcommonpb.GitilesLocation{ 550 Repo: "https://a.googlesource.com/infradata/config", 551 Ref: "refs/heads/main", 552 Path: "dev-configs/myservice", 553 }, 554 }, 555 }) 556 So(cfgSet.LatestRevision.Location, ShouldResembleProto, &cfgcommonpb.Location{ 557 Location: &cfgcommonpb.Location_GitilesLocation{ 558 GitilesLocation: &cfgcommonpb.GitilesLocation{ 559 Repo: "https://a.googlesource.com/infradata/config", 560 Ref: latestCommit.Id, 561 Path: "dev-configs/myservice", 562 }, 563 }, 564 }) 565 566 cfgSet.Location = nil 567 cfgSet.LatestRevision.Location = nil 568 So(cfgSet, ShouldResemble, &model.ConfigSet{ 569 ID: "services/myservice", 570 LatestRevision: expectedLatestRevInfo, 571 Version: model.CurrentCfgSetVersion, 572 }) 573 574 So(attempt.Success, ShouldBeTrue) 575 So(attempt.Message, ShouldEqual, "No Configs. Imported as empty") 576 }) 577 578 Convey("config set location change", func() { 579 cfgSetBeforeImport := &model.ConfigSet{ 580 ID: config.MustServiceSet("myservice"), 581 Location: &cfgcommonpb.Location{ 582 Location: &cfgcommonpb.Location_GitilesLocation{ 583 GitilesLocation: &cfgcommonpb.GitilesLocation{ 584 Repo: "https://a.googlesource.com/infradata/config", 585 Ref: "stale", 586 Path: "dev-configs/myservice", 587 }, 588 }, 589 }, 590 LatestRevision: model.RevisionInfo{ID: latestCommit.Id}, 591 } 592 So(datastore.Put(ctx, cfgSetBeforeImport), ShouldBeNil) 593 mockGtClient.EXPECT().Log(gomock.Any(), protoutil.MatcherEqual( 594 &gitilespb.LogRequest{ 595 Project: "infradata/config", 596 Committish: "refs/heads/main", 597 Path: "dev-configs/myservice", 598 PageSize: 1, 599 }, 600 )).Return(&gitilespb.LogResponse{ 601 Log: []*git.Commit{latestCommit}, 602 }, nil) 603 mockGtClient.EXPECT().Archive(gomock.Any(), gomock.Any()).Return(&gitilespb.ArchiveResponse{}, nil) 604 605 err := importer.ImportConfigSet(ctx, config.MustServiceSet("myservice")) 606 607 So(err, ShouldBeNil) 608 cfgSetAfterImport := &model.ConfigSet{ 609 ID: config.MustServiceSet("myservice"), 610 } 611 So(datastore.Get(ctx, cfgSetAfterImport), ShouldBeNil) 612 So(cfgSetAfterImport.Location.GetGitilesLocation().Ref, ShouldNotEqual, cfgSetBeforeImport.Location.GetGitilesLocation().Ref) 613 So(cfgSetAfterImport.Location, ShouldResembleProto, &cfgcommonpb.Location{ 614 Location: &cfgcommonpb.Location_GitilesLocation{ 615 GitilesLocation: &cfgcommonpb.GitilesLocation{ 616 Repo: "https://a.googlesource.com/infradata/config", 617 Ref: "refs/heads/main", 618 Path: "dev-configs/myservice", 619 }, 620 }, 621 }) 622 }) 623 624 Convey("no logs", func() { 625 mockGtClient.EXPECT().Log(gomock.Any(), gomock.Any()).Return(&gitilespb.LogResponse{ 626 Log: []*git.Commit{}, 627 }, nil) 628 err := importer.ImportConfigSet(ctx, config.MustServiceSet("myservice")) 629 So(err, ShouldBeNil) 630 attempt := &model.ImportAttempt{ 631 ConfigSet: datastore.MakeKey(ctx, model.ConfigSetKind, "services/myservice"), 632 } 633 So(datastore.Get(ctx, attempt), ShouldBeNil) 634 So(attempt.Success, ShouldBeTrue) 635 So(attempt.Message, ShouldContainSubstring, "no commit logs") 636 }) 637 638 Convey("validate", func() { 639 mockGtClient.EXPECT().Log(gomock.Any(), gomock.Any()).Return(&gitilespb.LogResponse{ 640 Log: []*git.Commit{latestCommit}, 641 }, nil) 642 643 tarGzContent, err := buildTarGz(map[string]any{"foo.cfg": "content"}) 644 So(err, ShouldBeNil) 645 mockGtClient.EXPECT().Archive(gomock.Any(), gomock.Any()).Return(&gitilespb.ArchiveResponse{ 646 Contents: tarGzContent, 647 }, nil) 648 mockGsClient.EXPECT().UploadIfMissing( 649 gomock.Any(), gomock.Eq(testGSBucket), 650 gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) 651 652 Convey("has warning", func() { 653 mockValidator.result = &cfgcommonpb.ValidationResult{ 654 Messages: []*cfgcommonpb.ValidationResult_Message{ 655 { 656 Path: "foo.cfg", 657 Severity: cfgcommonpb.ValidationResult_WARNING, 658 Text: "this is a warning", 659 }, 660 }, 661 } 662 err = importer.ImportConfigSet(ctx, cs) 663 So(err, ShouldBeNil) 664 revKey := datastore.MakeKey(ctx, model.ConfigSetKind, string(cs), model.RevisionKind, latestCommit.Id) 665 var files []*model.File 666 So(datastore.GetAll(ctx, datastore.NewQuery(model.FileKind).Ancestor(revKey), &files), ShouldBeNil) 667 So(files, ShouldHaveLength, 1) 668 So(files[0].Path, ShouldEqual, "foo.cfg") 669 attempt := &model.ImportAttempt{ 670 ConfigSet: datastore.MakeKey(ctx, model.ConfigSetKind, string(cs)), 671 } 672 So(datastore.Get(ctx, attempt), ShouldBeNil) 673 So(attempt.Success, ShouldBeTrue) 674 So(attempt.Revision.ID, ShouldEqual, latestCommit.Id) 675 So(attempt.Message, ShouldEqual, "Imported with warnings") 676 So(attempt.ValidationResult, ShouldResembleProto, mockValidator.result) 677 }) 678 Convey("has error", func() { 679 mockValidator.result = &cfgcommonpb.ValidationResult{ 680 Messages: []*cfgcommonpb.ValidationResult_Message{ 681 { 682 Path: "foo.cfg", 683 Severity: cfgcommonpb.ValidationResult_ERROR, 684 Text: "this is an error", 685 }, 686 }, 687 } 688 689 Convey("author email is google", func() { 690 latestCommit.Author = &git.Commit_User{ 691 Name: "author", 692 Email: "author@google.com", 693 } 694 695 err = importer.ImportConfigSet(ctx, cs) 696 So(err, ShouldBeNil) 697 698 So(metrics.RejectedCfgImportCounter.Get(ctx, string(cs), latestCommit.Id, "author"), ShouldEqual, 1) 699 }) 700 701 Convey("author email is non-google", func() { 702 latestCommit.Author = &git.Commit_User{ 703 Name: "author", 704 Email: "author@chrmoium.org", 705 } 706 707 err = importer.ImportConfigSet(ctx, cs) 708 So(err, ShouldBeNil) 709 710 So(metrics.RejectedCfgImportCounter.Get(ctx, string(cs), latestCommit.Id, ""), ShouldEqual, 1) 711 }) 712 713 revKey := datastore.MakeKey(ctx, model.ConfigSetKind, string(cs), model.RevisionKind, latestCommit.Id) 714 var files []*model.File 715 So(datastore.GetAll(ctx, datastore.NewQuery(model.FileKind).Ancestor(revKey), &files), ShouldBeNil) 716 So(files, ShouldBeEmpty) 717 attempt := &model.ImportAttempt{ 718 ConfigSet: datastore.MakeKey(ctx, model.ConfigSetKind, string(cs)), 719 } 720 So(datastore.Get(ctx, attempt), ShouldBeNil) 721 So(attempt.Success, ShouldBeFalse) 722 So(attempt.Message, ShouldEqual, "Invalid config") 723 So(attempt.Revision.ID, ShouldEqual, latestCommit.Id) 724 So(attempt.ValidationResult, ShouldResembleProto, mockValidator.result) 725 }) 726 }) 727 728 Convey("large config", func() { 729 mockGtClient.EXPECT().Log(gomock.Any(), gomock.Any()).Return(&gitilespb.LogResponse{ 730 Log: []*git.Commit{latestCommit}, 731 }, nil) 732 // Construct incompressible data which is larger than compressedContentLimit. 733 incompressible := make([]byte, compressedContentLimit+1024*1024) 734 _, err := rand.New(rand.NewSource(1234)).Read(incompressible) 735 So(err, ShouldBeNil) 736 compressed, err := gzipCompress(incompressible) 737 So(err, ShouldBeNil) 738 So(len(compressed) > compressedContentLimit, ShouldBeTrue) 739 740 tarGzContent, err := buildTarGz(map[string]any{"large": incompressible}) 741 So(err, ShouldBeNil) 742 expectedSha256 := sha256.Sum256(incompressible) 743 expectedGsFileName := "configs/sha256/" + hex.EncodeToString(expectedSha256[:]) 744 mockGtClient.EXPECT().Archive(gomock.Any(), gomock.Any()).Return(&gitilespb.ArchiveResponse{ 745 Contents: tarGzContent, 746 }, nil) 747 mockGsClient.EXPECT().UploadIfMissing( 748 gomock.Any(), gomock.Eq(testGSBucket), 749 gomock.Eq(expectedGsFileName), gomock.Any(), gomock.Any()).Return(true, nil) 750 751 err = importer.ImportConfigSet(ctx, config.MustServiceSet("myservice")) 752 753 So(err, ShouldBeNil) 754 revKey := datastore.MakeKey(ctx, model.ConfigSetKind, "services/myservice", model.RevisionKind, latestCommit.Id) 755 var files []*model.File 756 So(datastore.GetAll(ctx, datastore.NewQuery(model.FileKind).Ancestor(revKey), &files), ShouldBeNil) 757 So(files, ShouldHaveLength, 1) 758 file := files[0] 759 So(file.Path, ShouldEqual, "large") 760 So(file.Content, ShouldBeNil) 761 So(file.GcsURI, ShouldEqual, gs.MakePath(testGSBucket, expectedGsFileName)) 762 }) 763 }) 764 765 Convey("unhappy path", func() { 766 Convey("bad config set format", func() { 767 err := importer.ImportConfigSet(ctx, config.Set("bad")) 768 So(err, ShouldErrLike, "Invalid config set") 769 }) 770 771 Convey("project doesn't exist ", func() { 772 testutil.InjectSelfConfigs(ctx, map[string]proto.Message{ 773 common.ProjRegistryFilePath: &cfgcommonpb.ProjectsCfg{ 774 Projects: []*cfgcommonpb.Project{ 775 { 776 Id: "proj", 777 Location: &cfgcommonpb.Project_GitilesLocation{ 778 GitilesLocation: &cfgcommonpb.GitilesLocation{ 779 Repo: "https://chromium.googlesource.com/infra/infra", 780 Ref: "refs/heads/main", 781 Path: "generated", 782 }, 783 }, 784 }, 785 }, 786 }, 787 }) 788 err := importer.ImportConfigSet(ctx, config.MustProjectSet("unknown_proj")) 789 So(err, ShouldErrLike, `project "unknown_proj" not exist or has no gitiles location`) 790 So(ErrFatalTag.In(err), ShouldBeTrue) 791 }) 792 793 Convey("no project gitiles location", func() { 794 testutil.InjectSelfConfigs(ctx, map[string]proto.Message{ 795 common.ProjRegistryFilePath: &cfgcommonpb.ProjectsCfg{ 796 Projects: []*cfgcommonpb.Project{ 797 {Id: "proj"}, 798 }, 799 }, 800 }) 801 err := importer.ImportConfigSet(ctx, config.MustProjectSet("proj")) 802 So(err, ShouldErrLike, `project "proj" not exist or has no gitiles location`) 803 So(ErrFatalTag.In(err), ShouldBeTrue) 804 }) 805 806 Convey("cannot fetch logs", func() { 807 mockGtClient.EXPECT().Log(gomock.Any(), gomock.Any()).Return(nil, errors.New("gitiles internal errors")) 808 err := importer.ImportConfigSet(ctx, config.MustServiceSet("myservice")) 809 So(err, ShouldErrLike, "cannot fetch logs", "gitiles internal errors") 810 attempt := &model.ImportAttempt{ 811 ConfigSet: datastore.MakeKey(ctx, model.ConfigSetKind, "services/myservice"), 812 } 813 So(datastore.Get(ctx, attempt), ShouldBeNil) 814 So(attempt.Success, ShouldBeFalse) 815 So(attempt.Message, ShouldContainSubstring, "cannot fetch logs") 816 }) 817 818 Convey("bad tar file", func() { 819 loc := &cfgcommonpb.Location{ 820 Location: &cfgcommonpb.Location_GitilesLocation{ 821 GitilesLocation: &cfgcommonpb.GitilesLocation{ 822 Repo: "https://a.googlesource.com/infradata/config", 823 Ref: "refs/heads/main", 824 Path: "dev-configs/myservice", 825 }, 826 }, 827 } 828 cfgSetBeforeImport := &model.ConfigSet{ 829 ID: config.MustServiceSet("myservice"), 830 Location: loc, 831 LatestRevision: model.RevisionInfo{ID: "old revision"}, 832 } 833 So(datastore.Put(ctx, cfgSetBeforeImport), ShouldBeNil) 834 mockGtClient.EXPECT().Log(gomock.Any(), gomock.Any()).Return(&gitilespb.LogResponse{ 835 Log: []*git.Commit{latestCommit}, 836 }, nil) 837 mockGtClient.EXPECT().Archive(gomock.Any(), gomock.Any()).Return(&gitilespb.ArchiveResponse{ 838 Contents: []byte("invalid .tar.gz content"), 839 }, nil) 840 841 err := importer.ImportConfigSet(ctx, config.MustServiceSet("myservice")) 842 843 So(err, ShouldErrLike, "Failed to import services/myservice revision latest revision") 844 cfgSetAfterImport := &model.ConfigSet{ 845 ID: config.MustServiceSet("myservice"), 846 } 847 attempt := &model.ImportAttempt{ 848 ConfigSet: datastore.KeyForObj(ctx, cfgSetAfterImport), 849 } 850 So(datastore.Get(ctx, cfgSetAfterImport, attempt), ShouldBeNil) 851 852 So(cfgSetAfterImport.Location, ShouldResembleProto, cfgSetBeforeImport.Location) 853 So(cfgSetAfterImport.LatestRevision.ID, ShouldEqual, cfgSetBeforeImport.LatestRevision.ID) 854 855 So(attempt.Success, ShouldBeFalse) 856 So(attempt.Message, ShouldContainSubstring, "Failed to import services/myservice revision latest revision") 857 }) 858 859 Convey("failed to upload to GCS", func() { 860 mockGtClient.EXPECT().Log(gomock.Any(), gomock.Any()).Return(&gitilespb.LogResponse{ 861 Log: []*git.Commit{latestCommit}, 862 }, nil) 863 // Construct incompressible data which is larger than compressedContentLimit. 864 incompressible := make([]byte, compressedContentLimit+1024*1024) 865 _, err := rand.New(rand.NewSource(1234)).Read(incompressible) 866 So(err, ShouldBeNil) 867 compressed, err := gzipCompress(incompressible) 868 So(err, ShouldBeNil) 869 So(len(compressed) > compressedContentLimit, ShouldBeTrue) 870 tarGzContent, err := buildTarGz(map[string]any{"large": incompressible}) 871 So(err, ShouldBeNil) 872 expectedSha256 := sha256.Sum256(incompressible) 873 expectedGsFileName := "configs/sha256/" + hex.EncodeToString(expectedSha256[:]) 874 mockGtClient.EXPECT().Archive(gomock.Any(), gomock.Any()).Return(&gitilespb.ArchiveResponse{ 875 Contents: tarGzContent, 876 }, nil) 877 mockGsClient.EXPECT().UploadIfMissing( 878 gomock.Any(), gomock.Eq(testGSBucket), 879 gomock.Eq(expectedGsFileName), gomock.Any(), gomock.Any()).Return(false, errors.New("GCS internal error")) 880 881 err = importer.ImportConfigSet(ctx, config.MustServiceSet("myservice")) 882 883 So(err, ShouldErrLike, "failed to upload file", "GCS internal error") 884 revKey := datastore.MakeKey(ctx, model.ConfigSetKind, "services/myservice", model.RevisionKind, latestCommit.Id) 885 var files []*model.File 886 So(datastore.GetAll(ctx, datastore.NewQuery(model.FileKind).Ancestor(revKey), &files), ShouldBeNil) 887 So(files, ShouldHaveLength, 0) 888 889 attempt := &model.ImportAttempt{ 890 ConfigSet: datastore.MakeKey(ctx, model.ConfigSetKind, "services/myservice"), 891 } 892 So(datastore.Get(ctx, attempt), ShouldBeNil) 893 So(attempt.Success, ShouldBeFalse) 894 So(attempt.Message, ShouldContainSubstring, "failed to upload file") 895 }) 896 897 Convey("validation error", func() { 898 mockGtClient.EXPECT().Log(gomock.Any(), gomock.Any()).Return(&gitilespb.LogResponse{ 899 Log: []*git.Commit{latestCommit}, 900 }, nil) 901 902 tarGzContent, err := buildTarGz(map[string]any{"foo.cfg": "content"}) 903 So(err, ShouldBeNil) 904 mockGtClient.EXPECT().Archive(gomock.Any(), gomock.Any()).Return(&gitilespb.ArchiveResponse{ 905 Contents: tarGzContent, 906 }, nil) 907 mockGsClient.EXPECT().UploadIfMissing( 908 gomock.Any(), gomock.Eq(testGSBucket), 909 gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) 910 911 mockValidator.err = errors.New("something went wrong during validation") 912 err = importer.ImportConfigSet(ctx, cs) 913 So(err, ShouldErrLike, "something went wrong during validation") 914 }) 915 }) 916 }) 917 } 918 919 func TestReImport(t *testing.T) { 920 t.Parallel() 921 922 Convey("Reimport", t, func() { 923 rsp := httptest.NewRecorder() 924 rctx := &router.Context{ 925 Writer: rsp, 926 } 927 928 mockValidator := &mockValidator{} 929 importer := &Importer{ 930 Validator: mockValidator, 931 } 932 ctx := testutil.SetupContext() 933 userID := identity.Identity("user:user@example.com") 934 fakeAuthDB := authtest.NewFakeDB() 935 testutil.InjectSelfConfigs(ctx, map[string]proto.Message{ 936 common.ACLRegistryFilePath: &cfgcommonpb.AclCfg{ 937 ServiceAccessGroup: "service-access-group", 938 ServiceReimportGroup: "service-reimport-group", 939 }, 940 }) 941 fakeAuthDB.AddMocks( 942 authtest.MockMembership(userID, "service-access-group"), 943 authtest.MockMembership(userID, "service-reimport-group"), 944 ) 945 ctx = auth.WithState(ctx, &authtest.FakeState{ 946 Identity: userID, 947 FakeDB: fakeAuthDB, 948 }) 949 950 Convey("no ConfigSet param", func() { 951 rctx.Request = (&http.Request{}).WithContext(ctx) 952 importer.Reimport(rctx) 953 So(rsp.Code, ShouldEqual, http.StatusBadRequest) 954 So(rsp.Body.String(), ShouldContainSubstring, "config set is not specified") 955 }) 956 957 Convey("invalid config set", func() { 958 rctx.Request = (&http.Request{}).WithContext(ctx) 959 rctx.Params = httprouter.Params{ 960 {Key: "ConfigSet", Value: "badCfgSet"}, 961 } 962 importer.Reimport(rctx) 963 So(rsp.Code, ShouldEqual, http.StatusBadRequest) 964 So(rsp.Body.String(), ShouldContainSubstring, `invalid config set: unknown domain "badCfgSet" for config set "badCfgSet"`) 965 }) 966 967 Convey("no permission", func() { 968 ctx = auth.WithState(ctx, &authtest.FakeState{ 969 Identity: identity.Identity("user:random@example.com"), 970 FakeDB: authtest.NewFakeDB(), 971 }) 972 rctx.Request = (&http.Request{}).WithContext(ctx) 973 rctx.Params = httprouter.Params{ 974 {Key: "ConfigSet", Value: "services/myservice"}, 975 } 976 importer.Reimport(rctx) 977 So(rsp.Code, ShouldEqual, http.StatusForbidden) 978 So(rsp.Body.String(), ShouldContainSubstring, `"user:random@example.com" is not allowed to reimport services/myservice`) 979 }) 980 981 Convey("config set not found", func() { 982 rctx.Request = (&http.Request{}).WithContext(ctx) 983 rctx.Params = httprouter.Params{ 984 {Key: "ConfigSet", Value: "services/myservice"}, 985 } 986 importer.Reimport(rctx) 987 So(rsp.Code, ShouldEqual, http.StatusNotFound) 988 So(rsp.Body.String(), ShouldContainSubstring, `"services/myservice" is not found`) 989 }) 990 991 Convey("ok", func() { 992 ctx = settings.WithGlobalConfigLoc(ctx, &cfgcommonpb.GitilesLocation{ 993 Repo: "https://a.googlesource.com/infradata/config", 994 Ref: "refs/heads/main", 995 Path: "dev-configs", 996 }) 997 testutil.InjectConfigSet(ctx, "services/myservice", nil) 998 ctl := gomock.NewController(t) 999 mockGtClient := mock_gitiles.NewMockGitilesClient(ctl) 1000 ctx = context.WithValue(ctx, &clients.MockGitilesClientKey, mockGtClient) 1001 mockGtClient.EXPECT().Log(gomock.Any(), gomock.Any()).Return(&gitilespb.LogResponse{}, nil) 1002 1003 rctx.Request = (&http.Request{}).WithContext(ctx) 1004 rctx.Params = httprouter.Params{ 1005 {Key: "ConfigSet", Value: "services/myservice"}, 1006 } 1007 importer.Reimport(rctx) 1008 1009 So(rsp.Code, ShouldEqual, http.StatusOK) 1010 }) 1011 1012 Convey("internal error", func() { 1013 ctx = settings.WithGlobalConfigLoc(ctx, &cfgcommonpb.GitilesLocation{ 1014 Repo: "https://a.googlesource.com/infradata/config", 1015 Ref: "refs/heads/main", 1016 Path: "dev-configs", 1017 }) 1018 testutil.InjectConfigSet(ctx, "services/myservice", nil) 1019 ctl := gomock.NewController(t) 1020 mockGtClient := mock_gitiles.NewMockGitilesClient(ctl) 1021 ctx = context.WithValue(ctx, &clients.MockGitilesClientKey, mockGtClient) 1022 mockGtClient.EXPECT().Log(gomock.Any(), gomock.Any()).Return(nil, errors.New("gitiles internal error")) 1023 1024 rctx.Request = (&http.Request{}).WithContext(ctx) 1025 rctx.Params = httprouter.Params{ 1026 {Key: "ConfigSet", Value: "services/myservice"}, 1027 } 1028 importer.Reimport(rctx) 1029 1030 So(rsp.Code, ShouldEqual, http.StatusInternalServerError) 1031 So(rsp.Body.String(), ShouldContainSubstring, `error when reimporting "services/myservice"`) 1032 }) 1033 }) 1034 } 1035 1036 func buildTarGz(raw map[string]any) ([]byte, error) { 1037 var buf bytes.Buffer 1038 gzw := gzip.NewWriter(&buf) 1039 tw := tar.NewWriter(gzw) 1040 for name, body := range raw { 1041 hdr := &tar.Header{ 1042 Name: name, 1043 Typeflag: tar.TypeReg, 1044 } 1045 if strings.HasSuffix(name, "/") { 1046 hdr.Typeflag = tar.TypeDir 1047 } 1048 1049 var bodyBytes []byte 1050 switch v := body.(type) { 1051 case string: 1052 bodyBytes = []byte(body.(string)) 1053 case []byte: 1054 bodyBytes = body.([]byte) 1055 default: 1056 return nil, errors.Reason("unsupported type %T for %s", v, name).Err() 1057 } 1058 hdr.Size = int64(len(bodyBytes)) 1059 if err := tw.WriteHeader(hdr); err != nil { 1060 return nil, err 1061 } 1062 if _, err := tw.Write(bodyBytes); err != nil { 1063 return nil, err 1064 } 1065 } 1066 if err := tw.Flush(); err != nil { 1067 return nil, err 1068 } 1069 if err := tw.Close(); err != nil { 1070 return nil, err 1071 } 1072 if err := gzw.Flush(); err != nil { 1073 return nil, err 1074 } 1075 if err := gzw.Close(); err != nil { 1076 return nil, err 1077 } 1078 return buf.Bytes(), nil 1079 } 1080 1081 func getCfgSetsInTaskQueue(sch *tqtesting.Scheduler) []string { 1082 cfgSets := make([]string, len(sch.Tasks())) 1083 for i, task := range sch.Tasks() { 1084 cfgSets[i] = task.Payload.(*taskpb.ImportConfigs).ConfigSet 1085 } 1086 sort.Slice(cfgSets, func(i, j int) bool { return cfgSets[i] < cfgSets[j] }) 1087 return cfgSets 1088 } 1089 1090 func gzipCompress(b []byte) ([]byte, error) { 1091 buf := &bytes.Buffer{} 1092 gw := gzip.NewWriter(buf) 1093 if _, err := gw.Write(b); err != nil { 1094 return nil, err 1095 } 1096 if err := gw.Close(); err != nil { 1097 return nil, err 1098 } 1099 return buf.Bytes(), nil 1100 }