60 lines
1.8 KiB
Go
60 lines
1.8 KiB
Go
// Package provision holds the tenant creation rules shared by the control
|
|
// plane and sitesvc.
|
|
//
|
|
// These rules used to be duplicated: the control plane owned one copy and
|
|
// sitesvc mirrored it by hand. The copies had already drifted — sitesvc retried
|
|
// on a lost slug race while the control plane returned an error. This package
|
|
// is the single definition; neither service may reimplement any of it.
|
|
package provision
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
MinSlugLength = 3
|
|
MaxSlugLength = 40
|
|
)
|
|
|
|
var slugStrip = regexp.MustCompile(`[^a-z0-9]+`)
|
|
|
|
// ReservedSlugs are subdomain labels the platform needs for itself.
|
|
var ReservedSlugs = map[string]bool{
|
|
"www": true, "api": true, "app": true, "admin": true, "auth": true,
|
|
"install": true, "static": true, "_next": true, "default": true,
|
|
}
|
|
|
|
// Slugify lowercases a name and collapses every run of non-alphanumeric
|
|
// characters into a single hyphen, trimming hyphens from both ends.
|
|
func Slugify(name string) string {
|
|
s := strings.ToLower(name)
|
|
s = slugStrip.ReplaceAllString(s, "-")
|
|
return strings.Trim(s, "-")
|
|
}
|
|
|
|
// BaseSlug turns a name into a validated slug stem, or explains why it cannot.
|
|
func BaseSlug(name string) (string, error) {
|
|
base := Slugify(name)
|
|
if len(base) < MinSlugLength {
|
|
return "", fmt.Errorf("organisation name too short (slug must be at least %d characters)", MinSlugLength)
|
|
}
|
|
if len(base) > MaxSlugLength {
|
|
base = base[:MaxSlugLength]
|
|
}
|
|
if ReservedSlugs[base] {
|
|
return "", fmt.Errorf("that organisation name is reserved")
|
|
}
|
|
return base, nil
|
|
}
|
|
|
|
// NextSlug returns the candidate slug for a given attempt. Attempt 1 is the
|
|
// base itself; later attempts append a counter.
|
|
func NextSlug(base string, attempt int) string {
|
|
if attempt < 2 {
|
|
return base
|
|
}
|
|
return fmt.Sprintf("%s-%d", base, attempt)
|
|
}
|