mirror of
https://github.com/tale/headplane.git
synced 2026-08-27 15:37:04 +00:00
fix(config): stop lowercasing filesystem paths (#613)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# Next
|
# Next
|
||||||
|
|
||||||
|
- Fixed `server.data_path`, `headscale.config_path`, `headscale.dns_records_path` and `headscale.tls_cert_path` being silently lowercased, which pointed Headplane at a different location for any path containing a capital letter (closes [#612](https://github.com/tale/headplane/issues/612)).
|
||||||
- Fixed the Headplane agent falling back to an interactive Tailscale login. The agent now starts with a pre-auth-key, preserves its existing state across restarts, and auto-approves itself when Headscale requires manual approval (closes [#582](https://github.com/tale/headplane/issues/582)).
|
- Fixed the Headplane agent falling back to an interactive Tailscale login. The agent now starts with a pre-auth-key, preserves its existing state across restarts, and auto-approves itself when Headscale requires manual approval (closes [#582](https://github.com/tale/headplane/issues/582)).
|
||||||
- Added `integration.agent.tailscale_netns`, an agent-only opt-out from Tailscale's routing-loop socket handling for deployments where its fallback pins the agent's Headscale connection to the wrong interface. Existing behavior remains enabled by default.
|
- Added `integration.agent.tailscale_netns`, an agent-only opt-out from Tailscale's routing-loop socket handling for deployments where its fallback pins the agent's Headscale connection to the wrong interface. Existing behavior remains enabled by default.
|
||||||
- Fixed creating pre-auth keys with an expiry of 1000 days or more. The number input submitted its locale-formatted value (`365,000`, `365 000`, `365.000`), which either failed with a 500 or silently created a key with a truncated expiry. The raw value is now submitted and the server rejects malformed expiries with a 400 (closes [#596](https://github.com/tale/headplane/issues/596)).
|
- Fixed creating pre-auth keys with an expiry of 1000 days or more. The number input submitted its locale-formatted value (`365,000`, `365 000`, `365.000`), which either failed with a 500 or silently created a key with a truncated expiry. The raw value is now submitted and the server rejects malformed expiries with a 400 (closes [#596](https://github.com/tale/headplane/issues/596)).
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const serverConfig = type({
|
|||||||
host: 'string.ip = "0.0.0.0"',
|
host: 'string.ip = "0.0.0.0"',
|
||||||
port: "number.integer = 3000",
|
port: "number.integer = 3000",
|
||||||
base_url: "string.url?",
|
base_url: "string.url?",
|
||||||
data_path: 'string.lower = "/var/lib/headplane/"',
|
data_path: 'string = "/var/lib/headplane/"',
|
||||||
info_secret: "string?",
|
info_secret: "string?",
|
||||||
|
|
||||||
cookie_secret: "(32 <= string <= 32)",
|
cookie_secret: "(32 <= string <= 32)",
|
||||||
@@ -65,7 +65,7 @@ const partialServerConfig = type({
|
|||||||
host: "string.ip?",
|
host: "string.ip?",
|
||||||
port: "number.integer?",
|
port: "number.integer?",
|
||||||
base_url: "string.url?",
|
base_url: "string.url?",
|
||||||
data_path: "string.lower?",
|
data_path: "string?",
|
||||||
info_secret: "string?",
|
info_secret: "string?",
|
||||||
|
|
||||||
cookie_secret: "(32 <= string <= 32)?",
|
cookie_secret: "(32 <= string <= 32)?",
|
||||||
@@ -94,10 +94,10 @@ const headscaleConfig = type({
|
|||||||
.pipe((v) => (v.endsWith("/") ? v.slice(0, -1) : v))
|
.pipe((v) => (v.endsWith("/") ? v.slice(0, -1) : v))
|
||||||
.optional(),
|
.optional(),
|
||||||
api_key: "string?",
|
api_key: "string?",
|
||||||
config_path: "string.lower?",
|
config_path: "string?",
|
||||||
config_strict: "boolean = true",
|
config_strict: "boolean = true",
|
||||||
dns_records_path: "string.lower?",
|
dns_records_path: "string?",
|
||||||
tls_cert_path: "string.lower?",
|
tls_cert_path: "string?",
|
||||||
});
|
});
|
||||||
|
|
||||||
const partialHeadscaleConfig = type({
|
const partialHeadscaleConfig = type({
|
||||||
@@ -108,10 +108,10 @@ const partialHeadscaleConfig = type({
|
|||||||
.pipe((v) => (v.endsWith("/") ? v.slice(0, -1) : v))
|
.pipe((v) => (v.endsWith("/") ? v.slice(0, -1) : v))
|
||||||
.optional(),
|
.optional(),
|
||||||
api_key: "string?",
|
api_key: "string?",
|
||||||
config_path: "string.lower?",
|
config_path: "string?",
|
||||||
config_strict: "boolean?",
|
config_strict: "boolean?",
|
||||||
dns_records_path: "string.lower?",
|
dns_records_path: "string?",
|
||||||
tls_cert_path: "string.lower?",
|
tls_cert_path: "string?",
|
||||||
});
|
});
|
||||||
|
|
||||||
const assignableRole = '"admin" | "network_admin" | "it_admin" | "auditor" | "viewer" | "member"';
|
const assignableRole = '"admin" | "network_admin" | "it_admin" | "auditor" | "viewer" | "member"';
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { dump } from "js-yaml";
|
||||||
|
import { beforeEach, describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { loadConfig } from "~/server/config/load";
|
||||||
|
|
||||||
|
import { clearFakeFiles, createFakeFile } from "../setup/overlay-fs";
|
||||||
|
|
||||||
|
const CONFIG_PATH = "/etc/headplane/config.yaml";
|
||||||
|
|
||||||
|
function writeConfig(overrides: {
|
||||||
|
server?: Record<string, unknown>;
|
||||||
|
headscale?: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
|
createFakeFile(
|
||||||
|
CONFIG_PATH,
|
||||||
|
dump({
|
||||||
|
server: {
|
||||||
|
host: "0.0.0.0",
|
||||||
|
port: 3000,
|
||||||
|
cookie_secret: "abcdefghijklmnopqrstuvwxyz123456",
|
||||||
|
cookie_secure: false,
|
||||||
|
...overrides.server,
|
||||||
|
},
|
||||||
|
headscale: { url: "http://localhost:8080", ...overrides.headscale },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `string.lower` rewrites the value it validates rather than rejecting it, so
|
||||||
|
// declaring a filesystem path with it silently points Headplane at a different
|
||||||
|
// directory on a case-sensitive filesystem.
|
||||||
|
describe("filesystem paths preserve their casing", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
clearFakeFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("loadConfig_withMixedCaseDataPath_preservesIt", async () => {
|
||||||
|
writeConfig({ server: { data_path: "/srv/Headplane" } });
|
||||||
|
const config = await loadConfig(CONFIG_PATH);
|
||||||
|
|
||||||
|
expect(config.server.data_path).toBe("/srv/Headplane");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("loadConfig_withMixedCaseHeadscalePaths_preservesThem", async () => {
|
||||||
|
writeConfig({
|
||||||
|
headscale: {
|
||||||
|
config_path: "/opt/Headscale/config.yaml",
|
||||||
|
dns_records_path: "/opt/Headscale/records.json",
|
||||||
|
tls_cert_path: "/opt/Headscale/Cert.pem",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const config = await loadConfig(CONFIG_PATH);
|
||||||
|
|
||||||
|
expect(config.headscale.config_path).toBe("/opt/Headscale/config.yaml");
|
||||||
|
expect(config.headscale.dns_records_path).toBe("/opt/Headscale/records.json");
|
||||||
|
expect(config.headscale.tls_cert_path).toBe("/opt/Headscale/Cert.pem");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("loadConfig_withMixedCaseCookieDomain_stillLowercasesIt", async () => {
|
||||||
|
// Not a path: DNS is case-insensitive, so normalising here is correct and
|
||||||
|
// should stay.
|
||||||
|
writeConfig({ server: { cookie_domain: "Headplane.Example.COM" } });
|
||||||
|
const config = await loadConfig(CONFIG_PATH);
|
||||||
|
|
||||||
|
expect(config.server.cookie_domain).toBe("headplane.example.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user