Compare commits
364 Commits
v0.1.15
...
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 | ||
|
|
a035ba192a | ||
|
|
d90319bb53 | ||
|
|
51db5e1ed0 | ||
|
|
1ce2a52d70 | ||
|
|
fbbea22eee | ||
|
|
71f43c5bd4 | ||
|
|
79dce124a7 | ||
|
|
0bc042ae31 | ||
|
|
704d2eed32 | ||
|
|
3348301493 | ||
|
|
afeae4269c | ||
|
|
a4d1c9a7c0 | ||
|
|
26be47d072 | ||
|
|
cf3de10eff | ||
|
|
7ef885319e | ||
|
|
b0923d54ee | ||
|
|
be15f2b6a6 | ||
|
|
0c1d3341f4 | ||
|
|
1a9cad355c | ||
|
|
2c97fa929a | ||
|
|
e397793153 | ||
|
|
9bd279a8a0 | ||
|
|
dd7897feff | ||
|
|
d851a86a9b | ||
|
|
62133a91a6 | ||
|
|
5b30fc8aba | ||
|
|
2ed94bf509 | ||
|
|
1928a47961 | ||
|
|
19f5348802 | ||
|
|
f148240bcf | ||
|
|
f914931bc9 | ||
|
|
8c1033634d | ||
|
|
781b79f529 | ||
|
|
7d74e1d41e | ||
|
|
ad91703492 | ||
|
|
a007c81e9a | ||
|
|
39bffe3389 | ||
|
|
3b06c7b0a6 | ||
|
|
3f2767b28b | ||
|
|
312c6e685a | ||
|
|
d2b6ab75b7 | ||
|
|
9f1b00f04c | ||
|
|
dc16294b3d | ||
|
|
77dfcef168 | ||
|
|
30ef5841d6 | ||
|
|
217ba85ff8 | ||
|
|
71e2555391 |
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = crlf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -3,15 +3,15 @@ name: Bug report
|
||||
about: 创建一个报告来帮助我们改进
|
||||
title: "[Bug] 标题简要描述问题"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**描述问题**
|
||||
简要描述问题是什么
|
||||
简要描述问题是什么,1 个 ISSUE 只描述一个问题。
|
||||
|
||||
**复现步骤**
|
||||
复现该问题的步骤:
|
||||
|
||||
1. 去到 '...'
|
||||
2. 点击 '...'
|
||||
3. 滚动到 '...'
|
||||
@@ -24,9 +24,10 @@ assignees: ''
|
||||
如有可能,请添加截图以帮助解释问题。
|
||||
|
||||
**环境**
|
||||
|
||||
- 操作系统: [e.g. Windows, macOS]
|
||||
- 浏览器: [e.g. Chrome, Safari]
|
||||
- 仓库版本: [e.g. v1.0.0]
|
||||
|
||||
**其他信息**
|
||||
在此处添加关于该问题的任何其他信息。
|
||||
在此处添加关于该问题的任何其他信息。
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/feature_request.md
vendored
7
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -3,12 +3,11 @@ name: Feature request
|
||||
about: 提出一个新功能请求
|
||||
title: "[Feature] 简要描述你希望实现的功能"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**功能描述**
|
||||
简要描述你希望添加的功能和相关问题。
|
||||
简要描述你希望添加的功能和相关问题,1 个 ISSUE 只描述一个功能。
|
||||
|
||||
**动机**
|
||||
为什么这个功能对项目有帮助?
|
||||
@@ -17,4 +16,4 @@ assignees: ''
|
||||
描述你已经考虑过的替代方案。
|
||||
|
||||
**其他信息**
|
||||
在这里添加任何相关的附加信息或截图。
|
||||
在这里添加任何相关的附加信息或截图。
|
||||
|
||||
5
.github/workflows/push_image.yml
vendored
5
.github/workflows/push_image.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
usual2970/certimate
|
||||
usual2970/certimate
|
||||
registry.cn-shanghai.aliyuncs.com/usual2970/certimate
|
||||
|
||||
- name: Log in to DOCKERHUB
|
||||
@@ -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
|
||||
|
||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -1,14 +1,7 @@
|
||||
__debug_bin*
|
||||
vendor
|
||||
pb_data
|
||||
main
|
||||
./certimate
|
||||
build
|
||||
/docker/data
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.json
|
||||
!.vscode/settings.tailwind.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
@@ -16,5 +9,14 @@ build
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
__debug_bin*
|
||||
|
||||
vendor
|
||||
pb_data
|
||||
build
|
||||
main
|
||||
/ui/dist/*
|
||||
!/ui/dist/.gitkeep
|
||||
./dist
|
||||
./certimate
|
||||
/docker/data
|
||||
|
||||
6
.vscode/extensions.json
vendored
Normal file
6
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
20
.vscode/settings.json
vendored
Normal file
20
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"css.customData": [
|
||||
".vscode/settings.tailwind.json"
|
||||
],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"go.useLanguageServer": true,
|
||||
"gopls": {
|
||||
"formatting.gofumpt": true,
|
||||
},
|
||||
"[go]": {
|
||||
"editor.defaultFormatter": "golang.go"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
55
.vscode/settings.tailwind.json
vendored
Normal file
55
.vscode/settings.tailwind.json
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"version": 1.1,
|
||||
"atDirectives": [
|
||||
{
|
||||
"name": "@tailwind",
|
||||
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@apply",
|
||||
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@responsive",
|
||||
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@screen",
|
||||
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@variants",
|
||||
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -19,8 +19,8 @@ git clone https://github.com/your_username/certimate.git
|
||||
```
|
||||
|
||||
> **重要提示:**
|
||||
> 建议为每个 bug 修复或新功能创建一个从 `main` 分支派生的新分支。如果你计划提交多个 PR,请保持不同的改动在独立分支中,以便更容易进行代码审查并最终合并。
|
||||
> 保持一个 pr 只实现一个功能。
|
||||
> 建议为每个 Bug 修复或新功能创建一个从 `main` 分支派生的新分支。如果你计划提交多个 PR,请保持不同的改动在独立分支中,以便更容易进行代码审查并最终合并。
|
||||
> 保持一个 PR 只实现一个功能。
|
||||
|
||||
## 修改 Go 代码
|
||||
|
||||
@@ -28,6 +28,7 @@ git clone https://github.com/your_username/certimate.git
|
||||
|
||||
1. 进入根目录
|
||||
2. 运行以下命令启动服务:
|
||||
|
||||
```bash
|
||||
go run main.go serve
|
||||
```
|
||||
@@ -36,11 +37,12 @@ git clone https://github.com/your_username/certimate.git
|
||||
|
||||
**在向主仓库提交 PR 之前,建议你:**
|
||||
|
||||
- 使用 [gofumpt](https://github.com/mvdan/gofumpt) 对你的代码进行格式化。
|
||||
|
||||
- 为你的改动添加单元测试或集成测试(Certimate 使用 Go 的标准 `testing` 包)。你可以通过以下命令运行测试(在项目根目录下):
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
|
||||
```
|
||||
|
||||
## 修改管理页面 (Admin UI)
|
||||
@@ -49,17 +51,19 @@ Certimate 的管理页面是一个基于 React 和 Vite 构建的单页应用(
|
||||
|
||||
要启动 Admin UI:
|
||||
|
||||
1. 进入 `ui` 项目目录
|
||||
2. 运行 `npm install` 安装依赖
|
||||
1. 进入 `ui` 项目目录。
|
||||
|
||||
2. 运行 `npm install` 安装依赖。
|
||||
|
||||
3. 启动 Vite 开发服务器:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
你可以通过浏览器访问 `http://localhost:5173` 来查看运行中的管理页面。
|
||||
|
||||
由于 Admin UI 只是一个客户端应用,运行时需要 Certimate 的后端服务作为支撑。你可以手动运行Certimate,或者使用预构建的可执行文件。
|
||||
|
||||
由于 Admin UI 只是一个客户端应用,运行时需要 Certimate 的后端服务作为支撑。你可以手动运行 Certimate,或者使用预构建的可执行文件。
|
||||
|
||||
所有对 Admin UI 的修改将会自动反映在浏览器中,无需手动刷新页面。
|
||||
|
||||
@@ -69,4 +73,4 @@ Certimate 的管理页面是一个基于 React 和 Vite 构建的单页应用(
|
||||
npm run build
|
||||
```
|
||||
|
||||
完成所有步骤后,你可以将改动提交 PR 到 Certimate 主仓库。
|
||||
完成所有步骤后,你可以将改动提交 PR 到 Certimate 主仓库。
|
||||
|
||||
@@ -27,7 +27,9 @@ git clone https://github.com/your_username/certimate.git
|
||||
Once you have made changes to the Go code in Certimate, follow these steps to run the project:
|
||||
|
||||
1. Navigate to the root directory.
|
||||
|
||||
2. Start the service by running:
|
||||
|
||||
```bash
|
||||
go run main.go serve
|
||||
```
|
||||
@@ -36,6 +38,8 @@ This will start a web server at `http://localhost:8090` using the prebuilt Admin
|
||||
|
||||
**Before submitting a PR to the main repository, consider:**
|
||||
|
||||
- Format your source code by using [gofumpt](https://github.com/mvdan/gofumpt).
|
||||
|
||||
- Adding unit or integration tests for your changes. Certimate uses Go’s standard `testing` package. You can run tests using the following command (while in the root project directory):
|
||||
|
||||
```bash
|
||||
@@ -49,11 +53,15 @@ Certimate’s Admin UI is a single-page application (SPA) built using React and
|
||||
To start the Admin UI:
|
||||
|
||||
1. Navigate to the `ui` project directory.
|
||||
|
||||
2. Install the necessary dependencies by running:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Start the Vite development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
@@ -70,4 +78,4 @@ After completing your changes, build the Admin UI so it can be embedded into the
|
||||
npm run build
|
||||
```
|
||||
|
||||
Once all steps are completed, you are ready to submit a PR to the main Certimate repository.
|
||||
Once all steps are completed, you are ready to submit a PR to the main Certimate repository.
|
||||
|
||||
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"]
|
||||
5
Makefile
5
Makefile
@@ -34,4 +34,7 @@ help:
|
||||
@echo " make clean - 清理构建文件"
|
||||
@echo " make help - 显示此帮助信息"
|
||||
|
||||
.PHONY: all build clean 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
|
||||
|
||||
131
README.md
131
README.md
@@ -4,21 +4,22 @@
|
||||
|
||||
做个人产品或在小企业负责运维的同学,需要管理多个域名,要给域名申请证书。但手动申请证书有以下缺点:
|
||||
|
||||
1. 😱麻烦:申请、部署证书虽不困难,但也挺麻烦的,尤其是维护多个域名的时候。
|
||||
2. 😭易忘:当前免费证书有效期仅90天,这就要求定期操作,增加工作量的同时,也很容易忘掉,导致网站无法访问。
|
||||
1. 😱 麻烦:申请、部署证书虽不困难,但也挺麻烦的,尤其是维护多个域名的时候。
|
||||
2. 😭 易忘:当前免费证书有效期仅 90 天,这就要求定期操作,增加工作量的同时,也很容易忘掉,导致网站无法访问。
|
||||
|
||||
Certimate 就是为了解决上述问题而产生的,它具有以下特点:
|
||||
|
||||
1. 操作简单:自动申请、部署、续期 SSL 证书,全程无需人工干预。
|
||||
2. 支持私有部署:部署方法简单,只需下载二进制文件执行即可。二进制文件、docker 镜像全部用 github actions 生成,过程透明,可自行审计。
|
||||
2. 支持私有部署:部署方法简单,只需下载二进制文件执行即可。二进制文件、Docker 镜像全部用 Github Actions 生成,过程透明,可自行审计。
|
||||
3. 数据安全:由于是私有部署,所有数据均存储在本地,不会保存在服务商的服务器,确保数据的安全性。
|
||||
|
||||
相关文章:
|
||||
|
||||
* [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
||||
* [域名变量及部署授权组介绍](https://docs.certimate.me/blog/multi-deployer)
|
||||
- [⚠️⚠️⚠️V0.2.0-第一个不向后兼容的版本](https://docs.certimate.me/blog/v0.2.0)
|
||||
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
||||
- [域名变量及部署授权组介绍](https://docs.certimate.me/blog/multi-deployer)
|
||||
|
||||
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。使用文档请访问[https://docs.certimate.me](https://docs.certimate.me)
|
||||
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。使用文档请访问 [https://docs.certimate.me](https://docs.certimate.me)
|
||||
|
||||
## 一、安装
|
||||
|
||||
@@ -32,8 +33,14 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
|
||||
./certimate serve
|
||||
```
|
||||
|
||||
或运行以下命令自动给 Certimate 自身添加证书
|
||||
|
||||
```bash
|
||||
./certimate serve 你的域名
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> MacOS 在执行二进制文件时会提示:无法打开“certimate”,因为Apple无法检查其是否包含恶意软件。可在系统设置> 隐私与安全性> 安全性 中点击 "仍然允许",然后再次尝试执行二进制文件。
|
||||
> MacOS 在执行二进制文件时会提示:无法打开“Certimate”,因为 Apple 无法检查其是否包含恶意软件。可在“系统设置 > 隐私与安全性 > 安全性”中点击“仍然允许”,然后再次尝试执行二进制文件。
|
||||
|
||||
### 2. Docker 安装
|
||||
|
||||
@@ -48,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
|
||||
```
|
||||
|
||||
## 二、使用
|
||||
@@ -65,81 +71,94 @@ go run main.go serve
|
||||
|
||||
## 三、支持的服务商列表
|
||||
|
||||
| 服务商 | 是否域名服务商 | 是否部署服务 | 备注 |
|
||||
| ---------- | -------------- | ------------ | ------------------------------------------------------ |
|
||||
| 阿里云 | 是 | 是 | 支持阿里云注册的域名,支持部署到阿里云 CDN,OSS |
|
||||
| 腾讯云 | 是 | 是 | 支持腾讯云注册的域名,支持部署到腾讯云 CDN |
|
||||
| 七牛云 | 否 | 是 | 七牛云没有注册域名服务,支持部署到七牛云 CDN |
|
||||
| CloudFlare | 是 | 否 | 支持 CloudFlare 注册的域名,CloudFlare 服务自带SSL证书 |
|
||||
| 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>
|
||||
|
||||
## 五、概念
|
||||
|
||||
Certimate 的工作流程如下:
|
||||
|
||||
* 用户通过 Certimate 管理页面填写申请证书的信息,包括域名、dns 服务商的授权信息、以及要部署到的服务商的授权信息。
|
||||
* Certimate 向证书厂商的 API 发起申请请求,获取 SSL 证书。
|
||||
* Certimate 存储证书信息,包括证书内容、私钥、证书有效期等,并在证书即将过期时自动续期。
|
||||
* Certimate 向服务商的 API 发起部署请求,将证书部署到服务商的服务器上。
|
||||
- 用户通过 Certimate 管理页面填写申请证书的信息,包括域名、DNS 服务商的授权信息、以及要部署到的服务商的授权信息。
|
||||
- Certimate 向证书厂商的 API 发起申请请求,获取 SSL 证书。
|
||||
- Certimate 存储证书信息,包括证书内容、私钥、证书有效期等,并在证书即将过期时自动续期。
|
||||
- Certimate 向服务商的 API 发起部署请求,将证书部署到服务商的服务器上。
|
||||
|
||||
这就涉及域名、dns 服务商的授权信息、部署服务商的授权信息等。
|
||||
这就涉及域名、DNS 服务商的授权信息、部署服务商的授权信息等。
|
||||
|
||||
### 1. 域名
|
||||
|
||||
就是要申请证书的域名。
|
||||
|
||||
### 2. dns 服务商授权信息
|
||||
### 2. DNS 服务商授权信息
|
||||
|
||||
给域名申请证书需要证明域名是你的,所以我们手动申请证书的时候一般需要在域名服务商的控制台解析记录中添加一个 TXT 记录。
|
||||
给域名申请证书需要证明域名是你的,所以我们手动申请证书的时候一般需要在域名服务商的控制台解析记录中添加一个 TXT 域名解析记录。
|
||||
|
||||
Certimate 会自动添加一个 TXT 记录,你只需要在 Certimate 后台中填写你的域名服务商的授权信息即可。
|
||||
Certimate 会自动添加一个 TXT 域名解析记录,你只需要在 Certimate 后台中填写你的域名服务商的授权信息即可。
|
||||
|
||||
比如你在阿里云购买的域名,授权信息如下:
|
||||
|
||||
```bash
|
||||
accessKeyId: xxx
|
||||
accessKeySecret: TOKEN
|
||||
accessKeyId: your-access-key-id
|
||||
accessKeySecret: your-access-key-secret
|
||||
```
|
||||
|
||||
在腾讯云购买的域名,授权信息如下:
|
||||
|
||||
```bash
|
||||
secretId: xxx
|
||||
secretKey: TOKEN
|
||||
secretId: your-secret-id
|
||||
secretKey: your-secret-key
|
||||
```
|
||||
|
||||
注意,此授权信息需具有访问域名及 DNS 解析的管理权限,具体的权限清单请参阅各服务商自己的技术文档。
|
||||
|
||||
### 3. 部署服务商授权信息
|
||||
|
||||
Certimate 申请证书后,会自动将证书部署到你指定的目标上,比如阿里云 CDN 这时你需要填写阿里云的授权信息。Certimate 会根据你填写的授权信息及域名找到对应的 CDN 服务,并将证书部署到对应的 CDN 服务上。
|
||||
Certimate 申请证书后,会自动将证书部署到你指定的目标上,比如阿里云 CDN,Certimate 会根据你填写的授权信息及域名找到对应的 CDN 服务,并将证书部署到对应的 CDN 服务上。
|
||||
|
||||
部署服务商授权信息和 dns 服务商授权信息一致,区别在于 dns 服务商授权信息用于证明域名是你的,部署服务商授权信息用于提供证书部署的授权信息。
|
||||
部署服务商授权信息和 DNS 服务商授权信息基本一致,区别在于 DNS 服务商授权信息用于证明域名是你的,部署服务商授权信息用于提供证书部署的授权信息。
|
||||
|
||||
注意,此授权信息需具有访问部署目标服务的相关管理权限,具体的权限清单请参阅各服务商自己的技术文档。
|
||||
|
||||
## 六、常见问题
|
||||
|
||||
Q: 提供saas服务吗?
|
||||
Q: 提供 SaaS 服务吗?
|
||||
|
||||
> A: 不提供,目前仅支持self-hosted(私有部署)。
|
||||
> A: 不提供,目前仅支持 self-hosted(私有部署)。
|
||||
|
||||
Q: 数据安全?
|
||||
|
||||
> A: 由于仅支持私有部署,各种数据都保存在用户的服务器上。另外Certimate源码也开源,二进制包及Docker镜像打包过程全部使用Github actions进行,过程透明可见,可自行审计。
|
||||
> A: 由于仅支持私有部署,各种数据都保存在用户的服务器上。另外 Certimate 源码也开源,二进制包及 Docker 镜像打包过程全部使用 Github Actions 进行,过程透明可见,可自行审计。
|
||||
|
||||
Q: 自动续期证书?
|
||||
|
||||
> A: 已经申请的证书会在过期前10天自动续期。每天会检查一次证书是否快要过期,快要过期时会自动重新申请证书并部署到目标服务上。
|
||||
> A: 已经申请的证书会在**过期前 10 天**自动续期。每天会检查一次证书是否快要过期,快要过期时会自动重新申请证书并部署到目标服务上。
|
||||
|
||||
## 七、贡献
|
||||
|
||||
@@ -147,17 +166,27 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.
|
||||
|
||||
你可以通过以下方式来支持 Certimate 的开发:
|
||||
|
||||
* [提交代码:如果你发现了 bug 或有新的功能需求,而你又有相关经验,可以提交代码给我们](CONTRIBUTING.md)。
|
||||
* 提交 issue:功能建议或者 bug 可以[提交 issue](https://github.com/usual2970/certimate/issues) 给我们。
|
||||
- 提交代码:如果你发现了 Bug 或有新的功能需求,而你又有相关经验,可以[提交代码](CONTRIBUTING.md)给我们。
|
||||
- 提交 Issue:功能建议或者 Bug 可以[提交 Issue](https://github.com/usual2970/certimate/issues) 给我们。
|
||||
|
||||
支持更多服务商、UI 的优化改进、BUG 修复、文档完善等,欢迎大家提交 PR。
|
||||
支持更多服务商、UI 的优化改进、Bug 修复、文档完善等,欢迎大家提交 PR。
|
||||
|
||||
## 八、加入社区
|
||||
## 八、免责声明
|
||||
|
||||
* [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
|
||||
* 微信群聊(需要邀请入群,可先加作者好友)
|
||||
本软件依据 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 History
|
||||
## 十、Star 趋势图
|
||||
|
||||
[](https://starchart.cc/usual2970/certimate)
|
||||
|
||||
|
||||
102
README_EN.md
102
README_EN.md
@@ -15,10 +15,10 @@ Certimate was created to solve the above-mentioned issues and has the following
|
||||
|
||||
Related articles:
|
||||
|
||||
* [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
||||
* [Introduction to Domain Variables and Deployment Authorization Groups](https://docs.certimate.me/blog/multi-deployer)
|
||||
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
||||
- [Introduction to Domain Variables and Deployment Authorization Groups](https://docs.certimate.me/blog/multi-deployer)
|
||||
|
||||
Certimate aims to provide users with a secure and user-friendly SSL certificate management solution. For usage documentation, please visit.[https://docs.certimate.me](https://docs.certimate.me)
|
||||
Certimate aims to provide users with a secure and user-friendly SSL certificate management solution. For usage documentation, please visit [https://docs.certimate.me](https://docs.certimate.me).
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -32,6 +32,12 @@ You can download the precompiled binary files directly from the [Releases page](
|
||||
./certimate serve
|
||||
```
|
||||
|
||||
Or run the following command to automatically add a certificate to Certimate itself.
|
||||
|
||||
```bash
|
||||
./certimate serve yourDomain
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> When executing the binary file on macOS, you may see a prompt saying: “Cannot open ‘certimate’ because Apple cannot check it for malicious software.” You can go to System Preferences > Security & Privacy > General, then click “Allow Anyway,” and try executing the binary file again.
|
||||
|
||||
@@ -48,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
|
||||
@@ -65,35 +70,44 @@ password:1234567890
|
||||
|
||||
## List of Supported Providers
|
||||
|
||||
| Provider | Domain Registrar | Deployment Service | Remarks |
|
||||
| ------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------- |
|
||||
| Alibaba Cloud | Yes | Yes | Supports domains registered with Alibaba Cloud; supports deployment to Alibaba Cloud CDN and OSS. |
|
||||
| Tencent Cloud | Yes | Yes | Supports domains registered with Tencent Cloud; supports deployment to Tencent Cloud CDN. |
|
||||
| Qiniu Cloud | No | Yes | Qiniu Cloud does not offer domain registration services; supports deployment to Qiniu Cloud CDN. |
|
||||
| Cloudflare | Yes | No | Supports domains registered with Cloudflare; Cloudflare services come with SSL certificates. |
|
||||
| SSH | No | Yes | Supports deployment to SSH servers. |
|
||||
| WEBHOOK | No | Yes | Supports callbacks 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
|
||||
|
||||
The workflow of Certimate is as follows:
|
||||
|
||||
* Users fill in the certificate application information on the Certimate management page, including domain name, authorization information for the DNS provider, and authorization information for the service provider to deploy to.
|
||||
* Certimate sends a request to the certificate vendor's API to apply for an SSL certificate.
|
||||
* Certimate stores the certificate information, including the certificate content, private key, validity period, etc., and automatically renews the certificate when it is about to expire.
|
||||
* Certimate sends a deployment request to the service provider's API to deploy the certificate to the service provider's servers.
|
||||
- Users fill in the certificate application information on the Certimate management page, including domain name, authorization information for the DNS provider, and authorization information for the service provider to deploy to.
|
||||
- Certimate sends a request to the certificate vendor's API to apply for an SSL certificate.
|
||||
- Certimate stores the certificate information, including the certificate content, private key, validity period, etc., and automatically renews the certificate when it is about to expire.
|
||||
- Certimate sends a deployment request to the service provider's API to deploy the certificate to the service provider's servers.
|
||||
|
||||
This involves authorization information for the domain, DNS provider, and deployment service provider.
|
||||
|
||||
@@ -110,23 +124,27 @@ Certimate will automatically add a TXT record for you; you only need to fill in
|
||||
For example, if you purchased the domain from Alibaba Cloud, the authorization information would be as follows:
|
||||
|
||||
```bash
|
||||
accessKeyId: xxx
|
||||
accessKeySecret: TOKEN
|
||||
accessKeyId: your-access-key-id
|
||||
accessKeySecret: your-access-key-secret
|
||||
```
|
||||
|
||||
If you purchased the domain from Tencent Cloud, the authorization information would be as follows:
|
||||
|
||||
```bash
|
||||
secretId: xxx
|
||||
secretKey: TOKEN
|
||||
secretId: your-secret-id
|
||||
secretKey: your-secret-key
|
||||
```
|
||||
|
||||
Notes: This authorization information requires relevant administration permissions for accessing the DNS services. Please refer to the documentations of each service provider for the specific permissions list.
|
||||
|
||||
### 3. Authorization Information for the Deployment Service Provider
|
||||
|
||||
After Certimate applies for the certificate, it will automatically deploy the certificate to your specified target, such as Alibaba Cloud CDN. At this point, you need to fill in the authorization information for Alibaba Cloud. Certimate will use the authorization information and domain name you provided to locate the corresponding CDN service and deploy the certificate to that service.
|
||||
|
||||
The authorization information for the deployment service provider is the same as that for the DNS provider, with the distinction that the DNS provider's authorization information is used to prove that the domain belongs to you, while the deployment service provider's authorization information is used to provide authorization for the certificate deployment.
|
||||
|
||||
Notes: This authorization information requires relevant administration permissions to access the target deployment services. Please refer to the documentations of each service provider for the specific permissions list.
|
||||
|
||||
## FAQ
|
||||
|
||||
Q: Do you provide SaaS services?
|
||||
@@ -139,7 +157,7 @@ Q: Data Security?
|
||||
|
||||
Q: Automatic Certificate Renewal?
|
||||
|
||||
> A: Certificates that have already been issued will be automatically renewed 10 days before expiration. The system checks once a day to see if any certificates are nearing expiration, and if so, it will automatically reapply for the certificate and deploy it to the target service.
|
||||
> A: Certificates that have already been issued will be automatically renewed **10 days before expiration**. The system checks once a day to see if any certificates are nearing expiration, and if so, it will automatically reapply for the certificate and deploy it to the target service.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -147,14 +165,26 @@ Certimate is a free and open-source project, licensed under the [MIT License](LI
|
||||
|
||||
You can support the development of Certimate in the following ways:
|
||||
|
||||
* **Submit Code**: If you find a bug or have new feature requests, and you have relevant experience, [you can submit code to us](CONTRIBUTING_EN.md).
|
||||
* **Submit an Issue**: For feature suggestions or bugs, you can [submit an issue](https://github.com/usual2970/certimate/issues) to us.
|
||||
- **Submit Code**: If you find a bug or have new feature requests, and you have relevant experience, [you can submit code to us](CONTRIBUTING_EN.md).
|
||||
- **Submit an Issue**: For feature suggestions or bugs, you can [submit an issue](https://github.com/usual2970/certimate/issues) to us.
|
||||
|
||||
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)
|
||||
* Wechat Group
|
||||
- [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
|
||||
- Wechat Group
|
||||
|
||||
<img src="https://i.imgur.com/zSHEoIm.png" width="400"/>
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://starchart.cc/usual2970/certimate)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
version: '3.0'
|
||||
version: "3.0"
|
||||
services:
|
||||
certimate:
|
||||
image: registry.cn-shanghai.aliyuncs.com/usual2970/certimate:latest
|
||||
container_name: certimate_server
|
||||
ports:
|
||||
certimate:
|
||||
image: registry.cn-shanghai.aliyuncs.com/usual2970/certimate:latest
|
||||
container_name: certimate_server
|
||||
ports:
|
||||
- 8090:8090
|
||||
volumes:
|
||||
volumes:
|
||||
- ./data:/app/pb_data
|
||||
restart: unless-stopped
|
||||
restart: unless-stopped
|
||||
|
||||
156
go.mod
156
go.mod
@@ -1,26 +1,41 @@
|
||||
module certimate
|
||||
module github.com/usual2970/certimate
|
||||
|
||||
go 1.22
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.22.5
|
||||
toolchain go1.23.2
|
||||
|
||||
require (
|
||||
github.com/alibabacloud-go/cas-20200407/v2 v2.3.0
|
||||
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/go-acme/lego/v4 v4.17.4
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
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/common v1.0.992
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn 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.26.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 (
|
||||
@@ -28,65 +43,92 @@ 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/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.62.712 // 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.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // 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.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
||||
github.com/aws/smithy-go v1.20.3 // 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.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.97.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // 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.2 // 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
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
@@ -97,40 +139,40 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/miekg/dns v1.1.59 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
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/pmezard/go-difflib v1.0.0 // 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.898 // indirect
|
||||
github.com/tjfoc/gmsm v1.3.2 // 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
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
gocloud.dev v0.37.0 // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/term v0.23.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
golang.org/x/mod v0.21.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
|
||||
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.189.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a // indirect
|
||||
google.golang.org/grpc v1.65.0 // 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
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type aliyun struct {
|
||||
@@ -19,12 +21,12 @@ func NewAliyun(option *ApplyOption) Applicant {
|
||||
}
|
||||
|
||||
func (a *aliyun) Apply() (*Certificate, error) {
|
||||
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("ALICLOUD_ACCESS_KEY", access.AccessKeyId)
|
||||
os.Setenv("ALICLOUD_SECRET_KEY", access.AccessKeySecret)
|
||||
os.Setenv("ALICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
dnsProvider, err := alidns.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"certimate/internal/utils/app"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"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"
|
||||
@@ -20,31 +26,41 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
configTypeTencent = "tencent"
|
||||
configTypeAliyun = "aliyun"
|
||||
configTypeCloudflare = "cloudflare"
|
||||
configTypeNamesilo = "namesilo"
|
||||
configTypeGodaddy = "godaddy"
|
||||
configTypeAliyun = "aliyun"
|
||||
configTypeTencent = "tencent"
|
||||
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"
|
||||
|
||||
const defaultTimeout = 60
|
||||
|
||||
type Certificate struct {
|
||||
CertUrl string `json:"certUrl"`
|
||||
CertStableUrl string `json:"certStableUrl"`
|
||||
@@ -55,25 +71,67 @@ type Certificate struct {
|
||||
}
|
||||
|
||||
type ApplyOption struct {
|
||||
Email string `json:"email"`
|
||||
Domain string `json:"domain"`
|
||||
Access string `json:"access"`
|
||||
Nameservers string `json:"nameservers"`
|
||||
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 MyUser struct {
|
||||
type ApplyUser struct {
|
||||
Ca string
|
||||
Email string
|
||||
Registration *registration.Resource
|
||||
key crypto.PrivateKey
|
||||
key string
|
||||
}
|
||||
|
||||
func (u *MyUser) GetEmail() 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 {
|
||||
return u.Email
|
||||
}
|
||||
func (u MyUser) GetRegistration() *registration.Resource {
|
||||
|
||||
func (u ApplyUser) GetRegistration() *registration.Resource {
|
||||
return u.Registration
|
||||
}
|
||||
func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -82,32 +140,60 @@ type Applicant interface {
|
||||
}
|
||||
|
||||
func Get(record *models.Record) (Applicant, error) {
|
||||
access := record.ExpandedOne("access")
|
||||
email := record.GetString("email")
|
||||
if email == "" {
|
||||
email = defaultEmail
|
||||
if record.GetString("applyConfig") == "" {
|
||||
return nil, errors.New("applyConfig is empty")
|
||||
}
|
||||
|
||||
applyConfig := &domain.ApplyConfig{}
|
||||
record.UnmarshalJSONField("applyConfig", applyConfig)
|
||||
|
||||
access, err := app.GetApp().Dao().FindRecordById("access", applyConfig.Access)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("access record not found: %w", err)
|
||||
}
|
||||
|
||||
if applyConfig.Email == "" {
|
||||
applyConfig.Email = defaultEmail
|
||||
}
|
||||
|
||||
if applyConfig.Timeout == 0 {
|
||||
applyConfig.Timeout = defaultTimeout
|
||||
}
|
||||
|
||||
option := &ApplyOption{
|
||||
Email: email,
|
||||
Domain: record.GetString("domain"),
|
||||
Access: access.GetString("config"),
|
||||
Nameservers: record.GetString("nameservers"),
|
||||
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") {
|
||||
case configTypeTencent:
|
||||
return NewTencent(option), nil
|
||||
case configTypeAliyun:
|
||||
return NewAliyun(option), nil
|
||||
case configTypeTencent:
|
||||
return NewTencent(option), nil
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type SSLProviderConfig struct {
|
||||
@@ -116,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) {
|
||||
@@ -135,21 +224,20 @@ 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 := MyUser{
|
||||
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]
|
||||
config.Certificate.KeyType = certcrypto.RSA2048
|
||||
config.Certificate.KeyType = parseKeyAlgorithm(option.KeyAlgorithm)
|
||||
|
||||
// A client facilitates communication with the CA server.
|
||||
client, err := lego.NewClient(config)
|
||||
@@ -158,7 +246,7 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
|
||||
}
|
||||
|
||||
challengeOptions := make([]dns01.ChallengeOption, 0)
|
||||
nameservers := ParseNameservers(option.Nameservers)
|
||||
nameservers := parseNameservers(option.Nameservers)
|
||||
if len(nameservers) > 0 {
|
||||
challengeOptions = append(challengeOptions, dns01.AddRecursiveNameservers(nameservers))
|
||||
}
|
||||
@@ -166,20 +254,15 @@ 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)
|
||||
}
|
||||
myUser.Registration = reg
|
||||
|
||||
domains := []string{option.Domain}
|
||||
|
||||
// 如果是通配置符域名,把根域名也加入
|
||||
if strings.HasPrefix(option.Domain, "*.") && len(strings.Split(option.Domain, ".")) == 3 {
|
||||
rootDomain := strings.TrimPrefix(option.Domain, "*.")
|
||||
domains = append(domains, rootDomain)
|
||||
if !myUser.hasRegistration() {
|
||||
reg, err := getReg(client, sslProvider, myUser)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to register: %w", err)
|
||||
}
|
||||
myUser.Registration = reg
|
||||
}
|
||||
|
||||
domains := strings.Split(option.Domain, ";")
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: domains,
|
||||
Bundle: true,
|
||||
@@ -197,10 +280,18 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
|
||||
IssuerCertificate: string(certificates.IssuerCertificate),
|
||||
Csr: string(certificates.CSR),
|
||||
}, 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 {
|
||||
@@ -210,31 +301,46 @@ 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})
|
||||
|
||||
default:
|
||||
err = errors.New("unknown ssl provider")
|
||||
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func ParseNameservers(ns string) []string {
|
||||
func parseNameservers(ns string) []string {
|
||||
nameservers := make([]string, 0)
|
||||
|
||||
lines := strings.Split(ns, ";")
|
||||
|
||||
for _, line := range lines {
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
@@ -244,3 +350,22 @@ func ParseNameservers(ns string) []string {
|
||||
|
||||
return nameservers
|
||||
}
|
||||
|
||||
func parseKeyAlgorithm(algo string) certcrypto.KeyType {
|
||||
switch algo {
|
||||
case "RSA2048":
|
||||
return certcrypto.RSA2048
|
||||
case "RSA3072":
|
||||
return certcrypto.RSA3072
|
||||
case "RSA4096":
|
||||
return certcrypto.RSA4096
|
||||
case "RSA8192":
|
||||
return certcrypto.RSA8192
|
||||
case "EC256":
|
||||
return certcrypto.EC256
|
||||
case "EC384":
|
||||
return certcrypto.EC384
|
||||
default:
|
||||
return certcrypto.RSA2048
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
cf "github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type cloudflare struct {
|
||||
@@ -23,6 +25,7 @@ func (c *cloudflare) Apply() (*Certificate, error) {
|
||||
json.Unmarshal([]byte(c.option.Access), access)
|
||||
|
||||
os.Setenv("CLOUDFLARE_DNS_API_TOKEN", access.DnsApiToken)
|
||||
os.Setenv("CLOUDFLARE_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", c.option.Timeout))
|
||||
|
||||
provider, err := cf.NewDNSProvider()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
godaddyProvider "github.com/go-acme/lego/v4/providers/dns/godaddy"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type godaddy struct {
|
||||
@@ -19,12 +21,12 @@ func NewGodaddy(option *ApplyOption) Applicant {
|
||||
}
|
||||
|
||||
func (a *godaddy) Apply() (*Certificate, error) {
|
||||
|
||||
access := &domain.GodaddyAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("GODADDY_API_KEY", access.ApiKey)
|
||||
os.Setenv("GODADDY_API_SECRET", access.ApiSecret)
|
||||
os.Setenv("GODADDY_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
|
||||
dnsProvider, err := godaddyProvider.NewDNSProvider()
|
||||
if err != nil {
|
||||
|
||||
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)
|
||||
}
|
||||
43
internal/applicant/huaweicloud.go
Normal file
43
internal/applicant/huaweicloud.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
huaweicloudProvider "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type huaweicloud struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewHuaweiCloud(option *ApplyOption) Applicant {
|
||||
return &huaweicloud{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *huaweicloud) Apply() (*Certificate, error) {
|
||||
access := &domain.HuaweiCloudAccess{}
|
||||
json.Unmarshal([]byte(t.option.Access), access)
|
||||
|
||||
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))
|
||||
|
||||
dnsProvider, err := huaweicloudProvider.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(t.option, dnsProvider)
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
namesiloProvider "github.com/go-acme/lego/v4/providers/dns/namesilo"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type namesilo struct {
|
||||
@@ -19,11 +21,11 @@ func NewNamesilo(option *ApplyOption) Applicant {
|
||||
}
|
||||
|
||||
func (a *namesilo) Apply() (*Certificate, error) {
|
||||
|
||||
access := &domain.NameSiloAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("NAMESILO_API_KEY", access.ApiKey)
|
||||
os.Setenv("NAMESILO_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
|
||||
dnsProvider, err := namesiloProvider.NewDNSProvider()
|
||||
if err != nil {
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type tencent struct {
|
||||
@@ -19,12 +21,13 @@ func NewTencent(option *ApplyOption) Applicant {
|
||||
}
|
||||
|
||||
func (t *tencent) Apply() (*Certificate, error) {
|
||||
|
||||
access := &domain.TencentAccess{}
|
||||
json.Unmarshal([]byte(t.option.Access), access)
|
||||
|
||||
os.Setenv("TENCENTCLOUD_SECRET_ID", access.SecretId)
|
||||
os.Setenv("TENCENTCLOUD_SECRET_KEY", access.SecretKey)
|
||||
os.Setenv("TENCENTCLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
|
||||
|
||||
dnsProvider, err := tencentcloud.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
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,206 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"certimate/internal/applicant"
|
||||
"certimate/internal/domain"
|
||||
"certimate/internal/utils/rand"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
cas20200407 "github.com/alibabacloud-go/cas-20200407/v2/client"
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
)
|
||||
|
||||
type aliyun struct {
|
||||
client *cas20200407.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 {
|
||||
|
||||
// 查询有没有对应的资源
|
||||
resource, err := a.resource()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.infos = append(a.infos, toStr("查询对应的资源", resource))
|
||||
|
||||
// 查询有没有对应的联系人
|
||||
contacts, err := a.contacts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.infos = append(a.infos, toStr("查询联系人", contacts))
|
||||
|
||||
// 上传证书
|
||||
certId, err := a.uploadCert(&a.option.Certificate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.infos = append(a.infos, toStr("上传证书", certId))
|
||||
|
||||
// 部署证书
|
||||
jobId, err := a.deploy(resource, certId, contacts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.infos = append(a.infos, toStr("创建部署证书任务", jobId))
|
||||
|
||||
// 等待部署成功
|
||||
err = a.updateDeployStatus(*jobId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 部署成功后删除旧的证书
|
||||
a.deleteCert(resource)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *aliyun) updateDeployStatus(jobId int64) error {
|
||||
// 查询部署状态
|
||||
req := &cas20200407.UpdateDeploymentJobStatusRequest{
|
||||
JobId: tea.Int64(jobId),
|
||||
}
|
||||
|
||||
resp, err := a.client.UpdateDeploymentJobStatus(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.infos = append(a.infos, toStr("查询对应的资源", resp))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *aliyun) deleteCert(resource *cas20200407.ListCloudResourcesResponseBodyData) error {
|
||||
// 查询有没有对应的资源
|
||||
if resource.CertId == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 删除证书
|
||||
_, err := a.client.DeleteUserCertificate(&cas20200407.DeleteUserCertificateRequest{
|
||||
CertId: resource.CertId,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *aliyun) contacts() ([]*cas20200407.ListContactResponseBodyContactList, error) {
|
||||
listContactRequest := &cas20200407.ListContactRequest{}
|
||||
runtime := &util.RuntimeOptions{}
|
||||
|
||||
resp, err := a.client.ListContactWithOptions(listContactRequest, runtime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Body.TotalCount == nil {
|
||||
return nil, errors.New("no contact found")
|
||||
}
|
||||
|
||||
return resp.Body.ContactList, nil
|
||||
}
|
||||
|
||||
func (a *aliyun) deploy(resource *cas20200407.ListCloudResourcesResponseBodyData, certId int64, contacts []*cas20200407.ListContactResponseBodyContactList) (*int64, error) {
|
||||
contactIds := make([]string, 0, len(contacts))
|
||||
for _, contact := range contacts {
|
||||
contactIds = append(contactIds, fmt.Sprintf("%d", *contact.ContactId))
|
||||
}
|
||||
// 部署证书
|
||||
createCloudResourceRequest := &cas20200407.CreateDeploymentJobRequest{
|
||||
CertIds: tea.String(fmt.Sprintf("%d", certId)),
|
||||
Name: tea.String(a.option.Domain + rand.RandStr(6)),
|
||||
JobType: tea.String("user"),
|
||||
ResourceIds: tea.String(fmt.Sprintf("%d", *resource.Id)),
|
||||
ContactIds: tea.String(strings.Join(contactIds, ",")),
|
||||
}
|
||||
runtime := &util.RuntimeOptions{}
|
||||
|
||||
resp, err := a.client.CreateDeploymentJobWithOptions(createCloudResourceRequest, runtime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Body.JobId, nil
|
||||
}
|
||||
|
||||
func (a *aliyun) uploadCert(cert *applicant.Certificate) (int64, error) {
|
||||
uploadUserCertificateRequest := &cas20200407.UploadUserCertificateRequest{
|
||||
Cert: &cert.Certificate,
|
||||
Key: &cert.PrivateKey,
|
||||
Name: tea.String(a.option.Domain + rand.RandStr(6)),
|
||||
}
|
||||
runtime := &util.RuntimeOptions{}
|
||||
|
||||
resp, err := a.client.UploadUserCertificateWithOptions(uploadUserCertificateRequest, runtime)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return *resp.Body.CertId, nil
|
||||
}
|
||||
|
||||
func (a *aliyun) createClient(accessKeyId, accessKeySecret string) (_result *cas20200407.Client, _err error) {
|
||||
config := &openapi.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
config.Endpoint = tea.String("cas.aliyuncs.com")
|
||||
_result = &cas20200407.Client{}
|
||||
_result, _err = cas20200407.NewClient(config)
|
||||
return _result, _err
|
||||
}
|
||||
|
||||
func (a *aliyun) resource() (*cas20200407.ListCloudResourcesResponseBodyData, error) {
|
||||
|
||||
listCloudResourcesRequest := &cas20200407.ListCloudResourcesRequest{
|
||||
CloudProduct: tea.String(a.option.Product),
|
||||
Keyword: tea.String(a.option.Domain),
|
||||
}
|
||||
|
||||
resp, err := a.client.ListCloudResources(listCloudResourcesRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if *resp.Body.Total == 0 {
|
||||
return nil, errors.New("no resource found")
|
||||
}
|
||||
|
||||
return resp.Body.Data[0], 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
|
||||
}
|
||||
@@ -1,81 +1,88 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"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"
|
||||
|
||||
"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", a.option.Domain, a.option.DomainId)
|
||||
setCdnDomainSSLCertificateRequest := &cdn20180510.SetCdnDomainSSLCertificateRequest{
|
||||
DomainName: tea.String(a.option.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,86 +0,0 @@
|
||||
/*
|
||||
* @Author: Bin
|
||||
* @Date: 2024-09-17
|
||||
* @FilePath: /certimate/internal/deployer/aliyun_esa.go
|
||||
*/
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"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"
|
||||
)
|
||||
|
||||
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", a.option.Domain, a.option.DomainId)
|
||||
setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{
|
||||
DomainName: tea.String(a.option.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
|
||||
}
|
||||
@@ -1,109 +1,101 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"certimate/internal/applicant"
|
||||
"certimate/internal/utils/app"
|
||||
"certimate/internal/utils/variables"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"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"`
|
||||
}
|
||||
|
||||
type Deployer interface {
|
||||
Deploy(ctx context.Context) error
|
||||
GetInfo() []string
|
||||
GetInfos() []string
|
||||
GetID() string
|
||||
}
|
||||
|
||||
func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error) {
|
||||
rs := make([]Deployer, 0)
|
||||
|
||||
if record.GetString("targetAccess") != "" {
|
||||
singleDeployer, err := Get(record, cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rs = append(rs, singleDeployer)
|
||||
if record.GetString("deployConfig") == "" {
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
if record.GetString("group") != "" {
|
||||
group := record.ExpandedOne("group")
|
||||
|
||||
if errs := app.GetApp().Dao().ExpandRecord(group, []string{"access"}, nil); len(errs) > 0 {
|
||||
|
||||
errList := make([]error, 0)
|
||||
for name, err := range errs {
|
||||
errList = append(errList, fmt.Errorf("展开记录失败,%s: %w", name, err))
|
||||
}
|
||||
err := errors.Join(errList...)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records := group.ExpandedAll("access")
|
||||
|
||||
deployers, err := getByGroup(record, cert, records...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rs = append(rs, deployers...)
|
||||
deployConfigs := make([]domain.DeployConfig, 0)
|
||||
|
||||
err := record.UnmarshalJSONField("deployConfig", &deployConfigs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析部署配置失败: %w", err)
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
if len(deployConfigs) == 0 {
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func getByGroup(record *models.Record, cert *applicant.Certificate, accesses ...*models.Record) ([]Deployer, error) {
|
||||
|
||||
rs := make([]Deployer, 0)
|
||||
|
||||
for _, access := range accesses {
|
||||
deployer, err := getWithAccess(record, cert, access)
|
||||
for _, deployConfig := range deployConfigs {
|
||||
deployer, err := getWithDeployConfig(record, cert, deployConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rs = append(rs, deployer)
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
|
||||
}
|
||||
|
||||
func getWithAccess(record *models.Record, cert *applicant.Certificate, access *models.Record) (Deployer, error) {
|
||||
func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, deployConfig domain.DeployConfig) (Deployer, error) {
|
||||
access, err := app.GetApp().Dao().FindRecordById("access", deployConfig.Access)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("access record not found: %w", err)
|
||||
}
|
||||
|
||||
option := &DeployerOption{
|
||||
DomainId: record.Id,
|
||||
Domain: record.GetString("domain"),
|
||||
Product: getProduct(record),
|
||||
Access: access.GetString("config"),
|
||||
AceessRecord: access,
|
||||
Variables: variables.Parse2Map(record.GetString("variables")),
|
||||
AccessRecord: access,
|
||||
DeployConfig: deployConfig,
|
||||
}
|
||||
if cert != nil {
|
||||
option.Certificate = *cert
|
||||
@@ -114,42 +106,55 @@ func getWithAccess(record *models.Record, cert *applicant.Certificate, access *m
|
||||
}
|
||||
}
|
||||
|
||||
switch record.GetString("targetType") {
|
||||
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)
|
||||
switch deployConfig.Type {
|
||||
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 Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
|
||||
|
||||
access := record.ExpandedOne("targetAccess")
|
||||
|
||||
return getWithAccess(record, cert, access)
|
||||
}
|
||||
|
||||
func getProduct(record *models.Record) string {
|
||||
targetType := record.GetString("targetType")
|
||||
rs := strings.Split(targetType, "-")
|
||||
if len(rs) < 2 {
|
||||
return ""
|
||||
}
|
||||
return rs[1]
|
||||
return nil, errors.New("unsupported deploy target")
|
||||
}
|
||||
|
||||
func toStr(tag string, data any) string {
|
||||
|
||||
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,106 +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 {
|
||||
Command string `json:"command"`
|
||||
CertPath string `json:"certPath"`
|
||||
KeyPath string `json:"keyPath"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// 复制文件
|
||||
if err := copyFile(l.option.Certificate.Certificate, access.CertPath); err != nil {
|
||||
return fmt.Errorf("复制证书失败: %w", err)
|
||||
func (d *LocalDeployer) Deploy(ctx context.Context) error {
|
||||
// 执行前置命令
|
||||
preCommand := d.option.DeployConfig.GetConfigAsString("preCommand")
|
||||
if preCommand != "" {
|
||||
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.PrivateKey, access.KeyPath); 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
|
||||
}
|
||||
|
||||
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(access.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"
|
||||
"certimate/internal/domain"
|
||||
xhttp "certimate/internal/utils/http"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/qiniu/go-sdk/v7/auth"
|
||||
)
|
||||
|
||||
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", q.option.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", q.option.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: q.option.Domain,
|
||||
CommonName: q.option.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", q.option.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,86 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"certimate/internal/applicant"
|
||||
"testing"
|
||||
|
||||
"github.com/qiniu/go-sdk/v7/auth"
|
||||
)
|
||||
|
||||
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,163 +4,206 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
xpath "path"
|
||||
"strings"
|
||||
"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"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Key string `json:"key"`
|
||||
Port string `json:"port"`
|
||||
PreCommand string `json:"preCommand"`
|
||||
Command string `json:"command"`
|
||||
CertPath string `json:"certPath"`
|
||||
KeyPath string `json:"keyPath"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 将证书路径和命令中的变量替换为实际值
|
||||
for k, v := range s.option.Variables {
|
||||
key := fmt.Sprintf("${%s}", k)
|
||||
access.CertPath = strings.ReplaceAll(access.CertPath, key, v)
|
||||
access.KeyPath = strings.ReplaceAll(access.KeyPath, key, v)
|
||||
access.Command = strings.ReplaceAll(access.Command, key, v)
|
||||
access.PreCommand = strings.ReplaceAll(access.PreCommand, key, v)
|
||||
}
|
||||
|
||||
// 连接
|
||||
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))
|
||||
|
||||
// 执行前置命令
|
||||
if access.PreCommand != "" {
|
||||
err, stdout, stderr := s.sshExecCommand(client, access.PreCommand)
|
||||
preCommand := d.option.DeployConfig.GetConfigAsString("preCommand")
|
||||
if 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, access.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, access.KeyPath); err != nil {
|
||||
return fmt.Errorf("failed to upload private key: %w", err)
|
||||
}
|
||||
|
||||
s.infos = append(s.infos, toStr("ssh上传私钥成功", nil))
|
||||
|
||||
// 执行命令
|
||||
err, stdout, stderr := s.sshExecCommand(client, access.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) (error, string, string) {
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ssh session: %w", err), "", ""
|
||||
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 "", "", xerrors.Wrap(err, "failed to create ssh session")
|
||||
}
|
||||
|
||||
defer session.Close()
|
||||
var stdoutBuf bytes.Buffer
|
||||
session.Stdout = &stdoutBuf
|
||||
var stderrBuf bytes.Buffer
|
||||
session.Stderr = &stderrBuf
|
||||
err = session.Run(command)
|
||||
return err, stdoutBuf.String(), stderrBuf.String()
|
||||
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 {
|
||||
func (d *SSHDeployer) writeSftpFileString(sshCli *ssh.Client, path string, content string) error {
|
||||
return d.writeSftpFile(sshCli, path, []byte(content))
|
||||
}
|
||||
|
||||
sftpCli, err := sftp.NewClient(client)
|
||||
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 != "" {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@ import (
|
||||
|
||||
func TestPath(t *testing.T) {
|
||||
dir := path.Dir("./a/b/c")
|
||||
os.MkdirAll(dir, 0755)
|
||||
os.MkdirAll(dir, 0o755)
|
||||
}
|
||||
|
||||
@@ -1,108 +1,194 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"certimate/internal/utils/rand"
|
||||
"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"
|
||||
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"
|
||||
|
||||
"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) {
|
||||
func (d *TencentCDNDeployer) createSdkClients(secretId, secretKey string) (*tencentCDNDeployerSdkClients, error) {
|
||||
credential := common.NewCredential(secretId, secretKey)
|
||||
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
|
||||
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(true)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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.InstanceIdList = common.StringPtrs([]string{t.option.Domain})
|
||||
request.ResourceType = common.StringPtr("cdn")
|
||||
request.Status = common.Int64Ptr(1)
|
||||
|
||||
// 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
|
||||
resp, err := client.DeployCertificateInstance(request)
|
||||
|
||||
cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy certificate: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
t.infos = append(t.infos, toStr("部署证书", resp.Response))
|
||||
return nil
|
||||
|
||||
return &tencentCDNDeployerSdkClients{
|
||||
ssl: sslClient,
|
||||
cdn: cdnClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -2,66 +2,65 @@ package deployer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
xhttp "certimate/internal/utils/http"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/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 hookData struct {
|
||||
Domain string `json:"domain"`
|
||||
Certificate string `json:"certificate"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
}
|
||||
|
||||
type webhook struct {
|
||||
type WebhookDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewWebhook(option *DeployerOption) (Deployer, error) {
|
||||
|
||||
return &webhook{
|
||||
func NewWebhookDeployer(option *DeployerOption) (Deployer, error) {
|
||||
return &WebhookDeployer{
|
||||
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 (d *WebhookDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (w *webhook) GetInfo() []string {
|
||||
return w.infos
|
||||
func (d *WebhookDeployer) GetInfos() []string {
|
||||
return d.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)
|
||||
type webhookData struct {
|
||||
Domain string `json:"domain"`
|
||||
Certificate string `json:"certificate"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
|
||||
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,
|
||||
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,11 +5,34 @@ 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 {
|
||||
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 {
|
||||
DnsApiToken string `json:"dnsApiToken"`
|
||||
}
|
||||
@@ -19,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"`
|
||||
}
|
||||
@@ -28,3 +56,43 @@ type GodaddyAccess struct {
|
||||
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
|
||||
}
|
||||
144
internal/domain/domains.go
Normal file
144
internal/domain/domains.go
Normal file
@@ -0,0 +1,144 @@
|
||||
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"`
|
||||
DisableFollowCNAME bool `json:"disableFollowCNAME"`
|
||||
}
|
||||
|
||||
type DeployConfig struct {
|
||||
Id string `json:"id"`
|
||||
Access string `json:"access"`
|
||||
Type string `json:"type"`
|
||||
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
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"certimate/internal/applicant"
|
||||
"certimate/internal/deployer"
|
||||
"certimate/internal/utils/app"
|
||||
"context"
|
||||
"errors"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"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 {
|
||||
@@ -41,23 +51,14 @@ func deploy(ctx context.Context, record *models.Record) error {
|
||||
return err
|
||||
}
|
||||
history.record(checkPhase, "获取记录成功", nil)
|
||||
if errs := app.GetApp().Dao().ExpandRecord(currRecord, []string{"access", "targetAccess", "group"}, nil); len(errs) > 0 {
|
||||
|
||||
errList := make([]error, 0)
|
||||
for name, err := range errs {
|
||||
errList = append(errList, fmt.Errorf("展开记录失败,%s: %w", name, err))
|
||||
}
|
||||
err = errors.Join(errList...)
|
||||
app.GetApp().Logger().Error("展开记录失败", "err", err)
|
||||
history.record(checkPhase, "获取授权信息失败", &RecordInfo{Err: err})
|
||||
return err
|
||||
}
|
||||
history.record(checkPhase, "获取授权信息成功", nil)
|
||||
|
||||
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"))},
|
||||
@@ -72,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"))},
|
||||
})
|
||||
@@ -106,15 +107,22 @@ func deploy(ctx context.Context, record *models.Record) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 没有部署配置,也算成功
|
||||
if len(deployers) == 0 {
|
||||
history.record(deployPhase, "没有部署配置", &RecordInfo{Info: []string{"没有部署配置"}})
|
||||
history.setWholeSuccess(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, deployer := range deployers {
|
||||
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)
|
||||
|
||||
}
|
||||
@@ -126,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
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"certimate/internal/utils/app"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
func create(ctx context.Context, record *models.Record) error {
|
||||
@@ -19,7 +20,6 @@ func create(ctx context.Context, record *models.Record) error {
|
||||
app.GetApp().Logger().Error("deploy failed", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
scheduler := app.GetScheduler()
|
||||
@@ -27,7 +27,6 @@ func create(ctx context.Context, record *models.Record) error {
|
||||
err := scheduler.Add(record.Id, record.GetString("crontab"), func() {
|
||||
deploy(ctx, record)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
app.GetApp().Logger().Error("add cron job failed", "err", err)
|
||||
return fmt.Errorf("add cron job failed: %w", err)
|
||||
@@ -46,7 +45,6 @@ func update(ctx context.Context, record *models.Record) error {
|
||||
}
|
||||
|
||||
if record.GetBool("rightnow") {
|
||||
|
||||
go func() {
|
||||
if err := deploy(ctx, record); err != nil {
|
||||
app.GetApp().Logger().Error("deploy failed", "err", err)
|
||||
@@ -57,7 +55,6 @@ func update(ctx context.Context, record *models.Record) error {
|
||||
err := scheduler.Add(record.Id, record.GetString("crontab"), func() {
|
||||
deploy(ctx, record)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
app.GetApp().Logger().Error("update cron job failed", "err", err)
|
||||
return fmt.Errorf("update cron job failed: %w", err)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"certimate/internal/utils/app"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
const tableName = "domains"
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"certimate/internal/applicant"
|
||||
"certimate/internal/utils/app"
|
||||
"certimate/internal/utils/xtime"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"github.com/usual2970/certimate/internal/applicant"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
"github.com/usual2970/certimate/internal/utils/xtime"
|
||||
)
|
||||
|
||||
type historyItem struct {
|
||||
@@ -62,7 +63,6 @@ func (a *history) record(phase Phase, msg string, info *RecordInfo, pass ...bool
|
||||
Info: info.Info,
|
||||
Time: xtime.BeijingTimeStr(),
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (a *history) setCert(cert *applicant.Certificate) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"certimate/internal/notify"
|
||||
"certimate/internal/utils/app"
|
||||
"context"
|
||||
|
||||
"github.com/usual2970/certimate/internal/notify"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
func InitSchedule() {
|
||||
@@ -34,5 +35,4 @@ func InitSchedule() {
|
||||
// 启动定时任务
|
||||
app.GetScheduler().Start()
|
||||
app.GetApp().Logger().Info("定时任务启动成功", "total", app.GetScheduler().Total())
|
||||
|
||||
}
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"certimate/internal/utils/app"
|
||||
"certimate/internal/utils/xtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"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 {
|
||||
@@ -33,15 +28,14 @@ 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type notifyTemplates struct {
|
||||
@@ -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,18 +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)
|
||||
}
|
||||
@@ -1,29 +1,18 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"certimate/internal/utils/app"
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
notifyPackage "github.com/nikoksr/notify"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/nikoksr/notify/service/dingding"
|
||||
|
||||
"github.com/nikoksr/notify/service/telegram"
|
||||
|
||||
"github.com/nikoksr/notify/service/http"
|
||||
"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
|
||||
}
|
||||
@@ -31,100 +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) {
|
||||
func SendToChannel(subject, message string, channel string, channelConfig map[string]any) error {
|
||||
notifier, err := createNotifier(channel, channelConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='notifyChannels'")
|
||||
_, 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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user