psu/cmd/stackDeploy.go
2019-08-08 11:56:07 -04:00

194 lines
6.7 KiB
Go

package cmd
import (
"fmt"
"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"
"github.com/spf13/viper"
)
// stackDeployCmd represents the undeploy command
var stackDeployCmd = &cobra.Command{
Use: "deploy STACK_NAME",
Short: "Deploy a new stack or update an existing one",
Aliases: []string{"up", "create"},
Example: "psu stack deploy mystack --stack-file mystack.yml",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
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)
}
portainerClient, clientRetrievalErr := common.GetClient()
common.CheckError(clientRetrievalErr)
stackName := args[0]
retrievedStack, stackRetrievalErr := common.GetStackByName(stackName)
switch stackRetrievalErr.(type) {
case nil:
// We are updating an existing stack
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)
} else {
var stackFileContentRetrievalErr error
util.PrintVerbose("Getting stack file content...")
stackFileContent, stackFileContentRetrievalErr = portainerClient.GetStackFileContent(retrievedStack.Id)
common.CheckError(stackFileContentRetrievalErr)
}
var newEnvironmentVariables []client.StackEnv
if viper.GetBool("stack.deploy.replace-env") {
newEnvironmentVariables = loadedEnvironmentVariables
} else {
// Merge stack environment variables with the loaded ones
newEnvironmentVariables = retrievedStack.Env
LoadedVariablesLoop:
for _, loadedEnvironmentVariable := range loadedEnvironmentVariables {
for _, newEnvironmentVariable := range newEnvironmentVariables {
if loadedEnvironmentVariable.Name == newEnvironmentVariable.Name {
newEnvironmentVariable.Value = loadedEnvironmentVariable.Value
continue LoadedVariablesLoop
}
}
newEnvironmentVariables = append(newEnvironmentVariables, client.StackEnv{
Name: loadedEnvironmentVariable.Name,
Value: loadedEnvironmentVariable.Value,
})
}
}
util.PrintVerbose("Updating stack...")
err := portainerClient.UpdateStack(retrievedStack, newEnvironmentVariables, stackFileContent, viper.GetBool("stack.deploy.prune"), viper.GetString("stack.deploy.endpoint"))
common.CheckError(err)
case *common.StackNotFoundError:
// We are deploying a new stack
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)
swarmClusterId, selectionErr := getSwarmClusterId()
switch selectionErr.(type) {
case nil:
// It's a swarm cluster
util.PrintVerbose(fmt.Sprintf("Swarm cluster found with id %s", swarmClusterId))
util.PrintVerbose("Deploying stack...")
deploymentErr := portainerClient.CreateSwarmStack(stackName, loadedEnvironmentVariables, stackFileContent, swarmClusterId, viper.GetString("stack.deploy.endpoint"))
common.CheckError(deploymentErr)
case *valueNotFoundError:
// It's not a swarm cluster
util.PrintVerbose("Swarm cluster not found")
util.PrintVerbose("Deploying stack...")
deploymentErr := portainerClient.CreateComposeStack(stackName, loadedEnvironmentVariables, stackFileContent, viper.GetString("stack.deploy.endpoint"))
common.CheckError(deploymentErr)
default:
// Something else happened
common.CheckError(stackRetrievalErr)
}
default:
// Something else happened
common.CheckError(stackRetrievalErr)
}
},
}
func init() {
stackCmd.AddCommand(stackDeployCmd)
stackDeployCmd.Flags().StringP("stack-file", "c", "", "path to a file with the content of the stack")
stackDeployCmd.Flags().String("endpoint", "1", "endpoint ID")
stackDeployCmd.Flags().StringP("env-file", "e", "", "path to a file with environment variables used during stack deployment")
stackDeployCmd.Flags().Bool("replace-env", false, "replace environment variables instead of merging them")
stackDeployCmd.Flags().BoolP("prune", "r", false, "prune services that are no longer referenced (only available for Swarm stacks)")
viper.BindPFlag("stack.deploy.stack-file", stackDeployCmd.Flags().Lookup("stack-file"))
viper.BindPFlag("stack.deploy.endpoint", stackDeployCmd.Flags().Lookup("endpoint"))
viper.BindPFlag("stack.deploy.env-file", stackDeployCmd.Flags().Lookup("env-file"))
viper.BindPFlag("stack.deploy.replace-env", stackDeployCmd.Flags().Lookup("replace-env"))
viper.BindPFlag("stack.deploy.prune", stackDeployCmd.Flags().Lookup("prune"))
}
func getSwarmClusterId() (id string, err error) {
// Get docker information for endpoint
client, err := common.GetClient()
if err != nil {
return
}
util.PrintVerbose("Getting endpoint Docker info...")
result, err := client.GetEndpointDockerInfo(viper.GetString("stack.deploy.endpoint"))
if err != nil {
return
}
// Get swarm (if any) information for endpoint
swarmClusterId, err := selectValue(result, []string{"Swarm", "Cluster", "ID"})
if err != nil {
return
}
id = swarmClusterId.(string)
return
}
func selectValue(jsonMap map[string]interface{}, jsonPath []string) (interface{}, error) {
value := jsonMap[jsonPath[0]]
if value == nil {
return nil, &valueNotFoundError{}
} else if len(jsonPath) > 1 {
return selectValue(value.(map[string]interface{}), jsonPath[1:])
} else {
return value, nil
}
}
func loadStackFile(path string) (string, error) {
loadedStackFileContentBytes, readingErr := ioutil.ReadFile(path)
if readingErr != nil {
return "", readingErr
}
return string(loadedStackFileContentBytes), nil
}
// Load environment variables
func loadEnvironmentVariablesFile(path string) ([]client.StackEnv, error) {
var variables []client.StackEnv
variablesMap, readingErr := godotenv.Read(path)
if readingErr != nil {
return []client.StackEnv{}, readingErr
}
for key, value := range variablesMap {
variables = append(variables, client.StackEnv{
Name: key,
Value: value,
})
}
return variables, nil
}
type valueNotFoundError struct{}
func (e *valueNotFoundError) Error() string {
return "Value not found"
}