620 lines
14 KiB
Go
620 lines
14 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
SELF = "self"
|
|
COLLECTION = "collection"
|
|
)
|
|
|
|
var (
|
|
debug = false
|
|
dialer = &websocket.Dialer{}
|
|
privateFieldRegex = regexp.MustCompile("^[[:lower:]]")
|
|
)
|
|
|
|
type ClientOpts struct {
|
|
Url string
|
|
AccessKey string
|
|
SecretKey string
|
|
Timeout time.Duration
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func newApiError(resp *http.Response, url string) *ApiError {
|
|
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 NormalizeUrl(existingUrl string) (string, error) {
|
|
u, err := url.Parse(existingUrl)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if u.Path == "" || u.Path == "/" {
|
|
u.Path = "v2-beta"
|
|
} else if u.Path == "/v1" || strings.HasPrefix(u.Path, "/v1/") {
|
|
u.Path = strings.Replace(u.Path, "/v1", "/v2-beta", 1)
|
|
}
|
|
|
|
return u.String(), nil
|
|
}
|
|
|
|
func setupRancherBaseClient(rancherClient *RancherBaseClientImpl, opts *ClientOpts) error {
|
|
var err error
|
|
opts.Url, err = NormalizeUrl(opts.Url)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if opts.Timeout == 0 {
|
|
opts.Timeout = time.Second * 10
|
|
}
|
|
client := &http.Client{Timeout: opts.Timeout}
|
|
req, err := http.NewRequest("GET", opts.Url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req.SetBasicAuth(opts.AccessKey, opts.SecretKey)
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return newApiError(resp, opts.Url)
|
|
}
|
|
|
|
schemasUrls := resp.Header.Get("X-API-Schemas")
|
|
if len(schemasUrls) == 0 {
|
|
return errors.New("Failed to find schema at [" + opts.Url + "]")
|
|
}
|
|
|
|
if schemasUrls != opts.Url {
|
|
req, err = http.NewRequest("GET", schemasUrls, nil)
|
|
req.SetBasicAuth(opts.AccessKey, opts.SecretKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err = client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return newApiError(resp, opts.Url)
|
|
}
|
|
}
|
|
|
|
var schemas Schemas
|
|
bytes, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = json.Unmarshal(bytes, &schemas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rancherClient.Opts = opts
|
|
rancherClient.Schemas = &schemas
|
|
|
|
for _, schema := range schemas.Data {
|
|
rancherClient.Types[schema.Id] = schema
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func NewListOpts() *ListOpts {
|
|
return &ListOpts{
|
|
Filters: map[string]interface{}{},
|
|
}
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) setupRequest(req *http.Request) {
|
|
req.SetBasicAuth(rancherClient.Opts.AccessKey, rancherClient.Opts.SecretKey)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) newHttpClient() *http.Client {
|
|
if rancherClient.Opts.Timeout == 0 {
|
|
rancherClient.Opts.Timeout = time.Second * 10
|
|
}
|
|
return &http.Client{Timeout: rancherClient.Opts.Timeout}
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) doDelete(url string) error {
|
|
client := rancherClient.newHttpClient()
|
|
req, err := http.NewRequest("DELETE", url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rancherClient.setupRequest(req)
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
io.Copy(ioutil.Discard, resp.Body)
|
|
|
|
if resp.StatusCode >= 300 {
|
|
return newApiError(resp, url)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) Websocket(url string, headers map[string][]string) (*websocket.Conn, *http.Response, error) {
|
|
httpHeaders := http.Header{}
|
|
for k, v := range httpHeaders {
|
|
httpHeaders[k] = v
|
|
}
|
|
|
|
if rancherClient.Opts != nil {
|
|
s := rancherClient.Opts.AccessKey + ":" + rancherClient.Opts.SecretKey
|
|
httpHeaders.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(s)))
|
|
}
|
|
|
|
return dialer.Dial(url, http.Header(httpHeaders))
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) doGet(url string, opts *ListOpts, respObject interface{}) error {
|
|
if opts == nil {
|
|
opts = NewListOpts()
|
|
}
|
|
url, err := appendFilters(url, opts.Filters)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if debug {
|
|
fmt.Println("GET " + url)
|
|
}
|
|
|
|
client := rancherClient.newHttpClient()
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rancherClient.setupRequest(req)
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return newApiError(resp, url)
|
|
}
|
|
|
|
byteContent, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if debug {
|
|
fmt.Println("Response <= " + string(byteContent))
|
|
}
|
|
|
|
if err := json.Unmarshal(byteContent, respObject); err != nil {
|
|
return errors.Wrap(err, fmt.Sprintf("Failed to parse: %s", byteContent))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) List(schemaType string, opts *ListOpts, respObject interface{}) error {
|
|
return rancherClient.doList(schemaType, opts, respObject)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) doList(schemaType string, opts *ListOpts, respObject interface{}) error {
|
|
schema, ok := rancherClient.Types[schemaType]
|
|
if !ok {
|
|
return errors.New("Unknown schema type [" + schemaType + "]")
|
|
}
|
|
|
|
if !contains(schema.CollectionMethods, "GET") {
|
|
return errors.New("Resource type [" + schemaType + "] is not listable")
|
|
}
|
|
|
|
collectionUrl, ok := schema.Links[COLLECTION]
|
|
if !ok {
|
|
return errors.New("Failed to find collection URL for [" + schemaType + "]")
|
|
}
|
|
|
|
return rancherClient.doGet(collectionUrl, opts, respObject)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) doNext(nextUrl string, respObject interface{}) error {
|
|
return rancherClient.doGet(nextUrl, nil, respObject)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) Post(url string, createObj interface{}, respObject interface{}) error {
|
|
return rancherClient.doModify("POST", url, createObj, respObject)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) GetLink(resource Resource, link string, respObject interface{}) error {
|
|
url := resource.Links[link]
|
|
if url == "" {
|
|
return fmt.Errorf("Failed to find link: %s", link)
|
|
}
|
|
|
|
return rancherClient.doGet(url, &ListOpts{}, respObject)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) doModify(method string, url string, createObj interface{}, respObject interface{}) error {
|
|
bodyContent, err := json.Marshal(createObj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if debug {
|
|
fmt.Println(method + " " + url)
|
|
fmt.Println("Request => " + string(bodyContent))
|
|
}
|
|
|
|
client := rancherClient.newHttpClient()
|
|
req, err := http.NewRequest(method, url, bytes.NewBuffer(bodyContent))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rancherClient.setupRequest(req)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 300 {
|
|
return newApiError(resp, url)
|
|
}
|
|
|
|
byteContent, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(byteContent) > 0 {
|
|
if debug {
|
|
fmt.Println("Response <= " + string(byteContent))
|
|
}
|
|
return json.Unmarshal(byteContent, respObject)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) Create(schemaType string, createObj interface{}, respObject interface{}) error {
|
|
return rancherClient.doCreate(schemaType, createObj, respObject)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) doCreate(schemaType string, createObj interface{}, respObject interface{}) error {
|
|
if createObj == nil {
|
|
createObj = map[string]string{}
|
|
}
|
|
if respObject == nil {
|
|
respObject = &map[string]interface{}{}
|
|
}
|
|
schema, ok := rancherClient.Types[schemaType]
|
|
if !ok {
|
|
return errors.New("Unknown schema type [" + schemaType + "]")
|
|
}
|
|
|
|
if !contains(schema.CollectionMethods, "POST") {
|
|
return errors.New("Resource type [" + schemaType + "] is not creatable")
|
|
}
|
|
|
|
var collectionUrl string
|
|
collectionUrl, ok = schema.Links[COLLECTION]
|
|
if !ok {
|
|
// return errors.New("Failed to find collection URL for [" + schemaType + "]")
|
|
// This is a hack to address https://github.com/rancher/cattle/issues/254
|
|
re := regexp.MustCompile("schemas.*")
|
|
collectionUrl = re.ReplaceAllString(schema.Links[SELF], schema.PluralName)
|
|
}
|
|
|
|
return rancherClient.doModify("POST", collectionUrl, createObj, respObject)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) Update(schemaType string, existing *Resource, updates interface{}, respObject interface{}) error {
|
|
return rancherClient.doUpdate(schemaType, existing, updates, respObject)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) doUpdate(schemaType string, existing *Resource, updates interface{}, respObject interface{}) error {
|
|
if existing == nil {
|
|
return errors.New("Existing object is nil")
|
|
}
|
|
|
|
selfUrl, ok := existing.Links[SELF]
|
|
if !ok {
|
|
return errors.New(fmt.Sprintf("Failed to find self URL of [%v]", existing))
|
|
}
|
|
|
|
if updates == nil {
|
|
updates = map[string]string{}
|
|
}
|
|
|
|
if respObject == nil {
|
|
respObject = &map[string]interface{}{}
|
|
}
|
|
|
|
schema, ok := rancherClient.Types[schemaType]
|
|
if !ok {
|
|
return errors.New("Unknown schema type [" + schemaType + "]")
|
|
}
|
|
|
|
if !contains(schema.ResourceMethods, "PUT") {
|
|
return errors.New("Resource type [" + schemaType + "] is not updatable")
|
|
}
|
|
|
|
return rancherClient.doModify("PUT", selfUrl, updates, respObject)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) ById(schemaType string, id string, respObject interface{}) error {
|
|
return rancherClient.doById(schemaType, id, respObject)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) doById(schemaType string, id string, respObject interface{}) error {
|
|
schema, ok := rancherClient.Types[schemaType]
|
|
if !ok {
|
|
return errors.New("Unknown schema type [" + schemaType + "]")
|
|
}
|
|
|
|
if !contains(schema.ResourceMethods, "GET") {
|
|
return errors.New("Resource type [" + schemaType + "] can not be looked up by ID")
|
|
}
|
|
|
|
collectionUrl, ok := schema.Links[COLLECTION]
|
|
if !ok {
|
|
return errors.New("Failed to find collection URL for [" + schemaType + "]")
|
|
}
|
|
|
|
err := rancherClient.doGet(collectionUrl+"/"+id, nil, respObject)
|
|
//TODO check for 404 and return nil, nil
|
|
return err
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) Delete(existing *Resource) error {
|
|
if existing == nil {
|
|
return nil
|
|
}
|
|
return rancherClient.doResourceDelete(existing.Type, existing)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) doResourceDelete(schemaType string, existing *Resource) error {
|
|
schema, ok := rancherClient.Types[schemaType]
|
|
if !ok {
|
|
return errors.New("Unknown schema type [" + schemaType + "]")
|
|
}
|
|
|
|
if !contains(schema.ResourceMethods, "DELETE") {
|
|
return errors.New("Resource type [" + schemaType + "] can not be deleted")
|
|
}
|
|
|
|
selfUrl, ok := existing.Links[SELF]
|
|
if !ok {
|
|
return errors.New(fmt.Sprintf("Failed to find self URL of [%v]", existing))
|
|
}
|
|
|
|
return rancherClient.doDelete(selfUrl)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) Reload(existing *Resource, output interface{}) error {
|
|
selfUrl, ok := existing.Links[SELF]
|
|
if !ok {
|
|
return errors.New(fmt.Sprintf("Failed to find self URL of [%v]", existing))
|
|
}
|
|
|
|
return rancherClient.doGet(selfUrl, NewListOpts(), output)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) Action(schemaType string, action string,
|
|
existing *Resource, inputObject, respObject interface{}) error {
|
|
return rancherClient.doAction(schemaType, action, existing, inputObject, respObject)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) doAction(schemaType string, action string,
|
|
existing *Resource, inputObject, respObject interface{}) error {
|
|
|
|
if existing == nil {
|
|
return errors.New("Existing object is nil")
|
|
}
|
|
|
|
actionUrl, ok := existing.Actions[action]
|
|
if !ok {
|
|
return errors.New(fmt.Sprintf("Action [%v] not available on [%v]", action, existing))
|
|
}
|
|
|
|
_, ok = rancherClient.Types[schemaType]
|
|
if !ok {
|
|
return errors.New("Unknown schema type [" + schemaType + "]")
|
|
}
|
|
|
|
var input io.Reader
|
|
|
|
if inputObject != nil {
|
|
bodyContent, err := json.Marshal(inputObject)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if debug {
|
|
fmt.Println("Request => " + string(bodyContent))
|
|
}
|
|
input = bytes.NewBuffer(bodyContent)
|
|
}
|
|
|
|
client := rancherClient.newHttpClient()
|
|
req, err := http.NewRequest("POST", actionUrl, input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rancherClient.setupRequest(req)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Content-Length", "0")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 300 {
|
|
return newApiError(resp, actionUrl)
|
|
}
|
|
|
|
byteContent, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if debug {
|
|
fmt.Println("Response <= " + string(byteContent))
|
|
}
|
|
|
|
return json.Unmarshal(byteContent, respObject)
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) GetOpts() *ClientOpts {
|
|
return rancherClient.Opts
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) GetSchemas() *Schemas {
|
|
return rancherClient.Schemas
|
|
}
|
|
|
|
func (rancherClient *RancherBaseClientImpl) GetTypes() map[string]Schema {
|
|
return rancherClient.Types
|
|
}
|
|
|
|
func init() {
|
|
debug = os.Getenv("RANCHER_CLIENT_DEBUG") == "true"
|
|
if debug {
|
|
fmt.Println("Rancher client debug on")
|
|
}
|
|
}
|