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
services:
- docker:dind
variables:
POSTGRES_ENABLED: "false"
REVIEW_DISABLED: "true"
K8S_SECRET_NODE_ENV: production
K8S_SECRET_CRON_ENABLED: "false"
stages:
- test
- build
- backup
- deploy
- review
- test
- staging
- canary
- production
- incremental rollout 10%
- incremental rollout 25%
- incremental rollout 50%
- incremental rollout 100%
- cleanup
cache:
paths:
- node_modules/
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
include:
- template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
- template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
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:
- docker-build
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
- privileged
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 @@
"invite": ""
},
"osu": {
"proxyUrl": "",
"disableRateLimiting": false,
"clientId": "",
"clientSecret": "",
"apiKey": "",
"bannedIds": []
},
"cron": {
"enabled": true
},
"registrationDeadline": "Jul 24 2022 12:00 UTC+0",
"qualifiersDeadline": "Aug 13 2022 UTC+0"
}
{
"http": {
"host": "0.0.0.0",
"port": 5000,
"publicUrl": "https://open.corsace.io"
},
"mongo": {
"uri": "mongodb://mongo/corsace-open"
"uri": "mongodb://mongo-mongodb/corsace-open"
},
"discord": {
"roles": {
......@@ -31,9 +32,14 @@
"invite": "Z6vEMsr"
},
"osu": {
"proxyUrl": "http://osu-api.gitlab-managed-apps.svc.cluster.local",
"disableRateLimiting": true,
"clientId": "8286",
"bannedIds": []
},
"cron": {
"enabled": false
},
"registrationDeadline": "Jul 24 2022 23:59 UTC+0",
"qualifiersDeadline": "Aug 13 2022 UTC+0"
}
\ No newline at end of file
......@@ -10,3 +10,4 @@ npm run build:server &&
npm prune --production
cp config.production.json config.json
......@@ -31,8 +31,7 @@
"querystring": "^0.2.1",
"simple-oauth2": "^4.3.0",
"stoppable": "^1.1.0",
"winston": "^3.8.1",
"winston-daily-rotate-file": "^4.7.1"
"winston": "^3.8.1"
},
"devDependencies": {
"@kazupon/vue-i18n-loader": "^0.5.0",
......@@ -5580,14 +5579,6 @@
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"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": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz",
......@@ -7508,14 +7499,6 @@
"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": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.7.0.tgz",
......@@ -7986,14 +7969,6 @@
"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": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz",
......@@ -11554,23 +11529,6 @@
"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": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
......@@ -16007,14 +15965,6 @@
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"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": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz",
......@@ -17528,11 +17478,6 @@
"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": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.7.0.tgz",
......@@ -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": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz",
......@@ -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": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
......
NODE_ENV=production
SESSION_SECRET=
DISCORD_TOKEN=
DISCORD_CLIENTID=4
DISCORD_CLIENTSECRET=
OSU_PASSWORD=
OSU_APIKEY=
\ No newline at end of file
OSU_APIKEY=
OSU_CLIENTSECRET=
\ No newline at end of file
......@@ -41,7 +41,11 @@ export class App {
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;
constructor() {
......@@ -91,7 +95,6 @@ export class App {
this.logger.info("Starting app...");
// @ts-ignore mongoose typings don't have autoIndex
mongoose.connect(this.config.mongo.uri, { autoIndex: false });
mongoose.connection.on("error", (error) =>
Logger.getLogger("mongo").error("Connection error:", { error }));
......@@ -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.cron.init(this);
if (this.config.cron.enabled) {
this.cron.init(this);
}
return new Promise(async (resolve, reject) => {
try {
......@@ -291,6 +296,7 @@ export class App {
public stop(): Promise<void> {
this.logger.info("Stopping app...");
this.cron.stop();
return new Promise(async (resolve, reject) => {
await mongoose.connection.close();
await this.discordClient.destroy();
......
......@@ -42,11 +42,16 @@ export class Config {
invite: "",
};
public osu: {
proxyUrl: "",
disableRateLimiting: boolean,
clientId: "",
clientSecret: "",
apiKey?: "";
bannedIds: number[],
};
public cron: {
enabled: boolean;
};
public registrationDeadline: Date;
public qualifiersDeadline: Date;
......@@ -104,6 +109,11 @@ export class Config {
path: "osu.clientSecret",
type: String,
},
{
env: "CRON_ENABLED",
path: "cron.enabled",
type: Boolean,
},
];
for(const envOption of envOptions) {
......@@ -111,8 +121,8 @@ export class Config {
Logger.getLogger("config").error("Environment option " + envOption.env + " is needed for production deployment!");
process.exit(1);
}
const val = envOption.type(process.env[envOption.env]);
if(!val) {
const val = envOption.type === Boolean ? ['true', '1'].includes(process.env[envOption.env]?.toLowerCase()) : envOption.type(process.env[envOption.env]);
if(envOption.type !== Boolean && !val) {
Logger.getLogger("config").error("Environment option " + envOption.env + " is of incorrect type!");
process.exit(1);
}
......
......@@ -11,6 +11,10 @@ export class Cron {
private logger = Logger.getLogger("cron");
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 () => {
try {
if(app.config.registrationDeadline.getTime() > Date.now()) {
......
import * as winston from "winston";
import winstonDailyRotateFile from "winston-daily-rotate-file";
export abstract class Logger {
public static loggerTransports = [
new winston.transports.Console({ level: "silly", format: winston.format.simple() }),
new winstonDailyRotateFile({
dirname: "data/logs/",
filename: "%DATE%.log",
level: "info",
}),
new winston.transports.Console({ level: "silly" }),
];
public static getLogger(label?: string): winston.Logger {
return winston.createLogger({
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.timestamp(),
winston.format.json(),
......
......@@ -3,7 +3,7 @@ import { App } from "./App";
export const app = App.instance;
app.start();
process.on("SIGINT", async () => {
process.on("SIGTERM", async () => {
await app.stop();
process.exit(0);
});
import * as mongoose from "mongoose";
import mongoose from "mongoose";
import { ITeam, ITeamInfos, Team } from "./Team";
import { IUser, User, IUserInfos } from "./User";
......
import * as mongoose from "mongoose";
import mongoose from "mongoose";
import { ITeam, ITeamInfos } from "./Team";
import { IMatch, IMatchInfos, Match } from "./Match";
......
import * as mongoose from "mongoose";
import mongoose from "mongoose";
export interface IMap extends mongoose.Document {
setID: number;
......
import * as mongoose from "mongoose";
import mongoose from "mongoose";
import { ITeam, ITeamInfos, Team } from "./Team";
import { IStage } from "./Stage";
import { IMap, MapSchema, Mappool } from "./Mappool"
......
import * as mongoose from "mongoose";
import mongoose from "mongoose";
import { IUser } from "./User";
import { IMatch, Match } from "./Match";
import { ITeam, Team } from "./Team";
......
import * as mongoose from "mongoose";
import mongoose from "mongoose";
import { ITeam, ITeamInfos, Team } from "./Team";
import { IUser, IUserInfos, User } from "./User";
import { IMatchPlay, MatchPlaySchema, IMatchPlayInfos } from "./Match";
......
import * as mongoose from "mongoose";
import mongoose from "mongoose";
import { IMatch, IMatchInfos } from "./Match";
export interface IStage extends mongoose.Document {
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment