Browse Source

Fix metrics auth and enable fork CI smoke runs

pull/2552/head
Alexander Shirokii 4 months ago
parent
commit
1cbcdea9d7
  1. 4
      .github/workflows/deploy-pr.yml
  2. 3
      .github/workflows/lint.yml
  3. 3
      Dockerfile
  4. 8
      src/server/database/repositories/general/types.ts
  5. 16
      src/server/database/repositories/interface/service.ts
  6. 18
      src/server/utils/cache.ts
  7. 23
      src/server/utils/handler.ts
  8. 26
      src/test/unit/cache.spec.ts
  9. 43
      src/test/unit/general.spec.ts

4
.github/workflows/deploy-pr.yml

@ -2,6 +2,9 @@ name: Pull Request
on: on:
workflow_dispatch: workflow_dispatch:
push:
branches:
- ci/**
pull_request: pull_request:
concurrency: concurrency:
@ -12,7 +15,6 @@ jobs:
docker: docker:
name: Build Docker name: Build Docker
runs-on: ${{ matrix.arch.os }} runs-on: ${{ matrix.arch.os }}
if: github.repository_owner == 'wg-easy'
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:

3
.github/workflows/lint.yml

@ -4,13 +4,13 @@ on:
push: push:
branches: branches:
- master - master
- ci/**
pull_request: pull_request:
jobs: jobs:
docs: docs:
name: Check Docs name: Check Docs
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository_owner == 'wg-easy'
steps: steps:
- name: Checkout repository - name: Checkout repository
@ -37,7 +37,6 @@ jobs:
name: Lint name: Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: docs needs: docs
if: github.repository_owner == 'wg-easy'
strategy: strategy:
fail-fast: false fail-fast: false

3
Dockerfile

@ -34,7 +34,8 @@ RUN ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') && \
"https://github.com/go-acme/lego/releases/download/${LEGO_VERSION}/lego_${LEGO_VERSION}_linux_${ARCH}.tar.gz" && \ "https://github.com/go-acme/lego/releases/download/${LEGO_VERSION}/lego_${LEGO_VERSION}_linux_${ARCH}.tar.gz" && \
wget -qO /tmp/lego.tar.gz.sha256 \ wget -qO /tmp/lego.tar.gz.sha256 \
"https://github.com/go-acme/lego/releases/download/${LEGO_VERSION}/lego_${LEGO_VERSION#v}_checksums.txt" && \ "https://github.com/go-acme/lego/releases/download/${LEGO_VERSION}/lego_${LEGO_VERSION#v}_checksums.txt" && \
grep "lego_${LEGO_VERSION}_linux_${ARCH}.tar.gz" /tmp/lego.tar.gz.sha256 | sha256sum -c - && \ grep "lego_${LEGO_VERSION}_linux_${ARCH}.tar.gz" /tmp/lego.tar.gz.sha256 | \
awk '{ print $1 " /tmp/lego.tar.gz" }' | sha256sum -c - && \
tar -xzf /tmp/lego.tar.gz -C /tmp lego && \ tar -xzf /tmp/lego.tar.gz -C /tmp lego && \
mv /tmp/lego /usr/local/bin/lego && \ mv /tmp/lego /usr/local/bin/lego && \
chmod +x /usr/local/bin/lego && \ chmod +x /usr/local/bin/lego && \

8
src/server/database/repositories/general/types.ts

@ -18,6 +18,14 @@ export const GeneralUpdateSchema = z.object({
metricsPrometheus: metricsEnabled, metricsPrometheus: metricsEnabled,
metricsJson: metricsEnabled, metricsJson: metricsEnabled,
metricsPassword: metricsPassword, metricsPassword: metricsPassword,
}).superRefine((data, ctx) => {
if ((data.metricsPrometheus || data.metricsJson) && !data.metricsPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['metricsPassword'],
message: t('zod.general.metricsPassword'),
});
}
}); });
export type GeneralUpdateType = z.infer<typeof GeneralUpdateSchema>; export type GeneralUpdateType = z.infer<typeof GeneralUpdateSchema>;

16
src/server/database/repositories/interface/service.ts

@ -92,19 +92,19 @@ export class InterfaceService {
const clients = await tx.query.client.findMany().execute(); const clients = await tx.query.client.findMany().execute();
for (const client of clients) { for (const client of clients) {
// TODO: optimize const index = clients.findIndex(({ id }) => id === client.id);
const clients = await tx.query.client.findMany().execute(); const nextClients = clients.slice();
// only calculate ip if cidr has changed // only calculate ip if cidr has changed
let nextIpv4 = client.ipv4Address; let nextIpv4 = client.ipv4Address;
if (data.ipv4Cidr !== oldCidr.ipv4Cidr) { if (data.ipv4Cidr !== oldCidr.ipv4Cidr) {
nextIpv4 = nextIP(4, parseCidr(data.ipv4Cidr), clients); nextIpv4 = nextIP(4, parseCidr(data.ipv4Cidr), nextClients);
} }
let nextIpv6 = client.ipv6Address; let nextIpv6 = client.ipv6Address;
if (data.ipv6Cidr !== oldCidr.ipv6Cidr) { if (data.ipv6Cidr !== oldCidr.ipv6Cidr) {
nextIpv6 = nextIP(6, parseCidr(data.ipv6Cidr), clients); nextIpv6 = nextIP(6, parseCidr(data.ipv6Cidr), nextClients);
} }
await tx await tx
@ -115,6 +115,14 @@ export class InterfaceService {
}) })
.where(eq(clientSchema.id, client.id)) .where(eq(clientSchema.id, client.id))
.execute(); .execute();
if (index !== -1) {
clients[index] = {
...client,
ipv4Address: nextIpv4,
ipv6Address: nextIpv6,
};
}
} }
}); });
} }

18
src/server/utils/cache.ts

@ -8,17 +8,27 @@ type Opts = {
/** /**
* Cache function for 1 hour * Cache function for 1 hour
*/ */
export function cacheFunction<T>(fn: () => T, { expiry }: Opts): () => T { export function cacheFunction<T>(
let cache: { value: T; expiry: number } | null = null; fn: () => Promise<T>,
{ expiry }: Opts
): () => Promise<T> {
let cache: { value: Promise<T>; expiry: number } | null = null;
return (): T => { return (): Promise<T> => {
const now = Date.now(); const now = Date.now();
if (cache && cache.expiry > now) { if (cache && cache.expiry > now) {
return cache.value; return cache.value;
} }
const result = fn(); const result = fn().catch((error) => {
if (cache?.value === result) {
cache = null;
}
throw error;
});
cache = { cache = {
value: result, value: result,
expiry: now + expiry, expiry: now + expiry,

23
src/server/utils/handler.ts

@ -140,7 +140,20 @@ export const defineMetricsHandler = <
return defineEventHandler(async (event) => { return defineEventHandler(async (event) => {
const metricsConfig = await Database.general.getMetricsConfig(); const metricsConfig = await Database.general.getMetricsConfig();
if (metricsConfig.password) { if (metricsConfig[type] !== true) {
throw createError({
statusCode: 400,
statusMessage: 'Metrics not enabled',
});
}
if (!metricsConfig.password) {
throw createError({
statusCode: 503,
statusMessage: 'Metrics password is not configured',
});
}
const auth = getHeader(event, 'Authorization'); const auth = getHeader(event, 'Authorization');
if (!auth) { if (!auth) {
@ -167,14 +180,6 @@ export const defineMetricsHandler = <
statusMessage: 'Incorrect token', statusMessage: 'Incorrect token',
}); });
} }
}
if (metricsConfig[type] !== true) {
throw createError({
statusCode: 400,
statusMessage: 'Metrics not enabled',
});
}
return await handler({ event }); return await handler({ event });
}); });

26
src/test/unit/cache.spec.ts

@ -0,0 +1,26 @@
import { describe, expect, test, vi } from 'vitest';
import { cacheFunction } from '../../server/utils/cache';
describe('cacheFunction', () => {
test('retries after a rejected async result instead of caching the failure', async () => {
const fn = vi
.fn<() => Promise<string>>()
.mockRejectedValueOnce(new Error('boom'))
.mockResolvedValueOnce('ok');
const cached = cacheFunction(fn, { expiry: 60_000 });
await expect(cached()).rejects.toThrow('boom');
await expect(cached()).resolves.toBe('ok');
expect(fn).toHaveBeenCalledTimes(2);
});
test('reuses successful async results until expiry', async () => {
const fn = vi.fn<() => Promise<string>>().mockResolvedValue('ok');
const cached = cacheFunction(fn, { expiry: 60_000 });
await expect(cached()).resolves.toBe('ok');
await expect(cached()).resolves.toBe('ok');
expect(fn).toHaveBeenCalledTimes(1);
});
});

43
src/test/unit/general.spec.ts

@ -0,0 +1,43 @@
import { beforeAll, describe, expect, test } from 'vitest';
declare global {
var t: (value: string) => string;
}
let GeneralUpdateSchema: typeof import('../../server/database/repositories/general/types').GeneralUpdateSchema;
beforeAll(async () => {
globalThis.t = (value: string) => value;
({ GeneralUpdateSchema } = await import(
'../../server/database/repositories/general/types'
));
});
describe('GeneralUpdateSchema', () => {
test('requires a metrics password when metrics are enabled', async () => {
await expect(
GeneralUpdateSchema.parseAsync({
sessionTimeout: 60,
metricsPrometheus: true,
metricsJson: false,
metricsPassword: null,
})
).rejects.toThrow();
});
test('allows disabling metrics without a password', async () => {
await expect(
GeneralUpdateSchema.parseAsync({
sessionTimeout: 60,
metricsPrometheus: false,
metricsJson: false,
metricsPassword: null,
})
).resolves.toEqual({
sessionTimeout: 60,
metricsPrometheus: false,
metricsJson: false,
metricsPassword: null,
});
});
});
Loading…
Cancel
Save