go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/client/casclient/client.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 casclient provides remote-apis-sdks client with luci integration. 16 package casclient 17 18 import ( 19 "context" 20 "fmt" 21 "net" 22 "runtime" 23 "strings" 24 "time" 25 26 "github.com/bazelbuild/remote-apis-sdks/go/pkg/cas" 27 "github.com/bazelbuild/remote-apis-sdks/go/pkg/client" 28 "github.com/bazelbuild/remote-apis-sdks/go/pkg/contextmd" 29 "google.golang.org/grpc/credentials" 30 31 "go.chromium.org/luci/auth" 32 "go.chromium.org/luci/common/errors" 33 "go.chromium.org/luci/common/logging" 34 "go.chromium.org/luci/hardcoded/chromeinfra" 35 ) 36 37 // AddrProd is the PROD CAS service address. 38 const AddrProd = "remotebuildexecution.googleapis.com:443" 39 40 // New returns luci auth configured Client for RBE-CAS. 41 func New(ctx context.Context, addr string, instance string, opts auth.Options, readOnly bool) (*cas.Client, error) { 42 var dialParams client.DialParams 43 useLocal, err := isLocalAddr(addr) 44 if err != nil { 45 return nil, errors.Annotate(err, "invalid addr").Err() 46 } 47 if useLocal { 48 // Connect to local fake CAS server. 49 // See also go.chromium.org/luci/tools/cmd/fakecas 50 if instance != "" { 51 return nil, errors.Reason("do not specify instance with local address").Err() 52 } 53 instance = "instance" 54 dialParams = client.DialParams{ 55 Service: addr, 56 NoSecurity: true, 57 } 58 } else { 59 creds, err := perRPCCreds(ctx, instance, opts, readOnly) 60 if err != nil { 61 return nil, err 62 } 63 64 dialParams = client.DialParams{ 65 Service: addr, 66 UseExternalAuthToken: true, 67 ExternalPerRPCCreds: &client.PerRPCCreds{Creds: creds}, 68 } 69 } 70 71 conn, _, err := client.Dial(ctx, dialParams.Service, dialParams) 72 if err != nil { 73 return nil, errors.Annotate(err, "failed to dial RBE").Err() 74 } 75 76 cl, err := cas.NewClientWithConfig(ctx, conn, instance, DefaultConfig()) 77 if err != nil { 78 return nil, errors.Annotate(err, "failed to create client").Err() 79 } 80 return cl, nil 81 } 82 83 // DefaultConfig returns default CAS client configuration. 84 func DefaultConfig() cas.ClientConfig { 85 cfg := cas.DefaultClientConfig() 86 cfg.CompressedBytestreamThreshold = 0 // compress always 87 88 // Do not read file less than 10MiB twice. 89 cfg.SmallFileThreshold = 10 * 1024 * 1024 90 91 return cfg 92 } 93 94 func perRPCCreds(ctx context.Context, instance string, opts auth.Options, readOnly bool) (credentials.PerRPCCredentials, error) { 95 project := strings.Split(instance, "/")[1] 96 var role string 97 if readOnly { 98 role = "cas-read-only" 99 } else { 100 role = "cas-read-write" 101 } 102 103 // Construct auth.Options. 104 opts.ActAsServiceAccount = fmt.Sprintf("%s@%s.iam.gserviceaccount.com", role, project) 105 opts.ActViaLUCIRealm = fmt.Sprintf("@internal:%s/%s", project, role) 106 opts.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"} 107 108 if strings.HasSuffix(project, "-dev") || strings.HasSuffix(project, "-staging") { 109 // use dev token server for dev/staging projects. 110 opts.TokenServerHost = chromeinfra.TokenServerDevHost 111 } 112 113 creds, err := auth.NewAuthenticator(ctx, auth.SilentLogin, opts).PerRPCCredentials() 114 if err != nil { 115 return nil, errors.Annotate(err, "failed to get PerRPCCredentials").Err() 116 } 117 return creds, nil 118 } 119 120 // NewLegacy returns luci auth configured legacy Client for RBE. 121 // In general, NewClient is preferred. 122 // TODO(crbug.com/1225524): remove this. 123 func NewLegacy(ctx context.Context, addr string, instance string, opts auth.Options, readOnly bool) (*client.Client, error) { 124 useLocal, err := isLocalAddr(addr) 125 if err != nil { 126 return nil, errors.Annotate(err, "invalid addr").Err() 127 } 128 if useLocal { 129 // Connect to local fake CAS server. 130 // See also go.chromium.org/luci/tools/cmd/fakecas 131 if instance != "" { 132 logging.Warningf(ctx, "instance %q is given, but will be ignored.", instance) 133 } 134 dialParams := client.DialParams{ 135 Service: addr, 136 NoSecurity: true, 137 } 138 cl, err := client.NewClient(ctx, "instance", dialParams) 139 if err != nil { 140 return nil, errors.Annotate(err, "failed to create client").Err() 141 } 142 return cl, nil 143 } 144 145 creds, err := perRPCCreds(ctx, instance, opts, readOnly) 146 if err != nil { 147 return nil, err 148 } 149 dialParams := client.DialParams{ 150 Service: "remotebuildexecution.googleapis.com:443", 151 UseExternalAuthToken: true, 152 ExternalPerRPCCreds: &client.PerRPCCreds{Creds: creds}, 153 } 154 155 cl, err := client.NewClient(ctx, instance, dialParams, Options()...) 156 if err != nil { 157 logging.Errorf(ctx, "failed to create casclient: %+v", err) 158 return nil, errors.Annotate(err, "failed to create client").Err() 159 } 160 return cl, nil 161 } 162 163 // Options returns CAS client options. 164 func Options() []client.Opt { 165 casConcurrency := runtime.NumCPU() * 2 166 if runtime.GOOS == "windows" { 167 // This is for better file write performance on Windows (http://b/171672371#comment6). 168 casConcurrency = runtime.NumCPU() 169 } 170 171 rpcTimeouts := make(client.RPCTimeouts) 172 for k, v := range client.DefaultRPCTimeouts { 173 rpcTimeouts[k] = v 174 } 175 176 // Extend the timeout for write operations beyond the default, as writes can 177 // sometimes be quite slow. This timeout only applies to writing a single 178 // file chunk, so there isn't a risk of setting a timeout that's to low for 179 // large files. 180 rpcTimeouts["Write"] = 2 * time.Minute 181 182 // There's suspicion GetCapabilities sometimes takes longer than default 183 // 5 sec because it is the first call ever (and it needs to open the 184 // connection and refresh auth tokens). Give it more time. 185 rpcTimeouts["GetCapabilities"] = 30 * time.Second 186 187 return []client.Opt{ 188 client.CASConcurrency(casConcurrency), 189 client.UtilizeLocality(true), 190 &client.TreeSymlinkOpts{ 191 // Symlinks will be uploaded as-is... 192 Preserved: true, 193 // ... and the target file included in the CAS archive... 194 FollowsTarget: true, 195 // ... unless the target file is outside the root directory, in 196 // which case the target file will be uploaded instead of preserving 197 // the symlink. 198 MaterializeOutsideExecRoot: true, 199 }, 200 rpcTimeouts, 201 // Set restricted permission for written files. 202 client.DirMode(0700), 203 client.ExecutableMode(0700), 204 client.RegularMode(0600), 205 client.CompressedBytestreamThreshold(0), 206 } 207 } 208 209 // ContextWithMetadata attaches RBE related metadata with tool name to the 210 // given context. 211 func ContextWithMetadata(ctx context.Context, toolName string) (context.Context, error) { 212 ctx, err := contextmd.WithMetadata(ctx, &contextmd.Metadata{ 213 ToolName: toolName, 214 }) 215 if err != nil { 216 return nil, errors.Annotate(err, "failed to attach metadata").Err() 217 } 218 219 m, err := contextmd.ExtractMetadata(ctx) 220 if err != nil { 221 return nil, errors.Annotate(err, "failed to extract metadata").Err() 222 } 223 224 logging.Infof(ctx, "context metadata: %#+v", *m) 225 226 return ctx, nil 227 } 228 229 func isLocalAddr(addr string) (bool, error) { 230 tcpaddr, err := net.ResolveTCPAddr("tcp", addr) 231 if err != nil { 232 return false, err 233 } 234 if tcpaddr.IP == nil { 235 return true, nil 236 } 237 return tcpaddr.IP.IsLoopback(), nil 238 }