2019-08-02 02:48:38 +00:00
|
|
|
package common
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/tls"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2019-08-02 20:09:54 +00:00
|
|
|
"strings"
|
2019-08-02 02:48:38 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
)
|
|
|
|
|
|
|
|
var client *PortainerClient
|
|
|
|
|
2019-08-02 18:21:50 +00:00
|
|
|
type ClientConfig struct {
|
|
|
|
Url string
|
|
|
|
User string
|
|
|
|
Password string
|
|
|
|
Token string
|
|
|
|
DoNotUseToken bool
|
|
|
|
Insecure bool
|
|
|
|
Timeout time.Duration
|
2019-08-02 17:26:51 +00:00
|
|
|
}
|
|
|
|
|
2019-08-02 02:48:38 +00:00
|
|
|
type PortainerClient struct {
|
|
|
|
http.Client
|
2019-08-02 18:21:50 +00:00
|
|
|
url *url.URL
|
|
|
|
user string
|
|
|
|
password string
|
|
|
|
token string
|
|
|
|
doNotUseToken bool
|
2019-08-02 02:48:38 +00:00
|
|
|
}
|
|
|
|
|
2019-08-02 17:21:29 +00:00
|
|
|
// Check if an http.Response object has errors
|
2019-08-02 02:48:38 +00:00
|
|
|
func checkResponseForErrors(resp *http.Response) error {
|
|
|
|
if 300 <= resp.StatusCode {
|
|
|
|
// Guess it's a GenericError
|
|
|
|
respBody := GenericError{}
|
|
|
|
err := json.NewDecoder(resp.Body).Decode(&respBody)
|
|
|
|
if err != nil {
|
|
|
|
// It's not a GenericError
|
|
|
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
|
|
|
return errors.New(string(bodyBytes))
|
|
|
|
}
|
|
|
|
return &respBody
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-08-02 17:21:29 +00:00
|
|
|
// Do an http request
|
2019-08-02 02:48:38 +00:00
|
|
|
func (n *PortainerClient) do(uri, method string, request io.Reader, requestType string, headers http.Header) (resp *http.Response, err error) {
|
|
|
|
requestUrl, err := n.url.Parse(uri)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest(method, requestUrl.String(), request)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if headers != nil {
|
|
|
|
req.Header = headers
|
|
|
|
}
|
|
|
|
|
|
|
|
if request != nil {
|
|
|
|
req.Header.Set("Content-Type", requestType)
|
|
|
|
}
|
|
|
|
|
2019-08-02 18:21:50 +00:00
|
|
|
if !n.doNotUseToken {
|
|
|
|
if n.token == "" {
|
2019-08-02 20:22:58 +00:00
|
|
|
clientClone := n.Clone()
|
2019-08-02 18:21:50 +00:00
|
|
|
clientClone.doNotUseToken = true
|
|
|
|
n.token, err = clientClone.Authenticate()
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
PrintDebug(fmt.Sprintf("Auth token: %s", n.token))
|
|
|
|
}
|
2019-08-02 02:48:38 +00:00
|
|
|
req.Header.Set("Authorization", "Bearer "+n.token)
|
|
|
|
}
|
|
|
|
|
|
|
|
PrintDebugRequest("Request", req)
|
|
|
|
|
|
|
|
resp, err = n.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = checkResponseForErrors(resp)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
PrintDebugResponse("Response", resp)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-08-02 17:21:29 +00:00
|
|
|
// Do a JSON http request
|
2019-08-02 17:12:25 +00:00
|
|
|
func (n *PortainerClient) doJSON(uri, method string, request interface{}, response interface{}) error {
|
2019-08-02 02:48:38 +00:00
|
|
|
var body io.Reader
|
|
|
|
|
|
|
|
if request != nil {
|
|
|
|
reqBodyBytes, err := json.Marshal(request)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
body = bytes.NewReader(reqBodyBytes)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := n.do(uri, method, body, "application/json", nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if response != nil {
|
|
|
|
d := json.NewDecoder(resp.Body)
|
|
|
|
err := d.Decode(response)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-08-02 17:21:29 +00:00
|
|
|
// Authenticate a user to get an auth token
|
2019-08-02 18:21:50 +00:00
|
|
|
func (n *PortainerClient) Authenticate() (token string, err error) {
|
2019-08-02 02:48:38 +00:00
|
|
|
PrintVerbose("Getting auth token...")
|
|
|
|
|
|
|
|
reqBody := AuthenticateUserRequest{
|
2019-08-02 18:21:50 +00:00
|
|
|
Username: n.user,
|
|
|
|
Password: n.password,
|
2019-08-02 02:48:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
respBody := AuthenticateUserResponse{}
|
|
|
|
|
2019-08-02 17:12:25 +00:00
|
|
|
err = n.doJSON("auth", http.MethodPost, &reqBody, &respBody)
|
2019-08-02 02:48:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
token = respBody.Jwt
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-08-02 17:10:59 +00:00
|
|
|
// Get endpoints
|
|
|
|
func (n *PortainerClient) GetEndpoints() (endpoints []EndpointSubset, err error) {
|
|
|
|
PrintVerbose("Getting endpoints...")
|
2019-08-02 17:12:25 +00:00
|
|
|
err = n.doJSON("endpoints", http.MethodGet, nil, &endpoints)
|
2019-08-02 17:10:59 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get stacks, optionally filtered by swarmId and endpointId
|
|
|
|
func (n *PortainerClient) GetStacks(swarmId string, endpointId uint32) (stacks []Stack, err error) {
|
|
|
|
PrintVerbose("Getting stacks...")
|
|
|
|
|
|
|
|
filter := StackListFilter{
|
|
|
|
SwarmId: swarmId,
|
|
|
|
EndpointId: endpointId,
|
|
|
|
}
|
|
|
|
|
|
|
|
filterJsonBytes, _ := json.Marshal(filter)
|
|
|
|
filterJsonString := string(filterJsonBytes)
|
|
|
|
|
2019-08-02 17:12:25 +00:00
|
|
|
err = n.doJSON(fmt.Sprintf("stacks?filters=%s", filterJsonString), http.MethodGet, nil, &stacks)
|
2019-08-02 17:10:59 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create swarm stack
|
|
|
|
func (n *PortainerClient) CreateSwarmStack(stackName string, environmentVariables []StackEnv, stackFileContent string, swarmClusterId string, endpointId string) (err error) {
|
|
|
|
PrintVerbose("Deploying stack...")
|
|
|
|
|
|
|
|
reqBody := StackCreateRequest{
|
|
|
|
Name: stackName,
|
|
|
|
Env: environmentVariables,
|
|
|
|
SwarmID: swarmClusterId,
|
|
|
|
StackFileContent: stackFileContent,
|
|
|
|
}
|
|
|
|
|
2019-08-02 17:12:25 +00:00
|
|
|
err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%s", 1, "string", endpointId), http.MethodPost, &reqBody, nil)
|
2019-08-02 17:10:59 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create compose stack
|
|
|
|
func (n *PortainerClient) CreateComposeStack(stackName string, environmentVariables []StackEnv, stackFileContent string, endpointId string) (err error) {
|
|
|
|
PrintVerbose("Deploying stack...")
|
|
|
|
|
|
|
|
reqBody := StackCreateRequest{
|
|
|
|
Name: stackName,
|
|
|
|
Env: environmentVariables,
|
|
|
|
StackFileContent: stackFileContent,
|
|
|
|
}
|
|
|
|
|
2019-08-02 17:12:25 +00:00
|
|
|
err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%s", 2, "string", endpointId), http.MethodPost, &reqBody, nil)
|
2019-08-02 17:10:59 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update stack
|
|
|
|
func (n *PortainerClient) UpdateStack(stack Stack, environmentVariables []StackEnv, stackFileContent string, prune bool, endpointId string) (err error) {
|
|
|
|
PrintVerbose("Updating stack...")
|
|
|
|
|
|
|
|
reqBody := StackUpdateRequest{
|
|
|
|
Env: environmentVariables,
|
|
|
|
StackFileContent: stackFileContent,
|
|
|
|
Prune: prune,
|
|
|
|
}
|
|
|
|
|
2019-08-02 17:12:25 +00:00
|
|
|
err = n.doJSON(fmt.Sprintf("stacks/%v?endpointId=%s", stack.Id, endpointId), http.MethodPut, &reqBody, nil)
|
2019-08-02 17:10:59 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete stack
|
|
|
|
func (n *PortainerClient) DeleteStack(stackId uint32) (err error) {
|
|
|
|
PrintVerbose("Deleting stack...")
|
|
|
|
|
2019-08-02 17:12:25 +00:00
|
|
|
err = n.doJSON(fmt.Sprintf("stacks/%d", stackId), http.MethodDelete, nil, nil)
|
2019-08-02 17:10:59 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get stack file content
|
|
|
|
func (n *PortainerClient) GetStackFileContent(stackId uint32) (content string, err error) {
|
|
|
|
PrintVerbose("Getting stack file content...")
|
|
|
|
|
|
|
|
var respBody StackFileInspectResponse
|
|
|
|
|
2019-08-02 17:12:25 +00:00
|
|
|
err = n.doJSON(fmt.Sprintf("stacks/%v/file", stackId), http.MethodGet, nil, &respBody)
|
2019-08-02 17:10:59 +00:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
content = respBody.StackFileContent
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get endpoint Docker info
|
|
|
|
func (n *PortainerClient) GetEndpointDockerInfo(endpointId string) (info map[string]interface{}, err error) {
|
|
|
|
PrintVerbose("Getting endpoint Docker info...")
|
|
|
|
|
2019-08-02 17:12:25 +00:00
|
|
|
err = n.doJSON(fmt.Sprintf("endpoints/%v/docker/info", endpointId), http.MethodGet, nil, &info)
|
2019-08-02 17:10:59 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get Portainer status info
|
|
|
|
func (n *PortainerClient) GetStatus() (status Status, err error) {
|
2019-08-02 17:12:25 +00:00
|
|
|
err = n.doJSON("status", http.MethodGet, nil, &status)
|
2019-08-02 17:10:59 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-08-02 18:21:50 +00:00
|
|
|
// Get a clone of the client
|
2019-08-02 20:22:58 +00:00
|
|
|
func (n *PortainerClient) Clone() (c *PortainerClient) {
|
2019-08-02 18:21:50 +00:00
|
|
|
c = &PortainerClient{
|
|
|
|
url: n.url,
|
|
|
|
user: n.user,
|
|
|
|
password: n.password,
|
|
|
|
token: n.token,
|
|
|
|
doNotUseToken: n.doNotUseToken,
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Timeout = n.Timeout
|
|
|
|
|
|
|
|
c.Transport = n.Transport
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-08-02 17:21:29 +00:00
|
|
|
// Create a new client
|
2019-08-02 18:21:50 +00:00
|
|
|
func NewClient(config ClientConfig) (c *PortainerClient, err error) {
|
2019-08-02 20:09:54 +00:00
|
|
|
apiUrl, err := url.Parse(strings.TrimRight(config.Url, "/") + "/api/")
|
2019-08-02 02:48:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c = &PortainerClient{
|
2019-08-02 18:21:50 +00:00
|
|
|
url: apiUrl,
|
|
|
|
user: config.User,
|
|
|
|
password: config.Password,
|
|
|
|
token: config.Token,
|
2019-08-02 02:48:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
c.Timeout = config.Timeout
|
|
|
|
|
|
|
|
c.Transport = &http.Transport{
|
|
|
|
TLSClientConfig: &tls.Config{
|
|
|
|
InsecureSkipVerify: config.Insecure,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-08-02 17:21:29 +00:00
|
|
|
// Get the cached client or a new one
|
2019-08-02 02:48:38 +00:00
|
|
|
func GetClient() (c *PortainerClient, err error) {
|
|
|
|
if client == nil {
|
2019-08-02 18:21:50 +00:00
|
|
|
client, err = NewClient(ClientConfig{
|
2019-08-02 02:48:38 +00:00
|
|
|
Url: viper.GetString("url"),
|
|
|
|
User: viper.GetString("user"),
|
|
|
|
Password: viper.GetString("password"),
|
|
|
|
Token: viper.GetString("auth-token"),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
c = client
|
|
|
|
return
|
|
|
|
}
|