fix(docker-watcher): address final review findings

Security:
- Move config export behind auth middleware
- Validate OIDC callback token before storing in localStorage
- Use constant-time comparison for webhook secret
- Encrypt OIDC client secret at rest (like registry tokens)

Performance:
- Make TriggerDeploy async from HTTP handlers (return deploy ID
  immediately, run pipeline in background goroutine)

Robustness:
- Wrap api.ts res.json() in try/catch for non-JSON responses

i18n:
- Replace ~20 hardcoded English validation messages with $t() calls
- Localize ConfirmDialog cancel button, InstanceCard confirm titles,
  ProjectCard instance/instances pluralization
- Add validation keys to both en.json and ru.json
This commit is contained in:
2026-03-28 00:14:53 +03:00
parent a3aa5912d9
commit 1f81ca9eb0
17 changed files with 178 additions and 40 deletions
+48 -2
View File
@@ -72,8 +72,54 @@ func (d *Deployer) Drain() {
slog.Info("deployer: all deploys drained")
}
// TriggerDeploy is the main entry point for deployments. It orchestrates the full flow:
// pull image -> create container -> start -> configure proxy -> health check.
// AsyncTriggerDeploy creates a deploy record and returns the deploy ID immediately,
// then runs the full deploy pipeline in a background goroutine. Use this from HTTP handlers
// to avoid blocking the request. Progress is streamed via SSE.
func (d *Deployer) AsyncTriggerDeploy(ctx context.Context, projectID, stageID, imageTag string) (string, error) {
if d.shuttingDown.Load() {
return "", fmt.Errorf("deployer is shutting down, rejecting new deploy")
}
// Validate inputs synchronously so the caller gets immediate feedback.
project, err := d.store.GetProjectByID(projectID)
if err != nil {
return "", fmt.Errorf("get project: %w", err)
}
stage, err := d.store.GetStageByID(stageID)
if err != nil {
return "", fmt.Errorf("get stage: %w", err)
}
if err := d.validatePromoteFrom(stage, imageTag); err != nil {
return "", fmt.Errorf("promote validation: %w", err)
}
// Create deploy record synchronously so caller gets the ID.
deploy, err := d.store.CreateDeploy(store.Deploy{
ProjectID: projectID,
StageID: stageID,
ImageTag: imageTag,
Status: "pending",
})
if err != nil {
return "", fmt.Errorf("create deploy record: %w", err)
}
// Run the actual deploy in the background.
d.activeWg.Add(1)
go func() {
defer d.activeWg.Done()
// Use a detached context so client disconnect doesn't abort the deploy.
bgCtx := context.Background()
if err := d.runDeploy(bgCtx, project, stage, deploy.ID, imageTag); err != nil {
slog.Error("async deploy failed", "deploy_id", deploy.ID, "error", err)
}
}()
return deploy.ID, nil
}
// TriggerDeploy is the synchronous entry point for deployments (used by poller and webhook).
// It orchestrates the full flow: pull image -> create container -> start -> configure proxy -> health check.
// On failure, it rolls back (removes container, deletes proxy host, updates status).
func (d *Deployer) TriggerDeploy(ctx context.Context, projectID, stageID, imageTag string) error {
if d.shuttingDown.Load() {