mirror of https://github.com/certd/certd
refactor: 1
parent
f710c00c0d
commit
d66bc33761
|
@ -0,0 +1 @@
|
|||
export * from "./src";
|
|
@ -2,7 +2,7 @@
|
|||
"name": "@certd/pipeline",
|
||||
"private": true,
|
||||
"version": "0.3.0",
|
||||
"main": "./dist/pipeline.umd.js",
|
||||
"main": "./src/index.ts",
|
||||
"module": "./dist/pipeline.mjs",
|
||||
"types": "./dist/es/index.d.ts",
|
||||
"scripts": {
|
||||
|
@ -14,7 +14,10 @@
|
|||
"@certd/acme-client": "^0.3.0",
|
||||
"dayjs": "^1.11.6",
|
||||
"lodash": "^4.17.21",
|
||||
"node-forge": "^0.10.0"
|
||||
"node-forge": "^0.10.0",
|
||||
"log4js": "^6.3.0",
|
||||
"axios": "^0.21.1",
|
||||
"qs": "^6.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.186",
|
||||
|
@ -35,7 +38,6 @@
|
|||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"log4js": "^6.3.0",
|
||||
"mocha": "^10.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.8.4",
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import { AbstractAccess } from "./abstract-access";
|
||||
|
||||
export interface IAccessService {
|
||||
getById(id: any): Promise<AbstractAccess>;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { Registrable } from "../registry";
|
||||
import { accessRegistry } from "./registry";
|
||||
import { FormItemProps } from "../d.ts";
|
||||
import { AbstractAccess } from "./abstract-access";
|
||||
|
||||
export type AccessInput = FormItemProps & {
|
||||
title: string;
|
||||
|
@ -17,3 +18,7 @@ export function IsAccess(define: AccessDefine) {
|
|||
accessRegistry.install(target);
|
||||
};
|
||||
}
|
||||
|
||||
export interface IAccessService {
|
||||
getById(id: any): Promise<AbstractAccess>;
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
import { IsAccess } from "../api";
|
||||
import { AbstractAccess } from "../abstract-access";
|
||||
|
||||
@IsAccess({
|
||||
name: "aliyun",
|
||||
title: "阿里云授权",
|
||||
desc: "",
|
||||
input: {
|
||||
accessKeyId: {
|
||||
title: "accessKeyId",
|
||||
component: {
|
||||
placeholder: "accessKeyId",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
accessKeySecret: {
|
||||
title: "accessKeySecret",
|
||||
component: {
|
||||
placeholder: "accessKeySecret",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class AliyunAccess extends AbstractAccess {
|
||||
accessKeyId = "";
|
||||
accessKeySecret = "";
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from "./aliyun-access";
|
|
@ -1,3 +1,3 @@
|
|||
export * from "./api";
|
||||
export * from "./impl";
|
||||
export * from "./abstract-access";
|
||||
export * from "./registry";
|
||||
|
|
|
@ -2,12 +2,12 @@ import { ConcurrencyStrategy, Pipeline, ResultType, Runnable, RunStrategy, Stage
|
|||
import _ from "lodash";
|
||||
import { RunHistory } from "./run-history";
|
||||
import { pluginRegistry, TaskPlugin } from "../plugin";
|
||||
import { IAccessService } from "../access/access-service";
|
||||
import { ContextFactory, IContext } from "./context";
|
||||
import { IStorage } from "./storage";
|
||||
import { logger } from "../utils/util.log";
|
||||
import { Logger } from "log4js";
|
||||
|
||||
import { request } from "../utils/util.request";
|
||||
import { IAccessService } from "../access";
|
||||
export class Executor {
|
||||
userId: any;
|
||||
pipeline: Pipeline;
|
||||
|
@ -166,6 +166,7 @@ export class Executor {
|
|||
pipelineContext: this.pipelineContext,
|
||||
userContext: this.contextFactory.getContext("user", this.userId),
|
||||
logger,
|
||||
http: request,
|
||||
});
|
||||
return plugin;
|
||||
}
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
export * from "./executor";
|
||||
export * from "./run-history";
|
||||
export * from "./context";
|
||||
export * from "./storage";
|
||||
|
|
|
@ -2,12 +2,15 @@ import { AbstractRegistrable } from "../registry";
|
|||
import { CreateRecordOptions, IDnsProvider, DnsProviderDefine, RemoveRecordOptions } from "./api";
|
||||
import { AbstractAccess } from "../access";
|
||||
import { Logger } from "log4js";
|
||||
import { AxiosInstance } from "axios";
|
||||
export abstract class AbstractDnsProvider extends AbstractRegistrable<DnsProviderDefine> implements IDnsProvider {
|
||||
access!: AbstractAccess;
|
||||
logger!: Logger;
|
||||
doInit(options: { access: AbstractAccess; logger: Logger }) {
|
||||
http!: AxiosInstance;
|
||||
doInit(options: { access: AbstractAccess; logger: Logger; http: AxiosInstance }) {
|
||||
this.access = options.access;
|
||||
this.logger = options.logger;
|
||||
this.http = options.http;
|
||||
this.onInit();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import "./providers";
|
||||
export * from "./api";
|
||||
export * from "./registry";
|
||||
export * from "./abstract-dns-provider";
|
||||
|
|
|
@ -1,120 +0,0 @@
|
|||
import { AbstractDnsProvider } from "../abstract-dns-provider";
|
||||
import Core from "@alicloud/pop-core";
|
||||
import _ from "lodash";
|
||||
import { CreateRecordOptions, IDnsProvider, IsDnsProvider, RemoveRecordOptions } from "../api";
|
||||
|
||||
@IsDnsProvider({
|
||||
name: "aliyun",
|
||||
title: "阿里云",
|
||||
desc: "阿里云DNS解析提供商",
|
||||
accessType: "aliyun",
|
||||
})
|
||||
export class AliyunDnsProvider extends AbstractDnsProvider implements IDnsProvider {
|
||||
client: any;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
async onInit() {
|
||||
const access: any = this.access;
|
||||
this.client = new Core({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
endpoint: "https://alidns.aliyuncs.com",
|
||||
apiVersion: "2015-01-09",
|
||||
});
|
||||
}
|
||||
|
||||
async getDomainList() {
|
||||
const params = {
|
||||
RegionId: "cn-hangzhou",
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
const ret = await this.client.request("DescribeDomains", params, requestOption);
|
||||
return ret.Domains.Domain;
|
||||
}
|
||||
|
||||
async matchDomain(dnsRecord: string) {
|
||||
const list = await this.getDomainList();
|
||||
let domain = null;
|
||||
for (const item of list) {
|
||||
if (_.endsWith(dnsRecord, item.DomainName)) {
|
||||
domain = item.DomainName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!domain) {
|
||||
throw new Error("can not find Domain ," + dnsRecord);
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
|
||||
async getRecords(domain: string, rr: string, value: string) {
|
||||
const params: any = {
|
||||
RegionId: "cn-hangzhou",
|
||||
DomainName: domain,
|
||||
RRKeyWord: rr,
|
||||
ValueKeyWord: undefined,
|
||||
};
|
||||
if (value) {
|
||||
params.ValueKeyWord = value;
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
const ret = await this.client.request("DescribeDomainRecords", params, requestOption);
|
||||
return ret.DomainRecords.Record;
|
||||
}
|
||||
|
||||
async createRecord(options: CreateRecordOptions): Promise<any> {
|
||||
const { fullRecord, value, type } = options;
|
||||
this.logger.info("添加域名解析:", fullRecord, value);
|
||||
const domain = await this.matchDomain(fullRecord);
|
||||
const rr = fullRecord.replace("." + domain, "");
|
||||
|
||||
const params = {
|
||||
RegionId: "cn-hangzhou",
|
||||
DomainName: domain,
|
||||
RR: rr,
|
||||
Type: type,
|
||||
Value: value,
|
||||
// Line: 'oversea' // 海外
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
try {
|
||||
const ret = await this.client.request("AddDomainRecord", params, requestOption);
|
||||
this.logger.info("添加域名解析成功:", value, value, ret.RecordId);
|
||||
return ret.RecordId;
|
||||
} catch (e: any) {
|
||||
if (e.code === "DomainRecordDuplicate") {
|
||||
return;
|
||||
}
|
||||
this.logger.info("添加域名解析出错", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
async removeRecord(options: RemoveRecordOptions): Promise<any> {
|
||||
const { fullRecord, value, record } = options;
|
||||
const params = {
|
||||
RegionId: "cn-hangzhou",
|
||||
RecordId: record,
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
const ret = await this.client.request("DeleteDomainRecord", params, requestOption);
|
||||
this.logger.info("删除域名解析成功:", fullRecord, value, ret.RecordId);
|
||||
return ret.RecordId;
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
import "./aliyun-dns-provider";
|
|
@ -4,3 +4,4 @@ export * from "./access";
|
|||
export * from "./registry";
|
||||
export * from "./dns-provider";
|
||||
export * from "./plugin";
|
||||
export * from "./utils";
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { AbstractRegistrable } from "../registry";
|
||||
import { Logger } from "log4js";
|
||||
import { IAccessService } from "../access/access-service";
|
||||
import { IContext } from "../core/context";
|
||||
import { PluginDefine, TaskInput, TaskOutput, TaskPlugin } from "./api";
|
||||
import { IAccessService } from "../access";
|
||||
import { AxiosInstance } from "axios";
|
||||
|
||||
export abstract class AbstractPlugin extends AbstractRegistrable<PluginDefine> implements TaskPlugin {
|
||||
logger!: Logger;
|
||||
|
@ -12,12 +13,14 @@ export abstract class AbstractPlugin extends AbstractRegistrable<PluginDefine> i
|
|||
pipelineContext: IContext;
|
||||
// @ts-ignore
|
||||
userContext: IContext;
|
||||
http!: AxiosInstance;
|
||||
|
||||
async doInit(options: { accessService: IAccessService; pipelineContext: IContext; userContext: IContext; logger: Logger }) {
|
||||
async doInit(options: { accessService: IAccessService; pipelineContext: IContext; userContext: IContext; logger: Logger; http: AxiosInstance }) {
|
||||
this.accessService = options.accessService;
|
||||
this.pipelineContext = options.pipelineContext;
|
||||
this.userContext = options.userContext;
|
||||
this.logger = options.logger;
|
||||
this.http = options.http;
|
||||
await this.onInit();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import "./plugins";
|
||||
export * from "./api";
|
||||
export * from "./registry";
|
||||
export * from "./abstract-plugin";
|
||||
|
|
|
@ -1,199 +0,0 @@
|
|||
// @ts-ignore
|
||||
import * as acme from "@certd/acme-client";
|
||||
import _ from "lodash";
|
||||
import { AbstractDnsProvider } from "../../../dns-provider/abstract-dns-provider";
|
||||
import { IContext } from "../../../core/context";
|
||||
import { IDnsProvider } from "../../../dns-provider";
|
||||
import { Challenge } from "@certd/acme-client/types/rfc8555";
|
||||
import { Logger } from "log4js";
|
||||
export class AcmeService {
|
||||
userContext: IContext;
|
||||
logger: Logger;
|
||||
constructor(options: { userContext: IContext; logger: Logger }) {
|
||||
this.userContext = options.userContext;
|
||||
this.logger = options.logger;
|
||||
acme.setLogger((text: string) => {
|
||||
this.logger.info(text);
|
||||
});
|
||||
}
|
||||
|
||||
async getAccountConfig(email: string) {
|
||||
return (await this.userContext.get(this.buildAccountKey(email))) || {};
|
||||
}
|
||||
|
||||
buildAccountKey(email: string) {
|
||||
return "acme.config." + email;
|
||||
}
|
||||
|
||||
async saveAccountConfig(email: string, conf: any) {
|
||||
await this.userContext.set(this.buildAccountKey(email), conf);
|
||||
}
|
||||
|
||||
async getAcmeClient(email: string, isTest = false): Promise<acme.Client> {
|
||||
const conf = await this.getAccountConfig(email);
|
||||
if (conf.key == null) {
|
||||
conf.key = await this.createNewKey();
|
||||
await this.saveAccountConfig(email, conf);
|
||||
}
|
||||
if (isTest == null) {
|
||||
isTest = process.env.CERTD_MODE === "test";
|
||||
}
|
||||
const client = new acme.Client({
|
||||
directoryUrl: isTest ? acme.directory.letsencrypt.staging : acme.directory.letsencrypt.production,
|
||||
accountKey: conf.key,
|
||||
accountUrl: conf.accountUrl,
|
||||
backoffAttempts: 20,
|
||||
backoffMin: 5000,
|
||||
backoffMax: 10000,
|
||||
});
|
||||
|
||||
if (conf.accountUrl == null) {
|
||||
const accountPayload = {
|
||||
termsOfServiceAgreed: true,
|
||||
contact: [`mailto:${email}`],
|
||||
};
|
||||
await client.createAccount(accountPayload);
|
||||
conf.accountUrl = client.getAccountUrl();
|
||||
await this.saveAccountConfig(email, conf);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
async createNewKey() {
|
||||
const key = await acme.forge.createPrivateKey();
|
||||
return key.toString();
|
||||
}
|
||||
|
||||
async challengeCreateFn(authz: any, challenge: any, keyAuthorization: string, dnsProvider: IDnsProvider) {
|
||||
this.logger.info("Triggered challengeCreateFn()");
|
||||
|
||||
/* http-01 */
|
||||
if (challenge.type === "http-01") {
|
||||
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`;
|
||||
const fileContents = keyAuthorization;
|
||||
|
||||
this.logger.info(`Creating challenge response for ${authz.identifier.value} at path: ${filePath}`);
|
||||
|
||||
/* Replace this */
|
||||
this.logger.info(`Would write "${fileContents}" to path "${filePath}"`);
|
||||
// await fs.writeFileAsync(filePath, fileContents);
|
||||
} else if (challenge.type === "dns-01") {
|
||||
/* dns-01 */
|
||||
const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
|
||||
const recordValue = keyAuthorization;
|
||||
|
||||
this.logger.info(`Creating TXT record for ${authz.identifier.value}: ${dnsRecord}`);
|
||||
|
||||
/* Replace this */
|
||||
this.logger.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`);
|
||||
|
||||
return await dnsProvider.createRecord({
|
||||
fullRecord: dnsRecord,
|
||||
type: "TXT",
|
||||
value: recordValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function used to remove an ACME challenge response
|
||||
*
|
||||
* @param {object} authz Authorization object
|
||||
* @param {object} challenge Selected challenge
|
||||
* @param {string} keyAuthorization Authorization key
|
||||
* @param recordItem challengeCreateFn create record item
|
||||
* @param dnsProvider dnsProvider
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordItem: any, dnsProvider: IDnsProvider) {
|
||||
this.logger.info("Triggered challengeRemoveFn()");
|
||||
|
||||
/* http-01 */
|
||||
if (challenge.type === "http-01") {
|
||||
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`;
|
||||
|
||||
this.logger.info(`Removing challenge response for ${authz.identifier.value} at path: ${filePath}`);
|
||||
|
||||
/* Replace this */
|
||||
this.logger.info(`Would remove file on path "${filePath}"`);
|
||||
// await fs.unlinkAsync(filePath);
|
||||
} else if (challenge.type === "dns-01") {
|
||||
const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
|
||||
const recordValue = keyAuthorization;
|
||||
|
||||
this.logger.info(`Removing TXT record for ${authz.identifier.value}: ${dnsRecord}`);
|
||||
|
||||
/* Replace this */
|
||||
this.logger.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`);
|
||||
await dnsProvider.removeRecord({
|
||||
fullRecord: dnsRecord,
|
||||
type: "TXT",
|
||||
value: keyAuthorization,
|
||||
record: recordItem,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async order(options: { email: string; domains: string | string[]; dnsProvider: AbstractDnsProvider; csrInfo: any; isTest?: boolean }) {
|
||||
const { email, isTest, domains, csrInfo, dnsProvider } = options;
|
||||
const client: acme.Client = await this.getAcmeClient(email, isTest);
|
||||
|
||||
/* Create CSR */
|
||||
const { commonName, altNames } = this.buildCommonNameByDomains(domains);
|
||||
|
||||
const [key, csr] = await acme.forge.createCsr({
|
||||
commonName,
|
||||
...csrInfo,
|
||||
altNames,
|
||||
});
|
||||
if (dnsProvider == null) {
|
||||
throw new Error("dnsProvider 不能为空");
|
||||
}
|
||||
/* 自动申请证书 */
|
||||
const crt = await client.auto({
|
||||
csr,
|
||||
email: email,
|
||||
termsOfServiceAgreed: true,
|
||||
challengePriority: ["dns-01"],
|
||||
challengeCreateFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string): Promise<any> => {
|
||||
return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider);
|
||||
},
|
||||
challengeRemoveFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string, recordItem: any): Promise<any> => {
|
||||
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem, dnsProvider);
|
||||
},
|
||||
});
|
||||
|
||||
const cert = {
|
||||
crt: crt.toString(),
|
||||
key: key.toString(),
|
||||
csr: csr.toString(),
|
||||
};
|
||||
/* Done */
|
||||
this.logger.debug(`CSR:\n${cert.csr}`);
|
||||
this.logger.debug(`Certificate:\n${cert.crt}`);
|
||||
this.logger.info("证书申请成功");
|
||||
return cert;
|
||||
}
|
||||
|
||||
buildCommonNameByDomains(domains: string | string[]): {
|
||||
commonName: string;
|
||||
altNames: string[] | undefined;
|
||||
} {
|
||||
if (typeof domains === "string") {
|
||||
domains = domains.split(",");
|
||||
}
|
||||
if (domains.length === 0) {
|
||||
throw new Error("domain can not be empty");
|
||||
}
|
||||
const commonName = domains[0];
|
||||
let altNames: undefined | string[] = undefined;
|
||||
if (domains.length > 1) {
|
||||
altNames = _.slice(domains, 1);
|
||||
}
|
||||
return {
|
||||
commonName,
|
||||
altNames,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,236 +0,0 @@
|
|||
import { AbstractPlugin } from "../../abstract-plugin";
|
||||
import forge from "node-forge";
|
||||
import { IsTask, TaskInput, TaskOutput, TaskPlugin } from "../../api";
|
||||
import dayjs from "dayjs";
|
||||
import { dnsProviderRegistry } from "../../../dns-provider";
|
||||
import { AbstractDnsProvider } from "../../../dns-provider/abstract-dns-provider";
|
||||
import { AcmeService } from "./acme";
|
||||
import _ from "lodash";
|
||||
export type CertInfo = {
|
||||
crt: string;
|
||||
key: string;
|
||||
csr: string;
|
||||
};
|
||||
@IsTask(() => {
|
||||
return {
|
||||
name: "CertApply",
|
||||
title: "证书申请",
|
||||
desc: "免费通配符域名证书申请,支持多个域名打到同一个证书上",
|
||||
input: {
|
||||
domains: {
|
||||
title: "域名",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
open: false,
|
||||
},
|
||||
required: true,
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
helper:
|
||||
"支持通配符域名,例如: *.foo.com 、 *.test.handsfree.work\n" +
|
||||
"支持多个域名、多个子域名、多个通配符域名打到一个证书上(域名必须是在同一个DNS提供商解析)\n" +
|
||||
"多级子域名要分成多个域名输入(*.foo.com的证书不能用于xxx.yyy.foo.com)\n" +
|
||||
"输入一个回车之后,再输入下一个",
|
||||
},
|
||||
email: {
|
||||
title: "邮箱",
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
helper: "请输入邮箱",
|
||||
},
|
||||
dnsProviderType: {
|
||||
title: "DNS提供商",
|
||||
component: {
|
||||
name: "pi-dns-provider-selector",
|
||||
},
|
||||
required: true,
|
||||
helper: "请选择dns解析提供商",
|
||||
},
|
||||
dnsProviderAccess: {
|
||||
title: "DNS解析授权",
|
||||
component: {
|
||||
name: "pi-access-selector",
|
||||
},
|
||||
required: true,
|
||||
helper: "请选择dns解析提供商授权",
|
||||
},
|
||||
renewDays: {
|
||||
title: "更新天数",
|
||||
component: {
|
||||
name: "a-input-number",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
helper: "到期前多少天后更新证书",
|
||||
},
|
||||
forceUpdate: {
|
||||
title: "强制更新",
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
helper: "是否强制重新申请证书",
|
||||
},
|
||||
},
|
||||
default: {
|
||||
input: {
|
||||
renewDays: 20,
|
||||
forceUpdate: false,
|
||||
},
|
||||
},
|
||||
output: {
|
||||
cert: {
|
||||
key: "cert",
|
||||
type: "CertInfo",
|
||||
title: "域名证书",
|
||||
},
|
||||
},
|
||||
};
|
||||
})
|
||||
export class CertApplyPlugin extends AbstractPlugin implements TaskPlugin {
|
||||
// @ts-ignore
|
||||
acme: AcmeService;
|
||||
protected async onInit() {
|
||||
this.acme = new AcmeService({ userContext: this.userContext, logger: this.logger });
|
||||
}
|
||||
|
||||
async execute(input: TaskInput): Promise<TaskOutput> {
|
||||
const oldCert = await this.condition(input);
|
||||
if (oldCert != null) {
|
||||
return {
|
||||
cert: oldCert,
|
||||
};
|
||||
}
|
||||
const cert = await this.doCertApply(input);
|
||||
return { cert };
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否更新证书
|
||||
* @param input
|
||||
*/
|
||||
async condition(input: TaskInput) {
|
||||
if (input.forceUpdate) {
|
||||
return null;
|
||||
}
|
||||
let oldCert;
|
||||
try {
|
||||
oldCert = await this.readCurrentCert();
|
||||
} catch (e) {
|
||||
this.logger.warn("读取cert失败:", e);
|
||||
}
|
||||
if (oldCert == null) {
|
||||
this.logger.info("还未申请过,准备申请新证书");
|
||||
return null;
|
||||
}
|
||||
|
||||
const ret = this.isWillExpire(oldCert.expires, input.renewDays);
|
||||
if (!ret.isWillExpire) {
|
||||
this.logger.info(`证书还未过期:过期时间${dayjs(oldCert.expires).format("YYYY-MM-DD HH:mm:ss")},剩余${ret.leftDays}天`);
|
||||
return oldCert;
|
||||
}
|
||||
this.logger.info("即将过期,开始更新证书");
|
||||
return null;
|
||||
}
|
||||
|
||||
async doCertApply(input: TaskInput) {
|
||||
const email = input["email"];
|
||||
const domains = input["domains"];
|
||||
const dnsProviderType = input["dnsProviderType"];
|
||||
const dnsProviderAccessId = input["dnsProviderAccess"];
|
||||
const csrInfo = _.merge(
|
||||
{
|
||||
country: "CN",
|
||||
state: "GuangDong",
|
||||
locality: "ShengZhen",
|
||||
organization: "CertD Org.",
|
||||
organizationUnit: "IT Department",
|
||||
emailAddress: email,
|
||||
},
|
||||
input["csrInfo"]
|
||||
);
|
||||
this.logger.info("开始申请证书,", email, domains);
|
||||
|
||||
const dnsProviderClass = dnsProviderRegistry.get(dnsProviderType);
|
||||
const access = await this.accessService.getById(dnsProviderAccessId);
|
||||
// @ts-ignore
|
||||
const dnsProvider: AbstractDnsProvider = new dnsProviderClass();
|
||||
dnsProvider.doInit({ access, logger: this.logger });
|
||||
|
||||
const cert = await this.acme.order({
|
||||
email,
|
||||
domains,
|
||||
dnsProvider,
|
||||
csrInfo,
|
||||
isTest: false,
|
||||
});
|
||||
|
||||
await this.writeCert(cert);
|
||||
const ret = await this.readCurrentCert();
|
||||
|
||||
return {
|
||||
...ret,
|
||||
isNew: true,
|
||||
};
|
||||
}
|
||||
|
||||
formatCert(pem: string) {
|
||||
pem = pem.replace(/\r/g, "");
|
||||
pem = pem.replace(/\n\n/g, "\n");
|
||||
pem = pem.replace(/\n$/g, "");
|
||||
return pem;
|
||||
}
|
||||
|
||||
async writeCert(cert: { crt: string; key: string; csr: string }) {
|
||||
const newCert = {
|
||||
crt: this.formatCert(cert.crt),
|
||||
key: this.formatCert(cert.key),
|
||||
csr: this.formatCert(cert.csr),
|
||||
};
|
||||
await this.pipelineContext.set("cert", newCert);
|
||||
}
|
||||
|
||||
async readCurrentCert() {
|
||||
const cert: CertInfo = await this.pipelineContext.get("cert");
|
||||
if (cert == null) {
|
||||
return undefined;
|
||||
}
|
||||
const { detail, expires } = this.getCrtDetail(cert.crt);
|
||||
return {
|
||||
...cert,
|
||||
detail,
|
||||
expires: expires.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
getCrtDetail(crt: string) {
|
||||
const pki = forge.pki;
|
||||
const detail = pki.certificateFromPem(crt.toString());
|
||||
const expires = detail.validity.notAfter;
|
||||
return { detail, expires };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否过期,默认提前20天
|
||||
* @param expires
|
||||
* @param maxDays
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isWillExpire(expires: number, maxDays = 20) {
|
||||
if (expires == null) {
|
||||
throw new Error("过期时间不能为空");
|
||||
}
|
||||
// 检查有效期
|
||||
const leftDays = dayjs(expires).diff(dayjs(), "day");
|
||||
return {
|
||||
isWillExpire: leftDays < maxDays,
|
||||
leftDays,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
import { AbstractPlugin } from "../../abstract-plugin";
|
||||
import { IsTask, TaskInput, TaskOutput, TaskPlugin } from "../../api";
|
||||
import dayjs from "dayjs";
|
||||
import Core from "@alicloud/pop-core";
|
||||
import RPCClient from "@alicloud/pop-core";
|
||||
import { AliyunAccess } from "../../../access";
|
||||
import { CertInfo } from "../cert-plugin";
|
||||
import { RunStrategy } from "../../../d.ts";
|
||||
|
||||
@IsTask(() => {
|
||||
return {
|
||||
name: "DeployCertToAliyunCDN",
|
||||
title: "部署证书至阿里云CDN",
|
||||
desc: "依赖证书申请前置任务,自动部署域名证书至阿里云CDN",
|
||||
input: {
|
||||
domainName: {
|
||||
title: "CDN加速域名",
|
||||
helper: "你在阿里云上配置的CDN加速域名,比如certd.docmirror.cn",
|
||||
required: true,
|
||||
},
|
||||
certName: {
|
||||
title: "证书名称",
|
||||
helper: "上传后将以此名称作为前缀备注",
|
||||
},
|
||||
cert: {
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "pi-output-selector",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
accessId: {
|
||||
title: "Access授权",
|
||||
helper: "阿里云授权AccessKeyId、AccessKeySecret",
|
||||
component: {
|
||||
name: "pi-access-selector",
|
||||
type: "aliyun",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
output: {},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
};
|
||||
})
|
||||
export class DeployCertToAliyunCDN extends AbstractPlugin implements TaskPlugin {
|
||||
async execute(input: TaskInput): Promise<TaskOutput> {
|
||||
console.log("开始部署证书到阿里云cdn");
|
||||
const access = (await this.accessService.getById(input.accessId)) as AliyunAccess;
|
||||
const client = this.getClient(access);
|
||||
const params = await this.buildParams(input);
|
||||
await this.doRequest(client, params);
|
||||
console.log("部署完成");
|
||||
return {};
|
||||
}
|
||||
|
||||
getClient(access: AliyunAccess) {
|
||||
return new Core({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
endpoint: "https://cdn.aliyuncs.com",
|
||||
apiVersion: "2018-05-10",
|
||||
});
|
||||
}
|
||||
|
||||
async buildParams(input: TaskInput) {
|
||||
const { certName, domainName } = input;
|
||||
const CertName = (certName ?? "certd") + "-" + dayjs().format("YYYYMMDDHHmmss");
|
||||
const cert = input.cert as CertInfo;
|
||||
return {
|
||||
RegionId: "cn-hangzhou",
|
||||
DomainName: domainName,
|
||||
ServerCertificateStatus: "on",
|
||||
CertName: CertName,
|
||||
CertType: "upload",
|
||||
ServerCertificate: cert.crt,
|
||||
PrivateKey: cert.key,
|
||||
};
|
||||
}
|
||||
|
||||
async doRequest(client: RPCClient, params: any) {
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
};
|
||||
const ret: any = await client.request("SetDomainServerCertificate", params, requestOption);
|
||||
this.checkRet(ret);
|
||||
this.logger.info("设置cdn证书成功:", ret.RequestId);
|
||||
}
|
||||
|
||||
checkRet(ret: any) {
|
||||
if (ret.code != null) {
|
||||
throw new Error("执行失败:" + ret.Message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./cert-plugin/index";
|
||||
export * from "./echo-plugin";
|
||||
export * from "./deploy-to-cdn/index";
|
|
@ -0,0 +1,7 @@
|
|||
import sleep from "./util.sleep";
|
||||
import { request } from "./util.request";
|
||||
export * from "./util.log";
|
||||
export const utils = {
|
||||
sleep,
|
||||
http: request,
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
import axios from "axios";
|
||||
// @ts-ignore
|
||||
import qs from "qs";
|
||||
import { logger } from "./util.log";
|
||||
/**
|
||||
* @description 创建请求实例
|
||||
*/
|
||||
function createService() {
|
||||
// 创建一个 axios 实例
|
||||
const service = axios.create();
|
||||
// 请求拦截
|
||||
service.interceptors.request.use(
|
||||
(config: any) => {
|
||||
if (config.formData) {
|
||||
config.data = qs.stringify(config.formData, {
|
||||
arrayFormat: "indices",
|
||||
allowDots: true,
|
||||
}); // 序列化请求参数
|
||||
delete config.formData;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error: Error) => {
|
||||
// 发送失败
|
||||
logger.error(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// 响应拦截
|
||||
service.interceptors.response.use(
|
||||
(response: any) => {
|
||||
logger.info("http response:", JSON.stringify(response.data));
|
||||
return response.data;
|
||||
},
|
||||
(error: any) => {
|
||||
// const status = _.get(error, 'response.status')
|
||||
// switch (status) {
|
||||
// case 400: error.message = '请求错误'; break
|
||||
// case 401: error.message = '未授权,请登录'; break
|
||||
// case 403: error.message = '拒绝访问'; break
|
||||
// case 404: error.message = `请求地址出错: ${error.response.config.url}`; break
|
||||
// case 408: error.message = '请求超时'; break
|
||||
// case 500: error.message = '服务器内部错误'; break
|
||||
// case 501: error.message = '服务未实现'; break
|
||||
// case 502: error.message = '网关错误'; break
|
||||
// case 503: error.message = '服务不可用'; break
|
||||
// case 504: error.message = '网关超时'; break
|
||||
// case 505: error.message = 'HTTP版本不受支持'; break
|
||||
// default: break
|
||||
// }
|
||||
logger.error("请求出错:", error.response.config.url, error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
return service;
|
||||
}
|
||||
|
||||
export const request = createService();
|
|
@ -0,0 +1,7 @@
|
|||
export default function (timeout: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({});
|
||||
}, timeout);
|
||||
});
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { AbstractPlugin } from "../abstract-plugin";
|
||||
import { IsTask, TaskInput, TaskOutput, TaskPlugin } from "../api";
|
||||
import { AbstractPlugin, IsTask, TaskInput, TaskOutput, TaskPlugin } from "../src";
|
||||
|
||||
@IsTask(() => {
|
||||
return {
|
|
@ -1,11 +1,12 @@
|
|||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
import { EchoPlugin } from "../src/plugin/plugins";
|
||||
import { EchoPlugin } from "./echo-plugin";
|
||||
describe("task_plugin", function () {
|
||||
it("#taskplugin", function () {
|
||||
const echoPlugin = new EchoPlugin();
|
||||
const define = echoPlugin.define;
|
||||
echoPlugin.execute({ context: {}, props: { test: 111 } });
|
||||
// @ts-ignore
|
||||
const define = echoPlugin.getDefine();
|
||||
echoPlugin.execute({ context: {}, input: { test: 111 } });
|
||||
expect(define.name).eq("EchoPlugin");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { IAccessService } from "../../src/access/access-service";
|
||||
import { AbstractAccess, AliyunAccess } from "../../src";
|
||||
import { AbstractAccess, IAccessService } from "../../src";
|
||||
import { aliyunSecret } from "../user.secret";
|
||||
export class AccessServiceTest implements IAccessService {
|
||||
async getById(id: any): Promise<AbstractAccess> {
|
||||
return {
|
||||
...aliyunSecret,
|
||||
} as AliyunAccess;
|
||||
} as any;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
import { CertApplyPlugin } from "../../../src/plugin/plugins";
|
||||
import { pluginInitProps } from "../init.test";
|
||||
describe("CertApply", function () {
|
||||
it("#execute", async function () {
|
||||
this.timeout(120000);
|
||||
const plugin = new CertApplyPlugin();
|
||||
// @ts-ignore
|
||||
delete plugin.define;
|
||||
await plugin.doInit(pluginInitProps);
|
||||
const output = await plugin.execute({
|
||||
domains: ["*.docmirror.cn", "docmirror.cn"],
|
||||
email: "xiaojunnuo@qq.com",
|
||||
dnsProviderType: "aliyun",
|
||||
accessId: "111",
|
||||
forceUpdate: true,
|
||||
});
|
||||
const cert = output.cert;
|
||||
expect(plugin.getDefine().name).eq("CertApply");
|
||||
expect(cert.crt != null).eq(true);
|
||||
});
|
||||
});
|
|
@ -1,23 +0,0 @@
|
|||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
import { DeployCertToAliyunCDN } from "../../../src/plugin/plugins";
|
||||
import { pluginInitProps } from "../init.test";
|
||||
|
||||
describe("DeployToAliyunCDN", function () {
|
||||
it("#execute", async function () {
|
||||
this.timeout(120000);
|
||||
const plugin = new DeployCertToAliyunCDN();
|
||||
// @ts-ignore
|
||||
delete plugin.define;
|
||||
|
||||
await plugin.doInit(pluginInitProps);
|
||||
|
||||
const cert = await pluginInitProps.pipelineContext.get("cert");
|
||||
|
||||
await plugin.execute({
|
||||
cert,
|
||||
domainName: "certd-cdn-upload.docmirror.cn",
|
||||
});
|
||||
expect(plugin.getDefine().name).eq("DeployCertToAliyunCDN");
|
||||
});
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"extends": "standard",
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.test.js", "*.spec.js"],
|
||||
"rules": {
|
||||
"no-unused-expressions": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
.vscode/
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
/.idea/
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"name": "@certd/plugin-aliyun",
|
||||
"version": "0.3.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/fast-crud.es.js",
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alicloud/cs20151215": "^3.0.3",
|
||||
"@alicloud/openapi-client": "^0.4.0",
|
||||
"@alicloud/pop-core": "^1.7.10",
|
||||
"@certd/api": "^0.3.0",
|
||||
"dayjs": "^1.9.7",
|
||||
"lodash-es": "^4.17.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/certd": "^0.3.0",
|
||||
"@certd/plugin-common": "^0.3.0",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^7.15.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"mocha": "^8.2.1",
|
||||
"rollup": "^3.2.3"
|
||||
},
|
||||
"author": "Greper",
|
||||
"license": "MIT",
|
||||
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
export default {
|
||||
input: 'src/index.js',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/index.cjs',
|
||||
format: 'cjs'
|
||||
},
|
||||
{
|
||||
file: 'dist/index.es.js',
|
||||
format: 'es'
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
export class AliyunAccessProvider {
|
||||
static define () {
|
||||
return {
|
||||
name: 'aliyun',
|
||||
title: '阿里云',
|
||||
desc: '',
|
||||
input: {
|
||||
accessKeyId: {
|
||||
component: {
|
||||
placeholder: 'accessKeyId'
|
||||
},
|
||||
rules: [{ required: true, message: '必填项' }]
|
||||
},
|
||||
accessKeySecret: {
|
||||
component: {
|
||||
placeholder: 'accessKeySecret'
|
||||
},
|
||||
rules: [{ required: true, message: '必填项' }]
|
||||
|
||||
}
|
||||
},
|
||||
output: {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
import { AbstractDnsProvider } from '@certd/api'
|
||||
import Core from '@alicloud/pop-core'
|
||||
import _ from 'lodash'
|
||||
export class AliyunDnsProvider extends AbstractDnsProvider {
|
||||
static define () {
|
||||
return {
|
||||
name: 'aliyun',
|
||||
title: '阿里云',
|
||||
desc: '',
|
||||
input: {
|
||||
accessProvider: {
|
||||
title: '授权',
|
||||
helper: '需要aliyun类型的授权',
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'aliyun'
|
||||
},
|
||||
rules: [{ required: true, message: '必填项' }]
|
||||
}
|
||||
},
|
||||
output: {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor (args) {
|
||||
super(args)
|
||||
const { props } = args
|
||||
const accessProvider = this.getAccessProvider(props.accessProvider)
|
||||
this.client = new Core({
|
||||
accessKeyId: accessProvider.accessKeyId,
|
||||
accessKeySecret: accessProvider.accessKeySecret,
|
||||
endpoint: 'https://alidns.aliyuncs.com',
|
||||
apiVersion: '2015-01-09'
|
||||
})
|
||||
}
|
||||
|
||||
async getDomainList () {
|
||||
const params = {
|
||||
RegionId: 'cn-hangzhou'
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST'
|
||||
}
|
||||
|
||||
const ret = await this.client.request('DescribeDomains', params, requestOption)
|
||||
return ret.Domains.Domain
|
||||
}
|
||||
|
||||
async matchDomain (dnsRecord) {
|
||||
const list = await this.getDomainList()
|
||||
let domain = null
|
||||
for (const item of list) {
|
||||
if (_.endsWith(dnsRecord, item.DomainName)) {
|
||||
domain = item.DomainName
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!domain) {
|
||||
throw new Error('can not find Domain ,' + dnsRecord)
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
async getRecords (domain, rr, value) {
|
||||
const params = {
|
||||
RegionId: 'cn-hangzhou',
|
||||
DomainName: domain,
|
||||
RRKeyWord: rr
|
||||
}
|
||||
if (value) {
|
||||
params.ValueKeyWord = value
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST'
|
||||
}
|
||||
|
||||
const ret = await this.client.request('DescribeDomainRecords', params, requestOption)
|
||||
return ret.DomainRecords.Record
|
||||
}
|
||||
|
||||
async createRecord ({ fullRecord, type, value }) {
|
||||
this.logger.info('添加域名解析:', fullRecord, value)
|
||||
const domain = await this.matchDomain(fullRecord)
|
||||
const rr = fullRecord.replace('.' + domain, '')
|
||||
|
||||
const params = {
|
||||
RegionId: 'cn-hangzhou',
|
||||
DomainName: domain,
|
||||
RR: rr,
|
||||
Type: type,
|
||||
Value: value
|
||||
// Line: 'oversea' // 海外
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST'
|
||||
}
|
||||
|
||||
try {
|
||||
const ret = await this.client.request('AddDomainRecord', params, requestOption)
|
||||
this.logger.info('添加域名解析成功:', value, value, ret.RecordId)
|
||||
return ret.RecordId
|
||||
} catch (e) {
|
||||
if (e.code === 'DomainRecordDuplicate') {
|
||||
return
|
||||
}
|
||||
this.logger.info('添加域名解析出错', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async removeRecord ({ fullRecord, type, value, record }) {
|
||||
const params = {
|
||||
RegionId: 'cn-hangzhou',
|
||||
RecordId: record
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST'
|
||||
}
|
||||
|
||||
const ret = await this.client.request('DeleteDomainRecord', params, requestOption)
|
||||
this.logger.info('删除域名解析成功:', fullRecord, value, ret.RecordId)
|
||||
return ret.RecordId
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import { AliyunDnsProvider } from './dns-providers/aliyun.js'
|
||||
import { AliyunAccessProvider } from './access-providers/aliyun.js'
|
||||
import { UploadCertToAliyun } from './plugins/upload-to-aliyun/index.js'
|
||||
import { DeployCertToAliyunCDN } from './plugins/deploy-to-cdn/index.js'
|
||||
import { DeployCertToAliyunAckIngress } from './plugins/deploy-to-ack-ingress/index.js'
|
||||
|
||||
import { pluginRegistry, accessProviderRegistry, dnsProviderRegistry } from '@certd/api'
|
||||
|
||||
export const Plugins = {
|
||||
UploadCertToAliyun,
|
||||
DeployCertToAliyunCDN,
|
||||
DeployCertToAliyunAckIngress
|
||||
}
|
||||
export default {
|
||||
install () {
|
||||
_.forEach(Plugins, item => {
|
||||
pluginRegistry.install(item)
|
||||
})
|
||||
|
||||
accessProviderRegistry.install(AliyunAccessProvider)
|
||||
dnsProviderRegistry.install(AliyunDnsProvider)
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { AbstractPlugin } from '@certd/api'
|
||||
|
||||
export class AbstractAliyunPlugin extends AbstractPlugin {
|
||||
checkRet (ret) {
|
||||
if (ret.code != null) {
|
||||
throw new Error('执行失败:', ret.Message)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,199 +0,0 @@
|
|||
import { AbstractAliyunPlugin } from '../abstract-aliyun.js'
|
||||
import Core from '@alicloud/pop-core'
|
||||
import { K8sClient } from '@certd/plugin-common'
|
||||
const ROAClient = Core.ROAClient
|
||||
|
||||
const define = {
|
||||
name: 'deployCertToAliyunAckIngress',
|
||||
title: '部署到阿里云AckIngress',
|
||||
input: {
|
||||
clusterId: {
|
||||
title: '集群id',
|
||||
component: {
|
||||
placeholder: '集群id'
|
||||
}
|
||||
},
|
||||
secretName: {
|
||||
title: '保密字典Id',
|
||||
component: {
|
||||
placeholder: '保密字典Id'
|
||||
},
|
||||
required: true
|
||||
},
|
||||
regionId: {
|
||||
title: '大区',
|
||||
value: 'cn-shanghai',
|
||||
component: {
|
||||
placeholder: '集群所属大区'
|
||||
},
|
||||
required: true
|
||||
},
|
||||
namespace: {
|
||||
title: '命名空间',
|
||||
value: 'default',
|
||||
component: {
|
||||
placeholder: '命名空间'
|
||||
},
|
||||
required: true
|
||||
},
|
||||
ingressName: {
|
||||
title: 'ingress名称',
|
||||
value: '',
|
||||
component: {
|
||||
placeholder: 'ingress名称'
|
||||
},
|
||||
required: true,
|
||||
helper: '可以传入一个数组'
|
||||
},
|
||||
ingressClass: {
|
||||
title: 'ingress类型',
|
||||
value: 'nginx',
|
||||
component: {
|
||||
placeholder: '暂时只支持nginx类型'
|
||||
},
|
||||
required: true
|
||||
},
|
||||
isPrivateIpAddress: {
|
||||
title: '是否私网ip',
|
||||
value: false,
|
||||
component: {
|
||||
placeholder: '集群连接端点是否是私网ip'
|
||||
},
|
||||
helper: '如果您当前certd运行在同一个私网下,可以选择是。',
|
||||
required: true
|
||||
},
|
||||
accessProvider: {
|
||||
title: 'Access授权',
|
||||
type: [String, Object],
|
||||
helper: 'AccessKey、AccessSecret',
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'aliyun'
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
output: {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class DeployCertToAliyunAckIngress extends AbstractAliyunPlugin {
|
||||
static define () {
|
||||
return define
|
||||
}
|
||||
|
||||
async execute ({ cert, props, context }) {
|
||||
const accessProvider = this.getAccessProvider(props.accessProvider)
|
||||
const client = this.getClient(accessProvider, props.regionId)
|
||||
|
||||
const kubeConfigStr = await this.getKubeConfig(client, props.clusterId, props.isPrivateIpAddress)
|
||||
|
||||
this.logger.info('kubeconfig已成功获取')
|
||||
const k8sClient = new K8sClient(kubeConfigStr)
|
||||
const ingressType = props.ingressClass || 'qcloud'
|
||||
if (ingressType === 'qcloud') {
|
||||
throw new Error('暂未实现')
|
||||
// await this.patchQcloudCertSecret({ k8sClient, props, context })
|
||||
} else {
|
||||
await this.patchNginxCertSecret({ cert, k8sClient, props, context })
|
||||
}
|
||||
|
||||
await this.sleep(3000) // 停留2秒,等待secret部署完成
|
||||
// await this.restartIngress({ k8sClient, props })
|
||||
return true
|
||||
}
|
||||
|
||||
async restartIngress ({ k8sClient, props }) {
|
||||
const { namespace } = props
|
||||
|
||||
const body = {
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: this.appendTimeSuffix('certd')
|
||||
}
|
||||
}
|
||||
}
|
||||
const ingressList = await k8sClient.getIngressList({ namespace })
|
||||
console.log('ingressList:', ingressList)
|
||||
if (!ingressList || !ingressList.body || !ingressList.body.items) {
|
||||
return
|
||||
}
|
||||
const ingressNames = ingressList.body.items.filter(item => {
|
||||
if (!item.spec.tls) {
|
||||
return false
|
||||
}
|
||||
for (const tls of item.spec.tls) {
|
||||
if (tls.secretName === props.secretName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}).map(item => {
|
||||
return item.metadata.name
|
||||
})
|
||||
for (const ingress of ingressNames) {
|
||||
await k8sClient.patchIngress({ namespace, ingressName: ingress, body })
|
||||
this.logger.info(`ingress已重启:${ingress}`)
|
||||
}
|
||||
}
|
||||
|
||||
async patchNginxCertSecret ({ cert, k8sClient, props, context }) {
|
||||
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 } = props
|
||||
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
|
||||
getClient (aliyunProvider, regionId) {
|
||||
return new ROAClient({
|
||||
accessKeyId: aliyunProvider.accessKeyId,
|
||||
accessKeySecret: aliyunProvider.accessKeySecret,
|
||||
endpoint: `https://cs.${regionId}.aliyuncs.com`,
|
||||
apiVersion: '2015-12-15'
|
||||
})
|
||||
}
|
||||
|
||||
async getKubeConfig (client, clusterId, isPrivateIpAddress = false) {
|
||||
const httpMethod = 'GET'
|
||||
const uriPath = `/k8s/${clusterId}/user_config`
|
||||
const queries = {
|
||||
PrivateIpAddress: isPrivateIpAddress
|
||||
}
|
||||
const body = '{}'
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
const requestOption = {}
|
||||
|
||||
try {
|
||||
const res = await client.request(httpMethod, uriPath, queries, body, headers, requestOption)
|
||||
return res.config
|
||||
} catch (e) {
|
||||
console.error('请求出错:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
import { AbstractAliyunPlugin } from '../abstract-aliyun.js'
|
||||
import Core from '@alicloud/pop-core'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const define = {
|
||||
name: 'deployCertToAliyunCDN',
|
||||
title: '部署到阿里云CDN',
|
||||
input: {
|
||||
domainName: {
|
||||
title: 'cdn加速域名',
|
||||
component: {
|
||||
placeholder: 'cdn加速域名'
|
||||
},
|
||||
required: true
|
||||
},
|
||||
certName: {
|
||||
title: '证书名称',
|
||||
component: {
|
||||
placeholder: '上传后将以此名称作为前缀'
|
||||
}
|
||||
},
|
||||
from: {
|
||||
value: 'upload',
|
||||
title: '证书来源',
|
||||
required: true,
|
||||
component: {
|
||||
placeholder: '证书来源',
|
||||
name: 'a-select',
|
||||
options: [
|
||||
{ value: 'upload', label: '直接上传' }
|
||||
]
|
||||
}
|
||||
},
|
||||
accessProvider: {
|
||||
label: 'Access提供者',
|
||||
type: [String, Object],
|
||||
desc: 'access授权',
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'aliyun'
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
output: {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class DeployCertToAliyunCDN extends AbstractAliyunPlugin {
|
||||
static define () {
|
||||
return define
|
||||
}
|
||||
|
||||
async execute ({ cert, props, context }) {
|
||||
const accessProvider = this.getAccessProvider(props.accessProvider)
|
||||
const client = this.getClient(accessProvider)
|
||||
const params = this.buildParams(props, context, cert)
|
||||
await this.doRequest(client, params)
|
||||
}
|
||||
|
||||
getClient (aliyunProvider) {
|
||||
return new Core({
|
||||
accessKeyId: aliyunProvider.accessKeyId,
|
||||
accessKeySecret: aliyunProvider.accessKeySecret,
|
||||
endpoint: 'https://cdn.aliyuncs.com',
|
||||
apiVersion: '2018-05-10'
|
||||
})
|
||||
}
|
||||
|
||||
buildParams (args, context, cert) {
|
||||
const { certName, from, domainName } = args
|
||||
const CertName = certName + '-' + dayjs().format('YYYYMMDDHHmmss')
|
||||
|
||||
const params = {
|
||||
RegionId: 'cn-hangzhou',
|
||||
DomainName: domainName,
|
||||
ServerCertificateStatus: 'on',
|
||||
CertName: CertName,
|
||||
CertType: from,
|
||||
ServerCertificate: cert.crt,
|
||||
PrivateKey: cert.key
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
async doRequest (client, params) {
|
||||
const requestOption = {
|
||||
method: 'POST'
|
||||
}
|
||||
const ret = await client.request('SetDomainServerCertificate', params, requestOption)
|
||||
this.checkRet(ret)
|
||||
this.logger.info('设置cdn证书成功:', ret.RequestId)
|
||||
}
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
import Core from '@alicloud/pop-core'
|
||||
import { AbstractAliyunPlugin } from '../abstract-aliyun.js'
|
||||
import { ZoneOptions } from '../../utils/index.js'
|
||||
|
||||
const define = {
|
||||
name: 'uploadCertToAliyun',
|
||||
title: '上传证书到阿里云',
|
||||
desc: '',
|
||||
input: {
|
||||
name: {
|
||||
title: '证书名称',
|
||||
helper: '证书上传后将以此参数作为名称前缀'
|
||||
},
|
||||
regionId: {
|
||||
title: '大区',
|
||||
value: 'cn-hangzhou',
|
||||
component: {
|
||||
name: 'a-select',
|
||||
vModel: 'value',
|
||||
options: ZoneOptions
|
||||
},
|
||||
required: true
|
||||
},
|
||||
accessProvider: {
|
||||
title: 'Access授权',
|
||||
helper: 'Access授权',
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'aliyun'
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
output: {
|
||||
aliyunCertId: {
|
||||
type: String,
|
||||
desc: '上传成功后的阿里云CertId'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class UploadCertToAliyun extends AbstractAliyunPlugin {
|
||||
static define () {
|
||||
return define
|
||||
}
|
||||
|
||||
getClient (aliyunProvider) {
|
||||
return new Core({
|
||||
accessKeyId: aliyunProvider.accessKeyId,
|
||||
accessKeySecret: aliyunProvider.accessKeySecret,
|
||||
endpoint: 'https://cas.aliyuncs.com',
|
||||
apiVersion: '2018-07-13'
|
||||
})
|
||||
}
|
||||
|
||||
async execute ({ cert, props, context }) {
|
||||
const { name, accessProvider } = props
|
||||
const certName = this.appendTimeSuffix(name || cert.domain)
|
||||
const params = {
|
||||
RegionId: props.regionId || 'cn-hangzhou',
|
||||
Name: certName,
|
||||
Cert: cert.crt,
|
||||
Key: cert.key
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST'
|
||||
}
|
||||
|
||||
const provider = this.getAccessProvider(accessProvider)
|
||||
const client = this.getClient(provider)
|
||||
const ret = await client.request('CreateUserCertificate', params, requestOption)
|
||||
this.checkRet(ret)
|
||||
this.logger.info('证书上传成功:aliyunCertId=', ret.CertId)
|
||||
context.aliyunCertId = ret.CertId
|
||||
}
|
||||
|
||||
/**
|
||||
* 没用,现在阿里云证书不允许删除
|
||||
* @param accessProviders
|
||||
* @param cert
|
||||
* @param props
|
||||
* @param context
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async rollback ({ cert, props, context }) {
|
||||
const { accessProvider } = props
|
||||
const { aliyunCertId } = context
|
||||
this.logger.info('准备删除阿里云证书:', aliyunCertId)
|
||||
const params = {
|
||||
RegionId: props.regionId || 'cn-hangzhou',
|
||||
CertId: aliyunCertId
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST'
|
||||
}
|
||||
|
||||
const provider = this.getAccessProvider(accessProvider)
|
||||
const client = this.getClient(provider)
|
||||
const ret = await client.request('DeleteUserCertificate', params, requestOption)
|
||||
this.checkRet(ret)
|
||||
this.logger.info('证书删除成功:', aliyunCertId)
|
||||
delete context.aliyunCertId
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export const ZoneOptions = [
|
||||
{ value: 'cn-hangzhou' }
|
||||
]
|
|
@ -1,22 +0,0 @@
|
|||
import pkg from 'chai'
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
import PluginAliyun from '../../src/index.js'
|
||||
|
||||
// 安装默认插件和授权提供者
|
||||
PluginAliyun.install()
|
||||
const { expect } = pkg
|
||||
describe('AliyunDnsProvider', function () {
|
||||
it('#申请证书-aliyun', async function () {
|
||||
this.timeout(300000)
|
||||
const options = createOptions()
|
||||
options.args = { forceCert: true, test: false }
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.certApply()
|
||||
expect(cert).ok
|
||||
expect(cert.crt).ok
|
||||
expect(cert.key).ok
|
||||
expect(cert.detail).ok
|
||||
expect(cert.expires).ok
|
||||
})
|
||||
})
|
|
@ -1,39 +0,0 @@
|
|||
import pkg from 'chai'
|
||||
import { AliyunDnsProvider } from '../../src/dns-providers/aliyun.js'
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
const { expect } = pkg
|
||||
|
||||
export function getPluginOptions () {
|
||||
const options = createOptions()
|
||||
return { accessProviders: options.accessProviders, props: options.cert.dnsProvider }
|
||||
}
|
||||
|
||||
describe('AliyunDnsProvider', function () {
|
||||
it('#getDomainList', async function () {
|
||||
const options = getPluginOptions()
|
||||
const aliyunDnsProvider = new AliyunDnsProvider(options)
|
||||
const domainList = await aliyunDnsProvider.getDomainList()
|
||||
console.log('domainList', domainList)
|
||||
expect(domainList.length).gt(0)
|
||||
})
|
||||
|
||||
it('#getRecords', async function () {
|
||||
const options = getPluginOptions()
|
||||
const aliyunDnsProvider = new AliyunDnsProvider(options)
|
||||
const recordList = await aliyunDnsProvider.getRecords('docmirror.cn', '*')
|
||||
console.log('recordList', recordList)
|
||||
expect(recordList.length).gt(0)
|
||||
})
|
||||
|
||||
it('#createAndRemoveRecord', async function () {
|
||||
const options = getPluginOptions()
|
||||
const aliyunDnsProvider = new AliyunDnsProvider(options)
|
||||
const record = await aliyunDnsProvider.createRecord({ fullRecord: '___certd___.__test__.docmirror.cn', type: 'TXT', value: 'aaaa' })
|
||||
console.log('recordId', record)
|
||||
expect(record != null).ok
|
||||
|
||||
const recordId = await aliyunDnsProvider.removeRecord({ fullRecord: '___certd___.__test__.docmirror.cn', type: 'TXT', value: 'aaaa', record })
|
||||
console.log('recordId', recordId)
|
||||
expect(recordId != null).ok
|
||||
})
|
||||
})
|
|
@ -1,42 +0,0 @@
|
|||
import _ from 'lodash-es'
|
||||
import optionsPrivate from '../../../test/options.private.mjs'
|
||||
const defaultOptions = {
|
||||
version: '1.0.0',
|
||||
args: {
|
||||
directory: 'test',
|
||||
dry: false
|
||||
},
|
||||
accessProviders: {
|
||||
aliyun: {
|
||||
providerType: 'aliyun',
|
||||
accessKeyId: '',
|
||||
accessKeySecret: ''
|
||||
},
|
||||
myLinux: {
|
||||
providerType: 'SSH',
|
||||
username: 'xxx',
|
||||
password: 'xxx',
|
||||
host: '1111.com',
|
||||
port: 22,
|
||||
publicKey: ''
|
||||
}
|
||||
},
|
||||
cert: {
|
||||
domains: ['*.docmirror.club', 'docmirror.club'],
|
||||
email: 'xiaojunnuo@qq.com',
|
||||
dnsProvider: { type: 'aliyun', accessProvider: 'aliyun' },
|
||||
certProvider: 'letsencrypt',
|
||||
csrInfo: {
|
||||
country: 'CN',
|
||||
state: 'GuangDong',
|
||||
locality: 'ShengZhen',
|
||||
organization: 'CertD Org.',
|
||||
organizationUnit: 'IT Department',
|
||||
emailAddress: 'xiaojunnuo@qq.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_.merge(defaultOptions, optionsPrivate)
|
||||
|
||||
export default defaultOptions
|
|
@ -1,67 +0,0 @@
|
|||
import pkg from 'chai'
|
||||
import { DeployCertToAliyunAckIngress } from '../../src/plugins/deploy-to-ack-ingress/index.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
import { K8sClient } from '@certd/plugin-common'
|
||||
|
||||
const { expect } = pkg
|
||||
|
||||
async function getOptions () {
|
||||
const options = createOptions()
|
||||
options.args.test = false
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['*.docmirror.cn']
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const context = {}
|
||||
const deployOpts = {
|
||||
accessProviders: options.accessProviders,
|
||||
cert,
|
||||
props: {
|
||||
accessProvider: 'aliyun-yonsz-prod',
|
||||
regionId: 'cn-shanghai',
|
||||
clusterId: 'c9e107ca518314f70973636965037fc00',
|
||||
secretName: 'default-ingress-secret1638601684896',
|
||||
namespace: 'default',
|
||||
ingressClass: 'nginx'
|
||||
},
|
||||
context
|
||||
}
|
||||
return { options, deployOpts }
|
||||
}
|
||||
|
||||
describe('DeployCertToAliyunAckIngressNginx', function () {
|
||||
it('#getAliyunSecrets', async function () {
|
||||
this.timeout(50000)
|
||||
const { options, deployOpts } = await getOptions()
|
||||
const plugin = new DeployCertToAliyunAckIngress(options)
|
||||
const ackClient = plugin.getClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.regionId)
|
||||
const kubeConfig = await plugin.getKubeConfig(ackClient, deployOpts.props.clusterId, false)
|
||||
|
||||
const k8sClient = new K8sClient(kubeConfig)
|
||||
const secrets = await k8sClient.getSecret({ namespace: 'default' })
|
||||
|
||||
console.log('secrets:', secrets)
|
||||
})
|
||||
it('#getAliyunIngreses', async function () {
|
||||
this.timeout(50000)
|
||||
const { options, deployOpts } = await getOptions()
|
||||
const plugin = new DeployCertToAliyunAckIngress(options)
|
||||
const ackClient = plugin.getClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.regionId)
|
||||
const kubeConfig = await plugin.getKubeConfig(ackClient, deployOpts.props.clusterId, false)
|
||||
|
||||
const k8sClient = new K8sClient(kubeConfig)
|
||||
const list = await k8sClient.getIngressList({ namespace: 'default' })
|
||||
|
||||
console.log('list:', list)
|
||||
})
|
||||
it('#execute', async function () {
|
||||
this.timeout(5000)
|
||||
|
||||
const { options, deployOpts } = await getOptions()
|
||||
const plugin = new DeployCertToAliyunAckIngress(options)
|
||||
|
||||
const ret = await plugin.doExecute(deployOpts)
|
||||
console.log('success', ret)
|
||||
})
|
||||
})
|
|
@ -1,21 +0,0 @@
|
|||
import pkg from 'chai'
|
||||
import { DeployCertToAliyunCDN } from '../../src/plugins/deploy-to-cdn/index.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
const { expect } = pkg
|
||||
|
||||
describe('DeployToAliyunCDN', function () {
|
||||
it('#execute', async function () {
|
||||
this.timeout(5000)
|
||||
const options = createOptions()
|
||||
const plugin = new DeployCertToAliyunCDN(options)
|
||||
options.cert.domains = ['*.docmirror.cn', 'docmirror.cn']
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const ret = await plugin.doExecute({
|
||||
cert,
|
||||
props: { domainName: 'certd-cdn-upload.docmirror.cn', certName: 'certd部署测试', from: 'cas', accessProvider: 'aliyun' }
|
||||
})
|
||||
console.log('context:', context, ret)
|
||||
})
|
||||
})
|
|
@ -1,27 +0,0 @@
|
|||
import pkg from 'chai'
|
||||
import { UploadCertToAliyun } from '../../src/plugins/upload-to-aliyun/index.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
const { expect } = pkg
|
||||
describe('PluginUploadToAliyun', function () {
|
||||
it('#execute', async function () {
|
||||
this.timeout(5000)
|
||||
const options = createOptions()
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['_.docmirror.cn']
|
||||
const plugin = new UploadCertToAliyun(options)
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const context = {}
|
||||
const deployOpts = {
|
||||
cert,
|
||||
props: { accessProvider: 'aliyun' },
|
||||
context
|
||||
}
|
||||
await plugin.doExecute(deployOpts)
|
||||
console.log('context:', context)
|
||||
|
||||
// await plugin.sleep(1000)
|
||||
// await plugin.rollback(deployOpts)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,9 @@
|
|||
import { AbstractAccess, IAccessService } from "@certd/pipeline";
|
||||
import { aliyunSecret } from "../user.secret";
|
||||
export class AccessServiceTest implements IAccessService {
|
||||
async getById(id: any): Promise<AbstractAccess> {
|
||||
return {
|
||||
...aliyunSecret,
|
||||
} as any;
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"extends": "standard",
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.test.js", "*.spec.js"],
|
||||
"rules": {
|
||||
"no-unused-expressions": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
.vscode/
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
/.idea/
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"name": "@certd/plugin-common",
|
||||
"version": "0.3.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/fast-crud.es.js",
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/api": "^0.3.0",
|
||||
"kubernetes-client": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/certd": "^0.3.0",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^7.15.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"mocha": "^8.2.1",
|
||||
"rollup": "^3.2.3"
|
||||
},
|
||||
"author": "Greper",
|
||||
"license": "MIT",
|
||||
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
export default {
|
||||
input: 'src/index.js',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/index.cjs',
|
||||
format: 'cjs'
|
||||
},
|
||||
{
|
||||
file: 'dist/index.es.js',
|
||||
format: 'es'
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { K8sClient } from './lib/k8s.client.js'
|
|
@ -1,114 +0,0 @@
|
|||
import kubernetesClient from 'kubernetes-client'
|
||||
import { util } from '@certd/api'
|
||||
import Request from 'kubernetes-client/backends/request/index.js'
|
||||
import dns from 'dns'
|
||||
const { KubeConfig, Client } = kubernetesClient
|
||||
const logger = util.logger
|
||||
|
||||
export class K8sClient {
|
||||
constructor (kubeConfigStr) {
|
||||
this.kubeConfigStr = kubeConfigStr
|
||||
this.init()
|
||||
}
|
||||
|
||||
init () {
|
||||
const kubeconfig = new KubeConfig()
|
||||
kubeconfig.loadFromString(this.kubeConfigStr)
|
||||
const reqOpts = { kubeconfig, request: {} }
|
||||
if (this.lookup) {
|
||||
reqOpts.request.lookup = this.lookup
|
||||
}
|
||||
|
||||
const backend = new Request(reqOpts)
|
||||
this.client = new Client({ backend, version: '1.13' })
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param localRecords { [domain]:{ip:'xxx.xx.xxx'} }
|
||||
*/
|
||||
setLookup (localRecords) {
|
||||
this.lookup = (hostnameReq, options, callback) => {
|
||||
logger.info('custom lookup', hostnameReq, localRecords)
|
||||
if (localRecords[hostnameReq]) {
|
||||
logger.info('local record', hostnameReq, localRecords[hostnameReq])
|
||||
callback(null, localRecords[hostnameReq].ip, 4)
|
||||
} else {
|
||||
dns.lookup(hostnameReq, options, callback)
|
||||
}
|
||||
}
|
||||
this.init()
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 secret列表
|
||||
* @param opts = {namespace:default}
|
||||
* @returns secretsList
|
||||
*/
|
||||
async getSecret (opts = {}) {
|
||||
const namespace = opts.namespace || 'default'
|
||||
const secrets = await this.client.api.v1.namespaces(namespace).secrets.get()
|
||||
return secrets
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Secret
|
||||
* @param opts {namespace:default, body:yamlStr}
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async createSecret (opts) {
|
||||
const namespace = opts.namespace || 'default'
|
||||
const created = await this.client.api.v1.namespaces(namespace).secrets.post({
|
||||
body: opts.body
|
||||
})
|
||||
logger.info('new secrets:', created)
|
||||
return created
|
||||
}
|
||||
|
||||
async updateSecret (opts) {
|
||||
const namespace = opts.namespace || 'default'
|
||||
const secretName = opts.secretName
|
||||
if (secretName == null) {
|
||||
throw new Error('secretName 不能为空')
|
||||
}
|
||||
return await this.client.api.v1.namespaces(namespace).secrets(secretName).put({
|
||||
body: opts.body
|
||||
})
|
||||
}
|
||||
|
||||
async patchSecret (opts) {
|
||||
const namespace = opts.namespace || 'default'
|
||||
const secretName = opts.secretName
|
||||
if (secretName == null) {
|
||||
throw new Error('secretName 不能为空')
|
||||
}
|
||||
return await this.client.api.v1.namespaces(namespace).secrets(secretName).patch({
|
||||
body: opts.body
|
||||
})
|
||||
}
|
||||
|
||||
async getIngressList (opts) {
|
||||
const namespace = opts.namespace || 'default'
|
||||
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses.get()
|
||||
}
|
||||
|
||||
async getIngress (opts) {
|
||||
const namespace = opts.namespace || 'default'
|
||||
const ingressName = opts.ingressName
|
||||
if (!ingressName) {
|
||||
throw new Error('ingressName 不能为空')
|
||||
}
|
||||
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).get()
|
||||
}
|
||||
|
||||
async patchIngress (opts) {
|
||||
const namespace = opts.namespace || 'default'
|
||||
const ingressName = opts.ingressName
|
||||
if (!ingressName) {
|
||||
throw new Error('ingressName 不能为空')
|
||||
}
|
||||
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).patch({
|
||||
body: opts.body
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,14 +1,21 @@
|
|||
{
|
||||
"extends": "standard",
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.test.js", "*.spec.js"],
|
||||
"rules": {
|
||||
"no-unused-expressions": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/ban-ts-ignore": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// "no-unused-expressions": "off",
|
||||
"max-len": [0, 160, 2, { "ignoreUrls": true }]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,26 @@
|
|||
.vscode/
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
/.idea/
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
test/user.secret.ts
|
|
@ -1,31 +1,45 @@
|
|||
{
|
||||
"name": "@certd/plugin-host",
|
||||
"version": "0.3.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/fast-crud.es.js",
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/api": "^0.3.0",
|
||||
"dayjs": "^1.9.7",
|
||||
"lodash-es": "^4.17.20",
|
||||
"ssh2": "^0.8.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/certd": "^0.3.0",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^7.15.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"mocha": "^8.2.1",
|
||||
"rollup": "^3.2.3"
|
||||
},
|
||||
"author": "Greper",
|
||||
"license": "MIT",
|
||||
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
|
||||
"name": "@certd/plugin-host",
|
||||
"private": true,
|
||||
"version": "0.3.0",
|
||||
"main": "./src/index.ts",
|
||||
"module": "./dist/plugin-aliyun.mjs",
|
||||
"types": "./dist/es/plugin-aliyun.d.ts",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/pipeline": "^0.3.0",
|
||||
"ssh2": "^0.8.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"log4js": "^6.3.0",
|
||||
"dayjs": "^1.9.7",
|
||||
"lodash-es": "^4.17.20",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"vue-tsc": "^0.38.9",
|
||||
"@alicloud/cs20151215": "^3.0.3",
|
||||
"@alicloud/openapi-client": "^0.4.0",
|
||||
"@alicloud/pop-core": "^1.7.10",
|
||||
"@midwayjs/core": "^3.0.0",
|
||||
"@midwayjs/decorator": "^3.0.0",
|
||||
"@types/chai": "^4.3.3",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/node-forge": "^1.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.1",
|
||||
"@typescript-eslint/parser": "^5.38.1",
|
||||
"chai": "^4.3.6",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"log4js": "^6.3.0",
|
||||
"mocha": "^10.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
export default {
|
||||
input: 'src/index.js',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/index.cjs',
|
||||
format: 'cjs'
|
||||
},
|
||||
{
|
||||
file: 'dist/index.es.js',
|
||||
format: 'es'
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
export class SSHAccessProvider {
|
||||
static define () {
|
||||
return {
|
||||
name: 'ssh',
|
||||
title: '主机',
|
||||
desc: '',
|
||||
input: {
|
||||
host: { rules: [{ required: true, message: '此项必填' }] },
|
||||
port: {
|
||||
title: '端口',
|
||||
value: '22',
|
||||
rules: [{ required: true, message: '此项必填' }]
|
||||
},
|
||||
username: {
|
||||
value: 'root',
|
||||
rules: [{ required: true, message: '此项必填' }]
|
||||
},
|
||||
password: { helper: '登录密码' },
|
||||
privateKey: {
|
||||
title: '密钥',
|
||||
helper: '密钥或密码必填一项'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import { SSHAccessProvider } from './access-providers/ssh.js'
|
||||
|
||||
import { UploadCertToHost } from './plugins/upload-to-host/index.js'
|
||||
import { HostShellExecute } from './plugins/host-shell-execute/index.js'
|
||||
|
||||
import { pluginRegistry, accessProviderRegistry } from '@certd/api'
|
||||
|
||||
export const DefaultPlugins = {
|
||||
UploadCertToHost,
|
||||
HostShellExecute
|
||||
}
|
||||
export default {
|
||||
install () {
|
||||
_.forEach(DefaultPlugins, item => {
|
||||
pluginRegistry.install(item)
|
||||
})
|
||||
|
||||
accessProviderRegistry.install(SSHAccessProvider)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
import ssh2 from "ssh2";
|
||||
import path from "path";
|
||||
import _ from "lodash";
|
||||
import { Logger } from "log4js";
|
||||
export class SshClient {
|
||||
logger: Logger;
|
||||
constructor(logger: Logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param connectConf
|
||||
{
|
||||
host: '192.168.100.100',
|
||||
port: 22,
|
||||
username: 'frylock',
|
||||
password: 'nodejsrules'
|
||||
}
|
||||
* @param options
|
||||
*/
|
||||
uploadFiles(options: { connectConf: any; transports: any; sudo: boolean }) {
|
||||
const { connectConf, transports, sudo } = options;
|
||||
const conn = new ssh2.Client();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
conn
|
||||
.on("ready", () => {
|
||||
this.logger.info("连接服务器成功");
|
||||
conn.sftp(async (err: Error, sftp: any) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const transport of transports) {
|
||||
this.logger.info("上传文件:", JSON.stringify(transport));
|
||||
const sudoCmd = sudo ? "sudo" : "";
|
||||
await this.exec({ connectConf, script: `${sudoCmd} mkdir -p ${path.dirname(transport.remotePath)} ` });
|
||||
await this.fastPut({ sftp, ...transport });
|
||||
}
|
||||
resolve({});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
} finally {
|
||||
conn.end();
|
||||
}
|
||||
});
|
||||
})
|
||||
.connect(connectConf);
|
||||
});
|
||||
}
|
||||
|
||||
exec(options: { connectConf: any; script: string | Array<string> }) {
|
||||
let { script } = options;
|
||||
const { connectConf } = options;
|
||||
if (_.isArray(script)) {
|
||||
script = script.join("\n");
|
||||
}
|
||||
this.logger.info("执行命令:", script);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connect({
|
||||
connectConf,
|
||||
onReady: (conn: any) => {
|
||||
conn.exec(script, (err: Error, stream: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
let data: any = null;
|
||||
stream
|
||||
.on("close", (code: any, signal: any) => {
|
||||
this.logger.info(`[${connectConf.host}][close]:code:${code}`);
|
||||
data = data ? data.toString() : null;
|
||||
if (code === 0) {
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(new Error(data));
|
||||
}
|
||||
conn.end();
|
||||
})
|
||||
.on("data", (ret: any) => {
|
||||
this.logger.info(`[${connectConf.host}][info]: ` + ret);
|
||||
data = ret;
|
||||
})
|
||||
.stderr.on("data", (err: Error) => {
|
||||
this.logger.info(`[${connectConf.host}][error]: ` + err);
|
||||
data = err;
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
shell(options: { connectConf: any; script: string }) {
|
||||
const { connectConf, script } = options;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connect({
|
||||
connectConf,
|
||||
onReady: (conn: any) => {
|
||||
conn.shell((err: Error, stream: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
const output: any = [];
|
||||
stream
|
||||
.on("close", () => {
|
||||
this.logger.info("Stream :: close");
|
||||
conn.end();
|
||||
resolve(output);
|
||||
})
|
||||
.on("data", (data: any) => {
|
||||
this.logger.info("" + data);
|
||||
output.push("" + data);
|
||||
});
|
||||
stream.end(script + "\nexit\n");
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
connect(options: { connectConf: any; onReady: any }) {
|
||||
const { connectConf, onReady } = options;
|
||||
const conn = new ssh2.Client();
|
||||
conn
|
||||
.on("ready", () => {
|
||||
this.logger.info("Client :: ready");
|
||||
onReady(conn);
|
||||
})
|
||||
.connect(connectConf);
|
||||
return conn;
|
||||
}
|
||||
|
||||
fastPut(options: { sftp: any; localPath: string; remotePath: string }) {
|
||||
const { sftp, localPath, remotePath } = options;
|
||||
return new Promise((resolve, reject) => {
|
||||
sftp.fastPut(localPath, remotePath, (err: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve({});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import { IsTask, TaskInput, TaskOutput, TaskPlugin, AbstractPlugin, RunStrategy } from "@certd/pipeline";
|
||||
import { SshClient } from "../../lib/ssh";
|
||||
|
||||
@IsTask(() => {
|
||||
return {
|
||||
name: "hostShellExecute",
|
||||
title: "执行远程主机脚本命令",
|
||||
input: {
|
||||
accessId: {
|
||||
title: "主机登录配置",
|
||||
helper: "登录",
|
||||
component: {
|
||||
name: "pi-access-selector",
|
||||
type: "ssh",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
cert: {
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "pi-output-selector",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
script: {
|
||||
title: "shell脚本命令",
|
||||
component: {
|
||||
name: "a-textarea",
|
||||
vModel: "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
output: {},
|
||||
};
|
||||
})
|
||||
export class HostShellExecutePlugin extends AbstractPlugin implements TaskPlugin {
|
||||
async execute(input: TaskInput): Promise<TaskOutput> {
|
||||
const { script, accessId } = input;
|
||||
const connectConf = this.accessService.getById(accessId);
|
||||
const sshClient = new SshClient(this.logger);
|
||||
const ret = await sshClient.exec({
|
||||
connectConf,
|
||||
script,
|
||||
});
|
||||
this.logger.info("exec res:", ret);
|
||||
return {};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import { IsTask, TaskInput, TaskOutput, TaskPlugin, AbstractPlugin, RunStrategy } from "@certd/pipeline";
|
||||
import { SshClient } from "../../lib/ssh";
|
||||
|
||||
@IsTask(() => {
|
||||
return {
|
||||
name: "uploadCertToHost",
|
||||
title: "上传证书到主机",
|
||||
input: {
|
||||
crtPath: {
|
||||
title: "证书保存路径",
|
||||
},
|
||||
keyPath: {
|
||||
title: "私钥保存路径",
|
||||
},
|
||||
cert: {
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "pi-output-selector",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
accessId: {
|
||||
title: "主机登录配置",
|
||||
helper: "access授权",
|
||||
component: {
|
||||
name: "pi-access-selector",
|
||||
type: "ssh",
|
||||
},
|
||||
rules: [{ required: true, message: "此项必填" }],
|
||||
},
|
||||
sudo: {
|
||||
title: "是否sudo",
|
||||
component: {
|
||||
name: "a-checkbox",
|
||||
vModel: "checked",
|
||||
},
|
||||
},
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
output: {
|
||||
hostCrtPath: {
|
||||
title: "上传成功后的证书路径",
|
||||
},
|
||||
hostKeyPath: {
|
||||
title: "上传成功后的私钥路径",
|
||||
},
|
||||
},
|
||||
};
|
||||
})
|
||||
export class UploadCertToHostPlugin extends AbstractPlugin implements TaskPlugin {
|
||||
async execute(input: TaskInput): Promise<TaskOutput> {
|
||||
const { crtPath, keyPath, cert, accessId, sudo } = input;
|
||||
const connectConf = this.accessService.getById(accessId);
|
||||
const sshClient = new SshClient(this.logger);
|
||||
await sshClient.uploadFiles({
|
||||
connectConf,
|
||||
transports: [
|
||||
{
|
||||
localPath: cert.crtPath,
|
||||
remotePath: crtPath,
|
||||
},
|
||||
{
|
||||
localPath: cert.keyPath,
|
||||
remotePath: keyPath,
|
||||
},
|
||||
],
|
||||
sudo,
|
||||
});
|
||||
this.logger.info("证书上传成功:crtPath=", crtPath, ",keyPath=", keyPath);
|
||||
|
||||
return {
|
||||
hostCrtPath: crtPath,
|
||||
hostKeyPath: keyPath,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { AbstractPlugin } from '@certd/api'
|
||||
|
||||
export class AbstractHostPlugin extends AbstractPlugin {
|
||||
checkRet (ret) {
|
||||
if (ret.code != null) {
|
||||
throw new Error('执行失败:', ret.Message)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
import { AbstractHostPlugin } from '../abstract-host.js'
|
||||
import { SshClient } from '../ssh.js'
|
||||
export class HostShellExecute extends AbstractHostPlugin {
|
||||
/**
|
||||
* 插件定义
|
||||
* 名称
|
||||
* 入参
|
||||
* 出参
|
||||
*/
|
||||
static define () {
|
||||
return {
|
||||
name: 'hostShellExecute',
|
||||
title: '执行远程主机脚本命令',
|
||||
input: {
|
||||
accessProvider: {
|
||||
title: '主机登录配置',
|
||||
helper: '登录',
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'ssh'
|
||||
},
|
||||
required: true
|
||||
},
|
||||
script: {
|
||||
title: 'shell脚本命令',
|
||||
component: {
|
||||
name: 'a-textarea'
|
||||
}
|
||||
}
|
||||
},
|
||||
output: {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async execute ({ cert, props, context }) {
|
||||
const { script, accessProvider } = props
|
||||
const connectConf = this.getAccessProvider(accessProvider)
|
||||
const sshClient = new SshClient()
|
||||
const ret = await sshClient.exec({
|
||||
connectConf,
|
||||
script
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cert
|
||||
* @param props
|
||||
* @param context
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async rollback ({ cert, props, context }) {
|
||||
|
||||
}
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
import ssh2 from 'ssh2'
|
||||
import path from 'path'
|
||||
import { util } from '@certd/api'
|
||||
import _ from 'lodash'
|
||||
const logger = util.logger
|
||||
export class SshClient {
|
||||
/**
|
||||
*
|
||||
* @param connectConf
|
||||
{
|
||||
host: '192.168.100.100',
|
||||
port: 22,
|
||||
username: 'frylock',
|
||||
password: 'nodejsrules'
|
||||
}
|
||||
* @param transports
|
||||
*/
|
||||
uploadFiles ({ connectConf, transports, sudo = false }) {
|
||||
const conn = new ssh2.Client()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
conn.on('ready', () => {
|
||||
logger.info('连接服务器成功')
|
||||
conn.sftp(async (err, sftp) => {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
|
||||
try {
|
||||
for (const transport of transports) {
|
||||
logger.info('上传文件:', JSON.stringify(transport))
|
||||
sudo = sudo ? 'sudo' : ''
|
||||
await this.exec({ connectConf, script: `${sudo} mkdir -p ${path.dirname(transport.remotePath)} ` })
|
||||
await this.fastPut({ sftp, ...transport })
|
||||
}
|
||||
resolve()
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
} finally {
|
||||
conn.end()
|
||||
}
|
||||
})
|
||||
}).connect(connectConf)
|
||||
})
|
||||
}
|
||||
|
||||
exec ({ connectConf, script }) {
|
||||
if (_.isArray(script)) {
|
||||
script = script.join('\n')
|
||||
}
|
||||
console.log('执行命令:', script)
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connect({
|
||||
connectConf,
|
||||
onReady: (conn) => {
|
||||
conn.exec(script, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
let data = null
|
||||
stream.on('close', (code, signal) => {
|
||||
console.log(`[${connectConf.host}][close]:code:${code}`)
|
||||
data = data ? data.toString() : null
|
||||
if (code === 0) {
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(new Error(data))
|
||||
}
|
||||
conn.end()
|
||||
}).on('data', (ret) => {
|
||||
console.log(`[${connectConf.host}][info]: ` + ret)
|
||||
data = ret
|
||||
}).stderr.on('data', (err) => {
|
||||
console.log(`[${connectConf.host}][error]: ` + err)
|
||||
data = err
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
shell ({ connectConf, script }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connect({
|
||||
connectConf,
|
||||
onReady: (conn) => {
|
||||
conn.shell((err, stream) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
const output = []
|
||||
stream.on('close', () => {
|
||||
logger.info('Stream :: close')
|
||||
conn.end()
|
||||
resolve(output)
|
||||
}).on('data', (data) => {
|
||||
logger.info('' + data)
|
||||
output.push('' + data)
|
||||
})
|
||||
stream.end(script + '\nexit\n')
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
connect ({ connectConf, onReady }) {
|
||||
const conn = new ssh2.Client()
|
||||
conn.on('ready', () => {
|
||||
console.log('Client :: ready')
|
||||
onReady(conn)
|
||||
}).connect(connectConf)
|
||||
return conn
|
||||
}
|
||||
|
||||
fastPut ({ sftp, localPath, remotePath }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
sftp.fastPut(localPath, remotePath, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
import { AbstractHostPlugin } from '../abstract-host.js'
|
||||
import { SshClient } from '../ssh.js'
|
||||
export class UploadCertToHost extends AbstractHostPlugin {
|
||||
/**
|
||||
* 插件定义
|
||||
* 名称
|
||||
* 入参
|
||||
* 出参
|
||||
*/
|
||||
static define () {
|
||||
return {
|
||||
name: 'uploadCertToHost',
|
||||
title: '上传证书到主机',
|
||||
input: {
|
||||
crtPath: {
|
||||
title: '证书保存路径'
|
||||
},
|
||||
keyPath: {
|
||||
title: '私钥保存路径'
|
||||
},
|
||||
accessProvider: {
|
||||
title: '主机登录配置',
|
||||
helper: 'access授权',
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'ssh'
|
||||
},
|
||||
rules: [{ required: true, message: '此项必填' }]
|
||||
},
|
||||
sudo: {
|
||||
title: '是否sudo',
|
||||
component: {
|
||||
name: 'a-checkbox',
|
||||
vModel: 'checked'
|
||||
}
|
||||
}
|
||||
},
|
||||
output: {
|
||||
hostCrtPath: {
|
||||
helper: '上传成功后的证书路径'
|
||||
},
|
||||
hostKeyPath: {
|
||||
helper: '上传成功后的私钥路径'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async execute ({ cert, props, context }) {
|
||||
const { crtPath, keyPath, accessProvider } = props
|
||||
const connectConf = this.getAccessProvider(accessProvider)
|
||||
const sshClient = new SshClient()
|
||||
await sshClient.uploadFiles({
|
||||
connectConf,
|
||||
transports: [
|
||||
{
|
||||
localPath: cert.crtPath,
|
||||
remotePath: crtPath
|
||||
},
|
||||
{
|
||||
localPath: cert.keyPath,
|
||||
remotePath: keyPath
|
||||
}
|
||||
]
|
||||
})
|
||||
this.logger.info('证书上传成功:crtPath=', crtPath, ',keyPath=', keyPath)
|
||||
|
||||
context.hostCrtPath = crtPath
|
||||
context.hostKeyPath = keyPath
|
||||
return {
|
||||
hostCrtPath: crtPath,
|
||||
hostKeyPath: keyPath
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cert
|
||||
* @param props
|
||||
* @param context
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async rollback ({ cert, props, context }) {
|
||||
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import _ from 'lodash-es'
|
||||
import optionsPrivate from '../../../test/options.private.mjs'
|
||||
const defaultOptions = {
|
||||
version: '1.0.0',
|
||||
args: {
|
||||
directory: 'test',
|
||||
dry: false
|
||||
},
|
||||
accessProviders: {
|
||||
aliyun: {
|
||||
providerType: 'aliyun',
|
||||
accessKeyId: '',
|
||||
accessKeySecret: ''
|
||||
},
|
||||
myLinux: {
|
||||
providerType: 'SSH',
|
||||
username: 'xxx',
|
||||
password: 'xxx',
|
||||
host: '1111.com',
|
||||
port: 22,
|
||||
publicKey: ''
|
||||
}
|
||||
},
|
||||
cert: {
|
||||
domains: ['*.docmirror.club', 'docmirror.club'],
|
||||
email: 'xiaojunnuo@qq.com',
|
||||
dnsProvider: 'aliyun',
|
||||
certProvider: 'letsencrypt',
|
||||
csrInfo: {
|
||||
country: 'CN',
|
||||
state: 'GuangDong',
|
||||
locality: 'ShengZhen',
|
||||
organization: 'CertD Org.',
|
||||
organizationUnit: 'IT Department',
|
||||
emailAddress: 'xiaojunnuo@qq.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_.merge(defaultOptions, optionsPrivate)
|
||||
|
||||
export default defaultOptions
|
|
@ -1,52 +0,0 @@
|
|||
import pkg from 'chai'
|
||||
import { HostShellExecute } from '../../src/plugins/host-shell-execute/index.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
const { expect } = pkg
|
||||
describe('HostShellExecute', function () {
|
||||
it('#execute', async function () {
|
||||
this.timeout(10000)
|
||||
const options = createOptions()
|
||||
options.args = { test: false }
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['*.docmirror.cn']
|
||||
const plugin = new HostShellExecute(options)
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const context = {}
|
||||
const uploadOpts = {
|
||||
cert,
|
||||
props: { script: ['ls ', 'ls '], accessProvider: 'aliyun-ssh' },
|
||||
context
|
||||
}
|
||||
const ret = await plugin.doExecute(uploadOpts)
|
||||
expect(ret).ok
|
||||
console.log('-----' + JSON.stringify(ret))
|
||||
})
|
||||
|
||||
it('#execute-hk-restart-docker', async function () {
|
||||
this.timeout(10000)
|
||||
const options = createOptions()
|
||||
const plugin = new HostShellExecute(options)
|
||||
const uploadOpts = {
|
||||
props: { script: ['cd /home/ubuntu/deloy/nginx-proxy\nsudo docker-compose build\nsudo docker-compose up -d\n'], accessProvider: 'aliyun-ssh-hk' },
|
||||
context: {}
|
||||
}
|
||||
const ret = await plugin.doExecute(uploadOpts)
|
||||
expect(ret).ok
|
||||
console.log('-----' + JSON.stringify(ret))
|
||||
})
|
||||
|
||||
it('#execute-publicKey-login', async function () {
|
||||
this.timeout(10000)
|
||||
const options = createOptions()
|
||||
const plugin = new HostShellExecute(options)
|
||||
const shellOpts = {
|
||||
props: { script: ['ls'], accessProvider: 'tencent-ssh-base01' },
|
||||
context: {}
|
||||
}
|
||||
const ret = await plugin.doExecute(shellOpts)
|
||||
expect(ret).ok
|
||||
console.log('-----' + JSON.stringify(ret))
|
||||
})
|
||||
})
|
|
@ -1,48 +0,0 @@
|
|||
import pkg from 'chai'
|
||||
import { UploadCertToHost } from '../../src/plugins/upload-to-host/index.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
const { expect } = pkg
|
||||
describe('PluginUploadToHost', function () {
|
||||
it('#execute', async function () {
|
||||
this.timeout(10000)
|
||||
const options = createOptions()
|
||||
options.args = { test: false }
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['*.docmirror.cn']
|
||||
const plugin = new UploadCertToHost(options)
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const context = {}
|
||||
const uploadOpts = {
|
||||
cert,
|
||||
props: { crtPath: '/root/certd/test/test.crt', keyPath: '/root/certd/test/test.key', accessProvider: 'aliyun-ssh' },
|
||||
context
|
||||
}
|
||||
await plugin.doExecute(uploadOpts)
|
||||
console.log('context:', context)
|
||||
|
||||
await plugin.doRollback(uploadOpts)
|
||||
})
|
||||
|
||||
it('#execute-to-ubantu', async function () {
|
||||
this.timeout(10000)
|
||||
const options = createOptions()
|
||||
options.args = { test: false }
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['*.docmirror.cn']
|
||||
const plugin = new UploadCertToHost(options)
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const context = {}
|
||||
const uploadOpts = {
|
||||
cert,
|
||||
props: { crtPath: '/home/ubuntu/deloy/nginx-proxy/ssl/test.crt', keyPath: '/home/ubuntu/deloy/nginx-proxy/ssl/test.key', accessProvider: 'aliyun-ssh-hk' },
|
||||
context
|
||||
}
|
||||
await plugin.doExecute(uploadOpts)
|
||||
console.log('context:', context)
|
||||
|
||||
await plugin.doRollback(uploadOpts)
|
||||
})
|
||||
})
|
|
@ -1,14 +1,21 @@
|
|||
{
|
||||
"extends": "standard",
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.test.js", "*.spec.js"],
|
||||
"rules": {
|
||||
"no-unused-expressions": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/ban-ts-ignore": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// "no-unused-expressions": "off",
|
||||
"max-len": [0, 160, 2, { "ignoreUrls": true }]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,26 @@
|
|||
.vscode/
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
/.idea/
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
test/user.secret.ts
|
|
@ -1,33 +1,47 @@
|
|||
{
|
||||
"name": "@certd/plugin-tencent",
|
||||
"version": "0.3.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/fast-crud.es.js",
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/api": "^0.3.0",
|
||||
"@certd/plugin-common": "^0.3.0",
|
||||
"dayjs": "^1.9.7",
|
||||
"kubernetes-client": "^9.0.0",
|
||||
"lodash-es": "^4.17.20",
|
||||
"tencentcloud-sdk-nodejs": "^4.0.44"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/certd": "^0.3.0",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^7.15.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"mocha": "^8.2.1",
|
||||
"rollup": "^3.2.3"
|
||||
},
|
||||
"author": "Greper",
|
||||
"license": "MIT",
|
||||
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
|
||||
"name": "@certd/plugin-tencent",
|
||||
"private": true,
|
||||
"version": "0.3.0",
|
||||
"main": "./src/index.ts",
|
||||
"module": "./dist/plugin-aliyun.mjs",
|
||||
"types": "./dist/es/plugin-aliyun.d.ts",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/pipeline": "^0.3.0",
|
||||
"@certd/plugin-util": "^0.3.0",
|
||||
"tencentcloud-sdk-nodejs": "^4.0.44"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"log4js": "^6.3.0",
|
||||
"dayjs": "^1.9.7",
|
||||
"lodash-es": "^4.17.20",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"vue-tsc": "^0.38.9",
|
||||
"@alicloud/cs20151215": "^3.0.3",
|
||||
"@alicloud/openapi-client": "^0.4.0",
|
||||
"@alicloud/pop-core": "^1.7.10",
|
||||
"@midwayjs/core": "^3.0.0",
|
||||
"@midwayjs/decorator": "^3.0.0",
|
||||
"@types/chai": "^4.3.3",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/node-forge": "^1.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.1",
|
||||
"@typescript-eslint/parser": "^5.38.1",
|
||||
"chai": "^4.3.6",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"log4js": "^6.3.0",
|
||||
"mocha": "^10.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
export default {
|
||||
input: 'src/index.js',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/index.cjs',
|
||||
format: 'cjs'
|
||||
},
|
||||
{
|
||||
file: 'dist/index.es.js',
|
||||
format: 'es'
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
export class DnspodAccessProvider {
|
||||
static define () {
|
||||
return {
|
||||
name: 'dnspod',
|
||||
title: 'dnspod',
|
||||
desc: '腾讯云的域名解析接口已迁移到dnspod',
|
||||
input: {
|
||||
id: {
|
||||
component: {
|
||||
placeholder: 'dnspod接口账户id'
|
||||
},
|
||||
rules: [{ required: true, message: '该项必填' }]
|
||||
},
|
||||
token: {
|
||||
title: 'token',
|
||||
component: {
|
||||
placeholder: '开放接口token'
|
||||
},
|
||||
rules: [{ required: true, message: '该项必填' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
export class TencentAccessProvider {
|
||||
static define () {
|
||||
return {
|
||||
name: 'tencent',
|
||||
title: '腾讯云',
|
||||
input: {
|
||||
secretId: {
|
||||
component: {
|
||||
placeholder: 'secretId'
|
||||
},
|
||||
rules: [{ required: true, message: '该项必填' }]
|
||||
},
|
||||
secretKey: {
|
||||
component: {
|
||||
placeholder: 'secretKey'
|
||||
},
|
||||
rules: [{ required: true, message: '该项必填' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { AbstractAccess, IsAccess } from "@certd/pipeline";
|
||||
|
||||
@IsAccess({
|
||||
name: "tencent",
|
||||
title: "腾讯云",
|
||||
input: {
|
||||
secretId: {
|
||||
title: "secretId",
|
||||
component: {
|
||||
placeholder: "secretId",
|
||||
},
|
||||
rules: [{ required: true, message: "该项必填" }],
|
||||
},
|
||||
secretKey: {
|
||||
title: "secretKey",
|
||||
component: {
|
||||
placeholder: "secretKey",
|
||||
},
|
||||
rules: [{ required: true, message: "该项必填" }],
|
||||
},
|
||||
},
|
||||
})
|
||||
export class TencentAccess extends AbstractAccess {
|
||||
secretId = "";
|
||||
secretKey = "";
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
import { AbstractDnsProvider, CreateRecordOptions, IDnsProvider, IsDnsProvider, RemoveRecordOptions } from "@certd/pipeline";
|
||||
import _ from "lodash";
|
||||
import { DnspodAccess } from "../access";
|
||||
|
||||
@IsDnsProvider({
|
||||
name: "dnspod",
|
||||
title: "dnspod(腾讯云)",
|
||||
desc: "腾讯云的域名解析接口已迁移到dnspod",
|
||||
accessType: "dnspod",
|
||||
})
|
||||
export class DnspodDnsProvider extends AbstractDnsProvider implements IDnsProvider {
|
||||
loginToken: any;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
async onInit() {
|
||||
const access: DnspodAccess = this.access as DnspodAccess;
|
||||
this.loginToken = access.id + "," + access.token;
|
||||
}
|
||||
|
||||
async doRequest(options: any, successCodes: string[] = []) {
|
||||
const config: any = {
|
||||
// @ts-ignore
|
||||
method: "post",
|
||||
formData: {
|
||||
login_token: this.loginToken,
|
||||
format: "json",
|
||||
lang: "cn",
|
||||
error_on_empty: "no",
|
||||
},
|
||||
timeout: 5000,
|
||||
};
|
||||
_.merge(config, options);
|
||||
|
||||
const ret: any = await this.http.request(config);
|
||||
if (!ret || !ret.status) {
|
||||
const code = ret.status.code;
|
||||
if (code !== "1" || !successCodes.includes(code)) {
|
||||
throw new Error("请求失败:" + ret.status.message + ",api=" + config.url);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
async getDomainList() {
|
||||
const ret = await this.doRequest({
|
||||
url: "https://dnsapi.cn/Domain.List",
|
||||
});
|
||||
this.logger.debug("dnspod 域名列表:", ret.domains);
|
||||
return ret.domains;
|
||||
}
|
||||
|
||||
async createRecord(options: CreateRecordOptions): Promise<any> {
|
||||
const { fullRecord, value, type } = options;
|
||||
this.logger.info("添加域名解析:", fullRecord, value);
|
||||
const domainItem = await this.matchDomain(fullRecord);
|
||||
const domain = domainItem.name;
|
||||
const rr = fullRecord.replace("." + domain, "");
|
||||
|
||||
const ret = await this.doRequest(
|
||||
{
|
||||
url: "https://dnsapi.cn/Record.Create",
|
||||
formData: {
|
||||
domain,
|
||||
sub_domain: rr,
|
||||
record_type: type,
|
||||
record_line: "默认",
|
||||
value: value,
|
||||
mx: 1,
|
||||
},
|
||||
},
|
||||
["104"]
|
||||
); // 104错误码为记录已存在,无需再次添加
|
||||
this.logger.info("添加域名解析成功:", fullRecord, value, JSON.stringify(ret.record));
|
||||
return ret.record;
|
||||
}
|
||||
|
||||
async removeRecord(options: RemoveRecordOptions) {
|
||||
const { fullRecord, value, record } = options;
|
||||
const domain = await this.matchDomain(fullRecord);
|
||||
|
||||
const ret = await this.doRequest({
|
||||
url: "https://dnsapi.cn/Record.Remove",
|
||||
formData: {
|
||||
domain,
|
||||
record_id: record.id,
|
||||
},
|
||||
});
|
||||
this.logger.info("删除域名解析成功:", fullRecord, value);
|
||||
return ret.RecordId;
|
||||
}
|
||||
|
||||
async matchDomain(dnsRecord: any) {
|
||||
const list = await this.getDomainList();
|
||||
let domain = null;
|
||||
for (const item of list) {
|
||||
if (_.endsWith(dnsRecord, item.name)) {
|
||||
domain = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!domain) {
|
||||
throw new Error("找不到域名,请检查域名是否正确:" + dnsRecord);
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
import "./dnspod-dns-provider";
|
|
@ -1,96 +0,0 @@
|
|||
import { AbstractDnsProvider, util } from '@certd/api'
|
||||
import _ from 'lodash'
|
||||
const request = util.request
|
||||
export class DnspodDnsProvider extends AbstractDnsProvider {
|
||||
static define () {
|
||||
return {
|
||||
name: 'dnspod',
|
||||
title: 'dnspod(腾讯云)',
|
||||
desc: '腾讯云的域名解析接口已迁移到dnspod',
|
||||
input: {
|
||||
accessProvider: {
|
||||
title: '授权',
|
||||
helper: '需要dnspod类型的授权',
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'dnspod'
|
||||
},
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor (args) {
|
||||
super(args)
|
||||
const { props } = args
|
||||
const accessProvider = this.getAccessProvider(props.accessProvider)
|
||||
this.loginToken = accessProvider.id + ',' + accessProvider.token
|
||||
}
|
||||
|
||||
async doRequest (options, successCodes = []) {
|
||||
const config = {
|
||||
method: 'post',
|
||||
formData: {
|
||||
login_token: this.loginToken,
|
||||
format: 'json',
|
||||
lang: 'cn',
|
||||
error_on_empty: 'no'
|
||||
},
|
||||
timeout: 5000
|
||||
}
|
||||
_.merge(config, options)
|
||||
|
||||
const ret = await request(config)
|
||||
if (!ret || !ret.status) {
|
||||
const code = ret.status.code
|
||||
if (code !== '1' || !successCodes.includes(code)) {
|
||||
throw new Error('请求失败:' + ret.status.message + ',api=' + config.url)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
async getDomainList () {
|
||||
const ret = await this.doRequest({
|
||||
url: 'https://dnsapi.cn/Domain.List'
|
||||
})
|
||||
this.logger.debug('dnspod 域名列表:', ret.domains)
|
||||
return ret.domains
|
||||
}
|
||||
|
||||
async createRecord ({ fullRecord, type, value }) {
|
||||
this.logger.info('添加域名解析:', fullRecord, value)
|
||||
const domainItem = await this.matchDomain(fullRecord, 'name')
|
||||
const domain = domainItem.name
|
||||
const rr = fullRecord.replace('.' + domain, '')
|
||||
|
||||
const ret = await this.doRequest({
|
||||
url: 'https://dnsapi.cn/Record.Create',
|
||||
formData: {
|
||||
domain,
|
||||
sub_domain: rr,
|
||||
record_type: type,
|
||||
record_line: '默认',
|
||||
value: value,
|
||||
mx: 1
|
||||
}
|
||||
}, ['104'])// 104错误码为记录已存在,无需再次添加
|
||||
this.logger.info('添加域名解析成功:', fullRecord, value, JSON.stringify(ret.record))
|
||||
return ret.record
|
||||
}
|
||||
|
||||
async removeRecord ({ fullRecord, type, value, record }) {
|
||||
const domain = await this.matchDomain(fullRecord, 'name')
|
||||
|
||||
const ret = await this.doRequest({
|
||||
url: 'https://dnsapi.cn/Record.Remove',
|
||||
formData: {
|
||||
domain,
|
||||
record_id: record.id
|
||||
}
|
||||
})
|
||||
this.logger.info('删除域名解析成功:', fullRecord, value)
|
||||
return ret.RecordId
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import { TencentAccessProvider } from './access-providers/tencent.js'
|
||||
import { DnspodAccessProvider } from './access-providers/dnspod.js'
|
||||
import { DnspodDnsProvider } from './dns-providers/dnspod.js'
|
||||
|
||||
import { UploadCertToTencent } from './plugins/upload-to-tencent/index.js'
|
||||
|
||||
import { DeployCertToTencentCDN } from './plugins/deploy-to-cdn/index.js'
|
||||
|
||||
import { DeployCertToTencentCLB } from './plugins/deploy-to-clb/index.js'
|
||||
|
||||
import { DeployCertToTencentTKEIngress } from './plugins/deploy-to-tke-ingress/index.js'
|
||||
|
||||
import { pluginRegistry, accessProviderRegistry, dnsProviderRegistry } from '@certd/api'
|
||||
|
||||
export const DefaultPlugins = {
|
||||
UploadCertToTencent,
|
||||
DeployCertToTencentTKEIngress,
|
||||
DeployCertToTencentCDN,
|
||||
DeployCertToTencentCLB
|
||||
}
|
||||
export default {
|
||||
install () {
|
||||
_.forEach(DefaultPlugins, item => {
|
||||
pluginRegistry.install(item)
|
||||
})
|
||||
|
||||
accessProviderRegistry.install(TencentAccessProvider)
|
||||
accessProviderRegistry.install(DnspodAccessProvider)
|
||||
|
||||
dnsProviderRegistry.install(DnspodDnsProvider)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
import { AbstractPlugin } from '@certd/api'
|
||||
|
||||
export class AbstractTencentPlugin extends AbstractPlugin {
|
||||
checkRet (ret) {
|
||||
if (!ret || ret.Error) {
|
||||
throw new Error('执行失败:' + ret.Error.Code + ',' + ret.Error.Message)
|
||||
}
|
||||
}
|
||||
|
||||
getSafetyDomain (domain) {
|
||||
return domain.replace(/\*/g, '_')
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
import { AbstractTencentPlugin } from '../abstract-tencent.js'
|
||||
import dayjs from 'dayjs'
|
||||
import tencentcloud from 'tencentcloud-sdk-nodejs'
|
||||
|
||||
export class DeployCertToTencentCDN extends AbstractTencentPlugin {
|
||||
/**
|
||||
* 插件定义
|
||||
* 名称
|
||||
* 入参
|
||||
* 出参
|
||||
*/
|
||||
static define () {
|
||||
return {
|
||||
name: 'deployCertToTencentCDN',
|
||||
title: '部署到腾讯云CDN',
|
||||
input: {
|
||||
domainName: {
|
||||
title: 'cdn加速域名',
|
||||
rules: [{ required: true, message: '该项必填' }]
|
||||
},
|
||||
certName: {
|
||||
title: '证书名称',
|
||||
helper: '证书上传后将以此参数作为名称前缀'
|
||||
},
|
||||
accessProvider: {
|
||||
title: 'Access提供者',
|
||||
helper: 'access 授权',
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'tencent'
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
output: {
|
||||
tencentCertId: {
|
||||
type: String,
|
||||
desc: '证书来源选择上传时,将返回此id'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async execute ({ cert, props, context }) {
|
||||
const accessProvider = this.getAccessProvider(props.accessProvider)
|
||||
const client = this.getClient(accessProvider)
|
||||
const params = this.buildParams(props, context, cert)
|
||||
await this.doRequest(client, params)
|
||||
}
|
||||
|
||||
async rollback ({ cert, props, context }) {
|
||||
|
||||
}
|
||||
|
||||
getClient (accessProvider) {
|
||||
const CdnClient = tencentcloud.cdn.v20180606.Client
|
||||
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: accessProvider.secretId,
|
||||
secretKey: accessProvider.secretKey
|
||||
},
|
||||
region: '',
|
||||
profile: {
|
||||
httpProfile: {
|
||||
endpoint: 'cdn.tencentcloudapi.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new CdnClient(clientConfig)
|
||||
}
|
||||
|
||||
buildParams (props, context, cert) {
|
||||
const { domainName, from } = props
|
||||
const { tencentCertId } = context
|
||||
this.logger.info('部署腾讯云证书ID:', tencentCertId)
|
||||
const params = {
|
||||
Https: {
|
||||
Switch: 'on',
|
||||
CertInfo: {
|
||||
CertId: tencentCertId
|
||||
// Certificate: '1231',
|
||||
// PrivateKey: '1231'
|
||||
}
|
||||
},
|
||||
Domain: domainName
|
||||
}
|
||||
if (from === 'upload' || tencentCertId == null) {
|
||||
params.Https.CertInfo = {
|
||||
Certificate: cert.crt,
|
||||
PrivateKey: cert.key
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
async doRequest (client, params) {
|
||||
const ret = await client.UpdateDomainConfig(params)
|
||||
this.checkRet(ret)
|
||||
this.logger.info('设置腾讯云CDN证书成功:', ret.RequestId)
|
||||
return ret.RequestId
|
||||
}
|
||||
}
|
|
@ -1,198 +0,0 @@
|
|||
import { AbstractTencentPlugin } from '../abstract-tencent.js'
|
||||
import tencentcloud from 'tencentcloud-sdk-nodejs'
|
||||
export class DeployCertToTencentCLB extends AbstractTencentPlugin {
|
||||
/**
|
||||
* 插件定义
|
||||
* 名称
|
||||
* 入参
|
||||
* 出参
|
||||
*/
|
||||
static define () {
|
||||
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: '证书名称前缀'
|
||||
},
|
||||
accessProvider: {
|
||||
title: 'Access提供者',
|
||||
helper: 'access授权',
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'tencent'
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
output: {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async execute ({ cert, props, context }) {
|
||||
const accessProvider = this.getAccessProvider(props.accessProvider)
|
||||
const { region } = props
|
||||
const client = this.getClient(accessProvider, region)
|
||||
|
||||
const lastCertId = await this.getCertIdFromProps(client, props)
|
||||
if (!props.domain) {
|
||||
await this.updateListener(client, cert, props, context)
|
||||
} else {
|
||||
await this.updateByDomainAttr(client, cert, props, context)
|
||||
}
|
||||
|
||||
try {
|
||||
await this.sleep(2000)
|
||||
let newCertId = await this.getCertIdFromProps(client, props)
|
||||
if ((lastCertId && newCertId === lastCertId) || (!lastCertId && !newCertId)) {
|
||||
await this.sleep(2000)
|
||||
newCertId = await this.getCertIdFromProps(client, props)
|
||||
}
|
||||
if (newCertId === lastCertId) {
|
||||
return {}
|
||||
}
|
||||
this.logger.info('腾讯云证书ID:', newCertId)
|
||||
if (!context.tencentCertId) {
|
||||
context.tencentCertId = newCertId
|
||||
}
|
||||
return { tencentCertId: newCertId }
|
||||
} catch (e) {
|
||||
this.logger.warn('查询腾讯云证书失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
async getCertIdFromProps (client, props) {
|
||||
const listenerRet = await this.getListenerList(client, props.loadBalancerId, [props.listenerId])
|
||||
return this.getCertIdFromListener(listenerRet[0], props.domain)
|
||||
}
|
||||
|
||||
getCertIdFromListener (listener, domain) {
|
||||
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 rollback ({ cert, props, context }) {
|
||||
this.logger.warn('未实现rollback')
|
||||
}
|
||||
|
||||
async updateListener (client, cert, props, context) {
|
||||
const params = this.buildProps(props, context, cert)
|
||||
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, cert, props, context) {
|
||||
const params = this.buildProps(props, context, cert)
|
||||
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
|
||||
}
|
||||
|
||||
buildProps (props, context, cert) {
|
||||
const { certName } = props
|
||||
const { tencentCertId } = context
|
||||
this.logger.info('部署腾讯云证书ID:', tencentCertId)
|
||||
const params = {
|
||||
Certificate: {
|
||||
SSLMode: 'UNIDIRECTIONAL', // 单向认证
|
||||
CertId: tencentCertId
|
||||
},
|
||||
LoadBalancerId: props.loadBalancerId,
|
||||
ListenerId: props.listenerId
|
||||
}
|
||||
|
||||
if (tencentCertId == null) {
|
||||
params.Certificate.CertName = this.appendTimeSuffix(certName || cert.domain)
|
||||
params.Certificate.CertKey = cert.key
|
||||
params.Certificate.CertContent = cert.crt
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
async getCLBList (client, props) {
|
||||
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, balancerId, listenerIds) {
|
||||
// HTTPS
|
||||
const params = {
|
||||
LoadBalancerId: balancerId,
|
||||
Protocol: 'HTTPS',
|
||||
ListenerIds: listenerIds
|
||||
}
|
||||
const ret = await client.DescribeListeners(params)
|
||||
this.checkRet(ret)
|
||||
return ret.Listeners
|
||||
}
|
||||
|
||||
getClient (accessProvider, region) {
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -1,213 +0,0 @@
|
|||
import { AbstractTencentPlugin } from '../abstract-tencent.js'
|
||||
import tencentcloud from 'tencentcloud-sdk-nodejs'
|
||||
import { K8sClient } from '@certd/plugin-common'
|
||||
export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin {
|
||||
/**
|
||||
* 插件定义
|
||||
* 名称
|
||||
* 入参
|
||||
* 出参
|
||||
*/
|
||||
static define () {
|
||||
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'
|
||||
},
|
||||
/**
|
||||
* AccessProvider的key,或者一个包含access的具体的对象
|
||||
*/
|
||||
accessProvider: {
|
||||
title: 'Access授权',
|
||||
helper: 'access授权',
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'tencent'
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
output: {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async execute ({ cert, props, context }) {
|
||||
const accessProvider = this.getAccessProvider(props.accessProvider)
|
||||
const tkeClient = this.getTkeClient(accessProvider, props.region)
|
||||
const kubeConfigStr = await this.getTkeKubeConfig(tkeClient, props.clusterId)
|
||||
|
||||
this.logger.info('kubeconfig已成功获取')
|
||||
const k8sClient = new K8sClient(kubeConfigStr)
|
||||
if (props.clusterIp != null) {
|
||||
let clusterDomain = props.clusterDomain
|
||||
if (!clusterDomain) {
|
||||
clusterDomain = `${props.clusterId}.ccs.tencent-cloud.com`
|
||||
}
|
||||
// 修改内网解析ip地址
|
||||
k8sClient.setLookup({ [clusterDomain]: { ip: props.clusterIp } })
|
||||
}
|
||||
const ingressType = props.ingressClass || 'qcloud'
|
||||
if (ingressType === 'qcloud') {
|
||||
await this.patchQcloudCertSecret({ k8sClient, props, context })
|
||||
} else {
|
||||
await this.patchNginxCertSecret({ cert, k8sClient, props, context })
|
||||
}
|
||||
|
||||
await this.sleep(2000) // 停留2秒,等待secret部署完成
|
||||
await this.restartIngress({ k8sClient, props })
|
||||
return true
|
||||
}
|
||||
|
||||
getTkeClient (accessProvider, 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, clusterId) {
|
||||
// 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
|
||||
}
|
||||
|
||||
async patchQcloudCertSecret ({ k8sClient, props, context }) {
|
||||
const { tencentCertId } = context
|
||||
if (tencentCertId == null) {
|
||||
throw new Error('请先将【上传证书到腾讯云】作为前置任务')
|
||||
}
|
||||
this.logger.info('腾讯云证书ID:', tencentCertId)
|
||||
const certIdBase64 = Buffer.from(tencentCertId).toString('base64')
|
||||
|
||||
const { namespace, secretName } = props
|
||||
|
||||
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 k8sClient.patchSecret({ namespace, secretName: secret, body })
|
||||
this.logger.info(`CertSecret已更新:${secret}`)
|
||||
}
|
||||
}
|
||||
|
||||
async patchNginxCertSecret ({ cert, k8sClient, props, context }) {
|
||||
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 } = props
|
||||
|
||||
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 ({ k8sClient, props }) {
|
||||
const { namespace, ingressName } = props
|
||||
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
import tencentcloud from 'tencentcloud-sdk-nodejs'
|
||||
import { AbstractTencentPlugin } from '../abstract-tencent.js'
|
||||
|
||||
export class UploadCertToTencent extends AbstractTencentPlugin {
|
||||
/**
|
||||
* 插件定义
|
||||
* 名称
|
||||
* 入参
|
||||
* 出参
|
||||
*/
|
||||
static define () {
|
||||
return {
|
||||
name: 'uploadCertToTencent',
|
||||
title: '上传证书到腾讯云',
|
||||
desc: '成功后获取,tencentCertId',
|
||||
input: {
|
||||
name: {
|
||||
title: '证书名称'
|
||||
},
|
||||
accessProvider: {
|
||||
title: 'Access授权',
|
||||
helper: 'access授权',
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'tencent'
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
output: {
|
||||
tencentCertId: {
|
||||
type: String,
|
||||
desc: '上传成功后的腾讯云CertId'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getClient (accessProvider) {
|
||||
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 execute ({ cert, props, context, logger }) {
|
||||
const { name, accessProvider } = props
|
||||
const certName = this.appendTimeSuffix(name || cert.domain)
|
||||
|
||||
const provider = this.getAccessProvider(accessProvider)
|
||||
const client = this.getClient(provider)
|
||||
|
||||
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)
|
||||
context.tencentCertId = ret.CertificateId
|
||||
}
|
||||
|
||||
async rollback ({ cert, props, context }) {
|
||||
const { accessProvider } = props
|
||||
const provider = super.getAccessProvider(accessProvider)
|
||||
const client = this.getClient(provider)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import pkg from 'chai'
|
||||
import PluginTencent from '../../src/index.js'
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
const { expect } = pkg
|
||||
|
||||
// 安装默认插件和授权提供者
|
||||
PluginTencent.install()
|
||||
describe('DnspodDnsProvider', function () {
|
||||
it('#申请证书', async function () {
|
||||
this.timeout(300000)
|
||||
const options = createOptions()
|
||||
options.cert.domains = ['*.certd.xyz', '*.test.certd.xyz', '*.base.certd.xyz', 'certd.xyz']
|
||||
options.cert.dnsProvider = {
|
||||
type: 'dnspod',
|
||||
accessProvider: 'dnspod'
|
||||
}
|
||||
options.args = { forceCert: true }
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.certApply()
|
||||
expect(cert).ok
|
||||
expect(cert.crt).ok
|
||||
expect(cert.key).ok
|
||||
expect(cert.detail).ok
|
||||
expect(cert.expires).ok
|
||||
})
|
||||
})
|
|
@ -1,35 +0,0 @@
|
|||
import pkg from 'chai'
|
||||
import { DnspodDnsProvider } from '../../src/dns-providers/dnspod.js'
|
||||
import { createOptions, getDnsProviderOptions } from '../../../../../test/options.js'
|
||||
const { expect } = pkg
|
||||
describe('DnspodDnsProvider', function () {
|
||||
it('#getDomainList', async function () {
|
||||
let options = createOptions()
|
||||
options.cert.dnsProvider = {
|
||||
type: 'dnspod',
|
||||
accessProvider: 'dnspod'
|
||||
}
|
||||
options = getDnsProviderOptions(options)
|
||||
|
||||
const dnsProvider = new DnspodDnsProvider(options)
|
||||
const domainList = await dnsProvider.getDomainList()
|
||||
console.log('domainList', domainList)
|
||||
expect(domainList.length).gt(0)
|
||||
})
|
||||
|
||||
it('#createRecord&removeRecord', async function () {
|
||||
let options = createOptions()
|
||||
options.cert.dnsProvider = {
|
||||
type: 'dnspod',
|
||||
accessProvider: 'dnspod'
|
||||
}
|
||||
options = getDnsProviderOptions(options)
|
||||
|
||||
const dnsProvider = new DnspodDnsProvider(options)
|
||||
const record = await dnsProvider.createRecord({ fullRecord: '___certd___.__test__.certd.xyz', type: 'TXT', value: 'aaaa' })
|
||||
console.log('recordId', record.id)
|
||||
expect(record.id != null).ok
|
||||
|
||||
await dnsProvider.removeRecord({ fullRecord: '___certd___.__test__.certd.xyz', type: 'TXT', value: 'aaaa', record })
|
||||
})
|
||||
})
|
|
@ -1,52 +0,0 @@
|
|||
import pkg from 'chai'
|
||||
import { DeployCertToTencentCDN } from '../../src/plugins/deploy-to-cdn/index.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
import { UploadCertToTencent } from '../../src/plugins/upload-to-tencent/index.js'
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
const { expect } = pkg
|
||||
describe('DeployToTencentCDN', function () {
|
||||
it('#execute-from-store', async function () {
|
||||
const options = createOptions()
|
||||
options.args.test = false
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert('xiaojunnuo@qq.com', ['*.docmirror.cn'])
|
||||
const context = {}
|
||||
const uploadPlugin = new UploadCertToTencent(options)
|
||||
const uploadOptions = {
|
||||
cert,
|
||||
props: { name: 'certd部署测试', accessProvider: 'tencent' },
|
||||
context
|
||||
}
|
||||
await uploadPlugin.doExecute(uploadOptions)
|
||||
|
||||
const deployPlugin = new DeployCertToTencentCDN(options)
|
||||
const deployOpts = {
|
||||
cert,
|
||||
props: { domainName: 'tentcent-certd.docmirror.cn', certName: 'certd部署测试', accessProvider: 'tencent' },
|
||||
context
|
||||
}
|
||||
await deployPlugin.doExecute(deployOpts)
|
||||
console.log('context:', context)
|
||||
expect(context.tencentCertId).ok
|
||||
|
||||
await uploadPlugin.doRollback(uploadOptions)
|
||||
})
|
||||
it('#execute-upload', async function () {
|
||||
const options = createOptions()
|
||||
options.args.test = false
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['*.docmirror.cn']
|
||||
const plugin = new DeployCertToTencentCDN(options)
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const context = {}
|
||||
const deployOpts = {
|
||||
cert,
|
||||
props: { domainName: 'tentcent-certd.docmirror.cn', accessProvider: 'tencent' },
|
||||
context
|
||||
}
|
||||
const ret = await plugin.doExecute(deployOpts)
|
||||
console.log('context:', context, ret)
|
||||
expect(context).be.empty
|
||||
})
|
||||
})
|
|
@ -1,105 +0,0 @@
|
|||
import pkg from 'chai'
|
||||
import { DeployCertToTencentCLB } from '../../src/plugins/deploy-to-clb/index.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
import { UploadCertToTencent } from '../../src/plugins/upload-to-tencent/index.js'
|
||||
const { expect } = pkg
|
||||
describe('DeployToTencentCLB', function () {
|
||||
it('#execute-getClbList', async function () {
|
||||
const options = createOptions()
|
||||
options.args.test = false
|
||||
options.cert.dnsProvider = 'tencent-yonsz'
|
||||
const deployPlugin = new DeployCertToTencentCLB(options)
|
||||
const props = {
|
||||
region: 'ap-guangzhou',
|
||||
domain: 'certd-test-no-sni.base.yonsz.net',
|
||||
accessProvider: 'tencent-yonsz'
|
||||
}
|
||||
const accessProvider = deployPlugin.getAccessProvider(props.accessProvider)
|
||||
const { region } = props
|
||||
const client = deployPlugin.getClient(accessProvider, region)
|
||||
|
||||
const ret = await deployPlugin.getCLBList(client, props)
|
||||
expect(ret.length > 0).ok
|
||||
console.log('clb count:', ret.length)
|
||||
})
|
||||
it('#execute-getListenerList', async function () {
|
||||
const options = createOptions()
|
||||
options.args.test = false
|
||||
options.cert.dnsProvider = 'tencent-yonsz'
|
||||
const deployPlugin = new DeployCertToTencentCLB(options)
|
||||
const props = {
|
||||
region: 'ap-guangzhou',
|
||||
domain: 'certd-test-no-sni.base.yonsz.net',
|
||||
accessProvider: 'tencent-yonsz',
|
||||
loadBalancerId: 'lb-59yhe5xo',
|
||||
listenerId: 'lbl-1vfwx8dq'
|
||||
}
|
||||
const accessProvider = deployPlugin.getAccessProvider(props.accessProvider)
|
||||
const { region } = props
|
||||
const client = deployPlugin.getClient(accessProvider, region)
|
||||
|
||||
const ret = await deployPlugin.getListenerList(client, props.loadBalancerId, [props.listenerId])
|
||||
expect(ret.length > 0).ok
|
||||
console.log('clb count:', ret.length, ret)
|
||||
})
|
||||
|
||||
it('#execute-no-sni-listenerId', async function () {
|
||||
this.timeout(10000)
|
||||
const options = createOptions()
|
||||
options.args.test = false
|
||||
options.cert.dnsProvider = 'tencent-yonsz'
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['*.docmirror.cn']
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const deployPlugin = new DeployCertToTencentCLB(options)
|
||||
const context = {}
|
||||
const deployOpts = {
|
||||
cert,
|
||||
props: {
|
||||
region: 'ap-guangzhou',
|
||||
loadBalancerId: 'lb-59yhe5xo',
|
||||
listenerId: 'lbl-1vfwx8dq',
|
||||
accessProvider: 'tencent-yonsz'
|
||||
},
|
||||
context
|
||||
}
|
||||
const ret = await deployPlugin.doExecute(deployOpts)
|
||||
expect(ret).ok
|
||||
console.log('ret:', ret)
|
||||
|
||||
// 删除测试证书
|
||||
const uploadPlugin = new UploadCertToTencent(options)
|
||||
await uploadPlugin.doRollback(deployOpts)
|
||||
})
|
||||
|
||||
it('#execute-sni-listenerId', async function () {
|
||||
this.timeout(10000)
|
||||
const options = createOptions()
|
||||
options.args.test = false
|
||||
options.cert.dnsProvider = 'tencent-yonsz'
|
||||
const certd = new Certd(options)
|
||||
const cert = certd.readCurrentCert('xiaojunnuo@qq.com', ['*.docmirror.cn'])
|
||||
const deployPlugin = new DeployCertToTencentCLB(options)
|
||||
const context = {}
|
||||
const deployOpts = {
|
||||
cert,
|
||||
props: {
|
||||
region: 'ap-guangzhou',
|
||||
loadBalancerId: 'lb-59yhe5xo',
|
||||
listenerId: 'lbl-akbyf5ac',
|
||||
domain: 'certd-test-sni.base.yonsz.net',
|
||||
accessProvider: 'tencent-yonsz'
|
||||
},
|
||||
context
|
||||
}
|
||||
const ret = await deployPlugin.doExecute(deployOpts)
|
||||
console.log('ret:', ret)
|
||||
expect(ret).ok
|
||||
// 删除测试证书
|
||||
const uploadPlugin = new UploadCertToTencent(options)
|
||||
await uploadPlugin.doRollback(deployOpts)
|
||||
})
|
||||
})
|
|
@ -1,59 +0,0 @@
|
|||
import pkg from 'chai'
|
||||
import { DeployCertToTencentTKEIngress } from '../../src/plugins/deploy-to-tke-ingress/index.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
import { K8sClient } from '../../src/utils/util.k8s.client.js'
|
||||
|
||||
const { expect } = pkg
|
||||
|
||||
async function getOptions () {
|
||||
const options = createOptions()
|
||||
options.args.test = false
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['*.docmirror.cn']
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const context = {}
|
||||
const deployOpts = {
|
||||
accessProviders: options.accessProviders,
|
||||
cert,
|
||||
props: {
|
||||
accessProvider: 'tencent-yonsz',
|
||||
region: 'ap-guangzhou',
|
||||
clusterId: 'cls-6lbj1vee'
|
||||
},
|
||||
context
|
||||
}
|
||||
return { options, deployOpts }
|
||||
}
|
||||
|
||||
describe('DeployCertToTencentTKEIngressNginx', function () {
|
||||
it('#getTKESecrets', async function () {
|
||||
this.timeout(50000)
|
||||
const { options, deployOpts } = await getOptions()
|
||||
const plugin = new DeployCertToTencentTKEIngress(options)
|
||||
const tkeClient = plugin.getTkeClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.region)
|
||||
const kubeConfig = await plugin.getTkeKubeConfig(tkeClient, deployOpts.props.clusterId)
|
||||
|
||||
const k8sClient = new K8sClient(kubeConfig)
|
||||
k8sClient.setLookup({
|
||||
'cls-6lbj1vee.ccs.tencent-cloud.com': { ip: '13.123.123.123' }
|
||||
})
|
||||
const secrets = await k8sClient.getSecret({ namespace: 'stress' })
|
||||
|
||||
console.log('secrets:', secrets)
|
||||
})
|
||||
it('#execute', async function () {
|
||||
this.timeout(5000)
|
||||
|
||||
const { options, deployOpts } = await getOptions()
|
||||
deployOpts.props.ingressName = 'stress-ingress-nginx'
|
||||
deployOpts.props.ingressClass = 'nginx'
|
||||
deployOpts.props.secretName = 'stress-all'
|
||||
deployOpts.props.namespace = 'stress'
|
||||
const plugin = new DeployCertToTencentTKEIngress(options)
|
||||
|
||||
const ret = await plugin.doExecute(deployOpts)
|
||||
console.log('sucess', ret)
|
||||
})
|
||||
})
|
|
@ -1,57 +0,0 @@
|
|||
import pkg from 'chai'
|
||||
import { DeployCertToTencentTKEIngress } from '../../src/plugins/deploy-to-tke-ingress/index.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
import { K8sClient } from '../../src/utils/util.k8s.client.js'
|
||||
|
||||
const { expect } = pkg
|
||||
|
||||
async function getOptions () {
|
||||
const options = createOptions()
|
||||
options.args.test = false
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['*.docmirror.cn']
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const context = {}
|
||||
const deployOpts = {
|
||||
accessProviders: options.accessProviders,
|
||||
cert,
|
||||
props: {
|
||||
accessProvider: 'tencent-yonsz',
|
||||
region: 'ap-guangzhou',
|
||||
clusterId: 'cls-6lbj1vee'
|
||||
},
|
||||
context
|
||||
}
|
||||
return { options, deployOpts }
|
||||
}
|
||||
|
||||
describe('DeployCertToTencentTKEIngress', function () {
|
||||
it('#getTKESecrets', async function () {
|
||||
this.timeout(50000)
|
||||
const { options, deployOpts } = await getOptions()
|
||||
const plugin = new DeployCertToTencentTKEIngress(options)
|
||||
const tkeClient = plugin.getTkeClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.region)
|
||||
const kubeConfig = await plugin.getTkeKubeConfig(tkeClient, deployOpts.props.clusterId)
|
||||
|
||||
const k8sClient = new K8sClient(kubeConfig)
|
||||
k8sClient.setLookup({
|
||||
'cls-6lbj1vee.ccs.tencent-cloud.com': { ip: '13.123.123.123' }
|
||||
})
|
||||
const secrets = await k8sClient.getSecret({ namespace: 'default' })
|
||||
|
||||
console.log('secrets:', secrets)
|
||||
})
|
||||
it('#execute', async function () {
|
||||
this.timeout(5000)
|
||||
const { options, deployOpts } = await getOptions()
|
||||
deployOpts.props.ingressName = 'ingress-base'
|
||||
deployOpts.props.secretName = 'cert---docmirror-cn'
|
||||
deployOpts.context.tencentCertId = 'hNUZJrZf'
|
||||
const plugin = new DeployCertToTencentTKEIngress(options)
|
||||
|
||||
const ret = await plugin.doExecute(deployOpts)
|
||||
console.log('sucess', ret)
|
||||
})
|
||||
})
|
|
@ -1,27 +0,0 @@
|
|||
import pkg from 'chai'
|
||||
import { UploadCertToTencent } from '../../src/plugins/upload-to-tencent/index.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
const { expect } = pkg
|
||||
describe('PluginUploadToTencent', function () {
|
||||
it('#execute', async function () {
|
||||
const options = createOptions()
|
||||
const plugin = new UploadCertToTencent(options)
|
||||
options.args = { test: false }
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['*.docmirror.cn']
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const context = {}
|
||||
const uploadOpts = {
|
||||
accessProviders: options.accessProviders,
|
||||
cert,
|
||||
props: { name: 'certd部署测试', accessProvider: 'tencent' },
|
||||
context
|
||||
}
|
||||
await plugin.doExecute(uploadOpts)
|
||||
console.log('context:', context)
|
||||
|
||||
await plugin.doRollback(uploadOpts)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,116 @@
|
|||
import kubernetesClient from "kubernetes-client";
|
||||
import dns from "dns";
|
||||
import { logger } from "@certd/pipeline";
|
||||
|
||||
// @ts-ignore
|
||||
const { KubeConfig, Client, Request } = kubernetesClient;
|
||||
|
||||
export class K8sClient {
|
||||
kubeConfigStr: string;
|
||||
lookup!: any;
|
||||
client!: any;
|
||||
constructor(kubeConfigStr: string) {
|
||||
this.kubeConfigStr = kubeConfigStr;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
const kubeconfig = new KubeConfig();
|
||||
kubeconfig.loadFromString(this.kubeConfigStr);
|
||||
const reqOpts = { kubeconfig, request: {} } as any;
|
||||
if (this.lookup) {
|
||||
reqOpts.request.lookup = this.lookup;
|
||||
}
|
||||
|
||||
const backend = new Request(reqOpts);
|
||||
this.client = new Client({ backend, version: "1.13" });
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param localRecords { [domain]:{ip:'xxx.xx.xxx'} }
|
||||
*/
|
||||
setLookup(localRecords: { [key: string]: { ip: string } }) {
|
||||
this.lookup = (hostnameReq: any, options: any, callback: any) => {
|
||||
logger.info("custom lookup", hostnameReq, localRecords);
|
||||
if (localRecords[hostnameReq]) {
|
||||
logger.info("local record", hostnameReq, localRecords[hostnameReq]);
|
||||
callback(null, localRecords[hostnameReq].ip, 4);
|
||||
} else {
|
||||
dns.lookup(hostnameReq, options, callback);
|
||||
}
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 secret列表
|
||||
* @param opts = {namespace:default}
|
||||
* @returns secretsList
|
||||
*/
|
||||
async getSecret(opts: { namespace: string }) {
|
||||
const namespace = opts.namespace || "default";
|
||||
return await this.client.api.v1.namespaces(namespace).secrets.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Secret
|
||||
* @param opts {namespace:default, body:yamlStr}
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async createSecret(opts: any) {
|
||||
const namespace = opts.namespace || "default";
|
||||
const created = await this.client.api.v1.namespaces(namespace).secrets.post({
|
||||
body: opts.body,
|
||||
});
|
||||
logger.info("new secrets:", created);
|
||||
return created;
|
||||
}
|
||||
|
||||
async updateSecret(opts: any) {
|
||||
const namespace = opts.namespace || "default";
|
||||
const secretName = opts.secretName;
|
||||
if (secretName == null) {
|
||||
throw new Error("secretName 不能为空");
|
||||
}
|
||||
return await this.client.api.v1.namespaces(namespace).secrets(secretName).put({
|
||||
body: opts.body,
|
||||
});
|
||||
}
|
||||
|
||||
async patchSecret(opts: any) {
|
||||
const namespace = opts.namespace || "default";
|
||||
const secretName = opts.secretName;
|
||||
if (secretName == null) {
|
||||
throw new Error("secretName 不能为空");
|
||||
}
|
||||
return await this.client.api.v1.namespaces(namespace).secrets(secretName).patch({
|
||||
body: opts.body,
|
||||
});
|
||||
}
|
||||
|
||||
async getIngressList(opts: any) {
|
||||
const namespace = opts.namespace || "default";
|
||||
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses.get();
|
||||
}
|
||||
|
||||
async getIngress(opts: any) {
|
||||
const namespace = opts.namespace || "default";
|
||||
const ingressName = opts.ingressName;
|
||||
if (!ingressName) {
|
||||
throw new Error("ingressName 不能为空");
|
||||
}
|
||||
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).get();
|
||||
}
|
||||
|
||||
async patchIngress(opts: any) {
|
||||
const namespace = opts.namespace || "default";
|
||||
const ingressName = opts.ingressName;
|
||||
if (!ingressName) {
|
||||
throw new Error("ingressName 不能为空");
|
||||
}
|
||||
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).patch({
|
||||
body: opts.body,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1 +1 @@
|
|||
Subproject commit 7782a7de65f60e2b4611bca1e29b4ab19deec82b
|
||||
Subproject commit 77fb1fa8492d95c7f6698aaaadac34cb2de4de2b
|
Loading…
Reference in New Issue