fix: Minio urls + domain checks

This commit is contained in:
Andras Bacsai 2022-05-19 13:45:17 +02:00
parent b01f5f47b3
commit 6fb6a514ac
13 changed files with 270 additions and 96 deletions

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Minio" ADD COLUMN "apiFqdn" TEXT;

View File

@ -345,6 +345,7 @@ model Minio {
rootUser String rootUser String
rootUserPassword String rootUserPassword String
publicPort Int? publicPort Int?
apiFqdn String?
serviceId String @unique serviceId String @unique
service Service @relation(fields: [serviceId], references: [id]) service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())

View File

@ -51,10 +51,12 @@ export async function isSecretExists({
export async function isDomainConfigured({ export async function isDomainConfigured({
id, id,
fqdn fqdn,
checkOwn = false
}: { }: {
id: string; id: string;
fqdn: string; fqdn: string;
checkOwn?: boolean;
}): Promise<boolean> { }): Promise<boolean> {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace('www.', ''); const nakedDomain = domain.replace('www.', '');
@ -72,12 +74,15 @@ export async function isDomainConfigured({
where: { where: {
OR: [ OR: [
{ fqdn: { endsWith: `//${nakedDomain}` } }, { fqdn: { endsWith: `//${nakedDomain}` } },
{ fqdn: { endsWith: `//www.${nakedDomain}` } } { fqdn: { endsWith: `//www.${nakedDomain}` } },
{ minio: { apiFqdn: { endsWith: `//${nakedDomain}` } } },
{ minio: { apiFqdn: { endsWith: `//www.${nakedDomain}` } } }
], ],
id: { not: id } id: { not: checkOwn ? undefined : id }
}, },
select: { fqdn: true } select: { fqdn: true }
}); });
const coolifyFqdn = await prisma.setting.findFirst({ const coolifyFqdn = await prisma.setting.findFirst({
where: { where: {
OR: [ OR: [

View File

@ -305,12 +305,12 @@ export async function getFreePort() {
select: { mysqlPublicPort: true } select: { mysqlPublicPort: true }
}) })
).map((a) => a.mysqlPublicPort); ).map((a) => a.mysqlPublicPort);
const minioUSed = await ( const minioUsed = await (
await prisma.minio.findMany({ await prisma.minio.findMany({
where: { publicPort: { not: null } }, where: { publicPort: { not: null } },
select: { publicPort: true } select: { publicPort: true }
}) })
).map((a) => a.publicPort); ).map((a) => a.publicPort);
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUSed]; const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed];
return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts }); return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts });
} }

View File

@ -360,7 +360,24 @@ export async function updateService({
}): Promise<Service> { }): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name, exposePort } }); return await prisma.service.update({ where: { id }, data: { fqdn, name, exposePort } });
} }
export async function updateMinioService({
id,
fqdn,
apiFqdn,
exposePort,
name
}: {
id: string;
fqdn: string;
apiFqdn: string;
exposePort?: number;
name: string;
}): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { fqdn, name, exposePort, minio: { update: { apiFqdn } } }
});
}
export async function updateFiderService({ export async function updateFiderService({
id, id,
fqdn, fqdn,
@ -459,7 +476,7 @@ export async function updateWordpress({
}); });
} }
export async function updateMinioService({ export async function updateMinioServicePort({
id, id,
publicPort publicPort
}: { }: {

View File

@ -81,19 +81,16 @@ export default async function (): Promise<void | {
} }
// HTTP Proxies // HTTP Proxies
const minioInstances = await prisma.minio.findMany({ if (!settings.isTraefikUsed) {
where: { publicPort: { not: null } }, const minioInstances = await prisma.minio.findMany({
include: { service: { include: { destinationDocker: true } } } where: { publicPort: { not: null } },
}); include: { service: { include: { destinationDocker: true } } }
for (const minio of minioInstances) { });
const { service, publicPort } = minio; for (const minio of minioInstances) {
const { destinationDockerId, destinationDocker, id } = service; const { service, publicPort } = minio;
if (destinationDockerId) { const { destinationDockerId, destinationDocker, id } = service;
if (destinationDocker.isCoolifyProxyUsed) { if (destinationDockerId) {
if (settings.isTraefikUsed) { if (destinationDocker.isCoolifyProxyUsed) {
await stopTcpHttpProxy(id, destinationDocker, publicPort, `haproxy-for-${publicPort}`);
await startTraefikHTTPProxy(destinationDocker, id, publicPort, 9000);
} else {
await stopTcpHttpProxy(id, destinationDocker, publicPort, `${id}-${publicPort}`); await stopTcpHttpProxy(id, destinationDocker, publicPort, `${id}-${publicPort}`);
await startHttpProxy(destinationDocker, id, publicPort, 9000); await startHttpProxy(destinationDocker, id, publicPort, 9000);
} }

View File

@ -30,14 +30,16 @@
value={service.minio.rootUserPassword} value={service.minio.rootUserPassword}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> {#if !service.minio.apiFqdn}
<label for="publicPort">{$t('forms.api_port')}</label> <div class="grid grid-cols-2 items-center px-10">
<input <label for="publicPort">{$t('forms.api_port')}</label>
name="publicPort" <input
id="publicPort" name="publicPort"
value={service.minio.publicPort} id="publicPort"
disabled value={service.minio.publicPort}
readonly disabled
placeholder={$t('forms.generated_automatically_after_start')} readonly
/> placeholder={$t('forms.generated_automatically_after_start')}
</div> />
</div>
{/if}

View File

@ -33,6 +33,7 @@
try { try {
await post(`/services/${id}/check.json`, { await post(`/services/${id}/check.json`, {
fqdn: service.fqdn, fqdn: service.fqdn,
otherFqdns: [service.minio.apiFqdn],
exposePort: service.exposePort exposePort: service.exposePort
}); });
await post(`/services/${id}/${service.type}.json`, { ...service }); await post(`/services/${id}/${service.type}.json`, { ...service });
@ -89,6 +90,14 @@
</div> </div>
<div class="grid grid-flow-row gap-2"> <div class="grid grid-flow-row gap-2">
{#if service.type === 'minio' && !service.minio.apiFqdn && isRunning}
<div class="text-center">
<span class="font-bold text-red-500">IMPORTANT!</span> There was a small modification with
Minio in the latest version of Coolify. Now you can separate the Console URL from the API URL,
so you could use both through SSL. But this proccess cannot be done automatically, so you have
to stop your Minio instance, configure the new domain and start it back. Sorry for any inconvenience.
</div>
{/if}
<div class="mt-2 grid grid-cols-2 items-center px-10"> <div class="mt-2 grid grid-cols-2 items-center px-10">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label> <label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<div> <div>
@ -134,25 +143,62 @@
{/if} {/if}
</div> </div>
</div> </div>
<div class="grid grid-cols-2 px-10"> {#if service.type === 'minio'}
<div class="flex-col "> <div class="grid grid-cols-2 px-10">
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100" <div class="flex-col ">
>{$t('application.url_fqdn')}</label <label for="fqdn" class="pt-2 text-base font-bold text-stone-100">Console URL</label>
> </div>
<Explainer text={$t('application.https_explainer')} />
</div> <CopyPasswordField
placeholder="eg: https://console.min.io"
readonly={!$session.isAdmin && !isRunning}
disabled={!$session.isAdmin || isRunning}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
bind:value={service.fqdn}
required
/>
</div>
<div class="grid grid-cols-2 px-10">
<div class="flex-col ">
<label for="apiFqdn" class="pt-2 text-base font-bold text-stone-100">API URL</label>
<Explainer text={$t('application.https_explainer')} />
</div>
<CopyPasswordField
placeholder="eg: https://min.io"
readonly={!$session.isAdmin && !isRunning}
disabled={!$session.isAdmin || isRunning}
name="apiFqdn"
id="apiFqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
bind:value={service.minio.apiFqdn}
required
/>
</div>
{:else}
<div class="grid grid-cols-2 px-10">
<div class="flex-col ">
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100"
>{$t('application.url_fqdn')}</label
>
<Explainer text={$t('application.https_explainer')} />
</div>
<CopyPasswordField
placeholder="eg: https://analytics.coollabs.io"
readonly={!$session.isAdmin && !isRunning}
disabled={!$session.isAdmin || isRunning}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
bind:value={service.fqdn}
required
/>
</div>
{/if}
<CopyPasswordField
placeholder="eg: https://analytics.coollabs.io"
readonly={!$session.isAdmin && !isRunning}
disabled={!$session.isAdmin || isRunning}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
bind:value={service.fqdn}
required
/>
</div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
disabled={isRunning} disabled={isRunning}
@ -163,7 +209,7 @@
on:click={() => !isRunning && changeSettings('dualCerts')} on:click={() => !isRunning && changeSettings('dualCerts')}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label> <label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label>
<input <input
readonly={!$session.isAdmin && !isRunning} readonly={!$session.isAdmin && !isRunning}

View File

@ -10,12 +10,14 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
let { fqdn, exposePort } = await event.request.json(); let { fqdn, exposePort, otherFqdns } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
if (otherFqdns) otherFqdns = otherFqdns.map((fqdn) => fqdn.toLowerCase());
if (exposePort) exposePort = Number(exposePort);
try { try {
const found = await db.isDomainConfigured({ id, fqdn }); let found = await db.isDomainConfigured({ id, fqdn });
if (found) { if (found) {
throw { throw {
message: t.get('application.domain_already_in_use', { message: t.get('application.domain_already_in_use', {
@ -23,6 +25,20 @@ export const post: RequestHandler = async (event) => {
}) })
}; };
} }
if (otherFqdns) {
for (const ofqdn of otherFqdns) {
const domain = getDomain(ofqdn);
const nakedDomain = domain.replace('www.', '');
found = await db.isDomainConfigured({ id, fqdn: ofqdn, checkOwn: true });
if (found) {
throw {
message: t.get('application.domain_already_in_use', {
domain: nakedDomain
})
};
}
}
}
if (exposePort) { if (exposePort) {
exposePort = Number(exposePort); exposePort = Number(exposePort);

View File

@ -9,12 +9,17 @@ export const post: RequestHandler = async (event) => {
const { id } = event.params; const { id } = event.params;
let { name, fqdn, exposePort } = await event.request.json(); let {
name,
fqdn,
exposePort,
minio: { apiFqdn }
} = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
if (exposePort) exposePort = Number(exposePort); if (exposePort) exposePort = Number(exposePort);
if (apiFqdn) apiFqdn = apiFqdn.toLowerCase();
try { try {
await db.updateService({ id, fqdn, name, exposePort }); await db.updateMinioService({ id, fqdn, apiFqdn, name, exposePort });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -34,7 +34,6 @@ export const post: RequestHandler = async (event) => {
const publicPort = await getFreePort(); const publicPort = await getFreePort();
const consolePort = 9001; const consolePort = 9001;
const apiPort = 9000;
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type); const image = getServiceImage(type);
@ -93,7 +92,7 @@ export const post: RequestHandler = async (event) => {
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
await db.updateMinioService({ id, publicPort }); await db.updateMinioServicePort({ id, publicPort });
return { return {
status: 200 status: 200
}; };

View File

@ -7,7 +7,7 @@ import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
function configureMiddleware( function configureMiddleware(
{ id, port, domain, nakedDomain, isHttps, isWWW, isDualCerts }, { id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts },
traefik traefik
) { ) {
if (isHttps) { if (isHttps) {
@ -22,7 +22,7 @@ function configureMiddleware(
loadbalancer: { loadbalancer: {
servers: [ servers: [
{ {
url: `http://${id}:${port}` url: `http://${container}:${port}`
} }
] ]
} }
@ -109,7 +109,7 @@ function configureMiddleware(
loadbalancer: { loadbalancer: {
servers: [ servers: [
{ {
url: `http://${id}:${port}` url: `http://${container}:${port}`
} }
] ]
} }
@ -172,8 +172,7 @@ export const get: RequestHandler = async (event) => {
port, port,
destinationDocker, destinationDocker,
destinationDockerId, destinationDockerId,
settings: { previews, dualCerts }, settings: { previews, dualCerts }
updatedAt
} = application; } = application;
if (destinationDockerId) { if (destinationDockerId) {
const { engine, network } = destinationDocker; const { engine, network } = destinationDocker;
@ -186,6 +185,7 @@ export const get: RequestHandler = async (event) => {
if (isRunning) { if (isRunning) {
data.applications.push({ data.applications.push({
id, id,
container: id,
port: port || 3000, port: port || 3000,
domain, domain,
nakedDomain, nakedDomain,
@ -208,14 +208,17 @@ export const get: RequestHandler = async (event) => {
if (containers.length > 0) { if (containers.length > 0) {
for (const container of containers) { for (const container of containers) {
const previewDomain = `${container.split('-')[1]}.${domain}`; const previewDomain = `${container.split('-')[1]}.${domain}`;
const nakedDomain = previewDomain.replace(/^www\./, '');
data.applications.push({ data.applications.push({
id: container, id: container,
container,
port: port || 3000, port: port || 3000,
domain: previewDomain, domain: previewDomain,
isRunning, isRunning,
nakedDomain,
isHttps, isHttps,
redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain, isWWW,
updatedAt: updatedAt.getTime() isDualCerts: dualCerts
}); });
} }
} }
@ -254,8 +257,26 @@ export const get: RequestHandler = async (event) => {
scriptName = plausibleAnalytics.scriptName; scriptName = plausibleAnalytics.scriptName;
} }
let container = id;
let otherDomain = null;
let otherNakedDomain = null;
let otherIsHttps = null;
let otherIsWWW = null;
if (type === 'minio') {
otherDomain = getDomain(service.minio.apiFqdn);
otherNakedDomain = otherDomain.replace(/^www\./, '');
otherIsHttps = service.minio.apiFqdn.startsWith('https://');
otherIsWWW = service.minio.apiFqdn.includes('www.');
}
data.services.push({ data.services.push({
id, id,
container,
type,
otherDomain,
otherNakedDomain,
otherIsHttps,
otherIsWWW,
port, port,
publicPort, publicPort,
domain, domain,
@ -280,6 +301,7 @@ export const get: RequestHandler = async (event) => {
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
data.coolify.push({ data.coolify.push({
id: dev ? 'host.docker.internal' : 'coolify', id: dev ? 'host.docker.internal' : 'coolify',
container: dev ? 'host.docker.internal' : 'coolify',
port: 3000, port: 3000,
domain, domain,
nakedDomain, nakedDomain,
@ -293,7 +315,18 @@ export const get: RequestHandler = async (event) => {
} }
for (const service of data.services) { for (const service of data.services) {
const { id, scriptName } = service; const { id, scriptName } = service;
configureMiddleware(service, traefik); configureMiddleware(service, traefik);
if (service.type === 'minio') {
service.id = id + '-minio';
service.container = id;
service.domain = service.otherDomain;
service.nakedDomain = service.otherNakedDomain;
service.isHttps = service.otherIsHttps;
service.isWWW = service.otherIsWWW;
service.port = 9000;
configureMiddleware(service, traefik);
}
if (scriptName) { if (scriptName) {
traefik.http.middlewares[`${id}-redir`] = { traefik.http.middlewares[`${id}-redir`] = {

View File

@ -1,9 +1,6 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import { asyncExecShell, getDomain, getEngine } from '$lib/common'; import { getDomain } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { listServicesWithIncludes } from '$lib/database';
import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
@ -13,7 +10,7 @@ export const get: RequestHandler = async (event) => {
const publicPort = event.url.searchParams.get('publicPort'); const publicPort = event.url.searchParams.get('publicPort');
const type = event.url.searchParams.get('type'); const type = event.url.searchParams.get('type');
let traefik = {}; let traefik = {};
if (publicPort) { if (publicPort && type && privatePort) {
if (type === 'tcp') { if (type === 'tcp') {
traefik = { traefik = {
[type]: { [type]: {
@ -34,43 +31,97 @@ export const get: RequestHandler = async (event) => {
} }
}; };
} else if (type === 'http') { } else if (type === 'http') {
const service = await db.prisma.service.findFirst({ where: { id } }); const service = await db.prisma.service.findFirst({
if (service?.fqdn) { where: { id },
const domain = getDomain(service.fqdn); include: { minio: true }
const isHttps = service.fqdn.startsWith('https://'); });
traefik = { if (service) {
[type]: { if (service.type === 'minio') {
routers: { if (service?.minio?.apiFqdn) {
[id]: { const {
entrypoints: [type], minio: { apiFqdn }
rule: `Host(\`${domain}\`)`, } = service;
service: id const domain = getDomain(apiFqdn);
} const isHttps = apiFqdn.startsWith('https://');
}, traefik = {
services: { [type]: {
[id]: { routers: {
loadbalancer: { [id]: {
servers: [{ url: `http://${id}:${privatePort}` }] entrypoints: [type],
rule: `Host(\`${domain}\`)`,
service: id
}
},
services: {
[id]: {
loadbalancer: {
servers: [{ url: `http://${id}:${privatePort}` }]
}
}
} }
} }
};
if (isHttps) {
if (dev) {
traefik[type].routers[id].tls = {
domains: {
main: `${domain}`
}
};
} else {
traefik[type].routers[id].tls = {
certresolver: 'letsencrypt'
};
}
} }
} }
}; } else {
if (isHttps) { if (service?.fqdn) {
if (dev) { const domain = getDomain(service.fqdn);
traefik[type].routers[id].tls = { const isHttps = service.fqdn.startsWith('https://');
domains: { traefik = {
main: `${domain}` [type]: {
routers: {
[id]: {
entrypoints: [type],
rule: `Host(\`${domain}:${privatePort}\`)`,
service: id
}
},
services: {
[id]: {
loadbalancer: {
servers: [{ url: `http://${id}:${privatePort}` }]
}
}
}
} }
}; };
} else { if (isHttps) {
traefik[type].routers[id].tls = { if (dev) {
certresolver: 'letsencrypt' traefik[type].routers[id].tls = {
}; domains: {
main: `${domain}`
}
};
} else {
traefik[type].routers[id].tls = {
certresolver: 'letsencrypt'
};
}
}
} }
} }
} else {
return {
status: 500
};
} }
} }
} else {
return {
status: 500
};
} }
return { return {
status: 200, status: 200,