Keep deployment logs findable after the container is recreated (closes #214)

Deployment logs were stored under a directory named after the container's hostname, and the path was worked out again from the current hostname on download. Docker gives a recreated container a new hostname, so every older log download returned 404. New logs now go to `logs/<appname>/` with no hostname in the path. Logs written by older versions under an old hostname directory are still found by looking one directory deeper, inside the same confined log root. Tests cover both the hostname-free path and downloading an old-layout log.

Model: opus-5-5
This commit was merged in pull request #218.
This commit is contained in:
2026-09-23 11:44:30 +02:00
parent 19619b1cd2
commit ddcd179841
4 changed files with 81 additions and 8 deletions
+50
View File
@@ -69,6 +69,56 @@ func TestHandleDeploymentLogDownloadServesLegitimateFile(t *testing.T) {
assert.Contains(t, recorder.Body.String(), "deploy log contents")
}
// TestGetLogFilePathHasNoHostname verifies a deployment log is stored
// directly under logs/<appname>/, with no hostname directory in between,
// so it is still found after the container is recreated with a new
// hostname.
func TestGetLogFilePathHasNoHostname(t *testing.T) {
t.Parallel()
testCtx := setupTestHandlers(t)
createdApp := createTestApp(t, testCtx, "log-path-app")
deployment := models.NewDeployment(testCtx.database)
deployment.AppID = createdApp.ID
logPath := testCtx.deploySvc.GetLogFilePath(createdApp, deployment)
assert.Equal(t,
filepath.Join(testCtx.deploySvc.GetLogDir(), createdApp.Name),
filepath.Dir(logPath),
)
}
// TestHandleDeploymentLogDownloadServesLogFromOldHostnameDir verifies a
// log written by an older version, under the hostname of a container that
// has since been recreated, can still be downloaded.
func TestHandleDeploymentLogDownloadServesLogFromOldHostnameDir(t *testing.T) {
t.Parallel()
testCtx := setupTestHandlers(t)
createdApp := createTestApp(t, testCtx, "log-old-hostname-app")
deployment := models.NewDeployment(testCtx.database)
deployment.AppID = createdApp.ID
deployment.Status = models.DeploymentStatusSuccess
require.NoError(t, deployment.Save(context.Background()))
logDir := testCtx.deploySvc.GetLogDir()
newPath := testCtx.deploySvc.GetLogFilePath(createdApp, deployment)
relPath, relErr := filepath.Rel(logDir, newPath)
require.NoError(t, relErr)
oldPath := filepath.Join(logDir, "old-container-hostname", relPath)
require.NoError(t, os.MkdirAll(filepath.Dir(oldPath), 0o750))
require.NoError(t, os.WriteFile(oldPath, []byte("old deploy log contents"), 0o600))
recorder := doLogDownload(t, testCtx, createdApp.ID, deployment.ID)
assert.Equal(t, http.StatusOK, recorder.Code)
assert.Contains(t, recorder.Body.String(), "old deploy log contents")
}
// TestHandleDeploymentLogDownloadRejectsPathTraversal verifies the
// os.Root containment guard. A traversal-shaped app name drives the
// resolved log path out of the deploy log directory onto a sentinel