Check / check (pull_request) Skipped
App names may now contain dots, such as sneak.berlin: runs of lowercase letters and numbers joined by single dots or by hyphens, 2 to 63 characters. Docker accepts every such name in the app's image name, upaas-<name>; a dot needs a letter or number on both sides because Docker requires it. The rule also keeps a dot off either end, so the name is safe as a directory and log file name. The new and edit app forms use the same pattern. Their old one was not a valid regular expression under the flag browsers compile it with, so browsers ignored it. A test creates an app named sneak.berlin through the new app form, then builds and deploys it against the fake Docker API and checks the image and container names with Docker's own rules. Model: opus-5-5
122 lines
3.4 KiB
Go
122 lines
3.4 KiB
Go
package deploy_test
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/distribution/reference"
|
|
"github.com/docker/docker/daemon/names"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"sneak.berlin/go/upaas/internal/database"
|
|
"sneak.berlin/go/upaas/internal/globals"
|
|
"sneak.berlin/go/upaas/internal/handlers"
|
|
"sneak.berlin/go/upaas/internal/logger"
|
|
"sneak.berlin/go/upaas/internal/models"
|
|
"sneak.berlin/go/upaas/internal/service/app"
|
|
)
|
|
|
|
// TestDeployAppWithDotInName creates an app named sneak.berlin through the
|
|
// new app form, as a user does, then builds and deploys it against a fake
|
|
// Docker API and checks that Docker accepts the names of the image it
|
|
// builds and of the container it runs, using Docker's own rules for each.
|
|
func TestDeployAppWithDotInName(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
api := &fakeImageAPI{
|
|
images: map[string][]string{},
|
|
shortSHA: "abc1234",
|
|
nextID: "sha256:built",
|
|
}
|
|
svc, db := newImageTestService(t, api)
|
|
ctx := context.Background()
|
|
|
|
createdApp := createAppWithForm(t, db, "sneak.berlin")
|
|
|
|
deployment := models.NewDeployment(db)
|
|
deployment.AppID = createdApp.ID
|
|
require.NoError(t, deployment.Save(ctx))
|
|
|
|
imageID, err := svc.BuildImage(ctx, createdApp, deployment)
|
|
require.NoError(t, err)
|
|
require.NoError(t, svc.DeployContainer(ctx, createdApp, deployment, imageID))
|
|
|
|
images, _ := api.state()
|
|
|
|
api.mu.Lock()
|
|
containers := slices.Clone(api.containers)
|
|
api.mu.Unlock()
|
|
|
|
require.Equal(t, map[string][]string{
|
|
"sha256:built": {"upaas-sneak.berlin:abc1234"},
|
|
}, images)
|
|
|
|
_, err = reference.ParseNormalizedNamed("upaas-sneak.berlin:abc1234")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, []string{"upaas-sneak.berlin"}, containers)
|
|
assert.Regexp(t, names.RestrictedNamePattern, containers[0])
|
|
|
|
assert.DirExists(t, svc.GetBuildDirExported(createdApp.Name))
|
|
}
|
|
|
|
// createAppWithForm posts the new app form with the given name, which
|
|
// checks the name as it does for a user, and returns the app it created.
|
|
func createAppWithForm(
|
|
t *testing.T,
|
|
db *database.Database,
|
|
name string,
|
|
) *models.App {
|
|
t.Helper()
|
|
|
|
log := logger.NewForTest(slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
|
|
|
appSvc, err := app.New(nil, app.ServiceParams{Logger: log, Database: db})
|
|
require.NoError(t, err)
|
|
|
|
globalInstance, err := globals.New(nil)
|
|
require.NoError(t, err)
|
|
|
|
handlersInstance, err := handlers.New(nil, handlers.Params{
|
|
Logger: log,
|
|
Globals: globalInstance,
|
|
Database: db,
|
|
App: appSvc,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
form := url.Values{
|
|
"name": {name},
|
|
"repo_url": {"git@example.com:sneak/" + name + ".git"},
|
|
}
|
|
request := httptest.NewRequestWithContext(
|
|
t.Context(), http.MethodPost, "/apps", strings.NewReader(form.Encode()),
|
|
)
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handlersInstance.HandleAppCreate().ServeHTTP(recorder, request)
|
|
|
|
// The form redirects to the new app's page; it shows the form again,
|
|
// with the error, when it refuses the name.
|
|
require.Equal(t, http.StatusSeeOther, recorder.Code, recorder.Body.String())
|
|
|
|
appID, found := strings.CutPrefix(recorder.Header().Get("Location"), "/apps/")
|
|
require.True(t, found)
|
|
|
|
createdApp, err := models.FindApp(t.Context(), db, appID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdApp)
|
|
|
|
return createdApp
|
|
}
|