Files
nomad/vendor/github.com/joyent/triton-go/client/client.go
Seth Hoenig 15fb4c990a deps: Switch to Go modules for dependency management
This PR switches the Nomad repository from using govendor to Go modules
for managing dependencies. Aspects of the Nomad workflow remain pretty
much the same. The usual Makefile targets should continue to work as
they always did. The API submodule simply defers to the parent Nomad
version on the repository, keeping the semantics of API versioning that
currently exists.
2020-06-02 14:30:36 -05:00

601 lines
16 KiB
Go

//
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
package client
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"regexp"
"strings"
"time"
triton "github.com/joyent/triton-go"
"github.com/joyent/triton-go/authentication"
"github.com/joyent/triton-go/errors"
pkgerrors "github.com/pkg/errors"
)
var (
ErrDefaultAuth = pkgerrors.New("default SSH agent authentication requires SDC_KEY_ID / TRITON_KEY_ID and SSH_AUTH_SOCK")
ErrAccountName = pkgerrors.New("missing account name")
ErrMissingURL = pkgerrors.New("missing API URL")
InvalidTritonURL = "invalid format of Triton URL"
InvalidMantaURL = "invalid format of Manta URL"
InvalidServicesURL = "invalid format of Triton Service Groups URL"
InvalidDCInURL = "invalid data center in URL"
knownDCFormats = []string{
`https?://(.*).api.joyent.com`,
`https?://(.*).api.joyentcloud.com`,
`https?://(.*).api.samsungcloud.io`,
}
jpcFormatURL = "https://tsg.%s.svc.joyent.zone"
spcFormatURL = "https://tsg.%s.svc.samsungcloud.zone"
)
// Client represents a connection to the Triton Compute or Object Storage APIs.
type Client struct {
HTTPClient *http.Client
RequestHeader *http.Header
Authorizers []authentication.Signer
TritonURL url.URL
MantaURL url.URL
ServicesURL url.URL
AccountName string
Username string
}
func isPrivateInstall(url string) bool {
for _, pattern := range knownDCFormats {
re := regexp.MustCompile(pattern)
matches := re.FindStringSubmatch(url)
if len(matches) > 1 {
return false
}
}
return true
}
// parseDC parses out the data center commonly found in Triton URLs. Returns an
// error if the Triton URL does not include a known data center name, in which
// case a URL override (TRITON_TSG_URL) must be provided.
func parseDC(url string) (string, bool, error) {
isSamsung := false
if strings.Contains(url, "samsung") {
isSamsung = true
}
for _, pattern := range knownDCFormats {
re := regexp.MustCompile(pattern)
matches := re.FindStringSubmatch(url)
if len(matches) > 1 {
return matches[1], isSamsung, nil
}
}
return "", isSamsung, fmt.Errorf("failed to parse datacenter from '%s'", url)
}
// New is used to construct a Client in order to make API
// requests to the Triton API.
//
// At least one signer must be provided - example signers include
// authentication.PrivateKeySigner and authentication.SSHAgentSigner.
func New(tritonURL string, mantaURL string, accountName string, signers ...authentication.Signer) (*Client, error) {
if accountName == "" {
return nil, ErrAccountName
}
if tritonURL == "" && mantaURL == "" {
return nil, ErrMissingURL
}
cloudURL, err := url.Parse(tritonURL)
if err != nil {
return nil, pkgerrors.Wrapf(err, InvalidTritonURL)
}
storageURL, err := url.Parse(mantaURL)
if err != nil {
return nil, pkgerrors.Wrapf(err, InvalidMantaURL)
}
// Generate the Services URL (TSG) based on the current datacenter used in
// the Triton URL (if TritonURL is available). If TRITON_TSG_URL environment
// variable is available than override using that value instead.
tsgURL := triton.GetEnv("TSG_URL")
if tsgURL == "" && tritonURL != "" && !isPrivateInstall(tritonURL) {
currentDC, isSamsung, err := parseDC(tritonURL)
if err != nil {
return nil, pkgerrors.Wrapf(err, InvalidDCInURL)
}
tsgURL = fmt.Sprintf(jpcFormatURL, currentDC)
if isSamsung {
tsgURL = fmt.Sprintf(spcFormatURL, currentDC)
}
}
servicesURL, err := url.Parse(tsgURL)
if err != nil {
return nil, pkgerrors.Wrapf(err, InvalidServicesURL)
}
authorizers := make([]authentication.Signer, 0)
for _, key := range signers {
if key != nil {
authorizers = append(authorizers, key)
}
}
newClient := &Client{
HTTPClient: &http.Client{
Transport: httpTransport(false),
CheckRedirect: doNotFollowRedirects,
},
Authorizers: authorizers,
TritonURL: *cloudURL,
MantaURL: *storageURL,
ServicesURL: *servicesURL,
AccountName: accountName,
}
// Default to constructing an SSHAgentSigner if there are no other signers
// passed into NewClient and there's an TRITON_KEY_ID and SSH_AUTH_SOCK
// available in the user's environ(7).
if len(newClient.Authorizers) == 0 {
if err := newClient.DefaultAuth(); err != nil {
return nil, err
}
}
return newClient, nil
}
// initDefaultAuth provides a default key signer for a client. This should only
// be used internally if the client has no other key signer for authenticating
// with Triton. We first look for both `SDC_KEY_ID` and `SSH_AUTH_SOCK` in the
// user's environ(7). If so we default to the SSH agent key signer.
func (c *Client) DefaultAuth() error {
tritonKeyId := triton.GetEnv("KEY_ID")
if tritonKeyId != "" {
input := authentication.SSHAgentSignerInput{
KeyID: tritonKeyId,
AccountName: c.AccountName,
Username: c.Username,
}
defaultSigner, err := authentication.NewSSHAgentSigner(input)
if err != nil {
return pkgerrors.Wrapf(err, "unable to initialize NewSSHAgentSigner")
}
c.Authorizers = append(c.Authorizers, defaultSigner)
}
return ErrDefaultAuth
}
// InsecureSkipTLSVerify turns off TLS verification for the client connection. This
// allows connection to an endpoint with a certificate which was signed by a non-
// trusted CA, such as self-signed certificates. This can be useful when connecting
// to temporary Triton installations such as Triton Cloud-On-A-Laptop.
func (c *Client) InsecureSkipTLSVerify() {
if c.HTTPClient == nil {
return
}
c.HTTPClient.Transport = httpTransport(true)
}
// httpTransport is responsible for setting up our HTTP client's transport
// settings
func httpTransport(insecureSkipTLSVerify bool) *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
MaxIdleConns: 10,
IdleConnTimeout: 15 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecureSkipTLSVerify,
},
}
}
func doNotFollowRedirects(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}
// DecodeError decodes a backend Triton error into a more usable Go error type
func (c *Client) DecodeError(resp *http.Response, requestMethod string) error {
err := &errors.APIError{
StatusCode: resp.StatusCode,
}
if requestMethod != http.MethodHead && resp.Body != nil {
errorDecoder := json.NewDecoder(resp.Body)
if err := errorDecoder.Decode(err); err != nil {
return pkgerrors.Wrapf(err, "unable to decode error response")
}
}
if err.Message == "" {
err.Message = fmt.Sprintf("HTTP response returned status code %d", err.StatusCode)
}
return err
}
// overrideHeader overrides the header of the passed in HTTP request
func (c *Client) overrideHeader(req *http.Request) {
if c.RequestHeader != nil {
for k := range *c.RequestHeader {
req.Header.Set(k, c.RequestHeader.Get(k))
}
}
}
// resetHeader will reset the struct field that stores custom header
// information
func (c *Client) resetHeader() {
c.RequestHeader = nil
}
// -----------------------------------------------------------------------------
type RequestInput struct {
Method string
Path string
Query *url.Values
Headers *http.Header
Body interface{}
}
func (c *Client) ExecuteRequestURIParams(ctx context.Context, inputs RequestInput) (io.ReadCloser, error) {
defer c.resetHeader()
method := inputs.Method
path := inputs.Path
body := inputs.Body
query := inputs.Query
var requestBody io.Reader
if body != nil {
marshaled, err := json.MarshalIndent(body, "", " ")
if err != nil {
return nil, err
}
requestBody = bytes.NewReader(marshaled)
}
endpoint := c.TritonURL
endpoint.Path = path
if query != nil {
endpoint.RawQuery = query.Encode()
}
req, err := http.NewRequest(method, endpoint.String(), requestBody)
if err != nil {
return nil, pkgerrors.Wrapf(err, "unable to construct HTTP request")
}
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
// NewClient ensures there's always an authorizer (unless this is called
// outside that constructor).
authHeader, err := c.Authorizers[0].Sign(dateHeader, false)
if err != nil {
return nil, pkgerrors.Wrapf(err, "unable to sign HTTP request")
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("Accept", "application/json")
req.Header.Set("Accept-Version", triton.CloudAPIMajorVersion)
req.Header.Set("User-Agent", triton.UserAgent())
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
c.overrideHeader(req)
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, pkgerrors.Wrapf(err, "unable to execute HTTP request")
}
// We will only return a response from the API it is in the HTTP StatusCode
// 2xx range
// StatusMultipleChoices is StatusCode 300
if resp.StatusCode >= http.StatusOK &&
resp.StatusCode < http.StatusMultipleChoices {
return resp.Body, nil
}
return nil, c.DecodeError(resp, req.Method)
}
func (c *Client) ExecuteRequest(ctx context.Context, inputs RequestInput) (io.ReadCloser, error) {
return c.ExecuteRequestURIParams(ctx, inputs)
}
func (c *Client) ExecuteRequestRaw(ctx context.Context, inputs RequestInput) (*http.Response, error) {
defer c.resetHeader()
method := inputs.Method
path := inputs.Path
body := inputs.Body
query := inputs.Query
var requestBody io.Reader
if body != nil {
marshaled, err := json.MarshalIndent(body, "", " ")
if err != nil {
return nil, err
}
requestBody = bytes.NewReader(marshaled)
}
endpoint := c.TritonURL
endpoint.Path = path
if query != nil {
endpoint.RawQuery = query.Encode()
}
req, err := http.NewRequest(method, endpoint.String(), requestBody)
if err != nil {
return nil, pkgerrors.Wrapf(err, "unable to construct HTTP request")
}
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
// NewClient ensures there's always an authorizer (unless this is called
// outside that constructor).
authHeader, err := c.Authorizers[0].Sign(dateHeader, false)
if err != nil {
return nil, pkgerrors.Wrapf(err, "unable to sign HTTP request")
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("Accept", "application/json")
req.Header.Set("Accept-Version", triton.CloudAPIMajorVersion)
req.Header.Set("User-Agent", triton.UserAgent())
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
c.overrideHeader(req)
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, pkgerrors.Wrapf(err, "unable to execute HTTP request")
}
// We will only return a response from the API it is in the HTTP StatusCode
// 2xx range
// StatusMultipleChoices is StatusCode 300
if resp.StatusCode >= http.StatusOK &&
resp.StatusCode < http.StatusMultipleChoices {
return resp, nil
}
return nil, c.DecodeError(resp, req.Method)
}
func (c *Client) ExecuteRequestStorage(ctx context.Context, inputs RequestInput) (io.ReadCloser, http.Header, error) {
defer c.resetHeader()
method := inputs.Method
path := inputs.Path
query := inputs.Query
headers := inputs.Headers
body := inputs.Body
endpoint := c.MantaURL
endpoint.Path = path
var requestBody io.Reader
if body != nil {
marshaled, err := json.MarshalIndent(body, "", " ")
if err != nil {
return nil, nil, err
}
requestBody = bytes.NewReader(marshaled)
}
req, err := http.NewRequest(method, endpoint.String(), requestBody)
if err != nil {
return nil, nil, pkgerrors.Wrapf(err, "unable to construct HTTP request")
}
if body != nil && (headers == nil || headers.Get("Content-Type") == "") {
req.Header.Set("Content-Type", "application/json")
}
if headers != nil {
for key, values := range *headers {
for _, value := range values {
req.Header.Set(key, value)
}
}
}
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
authHeader, err := c.Authorizers[0].Sign(dateHeader, true)
if err != nil {
return nil, nil, pkgerrors.Wrapf(err, "unable to sign HTTP request")
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("Accept", "*/*")
req.Header.Set("User-Agent", triton.UserAgent())
if query != nil {
req.URL.RawQuery = query.Encode()
}
c.overrideHeader(req)
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, nil, pkgerrors.Wrapf(err, "unable to execute HTTP request")
}
// We will only return a response from the API it is in the HTTP StatusCode
// 2xx range
// StatusMultipleChoices is StatusCode 300
if resp.StatusCode >= http.StatusOK &&
resp.StatusCode < http.StatusMultipleChoices {
return resp.Body, resp.Header, nil
}
return nil, nil, c.DecodeError(resp, req.Method)
}
type RequestNoEncodeInput struct {
Method string
Path string
Query *url.Values
Headers *http.Header
Body io.Reader
}
func (c *Client) ExecuteRequestNoEncode(ctx context.Context, inputs RequestNoEncodeInput) (io.ReadCloser, http.Header, error) {
defer c.resetHeader()
method := inputs.Method
path := inputs.Path
query := inputs.Query
headers := inputs.Headers
body := inputs.Body
endpoint := c.MantaURL
endpoint.Path = path
req, err := http.NewRequest(method, endpoint.String(), body)
if err != nil {
return nil, nil, pkgerrors.Wrapf(err, "unable to construct HTTP request")
}
if headers != nil {
for key, values := range *headers {
for _, value := range values {
req.Header.Set(key, value)
}
}
}
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
authHeader, err := c.Authorizers[0].Sign(dateHeader, true)
if err != nil {
return nil, nil, pkgerrors.Wrapf(err, "unable to sign HTTP request")
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Version", triton.CloudAPIMajorVersion)
req.Header.Set("User-Agent", triton.UserAgent())
if query != nil {
req.URL.RawQuery = query.Encode()
}
c.overrideHeader(req)
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, nil, pkgerrors.Wrapf(err, "unable to execute HTTP request")
}
// We will only return a response from the API it is in the HTTP StatusCode
// 2xx range
// StatusMultipleChoices is StatusCode 300
if resp.StatusCode >= http.StatusOK &&
resp.StatusCode < http.StatusMultipleChoices {
return resp.Body, resp.Header, nil
}
return nil, nil, c.DecodeError(resp, req.Method)
}
func (c *Client) ExecuteRequestTSG(ctx context.Context, inputs RequestInput) (io.ReadCloser, error) {
defer c.resetHeader()
method := inputs.Method
path := inputs.Path
body := inputs.Body
query := inputs.Query
var requestBody io.Reader
if body != nil {
marshaled, err := json.MarshalIndent(body, "", " ")
if err != nil {
return nil, err
}
requestBody = bytes.NewReader(marshaled)
}
endpoint := c.ServicesURL
endpoint.Path = path
if query != nil {
endpoint.RawQuery = query.Encode()
}
req, err := http.NewRequest(method, endpoint.String(), requestBody)
if err != nil {
return nil, pkgerrors.Wrapf(err, "unable to construct HTTP request")
}
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
// NewClient ensures there's always an authorizer (unless this is called
// outside that constructor).
authHeader, err := c.Authorizers[0].Sign(dateHeader, false)
if err != nil {
return nil, pkgerrors.Wrapf(err, "unable to sign HTTP request")
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("Accept", "application/json")
req.Header.Set("Accept-Version", triton.CloudAPIMajorVersion)
req.Header.Set("User-Agent", triton.UserAgent())
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
c.overrideHeader(req)
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, pkgerrors.Wrapf(err, "unable to execute HTTP request")
}
if resp.StatusCode >= http.StatusOK &&
resp.StatusCode < http.StatusMultipleChoices {
return resp.Body, nil
}
return nil, fmt.Errorf("could not process backend TSG request")
}