feat: remote docker engine init

This commit is contained in:
Andras Bacsai
2022-07-18 14:02:53 +00:00
parent 0a8fd0516d
commit 537209d3fb
20 changed files with 809 additions and 429 deletions

View File

@@ -16,7 +16,7 @@
"dependencies": {
"@breejs/ts-worker": "2.0.0",
"@fastify/autoload": "5.1.0",
"@fastify/cookie": "7.1.0",
"@fastify/cookie": "7.2.0",
"@fastify/cors": "8.0.0",
"@fastify/env": "4.0.0",
"@fastify/jwt": "6.3.1",
@@ -43,16 +43,17 @@
"node-forge": "1.3.1",
"node-os-utils": "1.3.7",
"p-queue": "7.2.0",
"ssh-config": "4.1.6",
"strip-ansi": "7.0.1",
"unique-names-generator": "4.7.1"
},
"devDependencies": {
"@types/node": "18.0.4",
"@types/node": "18.0.6",
"@types/node-os-utils": "1.3.0",
"@typescript-eslint/eslint-plugin": "5.30.6",
"@typescript-eslint/parser": "5.30.6",
"esbuild": "0.14.49",
"eslint": "8.19.0",
"eslint": "8.20.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1",
"nodemon": "2.0.19",

View File

@@ -0,0 +1,21 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_DestinationDocker" (
"id" TEXT NOT NULL PRIMARY KEY,
"network" TEXT NOT NULL,
"name" TEXT NOT NULL,
"engine" TEXT,
"remoteEngine" BOOLEAN NOT NULL DEFAULT false,
"remoteIpAddress" TEXT,
"remoteUser" TEXT,
"remotePort" INTEGER,
"isCoolifyProxyUsed" BOOLEAN DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_DestinationDocker" ("createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "updatedAt") SELECT "createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "updatedAt" FROM "DestinationDocker";
DROP TABLE "DestinationDocker";
ALTER TABLE "new_DestinationDocker" RENAME TO "DestinationDocker";
CREATE UNIQUE INDEX "DestinationDocker_network_key" ON "DestinationDocker"("network");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,36 @@
-- CreateTable
CREATE TABLE "SshKey" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"privateKey" TEXT NOT NULL,
"destinationDockerId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "SshKey_destinationDockerId_fkey" FOREIGN KEY ("destinationDockerId") REFERENCES "DestinationDocker" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_DestinationDocker" (
"id" TEXT NOT NULL PRIMARY KEY,
"network" TEXT NOT NULL,
"name" TEXT NOT NULL,
"engine" TEXT,
"remoteEngine" BOOLEAN NOT NULL DEFAULT false,
"remoteIpAddress" TEXT,
"remoteUser" TEXT,
"remotePort" INTEGER,
"remoteVerified" BOOLEAN NOT NULL DEFAULT false,
"isCoolifyProxyUsed" BOOLEAN DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_DestinationDocker" ("createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "remoteIpAddress", "remotePort", "remoteUser", "updatedAt") SELECT "createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "remoteIpAddress", "remotePort", "remoteUser", "updatedAt" FROM "DestinationDocker";
DROP TABLE "DestinationDocker";
ALTER TABLE "new_DestinationDocker" RENAME TO "DestinationDocker";
CREATE UNIQUE INDEX "DestinationDocker_network_key" ON "DestinationDocker"("network");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;
-- CreateIndex
CREATE UNIQUE INDEX "SshKey_destinationDockerId_key" ON "SshKey"("destinationDockerId");

View File

@@ -200,8 +200,12 @@ model DestinationDocker {
id String @id @default(cuid())
network String @unique
name String
engine String
engine String?
remoteEngine Boolean @default(false)
remoteIpAddress String?
remoteUser String?
remotePort Int?
remoteVerified Boolean @default(false)
isCoolifyProxyUsed Boolean? @default(false)
teams Team[]
application Application[]
@@ -209,6 +213,17 @@ model DestinationDocker {
updatedAt DateTime @updatedAt
database Database[]
service Service[]
sshKey SshKey?
}
model SshKey {
id String @id @default(cuid())
name String
privateKey String
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
destinationDockerId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model GitSource {

View File

@@ -1,6 +1,8 @@
import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import { asyncExecShell, errorHandler, listSettings, prisma, startCoolifyProxy, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
import sshConfig from 'ssh-config'
import fs from 'fs/promises'
import { asyncExecShell, decrypt, errorHandler, listSettings, prisma, startCoolifyProxy, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
import { checkContainer, dockerInstance, getEngine } from '../../../../lib/docker';
import type { OnlyId } from '../../../../types';
@@ -44,7 +46,8 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
const { id } = request.params
const teamId = request.user?.teamId;
const destination = await prisma.destinationDocker.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { sshKey: true }
});
if (!destination && id !== 'new') {
throw { status: 404, message: `Destination not found.` };
@@ -80,39 +83,51 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
export async function newDestination(request: FastifyRequest<NewDestination>, reply: FastifyReply) {
try {
const { id } = request.params
let { name, network, engine, isCoolifyProxyUsed } = request.body
let { name, network, engine, isCoolifyProxyUsed, ipAddress, user, port, sshPrivateKey } = request.body
const teamId = request.user.teamId;
if (id === 'new') {
const host = getEngine(engine);
const docker = dockerInstance({ destinationDocker: { engine, network } });
const found = await docker.engine.listNetworks({ filters: { name: [`^${network}$`] } });
if (found.length === 0) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
}
await prisma.destinationDocker.create({
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed }
});
const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
const destination = destinations.find((destination) => destination.network === network);
if (engine) {
const host = getEngine(engine);
const docker = dockerInstance({ destinationDocker: { engine, network } });
const found = await docker.engine.listNetworks({ filters: { name: [`^${network}$`] } });
if (found.length === 0) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
}
await prisma.destinationDocker.create({
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed }
});
const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
const destination = destinations.find((destination) => destination.network === network);
if (destinations.length > 0) {
const proxyConfigured = destinations.find(
(destination) => destination.network !== network && destination.isCoolifyProxyUsed === true
);
if (proxyConfigured) {
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
if (destinations.length > 0) {
const proxyConfigured = destinations.find(
(destination) => destination.network !== network && destination.isCoolifyProxyUsed === true
);
if (proxyConfigured) {
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
}
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } });
}
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } });
}
if (isCoolifyProxyUsed) {
const settings = await prisma.setting.findFirst();
if (settings?.isTraefikUsed) {
await startTraefikProxy(engine);
} else {
await startCoolifyProxy(engine);
if (isCoolifyProxyUsed) {
const settings = await prisma.setting.findFirst();
if (settings?.isTraefikUsed) {
await startTraefikProxy(engine);
} else {
await startCoolifyProxy(engine);
}
}
return reply.code(201).send({ id: destination.id });
}
return reply.code(201).send({ id: destination.id });
if (ipAddress) {
await prisma.destinationDocker.create({
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed, remoteEngine: true, remoteIpAddress: ipAddress, remoteUser: user, remotePort: port }
});
return reply.code(201).send()
}
throw {
message: `Cannot save Docker Engine.`
};
} else {
await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } });
return reply.code(201).send();
@@ -120,6 +135,8 @@ export async function newDestination(request: FastifyRequest<NewDestination>, re
} catch ({ status, message }) {
return errorHandler({ status, message })
} finally {
await fs.rm('./id_rsa')
}
}
export async function deleteDestination(request: FastifyRequest<OnlyId>) {
@@ -195,3 +212,45 @@ export async function restartProxy(request: FastifyRequest<Proxy>) {
return errorHandler({ status, message })
}
}
export async function assignSSHKey(request: FastifyRequest) {
try {
const { id: sshKeyId } = request.body;
const { id } = request.params;
console.log({ id, sshKeyId })
await prisma.destinationDocker.update({ where: { id }, data: { sshKey: { connect: { id: sshKeyId } } } })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function verifyRemoteDockerEngine(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params;
const { sshKey: { privateKey }, remoteIpAddress, remotePort, remoteUser, network } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } })
await fs.writeFile('./id_rsa', decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 })
const host = `ssh://${remoteUser}@${remoteIpAddress}`
const config = sshConfig.parse('')
const found = config.find({ Host: remoteIpAddress })
if (!found) {
config.append({
Host: remoteIpAddress,
Port: remotePort.toString(),
User: remoteUser,
IdentityFile: '/workspace/coolify/apps/api/id_rsa',
StrictHostKeyChecking: 'no'
})
}
await fs.writeFile('/home/gitpod/.ssh/config', sshConfig.stringify(config))
const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`);
console.log({ stdout })
if (!stdout) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
}
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@@ -1,5 +1,5 @@
import { FastifyPluginAsync } from 'fastify';
import { checkDestination, deleteDestination, getDestination, listDestinations, newDestination, restartProxy, saveDestinationSettings, startProxy, stopProxy } from './handlers';
import { assignSSHKey, checkDestination, deleteDestination, getDestination, listDestinations, newDestination, restartProxy, saveDestinationSettings, startProxy, stopProxy, verifyRemoteDockerEngine } from './handlers';
import type { OnlyId } from '../../../../types';
import type { CheckDestination, NewDestination, Proxy, SaveDestinationSettings } from './types';
@@ -15,10 +15,14 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<NewDestination>('/:id', async (request, reply) => await newDestination(request, reply));
fastify.delete<OnlyId>('/:id', async (request) => await deleteDestination(request));
fastify.post<SaveDestinationSettings>('/:id/settings', async (request, reply) => await saveDestinationSettings(request));
fastify.post<Proxy>('/:id/start', async (request, reply) => await startProxy(request));
fastify.post<Proxy>('/:id/stop', async (request, reply) => await stopProxy(request));
fastify.post<Proxy>('/:id/restart', async (request, reply) => await restartProxy(request));
fastify.post<SaveDestinationSettings>('/:id/settings', async (request) => await saveDestinationSettings(request));
fastify.post<Proxy>('/:id/start', async (request,) => await startProxy(request));
fastify.post<Proxy>('/:id/stop', async (request) => await stopProxy(request));
fastify.post<Proxy>('/:id/restart', async (request) => await restartProxy(request));
fastify.post('/:id/configuration/sshKey', async (request) => await assignSSHKey(request));
fastify.post('/:id/verify', async (request, reply) => await verifyRemoteDockerEngine(request, reply));
};
export default root;

View File

@@ -1,15 +1,23 @@
import { promises as dns } from 'dns';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkDomainsIsValidInDNS, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
import { CheckDNS, CheckDomain, DeleteDomain, SaveSettings } from './types';
import { checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
export async function listAllSettings(request: FastifyRequest) {
try {
const settings = await listSettings();
const sshKeys = await prisma.sshKey.findMany()
const unencryptedKeys = []
if (sshKeys.length > 0) {
for (const key of sshKeys) {
unencryptedKeys.push({ id: key.id, name: key.name, privateKey: decrypt(key.privateKey), createdAt: key.createdAt })
}
}
return {
settings
settings,
sshKeys: unencryptedKeys
}
} catch ({ status, message }) {
return errorHandler({ status, message })
@@ -83,4 +91,30 @@ export async function checkDNS(request: FastifyRequest<CheckDNS>) {
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveSSHKey(request: FastifyRequest<SaveSSHKey>, reply: FastifyReply) {
try {
const { privateKey, name } = request.body;
const found = await prisma.sshKey.findMany({ where: { name } })
if (found.length > 0) {
throw {
message: "Name already used. Choose another one please."
}
}
const encryptedSSHKey = encrypt(privateKey)
await prisma.sshKey.create({ data: { name, privateKey: encryptedSSHKey } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteSSHKey(request: FastifyRequest<DeleteSSHKey>, reply: FastifyReply) {
try {
const { id } = request.body;
await prisma.sshKey.delete({ where: { id } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@@ -1,6 +1,6 @@
import { FastifyPluginAsync } from 'fastify';
import { checkDNS, checkDomain, deleteDomain, listAllSettings, saveSettings } from './handlers';
import { CheckDNS, CheckDomain, DeleteDomain, SaveSettings } from './types';
import { checkDNS, checkDomain, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
@@ -13,6 +13,9 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<CheckDNS>('/check', async (request) => await checkDNS(request));
fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request));
fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply));
fastify.delete<DeleteSSHKey>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply));
};
export default root;

View File

@@ -28,4 +28,15 @@ export interface CheckDNS {
Params: {
domain: string,
}
}
export interface SaveSSHKey {
Body: {
privateKey: string,
name: string
}
}
export interface DeleteSSHKey {
Body: {
id: string
}
}