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  }