feat: gemini refactor the code
This commit is contained in:
		
							
								
								
									
										395
									
								
								cmd/gemini/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										395
									
								
								cmd/gemini/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,395 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"log/slog" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"bytes" // 使用 bytes.Equal 替代 reflect.DeepEqual | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/docker/docker/api/types/container" | ||||
| 	"github.com/docker/docker/api/types/filters" | ||||
| 	"github.com/docker/docker/client" | ||||
| 	"github.com/fsnotify/fsnotify" | ||||
|  | ||||
| 	// 假设 repo 包内容已移到本地,或使用内部结构体 | ||||
| 	// "git.esin.io/lab/traefik-certs-exporter/repo" // 移除对私有库的硬依赖 | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	PemNamePrivKey   = "privkey.pem" | ||||
| 	PemNameChain     = "chain.pem" | ||||
| 	PemNameFullchain = "fullchain.pem" | ||||
| 	PemNameCert      = "cert.pem" | ||||
| ) | ||||
|  | ||||
| // Config 结构体:集中管理所有命令行参数和配置 | ||||
| type Config struct { | ||||
| 	ACMEFile       string | ||||
| 	PEMStoredDir   string | ||||
| 	DockerLabelKey string | ||||
| 	LogDebug       bool | ||||
| 	LogSource      bool | ||||
| } | ||||
|  | ||||
| // Exporter 结构体:封装应用状态、配置和外部依赖 | ||||
| type Exporter struct { | ||||
| 	Config *Config | ||||
| 	Logger *slog.Logger | ||||
| 	Docker *client.Client // 客户端只初始化一次 | ||||
| } | ||||
|  | ||||
| // =================================================================== | ||||
| // 1. 初始化和配置 | ||||
| // =================================================================== | ||||
|  | ||||
| func (cfg *Config) ParseFlags() { | ||||
| 	flag.BoolVar(&cfg.LogDebug, "log.debug", false, "bool value of debug") | ||||
| 	flag.BoolVar(&cfg.LogSource, "log.source", false, "bool value of log source file") | ||||
| 	flag.StringVar(&cfg.ACMEFile, "f", "./acme.json", "acme.json file path") | ||||
| 	flag.StringVar(&cfg.PEMStoredDir, "d", "./certs", "certs stored directory") | ||||
| 	flag.StringVar(&cfg.DockerLabelKey, "l", "traefik.cert.domain", "the key of the docker container label") | ||||
| 	flag.Parse() | ||||
| } | ||||
|  | ||||
| func useTextLogger(cfg *Config) *slog.Logger { | ||||
| 	logLevel := slog.LevelInfo | ||||
| 	if cfg.LogDebug { | ||||
| 		logLevel = slog.LevelDebug | ||||
| 	} | ||||
|  | ||||
| 	logger := slog.New( | ||||
| 		slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ | ||||
| 			AddSource: cfg.LogSource, | ||||
| 			Level:     logLevel, | ||||
| 		}), | ||||
| 	) | ||||
| 	slog.SetDefault(logger) | ||||
| 	return logger | ||||
| } | ||||
|  | ||||
| func main() { | ||||
| 	cfg := &Config{} | ||||
| 	cfg.ParseFlags() | ||||
|  | ||||
| 	logger := useTextLogger(cfg) | ||||
| 	logger.Info("start to run cert exporter", "acmeFile", cfg.ACMEFile, "certsDir", cfg.PEMStoredDir) | ||||
|  | ||||
| 	// 仅初始化一次 Docker 客户端 | ||||
| 	dockerClient, err := client.NewClientWithOpts(client.FromEnv) | ||||
| 	if err != nil { | ||||
| 		logger.Error("failed to connect docker daemon", "error", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	defer dockerClient.Close() | ||||
|  | ||||
| 	// 版本协商 | ||||
| 	dockerClient.NegotiateAPIVersion(context.Background()) | ||||
|  | ||||
| 	exporter := &Exporter{ | ||||
| 		Config: cfg, | ||||
| 		Logger: logger, | ||||
| 		Docker: dockerClient, | ||||
| 	} | ||||
|  | ||||
| 	if err := exporter.Run(); err != nil { | ||||
| 		exporter.Logger.Error("application exited with error", "error", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // =================================================================== | ||||
| // 2. 核心运行和文件监听 | ||||
| // =================================================================== | ||||
|  | ||||
| func (e *Exporter) Run() error { | ||||
| 	// 首次启动先运行一次 | ||||
| 	if err := e.DumpAllCerts(); err != nil { | ||||
| 		e.Logger.Error("initial dump failed", "error", err) | ||||
| 		// 初始失败应返回错误,但这里可能只需要警告并继续监听 | ||||
| 	} | ||||
|  | ||||
| 	// 优化 1:监听文件所在的父目录,而不是文件本身 | ||||
| 	acmeDir := filepath.Dir(e.Config.ACMEFile) | ||||
| 	acmeBase := filepath.Base(e.Config.ACMEFile) | ||||
|  | ||||
| 	watcher, err := fsnotify.NewWatcher() | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to create new watcher: %w", err) | ||||
| 	} | ||||
| 	defer watcher.Close() | ||||
|  | ||||
| 	if err := watcher.Add(acmeDir); err != nil { | ||||
| 		return fmt.Errorf("failed to add target directory to watcher: %w", err) | ||||
| 	} | ||||
| 	e.Logger.Info("watching directory for acme file changes", "directory", acmeDir) | ||||
|  | ||||
| 	// 用于去抖动 (Debouncing) | ||||
| 	const debounceDuration = 100 * time.Millisecond | ||||
| 	var lastEventTime time.Time | ||||
|  | ||||
| 	for { | ||||
| 		select { | ||||
| 		case event, ok := <-watcher.Events: | ||||
| 			if !ok { | ||||
| 				return fmt.Errorf("watcher events channel closed") | ||||
| 			} | ||||
| 			 | ||||
| 			// 优化 2:精确匹配 Traefik 的原子重命名和写入 | ||||
| 			// Traefik 的原子写入通常是:写入临时文件 -> RENAME 成 acme.json | ||||
| 			isTargetFile := filepath.Base(event.Name) == acmeBase | ||||
| 			 | ||||
| 			// 优化 3:Debounce(去抖动)处理,防止短时间内多次触发 | ||||
| 			if time.Since(lastEventTime) < debounceDuration { | ||||
| 				e.Logger.Debug("debounce: ignoring event", "event", event) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			// 监听 RENAME 或 WRITE 事件 | ||||
| 			if isTargetFile && (event.Has(fsnotify.Rename) || event.Has(fsnotify.Write)) { | ||||
| 				lastEventTime = time.Now() | ||||
| 				e.Logger.Info("received acme file change event, dumping certs...", "event", event.Op.String(), "file", event.Name) | ||||
| 				 | ||||
| 				// 给 Traefik 足够时间完成写入和重命名 | ||||
| 				time.Sleep(50 * time.Millisecond) | ||||
|  | ||||
| 				if err := e.DumpAllCerts(); err != nil { | ||||
| 					e.Logger.Error("dump certs failed after file event", "error", err) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 		case err, ok := <-watcher.Errors: | ||||
| 			if !ok { | ||||
| 				// 致命错误,退出循环 | ||||
| 				return fmt.Errorf("watcher errors channel closed") | ||||
| 			} | ||||
| 			// 非致命错误,记录并继续 | ||||
| 			e.Logger.Error("watcher received error", "error", err) | ||||
|  | ||||
| 		case <-context.Background().Done(): | ||||
| 			return context.Background().Err() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // =================================================================== | ||||
| // 3. 证书处理逻辑 | ||||
| // =================================================================== | ||||
|  | ||||
| // 使用内部结构体替代对外部私有 repo 包的依赖 | ||||
| type Certificate struct { | ||||
| 	Domain struct { Main string `json:"main"` } `json:"domain"` | ||||
| 	Key string `json:"key"` | ||||
| 	Certificate string `json:"certificate"` | ||||
| } | ||||
| type Account struct { | ||||
| 	Email string `json:"EmailAddress"` | ||||
| 	Registration struct { Body struct { Status string `json:"status"` } `json:"body"` } `json:"Registration"` | ||||
| } | ||||
| type Provider struct { | ||||
| 	Account Account `json:"Account"` | ||||
| 	Certificates []Certificate `json:"Certificates"` | ||||
| } | ||||
| type Resolvers map[string]Provider // top-level map | ||||
|  | ||||
| // DumpAllCerts 负责读取文件和循环处理 | ||||
| func (e *Exporter) DumpAllCerts() error { | ||||
| 	e.Logger.Info("start to dump certs from acme.json file") | ||||
| 	 | ||||
| 	resolvers, err := e.ReadJSONFile() | ||||
| 	if err != nil { | ||||
| 		return err // 文件读取失败是致命错误 | ||||
| 	} | ||||
|  | ||||
| 	// 优化:使用一个计数器来判断是否有任何域处理成功 | ||||
| 	successCount := 0 | ||||
| 	 | ||||
| 	for resolverName, provider := range resolvers { | ||||
| 		e.Logger.Info("found cert resolvers", "name", resolverName, "email", provider.Account.Email, "status", provider.Account.Registration.Body.Status) | ||||
|  | ||||
| 		for _, cert := range provider.Certificates { | ||||
| 			domain := cert.Domain.Main | ||||
| 			 | ||||
| 			// 优化:处理一个域失败,只记录错误,继续下一个 | ||||
| 			if err := e.DumpDomainCerts(domain, cert.Key, cert.Certificate); err != nil { | ||||
| 				e.Logger.Error("failed to dump cert for domain", "domain", domain, "error", err) | ||||
| 				continue // 继续处理下一个证书 | ||||
| 			} | ||||
| 			successCount++ | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	e.Logger.Info("cert dumping complete", "successful_certs", successCount) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (e *Exporter) ReadJSONFile() (Resolvers, error) { | ||||
| 	data, err := os.ReadFile(e.Config.ACMEFile) | ||||
| 	if err != nil { | ||||
| 		// 优化:更清晰的错误类型判断 | ||||
| 		if os.IsNotExist(err) { | ||||
| 			return nil, fmt.Errorf("acme file not exist: %s: %w", e.Config.ACMEFile, err) | ||||
| 		} | ||||
| 		return nil, fmt.Errorf("failed to read acme file: %s: %w", e.Config.ACMEFile, err) | ||||
| 	} | ||||
|  | ||||
| 	var resolvers Resolvers | ||||
| 	if err := json.Unmarshal(data, &resolvers); err != nil { | ||||
| 		return nil, fmt.Errorf("failed to unmarshal acme file: %w", err) | ||||
| 	} | ||||
| 	return resolvers, nil | ||||
| } | ||||
|  | ||||
| func (e *Exporter) DumpDomainCerts(domain string, encodedPriveKey, encodedCert string) error { | ||||
| 	pemFileDir := filepath.Join(e.Config.PEMStoredDir, domain) | ||||
| 	 | ||||
| 	// 确保目录存在 | ||||
| 	if err := os.MkdirAll(pemFileDir, 0755); err != nil { | ||||
| 		return fmt.Errorf("failed to make domain directory %s: %w", pemFileDir, err) | ||||
| 	} | ||||
|  | ||||
| 	// 1. 导出私钥 (privkey.pem) | ||||
| 	privkeyData, err := base64.StdEncoding.DecodeString(encodedPriveKey) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to decode private key for %s: %w", domain, err) | ||||
| 	} | ||||
| 	if _, err := e.CompareAndAtomicWritePem(pemFileDir, PemNamePrivKey, privkeyData, domain); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// 2. 导出完整链 (fullchain.pem) | ||||
| 	fullChainData, err := base64.StdEncoding.DecodeString(encodedCert) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to decode fullchain cert for %s: %w", domain, err) | ||||
| 	} | ||||
| 	 | ||||
| 	// 关键:检查 fullchain 是否有修改 | ||||
| 	modified, err := e.CompareAndAtomicWritePem(pemFileDir, PemNameFullchain, fullChainData, domain) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	 | ||||
| 	// 3. 导出证书和链 (cert.pem, chain.pem) | ||||
| 	pemCert, pemChain, err := DetachFullchainPem(string(fullChainData)) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to detach cert/chain for %s: %w", domain, err) | ||||
| 	} | ||||
| 	if _, err := e.CompareAndAtomicWritePem(pemFileDir, PemNameCert, []byte(pemCert), domain); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if _, err := e.CompareAndAtomicWritePem(pemFileDir, PemNameChain, []byte(pemChain), domain); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// 4. 重启容器 | ||||
| 	if modified { | ||||
| 		e.Logger.Info("fullchain was modified, restarting watched container", "domain", domain) | ||||
| 		if err := e.RestartDomainWatchedContainer(domain); err != nil { | ||||
| 			// 记录错误,但不返回,因为证书文件已经更新成功 | ||||
| 			e.Logger.Error("failed to restart docker container", "domain", domain, "error", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // DetachFullchainPem 将 FullChain 分离为 Cert 和 Chain | ||||
| func DetachFullchainPem(fullChain string) (string, string, error) { | ||||
| 	// 优化:确保 BEGIN CERTIFICATE 存在 | ||||
| 	idx := strings.Index(fullChain, "\n-----BEGIN CERTIFICATE-----") | ||||
| 	if idx == -1 { | ||||
| 		// 可能是单个证书,没有 chain | ||||
| 		// 更好的做法是依赖于 Go 的 x509 库来解析,这里暂时保留原逻辑,但添加检查 | ||||
| 		if strings.HasPrefix(fullChain, "-----BEGIN CERTIFICATE-----") { | ||||
| 			return fullChain, "", nil // 只有一个证书 | ||||
| 		} | ||||
| 		return "", "", fmt.Errorf("invalid fullchain format: cannot find chain separator") | ||||
| 	} | ||||
| 	return fullChain[:idx], fullChain[idx+1:], nil | ||||
| } | ||||
|  | ||||
| // 优化:重命名原函数,实现原子写入,并使用 bytes.Equal 提速 | ||||
| func (e *Exporter) CompareAndAtomicWritePem(fileDir, filename string, data []byte, domain string) (bool, error) { | ||||
| 	fp := filepath.Join(fileDir, filename) | ||||
| 	 | ||||
| 	// 1. 尝试读取原始内容进行比较 | ||||
| 	originalData, err := os.ReadFile(fp) | ||||
| 	if err == nil { | ||||
| 		// 优化:使用 bytes.Equal 提速 | ||||
| 		if bytes.Equal(data, originalData) { | ||||
| 			e.Logger.Debug("pem file content as before, pass", "file", fp, "domain", domain) | ||||
| 			return false, nil // 内容相同,不写入 | ||||
| 		} | ||||
| 	} else if !os.IsNotExist(err) { | ||||
| 		return false, fmt.Errorf("failed to read existing file %s: %w", fp, err) | ||||
| 	} | ||||
|  | ||||
| 	// 2. 内容不同或文件不存在,执行原子写入 | ||||
| 	tmpFp := fp + ".tmp" | ||||
| 	if err := os.WriteFile(tmpFp, data, 0644); err != nil { | ||||
| 		return false, fmt.Errorf("failed to write temporary file %s: %w", tmpFp, err) | ||||
| 	} | ||||
|  | ||||
| 	// 3. 原子重命名,覆盖原文件 | ||||
| 	if err := os.Rename(tmpFp, fp); err != nil { | ||||
| 		return false, fmt.Errorf("failed to atomically rename temp file %s to %s: %w", tmpFp, fp, err) | ||||
| 	} | ||||
| 	 | ||||
| 	e.Logger.Info("pem file content updated successfully", "file", fp, "domain", domain) | ||||
| 	return true, nil | ||||
| } | ||||
|  | ||||
| // =================================================================== | ||||
| // 4. Docker 交互 | ||||
| // =================================================================== | ||||
|  | ||||
| func (e *Exporter) RestartDomainWatchedContainer(domain string) error { | ||||
| 	label := fmt.Sprintf("%s=%s", e.Config.DockerLabelKey, domain) | ||||
| 	return e.FindDockerContainersAndRestart(label) | ||||
| } | ||||
|  | ||||
| func (e *Exporter) FindDockerContainersAndRestart(label string) error { | ||||
| 	ctx := context.Background() | ||||
| 	 | ||||
| 	// Docker 客户端已在 main 中初始化并传入 Exporter | ||||
| 	apiClient := e.Docker  | ||||
|  | ||||
| 	opts := container.ListOptions{ | ||||
| 		All: true, | ||||
| 		Filters: filters.NewArgs( | ||||
| 			filters.Arg("label", label), | ||||
| 		), | ||||
| 	} | ||||
| 	containers, err := apiClient.ContainerList(ctx, opts) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to list docker containers with label '%s': %w", label, err) | ||||
| 	} | ||||
|  | ||||
| 	if len(containers) == 0 { | ||||
| 		e.Logger.Debug("no containers found with label", "label", label) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// 优化:使用 noWaitTimeout 变量 | ||||
| 	noWaitTimeout := 0 | ||||
| 	stopOptions := container.StopOptions{Timeout: &noWaitTimeout}  | ||||
| 	 | ||||
| 	for _, ctr := range containers { | ||||
| 		e.Logger.Info("found container to restart", "id", ctr.ID[:12], "image", ctr.Image, "status", ctr.Status, "label", label) | ||||
|  | ||||
| 		if err := apiClient.ContainerRestart(ctx, ctr.ID, stopOptions); err != nil { | ||||
| 			// 优化:记录错误,并继续下一个容器 | ||||
| 			e.Logger.Error("failed to restart docker container", "id", ctr.ID[:12], "image", ctr.Image, "label", label, "error", err) | ||||
| 			continue | ||||
| 		} | ||||
| 		e.Logger.Info("successfully restarted container", "id", ctr.ID[:12]) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
		Reference in New Issue
	
	Block a user