feat(worker): adopt canonical public URL
Все проверки выполнены успешно
CI / test (push) Successful in 10m15s
Docker / Build and publish worker image (push) Successful in 34m59s

Этот коммит содержится в:
Gleb Tv
2026-08-12 20:48:01 +03:00
родитель a1ccd50aaf
Коммит cb23f123ae
19 изменённых файлов: 804 добавлений и 74 удалений

Просмотреть файл

@@ -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"} {