Skip to content

Commit 90c1481

Browse files
committed
添加证书部署工具的初始实现
- 使用Viper和初始YAML配置文件添加了配置管理。 - 实施的证书部署逻辑,包括下载,提取和移动证书。 - 设置记录功能。 - 创建用于定期任务和系统信息检索的调度程序。 - 添加了必要的GO模块和依赖项。 - 更新了.gitignore,以排除构建工件和
1 parent af512ab commit 90c1481

File tree

15 files changed

+1886
-1
lines changed

15 files changed

+1886
-1
lines changed

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,11 @@ go.work.sum
3030
# Editor/IDE
3131
# .idea/
3232
# .vscode/
33+
34+
bin
35+
cert-deploy
36+
certs
37+
ssl
38+
install.sh
39+
Makefile
40+
config.yaml

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,34 @@
1-
# deploy
1+
# 证书自动部署 CLI 工具
2+
3+
## 配置
4+
5+
### 配置文件
6+
7+
创建配置文件 `config.yaml`:
8+
9+
```yaml
10+
# 证书部署配置
11+
server:
12+
accessKey: ""
13+
14+
# 证书存储配置
15+
ssl:
16+
# 证书存储根目录
17+
path: "/etc/nginx/ssl"
18+
```
19+
20+
## 使用方法
21+
22+
### 基本命令
23+
24+
```bash
25+
# 显示帮助信息
26+
cert-deploy --help
27+
28+
# 启动守护进程模式
29+
cert-deploy daemon
30+
```
31+
32+
## 许可证
33+
34+
MIT License

go.mod

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module github.com/orange-juzipi/cert-deploy
2+
3+
go 1.25
4+
5+
require (
6+
connectrpc.com/connect v1.19.0
7+
github.com/spf13/cobra v1.10.1
8+
github.com/spf13/viper v1.21.0
9+
google.golang.org/protobuf v1.36.9
10+
)
11+
12+
require (
13+
github.com/fsnotify/fsnotify v1.9.0 // indirect
14+
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
15+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
16+
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
17+
github.com/sagikazarmark/locafero v0.12.0 // indirect
18+
github.com/spf13/afero v1.15.0 // indirect
19+
github.com/spf13/cast v1.10.0 // indirect
20+
github.com/spf13/pflag v1.0.10 // indirect
21+
github.com/subosito/gotenv v1.6.0 // indirect
22+
go.yaml.in/yaml/v3 v3.0.4 // indirect
23+
golang.org/x/sys v0.36.0 // indirect
24+
golang.org/x/text v0.29.0 // indirect
25+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
26+
)

go.sum

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
connectrpc.com/connect v1.19.0 h1:LuqUbq01PqbtL0o7vn0WMRXzR2nNsiINe5zfcJ24pJM=
2+
connectrpc.com/connect v1.19.0/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
3+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
4+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
7+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
8+
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
9+
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
10+
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
11+
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
12+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
13+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
14+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
15+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
16+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
17+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
18+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
19+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
20+
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
21+
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
22+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
23+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
24+
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
25+
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
26+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
27+
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
28+
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
29+
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
30+
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
31+
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
32+
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
33+
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
34+
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
35+
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
36+
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
37+
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
38+
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
39+
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
40+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
41+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
42+
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
43+
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
44+
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
45+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
46+
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
47+
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
48+
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
49+
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
50+
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
51+
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
52+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
53+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
54+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
55+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
56+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/client/cert_deploy.go

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
package client
2+
3+
import (
4+
"archive/tar"
5+
"archive/zip"
6+
"context"
7+
"fmt"
8+
"io"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"strings"
13+
"time"
14+
15+
"github.com/orange-juzipi/cert-deploy/internal/config"
16+
)
17+
18+
// CertDeployer 证书部署器
19+
type CertDeployer struct {
20+
client *Client
21+
}
22+
23+
// NewCertDeployer 创建证书部署器
24+
func NewCertDeployer(client *Client) *CertDeployer {
25+
return &CertDeployer{
26+
client: client,
27+
}
28+
}
29+
30+
// DeployCertificate 部署证书
31+
func (cd *CertDeployer) DeployCertificate(domain, url string) error {
32+
// 创建certs目录
33+
certsDir := "certs"
34+
if err := os.MkdirAll(certsDir, 0755); err != nil {
35+
return fmt.Errorf("创建证书目录失败: %w", err)
36+
}
37+
38+
// 文件名格式为 {domain}_certificates.zip
39+
fileName := fmt.Sprintf("%s_certificates.zip", domain)
40+
zipFile := filepath.Join(certsDir, fileName)
41+
42+
// 下载zip文件
43+
if err := cd.client.downloadFile(url, zipFile); err != nil {
44+
return fmt.Errorf("下载证书失败: %w", err)
45+
}
46+
47+
fmt.Printf("证书下载完成: %s\n", zipFile)
48+
49+
// 证书文件夹名
50+
folderName := domain + "_certificates"
51+
52+
// 1. 解压zip文件
53+
extractDir := filepath.Join(certsDir, folderName)
54+
if err := cd.extractZip(zipFile, extractDir); err != nil {
55+
return fmt.Errorf("解压证书失败: %w", err)
56+
}
57+
58+
// 2. 移动到配置的SSL目录
59+
sslPath := config.GetConfig().SSL.Path
60+
if err := cd.moveCertificates(extractDir, sslPath, folderName); err != nil {
61+
return fmt.Errorf("移动证书失败: %w", err)
62+
}
63+
64+
// 3. 测试nginx配置
65+
if err := cd.testNginxConfig(); err != nil {
66+
return fmt.Errorf("nginx配置测试失败: %w", err)
67+
}
68+
69+
// 4. 重新加载nginx
70+
if err := cd.reloadNginx(); err != nil {
71+
return fmt.Errorf("nginx重新加载失败: %w", err)
72+
}
73+
74+
fmt.Printf("证书部署完成: %s\n", domain)
75+
return nil
76+
}
77+
78+
// extractZip 解压zip文件
79+
func (cd *CertDeployer) extractZip(zipFile, extractDir string) error {
80+
// 创建解压目录
81+
if err := os.MkdirAll(extractDir, 0755); err != nil {
82+
return fmt.Errorf("创建解压目录失败: %w", err)
83+
}
84+
85+
// 打开zip文件
86+
reader, err := zip.OpenReader(zipFile)
87+
if err != nil {
88+
return fmt.Errorf("打开zip文件失败: %w", err)
89+
}
90+
defer reader.Close()
91+
92+
// 解压所有文件
93+
for _, file := range reader.File {
94+
// 构建目标文件路径
95+
targetPath := filepath.Join(extractDir, file.Name)
96+
97+
// 检查路径安全性
98+
if !strings.HasPrefix(targetPath, filepath.Clean(extractDir)+string(os.PathSeparator)) {
99+
return fmt.Errorf("不安全的文件路径: %s", file.Name)
100+
}
101+
102+
// 创建目录
103+
if file.FileInfo().IsDir() {
104+
if err := os.MkdirAll(targetPath, file.FileInfo().Mode()); err != nil {
105+
return fmt.Errorf("创建目录失败: %w", err)
106+
}
107+
continue
108+
}
109+
110+
// 创建文件目录
111+
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
112+
return fmt.Errorf("创建文件目录失败: %w", err)
113+
}
114+
115+
// 打开zip文件中的文件
116+
rc, err := file.Open()
117+
if err != nil {
118+
return fmt.Errorf("打开zip中的文件失败: %w", err)
119+
}
120+
121+
// 创建目标文件
122+
outFile, err := os.Create(targetPath)
123+
if err != nil {
124+
rc.Close()
125+
return fmt.Errorf("创建文件失败: %w", err)
126+
}
127+
128+
// 复制文件内容
129+
if _, err := io.Copy(outFile, rc); err != nil {
130+
rc.Close()
131+
outFile.Close()
132+
return fmt.Errorf("复制文件内容失败: %w", err)
133+
}
134+
135+
rc.Close()
136+
outFile.Close()
137+
138+
// 设置文件权限
139+
if err := os.Chmod(targetPath, file.FileInfo().Mode()); err != nil {
140+
return fmt.Errorf("设置文件权限失败: %w", err)
141+
}
142+
}
143+
144+
return nil
145+
}
146+
147+
// extractTar 解压tar文件
148+
func (cd *CertDeployer) extractTar(tarFile, extractDir string) error {
149+
// 创建解压目录
150+
if err := os.MkdirAll(extractDir, 0755); err != nil {
151+
return fmt.Errorf("创建解压目录失败: %w", err)
152+
}
153+
154+
// 打开tar文件
155+
file, err := os.Open(tarFile)
156+
if err != nil {
157+
return fmt.Errorf("打开tar文件失败: %w", err)
158+
}
159+
defer file.Close()
160+
161+
// 创建tar reader
162+
tr := tar.NewReader(file)
163+
164+
// 解压所有文件
165+
for {
166+
header, err := tr.Next()
167+
if err == io.EOF {
168+
break
169+
}
170+
if err != nil {
171+
return fmt.Errorf("读取tar文件失败: %w", err)
172+
}
173+
174+
// 构建目标文件路径
175+
targetPath := filepath.Join(extractDir, header.Name)
176+
177+
// 创建目录
178+
if header.Typeflag == tar.TypeDir {
179+
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
180+
return fmt.Errorf("创建目录失败: %w", err)
181+
}
182+
continue
183+
}
184+
185+
// 创建文件
186+
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
187+
return fmt.Errorf("创建文件目录失败: %w", err)
188+
}
189+
190+
outFile, err := os.Create(targetPath)
191+
if err != nil {
192+
return fmt.Errorf("创建文件失败: %w", err)
193+
}
194+
195+
// 复制文件内容
196+
if _, err := io.Copy(outFile, tr); err != nil {
197+
outFile.Close()
198+
return fmt.Errorf("复制文件内容失败: %w", err)
199+
}
200+
outFile.Close()
201+
202+
// 设置文件权限
203+
if err := os.Chmod(targetPath, os.FileMode(header.Mode)); err != nil {
204+
return fmt.Errorf("设置文件权限失败: %w", err)
205+
}
206+
}
207+
208+
return nil
209+
}
210+
211+
// moveCertificates 移动证书文件夹到SSL目录
212+
func (cd *CertDeployer) moveCertificates(sourceDir, sslPath, folderName string) error {
213+
// 确保SSL目录存在
214+
if err := os.MkdirAll(sslPath, 0755); err != nil {
215+
return fmt.Errorf("创建SSL目录失败: %w", err)
216+
}
217+
218+
// 构建目标路径
219+
targetDir := filepath.Join(sslPath, folderName)
220+
221+
// 如果目标目录已存在,先删除
222+
if _, err := os.Stat(targetDir); err == nil {
223+
if err := os.RemoveAll(targetDir); err != nil {
224+
return fmt.Errorf("删除已存在的目录失败: %w", err)
225+
}
226+
}
227+
228+
// 直接移动整个文件夹
229+
if err := os.Rename(sourceDir, targetDir); err != nil {
230+
return fmt.Errorf("移动证书文件夹失败: %w", err)
231+
}
232+
233+
fmt.Printf("移动证书文件夹: %s -> %s\n", sourceDir, targetDir)
234+
return nil
235+
}
236+
237+
// testNginxConfig 测试nginx配置
238+
func (cd *CertDeployer) testNginxConfig() error {
239+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
240+
defer cancel()
241+
242+
cmd := exec.CommandContext(ctx, "nginx", "-t")
243+
output, err := cmd.CombinedOutput()
244+
if err != nil {
245+
return fmt.Errorf("nginx配置测试失败: %w, 输出: %s", err, string(output))
246+
}
247+
248+
fmt.Println("nginx配置测试通过")
249+
return nil
250+
}
251+
252+
// reloadNginx 重新加载nginx
253+
func (cd *CertDeployer) reloadNginx() error {
254+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
255+
defer cancel()
256+
257+
cmd := exec.CommandContext(ctx, "nginx", "-s", "reload")
258+
output, err := cmd.CombinedOutput()
259+
if err != nil {
260+
return fmt.Errorf("nginx重新加载失败: %w, 输出: %s", err, string(output))
261+
}
262+
263+
fmt.Println("nginx重新加载成功")
264+
return nil
265+
}

0 commit comments

Comments
 (0)