From 5d8a92a9473fcc180633a01e18822ea8469c0020 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Tue, 20 Oct 2026 09:02:04 -0400 Subject: [PATCH 1/4] fix(plugin): order signed-since prerelease identifiers numerically (D-1108) --- scripts/gen-signed-since.sh | 4 +++- scripts/testdata/signed-since/expected.json | 16 ++++++++++++++-- scripts/testdata/signed-since/free-1.json | 7 ++++++- scripts/testdata/signed-since/free-2.json | 9 +++++++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/scripts/gen-signed-since.sh b/scripts/gen-signed-since.sh index a11ccd5bf..b3ea81504 100755 --- a/scripts/gen-signed-since.sh +++ b/scripts/gen-signed-since.sh @@ -53,7 +53,9 @@ floor_of() { def key: (. // "" | sub("^v"; "") | sub("\\+.*$"; "")) as $v | ($v | split("-")) as $p | ($p[0] | split(".") | map(tonumber? // 0)) as $n - | [($n[0] // 0), ($n[1] // 0), ($n[2] // 0), (if ($p | length) > 1 then 0 else 1 end), ($p[1:] | join("-"))]; + | ($p[1:] | join("-") | split(".") + | map(if test("^[0-9]+$") then [0, tonumber] else [1, .] end)) as $pre + | [($n[0] // 0), ($n[1] // 0), ($n[2] // 0), (if ($p | length) > 1 then 0 else 1 end), $pre]; ([ .[] | (.releases // {}) | to_entries[] | select(.value.release_signature != null and (.value.version // "") != "") | {slug: .key, version: .value.version} ] diff --git a/scripts/testdata/signed-since/expected.json b/scripts/testdata/signed-since/expected.json index 9de8f8dd6..183f63f98 100644 --- a/scripts/testdata/signed-since/expected.json +++ b/scripts/testdata/signed-since/expected.json @@ -1,4 +1,16 @@ { - "free": {"ci": "1.2.7", "minio": "v1.2.9-rc.1", "tokens": "1.2.9"}, - "licensed": {"ai": "1.1.2", "claw": "2.0.0+build7"} + "free": { + "ci": "1.2.7", + "minio": "v1.2.9-rc.1", + "tokens": "1.2.9", + "numeric-prerelease": "1.2.3-rc.2", + "numeric-before-text": "1.2.3-11", + "shorter-prerelease": "1.2.3-alpha", + "hyphen-prerelease": "1.2.3-alpha-beta.2", + "release-after-prerelease": "1.2.3-rc.2+build10" + }, + "licensed": { + "ai": "1.1.2", + "claw": "2.0.0+build7" + } } diff --git a/scripts/testdata/signed-since/free-1.json b/scripts/testdata/signed-since/free-1.json index aab0c3fa6..df1475935 100644 --- a/scripts/testdata/signed-since/free-1.json +++ b/scripts/testdata/signed-since/free-1.json @@ -22,7 +22,12 @@ }, "tokens": { "version": "1.2.6" - } + }, + "numeric-prerelease": {"version": "1.2.3-rc.10", "release_signature": {"key_id": "test-key", "alg": "ed25519", "sig": "AAAA"}}, + "numeric-before-text": {"version": "1.2.3-11", "release_signature": {"key_id": "test-key", "alg": "ed25519", "sig": "AAAA"}}, + "shorter-prerelease": {"version": "1.2.3-alpha.1", "release_signature": {"key_id": "test-key", "alg": "ed25519", "sig": "AAAA"}}, + "hyphen-prerelease": {"version": "1.2.3-alpha-beta.10", "release_signature": {"key_id": "test-key", "alg": "ed25519", "sig": "AAAA"}}, + "release-after-prerelease": {"version": "1.2.3", "release_signature": {"key_id": "test-key", "alg": "ed25519", "sig": "AAAA"}} }, "history": [ { diff --git a/scripts/testdata/signed-since/free-2.json b/scripts/testdata/signed-since/free-2.json index ee7751d9b..e14f03c9d 100644 --- a/scripts/testdata/signed-since/free-2.json +++ b/scripts/testdata/signed-since/free-2.json @@ -2,5 +2,10 @@ "ci": {"version": "1.2.10", "release_signature": {"key_id": "plugins-0123456789abcdef", "alg": "ed25519", "sig": "AAAA"}}, "minio": {"version": "v1.2.9-rc.1", "release_signature": {"key_id": "plugins-0123456789abcdef", "alg": "ed25519", "sig": "AAAA"}}, "tokens": {"version": "1.2.9", "release_signature": {"key_id": "plugins-0123456789abcdef", "alg": "ed25519", "sig": "AAAA"}}, - "unsigned-only": {"version": "1.2.9", "release_signature": null} -}} + "unsigned-only": {"version": "1.2.9", "release_signature": null}, + "numeric-prerelease": {"version": "1.2.3-rc.2", "release_signature": {"key_id": "test-key", "alg": "ed25519", "sig": "AAAA"}}, + "numeric-before-text": {"version": "1.2.3-2a", "release_signature": {"key_id": "test-key", "alg": "ed25519", "sig": "AAAA"}}, + "shorter-prerelease": {"version": "1.2.3-alpha", "release_signature": {"key_id": "test-key", "alg": "ed25519", "sig": "AAAA"}}, + "hyphen-prerelease": {"version": "1.2.3-alpha-beta.2", "release_signature": {"key_id": "test-key", "alg": "ed25519", "sig": "AAAA"}}, + "release-after-prerelease": {"version": "1.2.3-rc.2+build10", "release_signature": {"key_id": "test-key", "alg": "ed25519", "sig": "AAAA"}} + }} From 174dae9bd0949351ab0055c64caacb40bc87f46d Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Tue, 20 Oct 2026 09:13:32 -0400 Subject: [PATCH 2/4] fix(license): scrub diagnostics and keep migration keys out of URLs (D-0979) --- cmd/commands/license_grace.go | 6 +- cmd/commands/license_human_scrub_test.go | 90 ++++++++++++++++++++++++ cmd/commands/license_migrate_actions.go | 18 ++--- cmd/commands/license_migrate_api.go | 5 +- cmd/commands/license_scrub.go | 4 +- cmd/commands/license_validate.go | 8 +-- 6 files changed, 111 insertions(+), 20 deletions(-) create mode 100644 cmd/commands/license_human_scrub_test.go diff --git a/cmd/commands/license_grace.go b/cmd/commands/license_grace.go index eeac627d2..aebb1b2d9 100644 --- a/cmd/commands/license_grace.go +++ b/cmd/commands/license_grace.go @@ -147,12 +147,12 @@ every 6 hours. --remove-timer removes that timer by hand (--all: every project o masked := license.MaskKey(key) if err != nil { doc.Keys = append(doc.Keys, licenseRefreshKey{Key: masked, Error: "refresh failed: " + licenseScrub(err.Error())}) - fmt.Fprintf(os.Stderr, "%s: refresh failed: %v\n", masked, err) + fmt.Fprintf(os.Stderr, "%s: refresh failed: %v\n", masked, licenseScrub(err.Error(), key)) continue } if !result.Valid { doc.Keys = append(doc.Keys, licenseRefreshKey{Key: masked, Error: "invalid: " + licenseScrub(result.Message)}) - fmt.Fprintf(os.Stderr, "%s: invalid — %s\n", masked, result.Message) + fmt.Fprintf(os.Stderr, "%s: invalid — %s\n", masked, licenseScrub(result.Message, key)) continue } row := licenseRefreshKey{Key: masked, Refreshed: true, Tier: licenseScrub(result.Tier)} @@ -161,7 +161,7 @@ every 6 hours. --remove-timer removes that timer by hand (--all: every project o } doc.Keys = append(doc.Keys, row) fmt.Printf("%s: refreshed (tier: %s, expires: %s)\n", - masked, result.Tier, result.ExpiresAt.Format("2006-01-02")) + masked, row.Tier, result.ExpiresAt.Format("2006-01-02")) refreshed = true } setData(cmd, doc) diff --git a/cmd/commands/license_human_scrub_test.go b/cmd/commands/license_human_scrub_test.go new file mode 100644 index 000000000..5277bd19e --- /dev/null +++ b/cmd/commands/license_human_scrub_test.go @@ -0,0 +1,90 @@ +package commands + +import ( + "bytes" + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func TestLicenseHumanDiagnosticsScrubbed(t *testing.T) { + for _, tc := range []struct { + name string + cmd *cobra.Command + body map[string]any + link bool + }{ + {"validate reason", licenseValidateCmd, map[string]any{"valid": false}, false}, + {"refresh reason", licenseRefreshCmd, map[string]any{"valid": false}, false}, + {"revalidate reason", licenseRevalidateCmd, map[string]any{"valid": false}, false}, + {"migration error", licenseMigrateCmd, map[string]any{"error": "hostile"}, false}, + {"migration status", licenseMigrateCmd, map[string]any{"migration_status": "hostile"}, false}, + {"migration link error", licenseMigrateCmd, map[string]any{"error": "hostile"}, true}, + {"migration linked", licenseMigrateCmd, map[string]any{"already_migrated": true, "account_id": "hostile", "tier": "hostile"}, true}, + {"migration completed", licenseMigrateCmd, map[string]any{"migrated": true, "account_id": "hostile", "tier": "hostile"}, true}, + } { + t.Run(tc.name, func(t *testing.T) { + fx := newLicenseFixture(t) + t.Setenv("NSELF_LICENSE_KEY_1", fixtureLicenseKey) + hostile := fixtureLicenseKey + " " + fx.jwt + " " + fixtureAccessToken + for k, value := range tc.body { + if value == "hostile" { + tc.body[k] = hostile + } + } + tc.body["reason"] = hostile + fx.reply(t, tc.body) + flags := map[string]string{} + if tc.link { + flags["account-id"] = "00000000-0000-0000-0000-000000000001" + } + var diagnostics bytes.Buffer + tc.cmd.SetErr(&diagnostics) + t.Cleanup(func() { tc.cmd.SetErr(nil) }) + var out string + var runErr error + stderr := captureStderr(t, func() { + out, runErr = runLicenseJSON(t, tc.cmd, flags) + }) + all := out + stderr + diagnostics.String() + if runErr != nil { + all += runErr.Error() + } + for _, secret := range []string{fixtureLicenseKey, fx.jwt, fixtureAccessToken} { + if strings.Contains(all, secret) { + t.Error("human diagnostic or JSON contains an unredacted synthetic secret") + } + } + if !strings.Contains(all, "[REDACTED]") { + t.Error("expected hostile reply to reach the scrubbed diagnostic or document") + } + }) + } +} + +func TestMigrationStatusUsesHeader(t *testing.T) { + for _, key := range []string{fixtureLicenseKey, "synthetic-key-with+reserved&characters"} { + t.Run(key, func(t *testing.T) { + requests := make(chan *http.Request, 1) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests <- r + _, _ = w.Write([]byte(`{"migration_status":"legacy"}`)) + })) + defer srv.Close() + if _, err := fetchMigrationStatus(context.Background(), key, srv.URL); err != nil { + t.Fatal(err) + } + r := <-requests + if r.Method != http.MethodGet || r.URL.Path != "/license/migration-status" || r.URL.RawQuery != "" { + t.Error("migration status must use GET without a query string") + } + if r.Header.Get("X-License-Key") != key { + t.Error("migration status must send the key in X-License-Key") + } + }) + } +} diff --git a/cmd/commands/license_migrate_actions.go b/cmd/commands/license_migrate_actions.go index 3303ade63..c605e863f 100644 --- a/cmd/commands/license_migrate_actions.go +++ b/cmd/commands/license_migrate_actions.go @@ -71,13 +71,13 @@ func runMigrateCheck(ctx context.Context, pingURL string, doc *licenseMigrateDat status, err := fetchMigrationStatus(ctx, key, pingURL) if err != nil { doc.Keys = append(doc.Keys, licenseMigrateKey{Key: masked, Product: pp.DisplayName, Remaining: "?", Status: licenseScrub(fmt.Sprintf("Error: %v", err), migrateKeyFlag)}) - tbl.AddRow(masked, pp.DisplayName, "?", fmt.Sprintf("Error: %v", err)) + tbl.AddRow(masked, pp.DisplayName, "?", licenseScrub(fmt.Sprintf("Error: %v", err), key)) continue } if status.Error != "" { doc.Keys = append(doc.Keys, licenseMigrateKey{Key: masked, Product: pp.DisplayName, Remaining: "?", Status: licenseScrub(fmt.Sprintf("Error: %s", status.Error), migrateKeyFlag)}) - tbl.AddRow(masked, pp.DisplayName, "?", fmt.Sprintf("Error: %s", status.Error)) + tbl.AddRow(masked, pp.DisplayName, "?", licenseScrub(fmt.Sprintf("Error: %s", status.Error), key)) continue } @@ -92,7 +92,7 @@ func runMigrateCheck(ctx context.Context, pingURL string, doc *licenseMigrateDat } doc.Keys = append(doc.Keys, licenseMigrateKey{Key: masked, Product: pp.DisplayName, Remaining: daysStr, Status: licenseScrub(migStatus, migrateKeyFlag)}) - tbl.AddRow(masked, pp.DisplayName, daysStr, migStatus) + tbl.AddRow(masked, pp.DisplayName, daysStr, licenseScrub(migStatus, key)) } tbl.Render() @@ -134,29 +134,29 @@ func runMigrateLink(ctx context.Context, pingURL string, doc *licenseMigrateData } masked := license.MaskKey(keyToMigrate) - fmt.Printf("Linking key %s to account %s ...\n", masked, migrateAccountID) + fmt.Printf("Linking key %s to account %s ...\n", masked, licenseScrub(migrateAccountID, keyToMigrate)) resp, err := sendMigrateRequest(ctx, keyToMigrate, migrateAccountID, pingURL) if err != nil { - return fmt.Errorf("migration request failed: %w", err) + return scrubErr(fmt.Errorf("migration request failed: %w", err)) } if resp.Error != "" { - return fmt.Errorf("server error: %s", resp.Error) + return fmt.Errorf("server error: %s", licenseScrub(resp.Error, keyToMigrate)) } doc.Link = &licenseMigrateLink{Key: masked, AccountID: licenseScrub(resp.AccountID, keyToMigrate), Tier: licenseScrub(resp.Tier, keyToMigrate), AlreadyLinked: resp.AlreadyMigrated} if resp.AlreadyMigrated { fmt.Printf("Key %s is already linked to account %s (tier: %s).\n", - masked, resp.AccountID, resp.Tier) + masked, doc.Link.AccountID, doc.Link.Tier) return nil } if resp.Migrated { fmt.Printf("Migration complete.\n") fmt.Printf(" Key: %s\n", masked) - fmt.Printf(" Account: %s\n", resp.AccountID) - fmt.Printf(" Tier: %s\n", resp.Tier) + fmt.Printf(" Account: %s\n", doc.Link.AccountID) + fmt.Printf(" Tier: %s\n", doc.Link.Tier) fmt.Println() fmt.Println("Your key is now linked to your nSelf account.") fmt.Println("Run 'nself license status' to verify.") diff --git a/cmd/commands/license_migrate_api.go b/cmd/commands/license_migrate_api.go index e5b7776f4..f999f381b 100644 --- a/cmd/commands/license_migrate_api.go +++ b/cmd/commands/license_migrate_api.go @@ -66,16 +66,17 @@ func sendMigrateRequest(ctx context.Context, licenseKey, accountID, pingURL stri return &result, nil } -// fetchMigrationStatus — GET /license/migration-status?key=. +// fetchMigrationStatus uses the server-supported X-License-Key header to keep keys out of URLs. func fetchMigrationStatus(ctx context.Context, key string, pingURL string) (*migrationStatusResponse, error) { ctx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() - url := fmt.Sprintf("%s/license/migration-status?key=%s", pingURL, key) + url := pingURL + "/license/migration-status" req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } + req.Header.Set("X-License-Key", key) resp, err := httptimeout.License.Do(req) if err != nil { diff --git a/cmd/commands/license_scrub.go b/cmd/commands/license_scrub.go index 576a91a0f..5aedaf879 100644 --- a/cmd/commands/license_scrub.go +++ b/cmd/commands/license_scrub.go @@ -7,8 +7,8 @@ package commands // Inputs: a document value (any struct, slice, map or pointer tree) or an // error. Outputs: a copy of the same type with every string leaf scrubbed, or // an error with scrubbed text and the same classification (code, exit status). -// Constraints: human and v1.4 output are never touched (callers gate on -// jsonEnvelopeOn); a masked key, a UUID and a date pass unchanged; nil slices +// Constraints: document callers gate on jsonEnvelopeOn; human licence +// diagnostics also scrub server text. A masked key, UUID and date pass unchanged; nil slices // stay nil and empty ones stay empty. SPORT: P7-SURF-16, // contract:cli.json-envelope diff --git a/cmd/commands/license_validate.go b/cmd/commands/license_validate.go index 0f2655c69..e1b09111b 100644 --- a/cmd/commands/license_validate.go +++ b/cmd/commands/license_validate.go @@ -49,7 +49,7 @@ var licenseValidateCmd = &cobra.Command{ masked := license.MaskKey(key) if err != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: validation failed: %v\n", masked, err) + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: validation failed: %v\n", masked, licenseScrub(err.Error(), key)) allValid = false } else if valid { validKeys = append(validKeys, masked) @@ -244,10 +244,10 @@ Requires network connectivity.`, result, err := license.RefreshCache(ctx, keys[0]) if err != nil { - return fmt.Errorf("revalidation failed: %w", err) + return scrubErr(fmt.Errorf("revalidation failed: %w", err)) } if !result.Valid { - return fmt.Errorf("license is invalid: %s", result.Message) + return fmt.Errorf("license is invalid: %s", licenseScrub(result.Message)) } rev := licenseRevalidateData{Tier: licenseScrub(result.Tier)} @@ -255,7 +255,7 @@ Requires network connectivity.`, rev.ExpiresAt = result.ExpiresAt.Format("2006-01-02") } setData(cmd, rev) - fmt.Printf("License revalidated. Tier: %s\n", result.Tier) + fmt.Printf("License revalidated. Tier: %s\n", rev.Tier) if !result.ExpiresAt.IsZero() { fmt.Printf("Expires: %s\n", result.ExpiresAt.Format("2006-01-02")) } From 00ea3b0195fb63667ae8f8cb200f06373828d453 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Tue, 20 Oct 2026 09:14:10 -0400 Subject: [PATCH 3/4] fix(config): resolve get through the build environment cascade (D-0919) --- cmd/commands/config.go | 4 ++- cmd/commands/config_get_cascade_test.go | 43 +++++++++++++++++++++++++ cmd/commands/config_show_get.go | 17 ++++------ 3 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 cmd/commands/config_get_cascade_test.go diff --git a/cmd/commands/config.go b/cmd/commands/config.go index d414a594a..35a903933 100644 --- a/cmd/commands/config.go +++ b/cmd/commands/config.go @@ -40,7 +40,9 @@ Secret keys are any key whose name contains: SECRET, PASSWORD, KEY, TOKEN.`, var configGetCmd = &cobra.Command{ Use: "get ", Short: "Get a single configuration value", - Long: `Return the raw value of KEY from .env (or .env.{env} with --env). + Long: `Return the value of KEY from the same .env cascade used by build. +Use --env to select the cascade environment; otherwise ENV selects it. +Later layers override earlier ones, with .env.local winning by default. Output is the plain value only, making it safe for scripting. Exits non-zero if the key is not found.`, diff --git a/cmd/commands/config_get_cascade_test.go b/cmd/commands/config_get_cascade_test.go new file mode 100644 index 000000000..95984c7f2 --- /dev/null +++ b/cmd/commands/config_get_cascade_test.go @@ -0,0 +1,43 @@ +package commands + +import ( + "bytes" + "os" + "path/filepath" + "testing" +) + +// Purpose: config get must resolve the same cascade as a local build (D-0919). +// Inputs: fake dotenv layers and environment selectors. Outputs: winning value. +// Constraints: no config.Load, process exports, network, or real secrets. +func TestConfigGetCascade(t *testing.T) { + for _, tc := range []struct { + name, base, processEnv, flagEnv, legacy, want string + layers map[string]string + }{ + {"local override", "HASURA_JWT_KEY=fake-base\n", "", "", "", "fake-local", map[string]string{".env.local": "HASURA_JWT_KEY=fake-local\n"}}, + {"base environment selector", "ENV=prod\nHASURA_JWT_KEY=fake-base\n", "", "", "", "fake-prod", map[string]string{".env.prod": "HASURA_JWT_KEY=fake-prod\n"}}, + {"process environment selector", "ENV=prod\nHASURA_JWT_KEY=fake-base\n", "staging", "", "", "fake-staging", map[string]string{".env.staging": "HASURA_JWT_KEY=fake-staging\n"}}, + {"explicit environment selector", "HASURA_JWT_KEY=fake-base\n", "prod", "qa", "", "fake-qa", map[string]string{".env.qa": "HASURA_JWT_KEY=fake-qa\n"}}, + {"secrets override environment", "HASURA_JWT_KEY=fake-base\n", "", "", "", "fake-secret", map[string]string{".env.dev": "HASURA_JWT_KEY=fake-dev\n", ".env.secrets": "HASURA_JWT_KEY=fake-secret\n"}}, + {"legacy base wins", "HASURA_JWT_KEY=fake-base\n", "", "", "1", "fake-base", map[string]string{".env.local": "HASURA_JWT_KEY=fake-local\n"}}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("ENV", tc.processEnv) + t.Setenv("NSELF_LEGACY_ENV_ORDER", tc.legacy) + dir := makeNSelfProject(t, tc.base) + for name, body := range tc.layers { + if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o600); err != nil { + t.Fatal(err) + } + } + root := newConfigCmd() + root.SetErr(new(bytes.Buffer)) + root.SetArgs([]string{"config", "get", "HASURA_JWT_KEY", "--reveal", "--env", tc.flagEnv}) + out, err := captureStdout(t, root.Execute) + if err != nil || out != tc.want+"\n" { + t.Fatalf("output=%q err=%v, want %q", out, err, tc.want+"\n") + } + }) + } +} diff --git a/cmd/commands/config_show_get.go b/cmd/commands/config_show_get.go index 3a8923ea1..8ecb2064d 100644 --- a/cmd/commands/config_show_get.go +++ b/cmd/commands/config_show_get.go @@ -8,7 +8,6 @@ package commands import ( "fmt" "os" - "path/filepath" "sort" "strings" @@ -93,16 +92,14 @@ func runConfigGet(cmd *cobra.Command, args []string) error { return err } - envFile := envFileName(projectDir, envFlag) - pairs, err := config.ReadEnvFile(envFile) + if envFlag == "" { + envFlag, _ = config.ResolveEnv(projectDir) + } + explanation, err := config.ExplainKey(projectDir, envFlag, key) if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("env file not found: %s", envFile) - } - return config.EnvReadError(envFile, err) + return err } - - val, ok := pairs[key] + envFile, val, ok := explanation.Winner() if !ok { return fmt.Errorf("key not found: %s", key) } @@ -111,7 +108,7 @@ func runConfigGet(cmd *cobra.Command, args []string) error { // compat.V15(P7-REG-09): --json ignored -> envelope if jsonOut, _ := cmd.Flags().GetBool("json"); jsonOut && compat.V15() { return output.EmitData(pilotWriter(), "config get", configGetData{ - Key: key, Value: shown, Masked: shown != val, File: filepath.Base(envFile), + Key: key, Value: shown, Masked: shown != val, File: envFile, }) } From 221a18251e79fea2bc55c77b8bbd08dc4f74ba91 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Tue, 20 Oct 2026 09:15:13 -0400 Subject: [PATCH 4/4] fix(deploy): ignore unloaded nginx backup files in default-server checks (D-1146) --- internal/deploy/remote_backup_confd_test.go | 4 ++++ internal/deploy/remote_layout.go | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/deploy/remote_backup_confd_test.go b/internal/deploy/remote_backup_confd_test.go index 59a74b5d6..80ad24278 100644 --- a/internal/deploy/remote_backup_confd_test.go +++ b/internal/deploy/remote_backup_confd_test.go @@ -126,6 +126,10 @@ func TestKeptHostConfDDefaultServerCollision(t *testing.T) { "sites file": {"nginx/sites/other.conf", "server {\n listen 80 default_server;\n}\n", "", "nginx/sites/other.conf", true}, "conf.d-env file": {"nginx/conf.d-prod/x.conf", "server { listen 443 ssl default_server; }\n", "", "nginx/conf.d-prod/x.conf", true}, "conf.d sibling": {"nginx/conf.d/other.conf", "server {\n listen [::]:80 default_server;\n}\n", "", "nginx/conf.d/other.conf", true}, + "conf.d backup": {"nginx/conf.d/default.conf.bak", "server { listen 80 default_server; }\n", "", "", false}, + "env conf.d backup": {"nginx/conf.d-prod/default.conf.bak", "server { listen 80 default_server; }\n", "", "", false}, + "disabled site": {"nginx/sites/default.conf.disabled", "server { listen 80 default_server; }\n", "", "", false}, + "site text file": {"nginx/sites/notes.txt", "server { listen 80 default_server; }\n", "", "", false}, "commented out": {"nginx/sites/other.conf", "# listen 80 default_server;\nserver { listen 80; }\n", "", "", false}, "no default_server": {"nginx/sites/other.conf", "server { listen 80; }\n", "", "", false}, "word only": {"nginx/sites/other.conf", "server { server_name default_server; }\n", "", "", false}, diff --git a/internal/deploy/remote_layout.go b/internal/deploy/remote_layout.go index a99cb61ba..5bd2d8a0e 100644 --- a/internal/deploy/remote_layout.go +++ b/internal/deploy/remote_layout.go @@ -186,7 +186,7 @@ type KeptConfD struct { // deploy must not ship (D-0424): the path exists as a regular file with no line // in its first 5 starting with BuildNginxMarker, or is a symlink or anything that // is not a regular file. Also, when nginx/conf.d/default.conf is absent on the -// host and any other regular file under nginx/conf.d*/ or nginx/sites/ has a +// host and any other regular .conf file under nginx/conf.d*/ or nginx/sites/ has a // listen line with default_server, our default.conf is kept out (reason // DefaultServerIn), since two default servers fail nginx -t. The previous // deploy's record and the host's shipped list are ignored for conf.d: the host @@ -217,7 +217,7 @@ for f in %s; do if [ -L "$f" ]; then printf 'KEEP %%s\n' "$f" elif [ -f "$f" ]; then if ! head -n 5 -- "$f" 2>/dev/null | grep -q %s; then printf 'KEEP %%s\n' "$f"; fi elif [ -e "$f" ]; then printf 'KEEP %%s\n' "$f" -elif [ "$f" = %s ]; then for g in nginx/conf.d*/* nginx/sites/*; do if [ -f "$g" ] && grep -Eq '(^|[{;])[[:space:]]*listen[[:space:]][^#;]*default_server' -- "$g" 2>/dev/null; then printf 'DSRV %%s\t%%s\n' "$f" "$(san "$g")"; break; fi; done +elif [ "$f" = %s ]; then for g in nginx/conf.d*/*.conf nginx/sites/*.conf; do if [ -f "$g" ] && grep -Eq '(^|[{;])[[:space:]]*listen[[:space:]][^#;]*default_server' -- "$g" 2>/dev/null; then printf 'DSRV %%s\t%%s\n' "$f" "$(san "$g")"; break; fi; done fi done; exit 0`, q(remotePath), strings.Join(quoteAll(cand), " "), q("^"+strings.ReplaceAll(BuildNginxMarker, " ", "[ ]")), q(defaultServerConf)) out, err := r.Run(ctx, shScript(script))