Merge pull request #312 from PretendoNetwork/feat/server-config

Server provisioning
This commit is contained in:
William Oldham 2026-01-01 23:17:49 +00:00 committed by GitHub
commit 2f5a4b95db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 95 additions and 2 deletions

View File

@ -95,4 +95,5 @@ Configurations are loaded through environment variables. `.env` files are suppor
| `PN_ACT_CONFIG_DISCOURSE_API_USERNAME` | Used for anonymizing user accounts on Discourse during account deletion. Forum anonymization skipped if not set | Yes |
| `PN_ACT_CONFIG_GRPC_MIIVERSE_HOST` | Used to remove Miiverse user data during account deletion | No |
| `PN_ACT_CONFIG_GRPC_MIIVERSE_PORT` | Used to remove Miiverse user data during account deletion | No |
| `PN_ACT_CONFIG_GRPC_MIIVERSE_KEY_API` | Used to remove Miiverse user data during account deletion | No |
| `PN_ACT_CONFIG_GRPC_MIIVERSE_KEY_API` | Used to remove Miiverse user data during account deletion | No |
| `PN_ACT_PROVISIONING_SERVER_CONFIG` | Specify a path to a JSON file containing a list of servers to provision automatically to the DB | Yes |

View File

@ -13,7 +13,8 @@ export const disabledFeatures = {
email: false,
captcha: false,
s3: false,
datastore: false
datastore: false,
serverProvisioning: false
};
const hexadecimalStringRegex = /^[0-9a-f]+$/i;
@ -41,6 +42,9 @@ export const config: Config = {
connection_string: process.env.PN_ACT_CONFIG_MONGO_CONNECTION_STRING || '',
options: mongooseConnectOptions
},
provisioning: {
server_config: process.env.PN_ACT_PROVISIONING_SERVER_CONFIG || ''
},
redis: {
client: {
url: process.env.PN_ACT_CONFIG_REDIS_URL || ''
@ -282,6 +286,11 @@ if (!config.datastore.signature_secret) {
}
}
if (!config.provisioning.server_config) {
LOG_WARN('A server provisioning config file as not been set. Disabling feature, no server data will be provisioned. To enable feature set the PN_ACT_PROVISIONING_SERVER_CONFIG environment variable');
disabledFeatures.serverProvisioning = true;
}
if (!configValid) {
LOG_ERROR('Config is invalid. Exiting');
process.exit(0);

77
src/provisioning.ts Normal file
View File

@ -0,0 +1,77 @@
import fs from 'node:fs/promises';
import { z } from 'zod';
import mongoose from 'mongoose';
import { config, disabledFeatures } from './config-manager';
import { LOG_INFO, LOG_WARN } from './logger';
import { Server } from './models/server';
// Provisioning has a couple edgecases:
// - It will only update existing entries, will not add new one
// - Only the fields in the below schema will be updated
const serverProvisioningSchema = z.object({
servers: z.array(z.object({
id: z.string(),
name: z.string(),
ip: z.string(),
port: z.coerce.number()
}))
});
async function readServerProvisioning(configPath: string): Promise<z.infer<typeof serverProvisioningSchema>> {
const fileContents = await fs.readFile(configPath, 'utf-8');
const parsedConfig = JSON.parse(fileContents);
return serverProvisioningSchema.parse(parsedConfig);
}
export async function handleServerProvisioning(): Promise<void> {
const serverData = await readServerProvisioning(config.provisioning.server_config).catch((err) => {
LOG_WARN('Failed to parse server provisioning config:');
console.error(err);
});
if (!serverData) {
return;
}
LOG_INFO('Starting server provisioning');
for (const server of serverData.servers) {
const id = new mongoose.Types.ObjectId(server.id);
const result = await Server.findOneAndUpdate(id, {
$set: {
_id: id,
service_name: server.name,
ip: server.ip,
port: server.port
}
});
if (!result) {
LOG_WARN(`Could not find existing server DB entry for ID ${server.id} - skipping provisioning`);
}
}
LOG_INFO(`Finished provisioning ${serverData.servers.length} servers`);
}
export function startProvisioner(): void {
if (disabledFeatures.serverProvisioning) {
return;
}
const runProvisioning = (): void => {
handleServerProvisioning().catch((err) => {
LOG_WARN('Failed to provision servers:');
console.error(err);
});
};
// Run once at boot
runProvisioning();
(async (): Promise<void> => {
const watcher = fs.watch(config.provisioning.server_config);
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Dont need this var
for await (const _ of watcher) {
LOG_INFO('Detected a change in the server provisioning config');
runProvisioning();
}
})();
}

View File

@ -16,6 +16,7 @@ import api from '@/services/api';
import localcdn from '@/services/local-cdn';
import assets from '@/services/assets';
import { config, disabledFeatures } from '@/config-manager';
import { startProvisioner } from './provisioning';
process.title = 'Pretendo - Account';
process.on('uncaughtException', (err, origin) => {
@ -113,6 +114,8 @@ async function main(): Promise<void> {
await startGRPCServer();
LOG_SUCCESS(`gRPC server started on port ${config.grpc.port}`);
startProvisioner();
app.listen(config.http.port, () => {
LOG_SUCCESS(`HTTP server started on port ${config.http.port}`);
});

View File

@ -13,6 +13,9 @@ export interface Config {
connection_string: string;
options: mongoose.ConnectOptions;
};
provisioning: {
server_config: string;
};
redis: {
client: {
url: string;