Files
tuio/internal/tui/update.go
T
teste.inaba a05b98fdf5 feat: auto-update app-dono-cliente when a new image is pushed to :latest
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.
2026-08-26 12:30:44 -03:00

452 lines
10 KiB
Go

package tui
import (
"fmt"
"math/rand"
"charm.land/bubbles/v2/spinner"
tea "charm.land/bubbletea/v2"
)
const (
imageName = "hub.davinti.com.br:443/app-dono/app-cliente:latest"
wireguardImageName = "hub.davinti.com.br:443/davinti-vproxy:latest"
updaterImageName = "docker:cli"
configPath = "config.toml"
wireguardConfigPath = "envs"
)
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if key, ok := msg.(tea.KeyPressMsg); ok {
switch key.String() {
case "ctrl+c":
return m, tea.Quit
case "esc":
// Global "back" on input steps. Action/wait steps handle esc themselves.
if m.canGoBack() {
m.goBack()
return m, nil
}
}
}
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
// Fit inputs to the terminal: subtract the left padding, the "> " prompt and
// the cursor cell, with a sensible floor for very narrow terminals.
inputWidth := msg.Width - (padding * 2) - 4
if inputWidth < 20 {
inputWidth = 20
}
for _, f := range []*FormStep{
&m.loginForm, &m.wireguardForm, &m.appForm,
&m.serverForm, &m.dbForm, &m.certForm,
} {
f.SetWidth(inputWidth)
}
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}
switch m.currentStep {
case StepCheckDocker:
return m.updateCheckDocker(msg)
case StepDockerInstall:
return m.updateDockerInstall(msg)
case StepDockerLogin:
done, cmd := m.loginForm.Update(msg)
if done {
m.configValues.Login = m.loginForm.Values()
m.advance(StepDownloadImage)
return m, DownloadImageCmd(imageName, m.configValues.Login["user"], m.configValues.Login["password"])
}
return m, cmd
case StepDownloadImage:
return m.updateDownloadImage(msg)
// Ip & Wireguard
case StepIPQuestion:
return m.updateIPQuestion(msg)
case StepWireguardConfig:
done, cmd := m.wireguardForm.Update(msg)
if done {
m.configValues.Wireguard = m.wireguardForm.Values()
m.downloadDone = false
m.downloadMessage = ""
m.downloadError = nil
m.advance(StepGenerateWireguardFile)
return m, GenerateWireguardConfigFile(m.configValues, wireguardConfigPath)
}
return m, cmd
case StepGenerateWireguardFile:
return m.updateGenerateWireguardFile(msg)
case StepDownloadWireguard:
return m.updateDownloadWireguard(msg)
case StepRunWireguard:
return m.updateRunWireguardDocker(msg)
case StepAppConfig:
done, cmd := m.appForm.Update(msg)
if done {
m.configValues.Application = m.appForm.Values()
m.advance(StepServerConfig)
}
return m, cmd
case StepServerConfig:
done, cmd := m.serverForm.Update(msg)
if done {
m.configValues.Server = m.serverForm.Values()
m.advance(StepDatabaseConfig)
}
return m, cmd
case StepDatabaseConfig:
done, cmd := m.dbForm.Update(msg)
if done {
m.configValues.Database = m.dbForm.Values()
m.advance(StepCertConfig)
}
return m, cmd
case StepCertConfig:
done, cmd := m.certForm.Update(msg)
if done {
m.configValues.Cert = m.certForm.Values()
m.advance(StepReview)
}
return m, cmd
case StepReview:
return m.updateReview(msg)
case StepGenerateFile:
return m.updateGenerateFile(msg)
case StepRunDocker:
return m.updateRunDocker(msg)
case StepRunUpdater:
return m.updateRunUpdater(msg)
case StepDone:
return m, tea.Quit
}
return m, nil
}
func (m Model) updateCheckDocker(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case DockerCheckedMsg:
m.dockerInstalled = msg.Installed
m.checkDockerDone = true
case TickMsg:
if m.checkProgress < 1.0 {
m.checkProgress += 0.2 + (rand.Float64()*0.2 - 0.2)
if m.checkProgress > 1.0 {
m.checkProgress = 1.0
}
return m, TickCmd()
}
case tea.KeyPressMsg:
if m.checkDockerDone && m.checkProgress == 1 {
if m.dockerInstalled {
m.loading = true
m.advance(StepDockerLogin)
} else {
m.advance(StepDockerInstall)
}
}
}
return m, nil
}
func (m Model) updateDockerInstall(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.(type) {
case tea.KeyPressMsg:
return m, tea.Quit
}
return m, nil
}
func (m Model) updateDownloadImage(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case ImageDownloadFinishedMsg:
m.downloadDone = true
m.downloadMessage = msg.Message
m.downloadError = msg.Err
return m, nil
case tea.KeyPressMsg:
if m.downloadDone && m.downloadError == nil {
m.advance(StepIPQuestion)
} else if m.downloadDone {
switch msg.String() {
case "r":
m.downloadDone = false
m.downloadMessage = ""
m.downloadError = nil
return m, DownloadImageCmd(imageName, m.configValues.Login["user"], m.configValues.Login["password"])
default:
return m, tea.Quit
}
}
}
return m, nil
}
func (m Model) updateIPQuestion(msg tea.Msg) (tea.Model, tea.Cmd) {
numOptions := 2
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch msg.String() {
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < numOptions-1 {
m.cursor++
}
case "enter":
// Yes
if m.cursor == 0 {
m.advance(StepAppConfig)
return m, nil
}
m.advance(StepWireguardConfig)
}
}
return m, nil
}
func (m Model) updateDownloadWireguard(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case ImageDownloadFinishedMsg:
m.downloadDone = true
m.downloadMessage = msg.Message
m.downloadError = msg.Err
if m.downloadError == nil {
m.currentStep = StepRunWireguard
return m, RunWireguardContainer(wireguardConfigPath, m.configValues)
}
case tea.KeyPressMsg:
if m.downloadDone && m.downloadError == nil {
m.currentStep = StepRunWireguard
} else if m.downloadDone {
switch msg.String() {
case "r":
m.downloadDone = false
m.downloadMessage = ""
m.downloadError = nil
return m, DownloadImageCmd(wireguardImageName, m.configValues.Login["user"], m.configValues.Login["password"])
default:
return m, tea.Quit
}
}
}
return m, nil
}
func (m Model) updateGenerateWireguardFile(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case ConfigFileMsg:
m.finishedFile = true
m.configFileError = msg.Err
if msg.Err == nil {
m.currentStep = StepDownloadWireguard
return m, DownloadImageCmd(wireguardImageName, m.configValues.Login["user"], m.configValues.Login["password"])
}
case tea.KeyPressMsg:
if m.finishedFile && m.configFileError != nil {
// Validation error (e.g. MTU): go back to the form to correct it. The form
// is the top of the history stack, so goBack lands on it without leaving a
// duplicate entry; fall back to a direct set if history is unexpectedly empty.
if !m.goBack() {
m.currentStep = StepWireguardConfig
}
} else if m.finishedFile && m.configFileError == nil {
m.currentStep = StepDownloadWireguard
return m, DownloadImageCmd(wireguardImageName, m.configValues.Login["user"], m.configValues.Login["password"])
}
}
return m, nil
}
func (m Model) updateRunWireguardDocker(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case DockerRunMsg:
m.finishedDockerRun = true
m.dockerRunError = msg.Err
case tea.KeyPressMsg:
if m.finishedDockerRun && m.dockerRunError != nil {
switch msg.String() {
case "r":
m.finishedDockerRun = false
m.dockerRunError = nil
return m, RunWireguardContainer(wireguardConfigPath, m.configValues)
default:
return m, tea.Quit
}
} else if m.finishedDockerRun && m.dockerRunError == nil {
m.advance(StepAppConfig)
}
}
return m, nil
}
// updateReview shows the collected configuration for confirmation before any files are
// written. Enter confirms and generates config.toml; Esc (handled globally) goes back
// to the previous form to edit.
func (m Model) updateReview(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch msg.String() {
case "enter":
m.advance(StepGenerateFile)
m.finishedFile = false
m.configFileError = nil
return m, GenerateConfigFile(m.configValues, configPath)
}
}
return m, nil
}
func (m Model) updateGenerateFile(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case ConfigFileMsg:
m.finishedFile = true
m.configFileError = msg.Err
case tea.KeyPressMsg:
if m.finishedFile && m.configFileError != nil {
// Validation error: return to the review (top of the stack) so the user can
// navigate back to the forms; direct set as a fallback.
if !m.goBack() {
m.currentStep = StepReview
}
} else if m.finishedFile && m.configFileError == nil {
m.currentStep = StepRunDocker
m.finishedDockerRun = false
m.dockerRunError = nil
return m, RunAppContainer(
imageName,
"app-dono-cliente",
configPath,
fmt.Sprintf("/app/%s", configPath),
m.configValues,
)
}
}
return m, nil
}
func (m Model) updateRunDocker(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case DockerRunMsg:
m.finishedDockerRun = true
m.dockerRunError = msg.Err
case tea.KeyPressMsg:
if m.finishedDockerRun && m.dockerRunError != nil {
switch msg.String() {
case "r":
m.finishedDockerRun = false
m.dockerRunError = nil
return m, RunAppContainer(
imageName,
"app-dono-cliente",
configPath,
fmt.Sprintf("/app/%s", configPath),
m.configValues,
)
default:
return m, tea.Quit
}
} else if m.finishedDockerRun && m.dockerRunError == nil {
m.currentStep = StepRunUpdater
m.finishedDockerRun = false
m.dockerRunError = nil
return m, RunUpdaterContainer(
imageName,
"app-dono-cliente",
configPath,
fmt.Sprintf("/app/%s", configPath),
m.configValues,
)
}
}
return m, nil
}
// updateRunUpdater starts the auto-update agent that watches the app-dono-cliente
// container and pulls/recreates it whenever a new image is pushed to :latest.
func (m Model) updateRunUpdater(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case DockerRunMsg:
m.finishedDockerRun = true
m.dockerRunError = msg.Err
case tea.KeyPressMsg:
if m.finishedDockerRun && m.dockerRunError != nil {
switch msg.String() {
case "r":
m.finishedDockerRun = false
m.dockerRunError = nil
return m, RunUpdaterContainer(
imageName,
"app-dono-cliente",
configPath,
fmt.Sprintf("/app/%s", configPath),
m.configValues,
)
default:
return m, tea.Quit
}
} else if m.finishedDockerRun && m.dockerRunError == nil {
m.currentStep = StepDone
}
}
return m, nil
}