2018-05-10 22:40:16 +00:00
//
2020-04-15 22:05:11 +00:00
// Copyright 2019 Joyent, Inc.
2018-05-10 22:40:16 +00:00
//
// 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
2020-04-15 22:05:11 +00:00
func ( c * Client ) DecodeError ( resp * http . Response , requestMethod string , consumeBody bool ) error {
2018-05-10 22:40:16 +00:00
err := & errors . APIError {
StatusCode : resp . StatusCode ,
}
2020-04-15 22:05:11 +00:00
if requestMethod != http . MethodHead && resp . Body != nil && consumeBody {
2018-05-10 22:40:16 +00:00
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 { }
2020-04-15 22:05:11 +00:00
// If the response has the HTTP status code 410 (i.e., "Gone"), should we preserve the contents of the body for the caller?
PreserveGone bool
2018-05-10 22:40:16 +00:00
}
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
}
2020-04-15 22:05:11 +00:00
return nil , c . DecodeError ( resp , req . Method , true )
2018-05-10 22:40:16 +00:00
}
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
}
2020-04-15 22:05:11 +00:00
// GetMachine returns a HTTP 410 response for deleted instances, but the body of the response is still a valid machine object with a State value of "deleted". Return the object to the caller as well as an error.
if inputs . PreserveGone && resp . StatusCode == http . StatusGone {
// Do not consume the response body.
return resp , c . DecodeError ( resp , req . Method , false )
}
return nil , c . DecodeError ( resp , req . Method , true )
2018-05-10 22:40:16 +00:00
}
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
}
2020-04-15 22:05:11 +00:00
return nil , nil , c . DecodeError ( resp , req . Method , true )
2018-05-10 22:40:16 +00:00
}
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
}
2020-04-15 22:05:11 +00:00
return nil , nil , c . DecodeError ( resp , req . Method , true )
2018-05-10 22:40:16 +00:00
}
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" )
}