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  }