Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: ${{ env.IMAGE_NAME }}
readme-filepath: ./README.md
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ Thumbs.db
# Build artifacts
build/
dist/
dns-sync
dns-sync.exe
dns-sync-*
*.exe

# Configuration files with sensitive data
config.local.yaml
Expand Down Expand Up @@ -87,6 +85,7 @@ charts/*/requirements.lock

# Local development
local/
bin/
dev/

# Backup files
Expand Down
10 changes: 5 additions & 5 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ linters:
- revive
- stylecheck

disable:
- deadcode
- varcheck
- structcheck

exclusions:
rules:
- linters:
- gosec
text: G115
run:
timeout: 5m
tests: true
Expand Down
94 changes: 94 additions & 0 deletions cmd/dns-sync/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"

"github.com/flanksource/dns-sync/config"
"github.com/flanksource/dns-sync/sync"
)

var (
version = "dev"
commit = "unknown"
date = "unknown"
)

func main() {
var configFile = flag.String("config", "config.yaml", "Configuration file path")
var logLevel = flag.String("log-level", "info", "Log level (debug, info, warn, error)")
var showVersion = flag.Bool("version", false, "Show version information")
var dryRun = flag.Bool("dry-run", false, "Enable dry run mode (no changes made)")
var once = flag.Bool("once", false, "Run synchronization once and exit")
flag.Parse()

if *showVersion {
fmt.Printf("dns-sync version %s (commit: %s, built: %s)\n", version, commit, date)
os.Exit(0)
}

// Load configuration
cfg, err := config.Load(*configFile)
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
if dryRun != nil {
cfg.Sync.DryRun = *dryRun
}

// Setup logging
setupLogging(*logLevel)

// Initialize synchronizer
syncer := sync.NewSynchronizer(*cfg)

// Start the synchronizer
log.Println("Starting DNS synchronizer...")
if once != nil && *once {
if err, _ := syncer.Once(context.Background()); err != nil {
log.Fatalf("Synchronizer failed: %v", err)
}
} else {
// Setup graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Handle OS signals
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

go func() {
sig := <-sigCh
log.Printf("Received signal %v, shutting down gracefully...", sig)
cancel()
}()
if err := syncer.Start(ctx); err != nil {
log.Fatalf("Synchronizer failed: %v", err)
}
log.Println("DNS synchronizer stopped")
}

}

func setupLogging(level string) {
// Configure logging based on level
log.SetFlags(log.LstdFlags | log.Lshortfile)

switch level {
case "debug":
// Enable debug logging
case "info":
// Default info logging
case "warn":
// Warning and above
case "error":
// Error only
default:
log.Printf("Unknown log level: %s, using info", level)
}
}
31 changes: 16 additions & 15 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type Config struct {
Sync SyncConfig `yaml:"sync" json:"sync"`
}

type ConfigSpec Config
type Spec Config

// SyncConfig contains synchronization settings
type SyncConfig struct {
Expand Down Expand Up @@ -97,38 +97,39 @@ type ProviderConfig struct {

func (p ProviderConfig) String() string {
if p.AWS != nil {
return fmt.Sprintf("AWS")
return "AWS"
} else if p.Azure != nil {
return fmt.Sprintf("Azure{sub=%s}", p.Azure.SubscriptionID)
} else if p.DigitalOcean != nil {
return fmt.Sprintf("DigitalOcean{}")
return "DigitalOcean{}"
} else if p.IBMCloud != nil {
return fmt.Sprintf("IBMCloud")
return "IBMCloud"
} else if p.GoDaddy != nil {
return fmt.Sprintf("GoDaddy")
return "GoDaddy"
} else if p.Exoscale != nil {
return fmt.Sprintf("Exoscale")
return "Exoscale"
} else if p.RFC2136 != nil {
return fmt.Sprintf("RFC2136{host=%s,tsig=%s}", p.RFC2136.Host, p.RFC2136.TSIGKeyName)
} else if p.AlibabaCloud != nil {
return fmt.Sprintf("AlibabaCloud")
return "AlibabaCloud"
} else if p.TencentCloud != nil {
return fmt.Sprintf("TencentCloud")
return "TencentCloud"
} else if p.CloudFoundry != nil {
return fmt.Sprintf("CloudFoundry")
return "CloudFoundry"
} else if p.CoreDNS != nil {
return fmt.Sprintf("CoreDNS")
return "CoreDNS"
} else if p.Cloudflare != nil {
return "Cloudflare"
} else if p.TransIP != nil {
return fmt.Sprintf("TransIP")
return "TransIP"
} else if p.Pihole != nil {
return fmt.Sprintf("Pihole")
return "Pihole"
} else if p.Plural != nil {
return fmt.Sprintf("Plural")
return "Plural"
} else if p.Webhook != nil {
return fmt.Sprintf("Webhook")
return "Webhook"
} else if p.InMemory != nil {
return fmt.Sprintf("InMemory")
return "InMemory"
} else if p.File != nil {
return fmt.Sprintf("File{%s}", p.File.Path)
}
Expand Down
50 changes: 43 additions & 7 deletions config/providers/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"fmt"
"io"
"math"
"net"
"os"
"path/filepath"
Expand Down Expand Up @@ -34,7 +35,7 @@ func NewFileProvider(config config.FileProviderConfig, domainFilter endpoint.Dom
}

// Records retrieves all DNS records from the zone file
func (f *fileProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
func (f *fileProvider) Records(_ context.Context) ([]*endpoint.Endpoint, error) {
file, err := os.Open(f.config.Path)
if err != nil {

Expand Down Expand Up @@ -66,7 +67,11 @@ func (f *fileProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error
}

// ApplyChanges applies DNS record changes by updating the zone file
func (f *fileProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
func (f *fileProvider) ApplyChanges(_ context.Context, changes *plan.Changes) error {
if changes == nil {
return nil
}

if len(changes.Create) == 0 && len(changes.UpdateNew) == 0 && len(changes.Delete) == 0 {
return nil // No changes to apply
}
Expand Down Expand Up @@ -115,7 +120,17 @@ func (f *fileProvider) convertRRToEndpoint(rr dns.RR) (*endpoint.Endpoint, error

// Skip SOA records as they're not typically managed by external-dns
if header.Rrtype == dns.TypeSOA {
return nil, nil
soa, ok := rr.(*dns.SOA)
if !ok {
return nil, fmt.Errorf("failed to cast SOA record")
}
targets := []string{fmt.Sprintf("%s %d", strings.TrimSuffix(soa.Ns, "."), soa.Serial)}
return &endpoint.Endpoint{
DNSName: strings.TrimSuffix(header.Name, "."),
RecordType: dns.TypeToString[header.Rrtype],
Targets: targets,
RecordTTL: endpoint.TTL(f.getDefaultTTL(header.Ttl)),
}, nil
}

// Get the DNS name and remove trailing dot
Expand Down Expand Up @@ -155,12 +170,20 @@ func (f *fileProvider) convertRRToEndpoint(rr dns.RR) (*endpoint.Endpoint, error
DNSName: dnsName,
RecordType: recordType,
Targets: targets,
RecordTTL: endpoint.TTL(header.Ttl),
RecordTTL: endpoint.TTL(f.getDefaultTTL(header.Ttl)),
}

return endpoint, nil
}

// getDefaultTTL returns a consistent TTL value, normalizing 0 values to a default
func (f *fileProvider) getDefaultTTL(ttl uint32) uint32 {
if ttl == 0 {
return 300 // Use the same default as endpointToRRs
}
return ttl
}

// parseZoneFile reads and parses the entire zone file into a slice of DNS resource records
func (f *fileProvider) parseZoneFile() ([]dns.RR, error) {
file, err := os.Open(f.config.Path)
Expand Down Expand Up @@ -223,9 +246,9 @@ func (f *fileProvider) endpointToRRs(endpoint *endpoint.Endpoint) []dns.RR {
dnsName += "."
}

ttl := uint32(endpoint.RecordTTL)
if ttl == 0 {
ttl = 300 // Default TTL
ttl := uint32(300)
if endpoint.RecordTTL > 0 && int64(endpoint.RecordTTL) < math.MaxUint32 {
ttl = uint32(endpoint.RecordTTL)
}

// Create header template
Expand Down Expand Up @@ -316,6 +339,16 @@ func (f *fileProvider) endpointToRRs(endpoint *endpoint.Endpoint) []dns.RR {
Hdr: header,
Ptr: ptr,
})
case "SOA":
parts := strings.Fields(target)
if len(parts) >= 2 {
serial, _ := strconv.ParseUint(parts[1], 10, 32)
rrs = append(rrs, &dns.SOA{
Hdr: header,
Ns: parts[0],
Serial: uint32(serial),
})
}
}
}

Expand Down Expand Up @@ -370,6 +403,9 @@ func (f *fileProvider) recordsMatch(rr1, rr2 dns.RR) bool {
case *dns.PTR:
ptr1, ptr2 := rr1.(*dns.PTR), rr2.(*dns.PTR)
return ptr1.Ptr == ptr2.Ptr
case *dns.SOA:
soa1, soa2 := rr1.(*dns.SOA), rr2.(*dns.SOA)
return soa1.Ns == soa2.Ns && soa1.Serial == soa2.Serial
default:
// For other record types, fall back to string comparison
return strings.TrimSpace(strings.TrimPrefix(rr1.String(), h1.String())) ==
Expand Down
Loading
Loading