go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/rpcs/base.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 rpcs implements public API RPC handlers.
    16  package rpcs
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"strings"
    22  
    23  	"google.golang.org/grpc"
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/grpc/status"
    26  
    27  	"go.chromium.org/luci/common/data/stringset"
    28  	"go.chromium.org/luci/common/errors"
    29  	"go.chromium.org/luci/common/logging"
    30  	"go.chromium.org/luci/gae/service/datastore"
    31  	"go.chromium.org/luci/grpc/grpcutil"
    32  
    33  	apipb "go.chromium.org/luci/swarming/proto/api_v2"
    34  	"go.chromium.org/luci/swarming/server/acls"
    35  	"go.chromium.org/luci/swarming/server/cfg"
    36  	"go.chromium.org/luci/swarming/server/model"
    37  )
    38  
    39  var requestStateCtxKey = "swarming.rpcs.RequestState"
    40  
    41  const (
    42  	// Default size of a single page of results for listing queries.
    43  	defaultPageSize = 100
    44  	// Maximum allowed size of a single page of results for listing queries.
    45  	maxPageSize = 1000
    46  )
    47  
    48  // SwarmingServer implements Swarming gRPC service.
    49  //
    50  // It is a collection of various RPCs that didn't fit other services. Individual
    51  // RPCs are implemented in swarming_*.go files.
    52  type SwarmingServer struct {
    53  	apipb.UnimplementedSwarmingServer
    54  }
    55  
    56  // BotsServer implements Bots gRPC service.
    57  //
    58  // It exposes methods to view and manipulate state of Swarming bots. Individual
    59  // RPCs are implemented in bots_*.go files.
    60  type BotsServer struct {
    61  	apipb.UnimplementedBotsServer
    62  
    63  	// BotQuerySplitMode controls how "finely" to split BotInfo queries.
    64  	BotQuerySplitMode model.SplitMode
    65  }
    66  
    67  // TasksServer implements Tasks gRPC service.
    68  //
    69  // It exposes methods to view and manipulate state of Swarming tasks. Individual
    70  // RPCs are implemented in tasks_*.go files.
    71  type TasksServer struct {
    72  	apipb.UnimplementedTasksServer
    73  
    74  	// TaskQuerySplitMode controls how "finely" to split TaskResultSummary queries.
    75  	TaskQuerySplitMode model.SplitMode
    76  }
    77  
    78  // RequestState carries stated scoped to a single RPC handler.
    79  //
    80  // In production produced by ServerInterceptor. In tests can be injected into
    81  // the context via MockRequestState(...).
    82  //
    83  // Use State(ctx) to get the current value.
    84  type RequestState struct {
    85  	// Config is a snapshot of the server configuration when request started.
    86  	Config *cfg.Config
    87  	// ACL can be used to check ACLs.
    88  	ACL *acls.Checker
    89  }
    90  
    91  // ServerInterceptor returns an interceptor that initializes per-RPC context.
    92  //
    93  // The interceptor is active only for selected gRPC services. All other RPCs
    94  // are passed through unaffected.
    95  //
    96  // The initialized context will have RequestState populated, use State(ctx) to
    97  // get it.
    98  func ServerInterceptor(cfg *cfg.Provider, services []*grpc.ServiceDesc) grpcutil.UnifiedServerInterceptor {
    99  	serviceSet := stringset.New(len(services))
   100  	for _, svc := range services {
   101  		serviceSet.Add(svc.ServiceName)
   102  	}
   103  
   104  	return func(ctx context.Context, fullMethod string, handler func(ctx context.Context) error) error {
   105  		// fullMethod looks like "/<service>/<method>". Get "<service>".
   106  		if fullMethod == "" || fullMethod[0] != '/' {
   107  			panic(fmt.Sprintf("unexpected fullMethod %q", fullMethod))
   108  		}
   109  		service := fullMethod[1:strings.LastIndex(fullMethod, "/")]
   110  		if service == "" {
   111  			panic(fmt.Sprintf("unexpected fullMethod %q", fullMethod))
   112  		}
   113  
   114  		if !serviceSet.Has(service) {
   115  			return handler(ctx)
   116  		}
   117  
   118  		cfg := cfg.Config(ctx)
   119  		return handler(context.WithValue(ctx, &requestStateCtxKey, &RequestState{
   120  			Config: cfg,
   121  			ACL:    acls.NewChecker(ctx, cfg),
   122  		}))
   123  	}
   124  }
   125  
   126  // State accesses the per-request state in the context or panics if it is
   127  // not there.
   128  func State(ctx context.Context) *RequestState {
   129  	state, _ := ctx.Value(&requestStateCtxKey).(*RequestState)
   130  	if state == nil {
   131  		panic("no RequestState in the context")
   132  	}
   133  	return state
   134  }
   135  
   136  // ValidateLimit validates a page size limit in listing queries.
   137  func ValidateLimit(val int32) (int32, error) {
   138  	if val == 0 {
   139  		val = defaultPageSize
   140  	}
   141  	switch {
   142  	case val < 0:
   143  		return val, errors.Reason("must be positive, got %d", val).Err()
   144  	case val > maxPageSize:
   145  		return val, errors.Reason("must be less or equal to %d, got %d", maxPageSize, val).Err()
   146  	}
   147  	return val, nil
   148  }
   149  
   150  // FetchTaskRequest fetches a task request given its ID.
   151  //
   152  // Returns gRPC status errors, logs internal errors. Does not check ACLs yet.
   153  // It is the caller's responsibility.
   154  func FetchTaskRequest(ctx context.Context, taskID string) (*model.TaskRequest, error) {
   155  	if taskID == "" {
   156  		return nil, status.Errorf(codes.InvalidArgument, "task_id is required")
   157  	}
   158  	key, err := model.TaskIDToRequestKey(ctx, taskID)
   159  	if err != nil {
   160  		return nil, status.Errorf(codes.InvalidArgument, "task_id %s: %s", taskID, err)
   161  	}
   162  	req := &model.TaskRequest{Key: key}
   163  	switch err = datastore.Get(ctx, req); {
   164  	case errors.Is(err, datastore.ErrNoSuchEntity):
   165  		return nil, status.Errorf(codes.NotFound, "no such task")
   166  	case err != nil:
   167  		logging.Errorf(ctx, "Error fetching TaskRequest %s: %s", taskID, err)
   168  		return nil, status.Errorf(codes.Internal, "datastore error fetching the task")
   169  	default:
   170  		return req, nil
   171  	}
   172  }