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",
|
"name": "@certd/pipeline",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"main": "./dist/pipeline.umd.js",
|
"main": "./src/index.ts",
|
||||||
"module": "./dist/pipeline.mjs",
|
"module": "./dist/pipeline.mjs",
|
||||||
"types": "./dist/es/index.d.ts",
|
"types": "./dist/es/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -14,7 +14,10 @@
|
||||||
"@certd/acme-client": "^0.3.0",
|
"@certd/acme-client": "^0.3.0",
|
||||||
"dayjs": "^1.11.6",
|
"dayjs": "^1.11.6",
|
||||||
"lodash": "^4.17.21",
|
"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": {
|
"devDependencies": {
|
||||||
"@types/lodash": "^4.14.186",
|
"@types/lodash": "^4.14.186",
|
||||||
|
@ -35,7 +38,6 @@
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"log4js": "^6.3.0",
|
|
||||||
"mocha": "^10.1.0",
|
"mocha": "^10.1.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.8.4",
|
"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 { Registrable } from "../registry";
|
||||||
import { accessRegistry } from "./registry";
|
import { accessRegistry } from "./registry";
|
||||||
import { FormItemProps } from "../d.ts";
|
import { FormItemProps } from "../d.ts";
|
||||||
|
import { AbstractAccess } from "./abstract-access";
|
||||||
|
|
||||||
export type AccessInput = FormItemProps & {
|
export type AccessInput = FormItemProps & {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -17,3 +18,7 @@ export function IsAccess(define: AccessDefine) {
|
||||||
accessRegistry.install(target);
|
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 "./api";
|
||||||
export * from "./impl";
|
|
||||||
export * from "./abstract-access";
|
export * from "./abstract-access";
|
||||||
|
export * from "./registry";
|
||||||
|
|
|
@ -2,12 +2,12 @@ import { ConcurrencyStrategy, Pipeline, ResultType, Runnable, RunStrategy, Stage
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { RunHistory } from "./run-history";
|
import { RunHistory } from "./run-history";
|
||||||
import { pluginRegistry, TaskPlugin } from "../plugin";
|
import { pluginRegistry, TaskPlugin } from "../plugin";
|
||||||
import { IAccessService } from "../access/access-service";
|
|
||||||
import { ContextFactory, IContext } from "./context";
|
import { ContextFactory, IContext } from "./context";
|
||||||
import { IStorage } from "./storage";
|
import { IStorage } from "./storage";
|
||||||
import { logger } from "../utils/util.log";
|
import { logger } from "../utils/util.log";
|
||||||
import { Logger } from "log4js";
|
import { Logger } from "log4js";
|
||||||
|
import { request } from "../utils/util.request";
|
||||||
|
import { IAccessService } from "../access";
|
||||||
export class Executor {
|
export class Executor {
|
||||||
userId: any;
|
userId: any;
|
||||||
pipeline: Pipeline;
|
pipeline: Pipeline;
|
||||||
|
@ -166,6 +166,7 @@ export class Executor {
|
||||||
pipelineContext: this.pipelineContext,
|
pipelineContext: this.pipelineContext,
|
||||||
userContext: this.contextFactory.getContext("user", this.userId),
|
userContext: this.contextFactory.getContext("user", this.userId),
|
||||||
logger,
|
logger,
|
||||||
|
http: request,
|
||||||
});
|
});
|
||||||
return plugin;
|
return plugin;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
export * from "./executor";
|
export * from "./executor";
|
||||||
export * from "./run-history";
|
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 { CreateRecordOptions, IDnsProvider, DnsProviderDefine, RemoveRecordOptions } from "./api";
|
||||||
import { AbstractAccess } from "../access";
|
import { AbstractAccess } from "../access";
|
||||||
import { Logger } from "log4js";
|
import { Logger } from "log4js";
|
||||||
|
import { AxiosInstance } from "axios";
|
||||||
export abstract class AbstractDnsProvider extends AbstractRegistrable<DnsProviderDefine> implements IDnsProvider {
|
export abstract class AbstractDnsProvider extends AbstractRegistrable<DnsProviderDefine> implements IDnsProvider {
|
||||||
access!: AbstractAccess;
|
access!: AbstractAccess;
|
||||||
logger!: Logger;
|
logger!: Logger;
|
||||||
doInit(options: { access: AbstractAccess; logger: Logger }) {
|
http!: AxiosInstance;
|
||||||
|
doInit(options: { access: AbstractAccess; logger: Logger; http: AxiosInstance }) {
|
||||||
this.access = options.access;
|
this.access = options.access;
|
||||||
this.logger = options.logger;
|
this.logger = options.logger;
|
||||||
|
this.http = options.http;
|
||||||
this.onInit();
|
this.onInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import "./providers";
|
|
||||||
export * from "./api";
|
export * from "./api";
|
||||||
export * from "./registry";
|
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 "./registry";
|
||||||
export * from "./dns-provider";
|
export * from "./dns-provider";
|
||||||
export * from "./plugin";
|
export * from "./plugin";
|
||||||
|
export * from "./utils";
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { AbstractRegistrable } from "../registry";
|
import { AbstractRegistrable } from "../registry";
|
||||||
import { Logger } from "log4js";
|
import { Logger } from "log4js";
|
||||||
import { IAccessService } from "../access/access-service";
|
|
||||||
import { IContext } from "../core/context";
|
import { IContext } from "../core/context";
|
||||||
import { PluginDefine, TaskInput, TaskOutput, TaskPlugin } from "./api";
|
import { PluginDefine, TaskInput, TaskOutput, TaskPlugin } from "./api";
|
||||||
|
import { IAccessService } from "../access";
|
||||||
|
import { AxiosInstance } from "axios";
|
||||||
|
|
||||||
export abstract class AbstractPlugin extends AbstractRegistrable<PluginDefine> implements TaskPlugin {
|
export abstract class AbstractPlugin extends AbstractRegistrable<PluginDefine> implements TaskPlugin {
|
||||||
logger!: Logger;
|
logger!: Logger;
|
||||||
|
@ -12,12 +13,14 @@ export abstract class AbstractPlugin extends AbstractRegistrable<PluginDefine> i
|
||||||
pipelineContext: IContext;
|
pipelineContext: IContext;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
userContext: IContext;
|
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.accessService = options.accessService;
|
||||||
this.pipelineContext = options.pipelineContext;
|
this.pipelineContext = options.pipelineContext;
|
||||||
this.userContext = options.userContext;
|
this.userContext = options.userContext;
|
||||||
this.logger = options.logger;
|
this.logger = options.logger;
|
||||||
|
this.http = options.http;
|
||||||
await this.onInit();
|
await this.onInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import "./plugins";
|
|
||||||
export * from "./api";
|
export * from "./api";
|
||||||
export * from "./registry";
|
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 { AbstractPlugin, IsTask, TaskInput, TaskOutput, TaskPlugin } from "../src";
|
||||||
import { IsTask, TaskInput, TaskOutput, TaskPlugin } from "../api";
|
|
||||||
|
|
||||||
@IsTask(() => {
|
@IsTask(() => {
|
||||||
return {
|
return {
|
|
@ -1,11 +1,12 @@
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import "mocha";
|
import "mocha";
|
||||||
import { EchoPlugin } from "../src/plugin/plugins";
|
import { EchoPlugin } from "./echo-plugin";
|
||||||
describe("task_plugin", function () {
|
describe("task_plugin", function () {
|
||||||
it("#taskplugin", function () {
|
it("#taskplugin", function () {
|
||||||
const echoPlugin = new EchoPlugin();
|
const echoPlugin = new EchoPlugin();
|
||||||
const define = echoPlugin.define;
|
// @ts-ignore
|
||||||
echoPlugin.execute({ context: {}, props: { test: 111 } });
|
const define = echoPlugin.getDefine();
|
||||||
|
echoPlugin.execute({ context: {}, input: { test: 111 } });
|
||||||
expect(define.name).eq("EchoPlugin");
|
expect(define.name).eq("EchoPlugin");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { IAccessService } from "../../src/access/access-service";
|
import { AbstractAccess, IAccessService } from "../../src";
|
||||||
import { AbstractAccess, AliyunAccess } from "../../src";
|
|
||||||
import { aliyunSecret } from "../user.secret";
|
import { aliyunSecret } from "../user.secret";
|
||||||
export class AccessServiceTest implements IAccessService {
|
export class AccessServiceTest implements IAccessService {
|
||||||
async getById(id: any): Promise<AbstractAccess> {
|
async getById(id: any): Promise<AbstractAccess> {
|
||||||
return {
|
return {
|
||||||
...aliyunSecret,
|
...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": {
|
"env": {
|
||||||
"mocha": true
|
"mocha": true
|
||||||
},
|
},
|
||||||
"overrides": [
|
"rules": {
|
||||||
{
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
"files": ["*.test.js", "*.spec.js"],
|
"@typescript-eslint/ban-ts-ignore": "off",
|
||||||
"rules": {
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"no-unused-expressions": "off"
|
// "no-unused-expressions": "off",
|
||||||
}
|
"max-len": [0, 160, 2, { "ignoreUrls": true }]
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,26 @@
|
||||||
.vscode/
|
# Logs
|
||||||
node_modules/
|
logs
|
||||||
npm-debug.log
|
*.log
|
||||||
yarn-error.log
|
npm-debug.log*
|
||||||
yarn.lock
|
yarn-debug.log*
|
||||||
package-lock.json
|
yarn-error.log*
|
||||||
/.idea/
|
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",
|
"name": "@certd/plugin-host",
|
||||||
"version": "0.3.0",
|
"private": true,
|
||||||
"description": "",
|
"version": "0.3.0",
|
||||||
"type": "module",
|
"main": "./src/index.ts",
|
||||||
"main": "./dist/index.cjs",
|
"module": "./dist/plugin-aliyun.mjs",
|
||||||
"module": "./dist/fast-crud.es.js",
|
"types": "./dist/es/plugin-aliyun.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c"
|
"dev": "vite",
|
||||||
},
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"dependencies": {
|
"preview": "vite preview"
|
||||||
"@certd/api": "^0.3.0",
|
},
|
||||||
"dayjs": "^1.9.7",
|
"dependencies": {
|
||||||
"lodash-es": "^4.17.20",
|
"@certd/pipeline": "^0.3.0",
|
||||||
"ssh2": "^0.8.9"
|
"ssh2": "^0.8.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@certd/certd": "^0.3.0",
|
"log4js": "^6.3.0",
|
||||||
"chai": "^4.2.0",
|
"dayjs": "^1.9.7",
|
||||||
"eslint": "^7.15.0",
|
"lodash-es": "^4.17.20",
|
||||||
"eslint-config-standard": "^16.0.2",
|
"@types/lodash": "^4.14.186",
|
||||||
"eslint-plugin-import": "^2.22.1",
|
"vue-tsc": "^0.38.9",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"@alicloud/cs20151215": "^3.0.3",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"@alicloud/openapi-client": "^0.4.0",
|
||||||
"mocha": "^8.2.1",
|
"@alicloud/pop-core": "^1.7.10",
|
||||||
"rollup": "^3.2.3"
|
"@midwayjs/core": "^3.0.0",
|
||||||
},
|
"@midwayjs/decorator": "^3.0.0",
|
||||||
"author": "Greper",
|
"@types/chai": "^4.3.3",
|
||||||
"license": "MIT",
|
"@types/mocha": "^10.0.0",
|
||||||
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
|
"@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": {
|
"env": {
|
||||||
"mocha": true
|
"mocha": true
|
||||||
},
|
},
|
||||||
"overrides": [
|
"rules": {
|
||||||
{
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
"files": ["*.test.js", "*.spec.js"],
|
"@typescript-eslint/ban-ts-ignore": "off",
|
||||||
"rules": {
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"no-unused-expressions": "off"
|
// "no-unused-expressions": "off",
|
||||||
}
|
"max-len": [0, 160, 2, { "ignoreUrls": true }]
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,26 @@
|
||||||
.vscode/
|
# Logs
|
||||||
node_modules/
|
logs
|
||||||
npm-debug.log
|
*.log
|
||||||
yarn-error.log
|
npm-debug.log*
|
||||||
yarn.lock
|
yarn-debug.log*
|
||||||
package-lock.json
|
yarn-error.log*
|
||||||
/.idea/
|
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",
|
"name": "@certd/plugin-tencent",
|
||||||
"version": "0.3.0",
|
"private": true,
|
||||||
"description": "",
|
"version": "0.3.0",
|
||||||
"type": "module",
|
"main": "./src/index.ts",
|
||||||
"main": "./dist/index.cjs",
|
"module": "./dist/plugin-aliyun.mjs",
|
||||||
"module": "./dist/fast-crud.es.js",
|
"types": "./dist/es/plugin-aliyun.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c"
|
"dev": "vite",
|
||||||
},
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"dependencies": {
|
"preview": "vite preview"
|
||||||
"@certd/api": "^0.3.0",
|
},
|
||||||
"@certd/plugin-common": "^0.3.0",
|
"dependencies": {
|
||||||
"dayjs": "^1.9.7",
|
"@certd/pipeline": "^0.3.0",
|
||||||
"kubernetes-client": "^9.0.0",
|
"@certd/plugin-util": "^0.3.0",
|
||||||
"lodash-es": "^4.17.20",
|
"tencentcloud-sdk-nodejs": "^4.0.44"
|
||||||
"tencentcloud-sdk-nodejs": "^4.0.44"
|
},
|
||||||
},
|
"devDependencies": {
|
||||||
"devDependencies": {
|
"axios": "^0.21.1",
|
||||||
"@certd/certd": "^0.3.0",
|
"log4js": "^6.3.0",
|
||||||
"chai": "^4.2.0",
|
"dayjs": "^1.9.7",
|
||||||
"eslint": "^7.15.0",
|
"lodash-es": "^4.17.20",
|
||||||
"eslint-config-standard": "^16.0.2",
|
"@types/lodash": "^4.14.186",
|
||||||
"eslint-plugin-import": "^2.22.1",
|
"vue-tsc": "^0.38.9",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"@alicloud/cs20151215": "^3.0.3",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"@alicloud/openapi-client": "^0.4.0",
|
||||||
"mocha": "^8.2.1",
|
"@alicloud/pop-core": "^1.7.10",
|
||||||
"rollup": "^3.2.3"
|
"@midwayjs/core": "^3.0.0",
|
||||||
},
|
"@midwayjs/decorator": "^3.0.0",
|
||||||
"author": "Greper",
|
"@types/chai": "^4.3.3",
|
||||||
"license": "MIT",
|
"@types/mocha": "^10.0.0",
|
||||||
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
|
"@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