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 }