ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

coolify haproxy 集成简单说明

2022-04-10 14:02:39  阅读:192  来源: 互联网

标签:haproxy 集成 const await host coolify http id


coolify 以前介绍过,是一个开源heroku 以及netlify的替换方案,对于服务的访问层集成了haproxy 进行处理
细节上使用了haproxy 的dataplaneapi 进行处理

api 调用部分

核心是利用了模版引擎mustache 以及直接调用的dataplaneapi

  • 初始化部分
    主要是获取数据库数据,进行初始化
 
import { dev } from '$app/env';
import got from 'got';
import mustache from 'mustache';
import crypto from 'crypto';
 
import * as db from '$lib/database';
import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
 
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
 
let template = `program api 
  command /usr/bin/dataplaneapi -f /usr/local/etc/haproxy/dataplaneapi.hcl --userlist haproxy-dataplaneapi
  no option start-on-reload
 
global
  stats socket /var/run/api.sock user haproxy group haproxy mode 660 level admin expose-fd listeners
  log stdout format raw local0 debug
 
defaults 
  mode http
  log global
  timeout http-request 60s
  timeout connect 10s
  timeout client 60s
  timeout server 60s
 
userlist haproxy-dataplaneapi 
  user admin insecure-password "\${HAPROXY_PASSWORD}"
 
frontend http
  mode http
  bind :80
  bind :443 ssl crt /usr/local/etc/haproxy/ssl/ alpn h2,http/1.1
  acl is_certbot path_beg /.well-known/acme-challenge/
 
  {{#applications}}
  {{#isHttps}}
  http-request redirect scheme https code ${
    dev ? 302 : 301
  } if { hdr(host) -i {{domain}} } !{ ssl_fc }
  {{/isHttps}}
  http-request redirect location {{{redirectValue}}} code ${
    dev ? 302 : 301
  } if { req.hdr(host) -i {{redirectTo}} }
  {{/applications}}
 
  {{#services}}
  {{#isHttps}}
  http-request redirect scheme https code ${
    dev ? 302 : 301
  } if { hdr(host) -i {{domain}} } !{ ssl_fc }
  {{/isHttps}}
  http-request redirect location {{{redirectValue}}} code ${
    dev ? 302 : 301
  } if { req.hdr(host) -i {{redirectTo}} }
  {{/services}}
 
  {{#coolify}}
  {{#isHttps}}
  http-request redirect scheme https code ${
    dev ? 302 : 301
  } if { hdr(host) -i {{domain}} } !{ ssl_fc }
  {{/isHttps}}
  http-request redirect location {{{redirectValue}}} code ${
    dev ? 302 : 301
  } if { req.hdr(host) -i {{redirectTo}} }
  {{/coolify}}
 
  use_backend backend-certbot if is_certbot
  use_backend %[req.hdr(host),lower]
 
frontend stats 
  bind *:8404
  stats enable
  stats uri /
  stats admin if TRUE
  stats auth "\${HAPROXY_USERNAME}:\${HAPROXY_PASSWORD}"
 
backend backend-certbot 
  mode http
  server certbot host.docker.internal:9080
 
{{#applications}}
{{#isRunning}}
# updatedAt={{updatedAt}}
backend {{domain}}
  option forwardfor
  {{#isHttps}}
  http-request add-header X-Forwarded-Proto https
  {{/isHttps}}
  {{^isHttps}}
  http-request add-header X-Forwarded-Proto http
  {{/isHttps}}
  http-request add-header X-Forwarded-Host %[req.hdr(host),lower]
  server {{id}} {{id}}:{{port}}
{{/isRunning}}
{{/applications}}
 
{{#services}}
{{#isRunning}}
# updatedAt={{updatedAt}}
backend {{domain}}
  option forwardfor
  {{#isHttps}}
  http-request add-header X-Forwarded-Proto https
  {{/isHttps}}
  {{^isHttps}}
  http-request add-header X-Forwarded-Proto http
  {{/isHttps}}
  http-request add-header X-Forwarded-Host %[req.hdr(host),lower]
  server {{id}} {{id}}:{{port}}
{{/isRunning}}
{{/services}}
 
{{#coolify}}
backend {{domain}}
  option forwardfor
  option httpchk GET /undead.json
  {{#isHttps}}
  http-request add-header X-Forwarded-Proto https
  {{/isHttps}}
  {{^isHttps}}
  http-request add-header X-Forwarded-Proto http
  {{/isHttps}}
  http-request add-header X-Forwarded-Host %[req.hdr(host),lower]
  server {{id}} {{id}}:{{port}} check fall 10
{{/coolify}}
`;
export async function haproxyInstance() {
  const { proxyPassword } = await db.listSettings();
  return got.extend({
    prefixUrl: url,
    username: 'admin',
    password: proxyPassword
  });
}
 
export async function configureHAProxy() {
  const haproxy = await haproxyInstance();
  await checkHAProxy(haproxy);
 
  try {
    const data = {
      applications: [],
      services: [],
      coolify: []
    };
    const applications = await db.prisma.application.findMany({
      include: { destinationDocker: true, settings: true }
    });
    for (const application of applications) {
      const {
        fqdn,
        id,
        port,
        destinationDocker,
        destinationDockerId,
        settings: { previews },
        updatedAt
      } = application;
      if (destinationDockerId) {
        const { engine, network } = destinationDocker;
        const isRunning = await checkContainer(engine, id);
        if (fqdn) {
          const domain = getDomain(fqdn);
          const isHttps = fqdn.startsWith('https://');
          const isWWW = fqdn.includes('www.');
          const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
          if (isRunning) {
            data.applications.push({
              id,
              port: port || 3000,
              domain,
              isRunning,
              isHttps,
              redirectValue,
              redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
              updatedAt: updatedAt.getTime()
            });
          }
          if (previews) {
            const host = getEngine(engine);
            const { stdout } = await asyncExecShell(
              `DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
            );
            const containers = stdout
              .trim()
              .split('\n')
              .filter((a) => a)
              .map((c) => c.replace(/"/g, ''));
            if (containers.length > 0) {
              for (const container of containers) {
                let previewDomain = `${container.split('-')[1]}.${domain}`;
                data.applications.push({
                  id: container,
                  port: port || 3000,
                  domain: previewDomain,
                  isRunning,
                  isHttps,
                  redirectValue,
                  redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain,
                  updatedAt: updatedAt.getTime()
                });
              }
            }
          }
        }
      }
    }
    const services = await db.prisma.service.findMany({
      include: {
        destinationDocker: true,
        minio: true,
        plausibleAnalytics: true,
        vscodeserver: true,
        wordpress: true,
        ghost: true
      }
    });
 
    for (const service of services) {
      const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
      if (destinationDockerId) {
        const { engine } = destinationDocker;
        const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
        if (found) {
          const port = found.ports.main;
          const publicPort = service[type]?.publicPort;
          const isRunning = await checkContainer(engine, id);
          if (fqdn) {
            const domain = getDomain(fqdn);
            const isHttps = fqdn.startsWith('https://');
            const isWWW = fqdn.includes('www.');
            const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
            if (isRunning) {
              data.services.push({
                id,
                port,
                publicPort,
                domain,
                isRunning,
                isHttps,
                redirectValue,
                redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
                updatedAt: updatedAt.getTime()
              });
            }
          }
        }
      }
    }
    const { fqdn } = await db.prisma.setting.findFirst();
    if (fqdn) {
      const domain = getDomain(fqdn);
      const isHttps = fqdn.startsWith('https://');
      const isWWW = fqdn.includes('www.');
      const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
      data.coolify.push({
        id: dev ? 'host.docker.internal' : 'coolify',
        port: 3000,
        domain,
        isHttps,
        redirectValue,
        redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain
      });
    }
    const output = mustache.render(template, data);
    const newHash = crypto.createHash('md5').update(output).digest('hex');
    const { proxyHash, id } = await db.listSettings();
    if (proxyHash !== newHash) {
      await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } });
      await haproxy.post(`v2/services/haproxy/configuration/raw`, {
        searchParams: {
          skip_version: true
        },
        body: output,
        headers: {
          'Content-Type': 'text/plain'
        }
      });
    }
  } catch (error) {
    throw error;
  }
}
  • dataplaneapi 调用
    难度不大, 就是调用api,对于api 调用部分,我以前也写过文章可以参考
 
import { dev } from '$app/env';
import { asyncExecShell, getEngine } from '$lib/common';
import got from 'got';
import * as db from '$lib/database';
 
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
 
export const defaultProxyImage = `coolify-haproxy-alpine:latest`;
export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`;
export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`;
 
export async function haproxyInstance() {
  const { proxyPassword } = await db.listSettings();
  return got.extend({
    prefixUrl: url,
    username: 'admin',
    password: proxyPassword
  });
}
export async function getRawConfiguration(): Promise<RawHaproxyConfiguration> {
  return await (await haproxyInstance()).get(`v2/services/haproxy/configuration/raw`).json();
}
 
export async function getNextTransactionVersion(): Promise<number> {
  const raw = await getRawConfiguration();
  if (raw?._version) {
    return raw._version;
  }
  return 1;
}
 
export async function getNextTransactionId(): Promise<string> {
  const version = await getNextTransactionVersion();
  const newTransaction: NewTransaction = await (
    await haproxyInstance()
  )
    .post('v2/services/haproxy/transactions', {
      searchParams: {
        version
      }
    })
    .json();
  return newTransaction.id;
}
 
export async function completeTransaction(transactionId) {
  const haproxy = await haproxyInstance();
  return await haproxy.put(`v2/services/haproxy/transactions/${transactionId}`);
}
export async function deleteProxy({ id }) {
  const haproxy = await haproxyInstance();
  await checkHAProxy(haproxy);
 
  let transactionId;
  try {
    await haproxy.get(`v2/services/haproxy/configuration/backends/${id}`).json();
    transactionId = await getNextTransactionId();
    await haproxy
      .delete(`v2/services/haproxy/configuration/backends/${id}`, {
        searchParams: {
          transaction_id: transactionId
        }
      })
      .json();
    await haproxy.get(`v2/services/haproxy/configuration/frontends/${id}`).json();
    await haproxy
      .delete(`v2/services/haproxy/configuration/frontends/${id}`, {
        searchParams: {
          transaction_id: transactionId
        }
      })
      .json();
  } catch (error) {
    console.log(error.response?.body || error);
  } finally {
    if (transactionId) await completeTransaction(transactionId);
  }
}
 
export async function reloadHaproxy(engine) {
  const host = getEngine(engine);
  return await asyncExecShell(`DOCKER_HOST=${host} docker exec coolify-haproxy kill -HUP 1`);
}
export async function checkHAProxy(haproxy?: any) {
  if (!haproxy) haproxy = await haproxyInstance();
  try {
    await haproxy.get('v2/info');
  } catch (error) {
    throw {
      message:
        'Coolify Proxy is not running, but it should be!<br><br>Start it in the "Destinations" menu.'
    };
  }
}
 
export async function stopTcpHttpProxy(destinationDocker, publicPort) {
  const { engine } = destinationDocker;
  const host = getEngine(engine);
  const containerName = `haproxy-for-${publicPort}`;
  const found = await checkContainer(engine, containerName);
  try {
    if (found) {
      return await asyncExecShell(
        `DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}`
      );
    }
  } catch (error) {
    return error;
  }
}
export async function startTcpProxy(destinationDocker, id, publicPort, privatePort, volume = null) {
  const { network, engine } = destinationDocker;
  const host = getEngine(engine);
 
  const containerName = `haproxy-for-${publicPort}`;
  const found = await checkContainer(engine, containerName);
  const foundDB = await checkContainer(engine, id);
 
  try {
    if (foundDB && !found) {
      const { stdout: Config } = await asyncExecShell(
        `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
      );
      const ip = JSON.parse(Config)[0].Gateway;
      return await asyncExecShell(
        `DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} ${
          volume ? `-v ${volume}` : ''
        } -d coollabsio/${defaultProxyImageTcp}`
      );
    }
  } catch (error) {
    return error;
  }
}
export async function startHttpProxy(destinationDocker, id, publicPort, privatePort) {
  const { network, engine } = destinationDocker;
  const host = getEngine(engine);
 
  const containerName = `haproxy-for-${publicPort}`;
  const found = await checkContainer(engine, containerName);
  const foundDB = await checkContainer(engine, id);
 
  try {
    if (foundDB && !found) {
      const { stdout: Config } = await asyncExecShell(
        `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
      );
      const ip = JSON.parse(Config)[0].Gateway;
      return await asyncExecShell(
        `DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageHttp}`
      );
    }
  } catch (error) {
    return error;
  }
}
export async function startCoolifyProxy(engine) {
  const host = getEngine(engine);
  const found = await checkContainer(engine, 'coolify-haproxy');
  const { proxyPassword, proxyUser, id } = await db.listSettings();
  if (!found) {
    const { stdout: Config } = await asyncExecShell(
      `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
    );
    const ip = JSON.parse(Config)[0].Gateway;
    await asyncExecShell(
      `DOCKER_HOST="${host}" docker run -e HAPROXY_USERNAME=${proxyUser} -e HAPROXY_PASSWORD=${proxyPassword} --restart always --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' -v coolify-ssl-certs:/usr/local/etc/haproxy/ssl --network coolify-infra -p "80:80" -p "443:443" -p "8404:8404" -p "5555:5555" -p "5000:5000" --name coolify-haproxy -d coollabsio/${defaultProxyImage}`
    );
    await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } });
  }
  await configureNetworkCoolifyProxy(engine);
}
export async function checkContainer(engine, container) {
  const host = getEngine(engine);
  let containerFound = false;
 
  try {
    const { stdout } = await asyncExecShell(
      `DOCKER_HOST="${host}" docker inspect --format '{{json .State}}' ${container}`
    );
    const parsedStdout = JSON.parse(stdout);
    const status = parsedStdout.Status;
    const isRunning = status === 'running' ? true : false;
    if (status === 'exited' || status === 'created') {
      await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
    }
    if (isRunning) {
      containerFound = true;
    }
  } catch (err) {
    // Container not found
  }
  return containerFound;
}
 
export async function stopCoolifyProxy(engine) {
  const host = getEngine(engine);
  const found = await checkContainer(engine, 'coolify-haproxy');
  await db.setDestinationSettings({ engine, isCoolifyProxyUsed: false });
  const { id } = await db.prisma.setting.findFirst({});
  await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } });
  try {
    if (found) {
      await asyncExecShell(
        `DOCKER_HOST="${host}" docker stop -t 0 coolify-haproxy && docker rm coolify-haproxy`
      );
    }
  } catch (error) {
    return error;
  }
}
 
export async function configureNetworkCoolifyProxy(engine) {
  const host = getEngine(engine);
  const destinations = await db.prisma.destinationDocker.findMany({ where: { engine } });
  destinations.forEach(async (destination) => {
    try {
      await asyncExecShell(
        `DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-haproxy`
      );
    } catch (err) {
      // TODO: handle error
    }
  });
}

说明

实际上官方的处理部分并不是很好,比如获取事务版本的,是直接读取整个配置,实际上有更好的获取版本api

参考资料

https://github.com/haproxytech/dataplaneapi
https://www.haproxy.com/documentation/dataplaneapi/
https://www.cnblogs.com/rongfengliang/p/12914925.html
https://www.cnblogs.com/rongfengliang/p/13394103.html

标签:haproxy,集成,const,await,host,coolify,http,id
来源: https://www.cnblogs.com/rongfengliang/p/16125613.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有