package main import ( "bytes" "fmt" "os" "os/exec" "strings" "gopkg.in/yaml.v3" ) // TraefikConfig represents the structure of the main Traefik configuration type TraefikConfig struct { Experimental struct { Plugins struct { Badger struct { Version string `yaml:"version"` } `yaml:"badger"` } `yaml:"plugins"` } `yaml:"experimental"` CertificatesResolvers struct { LetsEncrypt struct { Acme struct { Email string `yaml:"email"` } `yaml:"acme"` } `yaml:"letsencrypt"` } `yaml:"certificatesResolvers"` } // DynamicConfig represents the structure of the dynamic configuration type DynamicConfig struct { HTTP struct { Routers map[string]struct { Rule string `yaml:"rule"` } `yaml:"routers"` } `yaml:"http"` } // ConfigValues holds the extracted configuration values type ConfigValues struct { DashboardDomain string LetsEncryptEmail string BadgerVersion string } // ReadTraefikConfig reads and extracts values from Traefik configuration files func ReadTraefikConfig(mainConfigPath, dynamicConfigPath string) (*ConfigValues, error) { // Read main config file mainConfigData, err := os.ReadFile(mainConfigPath) if err != nil { return nil, fmt.Errorf("error reading main config file: %w", err) } var mainConfig TraefikConfig if err := yaml.Unmarshal(mainConfigData, &mainConfig); err != nil { return nil, fmt.Errorf("error parsing main config file: %w", err) } // Read dynamic config file dynamicConfigData, err := os.ReadFile(dynamicConfigPath) if err != nil { return nil, fmt.Errorf("error reading dynamic config file: %w", err) } var dynamicConfig DynamicConfig if err := yaml.Unmarshal(dynamicConfigData, &dynamicConfig); err != nil { return nil, fmt.Errorf("error parsing dynamic config file: %w", err) } // Extract values values := &ConfigValues{ BadgerVersion: mainConfig.Experimental.Plugins.Badger.Version, LetsEncryptEmail: mainConfig.CertificatesResolvers.LetsEncrypt.Acme.Email, } // Extract DashboardDomain from router rules // Look for it in the main router rules for _, router := range dynamicConfig.HTTP.Routers { if router.Rule != "" { // Extract domain from Host(`mydomain.com`) if domain := extractDomainFromRule(router.Rule); domain != "" { values.DashboardDomain = domain break } } } return values, nil } // extractDomainFromRule extracts the domain from a router rule func extractDomainFromRule(rule string) string { // Look for the Host(`mydomain.com`) pattern if start := findPattern(rule, "Host(`"); start != -1 { end := findPattern(rule[start:], "`)") if end != -1 { return rule[start+6 : start+end] } } return "" } // findPattern finds the start of a pattern in a string func findPattern(s, pattern string) int { return bytes.Index([]byte(s), []byte(pattern)) } func copyDockerService(sourceFile, destFile, serviceName string) error { // Read source file sourceData, err := os.ReadFile(sourceFile) if err != nil { return fmt.Errorf("error reading source file: %w", err) } // Read destination file destData, err := os.ReadFile(destFile) if err != nil { return fmt.Errorf("error reading destination file: %w", err) } // Parse source Docker Compose YAML var sourceCompose map[string]interface{} if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil { return fmt.Errorf("error parsing source Docker Compose file: %w", err) } // Parse destination Docker Compose YAML var destCompose map[string]interface{} if err := yaml.Unmarshal(destData, &destCompose); err != nil { return fmt.Errorf("error parsing destination Docker Compose file: %w", err) } // Get services section from source sourceServices, ok := sourceCompose["services"].(map[string]interface{}) if !ok { return fmt.Errorf("services section not found in source file or has invalid format") } // Get the specific service configuration serviceConfig, ok := sourceServices[serviceName] if !ok { return fmt.Errorf("service '%s' not found in source file", serviceName) } // Get or create services section in destination destServices, ok := destCompose["services"].(map[string]interface{}) if !ok { // If services section doesn't exist, create it destServices = make(map[string]interface{}) destCompose["services"] = destServices } // Update service in destination destServices[serviceName] = serviceConfig // Marshal updated destination YAML // Use yaml.v3 encoder to preserve formatting and comments // updatedData, err := yaml.Marshal(destCompose) updatedData, err := MarshalYAMLWithIndent(destCompose, 2) if err != nil { return fmt.Errorf("error marshaling updated Docker Compose file: %w", err) } // Write updated YAML back to destination file if err := os.WriteFile(destFile, updatedData, 0644); err != nil { return fmt.Errorf("error writing to destination file: %w", err) } return nil } func backupConfig() error { // Backup docker-compose.yml if _, err := os.Stat("docker-compose.yml"); err == nil { if err := copyFile("docker-compose.yml", "docker-compose.yml.backup"); err != nil { return fmt.Errorf("failed to backup docker-compose.yml: %v", err) } } // Backup config directory if _, err := os.Stat("config"); err == nil { cmd := exec.Command("tar", "-czvf", "config.tar.gz", "config") if err := cmd.Run(); err != nil { return fmt.Errorf("failed to backup config directory: %v", err) } } return nil } func MarshalYAMLWithIndent(data interface{}, indent int) ([]byte, error) { buffer := new(bytes.Buffer) encoder := yaml.NewEncoder(buffer) encoder.SetIndent(indent) err := encoder.Encode(data) if err != nil { return nil, err } defer encoder.Close() return buffer.Bytes(), nil } func replaceInFile(filepath, oldStr, newStr string) error { // Read the file content content, err := os.ReadFile(filepath) if err != nil { return fmt.Errorf("error reading file: %v", err) } // Replace the string newContent := strings.Replace(string(content), oldStr, newStr, -1) // Write the modified content back to the file err = os.WriteFile(filepath, []byte(newContent), 0644) if err != nil { return fmt.Errorf("error writing file: %v", err) } return nil } func CheckAndAddTraefikLogVolume(composePath string) error { // Read the docker-compose.yml file data, err := os.ReadFile(composePath) if err != nil { return fmt.Errorf("error reading compose file: %w", err) } // Parse YAML into a generic map var compose map[string]interface{} if err := yaml.Unmarshal(data, &compose); err != nil { return fmt.Errorf("error parsing compose file: %w", err) } // Get services section services, ok := compose["services"].(map[string]interface{}) if !ok { return fmt.Errorf("services section not found or invalid") } // Get traefik service traefik, ok := services["traefik"].(map[string]interface{}) if !ok { return fmt.Errorf("traefik service not found or invalid") } // Check volumes logVolume := "./config/traefik/logs:/var/log/traefik" var volumes []interface{} if existingVolumes, ok := traefik["volumes"].([]interface{}); ok { // Check if volume already exists for _, v := range existingVolumes { if v.(string) == logVolume { fmt.Println("Traefik log volume is already configured") return nil } } volumes = existingVolumes } // Add new volume volumes = append(volumes, logVolume) traefik["volumes"] = volumes // Write updated config back to file newData, err := MarshalYAMLWithIndent(compose, 2) if err != nil { return fmt.Errorf("error marshaling updated compose file: %w", err) } if err := os.WriteFile(composePath, newData, 0644); err != nil { return fmt.Errorf("error writing updated compose file: %w", err) } fmt.Println("Added traefik log volume and created logs directory") return nil } // MergeYAML merges two YAML files, where the contents of the second file // are merged into the first file. In case of conflicts, values from the // second file take precedence. func MergeYAML(baseFile, overlayFile string) error { // Read the base YAML file baseContent, err := os.ReadFile(baseFile) if err != nil { return fmt.Errorf("error reading base file: %v", err) } // Read the overlay YAML file overlayContent, err := os.ReadFile(overlayFile) if err != nil { return fmt.Errorf("error reading overlay file: %v", err) } // Parse base YAML into a map var baseMap map[string]interface{} if err := yaml.Unmarshal(baseContent, &baseMap); err != nil { return fmt.Errorf("error parsing base YAML: %v", err) } // Parse overlay YAML into a map var overlayMap map[string]interface{} if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil { return fmt.Errorf("error parsing overlay YAML: %v", err) } // Merge the overlay into the base merged := mergeMap(baseMap, overlayMap) // Marshal the merged result back to YAML mergedContent, err := MarshalYAMLWithIndent(merged, 2) if err != nil { return fmt.Errorf("error marshaling merged YAML: %v", err) } // Write the merged content back to the base file if err := os.WriteFile(baseFile, mergedContent, 0644); err != nil { return fmt.Errorf("error writing merged YAML: %v", err) } return nil } // mergeMap recursively merges two maps func mergeMap(base, overlay map[string]interface{}) map[string]interface{} { result := make(map[string]interface{}) // Copy all key-values from base map for k, v := range base { result[k] = v } // Merge overlay values for k, v := range overlay { // If both maps have the same key and both values are maps, merge recursively if baseVal, ok := base[k]; ok { if baseMap, isBaseMap := baseVal.(map[string]interface{}); isBaseMap { if overlayMap, isOverlayMap := v.(map[string]interface{}); isOverlayMap { result[k] = mergeMap(baseMap, overlayMap) continue } } } // Otherwise, overlay value takes precedence result[k] = v } return result }