Compare commits
1052 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb77ba5885 | ||
|
|
34887cba7c | ||
|
|
14a3f055ee | ||
|
|
882bcdc954 | ||
|
|
801050d3ff | ||
|
|
f7a7c7f11c | ||
|
|
2aac79af54 | ||
|
|
813fc562e3 | ||
|
|
86fc556bbe | ||
|
|
680e6952ba | ||
|
|
e53b2161d4 | ||
|
|
b617b45495 | ||
|
|
583d308459 | ||
|
|
9354c60380 | ||
|
|
b8bbbee1e0 | ||
|
|
205275b52d | ||
|
|
30840bbba5 | ||
|
|
299a722aa9 | ||
|
|
9421da2cde | ||
|
|
312047e0ee | ||
|
|
4ac3618f7e | ||
|
|
3fb9b00f66 | ||
|
|
a492fbe18c | ||
|
|
0383233315 | ||
|
|
4752c49fed | ||
|
|
8149034bdc | ||
|
|
e7ce12772a | ||
|
|
0434f95a1e | ||
|
|
769b24aa88 | ||
|
|
94a0292fad | ||
|
|
410231cd1f | ||
|
|
1d4e048777 | ||
|
|
c16fe1a807 | ||
|
|
e8ed831b28 | ||
|
|
c7c89efbe7 | ||
|
|
d7f3d9c512 | ||
|
|
ce67cc5a39 | ||
|
|
945d0da36f | ||
|
|
898311c6e5 | ||
|
|
5d6bc03f21 | ||
|
|
018743299b | ||
|
|
18e7238067 | ||
|
|
b7d1ff8960 | ||
|
|
0d44373de6 | ||
|
|
3d680e50e2 | ||
|
|
9542079e20 | ||
|
|
081e83e0bf | ||
|
|
9c8ab98efb | ||
|
|
bf26db77cb | ||
|
|
08ea915d24 | ||
|
|
fb62f1e105 | ||
|
|
261b3a8546 | ||
|
|
563adbec2a | ||
|
|
c6dfe11bdb | ||
|
|
e4bfa90a77 | ||
|
|
b833d09466 | ||
|
|
3ec0ba7052 | ||
|
|
57eb66b889 | ||
|
|
a048eb95a9 | ||
|
|
23e58b914e | ||
|
|
1170f635fd | ||
|
|
553aceac44 | ||
|
|
e24de70c02 | ||
|
|
bba2b25757 | ||
|
|
4132ec3617 | ||
|
|
9b3c7e16c0 | ||
|
|
0448538073 | ||
|
|
80157496d5 | ||
|
|
62e2ed2fb8 | ||
|
|
a750592eb5 | ||
|
|
5e6d729631 | ||
|
|
24fe824757 | ||
|
|
84a3f3346a | ||
|
|
bd26dfecb8 | ||
|
|
43182de732 | ||
|
|
d58109f4be | ||
|
|
59935df6b1 | ||
|
|
252da5d7e1 | ||
|
|
c3e7590f53 | ||
|
|
65cd1dc850 | ||
|
|
2203bb5268 | ||
|
|
8e5c36968a | ||
|
|
9ad0e6fb57 | ||
|
|
7d55383cf7 | ||
|
|
6dc65eea2f | ||
|
|
7210f63884 | ||
|
|
f94db675fb | ||
|
|
e6cf4d3e07 | ||
|
|
cc5098c4bc | ||
|
|
025e606db4 | ||
|
|
d3e8bacd58 | ||
|
|
308b21bb33 | ||
|
|
262c1d7fcb | ||
|
|
722c3a0e83 | ||
|
|
f885b49daf | ||
|
|
6731c465e7 | ||
|
|
28811c46d8 | ||
|
|
599cf17c9e | ||
|
|
f0af36b59e | ||
|
|
e73e2739c1 | ||
|
|
efdeacf01a | ||
|
|
3a829ad53b | ||
|
|
605de595b1 | ||
|
|
daf22b7f15 | ||
|
|
0e8ebaa885 | ||
|
|
829fa29cf1 | ||
|
|
ddb46f9dda | ||
|
|
df1f216b5b | ||
|
|
b8b94dfd77 | ||
|
|
4489096e57 | ||
|
|
a4f736e0f3 | ||
|
|
d8935337d6 | ||
|
|
211f66dc0a | ||
|
|
d964b129b0 | ||
|
|
a758b1d6d4 | ||
|
|
037305d8cd | ||
|
|
cfdd3c621f | ||
|
|
5339963524 | ||
|
|
af7d05e669 | ||
|
|
bf1d03a30e | ||
|
|
3bb88d9f93 | ||
|
|
b0eb71421f | ||
|
|
e82a59289b | ||
|
|
8e23b14bf3 | ||
|
|
cd9dac7765 | ||
|
|
40f4488009 | ||
|
|
4c13a3e86a | ||
|
|
b139139f50 | ||
|
|
55e16f4b17 | ||
|
|
46b4ff73c9 | ||
|
|
970a1f0f79 | ||
|
|
11ff80ab28 | ||
|
|
7cd036f41e | ||
|
|
0909671be6 | ||
|
|
b798b824db | ||
|
|
71c093c042 | ||
|
|
9878c12512 | ||
|
|
b43797a0fb | ||
|
|
312589ab1c | ||
|
|
bff8add010 | ||
|
|
6b9f295167 | ||
|
|
9cdc59b272 | ||
|
|
0e8b271e8d | ||
|
|
87aae4087c | ||
|
|
75326b1ddd | ||
|
|
7d8dd523a2 | ||
|
|
993ca36755 | ||
|
|
c34346cb31 | ||
|
|
7643975ef9 | ||
|
|
799ad61dcc | ||
|
|
1c3cb1b21b | ||
|
|
9d9ca88ebe | ||
|
|
faad7cb6d7 | ||
|
|
d81a33f24a | ||
|
|
398337826e | ||
|
|
7469310fdb | ||
|
|
fe993b54f3 | ||
|
|
1ed1c62f76 | ||
|
|
524c4fd1e8 | ||
|
|
591df58992 | ||
|
|
4ad08d983a | ||
|
|
7a663d31cb | ||
|
|
e6fc92eccb | ||
|
|
97d692910b | ||
|
|
b546cf3ad0 | ||
|
|
6353f0139b | ||
|
|
c9e6bd0c2f | ||
|
|
1e67e9333e | ||
|
|
6f054ee594 | ||
|
|
05d43f38ce | ||
|
|
b8ab077b57 | ||
|
|
36dd4ef3eb | ||
|
|
a66e1c04c9 | ||
|
|
3098f6a82f | ||
|
|
4a8eaa9ffa | ||
|
|
3462e454d0 | ||
|
|
eabd16dd71 | ||
|
|
122d766cab | ||
|
|
980d1ee0b9 | ||
|
|
e9f248d8ec | ||
|
|
2906576de0 | ||
|
|
fd875feef3 | ||
|
|
871d3ece90 | ||
|
|
8014abc836 | ||
|
|
0840454143 | ||
|
|
06fd95782a | ||
|
|
ef0f0f6b43 | ||
|
|
b15bf8ef98 | ||
|
|
dd2087b101 | ||
|
|
12d0b66c61 | ||
|
|
f3bbb9e8b2 | ||
|
|
dcd646b465 | ||
|
|
bd4aa4806f | ||
|
|
b122bbced9 | ||
|
|
5edae845cb | ||
|
|
b7af3c10e4 | ||
|
|
2453048288 | ||
|
|
221b6a6fc6 | ||
|
|
851ad70a6c | ||
|
|
1844e9a9db | ||
|
|
04e9f4e909 | ||
|
|
e6e2587bfc | ||
|
|
70bd2f0581 | ||
|
|
9e08cfd1d1 | ||
|
|
cd93a2d72c | ||
|
|
11a4d4f55c | ||
|
|
f33570d514 | ||
|
|
1ee3b64a19 | ||
|
|
a3a56f3346 | ||
|
|
564ed8f0d3 | ||
|
|
268ec4bd7f | ||
|
|
e55e6cc512 | ||
|
|
6c6bb78568 | ||
|
|
178c62512d | ||
|
|
9eaf9fd933 | ||
|
|
04abf9dd76 | ||
|
|
355059df3c | ||
|
|
4a6c32877f | ||
|
|
258f6b5001 | ||
|
|
78d9501fce | ||
|
|
15975bb92c | ||
|
|
fef8c3cb1a | ||
|
|
4b226d7730 | ||
|
|
41abc8cab8 | ||
|
|
4d3b3834f5 | ||
|
|
9b1ba574ff | ||
|
|
ee7d950c15 | ||
|
|
8ca41f18be | ||
|
|
9d522bd9ef | ||
|
|
f9633616e2 | ||
|
|
9a0dd53cd7 | ||
|
|
6bbcec6163 | ||
|
|
fdee69bdaf | ||
|
|
2ec4adf7d5 | ||
|
|
709684d00f | ||
|
|
989fd1ec6d | ||
|
|
7d57c5abc0 | ||
|
|
0c42bb845d | ||
|
|
07037e8d49 | ||
|
|
a1fc3841df | ||
|
|
1fee156457 | ||
|
|
82bdccf7e7 | ||
|
|
7936c34472 | ||
|
|
365fd0bd5b | ||
|
|
f63ee41405 | ||
|
|
26359b9d16 | ||
|
|
5abdb577fb | ||
|
|
c67c4741e5 | ||
|
|
60bc4be9ea | ||
|
|
b83f87b6c8 | ||
|
|
0a73ba1a53 | ||
|
|
a4d397f24b | ||
|
|
809f231981 | ||
|
|
1499c637ee | ||
|
|
e5805a028b | ||
|
|
5cb0463cf6 | ||
|
|
12c208cad4 | ||
|
|
1946a4e68a | ||
|
|
dfed0af053 | ||
|
|
fc064aa9f8 | ||
|
|
edaf08361f | ||
|
|
92c93822b9 | ||
|
|
18a19096d3 | ||
|
|
7e707cd973 | ||
|
|
d33b8caf14 | ||
|
|
e533f9407f | ||
|
|
193a19b79c | ||
|
|
609a252ee0 | ||
|
|
3be70c3696 | ||
|
|
3c2fbd720f | ||
|
|
11b413d0dc | ||
|
|
a117fd7d93 | ||
|
|
794695c313 | ||
|
|
7478dd7f47 | ||
|
|
2d17501072 | ||
|
|
034bb71b10 | ||
|
|
97f102533c | ||
|
|
a90b6a8589 | ||
|
|
e8f6c665f9 | ||
|
|
6dac89e9a1 | ||
|
|
4f512a6cdd | ||
|
|
94f162c189 | ||
|
|
f5807d215f | ||
|
|
3189e65bad | ||
|
|
3febc53061 | ||
|
|
efd8135341 | ||
|
|
56405eff6c | ||
|
|
e0fad9d9a9 | ||
|
|
8fe942d8d5 | ||
|
|
cf98e789d8 | ||
|
|
ff58b9a317 | ||
|
|
65e7d390b8 | ||
|
|
54ae378e30 | ||
|
|
347695cf66 | ||
|
|
8f4d854b0d | ||
|
|
94bd846726 | ||
|
|
0365841549 | ||
|
|
74bd1f64a0 | ||
|
|
5bce03410e | ||
|
|
32ff658e84 | ||
|
|
c10ceed753 | ||
|
|
eb45b56a87 | ||
|
|
283b150d60 | ||
|
|
7329a22132 | ||
|
|
50b48d956f | ||
|
|
55d7a05af8 | ||
|
|
6c70b0655a | ||
|
|
0004eac764 | ||
|
|
5fe24465d7 | ||
|
|
364ceb2399 | ||
|
|
88b90986b1 | ||
|
|
5143823e43 | ||
|
|
363e23dd13 | ||
|
|
44a6190e17 | ||
|
|
4475ed0dea | ||
|
|
6a23da3de3 | ||
|
|
0f1d5a7730 | ||
|
|
5b4c3bb668 | ||
|
|
ad49f9d788 | ||
|
|
397ceefa02 | ||
|
|
e11b1ca4e8 | ||
|
|
8e983e7286 | ||
|
|
f970ae7529 | ||
|
|
b0973b5ca8 | ||
|
|
4784bf9dba | ||
|
|
6b8dbf5235 | ||
|
|
48f698e84b | ||
|
|
ec0cdf8b96 | ||
|
|
2a6cc01eed | ||
|
|
acc1365101 | ||
|
|
c5409c78ba | ||
|
|
b97de6c06b | ||
|
|
abe755cb69 | ||
|
|
4e3f499d76 | ||
|
|
3cebe51796 | ||
|
|
25bd17dc6e | ||
|
|
2525f54dc3 | ||
|
|
2127bb7e69 | ||
|
|
ed6d74f1ba | ||
|
|
02dd11f196 | ||
|
|
37b9ae30e2 | ||
|
|
0463dbcc75 | ||
|
|
111ef97d9c | ||
|
|
e8e854e392 | ||
|
|
cedbaf70c0 | ||
|
|
ff43b9ab3e | ||
|
|
47c4ba9dd6 | ||
|
|
2899aa1b19 | ||
|
|
ecff16c89d | ||
|
|
6ff738144a | ||
|
|
26028bb1eb | ||
|
|
eb4d5ddfd5 | ||
|
|
093ee006e4 | ||
|
|
9f8aa15af8 | ||
|
|
74d66a0131 | ||
|
|
626a86dea7 | ||
|
|
9ab029a296 | ||
|
|
8e1a81ae53 | ||
|
|
d76e1a3204 | ||
|
|
b585782007 | ||
|
|
2d198bcef7 | ||
|
|
0edcd9174f | ||
|
|
daa5b44f8e | ||
|
|
949660bc01 | ||
|
|
899a0b75b0 | ||
|
|
8cdb2afa69 | ||
|
|
00ec2ce33e | ||
|
|
2f7fd95684 | ||
|
|
55b1794004 | ||
|
|
e20972d4e7 | ||
|
|
749d727f50 | ||
|
|
9b524728c0 | ||
|
|
f81b4b9680 | ||
|
|
d2eaea7a44 | ||
|
|
f77c2dae23 | ||
|
|
a72737fdd5 | ||
|
|
4ab6b72e6f | ||
|
|
1468e74a6c | ||
|
|
09b5a21af1 | ||
|
|
6ad0d8e42f | ||
|
|
deb3b2f412 | ||
|
|
893391a3d1 | ||
|
|
7503d52857 | ||
|
|
fb860981d6 | ||
|
|
f302c7fb74 | ||
|
|
a8be2a77cf | ||
|
|
c2345e6118 | ||
|
|
539f8f3343 | ||
|
|
9a06c1e35b | ||
|
|
382de0d6d6 | ||
|
|
4883b3bb88 | ||
|
|
0a90523d61 | ||
|
|
fa63f2a838 | ||
|
|
fd8ac3ae37 | ||
|
|
51c1b193e5 | ||
|
|
ee99bcf8a1 | ||
|
|
324086ca49 | ||
|
|
e9610eaede | ||
|
|
7d5c714211 | ||
|
|
24e275fdb3 | ||
|
|
597b9d0e17 | ||
|
|
4d710a1aaf | ||
|
|
5de033814b | ||
|
|
aaec840d8c | ||
|
|
e579cf6ceb | ||
|
|
798e72f663 | ||
|
|
e79d862256 | ||
|
|
53133db456 | ||
|
|
39f8484b2a | ||
|
|
892256c0b9 | ||
|
|
0545945697 | ||
|
|
ad0125fe0d | ||
|
|
fb325b5447 | ||
|
|
56ff9e6344 | ||
|
|
74b431481d | ||
|
|
12102ef641 | ||
|
|
445541c38f | ||
|
|
820f03e162 | ||
|
|
516a958c66 | ||
|
|
7da101d5b7 | ||
|
|
9667f3309b | ||
|
|
82735f3c02 | ||
|
|
752acb591f | ||
|
|
43d851c7ef | ||
|
|
95e1fc6b5f | ||
|
|
a8a12a3b91 | ||
|
|
63a95723ac | ||
|
|
02f806ab99 | ||
|
|
da6526d5fa | ||
|
|
347d166250 | ||
|
|
ef22d9d07b | ||
|
|
e4fd1e78f5 | ||
|
|
4acbbf6e13 | ||
|
|
16f20dc01d | ||
|
|
8e4b3d12bd | ||
|
|
09b1bf6e2d | ||
|
|
7e4aa24459 | ||
|
|
7c94999efc | ||
|
|
e27d4f11ee | ||
|
|
914c5b4870 | ||
|
|
882f802585 | ||
|
|
5579780b12 | ||
|
|
fd6e41c566 | ||
|
|
984d2a47b8 | ||
|
|
92bae0c439 | ||
|
|
af5d7465a1 | ||
|
|
b620052b88 | ||
|
|
c13a7a7873 | ||
|
|
65b199d392 | ||
|
|
dc0b86281e | ||
|
|
b8796991b5 | ||
|
|
83447fff62 | ||
|
|
4a02c252d5 | ||
|
|
cb88df04b0 | ||
|
|
e888df2b9f | ||
|
|
17af07e4bb | ||
|
|
d1aed36154 | ||
|
|
eb97f7a661 | ||
|
|
be822ccc93 | ||
|
|
e2b52eed61 | ||
|
|
21717985ac | ||
|
|
3c4ffee7d3 | ||
|
|
3e1a457609 | ||
|
|
b28f0dc5e4 | ||
|
|
29561ed75d | ||
|
|
2e931d1f67 | ||
|
|
c907f22275 | ||
|
|
19ccac5c05 | ||
|
|
f9e3797cdd | ||
|
|
a30379bfdb | ||
|
|
dad1b4dfa6 | ||
|
|
643e09a4e6 | ||
|
|
56fc2d8b44 | ||
|
|
786f2f8678 | ||
|
|
ed689dba41 | ||
|
|
f779117ed6 | ||
|
|
c9e7e00f42 | ||
|
|
6019945d83 | ||
|
|
e0aed060aa | ||
|
|
1b03be774d | ||
|
|
c7ad61e319 | ||
|
|
563a32ed62 | ||
|
|
1d4b88339e | ||
|
|
1e2e88e299 | ||
|
|
29dda4ec66 | ||
|
|
6ccbdeb89a | ||
|
|
5ae460c922 | ||
|
|
48f3cc419b | ||
|
|
52e9341dab | ||
|
|
411c39b148 | ||
|
|
574ad0445e | ||
|
|
5b2bc6bff9 | ||
|
|
8a113e2bcb | ||
|
|
9aaf3ff5d8 | ||
|
|
6d612f42a8 | ||
|
|
e2fdc29ca0 | ||
|
|
b17dd04329 | ||
|
|
9a81d4a293 | ||
|
|
5f971ea7e8 | ||
|
|
a68feda29c | ||
|
|
579c411900 | ||
|
|
699f847d4a | ||
|
|
c370ac0609 | ||
|
|
5db18ab749 | ||
|
|
14ce139135 | ||
|
|
1745907bcb | ||
|
|
344c269f34 | ||
|
|
853feecfcc | ||
|
|
28647d6902 | ||
|
|
c89633fcd5 | ||
|
|
6e3d040127 | ||
|
|
a2ac836629 | ||
|
|
3c91f29a91 | ||
|
|
78600079a4 | ||
|
|
429f8ec85e | ||
|
|
8e5fce3e96 | ||
|
|
13238ae7b4 | ||
|
|
569f94e885 | ||
|
|
6e244d0657 | ||
|
|
97356328be | ||
|
|
f81fa2eb63 | ||
|
|
316a3c950f | ||
|
|
92528edbfb | ||
|
|
32fdb3ed88 | ||
|
|
63f824e3dd | ||
|
|
543d2d9d50 | ||
|
|
6f94f4d882 | ||
|
|
906141a415 | ||
|
|
2cfaf4e231 | ||
|
|
54217217aa | ||
|
|
c492e2de28 | ||
|
|
73fec61409 | ||
|
|
ea70429889 | ||
|
|
9febe47975 | ||
|
|
22d971db4b | ||
|
|
5139198691 | ||
|
|
0e1f720419 | ||
|
|
72896e052c | ||
|
|
469c24751e | ||
|
|
688a013d73 | ||
|
|
ff53866e9e | ||
|
|
1bac6174ad | ||
|
|
c451bf5e03 | ||
|
|
03d2f4ca32 | ||
|
|
46f02331fd | ||
|
|
7c3f2399c2 | ||
|
|
ea02190ad5 | ||
|
|
e2a148c25f | ||
|
|
b2eb5d2754 | ||
|
|
c72dc0d2c4 | ||
|
|
a40b078e9c | ||
|
|
61b7165bac | ||
|
|
a6f1f21c18 | ||
|
|
b734ffcf9d | ||
|
|
6d8301b159 | ||
|
|
66bdd923d7 | ||
|
|
879da92419 | ||
|
|
b9356a5653 | ||
|
|
d21c027db8 | ||
|
|
fe93334f86 | ||
|
|
d588e14a58 | ||
|
|
0b7b544d4e | ||
|
|
664bb692b6 | ||
|
|
970fba90e0 | ||
|
|
f6c338b50e | ||
|
|
3bc708b910 | ||
|
|
041325d67e | ||
|
|
51600e7ad0 | ||
|
|
4a0e3c9a69 | ||
|
|
41ff0241af | ||
|
|
12d62bde98 | ||
|
|
138e08e985 | ||
|
|
94408e8a9f | ||
|
|
f3c7d096bc | ||
|
|
774ed5d31e | ||
|
|
b07174b533 | ||
|
|
45de2cf1db | ||
|
|
81fe230be4 | ||
|
|
6673871db2 | ||
|
|
316bd58b68 | ||
|
|
ac4c375243 | ||
|
|
5da142ab83 | ||
|
|
cbf711ee60 | ||
|
|
4f5c1dc6d7 | ||
|
|
75c89b3d0b | ||
|
|
b8513eb0b6 | ||
|
|
a74ec95a6a | ||
|
|
0bc40fd676 | ||
|
|
b9e28db089 | ||
|
|
1f6b33f4f6 | ||
|
|
049707acdc | ||
|
|
886f166e66 | ||
|
|
3f9fda8a2d | ||
|
|
d32fce98ae | ||
|
|
5b9e39a449 | ||
|
|
4b931f782e | ||
|
|
24b591ed62 | ||
|
|
a41ee9c3ca | ||
|
|
5f5c835533 | ||
|
|
bc29cce645 | ||
|
|
98f4f1cc99 | ||
|
|
d11fc1c07e | ||
|
|
e019bfe136 | ||
|
|
57f8db010b | ||
|
|
0e1a964e7c | ||
|
|
469d4b35c1 | ||
|
|
a78a815ccc | ||
|
|
9f7cffce21 | ||
|
|
5ee5460612 | ||
|
|
1651cda5b4 | ||
|
|
9370f9d68f | ||
|
|
2a7be1b24d | ||
|
|
2965fb2b47 | ||
|
|
6c3c29dd11 | ||
|
|
adb43dfee1 | ||
|
|
c0386b153e | ||
|
|
5cabceb08e | ||
|
|
b67049f9aa | ||
|
|
7a2fc5e2fd | ||
|
|
5f213b5f51 | ||
|
|
97c73aae16 | ||
|
|
101d77e4ae | ||
|
|
0f945881a1 | ||
|
|
bee4ba10cb | ||
|
|
7e0f575e0a | ||
|
|
79c1da6d14 | ||
|
|
8dc86209df | ||
|
|
c61b2d2d3f | ||
|
|
c1f2437998 | ||
|
|
1039591677 | ||
|
|
d5568608f5 | ||
|
|
6bdcfaaef0 | ||
|
|
101d55bafa | ||
|
|
fa8ba061fb | ||
|
|
1b362673c0 | ||
|
|
11d654e902 | ||
|
|
e6e964aa8c | ||
|
|
c0dc9b1882 | ||
|
|
5b613bcf84 | ||
|
|
c71d14cafa | ||
|
|
60a13aaf17 | ||
|
|
c1f77dd92f | ||
|
|
ce4c590b1c | ||
|
|
3e1ecd60a1 | ||
|
|
2171faa330 | ||
|
|
d5e4ea385d | ||
|
|
d28b89f03e | ||
|
|
6adcc61447 | ||
|
|
ecde12ec23 | ||
|
|
c66027ae8a | ||
|
|
32f9c95dd0 | ||
|
|
1568e5a2a7 | ||
|
|
e4a534cb7c | ||
|
|
ee2cca17fe | ||
|
|
0869eaafdd | ||
|
|
69d4b3f93d | ||
|
|
61b37f38e2 | ||
|
|
c69c560de0 | ||
|
|
0d3e426dff | ||
|
|
4fe68d3b9f | ||
|
|
dab6ad917c | ||
|
|
a20b82b9cf | ||
|
|
087fd81879 | ||
|
|
d1dbbae101 | ||
|
|
3a2baba746 | ||
|
|
831f0ee5d9 | ||
|
|
e10fb64d6b | ||
|
|
8ecb71fb55 | ||
|
|
dea4106569 | ||
|
|
2dd8fb2ee2 | ||
|
|
2218be5d34 | ||
|
|
d712f07b96 | ||
|
|
b657405e46 | ||
|
|
974c320925 | ||
|
|
dd236b925d | ||
|
|
e264d71048 | ||
|
|
23f83b9377 | ||
|
|
db68721834 | ||
|
|
6a9cf2ed28 | ||
|
|
3dd79d447b | ||
|
|
e87ac72281 | ||
|
|
e430109228 | ||
|
|
e7e123af0d | ||
|
|
7b75dacb03 | ||
|
|
493f2a508b | ||
|
|
70b8aaf845 | ||
|
|
ab1c9bfdbc | ||
|
|
643d820965 | ||
|
|
8aa5c3ca65 | ||
|
|
7160589ac7 | ||
|
|
21cc1d43de | ||
|
|
793289ad97 | ||
|
|
bea2f00a90 | ||
|
|
d08da18b4a | ||
|
|
1e03fcff67 | ||
|
|
ffb516d343 | ||
|
|
45f9913bdb | ||
|
|
a6e9cc03c0 | ||
|
|
b5db2d565a | ||
|
|
e5518b1067 | ||
|
|
a5c9ed8d17 | ||
|
|
b5094a3cc9 | ||
|
|
99c5c8339d | ||
|
|
9f7e0f8a26 | ||
|
|
75bcbe52fd | ||
|
|
8c4a239631 | ||
|
|
503d9c34f8 | ||
|
|
d9f38c38a6 | ||
|
|
598d0705fb | ||
|
|
a0c08e841d | ||
|
|
8ed2b2475c | ||
|
|
e4e0a24a06 | ||
|
|
9839e2bb60 | ||
|
|
db10ed8378 | ||
|
|
ebffac7ba4 | ||
|
|
f99dd4f89a | ||
|
|
6badc0f419 | ||
|
|
aa1b69d7f2 | ||
|
|
eb3fec1ac0 | ||
|
|
0f772d55ab | ||
|
|
9cea6775d1 | ||
|
|
7e376071f5 | ||
|
|
9a937fa072 | ||
|
|
84c36a4eec | ||
|
|
155371cdd0 | ||
|
|
87e1749553 | ||
|
|
4ba7237326 | ||
|
|
6f1a375fee | ||
|
|
350160833b | ||
|
|
e4c51aece4 | ||
|
|
dfc192cb68 | ||
|
|
2a68372713 | ||
|
|
8af5235e4d | ||
|
|
7cf96d7d7e | ||
|
|
9c4831fa3f | ||
|
|
8cf1ffd38b | ||
|
|
3c70a4f455 | ||
|
|
ddb6a88392 | ||
|
|
61843a4997 | ||
|
|
3b9a7fe805 | ||
|
|
b686579acc | ||
|
|
01ede08a79 | ||
|
|
ae11d5ee3d | ||
|
|
9246878d0e | ||
|
|
5387c373e0 | ||
|
|
da76d1065e | ||
|
|
2213399f5e | ||
|
|
52dfa5e8c3 | ||
|
|
90058b2dae | ||
|
|
e695c8ee5c | ||
|
|
849e065bb2 | ||
|
|
b7cd07c996 | ||
|
|
52ee3863ae | ||
|
|
5ce5a08e41 | ||
|
|
8a16893082 | ||
|
|
c6a8f923e4 | ||
|
|
b6dd2248c8 | ||
|
|
1588179bc9 | ||
|
|
7f36621882 | ||
|
|
e256d36cd1 | ||
|
|
b2417ad902 | ||
|
|
67a32a98a9 | ||
|
|
78d9d5159a | ||
|
|
e2d29b8fa2 | ||
|
|
880c8819b4 | ||
|
|
6075cc5c95 | ||
|
|
5c1854948c | ||
|
|
7bd0cbce10 | ||
|
|
9c645a1efa | ||
|
|
6f088fd76a | ||
|
|
cb7a465d6c | ||
|
|
416f5e0986 | ||
|
|
86133ba52b | ||
|
|
047479426a | ||
|
|
fb2d292cbf | ||
|
|
75cf552e72 | ||
|
|
77537e7005 | ||
|
|
dae6ad2951 | ||
|
|
8a816ba44f | ||
|
|
a9d918aa95 | ||
|
|
a980904e38 | ||
|
|
4008c2bfd5 | ||
|
|
1184e52ba9 | ||
|
|
adbf40914e | ||
|
|
9b9083dfa1 | ||
|
|
6bd3b4998e | ||
|
|
1f602c00be | ||
|
|
4d0f7c2e02 | ||
|
|
c9024c5611 | ||
|
|
a92dc2bbe6 | ||
|
|
401fa3dcdd | ||
|
|
4e5373de73 | ||
|
|
956fbb7833 | ||
|
|
6217d3aacd | ||
|
|
8b1ae309fb | ||
|
|
52d24ff2f2 | ||
|
|
7a66bdf139 | ||
|
|
16bc12c15b | ||
|
|
0556d68a4e | ||
|
|
586c7fa927 | ||
|
|
9ef16ebcf9 | ||
|
|
d509445519 | ||
|
|
d7bff599b7 | ||
|
|
cda54085b9 | ||
|
|
984aae1ca6 | ||
|
|
695c99119f | ||
|
|
d7e205aee7 | ||
|
|
09919cb3cb | ||
|
|
ba73e04046 | ||
|
|
88cbf30fde | ||
|
|
ed37add29f | ||
|
|
6d25f9c205 | ||
|
|
01d30bb742 | ||
|
|
a1fec5f6ac | ||
|
|
ef9ddd27a5 | ||
|
|
b6203e57ed | ||
|
|
3fcea4ba2f | ||
|
|
a51f85826c | ||
|
|
c846945905 | ||
|
|
e2af21e0e1 | ||
|
|
929250810f | ||
|
|
cb162e063d | ||
|
|
63ffb9df14 | ||
|
|
a917d6c2c5 | ||
|
|
9e1e0dee1d | ||
|
|
7c1a2d5f91 | ||
|
|
cae33cfc4f | ||
|
|
d143df3f9f | ||
|
|
84a3817b15 | ||
|
|
525eb83d1e | ||
|
|
8b7295d77e | ||
|
|
df57c196e9 | ||
|
|
5ea5473bdd | ||
|
|
85faf8d517 | ||
|
|
abe6dbb5a2 | ||
|
|
afa446aabe | ||
|
|
2712f9a3f4 | ||
|
|
c40de5d3b2 | ||
|
|
2b1da81b98 | ||
|
|
d693d26323 | ||
|
|
2374bb56fa | ||
|
|
df71782719 | ||
|
|
599e718003 | ||
|
|
1cad816b17 | ||
|
|
0fa6d2980b | ||
|
|
c27818b3b0 | ||
|
|
047b3bc079 | ||
|
|
70e6920288 | ||
|
|
b5739c663d | ||
|
|
220d98a668 | ||
|
|
419b6eb626 | ||
|
|
9764fb481f | ||
|
|
ba01edc691 | ||
|
|
fba647313d | ||
|
|
2f146fffdc | ||
|
|
7654c79d12 | ||
|
|
cf35dbfd6e | ||
|
|
a6c002146c | ||
|
|
bb3009a124 | ||
|
|
b744363736 | ||
|
|
35c25987cd | ||
|
|
ca91a9e089 | ||
|
|
83ba3d4450 | ||
|
|
8fe0d342aa | ||
|
|
a4eff0b408 | ||
|
|
7b85da901d | ||
|
|
be27789ea8 | ||
|
|
07a443f6c4 | ||
|
|
588e89e8fe | ||
|
|
789c120fc9 | ||
|
|
fdfe54b6da | ||
|
|
3b50741f19 | ||
|
|
c5498b92a2 | ||
|
|
048150d779 | ||
|
|
7db933199a | ||
|
|
5c6be439e8 | ||
|
|
4e0134b70a | ||
|
|
d6ddf8e9f4 | ||
|
|
2facb160aa | ||
|
|
b44b8d09b2 | ||
|
|
65d9c6fe2f | ||
|
|
c522196029 | ||
|
|
a5d097e860 | ||
|
|
668f6ee36f | ||
|
|
4f2363230d | ||
|
|
2b93552d1d | ||
|
|
124af0b76d | ||
|
|
972df7c167 | ||
|
|
b4c17a6a12 | ||
|
|
92c03cbdf9 | ||
|
|
220478cd31 | ||
|
|
df905ade88 | ||
|
|
9ff3e22c80 | ||
|
|
37df882ed3 | ||
|
|
47050769fc | ||
|
|
65df759275 | ||
|
|
86761bd3a0 | ||
|
|
a842b6b925 | ||
|
|
09e4b24445 | ||
|
|
4916757d59 | ||
|
|
30b66adc3b | ||
|
|
13582d1a7b | ||
|
|
0b9312b549 | ||
|
|
bde51d8d38 | ||
|
|
643a666853 | ||
|
|
2d10fa0218 | ||
|
|
a59184ae5f | ||
|
|
82807fcc1b | ||
|
|
a6c93ef9b8 | ||
|
|
6a151865f7 | ||
|
|
414d8d140e | ||
|
|
51fb9dca58 | ||
|
|
6367785b1b | ||
|
|
03b2a9da66 | ||
|
|
aa7fb7da06 | ||
|
|
26d11de249 | ||
|
|
a9d5b53460 | ||
|
|
0daa9f1882 | ||
|
|
f799740d70 | ||
|
|
56886dcfe9 | ||
|
|
81e1e4a7ff | ||
|
|
9b5256716f | ||
|
|
446bf80f1d | ||
|
|
775b12aec1 | ||
|
|
6a80455c6c | ||
|
|
43b2ff7957 | ||
|
|
295b7779ee | ||
|
|
d1df088662 | ||
|
|
2b0f7aaf8a | ||
|
|
3265dd76ab | ||
|
|
d1d7b44303 | ||
|
|
56eced3813 | ||
|
|
bde2147dd3 | ||
|
|
c853f2976f | ||
|
|
8901f5d40e | ||
|
|
b66931003f | ||
|
|
9a75d2ac8f | ||
|
|
9132d47f4d | ||
|
|
42c5aea3f7 | ||
|
|
e2fd9c4cee | ||
|
|
f847b7ff62 | ||
|
|
9eae8f5077 | ||
|
|
2bacf76664 | ||
|
|
b2030caedc | ||
|
|
956c975c6d | ||
|
|
41bd321a4f | ||
|
|
952e9687d0 | ||
|
|
c298f8b952 | ||
|
|
e2562a5251 | ||
|
|
dbdb40baf9 | ||
|
|
52f40d982d | ||
|
|
fd04cec606 | ||
|
|
2ff923dd1b | ||
|
|
f4f13f91f2 | ||
|
|
034aa980e6 | ||
|
|
6ac7a51ce0 | ||
|
|
cf0c0e3e2c | ||
|
|
1b899575e0 | ||
|
|
23e5cb5669 | ||
|
|
ee9578b273 | ||
|
|
e4ba4c9b37 | ||
|
|
9ed64bdc9a | ||
|
|
e9b6fb55ff | ||
|
|
80caf881ae | ||
|
|
35c0ed2ba5 | ||
|
|
c36db3545f | ||
|
|
1ea0ba18cd | ||
|
|
327c83cbc8 | ||
|
|
a367585ab4 | ||
|
|
2994cb5c65 | ||
|
|
1bedb31a3c | ||
|
|
8fecebc254 | ||
|
|
44497a0969 | ||
|
|
5362371bda | ||
|
|
8b04e96a7d | ||
|
|
5d93334426 | ||
|
|
be84f3314f | ||
|
|
150b666d4b | ||
|
|
94579d65c4 | ||
|
|
551b06b4e8 | ||
|
|
76fc47a274 | ||
|
|
07b5760986 | ||
|
|
35e1bfcd7f | ||
|
|
b06ffc0eef | ||
|
|
24df7913fe | ||
|
|
83674e4b35 | ||
|
|
22d3aeb7b5 | ||
|
|
8809eef2ce | ||
|
|
cf005711c0 | ||
|
|
0a00d0c52f | ||
|
|
9aa17a0395 | ||
|
|
e4d190f1e7 | ||
|
|
65ecdf7dc2 | ||
|
|
0dfa5994cc | ||
|
|
5d2844fdb6 | ||
|
|
44332b9d07 | ||
|
|
9b8e73f1de | ||
|
|
20a23e148c | ||
|
|
076f0d5de9 | ||
|
|
0bcb6206f4 | ||
|
|
943b9827ee | ||
|
|
741f3ec212 | ||
|
|
613b6839b8 | ||
|
|
8549a17675 | ||
|
|
718cfccbea | ||
|
|
2458fa26d8 | ||
|
|
ac24684d2b | ||
|
|
106dbd9538 | ||
|
|
f9efb2b800 | ||
|
|
897d124d5b | ||
|
|
34daf9ccac | ||
|
|
269a97e81e | ||
|
|
2fd57621d8 | ||
|
|
76de837214 | ||
|
|
1e41020728 | ||
|
|
8a78e49bf0 | ||
|
|
e6726e4c02 | ||
|
|
76330a4a1a | ||
|
|
7e5f0097e4 | ||
|
|
18e1c02d1c | ||
|
|
28992f178e | ||
|
|
c41f34c352 | ||
|
|
6b5580a30c | ||
|
|
1dee14e32d | ||
|
|
1e3c4881d0 | ||
|
|
657964cda4 | ||
|
|
893aac916c | ||
|
|
68da6cf3ae | ||
|
|
0d96ea9eef | ||
|
|
0ceb44a7cd | ||
|
|
4fec0036cb | ||
|
|
f82eee4636 | ||
|
|
260cfb96ec | ||
|
|
f71a519674 | ||
|
|
369c146eca | ||
|
|
83264a6946 | ||
|
|
3c3d4e9109 | ||
|
|
ce55365292 | ||
|
|
be495839b6 | ||
|
|
a27a9f55a7 | ||
|
|
10e14caf35 | ||
|
|
59af246479 | ||
|
|
1f52eaca01 | ||
|
|
d833f4b5ff | ||
|
|
bfee39049d | ||
|
|
b4599df6c6 | ||
|
|
261c6f6956 | ||
|
|
b97d77c848 | ||
|
|
67ca7e3097 | ||
|
|
26fa8e75bd | ||
|
|
aeaa45b713 | ||
|
|
e1a3a3e7c7 |
@@ -8,3 +8,9 @@ indent_size = 2
|
||||
indent_style = space
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.go]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = tab
|
||||
|
||||
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: ["https://profile.ikit.fun/sponsors/"]
|
||||
80
.github/ISSUE_TEMPLATE/1-bug_report.yml
vendored
Normal file
80
.github/ISSUE_TEMPLATE/1-bug_report.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: "🐞 Bug Report"
|
||||
description: "报告缺陷来帮助我们完善。 / Create a report to help us improve."
|
||||
title: "[Bug] 简要描述你发现的缺陷"
|
||||
labels:
|
||||
- bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Welcome!
|
||||
|
||||
**在提交 Issue 之前,请确认以下事项**:
|
||||
1. 我**确认**已尝试过使用当前最新版本,并能复现问题。由于开发者精力有限,非当前最新版本的问题将被直接关闭,感谢理解。
|
||||
2. 我**确认**已搜索过[已有的 Issues](https://github.com/certimate-go/certimate/issues)(包括已关闭的),没有类似的问题。
|
||||
3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。
|
||||
4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。
|
||||
5. 请保持每个 Issue 只包含一个缺陷报告。如果有多个缺陷,请分别提交 Issue。
|
||||
|
||||
**Before you submit the issue, please make sure of the following checklist**:
|
||||
1. Yes, I'm using the latest release and can reproduce the issue. Issues that are not in the latest version will be closed directly.
|
||||
2. Yes, I've searched for [existing issues](https://github.com/certimate-go/certimate/issues) (including closed ones) on GitHub and didn't find any similar.
|
||||
3. Yes, I've read the [documentation](https://docs.certimate.me/en/) and didn't find any similar.
|
||||
4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly.
|
||||
5. Please limit one report per issue.
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: 软件版本 / Release Version
|
||||
description: 请提供 Certimate 的具体版本(请不要填写 `latest` 之类的无效版本号)。 / Please provide the specific version of Certimate.
|
||||
placeholder: (e.g. v1.0.0)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 缺陷描述 / Description
|
||||
description: 请详细清晰地描述你发现的缺陷或故障,如果可能请上传截图。 / Describe the bug you found in detail and clearly, and upload screenshots if possible.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 复现步骤 / Steps to reproduce
|
||||
description: 请提供可复现的完整步骤。 / Please walk us through it step by step.
|
||||
placeholder: |
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 日志 / Logs
|
||||
description: 在此处添加日志信息(如果有的话)。 / Add logs here if available.
|
||||
value: |-
|
||||
<details>
|
||||
|
||||
```console
|
||||
# 请在此粘贴日志 / Paste logs here
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 其他 / Miscellaneous
|
||||
description: 在此处添加关于该 Issue 的任何其他信息。 / Add any other context about the issue here.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 贡献 / Contribution
|
||||
options:
|
||||
- label: 我乐意为此贡献 PR! / I am interested in contributing a PR for this!
|
||||
required: false
|
||||
52
.github/ISSUE_TEMPLATE/2-feature_request.yml
vendored
Normal file
52
.github/ISSUE_TEMPLATE/2-feature_request.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: "💡 Feature Request"
|
||||
description: "提出新功能请求或改进意见。 / Suggest an idea for this project."
|
||||
title: "[Feature] 简要描述你希望实现的功能"
|
||||
labels:
|
||||
- enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Welcome!
|
||||
|
||||
**在提交 Issue 之前,请确认以下事项**:
|
||||
1. 我**确认**是基于当前最新大版本而提出的新功能请求或改进意见。
|
||||
2. 我**确认**已搜索过[已有的 Issues](https://github.com/certimate-go/certimate/issues)(包括已关闭的),没有类似的问题。
|
||||
3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。
|
||||
4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。
|
||||
5. 请保持每个 Issue 只包含一个功能请求。如果有多个需求,请分别提交 Issue。
|
||||
|
||||
**Before you submit the issue, please make sure of the following checklist**:
|
||||
1. Yes, I'm using the latest release.
|
||||
2. Yes, I've searched for [existing issues](https://github.com/certimate-go/certimate/issues) (including closed ones) on GitHub and didn't find any similar.
|
||||
3. Yes, I've read the [documentation](https://docs.certimate.me/en/) and didn't find any similar.
|
||||
4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly.
|
||||
5. Please limit one request per issue.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 功能描述 / Description
|
||||
description: 请详细清晰地描述你希望添加的功能,如果可能请上传截图。 / Describe the feature you'd like to add in detail and clearly, and upload screenshots if possible.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 请求动机 / Motivation
|
||||
description: 为什么这个功能对项目有帮助? / Why is this feature helpful to the project?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 其他 / Miscellaneous
|
||||
description: 在此处添加关于该 Issue 的任何其他信息(新增提供商请求请补充 API 文档链接等资料)。 / Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 贡献 / Contribution
|
||||
options:
|
||||
- label: 我乐意为此贡献 PR! / I am interested in contributing a PR for this!
|
||||
required: false
|
||||
44
.github/ISSUE_TEMPLATE/3-questions.yml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/3-questions.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: "❓ Questions"
|
||||
description: "遇到了困难需要求助? / Have problem in use and need help?"
|
||||
title: "简要描述你遇到的问题"
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Welcome!
|
||||
|
||||
**在提交 Issue 之前,请确认以下事项**:
|
||||
1. 我**确认**正在使用的是当前最新版本。
|
||||
2. 我**确认**已搜索过[已有的 Issues](https://github.com/certimate-go/certimate/issues)(包括已关闭的),没有类似的问题。
|
||||
3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。
|
||||
4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。
|
||||
5. 请保持每个 Issue 只包含一个问题求助。如果有多个问题,请分别提交 Issue。
|
||||
|
||||
**Before you submit the issue, please make sure of the following checklist**:
|
||||
1. Yes, I'm using the latest release.
|
||||
2. Yes, I've searched for [existing issues](https://github.com/certimate-go/certimate/issues) (including closed ones) on GitHub and didn't find any similar.
|
||||
3. Yes, I've read the [documentation](https://docs.certimate.me/en/) and didn't find any similar.
|
||||
4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly.
|
||||
5. Please limit one question per issue.
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: 软件版本 / Release Version
|
||||
description: 请提供 Certimate 的具体版本(请不要填写 `latest` 之类的无效版本号)。 / Please provide the specific version of Certimate.
|
||||
placeholder: (e.g. v1.0.0)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 问题描述 / Description
|
||||
description: 请详细清晰地描述你遇到的问题,如果可能请上传截图。 / Describe the problem you encountered in detail and clearly, and upload screenshots if possible.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 其他 / Miscellaneous
|
||||
description: 在此处添加关于该问题的任何其他信息。 / Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
||||
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,33 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: 创建一个报告来帮助我们改进
|
||||
title: "[Bug] 标题简要描述问题"
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**描述问题**
|
||||
简要描述问题是什么,1 个 ISSUE 只描述一个问题。
|
||||
|
||||
**复现步骤**
|
||||
复现该问题的步骤:
|
||||
|
||||
1. 去到 '...'
|
||||
2. 点击 '...'
|
||||
3. 滚动到 '...'
|
||||
4. 发现问题
|
||||
|
||||
**期望的结果**
|
||||
简要描述你期望发生的事情。
|
||||
|
||||
**截图**
|
||||
如有可能,请添加截图以帮助解释问题。
|
||||
|
||||
**环境**
|
||||
|
||||
- 操作系统: [e.g. Windows, macOS]
|
||||
- 浏览器: [e.g. Chrome, Safari]
|
||||
- 仓库版本: [e.g. v1.0.0]
|
||||
|
||||
**其他信息**
|
||||
在此处添加关于该问题的任何其他信息。
|
||||
9
.github/ISSUE_TEMPLATE/config.yml
vendored
9
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 加入频道讨论
|
||||
url: https://t.me/+ZXphsppxUg41YmVl
|
||||
about: 加入到电报频道寻求更多帮助
|
||||
- name: "🌐 加入频道讨论"
|
||||
about: "加入到电报频道寻求更多帮助。 / Join in our Telegram channel."
|
||||
url: "https://t.me/+ZXphsppxUg41YmVl"
|
||||
- name: "📖 常见问题"
|
||||
about: "请先阅读文档 FAQ,可能会有你需要的答案。 / Please take a look to FAQs."
|
||||
url: "https://docs.certimate.me/docs/reference/faq"
|
||||
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: 提出一个新功能请求
|
||||
title: "[Feature] 简要描述你希望实现的功能"
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**功能描述**
|
||||
简要描述你希望添加的功能和相关问题,1 个 ISSUE 只描述一个功能。
|
||||
|
||||
**动机**
|
||||
为什么这个功能对项目有帮助?
|
||||
|
||||
**替代方案**
|
||||
描述你已经考虑过的替代方案。
|
||||
|
||||
**其他信息**
|
||||
在这里添加任何相关的附加信息或截图。
|
||||
20
.github/workflows/push_image.yml
vendored
20
.github/workflows/push_image.yml
vendored
@@ -1,15 +1,17 @@
|
||||
name: Docker Image CI
|
||||
name: Docker Image CI (stable versions)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
- "v[0-9]*"
|
||||
- "!v*alpha*"
|
||||
- "!v*beta*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Tag version to be used for Docker image"
|
||||
required: true
|
||||
default: "v0.1.9"
|
||||
default: "latest"
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
@@ -30,19 +32,22 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
usual2970/certimate
|
||||
registry.cn-shanghai.aliyuncs.com/usual2970/certimate
|
||||
certimate/certimate
|
||||
registry.cn-shanghai.aliyuncs.com/certimate/certimate
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern=v{{version}}
|
||||
type=semver,pattern=v{{major}}.{{minor}}
|
||||
|
||||
- name: Log in to DOCKERHUB
|
||||
uses: docker/login-action@v3
|
||||
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Log in to ALIYUNCS
|
||||
uses: docker/login-action@v3
|
||||
|
||||
with:
|
||||
registry: registry.cn-shanghai.aliyuncs.com
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
@@ -56,4 +61,3 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
|
||||
61
.github/workflows/push_image_next.yml
vendored
Normal file
61
.github/workflows/push_image_next.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: Docker Image CI (preview versions)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]*-alpha*"
|
||||
- "v[0-9]*-beta*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Tag version to be used for Docker image"
|
||||
required: true
|
||||
default: "next"
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
certimate/certimate
|
||||
registry.cn-shanghai.aliyuncs.com/certimate/certimate
|
||||
tags: |
|
||||
type=ref,event=tag,pattern={{version}}
|
||||
flavor: |
|
||||
latest=false
|
||||
|
||||
- name: Log in to DOCKERHUB
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Log in to ALIYUNCS
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: registry.cn-shanghai.aliyuncs.com
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
195
.github/workflows/release.yml
vendored
195
.github/workflows/release.yml
vendored
@@ -1,12 +1,12 @@
|
||||
name: basebuild
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
- "v[0-9]*"
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
prepare-ui:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -19,19 +19,190 @@ jobs:
|
||||
with:
|
||||
node-version: 20.11.0
|
||||
|
||||
- name: Build WebUI
|
||||
run: |
|
||||
npm --prefix=./ui ci
|
||||
npm --prefix=./ui run build
|
||||
|
||||
- name: Upload UI build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ui-build
|
||||
path: ./ui/dist
|
||||
retention-days: 1
|
||||
|
||||
build-linux:
|
||||
needs: prepare-ui
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ">=1.22.5"
|
||||
go-version-file: "go.mod"
|
||||
|
||||
- name: Build Admin dashboard UI
|
||||
run: npm --prefix=./ui ci && npm --prefix=./ui run build
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
- name: Download UI build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
name: ui-build
|
||||
path: ./ui/dist
|
||||
|
||||
- name: Build Linux binaries
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: linux
|
||||
run: |
|
||||
mkdir -p dist/linux
|
||||
for ARCH in amd64 arm64 armv7; do
|
||||
if [ "$ARCH" == "armv7" ]; then
|
||||
go env -w GOARCH=arm
|
||||
go env -w GOARM=7
|
||||
else
|
||||
go env -w GOARCH=$ARCH
|
||||
go env -u GOARM
|
||||
fi
|
||||
go build -ldflags="-s -w -X github.com/certimate-go/certimate.Version=${GITHUB_REF#refs/tags/}" -o dist/linux/certimate_${GITHUB_REF#refs/tags/}_linux_$ARCH
|
||||
done
|
||||
|
||||
- name: Upload Linux binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-binaries
|
||||
path: dist/linux/
|
||||
retention-days: 1
|
||||
|
||||
build-macos:
|
||||
needs: prepare-ui
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
|
||||
- name: Download UI build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-build
|
||||
path: ./ui/dist
|
||||
|
||||
- name: Build macOS binaries
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
run: |
|
||||
mkdir -p dist/darwin
|
||||
for ARCH in amd64 arm64; do
|
||||
go env -w GOARCH=$ARCH
|
||||
go build -ldflags="-s -w -X github.com/certimate-go/certimate.Version=${GITHUB_REF#refs/tags/}" -o dist/darwin/certimate_${GITHUB_REF#refs/tags/}_darwin_$ARCH
|
||||
done
|
||||
|
||||
- name: Upload macOS binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-binaries
|
||||
path: dist/darwin/
|
||||
retention-days: 1
|
||||
|
||||
build-windows:
|
||||
needs: prepare-ui
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
|
||||
- name: Download UI build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-build
|
||||
path: ./ui/dist
|
||||
|
||||
- name: Build Windows binaries
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: windows
|
||||
run: |
|
||||
mkdir -p dist/windows
|
||||
for ARCH in amd64 arm64; do
|
||||
go env -w GOARCH=$ARCH
|
||||
go build -ldflags="-s -w -X github.com/certimate-go/certimate.Version=${GITHUB_REF#refs/tags/}" -o dist/windows/certimate_${GITHUB_REF#refs/tags/}_windows_$ARCH.exe
|
||||
done
|
||||
|
||||
- name: Upload Windows binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-binaries
|
||||
path: dist/windows/
|
||||
retention-days: 1
|
||||
|
||||
create-release:
|
||||
needs: [build-linux, build-macos, build-windows]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download all binaries
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./artifacts
|
||||
|
||||
- name: Prepare release assets
|
||||
run: |
|
||||
mkdir -p dist
|
||||
cp -r artifacts/linux-binaries/* dist/
|
||||
cp -r artifacts/macos-binaries/* dist/
|
||||
cp -r artifacts/windows-binaries/* dist/
|
||||
|
||||
find dist -type f -not -name "*.exe" -exec chmod +x {} \;
|
||||
|
||||
cd dist
|
||||
for bin in certimate_*; do
|
||||
if [[ "$bin" == *".exe" ]]; then
|
||||
entrypoint="certimate.exe"
|
||||
else
|
||||
entrypoint="certimate"
|
||||
fi
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
cp "$bin" "${tmpdir}/${entrypoint}"
|
||||
cp ../LICENSE ../README.md ../CHANGELOG.md "$tmpdir"
|
||||
|
||||
if [[ "$bin" == *".exe" ]]; then
|
||||
zip -j "${bin%.exe}.zip" "$tmpdir"/*
|
||||
else
|
||||
zip -j -X "${bin}.zip" "$tmpdir"/*
|
||||
fi
|
||||
|
||||
rm -rf "$tmpdir"
|
||||
done
|
||||
|
||||
sha256sum *.zip > checksums.txt
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
dist/*.zip
|
||||
dist/checksums.txt
|
||||
draft: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -15,8 +15,6 @@ vendor
|
||||
pb_data
|
||||
build
|
||||
main
|
||||
/ui/dist/*
|
||||
!/ui/dist/.gitkeep
|
||||
./dist
|
||||
./certimate
|
||||
/dist
|
||||
/docker/data
|
||||
/certimate
|
||||
|
||||
@@ -11,7 +11,7 @@ builds:
|
||||
main: ./
|
||||
binary: certimate
|
||||
ldflags:
|
||||
- -s -w -X github.com/usual2970/certimate.Version={{ .Version }}
|
||||
- -s -w -X github.com/certimate-go/certimate.Version={{ .Version }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
@@ -30,17 +30,20 @@ builds:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
|
||||
# upx:
|
||||
# - enabled: true
|
||||
|
||||
release:
|
||||
draft: true
|
||||
|
||||
archives:
|
||||
- id: archive_noncgo
|
||||
builds: [build_noncgo]
|
||||
format: zip
|
||||
format: "zip"
|
||||
files:
|
||||
- CHANGELOG.md
|
||||
- LICENSE.md
|
||||
- LICENSE
|
||||
- README.md
|
||||
- CHANGELOG.md
|
||||
|
||||
checksum:
|
||||
name_template: "checksums.txt"
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -11,10 +11,11 @@
|
||||
"gopls": {
|
||||
"formatting.gofumpt": true,
|
||||
},
|
||||
"typescript.tsdk": "ui/node_modules/typescript/lib",
|
||||
"[go]": {
|
||||
"editor.defaultFormatter": "golang.go"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1 @@
|
||||
## v0.0.3
|
||||
|
||||
- 解决一些 bug
|
||||
- 添加 README.md
|
||||
|
||||
## v0.0.1
|
||||
|
||||
- Initial release
|
||||
A full changelog of past releases is available on [GitHub Releases](https://github.com/certimate-go/certimate/releases) page.
|
||||
|
||||
116
CONTRIBUTING.md
116
CONTRIBUTING.md
@@ -1,76 +1,106 @@
|
||||
# 向 Certimate 贡献代码
|
||||
# 贡献指南
|
||||
|
||||
感谢你抽出时间来改进 Certimate!以下是向 Certimate 主仓库提交 PR(Pull Request)时的操作指南。
|
||||
非常感谢你抽出时间来帮助改进 Certimate!以下是向 Certimate 提交 Pull Request 时的操作指南。
|
||||
|
||||
- [向 Certimate 贡献代码](#向-certimate-贡献代码)
|
||||
- [前提条件](#前提条件)
|
||||
- [修改 Go 代码](#修改-go-代码)
|
||||
- [修改管理页面 (Admin UI)](#修改管理页面-admin-ui)
|
||||
我们需要保持敏捷和快速迭代,同时也希望确保贡献者能获得尽可能流畅的参与体验。这份贡献指南旨在帮助你熟悉代码库和我们的工作方式,让你可以尽快进入有趣的开发环节。
|
||||
|
||||
## 前提条件
|
||||
索引:
|
||||
|
||||
- Go 1.22+ (用于修改 Go 代码)
|
||||
- Node 20+ (用于修改 UI)
|
||||
- [开发](#开发)
|
||||
- [要求](#要求)
|
||||
- [后端代码](#后端代码)
|
||||
- [前端代码](#前端代码)
|
||||
- [提交 PR](#提交-pr)
|
||||
- [提交流程](#提交流程)
|
||||
- [获取帮助](#获取帮助)
|
||||
|
||||
如果还没有这样做,你可以 fork Certimate 的主仓库,并克隆到本地以便进行修改:
|
||||
---
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your_username/certimate.git
|
||||
```
|
||||
## 开发
|
||||
|
||||
> **重要提示:**
|
||||
> 建议为每个 Bug 修复或新功能创建一个从 `main` 分支派生的新分支。如果你计划提交多个 PR,请保持不同的改动在独立分支中,以便更容易进行代码审查并最终合并。
|
||||
> 保持一个 PR 只实现一个功能。
|
||||
### 要求
|
||||
|
||||
## 修改 Go 代码
|
||||
- Go 1.24+(用于修改后端代码)
|
||||
- Node.js 22.0+(用于修改前端代码)
|
||||
|
||||
假设你已经对 Certimate 的 Go 代码做了一些修改,现在你想要运行它:
|
||||
### 后端代码
|
||||
|
||||
1. 进入根目录
|
||||
2. 运行以下命令启动服务:
|
||||
Certimate 的后端代码是使用 Golang 开发的,是一个基于 [Pocketbase](https://github.com/pocketbase/pocketbase) 构建的单体应用。
|
||||
|
||||
假设你已经对 Certimate 的后端代码做出了一些修改,现在你想要运行它,请遵循以下步骤:
|
||||
|
||||
1. 进入根目录;
|
||||
2. 安装依赖:
|
||||
```bash
|
||||
go mod vendor
|
||||
```
|
||||
3. 启动本地开发服务:
|
||||
```bash
|
||||
go run main.go serve
|
||||
```
|
||||
|
||||
这将启动一个 Web 服务器,默认运行在 `http://localhost:8090`,并使用来自 `ui/dist` 的预构建管理页面。
|
||||
这将启动一个 Web 服务器,默认运行在 `http://localhost:8090`,并使用来自 `/ui/dist` 的预构建管理页面。
|
||||
|
||||
**在向主仓库提交 PR 之前,建议你:**
|
||||
> 如果你遇到报错 `ui/embed.go: pattern all:dist: no matching files found`,请参考“[前端代码](#前端代码)”这一小节构建 WebUI。
|
||||
|
||||
- 使用 [gofumpt](https://github.com/mvdan/gofumpt) 对你的代码进行格式化。
|
||||
**在向主仓库提交 PR 之前,你应该:**
|
||||
|
||||
- 为你的改动添加单元测试或集成测试(Certimate 使用 Go 的标准 `testing` 包)。你可以通过以下命令运行测试(在项目根目录下):
|
||||
- 使用 [gofumpt](https://github.com/mvdan/gofumpt) 格式化你的代码。推荐使用 VSCode,并安装 gofumpt 插件,以便在保存时自动格式化。
|
||||
- 为你的改动添加单元测试或集成测试(使用 Go 标准库中的 `testing` 包)。
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
### 前端代码
|
||||
|
||||
## 修改管理页面 (Admin UI)
|
||||
Certimate 的前端代码是使用 TypeScript 开发的,是一个基于 [React](https://github.com/facebook/react) 和 [Vite](https://github.com/vitejs/vite) 构建的单页应用。
|
||||
|
||||
Certimate 的管理页面是一个基于 React 和 Vite 构建的单页应用(SPA)。
|
||||
|
||||
要启动 Admin UI:
|
||||
|
||||
1. 进入 `ui` 项目目录。
|
||||
|
||||
2. 运行 `npm install` 安装依赖。
|
||||
假设你已经对 Certimate 的前端代码做出了一些修改,现在你想要运行它,请遵循以下步骤:
|
||||
|
||||
1. 进入 `/ui` 目录;
|
||||
2. 安装依赖:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
3. 启动 Vite 开发服务器:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
你可以通过浏览器访问 `http://localhost:5173` 来查看运行中的管理页面。
|
||||
这将启动一个 Web 服务器,默认运行在 `http://localhost:5173`,你可以通过浏览器访问来查看运行中的 WebUI。
|
||||
|
||||
由于 Admin UI 只是一个客户端应用,运行时需要 Certimate 的后端服务作为支撑。你可以手动运行 Certimate,或者使用预构建的可执行文件。
|
||||
|
||||
所有对 Admin UI 的修改将会自动反映在浏览器中,无需手动刷新页面。
|
||||
|
||||
完成修改后,运行以下命令来构建 Admin UI,以便它能被嵌入到 Go 包中:
|
||||
完成修改后,运行以下命令来构建 WebUI,以便它能被嵌入到 Go 包中:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
完成所有步骤后,你可以将改动提交 PR 到 Certimate 主仓库。
|
||||
**在向主仓库提交 PR 之前,你应该:**
|
||||
|
||||
- 使用 [ESLint](https://github.com/eslint/eslint) 格式化你的代码。推荐使用 VSCode,并安装 ESLint+Prettier 插件,以便在保存时自动格式化。
|
||||
|
||||
## 提交 PR
|
||||
|
||||
在提交 PR 之前,请先创建一个 Issue 来讨论你的修改方案,并等待来自项目维护者的反馈。这样做有助于:
|
||||
|
||||
- 让我们充分理解你的修改内容;
|
||||
- 评估修改是否符合项目路线图;
|
||||
- 避免重复工作;
|
||||
- 防止你投入时间到可能无法被合并的修改中。
|
||||
|
||||
### 提交流程
|
||||
|
||||
1. Fork 本仓库;
|
||||
2. 在提交 PR 之前,请先发起 Issue 讨论你想要做的修改;
|
||||
3. 为你的修改创建一个新的分支;
|
||||
4. 请为你的修改添加相应的测试;
|
||||
5. 确保你的代码能通过现有的测试;
|
||||
6. 请在 PR 描述中关联相关 Issue;
|
||||
7. 等待合并!
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 建议为每个新功能或 Bug 修复创建一个从 `main` 分支派生的新分支。如果你计划提交多个 PR,请保持不同的改动在独立分支中,以便更容易进行代码审查并最终合并。
|
||||
>
|
||||
> 保持一个 PR 只实现一个功能或修复。
|
||||
|
||||
## 获取帮助
|
||||
|
||||
如果你在贡献过程中遇到困难或问题,可以通过 GitHub Issues 向我们提问。
|
||||
|
||||
@@ -1,81 +1,106 @@
|
||||
# Contributing to Certimate
|
||||
# Contribution Guide
|
||||
|
||||
Thank you for taking the time to improve Certimate! Below is a guide for submitting a PR (Pull Request) to the main Certimate repository.
|
||||
Thank you for taking the time to improve Certimate! Below is a guide for submitting a PR (Pull Request) to the Certimate repository.
|
||||
|
||||
- [Contributing to Certimate](#contributing-to-certimate)
|
||||
We need to be nimble and ship fast given where we are, but we also want to make sure that contributors like you get as smooth an experience at contributing as possible. We've assembled this contribution guide for that purpose, aiming at getting you familiarized with the codebase & how we work with contributors, so you could quickly jump to the fun part.
|
||||
|
||||
Index:
|
||||
|
||||
- [Development](#development)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Making Changes in the Go Code](#making-changes-in-the-go-code)
|
||||
- [Making Changes in the Admin UI](#making-changes-in-the-admin-ui)
|
||||
- [Backend Code](#backend-code)
|
||||
- [Frontend Code](#frontend-code)
|
||||
- [Submitting PR](#submitting-pr)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Getting Help](#getting-help)
|
||||
|
||||
## Prerequisites
|
||||
---
|
||||
|
||||
- Go 1.22+ (for Go code changes)
|
||||
- Node 20+ (for Admin UI changes)
|
||||
## Development
|
||||
|
||||
If you haven't done so already, you can fork the Certimate repository and clone your fork to work locally:
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your_username/certimate.git
|
||||
```
|
||||
- Go 1.24+ (for backend code changes)
|
||||
- Node.js 22.0+ (for frontend code changes)
|
||||
|
||||
> **Important:**
|
||||
> It is recommended to create a new branch from `main` for each bug fix or feature. If you plan to submit multiple PRs, ensure the changes are in separate branches for easier review and eventual merge.
|
||||
> Keep each PR focused on a single feature or fix.
|
||||
### Backend Code
|
||||
|
||||
## Making Changes in the Go Code
|
||||
The backend code of Certimate is developed using Golang. It is a monolithic application based on [Pocketbase](https://github.com/pocketbase/pocketbase).
|
||||
|
||||
Once you have made changes to the Go code in Certimate, follow these steps to run the project:
|
||||
Once you have made changes to the backend code in Certimate, follow these steps to run the project:
|
||||
|
||||
1. Navigate to the root directory.
|
||||
|
||||
2. Start the service by running:
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
go mod vendor
|
||||
```
|
||||
3. Start the local development server:
|
||||
```bash
|
||||
go run main.go serve
|
||||
```
|
||||
|
||||
This will start a web server at `http://localhost:8090` using the prebuilt Admin UI located in `ui/dist`.
|
||||
This will start a web server at `http://localhost:8090` using the prebuilt WebUI located in `/ui/dist`.
|
||||
|
||||
**Before submitting a PR to the main repository, consider:**
|
||||
> If you encounter an error `ui/embed.go: pattern all:dist: no matching files found`, please refer to _[Frontend Code](#frontend-code)_ and build WebUI first.
|
||||
|
||||
- Format your source code by using [gofumpt](https://github.com/mvdan/gofumpt).
|
||||
**Before submitting a PR to the main repository, you should:**
|
||||
|
||||
- Adding unit or integration tests for your changes. Certimate uses Go’s standard `testing` package. You can run tests using the following command (while in the root project directory):
|
||||
- Format your source code by using [gofumpt](https://github.com/mvdan/gofumpt). Recommended using VSCode and installing the gofumpt plugin to automatically format when saving.
|
||||
- Adding unit or integration tests for your changes (with go standard library `testing` package).
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
### Frontend Code
|
||||
|
||||
## Making Changes in the Admin UI
|
||||
The frontend code of Certimate is developed using TypeScript. It is a SPA based on [React](https://github.com/facebook/react) and [Vite](https://github.com/vitejs/vite).
|
||||
|
||||
Certimate’s Admin UI is a single-page application (SPA) built using React and Vite.
|
||||
|
||||
To start the Admin UI:
|
||||
|
||||
1. Navigate to the `ui` project directory.
|
||||
|
||||
2. Install the necessary dependencies by running:
|
||||
Once you have made changes to the backend code in Certimate, follow these steps to run the project:
|
||||
|
||||
1. Navigate to the `/ui` directory.
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Start the Vite development server:
|
||||
|
||||
3. Start the local development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
You can now access the running Admin UI at `http://localhost:5173` in your browser.
|
||||
This will start a web server at `http://localhost:5173`. You can now access the WebUI in your browser.
|
||||
|
||||
Since the Admin UI is a client-side application, you will also need to have the Certimate backend running. You can either manually run Certimate or use a prebuilt executable.
|
||||
|
||||
Any changes you make in the Admin UI will be automatically reflected in the browser without requiring a page reload.
|
||||
|
||||
After completing your changes, build the Admin UI so it can be embedded into the Go package:
|
||||
After completing your changes, build the WebUI so it can be embedded into the Go package:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Once all steps are completed, you are ready to submit a PR to the main Certimate repository.
|
||||
**Before submitting a PR to the main repository, you should:**
|
||||
|
||||
- Format your source code by using [ESLint](https://github.com/eslint/eslint). Recommended using VSCode and installing the ESLint plugin to automatically format when saving.
|
||||
|
||||
## Submitting PR
|
||||
|
||||
Before opening a Pull Request, please open an issue to discuss the change and get feedback from the maintainers. This will helps us:
|
||||
|
||||
- To understand the context of the change.
|
||||
- To ensure it fits into Certimate's roadmap.
|
||||
- To prevent us from duplicating work.
|
||||
- To prevent you from spending time on a change that we may not be able to accept.
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
1. Fork the repository.
|
||||
2. Before you draft a PR, please open an issue to discuss the changes you want to make.
|
||||
3. Create a new branch for your changes.
|
||||
4. Please add tests for your changes accordingly.
|
||||
5. Ensure your code passes the existing tests.
|
||||
6. Please link the issue in the PR description.
|
||||
7. Get merged!
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> It is recommended to create a new branch from `main` for each bug fix or feature. If you plan to submit multiple PRs, ensure the changes are in separate branches for easier review and eventual merge.
|
||||
>
|
||||
> Keep each PR focused on a single feature or fix.
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you ever get stuck or get a burning question while contributing, simply shoot your queries our way via the GitHub issues.
|
||||
|
||||
21
Dockerfile
21
Dockerfile
@@ -1,33 +1,24 @@
|
||||
FROM node:20-alpine3.19 AS front-builder
|
||||
|
||||
FROM node:20-alpine3.19 AS webui-builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app/
|
||||
|
||||
RUN \
|
||||
cd /app/ui && \
|
||||
npm install && \
|
||||
npm run build
|
||||
|
||||
|
||||
FROM golang:1.22-alpine AS builder
|
||||
|
||||
FROM golang:1.24-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY ../. /app/
|
||||
|
||||
RUN rm -rf /app/ui/dist
|
||||
|
||||
COPY --from=front-builder /app/ui/dist /app/ui/dist
|
||||
|
||||
RUN go build -o certimate
|
||||
COPY --from=webui-builder /app/ui/dist /app/ui/dist
|
||||
ENV CGO_ENABLED=0
|
||||
RUN go build -ldflags="-s -w" -o certimate
|
||||
|
||||
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/certimate .
|
||||
|
||||
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]
|
||||
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 certimate-go
|
||||
Copyright (c) 2024 Yoan.Liu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
3
Makefile
3
Makefile
@@ -21,7 +21,8 @@ $(OS_ARCH):
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
GOOS=$(word 1,$(subst /, ,$@)) \
|
||||
GOARCH=$(word 2,$(subst /, ,$@)) \
|
||||
go build -o $(BUILD_DIR)/$(BINARY_NAME)_$(word 1,$(subst /, ,$@))_$(word 2,$(subst /, ,$@)) -ldflags="-X main.version=$(VERSION)" .
|
||||
CGO_ENABLED=0 \
|
||||
go build -o $(BUILD_DIR)/$(BINARY_NAME)_$(word 1,$(subst /, ,$@))_$(word 2,$(subst /, ,$@)) -ldflags="-X main.version=$(VERSION) -s -w" .
|
||||
|
||||
# 清理构建文件
|
||||
clean:
|
||||
|
||||
216
README.md
216
README.md
@@ -1,190 +1,112 @@
|
||||
[中文](README.md) | [English](README_EN.md)
|
||||
<h1 align="center">🔒 Certimate</h1>
|
||||
|
||||
# 🔒Certimate
|
||||
<div align="center">
|
||||
|
||||
做个人产品或在小企业负责运维的同学,需要管理多个域名,要给域名申请证书。但手动申请证书有以下缺点:
|
||||
[](https://github.com/certimate-go/certimate)
|
||||
[](https://github.com/certimate-go/certimate)
|
||||
[](https://hub.docker.com/r/certimate/certimate)
|
||||
[](https://github.com/certimate-go/certimate/releases)
|
||||
[](https://mit-license.org/)
|
||||
|
||||
1. 😱 麻烦:申请、部署证书虽不困难,但也挺麻烦的,尤其是维护多个域名的时候。
|
||||
2. 😭 易忘:当前免费证书有效期仅 90 天,这就要求定期操作,增加工作量的同时,也很容易忘掉,导致网站无法访问。
|
||||
</div>
|
||||
|
||||
Certimate 就是为了解决上述问题而产生的,它具有以下特点:
|
||||
<div align="center">
|
||||
|
||||
1. 操作简单:自动申请、部署、续期 SSL 证书,全程无需人工干预。
|
||||
2. 支持私有部署:部署方法简单,只需下载二进制文件执行即可。二进制文件、Docker 镜像全部用 Github Actions 生成,过程透明,可自行审计。
|
||||
3. 数据安全:由于是私有部署,所有数据均存储在本地,不会保存在服务商的服务器,确保数据的安全性。
|
||||
中文 | [English](README_EN.md)
|
||||
|
||||
相关文章:
|
||||
</div>
|
||||
|
||||
- [⚠️⚠️⚠️V0.2.0-第一个不向后兼容的版本](https://docs.certimate.me/blog/v0.2.0)
|
||||
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
||||
- [域名变量及部署授权组介绍](https://docs.certimate.me/blog/multi-deployer)
|
||||
---
|
||||
|
||||
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。使用文档请访问 [https://docs.certimate.me](https://docs.certimate.me)
|
||||
## 🚩 项目简介
|
||||
|
||||
## 一、安装
|
||||
做个人产品或者在中小企业里负责运维的同学,会遇到要管理多个域名的情况,需要给域名申请证书。但是手动申请证书有以下缺点:
|
||||
|
||||
安装 Certimate 非常简单,你可以选择以下方式之一进行安装:
|
||||
- 😱 麻烦:申请证书并部署到服务的流程虽不复杂,但也挺麻烦的,尤其是你有多个域名需要维护的时候。
|
||||
- 😭 易忘:另外当前免费证书的有效期只有 90 天,这就要求你定期的操作,增加了工作量的同时,你也很容易忘掉续期,从而导致网站访问不了。
|
||||
|
||||
### 1. 二进制文件
|
||||
Certimate 就是为了解决上述问题而产生的,它具有以下优势:
|
||||
|
||||
你可以直接从[Releases 页](https://github.com/usual2970/certimate/releases)下载预先编译好的二进制文件,解压后执行:
|
||||
- **本地部署**:一键安装,只需要下载二进制文件,然后直接运行即可。同时也支持 Docker 部署、源代码部署等方式。
|
||||
- **数据安全**:由于是私有部署,所有数据均存储在自己的服务器上,不会经过第三方,确保数据的隐私和安全。
|
||||
- **操作简单**:简单配置即可轻松申请 SSL 证书并部署到指定的目标上,在证书即将过期前自动续期,从申请证书到使用证书完全自动化,无需人工操作。
|
||||
|
||||
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。
|
||||
|
||||
## 💡 功能特性
|
||||
|
||||
- 灵活的工作流编排方式,证书从申请到部署完全自动化;
|
||||
- 支持单域名、多域名、泛域名证书,可选 RSA、ECC 签名算法;
|
||||
- 支持 PEM、PFX、JKS 等多种格式输出证书;
|
||||
- 支持 30+ 域名托管商(如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers));
|
||||
- 支持 100+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-hosting-providers));
|
||||
- 支持邮件、钉钉、飞书、企业微信、Webhook 等多种通知渠道;
|
||||
- 支持 Let's Encrypt、Buypass、Google Trust Services、SSL.com、ZeroSSL 等多种 ACME 证书颁发机构;
|
||||
- 更多特性等待探索。
|
||||
|
||||
## ⏱️ 快速启动
|
||||
|
||||
**5 分钟部署 Certimate!**
|
||||
|
||||
以二进制部署为例,从 [GitHub Releases](https://github.com/certimate-go/certimate/releases) 页面下载预先编译好的二进制可执行文件压缩包,解压缩后在终端中执行:
|
||||
|
||||
```bash
|
||||
./certimate serve
|
||||
```
|
||||
|
||||
或运行以下命令自动给 Certimate 自身添加证书
|
||||
浏览器中访问 `http://127.0.0.1:8090`。
|
||||
|
||||
```bash
|
||||
./certimate serve 你的域名
|
||||
```
|
||||
初始的管理员账号及密码:
|
||||
|
||||
> [!NOTE]
|
||||
> MacOS 在执行二进制文件时会提示:无法打开“Certimate”,因为 Apple 无法检查其是否包含恶意软件。可在“系统设置 > 隐私与安全性 > 安全性”中点击“仍然允许”,然后再次尝试执行二进制文件。
|
||||
- 账号:`admin@certimate.fun`
|
||||
- 密码:`1234567890`
|
||||
|
||||
### 2. Docker 安装
|
||||
即刻使用 Certimate。
|
||||
|
||||
```bash
|
||||
如何使用 Docker 或其他部署方式请参考文档。
|
||||
|
||||
mkdir -p ~/.certimate && cd ~/.certimate && curl -O https://raw.githubusercontent.com/usual2970/certimate/refs/heads/main/docker/docker-compose.yml && docker compose up -d
|
||||
## 📄 使用手册
|
||||
|
||||
```
|
||||
请访问文档站 [docs.certimate.me](https://docs.certimate.me/) 以阅读使用手册。
|
||||
|
||||
### 3. 源代码安装
|
||||
相关文章:
|
||||
|
||||
```bash
|
||||
git clone EMAIL:usual2970/certimate.git
|
||||
cd certimate
|
||||
make local.run
|
||||
```
|
||||
- [《使用 CNAME 完成 ACME DNS-01 质询》](https://docs.certimate.me/blog/cname)
|
||||
- [《v0.3.0:第二个不向后兼容的大版本》](https://docs.certimate.me/blog/v0.3.0)
|
||||
- [《v0.2.0:第一个不向后兼容的大版本》](https://docs.certimate.me/blog/v0.2.0)
|
||||
- [《Why Certimate?》](https://docs.certimate.me/blog/why-certimate)
|
||||
|
||||
## 二、使用
|
||||
## ⭐ 运行界面
|
||||
|
||||
执行完上述安装操作后,在浏览器中访问 `http://127.0.0.1:8090` 即可访问 Certimate 管理页面。
|
||||
[](https://www.bilibili.com/video/BV1xockeZEm2)
|
||||
|
||||
```bash
|
||||
用户名:admin@certimate.fun
|
||||
密码:1234567890
|
||||
```
|
||||
## 🤝 参与贡献
|
||||
|
||||

|
||||
Certimate 是一个免费且开源的项目。我们欢迎任何人为 Certimate 做出贡献,以帮助改善 Certimate。包括但不限于:提交代码、反馈缺陷、交流想法,或分享你基于 Certimate 的使用案例。同时,我们也欢迎用户在个人博客或社交媒体上分享 Certimate。
|
||||
|
||||
## 三、支持的服务商列表
|
||||
如果你想要贡献代码,请先阅读我们的[贡献指南](./CONTRIBUTING.md)。
|
||||
|
||||
| 服务商 | 支持申请证书 | 支持部署证书 | 备注 |
|
||||
| :--------: | :----------: | :----------: | ----------------------------------------------------------------- |
|
||||
| 阿里云 | √ | √ | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN、SLB |
|
||||
| 腾讯云 | √ | √ | 可签发在腾讯云注册的域名;可部署到腾讯云 COS、CDN、ECDN、CLB、TEO |
|
||||
| 华为云 | √ | √ | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB |
|
||||
| 七牛云 | | √ | 可部署到七牛云 CDN |
|
||||
| AWS | √ | | 可签发在 AWS Route53 托管的域名 |
|
||||
| CloudFlare | √ | | 可签发在 CloudFlare 注册的域名;CloudFlare 服务自带 SSL 证书 |
|
||||
| GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 |
|
||||
| Namesilo | √ | | 可签发在 Namesilo 注册的域名 |
|
||||
| PowerDNS | √ | | 可签发在 PowerDNS 托管的域名 |
|
||||
| HTTP 请求 | √ | | 可签发允许通过 HTTP 请求修改 DNS 的域名 |
|
||||
| 本地部署 | | √ | 可部署到本地服务器 |
|
||||
| SSH | | √ | 可部署到 SSH 服务器 |
|
||||
| Webhook | | √ | 可部署时回调到 Webhook |
|
||||
| Kubernetes | | √ | 可部署到 Kubernetes Secret |
|
||||
请在 https://github.com/certimate-go/certimate 提交 [Issues](https://github.com/certimate-go/certimate/issues) 和 [Pull Requests](https://github.com/certimate-go/certimate/pulls)。
|
||||
|
||||
## 四、系统截图
|
||||
#### 感谢以下贡献者对 Certimate 做出的贡献:
|
||||
|
||||

|
||||
[](https://github.com/certimate-go/certimate/graphs/contributors)
|
||||
|
||||

|
||||
## ⛔ 免责声明
|
||||
|
||||

|
||||
Certimate 遵循 [MIT License](https://opensource.org/licenses/MIT) 开源协议,完全免费提供,旨在“按现状”供用户使用。作者及贡献者不对使用本软件所产生的任何直接或间接后果承担责任,包括但不限于性能下降、数据丢失、服务中断、或任何其他类型的损害。
|
||||
|
||||

|
||||
**无任何保证**:本软件不提供任何明示或暗示的保证,包括但不限于对特定用途的适用性、无侵权性、商用性及可靠性的保证。
|
||||
|
||||

|
||||
**用户责任**:使用本软件即表示您理解并同意承担由此产生的一切风险及责任。
|
||||
|
||||
## 五、概念
|
||||
## 🌐 加入社群
|
||||
|
||||
Certimate 的工作流程如下:
|
||||
|
||||
- 用户通过 Certimate 管理页面填写申请证书的信息,包括域名、DNS 服务商的授权信息、以及要部署到的服务商的授权信息。
|
||||
- Certimate 向证书厂商的 API 发起申请请求,获取 SSL 证书。
|
||||
- Certimate 存储证书信息,包括证书内容、私钥、证书有效期等,并在证书即将过期时自动续期。
|
||||
- Certimate 向服务商的 API 发起部署请求,将证书部署到服务商的服务器上。
|
||||
|
||||
这就涉及域名、DNS 服务商的授权信息、部署服务商的授权信息等。
|
||||
|
||||
### 1. 域名
|
||||
|
||||
就是要申请证书的域名。
|
||||
|
||||
### 2. DNS 服务商授权信息
|
||||
|
||||
给域名申请证书需要证明域名是你的,所以我们手动申请证书的时候一般需要在域名服务商的控制台解析记录中添加一个 TXT 域名解析记录。
|
||||
|
||||
Certimate 会自动添加一个 TXT 域名解析记录,你只需要在 Certimate 后台中填写你的域名服务商的授权信息即可。
|
||||
|
||||
比如你在阿里云购买的域名,授权信息如下:
|
||||
|
||||
```bash
|
||||
accessKeyId: your-access-key-id
|
||||
accessKeySecret: your-access-key-secret
|
||||
```
|
||||
|
||||
在腾讯云购买的域名,授权信息如下:
|
||||
|
||||
```bash
|
||||
secretId: your-secret-id
|
||||
secretKey: your-secret-key
|
||||
```
|
||||
|
||||
注意,此授权信息需具有访问域名及 DNS 解析的管理权限,具体的权限清单请参阅各服务商自己的技术文档。
|
||||
|
||||
### 3. 部署服务商授权信息
|
||||
|
||||
Certimate 申请证书后,会自动将证书部署到你指定的目标上,比如阿里云 CDN,Certimate 会根据你填写的授权信息及域名找到对应的 CDN 服务,并将证书部署到对应的 CDN 服务上。
|
||||
|
||||
部署服务商授权信息和 DNS 服务商授权信息基本一致,区别在于 DNS 服务商授权信息用于证明域名是你的,部署服务商授权信息用于提供证书部署的授权信息。
|
||||
|
||||
注意,此授权信息需具有访问部署目标服务的相关管理权限,具体的权限清单请参阅各服务商自己的技术文档。
|
||||
|
||||
## 六、常见问题
|
||||
|
||||
Q: 提供 SaaS 服务吗?
|
||||
|
||||
> A: 不提供,目前仅支持 self-hosted(私有部署)。
|
||||
|
||||
Q: 数据安全?
|
||||
|
||||
> A: 由于仅支持私有部署,各种数据都保存在用户的服务器上。另外 Certimate 源码也开源,二进制包及 Docker 镜像打包过程全部使用 Github Actions 进行,过程透明可见,可自行审计。
|
||||
|
||||
Q: 自动续期证书?
|
||||
|
||||
> A: 已经申请的证书会在**过期前 10 天**自动续期。每天会检查一次证书是否快要过期,快要过期时会自动重新申请证书并部署到目标服务上。
|
||||
|
||||
## 七、贡献
|
||||
|
||||
Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.md)。你可以使用它做任何你想做的事,甚至把它当作一个付费服务提供给用户。
|
||||
|
||||
你可以通过以下方式来支持 Certimate 的开发:
|
||||
|
||||
- 提交代码:如果你发现了 Bug 或有新的功能需求,而你又有相关经验,可以[提交代码](CONTRIBUTING.md)给我们。
|
||||
- 提交 Issue:功能建议或者 Bug 可以[提交 Issue](https://github.com/usual2970/certimate/issues) 给我们。
|
||||
|
||||
支持更多服务商、UI 的优化改进、Bug 修复、文档完善等,欢迎大家提交 PR。
|
||||
|
||||
## 八、免责声明
|
||||
|
||||
本软件依据 MIT 许可证(MIT License)发布,免费提供,旨在“按现状”供用户使用。作者及贡献者不对使用本软件所产生的任何直接或间接后果承担责任,包括但不限于性能下降、数据丢失、服务中断、或任何其他类型的损害。
|
||||
|
||||
无任何保证:本软件不提供任何明示或暗示的保证,包括但不限于对特定用途的适用性、无侵权性、商用性及可靠性的保证。
|
||||
|
||||
用户责任:使用本软件即表示您理解并同意承担由此产生的一切风险及责任。
|
||||
|
||||
## 九、加入社区
|
||||
|
||||
- [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
|
||||
- [Telegram](https://t.me/+ZXphsppxUg41YmVl)
|
||||
- 微信群聊(超 200 人需邀请入群,可先加作者好友)
|
||||
|
||||
<img src="https://i.imgur.com/8xwsLTA.png" width="400"/>
|
||||
<img src="https://i.imgur.com/8xwsLTA.png" width="200"/>
|
||||
|
||||
## 十、Star 趋势图
|
||||
## 🚀 Star 趋势图
|
||||
|
||||
[](https://starchart.cc/usual2970/certimate)
|
||||
[](https://starchart.cc/certimate-go/certimate)
|
||||
|
||||
215
README_EN.md
215
README_EN.md
@@ -1,189 +1,110 @@
|
||||
[中文](README.md) | [English](README_EN.md)
|
||||
<h1 align="center">🔒 Certimate</h1>
|
||||
|
||||
# 🔒Certimate
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/certimate-go/certimate)
|
||||
[](https://github.com/certimate-go/certimate)
|
||||
[](https://hub.docker.com/r/certimate/certimate)
|
||||
[](https://github.com/certimate-go/certimate/releases)
|
||||
[](https://mit-license.org/)
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[中文](README.md) | English
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 🚩 Introduction
|
||||
|
||||
For individuals managing personal projects or those responsible for IT operations in small businesses who need to manage multiple domain names, applying for certificates manually comes with several drawbacks:
|
||||
|
||||
1. 😱Troublesome: Applying for and deploying certificates isn’t difficult, but it can be quite a hassle, especially when managing multiple domains.
|
||||
2. 😭Easily forgotten: The current free certificate has a validity period of only 90 days, requiring regular renewal operations. This increases the workload and makes it easy to forget, which can result in the website becoming inaccessible.
|
||||
- 😱 Troublesome: Applying for and deploying certificates isn’t difficult, but it can be quite a hassle, especially when managing multiple domains.
|
||||
- 😭 Easily forgotten: The current free certificate has a validity period of only 90 days, requiring regular renewal operations. This increases the workload and makes it easy to forget, which can result in the website becoming inaccessible.
|
||||
|
||||
Certimate was created to solve the above-mentioned issues and has the following features:
|
||||
Certimate was created to solve the above-mentioned issues and has the following advantages:
|
||||
|
||||
1. Simple operation: Automatically apply, deploy, and renew SSL certificates without any manual intervention.
|
||||
2. Support for self-hosted deployment: The deployment method is simple; you only need to download the binary file and execute it. Both the binary files and Docker images are generated using GitHub Actions, ensuring a transparent process that can be audited independently.
|
||||
3. Data security: Since it is a self-hosted deployment, all data is stored locally and will not be saved on the service provider’s servers, ensuring the security of the data.
|
||||
- **Local Deployment**: Simply to install, download the binary and run it directly. Supports Docker deployment and source code deployment for added flexibility.
|
||||
- **Data Security**: With private deployment, all data is stored on your own servers, ensuring it never resides on third-party systems and maintaining full control over your data.
|
||||
- **Easy Operation**: Effortlessly apply and deploy SSL certificates with minimal configuration. The system automatically renews certificates before expiration, providing a fully automated workflow, no manual intervention required.
|
||||
|
||||
Related articles:
|
||||
Certimate aims to provide users with a secure and user-friendly SSL certificate management solution.
|
||||
|
||||
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
||||
- [Introduction to Domain Variables and Deployment Authorization Groups](https://docs.certimate.me/blog/multi-deployer)
|
||||
## 💡 Features
|
||||
|
||||
Certimate aims to provide users with a secure and user-friendly SSL certificate management solution. For usage documentation, please visit [https://docs.certimate.me](https://docs.certimate.me).
|
||||
- Flexible workflow orchestration, fully automation from certificate application to deployment;
|
||||
- Supports single-domain, multi-domain, wildcard certificates, with options for RSA or ECC.
|
||||
- Supports various certificate formats such as PEM, PFX, JKS.
|
||||
- Supports more than 30+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-dns-providers));
|
||||
- Supports more than 100+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-hosting-providers));
|
||||
- Supports multiple notification channels including email, DingTalk, Feishu, WeCom, Webhook, and more;
|
||||
- Supports multiple ACME CAs including Let's Encrypt, Buypass, Google Trust Services,SSL.com, ZeroSSL, and more;
|
||||
- More features waiting to be discovered.
|
||||
|
||||
## Installation
|
||||
## ⏱️ Fast Track
|
||||
|
||||
Installing Certimate is very simple, you can choose one of the following methods for installation:
|
||||
**Deploy Certimate in 5 minutes!**
|
||||
|
||||
### 1. Binary File
|
||||
|
||||
You can download the precompiled binary files directly from the [Releases page](https://github.com/usual2970/certimate/releases), and after extracting them, execute:
|
||||
Download the archived package of precompiled binary files directly from [GitHub Releases](https://github.com/certimate-go/certimate/releases), extract and then execute:
|
||||
|
||||
```bash
|
||||
./certimate serve
|
||||
```
|
||||
|
||||
Or run the following command to automatically add a certificate to Certimate itself.
|
||||
Visit `http://127.0.0.1:8090` in your browser.
|
||||
|
||||
```bash
|
||||
./certimate serve yourDomain
|
||||
```
|
||||
Default administrator account:
|
||||
|
||||
> [!NOTE]
|
||||
> When executing the binary file on macOS, you may see a prompt saying: “Cannot open ‘certimate’ because Apple cannot check it for malicious software.” You can go to System Preferences > Security & Privacy > General, then click “Allow Anyway,” and try executing the binary file again.
|
||||
- Username: `admin@certimate.fun`
|
||||
- Password: `1234567890`
|
||||
|
||||
### 2. Docker Installation
|
||||
Work with Certimate right now. Or read other content in the documentation to learn more.
|
||||
|
||||
```bash
|
||||
## 📄 Documentation
|
||||
|
||||
mkdir -p ~/.certimate && cd ~/.certimate && curl -O https://raw.githubusercontent.com/usual2970/certimate/refs/heads/main/docker/docker-compose.yml && docker compose up -d
|
||||
For full documentation, please visit [docs.certimate.me](https://docs.certimate.me/en/).
|
||||
|
||||
```
|
||||
Related articles:
|
||||
|
||||
### 3. Source Code Installation
|
||||
- [_使用 CNAME 完成 ACME DNS-01 质询_](https://docs.certimate.me/blog/cname)
|
||||
- [_v0.3.0:第二个不向后兼容的大版本_](https://docs.certimate.me/blog/v0.3.0)
|
||||
- [_v0.2.0:第一个不向后兼容的大版本_](https://docs.certimate.me/blog/v0.2.0)
|
||||
- [_Why Certimate?_](https://docs.certimate.me/blog/why-certimate)
|
||||
|
||||
```bash
|
||||
git clone EMAIL:usual2970/certimate.git
|
||||
cd certimate
|
||||
make local.run
|
||||
```
|
||||
## ⭐ Screenshot
|
||||
|
||||
## Usage
|
||||
[](https://www.youtube.com/watch?v=am_yzdfyNOE)
|
||||
|
||||
After completing the installation steps above, you can access the Certimate management page by visiting <http://127.0.0.1:8090> in your browser.
|
||||
## 🤝 Contributing
|
||||
|
||||
```bash
|
||||
username:admin@certimate.fun
|
||||
password:1234567890
|
||||
```
|
||||
Certimate is a free and open-source project, and your feedback and contributions are needed and always welcome. Contributions include but are not limited to: submitting code, reporting bugs, sharing ideas, or showcasing your use cases based on Certimate. We also encourage users to share Certimate on personal blogs or social media.
|
||||
|
||||

|
||||
For those who'd like to contribute code, see our [Contribution Guide](./CONTRIBUTING_EN.md).
|
||||
|
||||
## List of Supported Providers
|
||||
[Issues](https://github.com/certimate-go/certimate/issues) and [Pull Requests](https://github.com/certimate-go/certimate/pulls) are opened at https://github.com/certimate-go/certimate.
|
||||
|
||||
| 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 |
|
||||
| Huawei Cloud | √ | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB |
|
||||
| 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 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 |
|
||||
#### Contributors
|
||||
|
||||
## Screenshots
|
||||
[](https://github.com/certimate-go/certimate/graphs/contributors)
|
||||
|
||||

|
||||
## ⛔ Disclaimer
|
||||
|
||||

|
||||
This repository is available under the [MIT License](https://opensource.org/licenses/MIT), 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 Responsibilities**: By using this software, you agree to take full responsibility for any outcomes resulting from its use.
|
||||
|
||||

|
||||
## 🌐 Join the Community
|
||||
|
||||
## Concepts
|
||||
|
||||
The workflow of Certimate is as follows:
|
||||
|
||||
- Users fill in the certificate application information on the Certimate management page, including domain name, authorization information for the DNS provider, and authorization information for the service provider to deploy to.
|
||||
- Certimate sends a request to the certificate vendor's API to apply for an SSL certificate.
|
||||
- Certimate stores the certificate information, including the certificate content, private key, validity period, etc., and automatically renews the certificate when it is about to expire.
|
||||
- Certimate sends a deployment request to the service provider's API to deploy the certificate to the service provider's servers.
|
||||
|
||||
This involves authorization information for the domain, DNS provider, and deployment service provider.
|
||||
|
||||
### 1. Domain
|
||||
|
||||
It involves the domain name for which the certificate is being requested.
|
||||
|
||||
### 2. Authorization Information for the DNS Provider
|
||||
|
||||
To apply for a certificate for a domain, you need to prove that the domain belongs to you. Therefore, when manually applying for a certificate, you typically need to add a TXT record to the DNS records in the domain provider's control panel.
|
||||
|
||||
Certimate will automatically add a TXT record for you; you only need to fill in the authorization information for your DNS provider in the Certimate backend.
|
||||
|
||||
For example, if you purchased the domain from Alibaba Cloud, the authorization information would be as follows:
|
||||
|
||||
```bash
|
||||
accessKeyId: your-access-key-id
|
||||
accessKeySecret: your-access-key-secret
|
||||
```
|
||||
|
||||
If you purchased the domain from Tencent Cloud, the authorization information would be as follows:
|
||||
|
||||
```bash
|
||||
secretId: your-secret-id
|
||||
secretKey: your-secret-key
|
||||
```
|
||||
|
||||
Notes: This authorization information requires relevant administration permissions for accessing the DNS services. Please refer to the documentations of each service provider for the specific permissions list.
|
||||
|
||||
### 3. Authorization Information for the Deployment Service Provider
|
||||
|
||||
After Certimate applies for the certificate, it will automatically deploy the certificate to your specified target, such as Alibaba Cloud CDN. At this point, you need to fill in the authorization information for Alibaba Cloud. Certimate will use the authorization information and domain name you provided to locate the corresponding CDN service and deploy the certificate to that service.
|
||||
|
||||
The authorization information for the deployment service provider is the same as that for the DNS provider, with the distinction that the DNS provider's authorization information is used to prove that the domain belongs to you, while the deployment service provider's authorization information is used to provide authorization for the certificate deployment.
|
||||
|
||||
Notes: This authorization information requires relevant administration permissions to access the target deployment services. Please refer to the documentations of each service provider for the specific permissions list.
|
||||
|
||||
## FAQ
|
||||
|
||||
Q: Do you provide SaaS services?
|
||||
|
||||
> A: No, we do not provide that. Currently, we only support self-hosted.
|
||||
|
||||
Q: Data Security?
|
||||
|
||||
> A: Since only self-hosted is supported, all data is stored on the user’s server. Additionally, the source code of Certimate is open-source, and the packaging process for binary files and Docker images is entirely done using GitHub Actions. This process is transparent and visible, allowing for independent auditing.
|
||||
|
||||
Q: Automatic Certificate Renewal?
|
||||
|
||||
> A: Certificates that have already been issued will be automatically renewed **10 days before expiration**. The system checks once a day to see if any certificates are nearing expiration, and if so, it will automatically reapply for the certificate and deploy it to the target service.
|
||||
|
||||
## Contributing
|
||||
|
||||
Certimate is a free and open-source project, licensed under the [MIT License](LICENSE.md). You can use it for anything you want, even offering it as a paid service to users.
|
||||
|
||||
You can support the development of Certimate in the following ways:
|
||||
|
||||
- **Submit Code**: If you find a bug or have new feature requests, and you have relevant experience, [you can submit code to us](CONTRIBUTING_EN.md).
|
||||
- **Submit an Issue**: For feature suggestions or bugs, you can [submit an issue](https://github.com/usual2970/certimate/issues) to us.
|
||||
|
||||
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)
|
||||
- [Telegram](https://t.me/+ZXphsppxUg41YmVl)
|
||||
- Wechat Group
|
||||
|
||||
<img src="https://i.imgur.com/zSHEoIm.png" width="400"/>
|
||||
<img src="https://i.imgur.com/zSHEoIm.png" width="200"/>
|
||||
|
||||
## Star History
|
||||
## 🚀 Star History
|
||||
|
||||
[](https://starchart.cc/usual2970/certimate)
|
||||
[](https://starchart.cc/certimate-go/certimate)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
version: "3.0"
|
||||
services:
|
||||
certimate:
|
||||
image: registry.cn-shanghai.aliyuncs.com/usual2970/certimate:latest
|
||||
container_name: certimate_server
|
||||
image: certimate/certimate:latest
|
||||
container_name: certimate
|
||||
ports:
|
||||
- 8090:8090
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- ./data:/app/pb_data
|
||||
restart: unless-stopped
|
||||
|
||||
299
go.mod
299
go.mod
@@ -1,181 +1,226 @@
|
||||
module github.com/usual2970/certimate
|
||||
module github.com/certimate-go/certimate
|
||||
|
||||
go 1.22.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.23.2
|
||||
toolchain go1.24.3
|
||||
|
||||
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.10
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.3.1
|
||||
github.com/Edgio/edgio-api v0.0.0-workspace
|
||||
github.com/G-Core/gcorelabscdn-go v1.0.31
|
||||
github.com/alibabacloud-go/alb-20200616/v2 v2.2.8
|
||||
github.com/alibabacloud-go/apig-20240327/v3 v3.2.2
|
||||
github.com/alibabacloud-go/cas-20200407/v3 v3.0.4
|
||||
github.com/alibabacloud-go/cdn-20180510/v5 v5.2.2
|
||||
github.com/alibabacloud-go/cloudapi-20160714/v5 v5.7.4
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.7
|
||||
github.com/alibabacloud-go/ddoscoo-20200101/v4 v4.0.0
|
||||
github.com/alibabacloud-go/esa-20240910/v2 v2.33.0
|
||||
github.com/alibabacloud-go/fc-20230330/v4 v4.3.5
|
||||
github.com/alibabacloud-go/fc-open-20210406/v2 v2.0.12
|
||||
github.com/alibabacloud-go/ga-20191120/v3 v3.1.8
|
||||
github.com/alibabacloud-go/live-20161101 v1.1.1
|
||||
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/alibabacloud-go/slb-20140515/v4 v4.0.10
|
||||
github.com/alibabacloud-go/tea v1.3.9
|
||||
github.com/alibabacloud-go/vod-20170321/v4 v4.8.4
|
||||
github.com/alibabacloud-go/waf-openapi-20211001/v5 v5.1.3
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/go-acme/lego/v4 v4.19.2
|
||||
github.com/gojek/heimdall/v7 v7.0.3
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114
|
||||
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
|
||||
github.com/nikoksr/notify v1.0.0
|
||||
github.com/aws/aws-sdk-go-v2/service/acm v1.32.0
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.46.1
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.42.0
|
||||
github.com/baidubce/bce-sdk-go v0.9.228
|
||||
github.com/blinkbean/dingtalk v1.1.3
|
||||
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.46
|
||||
github.com/go-acme/lego/v4 v4.23.1
|
||||
github.com/go-lark/lark v1.16.0
|
||||
github.com/go-resty/resty/v2 v2.16.5
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.150
|
||||
github.com/jdcloud-api/jdcloud-sdk-go v1.64.0
|
||||
github.com/libdns/dynv6 v1.0.0
|
||||
github.com/libdns/libdns v0.2.3
|
||||
github.com/luthermonson/go-proxmox v0.2.2
|
||||
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.1030
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030
|
||||
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
|
||||
github.com/pkg/sftp v1.13.9
|
||||
github.com/pocketbase/dbx v1.11.0
|
||||
github.com/pocketbase/pocketbase v0.28.2
|
||||
github.com/povsister/scp v0.0.0-20250504051308-e467f71ea63c
|
||||
github.com/qiniu/go-sdk/v7 v7.25.3
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1155
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1166
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1173
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap v1.0.1163
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.0.1150
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.0.1172
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.1169
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1166
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod v1.0.1164
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf v1.0.1170
|
||||
github.com/ucloud/ucloud-sdk-go v0.22.41
|
||||
github.com/volcengine/ve-tos-golang-sdk/v2 v2.7.12
|
||||
github.com/volcengine/volc-sdk-golang v1.0.208
|
||||
github.com/volcengine/volcengine-go-sdk v1.1.8
|
||||
gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1
|
||||
gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
|
||||
k8s.io/api v0.33.1
|
||||
k8s.io/apimachinery v0.33.1
|
||||
k8s.io/client-go v0.33.1
|
||||
software.sslmate.com/src/go-pkcs12 v0.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-fc-util v0.0.7 // indirect
|
||||
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect
|
||||
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-sdk v1.1.5 // 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/blinkbean/dingtalk v1.1.3 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/avast/retry-go v3.0.0+incompatible // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0 // indirect
|
||||
github.com/buger/goterm v1.0.4 // indirect
|
||||
github.com/diskfs/go-diskfs v1.5.0 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-lark/lark v1.14.1 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.16.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/imdario/mergo v0.3.6 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/google/gnostic-models v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/jinzhu/copier v0.3.4 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||
github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect
|
||||
github.com/nrdcg/desec v0.10.0 // indirect
|
||||
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||
github.com/peterhellberg/link v1.2.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/qiniu/dyn v1.3.0 // indirect
|
||||
github.com/qiniu/x v1.10.5 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.mongodb.org/mongo-driver v1.12.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.2 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ns1/ns1-go.v2 v2.13.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
|
||||
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // 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/dcdn-20180115/v3 v3.5.0
|
||||
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/endpoint-util v1.1.1 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.1 // 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.10 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.100 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.6 // 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/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/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 v1.36.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.9
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.62
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect
|
||||
github.com/aws/smithy-go v1.22.2 // 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/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.115.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.6 // indirect
|
||||
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/ganigeorgiev/fexpr v0.5.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // 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/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/miekg/dns v1.1.64 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.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/spf13/cast v1.8.0 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
gocloud.dev v0.37.0 // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/term v0.25.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.25.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||
google.golang.org/api v0.202.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
|
||||
google.golang.org/grpc v1.67.1 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
golang.org/x/image v0.27.0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/time v0.11.0
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // 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
|
||||
modernc.org/libc v1.55.3 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/sqlite v1.31.1 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
modernc.org/libc v1.65.7 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.37.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/Edgio/edgio-api v0.0.0-workspace => ./pkg/sdk3rd/edgio/edgio-api@v0.0.0-workspace
|
||||
|
||||
replace gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 => ./pkg/sdk3rd/cmcc/ecloudsdkcore@v1.0.0
|
||||
|
||||
replace gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1 => ./pkg/sdk3rd/cmcc/ecloudsdkclouddns@v1.0.1
|
||||
|
||||
32
internal/app/app.go
Normal file
32
internal/app/app.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
var instance core.App
|
||||
|
||||
var intanceOnce sync.Once
|
||||
|
||||
func GetApp() core.App {
|
||||
intanceOnce.Do(func() {
|
||||
instance = pocketbase.NewWithConfig(pocketbase.Config{
|
||||
HideStartBanner: true,
|
||||
})
|
||||
})
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
func GetDB() dbx.Builder {
|
||||
return GetApp().DB()
|
||||
}
|
||||
|
||||
func GetLogger() *slog.Logger {
|
||||
return GetApp().Logger()
|
||||
}
|
||||
@@ -2,17 +2,25 @@ package app
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/cron"
|
||||
)
|
||||
|
||||
var schedulerOnce sync.Once
|
||||
|
||||
var scheduler *cron.Cron
|
||||
|
||||
var schedulerOnce sync.Once
|
||||
|
||||
func GetScheduler() *cron.Cron {
|
||||
scheduler = GetApp().Cron()
|
||||
schedulerOnce.Do(func() {
|
||||
scheduler = cron.New()
|
||||
location, err := time.LoadLocation("Local")
|
||||
if err == nil {
|
||||
scheduler.Stop()
|
||||
scheduler.SetTimezone(location)
|
||||
scheduler.Start()
|
||||
}
|
||||
})
|
||||
|
||||
return scheduler
|
||||
31
internal/applicant/acme_ca.go
Normal file
31
internal/applicant/acme_ca.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package applicant
|
||||
|
||||
import "github.com/certimate-go/certimate/internal/domain"
|
||||
|
||||
const (
|
||||
caLetsEncrypt = string(domain.CAProviderTypeLetsEncrypt)
|
||||
caLetsEncryptStaging = string(domain.CAProviderTypeLetsEncryptStaging)
|
||||
caBuypass = string(domain.CAProviderTypeBuypass)
|
||||
caGoogleTrustServices = string(domain.CAProviderTypeGoogleTrustServices)
|
||||
caSSLCom = string(domain.CAProviderTypeSSLCom)
|
||||
caZeroSSL = string(domain.CAProviderTypeZeroSSL)
|
||||
caCustom = string(domain.CAProviderTypeACMECA)
|
||||
|
||||
caDefault = caLetsEncrypt
|
||||
)
|
||||
|
||||
var caDirUrls = map[string]string{
|
||||
caLetsEncrypt: "https://acme-v02.api.letsencrypt.org/directory",
|
||||
caLetsEncryptStaging: "https://acme-staging-v02.api.letsencrypt.org/directory",
|
||||
caBuypass: "https://api.buypass.com/acme/directory",
|
||||
caGoogleTrustServices: "https://dv.acme-v02.api.pki.goog/directory",
|
||||
caSSLCom: "https://acme.ssl.com/sslcom-dv-rsa",
|
||||
caSSLCom + "RSA": "https://acme.ssl.com/sslcom-dv-rsa",
|
||||
caSSLCom + "ECC": "https://acme.ssl.com/sslcom-dv-ecc",
|
||||
caZeroSSL: "https://acme.zerossl.com/v2/DV90",
|
||||
}
|
||||
|
||||
type acmeSSLProviderConfig struct {
|
||||
Config map[domain.CAProviderType]map[string]any `json:"config"`
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
206
internal/applicant/acme_user.go
Normal file
206
internal/applicant/acme_user.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/certimate-go/certimate/internal/domain"
|
||||
"github.com/certimate-go/certimate/internal/repository"
|
||||
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
|
||||
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
|
||||
)
|
||||
|
||||
type acmeUser struct {
|
||||
// 证书颁发机构标识。
|
||||
// 通常等同于 [CAProviderType] 的值。
|
||||
// 对于自定义 ACME CA,值为 "custom#{access_id}"。
|
||||
CA string
|
||||
// 邮箱。
|
||||
Email string
|
||||
// 注册信息。
|
||||
Registration *registration.Resource
|
||||
|
||||
// CSR 私钥。
|
||||
privkey string
|
||||
}
|
||||
|
||||
func newAcmeUser(ca, caAccessId, email string) (*acmeUser, error) {
|
||||
repo := repository.NewAcmeAccountRepository()
|
||||
|
||||
applyUser := &acmeUser{
|
||||
CA: ca,
|
||||
Email: email,
|
||||
}
|
||||
if ca == caCustom {
|
||||
applyUser.CA = fmt.Sprintf("%s#%s", ca, caAccessId)
|
||||
}
|
||||
|
||||
acmeAccount, err := repo.GetByCAAndEmail(applyUser.CA, applyUser.Email)
|
||||
if err != nil {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyPEM, err := xcert.ConvertECPrivateKeyToPEM(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
applyUser.privkey = keyPEM
|
||||
return applyUser, nil
|
||||
}
|
||||
|
||||
applyUser.Registration = acmeAccount.Resource
|
||||
applyUser.privkey = acmeAccount.Key
|
||||
|
||||
return applyUser, nil
|
||||
}
|
||||
|
||||
func (u *acmeUser) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
func (u acmeUser) GetRegistration() *registration.Resource {
|
||||
return u.Registration
|
||||
}
|
||||
|
||||
func (u *acmeUser) GetPrivateKey() crypto.PrivateKey {
|
||||
rs, _ := xcert.ParseECPrivateKeyFromPEM(u.privkey)
|
||||
return rs
|
||||
}
|
||||
|
||||
func (u *acmeUser) hasRegistration() bool {
|
||||
return u.Registration != nil
|
||||
}
|
||||
|
||||
func (u *acmeUser) getCAProvider() string {
|
||||
return strings.Split(u.CA, "#")[0]
|
||||
}
|
||||
|
||||
func (u *acmeUser) getPrivateKeyPEM() string {
|
||||
return u.privkey
|
||||
}
|
||||
|
||||
var registerGroup singleflight.Group
|
||||
|
||||
func registerAcmeUserWithSingleFlight(client *lego.Client, user *acmeUser, userRegisterOptions map[string]any) (*registration.Resource, error) {
|
||||
resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", user.CA, user.Email), func() (interface{}, error) {
|
||||
return registerAcmeUser(client, user, userRegisterOptions)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.(*registration.Resource), nil
|
||||
}
|
||||
|
||||
func registerAcmeUser(client *lego.Client, user *acmeUser, userRegisterOptions map[string]any) (*registration.Resource, error) {
|
||||
var reg *registration.Resource
|
||||
var err error
|
||||
switch user.getCAProvider() {
|
||||
case caLetsEncrypt, caLetsEncryptStaging:
|
||||
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
|
||||
case caBuypass:
|
||||
{
|
||||
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
}
|
||||
|
||||
case caGoogleTrustServices:
|
||||
{
|
||||
access := domain.AccessConfigForGoogleTrustServices{}
|
||||
if err := xmaps.Populate(userRegisterOptions, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: access.EabKid,
|
||||
HmacEncoded: access.EabHmacKey,
|
||||
})
|
||||
}
|
||||
|
||||
case caSSLCom:
|
||||
{
|
||||
access := domain.AccessConfigForSSLCom{}
|
||||
if err := xmaps.Populate(userRegisterOptions, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: access.EabKid,
|
||||
HmacEncoded: access.EabHmacKey,
|
||||
})
|
||||
}
|
||||
|
||||
case caZeroSSL:
|
||||
{
|
||||
access := domain.AccessConfigForZeroSSL{}
|
||||
if err := xmaps.Populate(userRegisterOptions, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: access.EabKid,
|
||||
HmacEncoded: access.EabHmacKey,
|
||||
})
|
||||
}
|
||||
|
||||
case caCustom:
|
||||
{
|
||||
access := domain.AccessConfigForACMECA{}
|
||||
if err := xmaps.Populate(userRegisterOptions, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
if access.EabKid == "" && access.EabHmacKey == "" {
|
||||
reg, err = client.Registration.Register(registration.RegisterOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
})
|
||||
} else {
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: access.EabKid,
|
||||
HmacEncoded: access.EabHmacKey,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("unsupported ca provider '%s'", user.CA)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repo := repository.NewAcmeAccountRepository()
|
||||
resp, err := repo.GetByCAAndEmail(user.CA, user.Email)
|
||||
if err == nil {
|
||||
user.privkey = resp.Key
|
||||
return resp.Resource, nil
|
||||
}
|
||||
|
||||
if _, err := repo.Save(context.Background(), &domain.AcmeAccount{
|
||||
CA: user.CA,
|
||||
Email: user.Email,
|
||||
Key: user.getPrivateKeyPEM(),
|
||||
Resource: reg,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to save acme account registration: %w", err)
|
||||
}
|
||||
|
||||
return reg, nil
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type aliyun struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewAliyun(option *ApplyOption) Applicant {
|
||||
return &aliyun{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *aliyun) Apply() (*Certificate, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("ALICLOUD_ACCESS_KEY", access.AccessKeyId)
|
||||
os.Setenv("ALICLOUD_SECRET_KEY", access.AccessKeySecret)
|
||||
os.Setenv("ALICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
dnsProvider, err := alidns.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(a.option, dnsProvider)
|
||||
}
|
||||
@@ -1,368 +1,276 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"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"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/certimate-go/certimate/internal/domain"
|
||||
"github.com/certimate-go/certimate/internal/repository"
|
||||
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
|
||||
xslices "github.com/certimate-go/certimate/pkg/utils/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
configTypeAliyun = "aliyun"
|
||||
configTypeTencent = "tencent"
|
||||
configTypeHuaweiCloud = "huaweicloud"
|
||||
configTypeAws = "aws"
|
||||
configTypeCloudflare = "cloudflare"
|
||||
configTypeNamesilo = "namesilo"
|
||||
configTypeGodaddy = "godaddy"
|
||||
configTypePdns = "pdns"
|
||||
configTypeHttpreq = "httpreq"
|
||||
)
|
||||
|
||||
const defaultSSLProvider = "letsencrypt"
|
||||
const (
|
||||
sslProviderLetsencrypt = "letsencrypt"
|
||||
sslProviderZeroSSL = "zerossl"
|
||||
sslProviderGts = "gts"
|
||||
)
|
||||
|
||||
const (
|
||||
zerosslUrl = "https://acme.zerossl.com/v2/DV90"
|
||||
letsencryptUrl = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
gtsUrl = "https://dv.acme-v02.api.pki.goog/directory"
|
||||
)
|
||||
|
||||
var sslProviderUrls = map[string]string{
|
||||
sslProviderLetsencrypt: letsencryptUrl,
|
||||
sslProviderZeroSSL: zerosslUrl,
|
||||
sslProviderGts: gtsUrl,
|
||||
}
|
||||
|
||||
const defaultEmail = "536464346@qq.com"
|
||||
|
||||
const defaultTimeout = 60
|
||||
|
||||
type Certificate struct {
|
||||
CertUrl string `json:"certUrl"`
|
||||
CertStableUrl string `json:"certStableUrl"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Certificate string `json:"certificate"`
|
||||
IssuerCertificate string `json:"issuerCertificate"`
|
||||
Csr string `json:"csr"`
|
||||
}
|
||||
|
||||
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"`
|
||||
DisableFollowCNAME bool `json:"disableFollowCNAME"`
|
||||
}
|
||||
|
||||
type ApplyUser struct {
|
||||
Ca string
|
||||
Email string
|
||||
Registration *registration.Resource
|
||||
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 {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
func (u ApplyUser) GetRegistration() *registration.Resource {
|
||||
return u.Registration
|
||||
}
|
||||
|
||||
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
|
||||
type ApplyResult struct {
|
||||
CSR string
|
||||
FullChainCertificate string
|
||||
IssuerCertificate string
|
||||
PrivateKey string
|
||||
ACMEAccountUrl string
|
||||
ACMECertUrl string
|
||||
ACMECertStableUrl string
|
||||
ARIReplaced bool
|
||||
}
|
||||
|
||||
type Applicant interface {
|
||||
Apply() (*Certificate, error)
|
||||
Apply(ctx context.Context) (*ApplyResult, error)
|
||||
}
|
||||
|
||||
func Get(record *models.Record) (Applicant, error) {
|
||||
if record.GetString("applyConfig") == "" {
|
||||
return nil, errors.New("applyConfig is empty")
|
||||
}
|
||||
|
||||
applyConfig := &domain.ApplyConfig{}
|
||||
record.UnmarshalJSONField("applyConfig", applyConfig)
|
||||
|
||||
access, err := app.GetApp().Dao().FindRecordById("access", applyConfig.Access)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("access record not found: %w", err)
|
||||
}
|
||||
|
||||
if applyConfig.Email == "" {
|
||||
applyConfig.Email = defaultEmail
|
||||
}
|
||||
|
||||
if applyConfig.Timeout == 0 {
|
||||
applyConfig.Timeout = defaultTimeout
|
||||
}
|
||||
|
||||
option := &ApplyOption{
|
||||
Email: applyConfig.Email,
|
||||
Domain: record.GetString("domain"),
|
||||
Access: access.GetString("config"),
|
||||
KeyAlgorithm: applyConfig.KeyAlgorithm,
|
||||
Nameservers: applyConfig.Nameservers,
|
||||
Timeout: applyConfig.Timeout,
|
||||
DisableFollowCNAME: applyConfig.DisableFollowCNAME,
|
||||
}
|
||||
|
||||
switch access.GetString("configType") {
|
||||
case configTypeAliyun:
|
||||
return NewAliyun(option), nil
|
||||
case configTypeTencent:
|
||||
return NewTencent(option), nil
|
||||
case configTypeHuaweiCloud:
|
||||
return NewHuaweiCloud(option), nil
|
||||
case configTypeAws:
|
||||
return NewAws(option), nil
|
||||
case configTypeCloudflare:
|
||||
return NewCloudflare(option), nil
|
||||
case configTypeNamesilo:
|
||||
return NewNamesilo(option), nil
|
||||
case configTypeGodaddy:
|
||||
return NewGodaddy(option), nil
|
||||
case configTypePdns:
|
||||
return NewPdns(option), nil
|
||||
case configTypeHttpreq:
|
||||
return NewHttpreq(option), nil
|
||||
default:
|
||||
return nil, errors.New("unknown config type")
|
||||
}
|
||||
type ApplicantWithWorkflowNodeConfig struct {
|
||||
Node *domain.WorkflowNode
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
type SSLProviderConfig struct {
|
||||
Config SSLProviderConfigContent `json:"config"`
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
type SSLProviderConfigContent struct {
|
||||
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) {
|
||||
record, _ := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='ssl-provider'")
|
||||
|
||||
sslProvider := &SSLProviderConfig{
|
||||
Config: SSLProviderConfigContent{},
|
||||
Provider: defaultSSLProvider,
|
||||
func NewWithWorkflowNode(config ApplicantWithWorkflowNodeConfig) (Applicant, error) {
|
||||
if config.Node == nil {
|
||||
return nil, fmt.Errorf("node is nil")
|
||||
}
|
||||
if record != nil {
|
||||
if err := record.UnmarshalJSONField("content", sslProvider); err != nil {
|
||||
return nil, err
|
||||
if config.Node.Type != domain.WorkflowNodeTypeApply {
|
||||
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeApply))
|
||||
}
|
||||
|
||||
nodeCfg := config.Node.GetConfigForApply()
|
||||
options := &applicantProviderOptions{
|
||||
Domains: xslices.Filter(strings.Split(nodeCfg.Domains, ";"), func(s string) bool { return s != "" }),
|
||||
ContactEmail: nodeCfg.ContactEmail,
|
||||
Provider: domain.ACMEDns01ProviderType(nodeCfg.Provider),
|
||||
ProviderAccessConfig: make(map[string]any),
|
||||
ProviderServiceConfig: nodeCfg.ProviderConfig,
|
||||
CAProvider: domain.CAProviderType(nodeCfg.CAProvider),
|
||||
CAProviderAccessConfig: make(map[string]any),
|
||||
CAProviderServiceConfig: nodeCfg.CAProviderConfig,
|
||||
KeyAlgorithm: nodeCfg.KeyAlgorithm,
|
||||
Nameservers: xslices.Filter(strings.Split(nodeCfg.Nameservers, ";"), func(s string) bool { return s != "" }),
|
||||
DnsPropagationWait: nodeCfg.DnsPropagationWait,
|
||||
DnsPropagationTimeout: nodeCfg.DnsPropagationTimeout,
|
||||
DnsTTL: nodeCfg.DnsTTL,
|
||||
DisableFollowCNAME: nodeCfg.DisableFollowCNAME,
|
||||
}
|
||||
|
||||
accessRepo := repository.NewAccessRepository()
|
||||
if nodeCfg.ProviderAccessId != "" {
|
||||
if access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId); err != nil {
|
||||
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err)
|
||||
} else {
|
||||
options.ProviderAccessConfig = access.Config
|
||||
}
|
||||
}
|
||||
if nodeCfg.CAProviderAccessId != "" {
|
||||
if access, err := accessRepo.GetById(context.Background(), nodeCfg.CAProviderAccessId); err != nil {
|
||||
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.CAProviderAccessId, err)
|
||||
} else {
|
||||
options.CAProviderAccessId = access.Id
|
||||
options.CAProviderAccessConfig = access.Config
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
settingsRepo := repository.NewSettingsRepository()
|
||||
if string(options.CAProvider) == "" {
|
||||
settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider")
|
||||
|
||||
myUser, err := newApplyUser(sslProvider.Provider, option.Email)
|
||||
sslProviderConfig := &acmeSSLProviderConfig{
|
||||
Config: make(map[domain.CAProviderType]map[string]any),
|
||||
Provider: caDefault,
|
||||
}
|
||||
if settings != nil {
|
||||
if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil {
|
||||
return nil, err
|
||||
} else if sslProviderConfig.Provider == "" {
|
||||
sslProviderConfig.Provider = caDefault
|
||||
}
|
||||
}
|
||||
|
||||
options.CAProvider = domain.CAProviderType(sslProviderConfig.Provider)
|
||||
options.CAProviderAccessConfig = sslProviderConfig.Config[options.CAProvider]
|
||||
}
|
||||
|
||||
certRepo := repository.NewCertificateRepository()
|
||||
lastCertificate, _ := certRepo.GetByWorkflowNodeId(context.Background(), config.Node.Id)
|
||||
if lastCertificate != nil && !lastCertificate.ACMERenewed {
|
||||
newCertSan := slices.Clone(options.Domains)
|
||||
oldCertSan := strings.Split(lastCertificate.SubjectAltNames, ";")
|
||||
slices.Sort(newCertSan)
|
||||
slices.Sort(oldCertSan)
|
||||
|
||||
if slices.Equal(newCertSan, oldCertSan) {
|
||||
lastCertX509, _ := certcrypto.ParsePEMCertificate([]byte(lastCertificate.Certificate))
|
||||
if lastCertX509 != nil {
|
||||
replacedARICertId, _ := certificate.MakeARICertID(lastCertX509)
|
||||
options.ARIReplaceAcct = lastCertificate.ACMEAccountUrl
|
||||
options.ARIReplaceCert = replacedARICertId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applicant, err := createApplicantProvider(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := lego.NewConfig(myUser)
|
||||
return &applicantImpl{
|
||||
applicant: applicant,
|
||||
options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM.
|
||||
config.CADirURL = sslProviderUrls[sslProvider.Provider]
|
||||
config.Certificate.KeyType = parseKeyAlgorithm(option.KeyAlgorithm)
|
||||
type applicantImpl struct {
|
||||
applicant challenge.Provider
|
||||
options *applicantProviderOptions
|
||||
}
|
||||
|
||||
// A client facilitates communication with the CA server.
|
||||
var _ Applicant = (*applicantImpl)(nil)
|
||||
|
||||
func (d *applicantImpl) Apply(ctx context.Context) (*ApplyResult, error) {
|
||||
limiter := getLimiter(fmt.Sprintf("apply_%s", d.options.ContactEmail))
|
||||
if err := limiter.Wait(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return applyUseLego(d.applicant, d.options)
|
||||
}
|
||||
|
||||
const (
|
||||
limitBurst = 300
|
||||
limitRate float64 = float64(1) / float64(36)
|
||||
)
|
||||
|
||||
var limiters sync.Map
|
||||
|
||||
func getLimiter(key string) *rate.Limiter {
|
||||
limiter, _ := limiters.LoadOrStore(key, rate.NewLimiter(rate.Limit(limitRate), 300))
|
||||
return limiter.(*rate.Limiter)
|
||||
}
|
||||
|
||||
func applyUseLego(legoProvider challenge.Provider, options *applicantProviderOptions) (*ApplyResult, error) {
|
||||
user, err := newAcmeUser(string(options.CAProvider), options.CAProviderAccessId, options.ContactEmail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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(options.DisableFollowCNAME))
|
||||
|
||||
// Create an ACME client config
|
||||
config := lego.NewConfig(user)
|
||||
config.Certificate.KeyType = parseLegoKeyAlgorithm(domain.CertificateKeyAlgorithmType(options.KeyAlgorithm))
|
||||
switch user.getCAProvider() {
|
||||
case caSSLCom:
|
||||
if strings.HasPrefix(options.KeyAlgorithm, "RSA") {
|
||||
config.CADirURL = caDirUrls[caSSLCom+"RSA"]
|
||||
} else if strings.HasPrefix(options.KeyAlgorithm, "EC") {
|
||||
config.CADirURL = caDirUrls[caSSLCom+"ECC"]
|
||||
} else {
|
||||
config.CADirURL = caDirUrls[caSSLCom]
|
||||
}
|
||||
|
||||
case caCustom:
|
||||
caDirURL := xmaps.GetString(options.CAProviderAccessConfig, "endpoint")
|
||||
if caDirURL != "" {
|
||||
config.CADirURL = caDirURL
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid ca provider endpoint")
|
||||
}
|
||||
|
||||
default:
|
||||
config.CADirURL = caDirUrls[user.CA]
|
||||
}
|
||||
|
||||
// Create an ACME client
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
challengeOptions := make([]dns01.ChallengeOption, 0)
|
||||
nameservers := parseNameservers(option.Nameservers)
|
||||
if len(nameservers) > 0 {
|
||||
challengeOptions = append(challengeOptions, dns01.AddRecursiveNameservers(nameservers))
|
||||
}
|
||||
// Set the DNS01 challenge provider
|
||||
client.Challenge.SetDNS01Provider(legoProvider,
|
||||
dns01.CondOption(
|
||||
len(options.Nameservers) > 0,
|
||||
dns01.AddRecursiveNameservers(dns01.ParseNameservers(options.Nameservers)),
|
||||
),
|
||||
dns01.CondOption(
|
||||
options.DnsPropagationWait > 0,
|
||||
dns01.PropagationWait(time.Duration(options.DnsPropagationWait)*time.Second, true),
|
||||
),
|
||||
dns01.CondOption(
|
||||
len(options.Nameservers) > 0 || options.DnsPropagationWait > 0,
|
||||
dns01.DisableAuthoritativeNssPropagationRequirement(),
|
||||
),
|
||||
)
|
||||
|
||||
client.Challenge.SetDNS01Provider(provider, challengeOptions...)
|
||||
|
||||
// New users will need to register
|
||||
if !myUser.hasRegistration() {
|
||||
reg, err := getReg(client, sslProvider, myUser)
|
||||
// New users need to register first
|
||||
if !user.hasRegistration() {
|
||||
reg, err := registerAcmeUserWithSingleFlight(client, user, options.CAProviderAccessConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to register: %w", err)
|
||||
return nil, fmt.Errorf("failed to register acme user: %w", err)
|
||||
}
|
||||
myUser.Registration = reg
|
||||
user.Registration = reg
|
||||
}
|
||||
|
||||
domains := strings.Split(option.Domain, ";")
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: domains,
|
||||
// Obtain a certificate
|
||||
certRequest := certificate.ObtainRequest{
|
||||
Domains: options.Domains,
|
||||
Bundle: true,
|
||||
}
|
||||
certificates, err := client.Certificate.Obtain(request)
|
||||
if options.ARIReplaceAcct == user.Registration.URI {
|
||||
certRequest.ReplacesCertID = options.ARIReplaceCert
|
||||
}
|
||||
|
||||
certResource, err := client.Certificate.Obtain(certRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Certificate{
|
||||
CertUrl: certificates.CertURL,
|
||||
CertStableUrl: certificates.CertStableURL,
|
||||
PrivateKey: string(certificates.PrivateKey),
|
||||
Certificate: string(certificates.Certificate),
|
||||
IssuerCertificate: string(certificates.IssuerCertificate),
|
||||
Csr: string(certificates.CSR),
|
||||
return &ApplyResult{
|
||||
CSR: strings.TrimSpace(string(certResource.CSR)),
|
||||
FullChainCertificate: strings.TrimSpace(string(certResource.Certificate)),
|
||||
IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)),
|
||||
PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)),
|
||||
ACMEAccountUrl: user.Registration.URI,
|
||||
ACMECertUrl: certResource.CertURL,
|
||||
ACMECertStableUrl: certResource.CertStableURL,
|
||||
ARIReplaced: certRequest.ReplacesCertID != "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
case sslProviderZeroSSL:
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: sslProvider.Config.Zerossl.EabKid,
|
||||
HmacEncoded: sslProvider.Config.Zerossl.EabHmacKey,
|
||||
})
|
||||
case sslProviderGts:
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: sslProvider.Config.Gts.EabKid,
|
||||
HmacEncoded: sslProvider.Config.Gts.EabHmacKey,
|
||||
})
|
||||
|
||||
case sslProviderLetsencrypt:
|
||||
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
|
||||
default:
|
||||
err = errors.New("unknown ssl provider")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repo := getAcmeAccountRepository()
|
||||
|
||||
resp, err := repo.GetByCAAndEmail(sslProvider.Provider, user.GetEmail())
|
||||
if err == nil {
|
||||
user.key = resp.Key
|
||||
return resp.Resource, nil
|
||||
}
|
||||
|
||||
if err := repo.Save(sslProvider.Provider, user.GetEmail(), user.getPrivateKeyString(), reg); err != nil {
|
||||
return nil, fmt.Errorf("failed to save registration: %w", err)
|
||||
}
|
||||
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
func parseNameservers(ns string) []string {
|
||||
nameservers := make([]string, 0)
|
||||
|
||||
lines := strings.Split(ns, ";")
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
nameservers = append(nameservers, line)
|
||||
}
|
||||
|
||||
return nameservers
|
||||
}
|
||||
|
||||
func parseKeyAlgorithm(algo string) certcrypto.KeyType {
|
||||
switch algo {
|
||||
case "RSA2048":
|
||||
return certcrypto.RSA2048
|
||||
case "RSA3072":
|
||||
return certcrypto.RSA3072
|
||||
case "RSA4096":
|
||||
return certcrypto.RSA4096
|
||||
case "RSA8192":
|
||||
return certcrypto.RSA8192
|
||||
case "EC256":
|
||||
return certcrypto.EC256
|
||||
case "EC384":
|
||||
return certcrypto.EC384
|
||||
default:
|
||||
return certcrypto.RSA2048
|
||||
}
|
||||
func parseLegoKeyAlgorithm(algo domain.CertificateKeyAlgorithmType) certcrypto.KeyType {
|
||||
alogMap := map[domain.CertificateKeyAlgorithmType]certcrypto.KeyType{
|
||||
domain.CertificateKeyAlgorithmTypeRSA2048: certcrypto.RSA2048,
|
||||
domain.CertificateKeyAlgorithmTypeRSA3072: certcrypto.RSA3072,
|
||||
domain.CertificateKeyAlgorithmTypeRSA4096: certcrypto.RSA4096,
|
||||
domain.CertificateKeyAlgorithmTypeRSA8192: certcrypto.RSA8192,
|
||||
domain.CertificateKeyAlgorithmTypeEC256: certcrypto.EC256,
|
||||
domain.CertificateKeyAlgorithmTypeEC384: certcrypto.EC384,
|
||||
domain.CertificateKeyAlgorithmTypeEC512: certcrypto.KeyType("P512"),
|
||||
}
|
||||
|
||||
if keyType, ok := alogMap[algo]; ok {
|
||||
return keyType
|
||||
}
|
||||
|
||||
return certcrypto.RSA2048
|
||||
}
|
||||
|
||||
44
internal/applicant/applicant_test.go
Normal file
44
internal/applicant/applicant_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package applicant_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
func TestRateLimit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
burst int
|
||||
rate rate.Limit
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
burst: 300,
|
||||
rate: rate.Limit(float64(1) / float64(20)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rl := rate.NewLimiter(tt.rate, tt.burst)
|
||||
if rl.Burst() != tt.burst {
|
||||
t.Errorf("Burst() = %v, want %v", rl.Burst(), tt.burst)
|
||||
}
|
||||
if rl.Limit() != tt.rate {
|
||||
t.Errorf("Limit() = %v, want %v", rl.Limit(), tt.rate)
|
||||
}
|
||||
|
||||
t.Log("consume all tokens at once", rl.AllowN(time.Now(), tt.burst))
|
||||
|
||||
t.Log("consume more", rl.Allow())
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
t.Log("consume after 5 seconds", rl.Allow())
|
||||
|
||||
time.Sleep(time.Second * 20)
|
||||
t.Log("consume after 20 seconds", rl.Allow())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/route53"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type aws struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewAws(option *ApplyOption) Applicant {
|
||||
return &aws{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *aws) Apply() (*Certificate, error) {
|
||||
access := &domain.AwsAccess{}
|
||||
json.Unmarshal([]byte(t.option.Access), access)
|
||||
|
||||
os.Setenv("AWS_REGION", access.Region)
|
||||
os.Setenv("AWS_ACCESS_KEY_ID", access.AccessKeyId)
|
||||
os.Setenv("AWS_SECRET_ACCESS_KEY", access.SecretAccessKey)
|
||||
os.Setenv("AWS_HOSTED_ZONE_ID", access.HostedZoneId)
|
||||
os.Setenv("AWS_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
|
||||
|
||||
dnsProvider, err := route53.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(t.option, dnsProvider)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
cf "github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type cloudflare struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewCloudflare(option *ApplyOption) Applicant {
|
||||
return &cloudflare{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cloudflare) Apply() (*Certificate, error) {
|
||||
access := &domain.CloudflareAccess{}
|
||||
json.Unmarshal([]byte(c.option.Access), access)
|
||||
|
||||
os.Setenv("CLOUDFLARE_DNS_API_TOKEN", access.DnsApiToken)
|
||||
os.Setenv("CLOUDFLARE_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", c.option.Timeout))
|
||||
|
||||
provider, err := cf.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(c.option, provider)
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
godaddyProvider "github.com/go-acme/lego/v4/providers/dns/godaddy"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type godaddy struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewGodaddy(option *ApplyOption) Applicant {
|
||||
return &godaddy{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *godaddy) Apply() (*Certificate, error) {
|
||||
access := &domain.GodaddyAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("GODADDY_API_KEY", access.ApiKey)
|
||||
os.Setenv("GODADDY_API_SECRET", access.ApiSecret)
|
||||
os.Setenv("GODADDY_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
|
||||
dnsProvider, err := godaddyProvider.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(a.option, dnsProvider)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/httpreq"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type httpReq struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewHttpreq(option *ApplyOption) Applicant {
|
||||
return &httpReq{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *httpReq) Apply() (*Certificate, error) {
|
||||
access := &domain.HttpreqAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("HTTPREQ_ENDPOINT", access.Endpoint)
|
||||
os.Setenv("HTTPREQ_MODE", access.Mode)
|
||||
os.Setenv("HTTPREQ_USERNAME", access.Username)
|
||||
os.Setenv("HTTPREQ_PASSWORD", access.Password)
|
||||
os.Setenv("HTTPREQ_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
dnsProvider, err := httpreq.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(a.option, dnsProvider)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
huaweicloudProvider "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type huaweicloud struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewHuaweiCloud(option *ApplyOption) Applicant {
|
||||
return &huaweicloud{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *huaweicloud) Apply() (*Certificate, error) {
|
||||
access := &domain.HuaweiCloudAccess{}
|
||||
json.Unmarshal([]byte(t.option.Access), access)
|
||||
|
||||
region := access.Region
|
||||
if region == "" {
|
||||
region = "cn-north-1"
|
||||
}
|
||||
|
||||
os.Setenv("HUAWEICLOUD_REGION", region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
|
||||
os.Setenv("HUAWEICLOUD_ACCESS_KEY_ID", access.AccessKeyId)
|
||||
os.Setenv("HUAWEICLOUD_SECRET_ACCESS_KEY", access.SecretAccessKey)
|
||||
os.Setenv("HUAWEICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
|
||||
|
||||
dnsProvider, err := huaweicloudProvider.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(t.option, dnsProvider)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
namesiloProvider "github.com/go-acme/lego/v4/providers/dns/namesilo"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type namesilo struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewNamesilo(option *ApplyOption) Applicant {
|
||||
return &namesilo{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *namesilo) Apply() (*Certificate, error) {
|
||||
access := &domain.NameSiloAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("NAMESILO_API_KEY", access.ApiKey)
|
||||
os.Setenv("NAMESILO_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
|
||||
dnsProvider, err := namesiloProvider.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(a.option, dnsProvider)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/pdns"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type powerdns struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewPdns(option *ApplyOption) Applicant {
|
||||
return &powerdns{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *powerdns) Apply() (*Certificate, error) {
|
||||
access := &domain.PdnsAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("PDNS_API_URL", access.ApiUrl)
|
||||
os.Setenv("PDNS_API_KEY", access.ApiKey)
|
||||
os.Setenv("PDNS_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
dnsProvider, err := pdns.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(a.option, dnsProvider)
|
||||
}
|
||||
683
internal/applicant/providers.go
Normal file
683
internal/applicant/providers.go
Normal file
@@ -0,0 +1,683 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
|
||||
"github.com/certimate-go/certimate/internal/domain"
|
||||
pACMEHttpReq "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/acmehttpreq"
|
||||
pAliyun "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/aliyun"
|
||||
pAliyunESA "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/aliyun-esa"
|
||||
pAWSRoute53 "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/aws-route53"
|
||||
pAzureDNS "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/azure-dns"
|
||||
pBaiduCloud "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/baiducloud"
|
||||
pBunny "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/bunny"
|
||||
pCloudflare "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/cloudflare"
|
||||
pClouDNS "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/cloudns"
|
||||
pCMCCCloud "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/cmcccloud"
|
||||
pConstellix "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/constellix"
|
||||
pCTCCCloud "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/ctcccloud"
|
||||
pDeSEC "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/desec"
|
||||
pDigitalOcean "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/digitalocean"
|
||||
pDNSLA "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/dnsla"
|
||||
pDuckDNS "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/duckdns"
|
||||
pDynv6 "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/dynv6"
|
||||
pGcore "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/gcore"
|
||||
pGname "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/gname"
|
||||
pGoDaddy "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/godaddy"
|
||||
pHetzner "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/hetzner"
|
||||
pHuaweiCloud "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/huaweicloud"
|
||||
pJDCloud "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/jdcloud"
|
||||
pNamecheap "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/namecheap"
|
||||
pNameDotCom "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/namedotcom"
|
||||
pNameSilo "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/namesilo"
|
||||
pNetcup "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/netcup"
|
||||
pNetlify "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/netlify"
|
||||
pNS1 "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/ns1"
|
||||
pPorkbun "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/porkbun"
|
||||
pPowerDNS "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/powerdns"
|
||||
pRainYun "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/rainyun"
|
||||
pTencentCloud "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/tencentcloud"
|
||||
pTencentCloudEO "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/tencentcloud-eo"
|
||||
pUCloudUDNR "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/ucloud-udnr"
|
||||
pVercel "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/vercel"
|
||||
pVolcEngine "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/volcengine"
|
||||
pWestcn "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/westcn"
|
||||
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
|
||||
)
|
||||
|
||||
type applicantProviderOptions struct {
|
||||
Domains []string
|
||||
ContactEmail string
|
||||
Provider domain.ACMEDns01ProviderType
|
||||
ProviderAccessConfig map[string]any
|
||||
ProviderServiceConfig map[string]any
|
||||
CAProvider domain.CAProviderType
|
||||
CAProviderAccessId string
|
||||
CAProviderAccessConfig map[string]any
|
||||
CAProviderServiceConfig map[string]any
|
||||
KeyAlgorithm string
|
||||
Nameservers []string
|
||||
DnsPropagationWait int32
|
||||
DnsPropagationTimeout int32
|
||||
DnsTTL int32
|
||||
DisableFollowCNAME bool
|
||||
ARIReplaceAcct string
|
||||
ARIReplaceCert string
|
||||
}
|
||||
|
||||
func createApplicantProvider(options *applicantProviderOptions) (challenge.Provider, error) {
|
||||
/*
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
switch options.Provider {
|
||||
case domain.ACMEDns01ProviderTypeACMEHttpReq:
|
||||
{
|
||||
access := domain.AccessConfigForACMEHttpReq{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pACMEHttpReq.NewChallengeProvider(&pACMEHttpReq.ChallengeProviderConfig{
|
||||
Endpoint: access.Endpoint,
|
||||
Mode: access.Mode,
|
||||
Username: access.Username,
|
||||
Password: access.Password,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeAliyun, domain.ACMEDns01ProviderTypeAliyunDNS, domain.ACMEDns01ProviderTypeAliyunESA:
|
||||
{
|
||||
access := domain.AccessConfigForAliyun{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.ACMEDns01ProviderTypeAliyun, domain.ACMEDns01ProviderTypeAliyunDNS:
|
||||
applicant, err := pAliyun.NewChallengeProvider(&pAliyun.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
|
||||
case domain.ACMEDns01ProviderTypeAliyunESA:
|
||||
applicant, err := pAliyunESA.NewChallengeProvider(&pAliyunESA.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: xmaps.GetString(options.ProviderServiceConfig, "region"),
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeAWS, domain.ACMEDns01ProviderTypeAWSRoute53:
|
||||
{
|
||||
access := domain.AccessConfigForAWS{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pAWSRoute53.NewChallengeProvider(&pAWSRoute53.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: xmaps.GetString(options.ProviderServiceConfig, "region"),
|
||||
HostedZoneId: xmaps.GetString(options.ProviderServiceConfig, "hostedZoneId"),
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeAzure, domain.ACMEDns01ProviderTypeAzureDNS:
|
||||
{
|
||||
access := domain.AccessConfigForAzure{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pAzureDNS.NewChallengeProvider(&pAzureDNS.ChallengeProviderConfig{
|
||||
TenantId: access.TenantId,
|
||||
ClientId: access.ClientId,
|
||||
ClientSecret: access.ClientSecret,
|
||||
CloudName: access.CloudName,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeBaiduCloud, domain.ACMEDns01ProviderTypeBaiduCloudDNS:
|
||||
{
|
||||
access := domain.AccessConfigForBaiduCloud{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pBaiduCloud.NewChallengeProvider(&pBaiduCloud.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeBunny:
|
||||
{
|
||||
access := domain.AccessConfigForBunny{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pBunny.NewChallengeProvider(&pBunny.ChallengeProviderConfig{
|
||||
ApiKey: access.ApiKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeCloudflare:
|
||||
{
|
||||
access := domain.AccessConfigForCloudflare{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pCloudflare.NewChallengeProvider(&pCloudflare.ChallengeProviderConfig{
|
||||
DnsApiToken: access.DnsApiToken,
|
||||
ZoneApiToken: access.ZoneApiToken,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeClouDNS:
|
||||
{
|
||||
access := domain.AccessConfigForClouDNS{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pClouDNS.NewChallengeProvider(&pClouDNS.ChallengeProviderConfig{
|
||||
AuthId: access.AuthId,
|
||||
AuthPassword: access.AuthPassword,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeCMCCCloud, domain.ACMEDns01ProviderTypeCMCCCloudDNS:
|
||||
{
|
||||
access := domain.AccessConfigForCMCCCloud{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pCMCCCloud.NewChallengeProvider(&pCMCCCloud.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeConstellix:
|
||||
{
|
||||
access := domain.AccessConfigForConstellix{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pConstellix.NewChallengeProvider(&pConstellix.ChallengeProviderConfig{
|
||||
ApiKey: access.ApiKey,
|
||||
SecretKey: access.SecretKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeCTCCCloud, domain.ACMEDns01ProviderTypeCTCCCloudSmartDNS:
|
||||
{
|
||||
access := domain.AccessConfigForCTCCCloud{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pCTCCCloud.NewChallengeProvider(&pCTCCCloud.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeDeSEC:
|
||||
{
|
||||
access := domain.AccessConfigForDeSEC{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pDeSEC.NewChallengeProvider(&pDeSEC.ChallengeProviderConfig{
|
||||
Token: access.Token,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeDigitalOcean:
|
||||
{
|
||||
access := domain.AccessConfigForDigitalOcean{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pDigitalOcean.NewChallengeProvider(&pDigitalOcean.ChallengeProviderConfig{
|
||||
AccessToken: access.AccessToken,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeDNSLA:
|
||||
{
|
||||
access := domain.AccessConfigForDNSLA{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pDNSLA.NewChallengeProvider(&pDNSLA.ChallengeProviderConfig{
|
||||
ApiId: access.ApiId,
|
||||
ApiSecret: access.ApiSecret,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeDuckDNS:
|
||||
{
|
||||
access := domain.AccessConfigForDuckDNS{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pDuckDNS.NewChallengeProvider(&pDuckDNS.ChallengeProviderConfig{
|
||||
Token: access.Token,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeDynv6:
|
||||
{
|
||||
access := domain.AccessConfigForDynv6{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pDynv6.NewChallengeProvider(&pDynv6.ChallengeProviderConfig{
|
||||
HttpToken: access.HttpToken,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeGcore:
|
||||
{
|
||||
access := domain.AccessConfigForGcore{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pGcore.NewChallengeProvider(&pGcore.ChallengeProviderConfig{
|
||||
ApiToken: access.ApiToken,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeGname:
|
||||
{
|
||||
access := domain.AccessConfigForGname{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pGname.NewChallengeProvider(&pGname.ChallengeProviderConfig{
|
||||
AppId: access.AppId,
|
||||
AppKey: access.AppKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeGoDaddy:
|
||||
{
|
||||
access := domain.AccessConfigForGoDaddy{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pGoDaddy.NewChallengeProvider(&pGoDaddy.ChallengeProviderConfig{
|
||||
ApiKey: access.ApiKey,
|
||||
ApiSecret: access.ApiSecret,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeHetzner:
|
||||
{
|
||||
access := domain.AccessConfigForHetzner{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pHetzner.NewChallengeProvider(&pHetzner.ChallengeProviderConfig{
|
||||
ApiToken: access.ApiToken,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeHuaweiCloud, domain.ACMEDns01ProviderTypeHuaweiCloudDNS:
|
||||
{
|
||||
access := domain.AccessConfigForHuaweiCloud{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pHuaweiCloud.NewChallengeProvider(&pHuaweiCloud.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: xmaps.GetString(options.ProviderServiceConfig, "region"),
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeJDCloud, domain.ACMEDns01ProviderTypeJDCloudDNS:
|
||||
{
|
||||
access := domain.AccessConfigForJDCloud{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pJDCloud.NewChallengeProvider(&pJDCloud.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
RegionId: xmaps.GetString(options.ProviderServiceConfig, "regionId"),
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeNamecheap:
|
||||
{
|
||||
access := domain.AccessConfigForNamecheap{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pNamecheap.NewChallengeProvider(&pNamecheap.ChallengeProviderConfig{
|
||||
Username: access.Username,
|
||||
ApiKey: access.ApiKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeNameDotCom:
|
||||
{
|
||||
access := domain.AccessConfigForNameDotCom{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pNameDotCom.NewChallengeProvider(&pNameDotCom.ChallengeProviderConfig{
|
||||
Username: access.Username,
|
||||
ApiToken: access.ApiToken,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeNameSilo:
|
||||
{
|
||||
access := domain.AccessConfigForNameSilo{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pNameSilo.NewChallengeProvider(&pNameSilo.ChallengeProviderConfig{
|
||||
ApiKey: access.ApiKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeNetcup:
|
||||
{
|
||||
access := domain.AccessConfigForNetcup{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pNetcup.NewChallengeProvider(&pNetcup.ChallengeProviderConfig{
|
||||
CustomerNumber: access.CustomerNumber,
|
||||
ApiKey: access.ApiKey,
|
||||
ApiPassword: access.ApiPassword,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeNetlify:
|
||||
{
|
||||
access := domain.AccessConfigForNetlify{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pNetlify.NewChallengeProvider(&pNetlify.ChallengeProviderConfig{
|
||||
ApiToken: access.ApiToken,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeNS1:
|
||||
{
|
||||
access := domain.AccessConfigForNS1{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pNS1.NewChallengeProvider(&pNS1.ChallengeProviderConfig{
|
||||
ApiKey: access.ApiKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypePorkbun:
|
||||
{
|
||||
access := domain.AccessConfigForPorkbun{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pPorkbun.NewChallengeProvider(&pPorkbun.ChallengeProviderConfig{
|
||||
ApiKey: access.ApiKey,
|
||||
SecretApiKey: access.SecretApiKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypePowerDNS:
|
||||
{
|
||||
access := domain.AccessConfigForPowerDNS{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pPowerDNS.NewChallengeProvider(&pPowerDNS.ChallengeProviderConfig{
|
||||
ServerUrl: access.ServerUrl,
|
||||
ApiKey: access.ApiKey,
|
||||
AllowInsecureConnections: access.AllowInsecureConnections,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeRainYun:
|
||||
{
|
||||
access := domain.AccessConfigForRainYun{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pRainYun.NewChallengeProvider(&pRainYun.ChallengeProviderConfig{
|
||||
ApiKey: access.ApiKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeTencentCloud, domain.ACMEDns01ProviderTypeTencentCloudDNS, domain.ACMEDns01ProviderTypeTencentCloudEO:
|
||||
{
|
||||
access := domain.AccessConfigForTencentCloud{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.ACMEDns01ProviderTypeTencentCloud, domain.ACMEDns01ProviderTypeTencentCloudDNS:
|
||||
applicant, err := pTencentCloud.NewChallengeProvider(&pTencentCloud.ChallengeProviderConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
|
||||
case domain.ACMEDns01ProviderTypeTencentCloudEO:
|
||||
applicant, err := pTencentCloudEO.NewChallengeProvider(&pTencentCloudEO.ChallengeProviderConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
ZoneId: xmaps.GetString(options.ProviderServiceConfig, "zoneId"),
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeUCloudUDNR:
|
||||
{
|
||||
access := domain.AccessConfigForUCloud{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pUCloudUDNR.NewChallengeProvider(&pUCloudUDNR.ChallengeProviderConfig{
|
||||
PrivateKey: access.PrivateKey,
|
||||
PublicKey: access.PublicKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeVercel:
|
||||
{
|
||||
access := domain.AccessConfigForVercel{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pVercel.NewChallengeProvider(&pVercel.ChallengeProviderConfig{
|
||||
ApiAccessToken: access.ApiAccessToken,
|
||||
TeamId: access.TeamId,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeVolcEngine, domain.ACMEDns01ProviderTypeVolcEngineDNS:
|
||||
{
|
||||
access := domain.AccessConfigForVolcEngine{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pVolcEngine.NewChallengeProvider(&pVolcEngine.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ACMEDns01ProviderTypeWestcn:
|
||||
{
|
||||
access := domain.AccessConfigForWestcn{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pWestcn.NewChallengeProvider(&pWestcn.ChallengeProviderConfig{
|
||||
Username: access.Username,
|
||||
ApiPassword: access.ApiPassword,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported applicant provider '%s'", string(options.Provider))
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type tencent struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewTencent(option *ApplyOption) Applicant {
|
||||
return &tencent{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tencent) Apply() (*Certificate, error) {
|
||||
access := &domain.TencentAccess{}
|
||||
json.Unmarshal([]byte(t.option.Access), access)
|
||||
|
||||
os.Setenv("TENCENTCLOUD_SECRET_ID", access.SecretId)
|
||||
os.Setenv("TENCENTCLOUD_SECRET_KEY", access.SecretKey)
|
||||
os.Setenv("TENCENTCLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
|
||||
|
||||
dnsProvider, err := tencentcloud.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(t.option, dnsProvider)
|
||||
}
|
||||
292
internal/certificate/service.go
Normal file
292
internal/certificate/service.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/pocketbase/dbx"
|
||||
|
||||
"github.com/certimate-go/certimate/internal/app"
|
||||
"github.com/certimate-go/certimate/internal/domain"
|
||||
"github.com/certimate-go/certimate/internal/domain/dtos"
|
||||
"github.com/certimate-go/certimate/internal/notify"
|
||||
"github.com/certimate-go/certimate/internal/repository"
|
||||
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultExpireSubject = "有 ${COUNT} 张证书即将过期"
|
||||
defaultExpireMessage = "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!"
|
||||
)
|
||||
|
||||
type certificateRepository interface {
|
||||
ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error)
|
||||
GetById(ctx context.Context, id string) (*domain.Certificate, error)
|
||||
DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error)
|
||||
}
|
||||
|
||||
type settingsRepository interface {
|
||||
GetByName(ctx context.Context, name string) (*domain.Settings, error)
|
||||
}
|
||||
|
||||
type CertificateService struct {
|
||||
certificateRepo certificateRepository
|
||||
settingsRepo settingsRepository
|
||||
}
|
||||
|
||||
func NewCertificateService(certificateRepo certificateRepository, settingsRepo settingsRepository) *CertificateService {
|
||||
return &CertificateService{
|
||||
certificateRepo: certificateRepo,
|
||||
settingsRepo: settingsRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CertificateService) InitSchedule(ctx context.Context) error {
|
||||
// 每日发送过期证书提醒
|
||||
app.GetScheduler().MustAdd("certificateExpireSoonNotify", "0 0 * * *", func() {
|
||||
certificates, err := s.certificateRepo.ListExpireSoon(context.Background())
|
||||
if err != nil {
|
||||
app.GetLogger().Error("failed to get certificates which expire soon", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
notification := buildExpireSoonNotification(certificates)
|
||||
if notification == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := notify.SendToAllChannels(notification.Subject, notification.Message); err != nil {
|
||||
app.GetLogger().Error("failed to send notification", "err", err)
|
||||
}
|
||||
})
|
||||
|
||||
// 每日清理过期证书
|
||||
app.GetScheduler().MustAdd("certificateExpiredCleanup", "0 0 * * *", func() {
|
||||
settings, err := s.settingsRepo.GetByName(ctx, "persistence")
|
||||
if err != nil {
|
||||
app.GetLogger().Error("failed to get persistence settings", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
var settingsContent *domain.PersistenceSettingsContent
|
||||
json.Unmarshal([]byte(settings.Content), &settingsContent)
|
||||
if settingsContent != nil && settingsContent.ExpiredCertificatesMaxDaysRetention != 0 {
|
||||
ret, err := s.certificateRepo.DeleteWhere(
|
||||
context.Background(),
|
||||
dbx.NewExp(fmt.Sprintf("expireAt<DATETIME('now', '-%d days')", settingsContent.ExpiredCertificatesMaxDaysRetention)),
|
||||
)
|
||||
if err != nil {
|
||||
app.GetLogger().Error("failed to delete expired certificates", "err", err)
|
||||
}
|
||||
|
||||
if ret > 0 {
|
||||
app.GetLogger().Info(fmt.Sprintf("cleanup %d expired certificates", ret))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.CertificateArchiveFileReq) (*dtos.CertificateArchiveFileResp, error) {
|
||||
certificate, err := s.certificateRepo.GetById(ctx, req.CertificateId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
zipWriter := zip.NewWriter(&buf)
|
||||
defer zipWriter.Close()
|
||||
|
||||
resp := &dtos.CertificateArchiveFileResp{
|
||||
FileFormat: "zip",
|
||||
}
|
||||
|
||||
switch strings.ToUpper(req.Format) {
|
||||
case "", "PEM":
|
||||
{
|
||||
certWriter, err := zipWriter.Create("certbundle.pem")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = certWriter.Write([]byte(certificate.Certificate))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyWriter, err := zipWriter.Create("privkey.pem")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = keyWriter.Write([]byte(certificate.PrivateKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = zipWriter.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.FileBytes = buf.Bytes()
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
case "PFX":
|
||||
{
|
||||
const pfxPassword = "certimate"
|
||||
|
||||
certPFX, err := xcert.TransformCertificateFromPEMToPFX(certificate.Certificate, certificate.PrivateKey, pfxPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certWriter, err := zipWriter.Create("cert.pfx")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = certWriter.Write(certPFX)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyWriter, err := zipWriter.Create("pfx-password.txt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = keyWriter.Write([]byte(pfxPassword))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = zipWriter.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.FileBytes = buf.Bytes()
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
case "JKS":
|
||||
{
|
||||
const jksPassword = "certimate"
|
||||
|
||||
certJKS, err := xcert.TransformCertificateFromPEMToJKS(certificate.Certificate, certificate.PrivateKey, jksPassword, jksPassword, jksPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certWriter, err := zipWriter.Create("cert.jks")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = certWriter.Write(certJKS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyWriter, err := zipWriter.Create("jks-password.txt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = keyWriter.Write([]byte(jksPassword))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = zipWriter.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.FileBytes = buf.Bytes()
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, domain.ErrInvalidParams
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CertificateService) ValidateCertificate(ctx context.Context, req *dtos.CertificateValidateCertificateReq) (*dtos.CertificateValidateCertificateResp, error) {
|
||||
certX509, err := xcert.ParseCertificateFromPEM(req.Certificate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if time.Now().After(certX509.NotAfter) {
|
||||
return nil, fmt.Errorf("certificate has expired at %s", certX509.NotAfter.UTC().Format(time.RFC3339))
|
||||
}
|
||||
|
||||
return &dtos.CertificateValidateCertificateResp{
|
||||
IsValid: true,
|
||||
Domains: strings.Join(certX509.DNSNames, ";"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *CertificateService) ValidatePrivateKey(ctx context.Context, req *dtos.CertificateValidatePrivateKeyReq) (*dtos.CertificateValidatePrivateKeyResp, error) {
|
||||
_, err := certcrypto.ParsePEMPrivateKey([]byte(req.PrivateKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dtos.CertificateValidatePrivateKeyResp{
|
||||
IsValid: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildExpireSoonNotification(certificates []*domain.Certificate) *struct {
|
||||
Subject string
|
||||
Message string
|
||||
} {
|
||||
if len(certificates) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
subject := defaultExpireSubject
|
||||
message := defaultExpireMessage
|
||||
|
||||
// 查询模板信息
|
||||
settingsRepo := repository.NewSettingsRepository()
|
||||
settings, err := settingsRepo.GetByName(context.Background(), "notifyTemplates")
|
||||
if err == nil {
|
||||
var templates *domain.NotifyTemplatesSettingsContent
|
||||
json.Unmarshal([]byte(settings.Content), &templates)
|
||||
|
||||
if templates != nil && len(templates.NotifyTemplates) > 0 {
|
||||
subject = templates.NotifyTemplates[0].Subject
|
||||
message = templates.NotifyTemplates[0].Message
|
||||
}
|
||||
}
|
||||
|
||||
// 替换变量
|
||||
count := len(certificates)
|
||||
domains := make([]string, count)
|
||||
for i, record := range certificates {
|
||||
domains[i] = record.SubjectAltNames
|
||||
}
|
||||
countStr := strconv.Itoa(count)
|
||||
domainStr := strings.Join(domains, ";")
|
||||
subject = strings.ReplaceAll(subject, "${COUNT}", countStr)
|
||||
subject = strings.ReplaceAll(subject, "${DOMAINS}", domainStr)
|
||||
message = strings.ReplaceAll(message, "${COUNT}", countStr)
|
||||
message = strings.ReplaceAll(message, "${DOMAINS}", domainStr)
|
||||
|
||||
// 返回消息
|
||||
return &struct {
|
||||
Subject string
|
||||
Message string
|
||||
}{Subject: subject, Message: message}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
alb20200616 "github.com/alibabacloud-go/alb-20200616/v2/client"
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
)
|
||||
|
||||
type AliyunALBDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *alb20200616.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewAliyunALBDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
|
||||
client, err := (&AliyunALBDeployer{}).createSdkClient(
|
||||
access.AccessKeyId,
|
||||
access.AccessKeySecret,
|
||||
option.DeployConfig.GetConfigAsString("region"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uploader, err := uploader.NewAliyunCASUploader(&uploader.AliyunCASUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: option.DeployConfig.GetConfigAsString("region"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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) GetInfo() []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) (*alb20200616.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-hangzhou" // ALB 服务默认区域:华东一杭州
|
||||
}
|
||||
|
||||
aConfig := &openapi.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 := alb20200616.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 := &alb20200616.GetLoadBalancerAttributeRequest{
|
||||
LoadBalancerId: tea.String(aliLoadbalancerId),
|
||||
}
|
||||
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute sdk request 'alb.GetLoadBalancerAttribute': %w", err)
|
||||
}
|
||||
|
||||
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 := &alb20200616.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 fmt.Errorf("failed to execute sdk request 'alb.ListListeners': %w", err)
|
||||
}
|
||||
|
||||
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 := &alb20200616.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 fmt.Errorf("failed to execute sdk request 'alb.ListListeners': %w", err)
|
||||
}
|
||||
|
||||
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
|
||||
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
|
||||
|
||||
// 批量更新监听证书
|
||||
var errs []error
|
||||
for _, aliListenerId := range aliListenerIds {
|
||||
if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.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
|
||||
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
|
||||
|
||||
// 更新监听
|
||||
if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.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 := &alb20200616.GetListenerAttributeRequest{
|
||||
ListenerId: tea.String(aliListenerId),
|
||||
}
|
||||
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute sdk request 'alb.GetListenerAttribute': %w", err)
|
||||
}
|
||||
|
||||
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 := &alb20200616.UpdateListenerAttributeRequest{
|
||||
ListenerId: tea.String(aliListenerId),
|
||||
Certificates: []*alb20200616.UpdateListenerAttributeRequestCertificates{{
|
||||
CertificateId: tea.String(aliCertId),
|
||||
}},
|
||||
}
|
||||
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute sdk request 'alb.UpdateListenerAttribute': %w", err)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已更新 ALB 监听配置", updateListenerAttributeResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
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"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/utils/rand"
|
||||
)
|
||||
|
||||
type AliyunCDNDeployer struct {
|
||||
client *cdn20180510.Client
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewAliyunCDNDeployer(option *DeployerOption) (*AliyunCDNDeployer, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
|
||||
d := &AliyunCDNDeployer{
|
||||
option: option,
|
||||
}
|
||||
|
||||
client, err := d.createClient(access.AccessKeyId, access.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AliyunCDNDeployer{
|
||||
client: client,
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunCDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *AliyunCDNDeployer) GetInfo() []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),
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("cdn设置证书", resp))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunCDNDeployer) createClient(accessKeyId, accessKeySecret string) (_result *cdn20180510.Client, _err error) {
|
||||
config := &openapi.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
config.Endpoint = tea.String("cdn.aliyuncs.com")
|
||||
_result = &cdn20180510.Client{}
|
||||
_result, _err = cdn20180510.NewClient(config)
|
||||
return _result, _err
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
slb20140515 "github.com/alibabacloud-go/slb-20140515/v4/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
)
|
||||
|
||||
type AliyunCLBDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *slb20140515.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewAliyunCLBDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
|
||||
client, err := (&AliyunCLBDeployer{}).createSdkClient(
|
||||
access.AccessKeyId,
|
||||
access.AccessKeySecret,
|
||||
option.DeployConfig.GetConfigAsString("region"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uploader, err := uploader.NewAliyunSLBUploader(&uploader.AliyunSLBUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: option.DeployConfig.GetConfigAsString("region"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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) GetInfo() []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) (*slb20140515.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-hangzhou" // CLB(SLB) 服务默认区域:华东一杭州
|
||||
}
|
||||
|
||||
aConfig := &openapi.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
|
||||
var endpoint string
|
||||
switch region {
|
||||
case "cn-hangzhou":
|
||||
case "cn-hangzhou-finance":
|
||||
case "cn-shanghai-finance-1":
|
||||
case "cn-shenzhen-finance-1":
|
||||
endpoint = "slb.aliyuncs.com"
|
||||
default:
|
||||
endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region)
|
||||
}
|
||||
aConfig.Endpoint = tea.String(endpoint)
|
||||
|
||||
client, err := slb20140515.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")
|
||||
if aliLoadbalancerId == "" {
|
||||
return errors.New("`loadbalancerId` is required")
|
||||
}
|
||||
|
||||
aliListenerPorts := make([]int32, 0)
|
||||
|
||||
// 查询负载均衡实例的详细信息
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute
|
||||
describeLoadBalancerAttributeReq := &slb20140515.DescribeLoadBalancerAttributeRequest{
|
||||
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
|
||||
LoadBalancerId: tea.String(aliLoadbalancerId),
|
||||
}
|
||||
describeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttribute(describeLoadBalancerAttributeReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerAttribute': %w", err)
|
||||
}
|
||||
|
||||
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 := &slb20140515.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 fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerListeners': %w", err)
|
||||
}
|
||||
|
||||
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
|
||||
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
|
||||
|
||||
// 批量更新监听证书
|
||||
var errs []error
|
||||
for _, aliListenerPort := range aliListenerPorts {
|
||||
if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, uploadResult.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
|
||||
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
|
||||
|
||||
// 更新监听
|
||||
if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, uploadResult.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 := &slb20140515.DescribeLoadBalancerHTTPSListenerAttributeRequest{
|
||||
LoadBalancerId: tea.String(aliLoadbalancerId),
|
||||
ListenerPort: tea.Int32(aliListenerPort),
|
||||
}
|
||||
describeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttribute(describeLoadBalancerHTTPSListenerAttributeReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute': %w", err)
|
||||
}
|
||||
|
||||
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 := &slb20140515.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 fmt.Errorf("failed to execute sdk request 'slb.DescribeDomainExtensions': %w", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
break
|
||||
}
|
||||
|
||||
setDomainExtensionAttributeReq := &slb20140515.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 fmt.Errorf("failed to execute sdk request 'slb.SetDomainExtensionAttribute': %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改监听配置
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute
|
||||
//
|
||||
// 注意修改监听配置要放在修改扩展域名之后
|
||||
setLoadBalancerHTTPSListenerAttributeReq := &slb20140515.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 fmt.Errorf("failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute': %w", err)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已更新 CLB HTTPS 监听配置", setLoadBalancerHTTPSListenerAttributeResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
nlb20220430 "github.com/alibabacloud-go/nlb-20220430/v2/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
)
|
||||
|
||||
type AliyunNLBDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *nlb20220430.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewAliyunNLBDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
|
||||
client, err := (&AliyunNLBDeployer{}).createSdkClient(
|
||||
access.AccessKeyId,
|
||||
access.AccessKeySecret,
|
||||
option.DeployConfig.GetConfigAsString("region"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uploader, err := uploader.NewAliyunCASUploader(&uploader.AliyunCASUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: option.DeployConfig.GetConfigAsString("region"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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) GetInfo() []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) (*nlb20220430.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-hangzhou" // NLB 服务默认区域:华东一杭州
|
||||
}
|
||||
|
||||
aConfig := &openapi.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 := nlb20220430.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 := &nlb20220430.GetLoadBalancerAttributeRequest{
|
||||
LoadBalancerId: tea.String(aliLoadbalancerId),
|
||||
}
|
||||
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute sdk request 'nlb.GetLoadBalancerAttribute': %w", err)
|
||||
}
|
||||
|
||||
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 := &nlb20220430.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 fmt.Errorf("failed to execute sdk request 'nlb.ListListeners': %w", err)
|
||||
}
|
||||
|
||||
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
|
||||
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
|
||||
|
||||
// 批量更新监听证书
|
||||
var errs []error
|
||||
for _, aliListenerId := range aliListenerIds {
|
||||
if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.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
|
||||
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
|
||||
|
||||
// 更新监听
|
||||
if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.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 := &nlb20220430.GetListenerAttributeRequest{
|
||||
ListenerId: tea.String(aliListenerId),
|
||||
}
|
||||
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute sdk request 'nlb.GetListenerAttribute': %w", err)
|
||||
}
|
||||
|
||||
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 := &nlb20220430.UpdateListenerAttributeRequest{
|
||||
ListenerId: tea.String(aliListenerId),
|
||||
CertificateIds: []*string{tea.String(aliCertId)},
|
||||
}
|
||||
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute sdk request 'nlb.UpdateListenerAttribute': %w", err)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已更新 NLB 监听配置", updateListenerAttributeResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type AliyunOSSDeployer struct {
|
||||
client *oss.Client
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewAliyunOSSDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
|
||||
d := &AliyunOSSDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}
|
||||
|
||||
client, err := d.createClient(access.AccessKeyId, access.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.client = client
|
||||
|
||||
return d, 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 {
|
||||
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"),
|
||||
CertificateConfiguration: &oss.CertificateConfiguration{
|
||||
Certificate: d.option.Certificate.Certificate,
|
||||
PrivateKey: d.option.Certificate.PrivateKey,
|
||||
Force: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("deploy aliyun oss error: %w", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
@@ -1,246 +1,74 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"log/slog"
|
||||
|
||||
"github.com/pavlo-v-chernykh/keystore-go/v4"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"software.sslmate.com/src/go-pkcs12"
|
||||
|
||||
"github.com/usual2970/certimate/internal/applicant"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
"github.com/certimate-go/certimate/internal/domain"
|
||||
"github.com/certimate-go/certimate/internal/repository"
|
||||
"github.com/certimate-go/certimate/pkg/core"
|
||||
)
|
||||
|
||||
const (
|
||||
targetAliyunOSS = "aliyun-oss"
|
||||
targetAliyunCDN = "aliyun-cdn"
|
||||
targetAliyunESA = "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"
|
||||
targetQiniuCdn = "qiniu-cdn"
|
||||
targetLocal = "local"
|
||||
targetSSH = "ssh"
|
||||
targetWebhook = "webhook"
|
||||
targetK8sSecret = "k8s-secret"
|
||||
)
|
||||
|
||||
type DeployerOption struct {
|
||||
DomainId string `json:"domainId"`
|
||||
Domain string `json:"domain"`
|
||||
Access string `json:"access"`
|
||||
AccessRecord *models.Record `json:"-"`
|
||||
DeployConfig domain.DeployConfig `json:"deployConfig"`
|
||||
Certificate applicant.Certificate `json:"certificate"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
|
||||
type Deployer interface {
|
||||
Deploy(ctx context.Context) error
|
||||
GetInfo() []string
|
||||
GetID() string
|
||||
}
|
||||
|
||||
func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error) {
|
||||
rs := make([]Deployer, 0)
|
||||
if record.GetString("deployConfig") == "" {
|
||||
return rs, nil
|
||||
type DeployerWithWorkflowNodeConfig struct {
|
||||
Node *domain.WorkflowNode
|
||||
Logger *slog.Logger
|
||||
CertificatePEM string
|
||||
PrivateKeyPEM string
|
||||
}
|
||||
|
||||
func NewWithWorkflowNode(config DeployerWithWorkflowNodeConfig) (Deployer, error) {
|
||||
if config.Node == nil {
|
||||
return nil, fmt.Errorf("node is nil")
|
||||
}
|
||||
if config.Node.Type != domain.WorkflowNodeTypeDeploy {
|
||||
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeDeploy))
|
||||
}
|
||||
|
||||
deployConfigs := make([]domain.DeployConfig, 0)
|
||||
|
||||
err := record.UnmarshalJSONField("deployConfig", &deployConfigs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析部署配置失败: %w", err)
|
||||
nodeCfg := config.Node.GetConfigForDeploy()
|
||||
options := &deployerProviderOptions{
|
||||
Provider: domain.DeploymentProviderType(nodeCfg.Provider),
|
||||
ProviderAccessConfig: make(map[string]any),
|
||||
ProviderServiceConfig: nodeCfg.ProviderConfig,
|
||||
}
|
||||
|
||||
if len(deployConfigs) == 0 {
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
for _, deployConfig := range deployConfigs {
|
||||
deployer, err := getWithDeployConfig(record, cert, deployConfig)
|
||||
accessRepo := repository.NewAccessRepository()
|
||||
if nodeCfg.ProviderAccessId != "" {
|
||||
access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err)
|
||||
} else {
|
||||
options.ProviderAccessConfig = access.Config
|
||||
}
|
||||
|
||||
rs = append(rs, deployer)
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, deployConfig domain.DeployConfig) (Deployer, error) {
|
||||
access, err := app.GetApp().Dao().FindRecordById("access", deployConfig.Access)
|
||||
deployer, err := createSSLDeployerProvider(options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("access record not found: %w", err)
|
||||
}
|
||||
|
||||
option := &DeployerOption{
|
||||
DomainId: record.Id,
|
||||
Domain: record.GetString("domain"),
|
||||
Access: access.GetString("config"),
|
||||
AccessRecord: access,
|
||||
DeployConfig: deployConfig,
|
||||
}
|
||||
if cert != nil {
|
||||
option.Certificate = *cert
|
||||
return nil, err
|
||||
} else {
|
||||
option.Certificate = applicant.Certificate{
|
||||
Certificate: record.GetString("certificate"),
|
||||
PrivateKey: record.GetString("privateKey"),
|
||||
}
|
||||
deployer.SetLogger(config.Logger)
|
||||
}
|
||||
|
||||
switch deployConfig.Type {
|
||||
case targetAliyunOSS:
|
||||
return NewAliyunOSSDeployer(option)
|
||||
case targetAliyunCDN:
|
||||
return NewAliyunCDNDeployer(option)
|
||||
case targetAliyunESA:
|
||||
return NewAliyunESADeployer(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 targetQiniuCdn:
|
||||
return NewQiniuCDNDeployer(option)
|
||||
case targetLocal:
|
||||
return NewLocalDeployer(option)
|
||||
case targetSSH:
|
||||
return NewSSHDeployer(option)
|
||||
case targetWebhook:
|
||||
return NewWebhookDeployer(option)
|
||||
case targetK8sSecret:
|
||||
return NewK8sSecretDeployer(option)
|
||||
}
|
||||
return nil, errors.New("unsupported deploy target")
|
||||
return &deployerImpl{
|
||||
provider: deployer,
|
||||
certPEM: config.CertificatePEM,
|
||||
privkeyPEM: config.PrivateKeyPEM,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toStr(tag string, data any) string {
|
||||
if data == nil {
|
||||
return tag
|
||||
}
|
||||
byts, _ := json.Marshal(data)
|
||||
return tag + ":" + string(byts)
|
||||
type deployerImpl struct {
|
||||
provider core.SSLDeployer
|
||||
certPEM string
|
||||
privkeyPEM string
|
||||
}
|
||||
|
||||
func getDeployString(conf domain.DeployConfig, key string) string {
|
||||
if _, ok := conf.Config[key]; !ok {
|
||||
return ""
|
||||
}
|
||||
var _ Deployer = (*deployerImpl)(nil)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func convertPEMToPFX(certificate string, privateKey string, password string) ([]byte, error) {
|
||||
cert, err := x509.ParseCertificateFromPEM(certificate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privkey, err := x509.ParsePKCS1PrivateKeyFromPEM(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode as pfx %w", err)
|
||||
}
|
||||
|
||||
return pfxData, nil
|
||||
}
|
||||
|
||||
func convertPEMToJKS(certificate string, privateKey string, alias string, keypass string, storepass string) ([]byte, error) {
|
||||
certBlock, _ := pem.Decode([]byte(certificate))
|
||||
if certBlock == nil {
|
||||
return nil, errors.New("failed to decode certificate PEM")
|
||||
}
|
||||
|
||||
privkeyBlock, _ := pem.Decode([]byte(privateKey))
|
||||
if privkeyBlock == nil {
|
||||
return nil, errors.New("failed to decode private key PEM")
|
||||
}
|
||||
|
||||
ks := keystore.New()
|
||||
entry := keystore.PrivateKeyEntry{
|
||||
CreationTime: time.Now(),
|
||||
PrivateKey: privkeyBlock.Bytes,
|
||||
CertificateChain: []keystore.Certificate{
|
||||
{
|
||||
Type: "X509",
|
||||
Content: certBlock.Bytes,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := ks.SetPrivateKeyEntry(alias, entry, []byte(keypass)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := ks.Store(&buf, []byte(storepass)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
func (d *deployerImpl) Deploy(ctx context.Context) error {
|
||||
_, err := d.provider.Deploy(ctx, d.certPEM, d.privkeyPEM)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/cast"
|
||||
)
|
||||
|
||||
type HuaweiCloudCDNDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
|
||||
sdkClient *hcCdn.CdnClient
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewHuaweiCloudCDNDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.HuaweiCloudAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := (&HuaweiCloudCDNDeployer{}).createSdkClient(
|
||||
access.AccessKeyId,
|
||||
access.SecretAccessKey,
|
||||
option.DeployConfig.GetConfigAsString("region"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: SCM 服务与 DNS 服务所支持的区域可能不一致,这里暂时不传而是使用默认值,仅支持华为云国内版
|
||||
uploader, err := uploader.NewHuaweiCloudSCMUploader(&uploader.HuaweiCloudSCMUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: "",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HuaweiCloudCDNDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudCDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudCDNDeployer) GetInfo() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error {
|
||||
// 查询加速域名配置
|
||||
// REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html
|
||||
showDomainFullConfigReq := &hcCdnModel.ShowDomainFullConfigRequest{
|
||||
DomainName: d.option.DeployConfig.GetConfigAsString("domain"),
|
||||
}
|
||||
showDomainFullConfigResp, err := d.sdkClient.ShowDomainFullConfig(showDomainFullConfigReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已查询到加速域名配置", showDomainFullConfigResp))
|
||||
|
||||
// 更新加速域名配置
|
||||
// REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html
|
||||
// REF: https://support.huaweicloud.com/usermanual-cdn/cdn_01_0306.html
|
||||
updateDomainMultiCertificatesReqBodyContent := &huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent{}
|
||||
updateDomainMultiCertificatesReqBodyContent.DomainName = d.option.DeployConfig.GetConfigAsString("domain")
|
||||
updateDomainMultiCertificatesReqBodyContent.HttpsSwitch = 1
|
||||
var updateDomainMultiCertificatesResp *hcCdnModel.UpdateDomainMultiCertificatesResponse
|
||||
if d.option.DeployConfig.GetConfigAsBool("useSCM") {
|
||||
// 上传证书到 SCM
|
||||
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
|
||||
|
||||
updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(2)
|
||||
updateDomainMultiCertificatesReqBodyContent.SCMCertificateId = cast.StringPtr(uploadResult.CertId)
|
||||
updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(uploadResult.CertName)
|
||||
} else {
|
||||
updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(0)
|
||||
updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(fmt.Sprintf("certimate-%d", time.Now().UnixMilli()))
|
||||
updateDomainMultiCertificatesReqBodyContent.Certificate = cast.StringPtr(d.option.Certificate.Certificate)
|
||||
updateDomainMultiCertificatesReqBodyContent.PrivateKey = cast.StringPtr(d.option.Certificate.PrivateKey)
|
||||
}
|
||||
updateDomainMultiCertificatesReqBodyContent = mergeHuaweiCloudCDNConfig(showDomainFullConfigResp.Configs, updateDomainMultiCertificatesReqBodyContent)
|
||||
updateDomainMultiCertificatesReq := &huaweicloudCDNUpdateDomainMultiCertificatesRequest{
|
||||
Body: &huaweicloudCDNUpdateDomainMultiCertificatesRequestBody{
|
||||
Https: updateDomainMultiCertificatesReqBodyContent,
|
||||
},
|
||||
}
|
||||
updateDomainMultiCertificatesResp, err = executeHuaweiCloudCDNUploadDomainMultiCertificates(d.sdkClient, updateDomainMultiCertificatesReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已更新加速域名配置", updateDomainMultiCertificatesResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcCdn.CdnClient, 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 := hcCdn.NewCdnClient(hcClient)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
type huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent struct {
|
||||
hcCdnModel.UpdateDomainMultiCertificatesRequestBodyContent `json:",inline"`
|
||||
|
||||
SCMCertificateId *string `json:"scm_certificate_id,omitempty"`
|
||||
}
|
||||
|
||||
type huaweicloudCDNUpdateDomainMultiCertificatesRequestBody struct {
|
||||
Https *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent `json:"https,omitempty"`
|
||||
}
|
||||
|
||||
type huaweicloudCDNUpdateDomainMultiCertificatesRequest struct {
|
||||
Body *huaweicloudCDNUpdateDomainMultiCertificatesRequestBody `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
func executeHuaweiCloudCDNUploadDomainMultiCertificates(client *hcCdn.CdnClient, request *huaweicloudCDNUpdateDomainMultiCertificatesRequest) (*hcCdnModel.UpdateDomainMultiCertificatesResponse, error) {
|
||||
// 华为云官方 SDK 中目前提供的字段缺失,这里暂时先需自定义请求
|
||||
// 可能需要等之后 SDK 更新
|
||||
|
||||
requestDef := hcCdn.GenReqDefForUpdateDomainMultiCertificates()
|
||||
|
||||
if resp, err := client.HcClient.Sync(request, requestDef); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return resp.(*hcCdnModel.UpdateDomainMultiCertificatesResponse), nil
|
||||
}
|
||||
}
|
||||
|
||||
func mergeHuaweiCloudCDNConfig(src *hcCdnModel.ConfigsGetBody, dest *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent) *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent {
|
||||
if src == nil {
|
||||
return dest
|
||||
}
|
||||
|
||||
// 华为云 API 中不传的字段表示使用默认值、而非保留原值,因此这里需要把原配置中的参数重新赋值回去
|
||||
// 而且蛋疼的是查询接口返回的数据结构和更新接口传入的参数结构不一致,需要做很多转化
|
||||
|
||||
if *src.OriginProtocol == "follow" {
|
||||
dest.AccessOriginWay = cast.Int32Ptr(1)
|
||||
} else if *src.OriginProtocol == "http" {
|
||||
dest.AccessOriginWay = cast.Int32Ptr(2)
|
||||
} else if *src.OriginProtocol == "https" {
|
||||
dest.AccessOriginWay = cast.Int32Ptr(3)
|
||||
}
|
||||
|
||||
if src.ForceRedirect != nil {
|
||||
dest.ForceRedirectConfig = &hcCdnModel.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" {
|
||||
dest.Http2 = cast.Int32Ptr(1)
|
||||
}
|
||||
}
|
||||
|
||||
return dest
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
"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, err
|
||||
}
|
||||
|
||||
client, err := (&HuaweiCloudELBDeployer{}).createSdkClient(
|
||||
access.AccessKeyId,
|
||||
access.SecretAccessKey,
|
||||
option.DeployConfig.GetConfigAsString("region"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uploader, err := uploader.NewHuaweiCloudELBUploader(&uploader.HuaweiCloudELBUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: option.DeployConfig.GetConfigAsString("region"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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) GetInfo() []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)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
request := &hcIamModel.KeystoneListProjectsRequest{
|
||||
Name: ®ion,
|
||||
}
|
||||
response, err := client.KeystoneListProjects(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if response.Projects == nil || len(*response.Projects) == 0 {
|
||||
return "", fmt.Errorf("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 fmt.Errorf("failed to execute sdk request 'elb.UpdateCertificate': %w", err)
|
||||
}
|
||||
|
||||
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 fmt.Errorf("failed to execute sdk request 'elb.ShowLoadBalancer': %w", err)
|
||||
}
|
||||
|
||||
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 fmt.Errorf("failed to execute sdk request 'elb.ListListeners': %w", err)
|
||||
}
|
||||
|
||||
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
|
||||
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
|
||||
|
||||
// 批量更新监听器证书
|
||||
var errs []error
|
||||
for _, hcListenerId := range hcListenerIds {
|
||||
if err := d.updateListenerCertificate(ctx, hcListenerId, uploadResult.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
|
||||
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
|
||||
|
||||
// 更新监听器证书
|
||||
if err := d.updateListenerCertificate(ctx, hcListenerId, uploadResult.CertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) updateListenerCertificate(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 fmt.Errorf("failed to execute sdk request 'elb.ShowListener': %w", err)
|
||||
}
|
||||
|
||||
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 fmt.Errorf("failed to execute sdk request 'elb.ListCertificates': %w", err)
|
||||
}
|
||||
|
||||
showNewCertificateReq := &hcElbModel.ShowCertificateRequest{
|
||||
CertificateId: hcCertId,
|
||||
}
|
||||
showNewCertificateResp, err := d.sdkClient.ShowCertificate(showNewCertificateReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute sdk request 'elb.ShowCertificate': %w", err)
|
||||
}
|
||||
|
||||
for _, certificate := range *listOldCertificateResp.Certificates {
|
||||
oldCertificate := certificate
|
||||
newCertificate := showNewCertificateResp.Certificate
|
||||
|
||||
if oldCertificate.SubjectAlternativeNames != nil && newCertificate.SubjectAlternativeNames != nil {
|
||||
oldCertificateSans := oldCertificate.SubjectAlternativeNames
|
||||
newCertificateSans := newCertificate.SubjectAlternativeNames
|
||||
sort.Strings(*oldCertificateSans)
|
||||
sort.Strings(*newCertificateSans)
|
||||
if strings.Join(*oldCertificateSans, ";") == strings.Join(*newCertificateSans, ";") {
|
||||
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 fmt.Errorf("failed to execute sdk request 'elb.UpdateListener': %w", err)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已更新 ELB 监听器", updateListenerResp))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8sMetaV1 "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
|
||||
}
|
||||
|
||||
func NewK8sSecretDeployer(option *DeployerOption) (Deployer, error) {
|
||||
return &K8sSecretDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *K8sSecretDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *K8sSecretDeployer) GetInfo() []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 create success.", nil))
|
||||
|
||||
namespace := getDeployString(d.option.DeployConfig, "namespace")
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
|
||||
secretName := getDeployString(d.option.DeployConfig, "secretName")
|
||||
if secretName == "" {
|
||||
return fmt.Errorf("k8s secret name is empty")
|
||||
}
|
||||
|
||||
secretDataKeyForCrt := getDeployString(d.option.DeployConfig, "secretDataKeyForCrt")
|
||||
if secretDataKeyForCrt == "" {
|
||||
namespace = "tls.crt"
|
||||
}
|
||||
|
||||
secretDataKeyForKey := getDeployString(d.option.DeployConfig, "secretDataKeyForKey")
|
||||
if secretDataKeyForKey == "" {
|
||||
namespace = "tls.key"
|
||||
}
|
||||
|
||||
certificate, err := x509.ParseCertificateFromPEM(d.option.Certificate.Certificate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse certificate: %w", err)
|
||||
}
|
||||
|
||||
secretPayload := corev1.Secret{
|
||||
TypeMeta: k8sMetaV1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: k8sMetaV1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Annotations: map[string]string{
|
||||
"certimate/domains": d.option.Domain,
|
||||
"certimate/alt-names": strings.Join(certificate.DNSNames, ","),
|
||||
"certimate/common-name": certificate.Subject.CommonName,
|
||||
"certimate/issuer-organization": strings.Join(certificate.Issuer.Organization, ","),
|
||||
},
|
||||
},
|
||||
Type: corev1.SecretType("kubernetes.io/tls"),
|
||||
}
|
||||
|
||||
secretPayload.Data = make(map[string][]byte)
|
||||
secretPayload.Data[secretDataKeyForCrt] = []byte(d.option.Certificate.Certificate)
|
||||
secretPayload.Data[secretDataKeyForKey] = []byte(d.option.Certificate.PrivateKey)
|
||||
|
||||
// 获取 Secret 实例
|
||||
_, err = client.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, k8sMetaV1.GetOptions{})
|
||||
if err != nil {
|
||||
_, err = client.CoreV1().Secrets(namespace).Create(context.TODO(), &secretPayload, k8sMetaV1.CreateOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create k8s secret: %w", err)
|
||||
} else {
|
||||
d.infos = append(d.infos, toStr("Certificate has been created in K8s Secret", nil))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 Secret 实例
|
||||
_, err = client.CoreV1().Secrets(namespace).Update(context.TODO(), &secretPayload, k8sMetaV1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update k8s secret: %w", err)
|
||||
}
|
||||
|
||||
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) {
|
||||
var config *rest.Config
|
||||
var err error
|
||||
if access.KubeConfig == "" {
|
||||
config, err = rest.InClusterConfig()
|
||||
} else {
|
||||
kubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(access.KubeConfig))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err = kubeConfig.ClientConfig()
|
||||
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/fs"
|
||||
)
|
||||
|
||||
type LocalDeployer struct {
|
||||
option *DeployerOption
|
||||
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,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *LocalDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *LocalDeployer) GetInfo() []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 := d.option.DeployConfig.GetConfigAsString("preCommand")
|
||||
if preCommand != "" {
|
||||
stdout, stderr, err := d.execCommand(preCommand)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("执行前置命令成功", stdout))
|
||||
}
|
||||
|
||||
// 写入证书和私钥文件
|
||||
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 fmt.Errorf("failed to save certificate file: %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 fmt.Errorf("failed to save private key file: %w", err)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("保存私钥成功", nil))
|
||||
|
||||
case certFormatPFX:
|
||||
pfxData, err := convertPEMToPFX(
|
||||
d.option.Certificate.Certificate,
|
||||
d.option.Certificate.PrivateKey,
|
||||
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert pem to pfx %w", err)
|
||||
}
|
||||
|
||||
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil {
|
||||
return fmt.Errorf("failed to save certificate file: %w", err)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("保存证书成功", nil))
|
||||
|
||||
case certFormatJKS:
|
||||
jksData, err := convertPEMToJKS(
|
||||
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 fmt.Errorf("failed to convert pem to pfx %w", err)
|
||||
}
|
||||
|
||||
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
|
||||
return fmt.Errorf("failed to save certificate file: %w", err)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("保存证书成功", nil))
|
||||
}
|
||||
|
||||
// 执行命令
|
||||
command := d.option.DeployConfig.GetConfigAsString("command")
|
||||
if command != "" {
|
||||
stdout, stderr, err := d.execCommand(command)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("执行命令成功", stdout))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *LocalDeployer) execCommand(command string) (string, string, error) {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
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 "", "", fmt.Errorf("unsupported shell")
|
||||
}
|
||||
|
||||
var stdoutBuf bytes.Buffer
|
||||
cmd.Stdout = &stdoutBuf
|
||||
var stderrBuf bytes.Buffer
|
||||
cmd.Stderr = &stderrBuf
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to execute script: %w", err)
|
||||
}
|
||||
|
||||
return stdoutBuf.String(), stderrBuf.String(), err
|
||||
}
|
||||
1483
internal/deployer/providers.go
Normal file
1483
internal/deployer/providers.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,214 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/qiniu/go-sdk/v7/auth"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
xhttp "github.com/usual2970/certimate/internal/utils/http"
|
||||
)
|
||||
|
||||
const qiniuGateway = "http://api.qiniu.com"
|
||||
|
||||
type QiniuCDNDeployer struct {
|
||||
option *DeployerOption
|
||||
info []string
|
||||
credentials *auth.Credentials
|
||||
}
|
||||
|
||||
func NewQiniuCDNDeployer(option *DeployerOption) (*QiniuCDNDeployer, error) {
|
||||
access := &domain.QiniuAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
|
||||
return &QiniuCDNDeployer{
|
||||
option: option,
|
||||
info: make([]string, 0),
|
||||
|
||||
credentials: auth.New(access.AccessKey, access.SecretKey),
|
||||
}, nil
|
||||
}
|
||||
|
||||
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) Deploy(ctx context.Context) error {
|
||||
// 上传证书
|
||||
certId, err := d.uploadCert()
|
||||
if err != nil {
|
||||
return fmt.Errorf("uploadCert failed: %w", err)
|
||||
}
|
||||
|
||||
// 获取域名信息
|
||||
domainInfo, err := d.getDomainInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getDomainInfo failed: %w", err)
|
||||
}
|
||||
|
||||
// 判断域名是否启用 https
|
||||
if domainInfo.Https != nil && domainInfo.Https.CertID != "" {
|
||||
// 启用了 https
|
||||
// 修改域名证书
|
||||
err = d.modifyDomainCert(certId, domainInfo.Https.ForceHttps, domainInfo.Https.Http2Enable)
|
||||
if err != nil {
|
||||
return fmt.Errorf("modifyDomainCert failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
// 没启用 https
|
||||
// 启用 https
|
||||
err = d.enableHttps(certId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("enableHttps failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *QiniuCDNDeployer) enableHttps(certId string) error {
|
||||
domain := d.option.DeployConfig.GetDomain()
|
||||
path := fmt.Sprintf("/domain/%s/sslize", 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) {
|
||||
domain := d.option.DeployConfig.GetDomain()
|
||||
|
||||
path := fmt.Sprintf("/domain/%s", 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, forceHttps, http2Enable bool) error {
|
||||
domain := d.option.DeployConfig.GetDomain()
|
||||
path := fmt.Sprintf("/domain/%s/httpsconf", domain)
|
||||
|
||||
body := &qiniuModifyDomainCertReq{
|
||||
CertID: certId,
|
||||
ForceHttps: forceHttps,
|
||||
Http2Enable: http2Enable,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,85 +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",
|
||||
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",
|
||||
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, true, true); (err != nil) != tt.wantErr {
|
||||
t.Errorf("qiuniu.modifyDomainCert() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/fs"
|
||||
)
|
||||
|
||||
type SSHDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewSSHDeployer(option *DeployerOption) (Deployer, error) {
|
||||
return &SSHDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *SSHDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *SSHDeployer) GetInfo() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *SSHDeployer) Deploy(ctx context.Context) error {
|
||||
access := &domain.SSHAccess{}
|
||||
if err := json.Unmarshal([]byte(d.option.Access), access); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 连接
|
||||
client, err := d.createSshClient(access)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
d.infos = append(d.infos, toStr("SSH 连接成功", nil))
|
||||
|
||||
// 执行前置命令
|
||||
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)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("SSH 执行前置命令成功", stdout))
|
||||
}
|
||||
|
||||
// 上传证书和私钥文件
|
||||
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 fmt.Errorf("failed to upload certificate file: %w", 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 fmt.Errorf("failed to upload private key file: %w", err)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("SSH 上传私钥成功", nil))
|
||||
|
||||
case certFormatPFX:
|
||||
pfxData, err := convertPEMToPFX(
|
||||
d.option.Certificate.Certificate,
|
||||
d.option.Certificate.PrivateKey,
|
||||
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert pem to pfx %w", err)
|
||||
}
|
||||
|
||||
if err := d.writeSftpFile(client, d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil {
|
||||
return fmt.Errorf("failed to upload certificate file: %w", err)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
|
||||
|
||||
case certFormatJKS:
|
||||
jksData, err := convertPEMToJKS(
|
||||
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 fmt.Errorf("failed to convert pem to pfx %w", err)
|
||||
}
|
||||
|
||||
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
|
||||
return fmt.Errorf("failed to save certificate file: %w", err)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("保存证书成功", nil))
|
||||
}
|
||||
|
||||
// 执行命令
|
||||
command := d.option.DeployConfig.GetConfigAsString("command")
|
||||
if command != "" {
|
||||
stdout, stderr, err := d.sshExecCommand(client, command)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("SSH 执行命令成功", stdout))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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(client *ssh.Client, command string) (string, string, error) {
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create ssh session: %w", err)
|
||||
}
|
||||
|
||||
defer session.Close()
|
||||
var stdoutBuf bytes.Buffer
|
||||
session.Stdout = &stdoutBuf
|
||||
var stderrBuf bytes.Buffer
|
||||
session.Stderr = &stderrBuf
|
||||
err = session.Run(command)
|
||||
return stdoutBuf.String(), stderrBuf.String(), err
|
||||
}
|
||||
|
||||
func (d *SSHDeployer) writeSftpFileString(client *ssh.Client, path string, content string) error {
|
||||
return d.writeSftpFile(client, path, []byte(content))
|
||||
}
|
||||
|
||||
func (d *SSHDeployer) writeSftpFile(client *ssh.Client, path string, data []byte) error {
|
||||
sftpCli, err := sftp.NewClient(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create sftp client: %w", err)
|
||||
}
|
||||
defer sftpCli.Close()
|
||||
|
||||
if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil {
|
||||
return fmt.Errorf("failed to create remote directory: %w", err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to remote file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPath(t *testing.T) {
|
||||
dir := path.Dir("./a/b/c")
|
||||
os.MkdirAll(dir, 0o755)
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
cdn "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"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/utils/rand"
|
||||
)
|
||||
|
||||
type TencentCDNDeployer struct {
|
||||
option *DeployerOption
|
||||
credential *common.Credential
|
||||
infos []string
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
credential := common.NewCredential(
|
||||
access.SecretId,
|
||||
access.SecretKey,
|
||||
)
|
||||
|
||||
return &TencentCDNDeployer{
|
||||
option: option,
|
||||
credential: credential,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentCDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *TencentCDNDeployer) GetInfo() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *TencentCDNDeployer) Deploy(ctx context.Context) error {
|
||||
// 上传证书
|
||||
certId, err := d.uploadCert()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
d.infos = append(d.infos, toStr("上传证书", certId))
|
||||
|
||||
if err := d.deploy(certId); err != nil {
|
||||
return fmt.Errorf("failed to deploy: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentCDNDeployer) uploadCert() (string, error) {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
|
||||
return *response.Response.CertificateId, 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(certId)
|
||||
if errGetList != nil {
|
||||
return fmt.Errorf("failed to get certificate domain list: %w", errGetList)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
d.infos = append(d.infos, "没有需要部署的实例")
|
||||
return nil
|
||||
}
|
||||
request.InstanceIdList = common.StringPtrs(list)
|
||||
} else { // 否则直接使用传入的域名
|
||||
deployed, _ := d.isDomainDeployed(certId, domain)
|
||||
if deployed {
|
||||
d.infos = append(d.infos, "域名已部署")
|
||||
return nil
|
||||
} else {
|
||||
request.InstanceIdList = common.StringPtrs([]string{domain})
|
||||
}
|
||||
}
|
||||
|
||||
// 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
|
||||
resp, err := client.DeployCertificateInstance(request)
|
||||
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(certId string) ([]string, error) {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "cdn.tencentcloudapi.com"
|
||||
client, _ := cdn.NewClient(d.credential, "", cpf)
|
||||
|
||||
request := cdn.NewDescribeCertDomainsRequest()
|
||||
|
||||
request.CertId = common.StringPtr(certId)
|
||||
|
||||
response, err := client.DescribeCertDomains(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get domain list: %w", err)
|
||||
}
|
||||
|
||||
deployedDomains, err := d.getDeployedDomainList(certId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get deployed domain list: %w", err)
|
||||
}
|
||||
|
||||
domains := make([]string, 0)
|
||||
for _, domain := range response.Response.Domains {
|
||||
domainStr := *domain
|
||||
if !slices.Contains(deployedDomains, domainStr) {
|
||||
domains = append(domains, domainStr)
|
||||
}
|
||||
}
|
||||
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
func (d *TencentCDNDeployer) isDomainDeployed(certId, domain string) (bool, error) {
|
||||
deployedDomains, err := d.getDeployedDomainList(certId)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return slices.Contains(deployedDomains, domain), nil
|
||||
}
|
||||
|
||||
func (d *TencentCDNDeployer) getDeployedDomainList(certId string) ([]string, error) {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
client, _ := ssl.NewClient(d.credential, "", cpf)
|
||||
|
||||
request := ssl.NewDescribeDeployedResourcesRequest()
|
||||
request.CertificateIds = common.StringPtrs([]string{certId})
|
||||
request.ResourceType = common.StringPtr("cdn")
|
||||
|
||||
response, err := client.DescribeDeployedResources(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get deployed domain list: %w", err)
|
||||
}
|
||||
|
||||
domains := make([]string, 0)
|
||||
for _, domain := range response.Response.DeployedResources[0].Resources {
|
||||
domains = append(domains, *domain)
|
||||
}
|
||||
|
||||
return domains, nil
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"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"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/utils/rand"
|
||||
)
|
||||
|
||||
type TencentCLBDeployer struct {
|
||||
option *DeployerOption
|
||||
credential *common.Credential
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewTencentCLBDeployer(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)
|
||||
}
|
||||
|
||||
credential := common.NewCredential(
|
||||
access.SecretId,
|
||||
access.SecretKey,
|
||||
)
|
||||
|
||||
return &TencentCLBDeployer{
|
||||
option: option,
|
||||
credential: credential,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentCLBDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *TencentCLBDeployer) GetInfo() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *TencentCLBDeployer) Deploy(ctx context.Context) error {
|
||||
// 上传证书
|
||||
certId, err := d.uploadCert()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
d.infos = append(d.infos, toStr("上传证书", certId))
|
||||
|
||||
if err := d.deploy(certId); err != nil {
|
||||
return fmt.Errorf("failed to deploy: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentCLBDeployer) uploadCert() (string, error) {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
|
||||
return *response.Response.CertificateId, nil
|
||||
}
|
||||
|
||||
func (d *TencentCLBDeployer) deploy(certId string) error {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
// 实例化要请求产品的client对象,clientProfile是可选的
|
||||
client, _ := ssl.NewClient(d.credential, getDeployString(d.option.DeployConfig, "region"), cpf)
|
||||
|
||||
// 实例化一个请求对象,每个接口都会对应一个request对象
|
||||
request := ssl.NewDeployCertificateInstanceRequest()
|
||||
|
||||
request.CertificateId = common.StringPtr(certId)
|
||||
request.ResourceType = common.StringPtr("clb")
|
||||
request.Status = common.Int64Ptr(1)
|
||||
|
||||
clbId := getDeployString(d.option.DeployConfig, "clbId")
|
||||
lsnId := getDeployString(d.option.DeployConfig, "lsnId")
|
||||
domain := getDeployString(d.option.DeployConfig, "domain")
|
||||
|
||||
if(domain == ""){
|
||||
// 未开启SNI,只需要精确到监听器
|
||||
request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s", clbId, lsnId)})
|
||||
}else{
|
||||
// 开启SNI,需要精确到域名,支持泛域名
|
||||
request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", clbId, lsnId, domain)})
|
||||
}
|
||||
|
||||
|
||||
// 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
|
||||
resp, err := client.DeployCertificateInstance(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy certificate: %w", err)
|
||||
}
|
||||
d.infos = append(d.infos, toStr("部署证书", resp.Response))
|
||||
return nil
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"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"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/utils/rand"
|
||||
)
|
||||
|
||||
type TencentCOSDeployer struct {
|
||||
option *DeployerOption
|
||||
credential *common.Credential
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewTencentCOSDeployer(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)
|
||||
}
|
||||
|
||||
credential := common.NewCredential(
|
||||
access.SecretId,
|
||||
access.SecretKey,
|
||||
)
|
||||
|
||||
return &TencentCOSDeployer{
|
||||
option: option,
|
||||
credential: credential,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentCOSDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *TencentCOSDeployer) GetInfo() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *TencentCOSDeployer) Deploy(ctx context.Context) error {
|
||||
// 上传证书
|
||||
certId, err := d.uploadCert()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
d.infos = append(d.infos, toStr("上传证书", certId))
|
||||
|
||||
if err := d.deploy(certId); err != nil {
|
||||
return fmt.Errorf("failed to deploy: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 上传证书,与CDN部署的上传方法一致。
|
||||
func (d *TencentCOSDeployer) uploadCert() (string, error) {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
|
||||
return *response.Response.CertificateId, nil
|
||||
}
|
||||
|
||||
func (d *TencentCOSDeployer) deploy(certId string) error {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
// 实例化要请求产品的client对象,clientProfile是可选的
|
||||
client, _ := ssl.NewClient(d.credential, getDeployString(d.option.DeployConfig, "region"), cpf)
|
||||
|
||||
// 实例化一个请求对象,每个接口都会对应一个request对象
|
||||
request := ssl.NewDeployCertificateInstanceRequest()
|
||||
|
||||
request.CertificateId = common.StringPtr(certId)
|
||||
request.ResourceType = common.StringPtr("cos")
|
||||
request.Status = common.Int64Ptr(1)
|
||||
|
||||
domain := getDeployString(d.option.DeployConfig, "domain")
|
||||
request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s#%s#%s", getDeployString(d.option.DeployConfig, "region"), getDeployString(d.option.DeployConfig, "bucket"), domain)})
|
||||
|
||||
// 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
|
||||
resp, err := client.DeployCertificateInstance(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy certificate: %w", err)
|
||||
}
|
||||
d.infos = append(d.infos, toStr("部署证书", resp.Response))
|
||||
return nil
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
cdn "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"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/utils/rand"
|
||||
)
|
||||
|
||||
type TencentECDNDeployer struct {
|
||||
option *DeployerOption
|
||||
credential *common.Credential
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewTencentECDNDeployer(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)
|
||||
}
|
||||
|
||||
credential := common.NewCredential(
|
||||
access.SecretId,
|
||||
access.SecretKey,
|
||||
)
|
||||
|
||||
return &TencentECDNDeployer{
|
||||
option: option,
|
||||
credential: credential,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentECDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *TencentECDNDeployer) GetInfo() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *TencentECDNDeployer) Deploy(ctx context.Context) error {
|
||||
// 上传证书
|
||||
certId, err := d.uploadCert()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
d.infos = append(d.infos, toStr("上传证书", certId))
|
||||
|
||||
if err := d.deploy(certId); err != nil {
|
||||
return fmt.Errorf("failed to deploy: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentECDNDeployer) uploadCert() (string, error) {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
|
||||
return *response.Response.CertificateId, nil
|
||||
}
|
||||
|
||||
func (d *TencentECDNDeployer) 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("ecdn")
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy certificate: %w", err)
|
||||
}
|
||||
d.infos = append(d.infos, toStr("部署证书", resp.Response))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentECDNDeployer) 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
|
||||
request.Product = common.StringPtr("ecdn")
|
||||
|
||||
response, err := client.DescribeCertDomains(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get domain list: %w", err)
|
||||
}
|
||||
|
||||
domains := make([]string, 0)
|
||||
for _, domain := range response.Response.Domains {
|
||||
domains = append(domains, *domain)
|
||||
}
|
||||
|
||||
return domains, nil
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
teo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901"
|
||||
"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"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/utils/rand"
|
||||
)
|
||||
|
||||
type TencentTEODeployer struct {
|
||||
option *DeployerOption
|
||||
credential *common.Credential
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewTencentTEODeployer(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)
|
||||
}
|
||||
|
||||
credential := common.NewCredential(
|
||||
access.SecretId,
|
||||
access.SecretKey,
|
||||
)
|
||||
|
||||
return &TencentTEODeployer{
|
||||
option: option,
|
||||
credential: credential,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentTEODeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *TencentTEODeployer) GetInfo() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *TencentTEODeployer) Deploy(ctx context.Context) error {
|
||||
// 上传证书
|
||||
certId, err := d.uploadCert()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
d.infos = append(d.infos, toStr("上传证书", certId))
|
||||
|
||||
if err := d.deploy(certId); err != nil {
|
||||
return fmt.Errorf("failed to deploy: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentTEODeployer) uploadCert() (string, error) {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
|
||||
return *response.Response.CertificateId, nil
|
||||
}
|
||||
|
||||
func (d *TencentTEODeployer) deploy(certId string) error {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "teo.tencentcloudapi.com"
|
||||
// 实例化要请求产品的client对象,clientProfile是可选的
|
||||
client, _ := teo.NewClient(d.credential, "", cpf)
|
||||
|
||||
// 实例化一个请求对象,每个接口都会对应一个request对象
|
||||
request := teo.NewModifyHostsCertificateRequest()
|
||||
|
||||
request.ZoneId = common.StringPtr(getDeployString(d.option.DeployConfig, "zoneId"))
|
||||
request.Mode = common.StringPtr("sslcert")
|
||||
request.ServerCertInfo = []*teo.ServerCertInfo{{
|
||||
CertId: common.StringPtr(certId),
|
||||
}}
|
||||
|
||||
domains := strings.Split(strings.ReplaceAll(d.option.Domain, "\r\n", "\n"),"\n")
|
||||
request.Hosts = common.StringPtrs(domains)
|
||||
|
||||
// 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
|
||||
resp, err := client.ModifyHostsCertificate(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy certificate: %w", err)
|
||||
}
|
||||
d.infos = append(d.infos, toStr("部署证书", resp.Response))
|
||||
return nil
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
xhttp "github.com/usual2970/certimate/internal/utils/http"
|
||||
)
|
||||
|
||||
type WebhookDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewWebhookDeployer(option *DeployerOption) (Deployer, error) {
|
||||
return &WebhookDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *WebhookDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *WebhookDeployer) GetInfo() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
type webhookData struct {
|
||||
Domain string `json:"domain"`
|
||||
Certificate string `json:"certificate"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
data := &webhookData{
|
||||
Domain: d.option.Domain,
|
||||
Certificate: d.option.Certificate.Certificate,
|
||||
PrivateKey: d.option.Certificate.PrivateKey,
|
||||
Variables: getDeployVariables(d.option.DeployConfig),
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("webhook response", string(resp)))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,73 +1,414 @@
|
||||
package domain
|
||||
|
||||
type AliyunAccess struct {
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const CollectionNameAccess = "access"
|
||||
|
||||
type Access struct {
|
||||
Meta
|
||||
Name string `json:"name" db:"name"`
|
||||
Provider string `json:"provider" db:"provider"`
|
||||
Config map[string]any `json:"config" db:"config"`
|
||||
Reserve string `json:"reserve,omitempty" db:"reserve"`
|
||||
DeletedAt *time.Time `json:"deleted" db:"deleted"`
|
||||
}
|
||||
|
||||
type AccessConfigFor1Panel struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
ApiVersion string `json:"apiVersion"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForACMECA struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
EabKid string `json:"eabKid,omitempty"`
|
||||
EabHmacKey string `json:"eabHmacKey,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForACMEHttpReq struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForAliyun struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
ResourceGroupId string `json:"resourceGroupId,omitempty"`
|
||||
}
|
||||
|
||||
type TencentAccess struct {
|
||||
SecretId string `json:"secretId"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
type AccessConfigForAPISIX struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
}
|
||||
|
||||
type HuaweiCloudAccess struct {
|
||||
Region string `json:"region"`
|
||||
type AccessConfigForAWS struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
}
|
||||
|
||||
type AwsAccess struct {
|
||||
Region string `json:"region"`
|
||||
type AccessConfigForAzure struct {
|
||||
TenantId string `json:"tenantId"`
|
||||
ClientId string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
CloudName string `json:"cloudName,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForBaiduCloud struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
HostedZoneId string `json:"hostedZoneId"`
|
||||
}
|
||||
|
||||
type CloudflareAccess struct {
|
||||
DnsApiToken string `json:"dnsApiToken"`
|
||||
type AccessConfigForBaishan struct {
|
||||
ApiToken string `json:"apiToken"`
|
||||
}
|
||||
|
||||
type QiniuAccess struct {
|
||||
type AccessConfigForBaotaPanel struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForBaotaWAF struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForBytePlus struct {
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type NameSiloAccess struct {
|
||||
type AccessConfigForBunny struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
type GodaddyAccess struct {
|
||||
type AccessConfigForCacheFly struct {
|
||||
ApiToken string `json:"apiToken"`
|
||||
}
|
||||
|
||||
type AccessConfigForCdnfly struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
ApiSecret string `json:"apiSecret"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForCloudflare struct {
|
||||
DnsApiToken string `json:"dnsApiToken"`
|
||||
ZoneApiToken string `json:"zoneApiToken,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForClouDNS struct {
|
||||
AuthId string `json:"authId"`
|
||||
AuthPassword string `json:"authPassword"`
|
||||
}
|
||||
|
||||
type AccessConfigForCMCCCloud struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
}
|
||||
|
||||
type AccessConfigForConstellix struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForCTCCCloud struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForDeSEC struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type AccessConfigForDigitalOcean struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
}
|
||||
|
||||
type AccessConfigForDingTalkBot struct {
|
||||
WebhookUrl string `json:"webhookUrl"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
type AccessConfigForDiscordBot struct {
|
||||
BotToken string `json:"botToken"`
|
||||
DefaultChannelId string `json:"defaultChannelId,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForDNSLA struct {
|
||||
ApiId string `json:"apiId"`
|
||||
ApiSecret string `json:"apiSecret"`
|
||||
}
|
||||
|
||||
type AccessConfigForDogeCloud struct {
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForDuckDNS struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type AccessConfigForDynv6 struct {
|
||||
HttpToken string `json:"httpToken"`
|
||||
}
|
||||
|
||||
type AccessConfigForEdgio struct {
|
||||
ClientId string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
}
|
||||
|
||||
type AccessConfigForEmail struct {
|
||||
SmtpHost string `json:"smtpHost"`
|
||||
SmtpPort int32 `json:"smtpPort"`
|
||||
SmtpTls bool `json:"smtpTls"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
DefaultSenderAddress string `json:"defaultSenderAddress,omitempty"`
|
||||
DefaultSenderName string `json:"defaultSenderName,omitempty"`
|
||||
DefaultReceiverAddress string `json:"defaultReceiverAddress,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForFlexCDN struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
ApiRole string `json:"apiRole"`
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForGcore struct {
|
||||
ApiToken string `json:"apiToken"`
|
||||
}
|
||||
|
||||
type AccessConfigForGname struct {
|
||||
AppId string `json:"appId"`
|
||||
AppKey string `json:"appKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForGoDaddy struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
ApiSecret string `json:"apiSecret"`
|
||||
}
|
||||
|
||||
type PdnsAccess struct {
|
||||
ApiUrl string `json:"apiUrl"`
|
||||
type AccessConfigForGoEdge struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
ApiRole string `json:"apiRole"`
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForGoogleTrustServices struct {
|
||||
EabKid string `json:"eabKid"`
|
||||
EabHmacKey string `json:"eabHmacKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForHetzner struct {
|
||||
ApiToken string `json:"apiToken"`
|
||||
}
|
||||
|
||||
type AccessConfigForHuaweiCloud struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForJDCloud struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
}
|
||||
|
||||
type AccessConfigForKubernetes struct {
|
||||
KubeConfig string `json:"kubeConfig,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForLarkBot struct {
|
||||
WebhookUrl string `json:"webhookUrl"`
|
||||
}
|
||||
|
||||
type AccessConfigForLeCDN struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
ApiVersion string `json:"apiVersion"`
|
||||
ApiRole string `json:"apiRole"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForMattermost struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
DefaultChannelId string `json:"defaultChannelId,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForNamecheap struct {
|
||||
Username string `json:"username"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForNameDotCom struct {
|
||||
Username string `json:"username"`
|
||||
ApiToken string `json:"apiToken"`
|
||||
}
|
||||
|
||||
type AccessConfigForNameSilo struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
type HttpreqAccess struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Mode string `json:"mode"`
|
||||
type AccessConfigForNetcup struct {
|
||||
CustomerNumber string `json:"customerNumber"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
ApiPassword string `json:"apiPassword"`
|
||||
}
|
||||
|
||||
type AccessConfigForNetlify struct {
|
||||
ApiToken string `json:"apiToken"`
|
||||
}
|
||||
|
||||
type AccessConfigForNS1 struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForPorkbun struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
SecretApiKey string `json:"secretApiKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForPowerDNS struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForProxmoxVE struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
ApiToken string `json:"apiToken"`
|
||||
ApiTokenSecret string `json:"apiTokenSecret,omitempty"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForQiniu struct {
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForRainYun struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForRatPanel struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
AccessTokenId int32 `json:"accessTokenId"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForSafeLine struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
ApiToken string `json:"apiToken"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForSlackBot struct {
|
||||
BotToken string `json:"botToken"`
|
||||
DefaultChannelId string `json:"defaultChannelId,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForSSH struct {
|
||||
Host string `json:"host"`
|
||||
Port int32 `json:"port"`
|
||||
AuthMethod string `json:"authMethod,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
KeyPassphrase string `json:"keyPassphrase,omitempty"`
|
||||
JumpServers []struct {
|
||||
Host string `json:"host"`
|
||||
Port int32 `json:"port"`
|
||||
AuthMethod string `json:"authMethod,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
KeyPassphrase string `json:"keyPassphrase,omitempty"`
|
||||
} `json:"jumpServers,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForSSLCom struct {
|
||||
EabKid string `json:"eabKid"`
|
||||
EabHmacKey string `json:"eabHmacKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForTelegramBot struct {
|
||||
BotToken string `json:"botToken"`
|
||||
DefaultChatId int64 `json:"defaultChatId,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForTencentCloud struct {
|
||||
SecretId string `json:"secretId"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForUCloud struct {
|
||||
PrivateKey string `json:"privateKey"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
ProjectId string `json:"projectId,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForUniCloud struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type LocalAccess struct{}
|
||||
|
||||
type SSHAccess struct {
|
||||
Host string `json:"host"`
|
||||
Port string `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Key string `json:"key"`
|
||||
KeyPassphrase string `json:"keyPassphrase"`
|
||||
type AccessConfigForUpyun struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type WebhookAccess struct {
|
||||
Url string `json:"url"`
|
||||
type AccessConfigForVercel struct {
|
||||
ApiAccessToken string `json:"apiAccessToken"`
|
||||
TeamId string `json:"teamId,omitempty"`
|
||||
}
|
||||
|
||||
type KubernetesAccess struct {
|
||||
KubeConfig string `json:"kubeConfig"`
|
||||
type AccessConfigForVolcEngine struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForWangsu struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForWebhook struct {
|
||||
Url string `json:"url"`
|
||||
Method string `json:"method,omitempty"`
|
||||
HeadersString string `json:"headers,omitempty"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
DefaultDataForDeployment string `json:"defaultDataForDeployment,omitempty"`
|
||||
DefaultDataForNotification string `json:"defaultDataForNotification,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForWeComBot struct {
|
||||
WebhookUrl string `json:"webhookUrl"`
|
||||
}
|
||||
|
||||
type AccessConfigForWestcn struct {
|
||||
Username string `json:"username"`
|
||||
ApiPassword string `json:"apiPassword"`
|
||||
}
|
||||
|
||||
type AccessConfigForZeroSSL struct {
|
||||
EabKid string `json:"eabKid"`
|
||||
EabHmacKey string `json:"eabHmacKey"`
|
||||
}
|
||||
|
||||
15
internal/domain/acme_account.go
Normal file
15
internal/domain/acme_account.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
)
|
||||
|
||||
const CollectionNameAcmeAccount = "acme_accounts"
|
||||
|
||||
type AcmeAccount struct {
|
||||
Meta
|
||||
CA string `json:"ca" db:"ca"`
|
||||
Email string `json:"email" db:"email"`
|
||||
Resource *registration.Resource `json:"resource" db:"resource"`
|
||||
Key string `json:"key" db:"key"`
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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
|
||||
}
|
||||
137
internal/domain/certificate.go
Normal file
137
internal/domain/certificate.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
|
||||
)
|
||||
|
||||
const CollectionNameCertificate = "certificate"
|
||||
|
||||
type Certificate struct {
|
||||
Meta
|
||||
Source CertificateSourceType `json:"source" db:"source"`
|
||||
SubjectAltNames string `json:"subjectAltNames" db:"subjectAltNames"`
|
||||
SerialNumber string `json:"serialNumber" db:"serialNumber"`
|
||||
Certificate string `json:"certificate" db:"certificate"`
|
||||
PrivateKey string `json:"privateKey" db:"privateKey"`
|
||||
IssuerOrg string `json:"issuerOrg" db:"issuerOrg"`
|
||||
IssuerCertificate string `json:"issuerCertificate" db:"issuerCertificate"`
|
||||
KeyAlgorithm CertificateKeyAlgorithmType `json:"keyAlgorithm" db:"keyAlgorithm"`
|
||||
EffectAt time.Time `json:"effectAt" db:"effectAt"`
|
||||
ExpireAt time.Time `json:"expireAt" db:"expireAt"`
|
||||
ACMEAccountUrl string `json:"acmeAccountUrl" db:"acmeAccountUrl"`
|
||||
ACMECertUrl string `json:"acmeCertUrl" db:"acmeCertUrl"`
|
||||
ACMECertStableUrl string `json:"acmeCertStableUrl" db:"acmeCertStableUrl"`
|
||||
ACMERenewed bool `json:"acmeRenewed" db:"acmeRenewed"`
|
||||
WorkflowId string `json:"workflowId" db:"workflowId"`
|
||||
WorkflowNodeId string `json:"workflowNodeId" db:"workflowNodeId"`
|
||||
WorkflowRunId string `json:"workflowRunId" db:"workflowRunId"`
|
||||
WorkflowOutputId string `json:"workflowOutputId" db:"workflowOutputId"`
|
||||
DeletedAt *time.Time `json:"deleted" db:"deleted"`
|
||||
}
|
||||
|
||||
func (c *Certificate) PopulateFromX509(certX509 *x509.Certificate) *Certificate {
|
||||
c.SubjectAltNames = strings.Join(certX509.DNSNames, ";")
|
||||
c.SerialNumber = strings.ToUpper(certX509.SerialNumber.Text(16))
|
||||
c.IssuerOrg = strings.Join(certX509.Issuer.Organization, ";")
|
||||
c.EffectAt = certX509.NotBefore
|
||||
c.ExpireAt = certX509.NotAfter
|
||||
|
||||
switch certX509.PublicKeyAlgorithm {
|
||||
case x509.RSA:
|
||||
{
|
||||
len := 0
|
||||
if pubkey, ok := certX509.PublicKey.(*rsa.PublicKey); ok {
|
||||
len = pubkey.N.BitLen()
|
||||
}
|
||||
|
||||
switch len {
|
||||
case 0:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType("RSA")
|
||||
case 2048:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA2048
|
||||
case 3072:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA3072
|
||||
case 4096:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA4096
|
||||
case 8192:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA8192
|
||||
default:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf("RSA%d", len))
|
||||
}
|
||||
}
|
||||
|
||||
case x509.ECDSA:
|
||||
{
|
||||
len := 0
|
||||
if pubkey, ok := certX509.PublicKey.(*ecdsa.PublicKey); ok {
|
||||
if pubkey.Curve != nil && pubkey.Curve.Params() != nil {
|
||||
len = pubkey.Curve.Params().BitSize
|
||||
}
|
||||
}
|
||||
|
||||
switch len {
|
||||
case 0:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType("EC")
|
||||
case 256:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC256
|
||||
case 384:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC384
|
||||
case 521:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC512
|
||||
default:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf("EC%d", len))
|
||||
}
|
||||
}
|
||||
|
||||
case x509.Ed25519:
|
||||
{
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType("ED25519")
|
||||
}
|
||||
|
||||
default:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType("")
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Certificate) PopulateFromPEM(certPEM, privkeyPEM string) *Certificate {
|
||||
c.Certificate = certPEM
|
||||
c.PrivateKey = privkeyPEM
|
||||
|
||||
_, issuerCertPEM, _ := xcert.ExtractCertificatesFromPEM(certPEM)
|
||||
c.IssuerCertificate = issuerCertPEM
|
||||
|
||||
certX509, _ := xcert.ParseCertificateFromPEM(certPEM)
|
||||
if certX509 != nil {
|
||||
c.PopulateFromX509(certX509)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
type CertificateSourceType string
|
||||
|
||||
const (
|
||||
CertificateSourceTypeWorkflow = CertificateSourceType("workflow")
|
||||
CertificateSourceTypeUpload = CertificateSourceType("upload")
|
||||
)
|
||||
|
||||
type CertificateKeyAlgorithmType string
|
||||
|
||||
const (
|
||||
CertificateKeyAlgorithmTypeRSA2048 = CertificateKeyAlgorithmType("RSA2048")
|
||||
CertificateKeyAlgorithmTypeRSA3072 = CertificateKeyAlgorithmType("RSA3072")
|
||||
CertificateKeyAlgorithmTypeRSA4096 = CertificateKeyAlgorithmType("RSA4096")
|
||||
CertificateKeyAlgorithmTypeRSA8192 = CertificateKeyAlgorithmType("RSA8192")
|
||||
CertificateKeyAlgorithmTypeEC256 = CertificateKeyAlgorithmType("EC256")
|
||||
CertificateKeyAlgorithmTypeEC384 = CertificateKeyAlgorithmType("EC384")
|
||||
CertificateKeyAlgorithmTypeEC512 = CertificateKeyAlgorithmType("EC512")
|
||||
)
|
||||
@@ -1,142 +0,0 @@
|
||||
package domain
|
||||
|
||||
import "strings"
|
||||
|
||||
type ApplyConfig struct {
|
||||
Email string `json:"email"`
|
||||
Access string `json:"access"`
|
||||
KeyAlgorithm string `json:"keyAlgorithm"`
|
||||
Nameservers string `json:"nameservers"`
|
||||
Timeout int64 `json:"timeout"`
|
||||
DisableFollowCNAME bool `json:"disableFollowCNAME"`
|
||||
}
|
||||
|
||||
type DeployConfig struct {
|
||||
Id string `json:"id"`
|
||||
Access string `json:"access"`
|
||||
Type string `json:"type"`
|
||||
Config map[string]any `json:"config"`
|
||||
}
|
||||
|
||||
// 以字符串形式获取配置项。
|
||||
//
|
||||
// 入参:
|
||||
// - key: 配置项的键。
|
||||
//
|
||||
// 出参:
|
||||
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回空字符串。
|
||||
func (dc *DeployConfig) GetConfigAsString(key string) string {
|
||||
return dc.GetConfigOrDefaultAsString(key, "")
|
||||
}
|
||||
|
||||
// 以字符串形式获取配置项。
|
||||
//
|
||||
// 入参:
|
||||
// - key: 配置项的键。
|
||||
// - defaultValue: 默认值。
|
||||
//
|
||||
// 出参:
|
||||
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回默认值。
|
||||
func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue string) string {
|
||||
if dc.Config == nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
if value, ok := dc.Config[key]; ok {
|
||||
if result, ok := value.(string); ok {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// 以 32 位整数形式获取配置项。
|
||||
//
|
||||
// 入参:
|
||||
// - key: 配置项的键。
|
||||
//
|
||||
// 出参:
|
||||
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回 0。
|
||||
func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
|
||||
return dc.GetConfigOrDefaultAsInt32(key, 0)
|
||||
}
|
||||
|
||||
// 以 32 位整数形式获取配置项。
|
||||
//
|
||||
// 入参:
|
||||
// - key: 配置项的键。
|
||||
// - defaultValue: 默认值。
|
||||
//
|
||||
// 出参:
|
||||
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回默认值。
|
||||
func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 {
|
||||
if dc.Config == nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
if value, ok := dc.Config[key]; ok {
|
||||
if result, ok := value.(int32); ok {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// 以布尔形式获取配置项。
|
||||
//
|
||||
// 入参:
|
||||
// - key: 配置项的键。
|
||||
//
|
||||
// 出参:
|
||||
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回 false。
|
||||
func (dc *DeployConfig) GetConfigAsBool(key string) bool {
|
||||
return dc.GetConfigOrDefaultAsBool(key, false)
|
||||
}
|
||||
|
||||
// 以布尔形式获取配置项。
|
||||
//
|
||||
// 入参:
|
||||
// - key: 配置项的键。
|
||||
// - defaultValue: 默认值。
|
||||
//
|
||||
// 出参:
|
||||
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回默认值。
|
||||
func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool) bool {
|
||||
if dc.Config == nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
if value, ok := dc.Config[key]; ok {
|
||||
if result, ok := value.(bool); ok {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
28
internal/domain/dtos/certificate.go
Normal file
28
internal/domain/dtos/certificate.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package dtos
|
||||
|
||||
type CertificateArchiveFileReq struct {
|
||||
CertificateId string `json:"-"`
|
||||
Format string `json:"format"`
|
||||
}
|
||||
|
||||
type CertificateArchiveFileResp struct {
|
||||
FileBytes []byte `json:"fileBytes"`
|
||||
FileFormat string `json:"fileFormat"`
|
||||
}
|
||||
|
||||
type CertificateValidateCertificateReq struct {
|
||||
Certificate string `json:"certificate"`
|
||||
}
|
||||
|
||||
type CertificateValidateCertificateResp struct {
|
||||
IsValid bool `json:"isValid"`
|
||||
Domains string `json:"domains,omitempty"`
|
||||
}
|
||||
|
||||
type CertificateValidatePrivateKeyReq struct {
|
||||
PrivateKey string `json:"privateKey"`
|
||||
}
|
||||
|
||||
type CertificateValidatePrivateKeyResp struct {
|
||||
IsValid bool `json:"isValid"`
|
||||
}
|
||||
7
internal/domain/dtos/notify.go
Normal file
7
internal/domain/dtos/notify.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package dtos
|
||||
|
||||
import "github.com/certimate-go/certimate/internal/domain"
|
||||
|
||||
type NotifyTestPushReq struct {
|
||||
Channel domain.NotifyChannelType `json:"channel"`
|
||||
}
|
||||
13
internal/domain/dtos/workflow.go
Normal file
13
internal/domain/dtos/workflow.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package dtos
|
||||
|
||||
import "github.com/certimate-go/certimate/internal/domain"
|
||||
|
||||
type WorkflowStartRunReq struct {
|
||||
WorkflowId string `json:"-"`
|
||||
RunTrigger domain.WorkflowTriggerType `json:"trigger"`
|
||||
}
|
||||
|
||||
type WorkflowCancelRunReq struct {
|
||||
WorkflowId string `json:"-"`
|
||||
RunId string `json:"-"`
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package domain
|
||||
|
||||
var ErrAuthFailed = NewXError(4999, "auth failed")
|
||||
|
||||
type XError struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func NewXError(code int, msg string) *XError {
|
||||
return &XError{code, msg}
|
||||
}
|
||||
|
||||
func (e *XError) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
func (e *XError) GetCode() int {
|
||||
if e.Code == 0 {
|
||||
return 100
|
||||
}
|
||||
return e.Code
|
||||
}
|
||||
30
internal/domain/error.go
Normal file
30
internal/domain/error.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package domain
|
||||
|
||||
var (
|
||||
ErrInvalidParams = NewError(400, "invalid params")
|
||||
ErrRecordNotFound = NewError(404, "record not found")
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func NewError(code int, msg string) *Error {
|
||||
if code == 0 {
|
||||
code = -1
|
||||
}
|
||||
|
||||
return &Error{code, msg}
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
func IsRecordNotFoundError(err error) bool {
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.Code == ErrRecordNotFound.Code
|
||||
}
|
||||
return false
|
||||
}
|
||||
630
internal/domain/expr/expr.go
Normal file
630
internal/domain/expr/expr.go
Normal file
@@ -0,0 +1,630 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type (
|
||||
ExprType string
|
||||
ExprComparisonOperator string
|
||||
ExprLogicalOperator string
|
||||
ExprValueType string
|
||||
)
|
||||
|
||||
const (
|
||||
GreaterThan ExprComparisonOperator = "gt"
|
||||
GreaterOrEqual ExprComparisonOperator = "gte"
|
||||
LessThan ExprComparisonOperator = "lt"
|
||||
LessOrEqual ExprComparisonOperator = "lte"
|
||||
Equal ExprComparisonOperator = "eq"
|
||||
NotEqual ExprComparisonOperator = "neq"
|
||||
|
||||
And ExprLogicalOperator = "and"
|
||||
Or ExprLogicalOperator = "or"
|
||||
Not ExprLogicalOperator = "not"
|
||||
|
||||
Number ExprValueType = "number"
|
||||
String ExprValueType = "string"
|
||||
Boolean ExprValueType = "boolean"
|
||||
|
||||
ConstantExprType ExprType = "const"
|
||||
VariantExprType ExprType = "var"
|
||||
ComparisonExprType ExprType = "comparison"
|
||||
LogicalExprType ExprType = "logical"
|
||||
NotExprType ExprType = "not"
|
||||
)
|
||||
|
||||
type EvalResult struct {
|
||||
Type ExprValueType
|
||||
Value any
|
||||
}
|
||||
|
||||
func (e *EvalResult) GetFloat64() (float64, error) {
|
||||
if e.Type != Number {
|
||||
return 0, fmt.Errorf("type mismatch: %s", e.Type)
|
||||
}
|
||||
|
||||
stringValue, ok := e.Value.(string)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("value is not a string: %v", e.Value)
|
||||
}
|
||||
|
||||
floatValue, err := strconv.ParseFloat(stringValue, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse float64: %v", err)
|
||||
}
|
||||
return floatValue, nil
|
||||
}
|
||||
|
||||
func (e *EvalResult) GetBool() (bool, error) {
|
||||
if e.Type != Boolean {
|
||||
return false, fmt.Errorf("type mismatch: %s", e.Type)
|
||||
}
|
||||
|
||||
strValue, ok := e.Value.(string)
|
||||
if ok {
|
||||
if strValue == "true" {
|
||||
return true, nil
|
||||
} else if strValue == "false" {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("value is not a boolean: %v", e.Value)
|
||||
}
|
||||
|
||||
boolValue, ok := e.Value.(bool)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("value is not a boolean: %v", e.Value)
|
||||
}
|
||||
|
||||
return boolValue, nil
|
||||
}
|
||||
|
||||
func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case String:
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: e.Value.(string) > other.Value.(string),
|
||||
}, nil
|
||||
|
||||
case Number:
|
||||
left, err := e.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
right, err := other.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: left > right,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) GreaterOrEqual(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case String:
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: e.Value.(string) >= other.Value.(string),
|
||||
}, nil
|
||||
|
||||
case Number:
|
||||
left, err := e.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
right, err := other.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: left >= right,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) LessThan(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case String:
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: e.Value.(string) < other.Value.(string),
|
||||
}, nil
|
||||
|
||||
case Number:
|
||||
left, err := e.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
right, err := other.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: left < right,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) LessOrEqual(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case String:
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: e.Value.(string) <= other.Value.(string),
|
||||
}, nil
|
||||
|
||||
case Number:
|
||||
left, err := e.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
right, err := other.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: left <= right,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) Equal(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case String:
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: e.Value.(string) == other.Value.(string),
|
||||
}, nil
|
||||
|
||||
case Number:
|
||||
left, err := e.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
right, err := other.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: left == right,
|
||||
}, nil
|
||||
|
||||
case Boolean:
|
||||
left, err := e.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
right, err := other.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: left == right,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) NotEqual(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case String:
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: e.Value.(string) != other.Value.(string),
|
||||
}, nil
|
||||
|
||||
case Number:
|
||||
left, err := e.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
right, err := other.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: left != right,
|
||||
}, nil
|
||||
|
||||
case Boolean:
|
||||
left, err := e.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
right, err := other.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: left != right,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) And(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case Boolean:
|
||||
left, err := e.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
right, err := other.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: left && right,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case Boolean:
|
||||
left, err := e.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
right, err := other.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: left || right,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) Not() (*EvalResult, error) {
|
||||
if e.Type != Boolean {
|
||||
return nil, fmt.Errorf("type mismatch: %s", e.Type)
|
||||
}
|
||||
|
||||
boolValue, err := e.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: !boolValue,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Expr interface {
|
||||
GetType() ExprType
|
||||
Eval(variables map[string]map[string]any) (*EvalResult, error)
|
||||
}
|
||||
|
||||
type ExprValueSelector struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type ExprValueType `json:"type"`
|
||||
}
|
||||
|
||||
type ConstantExpr struct {
|
||||
Type ExprType `json:"type"`
|
||||
Value string `json:"value"`
|
||||
ValueType ExprValueType `json:"valueType"`
|
||||
}
|
||||
|
||||
func (c ConstantExpr) GetType() ExprType { return c.Type }
|
||||
|
||||
func (c ConstantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
return &EvalResult{
|
||||
Type: c.ValueType,
|
||||
Value: c.Value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type VariantExpr struct {
|
||||
Type ExprType `json:"type"`
|
||||
Selector ExprValueSelector `json:"selector"`
|
||||
}
|
||||
|
||||
func (v VariantExpr) GetType() ExprType { return v.Type }
|
||||
|
||||
func (v VariantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
if v.Selector.Id == "" {
|
||||
return nil, fmt.Errorf("node id is empty")
|
||||
}
|
||||
if v.Selector.Name == "" {
|
||||
return nil, fmt.Errorf("name is empty")
|
||||
}
|
||||
|
||||
if _, ok := variables[v.Selector.Id]; !ok {
|
||||
return nil, fmt.Errorf("node %s not found", v.Selector.Id)
|
||||
}
|
||||
|
||||
if _, ok := variables[v.Selector.Id][v.Selector.Name]; !ok {
|
||||
return nil, fmt.Errorf("variable %s not found in node %s", v.Selector.Name, v.Selector.Id)
|
||||
}
|
||||
return &EvalResult{
|
||||
Type: v.Selector.Type,
|
||||
Value: variables[v.Selector.Id][v.Selector.Name],
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ComparisonExpr struct {
|
||||
Type ExprType `json:"type"` // compare
|
||||
Operator ExprComparisonOperator `json:"operator"`
|
||||
Left Expr `json:"left"`
|
||||
Right Expr `json:"right"`
|
||||
}
|
||||
|
||||
func (c ComparisonExpr) GetType() ExprType { return c.Type }
|
||||
|
||||
func (c ComparisonExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
left, err := c.Left.Eval(variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := c.Right.Eval(variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch c.Operator {
|
||||
case GreaterThan:
|
||||
return left.GreaterThan(right)
|
||||
case LessThan:
|
||||
return left.LessThan(right)
|
||||
case GreaterOrEqual:
|
||||
return left.GreaterOrEqual(right)
|
||||
case LessOrEqual:
|
||||
return left.LessOrEqual(right)
|
||||
case Equal:
|
||||
return left.Equal(right)
|
||||
case NotEqual:
|
||||
return left.NotEqual(right)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown expression operator: %s", c.Operator)
|
||||
}
|
||||
}
|
||||
|
||||
type LogicalExpr struct {
|
||||
Type ExprType `json:"type"` // logical
|
||||
Operator ExprLogicalOperator `json:"operator"`
|
||||
Left Expr `json:"left"`
|
||||
Right Expr `json:"right"`
|
||||
}
|
||||
|
||||
func (l LogicalExpr) GetType() ExprType { return l.Type }
|
||||
|
||||
func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
left, err := l.Left.Eval(variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := l.Right.Eval(variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch l.Operator {
|
||||
case And:
|
||||
return left.And(right)
|
||||
case Or:
|
||||
return left.Or(right)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown expression operator: %s", l.Operator)
|
||||
}
|
||||
}
|
||||
|
||||
type NotExpr struct {
|
||||
Type ExprType `json:"type"` // not
|
||||
Expr Expr `json:"expr"`
|
||||
}
|
||||
|
||||
func (n NotExpr) GetType() ExprType { return n.Type }
|
||||
|
||||
func (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
inner, err := n.Expr.Eval(variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inner.Not()
|
||||
}
|
||||
|
||||
type rawExpr struct {
|
||||
Type ExprType `json:"type"`
|
||||
}
|
||||
|
||||
func MarshalExpr(e Expr) ([]byte, error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
|
||||
func UnmarshalExpr(data []byte) (Expr, error) {
|
||||
var typ rawExpr
|
||||
if err := json.Unmarshal(data, &typ); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch typ.Type {
|
||||
case ConstantExprType:
|
||||
var e ConstantExpr
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e, nil
|
||||
case VariantExprType:
|
||||
var e VariantExpr
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e, nil
|
||||
case ComparisonExprType:
|
||||
var e ComparisonExprRaw
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.ToComparisonExpr()
|
||||
case LogicalExprType:
|
||||
var e LogicalExprRaw
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.ToLogicalExpr()
|
||||
case NotExprType:
|
||||
var e NotExprRaw
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.ToNotExpr()
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown expression type: %s", typ.Type)
|
||||
}
|
||||
}
|
||||
|
||||
type ComparisonExprRaw struct {
|
||||
Type ExprType `json:"type"`
|
||||
Operator ExprComparisonOperator `json:"operator"`
|
||||
Left json.RawMessage `json:"left"`
|
||||
Right json.RawMessage `json:"right"`
|
||||
}
|
||||
|
||||
func (r ComparisonExprRaw) ToComparisonExpr() (ComparisonExpr, error) {
|
||||
leftExpr, err := UnmarshalExpr(r.Left)
|
||||
if err != nil {
|
||||
return ComparisonExpr{}, err
|
||||
}
|
||||
rightExpr, err := UnmarshalExpr(r.Right)
|
||||
if err != nil {
|
||||
return ComparisonExpr{}, err
|
||||
}
|
||||
return ComparisonExpr{
|
||||
Type: r.Type,
|
||||
Operator: r.Operator,
|
||||
Left: leftExpr,
|
||||
Right: rightExpr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type LogicalExprRaw struct {
|
||||
Type ExprType `json:"type"`
|
||||
Operator ExprLogicalOperator `json:"operator"`
|
||||
Left json.RawMessage `json:"left"`
|
||||
Right json.RawMessage `json:"right"`
|
||||
}
|
||||
|
||||
func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) {
|
||||
left, err := UnmarshalExpr(r.Left)
|
||||
if err != nil {
|
||||
return LogicalExpr{}, err
|
||||
}
|
||||
right, err := UnmarshalExpr(r.Right)
|
||||
if err != nil {
|
||||
return LogicalExpr{}, err
|
||||
}
|
||||
return LogicalExpr{
|
||||
Type: r.Type,
|
||||
Operator: r.Operator,
|
||||
Left: left,
|
||||
Right: right,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type NotExprRaw struct {
|
||||
Type ExprType `json:"type"`
|
||||
Expr json.RawMessage `json:"expr"`
|
||||
}
|
||||
|
||||
func (r NotExprRaw) ToNotExpr() (NotExpr, error) {
|
||||
inner, err := UnmarshalExpr(r.Expr)
|
||||
if err != nil {
|
||||
return NotExpr{}, err
|
||||
}
|
||||
return NotExpr{
|
||||
Type: r.Type,
|
||||
Expr: inner,
|
||||
}, nil
|
||||
}
|
||||
127
internal/domain/expr/expr_test.go
Normal file
127
internal/domain/expr/expr_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogicalEval(t *testing.T) {
|
||||
// 测试逻辑表达式 and
|
||||
logicalExpr := LogicalExpr{
|
||||
Left: ConstantExpr{
|
||||
Type: "const",
|
||||
Value: "true",
|
||||
ValueType: "boolean",
|
||||
},
|
||||
Operator: And,
|
||||
Right: ConstantExpr{
|
||||
Type: "const",
|
||||
Value: "true",
|
||||
ValueType: "boolean",
|
||||
},
|
||||
}
|
||||
result, err := logicalExpr.Eval(nil)
|
||||
if err != nil {
|
||||
t.Errorf("failed to evaluate logical expression: %v", err)
|
||||
}
|
||||
if result.Value != true {
|
||||
t.Errorf("expected true, got %v", result)
|
||||
}
|
||||
|
||||
// 测试逻辑表达式 or
|
||||
orExpr := LogicalExpr{
|
||||
Left: ConstantExpr{
|
||||
Type: "const",
|
||||
Value: "true",
|
||||
ValueType: "boolean",
|
||||
},
|
||||
Operator: Or,
|
||||
Right: ConstantExpr{
|
||||
Type: "const",
|
||||
Value: "true",
|
||||
ValueType: "boolean",
|
||||
},
|
||||
}
|
||||
result, err = orExpr.Eval(nil)
|
||||
if err != nil {
|
||||
t.Errorf("failed to evaluate logical expression: %v", err)
|
||||
}
|
||||
if result.Value != true {
|
||||
t.Errorf("expected true, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalExpr(t *testing.T) {
|
||||
type args struct {
|
||||
data []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Expr
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
args: args{
|
||||
data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validity","type":"boolean"},"type":"var"},"operator":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"comparison"},"operator":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"operator":"eq","right":{"type":"const","value":2,"valueType":"number"},"type":"comparison"},"type":"logical"}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := UnmarshalExpr(tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("UnmarshalExpr() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got == nil {
|
||||
t.Errorf("UnmarshalExpr() got = nil, want %v", tt.want)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpr_Eval(t *testing.T) {
|
||||
type args struct {
|
||||
variables map[string]map[string]any
|
||||
data []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *EvalResult
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
args: args{
|
||||
variables: map[string]map[string]any{
|
||||
"ODnYSOXB6HQP2_vz6JcZE": {
|
||||
"certificate.validity": true,
|
||||
"certificate.daysLeft": 2,
|
||||
},
|
||||
},
|
||||
data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validity","type":"boolean"},"type":"var"},"operator":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"comparison"},"operator":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"operator":"eq","right":{"type":"const","value":2,"valueType":"number"},"type":"comparison"},"type":"logical"}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c, err := UnmarshalExpr(tt.args.data)
|
||||
if err != nil {
|
||||
t.Errorf("UnmarshalExpr() error = %v", err)
|
||||
return
|
||||
}
|
||||
got, err := c.Eval(tt.args.variables)
|
||||
t.Log("got:", got)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ConstExpr.Eval() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got.Value != true {
|
||||
t.Errorf("ConstExpr.Eval() got = %v, want %v", got.Value, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
9
internal/domain/meta.go
Normal file
9
internal/domain/meta.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type Meta struct {
|
||||
Id string `json:"id" db:"id"`
|
||||
CreatedAt time.Time `json:"created" db:"created"`
|
||||
UpdatedAt time.Time `json:"updated" db:"updated"`
|
||||
}
|
||||
@@ -1,14 +1,25 @@
|
||||
package domain
|
||||
|
||||
const (
|
||||
NotifyChannelDingtalk = "dingtalk"
|
||||
NotifyChannelWebhook = "webhook"
|
||||
NotifyChannelTelegram = "telegram"
|
||||
NotifyChannelLark = "lark"
|
||||
NotifyChannelServerChan = "serverchan"
|
||||
NotifyChannelMail = "mail"
|
||||
)
|
||||
type NotifyChannelType string
|
||||
|
||||
type NotifyTestPushReq struct {
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
/*
|
||||
消息通知渠道常量值。
|
||||
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
// Deprecated: v0.4.x 将废弃
|
||||
const (
|
||||
NotifyChannelTypeBark = NotifyChannelType("bark")
|
||||
NotifyChannelTypeDingTalk = NotifyChannelType("dingtalk")
|
||||
NotifyChannelTypeEmail = NotifyChannelType("email")
|
||||
NotifyChannelTypeGotify = NotifyChannelType("gotify")
|
||||
NotifyChannelTypeLark = NotifyChannelType("lark")
|
||||
NotifyChannelTypeMattermost = NotifyChannelType("mattermost")
|
||||
NotifyChannelTypePushover = NotifyChannelType("pushover")
|
||||
NotifyChannelTypePushPlus = NotifyChannelType("pushplus")
|
||||
NotifyChannelTypeServerChan = NotifyChannelType("serverchan")
|
||||
NotifyChannelTypeTelegram = NotifyChannelType("telegram")
|
||||
NotifyChannelTypeWebhook = NotifyChannelType("webhook")
|
||||
NotifyChannelTypeWeCom = NotifyChannelType("wecom")
|
||||
)
|
||||
|
||||
300
internal/domain/provider.go
Normal file
300
internal/domain/provider.go
Normal file
@@ -0,0 +1,300 @@
|
||||
package domain
|
||||
|
||||
type AccessProviderType string
|
||||
|
||||
/*
|
||||
授权提供商类型常量值。
|
||||
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
const (
|
||||
AccessProviderType1Panel = AccessProviderType("1panel")
|
||||
AccessProviderTypeACMECA = AccessProviderType("acmeca")
|
||||
AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq")
|
||||
AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai(预留)
|
||||
AccessProviderTypeAliyun = AccessProviderType("aliyun")
|
||||
AccessProviderTypeAPISIX = AccessProviderType("apisix")
|
||||
AccessProviderTypeAWS = AccessProviderType("aws")
|
||||
AccessProviderTypeAzure = AccessProviderType("azure")
|
||||
AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud")
|
||||
AccessProviderTypeBaishan = AccessProviderType("baishan")
|
||||
AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel")
|
||||
AccessProviderTypeBaotaWAF = AccessProviderType("baotawaf")
|
||||
AccessProviderTypeBytePlus = AccessProviderType("byteplus")
|
||||
AccessProviderTypeBunny = AccessProviderType("bunny")
|
||||
AccessProviderTypeBuypass = AccessProviderType("buypass")
|
||||
AccessProviderTypeCacheFly = AccessProviderType("cachefly")
|
||||
AccessProviderTypeCdnfly = AccessProviderType("cdnfly")
|
||||
AccessProviderTypeCloudflare = AccessProviderType("cloudflare")
|
||||
AccessProviderTypeClouDNS = AccessProviderType("cloudns")
|
||||
AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud")
|
||||
AccessProviderTypeConstellix = AccessProviderType("constellix")
|
||||
AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud")
|
||||
AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 联通云(预留)
|
||||
AccessProviderTypeDeSEC = AccessProviderType("desec")
|
||||
AccessProviderTypeDigitalOcean = AccessProviderType("digitalocean")
|
||||
AccessProviderTypeDingTalkBot = AccessProviderType("dingtalkbot")
|
||||
AccessProviderTypeDiscordBot = AccessProviderType("discordbot")
|
||||
AccessProviderTypeDNSLA = AccessProviderType("dnsla")
|
||||
AccessProviderTypeDogeCloud = AccessProviderType("dogecloud")
|
||||
AccessProviderTypeDuckDNS = AccessProviderType("duckdns")
|
||||
AccessProviderTypeDynv6 = AccessProviderType("dynv6")
|
||||
AccessProviderTypeEdgio = AccessProviderType("edgio")
|
||||
AccessProviderTypeEmail = AccessProviderType("email")
|
||||
AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留)
|
||||
AccessProviderTypeFlexCDN = AccessProviderType("flexcdn")
|
||||
AccessProviderTypeGname = AccessProviderType("gname")
|
||||
AccessProviderTypeGcore = AccessProviderType("gcore")
|
||||
AccessProviderTypeGoDaddy = AccessProviderType("godaddy")
|
||||
AccessProviderTypeGoEdge = AccessProviderType("goedge")
|
||||
AccessProviderTypeGoogleTrustServices = AccessProviderType("googletrustservices")
|
||||
AccessProviderTypeHetzner = AccessProviderType("hetzner")
|
||||
AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud")
|
||||
AccessProviderTypeJDCloud = AccessProviderType("jdcloud")
|
||||
AccessProviderTypeKubernetes = AccessProviderType("k8s")
|
||||
AccessProviderTypeLarkBot = AccessProviderType("larkbot")
|
||||
AccessProviderTypeLetsEncrypt = AccessProviderType("letsencrypt")
|
||||
AccessProviderTypeLetsEncryptStaging = AccessProviderType("letsencryptstaging")
|
||||
AccessProviderTypeLeCDN = AccessProviderType("lecdn")
|
||||
AccessProviderTypeLocal = AccessProviderType("local")
|
||||
AccessProviderTypeMattermost = AccessProviderType("mattermost")
|
||||
AccessProviderTypeNamecheap = AccessProviderType("namecheap")
|
||||
AccessProviderTypeNameDotCom = AccessProviderType("namedotcom")
|
||||
AccessProviderTypeNameSilo = AccessProviderType("namesilo")
|
||||
AccessProviderTypeNetcup = AccessProviderType("netcup")
|
||||
AccessProviderTypeNetlify = AccessProviderType("netlify")
|
||||
AccessProviderTypeNS1 = AccessProviderType("ns1")
|
||||
AccessProviderTypePorkbun = AccessProviderType("porkbun")
|
||||
AccessProviderTypePowerDNS = AccessProviderType("powerdns")
|
||||
AccessProviderTypeProxmoxVE = AccessProviderType("proxmoxve")
|
||||
AccessProviderTypeQiniu = AccessProviderType("qiniu")
|
||||
AccessProviderTypeQingCloud = AccessProviderType("qingcloud") // 青云(预留)
|
||||
AccessProviderTypeRainYun = AccessProviderType("rainyun")
|
||||
AccessProviderTypeRatPanel = AccessProviderType("ratpanel")
|
||||
AccessProviderTypeSafeLine = AccessProviderType("safeline")
|
||||
AccessProviderTypeSlackBot = AccessProviderType("slackbot")
|
||||
AccessProviderTypeSSH = AccessProviderType("ssh")
|
||||
AccessProviderTypeSSLCOM = AccessProviderType("sslcom")
|
||||
AccessProviderTypeTelegramBot = AccessProviderType("telegrambot")
|
||||
AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud")
|
||||
AccessProviderTypeUCloud = AccessProviderType("ucloud")
|
||||
AccessProviderTypeUniCloud = AccessProviderType("unicloud")
|
||||
AccessProviderTypeUpyun = AccessProviderType("upyun")
|
||||
AccessProviderTypeVercel = AccessProviderType("vercel")
|
||||
AccessProviderTypeVolcEngine = AccessProviderType("volcengine")
|
||||
AccessProviderTypeWangsu = AccessProviderType("wangsu")
|
||||
AccessProviderTypeWebhook = AccessProviderType("webhook")
|
||||
AccessProviderTypeWeComBot = AccessProviderType("wecombot")
|
||||
AccessProviderTypeWestcn = AccessProviderType("westcn")
|
||||
AccessProviderTypeZeroSSL = AccessProviderType("zerossl")
|
||||
)
|
||||
|
||||
type CAProviderType string
|
||||
|
||||
/*
|
||||
证书颁发机构提供商常量值。
|
||||
短横线前的部分始终等于授权提供商类型。
|
||||
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
const (
|
||||
CAProviderTypeACMECA = CAProviderType(AccessProviderTypeACMECA)
|
||||
CAProviderTypeBuypass = CAProviderType(AccessProviderTypeBuypass)
|
||||
CAProviderTypeGoogleTrustServices = CAProviderType(AccessProviderTypeGoogleTrustServices)
|
||||
CAProviderTypeLetsEncrypt = CAProviderType(AccessProviderTypeLetsEncrypt)
|
||||
CAProviderTypeLetsEncryptStaging = CAProviderType(AccessProviderTypeLetsEncryptStaging)
|
||||
CAProviderTypeSSLCom = CAProviderType(AccessProviderTypeSSLCOM)
|
||||
CAProviderTypeZeroSSL = CAProviderType(AccessProviderTypeZeroSSL)
|
||||
)
|
||||
|
||||
type ACMEDns01ProviderType string
|
||||
|
||||
/*
|
||||
ACME DNS-01 提供商常量值。
|
||||
短横线前的部分始终等于授权提供商类型。
|
||||
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
const (
|
||||
ACMEDns01ProviderTypeACMEHttpReq = ACMEDns01ProviderType(AccessProviderTypeACMEHttpReq)
|
||||
ACMEDns01ProviderTypeAliyun = ACMEDns01ProviderType(AccessProviderTypeAliyun) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAliyunDNS]
|
||||
ACMEDns01ProviderTypeAliyunDNS = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-dns")
|
||||
ACMEDns01ProviderTypeAliyunESA = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-esa")
|
||||
ACMEDns01ProviderTypeAWS = ACMEDns01ProviderType(AccessProviderTypeAWS) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAWSRoute53]
|
||||
ACMEDns01ProviderTypeAWSRoute53 = ACMEDns01ProviderType(AccessProviderTypeAWS + "-route53")
|
||||
ACMEDns01ProviderTypeAzure = ACMEDns01ProviderType(AccessProviderTypeAzure) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAzure]
|
||||
ACMEDns01ProviderTypeAzureDNS = ACMEDns01ProviderType(AccessProviderTypeAzure + "-dns")
|
||||
ACMEDns01ProviderTypeBaiduCloud = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeBaiduCloudDNS]
|
||||
ACMEDns01ProviderTypeBaiduCloudDNS = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud + "-dns")
|
||||
ACMEDns01ProviderTypeBunny = ACMEDns01ProviderType(AccessProviderTypeBunny)
|
||||
ACMEDns01ProviderTypeCloudflare = ACMEDns01ProviderType(AccessProviderTypeCloudflare)
|
||||
ACMEDns01ProviderTypeClouDNS = ACMEDns01ProviderType(AccessProviderTypeClouDNS)
|
||||
ACMEDns01ProviderTypeCMCCCloud = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCMCCCloudDNS]
|
||||
ACMEDns01ProviderTypeCMCCCloudDNS = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud + "-dns")
|
||||
ACMEDns01ProviderTypeConstellix = ACMEDns01ProviderType(AccessProviderTypeConstellix)
|
||||
ACMEDns01ProviderTypeCTCCCloud = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCTCCCloudSmartDNS]
|
||||
ACMEDns01ProviderTypeCTCCCloudSmartDNS = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud + "-smartdns")
|
||||
ACMEDns01ProviderTypeDeSEC = ACMEDns01ProviderType(AccessProviderTypeDeSEC)
|
||||
ACMEDns01ProviderTypeDigitalOcean = ACMEDns01ProviderType(AccessProviderTypeDigitalOcean)
|
||||
ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA)
|
||||
ACMEDns01ProviderTypeDuckDNS = ACMEDns01ProviderType(AccessProviderTypeDuckDNS)
|
||||
ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6)
|
||||
ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore)
|
||||
ACMEDns01ProviderTypeGname = ACMEDns01ProviderType(AccessProviderTypeGname)
|
||||
ACMEDns01ProviderTypeGoDaddy = ACMEDns01ProviderType(AccessProviderTypeGoDaddy)
|
||||
ACMEDns01ProviderTypeHetzner = ACMEDns01ProviderType(AccessProviderTypeHetzner)
|
||||
ACMEDns01ProviderTypeHuaweiCloud = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeHuaweiCloudDNS]
|
||||
ACMEDns01ProviderTypeHuaweiCloudDNS = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud + "-dns")
|
||||
ACMEDns01ProviderTypeJDCloud = ACMEDns01ProviderType(AccessProviderTypeJDCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeJDCloudDNS]
|
||||
ACMEDns01ProviderTypeJDCloudDNS = ACMEDns01ProviderType(AccessProviderTypeJDCloud + "-dns")
|
||||
ACMEDns01ProviderTypeNamecheap = ACMEDns01ProviderType(AccessProviderTypeNamecheap)
|
||||
ACMEDns01ProviderTypeNameDotCom = ACMEDns01ProviderType(AccessProviderTypeNameDotCom)
|
||||
ACMEDns01ProviderTypeNameSilo = ACMEDns01ProviderType(AccessProviderTypeNameSilo)
|
||||
ACMEDns01ProviderTypeNetcup = ACMEDns01ProviderType(AccessProviderTypeNetcup)
|
||||
ACMEDns01ProviderTypeNetlify = ACMEDns01ProviderType(AccessProviderTypeNetlify)
|
||||
ACMEDns01ProviderTypeNS1 = ACMEDns01ProviderType(AccessProviderTypeNS1)
|
||||
ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun)
|
||||
ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS)
|
||||
ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun)
|
||||
ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS]
|
||||
ACMEDns01ProviderTypeTencentCloudDNS = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-dns")
|
||||
ACMEDns01ProviderTypeTencentCloudEO = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-eo")
|
||||
ACMEDns01ProviderTypeUCloudUDNR = ACMEDns01ProviderType(AccessProviderTypeUCloud + "-udnr")
|
||||
ACMEDns01ProviderTypeVercel = ACMEDns01ProviderType(AccessProviderTypeVercel)
|
||||
ACMEDns01ProviderTypeVolcEngine = ACMEDns01ProviderType(AccessProviderTypeVolcEngine) // 兼容旧值,等同于 [ACMEDns01ProviderTypeVolcEngineDNS]
|
||||
ACMEDns01ProviderTypeVolcEngineDNS = ACMEDns01ProviderType(AccessProviderTypeVolcEngine + "-dns")
|
||||
ACMEDns01ProviderTypeWestcn = ACMEDns01ProviderType(AccessProviderTypeWestcn)
|
||||
)
|
||||
|
||||
type DeploymentProviderType string
|
||||
|
||||
/*
|
||||
部署证书主机提供商常量值。
|
||||
短横线前的部分始终等于授权提供商类型。
|
||||
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
const (
|
||||
DeploymentProviderType1PanelConsole = DeploymentProviderType(AccessProviderType1Panel + "-console")
|
||||
DeploymentProviderType1PanelSite = DeploymentProviderType(AccessProviderType1Panel + "-site")
|
||||
DeploymentProviderTypeAliyunALB = DeploymentProviderType(AccessProviderTypeAliyun + "-alb")
|
||||
DeploymentProviderTypeAliyunAPIGW = DeploymentProviderType(AccessProviderTypeAliyun + "-apigw")
|
||||
DeploymentProviderTypeAliyunCAS = DeploymentProviderType(AccessProviderTypeAliyun + "-cas")
|
||||
DeploymentProviderTypeAliyunCASDeploy = DeploymentProviderType(AccessProviderTypeAliyun + "-casdeploy")
|
||||
DeploymentProviderTypeAliyunCDN = DeploymentProviderType(AccessProviderTypeAliyun + "-cdn")
|
||||
DeploymentProviderTypeAliyunCLB = DeploymentProviderType(AccessProviderTypeAliyun + "-clb")
|
||||
DeploymentProviderTypeAliyunDCDN = DeploymentProviderType(AccessProviderTypeAliyun + "-dcdn")
|
||||
DeploymentProviderTypeAliyunDDoS = DeploymentProviderType(AccessProviderTypeAliyun + "-ddos")
|
||||
DeploymentProviderTypeAliyunESA = DeploymentProviderType(AccessProviderTypeAliyun + "-esa")
|
||||
DeploymentProviderTypeAliyunFC = DeploymentProviderType(AccessProviderTypeAliyun + "-fc")
|
||||
DeploymentProviderTypeAliyunGA = DeploymentProviderType(AccessProviderTypeAliyun + "-ga")
|
||||
DeploymentProviderTypeAliyunLive = DeploymentProviderType(AccessProviderTypeAliyun + "-live")
|
||||
DeploymentProviderTypeAliyunNLB = DeploymentProviderType(AccessProviderTypeAliyun + "-nlb")
|
||||
DeploymentProviderTypeAliyunOSS = DeploymentProviderType(AccessProviderTypeAliyun + "-oss")
|
||||
DeploymentProviderTypeAliyunVOD = DeploymentProviderType(AccessProviderTypeAliyun + "-vod")
|
||||
DeploymentProviderTypeAliyunWAF = DeploymentProviderType(AccessProviderTypeAliyun + "-waf")
|
||||
DeploymentProviderTypeAPISIX = DeploymentProviderType(AccessProviderTypeAWS + "-apisix")
|
||||
DeploymentProviderTypeAWSACM = DeploymentProviderType(AccessProviderTypeAWS + "-acm")
|
||||
DeploymentProviderTypeAWSCloudFront = DeploymentProviderType(AccessProviderTypeAWS + "-cloudfront")
|
||||
DeploymentProviderTypeAWSIAM = DeploymentProviderType(AccessProviderTypeAWS + "-iam")
|
||||
DeploymentProviderTypeAzureKeyVault = DeploymentProviderType(AccessProviderTypeAzure + "-keyvault")
|
||||
DeploymentProviderTypeBaiduCloudAppBLB = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-appblb")
|
||||
DeploymentProviderTypeBaiduCloudBLB = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-blb")
|
||||
DeploymentProviderTypeBaiduCloudCDN = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-cdn")
|
||||
DeploymentProviderTypeBaiduCloudCert = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-cert")
|
||||
DeploymentProviderTypeBaishanCDN = DeploymentProviderType(AccessProviderTypeBaishan + "-cdn")
|
||||
DeploymentProviderTypeBaotaPanelConsole = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-console")
|
||||
DeploymentProviderTypeBaotaPanelSite = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-site")
|
||||
DeploymentProviderTypeBaotaWAFConsole = DeploymentProviderType(AccessProviderTypeBaotaWAF + "-console")
|
||||
DeploymentProviderTypeBaotaWAFSite = DeploymentProviderType(AccessProviderTypeBaotaWAF + "-site")
|
||||
DeploymentProviderTypeBunnyCDN = DeploymentProviderType(AccessProviderTypeBunny + "-cdn")
|
||||
DeploymentProviderTypeBytePlusCDN = DeploymentProviderType(AccessProviderTypeBytePlus + "-cdn")
|
||||
DeploymentProviderTypeCacheFly = DeploymentProviderType(AccessProviderTypeCacheFly)
|
||||
DeploymentProviderTypeCdnfly = DeploymentProviderType(AccessProviderTypeCdnfly)
|
||||
DeploymentProviderTypeCTCCCloudAO = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ao")
|
||||
DeploymentProviderTypeCTCCCloudCDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cdn")
|
||||
DeploymentProviderTypeCTCCCloudCMS = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cms")
|
||||
DeploymentProviderTypeCTCCCloudELB = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-elb")
|
||||
DeploymentProviderTypeCTCCCloudICDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-icdn")
|
||||
DeploymentProviderTypeCTCCCloudLVDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ldvn")
|
||||
DeploymentProviderTypeDogeCloudCDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-cdn")
|
||||
DeploymentProviderTypeEdgioApplications = DeploymentProviderType(AccessProviderTypeEdgio + "-applications")
|
||||
DeploymentProviderTypeFlexCDN = DeploymentProviderType(AccessProviderTypeFlexCDN)
|
||||
DeploymentProviderTypeGcoreCDN = DeploymentProviderType(AccessProviderTypeGcore + "-cdn")
|
||||
DeploymentProviderTypeGoEdge = DeploymentProviderType(AccessProviderTypeGoEdge)
|
||||
DeploymentProviderTypeHuaweiCloudCDN = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-cdn")
|
||||
DeploymentProviderTypeHuaweiCloudELB = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-elb")
|
||||
DeploymentProviderTypeHuaweiCloudSCM = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-scm")
|
||||
DeploymentProviderTypeHuaweiCloudWAF = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-waf")
|
||||
DeploymentProviderTypeJDCloudALB = DeploymentProviderType(AccessProviderTypeJDCloud + "-alb")
|
||||
DeploymentProviderTypeJDCloudCDN = DeploymentProviderType(AccessProviderTypeJDCloud + "-cdn")
|
||||
DeploymentProviderTypeJDCloudLive = DeploymentProviderType(AccessProviderTypeJDCloud + "-live")
|
||||
DeploymentProviderTypeJDCloudVOD = DeploymentProviderType(AccessProviderTypeJDCloud + "-vod")
|
||||
DeploymentProviderTypeKubernetesSecret = DeploymentProviderType(AccessProviderTypeKubernetes + "-secret")
|
||||
DeploymentProviderTypeLeCDN = DeploymentProviderType(AccessProviderTypeLeCDN)
|
||||
DeploymentProviderTypeLocal = DeploymentProviderType(AccessProviderTypeLocal)
|
||||
DeploymentProviderTypeNetlifySite = DeploymentProviderType(AccessProviderTypeNetlify + "-site")
|
||||
DeploymentProviderTypeProxmoxVE = DeploymentProviderType(AccessProviderTypeProxmoxVE)
|
||||
DeploymentProviderTypeQiniuCDN = DeploymentProviderType(AccessProviderTypeQiniu + "-cdn")
|
||||
DeploymentProviderTypeQiniuKodo = DeploymentProviderType(AccessProviderTypeQiniu + "-kodo")
|
||||
DeploymentProviderTypeQiniuPili = DeploymentProviderType(AccessProviderTypeQiniu + "-pili")
|
||||
DeploymentProviderTypeRainYunRCDN = DeploymentProviderType(AccessProviderTypeRainYun + "-rcdn")
|
||||
DeploymentProviderTypeRatPanelConsole = DeploymentProviderType(AccessProviderTypeRatPanel + "-console")
|
||||
DeploymentProviderTypeRatPanelSite = DeploymentProviderType(AccessProviderTypeRatPanel + "-site")
|
||||
DeploymentProviderTypeSafeLine = DeploymentProviderType(AccessProviderTypeSafeLine)
|
||||
DeploymentProviderTypeSSH = DeploymentProviderType(AccessProviderTypeSSH)
|
||||
DeploymentProviderTypeTencentCloudCDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-cdn")
|
||||
DeploymentProviderTypeTencentCloudCLB = DeploymentProviderType(AccessProviderTypeTencentCloud + "-clb")
|
||||
DeploymentProviderTypeTencentCloudCOS = DeploymentProviderType(AccessProviderTypeTencentCloud + "-cos")
|
||||
DeploymentProviderTypeTencentCloudCSS = DeploymentProviderType(AccessProviderTypeTencentCloud + "-css")
|
||||
DeploymentProviderTypeTencentCloudECDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ecdn")
|
||||
DeploymentProviderTypeTencentCloudEO = DeploymentProviderType(AccessProviderTypeTencentCloud + "-eo")
|
||||
DeploymentProviderTypeTencentCloudGAAP = DeploymentProviderType(AccessProviderTypeTencentCloud + "-gaap")
|
||||
DeploymentProviderTypeTencentCloudSCF = DeploymentProviderType(AccessProviderTypeTencentCloud + "-scf")
|
||||
DeploymentProviderTypeTencentCloudSSL = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ssl")
|
||||
DeploymentProviderTypeTencentCloudSSLDeploy = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ssldeploy")
|
||||
DeploymentProviderTypeTencentCloudVOD = DeploymentProviderType(AccessProviderTypeTencentCloud + "-vod")
|
||||
DeploymentProviderTypeTencentCloudWAF = DeploymentProviderType(AccessProviderTypeTencentCloud + "-waf")
|
||||
DeploymentProviderTypeUCloudUCDN = DeploymentProviderType(AccessProviderTypeUCloud + "-ucdn")
|
||||
DeploymentProviderTypeUCloudUS3 = DeploymentProviderType(AccessProviderTypeUCloud + "-us3")
|
||||
DeploymentProviderTypeUniCloudWebHost = DeploymentProviderType(AccessProviderTypeUniCloud + "-webhost")
|
||||
DeploymentProviderTypeUpyunCDN = DeploymentProviderType(AccessProviderTypeUpyun + "-cdn")
|
||||
DeploymentProviderTypeUpyunFile = DeploymentProviderType(AccessProviderTypeUpyun + "-file")
|
||||
DeploymentProviderTypeVolcEngineALB = DeploymentProviderType(AccessProviderTypeVolcEngine + "-alb")
|
||||
DeploymentProviderTypeVolcEngineCDN = DeploymentProviderType(AccessProviderTypeVolcEngine + "-cdn")
|
||||
DeploymentProviderTypeVolcEngineCertCenter = DeploymentProviderType(AccessProviderTypeVolcEngine + "-certcenter")
|
||||
DeploymentProviderTypeVolcEngineCLB = DeploymentProviderType(AccessProviderTypeVolcEngine + "-clb")
|
||||
DeploymentProviderTypeVolcEngineDCDN = DeploymentProviderType(AccessProviderTypeVolcEngine + "-dcdn")
|
||||
DeploymentProviderTypeVolcEngineImageX = DeploymentProviderType(AccessProviderTypeVolcEngine + "-imagex")
|
||||
DeploymentProviderTypeVolcEngineLive = DeploymentProviderType(AccessProviderTypeVolcEngine + "-live")
|
||||
DeploymentProviderTypeVolcEngineTOS = DeploymentProviderType(AccessProviderTypeVolcEngine + "-tos")
|
||||
DeploymentProviderTypeWangsuCDN = DeploymentProviderType(AccessProviderTypeWangsu + "-cdn")
|
||||
DeploymentProviderTypeWangsuCDNPro = DeploymentProviderType(AccessProviderTypeWangsu + "-cdnpro")
|
||||
DeploymentProviderTypeWangsuCertificate = DeploymentProviderType(AccessProviderTypeWangsu + "-certificate")
|
||||
DeploymentProviderTypeWebhook = DeploymentProviderType(AccessProviderTypeWebhook)
|
||||
)
|
||||
|
||||
type NotificationProviderType string
|
||||
|
||||
/*
|
||||
消息通知提供商常量值。
|
||||
短横线前的部分始终等于授权提供商类型。
|
||||
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
const (
|
||||
NotificationProviderTypeDingTalkBot = NotificationProviderType(AccessProviderTypeDingTalkBot)
|
||||
NotificationProviderTypeDiscordBot = NotificationProviderType(AccessProviderTypeDiscordBot)
|
||||
NotificationProviderTypeEmail = NotificationProviderType(AccessProviderTypeEmail)
|
||||
NotificationProviderTypeLarkBot = NotificationProviderType(AccessProviderTypeLarkBot)
|
||||
NotificationProviderTypeMattermost = NotificationProviderType(AccessProviderTypeMattermost)
|
||||
NotificationProviderTypeSlackBot = NotificationProviderType(AccessProviderTypeSlackBot)
|
||||
NotificationProviderTypeTelegramBot = NotificationProviderType(AccessProviderTypeTelegramBot)
|
||||
NotificationProviderTypeWebhook = NotificationProviderType(AccessProviderTypeWebhook)
|
||||
NotificationProviderTypeWeComBot = NotificationProviderType(AccessProviderTypeWeComBot)
|
||||
)
|
||||
@@ -1,31 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
type ChannelsConfig map[string]map[string]any
|
||||
|
||||
func (s *Setting) GetChannelContent(channel string) (map[string]any, error) {
|
||||
conf := &ChannelsConfig{}
|
||||
if err := json.Unmarshal([]byte(s.Content), conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, ok := (*conf)[channel]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("channel %s not found", channel)
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
45
internal/domain/settings.go
Normal file
45
internal/domain/settings.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const CollectionNameSettings = "settings"
|
||||
|
||||
type Settings struct {
|
||||
Meta
|
||||
Name string `json:"name" db:"name"`
|
||||
Content string `json:"content" db:"content"`
|
||||
}
|
||||
|
||||
// Deprecated: v0.4.x 将废弃
|
||||
type NotifyTemplatesSettingsContent struct {
|
||||
NotifyTemplates []struct {
|
||||
Subject string `json:"subject"`
|
||||
Message string `json:"message"`
|
||||
} `json:"notifyTemplates"`
|
||||
}
|
||||
|
||||
// Deprecated: v0.4.x 将废弃
|
||||
type NotifyChannelsSettingsContent map[string]map[string]any
|
||||
|
||||
// Deprecated: v0.4.x 将废弃
|
||||
func (s *Settings) GetNotifyChannelConfig(channel string) (map[string]any, error) {
|
||||
conf := &NotifyChannelsSettingsContent{}
|
||||
if err := json.Unmarshal([]byte(s.Content), conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, ok := (*conf)[channel]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("channel \"%s\" not found", channel)
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type PersistenceSettingsContent struct {
|
||||
WorkflowRunsMaxDaysRetention int `json:"workflowRunsMaxDaysRetention"`
|
||||
ExpiredCertificatesMaxDaysRetention int `json:"expiredCertificatesMaxDaysRetention"`
|
||||
}
|
||||
11
internal/domain/statistics.go
Normal file
11
internal/domain/statistics.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package domain
|
||||
|
||||
type Statistics struct {
|
||||
CertificateTotal int `json:"certificateTotal"`
|
||||
CertificateExpireSoon int `json:"certificateExpireSoon"`
|
||||
CertificateExpired int `json:"certificateExpired"`
|
||||
|
||||
WorkflowTotal int `json:"workflowTotal"`
|
||||
WorkflowEnabled int `json:"workflowEnabled"`
|
||||
WorkflowDisabled int `json:"workflowDisabled"`
|
||||
}
|
||||
211
internal/domain/workflow.go
Normal file
211
internal/domain/workflow.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/certimate-go/certimate/internal/domain/expr"
|
||||
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
|
||||
)
|
||||
|
||||
const CollectionNameWorkflow = "workflow"
|
||||
|
||||
type Workflow struct {
|
||||
Meta
|
||||
Name string `json:"name" db:"name"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Trigger WorkflowTriggerType `json:"trigger" db:"trigger"`
|
||||
TriggerCron string `json:"triggerCron" db:"triggerCron"`
|
||||
Enabled bool `json:"enabled" db:"enabled"`
|
||||
Content *WorkflowNode `json:"content" db:"content"`
|
||||
Draft *WorkflowNode `json:"draft" db:"draft"`
|
||||
HasDraft bool `json:"hasDraft" db:"hasDraft"`
|
||||
LastRunId string `json:"lastRunId" db:"lastRunId"`
|
||||
LastRunStatus WorkflowRunStatusType `json:"lastRunStatus" db:"lastRunStatus"`
|
||||
LastRunTime time.Time `json:"lastRunTime" db:"lastRunTime"`
|
||||
}
|
||||
|
||||
type WorkflowNodeType string
|
||||
|
||||
const (
|
||||
WorkflowNodeTypeStart = WorkflowNodeType("start")
|
||||
WorkflowNodeTypeEnd = WorkflowNodeType("end")
|
||||
WorkflowNodeTypeApply = WorkflowNodeType("apply")
|
||||
WorkflowNodeTypeUpload = WorkflowNodeType("upload")
|
||||
WorkflowNodeTypeMonitor = WorkflowNodeType("monitor")
|
||||
WorkflowNodeTypeDeploy = WorkflowNodeType("deploy")
|
||||
WorkflowNodeTypeNotify = WorkflowNodeType("notify")
|
||||
WorkflowNodeTypeBranch = WorkflowNodeType("branch")
|
||||
WorkflowNodeTypeCondition = WorkflowNodeType("condition")
|
||||
WorkflowNodeTypeExecuteResultBranch = WorkflowNodeType("execute_result_branch")
|
||||
WorkflowNodeTypeExecuteSuccess = WorkflowNodeType("execute_success")
|
||||
WorkflowNodeTypeExecuteFailure = WorkflowNodeType("execute_failure")
|
||||
)
|
||||
|
||||
type WorkflowTriggerType string
|
||||
|
||||
const (
|
||||
WorkflowTriggerTypeAuto = WorkflowTriggerType("auto")
|
||||
WorkflowTriggerTypeManual = WorkflowTriggerType("manual")
|
||||
)
|
||||
|
||||
type WorkflowNode struct {
|
||||
Id string `json:"id"`
|
||||
Type WorkflowNodeType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
|
||||
Config map[string]any `json:"config"`
|
||||
Inputs []WorkflowNodeIO `json:"inputs"`
|
||||
Outputs []WorkflowNodeIO `json:"outputs"`
|
||||
|
||||
Next *WorkflowNode `json:"next,omitempty"`
|
||||
Branches []WorkflowNode `json:"branches,omitempty"`
|
||||
|
||||
Validated bool `json:"validated"`
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForApply struct {
|
||||
Domains string `json:"domains"` // 域名列表,以半角分号分隔
|
||||
ContactEmail string `json:"contactEmail"` // 联系邮箱
|
||||
ChallengeType string `json:"challengeType"` // TODO: 验证方式。目前仅支持 dns-01
|
||||
Provider string `json:"provider"` // DNS 提供商
|
||||
ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID
|
||||
ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置
|
||||
CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值时使用全局配置)
|
||||
CAProviderAccessId string `json:"caProviderAccessId,omitempty"` // CA 提供商授权记录 ID
|
||||
CAProviderConfig map[string]any `json:"caProviderConfig,omitempty"` // CA 提供商额外配置
|
||||
KeyAlgorithm string `json:"keyAlgorithm"` // 证书算法
|
||||
Nameservers string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔
|
||||
DnsPropagationWait int32 `json:"dnsPropagationWait,omitempty"` // DNS 传播等待时间,等同于 lego 的 `--dns-propagation-wait` 参数
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` // DNS 传播检查超时时间(零值时使用提供商的默认值)
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"` // DNS 解析记录 TTL(零值时使用提供商的默认值)
|
||||
DisableFollowCNAME bool `json:"disableFollowCNAME,omitempty"` // 是否关闭 CNAME 跟随
|
||||
DisableARI bool `json:"disableARI,omitempty"` // 是否关闭 ARI
|
||||
SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值时默认值 30)
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForUpload struct {
|
||||
Certificate string `json:"certificate"` // 证书 PEM 内容
|
||||
PrivateKey string `json:"privateKey"` // 私钥 PEM 内容
|
||||
Domains string `json:"domains,omitempty"`
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForMonitor struct {
|
||||
Host string `json:"host"` // 主机地址
|
||||
Port int32 `json:"port,omitempty"` // 端口(零值时默认值 443)
|
||||
Domain string `json:"domain,omitempty"` // 域名(零值时默认值 [Host])
|
||||
RequestPath string `json:"requestPath,omitempty"` // 请求路径
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForDeploy struct {
|
||||
Certificate string `json:"certificate"` // 前序节点输出的证书,形如“${NodeId}#certificate”
|
||||
Provider string `json:"provider"` // 主机提供商
|
||||
ProviderAccessId string `json:"providerAccessId,omitempty"` // 主机提供商授权记录 ID
|
||||
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 主机提供商额外配置
|
||||
SkipOnLastSucceeded bool `json:"skipOnLastSucceeded"` // 上次部署成功时是否跳过
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForNotify struct {
|
||||
Channel string `json:"channel,omitempty"` // Deprecated: v0.4.x 将废弃
|
||||
Provider string `json:"provider"` // 通知提供商
|
||||
ProviderAccessId string `json:"providerAccessId"` // 通知提供商授权记录 ID
|
||||
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置
|
||||
Subject string `json:"subject"` // 通知主题
|
||||
Message string `json:"message"` // 通知内容
|
||||
SkipOnAllPrevSkipped bool `json:"skipOnAllPrevSkipped"` // 前序节点均已跳过时是否跳过
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForCondition struct {
|
||||
Expression expr.Expr `json:"expression"` // 条件表达式
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
|
||||
return WorkflowNodeConfigForApply{
|
||||
Domains: xmaps.GetString(n.Config, "domains"),
|
||||
ContactEmail: xmaps.GetString(n.Config, "contactEmail"),
|
||||
Provider: xmaps.GetString(n.Config, "provider"),
|
||||
ProviderAccessId: xmaps.GetString(n.Config, "providerAccessId"),
|
||||
ProviderConfig: xmaps.GetKVMapAny(n.Config, "providerConfig"),
|
||||
CAProvider: xmaps.GetString(n.Config, "caProvider"),
|
||||
CAProviderAccessId: xmaps.GetString(n.Config, "caProviderAccessId"),
|
||||
CAProviderConfig: xmaps.GetKVMapAny(n.Config, "caProviderConfig"),
|
||||
KeyAlgorithm: xmaps.GetOrDefaultString(n.Config, "keyAlgorithm", string(CertificateKeyAlgorithmTypeRSA2048)),
|
||||
Nameservers: xmaps.GetString(n.Config, "nameservers"),
|
||||
DnsPropagationWait: xmaps.GetInt32(n.Config, "dnsPropagationWait"),
|
||||
DnsPropagationTimeout: xmaps.GetInt32(n.Config, "dnsPropagationTimeout"),
|
||||
DnsTTL: xmaps.GetInt32(n.Config, "dnsTTL"),
|
||||
DisableFollowCNAME: xmaps.GetBool(n.Config, "disableFollowCNAME"),
|
||||
DisableARI: xmaps.GetBool(n.Config, "disableARI"),
|
||||
SkipBeforeExpiryDays: xmaps.GetOrDefaultInt32(n.Config, "skipBeforeExpiryDays", 30),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForUpload() WorkflowNodeConfigForUpload {
|
||||
return WorkflowNodeConfigForUpload{
|
||||
Certificate: xmaps.GetString(n.Config, "certificate"),
|
||||
PrivateKey: xmaps.GetString(n.Config, "privateKey"),
|
||||
Domains: xmaps.GetString(n.Config, "domains"),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForMonitor() WorkflowNodeConfigForMonitor {
|
||||
host := xmaps.GetString(n.Config, "host")
|
||||
return WorkflowNodeConfigForMonitor{
|
||||
Host: host,
|
||||
Port: xmaps.GetOrDefaultInt32(n.Config, "port", 443),
|
||||
Domain: xmaps.GetOrDefaultString(n.Config, "domain", host),
|
||||
RequestPath: xmaps.GetString(n.Config, "path"),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy {
|
||||
return WorkflowNodeConfigForDeploy{
|
||||
Certificate: xmaps.GetString(n.Config, "certificate"),
|
||||
Provider: xmaps.GetString(n.Config, "provider"),
|
||||
ProviderAccessId: xmaps.GetString(n.Config, "providerAccessId"),
|
||||
ProviderConfig: xmaps.GetKVMapAny(n.Config, "providerConfig"),
|
||||
SkipOnLastSucceeded: xmaps.GetBool(n.Config, "skipOnLastSucceeded"),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify {
|
||||
return WorkflowNodeConfigForNotify{
|
||||
Channel: xmaps.GetString(n.Config, "channel"),
|
||||
Provider: xmaps.GetString(n.Config, "provider"),
|
||||
ProviderAccessId: xmaps.GetString(n.Config, "providerAccessId"),
|
||||
ProviderConfig: xmaps.GetKVMapAny(n.Config, "providerConfig"),
|
||||
Subject: xmaps.GetString(n.Config, "subject"),
|
||||
Message: xmaps.GetString(n.Config, "message"),
|
||||
SkipOnAllPrevSkipped: xmaps.GetBool(n.Config, "skipOnAllPrevSkipped"),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition {
|
||||
expression := n.Config["expression"]
|
||||
if expression == nil {
|
||||
return WorkflowNodeConfigForCondition{}
|
||||
}
|
||||
|
||||
exprRaw, _ := json.Marshal(expression)
|
||||
expr, err := expr.UnmarshalExpr([]byte(exprRaw))
|
||||
if err != nil {
|
||||
return WorkflowNodeConfigForCondition{}
|
||||
}
|
||||
|
||||
return WorkflowNodeConfigForCondition{
|
||||
Expression: expr,
|
||||
}
|
||||
}
|
||||
|
||||
type WorkflowNodeIO struct {
|
||||
Label string `json:"label"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Required bool `json:"required"`
|
||||
Value any `json:"value"`
|
||||
ValueSelector WorkflowNodeIOValueSelector `json:"valueSelector"`
|
||||
}
|
||||
|
||||
type WorkflowNodeIOValueSelector = expr.ExprValueSelector
|
||||
|
||||
const WorkflowNodeIONameCertificate string = "certificate"
|
||||
30
internal/domain/workflow_log.go
Normal file
30
internal/domain/workflow_log.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package domain
|
||||
|
||||
import "strings"
|
||||
|
||||
const CollectionNameWorkflowLog = "workflow_logs"
|
||||
|
||||
type WorkflowLog struct {
|
||||
Meta
|
||||
WorkflowId string `json:"workflowId" db:"workflowId"`
|
||||
RunId string `json:"workflorunIdwId" db:"runId"`
|
||||
NodeId string `json:"nodeId" db:"nodeId"`
|
||||
NodeName string `json:"nodeName" db:"nodeName"`
|
||||
Timestamp int64 `json:"timestamp" db:"timestamp"` // 毫秒级时间戳
|
||||
Level string `json:"level" db:"level"`
|
||||
Message string `json:"message" db:"message"`
|
||||
Data map[string]any `json:"data" db:"data"`
|
||||
}
|
||||
|
||||
type WorkflowLogs []WorkflowLog
|
||||
|
||||
func (r WorkflowLogs) ErrorString() string {
|
||||
var builder strings.Builder
|
||||
for _, log := range r {
|
||||
if log.Level == "ERROR" {
|
||||
builder.WriteString(log.Message)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(builder.String())
|
||||
}
|
||||
13
internal/domain/workflow_output.go
Normal file
13
internal/domain/workflow_output.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package domain
|
||||
|
||||
const CollectionNameWorkflowOutput = "workflow_output"
|
||||
|
||||
type WorkflowOutput struct {
|
||||
Meta
|
||||
WorkflowId string `json:"workflowId" db:"workflow"`
|
||||
RunId string `json:"runId" db:"runId"`
|
||||
NodeId string `json:"nodeId" db:"nodeId"`
|
||||
Node *WorkflowNode `json:"node" db:"node"`
|
||||
Outputs []WorkflowNodeIO `json:"outputs" db:"outputs"`
|
||||
Succeeded bool `json:"succeeded" db:"succeeded"`
|
||||
}
|
||||
28
internal/domain/workflow_run.go
Normal file
28
internal/domain/workflow_run.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const CollectionNameWorkflowRun = "workflow_run"
|
||||
|
||||
type WorkflowRun struct {
|
||||
Meta
|
||||
WorkflowId string `json:"workflowId" db:"workflowId"`
|
||||
Status WorkflowRunStatusType `json:"status" db:"status"`
|
||||
Trigger WorkflowTriggerType `json:"trigger" db:"trigger"`
|
||||
StartedAt time.Time `json:"startedAt" db:"startedAt"`
|
||||
EndedAt time.Time `json:"endedAt" db:"endedAt"`
|
||||
Detail *WorkflowNode `json:"detail" db:"detail"`
|
||||
Error string `json:"error" db:"error"`
|
||||
}
|
||||
|
||||
type WorkflowRunStatusType string
|
||||
|
||||
const (
|
||||
WorkflowRunStatusTypePending WorkflowRunStatusType = "pending"
|
||||
WorkflowRunStatusTypeRunning WorkflowRunStatusType = "running"
|
||||
WorkflowRunStatusTypeSucceeded WorkflowRunStatusType = "succeeded"
|
||||
WorkflowRunStatusTypeFailed WorkflowRunStatusType = "failed"
|
||||
WorkflowRunStatusTypeCanceled WorkflowRunStatusType = "canceled"
|
||||
)
|
||||
@@ -1,123 +0,0 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"github.com/usual2970/certimate/internal/applicant"
|
||||
"github.com/usual2970/certimate/internal/deployer"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
type Phase string
|
||||
|
||||
const (
|
||||
checkPhase Phase = "check"
|
||||
applyPhase Phase = "apply"
|
||||
deployPhase Phase = "deploy"
|
||||
)
|
||||
|
||||
func deploy(ctx context.Context, record *models.Record) error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
app.GetApp().Logger().Error("部署失败", "err", r)
|
||||
}
|
||||
}()
|
||||
var certificate *applicant.Certificate
|
||||
|
||||
history := NewHistory(record)
|
||||
defer history.commit()
|
||||
|
||||
// ############1.检查域名配置
|
||||
history.record(checkPhase, "开始检查", nil)
|
||||
|
||||
currRecord, err := app.GetApp().Dao().FindRecordById("domains", record.Id)
|
||||
if err != nil {
|
||||
app.GetApp().Logger().Error("获取记录失败", "err", err)
|
||||
history.record(checkPhase, "获取域名配置失败", &RecordInfo{Err: err})
|
||||
return err
|
||||
}
|
||||
history.record(checkPhase, "获取记录成功", nil)
|
||||
|
||||
cert := currRecord.GetString("certificate")
|
||||
expiredAt := currRecord.GetDateTime("expiredAt").Time()
|
||||
|
||||
if cert != "" && time.Until(expiredAt) > time.Hour*24*10 && currRecord.GetBool("deployed") {
|
||||
app.GetApp().Logger().Info("证书在有效期内")
|
||||
history.record(checkPhase, "证书在有效期内且已部署,跳过", &RecordInfo{
|
||||
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
|
||||
}, true)
|
||||
|
||||
// 跳过的情况也算成功
|
||||
history.setWholeSuccess(true)
|
||||
return nil
|
||||
}
|
||||
history.record(checkPhase, "检查通过", nil, true)
|
||||
|
||||
// ############2.申请证书
|
||||
history.record(applyPhase, "开始申请", nil)
|
||||
|
||||
if cert != "" && time.Until(expiredAt) > time.Hour*24 {
|
||||
history.record(applyPhase, "证书在有效期内,跳过", &RecordInfo{
|
||||
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
|
||||
})
|
||||
} else {
|
||||
applicant, err := applicant.Get(currRecord)
|
||||
if err != nil {
|
||||
history.record(applyPhase, "获取applicant失败", &RecordInfo{Err: err})
|
||||
app.GetApp().Logger().Error("获取applicant失败", "err", err)
|
||||
return err
|
||||
}
|
||||
certificate, err = applicant.Apply()
|
||||
if err != nil {
|
||||
history.record(applyPhase, "申请证书失败", &RecordInfo{Err: err})
|
||||
app.GetApp().Logger().Error("申请证书失败", "err", err)
|
||||
return err
|
||||
}
|
||||
history.record(applyPhase, "申请证书成功", &RecordInfo{
|
||||
Info: []string{fmt.Sprintf("证书地址: %s", certificate.CertUrl)},
|
||||
})
|
||||
history.setCert(certificate)
|
||||
}
|
||||
|
||||
history.record(applyPhase, "保存证书成功", nil, true)
|
||||
|
||||
// ############3.部署证书
|
||||
history.record(deployPhase, "开始部署", nil, false)
|
||||
deployers, err := deployer.Gets(currRecord, certificate)
|
||||
if err != nil {
|
||||
history.record(deployPhase, "获取deployer失败", &RecordInfo{Err: err})
|
||||
app.GetApp().Logger().Error("获取deployer失败", "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 没有部署配置,也算成功
|
||||
if len(deployers) == 0 {
|
||||
history.record(deployPhase, "没有部署配置", &RecordInfo{Info: []string{"没有部署配置"}})
|
||||
history.setWholeSuccess(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, deployer := range deployers {
|
||||
if err = deployer.Deploy(ctx); err != nil {
|
||||
|
||||
app.GetApp().Logger().Error("部署失败", "err", err)
|
||||
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfo()})
|
||||
return err
|
||||
}
|
||||
history.record(deployPhase, fmt.Sprintf("[%s]-部署成功", deployer.GetID()), &RecordInfo{
|
||||
Info: deployer.GetInfo(),
|
||||
}, false)
|
||||
|
||||
}
|
||||
|
||||
app.GetApp().Logger().Info("部署成功")
|
||||
history.record(deployPhase, "部署成功", nil, true)
|
||||
|
||||
history.setWholeSuccess(true)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
func create(ctx context.Context, record *models.Record) error {
|
||||
if !record.GetBool("enabled") {
|
||||
return nil
|
||||
}
|
||||
|
||||
if record.GetBool("rightnow") {
|
||||
go func() {
|
||||
if err := deploy(ctx, record); err != nil {
|
||||
app.GetApp().Logger().Error("deploy failed", "err", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
scheduler := app.GetScheduler()
|
||||
|
||||
err := scheduler.Add(record.Id, record.GetString("crontab"), func() {
|
||||
deploy(ctx, record)
|
||||
})
|
||||
if err != nil {
|
||||
app.GetApp().Logger().Error("add cron job failed", "err", err)
|
||||
return fmt.Errorf("add cron job failed: %w", err)
|
||||
}
|
||||
app.GetApp().Logger().Error("add cron job failed", "domain", record.GetString("domain"))
|
||||
|
||||
scheduler.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func update(ctx context.Context, record *models.Record) error {
|
||||
scheduler := app.GetScheduler()
|
||||
scheduler.Remove(record.Id)
|
||||
if !record.GetBool("enabled") {
|
||||
return nil
|
||||
}
|
||||
|
||||
if record.GetBool("rightnow") {
|
||||
go func() {
|
||||
if err := deploy(ctx, record); err != nil {
|
||||
app.GetApp().Logger().Error("deploy failed", "err", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
err := scheduler.Add(record.Id, record.GetString("crontab"), func() {
|
||||
deploy(ctx, record)
|
||||
})
|
||||
if err != nil {
|
||||
app.GetApp().Logger().Error("update cron job failed", "err", err)
|
||||
return fmt.Errorf("update cron job failed: %w", err)
|
||||
}
|
||||
app.GetApp().Logger().Info("update cron job success", "domain", record.GetString("domain"))
|
||||
|
||||
scheduler.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func delete(_ context.Context, record *models.Record) error {
|
||||
scheduler := app.GetScheduler()
|
||||
|
||||
scheduler.Remove(record.Id)
|
||||
scheduler.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func setRightnow(ctx context.Context, record *models.Record, ok bool) error {
|
||||
record.Set("rightnow", ok)
|
||||
return app.GetApp().Dao().SaveRecord(record)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
const tableName = "domains"
|
||||
|
||||
func AddEvent() error {
|
||||
app := app.GetApp()
|
||||
|
||||
app.OnRecordAfterCreateRequest(tableName).Add(func(e *core.RecordCreateEvent) error {
|
||||
return create(e.HttpContext.Request().Context(), e.Record)
|
||||
})
|
||||
|
||||
app.OnRecordAfterUpdateRequest(tableName).Add(func(e *core.RecordUpdateEvent) error {
|
||||
return update(e.HttpContext.Request().Context(), e.Record)
|
||||
})
|
||||
|
||||
app.OnRecordAfterDeleteRequest(tableName).Add(func(e *core.RecordDeleteEvent) error {
|
||||
return delete(e.HttpContext.Request().Context(), e.Record)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"github.com/usual2970/certimate/internal/applicant"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
"github.com/usual2970/certimate/internal/utils/xtime"
|
||||
)
|
||||
|
||||
type historyItem struct {
|
||||
Time string `json:"time"`
|
||||
Message string `json:"message"`
|
||||
Error string `json:"error"`
|
||||
Info []string `json:"info"`
|
||||
}
|
||||
|
||||
type RecordInfo struct {
|
||||
Err error `json:"err"`
|
||||
Info []string `json:"info"`
|
||||
}
|
||||
|
||||
type history struct {
|
||||
Domain string `json:"domain"`
|
||||
Log map[Phase][]historyItem `json:"log"`
|
||||
Phase Phase `json:"phase"`
|
||||
PhaseSuccess bool `json:"phaseSuccess"`
|
||||
DeployedAt string `json:"deployedAt"`
|
||||
Cert *applicant.Certificate `json:"cert"`
|
||||
WholeSuccess bool `json:"wholeSuccess"`
|
||||
}
|
||||
|
||||
func NewHistory(record *models.Record) *history {
|
||||
return &history{
|
||||
Domain: record.Id,
|
||||
DeployedAt: time.Now().UTC().Format("2006-01-02T15:04:05Z"),
|
||||
Log: make(map[Phase][]historyItem),
|
||||
Phase: checkPhase,
|
||||
PhaseSuccess: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *history) record(phase Phase, msg string, info *RecordInfo, pass ...bool) {
|
||||
if info == nil {
|
||||
info = &RecordInfo{}
|
||||
}
|
||||
a.Phase = phase
|
||||
if len(pass) > 0 {
|
||||
a.PhaseSuccess = pass[0]
|
||||
}
|
||||
|
||||
errMsg := ""
|
||||
if info.Err != nil {
|
||||
errMsg = info.Err.Error()
|
||||
a.PhaseSuccess = false
|
||||
}
|
||||
|
||||
a.Log[phase] = append(a.Log[phase], historyItem{
|
||||
Message: msg,
|
||||
Error: errMsg,
|
||||
Info: info.Info,
|
||||
Time: xtime.BeijingTimeStr(),
|
||||
})
|
||||
}
|
||||
|
||||
func (a *history) setCert(cert *applicant.Certificate) {
|
||||
a.Cert = cert
|
||||
}
|
||||
|
||||
func (a *history) setWholeSuccess(success bool) {
|
||||
a.WholeSuccess = success
|
||||
}
|
||||
|
||||
func (a *history) commit() error {
|
||||
collection, err := app.GetApp().Dao().FindCollectionByNameOrId("deployments")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := models.NewRecord(collection)
|
||||
|
||||
record.Set("domain", a.Domain)
|
||||
record.Set("deployedAt", a.DeployedAt)
|
||||
record.Set("log", a.Log)
|
||||
record.Set("phase", string(a.Phase))
|
||||
record.Set("phaseSuccess", a.PhaseSuccess)
|
||||
record.Set("wholeSuccess", a.WholeSuccess)
|
||||
|
||||
if err := app.GetApp().Dao().SaveRecord(record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
domainRecord, err := app.GetApp().Dao().FindRecordById("domains", a.Domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
domainRecord.Set("lastDeployedAt", a.DeployedAt)
|
||||
domainRecord.Set("lastDeployment", record.Id)
|
||||
domainRecord.Set("rightnow", false)
|
||||
if a.Phase == deployPhase && a.PhaseSuccess {
|
||||
domainRecord.Set("deployed", true)
|
||||
}
|
||||
cert := a.Cert
|
||||
if cert != nil {
|
||||
domainRecord.Set("certUrl", cert.CertUrl)
|
||||
domainRecord.Set("certStableUrl", cert.CertStableUrl)
|
||||
domainRecord.Set("privateKey", cert.PrivateKey)
|
||||
domainRecord.Set("certificate", cert.Certificate)
|
||||
domainRecord.Set("issuerCertificate", cert.IssuerCertificate)
|
||||
domainRecord.Set("csr", cert.Csr)
|
||||
domainRecord.Set("expiredAt", time.Now().Add(time.Hour*24*90))
|
||||
}
|
||||
|
||||
if err := app.GetApp().Dao().SaveRecord(domainRecord); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/usual2970/certimate/internal/notify"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
func InitSchedule() {
|
||||
// 查询所有启用的域名
|
||||
records, err := app.GetApp().Dao().FindRecordsByFilter("domains", "enabled=true", "-id", 500, 0)
|
||||
if err != nil {
|
||||
app.GetApp().Logger().Error("查询所有启用的域名失败", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 加入到定时任务
|
||||
for _, record := range records {
|
||||
if err := app.GetScheduler().Add(record.Id, record.GetString("crontab"), func() {
|
||||
if err := deploy(context.Background(), record); err != nil {
|
||||
app.GetApp().Logger().Error("部署失败", "err", err)
|
||||
return
|
||||
}
|
||||
}); err != nil {
|
||||
app.GetApp().Logger().Error("加入到定时任务失败", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 过期提醒
|
||||
app.GetScheduler().Add("expire", "0 0 * * *", func() {
|
||||
notify.PushExpireMsg()
|
||||
})
|
||||
|
||||
// 启动定时任务
|
||||
app.GetScheduler().Start()
|
||||
app.GetApp().Logger().Info("定时任务启动成功", "total", app.GetScheduler().Total())
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
"github.com/usual2970/certimate/internal/utils/xtime"
|
||||
)
|
||||
|
||||
type msg struct {
|
||||
subject string
|
||||
message string
|
||||
}
|
||||
|
||||
const (
|
||||
defaultExpireSubject = "您有{COUNT}张证书即将过期"
|
||||
defaultExpireMsg = "有{COUNT}张证书即将过期,域名分别为{DOMAINS},请保持关注!"
|
||||
)
|
||||
|
||||
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 {
|
||||
app.GetApp().Logger().Error("find expired domains by filter", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 组装消息
|
||||
msg := buildMsg(records)
|
||||
|
||||
if msg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := Send(msg.subject, msg.message); err != nil {
|
||||
app.GetApp().Logger().Error("send expire msg", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
type notifyTemplates struct {
|
||||
NotifyTemplates []notifyTemplate `json:"notifyTemplates"`
|
||||
}
|
||||
|
||||
type notifyTemplate struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func buildMsg(records []*models.Record) *msg {
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 查询模板信息
|
||||
templateRecord, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='templates'")
|
||||
title := defaultExpireSubject
|
||||
content := defaultExpireMsg
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 替换变量
|
||||
count := len(records)
|
||||
domains := make([]string, count)
|
||||
|
||||
for i, record := range records {
|
||||
domains[i] = record.GetString("domain")
|
||||
}
|
||||
|
||||
countStr := strconv.Itoa(count)
|
||||
domainStr := strings.Join(domains, ",")
|
||||
|
||||
title = strings.ReplaceAll(title, "{COUNT}", countStr)
|
||||
title = strings.ReplaceAll(title, "{DOMAINS}", domainStr)
|
||||
|
||||
content = strings.ReplaceAll(content, "{COUNT}", countStr)
|
||||
content = strings.ReplaceAll(content, "{DOMAINS}", domainStr)
|
||||
|
||||
// 返回消息
|
||||
return &msg{
|
||||
subject: title,
|
||||
message: content,
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
type Mail struct {
|
||||
senderAddress string
|
||||
smtpHostAddr string
|
||||
smtpHostPort string
|
||||
smtpAuth smtp.Auth
|
||||
receiverAddresses string
|
||||
}
|
||||
|
||||
func NewMail(senderAddress, receiverAddresses, smtpHostAddr, smtpHostPort string) *Mail {
|
||||
if(smtpHostPort == "") {
|
||||
smtpHostPort = "25"
|
||||
}
|
||||
|
||||
return &Mail{
|
||||
senderAddress: senderAddress,
|
||||
smtpHostAddr: smtpHostAddr,
|
||||
smtpHostPort: smtpHostPort,
|
||||
receiverAddresses: receiverAddresses,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mail) SetAuth(username, password string) {
|
||||
m.smtpAuth = smtp.PlainAuth("", username, password, m.smtpHostAddr)
|
||||
}
|
||||
|
||||
func (m *Mail) Send(ctx context.Context, subject, message string) error {
|
||||
// 构建邮件
|
||||
from := m.senderAddress
|
||||
to := []string{m.receiverAddresses}
|
||||
msg := []byte(
|
||||
"From: " + from + "\r\n" +
|
||||
"To: " + m.receiverAddresses + "\r\n" +
|
||||
"Subject: " + subject + "\r\n" +
|
||||
"\r\n" +
|
||||
message + "\r\n")
|
||||
|
||||
var smtpAddress string
|
||||
// 组装邮箱服务器地址
|
||||
if(m.smtpHostPort == "25"){
|
||||
smtpAddress = m.smtpHostAddr
|
||||
}else{
|
||||
smtpAddress = m.smtpHostAddr + ":" + m.smtpHostPort
|
||||
}
|
||||
|
||||
err := smtp.SendMail(smtpAddress, m.smtpAuth, from, to, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
74
internal/notify/notifier.go
Normal file
74
internal/notify/notifier.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/certimate-go/certimate/internal/domain"
|
||||
"github.com/certimate-go/certimate/internal/repository"
|
||||
"github.com/certimate-go/certimate/pkg/core"
|
||||
)
|
||||
|
||||
type Notifier interface {
|
||||
Notify(ctx context.Context) error
|
||||
}
|
||||
|
||||
type NotifierWithWorkflowNodeConfig struct {
|
||||
Node *domain.WorkflowNode
|
||||
Logger *slog.Logger
|
||||
Subject string
|
||||
Message string
|
||||
}
|
||||
|
||||
func NewWithWorkflowNode(config NotifierWithWorkflowNodeConfig) (Notifier, error) {
|
||||
if config.Node == nil {
|
||||
return nil, fmt.Errorf("node is nil")
|
||||
}
|
||||
if config.Node.Type != domain.WorkflowNodeTypeNotify {
|
||||
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeNotify))
|
||||
}
|
||||
|
||||
nodeCfg := config.Node.GetConfigForNotify()
|
||||
options := ¬ifierProviderOptions{
|
||||
Provider: domain.NotificationProviderType(nodeCfg.Provider),
|
||||
ProviderAccessConfig: make(map[string]any),
|
||||
ProviderServiceConfig: nodeCfg.ProviderConfig,
|
||||
}
|
||||
|
||||
accessRepo := repository.NewAccessRepository()
|
||||
if nodeCfg.ProviderAccessId != "" {
|
||||
access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err)
|
||||
} else {
|
||||
options.ProviderAccessConfig = access.Config
|
||||
}
|
||||
}
|
||||
|
||||
notifier, err := createNotifierProvider(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
notifier.SetLogger(config.Logger)
|
||||
}
|
||||
|
||||
return ¬ifierImpl{
|
||||
provider: notifier,
|
||||
subject: config.Subject,
|
||||
message: config.Message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type notifierImpl struct {
|
||||
provider core.Notifier
|
||||
subject string
|
||||
message string
|
||||
}
|
||||
|
||||
var _ Notifier = (*notifierImpl)(nil)
|
||||
|
||||
func (n *notifierImpl) Notify(ctx context.Context) error {
|
||||
_, err := n.provider.Notify(ctx, n.subject, n.message)
|
||||
return err
|
||||
}
|
||||
@@ -2,24 +2,20 @@ package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
stdhttp "net/http"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"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"
|
||||
"github.com/certimate-go/certimate/internal/domain"
|
||||
"github.com/certimate-go/certimate/internal/repository"
|
||||
"github.com/certimate-go/certimate/pkg/core"
|
||||
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
|
||||
)
|
||||
|
||||
func Send(title, content string) error {
|
||||
// 获取所有的推送渠道
|
||||
notifiers, err := getNotifiers()
|
||||
// Deprecated: v0.4.x 将废弃
|
||||
func SendToAllChannels(subject, message string) error {
|
||||
notifiers, err := getEnabledNotifiers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -27,167 +23,59 @@ 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)
|
||||
// Deprecated: v0.4.x 将废弃
|
||||
func SendToChannel(subject, message string, channel string, channelConfig map[string]any) error {
|
||||
notifier, err := createNotifierProviderUseGlobalSettings(domain.NotifyChannelType(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'")
|
||||
// Deprecated: v0.4.x 将废弃
|
||||
func getEnabledNotifiers() ([]core.Notifier, error) {
|
||||
settingsRepo := repository.NewSettingsRepository()
|
||||
settings, err := settingsRepo.GetByName(context.Background(), "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 := json.Unmarshal([]byte(settings.Content), &rs); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal notifyChannels error: %w", err)
|
||||
}
|
||||
|
||||
notifiers := make([]core.Notifier, 0)
|
||||
for k, v := range rs {
|
||||
|
||||
if !getBool(v, "enabled") {
|
||||
if !xmaps.GetBool(v, "enabled") {
|
||||
continue
|
||||
}
|
||||
|
||||
notifier, err := getNotifier(k, v)
|
||||
notifier, err := createNotifierProviderUseGlobalSettings(domain.NotifyChannelType(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
|
||||
case domain.NotifyChannelServerChan:
|
||||
return getServerChanNotifier(conf), nil
|
||||
case domain.NotifyChannelMail:
|
||||
return getMailNotifier(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 getServerChanNotifier(conf map[string]any) notifyPackage.Notifier {
|
||||
rs := http.New()
|
||||
|
||||
rs.AddReceivers(&http.Webhook{
|
||||
URL: getString(conf, "url"),
|
||||
Header: stdhttp.Header{},
|
||||
ContentType: "application/json",
|
||||
Method: stdhttp.MethodPost,
|
||||
BuildPayload: func(subject, message string) (payload any) {
|
||||
return map[string]string{
|
||||
"text": subject,
|
||||
"desp": message,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
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 getMailNotifier(conf map[string]any) notifyPackage.Notifier {
|
||||
rs := NewMail(getString(conf, "senderAddress"),getString(conf,"receiverAddress"), getString(conf, "smtpHostAddr"), getString(conf, "smtpHostPort"))
|
||||
|
||||
rs.SetAuth(getString(conf, "username"), getString(conf, "password"))
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
182
internal/notify/providers.go
Normal file
182
internal/notify/providers.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/certimate-go/certimate/internal/domain"
|
||||
"github.com/certimate-go/certimate/pkg/core"
|
||||
pDingTalkBot "github.com/certimate-go/certimate/pkg/core/notifier/providers/dingtalkbot"
|
||||
pDiscordBot "github.com/certimate-go/certimate/pkg/core/notifier/providers/discordbot"
|
||||
pEmail "github.com/certimate-go/certimate/pkg/core/notifier/providers/email"
|
||||
pLarkBot "github.com/certimate-go/certimate/pkg/core/notifier/providers/larkbot"
|
||||
pMattermost "github.com/certimate-go/certimate/pkg/core/notifier/providers/mattermost"
|
||||
pSlackBot "github.com/certimate-go/certimate/pkg/core/notifier/providers/slackbot"
|
||||
pTelegramBot "github.com/certimate-go/certimate/pkg/core/notifier/providers/telegrambot"
|
||||
pWebhook "github.com/certimate-go/certimate/pkg/core/notifier/providers/webhook"
|
||||
pWeComBot "github.com/certimate-go/certimate/pkg/core/notifier/providers/wecombot"
|
||||
xhttp "github.com/certimate-go/certimate/pkg/utils/http"
|
||||
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
|
||||
)
|
||||
|
||||
type notifierProviderOptions struct {
|
||||
Provider domain.NotificationProviderType
|
||||
ProviderAccessConfig map[string]any
|
||||
ProviderServiceConfig map[string]any
|
||||
}
|
||||
|
||||
func createNotifierProvider(options *notifierProviderOptions) (core.Notifier, error) {
|
||||
/*
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
switch options.Provider {
|
||||
case domain.NotificationProviderTypeDingTalkBot:
|
||||
{
|
||||
access := domain.AccessConfigForDingTalkBot{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
return pDingTalkBot.NewNotifierProvider(&pDingTalkBot.NotifierProviderConfig{
|
||||
WebhookUrl: access.WebhookUrl,
|
||||
Secret: access.Secret,
|
||||
})
|
||||
}
|
||||
|
||||
case domain.NotificationProviderTypeDiscordBot:
|
||||
{
|
||||
access := domain.AccessConfigForDiscordBot{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
return pDiscordBot.NewNotifierProvider(&pDiscordBot.NotifierProviderConfig{
|
||||
BotToken: access.BotToken,
|
||||
ChannelId: xmaps.GetOrDefaultString(options.ProviderServiceConfig, "channelId", access.DefaultChannelId),
|
||||
})
|
||||
}
|
||||
|
||||
case domain.NotificationProviderTypeEmail:
|
||||
{
|
||||
access := domain.AccessConfigForEmail{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
return pEmail.NewNotifierProvider(&pEmail.NotifierProviderConfig{
|
||||
SmtpHost: access.SmtpHost,
|
||||
SmtpPort: access.SmtpPort,
|
||||
SmtpTls: access.SmtpTls,
|
||||
Username: access.Username,
|
||||
Password: access.Password,
|
||||
SenderAddress: xmaps.GetOrDefaultString(options.ProviderServiceConfig, "senderAddress", access.DefaultSenderAddress),
|
||||
SenderName: xmaps.GetOrDefaultString(options.ProviderServiceConfig, "senderName", access.DefaultSenderName),
|
||||
ReceiverAddress: xmaps.GetOrDefaultString(options.ProviderServiceConfig, "receiverAddress", access.DefaultReceiverAddress),
|
||||
})
|
||||
}
|
||||
|
||||
case domain.NotificationProviderTypeLarkBot:
|
||||
{
|
||||
access := domain.AccessConfigForLarkBot{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
return pLarkBot.NewNotifierProvider(&pLarkBot.NotifierProviderConfig{
|
||||
WebhookUrl: access.WebhookUrl,
|
||||
})
|
||||
}
|
||||
|
||||
case domain.NotificationProviderTypeMattermost:
|
||||
{
|
||||
access := domain.AccessConfigForMattermost{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
return pMattermost.NewNotifierProvider(&pMattermost.NotifierProviderConfig{
|
||||
ServerUrl: access.ServerUrl,
|
||||
Username: access.Username,
|
||||
Password: access.Password,
|
||||
ChannelId: xmaps.GetOrDefaultString(options.ProviderServiceConfig, "channelId", access.DefaultChannelId),
|
||||
})
|
||||
}
|
||||
|
||||
case domain.NotificationProviderTypeSlackBot:
|
||||
{
|
||||
access := domain.AccessConfigForSlackBot{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
return pSlackBot.NewNotifierProvider(&pSlackBot.NotifierProviderConfig{
|
||||
BotToken: access.BotToken,
|
||||
ChannelId: xmaps.GetOrDefaultString(options.ProviderServiceConfig, "channelId", access.DefaultChannelId),
|
||||
})
|
||||
}
|
||||
|
||||
case domain.NotificationProviderTypeTelegramBot:
|
||||
{
|
||||
access := domain.AccessConfigForTelegramBot{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
return pTelegramBot.NewNotifierProvider(&pTelegramBot.NotifierProviderConfig{
|
||||
BotToken: access.BotToken,
|
||||
ChatId: xmaps.GetOrDefaultInt64(options.ProviderServiceConfig, "chatId", access.DefaultChatId),
|
||||
})
|
||||
}
|
||||
|
||||
case domain.NotificationProviderTypeWebhook:
|
||||
{
|
||||
access := domain.AccessConfigForWebhook{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
mergedHeaders := make(map[string]string)
|
||||
if defaultHeadersString := access.HeadersString; defaultHeadersString != "" {
|
||||
h, err := xhttp.ParseHeaders(defaultHeadersString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse webhook headers: %w", err)
|
||||
}
|
||||
for key := range h {
|
||||
mergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key)
|
||||
}
|
||||
}
|
||||
if extendedHeadersString := xmaps.GetString(options.ProviderServiceConfig, "headers"); extendedHeadersString != "" {
|
||||
h, err := xhttp.ParseHeaders(extendedHeadersString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse webhook headers: %w", err)
|
||||
}
|
||||
for key := range h {
|
||||
mergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key)
|
||||
}
|
||||
}
|
||||
|
||||
return pWebhook.NewNotifierProvider(&pWebhook.NotifierProviderConfig{
|
||||
WebhookUrl: access.Url,
|
||||
WebhookData: xmaps.GetOrDefaultString(options.ProviderServiceConfig, "webhookData", access.DefaultDataForNotification),
|
||||
Method: access.Method,
|
||||
Headers: mergedHeaders,
|
||||
AllowInsecureConnections: access.AllowInsecureConnections,
|
||||
})
|
||||
}
|
||||
|
||||
case domain.NotificationProviderTypeWeComBot:
|
||||
{
|
||||
access := domain.AccessConfigForWeComBot{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
return pWeComBot.NewNotifierProvider(&pWeComBot.NotifierProviderConfig{
|
||||
WebhookUrl: access.WebhookUrl,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported notifier provider '%s'", options.Provider)
|
||||
}
|
||||
108
internal/notify/providers_deprecated.go
Normal file
108
internal/notify/providers_deprecated.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/certimate-go/certimate/internal/domain"
|
||||
"github.com/certimate-go/certimate/pkg/core"
|
||||
pBark "github.com/certimate-go/certimate/pkg/core/notifier/providers/bark"
|
||||
pDingTalk "github.com/certimate-go/certimate/pkg/core/notifier/providers/dingtalkbot"
|
||||
pEmail "github.com/certimate-go/certimate/pkg/core/notifier/providers/email"
|
||||
pGotify "github.com/certimate-go/certimate/pkg/core/notifier/providers/gotify"
|
||||
pLark "github.com/certimate-go/certimate/pkg/core/notifier/providers/larkbot"
|
||||
pMattermost "github.com/certimate-go/certimate/pkg/core/notifier/providers/mattermost"
|
||||
pPushover "github.com/certimate-go/certimate/pkg/core/notifier/providers/pushover"
|
||||
pPushPlus "github.com/certimate-go/certimate/pkg/core/notifier/providers/pushplus"
|
||||
pServerChan "github.com/certimate-go/certimate/pkg/core/notifier/providers/serverchan"
|
||||
pTelegram "github.com/certimate-go/certimate/pkg/core/notifier/providers/telegrambot"
|
||||
pWebhook "github.com/certimate-go/certimate/pkg/core/notifier/providers/webhook"
|
||||
pWeCom "github.com/certimate-go/certimate/pkg/core/notifier/providers/wecombot"
|
||||
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
|
||||
)
|
||||
|
||||
// Deprecated: v0.4.x 将废弃
|
||||
func createNotifierProviderUseGlobalSettings(channel domain.NotifyChannelType, channelConfig map[string]any) (core.Notifier, error) {
|
||||
/*
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
switch channel {
|
||||
case domain.NotifyChannelTypeBark:
|
||||
return pBark.NewNotifierProvider(&pBark.NotifierProviderConfig{
|
||||
DeviceKey: xmaps.GetString(channelConfig, "deviceKey"),
|
||||
ServerUrl: xmaps.GetString(channelConfig, "serverUrl"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeDingTalk:
|
||||
return pDingTalk.NewNotifierProvider(&pDingTalk.NotifierProviderConfig{
|
||||
WebhookUrl: "https://oapi.dingtalk.com/robot/send?access_token=" + xmaps.GetString(channelConfig, "accessToken"),
|
||||
Secret: xmaps.GetString(channelConfig, "secret"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeEmail:
|
||||
return pEmail.NewNotifierProvider(&pEmail.NotifierProviderConfig{
|
||||
SmtpHost: xmaps.GetString(channelConfig, "smtpHost"),
|
||||
SmtpPort: xmaps.GetInt32(channelConfig, "smtpPort"),
|
||||
SmtpTls: xmaps.GetOrDefaultBool(channelConfig, "smtpTLS", true),
|
||||
Username: xmaps.GetOrDefaultString(channelConfig, "username", xmaps.GetString(channelConfig, "senderAddress")),
|
||||
Password: xmaps.GetString(channelConfig, "password"),
|
||||
SenderAddress: xmaps.GetString(channelConfig, "senderAddress"),
|
||||
ReceiverAddress: xmaps.GetString(channelConfig, "receiverAddress"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeGotify:
|
||||
return pGotify.NewNotifierProvider(&pGotify.NotifierProviderConfig{
|
||||
ServerUrl: xmaps.GetString(channelConfig, "url"),
|
||||
Token: xmaps.GetString(channelConfig, "token"),
|
||||
Priority: xmaps.GetOrDefaultInt64(channelConfig, "priority", 1),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeLark:
|
||||
return pLark.NewNotifierProvider(&pLark.NotifierProviderConfig{
|
||||
WebhookUrl: xmaps.GetString(channelConfig, "webhookUrl"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeMattermost:
|
||||
return pMattermost.NewNotifierProvider(&pMattermost.NotifierProviderConfig{
|
||||
ServerUrl: xmaps.GetString(channelConfig, "serverUrl"),
|
||||
ChannelId: xmaps.GetString(channelConfig, "channelId"),
|
||||
Username: xmaps.GetString(channelConfig, "username"),
|
||||
Password: xmaps.GetString(channelConfig, "password"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypePushover:
|
||||
return pPushover.NewNotifierProvider(&pPushover.NotifierProviderConfig{
|
||||
Token: xmaps.GetString(channelConfig, "token"),
|
||||
User: xmaps.GetString(channelConfig, "user"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypePushPlus:
|
||||
return pPushPlus.NewNotifierProvider(&pPushPlus.NotifierProviderConfig{
|
||||
Token: xmaps.GetString(channelConfig, "token"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeServerChan:
|
||||
return pServerChan.NewNotifierProvider(&pServerChan.NotifierProviderConfig{
|
||||
ServerUrl: xmaps.GetString(channelConfig, "url"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeTelegram:
|
||||
return pTelegram.NewNotifierProvider(&pTelegram.NotifierProviderConfig{
|
||||
BotToken: xmaps.GetString(channelConfig, "apiToken"),
|
||||
ChatId: xmaps.GetInt64(channelConfig, "chatId"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeWebhook:
|
||||
return pWebhook.NewNotifierProvider(&pWebhook.NotifierProviderConfig{
|
||||
WebhookUrl: xmaps.GetString(channelConfig, "url"),
|
||||
AllowInsecureConnections: xmaps.GetBool(channelConfig, "allowInsecureConnections"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeWeCom:
|
||||
return pWeCom.NewNotifierProvider(&pWeCom.NotifierProviderConfig{
|
||||
WebhookUrl: xmaps.GetString(channelConfig, "webhookUrl"),
|
||||
})
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported notifier channel '%s'", channelConfig)
|
||||
}
|
||||
@@ -4,43 +4,44 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/certimate-go/certimate/internal/domain"
|
||||
"github.com/certimate-go/certimate/internal/domain/dtos"
|
||||
)
|
||||
|
||||
// Deprecated: v0.4.x 将废弃
|
||||
const (
|
||||
notifyTestTitle = "测试通知"
|
||||
notifyTestBody = "欢迎使用 Certimate ,这是一条测试通知。"
|
||||
)
|
||||
|
||||
type SettingRepository interface {
|
||||
GetByName(ctx context.Context, name string) (*domain.Setting, error)
|
||||
// Deprecated: v0.4.x 将废弃
|
||||
type settingsRepository interface {
|
||||
GetByName(ctx context.Context, name string) (*domain.Settings, error)
|
||||
}
|
||||
|
||||
// Deprecated: v0.4.x 将废弃
|
||||
type NotifyService struct {
|
||||
settingRepo SettingRepository
|
||||
settingsRepo settingsRepository
|
||||
}
|
||||
|
||||
func NewNotifyService(settingRepo SettingRepository) *NotifyService {
|
||||
// Deprecated: v0.4.x 将废弃
|
||||
func NewNotifyService(settingsRepo settingsRepository) *NotifyService {
|
||||
return &NotifyService{
|
||||
settingRepo: settingRepo,
|
||||
settingsRepo: settingsRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NotifyService) Test(ctx context.Context, req *domain.NotifyTestPushReq) error {
|
||||
setting, err := n.settingRepo.GetByName(ctx, "notifyChannels")
|
||||
// Deprecated: v0.4.x 将废弃
|
||||
func (n *NotifyService) Test(ctx context.Context, req *dtos.NotifyTestPushReq) error {
|
||||
settings, err := n.settingsRepo.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 := settings.GetNotifyChannelConfig(string(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, string(req.Channel), channelConfig)
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package uploader
|
||||
|
||||
import "context"
|
||||
|
||||
// 表示定义证书上传者的抽象类型接口。
|
||||
// 云服务商通常会提供 SSL 证书管理服务,可供用户集中管理证书。
|
||||
// 注意与 `Deployer` 区分,“上传”通常为“部署”的前置操作。
|
||||
type Uploader interface {
|
||||
// 上传证书。
|
||||
//
|
||||
// 入参:
|
||||
// - ctx:上下文。
|
||||
// - certPem:证书 PEM 内容。
|
||||
// - privkeyPem:私钥 PEM 内容。
|
||||
//
|
||||
// 出参:
|
||||
// - res:上传结果。
|
||||
// - err: 错误。
|
||||
Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error)
|
||||
}
|
||||
|
||||
// 表示证书上传结果的数据结构,包含上传后的证书 ID、名称和其他数据。
|
||||
type UploadResult struct {
|
||||
CertId string `json:"certId"`
|
||||
CertName string `json:"certName"`
|
||||
CertData map[string]any `json:"certData,omitempty"`
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
package uploader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cas20200407 "github.com/alibabacloud-go/cas-20200407/v3/client"
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
)
|
||||
|
||||
type AliyunCASUploaderConfig struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
type AliyunCASUploader struct {
|
||||
config *AliyunCASUploaderConfig
|
||||
sdkClient *cas20200407.Client
|
||||
sdkRuntime *util.RuntimeOptions
|
||||
}
|
||||
|
||||
func NewAliyunCASUploader(config *AliyunCASUploaderConfig) (Uploader, error) {
|
||||
client, err := (&AliyunCASUploader{}).createSdkClient(
|
||||
config.AccessKeyId,
|
||||
config.AccessKeySecret,
|
||||
config.Region,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create sdk client: %w", err)
|
||||
}
|
||||
|
||||
return &AliyunCASUploader{
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sdkRuntime: &util.RuntimeOptions{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) {
|
||||
// 解析证书内容
|
||||
certX509, err := x509.ParseCertificateFromPEM(certPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询证书列表,避免重复上传
|
||||
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listusercertificateorder
|
||||
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-getusercertificatedetail
|
||||
listUserCertificateOrderPage := int64(1)
|
||||
listUserCertificateOrderLimit := int64(50)
|
||||
for {
|
||||
listUserCertificateOrderReq := &cas20200407.ListUserCertificateOrderRequest{
|
||||
CurrentPage: tea.Int64(listUserCertificateOrderPage),
|
||||
ShowSize: tea.Int64(listUserCertificateOrderLimit),
|
||||
OrderType: tea.String("CERT"),
|
||||
}
|
||||
listUserCertificateOrderResp, err := u.sdkClient.ListUserCertificateOrderWithOptions(listUserCertificateOrderReq, u.sdkRuntime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute sdk request 'cas.ListUserCertificateOrder': %w", err)
|
||||
}
|
||||
|
||||
if listUserCertificateOrderResp.Body.CertificateOrderList != nil {
|
||||
for _, certDetail := range listUserCertificateOrderResp.Body.CertificateOrderList {
|
||||
if strings.EqualFold(certX509.SerialNumber.Text(16), *certDetail.SerialNo) {
|
||||
getUserCertificateDetailReq := &cas20200407.GetUserCertificateDetailRequest{
|
||||
CertId: certDetail.CertificateId,
|
||||
}
|
||||
getUserCertificateDetailResp, err := u.sdkClient.GetUserCertificateDetailWithOptions(getUserCertificateDetailReq, u.sdkRuntime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute sdk request 'cas.GetUserCertificateDetail': %w", err)
|
||||
}
|
||||
|
||||
var isSameCert bool
|
||||
if *getUserCertificateDetailResp.Body.Cert == certPem {
|
||||
isSameCert = true
|
||||
} else {
|
||||
oldCertX509, err := x509.ParseCertificateFromPEM(*getUserCertificateDetailResp.Body.Cert)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
isSameCert = x509.EqualCertificate(certX509, oldCertX509)
|
||||
}
|
||||
|
||||
// 如果已存在相同证书,直接返回已有的证书信息
|
||||
if isSameCert {
|
||||
return &UploadResult{
|
||||
CertId: fmt.Sprintf("%d", tea.Int64Value(certDetail.CertificateId)),
|
||||
CertName: *certDetail.Name,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if listUserCertificateOrderResp.Body.CertificateOrderList == nil || len(listUserCertificateOrderResp.Body.CertificateOrderList) < int(listUserCertificateOrderLimit) {
|
||||
break
|
||||
} else {
|
||||
listUserCertificateOrderPage += 1
|
||||
if listUserCertificateOrderPage > 99 { // 避免死循环
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成新证书名(需符合阿里云命名规则)
|
||||
var certId, certName string
|
||||
certName = fmt.Sprintf("certimate_%d", time.Now().UnixMilli())
|
||||
|
||||
// 上传新证书
|
||||
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-uploadusercertificate
|
||||
uploadUserCertificateReq := &cas20200407.UploadUserCertificateRequest{
|
||||
Name: tea.String(certName),
|
||||
Cert: tea.String(certPem),
|
||||
Key: tea.String(privkeyPem),
|
||||
}
|
||||
uploadUserCertificateResp, err := u.sdkClient.UploadUserCertificateWithOptions(uploadUserCertificateReq, u.sdkRuntime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute sdk request 'cas.UploadUserCertificate': %w", err)
|
||||
}
|
||||
|
||||
certId = fmt.Sprintf("%d", tea.Int64Value(uploadUserCertificateResp.Body.CertId))
|
||||
return &UploadResult{
|
||||
CertId: certId,
|
||||
CertName: certName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *AliyunCASUploader) createSdkClient(accessKeyId, accessKeySecret, region string) (*cas20200407.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-hangzhou" // CAS 服务默认区域:华东一杭州
|
||||
}
|
||||
|
||||
aConfig := &openapi.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
|
||||
var endpoint string
|
||||
switch region {
|
||||
case "cn-hangzhou":
|
||||
endpoint = "cas.aliyuncs.com"
|
||||
default:
|
||||
endpoint = fmt.Sprintf("cas.%s.aliyuncs.com", region)
|
||||
}
|
||||
aConfig.Endpoint = tea.String(endpoint)
|
||||
|
||||
client, err := cas20200407.NewClient(aConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user