pangolin/install/config.go
2025-02-23 21:44:02 -05:00

353 lines
9.7 KiB
Go

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
}