Adds a StepRunUpdater step that starts app-dono-updater after the app container comes up: a poll loop (docker pull, compare image IDs, recreate on change) baked into the official docker:cli image via `sh -c`, reusing the same docker run argv as the initial container start so the two can't drift. Not built on Watchtower: containrrr/watchtower was archived upstream in Dec 2025 with no maintained successor recommended for production use, so this avoids taking on that dependency.
277 lines
8.0 KiB
Go
277 lines
8.0 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const networkName = "app-dono_app"
|
|
|
|
// containerAppPort is the port the app cliente container listens on internally.
|
|
// The user-provided port is only the host-side mapping (<host port>:containerAppPort).
|
|
const containerAppPort = 8080
|
|
|
|
func RunContainer(image string, name string, port int) error {
|
|
|
|
cmd := exec.Command(
|
|
"docker", "run",
|
|
"-d",
|
|
"--name", name,
|
|
"-p", fmt.Sprintf("%d:%d", port, port),
|
|
image,
|
|
)
|
|
|
|
return cmd.Run()
|
|
}
|
|
|
|
func PullImage(image string) (string, error) {
|
|
|
|
cmd := exec.Command("docker", "pull", image)
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
return string(out), err
|
|
}
|
|
|
|
func ImageExists(image string) bool {
|
|
cmd := exec.Command("docker", "image", "inspect", image)
|
|
err := cmd.Run()
|
|
|
|
return err == nil
|
|
}
|
|
|
|
func PushFileToContainer(container, filePath, destinationPath string) bool {
|
|
dest := fmt.Sprintf("%s:%s", container, destinationPath)
|
|
|
|
cmd := exec.Command("docker", "cp", filePath, dest)
|
|
|
|
err := cmd.Run()
|
|
return err == nil
|
|
}
|
|
|
|
func EnsureNetwork(name string) error {
|
|
cmd := exec.Command("docker", "network", "inspect", name)
|
|
if err := cmd.Run(); err == nil {
|
|
return nil
|
|
}
|
|
cmd = exec.Command("docker", "network", "create", name)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("erro ao criar network %s: %w\noutput: %s", name, err, string(out))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func RunWireguardDockerContainer(envFilePath string, cv ConfigValues) error {
|
|
containerName := "vproxy"
|
|
|
|
removeExistingContainer(containerName)
|
|
|
|
if err := EnsureNetwork(networkName); err != nil {
|
|
return err
|
|
}
|
|
|
|
absPath, err := filepath.Abs(envFilePath)
|
|
if err != nil {
|
|
return fmt.Errorf("erro ao resolver caminho absoluto: %w", err)
|
|
}
|
|
|
|
args := []string{
|
|
"run", "-it", "-d",
|
|
"--name", containerName,
|
|
"--network", networkName,
|
|
"--restart", "unless-stopped",
|
|
"--cap-add=NET_ADMIN",
|
|
"--device", "/dev/net/tun:/dev/net/tun",
|
|
"--log-opt", "max-size=5m",
|
|
"--log-opt", "max-file=1",
|
|
"--env-file", absPath,
|
|
}
|
|
if cv.Server["seccomp_unconfined"] == "Sim" {
|
|
args = append(args, "--security-opt", "seccomp=unconfined")
|
|
}
|
|
args = append(args, wireguardImageName)
|
|
cmd := exec.Command("docker", args...)
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("docker run falhou: %w\noutput: %s", err, string(out))
|
|
}
|
|
|
|
time.Sleep(2 * time.Second)
|
|
containerID := strings.TrimSpace(string(out))
|
|
return verifyContainerRunning(containerID)
|
|
}
|
|
|
|
// appClienteRunArgs builds the `docker run` argv for the app-dono-cliente container.
|
|
// Shared by RunAppClienteContainer (initial start) and RunUpdaterDockerContainer (which
|
|
// re-embeds the same argv in its recreate script), so the two never drift apart.
|
|
func appClienteRunArgs(image, containerName, configPath, configDestinationPath string, cv ConfigValues) ([]string, error) {
|
|
absPath, err := filepath.Abs(configPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("erro ao resolver caminho absoluto: %w", err)
|
|
}
|
|
|
|
currentUser, err := user.Current()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
uidGid := fmt.Sprintf("%s:%s", currentUser.Uid, currentUser.Gid)
|
|
|
|
args := []string{
|
|
"run", "-d",
|
|
"-u", uidGid,
|
|
"-p", fmt.Sprintf("%s:%d", cv.Server["port"], containerAppPort),
|
|
"--name", containerName,
|
|
"--network", networkName,
|
|
"--restart", "unless-stopped",
|
|
"-v", fmt.Sprintf("%s:%s", absPath, configDestinationPath),
|
|
"-v", fmt.Sprintf("%s:/app/certs", cv.Cert["cert_dir_path"]),
|
|
}
|
|
if cv.Server["seccomp_unconfined"] == "Sim" {
|
|
args = append(args, "--security-opt", "seccomp=unconfined")
|
|
}
|
|
args = append(args, image)
|
|
return args, nil
|
|
}
|
|
|
|
func RunAppClienteContainer(image, containerName, configPath, configDestinationPath string, cv ConfigValues) error {
|
|
removeExistingContainer(containerName)
|
|
|
|
if err := EnsureNetwork(networkName); err != nil {
|
|
return err
|
|
}
|
|
|
|
args, err := appClienteRunArgs(image, containerName, configPath, configDestinationPath, cv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd := exec.Command("docker", args...)
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("docker run falhou: %w\noutput: %s", err, string(out))
|
|
}
|
|
|
|
time.Sleep(2 * time.Second)
|
|
containerID := strings.TrimSpace(string(out))
|
|
return verifyContainerRunning(containerID)
|
|
}
|
|
|
|
// shellQuote POSIX single-quotes s for safe embedding in the updater's poll script:
|
|
// wrap in '...', escaping any embedded ' as '\''. Needed because cv fields (e.g.
|
|
// cert_dir_path) are operator-entered and end up inside a shell script, not a plain
|
|
// argv slot.
|
|
func shellQuote(s string) string {
|
|
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
|
}
|
|
|
|
func shellQuoteArgs(args []string) string {
|
|
quoted := make([]string, len(args))
|
|
for i, a := range args {
|
|
quoted[i] = shellQuote(a)
|
|
}
|
|
return strings.Join(quoted, " ")
|
|
}
|
|
|
|
// updaterPollIntervalSeconds is how often the updater checks the registry for a new
|
|
// app-cliente image.
|
|
const updaterPollIntervalSeconds = 300
|
|
|
|
// RunUpdaterDockerContainer starts a tiny self-contained auto-updater for the
|
|
// app-dono-cliente container: no third-party updater project, just the official
|
|
// `docker:cli` image running a poll loop (docker pull, compare image IDs, recreate on
|
|
// change) written in Go and handed to it via `sh -c`. This exists because Watchtower
|
|
// (the previous approach) was archived upstream with no maintained drop-in successor
|
|
// recommended for production use — see StepRunUpdater in update.go.
|
|
//
|
|
// It mounts the docker socket (to pull/recreate) and the host's docker config.json
|
|
// (written by the StepDockerLogin `docker login`) so `docker pull` can authenticate
|
|
// against the private registry.
|
|
func RunUpdaterDockerContainer(appImage, appContainerName, configPath, configDestinationPath string, cv ConfigValues) error {
|
|
updaterName := "app-dono-updater"
|
|
|
|
removeExistingContainer(updaterName)
|
|
|
|
if out, err := PullImage(updaterImageName); err != nil {
|
|
return fmt.Errorf("erro ao baixar imagem do atualizador: %w\noutput: %s", err, out)
|
|
}
|
|
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return fmt.Errorf("erro ao localizar diretório home: %w", err)
|
|
}
|
|
dockerConfigPath := filepath.Join(home, ".docker", "config.json")
|
|
|
|
recreateArgs, err := appClienteRunArgs(appImage, appContainerName, configPath, configDestinationPath, cv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
recreateCmd := "docker " + shellQuoteArgs(recreateArgs)
|
|
|
|
script := fmt.Sprintf(`set -e
|
|
IMAGE=%s
|
|
NAME=%s
|
|
while true; do
|
|
docker pull "$IMAGE" >/dev/null 2>&1 || true
|
|
CURRENT=$(docker inspect --format '{{.Image}}' "$NAME" 2>/dev/null || true)
|
|
LATEST=$(docker inspect --format '{{.Id}}' "$IMAGE" 2>/dev/null || true)
|
|
if [ -n "$LATEST" ] && [ "$CURRENT" != "$LATEST" ]; then
|
|
docker stop "$NAME" >/dev/null 2>&1 || true
|
|
docker rm "$NAME" >/dev/null 2>&1 || true
|
|
%s
|
|
fi
|
|
sleep %d
|
|
done
|
|
`, shellQuote(appImage), shellQuote(appContainerName), recreateCmd, updaterPollIntervalSeconds)
|
|
|
|
args := []string{
|
|
"run", "-d",
|
|
"--name", updaterName,
|
|
"--restart", "unless-stopped",
|
|
"-v", "/var/run/docker.sock:/var/run/docker.sock",
|
|
"-v", fmt.Sprintf("%s:/config.json", dockerConfigPath),
|
|
"--log-opt", "max-size=5m",
|
|
"--log-opt", "max-file=1",
|
|
"--entrypoint", "sh",
|
|
updaterImageName,
|
|
"-c", script,
|
|
}
|
|
cmd := exec.Command("docker", args...)
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("docker run falhou: %w\noutput: %s", err, string(out))
|
|
}
|
|
|
|
time.Sleep(2 * time.Second)
|
|
containerID := strings.TrimSpace(string(out))
|
|
return verifyContainerRunning(containerID)
|
|
}
|
|
|
|
func removeExistingContainer(name string) {
|
|
exec.Command("docker", "stop", name).Run()
|
|
exec.Command("docker", "rm", name).Run()
|
|
}
|
|
|
|
func verifyContainerRunning(containerID string) error {
|
|
cmd := exec.Command("docker", "inspect", "--format", "{{.State.Status}}", containerID)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return fmt.Errorf("erro ao inspecionar container: %w", err)
|
|
}
|
|
|
|
status := strings.TrimSpace(string(out))
|
|
if status != "running" {
|
|
logCmd := exec.Command("docker", "logs", "--tail", "20", containerID)
|
|
logs, _ := logCmd.CombinedOutput()
|
|
return fmt.Errorf("container parou com status %q.\nlogs:\n%s", status, string(logs))
|
|
}
|
|
|
|
return nil
|
|
}
|