github.com/drone/runner-go@v1.12.0/client/http.go (about) 1 // Copyright 2019 Drone.IO Inc. All rights reserved. 2 // Use of this source code is governed by the Polyform License 3 // that can be found in the LICENSE file. 4 5 package client 6 7 import ( 8 "bytes" 9 "context" 10 "crypto/tls" 11 "encoding/json" 12 "errors" 13 "fmt" 14 "io" 15 "io/ioutil" 16 "net/http" 17 "net/url" 18 "time" 19 20 "github.com/drone/drone-go/drone" 21 "github.com/drone/runner-go/logger" 22 ) 23 24 const ( 25 endpointNode = "/rpc/v2/nodes/%s" 26 endpointPing = "/rpc/v2/ping" 27 endpointStages = "/rpc/v2/stage" 28 endpointStage = "/rpc/v2/stage/%d" 29 endpointStep = "/rpc/v2/step/%d" 30 endpointWatch = "/rpc/v2/build/%d/watch" 31 endpointBatch = "/rpc/v2/step/%d/logs/batch" 32 endpointUpload = "/rpc/v2/step/%d/logs/upload" 33 endpointCard = "/rpc/v2/step/%d/card" 34 ) 35 36 var _ Client = (*HTTPClient)(nil) 37 38 // defaultClient is the default http.Client. 39 var defaultClient = &http.Client{ 40 CheckRedirect: func(*http.Request, []*http.Request) error { 41 return http.ErrUseLastResponse 42 }, 43 } 44 45 // New returns a new runner client. 46 func New(endpoint, secret string, skipverify bool) *HTTPClient { 47 client := &HTTPClient{ 48 Endpoint: endpoint, 49 Secret: secret, 50 SkipVerify: skipverify, 51 } 52 if skipverify { 53 client.Client = &http.Client{ 54 CheckRedirect: func(*http.Request, []*http.Request) error { 55 return http.ErrUseLastResponse 56 }, 57 Transport: &http.Transport{ 58 Proxy: http.ProxyFromEnvironment, 59 TLSClientConfig: &tls.Config{ 60 InsecureSkipVerify: true, 61 }, 62 }, 63 } 64 } 65 return client 66 } 67 68 // An HTTPClient manages communication with the runner API. 69 type HTTPClient struct { 70 Client *http.Client 71 Logger logger.Logger 72 Dumper logger.Dumper 73 Endpoint string 74 Secret string 75 SkipVerify bool 76 } 77 78 // Join notifies the server the runner is joining the cluster. 79 func (p *HTTPClient) Join(ctx context.Context, machine string) error { 80 return nil 81 } 82 83 // Leave notifies the server the runner is leaving the cluster. 84 func (p *HTTPClient) Leave(ctx context.Context, machine string) error { 85 return nil 86 } 87 88 // Ping sends a ping message to the server to test connectivity. 89 func (p *HTTPClient) Ping(ctx context.Context, machine string) error { 90 _, err := p.do(ctx, endpointPing, "POST", nil, nil) 91 return err 92 } 93 94 // Request requests the next available build stage for execution. 95 func (p *HTTPClient) Request(ctx context.Context, args *Filter) (*drone.Stage, error) { 96 src := args 97 dst := new(drone.Stage) 98 _, err := p.retry(ctx, endpointStages, "POST", src, dst) 99 return dst, err 100 } 101 102 // Accept accepts the build stage for execution. 103 func (p *HTTPClient) Accept(ctx context.Context, stage *drone.Stage) error { 104 uri := fmt.Sprintf(endpointStage+"?machine=%s", stage.ID, url.QueryEscape(stage.Machine)) 105 src := stage 106 dst := new(drone.Stage) 107 _, err := p.retry(ctx, uri, "POST", nil, dst) 108 if dst != nil { 109 src.Updated = dst.Updated 110 src.Version = dst.Version 111 } 112 return err 113 } 114 115 // Detail gets the build stage details for execution. 116 func (p *HTTPClient) Detail(ctx context.Context, stage *drone.Stage) (*Context, error) { 117 uri := fmt.Sprintf(endpointStage, stage.ID) 118 dst := new(Context) 119 _, err := p.retry(ctx, uri, "GET", nil, dst) 120 return dst, err 121 } 122 123 // Update updates the build stage. 124 func (p *HTTPClient) Update(ctx context.Context, stage *drone.Stage) error { 125 uri := fmt.Sprintf(endpointStage, stage.ID) 126 src := stage 127 dst := new(drone.Stage) 128 for i, step := range src.Steps { 129 // a properly implemented runner should never encounter 130 // input errors. these checks are included to help 131 // developers creating new runners. 132 if step.Number == 0 { 133 return fmt.Errorf("step[%d] missing number", i) 134 } 135 if step.StageID == 0 { 136 return fmt.Errorf("step[%d] missing stage id", i) 137 } 138 if step.Status == drone.StatusRunning && 139 step.Started == 0 { 140 return fmt.Errorf("step[%d] missing start time", i) 141 } 142 } 143 _, err := p.retry(ctx, uri, "PUT", src, dst) 144 if dst != nil { 145 src.Updated = dst.Updated 146 src.Version = dst.Version 147 148 set := map[int]*drone.Step{} 149 for _, step := range dst.Steps { 150 set[step.Number] = step 151 } 152 for _, step := range src.Steps { 153 from, ok := set[step.Number] 154 if ok { 155 step.ID = from.ID 156 step.StageID = from.StageID 157 step.Started = from.Started 158 step.Stopped = from.Stopped 159 step.Version = from.Version 160 } 161 } 162 } 163 return err 164 } 165 166 // UpdateStep updates the build step. 167 func (p *HTTPClient) UpdateStep(ctx context.Context, step *drone.Step) error { 168 uri := fmt.Sprintf(endpointStep, step.ID) 169 src := step 170 dst := new(drone.Step) 171 _, err := p.retry(ctx, uri, "PUT", src, dst) 172 if dst != nil { 173 src.Version = dst.Version 174 } 175 return err 176 } 177 178 // Watch watches for build cancellation requests. 179 func (p *HTTPClient) Watch(ctx context.Context, build int64) (bool, error) { 180 uri := fmt.Sprintf(endpointWatch, build) 181 res, err := p.retry(ctx, uri, "POST", nil, nil) 182 if err != nil { 183 return false, err 184 } 185 if res.StatusCode == 200 { 186 return true, nil 187 } 188 return false, nil 189 } 190 191 // Batch batch writes logs to the build logs. 192 func (p *HTTPClient) Batch(ctx context.Context, step int64, lines []*drone.Line) error { 193 uri := fmt.Sprintf(endpointBatch, step) 194 _, err := p.do(ctx, uri, "POST", &lines, nil) 195 return err 196 } 197 198 // Upload uploads the full logs to the server. 199 func (p *HTTPClient) Upload(ctx context.Context, step int64, lines []*drone.Line) error { 200 uri := fmt.Sprintf(endpointUpload, step) 201 _, err := p.retry(ctx, uri, "POST", &lines, nil) 202 return err 203 } 204 205 // UploadCard uploads a card to drone server. 206 func (p *HTTPClient) UploadCard(ctx context.Context, step int64, card *drone.CardInput) error { 207 uri := fmt.Sprintf(endpointCard, step) 208 _, err := p.retry(ctx, uri, "POST", &card, nil) 209 return err 210 } 211 212 func (p *HTTPClient) retry(ctx context.Context, path, method string, in, out interface{}) (*http.Response, error) { 213 for { 214 res, err := p.do(ctx, path, method, in, out) 215 // do not retry on Canceled or DeadlineExceeded 216 if err := ctx.Err(); err != nil { 217 p.logger().Tracef("http: context canceled") 218 return res, err 219 } 220 // do not retry on optimisitic lock errors 221 if err == ErrOptimisticLock { 222 return res, err 223 } 224 if res != nil { 225 // Check the response code. We retry on 500-range 226 // responses to allow the server time to recover, as 227 // 500's are typically not permanent errors and may 228 // relate to outages on the server side. 229 if res.StatusCode > 501 { 230 p.logger().Tracef("http: server error: re-connect and re-try: %s", err) 231 time.Sleep(time.Second * 10) 232 continue 233 } 234 // We also retry on 204 no content response codes, 235 // used by the server when a long-polling request 236 // is intentionally disconnected and should be 237 // automatically reconnected. 238 if res.StatusCode == 204 { 239 p.logger().Tracef("http: no content returned: re-connect and re-try") 240 time.Sleep(time.Second * 10) 241 continue 242 } 243 } else if err != nil { 244 p.logger().Tracef("http: request error: %s", err) 245 time.Sleep(time.Second * 10) 246 continue 247 } 248 return res, err 249 } 250 } 251 252 // do is a helper function that posts a signed http request with 253 // the input encoded and response decoded from json. 254 func (p *HTTPClient) do(ctx context.Context, path, method string, in, out interface{}) (*http.Response, error) { 255 var buf bytes.Buffer 256 257 // marshal the input payload into json format and copy 258 // to an io.ReadCloser. 259 if in != nil { 260 json.NewEncoder(&buf).Encode(in) 261 } 262 263 endpoint := p.Endpoint + path 264 req, err := http.NewRequest(method, endpoint, &buf) 265 if err != nil { 266 return nil, err 267 } 268 req = req.WithContext(ctx) 269 270 // the request should include the secret shared between 271 // the agent and server for authorization. 272 req.Header.Add("X-Drone-Token", p.Secret) 273 274 if p.Dumper != nil { 275 p.Dumper.DumpRequest(req) 276 } 277 278 res, err := p.client().Do(req) 279 if res != nil { 280 defer func() { 281 // drain the response body so we can reuse 282 // this connection. 283 io.Copy(ioutil.Discard, io.LimitReader(res.Body, 4096)) 284 res.Body.Close() 285 }() 286 } 287 if err != nil { 288 return res, err 289 } 290 291 if p.Dumper != nil { 292 p.Dumper.DumpResponse(res) 293 } 294 295 // if the response body return no content we exit 296 // immediately. We do not read or unmarshal the response 297 // and we do not return an error. 298 if res.StatusCode == 204 { 299 return res, nil 300 } 301 302 // Check the response for a 409 conflict. This indicates an 303 // optimistic lock error, in which case multiple clients may 304 // be attempting to update the same record. Convert this error 305 // code to a proper error. 306 if res.StatusCode == 409 { 307 return nil, ErrOptimisticLock 308 } 309 310 // else read the response body into a byte slice. 311 body, err := ioutil.ReadAll(res.Body) 312 if err != nil { 313 return res, err 314 } 315 316 if res.StatusCode > 299 { 317 // if the response body includes an error message 318 // we should return the error string. 319 if len(body) != 0 { 320 return res, errors.New( 321 string(body), 322 ) 323 } 324 // if the response body is empty we should return 325 // the default status code text. 326 return res, errors.New( 327 http.StatusText(res.StatusCode), 328 ) 329 } 330 if out == nil { 331 return res, nil 332 } 333 return res, json.Unmarshal(body, out) 334 } 335 336 // client is a helper funciton that returns the default client 337 // if a custom client is not defined. 338 func (p *HTTPClient) client() *http.Client { 339 if p.Client == nil { 340 return defaultClient 341 } 342 return p.Client 343 } 344 345 // logger is a helper funciton that returns the default logger 346 // if a custom logger is not defined. 347 func (p *HTTPClient) logger() logger.Logger { 348 if p.Logger == nil { 349 return logger.Discard() 350 } 351 return p.Logger 352 }