2017-11-11 04:44:02 +00:00
|
|
|
package clientbase
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2018-01-31 19:10:50 +00:00
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
2017-11-11 04:44:02 +00:00
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/rancher/norman/types"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
SELF = "self"
|
|
|
|
COLLECTION = "collection"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
debug = false
|
|
|
|
dialer = &websocket.Dialer{}
|
|
|
|
)
|
|
|
|
|
2018-04-10 20:29:10 +00:00
|
|
|
type APIBaseClientInterface interface {
|
|
|
|
Websocket(url string, headers map[string][]string) (*websocket.Conn, *http.Response, error)
|
|
|
|
List(schemaType string, opts *types.ListOpts, respObject interface{}) error
|
|
|
|
Post(url string, createObj interface{}, respObject interface{}) error
|
|
|
|
GetLink(resource types.Resource, link string, respObject interface{}) error
|
|
|
|
Create(schemaType string, createObj interface{}, respObject interface{}) error
|
|
|
|
Update(schemaType string, existing *types.Resource, updates interface{}, respObject interface{}) error
|
|
|
|
ByID(schemaType string, id string, respObject interface{}) error
|
|
|
|
Delete(existing *types.Resource) error
|
|
|
|
Reload(existing *types.Resource, output interface{}) error
|
|
|
|
Action(schemaType string, action string, existing *types.Resource, inputObject, respObject interface{}) error
|
|
|
|
}
|
|
|
|
|
|
|
|
type APIBaseClient struct {
|
|
|
|
Ops *APIOperations
|
|
|
|
Opts *ClientOpts
|
|
|
|
Types map[string]types.Schema
|
|
|
|
}
|
|
|
|
|
2017-11-11 04:44:02 +00:00
|
|
|
type ClientOpts struct {
|
|
|
|
URL string
|
|
|
|
AccessKey string
|
|
|
|
SecretKey string
|
2018-02-05 15:04:41 +00:00
|
|
|
TokenKey string
|
2017-11-11 04:44:02 +00:00
|
|
|
Timeout time.Duration
|
|
|
|
HTTPClient *http.Client
|
2018-01-31 19:10:50 +00:00
|
|
|
CACerts string
|
2018-02-13 23:21:22 +00:00
|
|
|
Insecure bool
|
2017-11-11 04:44:02 +00:00
|
|
|
}
|
|
|
|
|
2018-02-05 15:04:41 +00:00
|
|
|
func (c *ClientOpts) getAuthHeader() string {
|
|
|
|
if c.TokenKey != "" {
|
|
|
|
return "Bearer " + c.TokenKey
|
|
|
|
}
|
|
|
|
if c.AccessKey != "" && c.SecretKey != "" {
|
|
|
|
s := c.AccessKey + ":" + c.SecretKey
|
|
|
|
return "Basic " + base64.StdEncoding.EncodeToString([]byte(s))
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2017-11-11 04:44:02 +00:00
|
|
|
type APIError struct {
|
|
|
|
StatusCode int
|
|
|
|
URL string
|
|
|
|
Msg string
|
|
|
|
Status string
|
|
|
|
Body string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *APIError) Error() string {
|
|
|
|
return e.Msg
|
|
|
|
}
|
|
|
|
|
|
|
|
func IsNotFound(err error) bool {
|
|
|
|
apiError, ok := err.(*APIError)
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return apiError.StatusCode == http.StatusNotFound
|
|
|
|
}
|
|
|
|
|
2018-06-04 23:45:41 +00:00
|
|
|
func NewAPIError(resp *http.Response, url string) *APIError {
|
2017-11-11 04:44:02 +00:00
|
|
|
contents, err := ioutil.ReadAll(resp.Body)
|
|
|
|
var body string
|
|
|
|
if err != nil {
|
|
|
|
body = "Unreadable body."
|
|
|
|
} else {
|
|
|
|
body = string(contents)
|
|
|
|
}
|
|
|
|
|
|
|
|
data := map[string]interface{}{}
|
|
|
|
if json.Unmarshal(contents, &data) == nil {
|
|
|
|
delete(data, "id")
|
|
|
|
delete(data, "links")
|
|
|
|
delete(data, "actions")
|
|
|
|
delete(data, "type")
|
|
|
|
delete(data, "status")
|
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
for k, v := range data {
|
|
|
|
if v == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if buf.Len() > 0 {
|
|
|
|
buf.WriteString(", ")
|
|
|
|
}
|
|
|
|
fmt.Fprintf(buf, "%s=%v", k, v)
|
|
|
|
}
|
|
|
|
body = buf.String()
|
|
|
|
}
|
|
|
|
formattedMsg := fmt.Sprintf("Bad response statusCode [%d]. Status [%s]. Body: [%s] from [%s]",
|
|
|
|
resp.StatusCode, resp.Status, body, url)
|
|
|
|
return &APIError{
|
|
|
|
URL: url,
|
|
|
|
Msg: formattedMsg,
|
|
|
|
StatusCode: resp.StatusCode,
|
|
|
|
Status: resp.Status,
|
|
|
|
Body: body,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func contains(array []string, item string) bool {
|
|
|
|
for _, check := range array {
|
|
|
|
if check == item {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func appendFilters(urlString string, filters map[string]interface{}) (string, error) {
|
|
|
|
if len(filters) == 0 {
|
|
|
|
return urlString, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
u, err := url.Parse(urlString)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
q := u.Query()
|
|
|
|
for k, v := range filters {
|
|
|
|
if l, ok := v.([]string); ok {
|
|
|
|
for _, v := range l {
|
|
|
|
q.Add(k, v)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
q.Add(k, fmt.Sprintf("%v", v))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
u.RawQuery = q.Encode()
|
|
|
|
return u.String(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewAPIClient(opts *ClientOpts) (APIBaseClient, error) {
|
|
|
|
var err error
|
|
|
|
|
|
|
|
result := APIBaseClient{
|
|
|
|
Types: map[string]types.Schema{},
|
|
|
|
}
|
|
|
|
|
|
|
|
client := opts.HTTPClient
|
|
|
|
if client == nil {
|
|
|
|
client = &http.Client{}
|
|
|
|
}
|
|
|
|
|
|
|
|
if opts.Timeout == 0 {
|
|
|
|
opts.Timeout = time.Second * 10
|
|
|
|
}
|
|
|
|
|
|
|
|
client.Timeout = opts.Timeout
|
|
|
|
|
2018-01-31 19:10:50 +00:00
|
|
|
if opts.CACerts != "" {
|
|
|
|
roots := x509.NewCertPool()
|
|
|
|
ok := roots.AppendCertsFromPEM([]byte(opts.CACerts))
|
|
|
|
if !ok {
|
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
tr := &http.Transport{
|
|
|
|
TLSClientConfig: &tls.Config{
|
|
|
|
RootCAs: roots,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
client.Transport = tr
|
|
|
|
}
|
|
|
|
|
2018-02-13 23:21:22 +00:00
|
|
|
if opts.Insecure {
|
|
|
|
tr := &http.Transport{
|
|
|
|
TLSClientConfig: &tls.Config{
|
|
|
|
InsecureSkipVerify: opts.Insecure,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
client.Transport = tr
|
|
|
|
}
|
|
|
|
|
2017-11-11 04:44:02 +00:00
|
|
|
req, err := http.NewRequest("GET", opts.URL, nil)
|
|
|
|
if err != nil {
|
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
2018-02-05 15:04:41 +00:00
|
|
|
req.Header.Add("Authorization", opts.getAuthHeader())
|
2017-11-11 04:44:02 +00:00
|
|
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
if resp.StatusCode != 200 {
|
2018-06-04 23:45:41 +00:00
|
|
|
return result, NewAPIError(resp, opts.URL)
|
2017-11-11 04:44:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
schemasURLs := resp.Header.Get("X-API-Schemas")
|
|
|
|
if len(schemasURLs) == 0 {
|
|
|
|
return result, errors.New("Failed to find schema at [" + opts.URL + "]")
|
|
|
|
}
|
|
|
|
|
|
|
|
if schemasURLs != opts.URL {
|
|
|
|
req, err = http.NewRequest("GET", schemasURLs, nil)
|
2018-02-05 15:04:41 +00:00
|
|
|
req.Header.Add("Authorization", opts.getAuthHeader())
|
2017-11-11 04:44:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
if resp.StatusCode != 200 {
|
2018-06-04 23:45:41 +00:00
|
|
|
return result, NewAPIError(resp, opts.URL)
|
2017-11-11 04:44:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var schemas types.SchemaCollection
|
|
|
|
bytes, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = json.Unmarshal(bytes, &schemas)
|
|
|
|
if err != nil {
|
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, schema := range schemas.Data {
|
|
|
|
result.Types[schema.ID] = schema
|
|
|
|
}
|
|
|
|
|
|
|
|
result.Opts = opts
|
|
|
|
result.Ops = &APIOperations{
|
|
|
|
Opts: opts,
|
|
|
|
Client: client,
|
|
|
|
Types: result.Types,
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewListOpts() *types.ListOpts {
|
|
|
|
return &types.ListOpts{
|
|
|
|
Filters: map[string]interface{}{},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *APIBaseClient) Websocket(url string, headers map[string][]string) (*websocket.Conn, *http.Response, error) {
|
|
|
|
httpHeaders := http.Header{}
|
|
|
|
for k, v := range httpHeaders {
|
|
|
|
httpHeaders[k] = v
|
|
|
|
}
|
|
|
|
|
|
|
|
if a.Opts != nil {
|
2018-02-05 15:04:41 +00:00
|
|
|
httpHeaders.Add("Authorization", a.Opts.getAuthHeader())
|
2017-11-11 04:44:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return dialer.Dial(url, http.Header(httpHeaders))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *APIBaseClient) List(schemaType string, opts *types.ListOpts, respObject interface{}) error {
|
|
|
|
return a.Ops.DoList(schemaType, opts, respObject)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *APIBaseClient) Post(url string, createObj interface{}, respObject interface{}) error {
|
|
|
|
return a.Ops.DoModify("POST", url, createObj, respObject)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *APIBaseClient) GetLink(resource types.Resource, link string, respObject interface{}) error {
|
|
|
|
url := resource.Links[link]
|
|
|
|
if url == "" {
|
2017-11-21 20:46:30 +00:00
|
|
|
return fmt.Errorf("failed to find link: %s", link)
|
2017-11-11 04:44:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return a.Ops.DoGet(url, &types.ListOpts{}, respObject)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *APIBaseClient) Create(schemaType string, createObj interface{}, respObject interface{}) error {
|
|
|
|
return a.Ops.DoCreate(schemaType, createObj, respObject)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *APIBaseClient) Update(schemaType string, existing *types.Resource, updates interface{}, respObject interface{}) error {
|
|
|
|
return a.Ops.DoUpdate(schemaType, existing, updates, respObject)
|
|
|
|
}
|
|
|
|
|
2017-11-11 05:33:03 +00:00
|
|
|
func (a *APIBaseClient) ByID(schemaType string, id string, respObject interface{}) error {
|
|
|
|
return a.Ops.DoByID(schemaType, id, respObject)
|
2017-11-11 04:44:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (a *APIBaseClient) Delete(existing *types.Resource) error {
|
|
|
|
if existing == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return a.Ops.DoResourceDelete(existing.Type, existing)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *APIBaseClient) Reload(existing *types.Resource, output interface{}) error {
|
2017-11-21 20:46:30 +00:00
|
|
|
selfURL, ok := existing.Links[SELF]
|
2017-11-11 04:44:02 +00:00
|
|
|
if !ok {
|
2017-11-21 20:46:30 +00:00
|
|
|
return fmt.Errorf("failed to find self URL of [%v]", existing)
|
2017-11-11 04:44:02 +00:00
|
|
|
}
|
|
|
|
|
2017-11-21 20:46:30 +00:00
|
|
|
return a.Ops.DoGet(selfURL, NewListOpts(), output)
|
2017-11-11 04:44:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (a *APIBaseClient) Action(schemaType string, action string,
|
|
|
|
existing *types.Resource, inputObject, respObject interface{}) error {
|
|
|
|
return a.Ops.DoAction(schemaType, action, existing, inputObject, respObject)
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
debug = os.Getenv("RANCHER_CLIENT_DEBUG") == "true"
|
|
|
|
if debug {
|
|
|
|
fmt.Println("Rancher client debug on")
|
|
|
|
}
|
|
|
|
}
|