• coolify haproxy 集成简单说明


    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

  • 相关阅读:
    系统吞吐量(TPS)、用户并发量、性能测试概念和公式
    javascript 匿名函数和闭包
    构建高可扩Web架构和分布式系统实战
    Javascript prototype 的作用
    myeclipse下载地址
    tomacat7.0配置(windows)
    IEEE802.11
    C#(多态)
    C#(泛型集合的使用)
    C#(对象引用)
  • 原文地址:https://www.cnblogs.com/rongfengliang/p/16125613.html
Copyright © 2020-2023  润新知