353 lines
9.7 KiB
Go
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
|
|
}
|