|
3 | 3 |
|
4 | 4 | require "k8s-ruby" |
5 | 5 | require "logger" |
| 6 | +require "optparse" |
| 7 | +require "socket" |
6 | 8 |
|
7 | 9 | class HairpinProxyController |
8 | 10 | COMMENT_LINE_SUFFIX = "# Added by hairpin-proxy" |
@@ -32,7 +34,9 @@ def fetch_ingress_hosts |
32 | 34 | end |
33 | 35 | }.flatten |
34 | 36 | all_tls_blocks = all_ingresses.map { |r| r.spec.tls }.flatten.compact |
35 | | - all_tls_blocks.map(&:hosts).flatten.compact.sort.uniq |
| 37 | + hosts = all_tls_blocks.map(&:hosts).flatten.compact |
| 38 | + hosts.filter! { |host| /\A[A-Za-z0-9.\-_]+\z/.match?(host) } |
| 39 | + hosts.sort.uniq |
36 | 40 | end |
37 | 41 |
|
38 | 42 | def coredns_corefile_with_rewrite_rules(original_corefile, hosts) |
@@ -68,10 +72,69 @@ def check_and_rewrite_coredns |
68 | 72 | end |
69 | 73 | end |
70 | 74 |
|
| 75 | + def dns_rewrite_destination_ip_address |
| 76 | + Addrinfo.ip(DNS_REWRITE_DESTINATION).ip_address |
| 77 | + end |
| 78 | + |
| 79 | + def etchosts_with_rewrite_rules(original_etchosts, hosts) |
| 80 | + # Returns a String represeting the original /etc/hosts file, modified to include a rule for |
| 81 | + # mapping *hosts to dns_rewrite_destination_ip_address. This handles kubelet and the node's Docker engine, |
| 82 | + # which does not go through CoreDNS. |
| 83 | + # This is an idempotent transformation because our rewrites are labeled with COMMENT_LINE_SUFFIX. |
| 84 | + |
| 85 | + # Extract base configuration, without our hairpin-proxy rewrites |
| 86 | + our_lines, original_lines = original_etchosts.strip.split("\n").partition { |line| line.strip.end_with?(COMMENT_LINE_SUFFIX) } |
| 87 | + |
| 88 | + ip = dns_rewrite_destination_ip_address |
| 89 | + hostlist = hosts.join(" ") |
| 90 | + new_rewrite_line = "#{ip}\t#{hostlist} #{COMMENT_LINE_SUFFIX}" |
| 91 | + |
| 92 | + if our_lines == [new_rewrite_line] |
| 93 | + # Return early so that we're indifferent to the ordering of /etc/hosts lines. |
| 94 | + return original_etchosts |
| 95 | + end |
| 96 | + |
| 97 | + (original_lines + [new_rewrite_line]).join("\n") + "\n" |
| 98 | + end |
| 99 | + |
| 100 | + def check_and_rewrite_etchosts(etchosts_path) |
| 101 | + @log.info("Polling all Ingress resources and etchosts file at #{etchosts_path}...") |
| 102 | + hosts = fetch_ingress_hosts |
| 103 | + |
| 104 | + old_etchostsfile = File.read(etchosts_path) |
| 105 | + new_etchostsfile = etchosts_with_rewrite_rules(old_etchostsfile, hosts) |
| 106 | + |
| 107 | + if old_etchostsfile.strip != new_etchostsfile.strip |
| 108 | + @log.info("/etc/hosts has changed! New contents:\n#{new_etchostsfile}\nWriting to #{etchosts_path}...") |
| 109 | + File.write(etchosts_path, new_etchostsfile) |
| 110 | + end |
| 111 | + end |
| 112 | + |
71 | 113 | def main_loop |
| 114 | + etchosts_path = nil |
| 115 | + |
| 116 | + OptionParser.new { |opts| |
| 117 | + opts.on("--etc-hosts ETCHOSTSPATH", "Path to writable /etc/hosts file") do |h| |
| 118 | + etchosts_path = h |
| 119 | + raise "File #{etchosts_path} doesn't exist!" unless File.exist?(etchosts_path) |
| 120 | + raise "File #{etchosts_path} isn't writable!" unless File.writable?(etchosts_path) |
| 121 | + end |
| 122 | + }.parse! |
| 123 | + |
| 124 | + if etchosts_path && etchosts_path != "" |
| 125 | + @log.info("Starting in /etc/hosts mutation mode on #{etchosts_path}. (Intended to be run as a DaemonSet: one instance per Node.)") |
| 126 | + else |
| 127 | + etchosts_path = nil |
| 128 | + @log.info("Starting in CoreDNS mode. (Indended to be run as a Deployment: one instance per cluster.)") |
| 129 | + end |
| 130 | + |
72 | 131 | @log.info("Starting main_loop with #{POLL_INTERVAL}s polling interval.") |
73 | 132 | loop do |
74 | | - check_and_rewrite_coredns |
| 133 | + if etchosts_path.nil? |
| 134 | + check_and_rewrite_coredns |
| 135 | + else |
| 136 | + check_and_rewrite_etchosts(etchosts_path) |
| 137 | + end |
75 | 138 |
|
76 | 139 | sleep(POLL_INTERVAL) |
77 | 140 | end |
|
0 commit comments