psu/common/client.go
2019-08-08 11:56:07 -04:00

331 lines
7.7 KiB
Go

package common
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/spf13/viper"
)
var cachedClient *PortainerClient
type ClientConfig struct {
Url string
User string
Password string
Token string
DoNotUseToken bool
}
type PortainerClient struct {
httpClient *http.Client
url *url.URL
user string
password string
token string
doNotUseToken bool
}
// Check if an http.Response object has errors
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
}
// Do an http request
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)
}
if !n.doNotUseToken {
if n.token == "" {
clientClone := n.Clone()
clientClone.doNotUseToken = true
n.token, err = clientClone.Authenticate()
if err != nil {
return
}
PrintDebug(fmt.Sprintf("Auth token: %s", n.token))
}
req.Header.Set("Authorization", "Bearer "+n.token)
}
PrintDebugRequest("Request", req)
resp, err = n.httpClient.Do(req)
if err != nil {
return
}
err = checkResponseForErrors(resp)
if err != nil {
return
}
PrintDebugResponse("Response", resp)
return
}
// Do a JSON http request
func (n *PortainerClient) doJSON(uri, method string, request interface{}, response interface{}) error {
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
}
// Authenticate a user to get an auth token
func (n *PortainerClient) Authenticate() (token string, err error) {
PrintVerbose("Getting auth token...")
reqBody := AuthenticateUserRequest{
Username: n.user,
Password: n.password,
}
respBody := AuthenticateUserResponse{}
err = n.doJSON("auth", http.MethodPost, &reqBody, &respBody)
if err != nil {
return
}
token = respBody.Jwt
return
}
// Get endpoints
func (n *PortainerClient) GetEndpoints() (endpoints []EndpointSubset, err error) {
PrintVerbose("Getting endpoints...")
err = n.doJSON("endpoints", http.MethodGet, nil, &endpoints)
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)
err = n.doJSON(fmt.Sprintf("stacks?filters=%s", filterJsonString), http.MethodGet, nil, &stacks)
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,
}
err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%s", 1, "string", endpointId), http.MethodPost, &reqBody, nil)
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,
}
err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%s", 2, "string", endpointId), http.MethodPost, &reqBody, nil)
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,
}
err = n.doJSON(fmt.Sprintf("stacks/%v?endpointId=%s", stack.Id, endpointId), http.MethodPut, &reqBody, nil)
return
}
// Delete stack
func (n *PortainerClient) DeleteStack(stackId uint32) (err error) {
PrintVerbose("Deleting stack...")
err = n.doJSON(fmt.Sprintf("stacks/%d", stackId), http.MethodDelete, nil, nil)
return
}
// Get stack file content
func (n *PortainerClient) GetStackFileContent(stackId uint32) (content string, err error) {
PrintVerbose("Getting stack file content...")
var respBody StackFileInspectResponse
err = n.doJSON(fmt.Sprintf("stacks/%v/file", stackId), http.MethodGet, nil, &respBody)
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...")
err = n.doJSON(fmt.Sprintf("endpoints/%v/docker/info", endpointId), http.MethodGet, nil, &info)
return
}
// Get Portainer status info
func (n *PortainerClient) GetStatus() (status Status, err error) {
err = n.doJSON("status", http.MethodGet, nil, &status)
return
}
// Get a clone of the client
func (n *PortainerClient) Clone() (c *PortainerClient) {
c = &PortainerClient{
httpClient: n.httpClient,
url: n.url,
user: n.user,
password: n.password,
token: n.token,
doNotUseToken: n.doNotUseToken,
}
return
}
// Create a new client
func NewClient(httpClient *http.Client, config ClientConfig) (c *PortainerClient, err error) {
apiUrl, err := url.Parse(strings.TrimRight(config.Url, "/") + "/api/")
if err != nil {
return
}
c = &PortainerClient{
httpClient: httpClient,
url: apiUrl,
user: config.User,
password: config.Password,
token: config.Token,
}
return
}
// Get the cached client or a new one
func GetClient() (c *PortainerClient, err error) {
if cachedClient == nil {
cachedClient, err = GetDefaultClient()
if err != nil {
return
}
}
return cachedClient, nil
}
// Get the default client
func GetDefaultClient() (c *PortainerClient, err error) {
return NewClient(GetDefaultHttpClient(), GetDefaultClientConfig())
}
// Get the default config for a client
func GetDefaultClientConfig() ClientConfig {
return ClientConfig{
Url: viper.GetString("url"),
User: viper.GetString("user"),
Password: viper.GetString("password"),
Token: viper.GetString("auth-token"),
DoNotUseToken: false,
}
}
// Get the default http client for a Portainer client
func GetDefaultHttpClient() *http.Client {
return &http.Client{
Timeout: viper.GetDuration("timeout"),
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: viper.GetBool("insecure"),
},
},
}
}