package exec import ( "bytes" "errors" "fmt" "log" "net" "net/http" "os" "os/exec" "path" "strings" 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) { if endpoint == nil { return nil, errors.New("cannot call a compose command on an empty endpoint") } 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.URL != "" && !strings.HasPrefix(endpoint.URL, "unix://") && !strings.HasPrefix(endpoint.URL, "npipe://") { proxy, err := w.proxyManager.CreateAndRegisterComposeEndpointProxy(endpoint) if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { tunnel := w.proxyManager.GetReverseTunnel(endpoint) options = append(options, "-H", fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)) } else if endpoint.URL != "" && !(strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://")) { proxy, err := w.proxyManager.CreateAndRegisterComposeEndpointProxy(endpoint) 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 } 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://127.0.0.1:%d", port)) 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() defer w.proxyManager.DeleteEndpointProxy(endpoint) options = append(options, "-H", fmt.Sprintf("http://127.0.0.1:%d", port)) } options = append(options, "-H", fmt.Sprintf("http://localhost:%d", port)) } 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 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 }