diff --git a/apps/api/prisma/migrations/20220816133447_bot_deployments/migration.sql b/apps/api/prisma/migrations/20220816133447_bot_deployments/migration.sql new file mode 100644 index 000000000..3d6d92d0e --- /dev/null +++ b/apps/api/prisma/migrations/20220816133447_bot_deployments/migration.sql @@ -0,0 +1,20 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_ApplicationSettings" ( + "id" TEXT NOT NULL PRIMARY KEY, + "applicationId" TEXT NOT NULL, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "debug" BOOLEAN NOT NULL DEFAULT false, + "previews" BOOLEAN NOT NULL DEFAULT false, + "autodeploy" BOOLEAN NOT NULL DEFAULT true, + "isBot" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "previews", "updatedAt" FROM "ApplicationSettings"; +DROP TABLE "ApplicationSettings"; +ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings"; +CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/migrations/20220817082342_custom_dns_servers/migration.sql b/apps/api/prisma/migrations/20220817082342_custom_dns_servers/migration.sql new file mode 100644 index 000000000..03588b549 --- /dev/null +++ b/apps/api/prisma/migrations/20220817082342_custom_dns_servers/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Setting" ADD COLUMN "DNSServers" TEXT; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 4877c6ffb..695478cef 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -20,6 +20,7 @@ model Setting { proxyHash String? isAutoUpdateEnabled Boolean @default(false) isDNSCheckEnabled Boolean @default(true) + DNSServers String? isTraefikUsed Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -124,6 +125,7 @@ model ApplicationSettings { debug Boolean @default(false) previews Boolean @default(false) autodeploy Boolean @default(true) + isBot Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt application Application @relation(fields: [applicationId], references: [id]) diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index ed862a59e..18864dd6e 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -5,8 +5,10 @@ import env from '@fastify/env'; import cookie from '@fastify/cookie'; import path, { join } from 'path'; import autoLoad from '@fastify/autoload'; -import { asyncExecShell, isDev, listSettings, prisma } from './lib/common'; +import { asyncExecShell, isDev, listSettings, prisma, version } from './lib/common'; import { scheduler } from './lib/scheduler'; +import axios from 'axios'; +import compareVersions from 'compare-versions'; declare module 'fastify' { interface FastifyInstance { @@ -113,8 +115,22 @@ fastify.listen({ port, host }, async (err: any, address: any) => { setInterval(async () => { const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); if (isAutoUpdateEnabled) { - if (scheduler.workers.has('deployApplication')) { - scheduler.workers.get('deployApplication').postMessage("status:autoUpdater"); + const currentVersion = version; + const { data: versions } = await axios + .get( + `https://get.coollabs.io/versions.json` + , { + params: { + appId: process.env['COOLIFY_APP_ID'] || undefined, + version: currentVersion + } + }) + const latestVersion = versions['coolify'].main.version; + const isUpdateAvailable = compareVersions(latestVersion, currentVersion); + if (isUpdateAvailable === 1) { + if (scheduler.workers.has('deployApplication')) { + scheduler.workers.get('deployApplication').postMessage("status:autoUpdater"); + } } } }, isDev ? 5000 : 60000 * 15) diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index ff87ebe7c..a8c46d594 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -298,7 +298,6 @@ import * as buildpacks from '../lib/buildPacks'; } }; }); - console.log({port}) const composeFile = { version: '3.8', services: { diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index d67125db3..96921d82a 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -17,7 +17,7 @@ import { checkContainer, removeContainer } from './docker'; import { day } from './dayjs'; import * as serviceFields from './serviceFields' -export const version = '3.4.0'; +export const version = '3.5.0'; export const isDev = process.env.NODE_ENV === 'development'; const algorithm = 'aes-256-ctr'; @@ -307,6 +307,10 @@ export async function checkDoubleBranch(branch: string, projectId: number): Prom } export async function isDNSValid(hostname: any, domain: string): Promise { const { isIP } = await import('is-ip'); + const { DNSServers } = await listSettings(); + if (DNSServers) { + dns.setServers([DNSServers]); + } let resolves = []; try { if (isIP(hostname)) { @@ -320,7 +324,6 @@ export async function isDNSValid(hostname: any, domain: string): Promise { try { let ipDomainFound = false; - dns.setServers(['1.1.1.1', '8.8.8.8']); const dnsResolve = await dns.resolve4(domain); if (dnsResolve.length > 0) { for (const ip of dnsResolve) { @@ -412,7 +415,12 @@ export async function checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }): P const { isIP } = await import('is-ip'); const domain = getDomain(fqdn); const domainDualCert = domain.includes('www.') ? domain.replace('www.', '') : `www.${domain}`; - dns.setServers(['1.1.1.1', '8.8.8.8']); + + const { DNSServers } = await listSettings(); + if (DNSServers) { + dns.setServers([DNSServers]); + } + let resolves = []; try { if (isIP(hostname)) { @@ -1553,7 +1561,7 @@ export async function configureServiceType({ }); } else if (type === 'appwrite') { const opensslKeyV1 = encrypt(generatePassword()); - const executorSecret = encrypt(generatePassword()); + const executorSecret = encrypt(generatePassword()); const redisPassword = encrypt(generatePassword()); const mariadbHost = `${id}-mariadb` const mariadbUser = cuid(); diff --git a/apps/api/src/lib/scheduler.ts b/apps/api/src/lib/scheduler.ts index 148764351..b120dcea1 100644 --- a/apps/api/src/lib/scheduler.ts +++ b/apps/api/src/lib/scheduler.ts @@ -20,7 +20,6 @@ const options: any = { } if (message.caller === 'cleanupStorage') { if (!scheduler.workers.has('cleanupStorage')) { - await scheduler.stop('deployApplication'); await scheduler.run('cleanupStorage') } } diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index b3ef49133..ce03c6e7c 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -5,7 +5,7 @@ import axios from 'axios'; import { FastifyReply } from 'fastify'; import { day } from '../../../../lib/dayjs'; import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; -import { checkDomainsIsValidInDNS, checkDoubleBranch, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, prisma, stopBuild, uniqueName } from '../../../../lib/common'; +import { checkDomainsIsValidInDNS, checkDoubleBranch, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker'; import { scheduler } from '../../../../lib/scheduler'; @@ -18,7 +18,7 @@ export async function listApplications(request: FastifyRequest) { const { teamId } = request.user const applications = await prisma.application.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { teams: true, destinationDocker: true } + include: { teams: true, destinationDocker: true, settings: true } }); const settings = await prisma.setting.findFirst() return { @@ -90,10 +90,11 @@ export async function getApplication(request: FastifyRequest) { const { teamId } = request.user const appId = process.env['COOLIFY_APP_ID']; const application: any = await getApplicationFromDB(id, teamId); - + const settings = await listSettings(); return { application, - appId + appId, + settings }; } catch ({ status, message }) { @@ -275,7 +276,7 @@ export async function saveApplication(request: FastifyRequest, export async function saveApplicationSettings(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params - const { debug, previews, dualCerts, autodeploy, branch, projectId } = request.body + const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot } = request.body const isDouble = await checkDoubleBranch(branch, projectId); if (isDouble && autodeploy) { await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } }) @@ -283,7 +284,7 @@ export async function saveApplicationSettings(request: FastifyRequest, reply: minPort, maxPort, isAutoUpdateEnabled, - isDNSCheckEnabled + isDNSCheckEnabled, + DNSServers } = request.body const { id } = await listSettings(); await prisma.setting.update({ where: { id }, - data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled } + data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers } }); if (fqdn) { await prisma.setting.update({ where: { id }, data: { fqdn } }); @@ -54,6 +55,10 @@ export async function saveSettings(request: FastifyRequest, reply: export async function deleteDomain(request: FastifyRequest, reply: FastifyReply) { try { const { fqdn } = request.body + const { DNSServers } = await listSettings(); + if (DNSServers) { + dns.setServers([DNSServers]); + } let ip; try { ip = await dns.resolve(fqdn); diff --git a/apps/api/src/routes/api/v1/settings/types.ts b/apps/api/src/routes/api/v1/settings/types.ts index a33b614a4..d8fcf816d 100644 --- a/apps/api/src/routes/api/v1/settings/types.ts +++ b/apps/api/src/routes/api/v1/settings/types.ts @@ -8,7 +8,8 @@ export interface SaveSettings { minPort: number, maxPort: number, isAutoUpdateEnabled: boolean, - isDNSCheckEnabled: boolean + isDNSCheckEnabled: boolean, + DNSServers: string } } export interface DeleteDomain { diff --git a/apps/ui/src/lib/components/UpdateAvailable.svelte b/apps/ui/src/lib/components/UpdateAvailable.svelte index ba27d001b..4122aae3e 100644 --- a/apps/ui/src/lib/components/UpdateAvailable.svelte +++ b/apps/ui/src/lib/components/UpdateAvailable.svelte @@ -83,7 +83,7 @@ disabled={updateStatus.success === false} on:click={update} class="icons tooltip tooltip-right tooltip-primary bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105" - data-tip="Update available!" + data-tip="Update Available!" > {#if updateStatus.loading} Useful for creating staging environments.", - "values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating staging environments.", + "setup_secret_app_first": "You can add secrets to PR/MR deployments. Please add secrets to the application first.
Useful for creating staging environments.", + "values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating staging environments.", "redeploy": "Redeploy", "no_previews_available": "No previews available" }, @@ -163,9 +163,9 @@ }, "deployment_queued": "Deployment queued.", "confirm_to_delete": "Are you sure you would like to delete '{{name}}'?", - "stop_application": "Stop application", + "stop_application": "Stop Application", "permission_denied_stop_application": "You do not have permission to stop the application.", - "rebuild_application": "Rebuild application", + "rebuild_application": "Rebuild Application", "permission_denied_rebuild_application": "You do not have permission to rebuild application.", "build_and_start_application": "Deploy", "permission_denied_build_and_start_application": "You do not have permission to deploy application.", @@ -194,14 +194,14 @@ "application": "Application", "url_fqdn": "URL (FQDN)", "domain_fqdn": "Domain (FQDN)", - "https_explainer": "If you specify https, the application will be accessible only over https. SSL certificate will be generated for you.
If you specify www, the application will be redirected (302) from non-www and vice versa.

To modify the domain, you must first stop the application.

You must set your DNS to point to the server IP in advance.", + "https_explainer": "If you specify https, the application will be accessible only over https. SSL certificate will be generated for you.
If you specify www, the application will be redirected (302) from non-www and vice versa.

To modify the domain, you must first stop the application.

You must set your DNS to point to the server IP in advance.", "ssl_www_and_non_www": "Generate SSL for www and non-www?", - "ssl_explainer": "It will generate certificates for both www and non-www.
You need to have both DNS entries set in advance.

Useful if you expect to have visitors on both.", + "ssl_explainer": "It will generate certificates for both www and non-www.
You need to have both DNS entries set in advance.

Useful if you expect to have visitors on both.", "install_command": "Install Command", "build_command": "Build Command", "start_command": "Start Command", - "directory_to_use_explainer": "Directory to use as the base for all commands.
Could be useful with monorepos.", - "publish_directory_explainer": "Directory containing all the assets for deployment.
For example: dist,_site or public.", + "directory_to_use_explainer": "Directory to use as the base for all commands.
Could be useful with monorepos.", + "publish_directory_explainer": "Directory containing all the assets for deployment.
For example: dist,_site or public.", "features": "Features", "enable_automatic_deployment": "Enable Automatic Deployment", "enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.", diff --git a/apps/ui/src/lib/locales/fr.json b/apps/ui/src/lib/locales/fr.json index 418f72526..9dc5aa8c4 100644 --- a/apps/ui/src/lib/locales/fr.json +++ b/apps/ui/src/lib/locales/fr.json @@ -65,7 +65,7 @@ "features": "Caractéristiques", "git_repository": "Dépôt Git", "git_source": "Source Git", - "https_explainer": "Si vous spécifiez https, l'application sera accessible uniquement via https. \nUn certificat SSL sera généré pour vous.
Si vous spécifiez www, l'application sera redirigée (302) à partir de non-www et vice versa \n.

Pour modifier le domaine, vous devez d'abord arrêter l'application.

Vous devez configurer, en avance, votre DNS pour pointer vers l'IP du serveur.", + "https_explainer": "Si vous spécifiez https, l'application sera accessible uniquement via https. \nUn certificat SSL sera généré pour vous.
Si vous spécifiez www, l'application sera redirigée (302) à partir de non-www et vice versa \n.

Pour modifier le domaine, vous devez d'abord arrêter l'application.

Vous devez configurer, en avance, votre DNS pour pointer vers l'IP du serveur.", "install_command": "Commande d'installation", "logs": "Journaux des applications", "no_applications_found": "Aucune application trouvée", @@ -78,11 +78,11 @@ "need_during_buildtime": "Besoin pendant la build ?", "no_previews_available": "Aucun aperçu disponible", "redeploy": "Redéployer", - "setup_secret_app_first": "Vous pouvez ajouter des secrets aux déploiements PR/MR. \nVeuillez d'abord ajouter des secrets à l'application. \n
Utile pour créer des environnements de mise en scène.", - "values_overwriting_app_secrets": "Ces valeurs remplacent les secrets d'application dans les déploiements PR/MR. \nUtile pour créer des environnements de mise en scène." + "setup_secret_app_first": "Vous pouvez ajouter des secrets aux déploiements PR/MR. \nVeuillez d'abord ajouter des secrets à l'application. \n
Utile pour créer des environnements de mise en scène.", + "values_overwriting_app_secrets": "Ces valeurs remplacent les secrets d'application dans les déploiements PR/MR. \nUtile pour créer des environnements de mise en scène." }, "previews": "Aperçus", - "publish_directory_explainer": "Répertoire contenant tous les actifs à déployer. \n
Par exemple : dist,_site ou public.", + "publish_directory_explainer": "Répertoire contenant tous les actifs à déployer. \n
Par exemple : dist,_site ou public.", "rebuild_application": "Re-build l'application", "secret": "secrets", "secrets": { @@ -91,7 +91,7 @@ "use_isbuildsecret": "Utiliser isBuildSecret" }, "settings_saved": "Paramètres sauvegardés.", - "ssl_explainer": "Il générera des certificats pour www et non-www. \n
Vous devez avoir les deux entrées DNS définies à l'avance.

Utile si vous prévoyez d'avoir des visiteurs sur les deux.", + "ssl_explainer": "Il générera des certificats pour www et non-www. \n
Vous devez avoir les deux entrées DNS définies à l'avance.

Utile si vous prévoyez d'avoir des visiteurs sur les deux.", "ssl_www_and_non_www": "Générer SSL pour www et non-www ?", "start_command": "Démarrer la commande", "stop_application": "Arrêter l'application", diff --git a/apps/ui/src/lib/store.ts b/apps/ui/src/lib/store.ts index 8834782a4..7c851b96a 100644 --- a/apps/ui/src/lib/store.ts +++ b/apps/ui/src/lib/store.ts @@ -1,3 +1,4 @@ +import { dev } from '$app/env'; import cuid from 'cuid'; import { writable, readable, type Writable } from 'svelte/store'; @@ -70,7 +71,11 @@ export const features = readable({ }); export const location: Writable = writable(null) -export const setLocation = (resource: any) => { +export const setLocation = (resource: any, settings?: any) => { + if (resource.settings.isBot && resource.exposePort) { + disabledButton.set(false); + return location.set(`http://${dev ? 'localhost' : settings.ipv4}:${resource.exposePort}`) + } if (GITPOD_WORKSPACE_URL && resource.exposePort) { const { href } = new URL(GITPOD_WORKSPACE_URL); const newURL = href @@ -81,7 +86,12 @@ export const setLocation = (resource: any) => { const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, resource.exposePort)}` return location.set(newURL) } - return location.set(resource.fqdn) + if (resource.fqdn) { + return location.set(resource.fqdn) + } else { + location.set(null); + disabledButton.set(true); + } } export const toasts: any = writable([]) diff --git a/apps/ui/src/routes/applications/[id]/__layout.svelte b/apps/ui/src/routes/applications/[id]/__layout.svelte index 4cb126eda..e930df1ed 100644 --- a/apps/ui/src/routes/applications/[id]/__layout.svelte +++ b/apps/ui/src/routes/applications/[id]/__layout.svelte @@ -16,7 +16,7 @@ export const load: Load = async ({ fetch, url, params }) => { try { const response = await get(`/applications/${params.id}`); - let { application, appId, settings, isQueueActive } = response; + let { application, appId, settings } = response; if (!application || Object.entries(application).length === 0) { return { status: 302, @@ -36,7 +36,8 @@ return { props: { - application + application, + settings }, stuff: { application, @@ -52,7 +53,7 @@