k8s_internal_lb 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ ## Version 1.0.0 - 2020-03-13
2
+
3
+ Marking the current code as stable as it has been running successfully for half a year.
4
+
5
+ Still need to write up an example for doing endpoint discovery, but the code should support that fine as-is.
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).
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K8sInternalLb
4
+ VERSION = '1.0.0'
5
+ 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: []