Split common package into common, client, and util packages

This commit is contained in:
Juan Carlos Mejías Rodríguez 2019-08-02 20:25:22 -04:00
parent a3b58499b8
commit 9c79379191
14 changed files with 400 additions and 369 deletions

295
client/client.go Normal file
View File

@ -0,0 +1,295 @@
package client
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/greenled/portainer-stack-utils/util"
)
type StackListFilter struct {
SwarmId string `json:",omitempty"`
EndpointId uint32 `json:",omitempty"`
}
type ClientConfig struct {
Url string
User string
Password string
Token string
DoNotUseToken bool
}
type PortainerClient interface {
Authenticate() (token string, err error)
GetEndpoints() ([]EndpointSubset, error)
GetStacks(swarmId string, endpointId uint32) ([]Stack, error)
CreateSwarmStack(stackName string, environmentVariables []StackEnv, stackFileContent string, swarmClusterId string, endpointId string) error
CreateComposeStack(stackName string, environmentVariables []StackEnv, stackFileContent string, endpointId string) error
UpdateStack(stack Stack, environmentVariables []StackEnv, stackFileContent string, prune bool, endpointId string) error
DeleteStack(stackId uint32) error
GetStackFileContent(stackId uint32) (content string, err error)
GetEndpointDockerInfo(endpointId string) (info map[string]interface{}, err error)
GetStatus() (Status, error)
}
type PortainerClientImp 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 *PortainerClientImp) 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 == "" {
n.token, err = n.Authenticate()
if err != nil {
return
}
util.PrintDebug(fmt.Sprintf("Auth token: %s", n.token))
}
req.Header.Set("Authorization", "Bearer "+n.token)
}
util.PrintDebugRequest("Request", req)
resp, err = n.httpClient.Do(req)
if err != nil {
return
}
err = checkResponseForErrors(resp)
if err != nil {
return
}
util.PrintDebugResponse("Response", resp)
return
}
// Do a JSON http request
func (n *PortainerClientImp) 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 *PortainerClientImp) Authenticate() (token string, err error) {
util.PrintVerbose("Getting auth token...")
reqBody := AuthenticateUserRequest{
Username: n.user,
Password: n.password,
}
respBody := AuthenticateUserResponse{}
previousDoNotUseTokenValue := n.doNotUseToken
n.doNotUseToken = true
err = n.doJSON("auth", http.MethodPost, &reqBody, &respBody)
if err != nil {
return
}
n.doNotUseToken = previousDoNotUseTokenValue
token = respBody.Jwt
return
}
// Get endpoints
func (n *PortainerClientImp) GetEndpoints() (endpoints []EndpointSubset, err error) {
util.PrintVerbose("Getting endpoints...")
err = n.doJSON("endpoints", http.MethodGet, nil, &endpoints)
return
}
// Get stacks, optionally filtered by swarmId and endpointId
func (n *PortainerClientImp) GetStacks(swarmId string, endpointId uint32) (stacks []Stack, err error) {
util.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 *PortainerClientImp) CreateSwarmStack(stackName string, environmentVariables []StackEnv, stackFileContent string, swarmClusterId string, endpointId string) (err error) {
util.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 *PortainerClientImp) CreateComposeStack(stackName string, environmentVariables []StackEnv, stackFileContent string, endpointId string) (err error) {
util.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 *PortainerClientImp) UpdateStack(stack Stack, environmentVariables []StackEnv, stackFileContent string, prune bool, endpointId string) (err error) {
util.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 *PortainerClientImp) DeleteStack(stackId uint32) (err error) {
util.PrintVerbose("Deleting stack...")
err = n.doJSON(fmt.Sprintf("stacks/%d", stackId), http.MethodDelete, nil, nil)
return
}
// Get stack file content
func (n *PortainerClientImp) GetStackFileContent(stackId uint32) (content string, err error) {
util.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 *PortainerClientImp) GetEndpointDockerInfo(endpointId string) (info map[string]interface{}, err error) {
util.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 *PortainerClientImp) GetStatus() (status Status, err error) {
err = n.doJSON("status", http.MethodGet, nil, &status)
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 = &PortainerClientImp{
httpClient: httpClient,
url: apiUrl,
user: config.User,
password: config.Password,
token: config.Token,
}
return
}

View File

@ -1,4 +1,4 @@
package common
package client
import "fmt"

View File

@ -2,12 +2,13 @@ package cmd
import (
"fmt"
"github.com/greenled/portainer-stack-utils/common"
"log"
"os"
"github.com/greenled/portainer-stack-utils/util"
"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"log"
"os"
)
// configCmd represents the config command
@ -32,12 +33,12 @@ var configCmd = &cobra.Command{
if len(args) == 1 {
// Get config
value, configGettingErr := getConfig(args[0])
common.CheckError(configGettingErr)
util.CheckError(configGettingErr)
fmt.Println(value)
} else {
// Set config
configSettingErr := setConfig(args[0], args[1])
common.CheckError(configSettingErr)
util.CheckError(configSettingErr)
}
},
}
@ -67,7 +68,7 @@ func loadCofig() (*viper.Viper, error) {
// Read config from file
if configReadingErr := newViper.ReadInConfig(); configReadingErr != nil {
common.PrintVerbose(fmt.Sprintf("Could not read configuration from \"%s\". Expect all configuration values to be unset.", configFile))
util.PrintVerbose(fmt.Sprintf("Could not read configuration from \"%s\". Expect all configuration values to be unset.", configFile))
}
return newViper, nil

View File

@ -21,6 +21,8 @@ import (
"os"
"text/template"
"github.com/greenled/portainer-stack-utils/util"
"github.com/greenled/portainer-stack-utils/common"
"github.com/spf13/viper"
@ -34,23 +36,23 @@ var endpointListCmd = &cobra.Command{
Aliases: []string{"ls"},
Run: func(cmd *cobra.Command, args []string) {
client, err := common.GetClient()
common.CheckError(err)
util.CheckError(err)
endpoints, err := client.GetEndpoints()
common.CheckError(err)
util.CheckError(err)
if viper.GetString("endpoint.list.format") != "" {
// Print endpoint fields formatted
template, templateParsingErr := template.New("endpointTpl").Parse(viper.GetString("endpoint.list.format"))
common.CheckError(templateParsingErr)
util.CheckError(templateParsingErr)
for _, e := range endpoints {
templateExecutionErr := template.Execute(os.Stdout, e)
common.CheckError(templateExecutionErr)
util.CheckError(templateExecutionErr)
fmt.Println()
}
} else {
// Print all endpoint fields as a table
writer, err := common.NewTabWriter([]string{
writer, err := util.NewTabWriter([]string{
"ENDPOINT ID",
"NAME",
"TYPE",
@ -58,7 +60,7 @@ var endpointListCmd = &cobra.Command{
"PUBLIC URL",
"GROUP ID",
})
common.CheckError(err)
util.CheckError(err)
for _, e := range endpoints {
var endpointType string
if e.Type == 1 {
@ -75,10 +77,10 @@ var endpointListCmd = &cobra.Command{
e.PublicURL,
e.GroupID,
))
common.CheckError(err)
util.CheckError(err)
}
flushErr := writer.Flush()
common.CheckError(flushErr)
util.CheckError(flushErr)
}
},
}

View File

@ -3,6 +3,8 @@ package cmd
import (
"fmt"
"github.com/greenled/portainer-stack-utils/util"
"github.com/greenled/portainer-stack-utils/common"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -15,10 +17,10 @@ var loginCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
// Get auth token
client, err := common.GetDefaultClient()
common.CheckError(err)
util.CheckError(err)
authToken, err := client.Authenticate()
common.CheckError(err)
util.CheckError(err)
if viper.GetBool("login.print") {
fmt.Println(authToken)
@ -26,7 +28,7 @@ var loginCmd = &cobra.Command{
// Save auth token
configSettingErr := setConfig("auth-token", authToken)
common.CheckError(configSettingErr)
util.CheckError(configSettingErr)
},
}

View File

@ -2,12 +2,14 @@ package cmd
import (
"fmt"
"os"
"strings"
"github.com/greenled/portainer-stack-utils/common"
"github.com/greenled/portainer-stack-utils/util"
"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"os"
"strings"
)
var cfgFile string
@ -83,6 +85,6 @@ func initConfig() {
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
common.PrintVerbose("Using config file:", viper.ConfigFileUsed())
util.PrintVerbose("Using config file:", viper.ConfigFileUsed())
}
}

View File

@ -5,6 +5,10 @@ import (
"io/ioutil"
"log"
"github.com/greenled/portainer-stack-utils/util"
"github.com/greenled/portainer-stack-utils/client"
"github.com/greenled/portainer-stack-utils/common"
"github.com/joho/godotenv"
"github.com/spf13/cobra"
@ -19,35 +23,35 @@ var stackDeployCmd = &cobra.Command{
Example: "psu stack deploy mystack --stack-file mystack.yml",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var loadedEnvironmentVariables []common.StackEnv
var loadedEnvironmentVariables []client.StackEnv
if viper.GetString("stack.deploy.env-file") != "" {
var loadingErr error
loadedEnvironmentVariables, loadingErr = loadEnvironmentVariablesFile(viper.GetString("stack.deploy.env-file"))
common.CheckError(loadingErr)
util.CheckError(loadingErr)
}
client, clientRetrievalErr := common.GetClient()
common.CheckError(clientRetrievalErr)
portainerClient, clientRetrievalErr := common.GetClient()
util.CheckError(clientRetrievalErr)
stackName := args[0]
retrievedStack, stackRetrievalErr := common.GetStackByName(stackName)
switch stackRetrievalErr.(type) {
case nil:
// We are updating an existing stack
common.PrintVerbose(fmt.Sprintf("Stack %s found. Updating...", retrievedStack.Name))
util.PrintVerbose(fmt.Sprintf("Stack %s found. Updating...", retrievedStack.Name))
var stackFileContent string
if viper.GetString("stack.deploy.stack-file") != "" {
var loadingErr error
stackFileContent, loadingErr = loadStackFile(viper.GetString("stack.deploy.stack-file"))
common.CheckError(loadingErr)
util.CheckError(loadingErr)
} else {
var stackFileContentRetrievalErr error
stackFileContent, stackFileContentRetrievalErr = client.GetStackFileContent(retrievedStack.Id)
common.CheckError(stackFileContentRetrievalErr)
stackFileContent, stackFileContentRetrievalErr = portainerClient.GetStackFileContent(retrievedStack.Id)
util.CheckError(stackFileContentRetrievalErr)
}
var newEnvironmentVariables []common.StackEnv
var newEnvironmentVariables []client.StackEnv
if viper.GetBool("stack.deploy.replace-env") {
newEnvironmentVariables = loadedEnvironmentVariables
} else {
@ -61,44 +65,44 @@ var stackDeployCmd = &cobra.Command{
continue LoadedVariablesLoop
}
}
newEnvironmentVariables = append(newEnvironmentVariables, common.StackEnv{
newEnvironmentVariables = append(newEnvironmentVariables, client.StackEnv{
Name: loadedEnvironmentVariable.Name,
Value: loadedEnvironmentVariable.Value,
})
}
}
err := client.UpdateStack(retrievedStack, newEnvironmentVariables, stackFileContent, viper.GetBool("stack.deploy.prune"), viper.GetString("stack.deploy.endpoint"))
common.CheckError(err)
err := portainerClient.UpdateStack(retrievedStack, newEnvironmentVariables, stackFileContent, viper.GetBool("stack.deploy.prune"), viper.GetString("stack.deploy.endpoint"))
util.CheckError(err)
case *common.StackNotFoundError:
// We are deploying a new stack
common.PrintVerbose(fmt.Sprintf("Stack %s not found. Deploying...", stackName))
util.PrintVerbose(fmt.Sprintf("Stack %s not found. Deploying...", stackName))
if viper.GetString("stack.deploy.stack-file") == "" {
log.Fatalln("Specify a docker-compose file with --stack-file")
}
stackFileContent, loadingErr := loadStackFile(viper.GetString("stack.deploy.stack-file"))
common.CheckError(loadingErr)
util.CheckError(loadingErr)
swarmClusterId, selectionErr := getSwarmClusterId()
switch selectionErr.(type) {
case nil:
// It's a swarm cluster
common.PrintVerbose(fmt.Sprintf("Swarm cluster found with id %s", swarmClusterId))
deploymentErr := client.CreateSwarmStack(stackName, loadedEnvironmentVariables, stackFileContent, swarmClusterId, viper.GetString("stack.deploy.endpoint"))
common.CheckError(deploymentErr)
util.PrintVerbose(fmt.Sprintf("Swarm cluster found with id %s", swarmClusterId))
deploymentErr := portainerClient.CreateSwarmStack(stackName, loadedEnvironmentVariables, stackFileContent, swarmClusterId, viper.GetString("stack.deploy.endpoint"))
util.CheckError(deploymentErr)
case *valueNotFoundError:
// It's not a swarm cluster
common.PrintVerbose("Swarm cluster not found")
deploymentErr := client.CreateComposeStack(stackName, loadedEnvironmentVariables, stackFileContent, viper.GetString("stack.deploy.endpoint"))
common.CheckError(deploymentErr)
util.PrintVerbose("Swarm cluster not found")
deploymentErr := portainerClient.CreateComposeStack(stackName, loadedEnvironmentVariables, stackFileContent, viper.GetString("stack.deploy.endpoint"))
util.CheckError(deploymentErr)
default:
// Something else happened
common.CheckError(stackRetrievalErr)
util.CheckError(stackRetrievalErr)
}
default:
// Something else happened
common.CheckError(stackRetrievalErr)
util.CheckError(stackRetrievalErr)
}
},
}
@ -160,15 +164,15 @@ func loadStackFile(path string) (string, error) {
}
// Load environment variables
func loadEnvironmentVariablesFile(path string) ([]common.StackEnv, error) {
var variables []common.StackEnv
func loadEnvironmentVariablesFile(path string) ([]client.StackEnv, error) {
var variables []client.StackEnv
variablesMap, readingErr := godotenv.Read(path)
if readingErr != nil {
return []common.StackEnv{}, readingErr
return []client.StackEnv{}, readingErr
}
for key, value := range variablesMap {
variables = append(variables, common.StackEnv{
variables = append(variables, client.StackEnv{
Name: key,
Value: value,
})

View File

@ -5,6 +5,8 @@ import (
"os"
"text/template"
"github.com/greenled/portainer-stack-utils/util"
"github.com/greenled/portainer-stack-utils/common"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -18,29 +20,29 @@ var stackListCmd = &cobra.Command{
Example: "psu stack list --endpoint 1",
Run: func(cmd *cobra.Command, args []string) {
client, err := common.GetClient()
common.CheckError(err)
util.CheckError(err)
stacks, err := client.GetStacks(viper.GetString("stack.list.swarm"), viper.GetUint32("stack.list.endpoint"))
common.CheckError(err)
util.CheckError(err)
if viper.GetBool("stack.list.quiet") {
// Print only stack names
for _, s := range stacks {
_, err := fmt.Println(s.Name)
common.CheckError(err)
util.CheckError(err)
}
} else if viper.GetString("stack.list.format") != "" {
// Print stack fields formatted
template, templateParsingErr := template.New("stackTpl").Parse(viper.GetString("stack.list.format"))
common.CheckError(templateParsingErr)
util.CheckError(templateParsingErr)
for _, s := range stacks {
templateExecutionErr := template.Execute(os.Stdout, s)
common.CheckError(templateExecutionErr)
util.CheckError(templateExecutionErr)
fmt.Println()
}
} else {
// Print all stack fields as a table
writer, err := common.NewTabWriter([]string{
writer, err := util.NewTabWriter([]string{
"STACK ID",
"NAME",
"TYPE",
@ -49,7 +51,7 @@ var stackListCmd = &cobra.Command{
"ENDPOINT ID",
"SWARM ID",
})
common.CheckError(err)
util.CheckError(err)
for _, s := range stacks {
_, err := fmt.Fprintln(writer, fmt.Sprintf(
"%v\t%s\t%v\t%s\t%s\t%v\t%s",
@ -61,10 +63,10 @@ var stackListCmd = &cobra.Command{
s.EndpointID,
s.SwarmID,
))
common.CheckError(err)
util.CheckError(err)
}
flushErr := writer.Flush()
common.CheckError(flushErr)
util.CheckError(flushErr)
}
},
}

View File

@ -4,6 +4,8 @@ import (
"fmt"
"log"
"github.com/greenled/portainer-stack-utils/util"
"github.com/greenled/portainer-stack-utils/common"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -23,26 +25,26 @@ var stackRemoveCmd = &cobra.Command{
switch err.(type) {
case nil:
// The stack exists
common.PrintVerbose(fmt.Sprintf("Stack %s exists.", stackName))
util.PrintVerbose(fmt.Sprintf("Stack %s exists.", stackName))
stackId := stack.Id
common.PrintVerbose(fmt.Sprintf("Removing stack %s...", stackName))
util.PrintVerbose(fmt.Sprintf("Removing stack %s...", stackName))
client, err := common.GetClient()
common.CheckError(err)
util.CheckError(err)
err = client.DeleteStack(stackId)
common.CheckError(err)
util.CheckError(err)
case *common.StackNotFoundError:
// The stack does not exist
common.PrintVerbose(fmt.Sprintf("Stack %s does not exist.", stackName))
util.PrintVerbose(fmt.Sprintf("Stack %s does not exist.", stackName))
if viper.GetBool("stack.remove.strict") {
log.Fatalln(fmt.Sprintf("Stack %s does not exist.", stackName))
}
default:
// Something else happened
common.CheckError(err)
util.CheckError(err)
}
},
}

View File

@ -5,6 +5,8 @@ import (
"os"
"text/template"
"github.com/greenled/portainer-stack-utils/util"
"github.com/greenled/portainer-stack-utils/common"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -16,27 +18,27 @@ var statusCmd = &cobra.Command{
Short: "Check Portainer status",
Run: func(cmd *cobra.Command, args []string) {
client, err := common.GetClient()
common.CheckError(err)
util.CheckError(err)
respBody, err := client.GetStatus()
common.CheckError(err)
util.CheckError(err)
if viper.GetString("status.format") != "" {
// Print stack fields formatted
template, templateParsingErr := template.New("statusTpl").Parse(viper.GetString("status.format"))
common.CheckError(templateParsingErr)
util.CheckError(templateParsingErr)
templateExecutionErr := template.Execute(os.Stdout, respBody)
common.CheckError(templateExecutionErr)
util.CheckError(templateExecutionErr)
fmt.Println()
} else {
// Print status fields as a table
writer, newTabWriterErr := common.NewTabWriter([]string{
writer, newTabWriterErr := util.NewTabWriter([]string{
"VERSION",
"AUTHENTICATION",
"ENDPOINT MANAGEMENT",
"ANALYTICS",
})
common.CheckError(newTabWriterErr)
util.CheckError(newTabWriterErr)
_, printingErr := fmt.Fprintln(writer, fmt.Sprintf(
"%s\t%v\t%v\t%v",
@ -45,10 +47,10 @@ var statusCmd = &cobra.Command{
respBody.EndpointManagement,
respBody.Analytics,
))
common.CheckError(printingErr)
util.CheckError(printingErr)
flushErr := writer.Flush()
common.CheckError(flushErr)
util.CheckError(flushErr)
}
},
}

View File

@ -1,299 +1,18 @@
package common
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/greenled/portainer-stack-utils/client"
"github.com/spf13/viper"
)
var cachedClient PortainerClient
type ClientConfig struct {
Url string
User string
Password string
Token string
DoNotUseToken bool
}
type PortainerClient interface {
Authenticate() (token string, err error)
GetEndpoints() ([]EndpointSubset, error)
GetStacks(swarmId string, endpointId uint32) ([]Stack, error)
CreateSwarmStack(stackName string, environmentVariables []StackEnv, stackFileContent string, swarmClusterId string, endpointId string) error
CreateComposeStack(stackName string, environmentVariables []StackEnv, stackFileContent string, endpointId string) error
UpdateStack(stack Stack, environmentVariables []StackEnv, stackFileContent string, prune bool, endpointId string) error
DeleteStack(stackId uint32) error
GetStackFileContent(stackId uint32) (content string, err error)
GetEndpointDockerInfo(endpointId string) (info map[string]interface{}, err error)
GetStatus() (Status, error)
}
type PortainerClientImp 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 *PortainerClientImp) 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 == "" {
n.token, err = n.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 *PortainerClientImp) 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 *PortainerClientImp) Authenticate() (token string, err error) {
PrintVerbose("Getting auth token...")
reqBody := AuthenticateUserRequest{
Username: n.user,
Password: n.password,
}
respBody := AuthenticateUserResponse{}
previousDoNotUseTokenValue := n.doNotUseToken
n.doNotUseToken = true
err = n.doJSON("auth", http.MethodPost, &reqBody, &respBody)
if err != nil {
return
}
n.doNotUseToken = previousDoNotUseTokenValue
token = respBody.Jwt
return
}
// Get endpoints
func (n *PortainerClientImp) 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 *PortainerClientImp) 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 *PortainerClientImp) 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 *PortainerClientImp) 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 *PortainerClientImp) 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 *PortainerClientImp) 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 *PortainerClientImp) 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 *PortainerClientImp) 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 *PortainerClientImp) GetStatus() (status Status, err error) {
err = n.doJSON("status", http.MethodGet, nil, &status)
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 = &PortainerClientImp{
httpClient: httpClient,
url: apiUrl,
user: config.User,
password: config.Password,
token: config.Token,
}
return
}
var cachedClient client.PortainerClient
// Get the cached client or a new one
func GetClient() (c PortainerClient, err error) {
func GetClient() (c client.PortainerClient, err error) {
if cachedClient == nil {
cachedClient, err = GetDefaultClient()
if err != nil {
@ -304,13 +23,13 @@ func GetClient() (c PortainerClient, err error) {
}
// Get the default client
func GetDefaultClient() (c PortainerClient, err error) {
return NewClient(GetDefaultHttpClient(), GetDefaultClientConfig())
func GetDefaultClient() (c client.PortainerClient, err error) {
return client.NewClient(GetDefaultHttpClient(), GetDefaultClientConfig())
}
// Get the default config for a client
func GetDefaultClientConfig() ClientConfig {
return ClientConfig{
func GetDefaultClientConfig() client.ClientConfig {
return client.ClientConfig{
Url: viper.GetString("url"),
User: viper.GetString("user"),
Password: viper.GetString("password"),

View File

@ -2,9 +2,13 @@ package common
import (
"fmt"
"github.com/greenled/portainer-stack-utils/util"
"github.com/greenled/portainer-stack-utils/client"
)
func GetStackByName(name string) (stack Stack, err error) {
func GetStackByName(name string) (stack client.Stack, err error) {
client, err := GetClient()
if err != nil {
return
@ -15,7 +19,7 @@ func GetStackByName(name string) (stack Stack, err error) {
return
}
PrintVerbose(fmt.Sprintf("Getting stack %s...", name))
util.PrintVerbose(fmt.Sprintf("Getting stack %s...", name))
for _, stack := range stacks {
if stack.Name == name {
return stack, nil
@ -27,11 +31,6 @@ func GetStackByName(name string) (stack Stack, err error) {
return
}
type StackListFilter struct {
SwarmId string `json:",omitempty"`
EndpointId uint32 `json:",omitempty"`
}
// Custom customerrors
type StackNotFoundError struct {
StackName string

View File

@ -1,4 +1,4 @@
package common
package util
import (
"fmt"

View File

@ -1,15 +1,16 @@
package common
package util
import (
"bytes"
"fmt"
"github.com/spf13/viper"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"text/tabwriter"
"github.com/spf13/viper"
)
func PrintVerbose(a ...interface{}) {