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 }