Compare commits
317 Commits
v0.2.2
...
v0.2.23-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d36df211e | ||
|
|
cec56b6e49 | ||
|
|
dfd2fccc1e | ||
|
|
4ffb7ae969 | ||
|
|
302ddcdd07 | ||
|
|
65df759275 | ||
|
|
a842b6b925 | ||
|
|
4916757d59 | ||
|
|
30b66adc3b | ||
|
|
13582d1a7b | ||
|
|
0b9312b549 | ||
|
|
bde51d8d38 | ||
|
|
643a666853 | ||
|
|
a59184ae5f | ||
|
|
82807fcc1b | ||
|
|
a6c93ef9b8 | ||
|
|
6a151865f7 | ||
|
|
414d8d140e | ||
|
|
51fb9dca58 | ||
|
|
6367785b1b | ||
|
|
aa7fb7da06 | ||
|
|
26d11de249 | ||
|
|
0daa9f1882 | ||
|
|
56886dcfe9 | ||
|
|
81e1e4a7ff | ||
|
|
9b5256716f | ||
|
|
446bf80f1d | ||
|
|
6a80455c6c | ||
|
|
43b2ff7957 | ||
|
|
295b7779ee | ||
|
|
d1df088662 | ||
|
|
2b0f7aaf8a | ||
|
|
3265dd76ab | ||
|
|
d1d7b44303 | ||
|
|
56eced3813 | ||
|
|
c853f2976f | ||
|
|
b66931003f | ||
|
|
9a75d2ac8f | ||
|
|
42c5aea3f7 | ||
|
|
e2fd9c4cee | ||
|
|
f847b7ff62 | ||
|
|
9eae8f5077 | ||
|
|
2bacf76664 | ||
|
|
b2030caedc | ||
|
|
956c975c6d | ||
|
|
41bd321a4f | ||
|
|
952e9687d0 | ||
|
|
c298f8b952 | ||
|
|
e2562a5251 | ||
|
|
dbdb40baf9 | ||
|
|
2ff923dd1b | ||
|
|
f4f13f91f2 | ||
|
|
034aa980e6 | ||
|
|
6ac7a51ce0 | ||
|
|
cf0c0e3e2c | ||
|
|
1b899575e0 | ||
|
|
23e5cb5669 | ||
|
|
e4ba4c9b37 | ||
|
|
9ed64bdc9a | ||
|
|
e9b6fb55ff | ||
|
|
80caf881ae | ||
|
|
c36db3545f | ||
|
|
a367585ab4 | ||
|
|
2994cb5c65 | ||
|
|
1bedb31a3c | ||
|
|
8fecebc254 | ||
|
|
44497a0969 | ||
|
|
5362371bda | ||
|
|
8b04e96a7d | ||
|
|
5d93334426 | ||
|
|
150b666d4b | ||
|
|
94579d65c4 | ||
|
|
551b06b4e8 | ||
|
|
76fc47a274 | ||
|
|
35e1bfcd7f | ||
|
|
24df7913fe | ||
|
|
83674e4b35 | ||
|
|
22d3aeb7b5 | ||
|
|
cf005711c0 | ||
|
|
0a00d0c52f | ||
|
|
9aa17a0395 | ||
|
|
65ecdf7dc2 | ||
|
|
0dfa5994cc | ||
|
|
5d2844fdb6 | ||
|
|
44332b9d07 | ||
|
|
20a23e148c | ||
|
|
0bcb6206f4 | ||
|
|
943b9827ee | ||
|
|
741f3ec212 | ||
|
|
8549a17675 | ||
|
|
718cfccbea | ||
|
|
2458fa26d8 | ||
|
|
ac24684d2b | ||
|
|
106dbd9538 | ||
|
|
f9efb2b800 | ||
|
|
897d124d5b | ||
|
|
34daf9ccac | ||
|
|
269a97e81e | ||
|
|
2fd57621d8 | ||
|
|
76de837214 | ||
|
|
1e41020728 | ||
|
|
8a78e49bf0 | ||
|
|
e6726e4c02 | ||
|
|
76330a4a1a | ||
|
|
7e5f0097e4 | ||
|
|
18e1c02d1c | ||
|
|
28992f178e | ||
|
|
c41f34c352 | ||
|
|
6b5580a30c | ||
|
|
1dee14e32d | ||
|
|
1e3c4881d0 | ||
|
|
657964cda4 | ||
|
|
893aac916c | ||
|
|
68da6cf3ae | ||
|
|
0d96ea9eef | ||
|
|
0ceb44a7cd | ||
|
|
4fec0036cb | ||
|
|
f82eee4636 | ||
|
|
260cfb96ec | ||
|
|
f71a519674 | ||
|
|
369c146eca | ||
|
|
83264a6946 | ||
|
|
3c3d4e9109 | ||
|
|
ce55365292 | ||
|
|
be495839b6 | ||
|
|
a27a9f55a7 | ||
|
|
10e14caf35 | ||
|
|
59af246479 | ||
|
|
1f52eaca01 | ||
|
|
d833f4b5ff | ||
|
|
bfee39049d | ||
|
|
b4599df6c6 | ||
|
|
261c6f6956 | ||
|
|
b97d77c848 | ||
|
|
c1cefe0e7f | ||
|
|
55b77fdf5c | ||
|
|
16967c4ab1 | ||
|
|
61a4fd8657 | ||
|
|
67ca7e3097 | ||
|
|
26fa8e75bd | ||
|
|
aeaa45b713 | ||
|
|
edeac86f06 | ||
|
|
4e0c23165f | ||
|
|
feb851a3fc | ||
|
|
3103d60508 | ||
|
|
53be6b5f5b | ||
|
|
9d3e0d1090 | ||
|
|
f8aef129cf | ||
|
|
c419b2c8b4 | ||
|
|
e1a3a3e7c7 | ||
|
|
b47a1a13cb | ||
|
|
3397f424bc | ||
|
|
48672d1a44 | ||
|
|
38dc8a63d9 | ||
|
|
009e8fb976 | ||
|
|
6d7a91f49b | ||
|
|
9d4d14db06 | ||
|
|
c9f347f77a | ||
|
|
0396d8222e | ||
|
|
305f3de50f | ||
|
|
ffacfe0f42 | ||
|
|
be9e66c7d3 | ||
|
|
1238508bdb | ||
|
|
1ab5c4035a | ||
|
|
67fa9d91bf | ||
|
|
dc5f9abf20 | ||
|
|
7240a42fbc | ||
|
|
6fbb6d4992 | ||
|
|
86838f305b | ||
|
|
1b1b5939c5 | ||
|
|
ffdd61b5ee | ||
|
|
adad5d86ba | ||
|
|
e7870e2b05 | ||
|
|
548cbbfdd4 | ||
|
|
da4715e6dc | ||
|
|
506ab4f18e | ||
|
|
d87026d5be | ||
|
|
1690963aaf | ||
|
|
20d2c5699c | ||
|
|
e660e9cad1 | ||
|
|
26d7b0ba03 | ||
|
|
ee097b3135 | ||
|
|
f5052e9a58 | ||
|
|
3b3376899c | ||
|
|
a24a3595fa | ||
|
|
6a14d801f1 | ||
|
|
332c5c5127 | ||
|
|
f9568f1a4a | ||
|
|
b458720dca | ||
|
|
935a320100 | ||
|
|
361d0de17c | ||
|
|
024b3c936e | ||
|
|
dc720a5d99 | ||
|
|
af3e20709d | ||
|
|
ea9e9165b6 | ||
|
|
ee531dd186 | ||
|
|
51abe8de56 | ||
|
|
e2254faf15 | ||
|
|
cea6be37dc | ||
|
|
46dccb176e | ||
|
|
5411b9cb92 | ||
|
|
9f6ea410af | ||
|
|
528a3d9da8 | ||
|
|
564eb48ebe | ||
|
|
92a6b179d4 | ||
|
|
83393a4ee1 | ||
|
|
6875151717 | ||
|
|
2a8c6cf033 | ||
|
|
7544286b0f | ||
|
|
7c685646da | ||
|
|
d82a9c9253 | ||
|
|
59584a2961 | ||
|
|
195aa54cdc | ||
|
|
4b324e6a22 | ||
|
|
0e575a0ce7 | ||
|
|
7ab8517a93 | ||
|
|
1dca6ecf8d | ||
|
|
8bec234fe8 | ||
|
|
bff18a7be7 | ||
|
|
bac00491fe | ||
|
|
f8da3ded0d | ||
|
|
b01849eb0c | ||
|
|
c9eb487953 | ||
|
|
dc383644d6 | ||
|
|
a8f718afa0 | ||
|
|
cd76d170b2 | ||
|
|
7b129c11e9 | ||
|
|
f7972d5b68 | ||
|
|
b1a0d84033 | ||
|
|
969fba8a57 | ||
|
|
63865b5fbd | ||
|
|
46c32f15e3 | ||
|
|
5f62c887c0 | ||
|
|
c85beaa52b | ||
|
|
885cdfaec9 | ||
|
|
011130432c | ||
|
|
062d66222a | ||
|
|
e53749e16e | ||
|
|
dbfb84ec6d | ||
|
|
265842feeb | ||
|
|
0c35928eee | ||
|
|
ea4bcb4aaf | ||
|
|
716f5f1426 | ||
|
|
4e86c1eb45 | ||
|
|
0576a8bec3 | ||
|
|
97f334b5ab | ||
|
|
18a7bf0d66 | ||
|
|
908d33f186 | ||
|
|
68b9171390 | ||
|
|
45005a5073 | ||
|
|
028eb088a5 | ||
|
|
8f98664665 | ||
|
|
699385a8c4 | ||
|
|
64b7ed00f5 | ||
|
|
2c75d2bfde | ||
|
|
9c41b0e357 | ||
|
|
ec6f10053a | ||
|
|
0095600615 | ||
|
|
b031f00764 | ||
|
|
a4fc8dfc56 | ||
|
|
f168bd903d | ||
|
|
fc55e37454 | ||
|
|
84e2fd4f5c | ||
|
|
0037659462 | ||
|
|
364289894e | ||
|
|
f6a3f4edfa | ||
|
|
560d21c854 | ||
|
|
4719f99155 | ||
|
|
3a213dc9c3 | ||
|
|
0d07c7c234 | ||
|
|
21670f64d1 | ||
|
|
f0e7fe695d | ||
|
|
2bab727569 | ||
|
|
8d41a9aae7 | ||
|
|
896b5d3a13 | ||
|
|
88e64717cd | ||
|
|
2d275a14ab | ||
|
|
1b796cffd1 | ||
|
|
f53e54c8de | ||
|
|
a9b9be96cb | ||
|
|
1033885c99 | ||
|
|
c45ad3c901 | ||
|
|
24192b61c1 | ||
|
|
1562e92e74 | ||
|
|
17f72eb9cb | ||
|
|
57ae6d5b40 | ||
|
|
467e4c4634 | ||
|
|
d6d296b546 | ||
|
|
499bbe4fa7 | ||
|
|
ae814766f3 | ||
|
|
17cfeee374 | ||
|
|
efa394e9bd | ||
|
|
94ca0f27bf | ||
|
|
e08df5e6d8 | ||
|
|
952c6ef73d | ||
|
|
b9902c926f | ||
|
|
7fa6ea1797 | ||
|
|
be3cdbf585 | ||
|
|
6225969d4c | ||
|
|
4382474449 | ||
|
|
678ef9c232 | ||
|
|
3d535320b9 | ||
|
|
77d3e40ffb | ||
|
|
5dca64d3d3 | ||
|
|
02d582b564 | ||
|
|
8e906cbf23 | ||
|
|
411b7bbfe2 | ||
|
|
3093fc6b02 | ||
|
|
3c4b7d251a | ||
|
|
f87a1be192 | ||
|
|
9b91cbd67e | ||
|
|
9b5e1052a1 | ||
|
|
0d47d7cfd0 | ||
|
|
ef87975c80 | ||
|
|
9db757fbbb | ||
|
|
f6ef305441 | ||
|
|
004c6a8506 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,7 +7,7 @@ assignees: ""
|
||||
---
|
||||
|
||||
**描述问题**
|
||||
简要描述问题是什么
|
||||
简要描述问题是什么,1 个 ISSUE 只描述一个问题。
|
||||
|
||||
**复现步骤**
|
||||
复现该问题的步骤:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -7,7 +7,7 @@ assignees: ""
|
||||
---
|
||||
|
||||
**功能描述**
|
||||
简要描述你希望添加的功能和相关问题。
|
||||
简要描述你希望添加的功能和相关问题,1 个 ISSUE 只描述一个功能。
|
||||
|
||||
**动机**
|
||||
为什么这个功能对项目有帮助?
|
||||
|
||||
3
.github/workflows/push_image.yml
vendored
3
.github/workflows/push_image.yml
vendored
@@ -52,7 +52,8 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile_build
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -14,18 +14,18 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# - name: Set up Node.js
|
||||
# uses: actions/setup-node@v4
|
||||
# with:
|
||||
# node-version: 20.11.0
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.11.0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ">=1.22.5"
|
||||
|
||||
# - name: Build Admin dashboard UI
|
||||
# run: npm --prefix=./ui ci && npm --prefix=./ui run build
|
||||
- name: Build Admin dashboard UI
|
||||
run: npm --prefix=./ui ci && npm --prefix=./ui run build
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -9,13 +9,14 @@
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
__debug_bin*
|
||||
|
||||
vendor
|
||||
pb_data
|
||||
build
|
||||
main
|
||||
|
||||
/ui/dist/*
|
||||
!/ui/dist/.gitkeep
|
||||
./dist
|
||||
./certimate
|
||||
/docker/data
|
||||
|
||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
FROM node:20-alpine3.19 AS front-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app/
|
||||
|
||||
RUN \
|
||||
cd /app/ui && \
|
||||
npm install && \
|
||||
npm run build
|
||||
|
||||
|
||||
FROM golang:1.22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ../. /app/
|
||||
|
||||
RUN rm -rf /app/ui/dist
|
||||
|
||||
COPY --from=front-builder /app/ui/dist /app/ui/dist
|
||||
|
||||
RUN go build -o certimate
|
||||
|
||||
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/certimate .
|
||||
|
||||
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]
|
||||
@@ -1,16 +0,0 @@
|
||||
FROM golang:1.22-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ../. /app/
|
||||
|
||||
RUN go build -o certimate
|
||||
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/certimate .
|
||||
|
||||
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]
|
||||
3
Makefile
3
Makefile
@@ -35,3 +35,6 @@ help:
|
||||
@echo " make help - 显示此帮助信息"
|
||||
|
||||
.PHONY: all build clean help
|
||||
|
||||
local.run:
|
||||
go mod vendor&& npm --prefix=./ui install && npm --prefix=./ui run build && go run main.go serve --http 127.0.0.1:8090
|
||||
|
||||
63
README.md
63
README.md
@@ -55,8 +55,7 @@ mkdir -p ~/.certimate && cd ~/.certimate && curl -O https://raw.githubuserconten
|
||||
```bash
|
||||
git clone EMAIL:usual2970/certimate.git
|
||||
cd certimate
|
||||
go mod vendor
|
||||
go run main.go serve
|
||||
make local.run
|
||||
```
|
||||
|
||||
## 二、使用
|
||||
@@ -72,30 +71,35 @@ go run main.go serve
|
||||
|
||||
## 三、支持的服务商列表
|
||||
|
||||
| 服务商 | 支持申请证书 | 支持部署证书 | 备注 |
|
||||
| :--------: | :----------: | :----------: | ------------------------------------------------------------ |
|
||||
| 阿里云 | √ | √ | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN |
|
||||
| 腾讯云 | √ | √ | 可签发在腾讯云注册的域名;可部署到腾讯云 CDN |
|
||||
| 华为云 | √ | | 可签发在华为云注册的域名 |
|
||||
| 七牛云 | | √ | 可部署到七牛云 CDN |
|
||||
| CloudFlare | √ | | 可签发在 CloudFlare 注册的域名;CloudFlare 服务自带 SSL 证书 |
|
||||
| GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 |
|
||||
| Namesilo | √ | | 可签发在 Namesilo 注册的域名 |
|
||||
| 本地部署 | | √ | 可部署到本地服务器 |
|
||||
| SSH | | √ | 可部署到 SSH 服务器 |
|
||||
| Webhook | | √ | 可部署时回调到 Webhook |
|
||||
| 服务商 | 支持申请证书 | 支持部署证书 | 备注 |
|
||||
| :--------: | :----------: | :----------: | ----------------------------------------------------------------- |
|
||||
| 阿里云 | √ | √ | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN、SLB |
|
||||
| 腾讯云 | √ | √ | 可签发在腾讯云注册的域名;可部署到腾讯云 COS、CDN、ECDN、CLB、TEO |
|
||||
| 百度智能云 | | √ | 可部署到百度智能云 CDN |
|
||||
| 华为云 | √ | √ | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB |
|
||||
| 七牛云 | | √ | 可部署到七牛云 CDN |
|
||||
| 多吉云 | | √ | 可部署到多吉云 CDN |
|
||||
| 火山引擎 | √ | √ | 可签发在火山引擎注册的域名;可部署到火山引擎 Live、CDN |
|
||||
| AWS | √ | | 可签发在 AWS Route53 托管的域名 |
|
||||
| CloudFlare | √ | | 可签发在 CloudFlare 注册的域名;CloudFlare 服务自带 SSL 证书 |
|
||||
| GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 |
|
||||
| Namesilo | √ | | 可签发在 Namesilo 注册的域名 |
|
||||
| PowerDNS | √ | | 可签发在 PowerDNS 托管的域名 |
|
||||
| HTTP 请求 | √ | | 可签发允许通过 HTTP 请求修改 DNS 的域名 |
|
||||
| 本地部署 | | √ | 可部署到本地服务器 |
|
||||
| SSH | | √ | 可部署到 SSH 服务器 |
|
||||
| Webhook | | √ | 可部署时回调到 Webhook |
|
||||
| Kubernetes | | √ | 可部署到 Kubernetes Secret |
|
||||
|
||||
## 四、系统截图
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
<div align="center">
|
||||
<img src="https://i.imgur.com/SYjjbql.jpeg" title="Login page" width="95%"/>
|
||||
<img src="https://i.imgur.com/WMVbBId.jpeg" title="Dashboard page" width="47%"/>
|
||||
<img src="https://i.imgur.com/8wit3ZA.jpeg" title="Domains page" width="47%"/>
|
||||
<img src="https://i.imgur.com/EWtOoJ0.jpeg" title="Accesses page" width="47%"/>
|
||||
<img src="https://i.imgur.com/aaPtSW7.jpeg" title="History page" width="47%"/>
|
||||
</div>
|
||||
|
||||
## 五、概念
|
||||
|
||||
@@ -167,13 +171,22 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.
|
||||
|
||||
支持更多服务商、UI 的优化改进、Bug 修复、文档完善等,欢迎大家提交 PR。
|
||||
|
||||
## 八、加入社区
|
||||
## 八、免责声明
|
||||
|
||||
本软件依据 MIT 许可证(MIT License)发布,免费提供,旨在“按现状”供用户使用。作者及贡献者不对使用本软件所产生的任何直接或间接后果承担责任,包括但不限于性能下降、数据丢失、服务中断、或任何其他类型的损害。
|
||||
|
||||
无任何保证:本软件不提供任何明示或暗示的保证,包括但不限于对特定用途的适用性、无侵权性、商用性及可靠性的保证。
|
||||
|
||||
用户责任:使用本软件即表示您理解并同意承担由此产生的一切风险及责任。
|
||||
|
||||
## 九、加入社区
|
||||
|
||||
- [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
|
||||
- 微信群聊(超 200 人需邀请入群,可先加作者好友)
|
||||
|
||||
<img src="https://i.imgur.com/8xwsLTA.png" width="400"/>
|
||||
|
||||
## 九、Star 趋势图
|
||||
## 十、Star 趋势图
|
||||
|
||||
[](https://starchart.cc/usual2970/certimate)
|
||||
|
||||
|
||||
60
README_EN.md
60
README_EN.md
@@ -54,13 +54,12 @@ mkdir -p ~/.certimate && cd ~/.certimate && curl -O https://raw.githubuserconten
|
||||
```bash
|
||||
git clone EMAIL:usual2970/certimate.git
|
||||
cd certimate
|
||||
go mod vendor
|
||||
go run main.go serve
|
||||
make local.run
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
After completing the installation steps above, you can access the Certimate management page by visiting http://127.0.0.1:8090 in your browser.
|
||||
After completing the installation steps above, you can access the Certimate management page by visiting <http://127.0.0.1:8090> in your browser.
|
||||
|
||||
```bash
|
||||
username:admin@certimate.fun
|
||||
@@ -71,30 +70,35 @@ password:1234567890
|
||||
|
||||
## List of Supported Providers
|
||||
|
||||
| Provider | Registration | Deployment | Remarks |
|
||||
| :-----------: | :----------: | :--------: | ------------------------------------------------------------------------------------------- |
|
||||
| Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN |
|
||||
| Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud CDN |
|
||||
| Huawei Cloud | √ | | Supports domains registered on Huawei Cloud |
|
||||
| Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud CDN |
|
||||
| CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates |
|
||||
| GoDaddy | √ | | Supports domains registered on GoDaddy |
|
||||
| Namesilo | √ | | Supports domains registered on Namesilo |
|
||||
| Local Deploy | | √ | Supports deployment to local servers |
|
||||
| SSH | | √ | Supports deployment to SSH servers |
|
||||
| Webhook | | √ | Supports callback to Webhook |
|
||||
| Provider | Registration | Deployment | Remarks |
|
||||
| :-----------: | :----------: | :--------: |-------------------------------------------------------------------------------------------------------------|
|
||||
| Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN,SLB |
|
||||
| Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud COS, CDN, ECDN, CLB, TEO |
|
||||
| Baidu Cloud | | √ | Supports deployment to Baidu Cloud CDN |
|
||||
| Huawei Cloud | √ | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB |
|
||||
| Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud CDN |
|
||||
| Doge Cloud | | √ | Supports deployment to Doge Cloud CDN |
|
||||
| Volcengine | √ | √ | Supports domains registered on Volcengine; supports deployment to Volcengine Live, CDN |
|
||||
| AWS | √ | | Supports domains managed on AWS Route53 |
|
||||
| CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates |
|
||||
| GoDaddy | √ | | Supports domains registered on GoDaddy |
|
||||
| Namesilo | √ | | Supports domains registered on Namesilo |
|
||||
| PowerDNS | √ | | Supports domains managed on PowerDNS |
|
||||
| HTTP Request | √ | | Supports domains which allow managing DNS by HTTP request |
|
||||
| Local Deploy | | √ | Supports deployment to local servers |
|
||||
| SSH | | √ | Supports deployment to SSH servers |
|
||||
| Webhook | | √ | Supports callback to Webhook |
|
||||
| Kubernetes | | √ | Supports deployment to Kubernetes Secret |
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
<div align="center">
|
||||
<img src="https://i.imgur.com/SYjjbql.jpeg" title="Login page" width="95%"/>
|
||||
<img src="https://i.imgur.com/WMVbBId.jpeg" title="Dashboard page" width="47%"/>
|
||||
<img src="https://i.imgur.com/8wit3ZA.jpeg" title="Domains page" width="47%"/>
|
||||
<img src="https://i.imgur.com/EWtOoJ0.jpeg" title="Accesses page" width="47%"/>
|
||||
<img src="https://i.imgur.com/aaPtSW7.jpeg" title="History page" width="47%"/>
|
||||
</div>
|
||||
|
||||
## Concepts
|
||||
|
||||
@@ -166,6 +170,14 @@ You can support the development of Certimate in the following ways:
|
||||
|
||||
Support for more service providers, UI enhancements, bug fixes, and documentation improvements are all welcome. We encourage everyone to submit pull requests (PRs).
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This software is provided under the MIT License and distributed “as-is” without any warranty of any kind. The authors and contributors are not responsible for any damages or losses resulting from the use or inability to use this software, including but not limited to data loss, business interruption, or any other potential harm.
|
||||
|
||||
No Warranties: This software comes without any express or implied warranties, including but not limited to implied warranties of merchantability, fitness for a particular purpose, and non-infringement.
|
||||
|
||||
User Responsibility: By using this software, you agree to take full responsibility for any outcomes resulting from its use.
|
||||
|
||||
## Join the Community
|
||||
|
||||
- [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
|
||||
|
||||
129
go.mod
129
go.mod
@@ -1,27 +1,41 @@
|
||||
module certimate
|
||||
module github.com/usual2970/certimate
|
||||
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.23.2
|
||||
|
||||
require (
|
||||
github.com/alibabacloud-go/alb-20200616/v2 v2.2.1
|
||||
github.com/alibabacloud-go/cas-20200407/v3 v3.0.1
|
||||
github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10
|
||||
github.com/alibabacloud-go/nlb-20220430/v2 v2.0.3
|
||||
github.com/alibabacloud-go/slb-20140515/v4 v4.0.9
|
||||
github.com/alibabacloud-go/tea v1.2.2
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.6
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/go-acme/lego/v4 v4.19.2
|
||||
github.com/baidubce/bce-sdk-go v0.9.197
|
||||
github.com/go-acme/lego/v4 v4.20.2
|
||||
github.com/gojek/heimdall/v7 v7.0.3
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.120
|
||||
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
|
||||
github.com/nikoksr/notify v1.0.0
|
||||
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/pocketbase/dbx v1.10.1
|
||||
github.com/pocketbase/pocketbase v0.22.18
|
||||
github.com/qiniu/go-sdk/v7 v7.22.0
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1031
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1034
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
|
||||
golang.org/x/crypto v0.27.0
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030
|
||||
github.com/volcengine/volc-sdk-golang v1.0.184
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||
k8s.io/api v0.31.1
|
||||
k8s.io/apimachinery v0.31.1
|
||||
k8s.io/client-go v0.31.1
|
||||
software.sslmate.com/src/go-pkcs12 v0.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -29,61 +43,88 @@ require (
|
||||
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect
|
||||
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect
|
||||
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.46.0 // indirect
|
||||
github.com/blinkbean/dingtalk v1.1.3 // indirect
|
||||
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.35 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-lark/lark v1.14.1 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/imdario/mergo v0.3.6 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.mongodb.org/mongo-driver v1.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/dcdn-20180115/v3 v3.4.2
|
||||
github.com/alibabacloud-go/debug v1.0.0 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.4.5 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 // indirect
|
||||
github.com/aliyun/credentials-go v1.3.1 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.47 // indirect
|
||||
github.com/aliyun/credentials-go v1.3.10 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.33 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.42 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
|
||||
github.com/aws/smithy-go v1.20.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.66.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 // indirect
|
||||
github.com/aws/smithy-go v1.22.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.5.6 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.104.0 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.108.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
|
||||
github.com/domodwyer/mailyak/v3 v3.6.2
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
|
||||
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
@@ -104,14 +145,14 @@ require (
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/cobra v1.8.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1034 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
@@ -119,19 +160,19 @@ require (
|
||||
gocloud.dev v0.37.0 // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/net v0.29.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/term v0.24.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/term v0.25.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.25.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||
google.golang.org/api v0.197.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/grpc v1.66.1 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
google.golang.org/api v0.204.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
|
||||
google.golang.org/grpc v1.67.1 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e // indirect
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
||||
|
||||
"certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type aliyun struct {
|
||||
|
||||
@@ -7,8 +7,15 @@ import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
"github.com/usual2970/certimate/internal/repository"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
@@ -16,34 +23,38 @@ import (
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"certimate/internal/domain"
|
||||
"certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
const (
|
||||
configTypeAliyun = "aliyun"
|
||||
configTypeTencent = "tencent"
|
||||
configTypeHuaweicloud = "huaweicloud"
|
||||
configTypeHuaweiCloud = "huaweicloud"
|
||||
configTypeAws = "aws"
|
||||
configTypeCloudflare = "cloudflare"
|
||||
configTypeNamesilo = "namesilo"
|
||||
configTypeGodaddy = "godaddy"
|
||||
configTypePdns = "pdns"
|
||||
configTypeHttpreq = "httpreq"
|
||||
configTypeVolcengine = "volcengine"
|
||||
)
|
||||
|
||||
const defaultSSLProvider = "letsencrypt"
|
||||
const (
|
||||
sslProviderLetsencrypt = "letsencrypt"
|
||||
sslProviderZeroSSL = "zerossl"
|
||||
sslProviderGts = "gts"
|
||||
)
|
||||
|
||||
const (
|
||||
zerosslUrl = "https://acme.zerossl.com/v2/DV90"
|
||||
letsencryptUrl = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
gtsUrl = "https://dv.acme-v02.api.pki.goog/directory"
|
||||
)
|
||||
|
||||
var sslProviderUrls = map[string]string{
|
||||
sslProviderLetsencrypt: letsencryptUrl,
|
||||
sslProviderZeroSSL: zerosslUrl,
|
||||
sslProviderGts: gtsUrl,
|
||||
}
|
||||
|
||||
const defaultEmail = "536464346@qq.com"
|
||||
@@ -60,18 +71,47 @@ type Certificate struct {
|
||||
}
|
||||
|
||||
type ApplyOption struct {
|
||||
Email string `json:"email"`
|
||||
Domain string `json:"domain"`
|
||||
Access string `json:"access"`
|
||||
KeyAlgorithm string `json:"keyAlgorithm"`
|
||||
Nameservers string `json:"nameservers"`
|
||||
Timeout int64 `json:"timeout"`
|
||||
Email string `json:"email"`
|
||||
Domain string `json:"domain"`
|
||||
Access string `json:"access"`
|
||||
KeyAlgorithm string `json:"keyAlgorithm"`
|
||||
Nameservers string `json:"nameservers"`
|
||||
Timeout int64 `json:"timeout"`
|
||||
DisableFollowCNAME bool `json:"disableFollowCNAME"`
|
||||
}
|
||||
|
||||
type ApplyUser struct {
|
||||
Ca string
|
||||
Email string
|
||||
Registration *registration.Resource
|
||||
key crypto.PrivateKey
|
||||
key string
|
||||
}
|
||||
|
||||
func newApplyUser(ca, email string) (*ApplyUser, error) {
|
||||
repo := getAcmeAccountRepository()
|
||||
rs := &ApplyUser{
|
||||
Ca: ca,
|
||||
Email: email,
|
||||
}
|
||||
resp, err := repo.GetByCAAndEmail(ca, email)
|
||||
if err != nil {
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyStr, err := x509.ConvertECPrivateKeyToPEM(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rs.key = keyStr
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
rs.Registration = resp.Resource
|
||||
rs.key = resp.Key
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func (u *ApplyUser) GetEmail() string {
|
||||
@@ -83,6 +123,15 @@ func (u ApplyUser) GetRegistration() *registration.Resource {
|
||||
}
|
||||
|
||||
func (u *ApplyUser) GetPrivateKey() crypto.PrivateKey {
|
||||
rs, _ := x509.ParseECPrivateKeyFromPEM(u.key)
|
||||
return rs
|
||||
}
|
||||
|
||||
func (u *ApplyUser) hasRegistration() bool {
|
||||
return u.Registration != nil
|
||||
}
|
||||
|
||||
func (u *ApplyUser) getPrivateKeyString() string {
|
||||
return u.key
|
||||
}
|
||||
|
||||
@@ -112,12 +161,13 @@ func Get(record *models.Record) (Applicant, error) {
|
||||
}
|
||||
|
||||
option := &ApplyOption{
|
||||
Email: applyConfig.Email,
|
||||
Domain: record.GetString("domain"),
|
||||
Access: access.GetString("config"),
|
||||
KeyAlgorithm: applyConfig.KeyAlgorithm,
|
||||
Nameservers: applyConfig.Nameservers,
|
||||
Timeout: applyConfig.Timeout,
|
||||
Email: applyConfig.Email,
|
||||
Domain: record.GetString("domain"),
|
||||
Access: access.GetString("config"),
|
||||
KeyAlgorithm: applyConfig.KeyAlgorithm,
|
||||
Nameservers: applyConfig.Nameservers,
|
||||
Timeout: applyConfig.Timeout,
|
||||
DisableFollowCNAME: applyConfig.DisableFollowCNAME,
|
||||
}
|
||||
|
||||
switch access.GetString("configType") {
|
||||
@@ -125,14 +175,22 @@ func Get(record *models.Record) (Applicant, error) {
|
||||
return NewAliyun(option), nil
|
||||
case configTypeTencent:
|
||||
return NewTencent(option), nil
|
||||
case configTypeHuaweicloud:
|
||||
case configTypeHuaweiCloud:
|
||||
return NewHuaweiCloud(option), nil
|
||||
case configTypeAws:
|
||||
return NewAws(option), nil
|
||||
case configTypeCloudflare:
|
||||
return NewCloudflare(option), nil
|
||||
case configTypeNamesilo:
|
||||
return NewNamesilo(option), nil
|
||||
case configTypeGodaddy:
|
||||
return NewGodaddy(option), nil
|
||||
case configTypePdns:
|
||||
return NewPdns(option), nil
|
||||
case configTypeHttpreq:
|
||||
return NewHttpreq(option), nil
|
||||
case configTypeVolcengine:
|
||||
return NewVolcengine(option), nil
|
||||
default:
|
||||
return nil, errors.New("unknown config type")
|
||||
}
|
||||
@@ -144,10 +202,13 @@ type SSLProviderConfig struct {
|
||||
}
|
||||
|
||||
type SSLProviderConfigContent struct {
|
||||
Zerossl struct {
|
||||
EabHmacKey string `json:"eabHmacKey"`
|
||||
EabKid string `json:"eabKid"`
|
||||
}
|
||||
Zerossl SSLProviderEab `json:"zerossl"`
|
||||
Gts SSLProviderEab `json:"gts"`
|
||||
}
|
||||
|
||||
type SSLProviderEab struct {
|
||||
EabHmacKey string `json:"eabHmacKey"`
|
||||
EabKid string `json:"eabKid"`
|
||||
}
|
||||
|
||||
func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, error) {
|
||||
@@ -163,17 +224,16 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
|
||||
}
|
||||
}
|
||||
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
// Some unified lego environment variables are configured here.
|
||||
// link: https://github.com/go-acme/lego/issues/1867
|
||||
os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(option.DisableFollowCNAME))
|
||||
|
||||
myUser, err := newApplyUser(sslProvider.Provider, option.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
myUser := ApplyUser{
|
||||
Email: option.Email,
|
||||
key: privateKey,
|
||||
}
|
||||
|
||||
config := lego.NewConfig(&myUser)
|
||||
config := lego.NewConfig(myUser)
|
||||
|
||||
// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM.
|
||||
config.CADirURL = sslProviderUrls[sslProvider.Provider]
|
||||
@@ -194,11 +254,13 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
|
||||
client.Challenge.SetDNS01Provider(provider, challengeOptions...)
|
||||
|
||||
// New users will need to register
|
||||
reg, err := getReg(client, sslProvider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to register: %w", err)
|
||||
if !myUser.hasRegistration() {
|
||||
reg, err := getReg(client, sslProvider, myUser)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to register: %w", err)
|
||||
}
|
||||
myUser.Registration = reg
|
||||
}
|
||||
myUser.Registration = reg
|
||||
|
||||
domains := strings.Split(option.Domain, ";")
|
||||
request := certificate.ObtainRequest{
|
||||
@@ -220,7 +282,16 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getReg(client *lego.Client, sslProvider *SSLProviderConfig) (*registration.Resource, error) {
|
||||
type AcmeAccountRepository interface {
|
||||
GetByCAAndEmail(ca, email string) (*domain.AcmeAccount, error)
|
||||
Save(ca, email, key string, resource *registration.Resource) error
|
||||
}
|
||||
|
||||
func getAcmeAccountRepository() AcmeAccountRepository {
|
||||
return repository.NewAcmeAccountRepository()
|
||||
}
|
||||
|
||||
func getReg(client *lego.Client, sslProvider *SSLProviderConfig, user *ApplyUser) (*registration.Resource, error) {
|
||||
var reg *registration.Resource
|
||||
var err error
|
||||
switch sslProvider.Provider {
|
||||
@@ -230,6 +301,12 @@ func getReg(client *lego.Client, sslProvider *SSLProviderConfig) (*registration.
|
||||
Kid: sslProvider.Config.Zerossl.EabKid,
|
||||
HmacEncoded: sslProvider.Config.Zerossl.EabHmacKey,
|
||||
})
|
||||
case sslProviderGts:
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: sslProvider.Config.Gts.EabKid,
|
||||
HmacEncoded: sslProvider.Config.Gts.EabHmacKey,
|
||||
})
|
||||
|
||||
case sslProviderLetsencrypt:
|
||||
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
@@ -242,6 +319,18 @@ func getReg(client *lego.Client, sslProvider *SSLProviderConfig) (*registration.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repo := getAcmeAccountRepository()
|
||||
|
||||
resp, err := repo.GetByCAAndEmail(sslProvider.Provider, user.GetEmail())
|
||||
if err == nil {
|
||||
user.key = resp.Key
|
||||
return resp.Resource, nil
|
||||
}
|
||||
|
||||
if err := repo.Save(sslProvider.Provider, user.GetEmail(), user.getPrivateKeyString(), reg); err != nil {
|
||||
return nil, fmt.Errorf("failed to save registration: %w", err)
|
||||
}
|
||||
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
|
||||
39
internal/applicant/aws.go
Normal file
39
internal/applicant/aws.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/route53"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type aws struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewAws(option *ApplyOption) Applicant {
|
||||
return &aws{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *aws) Apply() (*Certificate, error) {
|
||||
access := &domain.AwsAccess{}
|
||||
json.Unmarshal([]byte(t.option.Access), access)
|
||||
|
||||
os.Setenv("AWS_REGION", access.Region)
|
||||
os.Setenv("AWS_ACCESS_KEY_ID", access.AccessKeyId)
|
||||
os.Setenv("AWS_SECRET_ACCESS_KEY", access.SecretAccessKey)
|
||||
os.Setenv("AWS_HOSTED_ZONE_ID", access.HostedZoneId)
|
||||
os.Setenv("AWS_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
|
||||
|
||||
dnsProvider, err := route53.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(t.option, dnsProvider)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
cf "github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
|
||||
"certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type cloudflare struct {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
godaddyProvider "github.com/go-acme/lego/v4/providers/dns/godaddy"
|
||||
|
||||
"certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type godaddy struct {
|
||||
|
||||
38
internal/applicant/httpreq.go
Normal file
38
internal/applicant/httpreq.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/httpreq"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type httpReq struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewHttpreq(option *ApplyOption) Applicant {
|
||||
return &httpReq{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *httpReq) Apply() (*Certificate, error) {
|
||||
access := &domain.HttpreqAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("HTTPREQ_ENDPOINT", access.Endpoint)
|
||||
os.Setenv("HTTPREQ_MODE", access.Mode)
|
||||
os.Setenv("HTTPREQ_USERNAME", access.Username)
|
||||
os.Setenv("HTTPREQ_PASSWORD", access.Password)
|
||||
os.Setenv("HTTPREQ_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
dnsProvider, err := httpreq.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(a.option, dnsProvider)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
huaweicloudProvider "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
|
||||
|
||||
"certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type huaweicloud struct {
|
||||
@@ -24,7 +24,12 @@ func (t *huaweicloud) Apply() (*Certificate, error) {
|
||||
access := &domain.HuaweiCloudAccess{}
|
||||
json.Unmarshal([]byte(t.option.Access), access)
|
||||
|
||||
os.Setenv("HUAWEICLOUD_REGION", access.Region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
|
||||
region := access.Region
|
||||
if region == "" {
|
||||
region = "cn-north-1"
|
||||
}
|
||||
|
||||
os.Setenv("HUAWEICLOUD_REGION", region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
|
||||
os.Setenv("HUAWEICLOUD_ACCESS_KEY_ID", access.AccessKeyId)
|
||||
os.Setenv("HUAWEICLOUD_SECRET_ACCESS_KEY", access.SecretAccessKey)
|
||||
os.Setenv("HUAWEICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
namesiloProvider "github.com/go-acme/lego/v4/providers/dns/namesilo"
|
||||
|
||||
"certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type namesilo struct {
|
||||
|
||||
36
internal/applicant/pdns.go
Normal file
36
internal/applicant/pdns.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/pdns"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type powerdns struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewPdns(option *ApplyOption) Applicant {
|
||||
return &powerdns{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *powerdns) Apply() (*Certificate, error) {
|
||||
access := &domain.PdnsAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("PDNS_API_URL", access.ApiUrl)
|
||||
os.Setenv("PDNS_API_KEY", access.ApiKey)
|
||||
os.Setenv("PDNS_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
dnsProvider, err := pdns.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(a.option, dnsProvider)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
||||
|
||||
"certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type tencent struct {
|
||||
|
||||
35
internal/applicant/volcengine.go
Normal file
35
internal/applicant/volcengine.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
volcengineDns "github.com/go-acme/lego/v4/providers/dns/volcengine"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type volcengine struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewVolcengine(option *ApplyOption) Applicant {
|
||||
return &volcengine{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *volcengine) Apply() (*Certificate, error) {
|
||||
access := &domain.VolcEngineAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("VOLC_ACCESSKEY", access.AccessKeyId)
|
||||
os.Setenv("VOLC_SECRETKEY", access.SecretAccessKey)
|
||||
os.Setenv("VOLC_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
dnsProvider, err := volcengineDns.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(a.option, dnsProvider)
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
|
||||
"certimate/internal/domain"
|
||||
)
|
||||
|
||||
type aliyun struct {
|
||||
client *oss.Client
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewAliyun(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
a := &aliyun{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}
|
||||
client, err := a.createClient(access.AccessKeyId, access.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.client = client
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *aliyun) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
|
||||
}
|
||||
|
||||
func (a *aliyun) GetInfo() []string {
|
||||
return a.infos
|
||||
}
|
||||
|
||||
func (a *aliyun) Deploy(ctx context.Context) error {
|
||||
err := a.client.PutBucketCnameWithCertificate(getDeployString(a.option.DeployConfig, "bucket"), oss.PutBucketCname{
|
||||
Cname: getDeployString(a.option.DeployConfig, "domain"),
|
||||
CertificateConfiguration: &oss.CertificateConfiguration{
|
||||
Certificate: a.option.Certificate.Certificate,
|
||||
PrivateKey: a.option.Certificate.PrivateKey,
|
||||
Force: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("deploy aliyun oss error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *aliyun) createClient(accessKeyId, accessKeySecret string) (*oss.Client, error) {
|
||||
client, err := oss.New(
|
||||
getDeployString(a.option.DeployConfig, "endpoint"),
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create aliyun client error: %w", err)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
281
internal/deployer/aliyun_alb.go
Normal file
281
internal/deployer/aliyun_alb.go
Normal file
@@ -0,0 +1,281 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
aliyunAlb "github.com/alibabacloud-go/alb-20200616/v2/client"
|
||||
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploaderAliyunCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
|
||||
)
|
||||
|
||||
type AliyunALBDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *aliyunAlb.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewAliyunALBDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
client, err := (&AliyunALBDeployer{}).createSdkClient(
|
||||
access.AccessKeyId,
|
||||
access.AccessKeySecret,
|
||||
option.DeployConfig.GetConfigAsString("region"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
aliCasRegion := option.DeployConfig.GetConfigAsString("region")
|
||||
if aliCasRegion != "" {
|
||||
// 阿里云 CAS 服务接入点是独立于 ALB 服务的
|
||||
// 国内版接入点:华东一杭州
|
||||
// 国际版接入点:亚太东南一新加坡
|
||||
if !strings.HasPrefix(aliCasRegion, "cn-") {
|
||||
aliCasRegion = "ap-southeast-1"
|
||||
} else {
|
||||
aliCasRegion = "cn-hangzhou"
|
||||
}
|
||||
}
|
||||
uploader, err := uploaderAliyunCas.New(&uploaderAliyunCas.AliyunCASUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: aliCasRegion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &AliyunALBDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) Deploy(ctx context.Context) error {
|
||||
switch d.option.DeployConfig.GetConfigAsString("resourceType") {
|
||||
case "loadbalancer":
|
||||
if err := d.deployToLoadbalancer(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
case "listener":
|
||||
if err := d.deployToListener(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported resource type")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunAlb.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-hangzhou" // ALB 服务默认区域:华东一杭州
|
||||
}
|
||||
|
||||
aConfig := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
|
||||
var endpoint string
|
||||
switch region {
|
||||
case "cn-hangzhou-finance":
|
||||
endpoint = "alb.cn-hangzhou.aliyuncs.com"
|
||||
default:
|
||||
endpoint = fmt.Sprintf("alb.%s.aliyuncs.com", region)
|
||||
}
|
||||
aConfig.Endpoint = tea.String(endpoint)
|
||||
|
||||
client, err := aliyunAlb.NewClient(aConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error {
|
||||
aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
|
||||
if aliLoadbalancerId == "" {
|
||||
return errors.New("`loadbalancerId` is required")
|
||||
}
|
||||
|
||||
aliListenerIds := make([]string, 0)
|
||||
|
||||
// 查询负载均衡实例的详细信息
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getloadbalancerattribute
|
||||
getLoadBalancerAttributeReq := &aliyunAlb.GetLoadBalancerAttributeRequest{
|
||||
LoadBalancerId: tea.String(aliLoadbalancerId),
|
||||
}
|
||||
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetLoadBalancerAttribute'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例", getLoadBalancerAttributeResp))
|
||||
|
||||
// 查询 HTTPS 监听列表
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
|
||||
listListenersPage := 1
|
||||
listListenersLimit := int32(100)
|
||||
var listListenersToken *string = nil
|
||||
for {
|
||||
listListenersReq := &aliyunAlb.ListListenersRequest{
|
||||
MaxResults: tea.Int32(listListenersLimit),
|
||||
NextToken: listListenersToken,
|
||||
LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)},
|
||||
ListenerProtocol: tea.String("HTTPS"),
|
||||
}
|
||||
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'")
|
||||
}
|
||||
|
||||
if listListenersResp.Body.Listeners != nil {
|
||||
for _, listener := range listListenersResp.Body.Listeners {
|
||||
aliListenerIds = append(aliListenerIds, *listener.ListenerId)
|
||||
}
|
||||
}
|
||||
|
||||
if listListenersResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenersToken = listListenersResp.Body.NextToken
|
||||
listListenersPage += 1
|
||||
}
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例下的全部 HTTPS 监听", aliListenerIds))
|
||||
|
||||
// 查询 QUIC 监听列表
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
|
||||
listListenersPage = 1
|
||||
listListenersToken = nil
|
||||
for {
|
||||
listListenersReq := &aliyunAlb.ListListenersRequest{
|
||||
MaxResults: tea.Int32(listListenersLimit),
|
||||
NextToken: listListenersToken,
|
||||
LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)},
|
||||
ListenerProtocol: tea.String("QUIC"),
|
||||
}
|
||||
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'")
|
||||
}
|
||||
|
||||
if listListenersResp.Body.Listeners != nil {
|
||||
for _, listener := range listListenersResp.Body.Listeners {
|
||||
aliListenerIds = append(aliListenerIds, *listener.ListenerId)
|
||||
}
|
||||
}
|
||||
|
||||
if listListenersResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenersToken = listListenersResp.Body.NextToken
|
||||
listListenersPage += 1
|
||||
}
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例下的全部 QUIC 监听", aliListenerIds))
|
||||
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 批量更新监听证书
|
||||
var errs []error
|
||||
for _, aliListenerId := range aliListenerIds {
|
||||
if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) deployToListener(ctx context.Context) error {
|
||||
aliListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
|
||||
if aliListenerId == "" {
|
||||
return errors.New("`listenerId` is required")
|
||||
}
|
||||
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 更新监听
|
||||
if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) updateListenerCertificate(ctx context.Context, aliListenerId string, aliCertId string) error {
|
||||
// 查询监听的属性
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getlistenerattribute
|
||||
getListenerAttributeReq := &aliyunAlb.GetListenerAttributeRequest{
|
||||
ListenerId: tea.String(aliListenerId),
|
||||
}
|
||||
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetListenerAttribute'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到 ALB 监听配置", getListenerAttributeResp))
|
||||
|
||||
// 修改监听的属性
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-updatelistenerattribute
|
||||
updateListenerAttributeReq := &aliyunAlb.UpdateListenerAttributeRequest{
|
||||
ListenerId: tea.String(aliListenerId),
|
||||
Certificates: []*aliyunAlb.UpdateListenerAttributeRequestCertificates{{
|
||||
CertificateId: tea.String(aliCertId),
|
||||
}},
|
||||
}
|
||||
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.UpdateListenerAttribute'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已更新 ALB 监听配置", updateListenerAttributeResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -4,79 +4,85 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
cdn20180510 "github.com/alibabacloud-go/cdn-20180510/v5/client"
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
aliyunCdn "github.com/alibabacloud-go/cdn-20180510/v5/client"
|
||||
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"certimate/internal/domain"
|
||||
"certimate/internal/utils/rand"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type AliyunCdn struct {
|
||||
client *cdn20180510.Client
|
||||
type AliyunCDNDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *aliyunCdn.Client
|
||||
}
|
||||
|
||||
func NewAliyunCdn(option *DeployerOption) (*AliyunCdn, error) {
|
||||
func NewAliyunCDNDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
a := &AliyunCdn{
|
||||
option: option,
|
||||
}
|
||||
client, err := a.createClient(access.AccessKeyId, access.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
return &AliyunCdn{
|
||||
client: client,
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
client, err := (&AliyunCDNDeployer{}).createSdkClient(
|
||||
access.AccessKeyId,
|
||||
access.AccessKeySecret,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
return &AliyunCDNDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *AliyunCdn) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
|
||||
func (d *AliyunCDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (a *AliyunCdn) GetInfo() []string {
|
||||
return a.infos
|
||||
func (d *AliyunCDNDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (a *AliyunCdn) Deploy(ctx context.Context) error {
|
||||
certName := fmt.Sprintf("%s-%s-%s", a.option.Domain, a.option.DomainId, rand.RandStr(6))
|
||||
setCdnDomainSSLCertificateRequest := &cdn20180510.SetCdnDomainSSLCertificateRequest{
|
||||
DomainName: tea.String(getDeployString(a.option.DeployConfig, "domain")),
|
||||
CertName: tea.String(certName),
|
||||
func (d *AliyunCDNDeployer) Deploy(ctx context.Context) error {
|
||||
// 设置 CDN 域名域名证书
|
||||
// REF: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-setcdndomainsslcertificate
|
||||
setCdnDomainSSLCertificateReq := &aliyunCdn.SetCdnDomainSSLCertificateRequest{
|
||||
DomainName: tea.String(d.option.DeployConfig.GetConfigAsString("domain")),
|
||||
CertRegion: tea.String(d.option.DeployConfig.GetConfigOrDefaultAsString("region", "cn-hangzhou")),
|
||||
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
|
||||
CertType: tea.String("upload"),
|
||||
SSLProtocol: tea.String("on"),
|
||||
SSLPub: tea.String(a.option.Certificate.Certificate),
|
||||
SSLPri: tea.String(a.option.Certificate.PrivateKey),
|
||||
CertRegion: tea.String("cn-hangzhou"),
|
||||
SSLPub: tea.String(d.option.Certificate.Certificate),
|
||||
SSLPri: tea.String(d.option.Certificate.PrivateKey),
|
||||
}
|
||||
|
||||
runtime := &util.RuntimeOptions{}
|
||||
|
||||
resp, err := a.client.SetCdnDomainSSLCertificateWithOptions(setCdnDomainSSLCertificateRequest, runtime)
|
||||
setCdnDomainSSLCertificateResp, err := d.sdkClient.SetCdnDomainSSLCertificate(setCdnDomainSSLCertificateReq)
|
||||
if err != nil {
|
||||
return err
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.SetCdnDomainSSLCertificate'")
|
||||
}
|
||||
|
||||
a.infos = append(a.infos, toStr("cdn设置证书", resp))
|
||||
d.infos = append(d.infos, toStr("已设置 CDN 域名证书", setCdnDomainSSLCertificateResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AliyunCdn) createClient(accessKeyId, accessKeySecret string) (_result *cdn20180510.Client, _err error) {
|
||||
config := &openapi.Config{
|
||||
func (d *AliyunCDNDeployer) createSdkClient(accessKeyId, accessKeySecret string) (*aliyunCdn.Client, error) {
|
||||
aConfig := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
Endpoint: tea.String("cdn.aliyuncs.com"),
|
||||
}
|
||||
config.Endpoint = tea.String("cdn.aliyuncs.com")
|
||||
_result = &cdn20180510.Client{}
|
||||
_result, _err = cdn20180510.NewClient(config)
|
||||
return _result, _err
|
||||
|
||||
client, err := aliyunCdn.NewClient(aConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
286
internal/deployer/aliyun_clb.go
Normal file
286
internal/deployer/aliyun_clb.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
aliyunSlb "github.com/alibabacloud-go/slb-20140515/v4/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploaderAliyunSlb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-slb"
|
||||
)
|
||||
|
||||
type AliyunCLBDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *aliyunSlb.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewAliyunCLBDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
client, err := (&AliyunCLBDeployer{}).createSdkClient(
|
||||
access.AccessKeyId,
|
||||
access.AccessKeySecret,
|
||||
option.DeployConfig.GetConfigAsString("region"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
uploader, err := uploaderAliyunSlb.New(&uploaderAliyunSlb.AliyunSLBUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: option.DeployConfig.GetConfigAsString("region"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &AliyunCLBDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) Deploy(ctx context.Context) error {
|
||||
switch d.option.DeployConfig.GetConfigAsString("resourceType") {
|
||||
case "loadbalancer":
|
||||
if err := d.deployToLoadbalancer(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
case "listener":
|
||||
if err := d.deployToListener(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported resource type")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-hangzhou" // CLB(SLB) 服务默认区域:华东一杭州
|
||||
}
|
||||
|
||||
aConfig := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
|
||||
var endpoint string
|
||||
switch region {
|
||||
case
|
||||
"cn-hangzhou",
|
||||
"cn-hangzhou-finance",
|
||||
"cn-shanghai-finance-1",
|
||||
"cn-shenzhen-finance-1":
|
||||
endpoint = "slb.aliyuncs.com"
|
||||
default:
|
||||
endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region)
|
||||
}
|
||||
aConfig.Endpoint = tea.String(endpoint)
|
||||
|
||||
client, err := aliyunSlb.NewClient(aConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context) error {
|
||||
aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
|
||||
aliListenerPorts := make([]int32, 0)
|
||||
if aliLoadbalancerId == "" {
|
||||
return errors.New("`loadbalancerId` is required")
|
||||
}
|
||||
|
||||
// 查询负载均衡实例的详细信息
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute
|
||||
describeLoadBalancerAttributeReq := &aliyunSlb.DescribeLoadBalancerAttributeRequest{
|
||||
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
|
||||
LoadBalancerId: tea.String(aliLoadbalancerId),
|
||||
}
|
||||
describeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttribute(describeLoadBalancerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerAttribute'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到 CLB 负载均衡实例", describeLoadBalancerAttributeResp))
|
||||
|
||||
// 查询 HTTPS 监听列表
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerlisteners
|
||||
listListenersPage := 1
|
||||
listListenersLimit := int32(100)
|
||||
var listListenersToken *string = nil
|
||||
for {
|
||||
describeLoadBalancerListenersReq := &aliyunSlb.DescribeLoadBalancerListenersRequest{
|
||||
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
|
||||
MaxResults: tea.Int32(listListenersLimit),
|
||||
NextToken: listListenersToken,
|
||||
LoadBalancerId: []*string{tea.String(aliLoadbalancerId)},
|
||||
ListenerProtocol: tea.String("https"),
|
||||
}
|
||||
describeLoadBalancerListenersResp, err := d.sdkClient.DescribeLoadBalancerListeners(describeLoadBalancerListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerListeners'")
|
||||
}
|
||||
|
||||
if describeLoadBalancerListenersResp.Body.Listeners != nil {
|
||||
for _, listener := range describeLoadBalancerListenersResp.Body.Listeners {
|
||||
aliListenerPorts = append(aliListenerPorts, *listener.ListenerPort)
|
||||
}
|
||||
}
|
||||
|
||||
if describeLoadBalancerListenersResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenersToken = describeLoadBalancerListenersResp.Body.NextToken
|
||||
listListenersPage += 1
|
||||
}
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到 CLB 负载均衡实例下的全部 HTTPS 监听", aliListenerPorts))
|
||||
|
||||
// 上传证书到 SLB
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 批量更新监听证书
|
||||
var errs []error
|
||||
for _, aliListenerPort := range aliListenerPorts {
|
||||
if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, upres.CertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) deployToListener(ctx context.Context) error {
|
||||
aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
|
||||
if aliLoadbalancerId == "" {
|
||||
return errors.New("`loadbalancerId` is required")
|
||||
}
|
||||
|
||||
aliListenerPort := d.option.DeployConfig.GetConfigAsInt32("listenerPort")
|
||||
if aliListenerPort == 0 {
|
||||
return errors.New("`listenerPort` is required")
|
||||
}
|
||||
|
||||
// 上传证书到 SLB
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 更新监听
|
||||
if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, upres.CertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, aliLoadbalancerId string, aliListenerPort int32, aliCertId string) error {
|
||||
// 查询监听配置
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerhttpslistenerattribute
|
||||
describeLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.DescribeLoadBalancerHTTPSListenerAttributeRequest{
|
||||
LoadBalancerId: tea.String(aliLoadbalancerId),
|
||||
ListenerPort: tea.Int32(aliListenerPort),
|
||||
}
|
||||
describeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttribute(describeLoadBalancerHTTPSListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到 CLB HTTPS 监听配置", describeLoadBalancerHTTPSListenerAttributeResp))
|
||||
|
||||
// 查询扩展域名
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describedomainextensions
|
||||
describeDomainExtensionsReq := &aliyunSlb.DescribeDomainExtensionsRequest{
|
||||
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
|
||||
LoadBalancerId: tea.String(aliLoadbalancerId),
|
||||
ListenerPort: tea.Int32(aliListenerPort),
|
||||
}
|
||||
describeDomainExtensionsResp, err := d.sdkClient.DescribeDomainExtensions(describeDomainExtensionsReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeDomainExtensions'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到 CLB 扩展域名", describeDomainExtensionsResp))
|
||||
|
||||
// 遍历修改扩展域名
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setdomainextensionattribute
|
||||
//
|
||||
// 这里仅修改跟被替换证书一致的扩展域名
|
||||
if describeDomainExtensionsResp.Body.DomainExtensions != nil && describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension != nil {
|
||||
for _, domainExtension := range describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension {
|
||||
if *domainExtension.ServerCertificateId != *describeLoadBalancerHTTPSListenerAttributeResp.Body.ServerCertificateId {
|
||||
continue
|
||||
}
|
||||
|
||||
setDomainExtensionAttributeReq := &aliyunSlb.SetDomainExtensionAttributeRequest{
|
||||
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
|
||||
DomainExtensionId: tea.String(*domainExtension.DomainExtensionId),
|
||||
ServerCertificateId: tea.String(aliCertId),
|
||||
}
|
||||
_, err := d.sdkClient.SetDomainExtensionAttribute(setDomainExtensionAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetDomainExtensionAttribute'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改监听配置
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute
|
||||
//
|
||||
// 注意修改监听配置要放在修改扩展域名之后
|
||||
setLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.SetLoadBalancerHTTPSListenerAttributeRequest{
|
||||
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
|
||||
LoadBalancerId: tea.String(aliLoadbalancerId),
|
||||
ListenerPort: tea.Int32(aliListenerPort),
|
||||
ServerCertificateId: tea.String(aliCertId),
|
||||
}
|
||||
setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.SetLoadBalancerHTTPSListenerAttribute(setLoadBalancerHTTPSListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已更新 CLB HTTPS 监听配置", setLoadBalancerHTTPSListenerAttributeResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
95
internal/deployer/aliyun_dcdn.go
Normal file
95
internal/deployer/aliyun_dcdn.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
aliyunDcdn "github.com/alibabacloud-go/dcdn-20180115/v3/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type AliyunDCDNDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *aliyunDcdn.Client
|
||||
}
|
||||
|
||||
func NewAliyunDCDNDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
client, err := (&AliyunDCDNDeployer{}).createSdkClient(
|
||||
access.AccessKeyId,
|
||||
access.AccessKeySecret,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
return &AliyunDCDNDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunDCDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *AliyunDCDNDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *AliyunDCDNDeployer) Deploy(ctx context.Context) error {
|
||||
// 支持泛解析域名,在 Aliyun DCDN 中泛解析域名表示为 .example.com
|
||||
domain := d.option.DeployConfig.GetConfigAsString("domain")
|
||||
if strings.HasPrefix(domain, "*") {
|
||||
domain = strings.TrimPrefix(domain, "*")
|
||||
}
|
||||
|
||||
// 配置域名证书
|
||||
// REF: https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-setdcdndomainsslcertificate
|
||||
setDcdnDomainSSLCertificateReq := &aliyunDcdn.SetDcdnDomainSSLCertificateRequest{
|
||||
DomainName: tea.String(domain),
|
||||
CertRegion: tea.String(d.option.DeployConfig.GetConfigOrDefaultAsString("region", "cn-hangzhou")),
|
||||
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
|
||||
CertType: tea.String("upload"),
|
||||
SSLProtocol: tea.String("on"),
|
||||
SSLPub: tea.String(d.option.Certificate.Certificate),
|
||||
SSLPri: tea.String(d.option.Certificate.PrivateKey),
|
||||
}
|
||||
setDcdnDomainSSLCertificateResp, err := d.sdkClient.SetDcdnDomainSSLCertificate(setDcdnDomainSSLCertificateReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'dcdn.SetDcdnDomainSSLCertificate'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已配置 DCDN 域名证书", setDcdnDomainSSLCertificateResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunDCDNDeployer) createSdkClient(accessKeyId, accessKeySecret string) (*aliyunDcdn.Client, error) {
|
||||
aConfig := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
Endpoint: tea.String("dcdn.aliyuncs.com"),
|
||||
}
|
||||
|
||||
client, err := aliyunDcdn.NewClient(aConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* @Author: Bin
|
||||
* @Date: 2024-09-17
|
||||
* @FilePath: /certimate/internal/deployer/aliyun_esa.go
|
||||
*/
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
dcdn20180115 "github.com/alibabacloud-go/dcdn-20180115/v3/client"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
|
||||
"certimate/internal/domain"
|
||||
"certimate/internal/utils/rand"
|
||||
)
|
||||
|
||||
type AliyunEsa struct {
|
||||
client *dcdn20180115.Client
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewAliyunEsa(option *DeployerOption) (*AliyunEsa, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
a := &AliyunEsa{
|
||||
option: option,
|
||||
}
|
||||
client, err := a.createClient(access.AccessKeyId, access.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AliyunEsa{
|
||||
client: client,
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *AliyunEsa) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
|
||||
}
|
||||
|
||||
func (a *AliyunEsa) GetInfo() []string {
|
||||
return a.infos
|
||||
}
|
||||
|
||||
func (a *AliyunEsa) Deploy(ctx context.Context) error {
|
||||
certName := fmt.Sprintf("%s-%s-%s", a.option.Domain, a.option.DomainId, rand.RandStr(6))
|
||||
setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{
|
||||
DomainName: tea.String(getDeployString(a.option.DeployConfig, "domain")),
|
||||
CertName: tea.String(certName),
|
||||
CertType: tea.String("upload"),
|
||||
SSLProtocol: tea.String("on"),
|
||||
SSLPub: tea.String(a.option.Certificate.Certificate),
|
||||
SSLPri: tea.String(a.option.Certificate.PrivateKey),
|
||||
CertRegion: tea.String("cn-hangzhou"),
|
||||
}
|
||||
|
||||
runtime := &util.RuntimeOptions{}
|
||||
|
||||
resp, err := a.client.SetDcdnDomainSSLCertificateWithOptions(setDcdnDomainSSLCertificateRequest, runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.infos = append(a.infos, toStr("dcdn设置证书", resp))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AliyunEsa) createClient(accessKeyId, accessKeySecret string) (_result *dcdn20180115.Client, _err error) {
|
||||
config := &openapi.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
config.Endpoint = tea.String("dcdn.aliyuncs.com")
|
||||
_result = &dcdn20180115.Client{}
|
||||
_result, _err = dcdn20180115.NewClient(config)
|
||||
return _result, _err
|
||||
}
|
||||
245
internal/deployer/aliyun_nlb.go
Normal file
245
internal/deployer/aliyun_nlb.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
aliyunNlb "github.com/alibabacloud-go/nlb-20220430/v2/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploaderAliyunCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
|
||||
)
|
||||
|
||||
type AliyunNLBDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *aliyunNlb.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewAliyunNLBDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
client, err := (&AliyunNLBDeployer{}).createSdkClient(
|
||||
access.AccessKeyId,
|
||||
access.AccessKeySecret,
|
||||
option.DeployConfig.GetConfigAsString("region"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
aliCasRegion := option.DeployConfig.GetConfigAsString("region")
|
||||
if aliCasRegion != "" {
|
||||
// 阿里云 CAS 服务接入点是独立于 NLB 服务的
|
||||
// 国内版接入点:华东一杭州
|
||||
// 国际版接入点:亚太东南一新加坡
|
||||
if !strings.HasPrefix(aliCasRegion, "cn-") {
|
||||
aliCasRegion = "ap-southeast-1"
|
||||
} else {
|
||||
aliCasRegion = "cn-hangzhou"
|
||||
}
|
||||
}
|
||||
uploader, err := uploaderAliyunCas.New(&uploaderAliyunCas.AliyunCASUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: aliCasRegion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &AliyunNLBDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) Deploy(ctx context.Context) error {
|
||||
switch d.option.DeployConfig.GetConfigAsString("resourceType") {
|
||||
case "loadbalancer":
|
||||
if err := d.deployToLoadbalancer(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
case "listener":
|
||||
if err := d.deployToListener(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported resource type")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunNlb.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-hangzhou" // NLB 服务默认区域:华东一杭州
|
||||
}
|
||||
|
||||
aConfig := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
|
||||
var endpoint string
|
||||
switch region {
|
||||
default:
|
||||
endpoint = fmt.Sprintf("nlb.%s.aliyuncs.com", region)
|
||||
}
|
||||
aConfig.Endpoint = tea.String(endpoint)
|
||||
|
||||
client, err := aliyunNlb.NewClient(aConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context) error {
|
||||
aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
|
||||
if aliLoadbalancerId == "" {
|
||||
return errors.New("`loadbalancerId` is required")
|
||||
}
|
||||
|
||||
aliListenerIds := make([]string, 0)
|
||||
|
||||
// 查询负载均衡实例的详细信息
|
||||
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getloadbalancerattribute
|
||||
getLoadBalancerAttributeReq := &aliyunNlb.GetLoadBalancerAttributeRequest{
|
||||
LoadBalancerId: tea.String(aliLoadbalancerId),
|
||||
}
|
||||
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetLoadBalancerAttribute'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到 NLB 负载均衡实例", getLoadBalancerAttributeResp))
|
||||
|
||||
// 查询 TCPSSL 监听列表
|
||||
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-listlisteners
|
||||
listListenersPage := 1
|
||||
listListenersLimit := int32(100)
|
||||
var listListenersToken *string = nil
|
||||
for {
|
||||
listListenersReq := &aliyunNlb.ListListenersRequest{
|
||||
MaxResults: tea.Int32(listListenersLimit),
|
||||
NextToken: listListenersToken,
|
||||
LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)},
|
||||
ListenerProtocol: tea.String("TCPSSL"),
|
||||
}
|
||||
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.ListListeners'")
|
||||
}
|
||||
|
||||
if listListenersResp.Body.Listeners != nil {
|
||||
for _, listener := range listListenersResp.Body.Listeners {
|
||||
aliListenerIds = append(aliListenerIds, *listener.ListenerId)
|
||||
}
|
||||
}
|
||||
|
||||
if listListenersResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenersToken = listListenersResp.Body.NextToken
|
||||
listListenersPage += 1
|
||||
}
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到 NLB 负载均衡实例下的全部 TCPSSL 监听", aliListenerIds))
|
||||
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 批量更新监听证书
|
||||
var errs []error
|
||||
for _, aliListenerId := range aliListenerIds {
|
||||
if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) deployToListener(ctx context.Context) error {
|
||||
aliListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
|
||||
if aliListenerId == "" {
|
||||
return errors.New("`listenerId` is required")
|
||||
}
|
||||
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 更新监听
|
||||
if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) updateListenerCertificate(ctx context.Context, aliListenerId string, aliCertId string) error {
|
||||
// 查询监听的属性
|
||||
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getlistenerattribute
|
||||
getListenerAttributeReq := &aliyunNlb.GetListenerAttributeRequest{
|
||||
ListenerId: tea.String(aliListenerId),
|
||||
}
|
||||
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetListenerAttribute'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到 NLB 监听配置", getListenerAttributeResp))
|
||||
|
||||
// 修改监听的属性
|
||||
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-updatelistenerattribute
|
||||
updateListenerAttributeReq := &aliyunNlb.UpdateListenerAttributeRequest{
|
||||
ListenerId: tea.String(aliListenerId),
|
||||
CertificateIds: []*string{tea.String(aliCertId)},
|
||||
}
|
||||
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.UpdateListenerAttribute'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已更新 NLB 监听配置", updateListenerAttributeResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
86
internal/deployer/aliyun_oss.go
Normal file
86
internal/deployer/aliyun_oss.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type AliyunOSSDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *oss.Client
|
||||
}
|
||||
|
||||
func NewAliyunOSSDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
client, err := (&AliyunOSSDeployer{}).createSdkClient(
|
||||
access.AccessKeyId,
|
||||
access.AccessKeySecret,
|
||||
option.DeployConfig.GetConfigAsString("endpoint"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
return &AliyunOSSDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunOSSDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *AliyunOSSDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *AliyunOSSDeployer) Deploy(ctx context.Context) error {
|
||||
aliBucket := d.option.DeployConfig.GetConfigAsString("bucket")
|
||||
if aliBucket == "" {
|
||||
return errors.New("`bucket` is required")
|
||||
}
|
||||
|
||||
// 为存储空间绑定自定义域名
|
||||
// REF: https://help.aliyun.com/zh/oss/developer-reference/putcname
|
||||
err := d.sdkClient.PutBucketCnameWithCertificate(aliBucket, oss.PutBucketCname{
|
||||
Cname: d.option.DeployConfig.GetConfigAsString("domain"),
|
||||
CertificateConfiguration: &oss.CertificateConfiguration{
|
||||
Certificate: d.option.Certificate.Certificate,
|
||||
PrivateKey: d.option.Certificate.PrivateKey,
|
||||
Force: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'oss.PutBucketCnameWithCertificate'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunOSSDeployer) createSdkClient(accessKeyId, accessKeySecret, endpoint string) (*oss.Client, error) {
|
||||
if endpoint == "" {
|
||||
endpoint = "oss.aliyuncs.com"
|
||||
}
|
||||
|
||||
client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
80
internal/deployer/baiducloud_cdn.go
Normal file
80
internal/deployer/baiducloud_cdn.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
bceCdn "github.com/baidubce/bce-sdk-go/services/cdn"
|
||||
bceCdnApi "github.com/baidubce/bce-sdk-go/services/cdn/api"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type BaiduCloudCDNDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *bceCdn.Client
|
||||
}
|
||||
|
||||
func NewBaiduCloudCDNDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.BaiduCloudAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
client, err := (&BaiduCloudCDNDeployer{}).createSdkClient(
|
||||
access.AccessKeyId,
|
||||
access.SecretAccessKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
return &BaiduCloudCDNDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *BaiduCloudCDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *BaiduCloudCDNDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *BaiduCloudCDNDeployer) Deploy(ctx context.Context) error {
|
||||
// 修改域名证书
|
||||
// REF: https://cloud.baidu.com/doc/CDN/s/qjzuz2hp8
|
||||
putCertResp, err := d.sdkClient.PutCert(
|
||||
d.option.DeployConfig.GetConfigAsString("domain"),
|
||||
&bceCdnApi.UserCertificate{
|
||||
CertName: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
|
||||
ServerData: d.option.Certificate.Certificate,
|
||||
PrivateData: d.option.Certificate.PrivateKey,
|
||||
},
|
||||
"ON",
|
||||
)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.PutCert'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已修改域名证书", putCertResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *BaiduCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey string) (*bceCdn.Client, error) {
|
||||
client, err := bceCdn.NewClient(accessKeyId, secretAccessKey, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
116
internal/deployer/byteplus_cdn.go
Normal file
116
internal/deployer/byteplus_cdn.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
bytepluscdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/byteplus-cdn"
|
||||
|
||||
"github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn"
|
||||
xerrors "github.com/pkg/errors"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
)
|
||||
|
||||
type ByteplusCDNDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
sdkClient *cdn.CDN
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewByteplusCDNDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.ByteplusAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
client := cdn.NewInstance()
|
||||
client.Client.SetAccessKey(access.AccessKey)
|
||||
client.Client.SetSecretKey(access.SecretKey)
|
||||
uploader, err := bytepluscdn.New(&bytepluscdn.ByteplusCDNUploaderConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
return &ByteplusCDNDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *ByteplusCDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *ByteplusCDNDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *ByteplusCDNDeployer) Deploy(ctx context.Context) error {
|
||||
apiCtx := context.Background()
|
||||
// 上传证书
|
||||
upres, err := d.sslUploader.Upload(apiCtx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
domains := make([]string, 0)
|
||||
configDomain := d.option.DeployConfig.GetConfigAsString("domain")
|
||||
if strings.HasPrefix(configDomain, "*.") {
|
||||
// 获取证书可以部署的域名
|
||||
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-describecertconfig-9ea17
|
||||
describeCertConfigReq := &cdn.DescribeCertConfigRequest{
|
||||
CertId: upres.CertId,
|
||||
}
|
||||
describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'")
|
||||
}
|
||||
for i := range describeCertConfigResp.Result.CertNotConfig {
|
||||
// 当前未启用 HTTPS 的加速域名列表。
|
||||
domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)
|
||||
}
|
||||
for i := range describeCertConfigResp.Result.OtherCertConfig {
|
||||
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联的证书不是您指定的证书。
|
||||
domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)
|
||||
}
|
||||
for i := range describeCertConfigResp.Result.SpecifiedCertConfig {
|
||||
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联了您指定的证书。
|
||||
d.infos = append(d.infos, fmt.Sprintf("%s域名已配置该证书", describeCertConfigResp.Result.SpecifiedCertConfig[i].Domain))
|
||||
}
|
||||
if len(domains) == 0 {
|
||||
if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 {
|
||||
// 所有匹配的域名都配置了该证书,跳过部署
|
||||
return nil
|
||||
} else {
|
||||
return xerrors.Errorf("未查询到匹配的域名: %s", configDomain)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
domains = append(domains, configDomain)
|
||||
}
|
||||
// 部署证书
|
||||
// REF: https://github.com/byteplus-sdk/byteplus-sdk-golang/blob/master/service/cdn/api_list.go#L306
|
||||
for i := range domains {
|
||||
batchDeployCertReq := &cdn.BatchDeployCertRequest{
|
||||
CertId: upres.CertId,
|
||||
Domain: domains[i],
|
||||
}
|
||||
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.BatchDeployCert'")
|
||||
} else {
|
||||
d.infos = append(d.infos, toStr(fmt.Sprintf("%s域名的证书已修改", domains[i]), batchDeployCertResp))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -5,32 +5,45 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"certimate/internal/applicant"
|
||||
"certimate/internal/domain"
|
||||
"certimate/internal/utils/app"
|
||||
"github.com/usual2970/certimate/internal/applicant"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
const (
|
||||
targetAliyunOss = "aliyun-oss"
|
||||
targetAliyunCdn = "aliyun-cdn"
|
||||
targetAliyunEsa = "aliyun-dcdn"
|
||||
targetSSH = "ssh"
|
||||
targetWebhook = "webhook"
|
||||
targetTencentCdn = "tencent-cdn"
|
||||
targetQiniuCdn = "qiniu-cdn"
|
||||
targetLocal = "local"
|
||||
targetAliyunOSS = "aliyun-oss"
|
||||
targetAliyunCDN = "aliyun-cdn"
|
||||
targetAliyunDCDN = "aliyun-dcdn"
|
||||
targetAliyunCLB = "aliyun-clb"
|
||||
targetAliyunALB = "aliyun-alb"
|
||||
targetAliyunNLB = "aliyun-nlb"
|
||||
targetTencentCDN = "tencent-cdn"
|
||||
targetTencentECDN = "tencent-ecdn"
|
||||
targetTencentCLB = "tencent-clb"
|
||||
targetTencentCOS = "tencent-cos"
|
||||
targetTencentTEO = "tencent-teo"
|
||||
targetHuaweiCloudCDN = "huaweicloud-cdn"
|
||||
targetHuaweiCloudELB = "huaweicloud-elb"
|
||||
targetBaiduCloudCDN = "baiducloud-cdn"
|
||||
targetVolcEngineLive = "volcengine-live"
|
||||
targetVolcEngineCDN = "volcengine-cdn"
|
||||
targetBytePlusCDN = "byteplus-cdn"
|
||||
targetQiniuCdn = "qiniu-cdn"
|
||||
targetDogeCloudCdn = "dogecloud-cdn"
|
||||
targetLocal = "local"
|
||||
targetSSH = "ssh"
|
||||
targetWebhook = "webhook"
|
||||
targetK8sSecret = "k8s-secret"
|
||||
)
|
||||
|
||||
type DeployerOption struct {
|
||||
DomainId string `json:"domainId"`
|
||||
Domain string `json:"domain"`
|
||||
Product string `json:"product"`
|
||||
Access string `json:"access"`
|
||||
AceessRecord *models.Record `json:"-"`
|
||||
AccessRecord *models.Record `json:"-"`
|
||||
DeployConfig domain.DeployConfig `json:"deployConfig"`
|
||||
Certificate applicant.Certificate `json:"certificate"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
@@ -38,7 +51,7 @@ type DeployerOption struct {
|
||||
|
||||
type Deployer interface {
|
||||
Deploy(ctx context.Context) error
|
||||
GetInfo() []string
|
||||
GetInfos() []string
|
||||
GetID() string
|
||||
}
|
||||
|
||||
@@ -60,7 +73,6 @@ func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error
|
||||
}
|
||||
|
||||
for _, deployConfig := range deployConfigs {
|
||||
|
||||
deployer, err := getWithDeployConfig(record, cert, deployConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -81,9 +93,8 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
|
||||
option := &DeployerOption{
|
||||
DomainId: record.Id,
|
||||
Domain: record.GetString("domain"),
|
||||
Product: getProduct(deployConfig.Type),
|
||||
Access: access.GetString("config"),
|
||||
AceessRecord: access,
|
||||
AccessRecord: access,
|
||||
DeployConfig: deployConfig,
|
||||
}
|
||||
if cert != nil {
|
||||
@@ -96,33 +107,54 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
|
||||
}
|
||||
|
||||
switch deployConfig.Type {
|
||||
case targetAliyunOss:
|
||||
return NewAliyun(option)
|
||||
case targetAliyunCdn:
|
||||
return NewAliyunCdn(option)
|
||||
case targetAliyunEsa:
|
||||
return NewAliyunEsa(option)
|
||||
case targetSSH:
|
||||
return NewSSH(option)
|
||||
case targetWebhook:
|
||||
return NewWebhook(option)
|
||||
case targetTencentCdn:
|
||||
return NewTencentCdn(option)
|
||||
case targetAliyunOSS:
|
||||
return NewAliyunOSSDeployer(option)
|
||||
case targetAliyunCDN:
|
||||
return NewAliyunCDNDeployer(option)
|
||||
case targetAliyunDCDN:
|
||||
return NewAliyunDCDNDeployer(option)
|
||||
case targetAliyunCLB:
|
||||
return NewAliyunCLBDeployer(option)
|
||||
case targetAliyunALB:
|
||||
return NewAliyunALBDeployer(option)
|
||||
case targetAliyunNLB:
|
||||
return NewAliyunNLBDeployer(option)
|
||||
case targetTencentCDN:
|
||||
return NewTencentCDNDeployer(option)
|
||||
case targetTencentECDN:
|
||||
return NewTencentECDNDeployer(option)
|
||||
case targetTencentCLB:
|
||||
return NewTencentCLBDeployer(option)
|
||||
case targetTencentCOS:
|
||||
return NewTencentCOSDeployer(option)
|
||||
case targetTencentTEO:
|
||||
return NewTencentTEODeployer(option)
|
||||
case targetHuaweiCloudCDN:
|
||||
return NewHuaweiCloudCDNDeployer(option)
|
||||
case targetHuaweiCloudELB:
|
||||
return NewHuaweiCloudELBDeployer(option)
|
||||
case targetBaiduCloudCDN:
|
||||
return NewBaiduCloudCDNDeployer(option)
|
||||
case targetQiniuCdn:
|
||||
|
||||
return NewQiNiu(option)
|
||||
return NewQiniuCDNDeployer(option)
|
||||
case targetDogeCloudCdn:
|
||||
return NewDogeCloudCDNDeployer(option)
|
||||
case targetLocal:
|
||||
return NewLocal(option), nil
|
||||
return NewLocalDeployer(option)
|
||||
case targetSSH:
|
||||
return NewSSHDeployer(option)
|
||||
case targetWebhook:
|
||||
return NewWebhookDeployer(option)
|
||||
case targetK8sSecret:
|
||||
return NewK8sSecretDeployer(option)
|
||||
case targetVolcEngineLive:
|
||||
return NewVolcengineLiveDeployer(option)
|
||||
case targetVolcEngineCDN:
|
||||
return NewVolcengineCDNDeployer(option)
|
||||
case targetBytePlusCDN:
|
||||
return NewByteplusCDNDeployer(option)
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func getProduct(t string) string {
|
||||
rs := strings.Split(t, "-")
|
||||
if len(rs) < 2 {
|
||||
return ""
|
||||
}
|
||||
return rs[1]
|
||||
return nil, errors.New("unsupported deploy target")
|
||||
}
|
||||
|
||||
func toStr(tag string, data any) string {
|
||||
@@ -132,38 +164,3 @@ func toStr(tag string, data any) string {
|
||||
byts, _ := json.Marshal(data)
|
||||
return tag + ":" + string(byts)
|
||||
}
|
||||
|
||||
func getDeployString(conf domain.DeployConfig, key string) string {
|
||||
if _, ok := conf.Config[key]; !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
val, ok := conf.Config[key].(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func getDeployVariables(conf domain.DeployConfig) map[string]string {
|
||||
rs := make(map[string]string)
|
||||
data, ok := conf.Config["variables"]
|
||||
if !ok {
|
||||
return rs
|
||||
}
|
||||
|
||||
bts, _ := json.Marshal(data)
|
||||
|
||||
kvData := make([]domain.KV, 0)
|
||||
|
||||
if err := json.Unmarshal(bts, &kvData); err != nil {
|
||||
return rs
|
||||
}
|
||||
|
||||
for _, kv := range kvData {
|
||||
rs[kv.Key] = kv.Value
|
||||
}
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
88
internal/deployer/dogecloud_cdn.go
Normal file
88
internal/deployer/dogecloud_cdn.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploaderDoge "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/dogecloud"
|
||||
doge "github.com/usual2970/certimate/internal/pkg/vendors/dogecloud-sdk"
|
||||
)
|
||||
|
||||
type DogeCloudCDNDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *doge.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewDogeCloudCDNDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.DogeCloudAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
client, err := (&DogeCloudCDNDeployer{}).createSdkClient(
|
||||
access.AccessKey,
|
||||
access.SecretKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
uploader, err := uploaderDoge.New(&uploaderDoge.DogeCloudUploaderConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &DogeCloudCDNDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DogeCloudCDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *DogeCloudCDNDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *DogeCloudCDNDeployer) Deploy(ctx context.Context) error {
|
||||
// 上传证书到 CDN
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 绑定证书
|
||||
// REF: https://docs.dogecloud.com/cdn/api-cert-bind
|
||||
bindCdnCertId, _ := strconv.ParseInt(upres.CertId, 10, 64)
|
||||
bindCdnCertResp, err := d.sdkClient.BindCdnCertWithDomain(bindCdnCertId, d.option.DeployConfig.GetConfigAsString("domain"))
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.BindCdnCert'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已绑定证书", bindCdnCertResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DogeCloudCDNDeployer) createSdkClient(accessKey, secretKey string) (*doge.Client, error) {
|
||||
client := doge.NewClient(accessKey, secretKey)
|
||||
return client, nil
|
||||
}
|
||||
374
internal/deployer/factory.go
Normal file
374
internal/deployer/factory.go
Normal file
@@ -0,0 +1,374 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
providerAliyunAlb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-alb"
|
||||
providerAliyunCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-cdn"
|
||||
providerAliyunClb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-clb"
|
||||
providerAliyunDcdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-dcdn"
|
||||
providerAliyunNlb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-nlb"
|
||||
providerAliyunOss "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-oss"
|
||||
providerBaiduCloudCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cdn"
|
||||
providerBytePlusCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn"
|
||||
providerDogeCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn"
|
||||
providerHuaweiCloudCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn"
|
||||
providerHuaweiCloudElb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-elb"
|
||||
providerK8sSecret "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/k8s-secret"
|
||||
providerLocal "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local"
|
||||
providerQiniuCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn"
|
||||
providerSSH "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh"
|
||||
providerTencentCloudCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn"
|
||||
providerTencentCloudClb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-clb"
|
||||
providerTencentCloudCos "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cos"
|
||||
providerTencentCloudEcdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ecdn"
|
||||
providerTencentCloudTeo "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-teo"
|
||||
providerVolcEngineCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-cdn"
|
||||
providerVolcEngineLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live"
|
||||
providerWebhook "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/maps"
|
||||
)
|
||||
|
||||
// TODO: 该方法目前未实际使用,将在后续迭代中替换
|
||||
func createDeployer(target string, accessConfig string, deployConfig map[string]any) (deployer.Deployer, deployer.Logger, error) {
|
||||
logger := deployer.NewDefaultLogger()
|
||||
|
||||
switch target {
|
||||
case targetAliyunALB, targetAliyunCDN, targetAliyunCLB, targetAliyunDCDN, targetAliyunNLB, targetAliyunOSS:
|
||||
{
|
||||
access := &domain.AliyunAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
switch target {
|
||||
case targetAliyunALB:
|
||||
deployer, err := providerAliyunAlb.NewWithLogger(&providerAliyunAlb.AliyunALBDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
ResourceType: providerAliyunAlb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
|
||||
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
|
||||
ListenerId: maps.GetValueAsString(deployConfig, "listenerId"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetAliyunCDN:
|
||||
deployer, err := providerAliyunCdn.NewWithLogger(&providerAliyunCdn.AliyunCDNDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetAliyunCLB:
|
||||
deployer, err := providerAliyunClb.NewWithLogger(&providerAliyunClb.AliyunCLBDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
ResourceType: providerAliyunClb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
|
||||
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
|
||||
ListenerPort: maps.GetValueAsInt32(deployConfig, "listenerPort"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetAliyunDCDN:
|
||||
deployer, err := providerAliyunDcdn.NewWithLogger(&providerAliyunDcdn.AliyunDCDNDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetAliyunNLB:
|
||||
deployer, err := providerAliyunNlb.NewWithLogger(&providerAliyunNlb.AliyunNLBDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
ResourceType: providerAliyunNlb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
|
||||
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
|
||||
ListenerId: maps.GetValueAsString(deployConfig, "listenerId"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetAliyunOSS:
|
||||
deployer, err := providerAliyunOss.NewWithLogger(&providerAliyunOss.AliyunOSSDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
Bucket: maps.GetValueAsString(deployConfig, "bucket"),
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case targetBaiduCloudCDN:
|
||||
{
|
||||
access := &domain.BaiduCloudAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := providerBaiduCloudCdn.NewWithLogger(&providerBaiduCloudCdn.BaiduCloudCDNDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
|
||||
case targetBytePlusCDN:
|
||||
{
|
||||
access := &domain.ByteplusAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := providerBytePlusCdn.NewWithLogger(&providerBytePlusCdn.BytePlusCDNDeployerConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
|
||||
case targetDogeCloudCdn:
|
||||
{
|
||||
access := &domain.DogeCloudAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := providerDogeCdn.NewWithLogger(&providerDogeCdn.DogeCloudCDNDeployerConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
|
||||
case targetHuaweiCloudCDN, targetHuaweiCloudELB:
|
||||
{
|
||||
access := &domain.HuaweiCloudAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
switch target {
|
||||
case targetHuaweiCloudCDN:
|
||||
deployer, err := providerHuaweiCloudCdn.NewWithLogger(&providerHuaweiCloudCdn.HuaweiCloudCDNDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetHuaweiCloudELB:
|
||||
deployer, err := providerHuaweiCloudElb.NewWithLogger(&providerHuaweiCloudElb.HuaweiCloudELBDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
ResourceType: providerHuaweiCloudElb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
|
||||
CertificateId: maps.GetValueAsString(deployConfig, "certificateId"),
|
||||
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
|
||||
ListenerId: maps.GetValueAsString(deployConfig, "listenerId"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case targetLocal:
|
||||
{
|
||||
deployer, err := providerLocal.NewWithLogger(&providerLocal.LocalDeployerConfig{
|
||||
ShellEnv: providerLocal.ShellEnvType(maps.GetValueAsString(deployConfig, "shellEnv")),
|
||||
PreCommand: maps.GetValueAsString(deployConfig, "preCommand"),
|
||||
PostCommand: maps.GetValueAsString(deployConfig, "postCommand"),
|
||||
OutputFormat: providerLocal.OutputFormatType(maps.GetValueOrDefaultAsString(deployConfig, "format", "PEM")),
|
||||
OutputCertPath: maps.GetValueAsString(deployConfig, "certPath"),
|
||||
OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"),
|
||||
PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"),
|
||||
JksAlias: maps.GetValueAsString(deployConfig, "jksAlias"),
|
||||
JksKeypass: maps.GetValueAsString(deployConfig, "jksKeypass"),
|
||||
JksStorepass: maps.GetValueAsString(deployConfig, "jksStorepass"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
|
||||
case targetK8sSecret:
|
||||
{
|
||||
access := &domain.KubernetesAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := providerK8sSecret.NewWithLogger(&providerK8sSecret.K8sSecretDeployerConfig{
|
||||
KubeConfig: access.KubeConfig,
|
||||
Namespace: maps.GetValueOrDefaultAsString(deployConfig, "namespace", "default"),
|
||||
SecretName: maps.GetValueAsString(deployConfig, "secretName"),
|
||||
SecretDataKeyForCrt: maps.GetValueOrDefaultAsString(deployConfig, "secretDataKeyForCrt", "tls.crt"),
|
||||
SecretDataKeyForKey: maps.GetValueOrDefaultAsString(deployConfig, "secretDataKeyForKey", "tls.key"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
|
||||
case targetQiniuCdn:
|
||||
{
|
||||
access := &domain.QiniuAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := providerQiniuCdn.NewWithLogger(&providerQiniuCdn.QiniuCDNDeployerConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
|
||||
case targetSSH:
|
||||
{
|
||||
access := &domain.SSHAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
sshPort, _ := strconv.ParseInt(access.Port, 10, 32)
|
||||
deployer, err := providerSSH.NewWithLogger(&providerSSH.SshDeployerConfig{
|
||||
SshHost: access.Host,
|
||||
SshPort: int32(sshPort),
|
||||
SshUsername: access.Username,
|
||||
SshPassword: access.Password,
|
||||
SshKey: access.Key,
|
||||
SshKeyPassphrase: access.KeyPassphrase,
|
||||
PreCommand: maps.GetValueAsString(deployConfig, "preCommand"),
|
||||
PostCommand: maps.GetValueAsString(deployConfig, "postCommand"),
|
||||
OutputFormat: providerSSH.OutputFormatType(maps.GetValueOrDefaultAsString(deployConfig, "format", "PEM")),
|
||||
OutputCertPath: maps.GetValueAsString(deployConfig, "certPath"),
|
||||
OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"),
|
||||
PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"),
|
||||
JksAlias: maps.GetValueAsString(deployConfig, "jksAlias"),
|
||||
JksKeypass: maps.GetValueAsString(deployConfig, "jksKeypass"),
|
||||
JksStorepass: maps.GetValueAsString(deployConfig, "jksStorepass"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
|
||||
case targetTencentCDN, targetTencentCLB, targetTencentCOS, targetTencentECDN, targetTencentTEO:
|
||||
{
|
||||
access := &domain.TencentAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
switch target {
|
||||
case targetTencentCDN:
|
||||
deployer, err := providerTencentCloudCdn.NewWithLogger(&providerTencentCloudCdn.TencentCloudCDNDeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetTencentCLB:
|
||||
deployer, err := providerTencentCloudClb.NewWithLogger(&providerTencentCloudClb.TencentCloudCLBDeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
ResourceType: providerTencentCloudClb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
|
||||
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
|
||||
ListenerId: maps.GetValueAsString(deployConfig, "listenerId"),
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetTencentCOS:
|
||||
deployer, err := providerTencentCloudCos.NewWithLogger(&providerTencentCloudCos.TencentCloudCOSDeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
Bucket: maps.GetValueAsString(deployConfig, "bucket"),
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetTencentECDN:
|
||||
deployer, err := providerTencentCloudEcdn.NewWithLogger(&providerTencentCloudEcdn.TencentCloudECDNDeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetTencentTEO:
|
||||
deployer, err := providerTencentCloudTeo.NewWithLogger(&providerTencentCloudTeo.TencentCloudTEODeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
ZoneId: maps.GetValueAsString(deployConfig, "zoneId"),
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case targetVolcEngineCDN, targetVolcEngineLive:
|
||||
{
|
||||
access := &domain.VolcEngineAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
switch target {
|
||||
case targetVolcEngineCDN:
|
||||
deployer, err := providerVolcEngineCdn.NewWithLogger(&providerVolcEngineCdn.VolcEngineCDNDeployerConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetVolcEngineLive:
|
||||
deployer, err := providerVolcEngineLive.NewWithLogger(&providerVolcEngineLive.VolcEngineLiveDeployerConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case targetWebhook:
|
||||
{
|
||||
access := &domain.WebhookAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := providerWebhook.NewWithLogger(&providerWebhook.WebhookDeployerConfig{
|
||||
Url: access.Url,
|
||||
Variables: nil, // TODO: 尚未实现
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("unsupported deployer target: %s", target)
|
||||
}
|
||||
143
internal/deployer/huaweicloud_cdn.go
Normal file
143
internal/deployer/huaweicloud_cdn.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
|
||||
hcCdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2"
|
||||
hcCdnModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model"
|
||||
hcCdnRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/region"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploaderHcScm "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-scm"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/cast"
|
||||
hcCdnEx "github.com/usual2970/certimate/internal/pkg/vendors/huaweicloud-cdn-sdk"
|
||||
)
|
||||
|
||||
type HuaweiCloudCDNDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *hcCdnEx.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewHuaweiCloudCDNDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.HuaweiCloudAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
client, err := (&HuaweiCloudCDNDeployer{}).createSdkClient(
|
||||
access.AccessKeyId,
|
||||
access.SecretAccessKey,
|
||||
option.DeployConfig.GetConfigAsString("region"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
uploader, err := uploaderHcScm.New(&uploaderHcScm.HuaweiCloudSCMUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: "",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &HuaweiCloudCDNDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudCDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudCDNDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error {
|
||||
// 上传证书到 SCM
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 查询加速域名配置
|
||||
// REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html
|
||||
showDomainFullConfigReq := &hcCdnModel.ShowDomainFullConfigRequest{
|
||||
DomainName: d.option.DeployConfig.GetConfigAsString("domain"),
|
||||
}
|
||||
showDomainFullConfigResp, err := d.sdkClient.ShowDomainFullConfig(showDomainFullConfigReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.ShowDomainFullConfig'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到加速域名配置", showDomainFullConfigResp))
|
||||
|
||||
// 更新加速域名配置
|
||||
// REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html
|
||||
// REF: https://support.huaweicloud.com/usermanual-cdn/cdn_01_0306.html
|
||||
updateDomainMultiCertificatesReqBodyContent := &hcCdnEx.UpdateDomainMultiCertificatesExRequestBodyContent{}
|
||||
updateDomainMultiCertificatesReqBodyContent.DomainName = d.option.DeployConfig.GetConfigAsString("domain")
|
||||
updateDomainMultiCertificatesReqBodyContent.HttpsSwitch = 1
|
||||
updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(2)
|
||||
updateDomainMultiCertificatesReqBodyContent.SCMCertificateId = cast.StringPtr(upres.CertId)
|
||||
updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(upres.CertName)
|
||||
updateDomainMultiCertificatesReqBodyContent = updateDomainMultiCertificatesReqBodyContent.MergeConfig(showDomainFullConfigResp.Configs)
|
||||
updateDomainMultiCertificatesReq := &hcCdnEx.UpdateDomainMultiCertificatesExRequest{
|
||||
Body: &hcCdnEx.UpdateDomainMultiCertificatesExRequestBody{
|
||||
Https: updateDomainMultiCertificatesReqBodyContent,
|
||||
},
|
||||
}
|
||||
updateDomainMultiCertificatesResp, err := d.sdkClient.UploadDomainMultiCertificatesEx(updateDomainMultiCertificatesReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadDomainMultiCertificatesEx'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已更新加速域名配置", updateDomainMultiCertificatesResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcCdnEx.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-north-1" // CDN 服务默认区域:华北一北京
|
||||
}
|
||||
|
||||
auth, err := global.NewCredentialsBuilder().
|
||||
WithAk(accessKeyId).
|
||||
WithSk(secretAccessKey).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hcRegion, err := hcCdnRegion.SafeValueOf(region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hcClient, err := hcCdn.CdnClientBuilder().
|
||||
WithRegion(hcRegion).
|
||||
WithCredential(auth).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := hcCdnEx.NewClient(hcClient)
|
||||
return client, nil
|
||||
}
|
||||
378
internal/deployer/huaweicloud_elb.go
Normal file
378
internal/deployer/huaweicloud_elb.go
Normal file
@@ -0,0 +1,378 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
|
||||
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
|
||||
hcElb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3"
|
||||
hcElbModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model"
|
||||
hcElbRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region"
|
||||
hcIam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3"
|
||||
hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model"
|
||||
hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region"
|
||||
xerrors "github.com/pkg/errors"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploaderHcElb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-elb"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/cast"
|
||||
)
|
||||
|
||||
type HuaweiCloudELBDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *hcElb.ElbClient
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewHuaweiCloudELBDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.HuaweiCloudAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
client, err := (&HuaweiCloudELBDeployer{}).createSdkClient(
|
||||
access.AccessKeyId,
|
||||
access.SecretAccessKey,
|
||||
option.DeployConfig.GetConfigAsString("region"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
uploader, err := uploaderHcElb.New(&uploaderHcElb.HuaweiCloudELBUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: option.DeployConfig.GetConfigAsString("region"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &HuaweiCloudELBDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) Deploy(ctx context.Context) error {
|
||||
switch d.option.DeployConfig.GetConfigAsString("resourceType") {
|
||||
case "certificate":
|
||||
// 部署到指定证书
|
||||
if err := d.deployToCertificate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
case "loadbalancer":
|
||||
// 部署到指定负载均衡器
|
||||
if err := d.deployToLoadbalancer(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
case "listener":
|
||||
// 部署到指定监听器
|
||||
if err := d.deployToListener(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported resource type")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) {
|
||||
if region == "" {
|
||||
region = "cn-north-4" // ELB 服务默认区域:华北四北京
|
||||
}
|
||||
|
||||
projectId, err := (&HuaweiCloudELBDeployer{}).getSdkProjectId(
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
region,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth, err := basic.NewCredentialsBuilder().
|
||||
WithAk(accessKeyId).
|
||||
WithSk(secretAccessKey).
|
||||
WithProjectId(projectId).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hcRegion, err := hcElbRegion.SafeValueOf(region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hcClient, err := hcElb.ElbClientBuilder().
|
||||
WithRegion(hcRegion).
|
||||
WithCredential(auth).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := hcElb.NewElbClient(hcClient)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (u *HuaweiCloudELBDeployer) getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) {
|
||||
if region == "" {
|
||||
region = "cn-north-4" // IAM 服务默认区域:华北四北京
|
||||
}
|
||||
|
||||
auth, err := global.NewCredentialsBuilder().
|
||||
WithAk(accessKeyId).
|
||||
WithSk(secretAccessKey).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hcRegion, err := hcIamRegion.SafeValueOf(region)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hcClient, err := hcIam.IamClientBuilder().
|
||||
WithRegion(hcRegion).
|
||||
WithCredential(auth).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
client := hcIam.NewIamClient(hcClient)
|
||||
|
||||
request := &hcIamModel.KeystoneListProjectsRequest{
|
||||
Name: ®ion,
|
||||
}
|
||||
response, err := client.KeystoneListProjects(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if response.Projects == nil || len(*response.Projects) == 0 {
|
||||
return "", errors.New("no project found")
|
||||
}
|
||||
|
||||
return (*response.Projects)[0].Id, nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context) error {
|
||||
hcCertId := d.option.DeployConfig.GetConfigAsString("certificateId")
|
||||
if hcCertId == "" {
|
||||
return errors.New("`certificateId` is required")
|
||||
}
|
||||
|
||||
// 更新证书
|
||||
// REF: https://support.huaweicloud.com/api-elb/UpdateCertificate.html
|
||||
updateCertificateReq := &hcElbModel.UpdateCertificateRequest{
|
||||
CertificateId: hcCertId,
|
||||
Body: &hcElbModel.UpdateCertificateRequestBody{
|
||||
Certificate: &hcElbModel.UpdateCertificateOption{
|
||||
Certificate: cast.StringPtr(d.option.Certificate.Certificate),
|
||||
PrivateKey: cast.StringPtr(d.option.Certificate.PrivateKey),
|
||||
},
|
||||
},
|
||||
}
|
||||
updateCertificateResp, err := d.sdkClient.UpdateCertificate(updateCertificateReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateCertificate'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已更新 ELB 证书", updateCertificateResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error {
|
||||
hcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
|
||||
if hcLoadbalancerId == "" {
|
||||
return errors.New("`loadbalancerId` is required")
|
||||
}
|
||||
|
||||
hcListenerIds := make([]string, 0)
|
||||
|
||||
// 查询负载均衡器详情
|
||||
// REF: https://support.huaweicloud.com/api-elb/ShowLoadBalancer.html
|
||||
showLoadBalancerReq := &hcElbModel.ShowLoadBalancerRequest{
|
||||
LoadbalancerId: hcLoadbalancerId,
|
||||
}
|
||||
showLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowLoadBalancer'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器", showLoadBalancerResp))
|
||||
|
||||
// 查询监听器列表
|
||||
// REF: https://support.huaweicloud.com/api-elb/ListListeners.html
|
||||
listListenersLimit := int32(2000)
|
||||
var listListenersMarker *string = nil
|
||||
for {
|
||||
listListenersReq := &hcElbModel.ListListenersRequest{
|
||||
Limit: cast.Int32Ptr(listListenersLimit),
|
||||
Marker: listListenersMarker,
|
||||
Protocol: &[]string{"HTTPS", "TERMINATED_HTTPS"},
|
||||
LoadbalancerId: &[]string{showLoadBalancerResp.Loadbalancer.Id},
|
||||
}
|
||||
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListListeners'")
|
||||
}
|
||||
|
||||
if listListenersResp.Listeners != nil {
|
||||
for _, listener := range *listListenersResp.Listeners {
|
||||
hcListenerIds = append(hcListenerIds, listener.Id)
|
||||
}
|
||||
}
|
||||
|
||||
if listListenersResp.Listeners == nil || len(*listListenersResp.Listeners) < int(listListenersLimit) {
|
||||
break
|
||||
} else {
|
||||
listListenersMarker = listListenersResp.PageInfo.NextMarker
|
||||
}
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器下的监听器", hcListenerIds))
|
||||
|
||||
// 上传证书到 SCM
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 批量更新监听器证书
|
||||
var errs []error
|
||||
for _, hcListenerId := range hcListenerIds {
|
||||
if err := d.modifyListenerCertificate(ctx, hcListenerId, upres.CertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context) error {
|
||||
hcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
|
||||
if hcListenerId == "" {
|
||||
return errors.New("`listenerId` is required")
|
||||
}
|
||||
|
||||
// 上传证书到 SCM
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 更新监听器证书
|
||||
if err := d.modifyListenerCertificate(ctx, hcListenerId, upres.CertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) modifyListenerCertificate(ctx context.Context, hcListenerId string, hcCertId string) error {
|
||||
// 查询监听器详情
|
||||
// REF: https://support.huaweicloud.com/api-elb/ShowListener.html
|
||||
showListenerReq := &hcElbModel.ShowListenerRequest{
|
||||
ListenerId: hcListenerId,
|
||||
}
|
||||
showListenerResp, err := d.sdkClient.ShowListener(showListenerReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowListener'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到 ELB 监听器", showListenerResp))
|
||||
|
||||
// 更新监听器
|
||||
// REF: https://support.huaweicloud.com/api-elb/UpdateListener.html
|
||||
updateListenerReq := &hcElbModel.UpdateListenerRequest{
|
||||
ListenerId: hcListenerId,
|
||||
Body: &hcElbModel.UpdateListenerRequestBody{
|
||||
Listener: &hcElbModel.UpdateListenerOption{
|
||||
DefaultTlsContainerRef: cast.StringPtr(hcCertId),
|
||||
},
|
||||
},
|
||||
}
|
||||
if showListenerResp.Listener.SniContainerRefs != nil {
|
||||
if len(showListenerResp.Listener.SniContainerRefs) > 0 {
|
||||
// 如果开启 SNI,需替换同 SAN 的证书
|
||||
sniCertIds := make([]string, 0)
|
||||
sniCertIds = append(sniCertIds, hcCertId)
|
||||
|
||||
listOldCertificateReq := &hcElbModel.ListCertificatesRequest{
|
||||
Id: &showListenerResp.Listener.SniContainerRefs,
|
||||
}
|
||||
listOldCertificateResp, err := d.sdkClient.ListCertificates(listOldCertificateReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListCertificates'")
|
||||
}
|
||||
|
||||
showNewCertificateReq := &hcElbModel.ShowCertificateRequest{
|
||||
CertificateId: hcCertId,
|
||||
}
|
||||
showNewCertificateResp, err := d.sdkClient.ShowCertificate(showNewCertificateReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowCertificate'")
|
||||
}
|
||||
|
||||
for _, certificate := range *listOldCertificateResp.Certificates {
|
||||
oldCertificate := certificate
|
||||
newCertificate := showNewCertificateResp.Certificate
|
||||
|
||||
if oldCertificate.SubjectAlternativeNames != nil && newCertificate.SubjectAlternativeNames != nil {
|
||||
if slices.Equal(*oldCertificate.SubjectAlternativeNames, *newCertificate.SubjectAlternativeNames) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if oldCertificate.Domain == newCertificate.Domain {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sniCertIds = append(sniCertIds, certificate.Id)
|
||||
}
|
||||
|
||||
updateListenerReq.Body.Listener.SniContainerRefs = &sniCertIds
|
||||
}
|
||||
|
||||
if showListenerResp.Listener.SniMatchAlgo != "" {
|
||||
updateListenerReq.Body.Listener.SniMatchAlgo = cast.StringPtr(showListenerResp.Listener.SniMatchAlgo)
|
||||
}
|
||||
}
|
||||
updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateListener'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已更新 ELB 监听器", updateListenerResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
136
internal/deployer/k8s_secret.go
Normal file
136
internal/deployer/k8s_secret.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
k8sCore "k8s.io/api/core/v1"
|
||||
k8sMeta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
)
|
||||
|
||||
type K8sSecretDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
k8sClient *kubernetes.Clientset
|
||||
}
|
||||
|
||||
func NewK8sSecretDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.KubernetesAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
client, err := (&K8sSecretDeployer{}).createK8sClient(access)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create k8s client")
|
||||
}
|
||||
|
||||
return &K8sSecretDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
k8sClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *K8sSecretDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *K8sSecretDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *K8sSecretDeployer) Deploy(ctx context.Context) error {
|
||||
namespace := d.option.DeployConfig.GetConfigAsString("namespace")
|
||||
secretName := d.option.DeployConfig.GetConfigAsString("secretName")
|
||||
secretDataKeyForCrt := d.option.DeployConfig.GetConfigOrDefaultAsString("secretDataKeyForCrt", "tls.crt")
|
||||
secretDataKeyForKey := d.option.DeployConfig.GetConfigOrDefaultAsString("secretDataKeyForKey", "tls.key")
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
if secretName == "" {
|
||||
return errors.New("`secretName` is required")
|
||||
}
|
||||
|
||||
certX509, err := x509.ParseCertificateFromPEM(d.option.Certificate.Certificate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secretPayload := k8sCore.Secret{
|
||||
TypeMeta: k8sMeta.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: k8sMeta.ObjectMeta{
|
||||
Name: secretName,
|
||||
Annotations: map[string]string{
|
||||
"certimate/domains": d.option.Domain,
|
||||
"certimate/alt-names": strings.Join(certX509.DNSNames, ","),
|
||||
"certimate/common-name": certX509.Subject.CommonName,
|
||||
"certimate/issuer-organization": strings.Join(certX509.Issuer.Organization, ","),
|
||||
},
|
||||
},
|
||||
Type: k8sCore.SecretType("kubernetes.io/tls"),
|
||||
}
|
||||
secretPayload.Data = make(map[string][]byte)
|
||||
secretPayload.Data[secretDataKeyForCrt] = []byte(d.option.Certificate.Certificate)
|
||||
secretPayload.Data[secretDataKeyForKey] = []byte(d.option.Certificate.PrivateKey)
|
||||
|
||||
// 获取 Secret 实例
|
||||
_, err = d.k8sClient.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, k8sMeta.GetOptions{})
|
||||
if err != nil {
|
||||
_, err = d.k8sClient.CoreV1().Secrets(namespace).Create(context.TODO(), &secretPayload, k8sMeta.CreateOptions{})
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to create k8s secret")
|
||||
} else {
|
||||
d.infos = append(d.infos, toStr("Certificate has been created in K8s Secret", nil))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 Secret 实例
|
||||
_, err = d.k8sClient.CoreV1().Secrets(namespace).Update(context.TODO(), &secretPayload, k8sMeta.UpdateOptions{})
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to update k8s secret")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("Certificate has been updated to K8s Secret", nil))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *K8sSecretDeployer) createK8sClient(access *domain.KubernetesAccess) (*kubernetes.Clientset, error) {
|
||||
var config *rest.Config
|
||||
var err error
|
||||
if access.KubeConfig == "" {
|
||||
config, err = rest.InClusterConfig()
|
||||
} else {
|
||||
kubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(access.KubeConfig))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err = kubeConfig.ClientConfig()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -1,111 +1,163 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/fs"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
)
|
||||
|
||||
type localAccess struct{}
|
||||
|
||||
type local struct {
|
||||
type LocalDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewLocal(option *DeployerOption) *local {
|
||||
return &local{
|
||||
const (
|
||||
certFormatPEM = "pem"
|
||||
certFormatPFX = "pfx"
|
||||
certFormatJKS = "jks"
|
||||
)
|
||||
|
||||
const (
|
||||
shellEnvSh = "sh"
|
||||
shellEnvCmd = "cmd"
|
||||
shellEnvPowershell = "powershell"
|
||||
)
|
||||
|
||||
func NewLocalDeployer(option *DeployerOption) (Deployer, error) {
|
||||
return &LocalDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *local) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", l.option.AceessRecord.GetString("name"), l.option.AceessRecord.Id)
|
||||
func (d *LocalDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (l *local) GetInfo() []string {
|
||||
func (d *LocalDeployer) GetInfos() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (l *local) Deploy(ctx context.Context) error {
|
||||
access := &localAccess{}
|
||||
if err := json.Unmarshal([]byte(l.option.Access), access); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
preCommand := getDeployString(l.option.DeployConfig, "preCommand")
|
||||
|
||||
func (d *LocalDeployer) Deploy(ctx context.Context) error {
|
||||
// 执行前置命令
|
||||
preCommand := d.option.DeployConfig.GetConfigAsString("preCommand")
|
||||
if preCommand != "" {
|
||||
if err := execCmd(preCommand); err != nil {
|
||||
return fmt.Errorf("执行前置命令失败: %w", err)
|
||||
stdout, stderr, err := d.execCommand(preCommand)
|
||||
if err != nil {
|
||||
return xerrors.Wrapf(err, "failed to run pre-command, stdout: %s, stderr: %s", stdout, stderr)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("执行前置命令成功", stdout))
|
||||
}
|
||||
|
||||
// 复制文件
|
||||
if err := copyFile(l.option.Certificate.Certificate, getDeployString(l.option.DeployConfig, "certPath")); err != nil {
|
||||
return fmt.Errorf("复制证书失败: %w", err)
|
||||
}
|
||||
// 写入证书和私钥文件
|
||||
switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) {
|
||||
case certFormatPEM:
|
||||
if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := copyFile(l.option.Certificate.PrivateKey, getDeployString(l.option.DeployConfig, "keyPath")); err != nil {
|
||||
return fmt.Errorf("复制私钥失败: %w", err)
|
||||
d.infos = append(d.infos, toStr("保存证书成功", nil))
|
||||
|
||||
if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("保存私钥成功", nil))
|
||||
|
||||
case certFormatPFX:
|
||||
pfxData, err := x509.TransformCertificateFromPEMToPFX(
|
||||
d.option.Certificate.Certificate,
|
||||
d.option.Certificate.PrivateKey,
|
||||
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("保存证书成功", nil))
|
||||
|
||||
case certFormatJKS:
|
||||
jksData, err := x509.TransformCertificateFromPEMToJKS(
|
||||
d.option.Certificate.Certificate,
|
||||
d.option.Certificate.PrivateKey,
|
||||
d.option.DeployConfig.GetConfigAsString("jksAlias"),
|
||||
d.option.DeployConfig.GetConfigAsString("jksKeypass"),
|
||||
d.option.DeployConfig.GetConfigAsString("jksStorepass"),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("保存证书成功", nil))
|
||||
|
||||
default:
|
||||
return errors.New("unsupported format")
|
||||
}
|
||||
|
||||
// 执行命令
|
||||
command := d.option.DeployConfig.GetConfigAsString("command")
|
||||
if command != "" {
|
||||
stdout, stderr, err := d.execCommand(command)
|
||||
if err != nil {
|
||||
return xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
|
||||
}
|
||||
|
||||
if err := execCmd(getDeployString(l.option.DeployConfig, "command")); err != nil {
|
||||
return fmt.Errorf("执行命令失败: %w", err)
|
||||
d.infos = append(d.infos, toStr("执行命令成功", stdout))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func execCmd(command string) error {
|
||||
// 执行命令
|
||||
func (d *LocalDeployer) execCommand(command string) (string, string, error) {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.Command("cmd", "/C", command)
|
||||
} else {
|
||||
switch d.option.DeployConfig.GetConfigAsString("shell") {
|
||||
case shellEnvSh:
|
||||
cmd = exec.Command("sh", "-c", command)
|
||||
|
||||
case shellEnvCmd:
|
||||
cmd = exec.Command("cmd", "/C", command)
|
||||
|
||||
case shellEnvPowershell:
|
||||
cmd = exec.Command("powershell", "-Command", command)
|
||||
|
||||
case "":
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.Command("cmd", "/C", command)
|
||||
} else {
|
||||
cmd = exec.Command("sh", "-c", command)
|
||||
}
|
||||
|
||||
default:
|
||||
return "", "", errors.New("unsupported shell")
|
||||
}
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
var stdoutBuf bytes.Buffer
|
||||
cmd.Stdout = &stdoutBuf
|
||||
var stderrBuf bytes.Buffer
|
||||
cmd.Stderr = &stderrBuf
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("执行命令失败: %w", err)
|
||||
return "", "", xerrors.Wrap(err, "failed to execute shell script")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(content string, path string) error {
|
||||
dir := filepath.Dir(path)
|
||||
|
||||
// 如果目录不存在,创建目录
|
||||
err := os.MkdirAll(dir, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建或打开文件
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建文件失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 写入内容到文件
|
||||
_, err = file.Write([]byte(content))
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入文件失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return stdoutBuf.String(), stderrBuf.String(), nil
|
||||
}
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/qiniu/go-sdk/v7/auth"
|
||||
|
||||
"certimate/internal/domain"
|
||||
xhttp "certimate/internal/utils/http"
|
||||
)
|
||||
|
||||
const qiniuGateway = "http://api.qiniu.com"
|
||||
|
||||
type qiuniu struct {
|
||||
option *DeployerOption
|
||||
info []string
|
||||
credentials *auth.Credentials
|
||||
}
|
||||
|
||||
func NewQiNiu(option *DeployerOption) (*qiuniu, error) {
|
||||
access := &domain.QiniuAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
|
||||
return &qiuniu{
|
||||
option: option,
|
||||
info: make([]string, 0),
|
||||
|
||||
credentials: auth.New(access.AccessKey, access.SecretKey),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *qiuniu) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
|
||||
}
|
||||
|
||||
func (q *qiuniu) GetInfo() []string {
|
||||
return q.info
|
||||
}
|
||||
|
||||
func (q *qiuniu) Deploy(ctx context.Context) error {
|
||||
// 上传证书
|
||||
certId, err := q.uploadCert()
|
||||
if err != nil {
|
||||
return fmt.Errorf("uploadCert failed: %w", err)
|
||||
}
|
||||
|
||||
// 获取域名信息
|
||||
domainInfo, err := q.getDomainInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getDomainInfo failed: %w", err)
|
||||
}
|
||||
|
||||
// 判断域名是否启用 https
|
||||
|
||||
if domainInfo.Https != nil && domainInfo.Https.CertID != "" {
|
||||
// 启用了 https
|
||||
// 修改域名证书
|
||||
err = q.modifyDomainCert(certId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("modifyDomainCert failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
// 没启用 https
|
||||
// 启用 https
|
||||
|
||||
err = q.enableHttps(certId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("enableHttps failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *qiuniu) enableHttps(certId string) error {
|
||||
path := fmt.Sprintf("/domain/%s/sslize", getDeployString(q.option.DeployConfig, "domain"))
|
||||
|
||||
body := &modifyDomainCertReq{
|
||||
CertID: certId,
|
||||
ForceHttps: true,
|
||||
Http2Enable: true,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("enable https failed: %w", err)
|
||||
}
|
||||
|
||||
_, err = q.req(qiniuGateway+path, http.MethodPut, bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("enable https failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type domainInfo struct {
|
||||
Https *modifyDomainCertReq `json:"https"`
|
||||
}
|
||||
|
||||
func (q *qiuniu) getDomainInfo() (*domainInfo, error) {
|
||||
path := fmt.Sprintf("/domain/%s", getDeployString(q.option.DeployConfig, "domain"))
|
||||
|
||||
res, err := q.req(qiniuGateway+path, http.MethodGet, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req failed: %w", err)
|
||||
}
|
||||
|
||||
resp := &domainInfo{}
|
||||
err = json.Unmarshal(res, resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("json.Unmarshal failed: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type uploadCertReq struct {
|
||||
Name string `json:"name"`
|
||||
CommonName string `json:"common_name"`
|
||||
Pri string `json:"pri"`
|
||||
Ca string `json:"ca"`
|
||||
}
|
||||
|
||||
type uploadCertResp struct {
|
||||
CertID string `json:"certID"`
|
||||
}
|
||||
|
||||
func (q *qiuniu) uploadCert() (string, error) {
|
||||
path := "/sslcert"
|
||||
|
||||
body := &uploadCertReq{
|
||||
Name: getDeployString(q.option.DeployConfig, "domain"),
|
||||
CommonName: getDeployString(q.option.DeployConfig, "domain"),
|
||||
Pri: q.option.Certificate.PrivateKey,
|
||||
Ca: q.option.Certificate.Certificate,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("json.Marshal failed: %w", err)
|
||||
}
|
||||
|
||||
res, err := q.req(qiniuGateway+path, http.MethodPost, bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("req failed: %w", err)
|
||||
}
|
||||
resp := &uploadCertResp{}
|
||||
err = json.Unmarshal(res, resp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("json.Unmarshal failed: %w", err)
|
||||
}
|
||||
|
||||
return resp.CertID, nil
|
||||
}
|
||||
|
||||
type modifyDomainCertReq struct {
|
||||
CertID string `json:"certId"`
|
||||
ForceHttps bool `json:"forceHttps"`
|
||||
Http2Enable bool `json:"http2Enable"`
|
||||
}
|
||||
|
||||
func (q *qiuniu) modifyDomainCert(certId string) error {
|
||||
path := fmt.Sprintf("/domain/%s/httpsconf", getDeployString(q.option.DeployConfig, "domain"))
|
||||
|
||||
body := &modifyDomainCertReq{
|
||||
CertID: certId,
|
||||
ForceHttps: true,
|
||||
Http2Enable: true,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("json.Marshal failed: %w", err)
|
||||
}
|
||||
|
||||
_, err = q.req(qiniuGateway+path, http.MethodPut, bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("req failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *qiuniu) req(url, method string, body io.Reader) ([]byte, error) {
|
||||
req := xhttp.BuildReq(url, method, body, map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
})
|
||||
|
||||
if err := q.credentials.AddToken(auth.TokenQBox, req); err != nil {
|
||||
return nil, fmt.Errorf("credentials.AddToken failed: %w", err)
|
||||
}
|
||||
|
||||
respBody, err := xhttp.ToRequest(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ToRequest failed: %w", err)
|
||||
}
|
||||
|
||||
defer respBody.Close()
|
||||
|
||||
res, err := io.ReadAll(respBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("io.ReadAll failed: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
113
internal/deployer/qiniu_cdn.go
Normal file
113
internal/deployer/qiniu_cdn.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
"github.com/qiniu/go-sdk/v7/auth"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploaderQiniu "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/qiniu-sslcert"
|
||||
qiniuEx "github.com/usual2970/certimate/internal/pkg/vendors/qiniu-sdk"
|
||||
)
|
||||
|
||||
type QiniuCDNDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *qiniuEx.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewQiniuCDNDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.QiniuAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
client, err := (&QiniuCDNDeployer{}).createSdkClient(
|
||||
access.AccessKey,
|
||||
access.SecretKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
uploader, err := uploaderQiniu.New(&uploaderQiniu.QiniuSSLCertUploaderConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &QiniuCDNDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *QiniuCDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *QiniuCDNDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *QiniuCDNDeployer) Deploy(ctx context.Context) error {
|
||||
// 上传证书
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 在七牛 CDN 中泛域名表示为 .example.com,需去除前缀星号
|
||||
domain := d.option.DeployConfig.GetConfigAsString("domain")
|
||||
if strings.HasPrefix(domain, "*") {
|
||||
domain = strings.TrimPrefix(domain, "*")
|
||||
}
|
||||
|
||||
// 获取域名信息
|
||||
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
|
||||
getDomainInfoResp, err := d.sdkClient.GetDomainInfo(domain)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDomainInfo'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已获取域名信息", getDomainInfoResp))
|
||||
|
||||
// 判断域名是否已启用 HTTPS。如果已启用,修改域名证书;否则,启用 HTTPS
|
||||
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
|
||||
if getDomainInfoResp.Https != nil && getDomainInfoResp.Https.CertID != "" {
|
||||
modifyDomainHttpsConfResp, err := d.sdkClient.ModifyDomainHttpsConf(domain, upres.CertId, getDomainInfoResp.Https.ForceHttps, getDomainInfoResp.Https.Http2Enable)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.ModifyDomainHttpsConf'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已修改域名证书", modifyDomainHttpsConfResp))
|
||||
} else {
|
||||
enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(domain, upres.CertId, true, true)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.EnableDomainHttps'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已将域名升级为 HTTPS", enableDomainHttpsResp))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *QiniuCDNDeployer) createSdkClient(accessKey, secretKey string) (*qiniuEx.Client, error) {
|
||||
credential := auth.New(accessKey, secretKey)
|
||||
client := qiniuEx.NewClient(credential)
|
||||
return client, nil
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qiniu/go-sdk/v7/auth"
|
||||
|
||||
"certimate/internal/applicant"
|
||||
)
|
||||
|
||||
func Test_qiuniu_uploadCert(t *testing.T) {
|
||||
type fields struct {
|
||||
option *DeployerOption
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test",
|
||||
fields: fields{
|
||||
option: &DeployerOption{
|
||||
DomainId: "1",
|
||||
Domain: "example.com",
|
||||
Product: "test",
|
||||
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
|
||||
Certificate: applicant.Certificate{
|
||||
Certificate: "",
|
||||
PrivateKey: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
q, _ := NewQiNiu(tt.fields.option)
|
||||
got, err := q.uploadCert()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("qiuniu.uploadCert() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("qiuniu.uploadCert() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_qiuniu_modifyDomainCert(t *testing.T) {
|
||||
type fields struct {
|
||||
option *DeployerOption
|
||||
info []string
|
||||
credentials *auth.Credentials
|
||||
}
|
||||
type args struct {
|
||||
certId string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test",
|
||||
fields: fields{
|
||||
option: &DeployerOption{
|
||||
DomainId: "1",
|
||||
Domain: "jt1.ikit.fun",
|
||||
Product: "test",
|
||||
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
q, _ := NewQiNiu(tt.fields.option)
|
||||
if err := q.modifyDomainCert(tt.args.certId); (err != nil) != tt.wantErr {
|
||||
t.Errorf("qiuniu.modifyDomainCert() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,96 +4,166 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
xpath "path"
|
||||
"path/filepath"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
"github.com/pkg/sftp"
|
||||
sshPkg "golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
)
|
||||
|
||||
type ssh struct {
|
||||
type SSHDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
type sshAccess struct {
|
||||
Host string `json:"host"`
|
||||
Port string `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Key string `json:"key"`
|
||||
KeyPassphrase string `json:"keyPassphrase"`
|
||||
}
|
||||
|
||||
func NewSSH(option *DeployerOption) (Deployer, error) {
|
||||
return &ssh{
|
||||
func NewSSHDeployer(option *DeployerOption) (Deployer, error) {
|
||||
return &SSHDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *ssh) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
|
||||
func (d *SSHDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (s *ssh) GetInfo() []string {
|
||||
return s.infos
|
||||
func (d *SSHDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (s *ssh) Deploy(ctx context.Context) error {
|
||||
access := &sshAccess{}
|
||||
if err := json.Unmarshal([]byte(s.option.Access), access); err != nil {
|
||||
func (d *SSHDeployer) Deploy(ctx context.Context) error {
|
||||
access := &domain.SSHAccess{}
|
||||
if err := json.Unmarshal([]byte(d.option.Access), access); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 连接
|
||||
client, err := s.getClient(access)
|
||||
client, err := d.createSshClient(access)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s.infos = append(s.infos, toStr("ssh连接成功", nil))
|
||||
d.infos = append(d.infos, toStr("SSH 连接成功", nil))
|
||||
|
||||
// 执行前置命令
|
||||
preCommand := getDeployString(s.option.DeployConfig, "preCommand")
|
||||
preCommand := d.option.DeployConfig.GetConfigAsString("preCommand")
|
||||
if preCommand != "" {
|
||||
stdout, stderr, err := s.sshExecCommand(client, preCommand)
|
||||
stdout, stderr, err := d.sshExecCommand(client, preCommand)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
||||
return xerrors.Wrapf(err, "failed to run pre-command: stdout: %s, stderr: %s", stdout, stderr)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("SSH 执行前置命令成功", stdout))
|
||||
}
|
||||
|
||||
// 上传证书
|
||||
if err := s.upload(client, s.option.Certificate.Certificate, getDeployString(s.option.DeployConfig, "certPath")); err != nil {
|
||||
return fmt.Errorf("failed to upload certificate: %w", err)
|
||||
// 上传证书和私钥文件
|
||||
switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) {
|
||||
case certFormatPEM:
|
||||
if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
|
||||
|
||||
if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("SSH 上传私钥成功", nil))
|
||||
|
||||
case certFormatPFX:
|
||||
pfxData, err := x509.TransformCertificateFromPEMToPFX(
|
||||
d.option.Certificate.Certificate,
|
||||
d.option.Certificate.PrivateKey,
|
||||
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.writeSftpFile(client, d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
|
||||
|
||||
case certFormatJKS:
|
||||
jksData, err := x509.TransformCertificateFromPEMToJKS(
|
||||
d.option.Certificate.Certificate,
|
||||
d.option.Certificate.PrivateKey,
|
||||
d.option.DeployConfig.GetConfigAsString("jksAlias"),
|
||||
d.option.DeployConfig.GetConfigAsString("jksKeypass"),
|
||||
d.option.DeployConfig.GetConfigAsString("jksStorepass"),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.writeSftpFile(client, d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
|
||||
|
||||
default:
|
||||
return errors.New("unsupported format")
|
||||
}
|
||||
|
||||
s.infos = append(s.infos, toStr("ssh上传证书成功", nil))
|
||||
|
||||
// 上传私钥
|
||||
if err := s.upload(client, s.option.Certificate.PrivateKey, getDeployString(s.option.DeployConfig, "keyPath")); err != nil {
|
||||
return fmt.Errorf("failed to upload private key: %w", err)
|
||||
}
|
||||
|
||||
s.infos = append(s.infos, toStr("ssh上传私钥成功", nil))
|
||||
|
||||
// 执行命令
|
||||
stdout, stderr, err := s.sshExecCommand(client, getDeployString(s.option.DeployConfig, "command"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
||||
}
|
||||
command := d.option.DeployConfig.GetConfigAsString("command")
|
||||
if command != "" {
|
||||
stdout, stderr, err := d.sshExecCommand(client, command)
|
||||
if err != nil {
|
||||
return xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
|
||||
}
|
||||
|
||||
s.infos = append(s.infos, toStr("ssh执行命令成功", stdout))
|
||||
d.infos = append(d.infos, toStr("SSH 执行命令成功", stdout))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ssh) sshExecCommand(client *sshPkg.Client, command string) (string, string, error) {
|
||||
session, err := client.NewSession()
|
||||
func (d *SSHDeployer) createSshClient(access *domain.SSHAccess) (*ssh.Client, error) {
|
||||
var authMethod ssh.AuthMethod
|
||||
|
||||
if access.Key != "" {
|
||||
var signer ssh.Signer
|
||||
var err error
|
||||
|
||||
if access.KeyPassphrase != "" {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(access.Key), []byte(access.KeyPassphrase))
|
||||
} else {
|
||||
signer, err = ssh.ParsePrivateKey([]byte(access.Key))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authMethod = ssh.PublicKeys(signer)
|
||||
} else {
|
||||
authMethod = ssh.Password(access.Password)
|
||||
}
|
||||
|
||||
return ssh.Dial("tcp", fmt.Sprintf("%s:%s", access.Host, access.Port), &ssh.ClientConfig{
|
||||
User: access.Username,
|
||||
Auth: []ssh.AuthMethod{
|
||||
authMethod,
|
||||
},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
})
|
||||
}
|
||||
|
||||
func (d *SSHDeployer) sshExecCommand(sshCli *ssh.Client, command string) (string, string, error) {
|
||||
session, err := sshCli.NewSession()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create ssh session: %w", err)
|
||||
return "", "", xerrors.Wrap(err, "failed to create ssh session")
|
||||
}
|
||||
|
||||
defer session.Close()
|
||||
@@ -102,60 +172,38 @@ func (s *ssh) sshExecCommand(client *sshPkg.Client, command string) (string, str
|
||||
var stderrBuf bytes.Buffer
|
||||
session.Stderr = &stderrBuf
|
||||
err = session.Run(command)
|
||||
return stdoutBuf.String(), stderrBuf.String(), err
|
||||
if err != nil {
|
||||
return "", "", xerrors.Wrap(err, "failed to execute ssh script")
|
||||
}
|
||||
|
||||
return stdoutBuf.String(), stderrBuf.String(), nil
|
||||
}
|
||||
|
||||
func (s *ssh) upload(client *sshPkg.Client, content, path string) error {
|
||||
sftpCli, err := sftp.NewClient(client)
|
||||
func (d *SSHDeployer) writeSftpFileString(sshCli *ssh.Client, path string, content string) error {
|
||||
return d.writeSftpFile(sshCli, path, []byte(content))
|
||||
}
|
||||
|
||||
func (d *SSHDeployer) writeSftpFile(sshCli *ssh.Client, path string, data []byte) error {
|
||||
sftpCli, err := sftp.NewClient(sshCli)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create sftp client: %w", err)
|
||||
return xerrors.Wrap(err, "failed to create sftp client")
|
||||
}
|
||||
defer sftpCli.Close()
|
||||
|
||||
if err := sftpCli.MkdirAll(xpath.Dir(path)); err != nil {
|
||||
return fmt.Errorf("failed to create remote directory: %w", err)
|
||||
if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil {
|
||||
return xerrors.Wrap(err, "failed to create remote directory")
|
||||
}
|
||||
|
||||
file, err := sftpCli.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open remote file: %w", err)
|
||||
return xerrors.Wrap(err, "failed to open remote file")
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write([]byte(content))
|
||||
_, err = file.Write(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to remote file: %w", err)
|
||||
return xerrors.Wrap(err, "failed to write to remote file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ssh) getClient(access *sshAccess) (*sshPkg.Client, error) {
|
||||
var authMethod sshPkg.AuthMethod
|
||||
|
||||
if access.Key != "" {
|
||||
var signer sshPkg.Signer
|
||||
var err error
|
||||
|
||||
if access.KeyPassphrase != "" {
|
||||
signer, err = sshPkg.ParsePrivateKeyWithPassphrase([]byte(access.Key), []byte(access.KeyPassphrase))
|
||||
} else {
|
||||
signer, err = sshPkg.ParsePrivateKey([]byte(access.Key))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authMethod = sshPkg.PublicKeys(signer)
|
||||
} else {
|
||||
authMethod = sshPkg.Password(access.Password)
|
||||
}
|
||||
|
||||
return sshPkg.Dial("tcp", fmt.Sprintf("%s:%s", access.Host, access.Port), &sshPkg.ClientConfig{
|
||||
User: access.Username,
|
||||
Auth: []sshPkg.AuthMethod{
|
||||
authMethod,
|
||||
},
|
||||
HostKeyCallback: sshPkg.InsecureIgnoreHostKey(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,143 +2,192 @@ package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
cdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
|
||||
xerrors "github.com/pkg/errors"
|
||||
tcCdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
||||
ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
|
||||
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"certimate/internal/domain"
|
||||
"certimate/internal/utils/rand"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
|
||||
)
|
||||
|
||||
type tencentCdn struct {
|
||||
option *DeployerOption
|
||||
credential *common.Credential
|
||||
infos []string
|
||||
type TencentCDNDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClients *tencentCDNDeployerSdkClients
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewTencentCdn(option *DeployerOption) (Deployer, error) {
|
||||
type tencentCDNDeployerSdkClients struct {
|
||||
ssl *tcSsl.Client
|
||||
cdn *tcCdn.Client
|
||||
}
|
||||
|
||||
func NewTencentCDNDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.TencentAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err)
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
credential := common.NewCredential(
|
||||
clients, err := (&TencentCDNDeployer{}).createSdkClients(
|
||||
access.SecretId,
|
||||
access.SecretKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk clients")
|
||||
}
|
||||
|
||||
return &tencentCdn{
|
||||
option: option,
|
||||
credential: credential,
|
||||
infos: make([]string, 0),
|
||||
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &TencentCDNDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClients: clients,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *tencentCdn) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
|
||||
func (d *TencentCDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (t *tencentCdn) GetInfo() []string {
|
||||
return t.infos
|
||||
func (d *TencentCDNDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (t *tencentCdn) Deploy(ctx context.Context) error {
|
||||
// 上传证书
|
||||
certId, err := t.uploadCert()
|
||||
func (d *TencentCDNDeployer) Deploy(ctx context.Context) error {
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload certificate: %w", err)
|
||||
return err
|
||||
}
|
||||
t.infos = append(t.infos, toStr("上传证书", certId))
|
||||
|
||||
if err := t.deploy(certId); err != nil {
|
||||
return fmt.Errorf("failed to deploy: %w", err)
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 获取待部署的 CDN 实例
|
||||
// 如果是泛域名,根据证书匹配 CDN 实例
|
||||
tcInstanceIds := make([]string, 0)
|
||||
domain := d.option.DeployConfig.GetConfigAsString("domain")
|
||||
if strings.HasPrefix(domain, "*") {
|
||||
domains, err := d.getDomainsByCertificateId(upres.CertId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tcInstanceIds = domains
|
||||
} else {
|
||||
tcInstanceIds = append(tcInstanceIds, domain)
|
||||
}
|
||||
|
||||
// 跳过已部署的 CDN 实例
|
||||
if len(tcInstanceIds) > 0 {
|
||||
deployedDomains, err := d.getDeployedDomainsByCertificateId(upres.CertId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
temp := make([]string, 0)
|
||||
for _, tcInstanceId := range tcInstanceIds {
|
||||
if !slices.Contains(deployedDomains, tcInstanceId) {
|
||||
temp = append(temp, tcInstanceId)
|
||||
}
|
||||
}
|
||||
tcInstanceIds = temp
|
||||
}
|
||||
if len(tcInstanceIds) == 0 {
|
||||
d.infos = append(d.infos, "已部署过或没有要部署的 CDN 实例")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 证书部署到 CDN 实例
|
||||
// REF: https://cloud.tencent.com/document/product/400/91667
|
||||
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
|
||||
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
|
||||
deployCertificateInstanceReq.ResourceType = common.StringPtr("cdn")
|
||||
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
|
||||
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(tcInstanceIds)
|
||||
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tencentCdn) uploadCert() (string, error) {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
func (d *TencentCDNDeployer) createSdkClients(secretId, secretKey string) (*tencentCDNDeployerSdkClients, error) {
|
||||
credential := common.NewCredential(secretId, secretKey)
|
||||
|
||||
client, _ := ssl.NewClient(t.credential, "", cpf)
|
||||
|
||||
request := ssl.NewUploadCertificateRequest()
|
||||
|
||||
request.CertificatePublicKey = common.StringPtr(t.option.Certificate.Certificate)
|
||||
request.CertificatePrivateKey = common.StringPtr(t.option.Certificate.PrivateKey)
|
||||
request.Alias = common.StringPtr(t.option.Domain + "_" + rand.RandStr(6))
|
||||
request.Repeatable = common.BoolPtr(false)
|
||||
|
||||
response, err := client.UploadCertificate(request)
|
||||
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to upload certificate: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return *response.Response.CertificateId, nil
|
||||
cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tencentCDNDeployerSdkClients{
|
||||
ssl: sslClient,
|
||||
cdn: cdnClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *tencentCdn) deploy(certId string) error {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
// 实例化要请求产品的client对象,clientProfile是可选的
|
||||
client, _ := ssl.NewClient(t.credential, "", cpf)
|
||||
|
||||
// 实例化一个请求对象,每个接口都会对应一个request对象
|
||||
request := ssl.NewDeployCertificateInstanceRequest()
|
||||
|
||||
request.CertificateId = common.StringPtr(certId)
|
||||
request.ResourceType = common.StringPtr("cdn")
|
||||
request.Status = common.Int64Ptr(1)
|
||||
|
||||
// 如果是泛域名就从cdn列表下获取SSL证书中的可用域名
|
||||
domain := getDeployString(t.option.DeployConfig, "domain")
|
||||
if strings.Contains(domain, "*") {
|
||||
list, errGetList := t.getDomainList()
|
||||
if errGetList != nil {
|
||||
return fmt.Errorf("failed to get certificate domain list: %w", errGetList)
|
||||
}
|
||||
if list == nil || len(list) == 0 {
|
||||
return fmt.Errorf("failed to get certificate domain list: empty list.")
|
||||
}
|
||||
request.InstanceIdList = common.StringPtrs(list)
|
||||
} else { // 否则直接使用传入的域名
|
||||
request.InstanceIdList = common.StringPtrs([]string{domain})
|
||||
}
|
||||
|
||||
// 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
|
||||
resp, err := client.DeployCertificateInstance(request)
|
||||
func (d *TencentCDNDeployer) getDomainsByCertificateId(tcCertId string) ([]string, error) {
|
||||
// 获取证书中的可用域名
|
||||
// REF: https://cloud.tencent.com/document/product/228/42491
|
||||
describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest()
|
||||
describeCertDomainsReq.CertId = common.StringPtr(tcCertId)
|
||||
describeCertDomainsReq.Product = common.StringPtr("cdn")
|
||||
describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy certificate: %w", err)
|
||||
}
|
||||
t.infos = append(t.infos, toStr("部署证书", resp.Response))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tencentCdn) getDomainList() ([]string, error) {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "cdn.tencentcloudapi.com"
|
||||
client, _ := cdn.NewClient(t.credential, "", cpf)
|
||||
|
||||
request := cdn.NewDescribeCertDomainsRequest()
|
||||
|
||||
cert := base64.StdEncoding.EncodeToString([]byte(t.option.Certificate.Certificate))
|
||||
request.Cert = &cert
|
||||
|
||||
response, err := client.DescribeCertDomains(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get domain list: %w", err)
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'")
|
||||
}
|
||||
|
||||
domains := make([]string, 0)
|
||||
for _, domain := range response.Response.Domains {
|
||||
domains = append(domains, *domain)
|
||||
if describeCertDomainsResp.Response.Domains == nil {
|
||||
for _, domain := range describeCertDomainsResp.Response.Domains {
|
||||
domains = append(domains, *domain)
|
||||
}
|
||||
}
|
||||
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
func (d *TencentCDNDeployer) getDeployedDomainsByCertificateId(tcCertId string) ([]string, error) {
|
||||
// 根据证书查询关联 CDN 域名
|
||||
// REF: https://cloud.tencent.com/document/product/400/62674
|
||||
describeDeployedResourcesReq := tcSsl.NewDescribeDeployedResourcesRequest()
|
||||
describeDeployedResourcesReq.CertificateIds = common.StringPtrs([]string{tcCertId})
|
||||
describeDeployedResourcesReq.ResourceType = common.StringPtr("cdn")
|
||||
describeDeployedResourcesResp, err := d.sdkClients.ssl.DescribeDeployedResources(describeDeployedResourcesReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeDeployedResources'")
|
||||
}
|
||||
|
||||
domains := make([]string, 0)
|
||||
if describeDeployedResourcesResp.Response.DeployedResources != nil {
|
||||
for _, deployedResource := range describeDeployedResourcesResp.Response.DeployedResources {
|
||||
for _, resource := range deployedResource.Resources {
|
||||
domains = append(domains, *resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return domains, nil
|
||||
|
||||
328
internal/deployer/tencent_clb.go
Normal file
328
internal/deployer/tencent_clb.go
Normal file
@@ -0,0 +1,328 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
tcClb "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb/v20180317"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
||||
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
|
||||
)
|
||||
|
||||
type TencentCLBDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClients *tencentCLBDeployerSdkClients
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
type tencentCLBDeployerSdkClients struct {
|
||||
ssl *tcSsl.Client
|
||||
clb *tcClb.Client
|
||||
}
|
||||
|
||||
func NewTencentCLBDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.TencentAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
clients, err := (&TencentCLBDeployer{}).createSdkClients(
|
||||
access.SecretId,
|
||||
access.SecretKey,
|
||||
option.DeployConfig.GetConfigAsString("region"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk clients")
|
||||
}
|
||||
|
||||
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &TencentCLBDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClients: clients,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentCLBDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *TencentCLBDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *TencentCLBDeployer) Deploy(ctx context.Context) error {
|
||||
switch d.option.DeployConfig.GetConfigAsString("resourceType") {
|
||||
case "ssl-deploy":
|
||||
// 通过 SSL 服务部署到云资源实例
|
||||
err := d.deployToInstanceUseSsl(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "loadbalancer":
|
||||
// 部署到指定负载均衡器
|
||||
if err := d.deployToLoadbalancer(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
case "listener":
|
||||
// 部署到指定监听器
|
||||
if err := d.deployToListener(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
case "ruledomain":
|
||||
// 部署到指定七层监听转发规则域名
|
||||
if err := d.deployToRuleDomain(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported resource type")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentCLBDeployer) createSdkClients(secretId, secretKey, region string) (*tencentCLBDeployerSdkClients, error) {
|
||||
credential := common.NewCredential(secretId, secretKey)
|
||||
|
||||
sslClient, err := tcSsl.NewClient(credential, region, profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clbClient, err := tcClb.NewClient(credential, region, profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tencentCLBDeployerSdkClients{
|
||||
ssl: sslClient,
|
||||
clb: clbClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentCLBDeployer) deployToInstanceUseSsl(ctx context.Context) error {
|
||||
tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
|
||||
tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
|
||||
tcDomain := d.option.DeployConfig.GetConfigAsString("domain")
|
||||
if tcLoadbalancerId == "" {
|
||||
return errors.New("`loadbalancerId` is required")
|
||||
}
|
||||
if tcListenerId == "" {
|
||||
return errors.New("`listenerId` is required")
|
||||
}
|
||||
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 证书部署到 CLB 实例
|
||||
// REF: https://cloud.tencent.com/document/product/400/91667
|
||||
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
|
||||
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
|
||||
deployCertificateInstanceReq.ResourceType = common.StringPtr("clb")
|
||||
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
|
||||
if tcDomain == "" {
|
||||
// 未开启 SNI,只需指定到监听器
|
||||
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s", tcLoadbalancerId, tcListenerId)})
|
||||
} else {
|
||||
// 开启 SNI,需指定到域名(支持泛域名)
|
||||
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", tcLoadbalancerId, tcListenerId, tcDomain)})
|
||||
}
|
||||
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentCLBDeployer) deployToLoadbalancer(ctx context.Context) error {
|
||||
tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
|
||||
tcListenerIds := make([]string, 0)
|
||||
if tcLoadbalancerId == "" {
|
||||
return errors.New("`loadbalancerId` is required")
|
||||
}
|
||||
|
||||
// 查询负载均衡器详细信息
|
||||
// REF: https://cloud.tencent.com/document/api/214/46916
|
||||
describeLoadBalancersDetailReq := tcClb.NewDescribeLoadBalancersDetailRequest()
|
||||
describeLoadBalancersDetailResp, err := d.sdkClients.clb.DescribeLoadBalancersDetail(describeLoadBalancersDetailReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeLoadBalancersDetail'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到负载均衡详细信息", describeLoadBalancersDetailResp))
|
||||
|
||||
// 查询监听器列表
|
||||
// REF: https://cloud.tencent.com/document/api/214/30686
|
||||
describeListenersReq := tcClb.NewDescribeListenersRequest()
|
||||
describeListenersReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId)
|
||||
describeListenersResp, err := d.sdkClients.clb.DescribeListeners(describeListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeListeners'")
|
||||
} else {
|
||||
if describeListenersResp.Response.Listeners != nil {
|
||||
for _, listener := range describeListenersResp.Response.Listeners {
|
||||
if listener.Protocol == nil || (*listener.Protocol != "HTTPS" && *listener.Protocol != "TCP_SSL" && *listener.Protocol != "QUIC") {
|
||||
continue
|
||||
}
|
||||
|
||||
tcListenerIds = append(tcListenerIds, *listener.ListenerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到负载均衡器下的监听器", tcListenerIds))
|
||||
|
||||
// 上传证书到 SCM
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 批量更新监听器证书
|
||||
var errs []error
|
||||
for _, tcListenerId := range tcListenerIds {
|
||||
if err := d.modifyListenerCertificate(ctx, tcLoadbalancerId, tcListenerId, upres.CertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentCLBDeployer) deployToListener(ctx context.Context) error {
|
||||
tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
|
||||
tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
|
||||
if tcLoadbalancerId == "" {
|
||||
return errors.New("`loadbalancerId` is required")
|
||||
}
|
||||
if tcListenerId == "" {
|
||||
return errors.New("`listenerId` is required")
|
||||
}
|
||||
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 更新监听器证书
|
||||
if err := d.modifyListenerCertificate(ctx, tcLoadbalancerId, tcListenerId, upres.CertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentCLBDeployer) deployToRuleDomain(ctx context.Context) error {
|
||||
tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
|
||||
tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
|
||||
tcDomain := d.option.DeployConfig.GetConfigAsString("domain")
|
||||
if tcLoadbalancerId == "" {
|
||||
return errors.New("`loadbalancerId` is required")
|
||||
}
|
||||
if tcListenerId == "" {
|
||||
return errors.New("`listenerId` is required")
|
||||
}
|
||||
if tcDomain == "" {
|
||||
return errors.New("`domain` is required")
|
||||
}
|
||||
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 修改负载均衡七层监听器转发规则的域名级别属性
|
||||
// REF: https://cloud.tencent.com/document/api/214/38092
|
||||
modifyDomainAttributesReq := tcClb.NewModifyDomainAttributesRequest()
|
||||
modifyDomainAttributesReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId)
|
||||
modifyDomainAttributesReq.ListenerId = common.StringPtr(tcListenerId)
|
||||
modifyDomainAttributesReq.Domain = common.StringPtr(tcDomain)
|
||||
modifyDomainAttributesReq.Certificate = &tcClb.CertificateInput{
|
||||
SSLMode: common.StringPtr("UNIDIRECTIONAL"),
|
||||
CertId: common.StringPtr(upres.CertId),
|
||||
}
|
||||
modifyDomainAttributesResp, err := d.sdkClients.clb.ModifyDomainAttributes(modifyDomainAttributesReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'clb.ModifyDomainAttributes'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已修改七层监听器转发规则的域名级别属性", modifyDomainAttributesResp.Response))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentCLBDeployer) modifyListenerCertificate(ctx context.Context, tcLoadbalancerId, tcListenerId, tcCertId string) error {
|
||||
// 查询监听器列表
|
||||
// REF: https://cloud.tencent.com/document/api/214/30686
|
||||
describeListenersReq := tcClb.NewDescribeListenersRequest()
|
||||
describeListenersReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId)
|
||||
describeListenersReq.ListenerIds = common.StringPtrs([]string{tcListenerId})
|
||||
describeListenersResp, err := d.sdkClients.clb.DescribeListeners(describeListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeListeners'")
|
||||
}
|
||||
if len(describeListenersResp.Response.Listeners) == 0 {
|
||||
d.infos = append(d.infos, toStr("未找到监听器", nil))
|
||||
return errors.New("listener not found")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到监听器属性", describeListenersResp.Response))
|
||||
|
||||
// 修改监听器属性
|
||||
// REF: https://cloud.tencent.com/document/product/214/30681
|
||||
modifyListenerReq := tcClb.NewModifyListenerRequest()
|
||||
modifyListenerReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId)
|
||||
modifyListenerReq.ListenerId = common.StringPtr(tcListenerId)
|
||||
modifyListenerReq.Certificate = &tcClb.CertificateInput{CertId: common.StringPtr(tcCertId)}
|
||||
if describeListenersResp.Response.Listeners[0].Certificate != nil && describeListenersResp.Response.Listeners[0].Certificate.SSLMode != nil {
|
||||
modifyListenerReq.Certificate.SSLMode = describeListenersResp.Response.Listeners[0].Certificate.SSLMode
|
||||
modifyListenerReq.Certificate.CertCaId = describeListenersResp.Response.Listeners[0].Certificate.CertCaId
|
||||
} else {
|
||||
modifyListenerReq.Certificate.SSLMode = common.StringPtr("UNIDIRECTIONAL")
|
||||
}
|
||||
modifyListenerResp, err := d.sdkClients.clb.ModifyListener(modifyListenerReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'clb.ModifyListener'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已修改监听器属性", modifyListenerResp.Response))
|
||||
|
||||
return nil
|
||||
}
|
||||
107
internal/deployer/tencent_cos.go
Normal file
107
internal/deployer/tencent_cos.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
||||
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
|
||||
)
|
||||
|
||||
type TencentCOSDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *tcSsl.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewTencentCOSDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.TencentAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
client, err := (&TencentCOSDeployer{}).createSdkClient(
|
||||
access.SecretId,
|
||||
access.SecretKey,
|
||||
option.DeployConfig.GetConfigAsString("region"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk clients")
|
||||
}
|
||||
|
||||
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &TencentCOSDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentCOSDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *TencentCOSDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *TencentCOSDeployer) Deploy(ctx context.Context) error {
|
||||
tcRegion := d.option.DeployConfig.GetConfigAsString("region")
|
||||
tcBucket := d.option.DeployConfig.GetConfigAsString("bucket")
|
||||
tcDomain := d.option.DeployConfig.GetConfigAsString("domain")
|
||||
if tcBucket == "" {
|
||||
return errors.New("`bucket` is required")
|
||||
}
|
||||
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 证书部署到 COS 实例
|
||||
// REF: https://cloud.tencent.com/document/product/400/91667
|
||||
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
|
||||
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
|
||||
deployCertificateInstanceReq.ResourceType = common.StringPtr("cos")
|
||||
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
|
||||
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s#%s#%s", tcRegion, tcBucket, tcDomain)})
|
||||
deployCertificateInstanceResp, err := d.sdkClient.DeployCertificateInstance(deployCertificateInstanceReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentCOSDeployer) createSdkClient(secretId, secretKey, region string) (*tcSsl.Client, error) {
|
||||
credential := common.NewCredential(secretId, secretKey)
|
||||
client, err := tcSsl.NewClient(credential, region, profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
154
internal/deployer/tencent_ecdn.go
Normal file
154
internal/deployer/tencent_ecdn.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
tcCdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
||||
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
|
||||
)
|
||||
|
||||
type TencentECDNDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClients *tencentECDNDeployerSdkClients
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
type tencentECDNDeployerSdkClients struct {
|
||||
ssl *tcSsl.Client
|
||||
cdn *tcCdn.Client
|
||||
}
|
||||
|
||||
func NewTencentECDNDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.TencentAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
clients, err := (&TencentECDNDeployer{}).createSdkClients(
|
||||
access.SecretId,
|
||||
access.SecretKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk clients")
|
||||
}
|
||||
|
||||
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &TencentECDNDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClients: clients,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentECDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *TencentECDNDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *TencentECDNDeployer) Deploy(ctx context.Context) error {
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 获取待部署的 ECDN 实例
|
||||
// 如果是泛域名,根据证书匹配 ECDN 实例
|
||||
aliInstanceIds := make([]string, 0)
|
||||
domain := d.option.DeployConfig.GetConfigAsString("domain")
|
||||
if strings.HasPrefix(domain, "*") {
|
||||
domains, err := d.getDomainsByCertificateId(upres.CertId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aliInstanceIds = domains
|
||||
} else {
|
||||
aliInstanceIds = append(aliInstanceIds, domain)
|
||||
}
|
||||
if len(aliInstanceIds) == 0 {
|
||||
d.infos = append(d.infos, "没有要部署的 ECDN 实例")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 证书部署到 ECDN 实例
|
||||
// REF: https://cloud.tencent.com/document/product/400/91667
|
||||
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
|
||||
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
|
||||
deployCertificateInstanceReq.ResourceType = common.StringPtr("ecdn")
|
||||
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
|
||||
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(aliInstanceIds)
|
||||
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentECDNDeployer) createSdkClients(secretId, secretKey string) (*tencentECDNDeployerSdkClients, error) {
|
||||
credential := common.NewCredential(secretId, secretKey)
|
||||
|
||||
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tencentECDNDeployerSdkClients{
|
||||
ssl: sslClient,
|
||||
cdn: cdnClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentECDNDeployer) getDomainsByCertificateId(tcCertId string) ([]string, error) {
|
||||
// 获取证书中的可用域名
|
||||
// REF: https://cloud.tencent.com/document/product/228/42491
|
||||
describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest()
|
||||
describeCertDomainsReq.CertId = common.StringPtr(tcCertId)
|
||||
describeCertDomainsReq.Product = common.StringPtr("ecdn")
|
||||
describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'")
|
||||
}
|
||||
|
||||
domains := make([]string, 0)
|
||||
if describeCertDomainsResp.Response.Domains == nil {
|
||||
for _, domain := range describeCertDomainsResp.Response.Domains {
|
||||
domains = append(domains, *domain)
|
||||
}
|
||||
}
|
||||
|
||||
return domains, nil
|
||||
}
|
||||
119
internal/deployer/tencent_teo.go
Normal file
119
internal/deployer/tencent_teo.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
||||
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
|
||||
tcTeo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
|
||||
)
|
||||
|
||||
type TencentTEODeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClients *tencentTEODeployerSdkClients
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
type tencentTEODeployerSdkClients struct {
|
||||
ssl *tcSsl.Client
|
||||
teo *tcTeo.Client
|
||||
}
|
||||
|
||||
func NewTencentTEODeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.TencentAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
clients, err := (&TencentTEODeployer{}).createSdkClients(
|
||||
access.SecretId,
|
||||
access.SecretKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk clients")
|
||||
}
|
||||
|
||||
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &TencentTEODeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClients: clients,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentTEODeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *TencentTEODeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *TencentTEODeployer) Deploy(ctx context.Context) error {
|
||||
tcZoneId := d.option.DeployConfig.GetConfigAsString("zoneId")
|
||||
if tcZoneId == "" {
|
||||
return xerrors.New("`zoneId` is required")
|
||||
}
|
||||
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
// 配置域名证书
|
||||
// REF: https://cloud.tencent.com/document/product/1552/80764
|
||||
modifyHostsCertificateReq := tcTeo.NewModifyHostsCertificateRequest()
|
||||
modifyHostsCertificateReq.ZoneId = common.StringPtr(tcZoneId)
|
||||
modifyHostsCertificateReq.Mode = common.StringPtr("sslcert")
|
||||
modifyHostsCertificateReq.Hosts = common.StringPtrs(strings.Split(strings.ReplaceAll(d.option.Domain, "\r\n", "\n"), "\n"))
|
||||
modifyHostsCertificateReq.ServerCertInfo = []*tcTeo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}}
|
||||
modifyHostsCertificateResp, err := d.sdkClients.teo.ModifyHostsCertificate(modifyHostsCertificateReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'teo.ModifyHostsCertificate'")
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已配置域名证书", modifyHostsCertificateResp.Response))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentTEODeployer) createSdkClients(secretId, secretKey string) (*tencentTEODeployerSdkClients, error) {
|
||||
credential := common.NewCredential(secretId, secretKey)
|
||||
|
||||
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
teoClient, err := tcTeo.NewClient(credential, "", profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tencentTEODeployerSdkClients{
|
||||
ssl: sslClient,
|
||||
teo: teoClient,
|
||||
}, nil
|
||||
}
|
||||
116
internal/deployer/volcengine_cdn.go
Normal file
116
internal/deployer/volcengine_cdn.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
volcenginecdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-cdn"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
"github.com/volcengine/volc-sdk-golang/service/cdn"
|
||||
)
|
||||
|
||||
type VolcengineCDNDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
sdkClient *cdn.CDN
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewVolcengineCDNDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.VolcEngineAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
client := cdn.NewInstance()
|
||||
client.Client.SetAccessKey(access.AccessKeyId)
|
||||
client.Client.SetSecretKey(access.SecretAccessKey)
|
||||
uploader, err := volcenginecdn.New(&volcenginecdn.VolcEngineCDNUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.SecretAccessKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
return &VolcengineCDNDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *VolcengineCDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *VolcengineCDNDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *VolcengineCDNDeployer) Deploy(ctx context.Context) error {
|
||||
apiCtx := context.Background()
|
||||
// 上传证书
|
||||
upres, err := d.sslUploader.Upload(apiCtx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
domains := make([]string, 0)
|
||||
configDomain := d.option.DeployConfig.GetConfigAsString("domain")
|
||||
if strings.HasPrefix(configDomain, "*.") {
|
||||
// 获取证书可以部署的域名
|
||||
// REF: https://www.volcengine.com/docs/6454/125711
|
||||
describeCertConfigReq := &cdn.DescribeCertConfigRequest{
|
||||
CertId: upres.CertId,
|
||||
}
|
||||
describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'")
|
||||
}
|
||||
for i := range describeCertConfigResp.Result.CertNotConfig {
|
||||
// 当前未启用 HTTPS 的加速域名列表。
|
||||
domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)
|
||||
}
|
||||
for i := range describeCertConfigResp.Result.OtherCertConfig {
|
||||
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联的证书不是您指定的证书。
|
||||
domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)
|
||||
}
|
||||
for i := range describeCertConfigResp.Result.SpecifiedCertConfig {
|
||||
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联了您指定的证书。
|
||||
d.infos = append(d.infos, fmt.Sprintf("%s域名已配置该证书", describeCertConfigResp.Result.SpecifiedCertConfig[i].Domain))
|
||||
}
|
||||
if len(domains) == 0 {
|
||||
if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 {
|
||||
// 所有匹配的域名都配置了该证书,跳过部署
|
||||
return nil
|
||||
} else {
|
||||
return xerrors.Errorf("未查询到匹配的域名: %s", configDomain)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
domains = append(domains, configDomain)
|
||||
}
|
||||
// 部署证书
|
||||
// REF: https://www.volcengine.com/docs/6454/125712
|
||||
for i := range domains {
|
||||
batchDeployCertReq := &cdn.BatchDeployCertRequest{
|
||||
CertId: upres.CertId,
|
||||
Domain: domains[i],
|
||||
}
|
||||
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.BatchDeployCert'")
|
||||
} else {
|
||||
d.infos = append(d.infos, toStr(fmt.Sprintf("%s域名的证书已修改", domains[i]), batchDeployCertResp))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
148
internal/deployer/volcengine_live.go
Normal file
148
internal/deployer/volcengine_live.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
volcenginelive "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-live"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/cast"
|
||||
"github.com/volcengine/volc-sdk-golang/base"
|
||||
live "github.com/volcengine/volc-sdk-golang/service/live/v20230101"
|
||||
)
|
||||
|
||||
type VolcengineLiveDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
sdkClient *live.Live
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewVolcengineLiveDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.VolcEngineAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
client := live.NewInstance()
|
||||
client.SetCredential(base.Credentials{
|
||||
AccessKeyID: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
})
|
||||
uploader, err := volcenginelive.New(&volcenginelive.VolcEngineLiveUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.SecretAccessKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
return &VolcengineLiveDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *VolcengineLiveDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *VolcengineLiveDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *VolcengineLiveDeployer) Deploy(ctx context.Context) error {
|
||||
apiCtx := context.Background()
|
||||
// 上传证书
|
||||
upres, err := d.sslUploader.Upload(apiCtx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
domains := make([]string, 0)
|
||||
configDomain := d.option.DeployConfig.GetConfigAsString("domain")
|
||||
if strings.HasPrefix(configDomain, "*.") {
|
||||
// 如果是泛域名,获取所有的域名并匹配
|
||||
matchDomains, err := d.getDomainsByWildcardDomain(apiCtx, configDomain)
|
||||
if err != nil {
|
||||
d.infos = append(d.infos, toStr("获取域名列表失败", upres))
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'live.ListDomainDetail'")
|
||||
}
|
||||
if len(matchDomains) == 0 {
|
||||
return xerrors.Errorf("未查询到匹配的域名: %s", configDomain)
|
||||
}
|
||||
domains = matchDomains
|
||||
} else {
|
||||
domains = append(domains, configDomain)
|
||||
}
|
||||
|
||||
// 部署证书
|
||||
// REF: https://www.volcengine.com/docs/6469/1186278#%E7%BB%91%E5%AE%9A%E8%AF%81%E4%B9%A6d
|
||||
for i := range domains {
|
||||
bindCertReq := &live.BindCertBody{
|
||||
ChainID: upres.CertId,
|
||||
Domain: domains[i],
|
||||
HTTPS: cast.BoolPtr(true),
|
||||
}
|
||||
bindCertResp, err := d.sdkClient.BindCert(apiCtx, bindCertReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'live.BindCert'")
|
||||
} else {
|
||||
d.infos = append(d.infos, toStr(fmt.Sprintf("%s域名的证书已修改", domains[i]), bindCertResp))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *VolcengineLiveDeployer) getDomainsByWildcardDomain(ctx context.Context, wildcardDomain string) ([]string, error) {
|
||||
pageNum := int32(1)
|
||||
searchTotal := 0
|
||||
domains := make([]string, 0)
|
||||
for {
|
||||
listDomainDetailReq := &live.ListDomainDetailBody{
|
||||
PageNum: pageNum,
|
||||
PageSize: 1000,
|
||||
}
|
||||
// 查询域名列表
|
||||
// REF: https://www.volcengine.com/docs/6469/1186277#%E6%9F%A5%E8%AF%A2%E5%9F%9F%E5%90%8D%E5%88%97%E8%A1%A8
|
||||
listDomainDetailResp, err := d.sdkClient.ListDomainDetail(ctx, listDomainDetailReq)
|
||||
if err != nil {
|
||||
return domains, err
|
||||
}
|
||||
if listDomainDetailResp.Result.DomainList != nil {
|
||||
for _, item := range listDomainDetailResp.Result.DomainList {
|
||||
if matchWildcardDomain(item.Domain, wildcardDomain) {
|
||||
domains = append(domains, item.Domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
searchTotal += len(listDomainDetailResp.Result.DomainList)
|
||||
if int(listDomainDetailResp.Result.Total) > searchTotal {
|
||||
pageNum++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
func matchWildcardDomain(domain, wildcardDomain string) bool {
|
||||
if strings.HasPrefix(wildcardDomain, "*.") {
|
||||
if "*."+domain == wildcardDomain {
|
||||
return true
|
||||
}
|
||||
regexPattern := "^([a-zA-Z0-9_-]+)\\." + regexp.QuoteMeta(wildcardDomain[2:]) + "$"
|
||||
regex := regexp.MustCompile(regexPattern)
|
||||
return regex.MatchString(domain)
|
||||
}
|
||||
return domain == wildcardDomain
|
||||
}
|
||||
@@ -7,63 +7,60 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
xhttp "certimate/internal/utils/http"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
xhttp "github.com/usual2970/certimate/internal/utils/http"
|
||||
)
|
||||
|
||||
type webhookAccess struct {
|
||||
Url string `json:"url"`
|
||||
type WebhookDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
type hookData struct {
|
||||
func NewWebhookDeployer(option *DeployerOption) (Deployer, error) {
|
||||
return &WebhookDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *WebhookDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *WebhookDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
type webhookData struct {
|
||||
Domain string `json:"domain"`
|
||||
Certificate string `json:"certificate"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
|
||||
type webhook struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewWebhook(option *DeployerOption) (Deployer, error) {
|
||||
return &webhook{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *webhook) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
|
||||
}
|
||||
|
||||
func (w *webhook) GetInfo() []string {
|
||||
return w.infos
|
||||
}
|
||||
|
||||
func (w *webhook) Deploy(ctx context.Context) error {
|
||||
access := &webhookAccess{}
|
||||
if err := json.Unmarshal([]byte(w.option.Access), access); err != nil {
|
||||
return fmt.Errorf("failed to parse hook access config: %w", err)
|
||||
func (d *WebhookDeployer) Deploy(ctx context.Context) error {
|
||||
access := &domain.WebhookAccess{}
|
||||
if err := json.Unmarshal([]byte(d.option.Access), access); err != nil {
|
||||
return xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
|
||||
data := &hookData{
|
||||
Domain: w.option.Domain,
|
||||
Certificate: w.option.Certificate.Certificate,
|
||||
PrivateKey: w.option.Certificate.PrivateKey,
|
||||
Variables: getDeployVariables(w.option.DeployConfig),
|
||||
data := &webhookData{
|
||||
Domain: d.option.Domain,
|
||||
Certificate: d.option.Certificate.Certificate,
|
||||
PrivateKey: d.option.Certificate.PrivateKey,
|
||||
Variables: d.option.DeployConfig.GetConfigAsVariables(),
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(data)
|
||||
|
||||
resp, err := xhttp.Req(access.Url, http.MethodPost, bytes.NewReader(body), map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send hook request: %w", err)
|
||||
return xerrors.Wrap(err, "failed to send webhook request")
|
||||
}
|
||||
|
||||
w.infos = append(w.infos, toStr("webhook response", string(resp)))
|
||||
d.infos = append(d.infos, toStr("Webhook Response", string(resp)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,15 +5,32 @@ type AliyunAccess struct {
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
}
|
||||
|
||||
type ByteplusAccess struct {
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type TencentAccess struct {
|
||||
SecretId string `json:"secretId"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type HuaweiCloudAccess struct {
|
||||
Region string `json:"region"`
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
type BaiduCloudAccess struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
}
|
||||
|
||||
type AwsAccess struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
Region string `json:"region"`
|
||||
HostedZoneId string `json:"hostedZoneId"`
|
||||
}
|
||||
|
||||
type CloudflareAccess struct {
|
||||
@@ -25,6 +42,11 @@ type QiniuAccess struct {
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type DogeCloudAccess struct {
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type NameSiloAccess struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
@@ -33,3 +55,44 @@ type GodaddyAccess struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
ApiSecret string `json:"apiSecret"`
|
||||
}
|
||||
|
||||
type PdnsAccess struct {
|
||||
ApiUrl string `json:"apiUrl"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
type VolcEngineAccess struct {
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
|
||||
// Deprecated: Use [AccessKey] and [SecretKey] instead in the future
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// Deprecated: Use [AccessKey] and [SecretKey] instead in the future
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
}
|
||||
|
||||
type HttpreqAccess struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Mode string `json:"mode"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type LocalAccess struct{}
|
||||
|
||||
type SSHAccess struct {
|
||||
Host string `json:"host"`
|
||||
Port string `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Key string `json:"key"`
|
||||
KeyPassphrase string `json:"keyPassphrase"`
|
||||
}
|
||||
|
||||
type WebhookAccess struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type KubernetesAccess struct {
|
||||
KubeConfig string `json:"kubeConfig"`
|
||||
}
|
||||
|
||||
17
internal/domain/acme_accounts.go
Normal file
17
internal/domain/acme_accounts.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
)
|
||||
|
||||
type AcmeAccount struct {
|
||||
Id string
|
||||
Ca string
|
||||
Email string
|
||||
Resource *registration.Resource
|
||||
Key string
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/maps"
|
||||
)
|
||||
|
||||
type ApplyConfig struct {
|
||||
Email string `json:"email"`
|
||||
Access string `json:"access"`
|
||||
KeyAlgorithm string `json:"keyAlgorithm"`
|
||||
Nameservers string `json:"nameservers"`
|
||||
Timeout int64 `json:"timeout"`
|
||||
Email string `json:"email"`
|
||||
Access string `json:"access"`
|
||||
KeyAlgorithm string `json:"keyAlgorithm"`
|
||||
Nameservers string `json:"nameservers"`
|
||||
Timeout int64 `json:"timeout"`
|
||||
DisableFollowCNAME bool `json:"disableFollowCNAME"`
|
||||
}
|
||||
|
||||
type DeployConfig struct {
|
||||
@@ -15,6 +23,121 @@ type DeployConfig struct {
|
||||
Config map[string]any `json:"config"`
|
||||
}
|
||||
|
||||
// 以字符串形式获取配置项。
|
||||
//
|
||||
// 入参:
|
||||
// - key: 配置项的键。
|
||||
//
|
||||
// 出参:
|
||||
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回空字符串。
|
||||
func (dc *DeployConfig) GetConfigAsString(key string) string {
|
||||
return maps.GetValueAsString(dc.Config, key)
|
||||
}
|
||||
|
||||
// 以字符串形式获取配置项。
|
||||
//
|
||||
// 入参:
|
||||
// - key: 配置项的键。
|
||||
// - defaultValue: 默认值。
|
||||
//
|
||||
// 出参:
|
||||
// - 配置项的值。如果配置项不存在、类型不是字符串或者值为零值,则返回默认值。
|
||||
func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue string) string {
|
||||
return maps.GetValueOrDefaultAsString(dc.Config, key, defaultValue)
|
||||
}
|
||||
|
||||
// 以 32 位整数形式获取配置项。
|
||||
//
|
||||
// 入参:
|
||||
// - key: 配置项的键。
|
||||
//
|
||||
// 出参:
|
||||
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回 0。
|
||||
func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
|
||||
return maps.GetValueAsInt32(dc.Config, key)
|
||||
}
|
||||
|
||||
// 以 32 位整数形式获取配置项。
|
||||
//
|
||||
// 入参:
|
||||
// - key: 配置项的键。
|
||||
// - defaultValue: 默认值。
|
||||
//
|
||||
// 出参:
|
||||
// - 配置项的值。如果配置项不存在、类型不是 32 位整数或者值为零值,则返回默认值。
|
||||
func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 {
|
||||
return maps.GetValueOrDefaultAsInt32(dc.Config, key, defaultValue)
|
||||
}
|
||||
|
||||
// 以布尔形式获取配置项。
|
||||
//
|
||||
// 入参:
|
||||
// - key: 配置项的键。
|
||||
//
|
||||
// 出参:
|
||||
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回 false。
|
||||
func (dc *DeployConfig) GetConfigAsBool(key string) bool {
|
||||
return maps.GetValueAsBool(dc.Config, key)
|
||||
}
|
||||
|
||||
// 以布尔形式获取配置项。
|
||||
//
|
||||
// 入参:
|
||||
// - key: 配置项的键。
|
||||
// - defaultValue: 默认值。
|
||||
//
|
||||
// 出参:
|
||||
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回默认值。
|
||||
func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool) bool {
|
||||
return maps.GetValueOrDefaultAsBool(dc.Config, key, defaultValue)
|
||||
}
|
||||
|
||||
// 以变量字典形式获取配置项。
|
||||
//
|
||||
// 出参:
|
||||
// - 变量字典。
|
||||
func (dc *DeployConfig) GetConfigAsVariables() map[string]string {
|
||||
rs := make(map[string]string)
|
||||
|
||||
if dc.Config != nil {
|
||||
value, ok := dc.Config["variables"]
|
||||
if !ok {
|
||||
return rs
|
||||
}
|
||||
|
||||
kvs := make([]KV, 0)
|
||||
bts, _ := json.Marshal(value)
|
||||
if err := json.Unmarshal(bts, &kvs); err != nil {
|
||||
return rs
|
||||
}
|
||||
|
||||
for _, kv := range kvs {
|
||||
rs[kv.Key] = kv.Value
|
||||
}
|
||||
}
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
// GetDomain returns the domain from the deploy config
|
||||
// if the domain is a wildcard domain, and wildcard is true, return the wildcard domain
|
||||
func (dc *DeployConfig) GetDomain(wildcard ...bool) string {
|
||||
val := dc.GetConfigAsString("domain")
|
||||
if val == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(val, "*") {
|
||||
return val
|
||||
}
|
||||
|
||||
if len(wildcard) > 0 && wildcard[0] {
|
||||
return val
|
||||
}
|
||||
|
||||
return strings.TrimPrefix(val, "*")
|
||||
}
|
||||
|
||||
type KV struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
|
||||
23
internal/domain/err.go
Normal file
23
internal/domain/err.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package domain
|
||||
|
||||
var ErrAuthFailed = NewXError(4999, "auth failed")
|
||||
|
||||
type XError struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func NewXError(code int, msg string) *XError {
|
||||
return &XError{code, msg}
|
||||
}
|
||||
|
||||
func (e *XError) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
func (e *XError) GetCode() int {
|
||||
if e.Code == 0 {
|
||||
return 100
|
||||
}
|
||||
return e.Code
|
||||
}
|
||||
15
internal/domain/notify.go
Normal file
15
internal/domain/notify.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package domain
|
||||
|
||||
const (
|
||||
NotifyChannelEmail = "email"
|
||||
NotifyChannelWebhook = "webhook"
|
||||
NotifyChannelDingtalk = "dingtalk"
|
||||
NotifyChannelLark = "lark"
|
||||
NotifyChannelTelegram = "telegram"
|
||||
NotifyChannelServerChan = "serverchan"
|
||||
NotifyChannelBark = "bark"
|
||||
)
|
||||
|
||||
type NotifyTestPushReq struct {
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
31
internal/domain/setting.go
Normal file
31
internal/domain/setting.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
type ChannelsConfig map[string]map[string]any
|
||||
|
||||
func (s *Setting) GetChannelContent(channel string) (map[string]any, error) {
|
||||
conf := &ChannelsConfig{}
|
||||
if err := json.Unmarshal([]byte(s.Content), conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, ok := (*conf)[channel]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("channel \"%s\" not found", channel)
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
@@ -2,14 +2,22 @@ package domains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"certimate/internal/applicant"
|
||||
"certimate/internal/deployer"
|
||||
"certimate/internal/utils/app"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/usual2970/certimate/internal/applicant"
|
||||
"github.com/usual2970/certimate/internal/deployer"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
)
|
||||
|
||||
type Phase string
|
||||
@@ -20,6 +28,8 @@ const (
|
||||
deployPhase Phase = "deploy"
|
||||
)
|
||||
|
||||
const validityDuration = time.Hour * 24 * 10
|
||||
|
||||
func deploy(ctx context.Context, record *models.Record) error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -45,7 +55,10 @@ func deploy(ctx context.Context, record *models.Record) error {
|
||||
cert := currRecord.GetString("certificate")
|
||||
expiredAt := currRecord.GetDateTime("expiredAt").Time()
|
||||
|
||||
if cert != "" && time.Until(expiredAt) > time.Hour*24*10 && currRecord.GetBool("deployed") {
|
||||
// 检查证书是否包含设置的所有域名
|
||||
changed := isCertChanged(cert, currRecord)
|
||||
|
||||
if cert != "" && time.Until(expiredAt) > validityDuration && currRecord.GetBool("deployed") && !changed {
|
||||
app.GetApp().Logger().Info("证书在有效期内")
|
||||
history.record(checkPhase, "证书在有效期内且已部署,跳过", &RecordInfo{
|
||||
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
|
||||
@@ -60,7 +73,7 @@ func deploy(ctx context.Context, record *models.Record) error {
|
||||
// ############2.申请证书
|
||||
history.record(applyPhase, "开始申请", nil)
|
||||
|
||||
if cert != "" && time.Until(expiredAt) > time.Hour*24 {
|
||||
if cert != "" && time.Until(expiredAt) > validityDuration && !changed {
|
||||
history.record(applyPhase, "证书在有效期内,跳过", &RecordInfo{
|
||||
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
|
||||
})
|
||||
@@ -105,11 +118,11 @@ func deploy(ctx context.Context, record *models.Record) error {
|
||||
if err = deployer.Deploy(ctx); err != nil {
|
||||
|
||||
app.GetApp().Logger().Error("部署失败", "err", err)
|
||||
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfo()})
|
||||
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfos()})
|
||||
return err
|
||||
}
|
||||
history.record(deployPhase, fmt.Sprintf("[%s]-部署成功", deployer.GetID()), &RecordInfo{
|
||||
Info: deployer.GetInfo(),
|
||||
Info: deployer.GetInfos(),
|
||||
}, false)
|
||||
|
||||
}
|
||||
@@ -121,3 +134,80 @@ func deploy(ctx context.Context, record *models.Record) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isCertChanged(certificate string, record *models.Record) bool {
|
||||
// 如果证书为空,直接返回true
|
||||
if certificate == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 解析证书
|
||||
cert, err := x509.ParseCertificateFromPEM(certificate)
|
||||
if err != nil {
|
||||
app.GetApp().Logger().Error("解析证书失败", "err", err)
|
||||
return true
|
||||
}
|
||||
|
||||
// 遍历域名列表,检查是否都在证书中,找到第一个不存在证书中域名时提前返回true
|
||||
for _, domain := range strings.Split(record.GetString("domain"), ";") {
|
||||
if !slices.Contains(cert.DNSNames, domain) && !slices.Contains(cert.DNSNames, "*."+removeLastSubdomain(domain)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 解析applyConfig
|
||||
applyConfig := &domain.ApplyConfig{}
|
||||
record.UnmarshalJSONField("applyConfig", applyConfig)
|
||||
|
||||
// 检查证书加密算法是否变更
|
||||
switch pubkey := cert.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
bitSize := pubkey.N.BitLen()
|
||||
switch bitSize {
|
||||
case 2048:
|
||||
// RSA2048
|
||||
if applyConfig.KeyAlgorithm != "" && applyConfig.KeyAlgorithm != "RSA2048" {
|
||||
return true
|
||||
}
|
||||
case 3072:
|
||||
// RSA3072
|
||||
if applyConfig.KeyAlgorithm != "RSA3072" {
|
||||
return true
|
||||
}
|
||||
case 4096:
|
||||
// RSA4096
|
||||
if applyConfig.KeyAlgorithm != "RSA4096" {
|
||||
return true
|
||||
}
|
||||
case 8192:
|
||||
// RSA8192
|
||||
if applyConfig.KeyAlgorithm != "RSA8192" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case *ecdsa.PublicKey:
|
||||
bitSize := pubkey.Curve.Params().BitSize
|
||||
switch bitSize {
|
||||
case 256:
|
||||
// EC256
|
||||
if applyConfig.KeyAlgorithm != "EC256" {
|
||||
return true
|
||||
}
|
||||
case 384:
|
||||
// EC384
|
||||
if applyConfig.KeyAlgorithm != "EC384" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func removeLastSubdomain(domain string) string {
|
||||
parts := strings.Split(domain, ".")
|
||||
if len(parts) > 1 {
|
||||
return strings.Join(parts[1:], ".")
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"certimate/internal/utils/app"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
func create(ctx context.Context, record *models.Record) error {
|
||||
|
||||
@@ -3,7 +3,7 @@ package domains
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
|
||||
"certimate/internal/utils/app"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
const tableName = "domains"
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"certimate/internal/applicant"
|
||||
"certimate/internal/utils/app"
|
||||
"certimate/internal/utils/xtime"
|
||||
"github.com/usual2970/certimate/internal/applicant"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
"github.com/usual2970/certimate/internal/utils/xtime"
|
||||
)
|
||||
|
||||
type historyItem struct {
|
||||
|
||||
@@ -3,8 +3,8 @@ package domains
|
||||
import (
|
||||
"context"
|
||||
|
||||
"certimate/internal/notify"
|
||||
"certimate/internal/utils/app"
|
||||
"github.com/usual2970/certimate/internal/notify"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
func InitSchedule() {
|
||||
|
||||
@@ -8,23 +8,17 @@ import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"certimate/internal/utils/app"
|
||||
"certimate/internal/utils/xtime"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
"github.com/usual2970/certimate/internal/utils/xtime"
|
||||
)
|
||||
|
||||
type msg struct {
|
||||
subject string
|
||||
message string
|
||||
}
|
||||
|
||||
const (
|
||||
defaultExpireSubject = "您有{COUNT}张证书即将过期"
|
||||
defaultExpireMsg = "有{COUNT}张证书即将过期,域名分别为{DOMAINS},请保持关注!"
|
||||
defaultExpireSubject = "您有 {COUNT} 张证书即将过期"
|
||||
defaultExpireMessage = "有 {COUNT} 张证书即将过期,域名分别为 {DOMAINS},请保持关注!"
|
||||
)
|
||||
|
||||
func PushExpireMsg() {
|
||||
// 查询即将过期的证书
|
||||
|
||||
records, err := app.GetApp().Dao().FindRecordsByFilter("domains", "expiredAt<{:time}&&certUrl!=''", "-created", 500, 0,
|
||||
dbx.Params{"time": xtime.GetTimeAfter(24 * time.Hour * 15)})
|
||||
if err != nil {
|
||||
@@ -34,12 +28,12 @@ func PushExpireMsg() {
|
||||
|
||||
// 组装消息
|
||||
msg := buildMsg(records)
|
||||
|
||||
if msg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := Send(msg.subject, msg.message); err != nil {
|
||||
// 发送通知
|
||||
if err := SendToAllChannels(msg.Subject, msg.Message); err != nil {
|
||||
app.GetApp().Logger().Error("send expire msg", "error", err)
|
||||
}
|
||||
}
|
||||
@@ -53,22 +47,27 @@ type notifyTemplate struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func buildMsg(records []*models.Record) *msg {
|
||||
type notifyMessage struct {
|
||||
Subject string
|
||||
Message string
|
||||
}
|
||||
|
||||
func buildMsg(records []*models.Record) *notifyMessage {
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 查询模板信息
|
||||
templateRecord, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='templates'")
|
||||
title := defaultExpireSubject
|
||||
content := defaultExpireMsg
|
||||
subject := defaultExpireSubject
|
||||
message := defaultExpireMessage
|
||||
|
||||
if err == nil {
|
||||
var templates *notifyTemplates
|
||||
templateRecord.UnmarshalJSONField("content", templates)
|
||||
if templates != nil && len(templates.NotifyTemplates) > 0 {
|
||||
title = templates.NotifyTemplates[0].Title
|
||||
content = templates.NotifyTemplates[0].Content
|
||||
subject = templates.NotifyTemplates[0].Title
|
||||
message = templates.NotifyTemplates[0].Content
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,17 +80,17 @@ func buildMsg(records []*models.Record) *msg {
|
||||
}
|
||||
|
||||
countStr := strconv.Itoa(count)
|
||||
domainStr := strings.Join(domains, ",")
|
||||
domainStr := strings.Join(domains, ";")
|
||||
|
||||
title = strings.ReplaceAll(title, "{COUNT}", countStr)
|
||||
title = strings.ReplaceAll(title, "{DOMAINS}", domainStr)
|
||||
subject = strings.ReplaceAll(subject, "{COUNT}", countStr)
|
||||
subject = strings.ReplaceAll(subject, "{DOMAINS}", domainStr)
|
||||
|
||||
content = strings.ReplaceAll(content, "{COUNT}", countStr)
|
||||
content = strings.ReplaceAll(content, "{DOMAINS}", domainStr)
|
||||
message = strings.ReplaceAll(message, "{COUNT}", countStr)
|
||||
message = strings.ReplaceAll(message, "{DOMAINS}", domainStr)
|
||||
|
||||
// 返回消息
|
||||
return &msg{
|
||||
subject: title,
|
||||
message: content,
|
||||
return ¬ifyMessage{
|
||||
Subject: subject,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
66
internal/notify/factory.go
Normal file
66
internal/notify/factory.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||
providerBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
|
||||
providerDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
|
||||
providerEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
|
||||
providerLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
|
||||
providerServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
|
||||
providerTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
|
||||
providerWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/maps"
|
||||
)
|
||||
|
||||
func createNotifier(channel string, channelConfig map[string]any) (notifier.Notifier, error) {
|
||||
switch channel {
|
||||
case domain.NotifyChannelEmail:
|
||||
return providerEmail.New(&providerEmail.EmailNotifierConfig{
|
||||
SmtpHost: maps.GetValueAsString(channelConfig, "smtpHost"),
|
||||
SmtpPort: maps.GetValueAsInt32(channelConfig, "smtpPort"),
|
||||
SmtpTLS: maps.GetValueOrDefaultAsBool(channelConfig, "smtpTLS", true),
|
||||
Username: maps.GetValueOrDefaultAsString(channelConfig, "username", maps.GetValueAsString(channelConfig, "senderAddress")),
|
||||
Password: maps.GetValueAsString(channelConfig, "password"),
|
||||
SenderAddress: maps.GetValueAsString(channelConfig, "senderAddress"),
|
||||
ReceiverAddress: maps.GetValueAsString(channelConfig, "receiverAddress"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelWebhook:
|
||||
return providerWebhook.New(&providerWebhook.WebhookNotifierConfig{
|
||||
Url: maps.GetValueAsString(channelConfig, "url"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelDingtalk:
|
||||
return providerDingTalk.New(&providerDingTalk.DingTalkNotifierConfig{
|
||||
AccessToken: maps.GetValueAsString(channelConfig, "accessToken"),
|
||||
Secret: maps.GetValueAsString(channelConfig, "secret"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelLark:
|
||||
return providerLark.New(&providerLark.LarkNotifierConfig{
|
||||
WebhookUrl: maps.GetValueAsString(channelConfig, "webhookUrl"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTelegram:
|
||||
return providerTelegram.New(&providerTelegram.TelegramNotifierConfig{
|
||||
ApiToken: maps.GetValueAsString(channelConfig, "apiToken"),
|
||||
ChatId: maps.GetValueAsInt64(channelConfig, "chatId"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelServerChan:
|
||||
return providerServerChan.New(&providerServerChan.ServerChanNotifierConfig{
|
||||
Url: maps.GetValueAsString(channelConfig, "url"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelBark:
|
||||
return providerBark.New(&providerBark.BarkNotifierConfig{
|
||||
DeviceKey: maps.GetValueAsString(channelConfig, "deviceKey"),
|
||||
ServerUrl: maps.GetValueAsString(channelConfig, "serverUrl"),
|
||||
})
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported notifier channel: %s", channelConfig)
|
||||
}
|
||||
@@ -3,25 +3,16 @@ package notify
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
notifyPackage "github.com/nikoksr/notify"
|
||||
"github.com/nikoksr/notify/service/dingding"
|
||||
"github.com/nikoksr/notify/service/http"
|
||||
"github.com/nikoksr/notify/service/telegram"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"certimate/internal/utils/app"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/maps"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
const (
|
||||
notifyChannelDingtalk = "dingtalk"
|
||||
notifyChannelWebhook = "webhook"
|
||||
notifyChannelTelegram = "telegram"
|
||||
)
|
||||
|
||||
func Send(title, content string) error {
|
||||
// 获取所有的推送渠道
|
||||
notifiers, err := getNotifiers()
|
||||
func SendToAllChannels(subject, message string) error {
|
||||
notifiers, err := getEnabledNotifiers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -29,97 +20,56 @@ func Send(title, content string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
n := notifyPackage.New()
|
||||
// 添加推送渠道
|
||||
n.UseServices(notifiers...)
|
||||
var eg errgroup.Group
|
||||
for _, n := range notifiers {
|
||||
if n == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
return n.Send(context.Background(), title, content)
|
||||
eg.Go(func() error {
|
||||
_, err := n.Notify(context.Background(), subject, message)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
err = eg.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
func getNotifiers() ([]notifyPackage.Notifier, error) {
|
||||
resp, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='notifyChannels'")
|
||||
func SendToChannel(subject, message string, channel string, channelConfig map[string]any) error {
|
||||
notifier, err := createNotifier(channel, channelConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = notifier.Notify(context.Background(), subject, message)
|
||||
return err
|
||||
}
|
||||
|
||||
func getEnabledNotifiers() ([]notifier.Notifier, error) {
|
||||
settings, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='notifyChannels'")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find notifyChannels error: %w", err)
|
||||
}
|
||||
|
||||
notifiers := make([]notifyPackage.Notifier, 0)
|
||||
|
||||
rs := make(map[string]map[string]any)
|
||||
|
||||
if err := resp.UnmarshalJSONField("content", &rs); err != nil {
|
||||
if err := settings.UnmarshalJSONField("content", &rs); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal notifyChannels error: %w", err)
|
||||
}
|
||||
|
||||
notifiers := make([]notifier.Notifier, 0)
|
||||
for k, v := range rs {
|
||||
|
||||
if !getBool(v, "enabled") {
|
||||
if !maps.GetValueAsBool(v, "enabled") {
|
||||
continue
|
||||
}
|
||||
|
||||
switch k {
|
||||
case notifyChannelTelegram:
|
||||
temp := getTelegramNotifier(v)
|
||||
if temp == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
notifiers = append(notifiers, temp)
|
||||
case notifyChannelDingtalk:
|
||||
notifiers = append(notifiers, getDingTalkNotifier(v))
|
||||
case notifyChannelWebhook:
|
||||
notifiers = append(notifiers, getWebhookNotifier(v))
|
||||
notifier, err := createNotifier(k, v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
notifiers = append(notifiers, notifier)
|
||||
}
|
||||
|
||||
return notifiers, nil
|
||||
}
|
||||
|
||||
func getWebhookNotifier(conf map[string]any) notifyPackage.Notifier {
|
||||
rs := http.New()
|
||||
|
||||
rs.AddReceiversURLs(getString(conf, "url"))
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
func getTelegramNotifier(conf map[string]any) notifyPackage.Notifier {
|
||||
rs, err := telegram.New(getString(conf, "apiToken"))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
chatId := getString(conf, "chatId")
|
||||
|
||||
id, err := strconv.ParseInt(chatId, 10, 64)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rs.AddReceivers(id)
|
||||
return rs
|
||||
}
|
||||
|
||||
func getDingTalkNotifier(conf map[string]any) notifyPackage.Notifier {
|
||||
return dingding.New(&dingding.Config{
|
||||
Token: getString(conf, "accessToken"),
|
||||
Secret: getString(conf, "secret"),
|
||||
})
|
||||
}
|
||||
|
||||
func getString(conf map[string]any, key string) string {
|
||||
if _, ok := conf[key]; !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return conf[key].(string)
|
||||
}
|
||||
|
||||
func getBool(conf map[string]any, key string) bool {
|
||||
if _, ok := conf[key]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return conf[key].(bool)
|
||||
}
|
||||
|
||||
41
internal/notify/service.go
Normal file
41
internal/notify/service.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
const (
|
||||
notifyTestTitle = "测试通知"
|
||||
notifyTestBody = "欢迎使用 Certimate ,这是一条测试通知。"
|
||||
)
|
||||
|
||||
type SettingRepository interface {
|
||||
GetByName(ctx context.Context, name string) (*domain.Setting, error)
|
||||
}
|
||||
|
||||
type NotifyService struct {
|
||||
settingRepo SettingRepository
|
||||
}
|
||||
|
||||
func NewNotifyService(settingRepo SettingRepository) *NotifyService {
|
||||
return &NotifyService{
|
||||
settingRepo: settingRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NotifyService) Test(ctx context.Context, req *domain.NotifyTestPushReq) error {
|
||||
setting, err := n.settingRepo.GetByName(ctx, "notifyChannels")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get notify channels settings: %w", err)
|
||||
}
|
||||
|
||||
channelConfig, err := setting.GetChannelContent(req.Channel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get notify channel \"%s\" config: %w", req.Channel, err)
|
||||
}
|
||||
|
||||
return SendToChannel(notifyTestTitle, notifyTestBody, req.Channel, channelConfig)
|
||||
}
|
||||
24
internal/pkg/core/deployer/deployer.go
Normal file
24
internal/pkg/core/deployer/deployer.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package deployer
|
||||
|
||||
import "context"
|
||||
|
||||
// 表示定义证书部署器的抽象类型接口。
|
||||
// 注意与 `Uploader` 区分,“部署”通常为“上传”的后置操作。
|
||||
type Deployer interface {
|
||||
// 部署证书。
|
||||
//
|
||||
// 入参:
|
||||
// - ctx:上下文。
|
||||
// - certPem:证书 PEM 内容。
|
||||
// - privkeyPem:私钥 PEM 内容。
|
||||
//
|
||||
// 出参:
|
||||
// - res:部署结果。
|
||||
// - err: 错误。
|
||||
Deploy(ctx context.Context, certPem string, privkeyPem string) (res *DeployResult, err error)
|
||||
}
|
||||
|
||||
// 表示证书部署结果的数据结构。
|
||||
type DeployResult struct {
|
||||
DeploymentData map[string]any `json:"deploymentData,omitempty"`
|
||||
}
|
||||
117
internal/pkg/core/deployer/logger.go
Normal file
117
internal/pkg/core/deployer/logger.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 表示定义证书部署器的日志记录器的抽象类型接口。
|
||||
type Logger interface {
|
||||
// 追加一条日志记录。
|
||||
// 该方法会将 `data` 以 JSON 序列化后拼接到 `tag` 结尾。
|
||||
//
|
||||
// 入参:
|
||||
// - tag:标签。
|
||||
// - data:数据。
|
||||
Logt(tag string, data ...any)
|
||||
|
||||
// 追加一条日志记录。
|
||||
// 该方法会将 `args` 以 `format` 格式化。
|
||||
//
|
||||
// 入参:
|
||||
// - format:格式化字符串。
|
||||
// - args:格式化参数。
|
||||
Logf(format string, args ...any)
|
||||
|
||||
// 获取所有日志记录。
|
||||
GetRecords() []string
|
||||
|
||||
// 清空所有日志记录。
|
||||
FlushRecords()
|
||||
}
|
||||
|
||||
// 表示默认的日志记录器类型。
|
||||
type DefaultLogger struct {
|
||||
records []string
|
||||
}
|
||||
|
||||
var _ Logger = (*DefaultLogger)(nil)
|
||||
|
||||
func (l *DefaultLogger) Logt(tag string, data ...any) {
|
||||
l.ensureInitialized()
|
||||
|
||||
temp := make([]string, len(data)+1)
|
||||
temp[0] = tag
|
||||
for i, v := range data {
|
||||
s := ""
|
||||
if v == nil {
|
||||
s = "<nil>"
|
||||
} else {
|
||||
switch reflect.ValueOf(v).Kind() {
|
||||
case reflect.String:
|
||||
s = v.(string)
|
||||
case reflect.Bool,
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64:
|
||||
s = fmt.Sprintf("%v", v)
|
||||
default:
|
||||
jsonData, _ := json.Marshal(v)
|
||||
s = string(jsonData)
|
||||
}
|
||||
}
|
||||
|
||||
temp[i+1] = s
|
||||
}
|
||||
|
||||
l.records = append(l.records, strings.Join(temp, ": "))
|
||||
}
|
||||
|
||||
func (l *DefaultLogger) Logf(format string, args ...any) {
|
||||
l.ensureInitialized()
|
||||
|
||||
l.records = append(l.records, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (l *DefaultLogger) GetRecords() []string {
|
||||
l.ensureInitialized()
|
||||
|
||||
temp := make([]string, len(l.records))
|
||||
copy(temp, l.records)
|
||||
return temp
|
||||
}
|
||||
|
||||
func (l *DefaultLogger) FlushRecords() {
|
||||
l.records = make([]string, 0)
|
||||
}
|
||||
|
||||
func (l *DefaultLogger) ensureInitialized() {
|
||||
if l.records == nil {
|
||||
l.records = make([]string, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func NewDefaultLogger() *DefaultLogger {
|
||||
return &DefaultLogger{
|
||||
records: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// 表示空的日志记录器类型。
|
||||
// 该日志记录器不会执行任何操作。
|
||||
type NilLogger struct{}
|
||||
|
||||
var _ Logger = (*NilLogger)(nil)
|
||||
|
||||
func (l *NilLogger) Logt(string, ...any) {}
|
||||
func (l *NilLogger) Logf(string, ...any) {}
|
||||
func (l *NilLogger) GetRecords() []string {
|
||||
return make([]string, 0)
|
||||
}
|
||||
func (l *NilLogger) FlushRecords() {}
|
||||
|
||||
func NewNilLogger() *NilLogger {
|
||||
return &NilLogger{}
|
||||
}
|
||||
56
internal/pkg/core/deployer/logger_test.go
Normal file
56
internal/pkg/core/deployer/logger_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package deployer_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
)
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v logger_test.go
|
||||
*/
|
||||
func TestLogger(t *testing.T) {
|
||||
t.Run("Logger_Appendt", func(t *testing.T) {
|
||||
logger := deployer.NewDefaultLogger()
|
||||
|
||||
logger.Logt("test")
|
||||
logger.Logt("test_nil", nil)
|
||||
logger.Logt("test_int", 1024)
|
||||
logger.Logt("test_string", "certimate")
|
||||
logger.Logt("test_map", map[string]interface{}{"key": "value"})
|
||||
logger.Logt("test_struct", struct{ Name string }{Name: "certimate"})
|
||||
logger.Logt("test_slice", []string{"certimate"})
|
||||
t.Log(logger.GetRecords())
|
||||
if len(logger.GetRecords()) != 7 {
|
||||
t.Errorf("expected 7 records, got %d", len(logger.GetRecords()))
|
||||
}
|
||||
|
||||
logger.FlushRecords()
|
||||
if len(logger.GetRecords()) != 0 {
|
||||
t.Errorf("expected 0 records, got %d", len(logger.GetRecords()))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Logger_Appendf", func(t *testing.T) {
|
||||
logger := deployer.NewDefaultLogger()
|
||||
|
||||
logger.Logf("test")
|
||||
logger.Logf("test_nil: %v", nil)
|
||||
logger.Logf("test_int: %v", 1024)
|
||||
logger.Logf("test_string: %v", "certimate")
|
||||
logger.Logf("test_map: %v", map[string]interface{}{"key": "value"})
|
||||
logger.Logf("test_struct: %v", struct{ Name string }{Name: "certimate"})
|
||||
logger.Logf("test_slice: %v", []string{"certimate"})
|
||||
t.Log(logger.GetRecords())
|
||||
if len(logger.GetRecords()) != 7 {
|
||||
t.Errorf("expected 7 records, got %d", len(logger.GetRecords()))
|
||||
}
|
||||
|
||||
logger.FlushRecords()
|
||||
if len(logger.GetRecords()) != 0 {
|
||||
t.Errorf("expected 0 records, got %d", len(logger.GetRecords()))
|
||||
}
|
||||
})
|
||||
}
|
||||
289
internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go
Normal file
289
internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package aliyunalb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
aliyunAlb "github.com/alibabacloud-go/alb-20200616/v2/client"
|
||||
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
|
||||
)
|
||||
|
||||
type AliyunALBDeployerConfig struct {
|
||||
// 阿里云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 阿里云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 阿里云地域。
|
||||
Region string `json:"region"`
|
||||
// 部署资源类型。
|
||||
ResourceType DeployResourceType `json:"resourceType"`
|
||||
// 负载均衡实例 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LOADBALANCER] 时必填。
|
||||
LoadbalancerId string `json:"loadbalancerId,omitempty"`
|
||||
// 负载均衡监听 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LISTENER] 时必填。
|
||||
ListenerId string `json:"listenerId,omitempty"`
|
||||
}
|
||||
|
||||
type AliyunALBDeployer struct {
|
||||
config *AliyunALBDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *aliyunAlb.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*AliyunALBDeployer)(nil)
|
||||
|
||||
func New(config *AliyunALBDeployerConfig) (*AliyunALBDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *AliyunALBDeployerConfig, logger deployer.Logger) (*AliyunALBDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
aliyunCasRegion := config.Region
|
||||
if aliyunCasRegion != "" {
|
||||
// 阿里云 CAS 服务接入点是独立于 ALB 服务的
|
||||
// 国内版固定接入点:华东一杭州
|
||||
// 国际版固定接入点:亚太东南一新加坡
|
||||
if !strings.HasPrefix(aliyunCasRegion, "cn-") {
|
||||
aliyunCasRegion = "ap-southeast-1"
|
||||
} else {
|
||||
aliyunCasRegion = "cn-hangzhou"
|
||||
}
|
||||
}
|
||||
uploader, err := providerCas.New(&providerCas.AliyunCASUploaderConfig{
|
||||
AccessKeyId: config.AccessKeyId,
|
||||
AccessKeySecret: config.AccessKeySecret,
|
||||
Region: aliyunCasRegion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &AliyunALBDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 CAS
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 根据部署资源类型决定部署方式
|
||||
switch d.config.ResourceType {
|
||||
case DEPLOY_RESOURCE_LOADBALANCER:
|
||||
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case DEPLOY_RESOURCE_LISTENER:
|
||||
if err := d.deployToListener(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
|
||||
listenerIds := make([]string, 0)
|
||||
|
||||
// 查询负载均衡实例的详细信息
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getloadbalancerattribute
|
||||
getLoadBalancerAttributeReq := &aliyunAlb.GetLoadBalancerAttributeRequest{
|
||||
LoadBalancerId: tea.String(d.config.LoadbalancerId),
|
||||
}
|
||||
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetLoadBalancerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ALB 负载均衡实例", getLoadBalancerAttributeResp)
|
||||
|
||||
// 查询 HTTPS 监听列表
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
|
||||
listListenersPage := 1
|
||||
listListenersLimit := int32(100)
|
||||
var listListenersToken *string = nil
|
||||
for {
|
||||
listListenersReq := &aliyunAlb.ListListenersRequest{
|
||||
MaxResults: tea.Int32(listListenersLimit),
|
||||
NextToken: listListenersToken,
|
||||
LoadBalancerIds: []*string{tea.String(d.config.LoadbalancerId)},
|
||||
ListenerProtocol: tea.String("HTTPS"),
|
||||
}
|
||||
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'")
|
||||
}
|
||||
|
||||
if listListenersResp.Body.Listeners != nil {
|
||||
for _, listener := range listListenersResp.Body.Listeners {
|
||||
listenerIds = append(listenerIds, *listener.ListenerId)
|
||||
}
|
||||
}
|
||||
|
||||
if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenersToken = listListenersResp.Body.NextToken
|
||||
listListenersPage += 1
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ALB 负载均衡实例下的全部 HTTPS 监听", listenerIds)
|
||||
|
||||
// 查询 QUIC 监听列表
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
|
||||
listListenersPage = 1
|
||||
listListenersToken = nil
|
||||
for {
|
||||
listListenersReq := &aliyunAlb.ListListenersRequest{
|
||||
MaxResults: tea.Int32(listListenersLimit),
|
||||
NextToken: listListenersToken,
|
||||
LoadBalancerIds: []*string{tea.String(d.config.LoadbalancerId)},
|
||||
ListenerProtocol: tea.String("QUIC"),
|
||||
}
|
||||
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'")
|
||||
}
|
||||
|
||||
if listListenersResp.Body.Listeners != nil {
|
||||
for _, listener := range listListenersResp.Body.Listeners {
|
||||
listenerIds = append(listenerIds, *listener.ListenerId)
|
||||
}
|
||||
}
|
||||
|
||||
if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenersToken = listListenersResp.Body.NextToken
|
||||
listListenersPage += 1
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ALB 负载均衡实例下的全部 QUIC 监听", listenerIds)
|
||||
|
||||
// 批量更新监听证书
|
||||
var errs []error
|
||||
for _, listenerId := range listenerIds {
|
||||
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) deployToListener(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.ListenerId == "" {
|
||||
return errors.New("config `listenerId` is required")
|
||||
}
|
||||
|
||||
// 更新监听
|
||||
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
|
||||
// 查询监听的属性
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getlistenerattribute
|
||||
getListenerAttributeReq := &aliyunAlb.GetListenerAttributeRequest{
|
||||
ListenerId: tea.String(cloudListenerId),
|
||||
}
|
||||
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetListenerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ALB 监听配置", getListenerAttributeResp)
|
||||
|
||||
// 修改监听的属性
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-updatelistenerattribute
|
||||
updateListenerAttributeReq := &aliyunAlb.UpdateListenerAttributeRequest{
|
||||
ListenerId: tea.String(cloudListenerId),
|
||||
Certificates: []*aliyunAlb.UpdateListenerAttributeRequestCertificates{{
|
||||
CertificateId: tea.String(cloudCertId),
|
||||
}},
|
||||
}
|
||||
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.UpdateListenerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已更新 ALB 监听配置", updateListenerAttributeResp)
|
||||
|
||||
// TODO: #347
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunAlb.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-hangzhou"
|
||||
}
|
||||
|
||||
// 接入点一览 https://www.alibabacloud.com/help/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-endpoint
|
||||
var endpoint string
|
||||
switch region {
|
||||
case "cn-hangzhou-finance":
|
||||
endpoint = "alb.cn-hangzhou.aliyuncs.com"
|
||||
default:
|
||||
endpoint = fmt.Sprintf("alb.%s.aliyuncs.com", region)
|
||||
}
|
||||
|
||||
config := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
Endpoint: tea.String(endpoint),
|
||||
}
|
||||
|
||||
client, err := aliyunAlb.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package aliyunalb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-alb"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fAccessKeySecret string
|
||||
fRegion string
|
||||
fLoadbalancerId string
|
||||
fListenerId string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNALB_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
|
||||
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v aliyun_alb_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_ACCESSKEYSECRET="your-access-key-secret" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_REGION="cn-hangzhou" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_LOADBALANCERID="your-alb-instance-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_LISTENERID="your-alb-listener-id"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunALBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_ToListener", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LISTENERID: %v", fListenerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunALBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
|
||||
ListenerId: fListenerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
10
internal/pkg/core/deployer/providers/aliyun-alb/defines.go
Normal file
10
internal/pkg/core/deployer/providers/aliyun-alb/defines.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package aliyunalb
|
||||
|
||||
type DeployResourceType string
|
||||
|
||||
const (
|
||||
// 资源类型:部署到指定负载均衡器。
|
||||
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
|
||||
// 资源类型:部署到指定监听器。
|
||||
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
package aliyuncdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
aliyunCdn "github.com/alibabacloud-go/cdn-20180510/v5/client"
|
||||
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
)
|
||||
|
||||
type AliyunCDNDeployerConfig struct {
|
||||
// 阿里云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 阿里云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 加速域名(不支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type AliyunCDNDeployer struct {
|
||||
config *AliyunCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *aliyunCdn.Client
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*AliyunCDNDeployer)(nil)
|
||||
|
||||
func New(config *AliyunCDNDeployerConfig) (*AliyunCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *AliyunCDNDeployerConfig, logger deployer.Logger) (*AliyunCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
return &AliyunCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 设置 CDN 域名域名证书
|
||||
// REF: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-setcdndomainsslcertificate
|
||||
setCdnDomainSSLCertificateReq := &aliyunCdn.SetCdnDomainSSLCertificateRequest{
|
||||
DomainName: tea.String(d.config.Domain),
|
||||
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
|
||||
CertType: tea.String("upload"),
|
||||
SSLProtocol: tea.String("on"),
|
||||
SSLPub: tea.String(certPem),
|
||||
SSLPri: tea.String(privkeyPem),
|
||||
}
|
||||
setCdnDomainSSLCertificateResp, err := d.sdkClient.SetCdnDomainSSLCertificate(setCdnDomainSSLCertificateReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.SetCdnDomainSSLCertificate'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已设置 CDN 域名证书", setCdnDomainSSLCertificateResp)
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, accessKeySecret string) (*aliyunCdn.Client, error) {
|
||||
config := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
Endpoint: tea.String("cdn.aliyuncs.com"),
|
||||
}
|
||||
|
||||
client, err := aliyunCdn.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package aliyuncdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-cdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fAccessKeySecret string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v aliyun_cdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCDN_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCDN_ACCESSKEYSECRET="your-access-key-secret" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunCDNDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
291
internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go
Normal file
291
internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package aliyunclb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
aliyunSlb "github.com/alibabacloud-go/slb-20140515/v4/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerSlb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-slb"
|
||||
)
|
||||
|
||||
type AliyunCLBDeployerConfig struct {
|
||||
// 阿里云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 阿里云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 阿里云地域。
|
||||
Region string `json:"region"`
|
||||
// 部署资源类型。
|
||||
ResourceType DeployResourceType `json:"resourceType"`
|
||||
// 负载均衡实例 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LOADBALANCER]、[DEPLOY_RESOURCE_LISTENER] 时必填。
|
||||
LoadbalancerId string `json:"loadbalancerId,omitempty"`
|
||||
// 负载均衡监听端口。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LISTENER] 时必填。
|
||||
ListenerPort int32 `json:"listenerPort,omitempty"`
|
||||
}
|
||||
|
||||
type AliyunCLBDeployer struct {
|
||||
config *AliyunCLBDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *aliyunSlb.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*AliyunCLBDeployer)(nil)
|
||||
|
||||
func New(config *AliyunCLBDeployerConfig) (*AliyunCLBDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *AliyunCLBDeployerConfig, logger deployer.Logger) (*AliyunCLBDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
uploader, err := providerSlb.New(&providerSlb.AliyunSLBUploaderConfig{
|
||||
AccessKeyId: config.AccessKeyId,
|
||||
AccessKeySecret: config.AccessKeySecret,
|
||||
Region: config.Region,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &AliyunCLBDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 SLB
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 根据部署资源类型决定部署方式
|
||||
switch d.config.ResourceType {
|
||||
case DEPLOY_RESOURCE_LOADBALANCER:
|
||||
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case DEPLOY_RESOURCE_LISTENER:
|
||||
if err := d.deployToListener(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
|
||||
listenerPorts := make([]int32, 0)
|
||||
|
||||
// 查询负载均衡实例的详细信息
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute
|
||||
describeLoadBalancerAttributeReq := &aliyunSlb.DescribeLoadBalancerAttributeRequest{
|
||||
RegionId: tea.String(d.config.Region),
|
||||
LoadBalancerId: tea.String(d.config.LoadbalancerId),
|
||||
}
|
||||
describeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttribute(describeLoadBalancerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 CLB 负载均衡实例", describeLoadBalancerAttributeResp)
|
||||
|
||||
// 查询 HTTPS 监听列表
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerlisteners
|
||||
listListenersPage := 1
|
||||
listListenersLimit := int32(100)
|
||||
var listListenersToken *string = nil
|
||||
for {
|
||||
describeLoadBalancerListenersReq := &aliyunSlb.DescribeLoadBalancerListenersRequest{
|
||||
RegionId: tea.String(d.config.Region),
|
||||
MaxResults: tea.Int32(listListenersLimit),
|
||||
NextToken: listListenersToken,
|
||||
LoadBalancerId: []*string{tea.String(d.config.LoadbalancerId)},
|
||||
ListenerProtocol: tea.String("https"),
|
||||
}
|
||||
describeLoadBalancerListenersResp, err := d.sdkClient.DescribeLoadBalancerListeners(describeLoadBalancerListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerListeners'")
|
||||
}
|
||||
|
||||
if describeLoadBalancerListenersResp.Body.Listeners != nil {
|
||||
for _, listener := range describeLoadBalancerListenersResp.Body.Listeners {
|
||||
listenerPorts = append(listenerPorts, *listener.ListenerPort)
|
||||
}
|
||||
}
|
||||
|
||||
if len(describeLoadBalancerListenersResp.Body.Listeners) == 0 || describeLoadBalancerListenersResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenersToken = describeLoadBalancerListenersResp.Body.NextToken
|
||||
listListenersPage += 1
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 CLB 负载均衡实例下的全部 HTTPS 监听", listenerPorts)
|
||||
|
||||
// 批量更新监听证书
|
||||
var errs []error
|
||||
for _, listenerPort := range listenerPorts {
|
||||
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listenerPort, cloudCertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) deployToListener(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
if d.config.ListenerPort == 0 {
|
||||
return errors.New("config `listenerPort` is required")
|
||||
}
|
||||
|
||||
// 更新监听
|
||||
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, d.config.ListenerPort, cloudCertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerPort int32, cloudCertId string) error {
|
||||
// 查询监听配置
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerhttpslistenerattribute
|
||||
describeLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.DescribeLoadBalancerHTTPSListenerAttributeRequest{
|
||||
LoadBalancerId: tea.String(cloudLoadbalancerId),
|
||||
ListenerPort: tea.Int32(cloudListenerPort),
|
||||
}
|
||||
describeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttribute(describeLoadBalancerHTTPSListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 CLB HTTPS 监听配置", describeLoadBalancerHTTPSListenerAttributeResp)
|
||||
|
||||
// 查询扩展域名
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describedomainextensions
|
||||
describeDomainExtensionsReq := &aliyunSlb.DescribeDomainExtensionsRequest{
|
||||
RegionId: tea.String(d.config.Region),
|
||||
LoadBalancerId: tea.String(cloudLoadbalancerId),
|
||||
ListenerPort: tea.Int32(cloudListenerPort),
|
||||
}
|
||||
describeDomainExtensionsResp, err := d.sdkClient.DescribeDomainExtensions(describeDomainExtensionsReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeDomainExtensions'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 CLB 扩展域名", describeDomainExtensionsResp)
|
||||
|
||||
// 遍历修改扩展域名
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setdomainextensionattribute
|
||||
//
|
||||
// 这里仅修改跟被替换证书一致的扩展域名
|
||||
if describeDomainExtensionsResp.Body.DomainExtensions != nil && describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension != nil {
|
||||
for _, domainExtension := range describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension {
|
||||
if *domainExtension.ServerCertificateId != *describeLoadBalancerHTTPSListenerAttributeResp.Body.ServerCertificateId {
|
||||
continue
|
||||
}
|
||||
|
||||
setDomainExtensionAttributeReq := &aliyunSlb.SetDomainExtensionAttributeRequest{
|
||||
RegionId: tea.String(d.config.Region),
|
||||
DomainExtensionId: tea.String(*domainExtension.DomainExtensionId),
|
||||
ServerCertificateId: tea.String(cloudCertId),
|
||||
}
|
||||
_, err := d.sdkClient.SetDomainExtensionAttribute(setDomainExtensionAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetDomainExtensionAttribute'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改监听配置
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute
|
||||
//
|
||||
// 注意修改监听配置要放在修改扩展域名之后
|
||||
setLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.SetLoadBalancerHTTPSListenerAttributeRequest{
|
||||
RegionId: tea.String(d.config.Region),
|
||||
LoadBalancerId: tea.String(cloudLoadbalancerId),
|
||||
ListenerPort: tea.Int32(cloudListenerPort),
|
||||
ServerCertificateId: tea.String(cloudCertId),
|
||||
}
|
||||
setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.SetLoadBalancerHTTPSListenerAttribute(setLoadBalancerHTTPSListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已更新 CLB HTTPS 监听配置", setLoadBalancerHTTPSListenerAttributeResp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-hangzhou" // CLB(SLB) 服务默认区域:华东一杭州
|
||||
}
|
||||
|
||||
// 接入点一览 https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-endpoint
|
||||
var endpoint string
|
||||
switch region {
|
||||
case
|
||||
"cn-hangzhou",
|
||||
"cn-hangzhou-finance",
|
||||
"cn-shanghai-finance-1",
|
||||
"cn-shenzhen-finance-1":
|
||||
endpoint = "slb.aliyuncs.com"
|
||||
default:
|
||||
endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region)
|
||||
}
|
||||
|
||||
config := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
Endpoint: tea.String(endpoint),
|
||||
}
|
||||
|
||||
client, err := aliyunSlb.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package aliyunclb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-clb"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fAccessKeySecret string
|
||||
fRegion string
|
||||
fLoadbalancerId string
|
||||
fListenerPort int
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNCLB_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
|
||||
flag.IntVar(&fListenerPort, argsPrefix+"LISTENERPORT", 443, "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v aliyun_clb_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCLB_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCLB_ACCESSKEYSECRET="your-access-key-secret" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCLB_REGION="cn-hangzhou" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCLB_LOADBALANCERID="your-clb-instance-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCLB_LISTENERPORT=443
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunCLBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_ToListener", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
fmt.Sprintf("LISTENERPORT: %v", fListenerPort),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunCLBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
ListenerPort: int32(fListenerPort),
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
10
internal/pkg/core/deployer/providers/aliyun-clb/defines.go
Normal file
10
internal/pkg/core/deployer/providers/aliyun-clb/defines.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package aliyunclb
|
||||
|
||||
type DeployResourceType string
|
||||
|
||||
const (
|
||||
// 资源类型:部署到指定负载均衡器。
|
||||
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
|
||||
// 资源类型:部署到指定监听器。
|
||||
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
package aliyundcdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
aliyunDcdn "github.com/alibabacloud-go/dcdn-20180115/v3/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
)
|
||||
|
||||
type AliyunDCDNDeployerConfig struct {
|
||||
// 阿里云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 阿里云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 加速域名(支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type AliyunDCDNDeployer struct {
|
||||
config *AliyunDCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *aliyunDcdn.Client
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*AliyunDCDNDeployer)(nil)
|
||||
|
||||
func New(config *AliyunDCDNDeployerConfig) (*AliyunDCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *AliyunDCDNDeployerConfig, logger deployer.Logger) (*AliyunDCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
return &AliyunDCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunDCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// "*.example.com" → ".example.com",适配阿里云 DCDN 要求的泛域名格式
|
||||
domain := strings.TrimPrefix(d.config.Domain, "*")
|
||||
|
||||
// 配置域名证书
|
||||
// REF: https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-setdcdndomainsslcertificate
|
||||
setDcdnDomainSSLCertificateReq := &aliyunDcdn.SetDcdnDomainSSLCertificateRequest{
|
||||
DomainName: tea.String(domain),
|
||||
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
|
||||
CertType: tea.String("upload"),
|
||||
SSLProtocol: tea.String("on"),
|
||||
SSLPub: tea.String(certPem),
|
||||
SSLPri: tea.String(privkeyPem),
|
||||
}
|
||||
setDcdnDomainSSLCertificateResp, err := d.sdkClient.SetDcdnDomainSSLCertificate(setDcdnDomainSSLCertificateReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'dcdn.SetDcdnDomainSSLCertificate'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已配置 DCDN 域名证书", setDcdnDomainSSLCertificateResp)
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, accessKeySecret string) (*aliyunDcdn.Client, error) {
|
||||
config := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
Endpoint: tea.String("dcdn.aliyuncs.com"),
|
||||
}
|
||||
|
||||
client, err := aliyunDcdn.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package aliyundcdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-dcdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fAccessKeySecret string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNDCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v aliyun_dcdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNDCDN_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNDCDN_ACCESSKEYSECRET="your-access-key-secret" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNDCDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunDCDNDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
251
internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go
Normal file
251
internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package aliyunnlb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
aliyunNlb "github.com/alibabacloud-go/nlb-20220430/v2/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
|
||||
)
|
||||
|
||||
type AliyunNLBDeployerConfig struct {
|
||||
// 阿里云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 阿里云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 阿里云地域。
|
||||
Region string `json:"region"`
|
||||
// 部署资源类型。
|
||||
ResourceType DeployResourceType `json:"resourceType"`
|
||||
// 负载均衡实例 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LOADBALANCER] 时必填。
|
||||
LoadbalancerId string `json:"loadbalancerId,omitempty"`
|
||||
// 负载均衡监听 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LISTENER] 时必填。
|
||||
ListenerId string `json:"listenerId,omitempty"`
|
||||
}
|
||||
|
||||
type AliyunNLBDeployer struct {
|
||||
config *AliyunNLBDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *aliyunNlb.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*AliyunNLBDeployer)(nil)
|
||||
|
||||
func New(config *AliyunNLBDeployerConfig) (*AliyunNLBDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *AliyunNLBDeployerConfig, logger deployer.Logger) (*AliyunNLBDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
aliyunCasRegion := config.Region
|
||||
if aliyunCasRegion != "" {
|
||||
// 阿里云 CAS 服务接入点是独立于 NLB 服务的
|
||||
// 国内版固定接入点:华东一杭州
|
||||
// 国际版固定接入点:亚太东南一新加坡
|
||||
if !strings.HasPrefix(aliyunCasRegion, "cn-") {
|
||||
aliyunCasRegion = "ap-southeast-1"
|
||||
} else {
|
||||
aliyunCasRegion = "cn-hangzhou"
|
||||
}
|
||||
}
|
||||
uploader, err := providerCas.New(&providerCas.AliyunCASUploaderConfig{
|
||||
AccessKeyId: config.AccessKeyId,
|
||||
AccessKeySecret: config.AccessKeySecret,
|
||||
Region: aliyunCasRegion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &AliyunNLBDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 CAS
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 根据部署资源类型决定部署方式
|
||||
switch d.config.ResourceType {
|
||||
case DEPLOY_RESOURCE_LOADBALANCER:
|
||||
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case DEPLOY_RESOURCE_LISTENER:
|
||||
if err := d.deployToListener(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
|
||||
listenerIds := make([]string, 0)
|
||||
|
||||
// 查询负载均衡实例的详细信息
|
||||
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getloadbalancerattribute
|
||||
getLoadBalancerAttributeReq := &aliyunNlb.GetLoadBalancerAttributeRequest{
|
||||
LoadBalancerId: tea.String(d.config.LoadbalancerId),
|
||||
}
|
||||
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetLoadBalancerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 NLB 负载均衡实例", getLoadBalancerAttributeResp)
|
||||
|
||||
// 查询 TCPSSL 监听列表
|
||||
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-listlisteners
|
||||
listListenersPage := 1
|
||||
listListenersLimit := int32(100)
|
||||
var listListenersToken *string = nil
|
||||
for {
|
||||
listListenersReq := &aliyunNlb.ListListenersRequest{
|
||||
MaxResults: tea.Int32(listListenersLimit),
|
||||
NextToken: listListenersToken,
|
||||
LoadBalancerIds: []*string{tea.String(d.config.LoadbalancerId)},
|
||||
ListenerProtocol: tea.String("TCPSSL"),
|
||||
}
|
||||
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.ListListeners'")
|
||||
}
|
||||
|
||||
if listListenersResp.Body.Listeners != nil {
|
||||
for _, listener := range listListenersResp.Body.Listeners {
|
||||
listenerIds = append(listenerIds, *listener.ListenerId)
|
||||
}
|
||||
}
|
||||
|
||||
if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenersToken = listListenersResp.Body.NextToken
|
||||
listListenersPage += 1
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 NLB 负载均衡实例下的全部 TCPSSL 监听", listenerIds)
|
||||
|
||||
// 批量更新监听证书
|
||||
var errs []error
|
||||
for _, listenerId := range listenerIds {
|
||||
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) deployToListener(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.ListenerId == "" {
|
||||
return errors.New("config `listenerId` is required")
|
||||
}
|
||||
|
||||
// 更新监听
|
||||
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
|
||||
// 查询监听的属性
|
||||
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getlistenerattribute
|
||||
getListenerAttributeReq := &aliyunNlb.GetListenerAttributeRequest{
|
||||
ListenerId: tea.String(cloudListenerId),
|
||||
}
|
||||
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetListenerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 NLB 监听配置", getListenerAttributeResp)
|
||||
|
||||
// 修改监听的属性
|
||||
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-updatelistenerattribute
|
||||
updateListenerAttributeReq := &aliyunNlb.UpdateListenerAttributeRequest{
|
||||
ListenerId: tea.String(cloudListenerId),
|
||||
CertificateIds: []*string{tea.String(cloudCertId)},
|
||||
}
|
||||
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.UpdateListenerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已更新 NLB 监听配置", updateListenerAttributeResp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunNlb.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-hangzhou" // NLB 服务默认区域:华东一杭州
|
||||
}
|
||||
|
||||
// 接入点一览 https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-endpoint
|
||||
var endpoint string
|
||||
switch region {
|
||||
default:
|
||||
endpoint = fmt.Sprintf("nlb.%s.aliyuncs.com", region)
|
||||
}
|
||||
|
||||
config := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
Endpoint: tea.String(endpoint),
|
||||
}
|
||||
|
||||
client, err := aliyunNlb.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package aliyunnlb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-nlb"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fAccessKeySecret string
|
||||
fRegion string
|
||||
fLoadbalancerId string
|
||||
fListenerId string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNNLB_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
|
||||
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v aliyun_nlb_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNNLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNNLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNNLB_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNNLB_ACCESSKEYSECRET="your-access-key-secret" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNNLB_REGION="cn-hangzhou" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNNLB_LOADBALANCERID="your-nlb-instance-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNNLB_LISTENERID="your-nlb-listener-id"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunNLBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_ToListener", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
fmt.Sprintf("LISTENERID: %v", fListenerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunNLBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
|
||||
ListenerId: fListenerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
10
internal/pkg/core/deployer/providers/aliyun-nlb/defines.go
Normal file
10
internal/pkg/core/deployer/providers/aliyun-nlb/defines.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package aliyunnlb
|
||||
|
||||
type DeployResourceType string
|
||||
|
||||
const (
|
||||
// 资源类型:部署到指定负载均衡器。
|
||||
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
|
||||
// 资源类型:部署到指定监听器。
|
||||
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
|
||||
)
|
||||
112
internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go
Normal file
112
internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package aliyunoss
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
)
|
||||
|
||||
type AliyunOSSDeployerConfig struct {
|
||||
// 阿里云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 阿里云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 阿里云地域。
|
||||
Region string `json:"region"`
|
||||
// 存储桶名。
|
||||
Bucket string `json:"bucket"`
|
||||
// 自定义域名(不支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type AliyunOSSDeployer struct {
|
||||
config *AliyunOSSDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *oss.Client
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*AliyunOSSDeployer)(nil)
|
||||
|
||||
func New(config *AliyunOSSDeployerConfig) (*AliyunOSSDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *AliyunOSSDeployerConfig, logger deployer.Logger) (*AliyunOSSDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
return &AliyunOSSDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunOSSDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
if d.config.Bucket == "" {
|
||||
return nil, errors.New("config `bucket` is required")
|
||||
}
|
||||
if d.config.Domain == "" {
|
||||
return nil, errors.New("config `domain` is required")
|
||||
}
|
||||
|
||||
// 为存储空间绑定自定义域名
|
||||
// REF: https://help.aliyun.com/zh/oss/developer-reference/putcname
|
||||
err := d.sdkClient.PutBucketCnameWithCertificate(d.config.Bucket, oss.PutBucketCname{
|
||||
Cname: d.config.Domain,
|
||||
CertificateConfiguration: &oss.CertificateConfiguration{
|
||||
Certificate: certPem,
|
||||
PrivateKey: privkeyPem,
|
||||
Force: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'oss.PutBucketCnameWithCertificate'")
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, accessKeySecret, region string) (*oss.Client, error) {
|
||||
// 接入点一览 https://help.aliyun.com/zh/oss/user-guide/regions-and-endpoints
|
||||
var endpoint string
|
||||
switch region {
|
||||
case "":
|
||||
endpoint = "oss.aliyuncs.com"
|
||||
case
|
||||
"cn-hzjbp",
|
||||
"cn-hzjbp-a",
|
||||
"cn-hzjbp-b":
|
||||
endpoint = "oss-cn-hzjbp-a-internal.aliyuncs.com"
|
||||
case
|
||||
"cn-shanghai-finance-1",
|
||||
"cn-shenzhen-finance-1",
|
||||
"cn-beijing-finance-1",
|
||||
"cn-north-2-gov-1":
|
||||
endpoint = fmt.Sprintf("oss-%s-internal.aliyuncs.com", region)
|
||||
default:
|
||||
endpoint = fmt.Sprintf("oss-%s.aliyuncs.com", region)
|
||||
}
|
||||
|
||||
client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package aliyunoss_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-oss"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fAccessKeySecret string
|
||||
fRegion string
|
||||
fBucket string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNOSS_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v aliyun_oss_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNOSS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNOSS_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNOSS_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNOSS_ACCESSKEYSECRET="your-access-key-secret" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNOSS_REGION="cn-hangzhou" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNOSS_BUCKET="your-oss-bucket" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNOSS_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("BUCKET: %v", fBucket),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunOSSDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
Bucket: fBucket,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package baiducloudcdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
bceCdn "github.com/baidubce/bce-sdk-go/services/cdn"
|
||||
bceCdnApi "github.com/baidubce/bce-sdk-go/services/cdn/api"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
)
|
||||
|
||||
type BaiduCloudCDNDeployerConfig struct {
|
||||
// 百度智能云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 百度智能云 SecretAccessKey。
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
// 加速域名(不支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type BaiduCloudCDNDeployer struct {
|
||||
config *BaiduCloudCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *bceCdn.Client
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*BaiduCloudCDNDeployer)(nil)
|
||||
|
||||
func New(config *BaiduCloudCDNDeployerConfig) (*BaiduCloudCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *BaiduCloudCDNDeployerConfig, logger deployer.Logger) (*BaiduCloudCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
return &BaiduCloudCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *BaiduCloudCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 修改域名证书
|
||||
// REF: https://cloud.baidu.com/doc/CDN/s/qjzuz2hp8
|
||||
putCertResp, err := d.sdkClient.PutCert(
|
||||
d.config.Domain,
|
||||
&bceCdnApi.UserCertificate{
|
||||
CertName: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
|
||||
ServerData: certPem,
|
||||
PrivateData: privkeyPem,
|
||||
},
|
||||
"ON",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.PutCert'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已修改域名证书", putCertResp)
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, secretAccessKey string) (*bceCdn.Client, error) {
|
||||
client, err := bceCdn.NewClient(accessKeyId, secretAccessKey, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package baiducloudcdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fSecretAccessKey string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v baiducloud_cdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \
|
||||
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.BaiduCloudCDNDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
SecretAccessKey: fSecretAccessKey,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package bytepluscdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
bpCdn "github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerCdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/byteplus-cdn"
|
||||
)
|
||||
|
||||
type BytePlusCDNDeployerConfig struct {
|
||||
// BytePlus AccessKey。
|
||||
AccessKey string `json:"accessKey"`
|
||||
// BytePlus SecretKey。
|
||||
SecretKey string `json:"secretKey"`
|
||||
// 加速域名(支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type BytePlusCDNDeployer struct {
|
||||
config *BytePlusCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *bpCdn.CDN
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*BytePlusCDNDeployer)(nil)
|
||||
|
||||
func New(config *BytePlusCDNDeployerConfig) (*BytePlusCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *BytePlusCDNDeployerConfig, logger deployer.Logger) (*BytePlusCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client := bpCdn.NewInstance()
|
||||
client.Client.SetAccessKey(config.AccessKey)
|
||||
client.Client.SetSecretKey(config.SecretKey)
|
||||
|
||||
uploader, err := providerCdn.New(&providerCdn.ByteplusCDNUploaderConfig{
|
||||
AccessKey: config.AccessKey,
|
||||
SecretKey: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &BytePlusCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *BytePlusCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 CDN
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
domains := make([]string, 0)
|
||||
if strings.HasPrefix(d.config.Domain, "*.") {
|
||||
// 获取指定证书可关联的域名
|
||||
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-describecertconfig-9ea17
|
||||
describeCertConfigReq := &bpCdn.DescribeCertConfigRequest{
|
||||
CertId: upres.CertId,
|
||||
}
|
||||
describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'")
|
||||
}
|
||||
|
||||
if describeCertConfigResp.Result.CertNotConfig != nil {
|
||||
for i := range describeCertConfigResp.Result.CertNotConfig {
|
||||
domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)
|
||||
}
|
||||
}
|
||||
|
||||
if describeCertConfigResp.Result.OtherCertConfig != nil {
|
||||
for i := range describeCertConfigResp.Result.OtherCertConfig {
|
||||
domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)
|
||||
}
|
||||
}
|
||||
|
||||
if len(domains) == 0 {
|
||||
if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 {
|
||||
// 所有可关联的域名都配置了该证书,跳过部署
|
||||
} else {
|
||||
return nil, xerrors.New("domain not found")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
domains = append(domains, d.config.Domain)
|
||||
}
|
||||
|
||||
if len(domains) > 0 {
|
||||
var errs []error
|
||||
|
||||
for _, domain := range domains {
|
||||
// 关联证书与加速域名
|
||||
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-batchdeploycert
|
||||
batchDeployCertReq := &bpCdn.BatchDeployCertRequest{
|
||||
CertId: upres.CertId,
|
||||
Domain: domain,
|
||||
}
|
||||
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
d.logger.Logt(fmt.Sprintf("已关联证书到域名 %s", domain), batchDeployCertResp)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.Join(errs...)
|
||||
}
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package bytepluscdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKey string
|
||||
fSecretKey string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_BYTEPLUSCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
|
||||
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v byteplus_cdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_ACCESSKEY="your-access-key" \
|
||||
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_SECRETKEY="your-secret-key" \
|
||||
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.BytePlusCDNDeployerConfig{
|
||||
AccessKey: fAccessKey,
|
||||
SecretKey: fSecretKey,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package dogecloudcdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerDoge "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/dogecloud"
|
||||
dogesdk "github.com/usual2970/certimate/internal/pkg/vendors/dogecloud-sdk"
|
||||
)
|
||||
|
||||
type DogeCloudCDNDeployerConfig struct {
|
||||
// 多吉云 AccessKey。
|
||||
AccessKey string `json:"accessKey"`
|
||||
// 多吉云 SecretKey。
|
||||
SecretKey string `json:"secretKey"`
|
||||
// 加速域名(不支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type DogeCloudCDNDeployer struct {
|
||||
config *DogeCloudCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *dogesdk.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*DogeCloudCDNDeployer)(nil)
|
||||
|
||||
func New(config *DogeCloudCDNDeployerConfig) (*DogeCloudCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *DogeCloudCDNDeployerConfig, logger deployer.Logger) (*DogeCloudCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client := dogesdk.NewClient(config.AccessKey, config.SecretKey)
|
||||
|
||||
uploader, err := providerDoge.New(&providerDoge.DogeCloudUploaderConfig{
|
||||
AccessKey: config.AccessKey,
|
||||
SecretKey: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &DogeCloudCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DogeCloudCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 CDN
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 绑定证书
|
||||
// REF: https://docs.dogecloud.com/cdn/api-cert-bind
|
||||
bindCdnCertId, _ := strconv.ParseInt(upres.CertId, 10, 64)
|
||||
bindCdnCertResp, err := d.sdkClient.BindCdnCertWithDomain(bindCdnCertId, d.config.Domain)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.BindCdnCert'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已绑定证书", bindCdnCertResp)
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package dogecloudcdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKey string
|
||||
fSecretKey string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_DOGECLOUDCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
|
||||
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v dogecloud_cdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_ACCESSKEY="your-access-key" \
|
||||
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_SECRETKEY="your-secret-key" \
|
||||
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.DogeCloudCDNDeployerConfig{
|
||||
AccessKey: fAccessKey,
|
||||
SecretKey: fSecretKey,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package huaweicloudcdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
|
||||
hcCdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2"
|
||||
hcCdnModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model"
|
||||
hcCdnRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/region"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerScm "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-scm"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/cast"
|
||||
huaweicloudsdk "github.com/usual2970/certimate/internal/pkg/vendors/huaweicloud-cdn-sdk"
|
||||
)
|
||||
|
||||
type HuaweiCloudCDNDeployerConfig struct {
|
||||
// 华为云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 华为云 SecretAccessKey。
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
// 华为云地域。
|
||||
Region string `json:"region"`
|
||||
// 加速域名(不支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type HuaweiCloudCDNDeployer struct {
|
||||
config *HuaweiCloudCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *huaweicloudsdk.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*HuaweiCloudCDNDeployer)(nil)
|
||||
|
||||
func New(config *HuaweiCloudCDNDeployerConfig) (*HuaweiCloudCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *HuaweiCloudCDNDeployerConfig, logger deployer.Logger) (*HuaweiCloudCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(
|
||||
config.AccessKeyId,
|
||||
config.SecretAccessKey,
|
||||
config.Region,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
uploader, err := providerScm.New(&providerScm.HuaweiCloudSCMUploaderConfig{
|
||||
AccessKeyId: config.AccessKeyId,
|
||||
SecretAccessKey: config.SecretAccessKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &HuaweiCloudCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 SCM
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 查询加速域名配置
|
||||
// REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html
|
||||
showDomainFullConfigReq := &hcCdnModel.ShowDomainFullConfigRequest{
|
||||
DomainName: d.config.Domain,
|
||||
}
|
||||
showDomainFullConfigResp, err := d.sdkClient.ShowDomainFullConfig(showDomainFullConfigReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ShowDomainFullConfig'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到加速域名配置", showDomainFullConfigResp)
|
||||
|
||||
// 更新加速域名配置
|
||||
// REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html
|
||||
// REF: https://support.huaweicloud.com/usermanual-cdn/cdn_01_0306.html
|
||||
updateDomainMultiCertificatesReqBodyContent := &huaweicloudsdk.UpdateDomainMultiCertificatesExRequestBodyContent{}
|
||||
updateDomainMultiCertificatesReqBodyContent.DomainName = d.config.Domain
|
||||
updateDomainMultiCertificatesReqBodyContent.HttpsSwitch = 1
|
||||
updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(2)
|
||||
updateDomainMultiCertificatesReqBodyContent.SCMCertificateId = cast.StringPtr(upres.CertId)
|
||||
updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(upres.CertName)
|
||||
updateDomainMultiCertificatesReqBodyContent = updateDomainMultiCertificatesReqBodyContent.MergeConfig(showDomainFullConfigResp.Configs)
|
||||
updateDomainMultiCertificatesReq := &huaweicloudsdk.UpdateDomainMultiCertificatesExRequest{
|
||||
Body: &huaweicloudsdk.UpdateDomainMultiCertificatesExRequestBody{
|
||||
Https: updateDomainMultiCertificatesReqBodyContent,
|
||||
},
|
||||
}
|
||||
updateDomainMultiCertificatesResp, err := d.sdkClient.UploadDomainMultiCertificatesEx(updateDomainMultiCertificatesReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadDomainMultiCertificatesEx'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已更新加速域名配置", updateDomainMultiCertificatesResp)
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, secretAccessKey, region string) (*huaweicloudsdk.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-north-1" // CDN 服务默认区域:华北一北京
|
||||
}
|
||||
|
||||
auth, err := global.NewCredentialsBuilder().
|
||||
WithAk(accessKeyId).
|
||||
WithSk(secretAccessKey).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hcRegion, err := hcCdnRegion.SafeValueOf(region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hcClient, err := hcCdn.CdnClientBuilder().
|
||||
WithRegion(hcRegion).
|
||||
WithCredential(auth).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := huaweicloudsdk.NewClient(hcClient)
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package huaweicloudcdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fSecretAccessKey string
|
||||
fRegion string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v huaweicloud_cdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_REGION="cn-north-1" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.HuaweiCloudCDNDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
SecretAccessKey: fSecretAccessKey,
|
||||
Region: fRegion,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package huaweicloudelb
|
||||
|
||||
type DeployResourceType string
|
||||
|
||||
const (
|
||||
// 资源类型:替换指定证书。
|
||||
DEPLOY_RESOURCE_CERTIFICATE = DeployResourceType("certificate")
|
||||
// 资源类型:部署到指定负载均衡器。
|
||||
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
|
||||
// 资源类型:部署到指定监听器。
|
||||
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
|
||||
)
|
||||
@@ -0,0 +1,395 @@
|
||||
package huaweicloudelb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
|
||||
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
|
||||
hcElb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3"
|
||||
hcElbModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model"
|
||||
hcElbRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region"
|
||||
hcIam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3"
|
||||
hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model"
|
||||
hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region"
|
||||
xerrors "github.com/pkg/errors"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerElb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-elb"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/cast"
|
||||
)
|
||||
|
||||
type HuaweiCloudELBDeployerConfig struct {
|
||||
// 华为云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 华为云 SecretAccessKey。
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
// 华为云地域。
|
||||
Region string `json:"region"`
|
||||
// 部署资源类型。
|
||||
ResourceType DeployResourceType `json:"resourceType"`
|
||||
// 证书 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_CERTIFICATE] 时必填。
|
||||
CertificateId string `json:"certificateId,omitempty"`
|
||||
// 负载均衡器 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LOADBALANCER] 时必填。
|
||||
LoadbalancerId string `json:"loadbalancerId,omitempty"`
|
||||
// 负载均衡监听 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LISTENER] 时必填。
|
||||
ListenerId string `json:"listenerId,omitempty"`
|
||||
}
|
||||
|
||||
type HuaweiCloudELBDeployer struct {
|
||||
config *HuaweiCloudELBDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *hcElb.ElbClient
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*HuaweiCloudELBDeployer)(nil)
|
||||
|
||||
func New(config *HuaweiCloudELBDeployerConfig) (*HuaweiCloudELBDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *HuaweiCloudELBDeployerConfig, logger deployer.Logger) (*HuaweiCloudELBDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey, config.Region)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
uploader, err := providerElb.New(&providerElb.HuaweiCloudELBUploaderConfig{
|
||||
AccessKeyId: config.AccessKeyId,
|
||||
SecretAccessKey: config.SecretAccessKey,
|
||||
Region: config.Region,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &HuaweiCloudELBDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 SCM
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 根据部署资源类型决定部署方式
|
||||
switch d.config.ResourceType {
|
||||
case DEPLOY_RESOURCE_CERTIFICATE:
|
||||
if err := d.deployToCertificate(ctx, certPem, privkeyPem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case DEPLOY_RESOURCE_LOADBALANCER:
|
||||
if err := d.deployToLoadbalancer(ctx, certPem, privkeyPem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case DEPLOY_RESOURCE_LISTENER:
|
||||
if err := d.deployToListener(ctx, certPem, privkeyPem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context, certPem string, privkeyPem string) error {
|
||||
if d.config.CertificateId == "" {
|
||||
return errors.New("config `certificateId` is required")
|
||||
}
|
||||
|
||||
// 更新证书
|
||||
// REF: https://support.huaweicloud.com/api-elb/UpdateCertificate.html
|
||||
updateCertificateReq := &hcElbModel.UpdateCertificateRequest{
|
||||
CertificateId: d.config.CertificateId,
|
||||
Body: &hcElbModel.UpdateCertificateRequestBody{
|
||||
Certificate: &hcElbModel.UpdateCertificateOption{
|
||||
Certificate: cast.StringPtr(certPem),
|
||||
PrivateKey: cast.StringPtr(privkeyPem),
|
||||
},
|
||||
},
|
||||
}
|
||||
updateCertificateResp, err := d.sdkClient.UpdateCertificate(updateCertificateReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateCertificate'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已更新 ELB 证书", updateCertificateResp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context, certPem string, privkeyPem string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
|
||||
listenerIds := make([]string, 0)
|
||||
|
||||
// 查询负载均衡器详情
|
||||
// REF: https://support.huaweicloud.com/api-elb/ShowLoadBalancer.html
|
||||
showLoadBalancerReq := &hcElbModel.ShowLoadBalancerRequest{
|
||||
LoadbalancerId: d.config.LoadbalancerId,
|
||||
}
|
||||
showLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowLoadBalancer'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ELB 负载均衡器", showLoadBalancerResp)
|
||||
|
||||
// 查询监听器列表
|
||||
// REF: https://support.huaweicloud.com/api-elb/ListListeners.html
|
||||
listListenersLimit := int32(2000)
|
||||
var listListenersMarker *string = nil
|
||||
for {
|
||||
listListenersReq := &hcElbModel.ListListenersRequest{
|
||||
Limit: cast.Int32Ptr(listListenersLimit),
|
||||
Marker: listListenersMarker,
|
||||
Protocol: &[]string{"HTTPS", "TERMINATED_HTTPS"},
|
||||
LoadbalancerId: &[]string{showLoadBalancerResp.Loadbalancer.Id},
|
||||
}
|
||||
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListListeners'")
|
||||
}
|
||||
|
||||
if listListenersResp.Listeners != nil {
|
||||
for _, listener := range *listListenersResp.Listeners {
|
||||
listenerIds = append(listenerIds, listener.Id)
|
||||
}
|
||||
}
|
||||
|
||||
if listListenersResp.Listeners == nil || len(*listListenersResp.Listeners) < int(listListenersLimit) {
|
||||
break
|
||||
} else {
|
||||
listListenersMarker = listListenersResp.PageInfo.NextMarker
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ELB 负载均衡器下的监听器", listenerIds)
|
||||
|
||||
// 上传证书到 SCM
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 批量更新监听器证书
|
||||
var errs []error
|
||||
for _, listenerId := range listenerIds {
|
||||
if err := d.modifyListenerCertificate(ctx, listenerId, upres.CertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context, certPem string, privkeyPem string) error {
|
||||
if d.config.ListenerId == "" {
|
||||
return errors.New("config `listenerId` is required")
|
||||
}
|
||||
|
||||
// 上传证书到 SCM
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 更新监听器证书
|
||||
if err := d.modifyListenerCertificate(ctx, d.config.ListenerId, upres.CertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) modifyListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
|
||||
// 查询监听器详情
|
||||
// REF: https://support.huaweicloud.com/api-elb/ShowListener.html
|
||||
showListenerReq := &hcElbModel.ShowListenerRequest{
|
||||
ListenerId: cloudListenerId,
|
||||
}
|
||||
showListenerResp, err := d.sdkClient.ShowListener(showListenerReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowListener'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ELB 监听器", showListenerResp)
|
||||
|
||||
// 更新监听器
|
||||
// REF: https://support.huaweicloud.com/api-elb/UpdateListener.html
|
||||
updateListenerReq := &hcElbModel.UpdateListenerRequest{
|
||||
ListenerId: cloudListenerId,
|
||||
Body: &hcElbModel.UpdateListenerRequestBody{
|
||||
Listener: &hcElbModel.UpdateListenerOption{
|
||||
DefaultTlsContainerRef: cast.StringPtr(cloudCertId),
|
||||
},
|
||||
},
|
||||
}
|
||||
if showListenerResp.Listener.SniContainerRefs != nil {
|
||||
if len(showListenerResp.Listener.SniContainerRefs) > 0 {
|
||||
// 如果开启 SNI,需替换同 SAN 的证书
|
||||
sniCertIds := make([]string, 0)
|
||||
sniCertIds = append(sniCertIds, cloudCertId)
|
||||
|
||||
listOldCertificateReq := &hcElbModel.ListCertificatesRequest{
|
||||
Id: &showListenerResp.Listener.SniContainerRefs,
|
||||
}
|
||||
listOldCertificateResp, err := d.sdkClient.ListCertificates(listOldCertificateReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListCertificates'")
|
||||
}
|
||||
|
||||
showNewCertificateReq := &hcElbModel.ShowCertificateRequest{
|
||||
CertificateId: cloudCertId,
|
||||
}
|
||||
showNewCertificateResp, err := d.sdkClient.ShowCertificate(showNewCertificateReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowCertificate'")
|
||||
}
|
||||
|
||||
for _, certificate := range *listOldCertificateResp.Certificates {
|
||||
oldCertificate := certificate
|
||||
newCertificate := showNewCertificateResp.Certificate
|
||||
|
||||
if oldCertificate.SubjectAlternativeNames != nil && newCertificate.SubjectAlternativeNames != nil {
|
||||
if slices.Equal(*oldCertificate.SubjectAlternativeNames, *newCertificate.SubjectAlternativeNames) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if oldCertificate.Domain == newCertificate.Domain {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sniCertIds = append(sniCertIds, certificate.Id)
|
||||
}
|
||||
|
||||
updateListenerReq.Body.Listener.SniContainerRefs = &sniCertIds
|
||||
}
|
||||
|
||||
if showListenerResp.Listener.SniMatchAlgo != "" {
|
||||
updateListenerReq.Body.Listener.SniMatchAlgo = cast.StringPtr(showListenerResp.Listener.SniMatchAlgo)
|
||||
}
|
||||
}
|
||||
updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateListener'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已更新 ELB 监听器", updateListenerResp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) {
|
||||
if region == "" {
|
||||
region = "cn-north-4" // ELB 服务默认区域:华北四北京
|
||||
}
|
||||
|
||||
projectId, err := getSdkProjectId(accessKeyId, secretAccessKey, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth, err := basic.NewCredentialsBuilder().
|
||||
WithAk(accessKeyId).
|
||||
WithSk(secretAccessKey).
|
||||
WithProjectId(projectId).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hcRegion, err := hcElbRegion.SafeValueOf(region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hcClient, err := hcElb.ElbClientBuilder().
|
||||
WithRegion(hcRegion).
|
||||
WithCredential(auth).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := hcElb.NewElbClient(hcClient)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) {
|
||||
if region == "" {
|
||||
region = "cn-north-4" // IAM 服务默认区域:华北四北京
|
||||
}
|
||||
|
||||
auth, err := global.NewCredentialsBuilder().
|
||||
WithAk(accessKeyId).
|
||||
WithSk(secretAccessKey).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hcRegion, err := hcIamRegion.SafeValueOf(region)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hcClient, err := hcIam.IamClientBuilder().
|
||||
WithRegion(hcRegion).
|
||||
WithCredential(auth).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
client := hcIam.NewIamClient(hcClient)
|
||||
|
||||
request := &hcIamModel.KeystoneListProjectsRequest{
|
||||
Name: ®ion,
|
||||
}
|
||||
response, err := client.KeystoneListProjects(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if response.Projects == nil || len(*response.Projects) == 0 {
|
||||
return "", errors.New("no project found")
|
||||
}
|
||||
|
||||
return (*response.Projects)[0].Id, nil
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package huaweicloudelb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-elb"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fSecretAccessKey string
|
||||
fRegion string
|
||||
fCertificateId string
|
||||
fLoadbalancerId string
|
||||
fListenerId string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_HUAWEICLOUDELB_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "")
|
||||
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
|
||||
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v huaweicloud_elb_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_SECRETACCESSKEY="your-secret-access-key" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_REGION="cn-north-1" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_CERTIFICATEID="your-elb-cert-id" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_LOADBALANCERID="your-elb-loadbalancer-id" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_LISTENERID="your-elb-listener-id"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy_ToCertificate", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.HuaweiCloudELBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
SecretAccessKey: fSecretAccessKey,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_CERTIFICATE,
|
||||
CertificateId: fCertificateId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.HuaweiCloudELBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
SecretAccessKey: fSecretAccessKey,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_ToListenerId", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LISTENERID: %v", fListenerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.HuaweiCloudELBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
SecretAccessKey: fSecretAccessKey,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
|
||||
ListenerId: fListenerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
160
internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go
Normal file
160
internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package k8ssecret
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
k8sCore "k8s.io/api/core/v1"
|
||||
k8sMeta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
)
|
||||
|
||||
type K8sSecretDeployerConfig struct {
|
||||
// kubeconfig 文件内容。
|
||||
KubeConfig string `json:"kubeConfig,omitempty"`
|
||||
// K8s 命名空间。
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
// K8s Secret 名称。
|
||||
SecretName string `json:"secretName"`
|
||||
// K8s Secret 中用于存放证书的 Key。
|
||||
SecretDataKeyForCrt string `json:"secretDataKeyForCrt,omitempty"`
|
||||
// K8s Secret 中用于存放私钥的 Key。
|
||||
SecretDataKeyForKey string `json:"secretDataKeyForKey,omitempty"`
|
||||
}
|
||||
|
||||
type K8sSecretDeployer struct {
|
||||
config *K8sSecretDeployerConfig
|
||||
logger deployer.Logger
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*K8sSecretDeployer)(nil)
|
||||
|
||||
func New(config *K8sSecretDeployerConfig) (*K8sSecretDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *K8sSecretDeployerConfig, logger deployer.Logger) (*K8sSecretDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
return &K8sSecretDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *K8sSecretDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
if d.config.Namespace == "" {
|
||||
return nil, errors.New("config `namespace` is required")
|
||||
}
|
||||
if d.config.SecretName == "" {
|
||||
return nil, errors.New("config `secretName` is required")
|
||||
}
|
||||
if d.config.SecretDataKeyForCrt == "" {
|
||||
return nil, errors.New("config `secretDataKeyForCrt` is required")
|
||||
}
|
||||
if d.config.SecretDataKeyForKey == "" {
|
||||
return nil, errors.New("config `secretDataKeyForKey` is required")
|
||||
}
|
||||
|
||||
certX509, err := x509.ParseCertificateFromPEM(certPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 连接
|
||||
client, err := createK8sClient(d.config.KubeConfig)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create k8s client")
|
||||
}
|
||||
|
||||
var secretPayload *k8sCore.Secret
|
||||
secretAnnotations := map[string]string{
|
||||
"certimate/common-name": certX509.Subject.CommonName,
|
||||
"certimate/subject-sn": certX509.Subject.SerialNumber,
|
||||
"certimate/subject-alt-names": strings.Join(certX509.DNSNames, ","),
|
||||
"certimate/issuer-sn": certX509.Issuer.SerialNumber,
|
||||
"certimate/issuer-org": strings.Join(certX509.Issuer.Organization, ","),
|
||||
}
|
||||
|
||||
// 获取 Secret 实例,如果不存在则创建
|
||||
secretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Get(context.TODO(), d.config.SecretName, k8sMeta.GetOptions{})
|
||||
if err != nil {
|
||||
secretPayload = &k8sCore.Secret{
|
||||
TypeMeta: k8sMeta.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: k8sMeta.ObjectMeta{
|
||||
Name: d.config.SecretName,
|
||||
Annotations: secretAnnotations,
|
||||
},
|
||||
Type: k8sCore.SecretType("kubernetes.io/tls"),
|
||||
}
|
||||
secretPayload.Data = make(map[string][]byte)
|
||||
secretPayload.Data[d.config.SecretDataKeyForCrt] = []byte(certPem)
|
||||
secretPayload.Data[d.config.SecretDataKeyForKey] = []byte(privkeyPem)
|
||||
|
||||
_, err = client.CoreV1().Secrets(d.config.Namespace).Create(context.TODO(), secretPayload, k8sMeta.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create k8s secret")
|
||||
} else {
|
||||
d.logger.Logf("k8s secret created", secretPayload)
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 Secret 实例
|
||||
secretPayload.Type = k8sCore.SecretType("kubernetes.io/tls")
|
||||
if secretPayload.ObjectMeta.Annotations == nil {
|
||||
secretPayload.ObjectMeta.Annotations = secretAnnotations
|
||||
} else {
|
||||
for k, v := range secretAnnotations {
|
||||
secretPayload.ObjectMeta.Annotations[k] = v
|
||||
}
|
||||
}
|
||||
secretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Update(context.TODO(), secretPayload, k8sMeta.UpdateOptions{})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to update k8s secret")
|
||||
}
|
||||
|
||||
d.logger.Logf("k8s secret updated", secretPayload)
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createK8sClient(kubeConfig string) (*kubernetes.Clientset, error) {
|
||||
var config *rest.Config
|
||||
var err error
|
||||
if kubeConfig == "" {
|
||||
config, err = rest.InClusterConfig()
|
||||
} else {
|
||||
kubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(kubeConfig))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err = kubeConfig.ClientConfig()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package k8ssecret_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/k8s-secret"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fNamespace string
|
||||
fSecretName string
|
||||
fSecretDataKeyForCrt string
|
||||
fSecretDataKeyForKey string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_K8SSECRET_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fNamespace, argsPrefix+"NAMESPACE", "default", "")
|
||||
flag.StringVar(&fSecretName, argsPrefix+"SECRETNAME", "", "")
|
||||
flag.StringVar(&fSecretDataKeyForCrt, argsPrefix+"SECRETDATAKEYFORCRT", "tls.crt", "")
|
||||
flag.StringVar(&fSecretDataKeyForKey, argsPrefix+"SECRETDATAKEYFORKEY", "tls.key", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v k8s_secret_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_K8SSECRET_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_K8SSECRET_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_K8SSECRET_NAMESPACE="default" \
|
||||
--CERTIMATE_DEPLOYER_K8SSECRET_SECRETNAME="secret" \
|
||||
--CERTIMATE_DEPLOYER_K8SSECRET_SECRETDATAKEYFORCRT="tls.crt" \
|
||||
--CERTIMATE_DEPLOYER_K8SSECRET_SECRETDATAKEYFORKEY="tls.key"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("NAMESPACE: %v", fNamespace),
|
||||
fmt.Sprintf("SECRETNAME: %v", fSecretName),
|
||||
fmt.Sprintf("SECRETDATAKEYFORCRT: %v", fSecretDataKeyForCrt),
|
||||
fmt.Sprintf("SECRETDATAKEYFORKEY: %v", fSecretDataKeyForKey),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.K8sSecretDeployerConfig{
|
||||
Namespace: fNamespace,
|
||||
SecretName: fSecretName,
|
||||
SecretDataKeyForCrt: fSecretDataKeyForCrt,
|
||||
SecretDataKeyForKey: fSecretDataKeyForKey,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
17
internal/pkg/core/deployer/providers/local/defines.go
Normal file
17
internal/pkg/core/deployer/providers/local/defines.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package local
|
||||
|
||||
type OutputFormatType string
|
||||
|
||||
const (
|
||||
OUTPUT_FORMAT_PEM = OutputFormatType("PEM")
|
||||
OUTPUT_FORMAT_PFX = OutputFormatType("PFX")
|
||||
OUTPUT_FORMAT_JKS = OutputFormatType("JKS")
|
||||
)
|
||||
|
||||
type ShellEnvType string
|
||||
|
||||
const (
|
||||
SHELL_ENV_SH = ShellEnvType("sh")
|
||||
SHELL_ENV_CMD = ShellEnvType("cmd")
|
||||
SHELL_ENV_POWERSHELL = ShellEnvType("powershell")
|
||||
)
|
||||
178
internal/pkg/core/deployer/providers/local/local.go
Normal file
178
internal/pkg/core/deployer/providers/local/local.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/fs"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
)
|
||||
|
||||
type LocalDeployerConfig struct {
|
||||
// Shell 执行环境。
|
||||
// 零值时默认根据操作系统决定。
|
||||
ShellEnv ShellEnvType `json:"shellEnv,omitempty"`
|
||||
// 前置命令。
|
||||
PreCommand string `json:"preCommand,omitempty"`
|
||||
// 后置命令。
|
||||
PostCommand string `json:"postCommand,omitempty"`
|
||||
// 输出证书格式。
|
||||
OutputFormat OutputFormatType `json:"outputFormat,omitempty"`
|
||||
// 输出证书文件路径。
|
||||
OutputCertPath string `json:"outputCertPath,omitempty"`
|
||||
// 输出私钥文件路径。
|
||||
OutputKeyPath string `json:"outputKeyPath,omitempty"`
|
||||
// PFX 导出密码。
|
||||
// 证书格式为 PFX 时必填。
|
||||
PfxPassword string `json:"pfxPassword,omitempty"`
|
||||
// JKS 别名。
|
||||
// 证书格式为 JKS 时必填。
|
||||
JksAlias string `json:"jksAlias,omitempty"`
|
||||
// JKS 密钥密码。
|
||||
// 证书格式为 JKS 时必填。
|
||||
JksKeypass string `json:"jksKeypass,omitempty"`
|
||||
// JKS 存储密码。
|
||||
// 证书格式为 JKS 时必填。
|
||||
JksStorepass string `json:"jksStorepass,omitempty"`
|
||||
}
|
||||
|
||||
type LocalDeployer struct {
|
||||
config *LocalDeployerConfig
|
||||
logger deployer.Logger
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*LocalDeployer)(nil)
|
||||
|
||||
func New(config *LocalDeployerConfig) (*LocalDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *LocalDeployerConfig, logger deployer.Logger) (*LocalDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
return &LocalDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *LocalDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 执行前置命令
|
||||
if d.config.PreCommand != "" {
|
||||
stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PreCommand)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrapf(err, "failed to run pre-command, stdout: %s, stderr: %s", stdout, stderr)
|
||||
}
|
||||
|
||||
d.logger.Logt("pre-command executed", stdout)
|
||||
}
|
||||
|
||||
// 写入证书和私钥文件
|
||||
switch d.config.OutputFormat {
|
||||
case OUTPUT_FORMAT_PEM:
|
||||
if err := fs.WriteFileString(d.config.OutputCertPath, certPem); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to save certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file saved")
|
||||
|
||||
if err := fs.WriteFileString(d.config.OutputKeyPath, privkeyPem); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to save private key file")
|
||||
}
|
||||
|
||||
d.logger.Logt("private key file saved")
|
||||
|
||||
case OUTPUT_FORMAT_PFX:
|
||||
pfxData, err := x509.TransformCertificateFromPEMToPFX(certPem, privkeyPem, d.config.PfxPassword)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to transform certificate to PFX")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate transformed to PFX")
|
||||
|
||||
if err := fs.WriteFile(d.config.OutputCertPath, pfxData); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to save certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file saved")
|
||||
|
||||
case OUTPUT_FORMAT_JKS:
|
||||
jksData, err := x509.TransformCertificateFromPEMToJKS(certPem, privkeyPem, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to transform certificate to JKS")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate transformed to JKS")
|
||||
|
||||
if err := fs.WriteFile(d.config.OutputCertPath, jksData); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to save certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded")
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported output format: %s", d.config.OutputFormat)
|
||||
}
|
||||
|
||||
// 执行后置命令
|
||||
if d.config.PostCommand != "" {
|
||||
stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PostCommand)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
|
||||
}
|
||||
|
||||
d.logger.Logt("post-command executed", stdout)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func execCommand(shellEnv ShellEnvType, command string) (string, string, error) {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
switch shellEnv {
|
||||
case SHELL_ENV_SH:
|
||||
cmd = exec.Command("sh", "-c", command)
|
||||
|
||||
case SHELL_ENV_CMD:
|
||||
cmd = exec.Command("cmd", "/C", command)
|
||||
|
||||
case SHELL_ENV_POWERSHELL:
|
||||
cmd = exec.Command("powershell", "-Command", command)
|
||||
|
||||
case "":
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.Command("cmd", "/C", command)
|
||||
} else {
|
||||
cmd = exec.Command("sh", "-c", command)
|
||||
}
|
||||
|
||||
default:
|
||||
return "", "", fmt.Errorf("unsupported shell env: %s", shellEnv)
|
||||
}
|
||||
|
||||
var stdoutBuf bytes.Buffer
|
||||
cmd.Stdout = &stdoutBuf
|
||||
var stderrBuf bytes.Buffer
|
||||
cmd.Stderr = &stderrBuf
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return "", "", xerrors.Wrap(err, "failed to execute shell command")
|
||||
}
|
||||
|
||||
return stdoutBuf.String(), stderrBuf.String(), nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user