Use ULID for app IDs and Docker label for container lookup

- Replace UUID with ULID for app ID generation (lexicographically sortable)
- Remove container_id column from apps table (migration 002)
- Add upaas.id Docker label to identify containers by app ID
- Implement FindContainerByAppID in Docker client to query by label
- Update handlers and deploy service to use label-based container lookup
- Show system-managed upaas.id label in UI with editing disabled

Container association is now determined dynamically via Docker label
rather than stored in the database, making the system more resilient
to container recreation or external changes.
This commit is contained in:
Jeffrey Paul 2025-12-29 16:06:40 +07:00
parent c13fd8c746
commit 5fb0b111fc
11 changed files with 142 additions and 91 deletions

1
go.mod
View File

@ -44,6 +44,7 @@ require (
github.com/moby/term v0.5.2 // indirect github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.1.0 // indirect github.com/morikuni/aec v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect

3
go.sum
View File

@ -86,10 +86,13 @@ github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=

View File

@ -0,0 +1,44 @@
-- Remove container_id from apps table
-- Container is now looked up via Docker label (upaas.id) instead of stored in database
-- SQLite doesn't support DROP COLUMN before version 3.35.0 (2021-03-12)
-- Use table rebuild for broader compatibility
-- Create new table without container_id
CREATE TABLE apps_new (
id TEXT PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
repo_url TEXT NOT NULL,
branch TEXT NOT NULL DEFAULT 'main',
dockerfile_path TEXT DEFAULT 'Dockerfile',
webhook_secret TEXT NOT NULL,
ssh_private_key TEXT NOT NULL,
ssh_public_key TEXT NOT NULL,
image_id TEXT,
status TEXT DEFAULT 'pending',
docker_network TEXT,
ntfy_topic TEXT,
slack_webhook TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Copy data (excluding container_id)
INSERT INTO apps_new (
id, name, repo_url, branch, dockerfile_path, webhook_secret,
ssh_private_key, ssh_public_key, image_id, status,
docker_network, ntfy_topic, slack_webhook, created_at, updated_at
)
SELECT
id, name, repo_url, branch, dockerfile_path, webhook_secret,
ssh_private_key, ssh_public_key, image_id, status,
docker_network, ntfy_topic, slack_webhook, created_at, updated_at
FROM apps;
-- Drop old table and rename new one
DROP TABLE apps;
ALTER TABLE apps_new RENAME TO apps;
-- Recreate indexes
CREATE INDEX idx_apps_status ON apps(status);
CREATE INDEX idx_apps_webhook_secret ON apps(webhook_secret);

View File

@ -12,6 +12,7 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client" "github.com/docker/docker/client"
@ -309,6 +310,51 @@ func (c *Client) IsContainerHealthy(
return inspect.State.Health.Status == "healthy", nil return inspect.State.Health.Status == "healthy", nil
} }
// LabelUpaasID is the Docker label key used to identify containers managed by upaas.
const LabelUpaasID = "upaas.id"
// ContainerInfo contains basic information about a container.
type ContainerInfo struct {
ID string
Running bool
}
// FindContainerByAppID finds a container by the upaas.id label.
// Returns nil if no container is found.
//
//nolint:nilnil // returning nil,nil is idiomatic for "not found"
func (c *Client) FindContainerByAppID(
ctx context.Context,
appID string,
) (*ContainerInfo, error) {
if c.docker == nil {
return nil, ErrNotConnected
}
filterArgs := filters.NewArgs()
filterArgs.Add("label", LabelUpaasID+"="+appID)
containers, err := c.docker.ContainerList(ctx, container.ListOptions{
All: true,
Filters: filterArgs,
})
if err != nil {
return nil, fmt.Errorf("failed to list containers: %w", err)
}
if len(containers) == 0 {
return nil, nil
}
// Return the first matching container
ctr := containers[0]
return &ContainerInfo{
ID: ctr.ID,
Running: ctr.State == "running",
}, nil
}
// cloneConfig holds configuration for a git clone operation. // cloneConfig holds configuration for a git clone operation.
type cloneConfig struct { type cloneConfig struct {
repoURL string repoURL string

View File

@ -348,7 +348,8 @@ func (h *Handlers) HandleAppLogs() http.HandlerFunc {
writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
if !application.ContainerID.Valid { containerInfo, containerErr := h.docker.FindContainerByAppID(request.Context(), appID)
if containerErr != nil || containerInfo == nil {
_, _ = writer.Write([]byte("No container running\n")) _, _ = writer.Write([]byte("No container running\n"))
return return
@ -361,14 +362,14 @@ func (h *Handlers) HandleAppLogs() http.HandlerFunc {
logs, logsErr := h.docker.ContainerLogs( logs, logsErr := h.docker.ContainerLogs(
request.Context(), request.Context(),
application.ContainerID.String, containerInfo.ID,
tail, tail,
) )
if logsErr != nil { if logsErr != nil {
h.log.Error("failed to get container logs", h.log.Error("failed to get container logs",
"error", logsErr, "error", logsErr,
"app", application.Name, "app", application.Name,
"container", application.ContainerID.String, "container", containerInfo.ID,
) )
_, _ = writer.Write([]byte("Failed to fetch container logs\n")) _, _ = writer.Write([]byte("Failed to fetch container logs\n"))
@ -396,22 +397,23 @@ func (h *Handlers) handleContainerAction(
action containerAction, action containerAction,
) { ) {
appID := chi.URLParam(request, "id") appID := chi.URLParam(request, "id")
ctx := request.Context()
application, findErr := models.FindApp(request.Context(), h.db, appID) application, findErr := models.FindApp(ctx, h.db, appID)
if findErr != nil || application == nil { if findErr != nil || application == nil {
http.NotFound(writer, request) http.NotFound(writer, request)
return return
} }
if !application.ContainerID.Valid { containerInfo, containerErr := h.docker.FindContainerByAppID(ctx, appID)
if containerErr != nil || containerInfo == nil {
http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther) http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
return return
} }
containerID := application.ContainerID.String containerID := containerInfo.ID
ctx := request.Context()
var actionErr error var actionErr error

View File

@ -34,7 +34,6 @@ type App struct {
WebhookSecret string WebhookSecret string
SSHPrivateKey string SSHPrivateKey string
SSHPublicKey string SSHPublicKey string
ContainerID sql.NullString
ImageID sql.NullString ImageID sql.NullString
Status AppStatus Status AppStatus
DockerNetwork sql.NullString DockerNetwork sql.NullString
@ -73,7 +72,7 @@ func (a *App) Delete(ctx context.Context) error {
func (a *App) Reload(ctx context.Context) error { func (a *App) Reload(ctx context.Context) error {
row := a.db.QueryRow(ctx, ` row := a.db.QueryRow(ctx, `
SELECT id, name, repo_url, branch, dockerfile_path, webhook_secret, SELECT id, name, repo_url, branch, dockerfile_path, webhook_secret,
ssh_private_key, ssh_public_key, container_id, image_id, status, ssh_private_key, ssh_public_key, image_id, status,
docker_network, ntfy_topic, slack_webhook, created_at, updated_at docker_network, ntfy_topic, slack_webhook, created_at, updated_at
FROM apps WHERE id = ?`, FROM apps WHERE id = ?`,
a.ID, a.ID,
@ -131,13 +130,13 @@ func (a *App) insert(ctx context.Context) error {
query := ` query := `
INSERT INTO apps ( INSERT INTO apps (
id, name, repo_url, branch, dockerfile_path, webhook_secret, id, name, repo_url, branch, dockerfile_path, webhook_secret,
ssh_private_key, ssh_public_key, container_id, image_id, status, ssh_private_key, ssh_public_key, image_id, status,
docker_network, ntfy_topic, slack_webhook docker_network, ntfy_topic, slack_webhook
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
_, err := a.db.Exec(ctx, query, _, err := a.db.Exec(ctx, query,
a.ID, a.Name, a.RepoURL, a.Branch, a.DockerfilePath, a.WebhookSecret, a.ID, a.Name, a.RepoURL, a.Branch, a.DockerfilePath, a.WebhookSecret,
a.SSHPrivateKey, a.SSHPublicKey, a.ContainerID, a.ImageID, a.Status, a.SSHPrivateKey, a.SSHPublicKey, a.ImageID, a.Status,
a.DockerNetwork, a.NtfyTopic, a.SlackWebhook, a.DockerNetwork, a.NtfyTopic, a.SlackWebhook,
) )
if err != nil { if err != nil {
@ -151,14 +150,14 @@ func (a *App) update(ctx context.Context) error {
query := ` query := `
UPDATE apps SET UPDATE apps SET
name = ?, repo_url = ?, branch = ?, dockerfile_path = ?, name = ?, repo_url = ?, branch = ?, dockerfile_path = ?,
container_id = ?, image_id = ?, status = ?, image_id = ?, status = ?,
docker_network = ?, ntfy_topic = ?, slack_webhook = ?, docker_network = ?, ntfy_topic = ?, slack_webhook = ?,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = ?` WHERE id = ?`
_, err := a.db.Exec(ctx, query, _, err := a.db.Exec(ctx, query,
a.Name, a.RepoURL, a.Branch, a.DockerfilePath, a.Name, a.RepoURL, a.Branch, a.DockerfilePath,
a.ContainerID, a.ImageID, a.Status, a.ImageID, a.Status,
a.DockerNetwork, a.NtfyTopic, a.SlackWebhook, a.DockerNetwork, a.NtfyTopic, a.SlackWebhook,
a.ID, a.ID,
) )
@ -171,7 +170,7 @@ func (a *App) scan(row *sql.Row) error {
&a.ID, &a.Name, &a.RepoURL, &a.Branch, &a.ID, &a.Name, &a.RepoURL, &a.Branch,
&a.DockerfilePath, &a.WebhookSecret, &a.DockerfilePath, &a.WebhookSecret,
&a.SSHPrivateKey, &a.SSHPublicKey, &a.SSHPrivateKey, &a.SSHPublicKey,
&a.ContainerID, &a.ImageID, &a.Status, &a.ImageID, &a.Status,
&a.DockerNetwork, &a.NtfyTopic, &a.SlackWebhook, &a.DockerNetwork, &a.NtfyTopic, &a.SlackWebhook,
&a.CreatedAt, &a.UpdatedAt, &a.CreatedAt, &a.UpdatedAt,
) )
@ -187,7 +186,7 @@ func scanApps(appDB *database.Database, rows *sql.Rows) ([]*App, error) {
&app.ID, &app.Name, &app.RepoURL, &app.Branch, &app.ID, &app.Name, &app.RepoURL, &app.Branch,
&app.DockerfilePath, &app.WebhookSecret, &app.DockerfilePath, &app.WebhookSecret,
&app.SSHPrivateKey, &app.SSHPublicKey, &app.SSHPrivateKey, &app.SSHPublicKey,
&app.ContainerID, &app.ImageID, &app.Status, &app.ImageID, &app.Status,
&app.DockerNetwork, &app.NtfyTopic, &app.SlackWebhook, &app.DockerNetwork, &app.NtfyTopic, &app.SlackWebhook,
&app.CreatedAt, &app.UpdatedAt, &app.CreatedAt, &app.UpdatedAt,
) )
@ -219,7 +218,7 @@ func FindApp(
row := appDB.QueryRow(ctx, ` row := appDB.QueryRow(ctx, `
SELECT id, name, repo_url, branch, dockerfile_path, webhook_secret, SELECT id, name, repo_url, branch, dockerfile_path, webhook_secret,
ssh_private_key, ssh_public_key, container_id, image_id, status, ssh_private_key, ssh_public_key, image_id, status,
docker_network, ntfy_topic, slack_webhook, created_at, updated_at docker_network, ntfy_topic, slack_webhook, created_at, updated_at
FROM apps WHERE id = ?`, FROM apps WHERE id = ?`,
appID, appID,
@ -249,7 +248,7 @@ func FindAppByWebhookSecret(
row := appDB.QueryRow(ctx, ` row := appDB.QueryRow(ctx, `
SELECT id, name, repo_url, branch, dockerfile_path, webhook_secret, SELECT id, name, repo_url, branch, dockerfile_path, webhook_secret,
ssh_private_key, ssh_public_key, container_id, image_id, status, ssh_private_key, ssh_public_key, image_id, status,
docker_network, ntfy_topic, slack_webhook, created_at, updated_at docker_network, ntfy_topic, slack_webhook, created_at, updated_at
FROM apps WHERE webhook_secret = ?`, FROM apps WHERE webhook_secret = ?`,
secret, secret,
@ -271,7 +270,7 @@ func FindAppByWebhookSecret(
func AllApps(ctx context.Context, appDB *database.Database) ([]*App, error) { func AllApps(ctx context.Context, appDB *database.Database) ([]*App, error) {
rows, err := appDB.Query(ctx, ` rows, err := appDB.Query(ctx, `
SELECT id, name, repo_url, branch, dockerfile_path, webhook_secret, SELECT id, name, repo_url, branch, dockerfile_path, webhook_secret,
ssh_private_key, ssh_public_key, container_id, image_id, status, ssh_private_key, ssh_public_key, image_id, status,
docker_network, ntfy_topic, slack_webhook, created_at, updated_at docker_network, ntfy_topic, slack_webhook, created_at, updated_at
FROM apps ORDER BY name`, FROM apps ORDER BY name`,
) )

View File

@ -223,7 +223,7 @@ func TestAppUpdate(t *testing.T) {
app.Name = "updated" app.Name = "updated"
app.Status = models.AppStatusRunning app.Status = models.AppStatusRunning
app.ContainerID = sql.NullString{String: "container123", Valid: true} app.ImageID = sql.NullString{String: "image123", Valid: true}
err := app.Save(context.Background()) err := app.Save(context.Background())
require.NoError(t, err) require.NoError(t, err)
@ -232,7 +232,7 @@ func TestAppUpdate(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "updated", found.Name) assert.Equal(t, "updated", found.Name)
assert.Equal(t, models.AppStatusRunning, found.Status) assert.Equal(t, models.AppStatusRunning, found.Status)
assert.Equal(t, "container123", found.ContainerID.String) assert.Equal(t, "image123", found.ImageID.String)
} }
func TestAppDelete(t *testing.T) { func TestAppDelete(t *testing.T) {

View File

@ -3,11 +3,14 @@ package app
import ( import (
"context" "context"
"crypto/rand"
"database/sql" "database/sql"
"fmt" "fmt"
"log/slog" "log/slog"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/oklog/ulid/v2"
"go.uber.org/fx" "go.uber.org/fx"
"git.eeqj.de/sneak/upaas/internal/database" "git.eeqj.de/sneak/upaas/internal/database"
@ -62,9 +65,9 @@ func (svc *Service) CreateApp(
return nil, fmt.Errorf("failed to generate SSH key pair: %w", err) return nil, fmt.Errorf("failed to generate SSH key pair: %w", err)
} }
// Create app // Create app with ULID
app := models.NewApp(svc.db) app := models.NewApp(svc.db)
app.ID = uuid.New().String() app.ID = ulid.MustNew(ulid.Timestamp(time.Now()), rand.Reader).String()
app.Name = input.Name app.Name = input.Name
app.RepoURL = input.RepoURL app.RepoURL = input.RepoURL
@ -324,20 +327,3 @@ func (svc *Service) UpdateAppStatus(
return nil return nil
} }
// UpdateAppContainer updates the container ID of an app.
func (svc *Service) UpdateAppContainer(
ctx context.Context,
app *models.App,
containerID, imageID string,
) error {
app.ContainerID = sql.NullString{String: containerID, Valid: containerID != ""}
app.ImageID = sql.NullString{String: imageID, Valid: imageID != ""}
saveErr := app.Save(ctx)
if saveErr != nil {
return fmt.Errorf("failed to save app container: %w", saveErr)
}
return nil
}

View File

@ -600,37 +600,3 @@ func TestUpdateAppStatus(testingT *testing.T) {
assert.Equal(t, models.AppStatusBuilding, reloaded.Status) assert.Equal(t, models.AppStatusBuilding, reloaded.Status)
}) })
} }
func TestUpdateAppContainer(testingT *testing.T) {
testingT.Parallel()
testingT.Run("updates container and image IDs", func(t *testing.T) {
t.Parallel()
svc, cleanup := setupTestService(t)
defer cleanup()
createdApp, err := svc.CreateApp(context.Background(), app.CreateAppInput{
Name: "container-test",
RepoURL: "git@example.com:user/repo.git",
})
require.NoError(t, err)
assert.False(t, createdApp.ContainerID.Valid)
assert.False(t, createdApp.ImageID.Valid)
err = svc.UpdateAppContainer(
context.Background(),
createdApp,
"container123",
"image456",
)
require.NoError(t, err)
reloaded, err := svc.GetApp(context.Background(), createdApp.ID)
require.NoError(t, err)
assert.True(t, reloaded.ContainerID.Valid)
assert.Equal(t, "container123", reloaded.ContainerID.String)
assert.True(t, reloaded.ImageID.Valid)
assert.Equal(t, "image456", reloaded.ImageID.String)
})
}

View File

@ -25,7 +25,7 @@ import (
const ( const (
healthCheckDelaySeconds = 60 healthCheckDelaySeconds = 60
// upaasLabelCount is the number of upaas-specific labels added to containers. // upaasLabelCount is the number of upaas-specific labels added to containers.
upaasLabelCount = 2 upaasLabelCount = 1
) )
// Sentinel errors for deployment failures. // Sentinel errors for deployment failures.
@ -104,12 +104,12 @@ func (svc *Service) Deploy(
svc.removeOldContainer(ctx, app, deployment) svc.removeOldContainer(ctx, app, deployment)
containerID, err := svc.createAndStartContainer(ctx, app, deployment, imageID) _, err = svc.createAndStartContainer(ctx, app, deployment, imageID)
if err != nil { if err != nil {
return err return err
} }
err = svc.updateAppRunning(ctx, app, containerID, imageID) err = svc.updateAppRunning(ctx, app, imageID)
if err != nil { if err != nil {
return err return err
} }
@ -243,13 +243,14 @@ func (svc *Service) removeOldContainer(
app *models.App, app *models.App,
deployment *models.Deployment, deployment *models.Deployment,
) { ) {
if !app.ContainerID.Valid || app.ContainerID.String == "" { containerInfo, err := svc.docker.FindContainerByAppID(ctx, app.ID)
if err != nil || containerInfo == nil {
return return
} }
svc.log.Info("removing old container", "id", app.ContainerID.String) svc.log.Info("removing old container", "id", containerInfo.ID)
removeErr := svc.docker.RemoveContainer(ctx, app.ContainerID.String, true) removeErr := svc.docker.RemoveContainer(ctx, containerInfo.ID, true)
if removeErr != nil { if removeErr != nil {
svc.log.Warn("failed to remove old container", "error", removeErr) svc.log.Warn("failed to remove old container", "error", removeErr)
} }
@ -350,8 +351,8 @@ func buildLabelMap(app *models.App, labels []*models.Label) map[string]string {
labelMap[label.Key] = label.Value labelMap[label.Key] = label.Value
} }
labelMap["upaas.app.id"] = app.ID // Add the upaas.id label to identify this container
labelMap["upaas.app.name"] = app.Name labelMap[docker.LabelUpaasID] = app.ID
return labelMap return labelMap
} }
@ -372,9 +373,8 @@ func buildVolumeMounts(volumes []*models.Volume) []docker.VolumeMount {
func (svc *Service) updateAppRunning( func (svc *Service) updateAppRunning(
ctx context.Context, ctx context.Context,
app *models.App, app *models.App,
containerID, imageID string, imageID string,
) error { ) error {
app.ContainerID = sql.NullString{String: containerID, Valid: true}
app.ImageID = sql.NullString{String: imageID, Valid: true} app.ImageID = sql.NullString{String: imageID, Valid: true}
app.Status = models.AppStatusRunning app.Status = models.AppStatusRunning
@ -405,14 +405,12 @@ func (svc *Service) checkHealthAfterDelay(
return return
} }
if !reloadedApp.ContainerID.Valid { containerInfo, containerErr := svc.docker.FindContainerByAppID(ctx, app.ID)
if containerErr != nil || containerInfo == nil {
return return
} }
healthy, err := svc.docker.IsContainerHealthy( healthy, err := svc.docker.IsContainerHealthy(ctx, containerInfo.ID)
ctx,
reloadedApp.ContainerID.String,
)
if err != nil { if err != nil {
svc.log.Error("failed to check container health", "error", err) svc.log.Error("failed to check container health", "error", err)
svc.notify.NotifyDeployFailed(ctx, reloadedApp, deployment, err) svc.notify.NotifyDeployFailed(ctx, reloadedApp, deployment, err)

View File

@ -122,7 +122,6 @@
<!-- Labels --> <!-- Labels -->
<div class="card p-6 mb-6"> <div class="card p-6 mb-6">
<h2 class="section-title mb-4">Docker Labels</h2> <h2 class="section-title mb-4">Docker Labels</h2>
{{if .Labels}}
<div class="overflow-x-auto mb-4"> <div class="overflow-x-auto mb-4">
<table class="table"> <table class="table">
<thead class="table-header"> <thead class="table-header">
@ -133,6 +132,14 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-body"> <tbody class="table-body">
<!-- System-managed upaas.id label -->
<tr class="bg-gray-50">
<td class="font-mono font-medium text-gray-600">upaas.id</td>
<td class="font-mono text-gray-500">{{.App.ID}}</td>
<td class="text-right">
<span class="text-xs text-gray-400">System</span>
</td>
</tr>
{{range .Labels}} {{range .Labels}}
<tr> <tr>
<td class="font-mono font-medium">{{.Key}}</td> <td class="font-mono font-medium">{{.Key}}</td>
@ -147,7 +154,6 @@
</tbody> </tbody>
</table> </table>
</div> </div>
{{end}}
<form method="POST" action="/apps/{{.App.ID}}/labels" class="flex flex-col sm:flex-row gap-2"> <form method="POST" action="/apps/{{.App.ID}}/labels" class="flex flex-col sm:flex-row gap-2">
<input type="text" name="key" placeholder="label.key" required class="input flex-1 font-mono text-sm"> <input type="text" name="key" placeholder="label.key" required class="input flex-1 font-mono text-sm">
<input type="text" name="value" placeholder="value" required class="input flex-1 font-mono text-sm"> <input type="text" name="value" placeholder="value" required class="input flex-1 font-mono text-sm">