internal/gitlab/client.go (92 lines of code) (raw):
package gitlab
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"path"
"regexp"
log "github.com/sirupsen/logrus"
)
const (
apiBaseURL = "/api/v4/"
//customUserAgent used to instrument usage of the release-cli
// https://gitlab.com/gitlab-org/gitlab/-/issues/296612
customUserAgent = "GitLab-release-cli"
)
var errMissingToken = errors.New("access token not provided")
// ErrorResponse expected from the API
type ErrorResponse struct {
statusCode int
Message string `json:"message,omitempty"`
// Err will only be populated if the Releases API returns an unexpected error and is not contained in Message
Err string `json:"error,omitempty"`
}
// Error implements the error interface. Wraps an error message from the API into an error type
func (er *ErrorResponse) Error() string {
err := fmt.Sprintf("API Error Response status_code: %d message: %s", er.statusCode, er.Message)
if er.Err != "" {
return fmt.Sprintf("%s error: %s", err, er.Err)
}
return err
}
// HTTPClient is an interface that describes the available actions of the client.
// Use http.Client during runtime.
// See mock_httpClient_test.go for a testing implementation
type HTTPClient interface {
Do(*http.Request) (*http.Response, error)
}
// Client is used to send requests to the GitLab API. Normally created with the `New` function
type Client struct {
baseURL string
jobToken string
privateToken string // used outside of CI
projectID string
httpClient HTTPClient
logger log.FieldLogger
}
// New creates a new GitLab Client
func New(serverURL, jobToken, privateToken, projectID string, httpClient HTTPClient, logger log.FieldLogger) (*Client, error) {
if jobToken == "" && privateToken == "" {
return nil, errMissingToken
}
u, err := url.Parse(serverURL)
if err != nil {
return nil, fmt.Errorf("failed to parse url: %w", err)
}
u.Path = path.Join(u.Path, apiBaseURL)
return &Client{
baseURL: u.String(),
jobToken: jobToken,
privateToken: privateToken,
projectID: projectID,
httpClient: httpClient,
logger: logger,
}, nil
}
// request creates a new request and attaches
func (gc *Client) request(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, gc.baseURL+url, body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
// if PRIVATE-TOKEN takes precedence over JOB-TOKEN
if gc.privateToken != "" {
req.Header.Set("PRIVATE-TOKEN", gc.privateToken)
} else {
req.Header.Set("JOB-TOKEN", gc.jobToken)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", customUserAgent)
if gc.logger.(*log.Entry).Logger.Level == log.DebugLevel {
dreq, err := httputil.DumpRequestOut(req, true)
printRequest(gc.logger, dreq, err)
}
return req, nil
}
func checkClosed(closer io.Closer) {
if err := closer.Close(); err != nil {
log.WithError(err).Warn("failed to close")
}
}
var tokenRegex = regexp.MustCompile("(.*-Token): (.*)")
func printRequest(log log.FieldLogger, dreq []byte, e error) {
if e != nil {
log.WithError(e).Debug("Error printing the request")
return
}
log.Debug("Sent request:")
fmt.Printf("%s\n", tokenRegex.ReplaceAll(dreq, []byte("$1: [MASKED]")))
}