mirror of https://github.com/portainer/portainer
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
268 lines
7.3 KiB
268 lines
7.3 KiB
package exec
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"runtime"
|
|
"strings"
|
|
|
|
portainer "github.com/portainer/portainer/api"
|
|
"github.com/portainer/portainer/api/dataservices"
|
|
"github.com/portainer/portainer/api/internal/registryutils"
|
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/segmentio/encoding/json"
|
|
)
|
|
|
|
// SwarmStackManager represents a service for managing stacks.
|
|
type SwarmStackManager struct {
|
|
binaryPath string
|
|
configPath string
|
|
signatureService portainer.DigitalSignatureService
|
|
fileService portainer.FileService
|
|
reverseTunnelService portainer.ReverseTunnelService
|
|
dataStore dataservices.DataStore
|
|
}
|
|
|
|
// NewSwarmStackManager initializes a new SwarmStackManager service.
|
|
// It also updates the configuration of the Docker CLI binary.
|
|
func NewSwarmStackManager(
|
|
binaryPath, configPath string,
|
|
signatureService portainer.DigitalSignatureService,
|
|
fileService portainer.FileService,
|
|
reverseTunnelService portainer.ReverseTunnelService,
|
|
datastore dataservices.DataStore,
|
|
) (*SwarmStackManager, error) {
|
|
manager := &SwarmStackManager{
|
|
binaryPath: binaryPath,
|
|
configPath: configPath,
|
|
signatureService: signatureService,
|
|
fileService: fileService,
|
|
reverseTunnelService: reverseTunnelService,
|
|
dataStore: datastore,
|
|
}
|
|
|
|
err := manager.updateDockerCLIConfiguration(manager.configPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return manager, nil
|
|
}
|
|
|
|
// Login executes the docker login command against a list of registries (including DockerHub).
|
|
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) error {
|
|
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, registry := range registries {
|
|
if registry.Authentication {
|
|
err = registryutils.EnsureRegTokenValid(manager.dataStore, ®istry)
|
|
if err != nil {
|
|
log.
|
|
Warn().
|
|
Err(err).
|
|
Str("RegistryName", registry.Name).
|
|
Msg("Failed to validate registry token. Skip logging with this registry.")
|
|
|
|
continue
|
|
}
|
|
|
|
username, password, err := registryutils.GetRegEffectiveCredential(®istry)
|
|
if err != nil {
|
|
log.
|
|
Warn().
|
|
Err(err).
|
|
Str("RegistryName", registry.Name).
|
|
Msg("Failed to get effective credential. Skip logging with this registry.")
|
|
|
|
continue
|
|
}
|
|
|
|
registryArgs := append(args, "login", "--username", username, "--password", password, registry.URL)
|
|
err = runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
|
if err != nil {
|
|
log.
|
|
Warn().
|
|
Err(err).
|
|
Str("RegistryName", registry.Name).
|
|
Msg("Failed to login.")
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Logout executes the docker logout command.
|
|
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
|
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
args = append(args, "logout")
|
|
|
|
return runCommandAndCaptureStdErr(command, args, nil, "")
|
|
}
|
|
|
|
// Deploy executes the docker stack deploy command.
|
|
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pullImage bool, endpoint *portainer.Endpoint) error {
|
|
filePaths := stackutils.GetStackFilePaths(stack, true)
|
|
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if prune {
|
|
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth")
|
|
} else {
|
|
args = append(args, "stack", "deploy", "--with-registry-auth")
|
|
}
|
|
|
|
if !pullImage {
|
|
args = append(args, "--resolve-image=never")
|
|
}
|
|
|
|
args = configureFilePaths(args, filePaths)
|
|
args = append(args, stack.Name)
|
|
|
|
env := make([]string, 0)
|
|
for _, envvar := range stack.Env {
|
|
env = append(env, envvar.Name+"="+envvar.Value)
|
|
}
|
|
|
|
return runCommandAndCaptureStdErr(command, args, env, stack.ProjectPath)
|
|
}
|
|
|
|
// Remove executes the docker stack rm command.
|
|
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
|
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
args = append(args, "stack", "rm", stack.Name)
|
|
|
|
return runCommandAndCaptureStdErr(command, args, nil, "")
|
|
}
|
|
|
|
func runCommandAndCaptureStdErr(command string, args []string, env []string, workingDir string) error {
|
|
var stderr bytes.Buffer
|
|
cmd := exec.Command(command, args...)
|
|
cmd.Stderr = &stderr
|
|
|
|
if workingDir != "" {
|
|
cmd.Dir = workingDir
|
|
}
|
|
|
|
if env != nil {
|
|
cmd.Env = os.Environ()
|
|
cmd.Env = append(cmd.Env, env...)
|
|
}
|
|
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return errors.New(stderr.String())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, configPath string, endpoint *portainer.Endpoint) (string, []string, error) {
|
|
// Assume Linux as a default
|
|
command := path.Join(binaryPath, "docker")
|
|
|
|
if runtime.GOOS == "windows" {
|
|
command = path.Join(binaryPath, "docker.exe")
|
|
}
|
|
|
|
args := make([]string, 0)
|
|
args = append(args, "--config", configPath)
|
|
|
|
endpointURL := endpoint.URL
|
|
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
|
tunnelAddr, err := manager.reverseTunnelService.TunnelAddr(endpoint)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
endpointURL = "tcp://" + tunnelAddr
|
|
}
|
|
|
|
args = append(args, "-H", endpointURL)
|
|
|
|
if endpoint.TLSConfig.TLS {
|
|
args = append(args, "--tls")
|
|
|
|
if !endpoint.TLSConfig.TLSSkipVerify {
|
|
args = append(args, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
|
|
} else {
|
|
args = append(args, "--tlscacert", "''")
|
|
}
|
|
|
|
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {
|
|
args = append(args, "--tlscert", endpoint.TLSConfig.TLSCertPath, "--tlskey", endpoint.TLSConfig.TLSKeyPath)
|
|
}
|
|
}
|
|
|
|
return command, args, nil
|
|
}
|
|
|
|
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
|
|
configFilePath := path.Join(configPath, "config.json")
|
|
config, err := manager.retrieveConfigurationFromDisk(configFilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
signature, err := manager.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if config["HttpHeaders"] == nil {
|
|
config["HttpHeaders"] = make(map[string]any)
|
|
}
|
|
|
|
headersObject := config["HttpHeaders"].(map[string]any)
|
|
headersObject["X-PortainerAgent-ManagerOperation"] = "1"
|
|
headersObject["X-PortainerAgent-Signature"] = signature
|
|
headersObject["X-PortainerAgent-PublicKey"] = manager.signatureService.EncodedPublicKey()
|
|
|
|
return manager.fileService.WriteJSONToFile(configFilePath, config)
|
|
}
|
|
|
|
func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (map[string]any, error) {
|
|
var config map[string]any
|
|
|
|
raw, err := manager.fileService.GetFileContent(path, "")
|
|
if err != nil {
|
|
return make(map[string]any), nil
|
|
}
|
|
|
|
err = json.Unmarshal(raw, &config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
func (manager *SwarmStackManager) NormalizeStackName(name string) string {
|
|
return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "")
|
|
}
|
|
|
|
func configureFilePaths(args []string, filePaths []string) []string {
|
|
for _, path := range filePaths {
|
|
args = append(args, "--compose-file", path)
|
|
}
|
|
|
|
return args
|
|
}
|