feat(worker): adopt canonical public URL
Все проверки выполнены успешно
CI / test (push) Successful in 10m15s
Docker / Build and publish worker image (push) Successful in 34m59s
Все проверки выполнены успешно
CI / test (push) Successful in 10m15s
Docker / Build and publish worker image (push) Successful in 34m59s
Этот коммит содержится в:
@@ -15,6 +15,8 @@ import (
|
||||
"unicode"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
|
||||
"rocketgit.ru/rsmon/worker/internal/distworker"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -34,11 +36,16 @@ const (
|
||||
// installEnvKeys is the canonical, ordered set of worker environment
|
||||
// variables the installer understands and writes to the unit's env
|
||||
// file. Order matters: the rendered file is stable and readable.
|
||||
//
|
||||
// PUBLIC_URL is the canonical advertised origin. WORKER_URL stays in the
|
||||
// list for the bounded migration so legacy env files still resolve; it
|
||||
// is dropped from the written file whenever PUBLIC_URL is also present.
|
||||
var installEnvKeys = []string{
|
||||
"RSMON_URL",
|
||||
"RSMON_TOKEN",
|
||||
"WORKER_HOST",
|
||||
"WORKER_PORT",
|
||||
"PUBLIC_URL",
|
||||
"WORKER_URL",
|
||||
"WORKER_LOGIN",
|
||||
"WORKER_PASSWORD",
|
||||
@@ -56,18 +63,19 @@ var installEnvKeys = []string{
|
||||
// installs a co-located worker under rsmon-worker-<name> with its own
|
||||
// binary path, config dir, data dir, systemd unit, and port.
|
||||
type InstallOptions struct {
|
||||
Binary string
|
||||
EnvFile string
|
||||
Token string
|
||||
URL string
|
||||
Host string
|
||||
Port string
|
||||
Login string
|
||||
Password string
|
||||
Name string
|
||||
Docker bool
|
||||
Image string
|
||||
NoStart bool
|
||||
Binary string
|
||||
EnvFile string
|
||||
Token string
|
||||
URL string
|
||||
PublicURL string
|
||||
Host string
|
||||
Port string
|
||||
Login string
|
||||
Password string
|
||||
Name string
|
||||
Docker bool
|
||||
Image string
|
||||
NoStart bool
|
||||
}
|
||||
|
||||
// paths is the fully-resolved on-disk layout for an instance. Every
|
||||
@@ -258,6 +266,7 @@ func resolveInstallEnv(opts InstallOptions, name string, fileEnv map[string]stri
|
||||
flagVals := map[string]string{
|
||||
"RSMON_URL": opts.URL,
|
||||
"RSMON_TOKEN": opts.Token,
|
||||
"PUBLIC_URL": opts.PublicURL,
|
||||
"WORKER_HOST": opts.Host,
|
||||
"WORKER_PORT": opts.Port,
|
||||
"WORKER_LOGIN": opts.Login,
|
||||
@@ -312,6 +321,26 @@ func resolveInstallEnv(opts InstallOptions, name string, fileEnv map[string]stri
|
||||
return nil, fmt.Errorf("WORKER_LOGIN and WORKER_PASSWORD must both be set or both be empty")
|
||||
}
|
||||
|
||||
// PUBLIC_URL is the canonical advertised origin. When both the
|
||||
// canonical and the legacy WORKER_URL resolve, the legacy variable
|
||||
// is superseded and must not be written to a fresh env file.
|
||||
if values["PUBLIC_URL"] != "" {
|
||||
delete(values, "WORKER_URL")
|
||||
}
|
||||
// Canonical PUBLIC_URL is held to the strict origin shape; the
|
||||
// legacy WORKER_URL only to the tolerant absolute-URL check so env
|
||||
// files that previously installed keep working.
|
||||
if v := values["PUBLIC_URL"]; v != "" {
|
||||
if err := distworker.ValidatePublicURL(v); err != nil {
|
||||
return nil, fmt.Errorf("PUBLIC_URL: %w", err)
|
||||
}
|
||||
}
|
||||
if v := values["WORKER_URL"]; v != "" {
|
||||
if err := distworker.ValidateAdvertisedURL(v); err != nil {
|
||||
return nil, fmt.Errorf("WORKER_URL: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate every value we will write is systemd/docker safe.
|
||||
for _, key := range installEnvKeys {
|
||||
v, ok := values[key]
|
||||
|
||||
@@ -90,6 +90,131 @@ func TestValidateEnvironmentFileInputErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveInstallEnvPublicURLWins verifies the installer canonicalizes
|
||||
// the advertised origin: PUBLIC_URL is written and the legacy WORKER_URL
|
||||
// is dropped from the resolved env when both are present.
|
||||
func TestResolveInstallEnvPublicURLWins(t *testing.T) {
|
||||
v, err := resolveInstallEnv(InstallOptions{}, "", map[string]string{
|
||||
"RSMON_URL": "https://rsmon.ru",
|
||||
"RSMON_TOKEN": "secret",
|
||||
"PUBLIC_URL": "https://worker.example.com",
|
||||
"WORKER_URL": "http://legacy.example.com",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v["PUBLIC_URL"] != "https://worker.example.com" {
|
||||
t.Fatalf("PUBLIC_URL not resolved: %+v", v)
|
||||
}
|
||||
if _, ok := v["WORKER_URL"]; ok {
|
||||
t.Fatalf("legacy WORKER_URL must be dropped when PUBLIC_URL is set: %+v", v)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveInstallEnvLegacyWorkerURLPassesThrough keeps the bounded
|
||||
// migration: an env file that only carries the legacy WORKER_URL still
|
||||
// resolves and is written unchanged so existing installs upgrade in place.
|
||||
func TestResolveInstallEnvLegacyWorkerURLPassesThrough(t *testing.T) {
|
||||
v, err := resolveInstallEnv(InstallOptions{}, "", map[string]string{
|
||||
"RSMON_URL": "https://rsmon.ru",
|
||||
"RSMON_TOKEN": "secret",
|
||||
"WORKER_URL": "https://legacy.example.com",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v["WORKER_URL"] != "https://legacy.example.com" {
|
||||
t.Fatalf("legacy WORKER_URL not preserved: %+v", v)
|
||||
}
|
||||
if v["PUBLIC_URL"] != "" {
|
||||
t.Fatalf("PUBLIC_URL must stay empty: %+v", v)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveInstallEnvLegacyWorkerURLTolerant verifies the bounded
|
||||
// migration does not newly reject legacy shapes that previously
|
||||
// installed (a path-bearing WORKER_URL) while a path-bearing PUBLIC_URL
|
||||
// stays strict.
|
||||
func TestResolveInstallEnvLegacyWorkerURLTolerant(t *testing.T) {
|
||||
v, err := resolveInstallEnv(InstallOptions{}, "", map[string]string{
|
||||
"RSMON_URL": "https://rsmon.ru",
|
||||
"RSMON_TOKEN": "secret",
|
||||
"WORKER_URL": "https://legacy.example.com/web",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("legacy WORKER_URL with a path must keep installing: %v", err)
|
||||
}
|
||||
if v["WORKER_URL"] != "https://legacy.example.com/web" {
|
||||
t.Fatalf("legacy WORKER_URL not preserved: %+v", v)
|
||||
}
|
||||
|
||||
_, err = resolveInstallEnv(InstallOptions{}, "", map[string]string{
|
||||
"RSMON_URL": "https://rsmon.ru",
|
||||
"RSMON_TOKEN": "secret",
|
||||
"PUBLIC_URL": "https://worker.example.com/web",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("path-bearing canonical PUBLIC_URL must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveInstallEnvPublicURLFlagBeatsEnv verifies the --public-url
|
||||
// flag follows the installer precedence: the flag wins over the env file
|
||||
// and the legacy WORKER_URL is dropped when PUBLIC_URL is present.
|
||||
func TestResolveInstallEnvPublicURLFlagBeatsEnv(t *testing.T) {
|
||||
v, err := resolveInstallEnv(InstallOptions{PublicURL: "https://flag.example.com"}, "", map[string]string{
|
||||
"RSMON_URL": "https://rsmon.ru",
|
||||
"RSMON_TOKEN": "secret",
|
||||
"PUBLIC_URL": "https://file.example.com",
|
||||
"WORKER_URL": "https://legacy.example.com",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v["PUBLIC_URL"] != "https://flag.example.com" {
|
||||
t.Fatalf("--public-url flag must win: %+v", v)
|
||||
}
|
||||
if _, ok := v["WORKER_URL"]; ok {
|
||||
t.Fatalf("legacy WORKER_URL must be dropped when PUBLIC_URL is set: %+v", v)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveInstallEnvRejectsMalformedPublicURL verifies the installer
|
||||
// rejects an advertised origin that violates the plan's origin shape
|
||||
// (path, userinfo, and non-http(s) schemes).
|
||||
func TestResolveInstallEnvRejectsMalformedPublicURL(t *testing.T) {
|
||||
for _, bad := range []string{
|
||||
"https://worker.example.com/web",
|
||||
"https://user:pass@worker.example.com",
|
||||
"ftp://worker.example.com",
|
||||
"worker.example.com",
|
||||
} {
|
||||
t.Run(bad, func(t *testing.T) {
|
||||
_, err := resolveInstallEnv(InstallOptions{}, "", map[string]string{
|
||||
"RSMON_URL": "https://rsmon.ru",
|
||||
"RSMON_TOKEN": "secret",
|
||||
"PUBLIC_URL": bad,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("PUBLIC_URL=%q accepted", bad)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRenderEnvFileOrder includes the canonical PUBLIC_URL ordering.
|
||||
func TestRenderEnvFilePublicURLEmptyOmitted(t *testing.T) {
|
||||
got := string(renderEnvFile(map[string]string{
|
||||
"RSMON_URL": "https://rsmon.ru",
|
||||
"RSMON_TOKEN": "secret",
|
||||
"PUBLIC_URL": "",
|
||||
"WORKER_URL": "",
|
||||
}))
|
||||
if strings.Contains(got, "PUBLIC_URL=") || strings.Contains(got, "WORKER_URL=") {
|
||||
t.Fatalf("empty public URL keys must be omitted: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironment(t *testing.T) {
|
||||
got := string(Environment("https://example.test", "secret"))
|
||||
for _, want := range []string{"RSMON_URL=https://example.test\n", "RSMON_TOKEN=secret\n", "WORKER_HOST=127.0.0.1\n"} {
|
||||
|
||||
Ссылка в новой задаче
Block a user