go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/internal/validation/validate.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 validation 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "sort" 22 "strings" 23 24 "golang.org/x/sync/errgroup" 25 26 "go.chromium.org/luci/common/gcloud/gs" 27 "go.chromium.org/luci/common/logging" 28 cfgcommonpb "go.chromium.org/luci/common/proto/config" 29 "go.chromium.org/luci/config" 30 "go.chromium.org/luci/config/validation" 31 32 "go.chromium.org/luci/config_service/internal/clients" 33 "go.chromium.org/luci/config_service/internal/model" 34 ) 35 36 // finder defines the interface for validator to find the corresponding services 37 // to validate the given config file. 38 type finder interface { 39 FindInterestedServices(ctx context.Context, cs config.Set, filePath string) []*model.Service 40 } 41 42 // Validator validates config files backed Google Cloud Storage. 43 type Validator struct { 44 // GsClient is used to talk Google Cloud Storage 45 GsClient clients.GsClient 46 // Finder is used to find the services that can validate a given config files. 47 Finder finder 48 // SelfRuleSet is the RuleSet that validates the configs against LUCI Config 49 // itself. 50 SelfRuleSet *validation.RuleSet 51 } 52 53 // File defines the interface of a config file in validation. 54 type File interface { 55 // GetPath returns the relative path to the config file from config root. 56 GetPath() string 57 // GetGSPath returns the GCS path to where the config file is stored. 58 // 59 // The GCS object that the path points to SHOULD exist before calling 60 // `Validate`. 61 GetGSPath() gs.Path 62 // GetRawContent returns the raw and uncompressed content of this config. 63 // 64 // This is currently used to validate the configs LUCI Config itself is 65 // interested in and validate configs against legacy services. 66 GetRawContent(context.Context) ([]byte, error) 67 } 68 69 // ensuring *model.File implements File interface 70 var _ File = (*model.File)(nil) 71 72 // Validate validates the provided config files. 73 func (v *Validator) Validate(ctx context.Context, cs config.Set, files []File) (*cfgcommonpb.ValidationResult, error) { 74 srvValidators := v.makeServiceValidators(ctx, cs, files) 75 if len(srvValidators) == 0 { // no service can validate input files. 76 return &cfgcommonpb.ValidationResult{}, nil 77 } 78 results := make([]*cfgcommonpb.ValidationResult, len(srvValidators)) 79 eg, ectx := errgroup.WithContext(ctx) 80 for i, sv := range srvValidators { 81 i, sv := i, sv 82 eg.Go(func() (err error) { 83 filePaths := make([]string, len(sv.files)) 84 for i, file := range sv.files { 85 filePaths[i] = file.GetPath() 86 } 87 logging.Debugf(ctx, "sending files [%s] to service %q to validate", filePaths, sv.service.Name) 88 switch results[i], err = sv.validate(ectx); { 89 case errors.Is(err, context.Canceled): 90 logging.Warningf(ctx, "validating configs against service %q for files [%s] is cancelled", sv.service.Name, filePaths) 91 return err 92 case err != nil: 93 err = fmt.Errorf("failed to validate configs against service %q for files [%s]: %w", sv.service.Name, filePaths, err) 94 logging.Errorf(ctx, "%s", err) 95 return err 96 } 97 return nil 98 }) 99 } 100 if err := eg.Wait(); err != nil { 101 return nil, err 102 } 103 return mergeResults(results), nil 104 } 105 106 // makeServiceValidators constructs `serviceValidator`s with files each service 107 // is interested in. The `serviceValidator` will be used to validate the config 108 // later on. 109 func (v *Validator) makeServiceValidators(ctx context.Context, cs config.Set, files []File) []*serviceValidator { 110 svs := make(map[string]*serviceValidator, len(files)) 111 for _, file := range files { 112 services := v.Finder.FindInterestedServices(ctx, cs, file.GetPath()) 113 for _, service := range services { 114 if _, ok := svs[service.Name]; !ok { 115 svs[service.Name] = &serviceValidator{ 116 service: service, 117 gsClient: v.GsClient, 118 selfRuleSet: v.SelfRuleSet, 119 cs: cs, 120 } 121 } 122 svs[service.Name].files = append(svs[service.Name].files, file) 123 } 124 } 125 ret := make([]*serviceValidator, 0, len(svs)) 126 for _, v := range svs { 127 ret = append(ret, v) 128 } 129 return ret 130 } 131 132 func mergeResults(results []*cfgcommonpb.ValidationResult) *cfgcommonpb.ValidationResult { 133 ret := &cfgcommonpb.ValidationResult{} 134 for _, res := range results { 135 ret.Messages = append(ret.Messages, res.GetMessages()...) 136 } 137 // Sort lexicographically by path first and then sort from most severe to 138 // least severe if path is the same. 139 sort.SliceStable(ret.Messages, func(i, j int) bool { 140 switch res := strings.Compare(ret.Messages[i].GetPath(), ret.Messages[j].GetPath()); res { 141 case 0: 142 return ret.Messages[i].GetSeverity() > ret.Messages[j].GetSeverity() 143 default: 144 return res < 0 145 } 146 }) 147 return ret 148 }