Compare commits
2
Commits
112cbfae1b
...
487b77d9d6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
487b77d9d6 | ||
|
|
5168db69d9 |
+12
-20
@@ -115,15 +115,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Appname string = "CHANGEME"
|
Appname string = "CHANGEME"
|
||||||
Version string
|
Version string
|
||||||
Buildarch string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
globals.Appname = Appname
|
globals.Appname = Appname
|
||||||
globals.Version = Version
|
globals.Version = Version
|
||||||
globals.Buildarch = Buildarch
|
|
||||||
|
|
||||||
fx.New(
|
fx.New(
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
@@ -823,7 +821,7 @@ func (l *Logger) Identify() {
|
|||||||
l.log.Info("starting",
|
l.log.Info("starting",
|
||||||
"appname", l.params.Globals.Appname,
|
"appname", l.params.Globals.Appname,
|
||||||
"version", l.params.Globals.Version,
|
"version", l.params.Globals.Version,
|
||||||
"buildarch", l.params.Globals.Buildarch,
|
"arch", runtime.GOARCH,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -943,23 +941,20 @@ import "go.uber.org/fx"
|
|||||||
|
|
||||||
// Package-level variables (set from main)
|
// Package-level variables (set from main)
|
||||||
var (
|
var (
|
||||||
Appname string
|
Appname string
|
||||||
Version string
|
Version string
|
||||||
Buildarch string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Struct for DI
|
// Struct for DI
|
||||||
type Globals struct {
|
type Globals struct {
|
||||||
Appname string
|
Appname string
|
||||||
Version string
|
Version string
|
||||||
Buildarch string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(lc fx.Lifecycle) (*Globals, error) {
|
func New(lc fx.Lifecycle) (*Globals, error) {
|
||||||
n := &Globals{
|
n := &Globals{
|
||||||
Appname: Appname,
|
Appname: Appname,
|
||||||
Buildarch: Buildarch,
|
Version: Version,
|
||||||
Version: Version,
|
|
||||||
}
|
}
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
@@ -970,15 +965,13 @@ func New(lc fx.Lifecycle) (*Globals, error) {
|
|||||||
```go
|
```go
|
||||||
// cmd/httpd/main.go
|
// cmd/httpd/main.go
|
||||||
var (
|
var (
|
||||||
Appname string = "CHANGEME" // Default, overridden by build
|
Appname string = "CHANGEME" // Default, overridden by build
|
||||||
Version string // Set at build time
|
Version string // Set at build time
|
||||||
Buildarch string // Set at build time
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
globals.Appname = Appname
|
globals.Appname = Appname
|
||||||
globals.Version = Version
|
globals.Version = Version
|
||||||
globals.Buildarch = Buildarch
|
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -989,10 +982,9 @@ Use ldflags to inject version information at build time:
|
|||||||
|
|
||||||
```makefile
|
```makefile
|
||||||
VERSION := $(shell git describe --tags --always)
|
VERSION := $(shell git describe --tags --always)
|
||||||
BUILDARCH := $(shell go env GOARCH)
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build -ldflags "-X main.Version=$(VERSION) -X main.Buildarch=$(BUILDARCH)" ./cmd/httpd
|
go build -ldflags "-X main.Version=$(VERSION)" ./cmd/httpd
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
BINARY := upaasd
|
BINARY := upaasd
|
||||||
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||||
BUILDARCH := $(shell go env GOARCH)
|
LDFLAGS := -X main.Version=$(VERSION)
|
||||||
LDFLAGS := -X main.Version=$(VERSION) -X main.Buildarch=$(BUILDARCH)
|
|
||||||
|
|
||||||
all: check build
|
all: check build
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,18 @@ regress.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-02: App names may contain dots, such as `sneak.berlin`: lowercase
|
||||||
|
letters and numbers joined by single dots or by hyphens, 2 to 63 characters.
|
||||||
|
Docker accepts every such name in the image name `upaas-<name>`; a dot needs a
|
||||||
|
letter or number on both sides because Docker requires it. The new and edit
|
||||||
|
app forms check the same rule; browsers ignored their old pattern, which was
|
||||||
|
not a valid regular expression there (#260).
|
||||||
|
|
||||||
|
- 2026-10-02: The build no longer passes the CPU architecture in: upaas reads it
|
||||||
|
from Go's `runtime.GOARCH` when it runs, and the startup log line reports it
|
||||||
|
as `arch`. `CONVENTIONS.md` follows the updated conventions in `sneak/prompts`
|
||||||
|
(#259).
|
||||||
|
|
||||||
- 2026-10-01: Built images are tagged `upaas-<app>:<short hash>`, git's short
|
- 2026-10-01: Built images are tagged `upaas-<app>:<short hash>`, git's short
|
||||||
form of the commit built, instead of the deployment number. A redeploy of a
|
form of the commit built, instead of the deployment number. A redeploy of a
|
||||||
commit gives its tag to the new image; the old one is kept while the app runs
|
commit gives its tag to the new image; the old one is kept while the app runs
|
||||||
|
|||||||
+2
-4
@@ -25,15 +25,13 @@ import (
|
|||||||
// Build-time variables injected by linker flags (-ldflags).
|
// Build-time variables injected by linker flags (-ldflags).
|
||||||
// These must be exported package-level variables for the build system.
|
// These must be exported package-level variables for the build system.
|
||||||
var (
|
var (
|
||||||
Appname = "upaas" //nolint:gochecknoglobals // build-time variable
|
Appname = "upaas" //nolint:gochecknoglobals // build-time variable
|
||||||
Version string //nolint:gochecknoglobals // build-time variable
|
Version string //nolint:gochecknoglobals // build-time variable
|
||||||
Buildarch string //nolint:gochecknoglobals // build-time variable
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
globals.SetAppname(Appname)
|
globals.SetAppname(Appname)
|
||||||
globals.SetVersion(Version)
|
globals.SetVersion(Version)
|
||||||
globals.SetBuildarch(Buildarch)
|
|
||||||
|
|
||||||
fx.New(
|
fx.New(
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ go 1.25
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/basicauth-go v0.0.0-20230316000542-bf6f9cbbf0f8
|
github.com/99designs/basicauth-go v0.0.0-20230316000542-bf6f9cbbf0f8
|
||||||
|
github.com/distribution/reference v0.6.0
|
||||||
github.com/docker/docker v27.3.1+incompatible
|
github.com/docker/docker v27.3.1+incompatible
|
||||||
github.com/docker/go-connections v0.6.0
|
github.com/docker/go-connections v0.6.0
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
@@ -39,7 +40,6 @@ require (
|
|||||||
github.com/containerd/ttrpc v1.2.5 // indirect
|
github.com/containerd/ttrpc v1.2.5 // indirect
|
||||||
github.com/containerd/typeurl/v2 v2.2.0 // indirect
|
github.com/containerd/typeurl/v2 v2.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
|||||||
@@ -12,17 +12,15 @@ import (
|
|||||||
//
|
//
|
||||||
//nolint:gochecknoglobals // Required for ldflags injection at build time
|
//nolint:gochecknoglobals // Required for ldflags injection at build time
|
||||||
var (
|
var (
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
appname string
|
appname string
|
||||||
version string
|
version string
|
||||||
buildarch string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Globals holds build-time variables for dependency injection.
|
// Globals holds build-time variables for dependency injection.
|
||||||
type Globals struct {
|
type Globals struct {
|
||||||
Appname string
|
Appname string
|
||||||
Version string
|
Version string
|
||||||
Buildarch string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Globals instance from package-level variables.
|
// New creates a new Globals instance from package-level variables.
|
||||||
@@ -31,9 +29,8 @@ func New(_ fx.Lifecycle) (*Globals, error) {
|
|||||||
defer mu.RUnlock()
|
defer mu.RUnlock()
|
||||||
|
|
||||||
return &Globals{
|
return &Globals{
|
||||||
Appname: appname,
|
Appname: appname,
|
||||||
Version: version,
|
Version: version,
|
||||||
Buildarch: buildarch,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,11 +49,3 @@ func SetVersion(ver string) {
|
|||||||
|
|
||||||
version = ver
|
version = ver
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetBuildarch sets the build architecture (used for testing and main init).
|
|
||||||
func SetBuildarch(arch string) {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
buildarch = arch
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ const (
|
|||||||
appNameMaxLength = 63
|
appNameMaxLength = 63
|
||||||
)
|
)
|
||||||
|
|
||||||
// validAppNameRe matches names containing only lowercase alphanumeric characters and
|
// validAppNameRe matches runs of lowercase letters and digits joined by
|
||||||
// hyphens, starting and ending with an alphanumeric character.
|
// single dots or by hyphens, such as "my-app" or "sneak.berlin". Docker
|
||||||
var validAppNameRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*[a-z0-9]$`)
|
// accepts every name it allows as the app's image name, upaas-<name>; a
|
||||||
|
// dot needs a letter or digit on both sides because Docker requires it.
|
||||||
|
// It also keeps the name from being "." or ".." or starting or ending
|
||||||
|
// with a dot, so it is safe as a directory and file name. The pattern
|
||||||
|
// attribute of the name field on the new and edit app forms is the same.
|
||||||
|
var validAppNameRe = regexp.MustCompile(`^[a-z0-9]+((\.|-+)[a-z0-9]+)*$`)
|
||||||
|
|
||||||
// validateAppName checks that the given app name is safe for use in Docker
|
|
||||||
// container names, image tags, and file system paths.
|
|
||||||
var (
|
var (
|
||||||
errAppNameLength = errors.New(
|
errAppNameLength = errors.New(
|
||||||
"app name must be between " +
|
"app name must be between " +
|
||||||
@@ -26,11 +29,14 @@ var (
|
|||||||
strconv.Itoa(appNameMaxLength) + " characters",
|
strconv.Itoa(appNameMaxLength) + " characters",
|
||||||
)
|
)
|
||||||
errAppNamePattern = errors.New(
|
errAppNamePattern = errors.New(
|
||||||
"app name must contain only lowercase letters, numbers, " +
|
"app name must contain only lowercase letters, numbers, hyphens, " +
|
||||||
"and hyphens, and must start and end with a letter or number",
|
"and dots, must start and end with a letter or number, " +
|
||||||
|
"and must have a letter or number on both sides of each dot",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// validateAppName checks that the given app name is safe for use in Docker
|
||||||
|
// container names, image tags, and file system paths.
|
||||||
func validateAppName(name string) error {
|
func validateAppName(name string) error {
|
||||||
if len(name) < appNameMinLength || len(name) > appNameMaxLength {
|
if len(name) < appNameMinLength || len(name) > appNameMaxLength {
|
||||||
return errAppNameLength
|
return errAppNameLength
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ func TestValidateAppName(t *testing.T) {
|
|||||||
{"valid two chars", "ab", false},
|
{"valid two chars", "ab", false},
|
||||||
{"valid complex", "my-cool-app-v2", false},
|
{"valid complex", "my-cool-app-v2", false},
|
||||||
{"valid all numbers", "123", false},
|
{"valid all numbers", "123", false},
|
||||||
|
{"valid double hyphen", "my--app", false},
|
||||||
|
{"valid domain", "sneak.berlin", false},
|
||||||
|
{"valid two dots", "www.sneak.berlin", false},
|
||||||
|
{"valid dot and hyphen", "my-app.example.com", false},
|
||||||
{"empty", "", true},
|
{"empty", "", true},
|
||||||
{"single char", "a", true},
|
{"single char", "a", true},
|
||||||
{"too long", "a" + string(make([]byte, 63)), true},
|
{"too long", "a" + string(make([]byte, 63)), true},
|
||||||
@@ -36,7 +40,12 @@ func TestValidateAppName(t *testing.T) {
|
|||||||
{"starts with hyphen", "-myapp", true},
|
{"starts with hyphen", "-myapp", true},
|
||||||
{"ends with hyphen", "myapp-", true},
|
{"ends with hyphen", "myapp-", true},
|
||||||
{"underscore", "my_app", true},
|
{"underscore", "my_app", true},
|
||||||
{"dot", "my.app", true},
|
{"two dots in a row", "a..b", true},
|
||||||
|
{"starts with dot", ".a", true},
|
||||||
|
{"ends with dot", "a.", true},
|
||||||
|
{"only dots", "..", true},
|
||||||
|
{"hyphen before dot", "a-.b", true},
|
||||||
|
{"hyphen after dot", "a.-b", true},
|
||||||
{"slash", "my/app", true},
|
{"slash", "my/app", true},
|
||||||
{"path traversal", "../etc/passwd", true},
|
{"path traversal", "../etc/passwd", true},
|
||||||
{"special chars", "app@name!", true},
|
{"special chars", "app@name!", true},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package logger
|
|||||||
import (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
|
|
||||||
@@ -81,6 +82,6 @@ func (l *Logger) Identify() {
|
|||||||
l.log.Info("starting",
|
l.log.Info("starting",
|
||||||
"appname", l.params.Globals.Appname,
|
"appname", l.params.Globals.Appname,
|
||||||
"version", l.params.Globals.Version,
|
"version", l.params.Globals.Version,
|
||||||
"buildarch", l.params.Globals.Buildarch,
|
"arch", runtime.GOARCH,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package logger //nolint:testpackage // sets the unexported log and params fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"sneak.berlin/go/upaas/internal/globals"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIdentifyLogsArchitectureFromRuntime(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
l := &Logger{
|
||||||
|
log: slog.New(slog.NewJSONHandler(&buf, nil)),
|
||||||
|
params: Params{
|
||||||
|
Globals: &globals.Globals{Appname: "upaas-test", Version: "test"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Identify()
|
||||||
|
|
||||||
|
var line map[string]any
|
||||||
|
|
||||||
|
require.NoError(t, json.Unmarshal(buf.Bytes(), &line))
|
||||||
|
assert.Equal(t, runtime.GOARCH, line["arch"])
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package deploy_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
"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/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestDeployAppWithDotInName deploys an app named sneak.berlin 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()
|
||||||
|
|
||||||
|
app := saveApp(t, db, "sneak.berlin", "", "")
|
||||||
|
|
||||||
|
deployment := models.NewDeployment(db)
|
||||||
|
deployment.AppID = app.ID
|
||||||
|
require.NoError(t, deployment.Save(ctx))
|
||||||
|
|
||||||
|
imageID, err := svc.BuildImage(ctx, app, deployment)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, svc.DeployContainer(ctx, app, 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(app.Name))
|
||||||
|
}
|
||||||
@@ -35,12 +35,13 @@ import (
|
|||||||
// an image's last tag, or an untagged image by its ID, deletes the image.
|
// an image's last tag, or an untagged image by its ID, deletes the image.
|
||||||
// It also answers the steps of a git clone that reports shortSHA.
|
// It also answers the steps of a git clone that reports shortSHA.
|
||||||
type fakeImageAPI struct {
|
type fakeImageAPI struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
images map[string][]string // image ID -> tags
|
images map[string][]string // image ID -> tags
|
||||||
shortSHA string // the commit's short hash the clone reports
|
shortSHA string // the commit's short hash the clone reports
|
||||||
nextID string // ID of the image the next build creates
|
nextID string // ID of the image the next build creates
|
||||||
removed []string // each tag or ID removed
|
removed []string // each tag or ID removed
|
||||||
forced bool // whether a removal was forced
|
forced bool // whether a removal was forced
|
||||||
|
containers []string // name of each named container created
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *fakeImageAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (api *fakeImageAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -67,6 +68,11 @@ func (api *fakeImageAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
case strings.HasSuffix(r.URL.Path, "/version"):
|
case strings.HasSuffix(r.URL.Path, "/version"):
|
||||||
_, _ = w.Write([]byte(`{"Version":"27.3.1","ApiVersion":"1.47"}`))
|
_, _ = w.Write([]byte(`{"Version":"27.3.1","ApiVersion":"1.47"}`))
|
||||||
case strings.HasSuffix(r.URL.Path, "/containers/create"):
|
case strings.HasSuffix(r.URL.Path, "/containers/create"):
|
||||||
|
// The git clone's container has no name; the app's has.
|
||||||
|
if containerName := r.URL.Query().Get("name"); containerName != "" {
|
||||||
|
api.containers = append(api.containers, containerName)
|
||||||
|
}
|
||||||
|
|
||||||
_, _ = w.Write([]byte(`{"Id":"gitcontainer"}`))
|
_, _ = w.Write([]byte(`{"Id":"gitcontainer"}`))
|
||||||
case strings.HasSuffix(r.URL.Path, "/logs"):
|
case strings.HasSuffix(r.URL.Path, "/logs"):
|
||||||
writeCloneOutput(w, api.shortSHA)
|
writeCloneOutput(w, api.shortSHA)
|
||||||
|
|||||||
@@ -113,6 +113,16 @@ func (svc *Service) BuildImage(
|
|||||||
return svc.buildImage(ctx, app, deployment)
|
return svc.buildImage(ctx, app, deployment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeployContainer exposes deployContainerWithTimeout for testing.
|
||||||
|
func (svc *Service) DeployContainer(
|
||||||
|
ctx context.Context,
|
||||||
|
app *models.App,
|
||||||
|
deployment *models.Deployment,
|
||||||
|
imageID docker.ImageID,
|
||||||
|
) error {
|
||||||
|
return svc.deployContainerWithTimeout(ctx, app, deployment, imageID)
|
||||||
|
}
|
||||||
|
|
||||||
// BuildContainerOptionsExported exposes buildContainerOptions for testing.
|
// BuildContainerOptionsExported exposes buildContainerOptions for testing.
|
||||||
func (svc *Service) BuildContainerOptionsExported(
|
func (svc *Service) BuildContainerOptionsExported(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|||||||
@@ -30,10 +30,12 @@
|
|||||||
name="name"
|
name="name"
|
||||||
value="{{.App.Name}}"
|
value="{{.App.Name}}"
|
||||||
required
|
required
|
||||||
pattern="[a-z0-9-]+"
|
minlength="2"
|
||||||
|
maxlength="63"
|
||||||
|
pattern="[a-z0-9]+((\.|-+)[a-z0-9]+)*"
|
||||||
class="input"
|
class="input"
|
||||||
>
|
>
|
||||||
<p class="text-sm text-gray-500 mt-1">Lowercase letters, numbers, and hyphens only</p>
|
<p class="text-sm text-gray-500 mt-1">Lowercase letters, numbers, hyphens, and dots, such as my-app or example.com</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -30,11 +30,13 @@
|
|||||||
name="name"
|
name="name"
|
||||||
value="{{.Name}}"
|
value="{{.Name}}"
|
||||||
required
|
required
|
||||||
pattern="[a-z0-9-]+"
|
minlength="2"
|
||||||
|
maxlength="63"
|
||||||
|
pattern="[a-z0-9]+((\.|-+)[a-z0-9]+)*"
|
||||||
class="input"
|
class="input"
|
||||||
placeholder="my-app"
|
placeholder="my-app"
|
||||||
>
|
>
|
||||||
<p class="text-sm text-gray-500 mt-1">Lowercase letters, numbers, and hyphens only</p>
|
<p class="text-sm text-gray-500 mt-1">Lowercase letters, numbers, hyphens, and dots, such as my-app or example.com</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
Reference in New Issue
Block a user