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" import "fmt"

View File

@ -2,12 +2,13 @@ package cmd
import ( import (
"fmt" "fmt"
"github.com/greenled/portainer-stack-utils/common" "log"
"os"
"github.com/greenled/portainer-stack-utils/util"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"log"
"os"
) )
// configCmd represents the config command // configCmd represents the config command
@ -32,12 +33,12 @@ var configCmd = &cobra.Command{
if len(args) == 1 { if len(args) == 1 {
// Get config // Get config
value, configGettingErr := getConfig(args[0]) value, configGettingErr := getConfig(args[0])
common.CheckError(configGettingErr) util.CheckError(configGettingErr)
fmt.Println(value) fmt.Println(value)
} else { } else {
// Set config // Set config
configSettingErr := setConfig(args[0], args[1]) 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 // Read config from file
if configReadingErr := newViper.ReadInConfig(); configReadingErr != nil { 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 return newViper, nil

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,299 +1,18 @@
package common package common
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http" "net/http"
"net/url"
"strings" "github.com/greenled/portainer-stack-utils/client"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var cachedClient PortainerClient var cachedClient client.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
}
// Get the cached client or a new one // Get the cached client or a new one
func GetClient() (c PortainerClient, err error) { func GetClient() (c client.PortainerClient, err error) {
if cachedClient == nil { if cachedClient == nil {
cachedClient, err = GetDefaultClient() cachedClient, err = GetDefaultClient()
if err != nil { if err != nil {
@ -304,13 +23,13 @@ func GetClient() (c PortainerClient, err error) {
} }
// Get the default client // Get the default client
func GetDefaultClient() (c PortainerClient, err error) { func GetDefaultClient() (c client.PortainerClient, err error) {
return NewClient(GetDefaultHttpClient(), GetDefaultClientConfig()) return client.NewClient(GetDefaultHttpClient(), GetDefaultClientConfig())
} }
// Get the default config for a client // Get the default config for a client
func GetDefaultClientConfig() ClientConfig { func GetDefaultClientConfig() client.ClientConfig {
return ClientConfig{ return client.ClientConfig{
Url: viper.GetString("url"), Url: viper.GetString("url"),
User: viper.GetString("user"), User: viper.GetString("user"),
Password: viper.GetString("password"), Password: viper.GetString("password"),

View File

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

View File

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

View File

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