feat: volume scopes redesign — replace shared/isolated with 6 scopes

Replace confusing shared/isolated volume modes with explicit scopes:
- instance: per-deploy isolated directory
- stage: shared within a stage across deploys
- project: shared across all stages
- project_named: named group within a project
- named: global named volume across projects
- ephemeral: tmpfs in-memory mount

Includes schema migration (shared→project, isolated→instance),
backward-compatible deployer resolution, scope metadata API endpoint,
and redesigned volume editor UI with scope guide cards and hints.
This commit is contained in:
2026-03-31 23:22:43 +03:00
parent 1a8dfefa77
commit 8fb959f81f
12 changed files with 424 additions and 112 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ func (d *Deployer) blueGreenDeploy(
containerName := docker.ContainerName(project.Name, stage.Name, imageTag)
portStr := fmt.Sprintf("%d/tcp", project.Port)
envVars := d.mergeEnvVars(project, stage.ID)
mounts := d.computeVolumeMounts(project.ID, stage.Name, imageTag, settings.BaseVolumePath)
mounts := d.computeVolumeMounts(project.ID, project.Name, stage.Name, imageTag, settings.BaseVolumePath)
containerCfg := docker.ContainerConfig{
Name: containerName,
+45 -10
View File
@@ -282,7 +282,7 @@ func (d *Deployer) executeDeploy(
containerName := docker.ContainerName(project.Name, stage.Name, imageTag)
portStr := fmt.Sprintf("%d/tcp", project.Port)
envVars := d.mergeEnvVars(project, stage.ID)
mounts := d.computeVolumeMounts(project.ID, stage.Name, imageTag, settings.BaseVolumePath)
mounts := d.computeVolumeMounts(project.ID, project.Name, stage.Name, imageTag, settings.BaseVolumePath)
containerCfg := docker.ContainerConfig{
Name: containerName,
@@ -619,9 +619,14 @@ func (d *Deployer) mergeEnvVars(project store.Project, stageID string) []string
}
// computeVolumeMounts builds Docker mount specifications from the project's volume config.
// For shared mode, source is used as-is.
// For isolated mode, source gets /{stage}-{tag}/ appended.
func (d *Deployer) computeVolumeMounts(projectID, stageName, imageTag, basePath string) []mount.Mount {
// Resolves the host path based on the volume's scope:
// - instance: {base}/{project}/{stage}-{tag}/{source}
// - stage: {base}/{project}/{stage}/{source}
// - project: {base}/{project}/{source}
// - project_named: {base}/{project}/_named/{name}/{source}
// - named: {base}/_named/{name}/{source}
// - ephemeral: tmpfs mount (no host path)
func (d *Deployer) computeVolumeMounts(projectID, projectName, stageName, imageTag, basePath string) []mount.Mount {
vols, err := d.store.GetVolumesByProjectID(projectID)
if err != nil {
slog.Warn("get project volumes", "project_id", projectID, "error", err)
@@ -634,14 +639,44 @@ func (d *Deployer) computeVolumeMounts(projectID, stageName, imageTag, basePath
mounts := make([]mount.Mount, 0, len(vols))
for _, vol := range vols {
source := vol.Source
// Prepend base path if source is relative (doesn't start with /).
if basePath != "" && !filepath.IsAbs(source) {
source = filepath.Join(basePath, source)
// Resolve scope — use Scope field, fall back to Mode for backward compat.
scope := vol.Scope
if scope == "" {
switch vol.Mode {
case "isolated":
scope = "instance"
default:
scope = "project"
}
}
if vol.Mode == "isolated" {
source = filepath.Join(source, fmt.Sprintf("%s-%s", stageName, imageTag))
// Ephemeral volumes use tmpfs — no host path.
if scope == "ephemeral" {
mounts = append(mounts, mount.Mount{
Type: mount.TypeTmpfs,
Target: vol.Target,
})
continue
}
// Build host path based on scope.
var source string
switch scope {
case "instance":
source = filepath.Join(basePath, projectName, fmt.Sprintf("%s-%s", stageName, imageTag), vol.Source)
case "stage":
source = filepath.Join(basePath, projectName, stageName, vol.Source)
case "project":
source = filepath.Join(basePath, projectName, vol.Source)
case "project_named":
source = filepath.Join(basePath, projectName, "_named", vol.Name, vol.Source)
case "named":
source = filepath.Join(basePath, "_named", vol.Name, vol.Source)
default:
// Fallback: treat as project scope.
source = filepath.Join(basePath, projectName, vol.Source)
}
mounts = append(mounts, mount.Mount{
Type: mount.TypeBind,
Source: source,