github.com/GoogleCloudPlatform/testgrid@v0.0.174/resultstore/client.go (about) 1 /* 2 Copyright 2019 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package resultstore 18 19 import ( 20 "context" 21 "crypto/x509" 22 "fmt" 23 "strings" 24 25 "github.com/google/uuid" 26 resultstore "google.golang.org/genproto/googleapis/devtools/resultstore/v2" 27 "google.golang.org/genproto/protobuf/field_mask" 28 "google.golang.org/grpc" 29 "google.golang.org/grpc/credentials" 30 "google.golang.org/grpc/credentials/oauth" 31 "google.golang.org/grpc/metadata" 32 ) 33 34 // Connect returns a secure gRPC connection. 35 // 36 // Authenticates as the service account if specified otherwise the default user. 37 func Connect(ctx context.Context, serviceAccountPath string) (*grpc.ClientConn, error) { 38 pool, err := x509.SystemCertPool() 39 if err != nil { 40 return nil, fmt.Errorf("system cert pool: %v", err) 41 } 42 creds := credentials.NewClientTLSFromCert(pool, "") 43 const scope = "https://www.googleapis.com/auth/cloud-platform" 44 var perRPC credentials.PerRPCCredentials 45 if serviceAccountPath != "" { 46 perRPC, err = oauth.NewServiceAccountFromFile(serviceAccountPath, scope) 47 } else { 48 perRPC, err = oauth.NewApplicationDefault(ctx, scope) 49 } 50 if err != nil { 51 return nil, fmt.Errorf("create oauth: %v", err) 52 } 53 conn, err := grpc.Dial( 54 "resultstore.googleapis.com:443", 55 grpc.WithTransportCredentials(creds), 56 grpc.WithPerRPCCredentials(perRPC), 57 ) 58 if err != nil { 59 return nil, fmt.Errorf("dial: %v", err) 60 } 61 62 return conn, nil 63 } 64 65 // Secret represents a secret authorization uuid to protect invocations. 66 type Secret string 67 68 // UUID represents a universally "unique" identifier. 69 func UUID() string { 70 return uuid.New().String() 71 } 72 73 // NewSecret returns a new, unique identifier. 74 func NewSecret() Secret { 75 return Secret(UUID()) 76 } 77 78 // Client provides ResultStore CRUD methods. 79 type Client struct { 80 up resultstore.ResultStoreUploadClient 81 down resultstore.ResultStoreDownloadClient 82 ctx context.Context 83 token string 84 } 85 86 // NewClient uses the specified gRPC connection to connect to ResultStore. 87 func NewClient(conn *grpc.ClientConn) *Client { 88 return &Client{ 89 up: resultstore.NewResultStoreUploadClient(conn), 90 down: resultstore.NewResultStoreDownloadClient(conn), 91 ctx: context.Background(), 92 } 93 } 94 95 // WithContext uses the specified context for all RPCs. 96 func (c *Client) WithContext(ctx context.Context) *Client { 97 c.ctx = ctx 98 return c 99 } 100 101 // WithSecret applies the specified secret to all requests. 102 func (c *Client) WithSecret(authorizationToken Secret) *Client { 103 c.token = string(authorizationToken) 104 return c 105 } 106 107 // Access resources 108 109 // Invocations provides Invocation CRUD methods. 110 func (c Client) Invocations() Invocations { 111 return Invocations{ 112 Client: c, 113 } 114 } 115 116 // Configurations provides CRUD methods for an invocation's configurations. 117 func (c Client) Configurations(invocationName string) Configurations { 118 return Configurations{ 119 Client: c, 120 inv: invocationName, 121 } 122 } 123 124 // Targets provides CRUD methods for an invocations's targets. 125 func (c Client) Targets(invocationName string) Targets { 126 return Targets{ 127 Client: c, 128 inv: invocationName, 129 } 130 } 131 132 // ConfiguredTargets provides CRUD methods for a target's configured targets. 133 func (c Client) ConfiguredTargets(targetName, configID string) ConfiguredTargets { 134 return ConfiguredTargets{ 135 Client: c, 136 target: targetName, 137 config: configID, 138 } 139 } 140 141 // Actions provides CRUD methods for a configured target. 142 func (c Client) Actions(configuredTargetName string) Actions { 143 return Actions{ 144 Client: c, 145 configuredTarget: configuredTargetName, 146 } 147 } 148 149 // Resources 150 151 // Invocations client. 152 type Invocations struct { 153 Client 154 } 155 156 // Targets client. 157 type Targets struct { 158 Client 159 inv string 160 } 161 162 // Configurations client. 163 type Configurations struct { 164 Client 165 inv string 166 } 167 168 // ConfiguredTargets client. 169 type ConfiguredTargets struct { 170 Client 171 target string 172 config string 173 } 174 175 // Actions client. 176 type Actions struct { 177 Client 178 configuredTarget string 179 } 180 181 // Mask methods 182 183 // fieldMask is required by gRPC for GET methods. 184 func fieldMask(ctx context.Context, fields ...string) context.Context { 185 return metadata.AppendToOutgoingContext(ctx, "X-Goog-FieldMask", strings.Join(fields, ",")) 186 } 187 188 // listMask adds the required next_page_token for list requests, as well as any other methods. 189 func listMask(ctx context.Context, fields ...string) context.Context { 190 return fieldMask(ctx, append(fields, "next_page_token")...) 191 } 192 193 // Target methods 194 195 // Create a new target with the specified id (target basename), returing the fully qualified path. 196 func (t Targets) Create(id string, target Target) (string, error) { 197 tgt, err := t.up.CreateTarget(t.ctx, &resultstore.CreateTargetRequest{ 198 Parent: t.inv, 199 TargetId: id, 200 Target: target.To(), 201 AuthorizationToken: t.token, 202 }) 203 if err != nil { 204 return "", err 205 } 206 return tgt.Name, nil 207 } 208 209 // List requested fields in targets, does not currently handle paging. 210 func (t Targets) List(fields ...string) ([]Target, error) { 211 resp, err := t.down.ListTargets(listMask(t.ctx, fields...), &resultstore.ListTargetsRequest{ 212 Parent: t.inv, 213 }) 214 if err != nil { 215 return nil, err 216 } 217 var targets []Target 218 for _, r := range resp.Targets { 219 targets = append(targets, fromTarget(r)) 220 } 221 return targets, nil 222 } 223 224 // Configuration methods 225 226 const ( 227 // Default is the expected single-configuration id. 228 Default = "default" 229 ) 230 231 // Create a new configuration using the specified basename, returning the fully qualified path. 232 func (c Configurations) Create(id string) (string, error) { 233 config, err := c.up.CreateConfiguration(c.ctx, &resultstore.CreateConfigurationRequest{ 234 Parent: c.inv, 235 ConfigId: id, 236 AuthorizationToken: c.token, 237 // Configuration is useless 238 }) 239 if err != nil { 240 return "", err 241 } 242 return config.Name, nil 243 } 244 245 // ConfiguredTarget methods 246 247 // Create a new configured target, returning the fully qualified path. 248 func (ct ConfiguredTargets) Create(act Action) (string, error) { 249 resp, err := ct.up.CreateConfiguredTarget(ct.ctx, &resultstore.CreateConfiguredTargetRequest{ 250 Parent: ct.target, 251 ConfigId: ct.config, 252 AuthorizationToken: ct.token, 253 ConfiguredTarget: &resultstore.ConfiguredTarget{ 254 StatusAttributes: status(act.Status, act.Description), 255 }, 256 }) 257 if err != nil { 258 return "", err 259 } 260 return resp.Name, nil 261 } 262 263 // Action methods 264 265 // Create a test action under the specified ID, returning the fully-qualified path. 266 // 267 // Technically there are also build actions, but these do not show up in the ResultStore UI. 268 func (a Actions) Create(id string, test Test) (string, error) { 269 resp, err := a.up.CreateAction(a.ctx, &resultstore.CreateActionRequest{ 270 Parent: a.configuredTarget, 271 ActionId: id, 272 AuthorizationToken: a.token, 273 Action: test.To(), 274 }) 275 if err != nil { 276 return "", err 277 } 278 return resp.Name, nil 279 } 280 281 // TestFields represent all fields this client cares about. 282 var TestFields = [...]string{ 283 "actions.name", 284 "actions.test_action", 285 "actions.description", 286 "actions.timing", 287 } 288 289 // List tests in this configured target. 290 func (a Actions) List(fields ...string) ([]Test, error) { 291 if len(fields) == 0 { 292 fields = TestFields[:] 293 } 294 resp, err := a.down.ListActions(listMask(a.ctx, fields...), &resultstore.ListActionsRequest{ 295 Parent: a.configuredTarget, 296 }) 297 if err != nil { 298 return nil, err 299 } 300 var ret []Test 301 for _, r := range resp.Actions { 302 ret = append(ret, fromTest(r)) 303 } 304 return ret, nil 305 } 306 307 // Invocation methods 308 309 // Create a new invocation (project must be specified). 310 func (i Invocations) Create(inv Invocation) (string, error) { 311 resp, err := i.up.CreateInvocation(i.ctx, &resultstore.CreateInvocationRequest{ 312 Invocation: inv.To(), 313 AuthorizationToken: i.token, 314 }) 315 if err != nil { 316 return "", err 317 } 318 return resp.Name, nil 319 } 320 321 // Update a pre-existing invocation at name. 322 func (i Invocations) Update(inv Invocation, fields ...string) error { 323 _, err := i.up.UpdateInvocation(i.ctx, &resultstore.UpdateInvocationRequest{ 324 Invocation: inv.To(), 325 UpdateMask: &field_mask.FieldMask{ 326 Paths: fields, 327 }, 328 AuthorizationToken: i.token, 329 }) 330 return err 331 } 332 333 // Finish an invocation, preventing further updates. 334 // TODO(fejta): consider renaming this to Finalize() 335 func (i Invocations) Finish(name string) error { 336 _, err := i.up.FinalizeInvocation(i.ctx, &resultstore.FinalizeInvocationRequest{ 337 Name: name, 338 AuthorizationToken: i.token, 339 }) 340 return err 341 } 342 343 // Get an existing invocation at name. 344 func (i Invocations) Get(name string, fields ...string) (*Invocation, error) { 345 inv, err := i.down.GetInvocation(fieldMask(i.ctx, fields...), &resultstore.GetInvocationRequest{Name: name}) 346 if err != nil { 347 return nil, err 348 } 349 resp := fromInvocation(inv) 350 return &resp, nil 351 } 352 353 func convertToInvocations(results *resultstore.SearchInvocationsResponse) []*Invocation { 354 invocations := []*Invocation{} 355 for _, invocation := range results.Invocations { 356 inv := fromInvocation(invocation) 357 invocations = append(invocations, &inv) 358 } 359 return invocations 360 } 361 362 // Search finds all the invocations that satisfies the query condition within a project. 363 func (i Invocations) Search(ctx context.Context, projectID string, query string, fields ...string) ([]*Invocation, error) { 364 results, err := i.down.SearchInvocations(fieldMask(ctx, fields...), &resultstore.SearchInvocationsRequest{ 365 ProjectId: projectID, 366 Query: query, 367 }) 368 if err != nil { 369 return nil, err 370 } 371 return convertToInvocations(results), nil 372 }