File: client.go

package info (click to toggle)
singularity-container 4.0.3%2Bds1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 21,672 kB
  • sloc: asm: 3,857; sh: 2,125; ansic: 1,677; awk: 414; makefile: 110; python: 99
file content (159 lines) | stat: -rw-r--r-- 4,482 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// Copyright (c) 2019-2023, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.

package client

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// errUnsupportedProtocolScheme is returned when an unsupported protocol scheme is encountered.
var errUnsupportedProtocolScheme = errors.New("unsupported protocol scheme")

// normalizeURL parses rawURL, and ensures the path component is terminated with a separator.
func normalizeURL(rawURL string) (*url.URL, error) {
	u, err := url.Parse(rawURL)
	if err != nil {
		return nil, err
	}

	if u.Scheme != "http" && u.Scheme != "https" {
		return nil, fmt.Errorf("%w %s", errUnsupportedProtocolScheme, u.Scheme)
	}

	// Ensure path is terminated with a separator, to prevent url.ResolveReference from stripping
	// the final path component of BaseURL when constructing request URL from a relative path.
	if !strings.HasSuffix(u.Path, "/") {
		u.Path += "/"
	}

	return u, nil
}

// clientOptions describes the options for a Client.
type clientOptions struct {
	baseURL     string
	bearerToken string
	userAgent   string
	transport   http.RoundTripper
}

// Option are used to populate co.
type Option func(co *clientOptions) error

// OptBaseURL sets the base URL of the build server to url.
func OptBaseURL(url string) Option {
	return func(co *clientOptions) error {
		co.baseURL = url
		return nil
	}
}

// OptBearerToken sets the bearer token to include in the "Authorization" header of each request.
func OptBearerToken(token string) Option {
	return func(co *clientOptions) error {
		co.bearerToken = token
		return nil
	}
}

// OptUserAgent sets the HTTP user agent to include in the "User-Agent" header of each request.
func OptUserAgent(agent string) Option {
	return func(co *clientOptions) error {
		co.userAgent = agent
		return nil
	}
}

// OptHTTPTransport sets the transport for HTTP requests to use.
func OptHTTPTransport(tr http.RoundTripper) Option {
	return func(co *clientOptions) error {
		co.transport = tr
		return nil
	}
}

// Client describes the client details.
type Client struct {
	baseURL                *url.URL     // Parsed base URL.
	bearerToken            string       // Bearer token to include in "Authorization" header.
	userAgent              string       // Value to include in "User-Agent" header.
	httpClient             *http.Client // Client to use for HTTP requests.
	buildContextHTTPClient *http.Client // Client to use for build context HTTP requests.
}

const defaultBaseURL = "https://build.sylabs.io/"

// NewClient returns a Client configured according to opts.
//
// By default, the Sylabs Build Service is used. To override this behaviour, use OptBaseURL.
//
// By default, requests are not authenticated. To override this behaviour, use OptBearerToken.
func NewClient(opts ...Option) (*Client, error) {
	co := clientOptions{
		baseURL:   defaultBaseURL,
		transport: http.DefaultTransport,
	}

	// Apply options.
	for _, opt := range opts {
		if err := opt(&co); err != nil {
			return nil, fmt.Errorf("%w", err)
		}
	}

	c := Client{
		bearerToken: co.bearerToken,
		userAgent:   co.userAgent,
		httpClient: &http.Client{
			Transport: co.transport,
			Timeout:   30 * time.Second, // use default from singularity
		},
		buildContextHTTPClient: &http.Client{Transport: co.transport},
	}

	// Normalize base URL.
	u, err := normalizeURL(co.baseURL)
	if err != nil {
		return nil, fmt.Errorf("%w", err)
	}
	c.baseURL = u

	return &c, nil
}

// newRequest returns a new Request given a method, ref, and optional body.
//
// The context controls the entire lifetime of a request and its response: obtaining a connection,
// sending the request, and reading the response headers and body.
func (c *Client) newRequest(ctx context.Context, method string, ref *url.URL, body io.Reader) (*http.Request, error) {
	u := c.baseURL.ResolveReference(ref)

	r, err := http.NewRequestWithContext(ctx, method, u.String(), body)
	if err != nil {
		return nil, err
	}

	c.setRequestHeaders(r.Header)

	return r, nil
}

// setRequestHeaders sets HTTP headers according to c.
func (c *Client) setRequestHeaders(h http.Header) {
	if v := c.bearerToken; v != "" {
		h.Set("Authorization", fmt.Sprintf("BEARER %s", v))
	}
	if v := c.userAgent; v != "" {
		h.Set("User-Agent", v)
	}
}