Files
portainer/api/exec/compose_wrapper.go
T
2021-01-19 15:43:53 +13:00

191 lines
5.0 KiB
Go

package exec
import (
"bytes"
"errors"
"fmt"
"log"
"net"
"net/http"
"os"
"os/exec"
"path"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
)
// ComposeWrapper is a wrapper for docker-compose binary
type ComposeWrapper struct {
binaryPath string
proxyManager proxy.Manager
}
// NewComposeWrapper returns a docker-compose wrapper if corresponding binary present, otherwise nil
func NewComposeWrapper(binaryPath string, proxyManager proxy.Manager) *ComposeWrapper {
if !IsBinaryPresent(programPath(binaryPath, "docker-compose")) {
return nil
}
return &ComposeWrapper{
binaryPath: binaryPath,
proxyManager: proxyManager,
}
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (w *ComposeWrapper) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
_, err := w.command([]string{"up", "-d"}, stack, endpoint)
return err
}
// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command
func (w *ComposeWrapper) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
_, err := w.command([]string{"down", "--remove-orphans"}, stack, endpoint)
return err
}
func (w *ComposeWrapper) command(command []string, stack *portainer.Stack, endpoint *portainer.Endpoint) ([]byte, error) {
program := programPath(w.binaryPath, "docker-compose")
options := setComposeFile(stack)
options = addProjectNameOption(options, stack)
options, err := addEnvFileOption(options, stack)
if err != nil {
return nil, err
}
if endpoint != nil {
if endpoint.URL != "" {
proxy, err := w.proxyManager.CreateAndRegisterComposeEndpointProxy(endpoint)
listener, err := net.Listen("tcp", ":0")
if err != nil {
return nil, err
}
log.Printf("Proxy Server: %v", proxy)
server := http.Server{
Handler: proxy,
}
shutdownChan := make(chan error, 1)
port := listener.Addr().(*net.TCPAddr).Port
go func() {
log.Printf("Starting Proxy server on %s...\n", fmt.Sprintf("http://localhost:%d", port))
// details are the same as for the `server.ListenAndServe()` section above
err := server.Serve(listener)
log.Printf("Proxy Server exited with '%v' error\n", err)
if err != http.ErrServerClosed {
log.Printf("Put '%v' error returned by Proxy Server to shutdown channel\n", err)
shutdownChan <- err
}
}()
defer server.Close()
options = append(options, "-H", fmt.Sprintf("http://localhost:%d", port))
}
}
// options = addTLSConnectionOptions(options, endpoint)
args := append(options, command...)
var stderr bytes.Buffer
cmd := exec.Command(program, args...)
cmd.Stderr = &stderr
out, err := cmd.Output()
if err != nil {
return out, errors.New(stderr.String())
}
return out, nil
}
func setComposeFile(stack *portainer.Stack) []string {
options := make([]string, 0)
if stack == nil || stack.EntryPoint == "" {
return options
}
composeFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
options = append(options, "-f", composeFilePath)
return options
}
func addTLSConnectionOptions(options []string, endpoint *portainer.Endpoint) []string {
if endpoint.TLSConfig.TLS {
options = append(options, "--tls")
if !endpoint.TLSConfig.TLSSkipVerify {
options = append(options, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
}
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {
options = append(options, "--tlscert", endpoint.TLSConfig.TLSCertPath, "--tlskey", endpoint.TLSConfig.TLSKeyPath)
}
}
return options
}
func addConnectionOptions(options []string, endpoint *portainer.Endpoint) []string {
if endpoint == nil {
return options
}
if endpoint.URL != "" {
options = append(options, "-H", endpoint.URL)
}
if endpoint.TLSConfig.TLS {
options = append(options, "--tls")
if !endpoint.TLSConfig.TLSSkipVerify {
options = append(options, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
}
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {
options = append(options, "--tlscert", endpoint.TLSConfig.TLSCertPath, "--tlskey", endpoint.TLSConfig.TLSKeyPath)
}
}
return options
}
func addProjectNameOption(options []string, stack *portainer.Stack) []string {
if stack == nil || stack.Name == "" {
return options
}
options = append(options, "-p", stack.Name)
return options
}
func addEnvFileOption(options []string, stack *portainer.Stack) ([]string, error) {
if stack == nil || stack.Env == nil || len(stack.Env) == 0 {
return options, nil
}
envFilePath := path.Join(stack.ProjectPath, "stack.env")
envfile, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return options, err
}
for _, v := range stack.Env {
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
}
envfile.Close()
options = append(options, "--env-file", envFilePath)
return options, nil
}