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  }