@@ -20,44 +20,20 @@ func init() {
20
20
DisableColors : true ,
21
21
FullTimestamp : true ,
22
22
})
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
- }
31
23
}
32
24
33
25
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
50
27
cf , err := cloudflare .NewWithAPIToken (os .Getenv ("CLOUDFLARE_API_TOKEN" ))
51
28
if err != nil {
52
29
log .Fatal (err )
53
30
}
54
31
55
32
ctx := context .Background ()
56
33
34
+ // Set up Traefik API client
57
35
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" ))
61
37
62
38
pollCh := pollTraefikRouters (client )
63
39
var cache []Router
@@ -66,159 +42,198 @@ func main() {
66
42
log .Fatal (poll .Err )
67
43
}
68
44
69
- // skip if no changes to traefik routers
45
+ // Skip if no changes to Traefik routers
70
46
if reflect .DeepEqual (cache , poll .Routers ) {
71
47
continue
72
48
}
73
49
74
50
log .Info ("changes detected" )
75
51
76
- // update the cache
52
+ // Update the cache
77
53
cache = poll .Routers
78
54
79
- ingress := []cloudflare. UnvalidatedIngressRule {}
80
-
55
+ // Collect unique domains from applicable routers
56
+ uniqueDomains := make ( map [ string ] struct {})
81
57
for _ , r := range poll .Routers {
82
- // Only enabled routes
58
+ // Filter routers
83
59
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" )) {
85
63
continue
86
64
}
87
65
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" )
96
72
}
97
73
98
74
domains , err := http .ParseDomains (r .Rule )
99
75
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 )
102
79
}
103
-
104
80
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 {}{}
120
82
}
121
83
}
122
84
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
124
103
ingress = append (ingress , cloudflare.UnvalidatedIngressRule {
125
104
Service : "http_status:404" ,
126
105
})
127
106
107
+ // Update Cloudflare tunnel configuration
128
108
err = updateTunnels (ctx , cf , ingress )
129
109
if err != nil {
130
110
log .Fatal (err )
131
111
}
132
112
}
133
113
}
134
114
135
- // Helper to create a bool pointer
136
- func boolPtr (b bool ) * bool {
137
- return & b
138
- }
139
-
140
115
func pollTraefikRouters (client * resty.Client ) (ch chan PollResponse ) {
141
116
ch = make (chan PollResponse )
142
117
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
147
120
c := time .Tick (10 * time .Second )
148
121
149
122
for range c {
150
123
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
+ }
151
134
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
+ }
164
142
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 )
171
153
}
172
154
173
155
ch <- pollRes
174
156
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
+ }
177
161
}
178
162
}()
179
163
return
180
164
}
181
165
182
166
func updateTunnels (ctx context.Context , cf * cloudflare.API , ingress []cloudflare.UnvalidatedIngressRule ) error {
183
- // Get Current tunnel config
167
+ // Get current tunnel config
184
168
aid := cloudflare .AccountIdentifier (os .Getenv ("CLOUDFLARE_ACCOUNT_ID" ))
185
169
cfg , err := cf .GetTunnelConfiguration (ctx , aid , os .Getenv ("CLOUDFLARE_TUNNEL_ID" ))
186
170
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 )
188
172
}
189
173
190
174
// Update config with new ingress rules
191
175
cfg .Config .Ingress = ingress
192
- cfg , err = cf .UpdateTunnelConfiguration (ctx , aid , cloudflare.TunnelConfigurationParams {
176
+ _ , err = cf .UpdateTunnelConfiguration (ctx , aid , cloudflare.TunnelConfigurationParams {
193
177
TunnelID : os .Getenv ("CLOUDFLARE_TUNNEL_ID" ),
194
178
Config : cfg .Config ,
195
179
})
196
180
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 )
198
182
}
199
183
200
184
log .Info ("tunnel config updated" )
201
185
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
+
203
220
for _ , i := range ingress {
204
221
if i .Hostname == "" {
205
222
continue
206
223
}
207
224
208
225
var proxied bool = true
209
-
210
226
record := cloudflare.DNSRecord {
211
227
Type : "CNAME" ,
212
228
Name : i .Hostname ,
213
- Content : fmt . Sprintf ( "%s.cfargotunnel.com" , os . Getenv ( "CLOUDFLARE_TUNNEL_ID" )) ,
229
+ Content : tunnelContent ,
214
230
TTL : 1 ,
215
231
Proxied : & proxied ,
216
232
}
217
233
218
- zid := cloudflare .ZoneIdentifier (os .Getenv ("CLOUDFLARE_ZONE_ID" ))
219
234
r , _ , err := cf .ListDNSRecords (ctx , zid , cloudflare.ListDNSRecordsParams {Name : i .Hostname })
220
235
if err != nil {
221
- return fmt .Errorf ("err checking DNS records, %s" , err . Error () )
236
+ return fmt .Errorf ("error checking DNS records: %s" , err )
222
237
}
223
238
224
239
if len (r ) == 0 {
@@ -230,7 +245,7 @@ func updateTunnels(ctx context.Context, cf *cloudflare.API, ingress []cloudflare
230
245
Proxied : record .Proxied ,
231
246
})
232
247
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 )
234
249
}
235
250
log .WithFields (log.Fields {
236
251
"domain" : record .Name ,
@@ -240,15 +255,15 @@ func updateTunnels(ctx context.Context, cf *cloudflare.API, ingress []cloudflare
240
255
241
256
if r [0 ].Content != record .Content {
242
257
_ , 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 ,
244
259
Name : record .Name ,
245
260
Type : record .Type ,
246
261
Content : record .Content ,
247
262
TTL : record .TTL ,
248
263
Proxied : record .Proxied ,
249
264
})
250
265
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 )
252
267
}
253
268
log .WithFields (log.Fields {
254
269
"domain" : record .Name ,
0 commit comments