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  }