github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/googlecloudbuild/client/client.go (about)

     1  /*
     2  Copyright 2021 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 client
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  	"time"
    24  
    25  	cloudbuild "cloud.google.com/go/cloudbuild/apiv1/v2"
    26  	"cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb"
    27  	"google.golang.org/api/iterator"
    28  	"google.golang.org/api/option"
    29  	"k8s.io/apimachinery/pkg/util/wait"
    30  )
    31  
    32  const (
    33  	// GCB automatically assigns random build ids to a new build, so it's not
    34  	// possible to associate a GCB build with a prow job by id without storing
    35  	// mapping somewhere else. Utilizing GCB tags for achieving this.
    36  
    37  	// ProwLabelSeparator formats labels key:val pairs into `{key} ::: {val}`
    38  	// The separator was arbitrarily selected.
    39  	ProwLabelSeparator = " ::: "
    40  )
    41  
    42  // Operator is the interface that's highly recommended to be used by any package
    43  // that imports current package. Unit test can be carried out with a mock
    44  // implemented under fake of current package.
    45  type Operator interface {
    46  	GetBuild(ctx context.Context, project, id string) (*cloudbuildpb.Build, error)
    47  	ListBuildsByTag(ctx context.Context, project string, tags []string) ([]*cloudbuildpb.Build, error)
    48  	CreateBuild(ctx context.Context, project string, bld *cloudbuildpb.Build) (*cloudbuildpb.Build, error)
    49  	CancelBuild(ctx context.Context, project, id string) (*cloudbuildpb.Build, error)
    50  }
    51  
    52  // ProwLabel formats labels key:val pairs into `{key} ::: {val}`.
    53  // These labels will be parsed by prow controller manager for mapping a GCB
    54  // build to a prow job.
    55  func ProwLabel(key, val string) string {
    56  	return key + ProwLabelSeparator + val
    57  }
    58  
    59  // KvPairFromProwLabel trims label into key:val pairs. returns empty string as value if
    60  // the label is not formatted as prow label format.
    61  func KvPairFromProwLabel(tag string) (key, val string) {
    62  	parts := strings.SplitN(tag, ProwLabelSeparator, 2)
    63  	if len(parts) != 2 {
    64  		return tag, ""
    65  	}
    66  	return parts[0], parts[1]
    67  }
    68  
    69  // GetProwLabels gets labels from cloud build struct, simulating k8s pods labels format
    70  // of map[string]string.
    71  func GetProwLabels(bld *cloudbuildpb.Build) map[string]string {
    72  	res := make(map[string]string)
    73  	for _, tag := range bld.Tags {
    74  		key, val := KvPairFromProwLabel(tag)
    75  		if val != "" {
    76  			res[key] = val
    77  		}
    78  	}
    79  	return res
    80  }
    81  
    82  var _ Operator = (*Client)(nil)
    83  
    84  // Client wraps native cloudbuild client.
    85  type Client struct {
    86  	interactor *cloudbuild.Client
    87  }
    88  
    89  // NewClient creates a new Client, with optional credentialFile.
    90  func NewClient(ctx context.Context, credentialFile string) (*Client, error) {
    91  	var opts []option.ClientOption
    92  	// Authenticating with key file if it's provided.
    93  	if len(credentialFile) > 0 {
    94  		opts = append(opts, option.WithCredentialsFile(credentialFile))
    95  	}
    96  
    97  	cbClient, err := cloudbuild.NewClient(ctx, opts...)
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  	return &Client{interactor: cbClient}, nil
   102  }
   103  
   104  // GetBuild gets build by GCB build id.
   105  func (c *Client) GetBuild(ctx context.Context, project, id string) (*cloudbuildpb.Build, error) {
   106  	return c.interactor.GetBuild(ctx, &cloudbuildpb.GetBuildRequest{
   107  		ProjectId: project,
   108  		Id:        id,
   109  	})
   110  }
   111  
   112  // ListBuildsByTag lists builds by GCB tags.
   113  //
   114  // This will be used by prow for listing builds triggered by prow, for example
   115  // `created-by-prow ::: true`.
   116  func (c *Client) ListBuildsByTag(ctx context.Context, project string, tags []string) ([]*cloudbuildpb.Build, error) {
   117  	// pageSize is used by ListBuildsByTag only, define here for locality
   118  	// reason. Can move up if pagination is needed by more functions.
   119  	const pageSize = 50
   120  	var res []*cloudbuildpb.Build
   121  	var tagsFilters []string
   122  	for _, tag := range tags {
   123  		tagsFilters = append(tagsFilters, "tags="+tag)
   124  	}
   125  
   126  	iter := c.interactor.ListBuilds(ctx, &cloudbuildpb.ListBuildsRequest{
   127  		ProjectId: project,
   128  		PageSize:  pageSize,
   129  		Filter:    strings.Join(tagsFilters, " AND "),
   130  	})
   131  	// ListBuilds already fetches all, just need to do pagination.
   132  	pager := iterator.NewPager(iter, pageSize, "")
   133  	for {
   134  		var buildsInPage []*cloudbuildpb.Build
   135  		nextPageToken, err := pager.NextPage(&buildsInPage)
   136  		if err != nil {
   137  			return nil, err
   138  		}
   139  		if buildsInPage != nil {
   140  			res = append(res, buildsInPage...)
   141  		}
   142  		if nextPageToken == "" {
   143  			break
   144  		}
   145  	}
   146  	return res, nil
   147  }
   148  
   149  // CreateBuild creates build and wait for the operation to complete.
   150  func (c *Client) CreateBuild(ctx context.Context, project string, bld *cloudbuildpb.Build) (*cloudbuildpb.Build, error) {
   151  	op, err := c.interactor.CreateBuild(ctx, &cloudbuildpb.CreateBuildRequest{
   152  		ProjectId: project,
   153  		Build:     bld,
   154  	})
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	// CreateBuild returns CreateBuildOperation, wait until the operation results in build.
   160  	// CreateBuildOperation contains `Wait` method, which however polls every minute, which is
   161  	// too slow. So use `wait.PollUntilContextTimeout` instead.
   162  	var triggered *cloudbuildpb.Build
   163  	const (
   164  		pollInterval = 100 * time.Millisecond
   165  		pollTimeout  = 30 * time.Second
   166  	)
   167  	if err := wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, func(ctx context.Context) (bool, error) {
   168  		triggered, err = op.Poll(ctx)
   169  		if err != nil {
   170  			return false, fmt.Errorf("failed to create build in project %s: %w", project, err)
   171  		}
   172  		// op.Poll surprisingly waits until the build completes before returning build,
   173  		// op.Metadata somehow does not wait, so use it instead.
   174  		meta, err := op.Metadata()
   175  		if err != nil {
   176  			return false, fmt.Errorf("failed to get metadata in project %s: %w", project, err)
   177  		}
   178  		triggered = meta.GetBuild()
   179  		return triggered != nil, nil
   180  	}); err != nil {
   181  		return nil, fmt.Errorf("failed waiting for build in project %s appear: %w", project, err)
   182  	}
   183  	return triggered, nil
   184  }
   185  
   186  // CancelBuild cancels build and wait for it.
   187  func (c *Client) CancelBuild(ctx context.Context, project, id string) (*cloudbuildpb.Build, error) {
   188  	return c.interactor.CancelBuild(ctx, &cloudbuildpb.CancelBuildRequest{
   189  		ProjectId: project,
   190  		Id:        id,
   191  	})
   192  }