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  }