go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/get_builder.go (about)

     1  // Copyright 2020 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 rpc
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sort"
    21  
    22  	"google.golang.org/protobuf/encoding/protojson"
    23  	"google.golang.org/protobuf/types/known/durationpb"
    24  	"google.golang.org/protobuf/types/known/structpb"
    25  
    26  	"go.chromium.org/luci/common/errors"
    27  	"go.chromium.org/luci/common/proto/reflectutil"
    28  	"go.chromium.org/luci/gae/service/datastore"
    29  	"go.chromium.org/luci/grpc/appstatus"
    30  
    31  	"go.chromium.org/luci/buildbucket/appengine/internal/config"
    32  	"go.chromium.org/luci/buildbucket/appengine/internal/perm"
    33  	"go.chromium.org/luci/buildbucket/appengine/model"
    34  	"go.chromium.org/luci/buildbucket/bbperms"
    35  	pb "go.chromium.org/luci/buildbucket/proto"
    36  	"go.chromium.org/luci/buildbucket/protoutil"
    37  )
    38  
    39  // validateGetBuilder validates the given request.
    40  func validateGetBuilder(req *pb.GetBuilderRequest) error {
    41  	if err := protoutil.ValidateBuilderID(req.Id); err != nil {
    42  		return errors.Annotate(err, "id").Err()
    43  	}
    44  
    45  	return nil
    46  }
    47  
    48  func stringsToRequestedDimensions(strDims []string) (map[string][]*pb.RequestedDimension, []string) {
    49  	// key -> slice of dimensions (key, value, expiration) with matching keys.
    50  	dims := make(map[string][]*pb.RequestedDimension)
    51  	var empty []string
    52  
    53  	for _, d := range strDims {
    54  		exp, k, v := config.ParseDimension(d)
    55  		if v == "" {
    56  			empty = append(empty, k)
    57  			continue
    58  		}
    59  		dim := &pb.RequestedDimension{
    60  			Key:   k,
    61  			Value: v,
    62  		}
    63  		if exp > 0 {
    64  			dim.Expiration = &durationpb.Duration{
    65  				Seconds: exp,
    66  			}
    67  		}
    68  		dims[k] = append(dims[k], dim)
    69  	}
    70  	return dims, empty
    71  }
    72  
    73  // applyShadowAdjustment makes a copy of the builder config then applies shadow
    74  // builder adjustments to it.
    75  func applyShadowAdjustment(cfg *pb.BuilderConfig) *pb.BuilderConfig {
    76  	rtnCfg := reflectutil.ShallowCopy(cfg).(*pb.BuilderConfig)
    77  	shadowBldrCfg := cfg.GetShadowBuilderAdjustments()
    78  	if shadowBldrCfg == nil {
    79  		return rtnCfg
    80  	}
    81  	if shadowBldrCfg.ServiceAccount != "" {
    82  		rtnCfg.ServiceAccount = shadowBldrCfg.ServiceAccount
    83  	}
    84  
    85  	if len(shadowBldrCfg.Dimensions) > 0 {
    86  		dims, _ := stringsToRequestedDimensions(rtnCfg.Dimensions)
    87  		shadowDims, empty := stringsToRequestedDimensions(shadowBldrCfg.Dimensions)
    88  
    89  		for k, d := range shadowDims {
    90  			dims[k] = d
    91  		}
    92  		for _, key := range empty {
    93  			delete(dims, key)
    94  		}
    95  		var updatedDims []string
    96  		for _, dims := range dims {
    97  			for _, dim := range dims {
    98  				dimStr := fmt.Sprintf("%s:%s", dim.Key, dim.Value)
    99  				if dim.Expiration != nil {
   100  					dimStr = fmt.Sprintf("%d:%s", dim.Expiration.Seconds, dimStr)
   101  				}
   102  				updatedDims = append(updatedDims, dimStr)
   103  			}
   104  		}
   105  		sort.Strings(updatedDims)
   106  		rtnCfg.Dimensions = updatedDims
   107  	}
   108  
   109  	if shadowBldrCfg.Properties != "" {
   110  		if rtnCfg.GetProperties() == "" {
   111  			rtnCfg.Properties = shadowBldrCfg.Properties
   112  		} else {
   113  			origProp := &structpb.Struct{}
   114  			shadowProp := &structpb.Struct{}
   115  			if err := protojson.Unmarshal([]byte(rtnCfg.Properties), origProp); err != nil {
   116  				// Builder config should have been validated already.
   117  				panic(errors.Annotate(err, "error unmarshaling builder properties for %q", rtnCfg.Name).Err())
   118  			}
   119  			if err := protojson.Unmarshal([]byte(shadowBldrCfg.Properties), shadowProp); err != nil {
   120  				// Builder config should have been validated already.
   121  				panic(errors.Annotate(err, "error unmarshaling builder shadow properties for %q", rtnCfg.Name).Err())
   122  			}
   123  			for k, v := range shadowProp.GetFields() {
   124  				origProp.Fields[k] = v
   125  			}
   126  			updatedProp, err := protojson.Marshal(origProp)
   127  			if err != nil {
   128  				panic(errors.Annotate(err, "error marshaling builder properties for %q", rtnCfg.Name).Err())
   129  			}
   130  			rtnCfg.Properties = string(updatedProp)
   131  		}
   132  	}
   133  	rtnCfg.ShadowBuilderAdjustments = nil
   134  	return rtnCfg
   135  }
   136  
   137  func trySynthesizeFromShadowedBuilder(ctx context.Context, req *pb.GetBuilderRequest) (*pb.BuilderItem, error) {
   138  	reqBucket := &model.Bucket{
   139  		Parent: model.ProjectKey(ctx, req.Id.Project),
   140  		ID:     req.Id.Bucket,
   141  	}
   142  	switch err := datastore.Get(ctx, reqBucket); {
   143  	case err == datastore.ErrNoSuchEntity:
   144  		return nil, perm.NotFoundErr(ctx)
   145  	case err != nil:
   146  		return nil, err
   147  	}
   148  	if len(reqBucket.Shadows) == 0 {
   149  		// This bucket doesn't shadow any other buckets.
   150  		return nil, perm.NotFoundErr(ctx)
   151  	}
   152  	var builders []*model.Builder
   153  	for _, shadowedBkt := range reqBucket.Shadows {
   154  		builders = append(builders, &model.Builder{
   155  			Parent: model.BucketKey(ctx, req.Id.Project, shadowedBkt),
   156  			ID:     req.Id.Builder,
   157  		})
   158  	}
   159  	if err := model.GetIgnoreMissing(ctx, builders); err != nil {
   160  		return nil, errors.Annotate(err, "failed to fetch entities").Err()
   161  	}
   162  	for _, bldr := range builders {
   163  		if bldr.Config != nil {
   164  			cfgCopy := applyShadowAdjustment(bldr.Config)
   165  			return &pb.BuilderItem{
   166  				Id:     req.Id,
   167  				Config: cfgCopy,
   168  			}, nil
   169  		}
   170  	}
   171  	return nil, perm.NotFoundErr(ctx)
   172  }
   173  
   174  // GetBuilder handles a request to retrieve a builder. Implements pb.BuildersServer.
   175  func (*Builders) GetBuilder(ctx context.Context, req *pb.GetBuilderRequest) (*pb.BuilderItem, error) {
   176  	if err := validateGetBuilder(req); err != nil {
   177  		return nil, appstatus.BadRequest(err)
   178  	}
   179  
   180  	if err := perm.HasInBuilder(ctx, bbperms.BuildersGet, req.Id); err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	builder := &model.Builder{
   185  		Parent: model.BucketKey(ctx, req.Id.Project, req.Id.Bucket),
   186  		ID:     req.Id.Builder,
   187  	}
   188  	switch err := datastore.Get(ctx, builder); {
   189  	case err == datastore.ErrNoSuchEntity:
   190  		return trySynthesizeFromShadowedBuilder(ctx, req)
   191  	case err != nil:
   192  		return nil, err
   193  	}
   194  
   195  	if req.Mask == nil {
   196  		req.Mask = &pb.BuilderMask{Type: pb.BuilderMask_CONFIG_ONLY}
   197  	}
   198  
   199  	response := &pb.BuilderItem{Id: req.Id}
   200  
   201  	switch req.Mask.Type {
   202  	case pb.BuilderMask_ALL:
   203  		response.Config = builder.Config
   204  		response.Metadata = builder.Metadata
   205  	case pb.BuilderMask_CONFIG_ONLY:
   206  		response.Config = builder.Config
   207  	case pb.BuilderMask_METADATA_ONLY:
   208  		response.Metadata = builder.Metadata
   209  	}
   210  
   211  	return response, nil
   212  }