diff --git a/.env.example b/.env.example index fca1193..2ccf56f 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,6 @@ DB_URL=mongodb://mongodb:27017 # Workspaces config filename CONFIG_FILE=config.yml + +# Cron schedule for ping services +PING_SCHEDULE="* * * * *" diff --git a/package.json b/package.json index 1b071dd..d328c7a 100644 --- a/package.json +++ b/package.json @@ -25,12 +25,16 @@ "@types/express": "^4.17.8", "@types/jest": "^26.0.15", "@types/node": "^14.14.2", + "@types/node-cron": "^2.0.3", + "@types/ping": "^0.2.0", "@types/ws": "^7.2.7", "ctproto": "^0.0.7", "dotenv": "^8.2.0", "express": "^4.17.1", "mongoose": "^5.10.11", "nanoid": "^3.1.18", + "node-cron": "^2.0.3", + "ping": "^0.4.0", "typescript": "^4.1.2", "ws": "^7.3.1", "yaml": "^1.10.0" diff --git a/src/config/index.ts b/src/config/index.ts index 9098dd4..f95155a 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -27,4 +27,8 @@ export default class Config { * Workspaces config-file path */ public static workspacesConfigPath: string = process.env.CONFIG_FILE || ''; + /** + * Cron schedule for setting pinging interval + */ + public static pingSchedule: string = process.env.PING_SCHEDULE || '*/30 * * * * *'; } diff --git a/src/controllers/statuses.ts b/src/controllers/statuses.ts new file mode 100644 index 0000000..e6a0822 --- /dev/null +++ b/src/controllers/statuses.ts @@ -0,0 +1,108 @@ +import ping from 'ping'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental,@typescript-eslint/no-unused-vars +import app from './../app'; +import WorkspacesService from './../services/workspace'; +import ServerProjectsStatuses, { ProjectStatus } from '../types/serverProjectsStatuses'; +import Workspace from './../types/workspace'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-unused-vars-experimental +import Client from 'ctproto/build/src/server/client'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-unused-vars-experimental +import { DevopsToolboxAuthData } from '../types/api/responses/authorize'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-unused-vars-experimental +import { ApiOutgoingMessage, ApiResponse } from '../types'; +import Server from '../services/server'; +import { NginxPayload } from '../types/servicePayload'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-unused-vars-experimental + +/** + * Statuses controller to work with updating/checking/sending statuses of servers' statuses for each workspace + */ +export default class StatusesController { + /** + * Update statuses and send to logged users + */ + public static async updateStatuses(): Promise { + const workspaces: Workspace[] | null = await WorkspacesService.find(); + + /** + * If workspaces exist,then updating statuses for them + */ + if (workspaces) { + for (const workspace of workspaces) { + /** + * For each server of workspace update statuses + */ + for (const s of workspace.servers) { + for (const service of s.services) { + /** + * If type of service is nginx, server's projects are pinged + */ + if (service.type === 'nginx') { + const projectsStatuses = await this.pingProjects(service.payload as NginxPayload[]); + const serverProjectsStatuses: ServerProjectsStatuses = { + projectsStatuses, + serverToken: s.token, + } as ServerProjectsStatuses; + + await Server.updateServicesStatuses(serverProjectsStatuses); + } + } + } + } + } + } + + /** + * Project availability check + * + * @param serverProject - array of workspace server's projects + */ + public static async checkProjectAvailability(serverProject: string): Promise { + if (serverProject === '') { + return { + host: 'Unnamed host', + isOnline: false, + }; + } else { + const pingServer = await ping.promise.probe(serverProject); + + return { + host: serverProject, + isOnline: pingServer.alive, + }; + } + } + + /** + * Ping projects of some server + * + * @param payload - list of project of payload of some service of some server + */ + public static async pingProjects(payload: NginxPayload[]): Promise { + const projectsStatuses:ProjectStatus[] = []; + + for (const payloadElement of payload) { + const projectStatus = await StatusesController.checkProjectAvailability(payloadElement.serverName); + + projectsStatuses.push(projectStatus); + } + + return projectsStatuses; + } + + /** + * Send statuses to clients + * + * @param statuses - array of statuses of all client services + * @param user - user + */ + // public static sendStatuses(statuses: ServerProjectsStatuses[], user: Client): void { + // if (typeof user.authData.userToken === 'string') { + // app.context.transport + // .clients + // .find((client) => client.authData.userToken === user.authData.userToken) + // .send('statuses-updated', { statuses }); + // } + // } +} diff --git a/src/database/models/serverServicesStatuses.ts b/src/database/models/serverServicesStatuses.ts new file mode 100644 index 0000000..fbf2ead --- /dev/null +++ b/src/database/models/serverServicesStatuses.ts @@ -0,0 +1,36 @@ +import mongoose from '..'; +import ServerProjectsStatuses from '../../types/serverProjectsStatuses'; + +/** + * Project status Schema + */ +const serverProjectsStatusesSchema: mongoose.Schema = new mongoose.Schema({ + /** + * Workspace server's token + */ + serverToken: { + type: String, + required: true, + }, + /** + * Workspace server's projects' names and statuses + */ + projectsStatuses: [ { + /** + * Host name + */ + host: { + type: String, + required: true, + }, + /** + * host status + */ + isOnline: { + type: Boolean, + required: true, + }, + } ], +}); + +export default mongoose.model('server_projects_statuses', serverProjectsStatusesSchema); diff --git a/src/database/models/workspace.ts b/src/database/models/workspace.ts index d66ad81..80c84c9 100644 --- a/src/database/models/workspace.ts +++ b/src/database/models/workspace.ts @@ -19,6 +19,7 @@ const workspaceSchema: mongoose.Schema = new mongoose.Schema({ type: String, required: true, }, + /** * Workspace servers */ diff --git a/src/index.ts b/src/index.ts index deb1672..887c0d0 100755 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,11 @@ /// import { CTProtoServer } from 'ctproto'; import app from './app'; +import cron from 'node-cron'; import Config from './config'; import { WorkspacesController, ApiRequest, ApiResponse, ApiOutgoingMessage } from './types'; import WorkspacesService from './services/workspace'; +import StatusesController from './controllers/statuses'; import { AuthorizeMessagePayload } from './types/api/requests/authorize'; import { DevopsToolboxAuthData } from './types/api/responses/authorize'; @@ -16,14 +18,12 @@ app.listen(Config.httpPort, Config.host, () => { * Initialize CTProto server for API */ const transport = new CTProtoServer({ - host: Config.host, port: Config.wsPort, async onAuth(authRequestPayload: AuthorizeMessagePayload): Promise { /** * Connected client's authorization token */ const authToken = authRequestPayload.token.toString(); - /** * Connected client's workspaces list */ @@ -63,3 +63,14 @@ const transport = new CTProtoServer { + StatusesController.updateStatuses() + .then() + .catch(e => { + console.log(e); + }); +}); diff --git a/src/services/server.ts b/src/services/server.ts new file mode 100644 index 0000000..5c17ea8 --- /dev/null +++ b/src/services/server.ts @@ -0,0 +1,43 @@ +import mongoose from '../database'; +import IServerProjectsStatuses from '../types/serverProjectsStatuses'; +import ServerProjectsStatuses from '../database/models/serverServicesStatuses'; + +/** + * Server of workspace with token,services and projects + */ +export default class Server { + /** + * Add new server + * + * @param server - new server + */ + public static async add(server: IServerProjectsStatuses): Promise { + const newServer = new ServerProjectsStatuses(server); + + return newServer.save(); + } + + /** + * Update of server projects' statuses in DB + * + * @param serverProjectsStatuses - server projects' statuses and server token + */ + public static async updateServicesStatuses(serverProjectsStatuses: IServerProjectsStatuses): Promise { + const server = await ServerProjectsStatuses.findOne({ serverToken: serverProjectsStatuses.serverToken }); + + if (!server) { + await this.add(serverProjectsStatuses); + } + + return ServerProjectsStatuses.updateOne({ + serverToken: serverProjectsStatuses.serverToken, + }, { + $set: { + projectsStatuses: serverProjectsStatuses.projectsStatuses, + }, + + }, { + new: true, + }); + } +} diff --git a/src/services/workspace.ts b/src/services/workspace.ts index a5fde14..2732ad7 100644 --- a/src/services/workspace.ts +++ b/src/services/workspace.ts @@ -1,6 +1,7 @@ import mongoose from '../database'; import Workspace from '../database/models/workspace'; import { Workspace as IWorkspace, Service } from '../types'; +import ServicePayload from '../types/servicePayload'; /** * Workspace service @@ -15,6 +16,15 @@ export default class WorkspacesService { return Workspace.find(workspaceOptions); } + /** + * Find one workspace with options + * + * @param workspaceOptions - Workspace options for looking for documents + */ + public static async findOne(workspaceOptions: mongoose.FilterQuery = {}): Promise { + return Workspace.findOne(workspaceOptions); + } + /** * Add new workspace * @@ -32,7 +42,7 @@ export default class WorkspacesService { * @param token - Server token * @param actualServices - Actual services */ - public static async updateServices(token: string | undefined, actualServices: Service[]): Promise { + public static async updateServices(token: string | undefined, actualServices: Service[]): Promise { return Workspace.findOneAndUpdate({ 'servers.token': token }, { $set: { 'servers.$.services': actualServices, diff --git a/src/types/api/index.ts b/src/types/api/index.ts index 191d7a0..a248b25 100644 --- a/src/types/api/index.ts +++ b/src/types/api/index.ts @@ -3,6 +3,7 @@ import Authorize from './requests/authorize'; import AuthorizeResponse from './responses/authorize'; import GetWorkspacesResponse from './responses/getWorkspaces'; import WorkspaceUpdatedMessage from './outgoing/workspaceUpdated'; +import StatusesUpdatedMessage from './outgoing/statusesUpdated'; /** * This file uses Discriminating Unions types for our API @@ -31,4 +32,5 @@ export type ApiResponse = */ export type ApiOutgoingMessage = | WorkspaceUpdatedMessage + | StatusesUpdatedMessage ; diff --git a/src/types/api/outgoing/statusesUpdated.ts b/src/types/api/outgoing/statusesUpdated.ts new file mode 100644 index 0000000..2f977db --- /dev/null +++ b/src/types/api/outgoing/statusesUpdated.ts @@ -0,0 +1,19 @@ +import { NewMessage } from 'ctproto/types'; +import ServerProjectsStatuses from '../../serverProjectsStatuses'; + +/** + * Data about the updated statuses + */ +interface StatusesUpdatedPayload { + /** + * The updated workspace + */ + projectsStatuses: ServerProjectsStatuses; +} + +/** + * Describes the outgoing message that will be sent when statuses of services will be updated + */ +export default interface StatusesUpdatedMessage extends NewMessage { + type: 'statuses-updated'; +} diff --git a/src/types/server.ts b/src/types/server.ts index 335a7b9..1658e85 100644 --- a/src/types/server.ts +++ b/src/types/server.ts @@ -1,5 +1,6 @@ import Service from './service'; import SSHConnectionInfo from './SSHConnectionInfo'; +import ServicePayload from './servicePayload'; /** * Interface for server @@ -24,5 +25,5 @@ export default interface Server { /** * List of services running on the server */ - services: Service[]; + services: Service[]; } diff --git a/src/types/serverProjectsStatuses.ts b/src/types/serverProjectsStatuses.ts new file mode 100644 index 0000000..05e0287 --- /dev/null +++ b/src/types/serverProjectsStatuses.ts @@ -0,0 +1,28 @@ +import mongoose from 'mongoose'; + +/** + * Project status of the server + */ +export interface ProjectStatus { + /** + * Name of host + */ + host: string; + /** + * State of host (online/offline) + */ + isOnline: boolean; +} +/** + * Status of service in workspace + */ +export default interface ServerProjectsStatuses extends mongoose.Document { + /** + * Server's (containing the services) id + */ + serverToken: string; + /** + * Projects with their statuses of the server + */ + projectsStatuses: ProjectStatus[]; +} diff --git a/src/types/service.ts b/src/types/service.ts index 49961ba..3df947f 100644 --- a/src/types/service.ts +++ b/src/types/service.ts @@ -1,8 +1,11 @@ +import { DockerPayload, NginxPayload } from './servicePayload'; + /** * Service is the working software we are watching for * for example, nginx or Docker */ -export default interface Service { + +export default interface Service { /** * What kind of service represented by a payload * Examples: 'nginx', 'docker' etc @@ -13,5 +16,18 @@ export default interface Service { * Useful data about the service * collected by Agent */ - payload: Record; + payload: ServicePayload[]; +} + +/** + * Service for case when we are working with nginx + */ +export interface NginxService extends Service { + type: 'nginx'; +} +/** + * Service for case when we are working with docker + */ +export interface DockerService extends Service { + type: 'docker'; } diff --git a/src/types/servicePayload.ts b/src/types/servicePayload.ts new file mode 100644 index 0000000..e556be4 --- /dev/null +++ b/src/types/servicePayload.ts @@ -0,0 +1,89 @@ +/** + * Port for inside/outside container + */ +export interface ContainerPort { + /** + * Host for inside container + */ + host: string, + /** + * Port for inside container + */ + port: string, + /** + * Type of inside container + */ + type: string, +} + +/** + * Information about connection that is used in docker + */ +export interface Port { + /** + * Port for inside container + */ + inner: ContainerPort, + /** + * Port for outside container + */ + outer: ContainerPort, +} + +/** + * Useful data about the nginx service + * collected by Agent + */ +export interface NginxPayload { + /** + * Port to listen + */ + listen: string, + /** + * Name of the server + */ + serverName: string, + /** + * Protocol and address of a proxied server + */ + proxyPass: string, +} +/** + * Useful data about the docker service + * collected by Agent + */ +export interface DockerPayload { + /** + * Name of container + */ + names: string, + /** + * Id of container + */ + containerId: string, + /** + * Image name + */ + image: string, + /** + * Date of creation + */ + created: Date, + /** + * Current status + */ + status: string, + /** + * Ports to connect + */ + ports: Port[] +} + +/** + * Type is used to unite all existing payloads + */ +type ServicePayload = + | NginxPayload + | DockerPayload; + +export default ServicePayload; diff --git a/tsconfig.json b/tsconfig.json index d65ce0d..a978fe0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,9 @@ "rootDir": "./src", "outDir": "./build", "esModuleInterop": true, - "strict": true + "strict": true, + "lib": [ + "es2019" + ] } } diff --git a/yarn.lock b/yarn.lock index 65ba12a..570de28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -659,6 +659,13 @@ "@types/bson" "*" "@types/node" "*" +"@types/node-cron@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-2.0.3.tgz#b5bb940523d265f6a36548856ec0c278ea5a35d6" + integrity sha512-gwBBGeY2XeYBLE0R01K9Sm2hvNcPGmoloL6aqthA3QmBB1GYXTHIJ42AGZL7bdXBRiwbRV8b6NB5iKpl20R3gw== + dependencies: + "@types/tz-offset" "*" + "@types/node@*", "@types/node@^14.14.2": version "14.14.14" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae" @@ -669,6 +676,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/ping@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@types/ping/-/ping-0.2.0.tgz#b97db062444e964ea6cb3a727e161cd37708e922" + integrity sha512-4DnmDRxd3DyCc5+b26z85l0oeTiJwm9JXr30Em97M+BV5LL0GkRoTRVd3dtzwCznHZaEcCAaJH0eHtGMYmLMUg== + "@types/prettier@^2.0.0": version "2.1.5" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" @@ -697,6 +709,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== +"@types/tz-offset@*": + version "0.0.0" + resolved "https://registry.yarnpkg.com/@types/tz-offset/-/tz-offset-0.0.0.tgz#d58f1cebd794148d245420f8f0660305d320e565" + integrity sha512-XLD/llTSB6EBe3thkN+/I0L+yCTB6sjrcVovQdx2Cnl6N6bTzHmwe/J8mWnsXFgxLrj/emzdv8IR4evKYG2qxQ== + "@types/ws@^7.2.7": version "7.4.0" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.0.tgz#499690ea08736e05a8186113dac37769ab251a0e" @@ -3875,6 +3892,14 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +node-cron@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-2.0.3.tgz#b9649784d0d6c00758410eef22fa54a10e3f602d" + integrity sha512-eJI+QitXlwcgiZwNNSRbqsjeZMp5shyajMR81RZCqeW0ZDEj4zU9tpd4nTh/1JsBiKbF8d08FCewiipDmVIYjg== + dependencies: + opencollective-postinstall "^2.0.0" + tz-offset "0.0.1" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -4045,6 +4070,11 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" +opencollective-postinstall@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" + integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -4233,6 +4263,14 @@ pify@^2.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= +ping@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/ping/-/ping-0.4.0.tgz#2f382d2143de59671a33c082abab28c853b29715" + integrity sha512-qymcd5Bwv74b9o7qzAduN8wffr3ba11ZLBjY6gPQR2LpQ++9He8keSRFSaBb3OcR4SvXCWhi9VbLD1ycUNhz9Q== + dependencies: + q "1.x" + underscore "^1.12.0" + pirates@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" @@ -4340,6 +4378,11 @@ pupa@^2.0.1: dependencies: escape-goat "^2.0.0" +q@1.x: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" @@ -5320,6 +5363,11 @@ typescript@^4.1.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== +tz-offset@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/tz-offset/-/tz-offset-0.0.1.tgz#fef920257024d3583ed9072a767721a18bdb8a76" + integrity sha512-kMBmblijHJXyOpKzgDhKx9INYU4u4E1RPMB0HqmKSgWG8vEcf3exEfLh4FFfzd3xdQOw9EuIy/cP0akY6rHopQ== + undefsafe@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae" @@ -5327,6 +5375,11 @@ undefsafe@^2.0.3: dependencies: debug "^2.2.0" +underscore@^1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.0.tgz#4814940551fc80587cef7840d1ebb0f16453be97" + integrity sha512-21rQzss/XPMjolTiIezSu3JAjgagXKROtNrYFEOWK109qY1Uv2tVjPTZ1ci2HgvQDA16gHYSthQIJfB+XId/rQ== + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"