mirror of
https://gitlab.com/psuapp/psu.git
synced 2024-08-30 18:12:34 +00:00
Split common package into common, client, and util packages
This commit is contained in:
parent
a3b58499b8
commit
9c79379191
295
client/client.go
Normal file
295
client/client.go
Normal 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
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package common
|
||||
package client
|
||||
|
||||
import "fmt"
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
297
common/client.go
297
common/client.go
@ -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"),
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
package common
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -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{}) {
|
Loading…
Reference in New Issue
Block a user