Builds are tagged upaas-<app>:<short hash>, git's own short form of the commit checked out, instead of the deployment number. A redeploy of a commit gives the tag to the new image. The cleanup after a deploy now also finds the app's untagged images among those its deployments recorded, and removes them by ID once the app neither runs them nor would roll back to them. It keeps every image any app uses, since apps that build the same commit can share one. The clone reads the commits from git's output after removing Docker's log headers, which had hidden the COMMIT: line; commit_sha is now saved on update so manual deploys keep the commit they recorded. Model: opus-5-5
94 lines
2.9 KiB
Go
94 lines
2.9 KiB
Go
package deploy_test
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"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"
|
|
)
|
|
|
|
// TestBuildImageLogsBuildErrorBeforeDeployError runs a build that fails
|
|
// against a fake Docker API and checks that the deploy fails with the
|
|
// build's own error, which the deployment log shows before the deploy's.
|
|
func TestBuildImageLogsBuildErrorBeforeDeployError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, "/containers/create"):
|
|
_, _ = w.Write([]byte(`{"Id":"gitcontainer"}`))
|
|
case strings.HasSuffix(r.URL.Path, "/logs"):
|
|
writeCloneOutput(w, "abc1234")
|
|
case strings.HasSuffix(r.URL.Path, "/version"):
|
|
_, _ = w.Write([]byte(`{"Version":"27.3.1","ApiVersion":"1.47"}`))
|
|
case strings.HasSuffix(r.URL.Path, "/build"):
|
|
_, _ = w.Write([]byte(`{"stream":"Step 1/1 : RUN false\n"}` + "\n" +
|
|
`{"errorDetail":{"message":"exit code: 1"},"error":"exit code: 1"}`))
|
|
default:
|
|
// The steps of the git clone, which succeeds.
|
|
_, _ = 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 = "buildapp-id"
|
|
app.Name = "buildapp"
|
|
app.Branch = "main"
|
|
require.NoError(t, app.Save(ctx))
|
|
|
|
deployment := models.NewDeployment(db)
|
|
deployment.AppID = app.ID
|
|
require.NoError(t, deployment.Save(ctx))
|
|
|
|
dataDir := t.TempDir()
|
|
cfg := &config.Config{DataDir: dataDir, HostDataDir: dataDir}
|
|
|
|
// The service has no notify service: the app has no ntfy topic and no
|
|
// Slack webhook, so the build failure notification sends nothing.
|
|
svc := deploy.NewTestServiceWithConfig(log, cfg, db, dockerClient)
|
|
|
|
_, err = svc.BuildImage(ctx, app, deployment)
|
|
require.EqualError(t, err, "failed to build image: exit code: 1")
|
|
|
|
logs := deployment.Logs.String
|
|
buildError := strings.Index(logs, "ERROR: exit code: 1")
|
|
deployError := strings.Index(logs, "ERROR: failed to build image: exit code: 1")
|
|
|
|
require.NotEqual(t, -1, buildError, logs)
|
|
assert.Less(t, buildError, deployError, logs)
|
|
}
|