mirror of
https://github.com/certd/certd.git
synced 2026-04-23 19:57:27 +08:00
refactor: 1
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
import { AbstractPlugin, IsTask, RunStrategy, TaskInput, TaskOutput, TaskPlugin, utils } from "@certd/pipeline";
|
||||
import tencentcloud from "tencentcloud-sdk-nodejs/index";
|
||||
import { TencentAccess } from "../../access";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
@IsTask(() => {
|
||||
return {
|
||||
name: "DeployCertToTencentCLB",
|
||||
title: "部署到腾讯云CLB",
|
||||
desc: "暂时只支持单向认证证书,暂时只支持通用负载均衡",
|
||||
input: {
|
||||
region: {
|
||||
title: "大区",
|
||||
value: "ap-guangzhou",
|
||||
component: {
|
||||
name: "a-select",
|
||||
options: [{ value: "ap-guangzhou" }],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
domain: {
|
||||
title: "域名",
|
||||
required: true,
|
||||
helper: "要更新的支持https的负载均衡的域名",
|
||||
},
|
||||
loadBalancerId: {
|
||||
title: "负载均衡ID",
|
||||
helper: "如果没有配置,则根据域名匹配负载均衡下的监听器(根据域名匹配时暂时只支持前100个)",
|
||||
required: true,
|
||||
},
|
||||
listenerId: {
|
||||
title: "监听器ID",
|
||||
helper: "如果没有配置,则根据域名或负载均衡id匹配监听器",
|
||||
},
|
||||
certName: {
|
||||
title: "证书名称前缀",
|
||||
},
|
||||
accessId: {
|
||||
title: "Access提供者",
|
||||
helper: "access授权",
|
||||
component: {
|
||||
name: "pi-access-selector",
|
||||
type: "tencent",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
cert: {
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "pi-output-selector",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
output: {},
|
||||
};
|
||||
})
|
||||
export class DeployToClbPlugin extends AbstractPlugin implements TaskPlugin {
|
||||
async execute(input: TaskInput): Promise<TaskOutput> {
|
||||
const { accessId, region, domain } = input;
|
||||
const accessProvider = (await this.accessService.getById(accessId)) as TencentAccess;
|
||||
const client = this.getClient(accessProvider, region);
|
||||
|
||||
const lastCertId = await this.getCertIdFromProps(client, input);
|
||||
if (!domain) {
|
||||
await this.updateListener(client, input);
|
||||
} else {
|
||||
await this.updateByDomainAttr(client, input);
|
||||
}
|
||||
|
||||
try {
|
||||
await utils.sleep(2000);
|
||||
let newCertId = await this.getCertIdFromProps(client, input);
|
||||
if ((lastCertId && newCertId === lastCertId) || (!lastCertId && !newCertId)) {
|
||||
await utils.sleep(2000);
|
||||
newCertId = await this.getCertIdFromProps(client, input);
|
||||
}
|
||||
if (newCertId === lastCertId) {
|
||||
return {};
|
||||
}
|
||||
this.logger.info("腾讯云证书ID:", newCertId);
|
||||
} catch (e) {
|
||||
this.logger.warn("查询腾讯云证书失败", e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async getCertIdFromProps(client: any, input: TaskInput) {
|
||||
const listenerRet = await this.getListenerList(client, input.loadBalancerId, [input.listenerId]);
|
||||
return this.getCertIdFromListener(listenerRet[0], input.domain);
|
||||
}
|
||||
|
||||
getCertIdFromListener(listener: any, domain: string) {
|
||||
let certId;
|
||||
if (!domain) {
|
||||
certId = listener.Certificate.CertId;
|
||||
} else {
|
||||
if (listener.Rules && listener.Rules.length > 0) {
|
||||
for (const rule of listener.Rules) {
|
||||
if (rule.Domain === domain) {
|
||||
if (rule.Certificate != null) {
|
||||
certId = rule.Certificate.CertId;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return certId;
|
||||
}
|
||||
|
||||
async updateListener(client: any, props: TaskInput) {
|
||||
const params = this.buildProps(props);
|
||||
const ret = await client.ModifyListener(params);
|
||||
this.checkRet(ret);
|
||||
this.logger.info("设置腾讯云CLB证书成功:", ret.RequestId, "->loadBalancerId:", props.loadBalancerId, "listenerId", props.listenerId);
|
||||
return ret;
|
||||
}
|
||||
|
||||
async updateByDomainAttr(client: any, props: TaskInput) {
|
||||
const params: any = this.buildProps(props);
|
||||
params.Domain = props.domain;
|
||||
const ret = await client.ModifyDomainAttributes(params);
|
||||
this.checkRet(ret);
|
||||
this.logger.info(
|
||||
"设置腾讯云CLB证书(sni)成功:",
|
||||
ret.RequestId,
|
||||
"->loadBalancerId:",
|
||||
props.loadBalancerId,
|
||||
"listenerId",
|
||||
props.listenerId,
|
||||
"domain:",
|
||||
props.domain
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
appendTimeSuffix(name: string) {
|
||||
if (name == null) {
|
||||
name = "certd";
|
||||
}
|
||||
return name + "-" + dayjs().format("YYYYMMDD-HHmmss");
|
||||
}
|
||||
buildProps(props: TaskInput) {
|
||||
const { certName, cert } = props;
|
||||
return {
|
||||
Certificate: {
|
||||
SSLMode: "UNIDIRECTIONAL", // 单向认证
|
||||
CertName: this.appendTimeSuffix(certName || cert.domain),
|
||||
CertKey: cert.key,
|
||||
CertContent: cert.crt,
|
||||
},
|
||||
LoadBalancerId: props.loadBalancerId,
|
||||
ListenerId: props.listenerId,
|
||||
};
|
||||
}
|
||||
|
||||
async getCLBList(client: any, props: TaskInput) {
|
||||
const params = {
|
||||
Limit: 100, // 最大暂时只支持100个,暂时没做翻页
|
||||
OrderBy: "CreateTime",
|
||||
OrderType: 0,
|
||||
...props.DescribeLoadBalancers,
|
||||
};
|
||||
const ret = await client.DescribeLoadBalancers(params);
|
||||
this.checkRet(ret);
|
||||
return ret.LoadBalancerSet;
|
||||
}
|
||||
|
||||
async getListenerList(client: any, balancerId: any, listenerIds: any) {
|
||||
// HTTPS
|
||||
const params = {
|
||||
LoadBalancerId: balancerId,
|
||||
Protocol: "HTTPS",
|
||||
ListenerIds: listenerIds,
|
||||
};
|
||||
const ret = await client.DescribeListeners(params);
|
||||
this.checkRet(ret);
|
||||
return ret.Listeners;
|
||||
}
|
||||
|
||||
getClient(accessProvider: TencentAccess, region: string) {
|
||||
const ClbClient = tencentcloud.clb.v20180317.Client;
|
||||
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: accessProvider.secretId,
|
||||
secretKey: accessProvider.secretKey,
|
||||
},
|
||||
region: region,
|
||||
profile: {
|
||||
httpProfile: {
|
||||
endpoint: "clb.tencentcloudapi.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return new ClbClient(clientConfig);
|
||||
}
|
||||
|
||||
checkRet(ret: any) {
|
||||
if (!ret || ret.Error) {
|
||||
throw new Error("执行失败:" + ret.Error.Code + "," + ret.Error.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import { AbstractPlugin, IsTask, RunStrategy, TaskInput, TaskOutput, TaskPlugin, utils } from "@certd/pipeline";
|
||||
import tencentcloud from "tencentcloud-sdk-nodejs/index";
|
||||
import { K8sClient } from "@certd/plugin-util";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
@IsTask(() => {
|
||||
return {
|
||||
name: "DeployCertToTencentTKEIngress",
|
||||
title: "部署到腾讯云TKE-ingress",
|
||||
desc: "需要【上传到腾讯云】作为前置任务",
|
||||
input: {
|
||||
region: {
|
||||
title: "大区",
|
||||
value: "ap-guangzhou",
|
||||
required: true,
|
||||
},
|
||||
clusterId: {
|
||||
title: "集群ID",
|
||||
required: true,
|
||||
desc: "例如:cls-6lbj1vee",
|
||||
request: true,
|
||||
},
|
||||
namespace: {
|
||||
title: "集群namespace",
|
||||
value: "default",
|
||||
required: true,
|
||||
},
|
||||
secreteName: {
|
||||
title: "证书的secret名称",
|
||||
required: true,
|
||||
},
|
||||
ingressName: {
|
||||
title: "ingress名称",
|
||||
required: true,
|
||||
},
|
||||
ingressClass: {
|
||||
title: "ingress类型",
|
||||
component: {
|
||||
name: "a-select",
|
||||
options: [{ value: "qcloud" }, { value: "nginx" }],
|
||||
},
|
||||
helper: "可选 qcloud / nginx",
|
||||
},
|
||||
clusterIp: {
|
||||
title: "集群内网ip",
|
||||
helper: "如果开启了外网的话,无需设置",
|
||||
},
|
||||
clusterDomain: {
|
||||
title: "集群域名",
|
||||
helper: "可不填,默认为:[clusterId].ccs.tencent-cloud.com",
|
||||
},
|
||||
|
||||
tencentCertId: {
|
||||
title: "腾讯云证书id",
|
||||
helper: "请选择“上传证书到腾讯云”前置任务的输出",
|
||||
component: {
|
||||
name: "pi-output-selector",
|
||||
from: "UploadCertToTencent",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* AccessProvider的key,或者一个包含access的具体的对象
|
||||
*/
|
||||
accessId: {
|
||||
title: "Access授权",
|
||||
helper: "access授权",
|
||||
component: {
|
||||
name: "pi-access-selector",
|
||||
type: "tencent",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
cert: {
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "pi-output-selector",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
output: {},
|
||||
};
|
||||
})
|
||||
export class DeployCertToTencentTKEIngressPlugin extends AbstractPlugin implements TaskPlugin {
|
||||
async execute(input: TaskInput): Promise<TaskOutput> {
|
||||
const { accessId, region, clusterId, clusterIp, ingressClass } = input;
|
||||
let { clusterDomain } = input;
|
||||
const accessProvider = this.accessService.getById(accessId);
|
||||
const tkeClient = this.getTkeClient(accessProvider, region);
|
||||
const kubeConfigStr = await this.getTkeKubeConfig(tkeClient, clusterId);
|
||||
|
||||
this.logger.info("kubeconfig已成功获取");
|
||||
const k8sClient = new K8sClient(kubeConfigStr);
|
||||
if (clusterIp != null) {
|
||||
if (!clusterDomain) {
|
||||
clusterDomain = `${clusterId}.ccs.tencent-cloud.com`;
|
||||
}
|
||||
// 修改内网解析ip地址
|
||||
k8sClient.setLookup({ [clusterDomain]: { ip: clusterIp } });
|
||||
}
|
||||
const ingressType = ingressClass || "qcloud";
|
||||
if (ingressType === "qcloud") {
|
||||
await this.patchQcloudCertSecret({ k8sClient, input });
|
||||
} else {
|
||||
await this.patchNginxCertSecret({ k8sClient, input });
|
||||
}
|
||||
|
||||
await utils.sleep(2000); // 停留2秒,等待secret部署完成
|
||||
await this.restartIngress({ k8sClient, input });
|
||||
return {};
|
||||
}
|
||||
|
||||
getTkeClient(accessProvider: any, region = "ap-guangzhou") {
|
||||
const TkeClient = tencentcloud.tke.v20180525.Client;
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: accessProvider.secretId,
|
||||
secretKey: accessProvider.secretKey,
|
||||
},
|
||||
region,
|
||||
profile: {
|
||||
httpProfile: {
|
||||
endpoint: "tke.tencentcloudapi.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return new TkeClient(clientConfig);
|
||||
}
|
||||
|
||||
async getTkeKubeConfig(client: any, clusterId: string) {
|
||||
// Depends on tencentcloud-sdk-nodejs version 4.0.3 or higher
|
||||
const params = {
|
||||
ClusterId: clusterId,
|
||||
};
|
||||
const ret = await client.DescribeClusterKubeconfig(params);
|
||||
this.checkRet(ret);
|
||||
this.logger.info("注意:后续操作需要在【集群->基本信息】中开启外网或内网访问,https://console.cloud.tencent.com/tke2/cluster");
|
||||
return ret.Kubeconfig;
|
||||
}
|
||||
|
||||
appendTimeSuffix(name: string) {
|
||||
if (name == null) {
|
||||
name = "certd";
|
||||
}
|
||||
return name + "-" + dayjs().format("YYYYMMDD-HHmmss");
|
||||
}
|
||||
|
||||
async patchQcloudCertSecret(options: { k8sClient: any; input: TaskInput }) {
|
||||
const { tencentCertId } = options.input;
|
||||
if (tencentCertId == null) {
|
||||
throw new Error("请先将【上传证书到腾讯云】作为前置任务");
|
||||
}
|
||||
this.logger.info("腾讯云证书ID:", tencentCertId);
|
||||
const certIdBase64 = Buffer.from(tencentCertId).toString("base64");
|
||||
|
||||
const { namespace, secretName } = options.input;
|
||||
|
||||
const body = {
|
||||
data: {
|
||||
qcloud_cert_id: certIdBase64,
|
||||
},
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: this.appendTimeSuffix("certd"),
|
||||
},
|
||||
},
|
||||
};
|
||||
let secretNames = secretName;
|
||||
if (typeof secretName === "string") {
|
||||
secretNames = [secretName];
|
||||
}
|
||||
for (const secret of secretNames) {
|
||||
await options.k8sClient.patchSecret({ namespace, secretName: secret, body });
|
||||
this.logger.info(`CertSecret已更新:${secret}`);
|
||||
}
|
||||
}
|
||||
|
||||
async patchNginxCertSecret(options: { k8sClient: any; input: TaskInput }) {
|
||||
const { k8sClient, input } = options;
|
||||
const { cert } = input;
|
||||
const crt = cert.crt;
|
||||
const key = cert.key;
|
||||
const crtBase64 = Buffer.from(crt).toString("base64");
|
||||
const keyBase64 = Buffer.from(key).toString("base64");
|
||||
|
||||
const { namespace, secretName } = input;
|
||||
|
||||
const body = {
|
||||
data: {
|
||||
"tls.crt": crtBase64,
|
||||
"tls.key": keyBase64,
|
||||
},
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: this.appendTimeSuffix("certd"),
|
||||
},
|
||||
},
|
||||
};
|
||||
let secretNames = secretName;
|
||||
if (typeof secretName === "string") {
|
||||
secretNames = [secretName];
|
||||
}
|
||||
for (const secret of secretNames) {
|
||||
await k8sClient.patchSecret({ namespace, secretName: secret, body });
|
||||
this.logger.info(`CertSecret已更新:${secret}`);
|
||||
}
|
||||
}
|
||||
|
||||
async restartIngress(options: { k8sClient: any; input: TaskInput }) {
|
||||
const { k8sClient, input } = options;
|
||||
const { namespace, ingressName } = input;
|
||||
|
||||
const body = {
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: this.appendTimeSuffix("certd"),
|
||||
},
|
||||
},
|
||||
};
|
||||
let ingressNames = ingressName;
|
||||
if (typeof ingressName === "string") {
|
||||
ingressNames = [ingressName];
|
||||
}
|
||||
for (const ingress of ingressNames) {
|
||||
await k8sClient.patchIngress({ namespace, ingressName: ingress, body });
|
||||
this.logger.info(`ingress已重启:${ingress}`);
|
||||
}
|
||||
}
|
||||
checkRet(ret: any) {
|
||||
if (!ret || ret.Error) {
|
||||
throw new Error("执行失败:" + ret.Error.Code + "," + ret.Error.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { AbstractPlugin, IsTask, RunStrategy, TaskInput, TaskOutput, TaskPlugin } from "@certd/pipeline";
|
||||
import tencentcloud from "tencentcloud-sdk-nodejs/index";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
@IsTask(() => {
|
||||
return {
|
||||
name: "UploadCertToTencent",
|
||||
title: "上传证书到腾讯云",
|
||||
desc: "上传成功后输出:tencentCertId",
|
||||
input: {
|
||||
name: {
|
||||
title: "证书名称",
|
||||
},
|
||||
accessId: {
|
||||
title: "Access授权",
|
||||
helper: "access授权",
|
||||
component: {
|
||||
name: "pi-access-selector",
|
||||
type: "tencent",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
cert: {
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "pi-output-selector",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
output: {
|
||||
tencentCertId: {
|
||||
title: "上传成功后的腾讯云CertId",
|
||||
},
|
||||
},
|
||||
};
|
||||
})
|
||||
export class UploadToTencentPlugin extends AbstractPlugin implements TaskPlugin {
|
||||
async execute(input: TaskInput): Promise<TaskOutput> {
|
||||
const { accessId, name, cert } = input;
|
||||
const accessProvider = this.accessService.getById(accessId);
|
||||
const certName = this.appendTimeSuffix(name || cert.domain);
|
||||
const client = this.getClient(accessProvider);
|
||||
|
||||
const params = {
|
||||
CertificatePublicKey: cert.crt,
|
||||
CertificatePrivateKey: cert.key,
|
||||
Alias: certName,
|
||||
};
|
||||
const ret = await client.UploadCertificate(params);
|
||||
this.checkRet(ret);
|
||||
this.logger.info("证书上传成功:tencentCertId=", ret.CertificateId);
|
||||
return { tencentCertId: ret.CertificateId };
|
||||
}
|
||||
|
||||
appendTimeSuffix(name: string) {
|
||||
if (name == null) {
|
||||
name = "certd";
|
||||
}
|
||||
return name + "-" + dayjs().format("YYYYMMDD-HHmmss");
|
||||
}
|
||||
|
||||
getClient(accessProvider: any) {
|
||||
const SslClient = tencentcloud.ssl.v20191205.Client;
|
||||
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: accessProvider.secretId,
|
||||
secretKey: accessProvider.secretKey,
|
||||
},
|
||||
region: "",
|
||||
profile: {
|
||||
httpProfile: {
|
||||
endpoint: "ssl.tencentcloudapi.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return new SslClient(clientConfig);
|
||||
}
|
||||
|
||||
// async rollback({ input }) {
|
||||
// const { accessId } = input;
|
||||
// const accessProvider = this.accessService.getById(accessId);
|
||||
// const client = this.getClient(accessProvider);
|
||||
//
|
||||
// const { tencentCertId } = context;
|
||||
// const params = {
|
||||
// CertificateId: tencentCertId,
|
||||
// };
|
||||
// const ret = await client.DeleteCertificate(params);
|
||||
// this.checkRet(ret);
|
||||
// this.logger.info("证书删除成功:DeleteResult=", ret.DeleteResult);
|
||||
// delete context.tencentCertId;
|
||||
// }
|
||||
checkRet(ret: any) {
|
||||
if (!ret || ret.Error) {
|
||||
throw new Error("执行失败:" + ret.Error.Code + "," + ret.Error.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user