diff --git a/README.md b/README.md index 36023b4fe..0a3ad1bf0 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ https://certd.handfree.work/ 1. 修改`docker-compose.yaml`中的镜像版本号 2. 运行`docker compose up -d` 即可 -如果使用`latest`版本 +如果需要使用最新版本 ```shell #重新拉取镜像 docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest @@ -98,7 +98,54 @@ docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest docker compose down docker compose up -d ``` +关于自动升级(仅限尝鲜建议非生产使用) +```yaml +version: '3.3' +services: + certd: + image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest + container_name: certd + restart: unless-stopped + volumes: + - /data/certd:/app/data + ports: + - "7001:7001" + - "7002:7002" + # 如果需要修改系统配置,可以通过环境变量传递;初次运行请保持默认配置 + environment: + - certd_system_resetAdminPasswd=false + # 如果需要切换数据库类型,可以在此处设置为mysql或postgres + # - certd_typeorm_dataSource_default_type=mysql + # - certd_typeorm_dataSource_default_host=localhost + # - certd_typeorm_dataSource_default_port=3306 + # - certd_typeorm_dataSource_default_username=root + # - certd_typeorm_dataSource_default_password=123456 + # - certd_typeorm_dataSource_default_database=certd + labels: + com.centurylinklabs.watchtower.enable: "true" + certd-updater: # 添加 Watchtower 服务 + image: containrrr/watchtower:latest + container_name: certd-updater + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock + # 配置 自动更新 + environment: + - WATCHTOWER_CLEANUP=true # 自动清理旧版本容器 + - WATCHTOWER_INCLUDE_STOPPED=false # 不更新已停止的容器 + - WATCHTOWER_LABEL_ENABLE=true # 根据容器标签进行更新 + - WATCHTOWER_POLL_INTERVAL=300 # 每 5 分钟检查一次更新 + +# 如果需要支持 IPv6,请取消以下注释 +# networks: +# ip6net: +# enable_ipv6: true +# ipam: +# config: +# - subnet: 2001:db8::/64 + +``` > 数据默认存在`/data/certd`目录下,不用担心数据丢失 diff --git a/packages/core/basic/src/utils/util.options.ts b/packages/core/basic/src/utils/util.options.ts index 96180e60e..4689c9df7 100644 --- a/packages/core/basic/src/utils/util.options.ts +++ b/packages/core/basic/src/utils/util.options.ts @@ -37,6 +37,8 @@ function buildGroupOptions(options: any[], inDomains: string[]) { } export const optionsUtils = { + //获取分组 groupByDomain, + //构建分组后的选项列表,常用 buildGroupOptions, }; diff --git a/packages/core/basic/src/utils/util.request.ts b/packages/core/basic/src/utils/util.request.ts index 9342d73aa..3be865ba0 100644 --- a/packages/core/basic/src/utils/util.request.ts +++ b/packages/core/basic/src/utils/util.request.ts @@ -1,4 +1,4 @@ -import axios, { AxiosRequestConfig } from 'axios'; +import axios, { AxiosHeaders, AxiosRequestConfig } from 'axios'; import { ILogger, logger } from './util.log.js'; import { Logger } from 'log4js'; import { HttpProxyAgent } from 'http-proxy-agent'; @@ -13,7 +13,7 @@ export class HttpError extends Error { statusText?: string; code?: string; request?: { baseURL: string; url: string; method: string; params?: any; data?: any }; - response?: { data: any }; + response?: { data: any; headers: AxiosHeaders }; cause?: any; constructor(error: any) { if (!error) { @@ -55,6 +55,7 @@ export class HttpError extends Error { this.response = { data: error.response?.data, + headers: error.response?.headers, }; const { stack, cause } = error; @@ -156,13 +157,13 @@ export function createAxiosService({ logger }: { logger: Logger }) { error.message = '请求错误'; break; case 401: - error.message = '未授权,请登录'; + error.message = '认证/登录失败'; break; case 403: error.message = '拒绝访问'; break; case 404: - error.message = `请求地址出错: ${error.response.config.url}`; + error.message = `请求地址出错`; break; case 408: error.message = '请求超时'; @@ -216,6 +217,7 @@ export type HttpRequestConfig = { logParams?: boolean; logRes?: boolean; httpProxy?: string; + returnResponse?: boolean; } & AxiosRequestConfig; export type HttpClient = { request(config: HttpRequestConfig): Promise>; diff --git a/packages/core/pipeline/src/core/executor.ts b/packages/core/pipeline/src/core/executor.ts index 874c525a7..3f09b402a 100644 --- a/packages/core/pipeline/src/core/executor.ts +++ b/packages/core/pipeline/src/core/executor.ts @@ -385,7 +385,7 @@ export class Executor { content = `流水线ID:${this.pipeline.id},运行ID:${this.runtime.id}`; } else if (when === "error") { subject = `执行失败,${this.pipeline.title}【${this.pipeline.id}】`; - content = `流水线ID:${this.pipeline.id},运行ID:${this.runtime.id}\n错误详情:${error.message}`; + content = `流水线ID:${this.pipeline.id},运行ID:${this.runtime.id}\n\n${this.currentStatusMap?.currentStep?.title} 执行失败\n\n错误详情:${error.message}`; } else { return; } diff --git a/packages/core/pipeline/src/core/run-history.ts b/packages/core/pipeline/src/core/run-history.ts index 1c2711867..be7e11117 100644 --- a/packages/core/pipeline/src/core/run-history.ts +++ b/packages/core/pipeline/src/core/run-history.ts @@ -134,6 +134,7 @@ export class RunHistory { export class RunnableCollection { private collection: RunnableMap = {}; private pipeline!: Pipeline; + currentStep!: Step; constructor(pipeline?: Pipeline) { if (!pipeline) { return; @@ -193,5 +194,8 @@ export class RunnableCollection { add(runnable: Runnable) { this.collection[runnable.id] = runnable; + if (runnable.runnableType === "step") { + this.currentStep = runnable as Step; + } } } diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts index 410754da7..085466a51 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts @@ -32,6 +32,7 @@ export type CertInfo = { pfx?: string; der?: string; jks?: string; + one?: string; }; export type SSLProvider = "letsencrypt" | "google" | "zerossl"; export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521"; diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts index d7d172cb4..f8dcf1f31 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts @@ -192,6 +192,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin { zip.file("cert.key", cert.key); zip.file("intermediate.crt", cert.ic); zip.file("origin.crt", cert.oc); + zip.file("one.pem", cert.one); if (cert.pfx) { zip.file("cert.pfx", Buffer.from(cert.pfx, "base64")); } @@ -209,6 +210,7 @@ cert.crt:证书文件,包含证书链,pem格式 cert.key:私钥文件,pem格式 intermediate.crt:中间证书文件,pem格式 origin.crt:原始证书文件,不含证书链,pem格式 +one.pem: 证书和私钥简单合并成一个文件,pem格式,crt正文+key正文 cert.pfx:pfx格式证书文件,iis服务器使用 cert.der:der格式证书文件 cert.jks:jks格式证书文件,java服务器使用 diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/cert-reader.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/cert-reader.ts index 907c3ab16..4128830d4 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/cert-reader.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/cert-reader.ts @@ -15,6 +15,7 @@ export type CertReaderHandleContext = { tmpDerPath?: string; tmpIcPath?: string; tmpJksPath?: string; + tmpOnePath?: string; }; export type CertReaderHandle = (ctx: CertReaderHandleContext) => Promise; export type HandleOpts = { logger: ILogger; handle: CertReaderHandle }; @@ -25,6 +26,7 @@ export class CertReader { key: string; csr: string; ic: string; //中间证书 + one: string; //crt + key 合成一个pem文件 detail: any; expires: number; @@ -46,6 +48,12 @@ export class CertReader { this.cert.oc = this.oc; } + this.one = certInfo.one; + if (!this.one) { + this.one = this.crt + "\n" + this.key; + this.cert.one = this.one; + } + const { detail, expires } = this.getCrtDetail(this.cert.crt); this.detail = detail; this.expires = expires.getTime(); @@ -88,7 +96,7 @@ export class CertReader { return domains; } - saveToFile(type: "crt" | "key" | "pfx" | "der" | "oc" | "ic" | "jks", filepath?: string) { + saveToFile(type: "crt" | "key" | "pfx" | "der" | "oc" | "one" | "ic" | "jks", filepath?: string) { if (!this.cert[type]) { return; } @@ -102,7 +110,7 @@ export class CertReader { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } - if (type === "crt" || type === "key" || type === "ic" || type === "oc") { + if (type === "crt" || type === "key" || type === "ic" || type === "oc" || type === "one") { fs.writeFileSync(filepath, this.cert[type]); } else { fs.writeFileSync(filepath, Buffer.from(this.cert[type], "base64")); @@ -120,6 +128,7 @@ export class CertReader { const tmpOcPath = this.saveToFile("oc"); const tmpDerPath = this.saveToFile("der"); const tmpJksPath = this.saveToFile("jks"); + const tmpOnePath = this.saveToFile("one"); logger.info("本地文件写入成功"); try { return await opts.handle({ @@ -131,6 +140,7 @@ export class CertReader { tmpIcPath: tmpIcPath, tmpJksPath: tmpJksPath, tmpOcPath: tmpOcPath, + tmpOnePath, }); } catch (err) { throw err; @@ -149,6 +159,7 @@ export class CertReader { removeFile(tmpDerPath); removeFile(tmpIcPath); removeFile(tmpJksPath); + removeFile(tmpOnePath); } } diff --git a/packages/ui/certd-server/src/plugins/plugin-host/plugin/upload-to-host/index.ts b/packages/ui/certd-server/src/plugins/plugin-host/plugin/upload-to-host/index.ts index 8dc519166..e9a691910 100644 --- a/packages/ui/certd-server/src/plugins/plugin-host/plugin/upload-to-host/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-host/plugin/upload-to-host/index.ts @@ -38,6 +38,7 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin { { value: 'pfx', label: 'pfx,一般用于IIS' }, { value: 'der', label: 'der,一般用于Apache' }, { value: 'jks', label: 'jks,一般用于JAVA应用' }, + { value: 'one', label: '证书私钥一体,crt+key简单合并为一个pem文件' }, ], }, required: true, @@ -150,6 +151,24 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin { }) jksPath!: string; + @TaskInput({ + title: '一体证书保存路径', + helper: '填写应用原本的证书保存路径,路径要包含证书文件名,例如:/tmp/crt_key.pem', + component: { + placeholder: '/root/deploy/app/crt_key.pem', + }, + mergeScript: ` + return { + show: ctx.compute(({form})=>{ + return form.certType === 'one'; + }) + } + `, + required: true, + rules: [{ type: 'filepath' }], + }) + onePath!: string; + @TaskInput({ title: '主机登录配置', helper: 'access授权',