Commit df7b0491 authored by Hugo "ThePooN" Denizart's avatar Hugo "ThePooN" Denizart
Browse files

Merge branch 'production-2022' into 'master'

Kubernetes-based production deploy for Open 2022

See merge request !11
parents 9b98fad0 b67e50bb
Pipeline #4827 passed with stages
in 3 minutes and 38 seconds
image: docker:dind variables:
services: POSTGRES_ENABLED: "false"
- docker:dind REVIEW_DISABLED: "true"
K8S_SECRET_NODE_ENV: production
K8S_SECRET_CRON_ENABLED: "false"
stages: stages:
- test
- build - build
- backup
- deploy - deploy
- review
- test
- staging
- canary
- production
- incremental rollout 10%
- incremental rollout 25%
- incremental rollout 50%
- incremental rollout 100%
- cleanup
cache: include:
paths: - template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
- node_modules/ - template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
variables:
CONTAINER_TEST_IMAGE: git.cartooncraft.fr/corsace/open-2020:$CI_BUILD_REF_NAME
CONTAINER_RELEASE_IMAGE: git.cartooncraft.fr/corsace/open-2020:latest
lint:
stage: test
image: node:16
script:
- apk update && apk add git
- npm i
- npm run lint
build: build:
stage: build
script:
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN git.cartooncraft.fr
- docker build --pull -t $CONTAINER_TEST_IMAGE .
- docker push $CONTAINER_TEST_IMAGE
- ./deploy-scripts/release-if-master.sh
tags: tags:
- docker-build - privileged
backup production:
stage: backup
only:
- master
script:
- ./deploy-scripts/install-requirements.sh
- eval $(ssh-agent -s)
- ./deploy-scripts/setup-ssh.sh
- ./deploy-scripts/backup-db.sh
- ./deploy-scripts/backup-avatars.sh
deploy to production:
stage: deploy
environment:
name: production
url: https://open.corsace.io
only:
- master
script:
- ./deploy-scripts/install-requirements.sh
- eval $(ssh-agent -s)
- ./deploy-scripts/setup-ssh.sh
- ./deploy-scripts/deploy.sh
workers:
cron:
replicaCount: 1
command:
- /bin/sh
- -c
- CRON_ENABLED=true node build/server/index.js
strategyType: Recreate
persistence:
enabled: true
volumes:
- name: avatars
mount:
path: /src/data/avatars
claim:
accessMode: ReadWriteMany
storageClass: rook-cephfs
size: 50Mi
resources:
requests:
memory: 512Mi
limits:
memory: 1024Mi
...@@ -37,11 +37,16 @@ ...@@ -37,11 +37,16 @@
"invite": "" "invite": ""
}, },
"osu": { "osu": {
"proxyUrl": "",
"disableRateLimiting": false,
"clientId": "", "clientId": "",
"clientSecret": "", "clientSecret": "",
"apiKey": "", "apiKey": "",
"bannedIds": [] "bannedIds": []
}, },
"cron": {
"enabled": true
},
"registrationDeadline": "Jul 24 2022 12:00 UTC+0", "registrationDeadline": "Jul 24 2022 12:00 UTC+0",
"qualifiersDeadline": "Aug 13 2022 UTC+0" "qualifiersDeadline": "Aug 13 2022 UTC+0"
} }
{ {
"http": { "http": {
"host": "0.0.0.0", "host": "0.0.0.0",
"port": 5000,
"publicUrl": "https://open.corsace.io" "publicUrl": "https://open.corsace.io"
}, },
"mongo": { "mongo": {
"uri": "mongodb://mongo/corsace-open" "uri": "mongodb://mongo-mongodb/corsace-open"
}, },
"discord": { "discord": {
"roles": { "roles": {
...@@ -31,9 +32,14 @@ ...@@ -31,9 +32,14 @@
"invite": "Z6vEMsr" "invite": "Z6vEMsr"
}, },
"osu": { "osu": {
"proxyUrl": "http://osu-api.gitlab-managed-apps.svc.cluster.local",
"disableRateLimiting": true,
"clientId": "8286", "clientId": "8286",
"bannedIds": [] "bannedIds": []
}, },
"cron": {
"enabled": false
},
"registrationDeadline": "Jul 24 2022 23:59 UTC+0", "registrationDeadline": "Jul 24 2022 23:59 UTC+0",
"qualifiersDeadline": "Aug 13 2022 UTC+0" "qualifiersDeadline": "Aug 13 2022 UTC+0"
} }
\ No newline at end of file
...@@ -10,3 +10,4 @@ npm run build:server && ...@@ -10,3 +10,4 @@ npm run build:server &&
npm prune --production npm prune --production
cp config.production.json config.json
...@@ -31,8 +31,7 @@ ...@@ -31,8 +31,7 @@
"querystring": "^0.2.1", "querystring": "^0.2.1",
"simple-oauth2": "^4.3.0", "simple-oauth2": "^4.3.0",
"stoppable": "^1.1.0", "stoppable": "^1.1.0",
"winston": "^3.8.1", "winston": "^3.8.1"
"winston-daily-rotate-file": "^4.7.1"
}, },
"devDependencies": { "devDependencies": {
"@kazupon/vue-i18n-loader": "^0.5.0", "@kazupon/vue-i18n-loader": "^0.5.0",
...@@ -5580,14 +5579,6 @@ ...@@ -5580,14 +5579,6 @@
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
}, },
"node_modules/file-stream-rotator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz",
"integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==",
"dependencies": {
"moment": "^2.29.1"
}
},
"node_modules/file-type": { "node_modules/file-type": {
"version": "9.0.0", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz", "resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz",
...@@ -7508,14 +7499,6 @@ ...@@ -7508,14 +7499,6 @@
"mkdirp": "bin/cmd.js" "mkdirp": "bin/cmd.js"
} }
}, },
"node_modules/moment": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==",
"engines": {
"node": "*"
}
},
"node_modules/mongodb": { "node_modules/mongodb": {
"version": "4.7.0", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.7.0.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.7.0.tgz",
...@@ -7986,14 +7969,6 @@ ...@@ -7986,14 +7969,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/object-hash": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
"engines": {
"node": ">= 6"
}
},
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz",
...@@ -11554,23 +11529,6 @@ ...@@ -11554,23 +11529,6 @@
"node": ">= 12.0.0" "node": ">= 12.0.0"
} }
}, },
"node_modules/winston-daily-rotate-file": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-4.7.1.tgz",
"integrity": "sha512-7LGPiYGBPNyGHLn9z33i96zx/bd71pjBn9tqQzO3I4Tayv94WPmBNwKC7CO1wPHdP9uvu+Md/1nr6VSH9h0iaA==",
"dependencies": {
"file-stream-rotator": "^0.6.1",
"object-hash": "^2.0.1",
"triple-beam": "^1.3.0",
"winston-transport": "^4.4.0"
},
"engines": {
"node": ">=8"
},
"peerDependencies": {
"winston": "^3"
}
},
"node_modules/winston-transport": { "node_modules/winston-transport": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
...@@ -16007,14 +15965,6 @@ ...@@ -16007,14 +15965,6 @@
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
}, },
"file-stream-rotator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz",
"integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==",
"requires": {
"moment": "^2.29.1"
}
},
"file-type": { "file-type": {
"version": "9.0.0", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz", "resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz",
...@@ -17528,11 +17478,6 @@ ...@@ -17528,11 +17478,6 @@
"minimist": "^1.2.5" "minimist": "^1.2.5"
} }
}, },
"moment": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw=="
},
"mongodb": { "mongodb": {
"version": "4.7.0", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.7.0.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.7.0.tgz",
...@@ -17915,11 +17860,6 @@ ...@@ -17915,11 +17860,6 @@
} }
} }
}, },
"object-hash": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="
},
"object-inspect": { "object-inspect": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz",
...@@ -20841,17 +20781,6 @@ ...@@ -20841,17 +20781,6 @@
} }
} }
}, },
"winston-daily-rotate-file": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-4.7.1.tgz",
"integrity": "sha512-7LGPiYGBPNyGHLn9z33i96zx/bd71pjBn9tqQzO3I4Tayv94WPmBNwKC7CO1wPHdP9uvu+Md/1nr6VSH9h0iaA==",
"requires": {
"file-stream-rotator": "^0.6.1",
"object-hash": "^2.0.1",
"triple-beam": "^1.3.0",
"winston-transport": "^4.4.0"
}
},
"winston-transport": { "winston-transport": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
......
...@@ -26,8 +26,7 @@ ...@@ -26,8 +26,7 @@
"querystring": "^0.2.1", "querystring": "^0.2.1",
"simple-oauth2": "^4.3.0", "simple-oauth2": "^4.3.0",
"stoppable": "^1.1.0", "stoppable": "^1.1.0",
"winston": "^3.8.1", "winston": "^3.8.1"
"winston-daily-rotate-file": "^4.7.1"
}, },
"devDependencies": { "devDependencies": {
"@kazupon/vue-i18n-loader": "^0.5.0", "@kazupon/vue-i18n-loader": "^0.5.0",
......
NODE_ENV=production NODE_ENV=production
SESSION_SECRET= SESSION_SECRET=
DISCORD_TOKEN= DISCORD_TOKEN=
DISCORD_CLIENTID=4
DISCORD_CLIENTSECRET= DISCORD_CLIENTSECRET=
OSU_PASSWORD= OSU_APIKEY=
OSU_APIKEY= OSU_CLIENTSECRET=
\ No newline at end of file \ No newline at end of file
...@@ -41,7 +41,11 @@ export class App { ...@@ -41,7 +41,11 @@ export class App {
public cron = new Cron(); public cron = new Cron();
public nodesu = this.config.osu.apiKey ? new nodesu.Client(this.config.osu.apiKey, { requestsPerMinute: 300 }) : null; public nodesu = this.config.osu.apiKey ? new nodesu.Client(this.config.osu.apiKey, {
baseUrl: this.config.osu.proxyUrl ? `${this.config.osu.proxyUrl}/api` : undefined,
disableRateLimiting: this.config.osu.disableRateLimiting,
requestsPerMinute: !this.config.osu.disableRateLimiting ? 300 : undefined,
}) : null;
public osuApiV2 = this.config.osu.clientId && this.config.osu.clientSecret ? new OsuApiV2(this.config.osu.clientId, this.config.osu.clientSecret) : null; public osuApiV2 = this.config.osu.clientId && this.config.osu.clientSecret ? new OsuApiV2(this.config.osu.clientId, this.config.osu.clientSecret) : null;
constructor() { constructor() {
...@@ -91,7 +95,6 @@ export class App { ...@@ -91,7 +95,6 @@ export class App {
this.logger.info("Starting app..."); this.logger.info("Starting app...");
// @ts-ignore mongoose typings don't have autoIndex
mongoose.connect(this.config.mongo.uri, { autoIndex: false }); mongoose.connect(this.config.mongo.uri, { autoIndex: false });
mongoose.connection.on("error", (error) => mongoose.connection.on("error", (error) =>
Logger.getLogger("mongo").error("Connection error:", { error })); Logger.getLogger("mongo").error("Connection error:", { error }));
...@@ -275,7 +278,9 @@ export class App { ...@@ -275,7 +278,9 @@ export class App {
this.discordClient.login(this.config.discord.token).catch((error) => Logger.getLogger("discord").error("Couldn't connect to Discord", { error })); this.discordClient.login(this.config.discord.token).catch((error) => Logger.getLogger("discord").error("Couldn't connect to Discord", { error }));
this.cron.init(this); if (this.config.cron.enabled) {
this.cron.init(this);
}
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
...@@ -291,6 +296,7 @@ export class App { ...@@ -291,6 +296,7 @@ export class App {
public stop(): Promise<void> { public stop(): Promise<void> {
this.logger.info("Stopping app..."); this.logger.info("Stopping app...");
this.cron.stop();
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
await mongoose.connection.close(); await mongoose.connection.close();
await this.discordClient.destroy(); await this.discordClient.destroy();
......
...@@ -42,11 +42,16 @@ export class Config { ...@@ -42,11 +42,16 @@ export class Config {
invite: "", invite: "",
}; };
public osu: { public osu: {
proxyUrl: "",
disableRateLimiting: boolean,
clientId: "", clientId: "",
clientSecret: "", clientSecret: "",
apiKey?: ""; apiKey?: "";
bannedIds: number[], bannedIds: number[],
}; };
public cron: {
enabled: boolean;
};
public registrationDeadline: Date; public registrationDeadline: Date;
public qualifiersDeadline: Date; public qualifiersDeadline: Date;
...@@ -104,6 +109,11 @@ export class Config { ...@@ -104,6 +109,11 @@ export class Config {
path: "osu.clientSecret", path: "osu.clientSecret",
type: String, type: String,
}, },
{
env: "CRON_ENABLED",
path: "cron.enabled",
type: Boolean,
},
]; ];
for(const envOption of envOptions) { for(const envOption of envOptions) {
...@@ -111,8 +121,8 @@ export class Config { ...@@ -111,8 +121,8 @@ export class Config {
Logger.getLogger("config").error("Environment option " + envOption.env + " is needed for production deployment!"); Logger.getLogger("config").error("Environment option " + envOption.env + " is needed for production deployment!");
process.exit(1); process.exit(1);
} }
const val = envOption.type(process.env[envOption.env]); const val = envOption.type === Boolean ? ['true', '1'].includes(process.env[envOption.env]?.toLowerCase()) : envOption.type(process.env[envOption.env]);
if(!val) { if(envOption.type !== Boolean && !val) {
Logger.getLogger("config").error("Environment option " + envOption.env + " is of incorrect type!"); Logger.getLogger("config").error("Environment option " + envOption.env + " is of incorrect type!");
process.exit(1); process.exit(1);
} }
......
...@@ -11,6 +11,10 @@ export class Cron { ...@@ -11,6 +11,10 @@ export class Cron {
private logger = Logger.getLogger("cron"); private logger = Logger.getLogger("cron");
public init(app: App) { public init(app: App) {
if(this.tasks.length !== 0) {
throw new Error('Cron tasks are already initiliazed!');
}
this.tasks.push(new CronJob("0 0 0 * * *", async () => { this.tasks.push(new CronJob("0 0 0 * * *", async () => {
try { try {
if(app.config.registrationDeadline.getTime() > Date.now()) { if(app.config.registrationDeadline.getTime() > Date.now()) {
......
import * as winston from "winston"; import * as winston from "winston";
import winstonDailyRotateFile from "winston-daily-rotate-file";
export abstract class Logger { export abstract class Logger {
public static loggerTransports = [ public static loggerTransports = [
new winston.transports.Console({ level: "silly", format: winston.format.simple() }), new winston.transports.Console({ level: "silly" }),
new winstonDailyRotateFile({
dirname: "data/logs/",
filename: "%DATE%.log",
level: "info",
}),
]; ];
public static getLogger(label?: string): winston.Logger { public static getLogger(label?: string): winston.Logger {
return winston.createLogger({ return winston.createLogger({
format: winston.format.combine( format: winston.format.combine(
winston.format.errors({
message: true,
stack: true,
}),
winston.format((info, opts) => {
for (const key of ['err', 'error']) {
if (info[key] && info[key] instanceof Error) {
info[key] = {
message: info[key].message,
stack: info[key].stack,
...info[key],
};
}
}
return info;
})(),
winston.format.label({ label }), winston.format.label({ label }),
winston.format.timestamp(), winston.format.timestamp(),
winston.format.json(), winston.format.json(),
......
...@@ -3,7 +3,7 @@ import { App } from "./App"; ...@@ -3,7 +3,7 @@ import { App } from "./App";
export const app = App.instance; export const app = App.instance;
app.start(); app.start();
process.on("SIGINT", async () => { process.on("SIGTERM", async () => {
await app.stop(); await app.stop();
process.exit(0); process.exit(0);
}); });
import * as mongoose from "mongoose"; import mongoose from "mongoose";
import { ITeam, ITeamInfos, Team } from "./Team"; import { ITeam, ITeamInfos, Team } from "./Team";
import { IUser, User, IUserInfos } from "./User"; import { IUser, User, IUserInfos } from "./User";
......
import * as mongoose from "mongoose"; import mongoose from "mongoose";
import { ITeam, ITeamInfos } from "./Team"; import { ITeam, ITeamInfos } from "./Team";
import { IMatch, IMatchInfos, Match } from "./Match"; import { IMatch, IMatchInfos, Match } from "./Match";
......
import * as mongoose from "mongoose"; import mongoose from "mongoose";
export interface IMap extends mongoose.Document { export interface IMap extends mongoose.Document {
setID: number; setID: number;
......
import * as mongoose from "mongoose"; import mongoose from "mongoose";
import { ITeam, ITeamInfos, Team } from "./Team";