Skip to content

Commit 8a81a2b

Browse files
committed
update
1 parent 82935d4 commit 8a81a2b

File tree

1 file changed

+116
-101
lines changed

1 file changed

+116
-101
lines changed

main.go

Lines changed: 116 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -20,44 +20,20 @@ func init() {
2020
DisableColors: true,
2121
FullTimestamp: true,
2222
})
23-
// Set log level based on environment variable or default to info
24-
logLevel := os.Getenv("LOG_LEVEL")
25-
if logLevel != "" {
26-
level, err := log.ParseLevel(logLevel)
27-
if err == nil {
28-
log.SetLevel(level)
29-
}
30-
}
3123
}
3224

3325
func main() {
34-
// Validate required environment variables
35-
requiredEnvVars := []string{
36-
"CLOUDFLARE_API_TOKEN",
37-
"CLOUDFLARE_ACCOUNT_ID",
38-
"CLOUDFLARE_TUNNEL_ID",
39-
"CLOUDFLARE_ZONE_ID",
40-
"TRAEFIK_SERVICE_ENDPOINT",
41-
"TRAEFIK_API_ENDPOINT",
42-
}
43-
44-
for _, envVar := range requiredEnvVars {
45-
if os.Getenv(envVar) == "" {
46-
log.Fatalf("Required environment variable %s is not set", envVar)
47-
}
48-
}
49-
26+
// Initialize Cloudflare API client
5027
cf, err := cloudflare.NewWithAPIToken(os.Getenv("CLOUDFLARE_API_TOKEN"))
5128
if err != nil {
5229
log.Fatal(err)
5330
}
5431

5532
ctx := context.Background()
5633

34+
// Set up Traefik API client
5735
client := resty.New().
58-
SetBaseURL(os.Getenv("TRAEFIK_API_ENDPOINT")).
59-
SetTimeout(10 * time.Second).
60-
SetRetryCount(3)
36+
SetBaseURL(os.Getenv("TRAEFIK_API_ENDPOINT"))
6137

6238
pollCh := pollTraefikRouters(client)
6339
var cache []Router
@@ -66,159 +42,198 @@ func main() {
6642
log.Fatal(poll.Err)
6743
}
6844

69-
// skip if no changes to traefik routers
45+
// Skip if no changes to Traefik routers
7046
if reflect.DeepEqual(cache, poll.Routers) {
7147
continue
7248
}
7349

7450
log.Info("changes detected")
7551

76-
// update the cache
52+
// Update the cache
7753
cache = poll.Routers
7854

79-
ingress := []cloudflare.UnvalidatedIngressRule{}
80-
55+
// Collect unique domains from applicable routers
56+
uniqueDomains := make(map[string]struct{})
8157
for _, r := range poll.Routers {
82-
// Only enabled routes
58+
// Filter routers
8359
if r.Status != "enabled" {
84-
log.Debugf("Skipping disabled route: %s", r.Rule)
60+
continue
61+
}
62+
if !contains(r.EntryPoints, os.Getenv("TRAEFIK_ENTRYPOINT")) {
8563
continue
8664
}
8765

88-
// Handle both HTTP and HTTPS routes
89-
// We want to include all routes, including those with TLS configured
90-
if os.Getenv("TRAEFIK_ENTRYPOINT") != "" {
91-
// If TRAEFIK_ENTRYPOINT is specified, only use routes with that entrypoint
92-
if !contains(r.EntryPoints, os.Getenv("TRAEFIK_ENTRYPOINT")) {
93-
log.Debugf("Skipping route with different entrypoint: %s", r.Rule)
94-
continue
95-
}
66+
// Log if router has TLS configured for debugging
67+
if r.TLS.CertResolver != "" {
68+
log.WithFields(log.Fields{
69+
"router": r.ServiceName,
70+
"rule": r.Rule,
71+
}).Warn("router has TLS configured but is included for tunnel")
9672
}
9773

9874
domains, err := http.ParseDomains(r.Rule)
9975
if err != nil {
100-
log.Errorf("Failed to parse domains from rule %s: %v", r.Rule, err)
101-
continue
76+
log.WithFields(log.Fields{
77+
"rule": r.Rule,
78+
}).Fatal("failed to parse domains: ", err)
10279
}
103-
10480
for _, domain := range domains {
105-
log.WithFields(log.Fields{
106-
"domain": domain,
107-
"service": os.Getenv("TRAEFIK_SERVICE_ENDPOINT"),
108-
"tls": r.TLS.CertResolver != "",
109-
}).Info("upserting tunnel")
110-
111-
ingress = append(ingress, cloudflare.UnvalidatedIngressRule{
112-
Hostname: domain,
113-
Service: os.Getenv("TRAEFIK_SERVICE_ENDPOINT"),
114-
OriginRequest: &cloudflare.OriginRequestConfig{
115-
HTTPHostHeader: &domain,
116-
// Add other origin request options if needed
117-
NoTLSVerify: boolPtr(true), // Skip TLS verification for internal traffic
118-
},
119-
})
81+
uniqueDomains[domain] = struct{}{}
12082
}
12183
}
12284

123-
// add catch-all rule
85+
// Build ingress rules from unique domains
86+
ingress := []cloudflare.UnvalidatedIngressRule{}
87+
for domain := range uniqueDomains {
88+
log.WithFields(log.Fields{
89+
"domain": domain,
90+
"service": os.Getenv("TRAEFIK_SERVICE_ENDPOINT"),
91+
}).Info("adding ingress rule")
92+
93+
ingress = append(ingress, cloudflare.UnvalidatedIngressRule{
94+
Hostname: domain,
95+
Service: os.Getenv("TRAEFIK_SERVICE_ENDPOINT"),
96+
OriginRequest: &cloudflare.OriginRequestConfig{
97+
HTTPHostHeader: &domain,
98+
},
99+
})
100+
}
101+
102+
// Add catch-all rule
124103
ingress = append(ingress, cloudflare.UnvalidatedIngressRule{
125104
Service: "http_status:404",
126105
})
127106

107+
// Update Cloudflare tunnel configuration
128108
err = updateTunnels(ctx, cf, ingress)
129109
if err != nil {
130110
log.Fatal(err)
131111
}
132112
}
133113
}
134114

135-
// Helper to create a bool pointer
136-
func boolPtr(b bool) *bool {
137-
return &b
138-
}
139-
140115
func pollTraefikRouters(client *resty.Client) (ch chan PollResponse) {
141116
ch = make(chan PollResponse)
142117
go func() {
143-
defer func() {
144-
close(ch)
145-
}()
146-
r := rand.New(rand.NewSource(time.Now().UnixNano()))
118+
defer close(ch)
119+
r := rand.New(rand.NewSource(time.Now().UnixNano())) // Use current time for better randomness
147120
c := time.Tick(10 * time.Second)
148121

149122
for range c {
150123
var pollRes PollResponse
124+
maxRetries := 5
125+
for attempt := 1; attempt <= maxRetries; attempt++ {
126+
_, pollRes.Err = client.R().
127+
EnableTrace().
128+
SetResult(&pollRes.Routers).
129+
Get("/api/http/routers")
130+
131+
if pollRes.Err == nil {
132+
break
133+
}
151134

152-
resp, err := client.R().
153-
EnableTrace().
154-
SetResult(&pollRes.Routers).
155-
Get("/api/http/routers")
156-
157-
pollRes.Err = err
158-
if err != nil {
159-
log.Errorf("Error polling Traefik API: %v", err)
160-
ch <- pollRes
161-
time.Sleep(5 * time.Second) // Wait before retrying
162-
continue
163-
}
135+
if attempt == maxRetries {
136+
log.WithFields(log.Fields{
137+
"attempts": maxRetries,
138+
"error": pollRes.Err,
139+
}).Error("failed to fetch Traefik routers after max retries")
140+
break
141+
}
164142

165-
if resp.StatusCode() != 200 {
166-
log.Errorf("Unexpected status code from Traefik API: %d", resp.StatusCode())
167-
pollRes.Err = fmt.Errorf("unexpected status code: %d", resp.StatusCode())
168-
ch <- pollRes
169-
time.Sleep(5 * time.Second) // Wait before retrying
170-
continue
143+
// Exponential backoff with jitter
144+
baseDelay := time.Duration(1<<uint(attempt)) * time.Second
145+
jitter := time.Duration(r.Int63n(int64(time.Second))) // Random jitter up to 1s
146+
delay := baseDelay + jitter
147+
log.WithFields(log.Fields{
148+
"attempt": attempt,
149+
"delay": delay,
150+
"error": pollRes.Err,
151+
}).Warn("transient API failure, retrying...")
152+
time.Sleep(delay)
171153
}
172154

173155
ch <- pollRes
174156

175-
jitter := time.Duration(r.Int31n(5000)) * time.Millisecond
176-
time.Sleep(jitter)
157+
if pollRes.Err == nil {
158+
jitter := time.Duration(r.Int31n(5000)) * time.Millisecond
159+
time.Sleep(jitter)
160+
}
177161
}
178162
}()
179163
return
180164
}
181165

182166
func updateTunnels(ctx context.Context, cf *cloudflare.API, ingress []cloudflare.UnvalidatedIngressRule) error {
183-
// Get Current tunnel config
167+
// Get current tunnel config
184168
aid := cloudflare.AccountIdentifier(os.Getenv("CLOUDFLARE_ACCOUNT_ID"))
185169
cfg, err := cf.GetTunnelConfiguration(ctx, aid, os.Getenv("CLOUDFLARE_TUNNEL_ID"))
186170
if err != nil {
187-
return fmt.Errorf("unable to pull current tunnel config, %s", err.Error())
171+
return fmt.Errorf("unable to pull current tunnel config: %s", err)
188172
}
189173

190174
// Update config with new ingress rules
191175
cfg.Config.Ingress = ingress
192-
cfg, err = cf.UpdateTunnelConfiguration(ctx, aid, cloudflare.TunnelConfigurationParams{
176+
_, err = cf.UpdateTunnelConfiguration(ctx, aid, cloudflare.TunnelConfigurationParams{
193177
TunnelID: os.Getenv("CLOUDFLARE_TUNNEL_ID"),
194178
Config: cfg.Config,
195179
})
196180
if err != nil {
197-
return fmt.Errorf("unable to update tunnel config, %s", err.Error())
181+
return fmt.Errorf("unable to update tunnel config: %s", err)
198182
}
199183

200184
log.Info("tunnel config updated")
201185

202-
// Update DNS to point to new tunnel
186+
// Update DNS records
187+
tunnelContent := fmt.Sprintf("%s.cfargotunnel.com", os.Getenv("CLOUDFLARE_TUNNEL_ID"))
188+
currentHostnames := make(map[string]struct{})
189+
for _, i := range ingress {
190+
if i.Hostname != "" {
191+
currentHostnames[i.Hostname] = struct{}{}
192+
}
193+
}
194+
195+
zid := cloudflare.ZoneIdentifier(os.Getenv("CLOUDFLARE_ZONE_ID"))
196+
records, _, err := cf.ListDNSRecords(ctx, zid, cloudflare.ListDNSRecordsParams{Type: "CNAME"})
197+
if err != nil {
198+
return fmt.Errorf("error listing DNS records: %s", err)
199+
}
200+
201+
for _, record := range records {
202+
if record.Type == "CNAME" && record.Content == tunnelContent {
203+
if _, exists := currentHostnames[record.Name]; !exists {
204+
// Delete unused CNAME record
205+
err := cf.DeleteDNSRecord(ctx, zid, record.ID)
206+
if err != nil {
207+
log.WithFields(log.Fields{
208+
"domain": record.Name,
209+
"error": err,
210+
}).Error("failed to delete unused DNS record")
211+
continue
212+
}
213+
log.WithFields(log.Fields{
214+
"domain": record.Name,
215+
}).Info("deleted unused DNS record")
216+
}
217+
}
218+
}
219+
203220
for _, i := range ingress {
204221
if i.Hostname == "" {
205222
continue
206223
}
207224

208225
var proxied bool = true
209-
210226
record := cloudflare.DNSRecord{
211227
Type: "CNAME",
212228
Name: i.Hostname,
213-
Content: fmt.Sprintf("%s.cfargotunnel.com", os.Getenv("CLOUDFLARE_TUNNEL_ID")),
229+
Content: tunnelContent,
214230
TTL: 1,
215231
Proxied: &proxied,
216232
}
217233

218-
zid := cloudflare.ZoneIdentifier(os.Getenv("CLOUDFLARE_ZONE_ID"))
219234
r, _, err := cf.ListDNSRecords(ctx, zid, cloudflare.ListDNSRecordsParams{Name: i.Hostname})
220235
if err != nil {
221-
return fmt.Errorf("err checking DNS records, %s", err.Error())
236+
return fmt.Errorf("error checking DNS records: %s", err)
222237
}
223238

224239
if len(r) == 0 {
@@ -230,7 +245,7 @@ func updateTunnels(ctx context.Context, cf *cloudflare.API, ingress []cloudflare
230245
Proxied: record.Proxied,
231246
})
232247
if err != nil {
233-
return fmt.Errorf("unable to create DNS record, %s", err.Error())
248+
return fmt.Errorf("unable to create DNS record: %s", err)
234249
}
235250
log.WithFields(log.Fields{
236251
"domain": record.Name,
@@ -240,15 +255,15 @@ func updateTunnels(ctx context.Context, cf *cloudflare.API, ingress []cloudflare
240255

241256
if r[0].Content != record.Content {
242257
_, err = cf.UpdateDNSRecord(ctx, zid, cloudflare.UpdateDNSRecordParams{
243-
ID: r[0].ID, // Use the actual ID from the retrieved record
258+
ID: r[0].ID,
244259
Name: record.Name,
245260
Type: record.Type,
246261
Content: record.Content,
247262
TTL: record.TTL,
248263
Proxied: record.Proxied,
249264
})
250265
if err != nil {
251-
return fmt.Errorf("could not update record for %s, %s", i.Hostname, err)
266+
return fmt.Errorf("could not update record for %s: %s", i.Hostname, err)
252267
}
253268
log.WithFields(log.Fields{
254269
"domain": record.Name,

0 commit comments

Comments
 (0)