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. 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
|
|
}
|