go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/rpc/list_config_sets.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 rpc
    16  
    17  import (
    18  	"context"
    19  
    20  	"google.golang.org/grpc/codes"
    21  	"google.golang.org/grpc/status"
    22  
    23  	"go.chromium.org/luci/common/logging"
    24  	"go.chromium.org/luci/common/proto/mask"
    25  	"go.chromium.org/luci/config"
    26  	"go.chromium.org/luci/gae/service/datastore"
    27  	"go.chromium.org/luci/server/auth"
    28  
    29  	"go.chromium.org/luci/config_service/internal/acl"
    30  	"go.chromium.org/luci/config_service/internal/model"
    31  	pb "go.chromium.org/luci/config_service/proto"
    32  )
    33  
    34  // ListConfigSets returns a list of config sets. Implements pb.ConfigsServer.
    35  func (c Configs) ListConfigSets(ctx context.Context, req *pb.ListConfigSetsRequest) (*pb.ListConfigSetsResponse, error) {
    36  	// Validate fields mask
    37  	m, err := toConfigSetMask(req.GetFields())
    38  	switch {
    39  	case err != nil:
    40  		return nil, status.Errorf(codes.InvalidArgument, "invalid fields mask: %s", err)
    41  	case m.MustIncludes("file_paths") != mask.Exclude:
    42  		return nil, status.Errorf(codes.InvalidArgument, `
    43  			'file_paths' is not supported in fields mask. Must specify a config set in
    44  			order to include file paths (via GetConfigSet rpc).`)
    45  	case m.MustIncludes("configs") != mask.Exclude:
    46  		return nil, status.Errorf(codes.InvalidArgument, `
    47  			'configs' is not supported in fields mask. Must specify a config set in
    48  			order to include file paths (via GetConfigSet rpc).`)
    49  	}
    50  
    51  	// Fetch config sets
    52  	query := datastore.NewQuery(model.ConfigSetKind)
    53  	switch req.GetDomain() {
    54  	case pb.ListConfigSetsRequest_SERVICE:
    55  		// Range query for config sets which start with "services/".
    56  		// Use Lt("__key__", "serivces0") because '0' is the next char of "/".
    57  		query = query.Gt("__key__", datastore.MakeKey(ctx, model.ConfigSetKind, string(config.ServiceDomain)+"/")).
    58  			Lt("__key__", datastore.MakeKey(ctx, model.ConfigSetKind, string(config.ServiceDomain)+"0"))
    59  	case pb.ListConfigSetsRequest_PROJECT:
    60  		query = query.Gt("__key__", datastore.MakeKey(ctx, model.ConfigSetKind, string(config.ProjectDomain)+"/")).
    61  			Lt("__key__", datastore.MakeKey(ctx, model.ConfigSetKind, string(config.ProjectDomain)+"0"))
    62  	}
    63  	var cfgSets []*model.ConfigSet
    64  	err = datastore.Run(ctx, query, func(cs *model.ConfigSet) error {
    65  		switch hasPerm, err := acl.CanReadConfigSet(ctx, cs.ID); {
    66  		case err != nil:
    67  			logging.Errorf(ctx, "cannot check %q read access for %q: %s", cs.ID, auth.CurrentIdentity(ctx), err)
    68  			return err
    69  		case hasPerm:
    70  			cfgSets = append(cfgSets, cs)
    71  		}
    72  		return nil
    73  	})
    74  	if err != nil {
    75  		logging.Errorf(ctx, "error when querying %s config sets: %s", req.Domain.String(), err)
    76  		return nil, status.Errorf(codes.Internal, "error while fetching config sets")
    77  	}
    78  
    79  	cfgSetsPb := make([]*pb.ConfigSet, len(cfgSets))
    80  	for i, cs := range cfgSets {
    81  		cfgSetsPb[i] = toConfigSetPb(cs)
    82  	}
    83  
    84  	// Fetch last_import_attempt if needed.
    85  	// TODO(crbug.com/1465995): Might be inconsistent between ConfigSet and ImportAttempt
    86  	// if the ConfigSet gets updated during the two fetches. But it’s rare and costs an
    87  	// extra call every time. So not putting it in a transaction for now.
    88  	if m.MustIncludes("last_import_attempt") != mask.Exclude {
    89  		attempts := make([]*model.ImportAttempt, len(cfgSets))
    90  		for i, cs := range cfgSets {
    91  			attempts[i] = &model.ImportAttempt{
    92  				ConfigSet: datastore.KeyForObj(ctx, cs),
    93  			}
    94  		}
    95  		if err := datastore.Get(ctx, attempts); err != nil {
    96  			logging.Errorf(ctx, "failed to fetch last import attempts: %s", err)
    97  			return nil, status.Errorf(codes.Internal, "error while fetching last import attempts")
    98  		}
    99  		for i, attempt := range attempts {
   100  			cfgSetsPb[i].LastImportAttempt = toImportAttempt(attempt)
   101  		}
   102  	}
   103  
   104  	// Trim ConfigSet proto.
   105  	for _, cfgSetPb := range cfgSetsPb {
   106  		if err := m.Trim(cfgSetPb); err != nil {
   107  			logging.Errorf(ctx, "cannot trim ConfigSet (%q) proto: %s", cfgSetPb.Name, err)
   108  			return nil, status.Errorf(codes.Internal, "error while constructing the response")
   109  		}
   110  	}
   111  	return &pb.ListConfigSetsResponse{ConfigSets: cfgSetsPb}, nil
   112  }