feat: optional NPM proxy per stage
Add enable_proxy boolean to stages (default true). When disabled, the deployer skips NPM proxy host creation — useful for internal services, workers, or externally-routed containers. UI shows toggle in Add Stage form and "No Proxy" badge on stage header.
This commit is contained in:
@@ -142,24 +142,29 @@ func (d *Deployer) blueGreenDeploy(
|
||||
}
|
||||
|
||||
// Step 5: Swap NPM proxy to green.
|
||||
if err := d.store.UpdateDeployStatus(deployID, "configuring_proxy", ""); err != nil {
|
||||
slog.Warn("update deploy status", "error", err)
|
||||
}
|
||||
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "configuring_proxy", "")
|
||||
var npmProxyID int
|
||||
if stage.EnableProxy {
|
||||
if err := d.store.UpdateDeployStatus(deployID, "configuring_proxy", ""); err != nil {
|
||||
slog.Warn("update deploy status", "error", err)
|
||||
}
|
||||
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "configuring_proxy", "")
|
||||
|
||||
npmProxyID, err := d.configureProxy(ctx, deployID, settings, containerName, project.Port, subdomain)
|
||||
if err != nil {
|
||||
return containerID, 0, instanceID, fmt.Errorf("configure proxy: %w", err)
|
||||
npmProxyID, err = d.configureProxy(ctx, deployID, settings, containerName, project.Port, subdomain)
|
||||
if err != nil {
|
||||
return containerID, 0, instanceID, fmt.Errorf("configure proxy: %w", err)
|
||||
}
|
||||
|
||||
inst.NpmProxyID = npmProxyID
|
||||
d.logDeploy(deployID, "Blue-green: proxy swapped to green container", "info")
|
||||
} else {
|
||||
d.logDeploy(deployID, "Blue-green: proxy skipped (disabled for this stage)", "info")
|
||||
}
|
||||
|
||||
inst.NpmProxyID = npmProxyID
|
||||
inst.Subdomain = subdomain
|
||||
if err := d.store.UpdateInstance(inst); err != nil {
|
||||
slog.Warn("update instance with proxy ID", "error", err)
|
||||
}
|
||||
|
||||
d.logDeploy(deployID, "Blue-green: proxy swapped to green container", "info")
|
||||
|
||||
// Step 6: Stop the blue container.
|
||||
if blueInstance != nil {
|
||||
d.logDeploy(deployID, fmt.Sprintf("Blue-green: stopping blue instance %s (tag: %s)", blueInstance.ID, blueInstance.ImageTag), "info")
|
||||
|
||||
@@ -336,22 +336,30 @@ func (d *Deployer) executeDeploy(
|
||||
d.publishInstanceStatus(instanceID, project.ID, stage.ID, "running")
|
||||
d.logDeploy(deployID, "Container started", "info")
|
||||
|
||||
// Step 4: Configure NPM proxy.
|
||||
if err := d.store.UpdateDeployStatus(deployID, "configuring_proxy", ""); err != nil {
|
||||
slog.Warn("update deploy status", "error", err)
|
||||
}
|
||||
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "configuring_proxy", "")
|
||||
// Step 4: Configure NPM proxy (optional per stage).
|
||||
if stage.EnableProxy {
|
||||
if err := d.store.UpdateDeployStatus(deployID, "configuring_proxy", ""); err != nil {
|
||||
slog.Warn("update deploy status", "error", err)
|
||||
}
|
||||
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "configuring_proxy", "")
|
||||
|
||||
npmProxyID, err = d.configureProxy(ctx, deployID, settings, containerName, project.Port, subdomain)
|
||||
if err != nil {
|
||||
return containerID, npmProxyID, instanceID, fmt.Errorf("configure proxy: %w", err)
|
||||
}
|
||||
npmProxyID, err = d.configureProxy(ctx, deployID, settings, containerName, project.Port, subdomain)
|
||||
if err != nil {
|
||||
return containerID, npmProxyID, instanceID, fmt.Errorf("configure proxy: %w", err)
|
||||
}
|
||||
|
||||
// Update instance with NPM proxy ID.
|
||||
inst.NpmProxyID = npmProxyID
|
||||
inst.Subdomain = subdomain
|
||||
if err := d.store.UpdateInstance(inst); err != nil {
|
||||
slog.Warn("update instance with proxy ID", "error", err)
|
||||
// Update instance with NPM proxy ID.
|
||||
inst.NpmProxyID = npmProxyID
|
||||
inst.Subdomain = subdomain
|
||||
if err := d.store.UpdateInstance(inst); err != nil {
|
||||
slog.Warn("update instance with proxy ID", "error", err)
|
||||
}
|
||||
} else {
|
||||
d.logDeploy(deployID, "Proxy creation skipped (disabled for this stage)", "info")
|
||||
inst.Subdomain = subdomain
|
||||
if err := d.store.UpdateInstance(inst); err != nil {
|
||||
slog.Warn("update instance", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Health check.
|
||||
|
||||
@@ -23,6 +23,7 @@ type Stage struct {
|
||||
AutoDeploy bool `json:"auto_deploy"`
|
||||
MaxInstances int `json:"max_instances"`
|
||||
Confirm bool `json:"confirm"`
|
||||
EnableProxy bool `json:"enable_proxy"`
|
||||
PromoteFrom string `json:"promote_from"`
|
||||
Subdomain string `json:"subdomain"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
|
||||
+14
-13
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const stageColumns = `id, project_id, name, tag_pattern, auto_deploy, max_instances, confirm, enable_proxy, promote_from, subdomain, created_at, updated_at`
|
||||
|
||||
// CreateStage inserts a new stage for a project.
|
||||
func (s *Store) CreateStage(st Stage) (Stage, error) {
|
||||
st.ID = uuid.New().String()
|
||||
@@ -15,10 +17,9 @@ func (s *Store) CreateStage(st Stage) (Stage, error) {
|
||||
st.UpdatedAt = st.CreatedAt
|
||||
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO stages (id, project_id, name, tag_pattern, auto_deploy, max_instances, confirm, promote_from, subdomain, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
`INSERT INTO stages (`+stageColumns+`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
st.ID, st.ProjectID, st.Name, st.TagPattern, BoolToInt(st.AutoDeploy), st.MaxInstances,
|
||||
BoolToInt(st.Confirm), st.PromoteFrom, st.Subdomain, st.CreatedAt, st.UpdatedAt,
|
||||
BoolToInt(st.Confirm), BoolToInt(st.EnableProxy), st.PromoteFrom, st.Subdomain, st.CreatedAt, st.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return Stage{}, fmt.Errorf("insert stage: %w", err)
|
||||
@@ -29,8 +30,7 @@ func (s *Store) CreateStage(st Stage) (Stage, error) {
|
||||
// GetStagesByProjectID returns all stages for a given project.
|
||||
func (s *Store) GetStagesByProjectID(projectID string) ([]Stage, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, project_id, name, tag_pattern, auto_deploy, max_instances, confirm, promote_from, subdomain, created_at, updated_at
|
||||
FROM stages WHERE project_id = ? ORDER BY name`, projectID,
|
||||
`SELECT `+stageColumns+` FROM stages WHERE project_id = ? ORDER BY name`, projectID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query stages: %w", err)
|
||||
@@ -51,12 +51,11 @@ func (s *Store) GetStagesByProjectID(projectID string) ([]Stage, error) {
|
||||
// GetStageByID returns a single stage by its ID.
|
||||
func (s *Store) GetStageByID(id string) (Stage, error) {
|
||||
var st Stage
|
||||
var autoDeploy, confirm int
|
||||
var autoDeploy, confirm, enableProxy int
|
||||
err := s.db.QueryRow(
|
||||
`SELECT id, project_id, name, tag_pattern, auto_deploy, max_instances, confirm, promote_from, subdomain, created_at, updated_at
|
||||
FROM stages WHERE id = ?`, id,
|
||||
`SELECT `+stageColumns+` FROM stages WHERE id = ?`, id,
|
||||
).Scan(&st.ID, &st.ProjectID, &st.Name, &st.TagPattern, &autoDeploy, &st.MaxInstances,
|
||||
&confirm, &st.PromoteFrom, &st.Subdomain, &st.CreatedAt, &st.UpdatedAt)
|
||||
&confirm, &enableProxy, &st.PromoteFrom, &st.Subdomain, &st.CreatedAt, &st.UpdatedAt)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return Stage{}, fmt.Errorf("stage %s: %w", id, ErrNotFound)
|
||||
}
|
||||
@@ -65,6 +64,7 @@ func (s *Store) GetStageByID(id string) (Stage, error) {
|
||||
}
|
||||
st.AutoDeploy = autoDeploy != 0
|
||||
st.Confirm = confirm != 0
|
||||
st.EnableProxy = enableProxy != 0
|
||||
return st, nil
|
||||
}
|
||||
|
||||
@@ -72,10 +72,10 @@ func (s *Store) GetStageByID(id string) (Stage, error) {
|
||||
func (s *Store) UpdateStage(st Stage) error {
|
||||
st.UpdatedAt = Now()
|
||||
result, err := s.db.Exec(
|
||||
`UPDATE stages SET name=?, tag_pattern=?, auto_deploy=?, max_instances=?, confirm=?, promote_from=?, subdomain=?, updated_at=?
|
||||
`UPDATE stages SET name=?, tag_pattern=?, auto_deploy=?, max_instances=?, confirm=?, enable_proxy=?, promote_from=?, subdomain=?, updated_at=?
|
||||
WHERE id=?`,
|
||||
st.Name, st.TagPattern, BoolToInt(st.AutoDeploy), st.MaxInstances,
|
||||
BoolToInt(st.Confirm), st.PromoteFrom, st.Subdomain, st.UpdatedAt, st.ID,
|
||||
BoolToInt(st.Confirm), BoolToInt(st.EnableProxy), st.PromoteFrom, st.Subdomain, st.UpdatedAt, st.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update stage: %w", err)
|
||||
@@ -111,13 +111,14 @@ func BoolToInt(b bool) int {
|
||||
// scanStage scans a stage row from a *sql.Rows cursor.
|
||||
func scanStage(rows *sql.Rows) (Stage, error) {
|
||||
var st Stage
|
||||
var autoDeploy, confirm int
|
||||
var autoDeploy, confirm, enableProxy int
|
||||
err := rows.Scan(&st.ID, &st.ProjectID, &st.Name, &st.TagPattern, &autoDeploy, &st.MaxInstances,
|
||||
&confirm, &st.PromoteFrom, &st.Subdomain, &st.CreatedAt, &st.UpdatedAt)
|
||||
&confirm, &enableProxy, &st.PromoteFrom, &st.Subdomain, &st.CreatedAt, &st.UpdatedAt)
|
||||
if err != nil {
|
||||
return Stage{}, fmt.Errorf("scan stage: %w", err)
|
||||
}
|
||||
st.AutoDeploy = autoDeploy != 0
|
||||
st.Confirm = confirm != 0
|
||||
st.EnableProxy = enableProxy != 0
|
||||
return st, nil
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@ func (s *Store) runMigrations() error {
|
||||
`ALTER TABLE registries ADD COLUMN owner TEXT NOT NULL DEFAULT ''`,
|
||||
// Add base_volume_path to settings (2026-03-28).
|
||||
`ALTER TABLE settings ADD COLUMN base_volume_path TEXT NOT NULL DEFAULT ''`,
|
||||
// Add enable_proxy to stages (2026-03-29). Default true for backwards compat.
|
||||
`ALTER TABLE stages ADD COLUMN enable_proxy INTEGER NOT NULL DEFAULT 1`,
|
||||
}
|
||||
|
||||
for _, m := range migrations {
|
||||
@@ -126,6 +128,7 @@ CREATE TABLE IF NOT EXISTS stages (
|
||||
auto_deploy INTEGER NOT NULL DEFAULT 0,
|
||||
max_instances INTEGER NOT NULL DEFAULT 1,
|
||||
confirm INTEGER NOT NULL DEFAULT 0,
|
||||
enable_proxy INTEGER NOT NULL DEFAULT 1,
|
||||
promote_from TEXT NOT NULL DEFAULT '',
|
||||
subdomain TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
|
||||
Reference in New Issue
Block a user