diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index 44b5f7003..7b98d5cf9 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -104,7 +104,9 @@ jobs: - name: Create & publish manifest run: | docker manifest create coollabsio/coolify:${{steps.package-version.outputs.current-version}} --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64 + docker manifest create coollabsio/coolify:latest --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64 docker manifest push coollabsio/coolify:${{steps.package-version.outputs.current-version}} + docker manifest push coollabsio/coolify:latest - uses: sarisia/actions-status-discord@v1 if: always() with: diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index c8fea7548..6f79b94e8 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -141,9 +141,12 @@ import * as buildpacks from '../lib/buildPacks'; } catch (error) { // } - let envs = ['NODE_ENV=production', `PORT=${port}`]; + let envs = []; if (secrets.length > 0) { - envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)]; + envs = [ + ...envs, + ...generateSecrets(secrets, pullmergeRequestId, false, port) + ]; } await fs.writeFile(`${workdir}/Dockerfile`, simpleDockerfile); if (dockerRegistry) { @@ -676,9 +679,12 @@ import * as buildpacks from '../lib/buildPacks'; } catch (error) { // } - let envs = ['NODE_ENV=production', `PORT=${port}`]; + let envs = []; if (secrets.length > 0) { - envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)]; + envs = [ + ...envs, + ...generateSecrets(secrets, pullmergeRequestId, false, port) + ]; } if (dockerRegistry) { const { url, username, password } = dockerRegistry; diff --git a/apps/api/src/lib/buildPacks/compose.ts b/apps/api/src/lib/buildPacks/compose.ts index d4adb796a..3f2bd5878 100644 --- a/apps/api/src/lib/buildPacks/compose.ts +++ b/apps/api/src/lib/buildPacks/compose.ts @@ -25,9 +25,9 @@ export default async function (data) { if (!dockerComposeYaml.services) { throw 'No Services found in docker-compose file.'; } - let envs = ['NODE_ENV=production']; + let envs = []; if (secrets.length > 0) { - envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)]; + envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, null)]; } const composeVolumes = []; diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 69a77aa9a..5087d4dcc 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -19,7 +19,7 @@ import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common import { scheduler } from './scheduler'; import type { ExecaChildProcess } from 'execa'; -export const version = '3.12.3'; +export const version = '3.12.4'; export const isDev = process.env.NODE_ENV === 'development'; export const sentryDSN = 'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216'; @@ -1879,7 +1879,8 @@ export async function pushToRegistry( export function generateSecrets( secrets: Array, pullmergeRequestId: string, - isBuild = false + isBuild = false, + port = null ): Array { const envs = []; const isPRMRSecret = secrets.filter((s) => s.isPRMRSecret); @@ -1918,5 +1919,13 @@ export function generateSecrets( } }); } + const portFound = envs.filter((env) => env.startsWith('PORT')); + if (portFound.length === 0 && port && !isBuild) { + envs.push(`PORT=${port}`); + } + const nodeEnv = envs.filter((env) => env.startsWith('NODE_ENV')); + if (nodeEnv.length === 0 && !isBuild) { + envs.push(`NODE_ENV=production`); + } return envs; } diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index ebc70e713..ecbe67c6c 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -569,10 +569,12 @@ export async function restartApplication( } = application; let location = null; - let envs = ['NODE_ENV=production', `PORT=${port}`]; + let envs = []; if (secrets.length > 0) { - envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)]; + envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, port)]; } + console.log(envs); + const { workdir } = await createDirectories({ repository, buildId }); const labels = []; let image = null; @@ -659,6 +661,7 @@ export async function restartApplication( }, volumes: Object.assign({}, ...composeVolumes) }; + console.log(yaml.dump(composeFile)); await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); try { await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` }); @@ -1370,9 +1373,9 @@ export async function restartPreview( exposePort } = application; - let envs = ['NODE_ENV=production', `PORT=${port}`]; + let envs = []; if (secrets.length > 0) { - envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)]; + envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, port)]; } const { workdir } = await createDirectories({ repository, buildId }); const labels = []; diff --git a/apps/client/package.json b/apps/client/package.json index b9fda399a..220c9b248 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -44,7 +44,10 @@ "daisyui": "2.41.0", "flowbite-svelte": "0.28.0", "js-cookie": "3.0.1", + "js-yaml": "4.1.0", + "p-limit": "4.0.0", "server": "workspace:*", - "superjson": "1.11.0" + "superjson": "1.11.0", + "svelte-select": "4.4.7" } } diff --git a/apps/client/src/app.d.ts b/apps/client/src/app.d.ts index 8f4d63895..b527fe7bd 100644 --- a/apps/client/src/app.d.ts +++ b/apps/client/src/app.d.ts @@ -7,3 +7,6 @@ declare namespace App { // interface Error {} // interface Platform {} } + +declare const GITPOD_WORKSPACE_URL: string; +declare const CODESANDBOX_HOST: string; diff --git a/apps/client/src/lib/common.ts b/apps/client/src/lib/common.ts index ab3b55370..168c36ed1 100644 --- a/apps/client/src/lib/common.ts +++ b/apps/client/src/lib/common.ts @@ -1,14 +1,17 @@ +import { dev } from '$app/environment'; import { addToast } from './store'; - +import Cookies from 'js-cookie'; export const asyncSleep = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay)); export function errorNotification(error: any | { message: string }): void { if (error instanceof Error) { + console.error(error.message) addToast({ message: error.message, type: 'error' }); } else { + console.error(error) addToast({ message: error, type: 'error' @@ -18,3 +21,165 @@ export function errorNotification(error: any | { message: string }): void { export function getRndInteger(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } + +export function getDomain(domain: string) { + return domain?.replace('https://', '').replace('http://', ''); +} + +export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno', 'laravel', 'heroku']; +export const staticDeployments = [ + 'react', + 'vuejs', + 'static', + 'svelte', + 'gatsby', + 'php', + 'astro', + 'eleventy' +]; + +export function getAPIUrl() { + if (GITPOD_WORKSPACE_URL) { + const { href } = new URL(GITPOD_WORKSPACE_URL); + const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, ''); + return newURL; + } + if (CODESANDBOX_HOST) { + return `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`; + } + return dev ? `http://${window.location.hostname}:3001` : 'http://localhost:3000'; +} +export function getWebhookUrl(type: string) { + if (GITPOD_WORKSPACE_URL) { + const { href } = new URL(GITPOD_WORKSPACE_URL); + const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, ''); + if (type === 'github') { + return `${newURL}/webhooks/github/events`; + } + if (type === 'gitlab') { + return `${newURL}/webhooks/gitlab/events`; + } + } + if (CODESANDBOX_HOST) { + const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`; + if (type === 'github') { + return `${newURL}/webhooks/github/events`; + } + if (type === 'gitlab') { + return `${newURL}/webhooks/gitlab/events`; + } + } + return `https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21/events`; +} + +async function send({ + method, + path, + data = null, + headers, + timeout = 120000 +}: { + method: string; + path: string; + data?: any; + headers?: any; + timeout?: number; +}): Promise> { + const token = Cookies.get('token'); + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + const opts: any = { method, headers: {}, body: null, signal: controller.signal }; + if (data && Object.keys(data).length > 0) { + const parsedData = data; + for (const [key, value] of Object.entries(data)) { + if (value === '') { + parsedData[key] = null; + } + } + if (parsedData) { + opts.headers['Content-Type'] = 'application/json'; + opts.body = JSON.stringify(parsedData); + } + } + + if (headers) { + opts.headers = { + ...opts.headers, + ...headers + }; + } + if (token && !path.startsWith('https://')) { + opts.headers = { + ...opts.headers, + Authorization: `Bearer ${token}` + }; + } + if (!path.startsWith('https://')) { + path = `/api/v1${path}`; + } + + if (dev && !path.startsWith('https://')) { + path = `${getAPIUrl()}${path}`; + } + if (method === 'POST' && data && !opts.body) { + opts.body = data; + } + const response = await fetch(`${path}`, opts); + + clearTimeout(id); + + const contentType = response.headers.get('content-type'); + + let responseData = {}; + if (contentType) { + if (contentType?.indexOf('application/json') !== -1) { + responseData = await response.json(); + } else if (contentType?.indexOf('text/plain') !== -1) { + responseData = await response.text(); + } else { + return {}; + } + } else { + return {}; + } + if (!response.ok) { + if ( + response.status === 401 && + !path.startsWith('https://api.github') && + !path.includes('/v4/') + ) { + Cookies.remove('token'); + } + + throw responseData; + } + return responseData; +} + +export function get(path: string, headers?: Record): Promise> { + return send({ method: 'GET', path, headers }); +} + +export function del( + path: string, + data: Record, + headers?: Record +): Promise> { + return send({ method: 'DELETE', path, data, headers }); +} + +export function post( + path: string, + data: Record | FormData, + headers?: Record +): Promise> { + return send({ method: 'POST', path, data, headers }); +} + +export function put( + path: string, + data: Record, + headers?: Record +): Promise> { + return send({ method: 'PUT', path, data, headers }); +} diff --git a/apps/client/src/lib/components/Beta.svelte b/apps/client/src/lib/components/Beta.svelte new file mode 100644 index 000000000..279401fcf --- /dev/null +++ b/apps/client/src/lib/components/Beta.svelte @@ -0,0 +1 @@ + BETA \ No newline at end of file diff --git a/apps/client/src/lib/components/CopyPasswordField.svelte b/apps/client/src/lib/components/CopyPasswordField.svelte new file mode 100644 index 000000000..a0a474750 --- /dev/null +++ b/apps/client/src/lib/components/CopyPasswordField.svelte @@ -0,0 +1,156 @@ + + +
+ {#if !isPasswordField || showPassword} + {#if textarea} + + {:else} + + {/if} + {:else} + + {/if} + +
+
+ {#if isPasswordField} + +
(showPassword = !showPassword)}> + {#if showPassword} + + + + {:else} + + + + + {/if} +
+ {/if} + {#if value && isHttps} + +
+ + + + + +
+ {/if} +
+
+
diff --git a/apps/client/src/lib/components/Explainer.svelte b/apps/client/src/lib/components/Explainer.svelte new file mode 100644 index 000000000..924ce70d6 --- /dev/null +++ b/apps/client/src/lib/components/Explainer.svelte @@ -0,0 +1,38 @@ + + +
+ + + + + +
diff --git a/apps/client/src/lib/components/Setting.svelte b/apps/client/src/lib/components/Setting.svelte new file mode 100644 index 000000000..555323b37 --- /dev/null +++ b/apps/client/src/lib/components/Setting.svelte @@ -0,0 +1,87 @@ + + +
+
+ + +
+
+
+ +
+ Use setting + + + + +
+
+ +{#if dataTooltip} + {dataTooltip} +{/if} diff --git a/apps/client/src/lib/store.ts b/apps/client/src/lib/store.ts index 8e3687dca..77510ba95 100644 --- a/apps/client/src/lib/store.ts +++ b/apps/client/src/lib/store.ts @@ -21,7 +21,8 @@ export const trpc = createTRPCProxyClient({ }) ] }); - +export const disabledButton: Writable = writable(false); +export const location: Writable = writable(null) interface AppSession { isRegistrationEnabled: boolean; token?: string; @@ -139,3 +140,33 @@ export const status: Writable = writable({ isPublic: false } }); + +export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) { + return !!( + (isAdmin && application.buildPack === 'compose') || + ((application.fqdn || application.settings.isBot) && + ((application.gitSource && application.repository && application.buildPack) || + application.simpleDockerfile) && + application.destinationDocker) + ); +} +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.replace('https://', `https://${resource.exposePort}-`).replace(/\/$/, ''); + return location.set(newURL); + } else if (CODESANDBOX_HOST) { + const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, resource.exposePort)}`; + return location.set(newURL); + } + if (resource.fqdn) { + return location.set(resource.fqdn); + } else { + location.set(null); + disabledButton.set(false); + } +}; diff --git a/apps/client/src/routes/+page.ts b/apps/client/src/routes/+page.ts index 4652e3d53..3465b727e 100644 --- a/apps/client/src/routes/+page.ts +++ b/apps/client/src/routes/+page.ts @@ -1,11 +1,8 @@ import { error } from '@sveltejs/kit'; import { trpc } from '$lib/store'; -import type { LayoutLoad } from './$types'; -import { redirect } from '@sveltejs/kit'; -import Cookies from 'js-cookie'; export const ssr = false; -export const load: LayoutLoad = async ({ url }) => { +export const load = async () => { try { return await trpc.dashboard.resources.query(); } catch (err) { diff --git a/apps/client/src/routes/applications/[id]/+page.svelte b/apps/client/src/routes/applications/[id]/+page.svelte index e69de29bb..0a431b054 100644 --- a/apps/client/src/routes/applications/[id]/+page.svelte +++ b/apps/client/src/routes/applications/[id]/+page.svelte @@ -0,0 +1,1228 @@ + + +
+
handleSubmit()}> +
+
+
General
+ {#if $appSession.isAdmin} + + {/if} +
+
+
+ + +
+ {#if !isSimpleDockerfile} +
+ + {#if isDisabled || application.settings?.isPublicRepository} + + {:else} + + {/if} +
+
+ + +
+
+ + {#if isDisabled || application.settings?.isPublicRepository} + + {:else} + + {/if} +
+ {/if} +
+ + {#if isDisabled} + + {:else} + + + {/if} +
+ {#if application.dockerRegistry?.id && application.gitSourceId} +
+ + +
+ {/if} + {#if !isSimpleDockerfile} +
+ + {#if isDisabled} + + {:else} + + + {/if} +
+ {/if} +
+ +
+ +
+
+ {#if application.buildPack !== 'compose'} +
+ changeSettings('isBot')} + title="Is your application a bot?" + description="You can deploy applications without domains or make them to listen on the Exposed Port.

Useful to host Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection." + disabled={isDisabled} + /> +
+ {/if} + {#if !isBot && application.buildPack !== 'compose'} +
+ +
+ + {#if forceSave} +
+ {#if isNonWWWDomainOK} + + {:else} + + {/if} + {#if dualCerts} + {#if isWWWDomainOK} + + {:else} + + {/if} + {/if} +
+ {/if} +
+
+
+ !isDisabled && changeSettings('dualCerts')} + /> +
+ {#if isHttps && application.buildPack !== 'compose'} +
+ changeSettings('isCustomSSL')} + /> +
+ {/if} + {/if} +
+ {#if isSimpleDockerfile} +
+ Configuration +
+ +
+
+ +
+