go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/internal/validation/examine.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  	"net/http"
    22  	"sort"
    23  	"strings"
    24  	"sync"
    25  
    26  	"cloud.google.com/go/storage"
    27  	"golang.org/x/sync/errgroup"
    28  
    29  	"go.chromium.org/luci/common/gcloud/gs"
    30  	"go.chromium.org/luci/common/logging"
    31  	"go.chromium.org/luci/config"
    32  
    33  	"go.chromium.org/luci/config_service/internal/common"
    34  )
    35  
    36  // ExamineResult is the result of `Examine` method.
    37  type ExamineResult struct {
    38  	// MissingFiles are files that don't have the corresponding GCS objects.
    39  	//
    40  	// Use the attached signed URL to instruct client to upload the config
    41  	// content.
    42  	MissingFiles []struct {
    43  		File      File
    44  		SignedURL string
    45  	}
    46  	// UnvalidatableFiles are files that no service can validate.
    47  	//
    48  	// Those files SHOULD not be included in the validation request.
    49  	UnvalidatableFiles []File
    50  }
    51  
    52  // Passed return True if the config files passed the examination and can
    53  // proceed to `Validate`.
    54  func (er *ExamineResult) Passed() bool {
    55  	return er == nil || (len(er.MissingFiles) == 0 && len(er.UnvalidatableFiles) == 0)
    56  }
    57  
    58  var signedPutHeaders = map[string]string{
    59  	"Content-Encoding":            "gzip",
    60  	"x-goog-content-length-range": fmt.Sprintf("0,%d", common.ConfigMaxSize),
    61  }
    62  
    63  // Examine examines the configs files to ensure successful validation.
    64  func (v *Validator) Examine(ctx context.Context, cs config.Set, files []File) (*ExamineResult, error) {
    65  	eg, ectx := errgroup.WithContext(ctx)
    66  	eg.SetLimit(8)
    67  	ret := &ExamineResult{}
    68  	var mu sync.Mutex
    69  	for _, file := range files {
    70  		file := file
    71  		eg.Go(func() error {
    72  			services := v.Finder.FindInterestedServices(ectx, cs, file.GetPath())
    73  			if len(services) == 0 {
    74  				mu.Lock()
    75  				ret.UnvalidatableFiles = append(ret.UnvalidatableFiles, file)
    76  				mu.Unlock()
    77  				return nil
    78  			}
    79  
    80  			bucket, object := file.GetGSPath().Split()
    81  			switch err := v.GsClient.Touch(ectx, bucket, object); {
    82  			case errors.Is(err, context.Canceled):
    83  				logging.Warningf(ctx, "touching config file %q is cancelled", file.GetPath())
    84  				return err
    85  			case errors.Is(err, storage.ErrObjectNotExist):
    86  				urls, err := common.CreateSignedURLs(ectx, v.GsClient, []gs.Path{file.GetGSPath()}, http.MethodPut, signedPutHeaders)
    87  				switch {
    88  				case errors.Is(err, context.Canceled):
    89  					logging.Warningf(ctx, "creating signed url for GS path %q is cancelled", file.GetGSPath())
    90  					return err
    91  				case err != nil:
    92  					logging.Errorf(ctx, "failed to create signed url for GS path %q: %s", file.GetGSPath(), err)
    93  					return err
    94  				}
    95  				mu.Lock()
    96  				ret.MissingFiles = append(ret.MissingFiles, struct {
    97  					File      File
    98  					SignedURL string
    99  				}{File: file, SignedURL: urls[0]})
   100  				mu.Unlock()
   101  				return nil
   102  			case err != nil:
   103  				logging.Errorf(ctx, "failed to touch config file %q: %s", file.GetPath(), err)
   104  				return err
   105  			default:
   106  				return nil // file ready for validation.
   107  			}
   108  		})
   109  	}
   110  	if err := eg.Wait(); err != nil {
   111  		return nil, err
   112  	}
   113  	sort.SliceStable(ret.MissingFiles, func(i, j int) bool {
   114  		return strings.Compare(ret.MissingFiles[i].File.GetPath(), ret.MissingFiles[j].File.GetPath()) < 0
   115  	})
   116  	sort.SliceStable(ret.UnvalidatableFiles, func(i, j int) bool {
   117  		return strings.Compare(ret.UnvalidatableFiles[i].GetPath(), ret.UnvalidatableFiles[j].GetPath()) < 0
   118  	})
   119  	return ret, nil
   120  }