github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/updater/resultstore/client.go (about) 1 /* 2 Copyright 2023 The TestGrid 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 fetches and process results from ResultStore. 18 package resultstore 19 20 import ( 21 "context" 22 "crypto/x509" 23 "fmt" 24 "strings" 25 26 "github.com/sirupsen/logrus" 27 "google.golang.org/genproto/googleapis/devtools/resultstore/v2" 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 type resultStoreClient interface { 35 SearchInvocations(context.Context, *resultstore.SearchInvocationsRequest, ...grpc.CallOption) (*resultstore.SearchInvocationsResponse, error) 36 SearchConfiguredTargets(context.Context, *resultstore.SearchConfiguredTargetsRequest, ...grpc.CallOption) (*resultstore.SearchConfiguredTargetsResponse, error) 37 ExportInvocation(context.Context, *resultstore.ExportInvocationRequest, ...grpc.CallOption) (*resultstore.ExportInvocationResponse, error) 38 } 39 40 // DownloadClient provides a client to download ResultStore results from. 41 type DownloadClient struct { 42 client resultStoreClient 43 token string 44 } 45 46 // NewClient uses the specified gRPC connection to connect to ResultStore. 47 func NewClient(conn *grpc.ClientConn) *DownloadClient { 48 return &DownloadClient{ 49 client: resultstore.NewResultStoreDownloadClient(conn), 50 } 51 } 52 53 // Connect returns a secure gRPC connection. 54 // 55 // Authenticates as the service account if specified otherwise the default user. 56 func Connect(ctx context.Context, serviceAccountPath string) (*grpc.ClientConn, error) { 57 pool, err := x509.SystemCertPool() 58 if err != nil { 59 return nil, fmt.Errorf("system cert pool: %v", err) 60 } 61 creds := credentials.NewClientTLSFromCert(pool, "") 62 const scope = "https://www.googleapis.com/auth/cloud-platform" 63 var perRPC credentials.PerRPCCredentials 64 if serviceAccountPath != "" { 65 perRPC, err = oauth.NewServiceAccountFromFile(serviceAccountPath, scope) 66 } else { 67 perRPC, err = oauth.NewApplicationDefault(ctx, scope) 68 } 69 if err != nil { 70 return nil, fmt.Errorf("create oauth: %v", err) 71 } 72 conn, err := grpc.Dial( 73 "resultstore.googleapis.com:443", 74 grpc.WithTransportCredentials(creds), 75 grpc.WithPerRPCCredentials(perRPC), 76 ) 77 if err != nil { 78 return nil, fmt.Errorf("dial: %v", err) 79 } 80 81 return conn, nil 82 } 83 84 // Search finds all the invocations that satisfies the query condition within a project. 85 func (c *DownloadClient) Search(ctx context.Context, log logrus.FieldLogger, query, projectID string) ([]string, error) { 86 var invIDs []string 87 nextPageToken := "" 88 searchTargets := strings.Contains(query, "id.target_id=") 89 for { 90 var ids []string 91 var err error 92 if searchTargets { 93 ids, nextPageToken, err = c.targetSearch(ctx, log, query, projectID, nextPageToken) 94 } else { 95 ids, nextPageToken, err = c.invocationSearch(ctx, log, query, projectID, nextPageToken) 96 } 97 if err != nil { 98 return nil, err 99 } 100 invIDs = append(invIDs, ids...) 101 if nextPageToken == "" { 102 break 103 } 104 } 105 return invIDs, nil 106 } 107 108 func (c *DownloadClient) invocationSearch(ctx context.Context, log logrus.FieldLogger, query, projectID, nextPageToken string) ([]string, string, error) { 109 fieldMaskCtx := fieldMask( 110 ctx, 111 "next_page_token", 112 "invocations.id", 113 ) 114 req := &resultstore.SearchInvocationsRequest{ 115 Query: query, 116 ProjectId: projectID, 117 PageStart: &resultstore.SearchInvocationsRequest_PageToken{ 118 PageToken: nextPageToken, 119 }, 120 } 121 resp, err := c.client.SearchInvocations(fieldMaskCtx, req) 122 if err != nil { 123 return nil, "", err 124 } 125 var ids []string 126 for _, inv := range resp.GetInvocations() { 127 ids = append(ids, inv.GetId().GetInvocationId()) 128 } 129 return ids, resp.GetNextPageToken(), err 130 } 131 132 func (c *DownloadClient) targetSearch(ctx context.Context, log logrus.FieldLogger, query, projectID, nextPageToken string) ([]string, string, error) { 133 fieldMaskCtx := fieldMask( 134 ctx, 135 "next_page_token", 136 "configured_targets.id", 137 ) 138 req := &resultstore.SearchConfiguredTargetsRequest{ 139 Query: query, 140 ProjectId: projectID, 141 Parent: "invocations/-/targets/-", 142 PageStart: &resultstore.SearchConfiguredTargetsRequest_PageToken{ 143 PageToken: nextPageToken, 144 }, 145 } 146 resp, err := c.client.SearchConfiguredTargets(fieldMaskCtx, req) 147 if err != nil { 148 return nil, "", err 149 } 150 var ids []string 151 for _, target := range resp.GetConfiguredTargets() { 152 ids = append(ids, target.GetId().GetInvocationId()) 153 } 154 return ids, resp.GetNextPageToken(), err 155 } 156 157 // FetchResult provides a interface to store Resultstore invocation data. 158 type FetchResult struct { 159 Invocation *resultstore.Invocation 160 Actions []*resultstore.Action 161 ConfiguredTargets []*resultstore.ConfiguredTarget 162 Targets []*resultstore.Target 163 } 164 165 // fieldMask is required by gRPC for GET methods. 166 func fieldMask(ctx context.Context, fields ...string) context.Context { 167 return metadata.AppendToOutgoingContext(ctx, "X-Goog-FieldMask", strings.Join(fields, ",")) 168 } 169 170 // FetchInvocation returns all details for a given invocation. 171 func (c *DownloadClient) FetchInvocation(ctx context.Context, log logrus.FieldLogger, invocationID string) (*FetchResult, error) { 172 name := fmt.Sprintf("invocations/%s", invocationID) 173 nextPageToken := "" 174 result := &FetchResult{} 175 fieldMaskCtx := fieldMask( 176 ctx, 177 "next_page_token", 178 "invocation.id", 179 "invocation.timing", 180 "invocation.status_attributes", 181 "invocation.properties", 182 "invocation.invocation_attributes", 183 "targets.id", 184 "targets.timing", 185 "targets.status_attributes", 186 "targets.properties", 187 "actions.id", 188 "actions.timing", 189 "actions.properties", 190 "actions.status_attributes", 191 "actions.test_action", 192 "configured_targets.id", 193 "configured_targets.status_attributes", 194 "configured_targets.test_attributes", 195 "configured_targets.timing", 196 ) 197 for { 198 req := &resultstore.ExportInvocationRequest{ 199 Name: name, 200 PageStart: &resultstore.ExportInvocationRequest_PageToken{ 201 PageToken: nextPageToken, 202 }, 203 } 204 resp, err := c.client.ExportInvocation(fieldMaskCtx, req) 205 if err != nil { 206 return nil, err 207 } 208 if result.Invocation == nil { 209 result.Invocation = resp.GetInvocation() 210 } 211 result.Actions = append(result.Actions, resp.GetActions()...) 212 result.ConfiguredTargets = append(result.ConfiguredTargets, resp.GetConfiguredTargets()...) 213 result.Targets = append(result.Targets, resp.GetTargets()...) 214 if resp.GetNextPageToken() == "" { 215 break 216 } 217 nextPageToken = resp.GetNextPageToken() 218 } 219 return result, nil 220 }