go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/cli/base/validate.go (about) 1 // Copyright 2019 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 base 16 17 import ( 18 "context" 19 "fmt" 20 "net/http" 21 "os" 22 "strings" 23 "sync" 24 25 "github.com/bazelbuild/buildtools/build" 26 "google.golang.org/grpc" 27 28 "go.chromium.org/luci/common/errors" 29 "go.chromium.org/luci/common/logging" 30 "go.chromium.org/luci/starlark/interpreter" 31 32 "go.chromium.org/luci/lucicfg" 33 "go.chromium.org/luci/lucicfg/buildifier" 34 ) 35 36 // ValidateParams contains parameters for Validate call. 37 type ValidateParams struct { 38 Loader interpreter.Loader // represents the main package 39 Source []string // paths to lint, relative to the main package 40 Output lucicfg.Output // generated output files to validate 41 Meta lucicfg.Meta // validation options (settable through Starlark) 42 43 // LegacyConfigServiceClientFactory returns a HTTP client that is used to end 44 // request to LUCI Config service. 45 // 46 // This is usually just subcommand.LegacyConfigServiceClient. 47 LegacyConfigServiceClient LegacyConfigServiceClientFactory 48 49 // ConfigServiceConn returns a gRPC connection that can be used to send 50 // request to LUCI Config service. 51 // 52 // This is usually just subcommand.MakeConfigServiceConn. 53 ConfigServiceConn ConfigServiceConnFactory 54 } 55 56 // LegacyConfigServiceClientFactory returns a HTTP client that is used to end 57 // request to LUCI Config service. 58 type LegacyConfigServiceClientFactory func(ctx context.Context) (*http.Client, error) 59 60 // ConfigServiceConnFactory returns a gRPC connection that can be used to send 61 // request to LUCI Config service. 62 type ConfigServiceConnFactory func(ctx context.Context, host string) (*grpc.ClientConn, error) 63 64 // Validate validates both input source code and generated config files. 65 // 66 // It is a common part of subcommands that validate configs. 67 // 68 // Source code is checked using buildifier linters and formatters, if enabled. 69 // This is controlled by LintChecks meta args. 70 // 71 // Generated config files are split into 0 or more config sets and sent to 72 // the LUCI Config remote service for validation, if enabled. This is controlled 73 // by ConfigServiceHost meta arg. 74 // 75 // Dumps all validation errors to the stderr. In addition to detailed validation 76 // results, also returns a multi-error with all blocking errors. 77 func Validate(ctx context.Context, params ValidateParams, getRewriterForPath func(path string) (*build.Rewriter, error)) ([]*buildifier.Finding, []*lucicfg.ValidationResult, error) { 78 wg := sync.WaitGroup{} 79 wg.Add(2) 80 var localRes []*buildifier.Finding 81 var localErr error 82 83 go func() { 84 defer wg.Done() 85 localRes, localErr = buildifier.Lint( 86 params.Loader, 87 params.Source, 88 params.Meta.LintChecks, 89 getRewriterForPath, 90 ) 91 }() 92 93 var remoteRes []*lucicfg.ValidationResult 94 var remoteErr error 95 go func() { 96 defer wg.Done() 97 remoteRes, remoteErr = validateOutput(ctx, 98 params.Output, 99 params.LegacyConfigServiceClient, 100 params.ConfigServiceConn, 101 params.Meta.ConfigServiceHost, 102 params.Meta.FailOnWarnings, 103 ) 104 }() 105 106 wg.Wait() 107 108 first := true 109 for _, r := range localRes { 110 if text := r.Format(); text != "" { 111 if first { 112 fmt.Fprintf(os.Stderr, "--------------------------------------------\n") 113 fmt.Fprintf(os.Stderr, "Formatting and linting errors\n") 114 fmt.Fprintf(os.Stderr, "--------------------------------------------\n") 115 first = false 116 } 117 fmt.Fprintf(os.Stderr, "%s", text) 118 } 119 } 120 121 first = true 122 for _, r := range remoteRes { 123 if text := r.Format(); text != "" { 124 if first { 125 fmt.Fprintf(os.Stderr, "--------------------------------------------\n") 126 fmt.Fprintf(os.Stderr, "LUCI Config validation errors\n") 127 fmt.Fprintf(os.Stderr, "--------------------------------------------\n") 128 first = false 129 } 130 fmt.Fprintf(os.Stderr, "%s", text) 131 } 132 } 133 134 var merr errors.MultiError 135 merr = mergeMerr(merr, localErr) 136 merr = mergeMerr(merr, remoteErr) 137 if len(merr) != 0 { 138 return localRes, remoteRes, merr 139 } 140 return localRes, remoteRes, nil 141 } 142 143 // mergeMerr adds errs to merr returning new merr. 144 func mergeMerr(merr errors.MultiError, err error) errors.MultiError { 145 if err == nil { 146 return merr 147 } 148 if many, ok := err.(errors.MultiError); ok { 149 return append(merr, many...) 150 } 151 return append(merr, err) 152 } 153 154 // validateOutput splits the output into 0 or more config sets and sends them 155 // for validation to LUCI Config. 156 func validateOutput(ctx context.Context, output lucicfg.Output, 157 legacyClientFactory LegacyConfigServiceClientFactory, 158 clientConnFactory ConfigServiceConnFactory, 159 host string, failOnWarns bool) ([]*lucicfg.ValidationResult, error) { 160 configSets, err := output.ConfigSets() 161 if len(configSets) == 0 || err != nil { 162 return nil, err // nothing to validate or failed to serialize 163 } 164 165 // Log the warning only if there were some config sets we needed to validate. 166 if host == "" { 167 logging.Warningf(ctx, "Config service host is not set, skipping validation against LUCI Config service") 168 return nil, nil 169 } 170 171 var validator lucicfg.ConfigSetValidator 172 if strings.HasSuffix(host, "luci.app") { 173 conn, err := clientConnFactory(ctx, host) 174 if err != nil { 175 return nil, err 176 } 177 defer func() { 178 if err := conn.Close(); err != nil { 179 logging.Warningf(ctx, "failed to close the connection to config service: %s", err) 180 } 181 }() 182 validator = lucicfg.NewRemoteValidator(conn) 183 } else { 184 logging.Warningf(ctx, "The legacy LUCI Config service is deprecated. Please use the new LUCI Config service host \"config.luci.app\" by passing it through -config-service-host.") 185 configClient, err := legacyClientFactory(ctx) 186 if err != nil { 187 return nil, err 188 } 189 validator = lucicfg.LegacyRemoteValidator(configClient, host) 190 } 191 192 // Validate all config sets in parallel. 193 results := make([]*lucicfg.ValidationResult, len(configSets)) 194 wg := sync.WaitGroup{} 195 wg.Add(len(configSets)) 196 for i, cs := range configSets { 197 i, cs := i, cs 198 go func() { 199 results[i] = cs.Validate(ctx, validator) 200 wg.Done() 201 }() 202 } 203 wg.Wait() 204 205 // Assemble the final verdict. Note that OverallError mutates r.Failed. 206 var merr errors.MultiError 207 for _, r := range results { 208 if err := r.OverallError(failOnWarns); err != nil { 209 merr = append(merr, err) 210 } 211 } 212 213 if len(merr) != 0 { 214 return results, merr 215 } 216 return results, nil 217 }