feat: add e2e integration tests and canonical versioning

This commit is contained in:
Aarnav Tale
2025-11-16 18:31:09 -05:00
parent 63d1e84ebe
commit 08a251cc8c
16 changed files with 536 additions and 17 deletions
+74
View File
@@ -0,0 +1,74 @@
server_url: http://localhost:8080
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false
noise:
private_key_path: /var/lib/headscale/noise_private.key
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
allocation: sequential
derp:
server:
enabled: false
region_id: 999
region_code: "headscale"
region_name: "Headscale Embedded DERP"
verify_clients: true
stun_listen_addr: "0.0.0.0:3478"
private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true
ipv4: 198.51.100.1
ipv6: 2001:db8::1
urls:
- https://controlplane.tailscale.com/derpmap/default
paths: []
auto_update_enabled: true
update_frequency: 3h
disable_check_updates: false
ephemeral_node_inactivity_timeout: 30m
database:
type: sqlite
debug: false
gorm:
prepare_stmt: true
parameterized_queries: true
skip_err_record_not_found: true
slow_threshold: 1000
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
wal_autocheckpoint: 1000
acme_url: https://acme-v02.api.letsencrypt.org/directory
acme_email: ""
tls_letsencrypt_hostname: ""
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ":http"
tls_cert_path: ""
tls_key_path: ""
log:
level: info
format: text
policy:
mode: database
path: ""
dns:
magic_dns: true
base_domain: example.com
override_local_dns: true
nameservers:
global:
- 1.1.1.1
- 1.0.0.1
- 2606:4700:4700::1111
- 2606:4700:4700::1001
split: {}
search_domains: []
extra_records: []
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
logtail:
enabled: false
randomize_client_port: false
+49
View File
@@ -0,0 +1,49 @@
import hashes from '~/openapi-operation-hashes.json';
import {
createHeadscaleInterface,
type HeadscaleApiInterface,
} from '~/server/headscale/api';
import { type HeadscaleEnv, startHeadscale } from './start-headscale';
export type Version = keyof typeof hashes;
export const HS_VERSIONS = Object.keys(hashes) as Version[];
interface VersionStateEntry {
env: HeadscaleEnv;
bootstrap: HeadscaleApiInterface;
}
const versionState = new Map<Version, VersionStateEntry>();
async function ensureVersion(version: Version) {
if (versionState.has(version)) {
return versionState.get(version)!;
}
const env = await startHeadscale(version);
const bootstrap = await createHeadscaleInterface(env.apiUrl);
const entry = { env, bootstrap };
versionState.set(version, entry);
return entry;
}
export async function getBootstrapClient(version: Version) {
const { bootstrap } = await ensureVersion(version);
return bootstrap;
}
export async function getRuntimeClient(version: Version) {
const { env, bootstrap } = await ensureVersion(version);
return bootstrap.getRuntimeClient(env.apiKey);
}
export async function stopAllVersions() {
for (const { env } of versionState.values()) {
await env.container.stop({
remove: true,
removeVolumes: true,
});
}
versionState.clear();
}
@@ -0,0 +1,53 @@
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import tc from 'testcontainers';
export interface HeadscaleEnv {
container: tc.StartedTestContainer;
apiUrl: string;
apiKey: string;
}
const cwd = fileURLToPath(import.meta.url);
const config = join(cwd, '..', 'config.yaml');
export async function startHeadscale(version: string): Promise<HeadscaleEnv> {
const container = await new tc.GenericContainer(
`headscale/headscale:${version}`,
)
.withExposedPorts(8080)
.withWaitStrategy(
tc.Wait.forHttp('/health', 8080)
.withStartupTimeout(30_000)
.forStatusCode(200),
)
.withCopyFilesToContainer([
{
source: config,
target: '/etc/headscale/config.yaml',
},
])
.withCommand(['serve'])
.start();
const host = container.getHost();
const port = container.getMappedPort(8080);
const apiUrl = `http://${host}:${port}`;
const exec = await container.exec([
'headscale',
'apikeys',
'create',
'-o',
'json',
]);
if (exec.exitCode !== 0) {
throw new Error(`headscale apikeys create failed:\n${exec.stderr}`);
}
const apiKey = JSON.parse(exec.stdout.toString());
return { container, apiUrl, apiKey };
}
// await startHeadscale('0.26.0');
+11
View File
@@ -0,0 +1,11 @@
import { getBootstrapClient, HS_VERSIONS, stopAllVersions } from './env';
export async function setup() {
for (const version of HS_VERSIONS) {
await getBootstrapClient(version);
}
}
export async function teardown() {
await stopAllVersions();
}