terraform/vendor/github.com/apparentlymart/go-rundeck-api/rundeck/client.go

303 lines
6.8 KiB
Go

// Package rundeck provides a client for interacting with a Rundeck instance
// via its HTTP API.
//
// Instantiate a Client with the NewClient function to get started.
//
// At present this package uses Rundeck API version 13.
package rundeck
import (
"bytes"
"crypto/tls"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"mime/multipart"
"strings"
)
// ClientConfig is used with NewClient to specify initialization settings.
type ClientConfig struct {
// The base URL of the Rundeck instance.
BaseURL string
// The API auth token generated from user settings in the Rundeck UI.
AuthToken string
// Don't fail if the server uses SSL with an un-verifiable certificate.
// This is not recommended except during development/debugging.
AllowUnverifiedSSL bool
}
// Client is a Rundeck API client interface.
type Client struct {
httpClient *http.Client
apiURL *url.URL
authToken string
}
type request struct {
Method string
PathParts []string
QueryArgs map[string]string
Headers map[string]string
BodyBytes []byte
}
// NewClient returns a configured Rundeck client.
func NewClient(config *ClientConfig) (*Client, error) {
t := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: config.AllowUnverifiedSSL,
},
}
httpClient := &http.Client{
Transport: t,
}
apiPath, _ := url.Parse("api/13/")
baseURL, err := url.Parse(config.BaseURL)
if err != nil {
return nil, fmt.Errorf("invalid base URL: %s", err.Error())
}
apiURL := baseURL.ResolveReference(apiPath)
return &Client{
httpClient: httpClient,
apiURL: apiURL,
authToken: config.AuthToken,
}, nil
}
func (c *Client) rawRequest(req *request) ([]byte, error) {
res, err := c.httpClient.Do(req.MakeHTTPRequest(c))
if err != nil {
return nil, err
}
resBodyBytes, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode == 404 {
return nil, &NotFoundError{}
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
if strings.HasPrefix(res.Header.Get("Content-Type"), "text/xml") {
var richErr Error
err = xml.Unmarshal(resBodyBytes, &richErr)
if err != nil {
return nil, fmt.Errorf("HTTP Error %i with error decoding XML body: %s", res.StatusCode, err.Error())
}
return nil, richErr
}
return nil, fmt.Errorf("HTTP Error %i", res.StatusCode)
}
if res.StatusCode != 200 && res.StatusCode != 201 {
return nil, nil
}
return resBodyBytes, nil
}
func (c *Client) xmlRequest(method string, pathParts []string, query map[string]string, reqBody interface{}, result interface{}) error {
var err error
var reqBodyBytes []byte
reqBodyBytes = nil
if reqBody != nil {
reqBodyBytes, err = xml.Marshal(reqBody)
if err != nil {
return err
}
}
req := &request{
Method: method,
PathParts: pathParts,
QueryArgs: query,
BodyBytes: reqBodyBytes,
Headers: map[string]string{
"Accept": "application/xml",
},
}
if reqBody != nil {
req.Headers["Content-Type"] = "application/xml"
}
resBodyBytes, err := c.rawRequest(req)
if err != nil {
return err
}
if result != nil {
if resBodyBytes == nil {
return fmt.Errorf("server did not return an XML payload")
}
err = xml.Unmarshal(resBodyBytes, result)
if err != nil {
return fmt.Errorf("error decoding response XML payload: %s", err.Error())
}
}
return nil
}
func (c *Client) get(pathParts []string, query map[string]string, result interface{}) error {
return c.xmlRequest("GET", pathParts, query, nil, result)
}
func (c *Client) rawGet(pathParts []string, query map[string]string, accept string) (string, error) {
req := &request{
Method: "GET",
PathParts: pathParts,
QueryArgs: query,
Headers: map[string]string{
"Accept": accept,
},
}
resBodyBytes, err := c.rawRequest(req)
if err != nil {
return "", err
}
return string(resBodyBytes), nil
}
func (c *Client) post(pathParts []string, query map[string]string, reqBody interface{}, result interface{}) error {
return c.xmlRequest("POST", pathParts, query, reqBody, result)
}
func (c *Client) put(pathParts []string, reqBody interface{}, result interface{}) error {
return c.xmlRequest("PUT", pathParts, nil, reqBody, result)
}
func (c *Client) delete(pathParts []string) error {
return c.xmlRequest("DELETE", pathParts, nil, nil, nil)
}
func (c *Client) postXMLBatch(pathParts []string, args map[string]string, xmlBatch interface{}, result interface{}) error {
req := &http.Request{
Method: "POST",
Header: http.Header{},
}
req.Header.Add("User-Agent", "Go-Rundeck-API")
req.Header.Add("X-Rundeck-Auth-Token", c.authToken)
urlPath := &url.URL{
Path: strings.Join(pathParts, "/"),
}
reqURL := c.apiURL.ResolveReference(urlPath)
req.URL = reqURL
buf := bytes.Buffer{}
writer := multipart.NewWriter(&buf)
for k, v := range args {
err := writer.WriteField(k, v)
if err != nil {
return err
}
}
partWriter, err := writer.CreateFormFile("xmlBatch", "batch.xml")
if err != nil {
return err
}
reqBodyBytes, err := xml.Marshal(xmlBatch)
if err != nil {
return err
}
_, err = partWriter.Write(reqBodyBytes)
if err != nil {
return err
}
writer.Close()
reqBodyReader := bytes.NewReader(buf.Bytes())
req.Body = ioutil.NopCloser(reqBodyReader)
req.ContentLength = int64(buf.Len())
req.Header.Add("Content-Type", writer.FormDataContentType())
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
resBodyBytes, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
if strings.HasPrefix(res.Header.Get("Content-Type"), "text/xml") {
var richErr Error
err = xml.Unmarshal(resBodyBytes, &richErr)
if err != nil {
return fmt.Errorf("HTTP Error %i with error decoding XML body: %s", res.StatusCode, err.Error())
}
return richErr
}
return fmt.Errorf("HTTP Error %i", res.StatusCode)
}
if result != nil {
if res.StatusCode != 200 && res.StatusCode != 201 {
return fmt.Errorf("server did not return an XML payload")
}
err = xml.Unmarshal(resBodyBytes, result)
if err != nil {
return fmt.Errorf("error decoding response XML payload: %s", err.Error())
}
}
return nil
}
func (r *request) MakeHTTPRequest(client *Client) *http.Request {
req := &http.Request{
Method: r.Method,
Header: http.Header{},
}
// Automatic/mandatory HTTP headers first
req.Header.Add("User-Agent", "Go-Rundeck-API")
req.Header.Add("X-Rundeck-Auth-Token", client.authToken)
for k, v := range r.Headers {
req.Header.Add(k, v)
}
urlPath := &url.URL{
Path: strings.Join(r.PathParts, "/"),
}
reqURL := client.apiURL.ResolveReference(urlPath)
req.URL = reqURL
if len(r.QueryArgs) > 0 {
urlQuery := url.Values{}
for k, v := range r.QueryArgs {
urlQuery.Add(k, v)
}
reqURL.RawQuery = urlQuery.Encode()
}
if r.BodyBytes != nil {
req.Body = ioutil.NopCloser(bytes.NewReader(r.BodyBytes))
req.ContentLength = int64(len(r.BodyBytes))
}
return req
}