k8s_internal_lb 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.md +21 -0
- data/README.md +27 -0
- data/bin/k8s_internal_lb +54 -0
- data/lib/k8s_internal_lb.rb +38 -0
- data/lib/k8s_internal_lb/address.rb +64 -0
- data/lib/k8s_internal_lb/client.rb +157 -0
- data/lib/k8s_internal_lb/endpoint.rb +57 -0
- data/lib/k8s_internal_lb/port.rb +67 -0
- data/lib/k8s_internal_lb/service.rb +75 -0
- data/lib/k8s_internal_lb/services/http.rb +106 -0
- data/lib/k8s_internal_lb/services/tcp.rb +45 -0
- data/lib/k8s_internal_lb/version.rb +5 -0
- metadata +158 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 300b5f39966679025bdaedf677e720a095c7674d68e695004c6dc7ed6fac59d6
|
4
|
+
data.tar.gz: c2faa51598fb6d92230bcbb21f1b402f37885e2f64182231f0ffd17158ea4b83
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c306f3cb2a0052cc9938d5820d1c88e4b5d187cff0b66570dd48d159f884a581e1b24957c800cb6bdb572ebd7718267b27873a38841ec23f406915d910cebb83
|
7
|
+
data.tar.gz: ba80dde312f844a5babaff213ae5745e352fa65d0763ef07b06f02383997d1822162a178e0ce762e7babf3432a3b2d2ef86b0f5f2fc4997f4874b220ca8acf2b
|
data/CHANGELOG.md
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 Alexander Olofsson
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Kubernetes Internal Load-balancer
|
2
|
+
|
3
|
+
This is a Ruby application to configure your K8s cluster to work as a load-balancer, by utilizing the ingress and service/endpoint resources.
|
4
|
+
|
5
|
+
The common flow is to set up an ingress to talk to a ClusterIP service without a selector, and letting this application populate the endpoints list.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Install it yourself as:
|
10
|
+
|
11
|
+
$ gem install k8s_internal_lb
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
Run the application by specifying a configuration rb file, it can run in both one-shot mode as well as continuously.
|
16
|
+
|
17
|
+
$ k8s_internal_lb
|
18
|
+
|
19
|
+
Check the provided [examples](examples/) for ideas on how to configure the system.
|
20
|
+
|
21
|
+
## Contributing
|
22
|
+
|
23
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ananace/k8s_internal_lb
|
24
|
+
|
25
|
+
## License
|
26
|
+
|
27
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/bin/k8s_internal_lb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'k8s_internal_lb'
|
5
|
+
require 'optparse'
|
6
|
+
require 'ostruct'
|
7
|
+
|
8
|
+
options = OpenStruct.new
|
9
|
+
parser = OptParse.new do |opts|
|
10
|
+
opts.banner = 'Usage: k8s_internal_lb [options...]'
|
11
|
+
|
12
|
+
opts.on('-c', '--config=FILE', 'Run with a specific configuration file, can be specified multiple times') do |file|
|
13
|
+
raise ArgumentError, 'Not a valid path' unless File.exist? file
|
14
|
+
|
15
|
+
(options.config_files ||= []) << file
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on('-v', '--verbose', 'Increase log level') do
|
19
|
+
options.verbose = true
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on('-h', '--help', 'Print this text and exit') do
|
23
|
+
puts parser
|
24
|
+
exit
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on('-V', '--version', 'Print the application version and exit') do
|
28
|
+
puts "K8sInternalLb v#{K8sInternalLb::VERSION}"
|
29
|
+
exit
|
30
|
+
end
|
31
|
+
end
|
32
|
+
parser.parse!
|
33
|
+
|
34
|
+
if options.config_files&.any?
|
35
|
+
options.config_files.each do |file|
|
36
|
+
if File.directory? file
|
37
|
+
Dir.entries(file).select { |f| f.end_with? '.rb' }.each do |f|
|
38
|
+
load File.join(file, f)
|
39
|
+
end
|
40
|
+
else
|
41
|
+
load file
|
42
|
+
end
|
43
|
+
end
|
44
|
+
else
|
45
|
+
load '/etc/k8s_internal_lb.rb' if File.exist? '/etc/k8s_internal_lb.rb'
|
46
|
+
load './config.rb' if File.exist? 'config.rb'
|
47
|
+
end
|
48
|
+
|
49
|
+
K8sInternalLb.debug! if options.verbose
|
50
|
+
|
51
|
+
client = K8sInternalLb::Client.instance
|
52
|
+
raise 'No services loaded, aborting' if client.services.empty?
|
53
|
+
|
54
|
+
client.run
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'k8s_internal_lb/version'
|
4
|
+
require 'kubeclient'
|
5
|
+
|
6
|
+
autoload :Logging, 'logging'
|
7
|
+
|
8
|
+
module K8sInternalLb
|
9
|
+
autoload :Address, 'k8s_internal_lb/address'
|
10
|
+
autoload :Client, 'k8s_internal_lb/client'
|
11
|
+
autoload :Endpoint, 'k8s_internal_lb/endpoint'
|
12
|
+
autoload :Port, 'k8s_internal_lb/port'
|
13
|
+
autoload :Service, 'k8s_internal_lb/service'
|
14
|
+
|
15
|
+
module Services
|
16
|
+
autoload :HTTP, 'k8s_internal_lb/services/http'
|
17
|
+
autoload :TCP, 'k8s_internal_lb/services/tcp'
|
18
|
+
end
|
19
|
+
|
20
|
+
class Error < StandardError; end
|
21
|
+
|
22
|
+
def self.configure!(&block)
|
23
|
+
block.call Client.instance
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.debug!
|
27
|
+
logger.level = :debug
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.logger
|
31
|
+
@logger ||= ::Logging.logger[self].tap do |logger|
|
32
|
+
logger.add_appenders ::Logging.appenders.stdout
|
33
|
+
logger.level = :info
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
K8sInternalLb.logger # Set up logger
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ipaddr'
|
4
|
+
autoload :Resolv, 'resolv'
|
5
|
+
|
6
|
+
module K8sInternalLb
|
7
|
+
class Address
|
8
|
+
attr_reader :hostname, :ip
|
9
|
+
|
10
|
+
def initialize(hostname: nil, ip: nil, fqdn: nil)
|
11
|
+
raise ArgumentError, 'missing keyword: ip' if fqdn.nil? && ip.nil?
|
12
|
+
|
13
|
+
if fqdn
|
14
|
+
ip ||= Resolv.getaddress fqdn
|
15
|
+
hostname ||= fqdn.split('.').first
|
16
|
+
end
|
17
|
+
|
18
|
+
self.hostname = hostname
|
19
|
+
self.ip = ip
|
20
|
+
end
|
21
|
+
|
22
|
+
def hostname=(hostname)
|
23
|
+
if hostname.nil? || hostname.empty?
|
24
|
+
@hostname = nil
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
hostname = hostname.to_s.downcase
|
29
|
+
|
30
|
+
raise ArgumentError, 'Hostname is not allowed to be an FQDN' if hostname.include? '.'
|
31
|
+
|
32
|
+
@hostname = hostname
|
33
|
+
end
|
34
|
+
|
35
|
+
def ip=(ip)
|
36
|
+
ip = IPAddr.new(ip.to_s) unless ip.is_a? IPAddr
|
37
|
+
|
38
|
+
@ip = ip
|
39
|
+
end
|
40
|
+
|
41
|
+
# JSON encoding
|
42
|
+
def to_json(*params)
|
43
|
+
{
|
44
|
+
hostname: hostname,
|
45
|
+
ip: ip
|
46
|
+
}.compact.to_json(*params)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Equality overriding
|
50
|
+
def ==(other)
|
51
|
+
return unless other.respond_to?(:hostname) && other.respond_to?(:ip)
|
52
|
+
|
53
|
+
hostname == other.hostname && ip == other.ip
|
54
|
+
end
|
55
|
+
|
56
|
+
def hash
|
57
|
+
[hostname, ip].hash
|
58
|
+
end
|
59
|
+
|
60
|
+
def eql?(other)
|
61
|
+
self == other
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module K8sInternalLb
|
6
|
+
class Client
|
7
|
+
TIMESTAMP_ANNOTATION = 'com.github.ananace.k8s-internal-lb/timestamp'
|
8
|
+
|
9
|
+
attr_accessor :kubeclient_options, :namespace, :auth_options, :ssl_options, :server, :api_version
|
10
|
+
attr_accessor :sleep_duration
|
11
|
+
attr_reader :services
|
12
|
+
|
13
|
+
def self.instance
|
14
|
+
@instance ||= Client.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def in_cluster?
|
18
|
+
# FIXME: Better detection, actually look for the necessary cluster components
|
19
|
+
Dir.exist? '/var/run/secrets/kubernetes.io'
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_service(name, **data)
|
23
|
+
service = nil
|
24
|
+
|
25
|
+
if name.is_a? Service
|
26
|
+
service = name
|
27
|
+
name = service.name
|
28
|
+
else
|
29
|
+
data[:name] ||= name
|
30
|
+
service = Service.create(**data)
|
31
|
+
end
|
32
|
+
|
33
|
+
k8s_service = get_endpoint(service)
|
34
|
+
raise 'Unable to find service' if k8s_service.nil?
|
35
|
+
|
36
|
+
if k8s_service.metadata&.annotations&.to_hash&.key? TIMESTAMP_ANNOTATION
|
37
|
+
ts = k8s_service.annotations[TIMESTAMP_ANNOTATION]
|
38
|
+
if ts =~ /\A\d+\z/
|
39
|
+
service.last_update = Time.at(ts.to_i)
|
40
|
+
else
|
41
|
+
service.last_update = Time.parse(ts)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
@services[name] = service
|
46
|
+
end
|
47
|
+
|
48
|
+
def remove_service(name)
|
49
|
+
@services.delete name
|
50
|
+
end
|
51
|
+
|
52
|
+
def run
|
53
|
+
loop do
|
54
|
+
sleep_duration = @sleep_duration
|
55
|
+
|
56
|
+
@services.each do |name, service|
|
57
|
+
logger.debug "Checking #{name} for interval"
|
58
|
+
|
59
|
+
diff = (Time.now - service.last_update)
|
60
|
+
until_next = service.interval - diff
|
61
|
+
sleep_duration = until_next if until_next.positive? && until_next < sleep_duration
|
62
|
+
|
63
|
+
next unless diff >= service.interval
|
64
|
+
|
65
|
+
logger.debug "Interval reached on #{name}, running update"
|
66
|
+
update(service)
|
67
|
+
end
|
68
|
+
|
69
|
+
sleep sleep_duration
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def initialize
|
76
|
+
@sleep_duration = 5
|
77
|
+
|
78
|
+
@kubeclient_options = {}
|
79
|
+
@auth_options = {}
|
80
|
+
@ssl_options = {}
|
81
|
+
|
82
|
+
@namespace = nil
|
83
|
+
@server = nil
|
84
|
+
@api_version = 'v1'
|
85
|
+
|
86
|
+
@services = {}
|
87
|
+
|
88
|
+
return unless in_cluster?
|
89
|
+
|
90
|
+
@server = 'https://kubernetes.default.svc'
|
91
|
+
@namespace ||= File.read('/var/run/secrets/kubernetes.io/serviceaccount/namespace')
|
92
|
+
if @auth_options.empty?
|
93
|
+
@auth_options = {
|
94
|
+
bearer_token_file: '/var/run/secrets/kubernetes.io/serviceaccount/token'
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
return unless File.exist?('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt')
|
99
|
+
|
100
|
+
@ssl_options[:ca_file] = '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
|
101
|
+
end
|
102
|
+
|
103
|
+
def logger
|
104
|
+
@logger ||= Logging::Logger[self]
|
105
|
+
end
|
106
|
+
|
107
|
+
def update(service, force: false)
|
108
|
+
service = @services[service] unless service.is_a? Service
|
109
|
+
|
110
|
+
old_endpoints = service.endpoints.dup
|
111
|
+
service.last_update = Time.now
|
112
|
+
service.update
|
113
|
+
endpoints = service.endpoints
|
114
|
+
|
115
|
+
return true if old_endpoints == endpoints && !force
|
116
|
+
|
117
|
+
logger.info "Active endpoints have changed for #{service.name}, updating cluster data to #{service.to_subsets.to_json}"
|
118
|
+
|
119
|
+
kubeclient.patch_endpoint(
|
120
|
+
service.name,
|
121
|
+
{
|
122
|
+
metadata: {
|
123
|
+
annotations: {
|
124
|
+
TIMESTAMP_ANNOTATION => Time.now.to_s
|
125
|
+
}
|
126
|
+
},
|
127
|
+
subsets: service.to_subsets
|
128
|
+
},
|
129
|
+
service.namespace || namespace
|
130
|
+
)
|
131
|
+
rescue StandardError => e
|
132
|
+
raise e
|
133
|
+
end
|
134
|
+
|
135
|
+
def get_service(service)
|
136
|
+
kubeclient.get_service(service.name, service.namespace || namespace)
|
137
|
+
rescue Kubeclient::ResourceNotFoundError
|
138
|
+
nil
|
139
|
+
end
|
140
|
+
|
141
|
+
def get_endpoint(service)
|
142
|
+
kubeclient.get_endpoint(service.name, service.namespace || namespace)
|
143
|
+
rescue Kubeclient::ResourceNotFoundError
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
|
147
|
+
def kubeclient
|
148
|
+
@kubeclient ||= Kubeclient::Client.new(
|
149
|
+
server,
|
150
|
+
api_version,
|
151
|
+
auth_options: auth_options,
|
152
|
+
ssl_options: ssl_options,
|
153
|
+
**kubeclient_options
|
154
|
+
)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module K8sInternalLb
|
4
|
+
class Endpoint
|
5
|
+
attr_reader :address, :port, :status
|
6
|
+
|
7
|
+
def initialize(address:, port:, status:)
|
8
|
+
self.address = address
|
9
|
+
self.port = port
|
10
|
+
self.status = status
|
11
|
+
end
|
12
|
+
|
13
|
+
def address=(address)
|
14
|
+
raise ArgumentError, 'Address must be an Address object' unless address.is_a? Address
|
15
|
+
|
16
|
+
@address = address
|
17
|
+
end
|
18
|
+
|
19
|
+
def port=(port)
|
20
|
+
raise ArgumentError, 'Port must be a Port object' unless port.is_a? Port
|
21
|
+
|
22
|
+
@port = port
|
23
|
+
end
|
24
|
+
|
25
|
+
def status=(status)
|
26
|
+
status = status ? :ready : :not_ready if [true, false].include? status
|
27
|
+
status = status.to_s.downcase.to_sym
|
28
|
+
|
29
|
+
raise ArgumentError, 'Status must be one of :ready, :not_ready' unless %i[ready not_ready].include? status
|
30
|
+
|
31
|
+
@status = status
|
32
|
+
end
|
33
|
+
|
34
|
+
def ready?
|
35
|
+
@status == :ready
|
36
|
+
end
|
37
|
+
|
38
|
+
def not_ready?
|
39
|
+
@status == :not_ready
|
40
|
+
end
|
41
|
+
|
42
|
+
# Equality overriding
|
43
|
+
def ==(other)
|
44
|
+
return unless other.respond_to?(:address) && other.respond_to?(:port) && other.respond_to?(:status)
|
45
|
+
|
46
|
+
address == other.address && port == other.port && status == other.status
|
47
|
+
end
|
48
|
+
|
49
|
+
def hash
|
50
|
+
[address, port, status].hash
|
51
|
+
end
|
52
|
+
|
53
|
+
def eql?(other)
|
54
|
+
self == other
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module K8sInternalLb
|
4
|
+
class Port
|
5
|
+
attr_reader :protocol, :port
|
6
|
+
attr_accessor :name
|
7
|
+
|
8
|
+
def initialize(name: nil, port:, protocol: :TCP)
|
9
|
+
name = nil if name&.empty?
|
10
|
+
@name = name
|
11
|
+
self.port = port
|
12
|
+
self.protocol = protocol
|
13
|
+
end
|
14
|
+
|
15
|
+
def protocol=(protocol)
|
16
|
+
protocol = protocol.to_s.upcase.to_sym
|
17
|
+
|
18
|
+
raise ArgumentError, 'Protocol must be one of :TCP, :UDP, :SCTP' unless %i[TCP UDP SCTP].include? protocol
|
19
|
+
|
20
|
+
@protocol = protocol
|
21
|
+
end
|
22
|
+
|
23
|
+
def port=(port)
|
24
|
+
port = port.to_i unless port.is_a? Integer
|
25
|
+
|
26
|
+
raise ArgumentError, 'Port must be a valid port number' unless (1..65_535).include? port
|
27
|
+
|
28
|
+
@port = port
|
29
|
+
end
|
30
|
+
|
31
|
+
def tcp?
|
32
|
+
protocol == :TCP
|
33
|
+
end
|
34
|
+
|
35
|
+
def udp?
|
36
|
+
protocol == :UDP
|
37
|
+
end
|
38
|
+
|
39
|
+
def sctp?
|
40
|
+
protocol == :SCTP
|
41
|
+
end
|
42
|
+
|
43
|
+
# JSON encoding
|
44
|
+
def to_json(*params)
|
45
|
+
{
|
46
|
+
name: name,
|
47
|
+
port: port,
|
48
|
+
protocol: protocol
|
49
|
+
}.compact.to_json(*params)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Equality overriding
|
53
|
+
def ==(other)
|
54
|
+
return unless other.respond_to?(:name) && other.respond_to?(:port) && other.respond_to?(:protocol)
|
55
|
+
|
56
|
+
name == other.name && port == other.port && protocol == other.protocol
|
57
|
+
end
|
58
|
+
|
59
|
+
def hash
|
60
|
+
[name, port, protocol].hash
|
61
|
+
end
|
62
|
+
|
63
|
+
def eql?(other)
|
64
|
+
self == other
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module K8sInternalLb
|
4
|
+
class Service
|
5
|
+
attr_reader :name
|
6
|
+
attr_accessor :namespace, :interval, :last_update, :endpoints, :ports
|
7
|
+
|
8
|
+
def self.create(type: :TCP, **params)
|
9
|
+
raise ArgumentError, 'Must specify service type' if type.nil?
|
10
|
+
|
11
|
+
klass = Services.const_get type
|
12
|
+
raise ArgumentError, 'Unknown service type' if klass.nil?
|
13
|
+
|
14
|
+
klass.new(**params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def logger
|
18
|
+
@logger ||= Logging::Logger[self]
|
19
|
+
end
|
20
|
+
|
21
|
+
def update
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_subsets
|
26
|
+
grouped = endpoints.group_by(&:port)
|
27
|
+
|
28
|
+
# TODO: Find all port combinations that result in the same list of ready
|
29
|
+
# and not-ready addresses, and combine them into a single pair of
|
30
|
+
# multiple ports.
|
31
|
+
#
|
32
|
+
# {
|
33
|
+
# 1 => { active: [A, B], inactive: [C] },
|
34
|
+
# 2 => { active: [A, B], inactive: [C] }
|
35
|
+
# }
|
36
|
+
# =>
|
37
|
+
# {
|
38
|
+
# [1,2] => { active: [A, B], inactive: [C] }
|
39
|
+
# }
|
40
|
+
|
41
|
+
grouped = grouped.map do |p, g|
|
42
|
+
{
|
43
|
+
addresses: g.select(&:ready?).map(&:address),
|
44
|
+
notReadyAddresses: g.select(&:not_ready?).map(&:address),
|
45
|
+
ports: [p]
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
# grouped = grouped.group_by { |s| s[:addresses] + s[:notReadyAddresses] }
|
50
|
+
# .map do |_, s|
|
51
|
+
# v = s.first
|
52
|
+
#
|
53
|
+
# v[:ports] = s.reduce([]) { |sum, e| sum << e[:ports] }
|
54
|
+
#
|
55
|
+
# v
|
56
|
+
# end
|
57
|
+
|
58
|
+
grouped
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def initialize(name:, namespace: nil, ports:, interval: 10, **_params)
|
64
|
+
raise ArgumentError, 'Ports must be a list of Port objects' unless ports.is_a?(Array) && ports.all? { |p| p.is_a? Port }
|
65
|
+
raise ArgumentError, 'Interval must be a positive number' unless interval.is_a?(Numeric) && interval.positive?
|
66
|
+
|
67
|
+
@name = name
|
68
|
+
@namespace = namespace
|
69
|
+
@ports = ports
|
70
|
+
@interval = interval
|
71
|
+
@last_update = Time.at(0)
|
72
|
+
@endpoints = []
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
|
5
|
+
module K8sInternalLb
|
6
|
+
module Services
|
7
|
+
class HTTP < Service
|
8
|
+
attr_accessor :timeout, :http_opts
|
9
|
+
attr_reader :addresses, :method, :expects
|
10
|
+
|
11
|
+
def initialize(addresses:, method: :head, expects: :success, timeout: 5, http_opts: {}, **params)
|
12
|
+
params[:ports] ||= []
|
13
|
+
super
|
14
|
+
|
15
|
+
self.method = method
|
16
|
+
self.expects = expects
|
17
|
+
self.addresses = addresses
|
18
|
+
|
19
|
+
@timeout = timeout
|
20
|
+
@http_opts = http_opts
|
21
|
+
|
22
|
+
@address_hash = nil
|
23
|
+
@port_hash = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def ports
|
27
|
+
# Ensure data is recalculated if addresses or ports change
|
28
|
+
address_hash = @addresses.hash
|
29
|
+
port_hash = super.hash
|
30
|
+
@http_ports = nil if @address_hash != address_hash
|
31
|
+
@http_ports = nil if @port_hash != port_hash
|
32
|
+
@address_hash = address_hash
|
33
|
+
@port_hash = port_hash
|
34
|
+
|
35
|
+
@http_ports ||= begin
|
36
|
+
http_ports = @addresses.map { |addr| Port.new(port: addr.port) }.uniq
|
37
|
+
|
38
|
+
# Copy port names over where appropriate
|
39
|
+
super.each do |port|
|
40
|
+
http_port = http_ports.find { |hp| hp.port == port.port }
|
41
|
+
next unless http_port
|
42
|
+
|
43
|
+
http_port.name = port.name
|
44
|
+
end
|
45
|
+
|
46
|
+
http_ports
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def addresses=(addresses)
|
51
|
+
addresses = addresses.map do |addr|
|
52
|
+
addr = URI(addr)
|
53
|
+
|
54
|
+
addr.path = '/' if addr.path.empty?
|
55
|
+
|
56
|
+
addr
|
57
|
+
end
|
58
|
+
|
59
|
+
@addresses = addresses
|
60
|
+
end
|
61
|
+
|
62
|
+
def method=(method)
|
63
|
+
raise ArgumentError, 'Invalid HTTP request method' unless %i[get get2 head head2 options post put].include? method
|
64
|
+
|
65
|
+
@method = method
|
66
|
+
end
|
67
|
+
|
68
|
+
def expects=(expects)
|
69
|
+
raise ArgumentError, 'Invalid expects type' unless expects == :success || [Integer, Proc].include?(expects.class)
|
70
|
+
|
71
|
+
@expects = expects
|
72
|
+
end
|
73
|
+
|
74
|
+
def update
|
75
|
+
@endpoints = addresses.map do |addr|
|
76
|
+
available = false
|
77
|
+
|
78
|
+
begin
|
79
|
+
ssl = addr.scheme == 'https'
|
80
|
+
|
81
|
+
Net::HTTP.start(addr.host, addr.port, use_ssl: ssl, read_timeout: timeout, **http_opts) do |h|
|
82
|
+
resp = h.send(@method, addr.path)
|
83
|
+
logger.debug "#{addr} - #{resp.inspect}"
|
84
|
+
|
85
|
+
available = if @expects == :success
|
86
|
+
resp.is_a? Net::HTTPSuccess
|
87
|
+
elsif @expects.is_a? Numeric
|
88
|
+
resp.code == @expects
|
89
|
+
elsif @expects.is_a? Proc
|
90
|
+
@expects.call(resp)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
rescue StandardError => e
|
94
|
+
logger.debug "#{addr} - #{e.class}: #{e.message}\n#{e.backtrace[0, 20].join("\n")}"
|
95
|
+
available = false # Assume failures to mean inaccessibility
|
96
|
+
end
|
97
|
+
|
98
|
+
e_addr = Address.new fqdn: addr.host
|
99
|
+
Endpoint.new address: e_addr, port: ports.find { |p| p.port == addr.port }, status: available
|
100
|
+
end
|
101
|
+
|
102
|
+
true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
module K8sInternalLb
|
7
|
+
module Services
|
8
|
+
class TCP < Service
|
9
|
+
attr_accessor :addresses, :timeout
|
10
|
+
|
11
|
+
def initialize(addresses:, timeout: 1, **params)
|
12
|
+
super
|
13
|
+
|
14
|
+
@addresses = addresses
|
15
|
+
@timeout = timeout
|
16
|
+
end
|
17
|
+
|
18
|
+
def update
|
19
|
+
raise 'No TCP ports provided' if ports.select(&:tcp?).empty?
|
20
|
+
|
21
|
+
@endpoints = addresses.map do |addr|
|
22
|
+
ports.select(&:tcp?).map do |port|
|
23
|
+
available = \
|
24
|
+
begin
|
25
|
+
Timeout.timeout(timeout) do
|
26
|
+
begin
|
27
|
+
TCPSocket.new(addr.ip.to_s, port.port).close
|
28
|
+
true
|
29
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
rescue Timeout::Error
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
Endpoint.new address: addr, port: port, status: available
|
38
|
+
end
|
39
|
+
end.flatten
|
40
|
+
|
41
|
+
true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
metadata
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: k8s_internal_lb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alexander Olofsson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-03-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: mocha
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: simplecov
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: test-unit
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: kubeclient
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: logging
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: thor
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: A ruby application for setting up your k8s cluster as a load balancer.
|
112
|
+
email:
|
113
|
+
- alexander.olofsson@liu.se
|
114
|
+
executables:
|
115
|
+
- k8s_internal_lb
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files:
|
118
|
+
- CHANGELOG.md
|
119
|
+
- LICENSE.md
|
120
|
+
- README.md
|
121
|
+
files:
|
122
|
+
- CHANGELOG.md
|
123
|
+
- LICENSE.md
|
124
|
+
- README.md
|
125
|
+
- bin/k8s_internal_lb
|
126
|
+
- lib/k8s_internal_lb.rb
|
127
|
+
- lib/k8s_internal_lb/address.rb
|
128
|
+
- lib/k8s_internal_lb/client.rb
|
129
|
+
- lib/k8s_internal_lb/endpoint.rb
|
130
|
+
- lib/k8s_internal_lb/port.rb
|
131
|
+
- lib/k8s_internal_lb/service.rb
|
132
|
+
- lib/k8s_internal_lb/services/http.rb
|
133
|
+
- lib/k8s_internal_lb/services/tcp.rb
|
134
|
+
- lib/k8s_internal_lb/version.rb
|
135
|
+
homepage: https://github.com/ananace/k8s_internal_lb
|
136
|
+
licenses:
|
137
|
+
- MIT
|
138
|
+
metadata: {}
|
139
|
+
post_install_message:
|
140
|
+
rdoc_options: []
|
141
|
+
require_paths:
|
142
|
+
- lib
|
143
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
144
|
+
requirements:
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0'
|
148
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
requirements: []
|
154
|
+
rubygems_version: 3.1.2
|
155
|
+
signing_key:
|
156
|
+
specification_version: 4
|
157
|
+
summary: A ruby application for setting up your k8s cluster as a load balancer.
|
158
|
+
test_files: []
|