408 lines
9.9 KiB
Go
408 lines
9.9 KiB
Go
package cfclient
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/net/context"
|
|
"golang.org/x/oauth2"
|
|
"golang.org/x/oauth2/clientcredentials"
|
|
)
|
|
|
|
//Client used to communicate with Cloud Foundry
|
|
type Client struct {
|
|
Config Config
|
|
Endpoint Endpoint
|
|
}
|
|
|
|
type Endpoint struct {
|
|
DopplerEndpoint string `json:"doppler_logging_endpoint"`
|
|
LoggingEndpoint string `json:"logging_endpoint"`
|
|
AuthEndpoint string `json:"authorization_endpoint"`
|
|
TokenEndpoint string `json:"token_endpoint"`
|
|
}
|
|
|
|
//Config is used to configure the creation of a client
|
|
type Config struct {
|
|
ApiAddress string `json:"api_url"`
|
|
Username string `json:"user"`
|
|
Password string `json:"password"`
|
|
ClientID string `json:"client_id"`
|
|
ClientSecret string `json:"client_secret"`
|
|
SkipSslValidation bool `json:"skip_ssl_validation"`
|
|
HttpClient *http.Client
|
|
Token string `json:"auth_token"`
|
|
TokenSource oauth2.TokenSource
|
|
tokenSourceDeadline *time.Time
|
|
UserAgent string `json:"user_agent"`
|
|
}
|
|
|
|
// Request is used to help build up a request
|
|
type Request struct {
|
|
method string
|
|
url string
|
|
params url.Values
|
|
body io.Reader
|
|
obj interface{}
|
|
}
|
|
|
|
//DefaultConfig configuration for client
|
|
//Keep LoginAdress for backward compatibility
|
|
//Need to be remove in close future
|
|
func DefaultConfig() *Config {
|
|
return &Config{
|
|
ApiAddress: "http://api.bosh-lite.com",
|
|
Username: "admin",
|
|
Password: "admin",
|
|
Token: "",
|
|
SkipSslValidation: false,
|
|
HttpClient: http.DefaultClient,
|
|
UserAgent: "Go-CF-client/1.1",
|
|
}
|
|
}
|
|
|
|
func DefaultEndpoint() *Endpoint {
|
|
return &Endpoint{
|
|
DopplerEndpoint: "wss://doppler.10.244.0.34.xip.io:443",
|
|
LoggingEndpoint: "wss://loggregator.10.244.0.34.xip.io:443",
|
|
TokenEndpoint: "https://uaa.10.244.0.34.xip.io",
|
|
AuthEndpoint: "https://login.10.244.0.34.xip.io",
|
|
}
|
|
}
|
|
|
|
// NewClient returns a new client
|
|
func NewClient(config *Config) (client *Client, err error) {
|
|
// bootstrap the config
|
|
defConfig := DefaultConfig()
|
|
|
|
if len(config.ApiAddress) == 0 {
|
|
config.ApiAddress = defConfig.ApiAddress
|
|
}
|
|
|
|
if len(config.Username) == 0 {
|
|
config.Username = defConfig.Username
|
|
}
|
|
|
|
if len(config.Password) == 0 {
|
|
config.Password = defConfig.Password
|
|
}
|
|
|
|
if len(config.Token) == 0 {
|
|
config.Token = defConfig.Token
|
|
}
|
|
|
|
if len(config.UserAgent) == 0 {
|
|
config.UserAgent = defConfig.UserAgent
|
|
}
|
|
|
|
if config.HttpClient == nil {
|
|
config.HttpClient = defConfig.HttpClient
|
|
}
|
|
|
|
if config.HttpClient.Transport == nil {
|
|
config.HttpClient.Transport = shallowDefaultTransport()
|
|
}
|
|
|
|
var tp *http.Transport
|
|
|
|
switch t := config.HttpClient.Transport.(type) {
|
|
case *http.Transport:
|
|
tp = t
|
|
case *oauth2.Transport:
|
|
if bt, ok := t.Base.(*http.Transport); ok {
|
|
tp = bt
|
|
}
|
|
}
|
|
|
|
if tp != nil {
|
|
if tp.TLSClientConfig == nil {
|
|
tp.TLSClientConfig = &tls.Config{}
|
|
}
|
|
tp.TLSClientConfig.InsecureSkipVerify = config.SkipSslValidation
|
|
}
|
|
|
|
config.ApiAddress = strings.TrimRight(config.ApiAddress, "/")
|
|
|
|
client = &Client{
|
|
Config: *config,
|
|
}
|
|
|
|
if err := client.refreshEndpoint(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
func shallowDefaultTransport() *http.Transport {
|
|
defaultTransport := http.DefaultTransport.(*http.Transport)
|
|
return &http.Transport{
|
|
Proxy: defaultTransport.Proxy,
|
|
TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout,
|
|
ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
|
|
}
|
|
}
|
|
|
|
func getUserAuth(ctx context.Context, config Config, endpoint *Endpoint) (Config, error) {
|
|
authConfig := &oauth2.Config{
|
|
ClientID: "cf",
|
|
Scopes: []string{""},
|
|
Endpoint: oauth2.Endpoint{
|
|
AuthURL: endpoint.AuthEndpoint + "/oauth/auth",
|
|
TokenURL: endpoint.TokenEndpoint + "/oauth/token",
|
|
},
|
|
}
|
|
|
|
token, err := authConfig.PasswordCredentialsToken(ctx, config.Username, config.Password)
|
|
if err != nil {
|
|
return config, errors.Wrap(err, "Error getting token")
|
|
}
|
|
|
|
config.tokenSourceDeadline = &token.Expiry
|
|
config.TokenSource = authConfig.TokenSource(ctx, token)
|
|
config.HttpClient = oauth2.NewClient(ctx, config.TokenSource)
|
|
|
|
return config, err
|
|
}
|
|
|
|
func getClientAuth(ctx context.Context, config Config, endpoint *Endpoint) Config {
|
|
authConfig := &clientcredentials.Config{
|
|
ClientID: config.ClientID,
|
|
ClientSecret: config.ClientSecret,
|
|
TokenURL: endpoint.TokenEndpoint + "/oauth/token",
|
|
}
|
|
|
|
config.TokenSource = authConfig.TokenSource(ctx)
|
|
config.HttpClient = authConfig.Client(ctx)
|
|
return config
|
|
}
|
|
|
|
// getUserTokenAuth initializes client credentials from existing bearer token.
|
|
func getUserTokenAuth(ctx context.Context, config Config, endpoint *Endpoint) Config {
|
|
authConfig := &oauth2.Config{
|
|
ClientID: "cf",
|
|
Scopes: []string{""},
|
|
Endpoint: oauth2.Endpoint{
|
|
AuthURL: endpoint.AuthEndpoint + "/oauth/auth",
|
|
TokenURL: endpoint.TokenEndpoint + "/oauth/token",
|
|
},
|
|
}
|
|
|
|
// Token is expected to have no "bearer" prefix
|
|
token := &oauth2.Token{
|
|
AccessToken: config.Token,
|
|
TokenType: "Bearer"}
|
|
|
|
config.TokenSource = authConfig.TokenSource(ctx, token)
|
|
config.HttpClient = oauth2.NewClient(ctx, config.TokenSource)
|
|
|
|
return config
|
|
}
|
|
|
|
func getInfo(api string, httpClient *http.Client) (*Endpoint, error) {
|
|
var endpoint Endpoint
|
|
|
|
if api == "" {
|
|
return DefaultEndpoint(), nil
|
|
}
|
|
|
|
resp, err := httpClient.Get(api + "/v2/info")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
err = decodeBody(resp, &endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &endpoint, err
|
|
}
|
|
|
|
// NewRequest is used to create a new Request
|
|
func (c *Client) NewRequest(method, path string) *Request {
|
|
r := &Request{
|
|
method: method,
|
|
url: c.Config.ApiAddress + path,
|
|
params: make(map[string][]string),
|
|
}
|
|
return r
|
|
}
|
|
|
|
// NewRequestWithBody is used to create a new request with
|
|
// arbigtrary body io.Reader.
|
|
func (c *Client) NewRequestWithBody(method, path string, body io.Reader) *Request {
|
|
r := c.NewRequest(method, path)
|
|
|
|
// Set request body
|
|
r.body = body
|
|
|
|
return r
|
|
}
|
|
|
|
// DoRequest runs a request with our client
|
|
func (c *Client) DoRequest(r *Request) (*http.Response, error) {
|
|
req, err := r.toHTTP()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.Do(req)
|
|
}
|
|
|
|
// DoRequestWithoutRedirects executes the request without following redirects
|
|
func (c *Client) DoRequestWithoutRedirects(r *Request) (*http.Response, error) {
|
|
prevCheckRedirect := c.Config.HttpClient.CheckRedirect
|
|
c.Config.HttpClient.CheckRedirect = func(httpReq *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
}
|
|
defer func() {
|
|
c.Config.HttpClient.CheckRedirect = prevCheckRedirect
|
|
}()
|
|
return c.DoRequest(r)
|
|
}
|
|
|
|
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
|
req.Header.Set("User-Agent", c.Config.UserAgent)
|
|
if req.Body != nil && req.Header.Get("Content-type") == "" {
|
|
req.Header.Set("Content-type", "application/json")
|
|
}
|
|
|
|
resp, err := c.Config.HttpClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode >= http.StatusBadRequest {
|
|
return c.handleError(resp)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (c *Client) handleError(resp *http.Response) (*http.Response, error) {
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
return resp, CloudFoundryHTTPError{
|
|
StatusCode: resp.StatusCode,
|
|
Status: resp.Status,
|
|
Body: body,
|
|
}
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Unmarshal V2 error response
|
|
if strings.HasPrefix(resp.Request.URL.Path, "/v2/") {
|
|
var cfErr CloudFoundryError
|
|
if err := json.Unmarshal(body, &cfErr); err != nil {
|
|
return resp, CloudFoundryHTTPError{
|
|
StatusCode: resp.StatusCode,
|
|
Status: resp.Status,
|
|
Body: body,
|
|
}
|
|
}
|
|
return nil, cfErr
|
|
}
|
|
|
|
// Unmarshal a V3 error response and convert it into a V2 model
|
|
var cfErrorsV3 CloudFoundryErrorsV3
|
|
if err := json.Unmarshal(body, &cfErrorsV3); err != nil {
|
|
return resp, CloudFoundryHTTPError{
|
|
StatusCode: resp.StatusCode,
|
|
Status: resp.Status,
|
|
Body: body,
|
|
}
|
|
}
|
|
return nil, NewCloudFoundryErrorFromV3Errors(cfErrorsV3)
|
|
}
|
|
|
|
func (c *Client) refreshEndpoint() error {
|
|
// we want to keep the Timeout value from config.HttpClient
|
|
timeout := c.Config.HttpClient.Timeout
|
|
|
|
ctx := context.Background()
|
|
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.Config.HttpClient)
|
|
|
|
endpoint, err := getInfo(c.Config.ApiAddress, oauth2.NewClient(ctx, nil))
|
|
|
|
if err != nil {
|
|
return errors.Wrap(err, "Could not get api /v2/info")
|
|
}
|
|
|
|
switch {
|
|
case c.Config.Token != "":
|
|
c.Config = getUserTokenAuth(ctx, c.Config, endpoint)
|
|
case c.Config.ClientID != "":
|
|
c.Config = getClientAuth(ctx, c.Config, endpoint)
|
|
default:
|
|
c.Config, err = getUserAuth(ctx, c.Config, endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// make sure original Timeout value will be used
|
|
if c.Config.HttpClient.Timeout != timeout {
|
|
c.Config.HttpClient.Timeout = timeout
|
|
}
|
|
|
|
c.Endpoint = *endpoint
|
|
return nil
|
|
}
|
|
|
|
// toHTTP converts the request to an HTTP Request
|
|
func (r *Request) toHTTP() (*http.Request, error) {
|
|
|
|
// Check if we should encode the body
|
|
if r.body == nil && r.obj != nil {
|
|
b, err := encodeBody(r.obj)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.body = b
|
|
}
|
|
|
|
// Create the HTTP Request
|
|
return http.NewRequest(r.method, r.url, r.body)
|
|
}
|
|
|
|
// decodeBody is used to JSON decode a body
|
|
func decodeBody(resp *http.Response, out interface{}) error {
|
|
defer resp.Body.Close()
|
|
dec := json.NewDecoder(resp.Body)
|
|
return dec.Decode(out)
|
|
}
|
|
|
|
// encodeBody is used to encode a request body
|
|
func encodeBody(obj interface{}) (io.Reader, error) {
|
|
buf := bytes.NewBuffer(nil)
|
|
enc := json.NewEncoder(buf)
|
|
if err := enc.Encode(obj); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf, nil
|
|
}
|
|
|
|
func (c *Client) GetToken() (string, error) {
|
|
if c.Config.tokenSourceDeadline != nil && c.Config.tokenSourceDeadline.Before(time.Now()) {
|
|
if err := c.refreshEndpoint(); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
token, err := c.Config.TokenSource.Token()
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "Error getting bearer token")
|
|
}
|
|
return "bearer " + token.AccessToken, nil
|
|
}
|