Every deploy built and tagged a new image and nothing removed the old ones, so disk use grew with each push. After a successful deploy, upaas now removes the app's old `upaas-<app>:<N>` tags by name, without force, keeping the current image and the previous one that rollback starts. Docker deletes an image only when no other tag or container still uses it, so an image shared with another app stays. A failed removal is a warning in the deployment log, not a failed deploy. The post-deploy step is one function, tested against a fake Docker API. Judgement call: untagged images from other stages of a multi-stage build stay; they are the build cache. On the first deploy after upgrading, all older images of that app are removed at once. Model: opus-5-5
107 lines
2.9 KiB
Go
107 lines
2.9 KiB
Go
package deploy_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/fx/fxtest"
|
|
|
|
"sneak.berlin/go/upaas/internal/config"
|
|
"sneak.berlin/go/upaas/internal/database"
|
|
"sneak.berlin/go/upaas/internal/docker"
|
|
"sneak.berlin/go/upaas/internal/logger"
|
|
"sneak.berlin/go/upaas/internal/models"
|
|
"sneak.berlin/go/upaas/internal/service/deploy"
|
|
)
|
|
|
|
// TestRecordDeployedImageRemovesOldImages runs the step after a deploy
|
|
// against a fake Docker API. Image one is also tagged for another app,
|
|
// image two was the previous image, three the current one, four is new.
|
|
func TestRecordDeployedImageRemovesOldImages(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
mu sync.Mutex
|
|
removed []string
|
|
forced bool
|
|
)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
switch {
|
|
case r.Method == http.MethodDelete:
|
|
_, name, _ := strings.Cut(r.URL.Path, "/images/")
|
|
|
|
mu.Lock()
|
|
|
|
removed = append(removed, name)
|
|
forced = forced || r.URL.Query().Get("force") != ""
|
|
mu.Unlock()
|
|
|
|
_, _ = w.Write([]byte(`[]`))
|
|
case strings.HasSuffix(r.URL.Path, "/images/json"):
|
|
_, _ = w.Write([]byte(`[
|
|
{"Id":"sha256:one","RepoTags":["upaas-myapp:1","upaas-otherapp:7"]},
|
|
{"Id":"sha256:two","RepoTags":["upaas-myapp:2"]},
|
|
{"Id":"sha256:three","RepoTags":["upaas-myapp:3"]},
|
|
{"Id":"sha256:four","RepoTags":["upaas-myapp:4"]}
|
|
]`))
|
|
default:
|
|
_, _ = w.Write([]byte(`{}`))
|
|
}
|
|
},
|
|
))
|
|
t.Cleanup(srv.Close)
|
|
|
|
log := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
|
lifecycle := fxtest.NewLifecycle(t)
|
|
|
|
dockerClient, err := docker.New(lifecycle, docker.Params{
|
|
Logger: logger.NewForTest(log),
|
|
Config: &config.Config{DockerHost: "tcp://" + srv.Listener.Addr().String()},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
lifecycle.RequireStart()
|
|
t.Cleanup(lifecycle.RequireStop)
|
|
|
|
db := database.NewTestDatabase(t)
|
|
ctx := context.Background()
|
|
|
|
app := models.NewApp(db)
|
|
app.ID = "myapp-id"
|
|
app.Name = "myapp"
|
|
app.ImageID = sql.NullString{String: "sha256:three", Valid: true}
|
|
app.PreviousImageID = sql.NullString{String: "sha256:two", Valid: true}
|
|
require.NoError(t, app.Save(ctx))
|
|
|
|
deployment := models.NewDeployment(db)
|
|
deployment.AppID = app.ID
|
|
require.NoError(t, deployment.Save(ctx))
|
|
|
|
svc := deploy.NewTestServiceWithConfig(log, &config.Config{}, dockerClient)
|
|
|
|
err = svc.RecordDeployedImage(ctx, app, deployment, "sha256:four")
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "sha256:four", app.ImageID.String)
|
|
assert.Equal(t, "sha256:three", app.PreviousImageID.String)
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
assert.Equal(t, []string{"upaas-myapp:1", "upaas-myapp:2"}, removed)
|
|
assert.False(t, forced, "old image tags must be removed without force")
|
|
}
|