|
1 | 1 | #!/usr/bin/env ruby |
2 | 2 | # frozen_string_literal: true |
3 | 3 |
|
4 | | -STDOUT.sync = true |
5 | | - |
6 | 4 | require "k8s-client" |
| 5 | +require "logger" |
7 | 6 |
|
8 | | -def ingress_hosts(k8s) |
9 | | - all_ingresses = k8s.api("extensions/v1beta1").resource("ingresses").list |
10 | | - |
11 | | - all_tls_blocks = all_ingresses.map { |r| r.spec.tls }.flatten.compact |
| 7 | +class HairpinProxyController |
| 8 | + COREDNS_CONFIGMAP_LINE_SUFFIX = "# Added by hairpin-proxy" |
| 9 | + DNS_REWRITE_DESTINATION = "hairpin-proxy.hairpin-proxy.svc.cluster.local" |
| 10 | + POLL_INTERVAL = ENV.fetch("POLL_INTERVAL", "15").to_i.clamp(1..) |
12 | 11 |
|
13 | | - all_tls_blocks.map(&:hosts).flatten.compact.sort.uniq |
14 | | -end |
| 12 | + def initialize |
| 13 | + @k8s = K8s::Client.in_cluster_config |
15 | 14 |
|
16 | | -def rewrite_coredns_corefile(cf, hosts) |
17 | | - cflines = cf.strip.split("\n").reject { |line| line.strip.end_with?("# Added by hairpin-proxy") } |
| 15 | + STDOUT.sync = true |
| 16 | + @log = Logger.new(STDOUT) |
| 17 | + end |
18 | 18 |
|
19 | | - main_server_line = cflines.index { |line| line.strip.start_with?(".:53 {") } |
20 | | - raise "Can't find main server line! '.:53 {' in Corefile" if main_server_line.nil? |
| 19 | + def fetch_ingress_hosts |
| 20 | + # Return a sorted Array of all unique hostnames mentioned in Ingress spec.tls.hosts blocks, in all namespaces. |
| 21 | + all_ingresses = @k8s.api("extensions/v1beta1").resource("ingresses").list |
| 22 | + all_tls_blocks = all_ingresses.map { |r| r.spec.tls }.flatten.compact |
| 23 | + all_tls_blocks.map(&:hosts).flatten.compact.sort.uniq |
| 24 | + end |
21 | 25 |
|
22 | | - rewrite_lines = hosts.map { |host| " rewrite name #{host} hairpin-proxy.hairpin-proxy.svc.cluster.local # Added by hairpin-proxy" } |
| 26 | + def coredns_corefile_with_rewrite_rules(original_corefile, hosts) |
| 27 | + # Return a String representing the original CoreDNS Corefile, modified to include rewrite rules for each of *hosts. |
| 28 | + # This is an idempotent transformation because our rewrites are labeled with COREDNS_CONFIGMAP_LINE_SUFFIX. |
23 | 29 |
|
24 | | - cflines.insert(main_server_line + 1, *rewrite_lines) |
| 30 | + # Extract base configuration, without our hairpin-proxy rewrites |
| 31 | + cflines = original_corefile.strip.split("\n").reject { |line| line.strip.end_with?(COREDNS_CONFIGMAP_LINE_SUFFIX) } |
25 | 32 |
|
26 | | - cflines.join("\n") |
27 | | -end |
| 33 | + # Create rewrite rules |
| 34 | + rewrite_lines = hosts.map { |host| " rewrite name #{host} #{DNS_REWRITE_DESTINATION} #{COREDNS_CONFIGMAP_LINE_SUFFIX}" } |
28 | 35 |
|
29 | | -def main |
30 | | - client = K8s::Client.in_cluster_config |
| 36 | + # Inject at the start of the main ".:53 { ... }" configuration block |
| 37 | + main_server_line = cflines.index { |line| line.strip.start_with?(".:53 {") } |
| 38 | + raise "Can't find main server line! '.:53 {' in Corefile" if main_server_line.nil? |
| 39 | + cflines.insert(main_server_line + 1, *rewrite_lines) |
31 | 40 |
|
32 | | - loop do |
33 | | - puts "#{Time.now}: Fetching..." |
| 41 | + cflines.join("\n") |
| 42 | + end |
34 | 43 |
|
35 | | - hosts = ingress_hosts(client) |
36 | | - cm = client.api.resource("configmaps", namespace: "kube-system").get("coredns") |
| 44 | + def main_loop |
| 45 | + @log.info("Starting main_loop with #{POLL_INTERVAL}s polling interval.") |
| 46 | + loop do |
| 47 | + @log.info("Polling all Ingress resources and CoreDNS configuration...") |
| 48 | + hosts = fetch_ingress_hosts |
| 49 | + cm = @k8s.api.resource("configmaps", namespace: "kube-system").get("coredns") |
37 | 50 |
|
38 | | - old_corefile = cm.data.Corefile |
39 | | - new_corefile = rewrite_coredns_corefile(old_corefile, hosts) |
| 51 | + old_corefile = cm.data.Corefile |
| 52 | + new_corefile = coredns_corefile_with_rewrite_rules(old_corefile, hosts) |
40 | 53 |
|
41 | | - if old_corefile.strip != new_corefile.strip |
42 | | - puts "#{Time.now}: Corefile changed!" |
43 | | - puts new_corefile |
| 54 | + if old_corefile.strip != new_corefile.strip |
| 55 | + @log.info("Corefile has changed! New contents:\n#{new_corefile}\nSending updated ConfigMap to Kubernetes API server...") |
| 56 | + cm.data.Corefile = new_corefile |
| 57 | + @k8s.api.resource("configmaps", namespace: "kube-system").update_resource(cm) |
| 58 | + end |
44 | 59 |
|
45 | | - puts "#{Time.now}: Updating ConfigMap." |
46 | | - cm.data.Corefile = new_corefile |
47 | | - client.api.resource("configmaps", namespace: "kube-system").update_resource(cm) |
| 60 | + sleep(POLL_INTERVAL) |
48 | 61 | end |
49 | | - |
50 | | - sleep(15) |
51 | 62 | end |
52 | 63 | end |
53 | 64 |
|
54 | | -main |
| 65 | +HairpinProxyController.new.main_loop if $PROGRAM_NAME == __FILE__ |
0 commit comments