go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/internal/importer/importer.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 handles all configs importing. 16 package importer 17 18 import ( 19 "archive/tar" 20 "bytes" 21 "context" 22 "crypto/sha256" 23 "encoding/hex" 24 "fmt" 25 "io" 26 "net/http" 27 "strings" 28 "time" 29 30 "cloud.google.com/go/storage" 31 "github.com/klauspost/compress/gzip" 32 "google.golang.org/protobuf/proto" 33 34 "go.chromium.org/luci/common/api/gitiles" 35 "go.chromium.org/luci/common/clock" 36 "go.chromium.org/luci/common/data/stringset" 37 "go.chromium.org/luci/common/errors" 38 "go.chromium.org/luci/common/gcloud/gs" 39 "go.chromium.org/luci/common/logging" 40 cfgcommonpb "go.chromium.org/luci/common/proto/config" 41 "go.chromium.org/luci/common/proto/git" 42 gitilespb "go.chromium.org/luci/common/proto/gitiles" 43 "go.chromium.org/luci/common/sync/parallel" 44 "go.chromium.org/luci/config" 45 "go.chromium.org/luci/gae/service/datastore" 46 "go.chromium.org/luci/server/auth" 47 "go.chromium.org/luci/server/cron" 48 "go.chromium.org/luci/server/router" 49 "go.chromium.org/luci/server/tq" 50 51 "go.chromium.org/luci/config_service/internal/acl" 52 "go.chromium.org/luci/config_service/internal/clients" 53 "go.chromium.org/luci/config_service/internal/common" 54 "go.chromium.org/luci/config_service/internal/metrics" 55 "go.chromium.org/luci/config_service/internal/model" 56 "go.chromium.org/luci/config_service/internal/settings" 57 "go.chromium.org/luci/config_service/internal/taskpb" 58 "go.chromium.org/luci/config_service/internal/validation" 59 ) 60 61 const ( 62 // compressedContentLimit is the maximum allowed compressed content size to 63 // store into Datastore, in order to avoid exceeding 1MiB limit per entity. 64 compressedContentLimit = 800 * 1024 65 66 // maxRetryCount is the maximum number of retries allowed when importing 67 // a single config set hit non-fatal error. 68 maxRetryCount = 5 69 70 // importAllConfigsInterval is the interval between two import config cron 71 // jobs. 72 importAllConfigsInterval = 10 * time.Minute 73 ) 74 75 var ( 76 // ErrFatalTag is an error tag to indicate an unrecoverable error in the 77 // configs importing flow. 78 ErrFatalTag = errors.BoolTag{Key: errors.NewTagKey("A config importing unrecoverable error")} 79 ) 80 81 // validator defines the interface to interact with validation logic. 82 type validator interface { 83 Validate(context.Context, config.Set, []validation.File) (*cfgcommonpb.ValidationResult, error) 84 } 85 86 // Importer is able to import a config set. 87 type Importer struct { 88 // GSBucket is the bucket name where the imported configs will be stored to. 89 GSBucket string 90 // Validator is used to validate the configs before import. 91 Validator validator 92 } 93 94 // RegisterImportConfigsCron register the cron to trigger import for all config 95 // sets 96 func (i *Importer) RegisterImportConfigsCron(dispatcher *tq.Dispatcher) { 97 i.registerTQTask(dispatcher) 98 cron.RegisterHandler("import-configs", func(ctx context.Context) error { 99 return importAllConfigs(ctx, dispatcher) 100 }) 101 } 102 103 func (i *Importer) registerTQTask(dispatcher *tq.Dispatcher) { 104 dispatcher.RegisterTaskClass(tq.TaskClass{ 105 ID: "import-configs", 106 Kind: tq.NonTransactional, 107 Prototype: (*taskpb.ImportConfigs)(nil), 108 Queue: "backend-v2", 109 Handler: func(ctx context.Context, payload proto.Message) error { 110 task := payload.(*taskpb.ImportConfigs) 111 switch err := i.ImportConfigSet(ctx, config.Set(task.GetConfigSet())); { 112 case ErrFatalTag.In(err): 113 return tq.Fatal.Apply(err) 114 case err != nil: // non-fatal error 115 if info := tq.TaskExecutionInfo(ctx); info != nil && info.ExecutionCount >= maxRetryCount { 116 // ignore the task as it exceeds the max retry count. Alert should be 117 // set up to monitor the number of retries. 118 return tq.Ignore.Apply(err) 119 } 120 return err // tq will retry the error 121 default: 122 return nil 123 } 124 }, 125 }) 126 } 127 128 // importAllConfigs schedules a task for each service and project config set to 129 // import configs from Gitiles and clean up stale config sets. 130 func importAllConfigs(ctx context.Context, dispatcher *tq.Dispatcher) error { 131 cfgLoc := settings.GetGlobalConfigLoc(ctx) 132 133 // Get all config sets. 134 cfgSets, err := getAllServiceCfgSets(ctx, cfgLoc) 135 if err != nil { 136 return errors.Annotate(err, "failed to load service config sets").Err() 137 } 138 sets, err := getAllProjCfgSets(ctx) 139 if err != nil { 140 return errors.Annotate(err, "failed to load project config sets").Err() 141 } 142 cfgSets = append(cfgSets, sets...) 143 144 // Enqueue tasks 145 err = parallel.WorkPool(8, func(workCh chan<- func() error) { 146 for _, cs := range cfgSets { 147 cs := cs 148 workCh <- func() error { 149 err := dispatcher.AddTask(ctx, &tq.Task{ 150 Payload: &taskpb.ImportConfigs{ConfigSet: cs}, 151 Title: fmt.Sprintf("configset/%s", cs), 152 ETA: clock.Now(ctx).Add(common.DistributeOffset(importAllConfigsInterval, "config_set", string(cs))), 153 }) 154 return errors.Annotate(err, "failed to enqueue ImportConfigs task for %q: %s", cs, err).Err() 155 } 156 } 157 }) 158 if err != nil { 159 return err 160 } 161 162 // Delete stale config sets. 163 var keys []*datastore.Key 164 if err := datastore.GetAll(ctx, datastore.NewQuery(model.ConfigSetKind).KeysOnly(true), &keys); err != nil { 165 return errors.Annotate(err, "failed to fetch all config sets from Datastore").Err() 166 } 167 cfgSetsInDB := stringset.New(len(keys)) 168 for _, key := range keys { 169 cfgSetsInDB.Add(key.StringID()) 170 } 171 cfgSetsInDB.DelAll(cfgSets) 172 if len(cfgSetsInDB) > 0 { 173 logging.Infof(ctx, "deleting stale config sets: %v", cfgSetsInDB) 174 var toDel []*datastore.Key 175 for _, cs := range cfgSetsInDB.ToSlice() { 176 toDel = append(toDel, datastore.KeyForObj(ctx, &model.ConfigSet{ID: config.Set(cs)})) 177 } 178 return datastore.Delete(ctx, toDel) 179 } 180 return nil 181 } 182 183 // getAllProjCfgSets fetches all "projects/*" config sets 184 func getAllProjCfgSets(ctx context.Context) ([]string, error) { 185 projectsCfg := &cfgcommonpb.ProjectsCfg{} 186 var nerr *model.NoSuchConfigError 187 switch err := common.LoadSelfConfig[*cfgcommonpb.ProjectsCfg](ctx, common.ProjRegistryFilePath, projectsCfg); { 188 case errors.As(err, &nerr) && nerr.IsUnknownConfigSet(): 189 // May happen on the cron job first run. Just log the warning. 190 logging.Warningf(ctx, "failed to compose all project config sets because the self config set is missing") 191 return nil, nil 192 case err != nil: 193 return nil, err 194 } 195 cfgsets := make([]string, len(projectsCfg.Projects)) 196 for i, proj := range projectsCfg.Projects { 197 cs, err := config.ProjectSet(proj.Id) 198 if err != nil { 199 return nil, err 200 } 201 cfgsets[i] = string(cs) 202 } 203 return cfgsets, nil 204 } 205 206 // getAllServiceCfgSets returns all "service/*" config sets. 207 func getAllServiceCfgSets(ctx context.Context, cfgLoc *cfgcommonpb.GitilesLocation) ([]string, error) { 208 if cfgLoc == nil { 209 return nil, nil 210 } 211 host, project, err := gitiles.ParseRepoURL(cfgLoc.Repo) 212 if err != nil { 213 return nil, errors.Annotate(err, "invalid gitiles repo: %s", cfgLoc.Repo).Err() 214 } 215 gitilesClient, err := clients.NewGitilesClient(ctx, host, "") 216 if err != nil { 217 return nil, errors.Annotate(err, "failed to create a gitiles client").Err() 218 } 219 220 res, err := gitilesClient.ListFiles(ctx, &gitilespb.ListFilesRequest{ 221 Project: project, 222 Committish: cfgLoc.Ref, 223 Path: cfgLoc.Path, 224 }) 225 if err != nil { 226 return nil, errors.Annotate(err, "failed to call Gitiles to list files").Err() 227 } 228 var cfgSets []string 229 for _, f := range res.GetFiles() { 230 if f.Type != git.File_TREE { 231 continue 232 } 233 cs, err := config.ServiceSet(f.GetPath()) 234 if err != nil { 235 logging.Errorf(ctx, "skip importing service config: %s", err) 236 continue 237 } 238 cfgSets = append(cfgSets, string(cs)) 239 } 240 return cfgSets, nil 241 } 242 243 // ImportConfigSet tries to import a config set. 244 // TODO(crbug.com/1446839): Optional: for ErrFatalTag errors or errors which are 245 // retried many times, may send notifications to Config Service owners in future 246 // after the notification functionality is done. 247 func (i *Importer) ImportConfigSet(ctx context.Context, cfgSet config.Set) error { 248 if sID := cfgSet.Service(); sID != "" { 249 globalCfgLoc := settings.GetGlobalConfigLoc(ctx) 250 return i.importConfigSet(ctx, cfgSet, &cfgcommonpb.GitilesLocation{ 251 Repo: globalCfgLoc.Repo, 252 Ref: globalCfgLoc.Ref, 253 Path: strings.TrimPrefix(fmt.Sprintf("%s/%s", globalCfgLoc.Path, sID), "/"), 254 }) 255 } else if pID := cfgSet.Project(); pID != "" { 256 return i.importProject(ctx, pID) 257 } 258 return errors.Reason("Invalid config set: %q", cfgSet).Tag(ErrFatalTag).Err() 259 } 260 261 // importProject imports a project config set. 262 func (i *Importer) importProject(ctx context.Context, projectID string) error { 263 projectsCfg := &cfgcommonpb.ProjectsCfg{} 264 if err := common.LoadSelfConfig[*cfgcommonpb.ProjectsCfg](ctx, common.ProjRegistryFilePath, projectsCfg); err != nil { 265 return ErrFatalTag.Apply(err) 266 } 267 var projLoc *cfgcommonpb.GitilesLocation 268 for _, p := range projectsCfg.GetProjects() { 269 if p.Id == projectID { 270 projLoc = p.GetGitilesLocation() 271 break 272 } 273 } 274 if projLoc == nil { 275 return errors.Reason("project %q not exist or has no gitiles location", projectID).Tag(ErrFatalTag).Err() 276 } 277 return i.importConfigSet(ctx, config.MustProjectSet(projectID), projLoc) 278 } 279 280 // importConfigSet tries to import the latest version of the given config set. 281 // TODO(crbug.com/1446839): Add code to report to metrics 282 func (i *Importer) importConfigSet(ctx context.Context, cfgSet config.Set, loc *cfgcommonpb.GitilesLocation) error { 283 ctx = logging.SetFields(ctx, logging.Fields{ 284 "ConfigSet": string(cfgSet), 285 "Location": common.GitilesURL(loc), 286 }) 287 logging.Infof(ctx, "Start importing configs") 288 saveAttempt := func(success bool, msg string, commit *git.Commit) error { 289 attempt := &model.ImportAttempt{ 290 ConfigSet: datastore.KeyForObj(ctx, &model.ConfigSet{ID: cfgSet}), 291 Success: success, 292 Message: msg, 293 Revision: model.RevisionInfo{ 294 Location: &cfgcommonpb.Location{ 295 Location: &cfgcommonpb.Location_GitilesLocation{ 296 GitilesLocation: proto.Clone(loc).(*cfgcommonpb.GitilesLocation), 297 }, 298 }, 299 }, 300 } 301 if commit != nil { 302 attempt.Revision.ID = commit.Id 303 attempt.Revision.Location.GetGitilesLocation().Ref = commit.Id 304 attempt.Revision.CommitTime = commit.Committer.GetTime().AsTime() 305 attempt.Revision.CommitterEmail = commit.Committer.GetEmail() 306 attempt.Revision.AuthorEmail = commit.Author.GetEmail() 307 } 308 return datastore.Put(ctx, attempt) 309 } 310 311 host, project, err := gitiles.ParseRepoURL(loc.Repo) 312 if err != nil { 313 err = errors.Annotate(err, "invalid gitiles repo: %s", loc.Repo).Err() 314 return ErrFatalTag.Apply(errors.Append(err, saveAttempt(false, err.Error(), nil))) 315 } 316 gtClient, err := clients.NewGitilesClient(ctx, host, cfgSet.Project()) 317 if err != nil { 318 err = errors.Annotate(err, "failed to create a gitiles client").Err() 319 return ErrFatalTag.Apply(errors.Append(err, saveAttempt(false, err.Error(), nil))) 320 } 321 322 logRes, err := gtClient.Log(ctx, &gitilespb.LogRequest{ 323 Project: project, 324 Committish: loc.Ref, 325 Path: loc.Path, 326 PageSize: 1, 327 }) 328 if err != nil { 329 err = errors.Annotate(err, "cannot fetch logs from %s", common.GitilesURL(loc)).Err() 330 return errors.Append(err, saveAttempt(false, err.Error(), nil)) 331 } 332 if logRes == nil || len(logRes.GetLog()) == 0 { 333 logging.Warningf(ctx, "No commits") 334 return saveAttempt(true, "no commit logs", nil) 335 } 336 latestCommit := logRes.Log[0] 337 338 cfgSetInDB := &model.ConfigSet{ID: cfgSet} 339 lastAttempt := &model.ImportAttempt{ConfigSet: datastore.KeyForObj(ctx, cfgSetInDB)} 340 switch err := datastore.Get(ctx, cfgSetInDB, lastAttempt); { 341 case errors.Contains(err, datastore.ErrNoSuchEntity): // proceed with importing 342 case err != nil: 343 err = errors.Annotate(err, "failed to load config set %q or its last attempt", cfgSet).Err() 344 return errors.Append(err, saveAttempt(false, err.Error(), latestCommit)) 345 case cfgSetInDB.LatestRevision.ID == latestCommit.Id && 346 proto.Equal(cfgSetInDB.Location.GetGitilesLocation(), loc) && 347 cfgSetInDB.Version == model.CurrentCfgSetVersion && 348 lastAttempt.Success && // otherwise, something wrong with lastAttempt, better to import again. 349 len(lastAttempt.ValidationResult.GetMessages()) == 0: // avoid overriding lastAttempt's validationResult. 350 logging.Debugf(ctx, "Already up-to-date") 351 return saveAttempt(true, "Up-to-date", latestCommit) 352 } 353 354 logging.Infof(ctx, "Rolling %s => %s", cfgSetInDB.LatestRevision.ID, latestCommit.Id) 355 err = i.importRevision(ctx, cfgSet, loc, latestCommit, gtClient, project) 356 if err != nil { 357 err = errors.Annotate(err, "Failed to import %s revision %s", cfgSet, latestCommit.Id).Err() 358 return errors.Append(err, saveAttempt(false, err.Error(), latestCommit)) 359 } 360 return nil 361 } 362 363 // importRevision imports a referenced Gitiles revision into a config set. 364 // It only imports when all files are valid. 365 // TODO(crbug.com/1446839): send notifications for any validation errors. 366 func (i *Importer) importRevision(ctx context.Context, cfgSet config.Set, loc *cfgcommonpb.GitilesLocation, commit *git.Commit, gtClient gitilespb.GitilesClient, gitilesProj string) error { 367 if loc == nil || commit == nil { 368 return nil 369 } 370 res, err := gtClient.Archive(ctx, &gitilespb.ArchiveRequest{ 371 Project: gitilesProj, 372 Ref: commit.Id, 373 Path: loc.Path, 374 Format: gitilespb.ArchiveRequest_GZIP, 375 }) 376 if err != nil { 377 return err 378 } 379 rev := model.RevisionInfo{ 380 ID: commit.Id, 381 CommitTime: commit.Committer.GetTime().AsTime(), 382 CommitterEmail: commit.Committer.GetEmail(), 383 AuthorEmail: commit.Author.GetEmail(), 384 Location: &cfgcommonpb.Location{ 385 Location: &cfgcommonpb.Location_GitilesLocation{ 386 GitilesLocation: &cfgcommonpb.GitilesLocation{ 387 Repo: loc.Repo, 388 Ref: commit.Id, // TODO(crbug.com/1446839): rename to committish. 389 Path: loc.Path, 390 }, 391 }, 392 }, 393 } 394 attempt := &model.ImportAttempt{ 395 ConfigSet: datastore.MakeKey(ctx, model.ConfigSetKind, string(cfgSet)), 396 Revision: rev, 397 } 398 configSet := &model.ConfigSet{ 399 ID: cfgSet, 400 Location: &cfgcommonpb.Location{ 401 Location: &cfgcommonpb.Location_GitilesLocation{GitilesLocation: loc}, 402 }, 403 LatestRevision: rev, 404 } 405 406 if res == nil || len(res.Contents) == 0 { 407 logging.Warningf(ctx, "Configs for %s don't exist. They may be deleted", cfgSet) 408 attempt.Success = true 409 attempt.Message = "No Configs. Imported as empty" 410 return datastore.RunInTransaction(ctx, func(c context.Context) error { 411 return datastore.Put(ctx, configSet, attempt) 412 }, nil) 413 } 414 415 var files []*model.File 416 gzReader, err := gzip.NewReader(bytes.NewReader(res.Contents)) 417 if err != nil { 418 return errors.Annotate(err, "failed to ungzip gitiles archive").Err() 419 } 420 defer func() { 421 // Ignore the error. Failing to close it doesn't impact Luci-config 422 // recognize configs correctly, as they are already saved into data storage. 423 _ = gzReader.Close() 424 }() 425 426 tarReader := tar.NewReader(gzReader) 427 for { 428 header, err := tarReader.Next() 429 if err == io.EOF { 430 break 431 } 432 if err != nil { 433 return errors.Annotate(err, "failed to extract gitiles archive").Err() 434 } 435 if header.Typeflag != tar.TypeReg { 436 continue 437 } 438 439 filePath := header.Name 440 logging.Infof(ctx, "Processing file: %q", filePath) 441 file := &model.File{ 442 Path: filePath, 443 Revision: datastore.MakeKey(ctx, model.ConfigSetKind, string(cfgSet), model.RevisionKind, commit.Id), 444 Size: header.Size, 445 Location: &cfgcommonpb.Location{ 446 Location: &cfgcommonpb.Location_GitilesLocation{ 447 GitilesLocation: &cfgcommonpb.GitilesLocation{ 448 Repo: loc.Repo, 449 Ref: commit.Id, 450 Path: strings.TrimPrefix(fmt.Sprintf("%s/%s", loc.Path, filePath), "/"), 451 }, 452 }, 453 }, 454 } 455 if file.ContentSHA256, file.Content, err = hashAndCompressConfig(tarReader); err != nil { 456 return errors.Annotate(err, "filepath: %q", filePath).Err() 457 } 458 gsFileName := fmt.Sprintf("%s/sha256/%s", common.GSProdCfgFolder, file.ContentSHA256) 459 _, err = clients.GetGsClient(ctx).UploadIfMissing(ctx, i.GSBucket, gsFileName, file.Content, func(attrs *storage.ObjectAttrs) { 460 attrs.ContentEncoding = "gzip" 461 }) 462 if err != nil { 463 return errors.Annotate(err, "failed to upload file %s as %s", filePath, gsFileName).Err() 464 } 465 file.GcsURI = gs.MakePath(i.GSBucket, gsFileName) 466 if len(file.Content) > compressedContentLimit { 467 // Don't save the compressed content if the content is above the limit. 468 // Since Datastore has 1MiB entity size limit. 469 file.Content = nil 470 } 471 files = append(files, file) 472 } 473 if err := i.validateAndPopulateAttempt(ctx, cfgSet, files, attempt); err != nil { 474 return err 475 } 476 if !attempt.Success { 477 if author, ok := strings.CutSuffix(commit.Author.GetEmail(), "@google.com"); ok { 478 metrics.RejectedCfgImportCounter.Add(ctx, 1, string(cfgSet), commit.Id, author) 479 } else { 480 metrics.RejectedCfgImportCounter.Add(ctx, 1, string(cfgSet), commit.Id, "") 481 } 482 return errors.Annotate(datastore.Put(ctx, attempt), "saving attempt").Err() 483 } 484 // The rejection event rarely happen. Add by 0 to ensure the corresponding 485 // streamz counter can properly handle this metric. 486 metrics.RejectedCfgImportCounter.Add(ctx, 0, string(cfgSet), "", "") 487 488 logging.Infof(ctx, "Storing %d files, updating ConfigSet %s and ImportAttempt", len(files), cfgSet) 489 now := clock.Now(ctx).UTC() 490 for _, f := range files { 491 f.CreateTime = now 492 } 493 // Datastore transaction has a maximum size of 10MB. 494 if err := datastore.Put(ctx, files); err != nil { 495 return errors.Annotate(err, "failed to store files").Err() 496 } 497 return datastore.RunInTransaction(ctx, func(c context.Context) error { 498 return datastore.Put(ctx, configSet, attempt) 499 }, nil) 500 } 501 502 // hashAndCompressConfig reads the config and returns the sha256 of the config 503 // and the gzip-compressed bytes. 504 func hashAndCompressConfig(reader io.Reader) (string, []byte, error) { 505 sha := sha256.New() 506 compressed := &bytes.Buffer{} 507 gzipWriter := gzip.NewWriter(compressed) 508 multiWriter := io.MultiWriter(sha, gzipWriter) 509 if _, err := io.Copy(multiWriter, reader); err != nil { 510 _ = gzipWriter.Close() 511 return "", nil, errors.Annotate(err, "error reading tar file").Err() 512 } 513 if err := gzipWriter.Close(); err != nil { 514 return "", nil, errors.Annotate(err, "failed to close gzip writer").Err() 515 } 516 return hex.EncodeToString(sha.Sum(nil)), compressed.Bytes(), nil 517 } 518 519 func (i *Importer) validateAndPopulateAttempt(ctx context.Context, cfgSet config.Set, files []*model.File, attempt *model.ImportAttempt) error { 520 vfs := make([]validation.File, len(files)) 521 for i, f := range files { 522 vfs[i] = f 523 } 524 vr, err := i.Validator.Validate(ctx, cfgSet, vfs) 525 if err != nil { 526 return errors.Annotate(err, "validating config set %q", cfgSet).Err() 527 } 528 attempt.Success = true // be optimistic 529 attempt.Message = "Imported" 530 attempt.ValidationResult = vr 531 for _, msg := range attempt.ValidationResult.GetMessages() { 532 switch sev := msg.GetSeverity(); { 533 case sev >= cfgcommonpb.ValidationResult_ERROR: 534 attempt.Success = false 535 attempt.Message = "Invalid config" 536 return nil 537 case sev == cfgcommonpb.ValidationResult_WARNING: 538 attempt.Message = "Imported with warnings" 539 } 540 } 541 return nil 542 } 543 544 // Reimport handles the HTTP request of reimporting a single config set. 545 func (i *Importer) Reimport(c *router.Context) { 546 ctx := c.Request.Context() 547 caller := auth.CurrentIdentity(ctx) 548 cs := config.Set(strings.Trim(c.Params.ByName("ConfigSet"), "/")) 549 550 if cs == "" { 551 http.Error(c.Writer, "config set is not specified", http.StatusBadRequest) 552 return 553 } else if err := cs.Validate(); err != nil { 554 http.Error(c.Writer, fmt.Sprintf("invalid config set: %s", err), http.StatusBadRequest) 555 return 556 } 557 558 switch hasPerm, err := acl.CanReimportConfigSet(ctx, cs); { 559 case err != nil: 560 logging.Errorf(ctx, "cannot check permission for %q: %s", caller, err) 561 http.Error(c.Writer, fmt.Sprintf("cannot check permission for %q", caller), http.StatusInternalServerError) 562 return 563 case !hasPerm: 564 logging.Infof(ctx, "%q does not have access to %s", caller, cs) 565 http.Error(c.Writer, fmt.Sprintf("%q is not allowed to reimport %s", caller, cs), http.StatusForbidden) 566 return 567 } 568 569 switch exists, err := datastore.Exists(ctx, &model.ConfigSet{ID: cs}); { 570 case err != nil: 571 logging.Errorf(ctx, "failed to check existence of %s", cs) 572 http.Error(c.Writer, fmt.Sprintf("error when reimporting %s", cs), http.StatusInternalServerError) 573 return 574 case !exists.All(): 575 logging.Infof(ctx, "config set %s doesn't exist", cs) 576 http.Error(c.Writer, fmt.Sprintf("%q is not found", cs), http.StatusNotFound) 577 return 578 } 579 580 if err := i.ImportConfigSet(ctx, cs); err != nil { 581 logging.Errorf(ctx, "cannot re-import config set %s: %s", cs, err) 582 http.Error(c.Writer, fmt.Sprintf("error when reimporting %q", cs), http.StatusInternalServerError) 583 return 584 } 585 c.Writer.WriteHeader(http.StatusOK) 586 }