blob: 4eae96e565a18435a424af07c30f2e7997a91ed0 [file] [log] [blame]
// Copyright 2019 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package base
import (
"context"
"fmt"
"os"
"sync"
config "go.chromium.org/luci/common/api/luci_config/config/v1"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/starlark/interpreter"
"go.chromium.org/luci/lucicfg"
"go.chromium.org/luci/lucicfg/buildifier"
)
// ValidateParams contains parameters for Validate call.
type ValidateParams struct {
Loader interpreter.Loader // represents the main package
Source []string // paths to lint, relative to the main package
Output lucicfg.Output // generated output files to validate
Meta lucicfg.Meta // validation options (settable through Starlark)
// ConfigService returns a LUCI Config RPC client that sends requests
// to the given host.
//
// This is usually just subcommand.ConfigService.
ConfigService ConfigServiceFactory
}
// ConfigServiceFactory returns a LUCI Config RPC client that sends requests
// to the given host.
type ConfigServiceFactory func(ctx context.Context, host string) (*config.Service, error)
// Validate validates both input source code and generated config files.
//
// It is a common part of subcommands that validate configs.
//
// Source code is checked using buildifier linters and formatters, if enabled.
// This is controlled by LintChecks meta args.
//
// Generated config files are split into 0 or more config sets and sent to
// the LUCI Config remote service for validation, if enabled. This is controlled
// by ConfigServiceHost meta arg.
//
// Dumps all validation errors to the stderr. In addition to detailed validation
// results, also returns a multi-error with all blocking errors.
func Validate(ctx context.Context, params ValidateParams) ([]*buildifier.Finding, []*lucicfg.ValidationResult, error) {
wg := sync.WaitGroup{}
wg.Add(2)
var localRes []*buildifier.Finding
var localErr error
go func() {
defer wg.Done()
localRes, localErr = buildifier.Lint(
params.Loader,
params.Source,
params.Meta.LintChecks,
)
}()
var remoteRes []*lucicfg.ValidationResult
var remoteErr error
go func() {
defer wg.Done()
remoteRes, remoteErr = validateOutput(ctx,
params.Output,
params.ConfigService,
params.Meta.ConfigServiceHost,
params.Meta.FailOnWarnings,
)
}()
wg.Wait()
first := true
for _, r := range localRes {
if text := r.Format(); text != "" {
if first {
fmt.Fprintf(os.Stderr, "--------------------------------------------\n")
fmt.Fprintf(os.Stderr, "Formatting and linting errors\n")
fmt.Fprintf(os.Stderr, "--------------------------------------------\n")
first = false
}
fmt.Fprintf(os.Stderr, "%s", text)
}
}
first = true
for _, r := range remoteRes {
if text := r.Format(); text != "" {
if first {
fmt.Fprintf(os.Stderr, "--------------------------------------------\n")
fmt.Fprintf(os.Stderr, "LUCI Config validation errors\n")
fmt.Fprintf(os.Stderr, "--------------------------------------------\n")
first = false
}
fmt.Fprintf(os.Stderr, "%s", text)
}
}
var merr errors.MultiError
merr = mergeMerr(merr, localErr)
merr = mergeMerr(merr, remoteErr)
if len(merr) != 0 {
return localRes, remoteRes, merr
}
return localRes, remoteRes, nil
}
// mergeMerr adds errs to merr returning new merr.
func mergeMerr(merr errors.MultiError, err error) errors.MultiError {
if err == nil {
return merr
}
if many, ok := err.(errors.MultiError); ok {
return append(merr, many...)
}
return append(merr, err)
}
// validateOutput splits the output into 0 or more config sets and sends them
// for validation to LUCI Config.
func validateOutput(ctx context.Context, output lucicfg.Output, svc ConfigServiceFactory, host string, failOnWarns bool) ([]*lucicfg.ValidationResult, error) {
configSets, err := output.ConfigSets()
if len(configSets) == 0 || err != nil {
return nil, err // nothing to validate or failed to serialize
}
// Log the warning only if there were some config sets we needed to validate.
if host == "" {
logging.Warningf(ctx, "Config service host is not set, skipping validation against LUCI Config service")
return nil, nil
}
srv, err := svc(ctx, host)
if err != nil {
return nil, err
}
validator := lucicfg.RemoteValidator(srv)
// Validate all config sets in parallel.
results := make([]*lucicfg.ValidationResult, len(configSets))
wg := sync.WaitGroup{}
wg.Add(len(configSets))
for i, cs := range configSets {
i, cs := i, cs
go func() {
results[i] = cs.Validate(ctx, validator)
wg.Done()
}()
}
wg.Wait()
// Assemble the final verdict. Note that OverallError mutates r.Failed.
var merr errors.MultiError
for _, r := range results {
if err := r.OverallError(failOnWarns); err != nil {
merr = append(merr, err)
}
}
if len(merr) != 0 {
return results, merr
}
return results, nil
}