Compare commits

...

267 Commits

Author SHA1 Message Date
Yoan.liu
9d36df211e Update version.ts 2024-12-11 09:07:44 +08:00
Yoan.liu
cec56b6e49 Merge pull request #372 from fudiwei/bugfix/huaweicloud-cdn-deploy-panic
bugfix #361
2024-12-11 09:06:26 +08:00
Fu Diwei
dfd2fccc1e fix: #361 2024-12-10 19:19:40 +08:00
yoan
4ffb7ae969 update version 2024-11-24 21:00:57 +08:00
yoan
302ddcdd07 update version 2024-11-24 21:00:21 +08:00
Yoan.liu
65df759275 Merge pull request #348 from fudiwei/feat/deployer
feat: deployers
2024-11-22 11:14:24 +08:00
Fu Diwei
a842b6b925 fix: illegal arguments 2024-11-21 20:23:01 +08:00
Fu Diwei
4916757d59 feat: add Deployer factory 2024-11-21 11:23:15 +08:00
Fu Diwei
30b66adc3b refactor: replace Append* to Log* in DeployerLogger 2024-11-21 10:35:45 +08:00
Fu Diwei
13582d1a7b test: add unit test cases 2024-11-21 10:29:04 +08:00
Fu Diwei
0b9312b549 feat: implement more Deployer 2024-11-20 23:51:26 +08:00
Fu Diwei
bde51d8d38 feat: implement more Deployer 2024-11-20 22:58:01 +08:00
Fu Diwei
643a666853 feat: implement more Deployer 2024-11-20 21:02:29 +08:00
Fu Diwei
a59184ae5f fix: update GetValueOrDefault util functions to return default value for zero values 2024-11-20 07:49:50 +08:00
Fu Diwei
82807fcc1b refactor: clean code 2024-11-19 22:43:15 +08:00
Fu Diwei
a6c93ef9b8 test: fix typo 2024-11-19 22:11:47 +08:00
Fu Diwei
6a151865f7 feat: implement k8s secret Deployer 2024-11-19 22:04:00 +08:00
Fu Diwei
414d8d140e test: use flag arguments in test cases for Notifier and Deployer 2024-11-19 21:18:36 +08:00
Fu Diwei
51fb9dca58 test: add some unit test cases for new Deployer 2024-11-19 20:03:51 +08:00
Fu Diwei
6367785b1b feat: implement local, ssh, webhook Deployer 2024-11-19 19:09:48 +08:00
Fu Diwei
aa7fb7da06 Merge branch 'main' into feat/deployer 2024-11-19 09:09:38 +08:00
Fu Diwei
26d11de249 feat: add deployer interface 2024-11-19 09:08:49 +08:00
yoan
0daa9f1882 v0.2.21 2024-11-19 09:07:08 +08:00
yoan
56886dcfe9 Merge branch 'LeoChen98-fix-reapply-when-domain-list-changed' 2024-11-18 20:03:16 +08:00
yoan
81e1e4a7ff validity duration 2024-11-18 20:03:11 +08:00
yoan
9b5256716f Merge branch 'fix-reapply-when-domain-list-changed' of github.com:LeoChen98/certimate into LeoChen98-fix-reapply-when-domain-list-changed 2024-11-18 19:58:36 +08:00
usual2970
446bf80f1d Merge pull request #346 from jarod/main
feat: add deployer BytePlus CDN
2024-11-18 19:43:58 +08:00
Jarod Liu
6a80455c6c fix: byteplus access provider 2024-11-18 10:51:51 +08:00
Fu Diwei
43b2ff7957 refactor: extract x509 transformer utils 2024-11-18 09:12:15 +08:00
Fu Diwei
295b7779ee refactor: clean code 2024-11-18 09:10:28 +08:00
Jarod Liu
d1df088662 fix: 补充Provider Access 的 UI 实现 2024-11-16 09:52:28 +08:00
Jarod Liu
2b0f7aaf8a feat: add deployer BytePlus CDN 2024-11-16 09:18:58 +08:00
Leo Chen
3265dd76ab edit comments for the forward changes 2024-11-15 20:45:08 +08:00
Leo Chen
d1d7b44303 Invert the changed logic to match the function name 2024-11-15 20:37:36 +08:00
Leo Chen
56eced3813 Invert the boolean value to match the function name 2024-11-15 20:36:47 +08:00
yoan
c853f2976f v0.2.20 2024-11-15 08:07:37 +08:00
usual2970
b66931003f Merge pull request #342 from belier-cn/volcengine-cdn
feat: add volcengine cdn deployer
2024-11-15 08:05:27 +08:00
Leo Chen
9a75d2ac8f add key algorithm check 2024-11-15 00:33:09 +08:00
belier
42c5aea3f7 docs: update README_EN.md 2024-11-14 14:28:39 +08:00
belier
e2fd9c4cee style: modify variable name 2024-11-14 14:28:35 +08:00
belier
f847b7ff62 improvement: improve certificate fingerprint comparison 2024-11-14 14:19:00 +08:00
belier
9eae8f5077 feat: add volcengine cdn deployer 2024-11-14 13:39:23 +08:00
usual2970
2bacf76664 Merge pull request #339 from belier-cn/main
feat: add volcengine dns provider and add volcengine live deployer
2024-11-14 09:42:26 +08:00
usual2970
b2030caedc Merge pull request #337 from fudiwei/bugfix/syntax-error
fix switch-case syntax error
2024-11-14 09:35:58 +08:00
usual2970
956c975c6d Merge pull request #333 from JiangJamm/feat/notify_setting_expand
feat: 使系统设置中的消息推送设置列表打开后能够关闭
2024-11-14 09:34:52 +08:00
Leo Chen
41bd321a4f fixed: not reapply when domain list changed
fixed #334
2024-11-13 18:52:29 +08:00
Leo Chen
952e9687d0 fix misspelling var name 2024-11-13 17:58:56 +08:00
belier
c298f8b952 docs: Add Volcengine Information to README.md 2024-11-13 16:18:04 +08:00
belier
e2562a5251 feat: add volcengine dns provider and add volcengine live deployer 2024-11-13 15:36:46 +08:00
Fu Diwei
dbdb40baf9 fix: fix switch-case syntax error 2024-11-13 13:44:44 +08:00
yoan
2ff923dd1b v0.2.19 2024-11-13 08:16:19 +08:00
usual2970
f4f13f91f2 Merge pull request #331 from fudiwei/bugfix/qiniu-wildcard-domain
bugfix #330
2024-11-13 08:14:32 +08:00
usual2970
034aa980e6 Merge pull request #329 from fudiwei/bugfix/aliyun-clb-deploy-error
bugfix #326
2024-11-13 08:14:19 +08:00
usual2970
6ac7a51ce0 Merge pull request #328 from fudiwei/bugfix/tencentcloud-deploy-config-not-saving
bugfix #324
2024-11-13 08:14:05 +08:00
usual2970
cf0c0e3e2c Merge pull request #327 from LeoChen98/fix-tencent-cos-instance-not-found
fixed: instance not found when deploying tencent COS
2024-11-13 08:13:49 +08:00
JiangJamm
1b899575e0 feat: 使系统设置中的消息推送设置列表打开后能够关闭 2024-11-13 01:10:26 +08:00
Fu Diwei
23e5cb5669 fix: #330 2024-11-12 21:41:06 +08:00
Fu Diwei
e4ba4c9b37 fix: #326 2024-11-12 20:35:31 +08:00
Fu Diwei
9ed64bdc9a fix: #324 2024-11-12 20:20:54 +08:00
Leo Chen
e9b6fb55ff fixed: instance possible not found when deploying tencent CLB via SSL api
修复了重构导致腾讯云CLB通过SSL接口部署时可能找不到实例的bug
2024-11-12 17:59:13 +08:00
Leo Chen
80caf881ae fixed: instance not found when deploying tencent COS
修复了重构导致腾讯云COS部署时找不到实例的bug
2024-11-12 17:56:41 +08:00
usual2970
c36db3545f Merge pull request #321 from fudiwei/feat/notifier
feat: notifiers
2024-11-11 18:16:30 +08:00
yoan
a367585ab4 v0.2.18 2024-11-11 07:58:13 +08:00
Fu Diwei
2994cb5c65 test: add unit test case for email notifier 2024-11-10 20:28:01 +08:00
Fu Diwei
1bedb31a3c fix: fix typo 2024-11-10 20:06:18 +08:00
Fu Diwei
8fecebc254 feat: show loading button when pushing test notifications 2024-11-10 20:00:19 +08:00
Fu Diwei
44497a0969 feat: new UI for notify settings 2024-11-10 19:52:50 +08:00
usual2970
5362371bda Merge pull request #319 from fudiwei/bugfix/aliyun-api-error
bugfix #318
2024-11-10 19:40:40 +08:00
Fu Diwei
8b04e96a7d feat: new UI for email notify settings 2024-11-10 18:21:43 +08:00
Fu Diwei
5d93334426 refactor: re-implement logic of notify 2024-11-10 18:03:20 +08:00
Fu Diwei
150b666d4b refactor: maps utils 2024-11-09 20:46:49 +08:00
Fu Diwei
94579d65c4 refactor: clean code 2024-11-09 20:29:13 +08:00
Fu Diwei
551b06b4e8 feat: notifier 2024-11-09 20:06:22 +08:00
Fu Diwei
76fc47a274 Merge branch 'main' into feat/notifier 2024-11-09 12:14:21 +08:00
yoan
35e1bfcd7f Update readme 2024-11-09 11:37:34 +08:00
Fu Diwei
24df7913fe feat: support aliyun global ALB/NLB 2024-11-09 09:54:49 +08:00
Fu Diwei
83674e4b35 refactor: ensure compile-time check for Uploader implementations 2024-11-09 09:47:14 +08:00
Fu Diwei
22d3aeb7b5 fix: #318 2024-11-09 09:41:05 +08:00
yoan
cf005711c0 v0.2.17 2024-11-08 08:11:04 +08:00
usual2970
0a00d0c52f Merge pull request #314 from fudiwei/bugfix/dogecloud-api-error
bugfix #313
2024-11-08 08:10:18 +08:00
usual2970
9aa17a0395 Merge pull request #315 from fudiwei/bugfix/qiniu-panic
bugfix #304
2024-11-08 08:09:41 +08:00
Fu Diwei
65ecdf7dc2 update README 2024-11-07 17:36:41 +08:00
Fu Diwei
0dfa5994cc fix: #304 2024-11-07 17:35:43 +08:00
Fu Diwei
5d2844fdb6 fix: #313 2024-11-07 15:01:46 +08:00
yoan
44332b9d07 v0.2.16 2024-11-07 08:09:25 +08:00
usual2970
20a23e148c Merge pull request #309 from fudiwei/bugfix/dogecloud-api-error
bugfix #308
2024-11-07 08:06:55 +08:00
RHQYZ
0bcb6206f4 fix #308 2024-11-06 11:07:24 +08:00
yoan
943b9827ee v0.2.15 2024-11-06 07:12:48 +08:00
usual2970
741f3ec212 Merge pull request #306 from fudiwei/bugfix/dogecloud-api-error
bugfix #303
2024-11-06 07:08:12 +08:00
Fu Diwei
8549a17675 fix: #303 2024-11-05 18:16:21 +08:00
yoan
718cfccbea resolve new sftp client failure 2024-11-05 08:35:37 +08:00
yoan
2458fa26d8 v0.2.14 2024-11-05 08:30:28 +08:00
yoan
ac24684d2b Merge branch 'main' of github.com:usual2970/certimate 2024-11-05 08:29:58 +08:00
yoan
106dbd9538 Merge branch 'fudiwei-feat/cloud-cdn' 2024-11-05 08:29:30 +08:00
yoan
f9efb2b800 migration 2024-11-05 08:28:35 +08:00
usual2970
897d124d5b Merge pull request #299 from fudiwei/bugfix/ssh-jks
bugfix #298
2024-11-05 08:15:13 +08:00
Fu Diwei
34daf9ccac refactor: clean code 2024-11-04 12:54:23 +08:00
Fu Diwei
269a97e81e feat: add baiducloud cdn deployer 2024-11-04 12:44:53 +08:00
Fu Diwei
2fd57621d8 fix: #298 2024-11-04 11:20:35 +08:00
Fu Diwei
76de837214 feat: add baiducloud provider 2024-11-04 11:11:00 +08:00
Fu Diwei
1e41020728 feat: add dogecloud cdn deployer 2024-11-04 10:34:05 +08:00
Fu Diwei
8a78e49bf0 feat: add dogecloud provider 2024-11-04 10:30:18 +08:00
yoan
e6726e4c02 v0.2.13 2024-11-04 08:07:05 +08:00
yoan
76330a4a1a v0.2.12 2024-11-04 07:49:00 +08:00
usual2970
7e5f0097e4 Merge pull request #296 from usual2970/hotfix/email
fix: resolve email notification delivery failure
2024-11-02 13:09:37 +08:00
yoan
18e1c02d1c fix: resolve email notification delivery failure 2024-11-02 10:17:16 +08:00
usual2970
28992f178e Merge pull request #294 from funnyzak/bark_notify
feat: add Bark notifier
2024-11-02 09:52:13 +08:00
usual2970
c41f34c352 Merge pull request #276 from fudiwei/feat/cloud-load-balance
feat: tencent clb deployer
2024-11-02 09:46:42 +08:00
Fu Diwei
6b5580a30c refactor: clean code 2024-11-01 15:56:22 +08:00
Fu Diwei
1dee14e32d refactor: adjust project structure 2024-11-01 15:54:05 +08:00
Fu Diwei
1e3c4881d0 refactor: remove unused certificate name in TencentCloudSSLUploader 2024-11-01 15:33:02 +08:00
Leon
657964cda4 feat: add Bark notification channel and related settings 2024-11-01 11:35:09 +08:00
Fu Diwei
893aac916c feat(ui): show deploy provider name rather than access provider name in DeployList 2024-10-31 20:25:06 +08:00
Fu Diwei
68da6cf3ae fix: fix import cycle 2024-10-31 20:03:04 +08:00
Fu Diwei
0d96ea9eef refactor: deprecate internal/deployer/deployer.getDeployVariables 2024-10-31 19:59:21 +08:00
Fu Diwei
0ceb44a7cd refactor: deprecate internal/utils/rand.RandStr 2024-10-31 19:53:48 +08:00
Fu Diwei
4fec0036cb refactor: fix typo 2024-10-31 18:25:22 +08:00
Fu Diwei
f82eee4636 refactor: clean code 2024-10-31 14:30:16 +08:00
Fu Diwei
260cfb96ec refactor(ui): declare deploy config params 2024-10-31 14:27:11 +08:00
Fu Diwei
f71a519674 refactor: clean code 2024-10-31 13:41:21 +08:00
Fu Diwei
369c146eca feat: support tencent clb deployment in multiple ways 2024-10-31 13:24:43 +08:00
Fu Diwei
83264a6946 refactor: clean code 2024-10-31 11:37:16 +08:00
Fu Diwei
3c3d4e9109 refactor: extend qiniu sdk 2024-10-31 11:37:03 +08:00
Fu Diwei
ce55365292 refactor: extend huaweicloud cdn sdk 2024-10-31 10:14:27 +08:00
Fu Diwei
be495839b6 Merge branch 'main' into feat/cloud-load-balance 2024-10-31 09:14:57 +08:00
usual2970
a27a9f55a7 Merge pull request #284 from usual2970/feat/ui-1030
Fix the issue where long domain names or titles overlap the next column.
2024-10-31 08:15:22 +08:00
usual2970
10e14caf35 Merge pull request #285 from LeoChen98/fix-tencent-cos-locales-loss
fix: tencent cos ui locales loss
2024-10-31 08:15:04 +08:00
Fu Diwei
59af246479 refactor: clean code 2024-10-30 19:37:44 +08:00
Leo Chen
1f52eaca01 fix: tencent cos ui locales loss 2024-10-30 17:09:11 +08:00
yoan
d833f4b5ff fix cos region validate 2024-10-30 16:08:32 +08:00
yoan
bfee39049d Merge branch 'LeoChen98-feat-add-netsh-preset' 2024-10-30 12:29:07 +08:00
yoan
b4599df6c6 code format 2024-10-30 12:28:59 +08:00
yoan
261c6f6956 Merge branch 'feat-add-netsh-preset' of github.com:LeoChen98/certimate into LeoChen98-feat-add-netsh-preset 2024-10-30 12:26:06 +08:00
yoan
b97d77c848 Fix the issue where long domain names or titles overlap the next column. 2024-10-30 11:57:16 +08:00
yoan
c1cefe0e7f v0.2.11 2024-10-30 11:07:59 +08:00
yoan
55b77fdf5c Fix the issue where the deployment type could not be selected 2024-10-30 11:03:41 +08:00
yoan
16967c4ab1 fix tencent cdn deploy 2024-10-30 09:31:51 +08:00
yoan
61a4fd8657 v0.2.10 2024-10-30 07:04:05 +08:00
Leo Chen
67ca7e3097 feat: add netsh preset
新增本地Windows下使用netsh绑定证书的预设
2024-10-29 21:43:20 +08:00
Fu Diwei
26fa8e75bd refactor: clean code 2024-10-29 21:32:48 +08:00
Fu Diwei
aeaa45b713 Merge branch 'main' into feat/cloud-load-balance 2024-10-29 09:12:39 +08:00
yoan
edeac86f06 Merge branch 'fudiwei-feat/multiple-certificate-formats' 2024-10-29 08:46:06 +08:00
yoan
4e0c23165f fix conflict 2024-10-29 08:45:51 +08:00
usual2970
feb851a3fc Merge pull request #273 from LeoChen98/enhance-tencent-cdn-dupe-deploy
enhance: resolve error on tencent cdn dupe deployment
2024-10-29 08:39:57 +08:00
usual2970
3103d60508 Merge pull request #274 from PittyXu/feat/k8s
fix: k8s部署更新报错
2024-10-29 08:39:15 +08:00
usual2970
53be6b5f5b Merge pull request #272 from LeoChen98/feat-add-mail-push
feat: add mail push
2024-10-29 08:38:10 +08:00
usual2970
9d3e0d1090 Merge pull request #278 from usual2970/feat/searchable_select
feat: Searchable when selecting authorization type
2024-10-29 08:37:53 +08:00
yoan
f8aef129cf Searchable when selecting authorization type 2024-10-28 22:52:25 +08:00
Leo Chen
c419b2c8b4 use slice pkg 2024-10-28 20:28:13 +08:00
Fu Diwei
e1a3a3e7c7 refactor: clean code 2024-10-28 14:15:33 +08:00
Fu Diwei
b47a1a13cb feat: support jks format 2024-10-28 11:49:44 +08:00
徐雪君
3397f424bc fix: k8s部署更新报错 #266 2024-10-28 11:15:08 +08:00
yoan
48672d1a44 v0.2.9 2024-10-28 08:48:30 +08:00
Leo Chen
38dc8a63d9 enhance: resolve error on tencent cdn dupe deployment
优化:腾讯云cdn重复部署报错的问题
2024-10-27 23:48:52 +08:00
Fu Diwei
009e8fb976 feat: preset scripts on deployment to local 2024-10-27 21:10:19 +08:00
Fu Diwei
6d7a91f49b refactor: clean code 2024-10-27 20:44:38 +08:00
yoan
9d4d14db06 Update README.md 2024-10-27 20:42:47 +08:00
Leo Chen
c9f347f77a fix mail push onchange 2024-10-27 20:27:46 +08:00
Leo Chen
0396d8222e feat: add mail push
新增电子邮箱推送
2024-10-27 20:21:34 +08:00
Fu Diwei
305f3de50f Merge branch 'main' into feat/multiple-certificate-formats 2024-10-27 20:17:04 +08:00
yoan
ffacfe0f42 Merge branch 'LeoChen98-feat-serverchan-push-tube' 2024-10-27 09:18:46 +08:00
yoan
be9e66c7d3 Merge branch 'feat-serverchan-push-tube' of github.com:LeoChen98/certimate into LeoChen98-feat-serverchan-push-tube 2024-10-27 09:15:12 +08:00
yoan
1238508bdb Merge branch 'fudiwei-feat/cloud-load-balance' 2024-10-27 09:12:05 +08:00
yoan
1ab5c4035a fix conflict 2024-10-27 09:10:12 +08:00
yoan
67fa9d91bf Merge branch 'PittyXu-feat/k8s' 2024-10-27 08:38:44 +08:00
yoan
dc5f9abf20 detail ajustments 2024-10-27 08:37:42 +08:00
yoan
7240a42fbc Merge branch 'feat/k8s' of github.com:PittyXu/certimate into PittyXu-feat/k8s 2024-10-27 08:35:36 +08:00
yoan
6fbb6d4992 Merge branch 'LeoChen98-feat-tecent-ecdn-teo-deploy' 2024-10-27 08:33:00 +08:00
yoan
86838f305b detail ajustments 2024-10-27 08:32:48 +08:00
yoan
1b1b5939c5 Merge branch 'feat-tecent-ecdn-teo-deploy' of github.com:LeoChen98/certimate into LeoChen98-feat-tecent-ecdn-teo-deploy 2024-10-27 08:07:48 +08:00
Leo Chen
ffdd61b5ee feat: add ServerChan notifier
新增Server酱通知
2024-10-27 04:01:42 +08:00
Fu Diwei
adad5d86ba feat: support specified format on deployment to local/ssh 2024-10-27 00:19:34 +08:00
Fu Diwei
e7870e2b05 feat: support specified shell on deployment to local 2024-10-26 22:22:28 +08:00
徐雪君
548cbbfdd4 feat: k8s部署支持ServiceAccount权限 2024-10-26 22:15:16 +08:00
Fu Diwei
da4715e6dc fix: fix aliyun nlb endpoint 2024-10-26 13:18:15 +08:00
Fu Diwei
506ab4f18e feat: support quic listener in deployment to aliyun alb 2024-10-26 13:15:01 +08:00
Fu Diwei
d87026d5be feat: add aliyun nlb deployer 2024-10-26 12:52:55 +08:00
Fu Diwei
1690963aaf feat: add aliyun alb deployer 2024-10-26 12:40:45 +08:00
Fu Diwei
20d2c5699c feat: add aliyun clb deployer 2024-10-26 00:31:38 +08:00
Fu Diwei
e660e9cad1 feat: add aliyun slb uploader 2024-10-25 23:13:33 +08:00
Fu Diwei
26d7b0ba03 refactor: clean code 2024-10-25 23:03:52 +08:00
Leo Chen
ee097b3135 update README for tencent TEO support 2024-10-25 22:21:30 +08:00
Leo Chen
f5052e9a58 fix the missing parentheses 2024-10-25 22:18:40 +08:00
Leo Chen
3b3376899c add feat: tencent TEO deploy support
新增腾讯TEO(Edge One)部署方式
2024-10-25 22:16:27 +08:00
Leo Chen
a24a3595fa feat: add tencent ECDN deploy 2024-10-25 18:47:41 +08:00
Leo Chen
6a14d801f1 fix type incompatible error 2024-10-25 18:32:45 +08:00
yoan
332c5c5127 fix error type 2024-10-25 18:32:32 +08:00
usual2970
f9568f1a4a Merge pull request #254 from fudiwei/feat/cloud-load-balance
feat: huaweicloud elb deployer
2024-10-25 17:43:11 +08:00
usual2970
b458720dca Merge pull request #257 from belier-cn/main
feat: keep qiniu cdn https configuration
2024-10-25 16:16:20 +08:00
belier
935a320100 feat: keep qiniu cdn https configuration 2024-10-25 14:45:48 +08:00
yoan
361d0de17c v0.2.8 2024-10-25 08:10:05 +08:00
Fu Diwei
024b3c936e Merge branch 'main' into feat/cloud-load-balance 2024-10-24 22:45:25 +08:00
Fu Diwei
dc720a5d99 feat: add huaweicloud elb deployer 2024-10-24 22:37:55 +08:00
Fu Diwei
af3e20709d refactor: clean code 2024-10-24 21:42:39 +08:00
yoan
ea9e9165b6 Fix the issue where log information is not displayed. 2024-10-24 21:03:57 +08:00
Fu Diwei
ee531dd186 fix: aliyun oss deploy config validation error 2024-10-24 20:49:51 +08:00
yoan
51abe8de56 Merge branch 'zzci-main' 2024-10-24 20:47:00 +08:00
yoan
e2254faf15 Reuse the x509 package 2024-10-24 20:44:41 +08:00
Fu Diwei
cea6be37dc feat: allow set a different region on deployment to huaweicloud cdn 2024-10-24 20:16:23 +08:00
Roy
46dccb176e fix typo, get annotations from cert. 2024-10-24 18:39:18 +08:00
Roy
5411b9cb92 change annotations to certimage. 2024-10-24 17:06:57 +07:00
Roy
9f6ea410af Update k8s_secret.go 2024-10-24 17:05:05 +07:00
Roy
528a3d9da8 support create secret, add cert annotations. 2024-10-24 17:56:36 +08:00
yoan
564eb48ebe update dark mod stype 2024-10-24 08:59:17 +08:00
usual2970
92a6b179d4 Merge pull request #247 from LeoChen98/feat-tencent-clb
feat: add support for tencent CLB
2024-10-24 08:03:28 +08:00
Leo Chen
83393a4ee1 update readme for tencent clb support 2024-10-24 00:00:24 +08:00
Leo Chen
6875151717 fix tencent clb deploy failed
- 新增region参数
- 新增配置说明
2024-10-23 23:56:22 +08:00
usual2970
2a8c6cf033 Merge pull request #244 from usual2970/feat/gts
Support Google Trust Services
2024-10-23 21:13:50 +08:00
Leo Chen
7544286b0f add support for tencent CLB
新增腾讯云CLB负载均衡配置支持
2024-10-23 18:57:12 +08:00
Leo Chen
7c685646da fix tencent cos ui placeholder 2024-10-23 18:48:01 +08:00
Leo Chen
d82a9c9253 fix tencent cos ui onload verify 2024-10-23 18:45:36 +08:00
Leo Chen
59584a2961 fix tencent cos input verify 2024-10-23 18:40:52 +08:00
Leo Chen
195aa54cdc add wildcase domain supported ui label 2024-10-23 18:21:18 +08:00
Leo Chen
4b324e6a22 fix tencent COS ui 2024-10-23 18:19:35 +08:00
Leo Chen
0e575a0ce7 rename tencent_cos.go 2024-10-23 17:40:32 +08:00
yoan
7ab8517a93 Handle concurrency issues in a simple way. 2024-10-23 17:32:35 +08:00
yoan
1dca6ecf8d An account for many customers 2024-10-23 16:25:21 +08:00
yoan
8bec234fe8 gts support 2024-10-23 13:22:17 +08:00
yoan
bff18a7be7 update vite plugin to preserve special file 2024-10-23 08:36:25 +08:00
yoan
bac00491fe v0.2.7 2024-10-23 07:56:29 +08:00
usual2970
f8da3ded0d Merge pull request #242 from LeoChen98/feat-tencent-cos-support
feat: add tencent cos deploy support
2024-10-23 07:55:03 +08:00
Leo Chen
b01849eb0c update readme for new feat. 2024-10-22 22:32:35 +08:00
Leo Chen
c9eb487953 fix ui-deployer route 2024-10-22 22:18:52 +08:00
yoan
dc383644d6 update make file 2024-10-22 22:13:01 +08:00
Leo Chen
a8f718afa0 add tencent-cos ui
表单主要从OSS表单修改
2024-10-22 21:56:26 +08:00
Leo Chen
cd76d170b2 fix region of cos 2024-10-22 21:55:49 +08:00
Leo Chen
7b129c11e9 Merge branch 'usual2970:main' into feat-tencent-cos-support 2024-10-22 21:22:45 +08:00
Leo Chen
f7972d5b68 remove unused imports 2024-10-22 21:19:20 +08:00
usual2970
b1a0d84033 Merge pull request #227 from fudiwei/feat/cloud-load-balance
feat: cloud load balance pre-works
2024-10-22 21:18:46 +08:00
usual2970
969fba8a57 Merge branch 'main' into feat/cloud-load-balance 2024-10-22 21:13:59 +08:00
Leo Chen
63865b5fbd fix ResourceType 2024-10-22 21:11:49 +08:00
Leo Chen
46c32f15e3 feat: add tencent cos deploy support
新增腾讯云COS配置支持
2024-10-22 21:07:58 +08:00
usual2970
5f62c887c0 Merge pull request #236 from liburdi/docs/upd_readme
feat: reamdme里面的源码启动命令已经失效,因为ui/dist不在代码仓库中管理
2024-10-22 20:11:29 +08:00
usual2970
c85beaa52b Merge pull request #238 from usual2970/feat/qiniu_wildcard
Qiniu cdn supports wildcard domain deployment
2024-10-22 20:08:40 +08:00
Fu Diwei
885cdfaec9 fix: fix repeat certificates judgement logical in tencentcloud ssl uploader 2024-10-22 18:39:42 +08:00
Fu Diwei
011130432c refactor: clean code 2024-10-22 18:06:56 +08:00
Fu Diwei
062d66222a refactor: divide DeployList 2024-10-22 17:44:39 +08:00
Fu Diwei
e53749e16e refactor: clean code 2024-10-22 15:58:58 +08:00
yoan
dbfb84ec6d qiniu cdn supports wildcard domain deployment 2024-10-22 15:41:44 +08:00
liburdi
265842feeb feat: 根据输入的参数,显示不同的内容 2024-10-22 13:52:33 +08:00
liburdi
0c35928eee feat: 启动时终端打印 2024-10-22 12:26:08 +08:00
liburdi
ea4bcb4aaf feat: reamdme里面的源码启动命令已经失效,因为ui/dist不在代码仓库中管理 2024-10-22 12:17:43 +08:00
Fu Diwei
716f5f1426 refactor: clean code 2024-10-22 11:38:55 +08:00
yoan
4e86c1eb45 v0.2.6 2024-10-22 08:26:27 +08:00
yoan
0576a8bec3 Merge branch 'main' of github.com:usual2970/certimate 2024-10-22 07:32:13 +08:00
yoan
97f334b5ab v0.2.6 2024-10-22 07:31:47 +08:00
Fu Diwei
18a7bf0d66 feat: set default region when applying certificates by huaweicloud 2024-10-21 15:10:14 +08:00
Fu Diwei
908d33f186 refactor: clean code 2024-10-21 15:07:09 +08:00
Fu Diwei
68b9171390 Merge branch 'main' into feat/cloud-load-balance 2024-10-21 14:55:19 +08:00
Fu Diwei
45005a5073 refactor: clean code 2024-10-21 14:53:43 +08:00
usual2970
028eb088a5 Merge pull request #229 from PBK-B/feat_disable_cname
feat: 支持配置 CNAME 认证跟随禁用
2024-10-21 13:53:21 +08:00
PBK-B
8f98664665 fix: handle i18n label naming conventions 2024-10-21 09:18:41 +08:00
PBK-B
699385a8c4 fix: adjust the string conversion syntax 2024-10-21 09:18:41 +08:00
PBK-B
64b7ed00f5 feat: docking disableFollowCNAME config item function #228 2024-10-21 09:18:41 +08:00
PBK-B
2c75d2bfde feat: domain config add disable follow CNAME #228 2024-10-21 09:18:41 +08:00
Fu Diwei
9c41b0e357 refactor: clean code 2024-10-21 09:15:36 +08:00
Fu Diwei
b031f00764 feat: add aliyun cas uploader 2024-10-21 00:35:16 +08:00
Fu Diwei
a4fc8dfc56 feat: add tencentcloud ssl uploader 2024-10-20 23:53:10 +08:00
Fu Diwei
f168bd903d feat: add huaweicloud elb uploader 2024-10-20 21:33:08 +08:00
Fu Diwei
fc55e37454 chore: git keep /ui/dist 2024-10-20 21:09:28 +08:00
Fu Diwei
f6a3f4edfa refactor: optimize code 2024-10-20 20:42:13 +08:00
Fu Diwei
560d21c854 refactor: optimize code 2024-10-20 20:10:07 +08:00
Fu Diwei
3a213dc9c3 feat: do not use region from access when deploy to huaweicloud cdn 2024-10-20 17:51:36 +08:00
Fu Diwei
f0e7fe695d clean code 2024-10-20 17:24:23 +08:00
Fu Diwei
8d41a9aae7 Merge branch 'main' into feat/cloud-load-balance 2024-10-20 16:48:18 +08:00
Fu Diwei
896b5d3a13 Merge branch 'main' into feat/cloud-load-balance 2024-10-20 16:48:06 +08:00
Fu Diwei
88e64717cd feat: support using scm service on deployment to huaweicloud cdn 2024-10-20 16:42:05 +08:00
253 changed files with 23668 additions and 6464 deletions

View File

@@ -56,3 +56,4 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}

4
.gitignore vendored
View File

@@ -10,11 +10,13 @@
*.sln
*.sw?
__debug_bin*
vendor
pb_data
build
main
ui/dist
/ui/dist/*
!/ui/dist/.gitkeep
./dist
./certimate
/docker/data

View File

@@ -35,3 +35,6 @@ help:
@echo " make help - 显示此帮助信息"
.PHONY: all build clean help
local.run:
go mod vendor&& npm --prefix=./ui install && npm --prefix=./ui run build && go run main.go serve --http 127.0.0.1:8090

View File

@@ -55,8 +55,7 @@ mkdir -p ~/.certimate && cd ~/.certimate && curl -O https://raw.githubuserconten
```bash
git clone EMAIL:usual2970/certimate.git
cd certimate
go mod vendor
go run main.go serve
make local.run
```
## 二、使用
@@ -72,34 +71,35 @@ go run main.go serve
## 三、支持的服务商列表
| 服务商 | 支持申请证书 | 支持部署证书 | 备注 |
| :--------: | :----------: | :----------: | ------------------------------------------------------------ |
| 阿里云 | √ | √ | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN |
| 腾讯云 | √ | √ | 可签发在腾讯云注册的域名;可部署到腾讯云 CDN |
| 华为云 | | √ | 可签发在华为云注册的域名;可部署到华为云 CDN |
| 七牛云 | | √ | 可部署到七牛云 CDN |
| AWS | √ | | 可签发在 AWS Route53 托管的域名 |
| CloudFlare | | | 可签发在 CloudFlare 注册的域名CloudFlare 服务自带 SSL 证书 |
| GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 |
| Namesilo | √ | | 可签发在 Namesilo 注册的域名 |
| PowerDNS | √ | | 可签发通过PowerDNS管理的域名 |
| HTTP request | √ | | 可签发通过HTTP Request修改dns的域名 |
| 本地部署 | | √ | 可部署到本地服务器 |
| SSH | | √ | 可部署到 SSH 服务器 |
| Webhook | | | 可部署时回调到 Webhook |
| Kubernetes | | √ | 可部署到 Kubernetes Secret |
| 服务商 | 支持申请证书 | 支持部署证书 | 备注 |
| :--------: | :----------: | :----------: | ----------------------------------------------------------------- |
| 阿里云 | √ | √ | 可签发在阿里云注册的域名;可部署到阿里云 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 |
## 四、系统截图
![login](https://i.imgur.com/SYjjbql.jpeg)
![dashboard](https://i.imgur.com/WMVbBId.jpeg)
![domains](https://i.imgur.com/8wit3ZA.jpeg)
![accesses](https://i.imgur.com/EWtOoJ0.jpeg)
![history](https://i.imgur.com/aaPtSW7.jpeg)
<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>
## 五、概念
@@ -171,13 +171,22 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.
支持更多服务商、UI 的优化改进、Bug 修复、文档完善等,欢迎大家提交 PR。
## 八、加入社区
## 八、免责声明
本软件依据 MIT 许可证MIT License发布免费提供旨在“按现状”供用户使用。作者及贡献者不对使用本软件所产生的任何直接或间接后果承担责任包括但不限于性能下降、数据丢失、服务中断、或任何其他类型的损害。
无任何保证:本软件不提供任何明示或暗示的保证,包括但不限于对特定用途的适用性、无侵权性、商用性及可靠性的保证。
用户责任:使用本软件即表示您理解并同意承担由此产生的一切风险及责任。
## 九、加入社区
- [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
- 微信群聊(超 200 人需邀请入群,可先加作者好友)
<img src="https://i.imgur.com/8xwsLTA.png" width="400"/>
## 、Star 趋势图
## 、Star 趋势图
[![Stargazers over time](https://starchart.cc/usual2970/certimate.svg?variant=adaptive)](https://starchart.cc/usual2970/certimate)

View File

@@ -54,13 +54,12 @@ mkdir -p ~/.certimate && cd ~/.certimate && curl -O https://raw.githubuserconten
```bash
git clone EMAIL:usual2970/certimate.git
cd certimate
go mod vendor
go run main.go serve
make local.run
```
## Usage
After completing the installation steps above, you can access the Certimate management page by visiting http://127.0.0.1:8090 in your browser.
After completing the installation steps above, you can access the Certimate management page by visiting <http://127.0.0.1:8090> in your browser.
```bash
usernameadmin@certimate.fun
@@ -71,35 +70,35 @@ password1234567890
## List of Supported Providers
| Provider | Registration | Deployment | Remarks |
| :-----------: | :----------: | :--------: | ------------------------------------------------------------------------------------------- |
| Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN |
| Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud CDN |
| Huawei Cloud | | √ | Supports domains registered on Huawei; supports deployment to Huawei Cloud CDN |
| Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud 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 by PowerDNS |
| HTTP request | √ | | Supports domains dns managed 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 |
| 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
![login](https://i.imgur.com/SYjjbql.jpeg)
![dashboard](https://i.imgur.com/WMVbBId.jpeg)
![domains](https://i.imgur.com/8wit3ZA.jpeg)
![accesses](https://i.imgur.com/EWtOoJ0.jpeg)
![history](https://i.imgur.com/aaPtSW7.jpeg)
<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
@@ -171,6 +170,14 @@ You can support the development of Certimate in the following ways:
Support for more service providers, UI enhancements, bug fixes, and documentation improvements are all welcome. We encourage everyone to submit pull requests (PRs).
## Disclaimer
This software is provided under the MIT License and distributed “as-is” without any warranty of any kind. The authors and contributors are not responsible for any damages or losses resulting from the use or inability to use this software, including but not limited to data loss, business interruption, or any other potential harm.
No Warranties: This software comes without any express or implied warranties, including but not limited to implied warranties of merchantability, fitness for a particular purpose, and non-infringement.
User Responsibility: By using this software, you agree to take full responsibility for any outcomes resulting from its use.
## Join the Community
- [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)

102
go.mod
View File

@@ -5,26 +5,37 @@ go 1.22.0
toolchain go1.23.2
require (
github.com/alibabacloud-go/alb-20200616/v2 v2.2.1
github.com/alibabacloud-go/cas-20200407/v3 v3.0.1
github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10
github.com/alibabacloud-go/nlb-20220430/v2 v2.0.3
github.com/alibabacloud-go/slb-20140515/v4 v4.0.9
github.com/alibabacloud-go/tea v1.2.2
github.com/alibabacloud-go/tea-utils/v2 v2.0.6
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/go-acme/lego/v4 v4.19.2
github.com/baidubce/bce-sdk-go v0.9.197
github.com/go-acme/lego/v4 v4.20.2
github.com/gojek/heimdall/v7 v7.0.3
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.120
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
github.com/nikoksr/notify v1.0.0
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
github.com/pkg/sftp v1.13.6
github.com/pocketbase/dbx v1.10.1
github.com/pocketbase/pocketbase v0.22.18
github.com/qiniu/go-sdk/v7 v7.22.0
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1031
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1034
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
golang.org/x/crypto v0.27.0
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030
github.com/volcengine/volc-sdk-golang v1.0.184
golang.org/x/crypto v0.28.0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
k8s.io/api v0.31.1
k8s.io/apimachinery v0.31.1
k8s.io/client-go v0.31.1
software.sslmate.com/src/go-pkcs12 v0.5.0
)
require (
@@ -32,8 +43,10 @@ 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/aws/aws-sdk-go-v2/service/route53 v1.43.2 // 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
@@ -57,7 +70,6 @@ require (
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/api v0.31.1 // 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
@@ -69,50 +81,50 @@ require (
require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/dcdn-20180115/v3 v3.4.2
github.com/alibabacloud-go/debug v1.0.0 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
github.com/alibabacloud-go/tea-utils v1.4.5 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 // indirect
github.com/aliyun/credentials-go v1.3.1 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.47 // indirect
github.com/aliyun/credentials-go v1.3.10 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.33 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
github.com/aws/aws-sdk-go-v2 v1.32.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect
github.com/aws/aws-sdk-go-v2/config v1.28.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.42 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
github.com/aws/smithy-go v1.20.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.3 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.66.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 // indirect
github.com/aws/smithy-go v1.22.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/clbanning/mxj/v2 v2.5.6 // indirect
github.com/cloudflare/cloudflare-go v0.104.0 // indirect
github.com/cloudflare/cloudflare-go v0.108.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -133,14 +145,14 @@ require (
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/errors v0.9.1
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1034 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
@@ -148,19 +160,19 @@ require (
gocloud.dev v0.37.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/term v0.24.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/time v0.6.0 // indirect
golang.org/x/sync v0.8.0
golang.org/x/sys v0.26.0 // indirect
golang.org/x/term v0.25.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.25.0 // indirect
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
google.golang.org/api v0.197.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.66.1 // indirect
google.golang.org/protobuf v1.34.2 // indirect
google.golang.org/api v0.204.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e // indirect

800
go.sum
View File

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,13 @@ import (
"crypto/rand"
"errors"
"fmt"
"os"
"strconv"
"strings"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
"github.com/usual2970/certimate/internal/repository"
"github.com/usual2970/certimate/internal/utils/app"
"github.com/go-acme/lego/v4/certcrypto"
@@ -31,22 +35,26 @@ const (
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"
@@ -63,18 +71,47 @@ type Certificate struct {
}
type ApplyOption struct {
Email string `json:"email"`
Domain string `json:"domain"`
Access string `json:"access"`
KeyAlgorithm string `json:"keyAlgorithm"`
Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"`
Email string `json:"email"`
Domain string `json:"domain"`
Access string `json:"access"`
KeyAlgorithm string `json:"keyAlgorithm"`
Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"`
DisableFollowCNAME bool `json:"disableFollowCNAME"`
}
type ApplyUser struct {
Ca string
Email string
Registration *registration.Resource
key crypto.PrivateKey
key string
}
func newApplyUser(ca, email string) (*ApplyUser, error) {
repo := getAcmeAccountRepository()
rs := &ApplyUser{
Ca: ca,
Email: email,
}
resp, err := repo.GetByCAAndEmail(ca, email)
if err != nil {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
keyStr, err := x509.ConvertECPrivateKeyToPEM(privateKey)
if err != nil {
return nil, err
}
rs.key = keyStr
return rs, nil
}
rs.Registration = resp.Resource
rs.key = resp.Key
return rs, nil
}
func (u *ApplyUser) GetEmail() string {
@@ -86,6 +123,15 @@ func (u ApplyUser) GetRegistration() *registration.Resource {
}
func (u *ApplyUser) GetPrivateKey() crypto.PrivateKey {
rs, _ := x509.ParseECPrivateKeyFromPEM(u.key)
return rs
}
func (u *ApplyUser) hasRegistration() bool {
return u.Registration != nil
}
func (u *ApplyUser) getPrivateKeyString() string {
return u.key
}
@@ -115,12 +161,13 @@ func Get(record *models.Record) (Applicant, error) {
}
option := &ApplyOption{
Email: applyConfig.Email,
Domain: record.GetString("domain"),
Access: access.GetString("config"),
KeyAlgorithm: applyConfig.KeyAlgorithm,
Nameservers: applyConfig.Nameservers,
Timeout: applyConfig.Timeout,
Email: applyConfig.Email,
Domain: record.GetString("domain"),
Access: access.GetString("config"),
KeyAlgorithm: applyConfig.KeyAlgorithm,
Nameservers: applyConfig.Nameservers,
Timeout: applyConfig.Timeout,
DisableFollowCNAME: applyConfig.DisableFollowCNAME,
}
switch access.GetString("configType") {
@@ -142,6 +189,8 @@ func Get(record *models.Record) (Applicant, error) {
return NewPdns(option), nil
case configTypeHttpreq:
return NewHttpreq(option), nil
case configTypeVolcengine:
return NewVolcengine(option), nil
default:
return nil, errors.New("unknown config type")
}
@@ -153,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) {
@@ -172,17 +224,16 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
}
}
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
// Some unified lego environment variables are configured here.
// link: https://github.com/go-acme/lego/issues/1867
os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(option.DisableFollowCNAME))
myUser, err := newApplyUser(sslProvider.Provider, option.Email)
if err != nil {
return nil, err
}
myUser := ApplyUser{
Email: option.Email,
key: privateKey,
}
config := lego.NewConfig(&myUser)
config := lego.NewConfig(myUser)
// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM.
config.CADirURL = sslProviderUrls[sslProvider.Provider]
@@ -203,11 +254,13 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
client.Challenge.SetDNS01Provider(provider, challengeOptions...)
// New users will need to register
reg, err := getReg(client, sslProvider)
if err != nil {
return nil, fmt.Errorf("failed to register: %w", err)
if !myUser.hasRegistration() {
reg, err := getReg(client, sslProvider, myUser)
if err != nil {
return nil, fmt.Errorf("failed to register: %w", err)
}
myUser.Registration = reg
}
myUser.Registration = reg
domains := strings.Split(option.Domain, ";")
request := certificate.ObtainRequest{
@@ -229,7 +282,16 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
}, nil
}
func getReg(client *lego.Client, sslProvider *SSLProviderConfig) (*registration.Resource, error) {
type AcmeAccountRepository interface {
GetByCAAndEmail(ca, email string) (*domain.AcmeAccount, error)
Save(ca, email, key string, resource *registration.Resource) error
}
func getAcmeAccountRepository() AcmeAccountRepository {
return repository.NewAcmeAccountRepository()
}
func getReg(client *lego.Client, sslProvider *SSLProviderConfig, user *ApplyUser) (*registration.Resource, error) {
var reg *registration.Resource
var err error
switch sslProvider.Provider {
@@ -239,6 +301,12 @@ func getReg(client *lego.Client, sslProvider *SSLProviderConfig) (*registration.
Kid: sslProvider.Config.Zerossl.EabKid,
HmacEncoded: sslProvider.Config.Zerossl.EabHmacKey,
})
case sslProviderGts:
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: sslProvider.Config.Gts.EabKid,
HmacEncoded: sslProvider.Config.Gts.EabHmacKey,
})
case sslProviderLetsencrypt:
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
@@ -251,6 +319,18 @@ func getReg(client *lego.Client, sslProvider *SSLProviderConfig) (*registration.
return nil, err
}
repo := getAcmeAccountRepository()
resp, err := repo.GetByCAAndEmail(sslProvider.Provider, user.GetEmail())
if err == nil {
user.key = resp.Key
return resp.Resource, nil
}
if err := repo.Save(sslProvider.Provider, user.GetEmail(), user.getPrivateKeyString(), reg); err != nil {
return nil, fmt.Errorf("failed to save registration: %w", err)
}
return reg, nil
}

View File

@@ -24,7 +24,12 @@ func (t *huaweicloud) Apply() (*Certificate, error) {
access := &domain.HuaweiCloudAccess{}
json.Unmarshal([]byte(t.option.Access), access)
os.Setenv("HUAWEICLOUD_REGION", access.Region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
region := access.Region
if region == "" {
region = "cn-north-1"
}
os.Setenv("HUAWEICLOUD_REGION", region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
os.Setenv("HUAWEICLOUD_ACCESS_KEY_ID", access.AccessKeyId)
os.Setenv("HUAWEICLOUD_SECRET_ACCESS_KEY", access.SecretAccessKey)
os.Setenv("HUAWEICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))

View 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)
}

View 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
}

View File

@@ -4,39 +4,41 @@ import (
"context"
"encoding/json"
"fmt"
"time"
cdn20180510 "github.com/alibabacloud-go/cdn-20180510/v5/client"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
aliyunCdn "github.com/alibabacloud-go/cdn-20180510/v5/client"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/rand"
)
type AliyunCDNDeployer struct {
client *cdn20180510.Client
option *DeployerOption
infos []string
sdkClient *aliyunCdn.Client
}
func NewAliyunCDNDeployer(option *DeployerOption) (*AliyunCDNDeployer, error) {
func NewAliyunCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
d := &AliyunCDNDeployer{
option: option,
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := d.createClient(access.AccessKeyId, access.AccessKeySecret)
client, err := (&AliyunCDNDeployer{}).createSdkClient(
access.AccessKeyId,
access.AccessKeySecret,
)
if err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &AliyunCDNDeployer{
client: client,
option: option,
infos: make([]string, 0),
option: option,
infos: make([]string, 0),
sdkClient: client,
}, nil
}
@@ -44,41 +46,43 @@ func (d *AliyunCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunCDNDeployer) GetInfo() []string {
func (d *AliyunCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *AliyunCDNDeployer) Deploy(ctx context.Context) error {
certName := fmt.Sprintf("%s-%s-%s", d.option.Domain, d.option.DomainId, rand.RandStr(6))
setCdnDomainSSLCertificateRequest := &cdn20180510.SetCdnDomainSSLCertificateRequest{
DomainName: tea.String(getDeployString(d.option.DeployConfig, "domain")),
CertName: tea.String(certName),
// 设置 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(d.option.Certificate.Certificate),
SSLPri: tea.String(d.option.Certificate.PrivateKey),
CertRegion: tea.String("cn-hangzhou"),
}
runtime := &util.RuntimeOptions{}
resp, err := d.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'")
}
d.infos = append(d.infos, toStr("cdn设置证书", resp))
d.infos = append(d.infos, toStr("已设置 CDN 域名证书", setCdnDomainSSLCertificateResp))
return nil
}
func (d *AliyunCDNDeployer) 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
}

View 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
}

View 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
}

View File

@@ -1,97 +0,0 @@
/*
* @Author: Bin
* @Date: 2024-09-17
* @FilePath: /certimate/internal/deployer/aliyun_esa.go
*/
package deployer
import (
"context"
"encoding/json"
"fmt"
"strings"
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"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/rand"
)
type AliyunESADeployer struct {
client *dcdn20180115.Client
option *DeployerOption
infos []string
}
func NewAliyunESADeployer(option *DeployerOption) (*AliyunESADeployer, error) {
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
d := &AliyunESADeployer{
option: option,
}
client, err := d.createClient(access.AccessKeyId, access.AccessKeySecret)
if err != nil {
return nil, err
}
return &AliyunESADeployer{
client: client,
option: option,
infos: make([]string, 0),
}, nil
}
func (d *AliyunESADeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunESADeployer) GetInfo() []string {
return d.infos
}
func (d *AliyunESADeployer) Deploy(ctx context.Context) error {
certName := fmt.Sprintf("%s-%s-%s", d.option.Domain, d.option.DomainId, rand.RandStr(6))
// 支持泛解析域名,在 Aliyun DCND 中泛解析域名表示为 .example.com
domain := getDeployString(d.option.DeployConfig, "domain")
if strings.HasPrefix(domain, "*") {
domain = strings.TrimPrefix(domain, "*")
}
setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{
DomainName: tea.String(domain),
CertName: tea.String(certName),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),
SSLPub: tea.String(d.option.Certificate.Certificate),
SSLPri: tea.String(d.option.Certificate.PrivateKey),
CertRegion: tea.String("cn-hangzhou"),
}
runtime := &util.RuntimeOptions{}
resp, err := d.client.SetDcdnDomainSSLCertificateWithOptions(setDcdnDomainSSLCertificateRequest, runtime)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("dcdn设置证书", resp))
return nil
}
func (d *AliyunESADeployer) 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
}

View 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
}

View File

@@ -3,48 +3,62 @@ 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 {
client *oss.Client
option *DeployerOption
infos []string
sdkClient *oss.Client
}
func NewAliyunOSSDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
d := &AliyunOSSDeployer{
option: option,
infos: make([]string, 0),
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := d.createClient(access.AccessKeyId, access.AccessKeySecret)
client, err := (&AliyunOSSDeployer{}).createSdkClient(
access.AccessKeyId,
access.AccessKeySecret,
option.DeployConfig.GetConfigAsString("endpoint"),
)
if err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
d.client = client
return d, nil
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) GetInfo() []string {
func (d *AliyunOSSDeployer) GetInfos() []string {
return d.infos
}
func (d *AliyunOSSDeployer) Deploy(ctx context.Context) error {
err := d.client.PutBucketCnameWithCertificate(getDeployString(d.option.DeployConfig, "bucket"), oss.PutBucketCname{
Cname: getDeployString(d.option.DeployConfig, "domain"),
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,
@@ -52,19 +66,21 @@ func (d *AliyunOSSDeployer) Deploy(ctx context.Context) error {
},
})
if err != nil {
return fmt.Errorf("deploy aliyun oss error: %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'oss.PutBucketCnameWithCertificate'")
}
return nil
}
func (d *AliyunOSSDeployer) createClient(accessKeyId, accessKeySecret string) (*oss.Client, error) {
client, err := oss.New(
getDeployString(d.option.DeployConfig, "endpoint"),
accessKeyId,
accessKeySecret,
)
if err != nil {
return nil, fmt.Errorf("create aliyun client error: %w", err)
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
}

View 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
}

View 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
}

View File

@@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/pocketbase/pocketbase/models"
@@ -17,10 +16,23 @@ import (
const (
targetAliyunOSS = "aliyun-oss"
targetAliyunCDN = "aliyun-cdn"
targetAliyunESA = "aliyun-dcdn"
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"
@@ -30,7 +42,6 @@ const (
type DeployerOption struct {
DomainId string `json:"domainId"`
Domain string `json:"domain"`
Product string `json:"product"`
Access string `json:"access"`
AccessRecord *models.Record `json:"-"`
DeployConfig domain.DeployConfig `json:"deployConfig"`
@@ -40,7 +51,7 @@ type DeployerOption struct {
type Deployer interface {
Deploy(ctx context.Context) error
GetInfo() []string
GetInfos() []string
GetID() string
}
@@ -82,7 +93,6 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
option := &DeployerOption{
DomainId: record.Id,
Domain: record.GetString("domain"),
Product: getProduct(deployConfig.Type),
Access: access.GetString("config"),
AccessRecord: access,
DeployConfig: deployConfig,
@@ -101,14 +111,34 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
return NewAliyunOSSDeployer(option)
case targetAliyunCDN:
return NewAliyunCDNDeployer(option)
case targetAliyunESA:
return NewAliyunESADeployer(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 NewQiniuCDNDeployer(option)
case targetDogeCloudCdn:
return NewDogeCloudCDNDeployer(option)
case targetLocal:
return NewLocalDeployer(option)
case targetSSH:
@@ -117,16 +147,14 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
return NewWebhookDeployer(option)
case targetK8sSecret:
return NewK8sSecretDeployer(option)
case targetVolcEngineLive:
return NewVolcengineLiveDeployer(option)
case targetVolcEngineCDN:
return NewVolcengineCDNDeployer(option)
case targetBytePlusCDN:
return NewByteplusCDNDeployer(option)
}
return nil, errors.New("not implemented")
}
func getProduct(t string) string {
rs := strings.Split(t, "-")
if len(rs) < 2 {
return ""
}
return rs[1]
return nil, errors.New("unsupported deploy target")
}
func toStr(tag string, data any) string {
@@ -136,38 +164,3 @@ func toStr(tag string, data any) string {
byts, _ := json.Marshal(data)
return tag + "" + string(byts)
}
func getDeployString(conf domain.DeployConfig, key string) string {
if _, ok := conf.Config[key]; !ok {
return ""
}
val, ok := conf.Config[key].(string)
if !ok {
return ""
}
return val
}
func getDeployVariables(conf domain.DeployConfig) map[string]string {
rs := make(map[string]string)
data, ok := conf.Config["variables"]
if !ok {
return rs
}
bts, _ := json.Marshal(data)
kvData := make([]domain.KV, 0)
if err := json.Unmarshal(bts, &kvData); err != nil {
return rs
}
for _, kv := range kvData {
rs[kv.Key] = kv.Value
}
return rs
}

View 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
}

View 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)
}

View File

@@ -6,23 +6,55 @@ import (
"fmt"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
cdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2"
cdnModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model"
cdnRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/region"
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/utils/rand"
"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),
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
@@ -30,50 +62,49 @@ func (d *HuaweiCloudCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *HuaweiCloudCDNDeployer) GetInfo() []string {
func (d *HuaweiCloudCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error {
access := &domain.HuaweiCloudAccess{}
if err := json.Unmarshal([]byte(d.option.Access), access); err != nil {
return err
}
client, err := d.createClient(access)
// 上传证书到 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("HuaweiCloudCdnClient 创建成功", nil))
d.infos = append(d.infos, toStr("已上传证书", upres))
// 查询加速域名配置
showDomainFullConfigReq := &cdnModel.ShowDomainFullConfigRequest{
DomainName: getDeployString(d.option.DeployConfig, "domain"),
// REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html
showDomainFullConfigReq := &hcCdnModel.ShowDomainFullConfigRequest{
DomainName: d.option.DeployConfig.GetConfigAsString("domain"),
}
showDomainFullConfigResp, err := client.ShowDomainFullConfig(showDomainFullConfigReq)
showDomainFullConfigResp, err := d.sdkClient.ShowDomainFullConfig(showDomainFullConfigReq)
if err != nil {
return err
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.ShowDomainFullConfig'")
}
d.infos = append(d.infos, toStr("已查询到加速域名配置", showDomainFullConfigResp))
// 更新加速域名配置
certName := fmt.Sprintf("%s-%s", d.option.DomainId, rand.RandStr(12))
updateDomainMultiCertificatesReq := &cdnModel.UpdateDomainMultiCertificatesRequest{
Body: &cdnModel.UpdateDomainMultiCertificatesRequestBody{
Https: mergeHuaweiCloudCDNConfig(showDomainFullConfigResp.Configs, &cdnModel.UpdateDomainMultiCertificatesRequestBodyContent{
DomainName: getDeployString(d.option.DeployConfig, "domain"),
HttpsSwitch: 1,
CertName: &certName,
Certificate: &d.option.Certificate.Certificate,
PrivateKey: &d.option.Certificate.PrivateKey,
}),
// 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 := client.UpdateDomainMultiCertificates(updateDomainMultiCertificatesReq)
updateDomainMultiCertificatesResp, err := d.sdkClient.UploadDomainMultiCertificatesEx(updateDomainMultiCertificatesReq)
if err != nil {
return err
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadDomainMultiCertificatesEx'")
}
d.infos = append(d.infos, toStr("已更新加速域名配置", updateDomainMultiCertificatesResp))
@@ -81,70 +112,32 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error {
return nil
}
func (d *HuaweiCloudCDNDeployer) createClient(access *domain.HuaweiCloudAccess) (*cdn.CdnClient, error) {
func (d *HuaweiCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcCdnEx.Client, error) {
if region == "" {
region = "cn-north-1" // CDN 服务默认区域:华北一北京
}
auth, err := global.NewCredentialsBuilder().
WithAk(access.AccessKeyId).
WithSk(access.SecretAccessKey).
WithAk(accessKeyId).
WithSk(secretAccessKey).
SafeBuild()
if err != nil {
return nil, err
}
region, err := cdnRegion.SafeValueOf(access.Region)
hcRegion, err := hcCdnRegion.SafeValueOf(region)
if err != nil {
return nil, err
}
hcClient, err := cdn.CdnClientBuilder().
WithRegion(region).
hcClient, err := hcCdn.CdnClientBuilder().
WithRegion(hcRegion).
WithCredential(auth).
SafeBuild()
if err != nil {
return nil, err
}
client := cdn.NewCdnClient(hcClient)
client := hcCdnEx.NewClient(hcClient)
return client, nil
}
func mergeHuaweiCloudCDNConfig(src *cdnModel.ConfigsGetBody, dest *cdnModel.UpdateDomainMultiCertificatesRequestBodyContent) *cdnModel.UpdateDomainMultiCertificatesRequestBodyContent {
if src == nil {
return dest
}
// 华为云 API 中不传的字段表示使用默认值、而非保留原值,因此这里需要把原配置中的参数重新赋值回去
// 而且蛋疼的是查询接口返回的数据结构和更新接口传入的参数结构不一致,需要做很多转化
// REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html
// REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html
if *src.OriginProtocol == "follow" {
accessOriginWay := int32(1)
dest.AccessOriginWay = &accessOriginWay
} else if *src.OriginProtocol == "http" {
accessOriginWay := int32(2)
dest.AccessOriginWay = &accessOriginWay
} else if *src.OriginProtocol == "https" {
accessOriginWay := int32(3)
dest.AccessOriginWay = &accessOriginWay
}
if src.ForceRedirect != nil {
dest.ForceRedirectConfig = &cdnModel.ForceRedirect{}
if src.ForceRedirect.Status == "on" {
dest.ForceRedirectConfig.Switch = 1
dest.ForceRedirectConfig.RedirectType = src.ForceRedirect.Type
} else {
dest.ForceRedirectConfig.Switch = 0
}
}
if src.Https != nil {
if *src.Https.Http2Status == "on" {
http2 := int32(1)
dest.Http2 = &http2
}
}
return dest
}

View 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: &region,
}
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
}

View File

@@ -3,24 +3,43 @@ package deployer
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
k8sMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
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),
option: option,
infos: make([]string, 0),
k8sClient: client,
}, nil
}
@@ -28,73 +47,82 @@ func (d *K8sSecretDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *K8sSecretDeployer) GetInfo() []string {
func (d *K8sSecretDeployer) GetInfos() []string {
return d.infos
}
func (d *K8sSecretDeployer) Deploy(ctx context.Context) error {
access := &domain.KubernetesAccess{}
if err := json.Unmarshal([]byte(d.option.Access), access); err != nil {
return err
}
client, err := d.createClient(access)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("kubeClient 创建成功", nil))
namespace := getDeployString(d.option.DeployConfig, "namespace")
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"
}
secretName := getDeployString(d.option.DeployConfig, "secretName")
if secretName == "" {
return fmt.Errorf("k8s secret name is empty")
return errors.New("`secretName` is required")
}
secretDataKeyForCrt := getDeployString(d.option.DeployConfig, "secretDataKeyForCrt")
if secretDataKeyForCrt == "" {
namespace = "tls.crt"
certX509, err := x509.ParseCertificateFromPEM(d.option.Certificate.Certificate)
if err != nil {
return err
}
secretDataKeyForKey := getDeployString(d.option.DeployConfig, "secretDataKeyForKey")
if secretDataKeyForKey == "" {
namespace = "tls.key"
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 实例
secret, err := client.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, k8sMetaV1.GetOptions{})
_, err = d.k8sClient.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, k8sMeta.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get k8s secret: %w", err)
_, 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 Data
secret.Data[secretDataKeyForCrt] = []byte(d.option.Certificate.Certificate)
secret.Data[secretDataKeyForKey] = []byte(d.option.Certificate.PrivateKey)
_, err = client.CoreV1().Secrets(namespace).Update(context.TODO(), secret, k8sMetaV1.UpdateOptions{})
// 更新 Secret 实例
_, err = d.k8sClient.CoreV1().Secrets(namespace).Update(context.TODO(), &secretPayload, k8sMeta.UpdateOptions{})
if err != nil {
return fmt.Errorf("failed to update k8s secret: %w", err)
return xerrors.Wrap(err, "failed to update k8s secret")
}
d.infos = append(d.infos, toStr("证书已更新到 K8s Secret", nil))
d.infos = append(d.infos, toStr("Certificate has been updated to K8s Secret", nil))
return nil
}
func (d *K8sSecretDeployer) createClient(access *domain.KubernetesAccess) (*kubernetes.Clientset, error) {
kubeConfig, err := clientcmd.Load([]byte(access.KubeConfig))
if err != nil {
return nil, err
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()
}
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: ""},
&clientcmd.ConfigOverrides{CurrentContext: kubeConfig.CurrentContext},
)
config, err := clientConfig.ClientConfig()
if err != nil {
return nil, err
}

View File

@@ -1,15 +1,17 @@
package deployer
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"github.com/usual2970/certimate/internal/domain"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/utils/fs"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type LocalDeployer struct {
@@ -17,6 +19,18 @@ type LocalDeployer struct {
infos []string
}
const (
certFormatPEM = "pem"
certFormatPFX = "pfx"
certFormatJKS = "jks"
)
const (
shellEnvSh = "sh"
shellEnvCmd = "cmd"
shellEnvPowershell = "powershell"
)
func NewLocalDeployer(option *DeployerOption) (Deployer, error) {
return &LocalDeployer{
option: option,
@@ -28,84 +42,122 @@ func (d *LocalDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *LocalDeployer) GetInfo() []string {
func (d *LocalDeployer) GetInfos() []string {
return []string{}
}
func (d *LocalDeployer) Deploy(ctx context.Context) error {
access := &domain.LocalAccess{}
if err := json.Unmarshal([]byte(d.option.Access), access); err != nil {
return err
}
preCommand := getDeployString(d.option.DeployConfig, "preCommand")
// 执行前置命令
preCommand := d.option.DeployConfig.GetConfigAsString("preCommand")
if preCommand != "" {
if err := execCmd(preCommand); err != nil {
return fmt.Errorf("执行前置命令失败: %w", err)
stdout, stderr, err := d.execCommand(preCommand)
if err != nil {
return xerrors.Wrapf(err, "failed to run pre-command, stdout: %s, stderr: %s", stdout, stderr)
}
d.infos = append(d.infos, toStr("执行前置命令成功", stdout))
}
// 复制证书文件
if err := copyFile(getDeployString(d.option.DeployConfig, "certPath"), d.option.Certificate.Certificate); err != nil {
return fmt.Errorf("复制证书失败: %w", err)
}
// 写入证书和私钥文件
switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) {
case certFormatPEM:
if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil {
return err
}
// 复制私钥文件
if err := copyFile(getDeployString(d.option.DeployConfig, "keyPath"), d.option.Certificate.PrivateKey); err != nil {
return fmt.Errorf("复制私钥失败: %w", err)
d.infos = append(d.infos, toStr("保存证书成功", nil))
if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil {
return err
}
d.infos = append(d.infos, toStr("保存私钥成功", nil))
case certFormatPFX:
pfxData, err := x509.TransformCertificateFromPEMToPFX(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
)
if err != nil {
return err
}
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil {
return err
}
d.infos = append(d.infos, toStr("保存证书成功", nil))
case certFormatJKS:
jksData, err := x509.TransformCertificateFromPEMToJKS(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("jksAlias"),
d.option.DeployConfig.GetConfigAsString("jksKeypass"),
d.option.DeployConfig.GetConfigAsString("jksStorepass"),
)
if err != nil {
return err
}
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
return err
}
d.infos = append(d.infos, toStr("保存证书成功", nil))
default:
return errors.New("unsupported format")
}
// 执行命令
if err := execCmd(getDeployString(d.option.DeployConfig, "command")); err != nil {
return fmt.Errorf("执行命令失败: %w", err)
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)
}
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(path string, content 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
}

View File

@@ -1,36 +1,55 @@
package deployer
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
xerrors "github.com/pkg/errors"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/usual2970/certimate/internal/domain"
xhttp "github.com/usual2970/certimate/internal/utils/http"
"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"
)
const qiniuGateway = "http://api.qiniu.com"
type QiniuCDNDeployer struct {
option *DeployerOption
info []string
credentials *auth.Credentials
option *DeployerOption
infos []string
sdkClient *qiniuEx.Client
sslUploader uploader.Uploader
}
func NewQiniuCDNDeployer(option *DeployerOption) (*QiniuCDNDeployer, error) {
func NewQiniuCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.QiniuAccess{}
json.Unmarshal([]byte(option.Access), access)
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,
info: make([]string, 0),
credentials: auth.New(access.AccessKey, access.SecretKey),
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
@@ -38,173 +57,57 @@ func (d *QiniuCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *QiniuCDNDeployer) GetInfo() []string {
return d.info
func (d *QiniuCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *QiniuCDNDeployer) Deploy(ctx context.Context) error {
// 上传证书
certId, err := d.uploadCert()
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return fmt.Errorf("uploadCert failed: %w", err)
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, "*")
}
// 获取域名信息
domainInfo, err := d.getDomainInfo()
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
getDomainInfoResp, err := d.sdkClient.GetDomainInfo(domain)
if err != nil {
return fmt.Errorf("getDomainInfo failed: %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDomainInfo'")
}
// 判断域名是否启用 https
if domainInfo.Https != nil && domainInfo.Https.CertID != "" {
// 启用了 https
// 修改域名证书
err = d.modifyDomainCert(certId)
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 fmt.Errorf("modifyDomainCert failed: %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.ModifyDomainHttpsConf'")
}
d.infos = append(d.infos, toStr("已修改域名证书", modifyDomainHttpsConfResp))
} else {
// 没启用 https
// 启用 https
err = d.enableHttps(certId)
enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(domain, upres.CertId, true, true)
if err != nil {
return fmt.Errorf("enableHttps failed: %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.EnableDomainHttps'")
}
d.infos = append(d.infos, toStr("已将域名升级为 HTTPS", enableDomainHttpsResp))
}
return nil
}
func (d *QiniuCDNDeployer) enableHttps(certId string) error {
path := fmt.Sprintf("/domain/%s/sslize", getDeployString(d.option.DeployConfig, "domain"))
body := &qiniuModifyDomainCertReq{
CertID: certId,
ForceHttps: true,
Http2Enable: true,
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("enable https failed: %w", err)
}
_, err = d.req(qiniuGateway+path, http.MethodPut, bytes.NewReader(bodyBytes))
if err != nil {
return fmt.Errorf("enable https failed: %w", err)
}
return nil
}
type qiniuDomainInfo struct {
Https *qiniuModifyDomainCertReq `json:"https"`
}
func (d *QiniuCDNDeployer) getDomainInfo() (*qiniuDomainInfo, error) {
path := fmt.Sprintf("/domain/%s", getDeployString(d.option.DeployConfig, "domain"))
res, err := d.req(qiniuGateway+path, http.MethodGet, nil)
if err != nil {
return nil, fmt.Errorf("req failed: %w", err)
}
resp := &qiniuDomainInfo{}
err = json.Unmarshal(res, resp)
if err != nil {
return nil, fmt.Errorf("json.Unmarshal failed: %w", err)
}
return resp, nil
}
type qiniuUploadCertReq struct {
Name string `json:"name"`
CommonName string `json:"common_name"`
Pri string `json:"pri"`
Ca string `json:"ca"`
}
type qiniuUploadCertResp struct {
CertID string `json:"certID"`
}
func (d *QiniuCDNDeployer) uploadCert() (string, error) {
path := "/sslcert"
body := &qiniuUploadCertReq{
Name: getDeployString(d.option.DeployConfig, "domain"),
CommonName: getDeployString(d.option.DeployConfig, "domain"),
Pri: d.option.Certificate.PrivateKey,
Ca: d.option.Certificate.Certificate,
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return "", fmt.Errorf("json.Marshal failed: %w", err)
}
res, err := d.req(qiniuGateway+path, http.MethodPost, bytes.NewReader(bodyBytes))
if err != nil {
return "", fmt.Errorf("req failed: %w", err)
}
resp := &qiniuUploadCertResp{}
err = json.Unmarshal(res, resp)
if err != nil {
return "", fmt.Errorf("json.Unmarshal failed: %w", err)
}
return resp.CertID, nil
}
type qiniuModifyDomainCertReq struct {
CertID string `json:"certId"`
ForceHttps bool `json:"forceHttps"`
Http2Enable bool `json:"http2Enable"`
}
func (d *QiniuCDNDeployer) modifyDomainCert(certId string) error {
path := fmt.Sprintf("/domain/%s/httpsconf", getDeployString(d.option.DeployConfig, "domain"))
body := &qiniuModifyDomainCertReq{
CertID: certId,
ForceHttps: true,
Http2Enable: true,
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("json.Marshal failed: %w", err)
}
_, err = d.req(qiniuGateway+path, http.MethodPut, bytes.NewReader(bodyBytes))
if err != nil {
return fmt.Errorf("req failed: %w", err)
}
return nil
}
func (d *QiniuCDNDeployer) req(url, method string, body io.Reader) ([]byte, error) {
req := xhttp.BuildReq(url, method, body, map[string]string{
"Content-Type": "application/json",
})
if err := d.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
func (u *QiniuCDNDeployer) createSdkClient(accessKey, secretKey string) (*qiniuEx.Client, error) {
credential := auth.New(accessKey, secretKey)
client := qiniuEx.NewClient(credential)
return client, nil
}

View File

@@ -1,87 +0,0 @@
package deployer
import (
"testing"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/usual2970/certimate/internal/applicant"
)
func Test_qiuniu_uploadCert(t *testing.T) {
type fields struct {
option *DeployerOption
}
tests := []struct {
name string
fields fields
want string
wantErr bool
}{
{
name: "test",
fields: fields{
option: &DeployerOption{
DomainId: "1",
Domain: "example.com",
Product: "test",
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
Certificate: applicant.Certificate{
Certificate: "",
PrivateKey: "",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q, _ := NewQiniuCDNDeployer(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, _ := NewQiniuCDNDeployer(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)
}
})
}
}

View File

@@ -4,14 +4,17 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
xpath "path"
"path/filepath"
xerrors "github.com/pkg/errors"
"github.com/pkg/sftp"
sshPkg "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type SSHDeployer struct {
@@ -30,7 +33,7 @@ func (d *SSHDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *SSHDeployer) GetInfo() []string {
func (d *SSHDeployer) GetInfos() []string {
return d.infos
}
@@ -41,52 +44,126 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
}
// 连接
client, err := d.createClient(access)
client, err := d.createSshClient(access)
if err != nil {
return err
}
defer client.Close()
d.infos = append(d.infos, toStr("ssh连接成功", nil))
d.infos = append(d.infos, toStr("SSH 连接成功", nil))
// 执行前置命令
preCommand := getDeployString(d.option.DeployConfig, "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 := d.upload(client, d.option.Certificate.Certificate, getDeployString(d.option.DeployConfig, "certPath")); err != nil {
return fmt.Errorf("failed to upload certificate: %w", err)
// 上传证书和私钥文件
switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) {
case certFormatPEM:
if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil {
return err
}
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil {
return err
}
d.infos = append(d.infos, toStr("SSH 上传私钥成功", nil))
case certFormatPFX:
pfxData, err := x509.TransformCertificateFromPEMToPFX(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
)
if err != nil {
return err
}
if err := d.writeSftpFile(client, d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil {
return err
}
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
case certFormatJKS:
jksData, err := x509.TransformCertificateFromPEMToJKS(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("jksAlias"),
d.option.DeployConfig.GetConfigAsString("jksKeypass"),
d.option.DeployConfig.GetConfigAsString("jksStorepass"),
)
if err != nil {
return err
}
if err := d.writeSftpFile(client, d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
return err
}
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
default:
return errors.New("unsupported format")
}
d.infos = append(d.infos, toStr("ssh上传证书成功", nil))
// 上传私钥
if err := d.upload(client, d.option.Certificate.PrivateKey, getDeployString(d.option.DeployConfig, "keyPath")); err != nil {
return fmt.Errorf("failed to upload private key: %w", err)
}
d.infos = append(d.infos, toStr("ssh上传私钥成功", nil))
// 执行命令
stdout, stderr, err := d.sshExecCommand(client, getDeployString(d.option.DeployConfig, "command"))
if err != nil {
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
}
command := d.option.DeployConfig.GetConfigAsString("command")
if command != "" {
stdout, stderr, err := d.sshExecCommand(client, command)
if err != nil {
return xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
}
d.infos = append(d.infos, toStr("ssh执行命令成功", stdout))
d.infos = append(d.infos, toStr("SSH 执行命令成功", stdout))
}
return nil
}
func (d *SSHDeployer) sshExecCommand(client *sshPkg.Client, command string) (string, string, error) {
session, err := client.NewSession()
func (d *SSHDeployer) createSshClient(access *domain.SSHAccess) (*ssh.Client, error) {
var authMethod ssh.AuthMethod
if access.Key != "" {
var signer ssh.Signer
var err error
if access.KeyPassphrase != "" {
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(access.Key), []byte(access.KeyPassphrase))
} else {
signer, err = ssh.ParsePrivateKey([]byte(access.Key))
}
if err != nil {
return nil, err
}
authMethod = ssh.PublicKeys(signer)
} else {
authMethod = ssh.Password(access.Password)
}
return ssh.Dial("tcp", fmt.Sprintf("%s:%s", access.Host, access.Port), &ssh.ClientConfig{
User: access.Username,
Auth: []ssh.AuthMethod{
authMethod,
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
}
func (d *SSHDeployer) sshExecCommand(sshCli *ssh.Client, command string) (string, string, error) {
session, err := sshCli.NewSession()
if err != nil {
return "", "", fmt.Errorf("failed to create ssh session: %w", err)
return "", "", xerrors.Wrap(err, "failed to create ssh session")
}
defer session.Close()
@@ -95,60 +172,38 @@ func (d *SSHDeployer) sshExecCommand(client *sshPkg.Client, command string) (str
var stderrBuf bytes.Buffer
session.Stderr = &stderrBuf
err = session.Run(command)
return stdoutBuf.String(), stderrBuf.String(), err
if err != nil {
return "", "", xerrors.Wrap(err, "failed to execute ssh script")
}
return stdoutBuf.String(), stderrBuf.String(), nil
}
func (d *SSHDeployer) upload(client *sshPkg.Client, content, path string) error {
sftpCli, err := sftp.NewClient(client)
func (d *SSHDeployer) writeSftpFileString(sshCli *ssh.Client, path string, content string) error {
return d.writeSftpFile(sshCli, path, []byte(content))
}
func (d *SSHDeployer) writeSftpFile(sshCli *ssh.Client, path string, data []byte) error {
sftpCli, err := sftp.NewClient(sshCli)
if err != nil {
return fmt.Errorf("failed to create sftp client: %w", err)
return xerrors.Wrap(err, "failed to create sftp client")
}
defer sftpCli.Close()
if err := sftpCli.MkdirAll(xpath.Dir(path)); err != nil {
return fmt.Errorf("failed to create remote directory: %w", err)
if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil {
return xerrors.Wrap(err, "failed to create remote directory")
}
file, err := sftpCli.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
if err != nil {
return fmt.Errorf("failed to open remote file: %w", err)
return xerrors.Wrap(err, "failed to open remote file")
}
defer file.Close()
_, err = file.Write([]byte(content))
_, err = file.Write(data)
if err != nil {
return fmt.Errorf("failed to write to remote file: %w", err)
return xerrors.Wrap(err, "failed to write to remote file")
}
return nil
}
func (d *SSHDeployer) createClient(access *domain.SSHAccess) (*sshPkg.Client, error) {
var authMethod sshPkg.AuthMethod
if access.Key != "" {
var signer sshPkg.Signer
var err error
if access.KeyPassphrase != "" {
signer, err = sshPkg.ParsePrivateKeyWithPassphrase([]byte(access.Key), []byte(access.KeyPassphrase))
} else {
signer, err = sshPkg.ParsePrivateKey([]byte(access.Key))
}
if err != nil {
return nil, err
}
authMethod = sshPkg.PublicKeys(signer)
} else {
authMethod = sshPkg.Password(access.Password)
}
return sshPkg.Dial("tcp", fmt.Sprintf("%s:%s", access.Host, access.Port), &sshPkg.ClientConfig{
User: access.Username,
Auth: []sshPkg.AuthMethod{
authMethod,
},
HostKeyCallback: sshPkg.InsecureIgnoreHostKey(),
})
}

View File

@@ -2,41 +2,62 @@ package deployer
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
cdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
xerrors "github.com/pkg/errors"
tcCdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"golang.org/x/exp/slices"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/rand"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCDNDeployer struct {
option *DeployerOption
credential *common.Credential
infos []string
option *DeployerOption
infos []string
sdkClients *tencentCDNDeployerSdkClients
sslUploader uploader.Uploader
}
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")
}
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,
credential: credential,
infos: make([]string, 0),
option: option,
infos: make([]string, 0),
sdkClients: clients,
sslUploader: uploader,
}, nil
}
@@ -44,101 +65,129 @@ func (d *TencentCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *TencentCDNDeployer) GetInfo() []string {
func (d *TencentCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *TencentCDNDeployer) Deploy(ctx context.Context) error {
// 上传证书
certId, err := d.uploadCert()
// 上传证书到 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
}
d.infos = append(d.infos, toStr("上传证书", certId))
if err := d.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 (d *TencentCDNDeployer) uploadCert() (string, error) {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
func (d *TencentCDNDeployer) createSdkClients(secretId, secretKey string) (*tencentCDNDeployerSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
client, _ := ssl.NewClient(d.credential, "", cpf)
request := ssl.NewUploadCertificateRequest()
request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate)
request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey)
request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6))
request.Repeatable = common.BoolPtr(false)
response, err := client.UploadCertificate(request)
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return "", fmt.Errorf("failed to upload certificate: %w", err)
return nil, err
}
return *response.Response.CertificateId, nil
cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
return &tencentCDNDeployerSdkClients{
ssl: sslClient,
cdn: cdnClient,
}, nil
}
func (d *TencentCDNDeployer) deploy(certId string) error {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
// 实例化要请求产品的client对象,clientProfile是可选的
client, _ := ssl.NewClient(d.credential, "", cpf)
// 实例化一个请求对象,每个接口都会对应一个request对象
request := ssl.NewDeployCertificateInstanceRequest()
request.CertificateId = common.StringPtr(certId)
request.ResourceType = common.StringPtr("cdn")
request.Status = common.Int64Ptr(1)
// 如果是泛域名就从cdn列表下获取SSL证书中的可用域名
domain := getDeployString(d.option.DeployConfig, "domain")
if strings.Contains(domain, "*") {
list, errGetList := d.getDomainList()
if errGetList != nil {
return fmt.Errorf("failed to get certificate domain list: %w", errGetList)
}
if list == nil || len(list) == 0 {
return fmt.Errorf("failed to get certificate domain list: empty list.")
}
request.InstanceIdList = common.StringPtrs(list)
} else { // 否则直接使用传入的域名
request.InstanceIdList = common.StringPtrs([]string{domain})
}
// 返回的resp是一个DeployCertificateInstanceResponse的实例与请求对象对应
resp, err := client.DeployCertificateInstance(request)
func (d *TencentCDNDeployer) getDomainsByCertificateId(tcCertId string) ([]string, error) {
// 获取证书中的可用域名
// REF: https://cloud.tencent.com/document/product/228/42491
describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest()
describeCertDomainsReq.CertId = common.StringPtr(tcCertId)
describeCertDomainsReq.Product = common.StringPtr("cdn")
describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq)
if err != nil {
return fmt.Errorf("failed to deploy certificate: %w", err)
}
d.infos = append(d.infos, toStr("部署证书", resp.Response))
return nil
}
func (d *TencentCDNDeployer) getDomainList() ([]string, error) {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "cdn.tencentcloudapi.com"
client, _ := cdn.NewClient(d.credential, "", cpf)
request := cdn.NewDescribeCertDomainsRequest()
cert := base64.StdEncoding.EncodeToString([]byte(d.option.Certificate.Certificate))
request.Cert = &cert
response, err := client.DescribeCertDomains(request)
if err != nil {
return nil, fmt.Errorf("failed to get domain list: %w", err)
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'")
}
domains := make([]string, 0)
for _, domain := range response.Response.Domains {
domains = append(domains, *domain)
if describeCertDomainsResp.Response.Domains == nil {
for _, domain := range describeCertDomainsResp.Response.Domains {
domains = append(domains, *domain)
}
}
return domains, nil
}
func (d *TencentCDNDeployer) getDeployedDomainsByCertificateId(tcCertId string) ([]string, error) {
// 根据证书查询关联 CDN 域名
// REF: https://cloud.tencent.com/document/product/400/62674
describeDeployedResourcesReq := tcSsl.NewDescribeDeployedResourcesRequest()
describeDeployedResourcesReq.CertificateIds = common.StringPtrs([]string{tcCertId})
describeDeployedResourcesReq.ResourceType = common.StringPtr("cdn")
describeDeployedResourcesResp, err := d.sdkClients.ssl.DescribeDeployedResources(describeDeployedResourcesReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeDeployedResources'")
}
domains := make([]string, 0)
if describeDeployedResourcesResp.Response.DeployedResources != nil {
for _, deployedResource := range describeDeployedResourcesResp.Response.DeployedResources {
for _, resource := range deployedResource.Resources {
domains = append(domains, *resource)
}
}
}
return domains, nil

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View File

@@ -7,6 +7,8 @@ import (
"fmt"
"net/http"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
xhttp "github.com/usual2970/certimate/internal/utils/http"
)
@@ -27,7 +29,7 @@ func (d *WebhookDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *WebhookDeployer) GetInfo() []string {
func (d *WebhookDeployer) GetInfos() []string {
return d.infos
}
@@ -41,26 +43,24 @@ type webhookData struct {
func (d *WebhookDeployer) Deploy(ctx context.Context) error {
access := &domain.WebhookAccess{}
if err := json.Unmarshal([]byte(d.option.Access), access); err != nil {
return fmt.Errorf("failed to parse hook access config: %w", err)
return xerrors.Wrap(err, "failed to get access")
}
data := &webhookData{
Domain: d.option.Domain,
Certificate: d.option.Certificate.Certificate,
PrivateKey: d.option.Certificate.PrivateKey,
Variables: getDeployVariables(d.option.DeployConfig),
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")
}
d.infos = append(d.infos, toStr("webhook response", string(resp)))
d.infos = append(d.infos, toStr("Webhook Response", string(resp)))
return nil
}

View File

@@ -5,21 +5,31 @@ 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 {
Region string `json:"region"`
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
Region string `json:"region"`
HostedZoneId string `json:"hostedZoneId"`
}
@@ -32,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"`
}
@@ -46,6 +61,16 @@ type PdnsAccess struct {
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"`

View 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
}

View File

@@ -1,11 +1,19 @@
package domain
import (
"encoding/json"
"strings"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
)
type ApplyConfig struct {
Email string `json:"email"`
Access string `json:"access"`
KeyAlgorithm string `json:"keyAlgorithm"`
Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"`
Email string `json:"email"`
Access string `json:"access"`
KeyAlgorithm string `json:"keyAlgorithm"`
Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"`
DisableFollowCNAME bool `json:"disableFollowCNAME"`
}
type DeployConfig struct {
@@ -15,6 +23,121 @@ type DeployConfig struct {
Config map[string]any `json:"config"`
}
// 以字符串形式获取配置项。
//
// 入参:
// - key: 配置项的键。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回空字符串。
func (dc *DeployConfig) GetConfigAsString(key string) string {
return maps.GetValueAsString(dc.Config, key)
}
// 以字符串形式获取配置项。
//
// 入参:
// - key: 配置项的键。
// - defaultValue: 默认值。
//
// 出参:
// - 配置项的值。如果配置项不存在、类型不是字符串或者值为零值,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue string) string {
return maps.GetValueOrDefaultAsString(dc.Config, key, defaultValue)
}
// 以 32 位整数形式获取配置项。
//
// 入参:
// - key: 配置项的键。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回 0。
func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
return maps.GetValueAsInt32(dc.Config, key)
}
// 以 32 位整数形式获取配置项。
//
// 入参:
// - key: 配置项的键。
// - defaultValue: 默认值。
//
// 出参:
// - 配置项的值。如果配置项不存在、类型不是 32 位整数或者值为零值,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 {
return maps.GetValueOrDefaultAsInt32(dc.Config, key, defaultValue)
}
// 以布尔形式获取配置项。
//
// 入参:
// - key: 配置项的键。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回 false。
func (dc *DeployConfig) GetConfigAsBool(key string) bool {
return maps.GetValueAsBool(dc.Config, key)
}
// 以布尔形式获取配置项。
//
// 入参:
// - key: 配置项的键。
// - defaultValue: 默认值。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool) bool {
return maps.GetValueOrDefaultAsBool(dc.Config, key, defaultValue)
}
// 以变量字典形式获取配置项。
//
// 出参:
// - 变量字典。
func (dc *DeployConfig) GetConfigAsVariables() map[string]string {
rs := make(map[string]string)
if dc.Config != nil {
value, ok := dc.Config["variables"]
if !ok {
return rs
}
kvs := make([]KV, 0)
bts, _ := json.Marshal(value)
if err := json.Unmarshal(bts, &kvs); err != nil {
return rs
}
for _, kv := range kvs {
rs[kv.Key] = kv.Value
}
}
return rs
}
// GetDomain returns the domain from the deploy config
// if the domain is a wildcard domain, and wildcard is true, return the wildcard domain
func (dc *DeployConfig) GetDomain(wildcard ...bool) string {
val := dc.GetConfigAsString("domain")
if val == "" {
return ""
}
if !strings.HasPrefix(val, "*") {
return val
}
if len(wildcard) > 0 && wildcard[0] {
return val
}
return strings.TrimPrefix(val, "*")
}
type KV struct {
Key string `json:"key"`
Value string `json:"value"`

View File

@@ -1,10 +1,13 @@
package domain
const (
NotifyChannelDingtalk = "dingtalk"
NotifyChannelWebhook = "webhook"
NotifyChannelTelegram = "telegram"
NotifyChannelLark = "lark"
NotifyChannelEmail = "email"
NotifyChannelWebhook = "webhook"
NotifyChannelDingtalk = "dingtalk"
NotifyChannelLark = "lark"
NotifyChannelTelegram = "telegram"
NotifyChannelServerChan = "serverchan"
NotifyChannelBark = "bark"
)
type NotifyTestPushReq struct {

View File

@@ -24,7 +24,7 @@ func (s *Setting) GetChannelContent(channel string) (map[string]any, error) {
v, ok := (*conf)[channel]
if !ok {
return nil, fmt.Errorf("channel %s not found", channel)
return nil, fmt.Errorf("channel \"%s\" not found", channel)
}
return v, nil

View File

@@ -2,14 +2,22 @@ package domains
import (
"context"
"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 {
@@ -45,7 +55,10 @@ func deploy(ctx context.Context, record *models.Record) error {
cert := currRecord.GetString("certificate")
expiredAt := currRecord.GetDateTime("expiredAt").Time()
if cert != "" && time.Until(expiredAt) > time.Hour*24*10 && currRecord.GetBool("deployed") {
// 检查证书是否包含设置的所有域名
changed := isCertChanged(cert, currRecord)
if cert != "" && time.Until(expiredAt) > validityDuration && currRecord.GetBool("deployed") && !changed {
app.GetApp().Logger().Info("证书在有效期内")
history.record(checkPhase, "证书在有效期内且已部署,跳过", &RecordInfo{
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
@@ -60,7 +73,7 @@ func deploy(ctx context.Context, record *models.Record) error {
// ############2.申请证书
history.record(applyPhase, "开始申请", nil)
if cert != "" && time.Until(expiredAt) > time.Hour*24 {
if cert != "" && time.Until(expiredAt) > validityDuration && !changed {
history.record(applyPhase, "证书在有效期内,跳过", &RecordInfo{
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
})
@@ -105,11 +118,11 @@ func deploy(ctx context.Context, record *models.Record) error {
if err = deployer.Deploy(ctx); err != nil {
app.GetApp().Logger().Error("部署失败", "err", err)
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfo()})
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfos()})
return err
}
history.record(deployPhase, fmt.Sprintf("[%s]-部署成功", deployer.GetID()), &RecordInfo{
Info: deployer.GetInfo(),
Info: deployer.GetInfos(),
}, false)
}
@@ -121,3 +134,80 @@ func deploy(ctx context.Context, record *models.Record) error {
return nil
}
func isCertChanged(certificate string, record *models.Record) bool {
// 如果证书为空直接返回true
if certificate == "" {
return true
}
// 解析证书
cert, err := x509.ParseCertificateFromPEM(certificate)
if err != nil {
app.GetApp().Logger().Error("解析证书失败", "err", err)
return true
}
// 遍历域名列表检查是否都在证书中找到第一个不存在证书中域名时提前返回true
for _, domain := range strings.Split(record.GetString("domain"), ";") {
if !slices.Contains(cert.DNSNames, domain) && !slices.Contains(cert.DNSNames, "*."+removeLastSubdomain(domain)) {
return true
}
}
// 解析applyConfig
applyConfig := &domain.ApplyConfig{}
record.UnmarshalJSONField("applyConfig", applyConfig)
// 检查证书加密算法是否变更
switch pubkey := cert.PublicKey.(type) {
case *rsa.PublicKey:
bitSize := pubkey.N.BitLen()
switch bitSize {
case 2048:
// RSA2048
if applyConfig.KeyAlgorithm != "" && applyConfig.KeyAlgorithm != "RSA2048" {
return true
}
case 3072:
// RSA3072
if applyConfig.KeyAlgorithm != "RSA3072" {
return true
}
case 4096:
// RSA4096
if applyConfig.KeyAlgorithm != "RSA4096" {
return true
}
case 8192:
// RSA8192
if applyConfig.KeyAlgorithm != "RSA8192" {
return true
}
}
case *ecdsa.PublicKey:
bitSize := pubkey.Curve.Params().BitSize
switch bitSize {
case 256:
// EC256
if applyConfig.KeyAlgorithm != "EC256" {
return true
}
case 384:
// EC384
if applyConfig.KeyAlgorithm != "EC384" {
return true
}
}
}
return false
}
func removeLastSubdomain(domain string) string {
parts := strings.Split(domain, ".")
if len(parts) > 1 {
return strings.Join(parts[1:], ".")
}
return domain
}

View File

@@ -12,19 +12,13 @@ import (
"github.com/usual2970/certimate/internal/utils/xtime"
)
type msg struct {
subject string
message string
}
const (
defaultExpireSubject = "您有{COUNT}张证书即将过期"
defaultExpireMsg = "有{COUNT}张证书即将过期,域名分别为{DOMAINS},请保持关注!"
defaultExpireSubject = "您有 {COUNT} 张证书即将过期"
defaultExpireMessage = "有 {COUNT} 张证书即将过期域名分别为 {DOMAINS}请保持关注!"
)
func PushExpireMsg() {
// 查询即将过期的证书
records, err := app.GetApp().Dao().FindRecordsByFilter("domains", "expiredAt<{:time}&&certUrl!=''", "-created", 500, 0,
dbx.Params{"time": xtime.GetTimeAfter(24 * time.Hour * 15)})
if err != nil {
@@ -34,12 +28,12 @@ func PushExpireMsg() {
// 组装消息
msg := buildMsg(records)
if msg == nil {
return
}
if err := Send(msg.subject, msg.message); err != nil {
// 发送通知
if err := SendToAllChannels(msg.Subject, msg.Message); err != nil {
app.GetApp().Logger().Error("send expire msg", "error", err)
}
}
@@ -53,22 +47,27 @@ type notifyTemplate struct {
Content string `json:"content"`
}
func buildMsg(records []*models.Record) *msg {
type notifyMessage struct {
Subject string
Message string
}
func buildMsg(records []*models.Record) *notifyMessage {
if len(records) == 0 {
return nil
}
// 查询模板信息
templateRecord, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='templates'")
title := defaultExpireSubject
content := defaultExpireMsg
subject := defaultExpireSubject
message := defaultExpireMessage
if err == nil {
var templates *notifyTemplates
templateRecord.UnmarshalJSONField("content", templates)
if templates != nil && len(templates.NotifyTemplates) > 0 {
title = templates.NotifyTemplates[0].Title
content = templates.NotifyTemplates[0].Content
subject = templates.NotifyTemplates[0].Title
message = templates.NotifyTemplates[0].Content
}
}
@@ -81,17 +80,17 @@ func buildMsg(records []*models.Record) *msg {
}
countStr := strconv.Itoa(count)
domainStr := strings.Join(domains, ",")
domainStr := strings.Join(domains, ";")
title = strings.ReplaceAll(title, "{COUNT}", countStr)
title = strings.ReplaceAll(title, "{DOMAINS}", domainStr)
subject = strings.ReplaceAll(subject, "{COUNT}", countStr)
subject = strings.ReplaceAll(subject, "{DOMAINS}", domainStr)
content = strings.ReplaceAll(content, "{COUNT}", countStr)
content = strings.ReplaceAll(content, "{DOMAINS}", domainStr)
message = strings.ReplaceAll(message, "{COUNT}", countStr)
message = strings.ReplaceAll(message, "{DOMAINS}", domainStr)
// 返回消息
return &msg{
subject: title,
message: content,
return &notifyMessage{
Subject: subject,
Message: message,
}
}

View 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)
}

View File

@@ -3,21 +3,16 @@ package notify
import (
"context"
"fmt"
"strconv"
"github.com/usual2970/certimate/internal/domain"
"golang.org/x/sync/errgroup"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
"github.com/usual2970/certimate/internal/utils/app"
notifyPackage "github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/dingding"
"github.com/nikoksr/notify/service/http"
"github.com/nikoksr/notify/service/lark"
"github.com/nikoksr/notify/service/telegram"
)
func Send(title, content string) error {
// 获取所有的推送渠道
notifiers, err := getNotifiers()
func SendToAllChannels(subject, message string) error {
notifiers, err := getEnabledNotifiers()
if err != nil {
return err
}
@@ -25,136 +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
}
type sendTestParam struct {
Title string `json:"title"`
Content string `json:"content"`
Channel string `json:"channel"`
Conf map[string]any `json:"conf"`
}
func SendTest(param *sendTestParam) error {
notifier, err := getNotifier(param.Channel, param.Conf)
func SendToChannel(subject, message string, channel string, channelConfig map[string]any) error {
notifier, err := createNotifier(channel, channelConfig)
if err != nil {
return err
}
n := notifyPackage.New()
// 添加推送渠道
n.UseServices(notifier)
// 发送消息
return n.Send(context.Background(), param.Title, param.Content)
_, err = notifier.Notify(context.Background(), subject, message)
return err
}
func getNotifiers() ([]notifyPackage.Notifier, error) {
resp, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='notifyChannels'")
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
}
notifier, err := getNotifier(k, v)
notifier, err := createNotifier(k, v)
if err != nil {
continue
}
notifiers = append(notifiers, notifier)
}
return notifiers, nil
}
func getNotifier(channel string, conf map[string]any) (notifyPackage.Notifier, error) {
switch channel {
case domain.NotifyChannelTelegram:
temp := getTelegramNotifier(conf)
if temp == nil {
return nil, fmt.Errorf("telegram notifier config error")
}
return temp, nil
case domain.NotifyChannelDingtalk:
return getDingTalkNotifier(conf), nil
case domain.NotifyChannelLark:
return getLarkNotifier(conf), nil
case domain.NotifyChannelWebhook:
return getWebhookNotifier(conf), nil
}
return nil, fmt.Errorf("notifier not found")
}
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 getLarkNotifier(conf map[string]any) notifyPackage.Notifier {
return lark.NewWebhookService(getString(conf, "webhookUrl"))
}
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)
}

View File

@@ -29,18 +29,13 @@ func NewNotifyService(settingRepo SettingRepository) *NotifyService {
func (n *NotifyService) Test(ctx context.Context, req *domain.NotifyTestPushReq) error {
setting, err := n.settingRepo.GetByName(ctx, "notifyChannels")
if err != nil {
return fmt.Errorf("get notify channels setting failed: %w", err)
return fmt.Errorf("failed to get notify channels settings: %w", err)
}
conf, err := setting.GetChannelContent(req.Channel)
channelConfig, err := setting.GetChannelContent(req.Channel)
if err != nil {
return fmt.Errorf("get notify channel %s config failed: %w", req.Channel, err)
return fmt.Errorf("failed to get notify channel \"%s\" config: %w", req.Channel, err)
}
return SendTest(&sendTestParam{
Title: notifyTestTitle,
Content: notifyTestBody,
Channel: req.Channel,
Conf: conf,
})
return SendToChannel(notifyTestTitle, notifyTestBody, req.Channel, channelConfig)
}

View 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"`
}

View 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{}
}

View 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()))
}
})
}

View 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
}

View File

@@ -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)
})
}

View File

@@ -0,0 +1,10 @@
package aliyunalb
type DeployResourceType string
const (
// 资源类型:部署到指定负载均衡器。
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
)

View File

@@ -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
}

View File

@@ -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)
})
}

View 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
}

View File

@@ -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)
})
}

View File

@@ -0,0 +1,10 @@
package aliyunclb
type DeployResourceType string
const (
// 资源类型:部署到指定负载均衡器。
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
)

View File

@@ -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
}

View File

@@ -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)
})
}

View 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
}

View File

@@ -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)
})
}

View File

@@ -0,0 +1,10 @@
package aliyunnlb
type DeployResourceType string
const (
// 资源类型:部署到指定负载均衡器。
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
)

View 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
}

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -0,0 +1,80 @@
package huaweicloudcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fRegion string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v huaweicloud_cdn_test.go -args \
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_REGION="cn-north-1" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.HuaweiCloudCDNDeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Region: fRegion,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,12 @@
package huaweicloudelb
type DeployResourceType string
const (
// 资源类型:替换指定证书。
DEPLOY_RESOURCE_CERTIFICATE = DeployResourceType("certificate")
// 资源类型:部署到指定负载均衡器。
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
)

View File

@@ -0,0 +1,395 @@
package huaweicloudelb
import (
"context"
"errors"
"fmt"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
hcElb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3"
hcElbModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model"
hcElbRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region"
hcIam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3"
hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model"
hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region"
xerrors "github.com/pkg/errors"
"golang.org/x/exp/slices"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerElb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-elb"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
)
type HuaweiCloudELBDeployerConfig struct {
// 华为云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 华为云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 华为云地域。
Region string `json:"region"`
// 部署资源类型。
ResourceType DeployResourceType `json:"resourceType"`
// 证书 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_CERTIFICATE] 时必填。
CertificateId string `json:"certificateId,omitempty"`
// 负载均衡器 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_LOADBALANCER] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_LISTENER] 时必填。
ListenerId string `json:"listenerId,omitempty"`
}
type HuaweiCloudELBDeployer struct {
config *HuaweiCloudELBDeployerConfig
logger deployer.Logger
sdkClient *hcElb.ElbClient
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*HuaweiCloudELBDeployer)(nil)
func New(config *HuaweiCloudELBDeployerConfig) (*HuaweiCloudELBDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *HuaweiCloudELBDeployerConfig, logger deployer.Logger) (*HuaweiCloudELBDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := providerElb.New(&providerElb.HuaweiCloudELBUploaderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
Region: config.Region,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &HuaweiCloudELBDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *HuaweiCloudELBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 SCM
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case DEPLOY_RESOURCE_CERTIFICATE:
if err := d.deployToCertificate(ctx, certPem, privkeyPem); err != nil {
return nil, err
}
case DEPLOY_RESOURCE_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, certPem, privkeyPem); err != nil {
return nil, err
}
case DEPLOY_RESOURCE_LISTENER:
if err := d.deployToListener(ctx, certPem, privkeyPem); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
}
return &deployer.DeployResult{}, nil
}
func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context, certPem string, privkeyPem string) error {
if d.config.CertificateId == "" {
return errors.New("config `certificateId` is required")
}
// 更新证书
// REF: https://support.huaweicloud.com/api-elb/UpdateCertificate.html
updateCertificateReq := &hcElbModel.UpdateCertificateRequest{
CertificateId: d.config.CertificateId,
Body: &hcElbModel.UpdateCertificateRequestBody{
Certificate: &hcElbModel.UpdateCertificateOption{
Certificate: cast.StringPtr(certPem),
PrivateKey: cast.StringPtr(privkeyPem),
},
},
}
updateCertificateResp, err := d.sdkClient.UpdateCertificate(updateCertificateReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateCertificate'")
}
d.logger.Logt("已更新 ELB 证书", updateCertificateResp)
return nil
}
func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context, certPem string, privkeyPem string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
listenerIds := make([]string, 0)
// 查询负载均衡器详情
// REF: https://support.huaweicloud.com/api-elb/ShowLoadBalancer.html
showLoadBalancerReq := &hcElbModel.ShowLoadBalancerRequest{
LoadbalancerId: d.config.LoadbalancerId,
}
showLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowLoadBalancer'")
}
d.logger.Logt("已查询到 ELB 负载均衡器", showLoadBalancerResp)
// 查询监听器列表
// REF: https://support.huaweicloud.com/api-elb/ListListeners.html
listListenersLimit := int32(2000)
var listListenersMarker *string = nil
for {
listListenersReq := &hcElbModel.ListListenersRequest{
Limit: cast.Int32Ptr(listListenersLimit),
Marker: listListenersMarker,
Protocol: &[]string{"HTTPS", "TERMINATED_HTTPS"},
LoadbalancerId: &[]string{showLoadBalancerResp.Loadbalancer.Id},
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListListeners'")
}
if listListenersResp.Listeners != nil {
for _, listener := range *listListenersResp.Listeners {
listenerIds = append(listenerIds, listener.Id)
}
}
if listListenersResp.Listeners == nil || len(*listListenersResp.Listeners) < int(listListenersLimit) {
break
} else {
listListenersMarker = listListenersResp.PageInfo.NextMarker
}
}
d.logger.Logt("已查询到 ELB 负载均衡器下的监听器", listenerIds)
// 上传证书到 SCM
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 批量更新监听器证书
var errs []error
for _, listenerId := range listenerIds {
if err := d.modifyListenerCertificate(ctx, listenerId, upres.CertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context, certPem string, privkeyPem string) error {
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 上传证书到 SCM
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 更新监听器证书
if err := d.modifyListenerCertificate(ctx, d.config.ListenerId, upres.CertId); err != nil {
return err
}
return nil
}
func (d *HuaweiCloudELBDeployer) modifyListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
// 查询监听器详情
// REF: https://support.huaweicloud.com/api-elb/ShowListener.html
showListenerReq := &hcElbModel.ShowListenerRequest{
ListenerId: cloudListenerId,
}
showListenerResp, err := d.sdkClient.ShowListener(showListenerReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowListener'")
}
d.logger.Logt("已查询到 ELB 监听器", showListenerResp)
// 更新监听器
// REF: https://support.huaweicloud.com/api-elb/UpdateListener.html
updateListenerReq := &hcElbModel.UpdateListenerRequest{
ListenerId: cloudListenerId,
Body: &hcElbModel.UpdateListenerRequestBody{
Listener: &hcElbModel.UpdateListenerOption{
DefaultTlsContainerRef: cast.StringPtr(cloudCertId),
},
},
}
if showListenerResp.Listener.SniContainerRefs != nil {
if len(showListenerResp.Listener.SniContainerRefs) > 0 {
// 如果开启 SNI需替换同 SAN 的证书
sniCertIds := make([]string, 0)
sniCertIds = append(sniCertIds, cloudCertId)
listOldCertificateReq := &hcElbModel.ListCertificatesRequest{
Id: &showListenerResp.Listener.SniContainerRefs,
}
listOldCertificateResp, err := d.sdkClient.ListCertificates(listOldCertificateReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListCertificates'")
}
showNewCertificateReq := &hcElbModel.ShowCertificateRequest{
CertificateId: cloudCertId,
}
showNewCertificateResp, err := d.sdkClient.ShowCertificate(showNewCertificateReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowCertificate'")
}
for _, certificate := range *listOldCertificateResp.Certificates {
oldCertificate := certificate
newCertificate := showNewCertificateResp.Certificate
if oldCertificate.SubjectAlternativeNames != nil && newCertificate.SubjectAlternativeNames != nil {
if slices.Equal(*oldCertificate.SubjectAlternativeNames, *newCertificate.SubjectAlternativeNames) {
continue
}
} else {
if oldCertificate.Domain == newCertificate.Domain {
continue
}
}
sniCertIds = append(sniCertIds, certificate.Id)
}
updateListenerReq.Body.Listener.SniContainerRefs = &sniCertIds
}
if showListenerResp.Listener.SniMatchAlgo != "" {
updateListenerReq.Body.Listener.SniMatchAlgo = cast.StringPtr(showListenerResp.Listener.SniMatchAlgo)
}
}
updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateListener'")
}
d.logger.Logt("已更新 ELB 监听器", updateListenerResp)
return nil
}
func createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) {
if region == "" {
region = "cn-north-4" // ELB 服务默认区域:华北四北京
}
projectId, err := getSdkProjectId(accessKeyId, secretAccessKey, region)
if err != nil {
return nil, err
}
auth, err := basic.NewCredentialsBuilder().
WithAk(accessKeyId).
WithSk(secretAccessKey).
WithProjectId(projectId).
SafeBuild()
if err != nil {
return nil, err
}
hcRegion, err := hcElbRegion.SafeValueOf(region)
if err != nil {
return nil, err
}
hcClient, err := hcElb.ElbClientBuilder().
WithRegion(hcRegion).
WithCredential(auth).
SafeBuild()
if err != nil {
return nil, err
}
client := hcElb.NewElbClient(hcClient)
return client, nil
}
func getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) {
if region == "" {
region = "cn-north-4" // IAM 服务默认区域:华北四北京
}
auth, err := global.NewCredentialsBuilder().
WithAk(accessKeyId).
WithSk(secretAccessKey).
SafeBuild()
if err != nil {
return "", err
}
hcRegion, err := hcIamRegion.SafeValueOf(region)
if err != nil {
return "", err
}
hcClient, err := hcIam.IamClientBuilder().
WithRegion(hcRegion).
WithCredential(auth).
SafeBuild()
if err != nil {
return "", err
}
client := hcIam.NewIamClient(hcClient)
request := &hcIamModel.KeystoneListProjectsRequest{
Name: &region,
}
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
}

View File

@@ -0,0 +1,155 @@
package huaweicloudelb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-elb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fRegion string
fCertificateId string
fLoadbalancerId string
fListenerId string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_HUAWEICLOUDELB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
}
/*
Shell command to run this test:
go test -v huaweicloud_elb_test.go -args \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_REGION="cn-north-1" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_CERTIFICATEID="your-elb-cert-id" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_LOADBALANCERID="your-elb-loadbalancer-id" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_LISTENERID="your-elb-listener-id"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToCertificate", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
}, "\n"))
deployer, err := provider.New(&provider.HuaweiCloudELBDeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_CERTIFICATE,
CertificateId: fCertificateId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
}, "\n"))
deployer, err := provider.New(&provider.HuaweiCloudELBDeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
LoadbalancerId: fLoadbalancerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToListenerId", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LISTENERID: %v", fListenerId),
}, "\n"))
deployer, err := provider.New(&provider.HuaweiCloudELBDeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
ListenerId: fListenerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,160 @@
package k8ssecret
import (
"context"
"errors"
"strings"
xerrors "github.com/pkg/errors"
k8sCore "k8s.io/api/core/v1"
k8sMeta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type K8sSecretDeployerConfig struct {
// kubeconfig 文件内容。
KubeConfig string `json:"kubeConfig,omitempty"`
// K8s 命名空间。
Namespace string `json:"namespace,omitempty"`
// K8s Secret 名称。
SecretName string `json:"secretName"`
// K8s Secret 中用于存放证书的 Key。
SecretDataKeyForCrt string `json:"secretDataKeyForCrt,omitempty"`
// K8s Secret 中用于存放私钥的 Key。
SecretDataKeyForKey string `json:"secretDataKeyForKey,omitempty"`
}
type K8sSecretDeployer struct {
config *K8sSecretDeployerConfig
logger deployer.Logger
}
var _ deployer.Deployer = (*K8sSecretDeployer)(nil)
func New(config *K8sSecretDeployerConfig) (*K8sSecretDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *K8sSecretDeployerConfig, logger deployer.Logger) (*K8sSecretDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
return &K8sSecretDeployer{
logger: logger,
config: config,
}, nil
}
func (d *K8sSecretDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
if d.config.Namespace == "" {
return nil, errors.New("config `namespace` is required")
}
if d.config.SecretName == "" {
return nil, errors.New("config `secretName` is required")
}
if d.config.SecretDataKeyForCrt == "" {
return nil, errors.New("config `secretDataKeyForCrt` is required")
}
if d.config.SecretDataKeyForKey == "" {
return nil, errors.New("config `secretDataKeyForKey` is required")
}
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 连接
client, err := createK8sClient(d.config.KubeConfig)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create k8s client")
}
var secretPayload *k8sCore.Secret
secretAnnotations := map[string]string{
"certimate/common-name": certX509.Subject.CommonName,
"certimate/subject-sn": certX509.Subject.SerialNumber,
"certimate/subject-alt-names": strings.Join(certX509.DNSNames, ","),
"certimate/issuer-sn": certX509.Issuer.SerialNumber,
"certimate/issuer-org": strings.Join(certX509.Issuer.Organization, ","),
}
// 获取 Secret 实例,如果不存在则创建
secretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Get(context.TODO(), d.config.SecretName, k8sMeta.GetOptions{})
if err != nil {
secretPayload = &k8sCore.Secret{
TypeMeta: k8sMeta.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: k8sMeta.ObjectMeta{
Name: d.config.SecretName,
Annotations: secretAnnotations,
},
Type: k8sCore.SecretType("kubernetes.io/tls"),
}
secretPayload.Data = make(map[string][]byte)
secretPayload.Data[d.config.SecretDataKeyForCrt] = []byte(certPem)
secretPayload.Data[d.config.SecretDataKeyForKey] = []byte(privkeyPem)
_, err = client.CoreV1().Secrets(d.config.Namespace).Create(context.TODO(), secretPayload, k8sMeta.CreateOptions{})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create k8s secret")
} else {
d.logger.Logf("k8s secret created", secretPayload)
return &deployer.DeployResult{}, nil
}
}
// 更新 Secret 实例
secretPayload.Type = k8sCore.SecretType("kubernetes.io/tls")
if secretPayload.ObjectMeta.Annotations == nil {
secretPayload.ObjectMeta.Annotations = secretAnnotations
} else {
for k, v := range secretAnnotations {
secretPayload.ObjectMeta.Annotations[k] = v
}
}
secretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Update(context.TODO(), secretPayload, k8sMeta.UpdateOptions{})
if err != nil {
return nil, xerrors.Wrap(err, "failed to update k8s secret")
}
d.logger.Logf("k8s secret updated", secretPayload)
return &deployer.DeployResult{}, nil
}
func createK8sClient(kubeConfig string) (*kubernetes.Clientset, error) {
var config *rest.Config
var err error
if kubeConfig == "" {
config, err = rest.InClusterConfig()
} else {
kubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(kubeConfig))
if err != nil {
return nil, err
}
config, err = kubeConfig.ClientConfig()
}
if err != nil {
return nil, err
}
client, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -0,0 +1,80 @@
package k8ssecret_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/k8s-secret"
)
var (
fInputCertPath string
fInputKeyPath string
fNamespace string
fSecretName string
fSecretDataKeyForCrt string
fSecretDataKeyForKey string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_K8SSECRET_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fNamespace, argsPrefix+"NAMESPACE", "default", "")
flag.StringVar(&fSecretName, argsPrefix+"SECRETNAME", "", "")
flag.StringVar(&fSecretDataKeyForCrt, argsPrefix+"SECRETDATAKEYFORCRT", "tls.crt", "")
flag.StringVar(&fSecretDataKeyForKey, argsPrefix+"SECRETDATAKEYFORKEY", "tls.key", "")
}
/*
Shell command to run this test:
go test -v k8s_secret_test.go -args \
--CERTIMATE_DEPLOYER_K8SSECRET_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_K8SSECRET_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_K8SSECRET_NAMESPACE="default" \
--CERTIMATE_DEPLOYER_K8SSECRET_SECRETNAME="secret" \
--CERTIMATE_DEPLOYER_K8SSECRET_SECRETDATAKEYFORCRT="tls.crt" \
--CERTIMATE_DEPLOYER_K8SSECRET_SECRETDATAKEYFORKEY="tls.key"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("NAMESPACE: %v", fNamespace),
fmt.Sprintf("SECRETNAME: %v", fSecretName),
fmt.Sprintf("SECRETDATAKEYFORCRT: %v", fSecretDataKeyForCrt),
fmt.Sprintf("SECRETDATAKEYFORKEY: %v", fSecretDataKeyForKey),
}, "\n"))
deployer, err := provider.New(&provider.K8sSecretDeployerConfig{
Namespace: fNamespace,
SecretName: fSecretName,
SecretDataKeyForCrt: fSecretDataKeyForCrt,
SecretDataKeyForKey: fSecretDataKeyForKey,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,17 @@
package local
type OutputFormatType string
const (
OUTPUT_FORMAT_PEM = OutputFormatType("PEM")
OUTPUT_FORMAT_PFX = OutputFormatType("PFX")
OUTPUT_FORMAT_JKS = OutputFormatType("JKS")
)
type ShellEnvType string
const (
SHELL_ENV_SH = ShellEnvType("sh")
SHELL_ENV_CMD = ShellEnvType("cmd")
SHELL_ENV_POWERSHELL = ShellEnvType("powershell")
)

View File

@@ -0,0 +1,178 @@
package local
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"runtime"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/utils/fs"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type LocalDeployerConfig struct {
// Shell 执行环境。
// 零值时默认根据操作系统决定。
ShellEnv ShellEnvType `json:"shellEnv,omitempty"`
// 前置命令。
PreCommand string `json:"preCommand,omitempty"`
// 后置命令。
PostCommand string `json:"postCommand,omitempty"`
// 输出证书格式。
OutputFormat OutputFormatType `json:"outputFormat,omitempty"`
// 输出证书文件路径。
OutputCertPath string `json:"outputCertPath,omitempty"`
// 输出私钥文件路径。
OutputKeyPath string `json:"outputKeyPath,omitempty"`
// PFX 导出密码。
// 证书格式为 PFX 时必填。
PfxPassword string `json:"pfxPassword,omitempty"`
// JKS 别名。
// 证书格式为 JKS 时必填。
JksAlias string `json:"jksAlias,omitempty"`
// JKS 密钥密码。
// 证书格式为 JKS 时必填。
JksKeypass string `json:"jksKeypass,omitempty"`
// JKS 存储密码。
// 证书格式为 JKS 时必填。
JksStorepass string `json:"jksStorepass,omitempty"`
}
type LocalDeployer struct {
config *LocalDeployerConfig
logger deployer.Logger
}
var _ deployer.Deployer = (*LocalDeployer)(nil)
func New(config *LocalDeployerConfig) (*LocalDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *LocalDeployerConfig, logger deployer.Logger) (*LocalDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
return &LocalDeployer{
logger: logger,
config: config,
}, nil
}
func (d *LocalDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 执行前置命令
if d.config.PreCommand != "" {
stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PreCommand)
if err != nil {
return nil, xerrors.Wrapf(err, "failed to run pre-command, stdout: %s, stderr: %s", stdout, stderr)
}
d.logger.Logt("pre-command executed", stdout)
}
// 写入证书和私钥文件
switch d.config.OutputFormat {
case OUTPUT_FORMAT_PEM:
if err := fs.WriteFileString(d.config.OutputCertPath, certPem); err != nil {
return nil, xerrors.Wrap(err, "failed to save certificate file")
}
d.logger.Logt("certificate file saved")
if err := fs.WriteFileString(d.config.OutputKeyPath, privkeyPem); err != nil {
return nil, xerrors.Wrap(err, "failed to save private key file")
}
d.logger.Logt("private key file saved")
case OUTPUT_FORMAT_PFX:
pfxData, err := x509.TransformCertificateFromPEMToPFX(certPem, privkeyPem, d.config.PfxPassword)
if err != nil {
return nil, xerrors.Wrap(err, "failed to transform certificate to PFX")
}
d.logger.Logt("certificate transformed to PFX")
if err := fs.WriteFile(d.config.OutputCertPath, pfxData); err != nil {
return nil, xerrors.Wrap(err, "failed to save certificate file")
}
d.logger.Logt("certificate file saved")
case OUTPUT_FORMAT_JKS:
jksData, err := x509.TransformCertificateFromPEMToJKS(certPem, privkeyPem, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)
if err != nil {
return nil, xerrors.Wrap(err, "failed to transform certificate to JKS")
}
d.logger.Logt("certificate transformed to JKS")
if err := fs.WriteFile(d.config.OutputCertPath, jksData); err != nil {
return nil, xerrors.Wrap(err, "failed to save certificate file")
}
d.logger.Logt("certificate file uploaded")
default:
return nil, fmt.Errorf("unsupported output format: %s", d.config.OutputFormat)
}
// 执行后置命令
if d.config.PostCommand != "" {
stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PostCommand)
if err != nil {
return nil, xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
}
d.logger.Logt("post-command executed", stdout)
}
return &deployer.DeployResult{}, nil
}
func execCommand(shellEnv ShellEnvType, command string) (string, string, error) {
var cmd *exec.Cmd
switch shellEnv {
case SHELL_ENV_SH:
cmd = exec.Command("sh", "-c", command)
case SHELL_ENV_CMD:
cmd = exec.Command("cmd", "/C", command)
case SHELL_ENV_POWERSHELL:
cmd = exec.Command("powershell", "-Command", command)
case "":
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/C", command)
} else {
cmd = exec.Command("sh", "-c", command)
}
default:
return "", "", fmt.Errorf("unsupported shell env: %s", shellEnv)
}
var stdoutBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
var stderrBuf bytes.Buffer
cmd.Stderr = &stderrBuf
err := cmd.Run()
if err != nil {
return "", "", xerrors.Wrap(err, "failed to execute shell command")
}
return stdoutBuf.String(), stderrBuf.String(), nil
}

View File

@@ -0,0 +1,186 @@
package local_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local"
)
var (
fInputCertPath string
fInputKeyPath string
fOutputCertPath string
fOutputKeyPath string
fPfxPassword string
fJksAlias string
fJksKeypass string
fJksStorepass string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_LOCAL_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fOutputCertPath, argsPrefix+"OUTPUTCERTPATH", "", "")
flag.StringVar(&fOutputKeyPath, argsPrefix+"OUTPUTKEYPATH", "", "")
flag.StringVar(&fPfxPassword, argsPrefix+"PFXPASSWORD", "", "")
flag.StringVar(&fJksAlias, argsPrefix+"JKSALIAS", "", "")
flag.StringVar(&fJksKeypass, argsPrefix+"JKSKEYPASS", "", "")
flag.StringVar(&fJksStorepass, argsPrefix+"JKSSTOREPASS", "", "")
}
/*
Shell command to run this test:
go test -v local_test.go -args \
--CERTIMATE_DEPLOYER_LOCAL_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_LOCAL_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_LOCAL_OUTPUTCERTPATH="/path/to/your-output-cert" \
--CERTIMATE_DEPLOYER_LOCAL_OUTPUTKEYPATH="/path/to/your-output-key" \
--CERTIMATE_DEPLOYER_LOCAL_PFXPASSWORD="your-pfx-password" \
--CERTIMATE_DEPLOYER_LOCAL_JKSALIAS="your-jks-alias" \
--CERTIMATE_DEPLOYER_LOCAL_JKSKEYPASS="your-jks-keypass" \
--CERTIMATE_DEPLOYER_LOCAL_JKSSTOREPASS="your-jks-storepass"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_PEM", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
}, "\n"))
deployer, err := provider.New(&provider.LocalDeployerConfig{
OutputCertPath: fOutputCertPath,
OutputKeyPath: fOutputKeyPath,
})
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
}
fstat1, err := os.Stat(fOutputCertPath)
if err != nil {
t.Errorf("err: %+v", err)
return
} else if fstat1.Size() == 0 {
t.Errorf("err: empty output certificate file")
return
}
fstat2, err := os.Stat(fOutputKeyPath)
if err != nil {
t.Errorf("err: %+v", err)
return
} else if fstat2.Size() == 0 {
t.Errorf("err: empty output private key file")
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_PFX", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
fmt.Sprintf("PFXPASSWORD: %v", fPfxPassword),
}, "\n"))
deployer, err := provider.New(&provider.LocalDeployerConfig{
OutputFormat: provider.OUTPUT_FORMAT_PFX,
OutputCertPath: fOutputCertPath,
OutputKeyPath: fOutputKeyPath,
PfxPassword: fPfxPassword,
})
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
}
fstat, err := os.Stat(fOutputCertPath)
if err != nil {
t.Errorf("err: %+v", err)
return
} else if fstat.Size() == 0 {
t.Errorf("err: empty output certificate file")
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_JKS", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
fmt.Sprintf("JKSALIAS: %v", fJksAlias),
fmt.Sprintf("JKSKEYPASS: %v", fJksKeypass),
fmt.Sprintf("JKSSTOREPASS: %v", fJksStorepass),
}, "\n"))
deployer, err := provider.New(&provider.LocalDeployerConfig{
OutputFormat: provider.OUTPUT_FORMAT_JKS,
OutputCertPath: fOutputCertPath,
OutputKeyPath: fOutputKeyPath,
JksAlias: fJksAlias,
JksKeypass: fJksKeypass,
JksStorepass: fJksStorepass,
})
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
}
fstat, err := os.Stat(fOutputCertPath)
if err != nil {
t.Errorf("err: %+v", err)
return
} else if fstat.Size() == 0 {
t.Errorf("err: empty output certificate file")
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,106 @@
package qiniucdn
import (
"context"
"errors"
"strings"
xerrors "github.com/pkg/errors"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerQiniu "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/qiniu-sslcert"
qiniusdk "github.com/usual2970/certimate/internal/pkg/vendors/qiniu-sdk"
)
type QiniuCDNDeployerConfig struct {
// 七牛云 AccessKey。
AccessKey string `json:"accessKey"`
// 七牛云 SecretKey。
SecretKey string `json:"secretKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type QiniuCDNDeployer struct {
config *QiniuCDNDeployerConfig
logger deployer.Logger
sdkClient *qiniusdk.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*QiniuCDNDeployer)(nil)
func New(config *QiniuCDNDeployerConfig) (*QiniuCDNDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *QiniuCDNDeployerConfig, logger deployer.Logger) (*QiniuCDNDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client := qiniusdk.NewClient(auth.New(config.AccessKey, config.SecretKey))
uploader, err := providerQiniu.New(&providerQiniu.QiniuSSLCertUploaderConfig{
AccessKey: config.AccessKey,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &QiniuCDNDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *QiniuCDNDeployer) 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)
// "*.example.com" → ".example.com",适配七牛云 CDN 要求的泛域名格式
domain := strings.TrimPrefix(d.config.Domain, "*")
// 获取域名信息
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
getDomainInfoResp, err := d.sdkClient.GetDomainInfo(domain)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDomainInfo'")
}
d.logger.Logt("已获取域名信息", 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 nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ModifyDomainHttpsConf'")
}
d.logger.Logt("已修改域名证书", modifyDomainHttpsConfResp)
} else {
enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(domain, upres.CertId, true, true)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.EnableDomainHttps'")
}
d.logger.Logt("已将域名升级为 HTTPS", enableDomainHttpsResp)
}
return &deployer.DeployResult{}, nil
}

View File

@@ -0,0 +1,75 @@
package qiniucdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKey string
fSecretKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_QINIUCDN_"
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 qiniu_cdn_test.go -args \
--CERTIMATE_DEPLOYER_QINIUCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_QINIUCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_QINIUCDN_ACCESSKEY="your-access-key" \
--CERTIMATE_DEPLOYER_QINIUCDN_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_QINIUCDN_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.QiniuCDNDeployerConfig{
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)
})
}

View File

@@ -0,0 +1,9 @@
package ssh
type OutputFormatType string
const (
OUTPUT_FORMAT_PEM = OutputFormatType("PEM")
OUTPUT_FORMAT_PFX = OutputFormatType("PFX")
OUTPUT_FORMAT_JKS = OutputFormatType("JKS")
)

View File

@@ -0,0 +1,252 @@
package ssh
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
xerrors "github.com/pkg/errors"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type SshDeployerConfig struct {
// SSH 主机。
// 零值时默认为 "localhost"。
SshHost string `json:"sshHost,omitempty"`
// SSH 端口。
// 零值时默认为 22。
SshPort int32 `json:"sshPort,omitempty"`
// SSH 登录用户名。
SshUsername string `json:"sshUsername,omitempty"`
// SSH 登录密码。
SshPassword string `json:"sshPassword,omitempty"`
// SSH 登录私钥。
SshKey string `json:"sshKey,omitempty"`
// SSH 登录私钥口令。
SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"`
// 前置命令。
PreCommand string `json:"preCommand,omitempty"`
// 后置命令。
PostCommand string `json:"postCommand,omitempty"`
// 输出证书格式。
OutputFormat OutputFormatType `json:"outputFormat,omitempty"`
// 输出证书文件路径。
OutputCertPath string `json:"outputCertPath,omitempty"`
// 输出私钥文件路径。
OutputKeyPath string `json:"outputKeyPath,omitempty"`
// PFX 导出密码。
// 证书格式为 PFX 时必填。
PfxPassword string `json:"pfxPassword,omitempty"`
// JKS 别名。
// 证书格式为 JKS 时必填。
JksAlias string `json:"jksAlias,omitempty"`
// JKS 密钥密码。
// 证书格式为 JKS 时必填。
JksKeypass string `json:"jksKeypass,omitempty"`
// JKS 存储密码。
// 证书格式为 JKS 时必填。
JksStorepass string `json:"jksStorepass,omitempty"`
}
type SshDeployer struct {
config *SshDeployerConfig
logger deployer.Logger
}
var _ deployer.Deployer = (*SshDeployer)(nil)
func New(config *SshDeployerConfig) (*SshDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *SshDeployerConfig, logger deployer.Logger) (*SshDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
return &SshDeployer{
logger: logger,
config: config,
}, nil
}
func (d *SshDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 连接
client, err := createSshClient(
d.config.SshHost,
d.config.SshPort,
d.config.SshUsername,
d.config.SshPassword,
d.config.SshKey,
d.config.SshKeyPassphrase,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssh client")
}
defer client.Close()
d.logger.Logt("SSH connected")
// 执行前置命令
if d.config.PreCommand != "" {
stdout, stderr, err := execSshCommand(client, d.config.PreCommand)
if err != nil {
return nil, xerrors.Wrapf(err, "failed to run pre-command: stdout: %s, stderr: %s", stdout, stderr)
}
d.logger.Logt("SSH pre-command executed", stdout)
}
// 上传证书和私钥文件
switch d.config.OutputFormat {
case OUTPUT_FORMAT_PEM:
if err := writeSftpFileString(client, d.config.OutputCertPath, certPem); err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded")
if err := writeSftpFileString(client, d.config.OutputKeyPath, privkeyPem); err != nil {
return nil, xerrors.Wrap(err, "failed to upload private key file")
}
d.logger.Logt("private key file uploaded")
case OUTPUT_FORMAT_PFX:
pfxData, err := x509.TransformCertificateFromPEMToPFX(certPem, privkeyPem, d.config.PfxPassword)
if err != nil {
return nil, xerrors.Wrap(err, "failed to transform certificate to PFX")
}
d.logger.Logt("certificate transformed to PFX")
if err := writeSftpFile(client, d.config.OutputCertPath, pfxData); err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded")
case OUTPUT_FORMAT_JKS:
jksData, err := x509.TransformCertificateFromPEMToJKS(certPem, privkeyPem, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)
if err != nil {
return nil, xerrors.Wrap(err, "failed to transform certificate to JKS")
}
d.logger.Logt("certificate transformed to JKS")
if err := writeSftpFile(client, d.config.OutputCertPath, jksData); err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded")
default:
return nil, fmt.Errorf("unsupported output format: %s", d.config.OutputFormat)
}
// 执行后置命令
if d.config.PostCommand != "" {
stdout, stderr, err := execSshCommand(client, d.config.PostCommand)
if err != nil {
return nil, xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
}
d.logger.Logt("SSH post-command executed", stdout)
}
return &deployer.DeployResult{}, nil
}
func createSshClient(host string, port int32, username string, password string, key string, keyPassphrase string) (*ssh.Client, error) {
if host == "" {
host = "localhost"
}
if port == 0 {
port = 22
}
var authMethod ssh.AuthMethod
if key != "" {
var signer ssh.Signer
var err error
if keyPassphrase != "" {
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(keyPassphrase))
} else {
signer, err = ssh.ParsePrivateKey([]byte(key))
}
if err != nil {
return nil, err
}
authMethod = ssh.PublicKeys(signer)
} else {
authMethod = ssh.Password(password)
}
return ssh.Dial("tcp", fmt.Sprintf("%s:%d", host, port), &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{authMethod},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
}
func execSshCommand(sshCli *ssh.Client, command string) (string, string, error) {
session, err := sshCli.NewSession()
if err != nil {
return "", "", err
}
defer session.Close()
var stdoutBuf bytes.Buffer
session.Stdout = &stdoutBuf
var stderrBuf bytes.Buffer
session.Stderr = &stderrBuf
err = session.Run(command)
if err != nil {
return "", "", err
}
return stdoutBuf.String(), stderrBuf.String(), nil
}
func writeSftpFileString(sshCli *ssh.Client, path string, content string) error {
return writeSftpFile(sshCli, path, []byte(content))
}
func writeSftpFile(sshCli *ssh.Client, path string, data []byte) error {
sftpCli, err := sftp.NewClient(sshCli)
if err != nil {
return xerrors.Wrap(err, "failed to create sftp client")
}
defer sftpCli.Close()
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 xerrors.Wrap(err, "failed to open remote file")
}
defer file.Close()
_, err = file.Write(data)
if err != nil {
return xerrors.Wrap(err, "failed to write to remote file")
}
return nil
}

View File

@@ -0,0 +1,90 @@
package ssh_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh"
)
var (
fInputCertPath string
fInputKeyPath string
fSshHost string
fSshPort int
fSshUsername string
fSshPassword string
fOutputCertPath string
fOutputKeyPath string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_SSH_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fSshHost, argsPrefix+"SSHHOST", "", "")
flag.IntVar(&fSshPort, argsPrefix+"SSHPORT", 0, "")
flag.StringVar(&fSshUsername, argsPrefix+"SSHUSERNAME", "", "")
flag.StringVar(&fSshPassword, argsPrefix+"SSHPASSWORD", "", "")
flag.StringVar(&fOutputCertPath, argsPrefix+"OUTPUTCERTPATH", "", "")
flag.StringVar(&fOutputKeyPath, argsPrefix+"OUTPUTKEYPATH", "", "")
}
/*
Shell command to run this test:
go test -v ssh_test.go -args \
--CERTIMATE_DEPLOYER_SSH_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_SSH_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_SSH_SSHHOST="localhost" \
--CERTIMATE_DEPLOYER_SSH_SSHPORT=22 \
--CERTIMATE_DEPLOYER_SSH_SSHUSERNAME="root" \
--CERTIMATE_DEPLOYER_SSH_SSHPASSWORD="password" \
--CERTIMATE_DEPLOYER_SSH_OUTPUTCERTPATH="/path/to/your-output-cert.pem" \
--CERTIMATE_DEPLOYER_SSH_OUTPUTKEYPATH="/path/to/your-output-key.pem"
*/
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("SSHHOST: %v", fSshHost),
fmt.Sprintf("SSHPORT: %v", fSshPort),
fmt.Sprintf("SSHUSERNAME: %v", fSshUsername),
fmt.Sprintf("SSHPASSWORD: %v", fSshPassword),
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
}, "\n"))
deployer, err := provider.New(&provider.SshDeployerConfig{
SshHost: fSshHost,
SshPort: int32(fSshPort),
SshUsername: fSshUsername,
SshPassword: fSshPassword,
OutputCertPath: fOutputCertPath,
OutputKeyPath: fOutputKeyPath,
})
if err != nil {
t.Errorf("err: %+v", err)
panic(err)
}
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)
panic(err)
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,198 @@
package tencentcloudcdn
import (
"context"
"errors"
"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"
"golang.org/x/exp/slices"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCloudCDNDeployerConfig struct {
// 腾讯云 SecretId。
SecretId string `json:"secretId"`
// 腾讯云 SecretKey。
SecretKey string `json:"secretKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type TencentCloudCDNDeployer struct {
config *TencentCloudCDNDeployerConfig
logger deployer.Logger
sdkClients *tencentCloudCDNDeployerSdkClients
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*TencentCloudCDNDeployer)(nil)
type tencentCloudCDNDeployerSdkClients struct {
ssl *tcSsl.Client
cdn *tcCdn.Client
}
func New(config *TencentCloudCDNDeployerConfig) (*TencentCloudCDNDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *TencentCloudCDNDeployerConfig, logger deployer.Logger) (*TencentCloudCDNDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
clients, err := createSdkClients(config.SecretId, config.SecretKey)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
SecretId: config.SecretId,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCloudCDNDeployer{
logger: logger,
config: config,
sdkClients: clients,
sslUploader: uploader,
}, nil
}
func (d *TencentCloudCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 SSL
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)
// 获取待部署的 CDN 实例
// 如果是泛域名,根据证书匹配 CDN 实例
instanceIds := make([]string, 0)
if strings.HasPrefix(d.config.Domain, "*.") {
domains, err := d.getDomainsByCertificateId(upres.CertId)
if err != nil {
return nil, err
}
instanceIds = domains
} else {
instanceIds = append(instanceIds, d.config.Domain)
}
// 跳过已部署的 CDN 实例
if len(instanceIds) > 0 {
deployedDomains, err := d.getDeployedDomainsByCertificateId(upres.CertId)
if err != nil {
return nil, err
}
temp := make([]string, 0)
for _, instanceId := range instanceIds {
if !slices.Contains(deployedDomains, instanceId) {
temp = append(temp, instanceId)
}
}
instanceIds = temp
}
if len(instanceIds) == 0 {
d.logger.Logt("已部署过或没有要部署的 CDN 实例")
} else {
// 证书部署到 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(instanceIds)
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.logger.Logt("已部署证书到云资源实例", deployCertificateInstanceResp.Response)
}
return &deployer.DeployResult{}, nil
}
func (d *TencentCloudCDNDeployer) getDomainsByCertificateId(cloudCertId string) ([]string, error) {
// 获取证书中的可用域名
// REF: https://cloud.tencent.com/document/product/228/42491
describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest()
describeCertDomainsReq.CertId = common.StringPtr(cloudCertId)
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 *TencentCloudCDNDeployer) getDeployedDomainsByCertificateId(cloudCertId string) ([]string, error) {
// 根据证书查询关联 CDN 域名
// REF: https://cloud.tencent.com/document/product/400/62674
describeDeployedResourcesReq := tcSsl.NewDescribeDeployedResourcesRequest()
describeDeployedResourcesReq.CertificateIds = common.StringPtrs([]string{cloudCertId})
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
}
func createSdkClients(secretId, secretKey string) (*tencentCloudCDNDeployerSdkClients, 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 &tencentCloudCDNDeployerSdkClients{
ssl: sslClient,
cdn: cdnClient,
}, nil
}

View File

@@ -0,0 +1,75 @@
package tencentcloudcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fSecretId string
fSecretKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v tencentcloud_cdn_test.go -args \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_SECRETID="your-secret-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_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("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudCDNDeployerConfig{
SecretId: fSecretId,
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)
})
}

View File

@@ -0,0 +1,14 @@
package tencentcloudclb
type DeployResourceType string
const (
// 资源类型:通过 SSL 服务部署到云资源实例。
DEPLOY_RESOURCE_USE_SSLDEPLOY = DeployResourceType("ssl-deploy")
// 资源类型:部署到指定负载均衡器。
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
// 资源类型:部署到指定转发规则域名。
DEPLOY_RESOURCE_RULEDOMAIN = DeployResourceType("ruledomain")
)

View File

@@ -0,0 +1,305 @@
package tencentcloudclb
import (
"context"
"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/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCloudCLBDeployerConfig struct {
// 腾讯云 SecretId。
SecretId string `json:"secretId"`
// 腾讯云 SecretKey。
SecretKey string `json:"secretKey"`
// 腾讯云地域。
Region string `json:"region"`
// 部署资源类型。
ResourceType DeployResourceType `json:"resourceType"`
// 负载均衡器 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_SSLDEPLOY]、[DEPLOY_RESOURCE_LOADBALANCER]、[DEPLOY_RESOURCE_RULEDOMAIN] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_SSLDEPLOY]、[DEPLOY_RESOURCE_LOADBALANCER]、[DEPLOY_RESOURCE_LISTENER]、[DEPLOY_RESOURCE_RULEDOMAIN] 时必填。
ListenerId string `json:"listenerId,omitempty"`
// SNI 域名或七层转发规则域名(支持泛域名)。
// 部署资源类型为 [DEPLOY_RESOURCE_SSLDEPLOY] 时选填;部署资源类型为 [DEPLOY_RESOURCE_RULEDOMAIN] 时必填。
Domain string `json:"domain,omitempty"`
}
type TencentCloudCLBDeployer struct {
config *TencentCloudCLBDeployerConfig
logger deployer.Logger
sdkClients *wSdkClients
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*TencentCloudCLBDeployer)(nil)
type wSdkClients struct {
ssl *tcSsl.Client
clb *tcClb.Client
}
func New(config *TencentCloudCLBDeployerConfig) (*TencentCloudCLBDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *TencentCloudCLBDeployerConfig, logger deployer.Logger) (*TencentCloudCLBDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
clients, err := createSdkClients(config.SecretId, config.SecretKey, config.Region)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
SecretId: config.SecretId,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCloudCLBDeployer{
logger: logger,
config: config,
sdkClients: clients,
sslUploader: uploader,
}, nil
}
func (d *TencentCloudCLBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 SSL
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_USE_SSLDEPLOY:
if err := d.deployToInstanceUseSsl(ctx, upres.CertId); err != nil {
return nil, err
}
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
}
case DEPLOY_RESOURCE_RULEDOMAIN:
if err := d.deployToRuleDomain(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 *TencentCloudCLBDeployer) deployToInstanceUseSsl(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 证书部署到 CLB 实例
// REF: https://cloud.tencent.com/document/product/400/91667
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
deployCertificateInstanceReq.CertificateId = common.StringPtr(cloudCertId)
deployCertificateInstanceReq.ResourceType = common.StringPtr("clb")
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
if d.config.Domain == "" {
// 未开启 SNI只需指定到监听器
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s", d.config.LoadbalancerId, d.config.ListenerId)})
} else {
// 开启 SNI需指定到域名支持泛域名
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", d.config.LoadbalancerId, d.config.ListenerId, d.config.Domain)})
}
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.logger.Logt("已部署证书到云资源实例", deployCertificateInstanceResp.Response)
return nil
}
func (d *TencentCloudCLBDeployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
listenerIds := make([]string, 0)
// 查询监听器列表
// REF: https://cloud.tencent.com/document/api/214/30686
describeListenersReq := tcClb.NewDescribeListenersRequest()
describeListenersReq.LoadBalancerId = common.StringPtr(d.config.LoadbalancerId)
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
}
listenerIds = append(listenerIds, *listener.ListenerId)
}
}
}
d.logger.Logt("已查询到负载均衡器下的监听器", listenerIds)
// 批量更新监听器证书
if len(listenerIds) > 0 {
var errs []error
for _, listenerId := range listenerIds {
if err := d.modifyListenerCertificate(ctx, d.config.LoadbalancerId, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *TencentCloudCLBDeployer) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 更新监听器证书
if err := d.modifyListenerCertificate(ctx, d.config.LoadbalancerId, d.config.ListenerId, cloudCertId); err != nil {
return err
}
return nil
}
func (d *TencentCloudCLBDeployer) deployToRuleDomain(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
if d.config.Domain == "" {
return errors.New("config `domain` is required")
}
// 修改负载均衡七层监听器转发规则的域名级别属性
// REF: https://cloud.tencent.com/document/api/214/38092
modifyDomainAttributesReq := tcClb.NewModifyDomainAttributesRequest()
modifyDomainAttributesReq.LoadBalancerId = common.StringPtr(d.config.LoadbalancerId)
modifyDomainAttributesReq.ListenerId = common.StringPtr(d.config.ListenerId)
modifyDomainAttributesReq.Domain = common.StringPtr(d.config.Domain)
modifyDomainAttributesReq.Certificate = &tcClb.CertificateInput{
SSLMode: common.StringPtr("UNIDIRECTIONAL"),
CertId: common.StringPtr(cloudCertId),
}
modifyDomainAttributesResp, err := d.sdkClients.clb.ModifyDomainAttributes(modifyDomainAttributesReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.ModifyDomainAttributes'")
}
d.logger.Logt("已修改七层监听器转发规则的域名级别属性", modifyDomainAttributesResp.Response)
return nil
}
func (d *TencentCloudCLBDeployer) modifyListenerCertificate(ctx context.Context, cloudLoadbalancerId, cloudListenerId, cloudCertId string) error {
// 查询监听器列表
// REF: https://cloud.tencent.com/document/api/214/30686
describeListenersReq := tcClb.NewDescribeListenersRequest()
describeListenersReq.LoadBalancerId = common.StringPtr(cloudLoadbalancerId)
describeListenersReq.ListenerIds = common.StringPtrs([]string{cloudListenerId})
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 {
return errors.New("listener not found")
}
d.logger.Logt("已查询到监听器属性", describeListenersResp.Response)
// 修改监听器属性
// REF: https://cloud.tencent.com/document/product/214/30681
modifyListenerReq := tcClb.NewModifyListenerRequest()
modifyListenerReq.LoadBalancerId = common.StringPtr(cloudLoadbalancerId)
modifyListenerReq.ListenerId = common.StringPtr(cloudListenerId)
modifyListenerReq.Certificate = &tcClb.CertificateInput{CertId: common.StringPtr(cloudCertId)}
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.logger.Logt("已修改监听器属性", modifyListenerResp.Response)
return nil
}
func createSdkClients(secretId, secretKey, region string) (*wSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
// 注意虽然官方文档中地域无需指定,但实际需要部署到 CLB 时必传
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 &wSdkClients{
ssl: sslClient,
clb: clbClient,
}, nil
}

View File

@@ -0,0 +1,199 @@
package tencentcloudclb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-clb"
)
var (
fInputCertPath string
fInputKeyPath string
fSecretId string
fSecretKey string
fRegion string
fLoadbalancerId string
fListenerId string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v tencentcloud_clb_test.go -args \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_SECRETID="your-secret-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_REGION="ap-guangzhou" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_LOADBALANCERID="your-clb-lb-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_LISTENERID="your-clb-lbl-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_UseSslDeploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("LISTENERID: %v", fListenerId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudCLBDeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_USE_SSLDEPLOY,
LoadbalancerId: fLoadbalancerId,
ListenerId: fListenerId,
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)
})
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("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudCLBDeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
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("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("LISTENERID: %v", fListenerId),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudCLBDeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
LoadbalancerId: fLoadbalancerId,
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)
})
t.Run("Deploy_ToRuleDomain", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("LISTENERID: %v", fListenerId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudCLBDeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_RULEDOMAIN,
LoadbalancerId: fLoadbalancerId,
ListenerId: fListenerId,
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)
})
}

View File

@@ -0,0 +1,115 @@
package tencentcloudcdn
import (
"context"
"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/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCloudCOSDeployerConfig struct {
// 腾讯云 SecretId。
SecretId string `json:"secretId"`
// 腾讯云 SecretKey。
SecretKey string `json:"secretKey"`
// 腾讯云地域。
Region string `json:"region"`
// 存储桶名。
Bucket string `json:"bucket"`
// 自定义域名(不支持泛域名)。
Domain string `json:"domain"`
}
type TencentCloudCOSDeployer struct {
config *TencentCloudCOSDeployerConfig
logger deployer.Logger
sdkClient *tcSsl.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*TencentCloudCOSDeployer)(nil)
func New(config *TencentCloudCOSDeployerConfig) (*TencentCloudCOSDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *TencentCloudCOSDeployerConfig, logger deployer.Logger) (*TencentCloudCOSDeployer, 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.SecretId, config.SecretKey, config.Region)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
SecretId: config.SecretId,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCloudCOSDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *TencentCloudCOSDeployer) 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")
}
// 上传证书到 SSL
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)
// 证书部署到 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", d.config.Region, d.config.Bucket, d.config.Domain)})
deployCertificateInstanceResp, err := d.sdkClient.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.logger.Logt("已部署证书到云资源实例", deployCertificateInstanceResp.Response)
return &deployer.DeployResult{}, nil
}
func 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
}

View File

@@ -0,0 +1,85 @@
package tencentcloudcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cos"
)
var (
fInputCertPath string
fInputKeyPath string
fSecretId string
fSecretKey string
fRegion string
fBucket string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v tencentcloud_cos_test.go -args \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_SECRETID="your-secret-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_REGION="ap-guangzhou" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_BUCKET="your-cos-bucket" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_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("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("BUCKET: %v", fBucket),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudCOSDeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
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)
})
}

View File

@@ -0,0 +1,158 @@
package tencentcloudecdn
import (
"context"
"errors"
"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/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCloudECDNDeployerConfig struct {
// 腾讯云 SecretId。
SecretId string `json:"secretId"`
// 腾讯云 SecretKey。
SecretKey string `json:"secretKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type TencentCloudECDNDeployer struct {
config *TencentCloudECDNDeployerConfig
logger deployer.Logger
sdkClients *wSdkClients
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*TencentCloudECDNDeployer)(nil)
type wSdkClients struct {
ssl *tcSsl.Client
cdn *tcCdn.Client
}
func New(config *TencentCloudECDNDeployerConfig) (*TencentCloudECDNDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *TencentCloudECDNDeployerConfig, logger deployer.Logger) (*TencentCloudECDNDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
clients, err := createSdkClients(config.SecretId, config.SecretKey)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
SecretId: config.SecretId,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCloudECDNDeployer{
logger: logger,
config: config,
sdkClients: clients,
sslUploader: uploader,
}, nil
}
func (d *TencentCloudECDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 SSL
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)
// 获取待部署的 CDN 实例
// 如果是泛域名,根据证书匹配 CDN 实例
instanceIds := make([]string, 0)
if strings.HasPrefix(d.config.Domain, "*.") {
domains, err := d.getDomainsByCertificateId(upres.CertId)
if err != nil {
return nil, err
}
instanceIds = domains
} else {
instanceIds = append(instanceIds, d.config.Domain)
}
if len(instanceIds) == 0 {
d.logger.Logt("已部署过或没有要部署的 ECDN 实例")
} else {
// 证书部署到 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(instanceIds)
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.logger.Logt("已部署证书到云资源实例", deployCertificateInstanceResp.Response)
}
return &deployer.DeployResult{}, nil
}
func (d *TencentCloudECDNDeployer) getDomainsByCertificateId(cloudCertId string) ([]string, error) {
// 获取证书中的可用域名
// REF: https://cloud.tencent.com/document/product/228/42491
describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest()
describeCertDomainsReq.CertId = common.StringPtr(cloudCertId)
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
}
func createSdkClients(secretId, secretKey string) (*wSdkClients, 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 &wSdkClients{
ssl: sslClient,
cdn: cdnClient,
}, nil
}

View File

@@ -0,0 +1,75 @@
package tencentcloudecdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ecdn"
)
var (
fInputCertPath string
fInputKeyPath string
fSecretId string
fSecretKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v tencentcloud_ecdn_test.go -args \
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_SECRETID="your-secret-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_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("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudECDNDeployerConfig{
SecretId: fSecretId,
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)
})
}

View File

@@ -0,0 +1,124 @@
package tencentcloudeteo
import (
"context"
"errors"
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/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCloudTEODeployerConfig struct {
// 腾讯云 SecretId。
SecretId string `json:"secretId"`
// 腾讯云 SecretKey。
SecretKey string `json:"secretKey"`
// 站点 ID。
ZoneId string `json:"zoneId"`
// 加速域名(不支持泛域名)。
Domain string `json:"domain"`
}
type TencentCloudTEODeployer struct {
config *TencentCloudTEODeployerConfig
logger deployer.Logger
sdkClients *wSdkClients
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*TencentCloudTEODeployer)(nil)
type wSdkClients struct {
ssl *tcSsl.Client
teo *tcTeo.Client
}
func New(config *TencentCloudTEODeployerConfig) (*TencentCloudTEODeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *TencentCloudTEODeployerConfig, logger deployer.Logger) (*TencentCloudTEODeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
clients, err := createSdkClients(config.SecretId, config.SecretKey)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
SecretId: config.SecretId,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCloudTEODeployer{
logger: logger,
config: config,
sdkClients: clients,
sslUploader: uploader,
}, nil
}
func (d *TencentCloudTEODeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
if d.config.ZoneId == "" {
return nil, xerrors.New("config `zoneId` is required")
}
// 上传证书到 SSL
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://cloud.tencent.com/document/product/1552/80764
modifyHostsCertificateReq := tcTeo.NewModifyHostsCertificateRequest()
modifyHostsCertificateReq.ZoneId = common.StringPtr(d.config.ZoneId)
modifyHostsCertificateReq.Mode = common.StringPtr("sslcert")
modifyHostsCertificateReq.Hosts = common.StringPtrs([]string{d.config.Domain})
modifyHostsCertificateReq.ServerCertInfo = []*tcTeo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}}
modifyHostsCertificateResp, err := d.sdkClients.teo.ModifyHostsCertificate(modifyHostsCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'teo.ModifyHostsCertificate'")
}
d.logger.Logt("已配置域名证书", modifyHostsCertificateResp.Response)
return &deployer.DeployResult{}, nil
}
func createSdkClients(secretId, secretKey string) (*wSdkClients, 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 &wSdkClients{
ssl: sslClient,
teo: teoClient,
}, nil
}

View File

@@ -0,0 +1,80 @@
package tencentcloudeteo_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-teo"
)
var (
fInputCertPath string
fInputKeyPath string
fSecretId string
fSecretKey string
fZoneId string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fZoneId, argsPrefix+"ZONEID", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v tencentcloud_cdn_test.go -args \
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_SECRETID="your-secret-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_ZONEID="your-zone-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_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("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("ZONEID: %v", fZoneId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudTEODeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
ZoneId: fZoneId,
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)
})
}

View File

@@ -0,0 +1,136 @@
package volcenginecdn
import (
"context"
"errors"
"fmt"
"strings"
xerrors "github.com/pkg/errors"
veCdn "github.com/volcengine/volc-sdk-golang/service/cdn"
"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/volcengine-cdn"
)
type VolcEngineCDNDeployerConfig struct {
// 火山引擎 AccessKey。
AccessKey string `json:"accessKey"`
// 火山引擎 SecretKey。
SecretKey string `json:"secretKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type VolcEngineCDNDeployer struct {
config *VolcEngineCDNDeployerConfig
logger deployer.Logger
sdkClient *veCdn.CDN
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*VolcEngineCDNDeployer)(nil)
func New(config *VolcEngineCDNDeployerConfig) (*VolcEngineCDNDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *VolcEngineCDNDeployerConfig, logger deployer.Logger) (*VolcEngineCDNDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client := veCdn.NewInstance()
client.Client.SetAccessKey(config.AccessKey)
client.Client.SetSecretKey(config.SecretKey)
uploader, err := providerCdn.New(&providerCdn.VolcEngineCDNUploaderConfig{
AccessKeyId: config.AccessKey,
AccessKeySecret: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &VolcEngineCDNDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *VolcEngineCDNDeployer) 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://www.volcengine.com/docs/6454/125711
describeCertConfigReq := &veCdn.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://www.volcengine.com/docs/6454/125712
batchDeployCertReq := &veCdn.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
}

View File

@@ -0,0 +1,75 @@
package volcenginecdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKey string
fSecretKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_VOLCENGINECDN_"
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 volcengine_cdn_test.go -args \
--CERTIMATE_DEPLOYER_VOLCENGINECDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_VOLCENGINECDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_VOLCENGINECDN_ACCESSKEY="your-access-key" \
--CERTIMATE_DEPLOYER_VOLCENGINECDN_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_VOLCENGINECDN_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.VolcEngineCDNDeployerConfig{
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)
})
}

View File

@@ -0,0 +1,146 @@
package volcenginelive
import (
"context"
"errors"
"fmt"
"strings"
xerrors "github.com/pkg/errors"
veLive "github.com/volcengine/volc-sdk-golang/service/live/v20230101"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerLive "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-live"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
)
type VolcEngineLiveDeployerConfig struct {
// 火山引擎 AccessKey。
AccessKey string `json:"accessKey"`
// 火山引擎 SecretKey。
SecretKey string `json:"secretKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type VolcEngineLiveDeployer struct {
config *VolcEngineLiveDeployerConfig
logger deployer.Logger
sdkClient *veLive.Live
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*VolcEngineLiveDeployer)(nil)
func New(config *VolcEngineLiveDeployerConfig) (*VolcEngineLiveDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *VolcEngineLiveDeployerConfig, logger deployer.Logger) (*VolcEngineLiveDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client := veLive.NewInstance()
client.SetAccessKey(config.AccessKey)
client.SetSecretKey(config.SecretKey)
uploader, err := providerLive.New(&providerLive.VolcEngineLiveUploaderConfig{
AccessKeyId: config.AccessKey,
AccessKeySecret: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &VolcEngineLiveDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *VolcEngineLiveDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 Live
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, "*.") {
listDomainDetailPageNum := int32(1)
listDomainDetailPageSize := int32(1000)
listDomainDetailTotal := 0
for {
// 查询域名列表
// 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
listDomainDetailReq := &veLive.ListDomainDetailBody{
PageNum: listDomainDetailPageNum,
PageSize: listDomainDetailPageSize,
}
listDomainDetailResp, err := d.sdkClient.ListDomainDetail(ctx, listDomainDetailReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'live.ListDomainDetail'")
}
if listDomainDetailResp.Result.DomainList != nil {
for _, item := range listDomainDetailResp.Result.DomainList {
// 仅匹配泛域名的下一级子域名
wildcardDomain := strings.TrimPrefix(d.config.Domain, "*")
if strings.HasSuffix(item.Domain, wildcardDomain) && !strings.Contains(strings.TrimSuffix(item.Domain, wildcardDomain), ".") {
domains = append(domains, item.Domain)
}
}
}
listDomainDetailLen := len(listDomainDetailResp.Result.DomainList)
if listDomainDetailLen < int(listDomainDetailPageSize) || int(listDomainDetailResp.Result.Total) <= listDomainDetailTotal+listDomainDetailLen {
break
} else {
listDomainDetailPageNum++
listDomainDetailTotal += listDomainDetailLen
}
}
if len(domains) == 0 {
return nil, xerrors.Errorf("未查询到匹配的域名: %s", d.config.Domain)
}
} else {
domains = append(domains, d.config.Domain)
}
if len(domains) > 0 {
var errs []error
for _, domain := range domains {
// 绑定证书
// REF: https://www.volcengine.com/docs/6469/1186278#%E7%BB%91%E5%AE%9A%E8%AF%81%E4%B9%A6
bindCertReq := &veLive.BindCertBody{
ChainID: upres.CertId,
Domain: domain,
HTTPS: cast.BoolPtr(true),
}
bindCertResp, err := d.sdkClient.BindCert(ctx, bindCertReq)
if err != nil {
errs = append(errs, err)
} else {
d.logger.Logt(fmt.Sprintf("已绑定证书到域名 %s", domain), bindCertResp)
}
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
}
return &deployer.DeployResult{}, nil
}

Some files were not shown because too many files have changed in this diff Show More