303 lines
6.8 KiB
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
|
|
}
|