fluent-plugin-label-router 0.1.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 752b0fee18d27194a9e3173eaa789626d9a27e43
4
- data.tar.gz: c99654113f3b085822b50ca9491cfb99e5e55b55
2
+ SHA256:
3
+ metadata.gz: e7a22624dbabbce0daa1bcf3d6845bf15e377d9453a5e733ad6c91b1c9d886d5
4
+ data.tar.gz: 5cb0f6eed3e37706bd2f09c0f399acd4fb6dc714aadb7d9879309575904dccfe
5
5
  SHA512:
6
- metadata.gz: b28bb46f96513f43f029e956c06b4262ffc0afe50ab7e70a380acf87751081a827a42b0060349c31811bf1bf71c188bc21b6f726c4c0ee1b9f7d7cebb7705c53
7
- data.tar.gz: 91a600e6105e278407b9d423ea7baf4547c2da741ecd044c9b4be0b7db436a032f6916e89688230c1376945db0c3379df73e8761cdc50b1577c4b3bd97369270
6
+ metadata.gz: f1240df83181e1e6188121c48dbffc4fbe226c756064155e1457dd2478aae79d6740dcb5b403202fd2ef0d55b7850f1937e6aed9f414dc73615f24f5ae3e0f2a
7
+ data.tar.gz: 13093eb11d980427a8bc5540612da4bca7a199a6aa848c2054ec157a930b4db372cd6c6ceed4ec854f783e165bfe7448f6ad53add6409f150460b49f522ea1e4
data/README.md CHANGED
@@ -32,7 +32,10 @@ $ bundle
32
32
 
33
33
  ## Configuration
34
34
 
35
- The configuration builds from `<route>` sections.
35
+ The configuration builds from `<route>` sections. Each `route` section
36
+ can have several `<match>` statement. These statements computed in order and
37
+ positive (or in case of *negate true* negative) results break the evaluation.
38
+ We can say that the sections are coupled in a **lazy evaluation OR**.
36
39
 
37
40
  ```
38
41
  <match example.tag**>
@@ -41,32 +44,88 @@ The configuration builds from `` sections.
41
44
  ...
42
45
  </route>
43
46
  <route>
44
- ...
47
+ <match>
48
+ ...
49
+ </match>
50
+ <match> #Exclude
51
+ negate true
52
+ ...
53
+ </match>
45
54
  </route>
46
55
  </match>
47
56
  ```
48
57
 
49
- | Parameter | Description | Default |
50
- |-----------|-------------|---------|
51
- | labels | Label definition to match record. Example: app:nginx | nil |
52
- | namespace | Namespaces definition to filter the record. Ignored if left empty. | "" |
53
- | @label | New @LABEL if selectors matched | nil |
54
- | tag | New tag if selectors matched | "" |
55
- | emit_mode | Emit mode. If `batch`, the plugin will emit events per labels matched. Enum: record, batch | batch |
56
- | sticky_tags | Sticky tags will match only one record from an event stream. The same tag will be treated the same way | true |
58
+ Configuration reference
59
+
60
+ | Parameter | Description | Type | Default |
61
+ |---------------|--------------------------------------------------------------------------------------------------------|---------|---------|
62
+ | emit_mode | Emit mode. If `batch`, the plugin will emit events per labels matched. Enum: record, batch | enum | batch |
63
+ | sticky_tags | Sticky tags will match only one record from an event stream. The same tag will be treated the same way | bool | true |
64
+ | default_route | If defined all non-matching record passes to this label. | string | "" |
65
+ | default_tag | If defined all non-matching record rewrited to this tag. (Can be used with label simoultanesly) | string | "" |
66
+ | \<route\> | Route the log if match with parameters defined | []route | nil |
67
+
68
+ #### \<route\>
69
+ | Parameter | Description | Type | Default |
70
+ |---------------|--------------------------------------------------------------------------------------------------------|---------|---------|
71
+ | @label | Route the matching record to the given `label` | string | "" |
72
+ | tag | Tag the matching record to the given `tag` | string | "" |
73
+ | \<match\> | List of match statements. Repeatable. | []match | nil |
74
+
75
+
76
+ #### \<match\>
77
+ | Parameter | Description | Type | Default |
78
+ |-----------------|-------------------------------------------------------------------------------|----------|----------|
79
+ | labels | Label definition to match record. Example: `app:nginx` | Hash | nil |
80
+ | namespaces | Comma separated list of namespaces. Ignored if left empty. | []string | nil |
81
+ | hosts | Comma separated list of hosts. Ignored if left empty. | []string | nil |
82
+ | container_names | Comma separated list of container names. Ignored if left empty. | []string | nil |
83
+ | negate | Negate the selector meaning to exclude matches | bool | false |
84
+
85
+ ## Rules of thumb
86
+
87
+ 1. Defining more than one namespace in `namespaces` inside a `match` statement
88
+ will check whether any of that namespaces matches.
89
+
90
+ 2. Using `sticky_tags` means that only the **first** record will be analysed per `tag`.
91
+ Keep that in mind if you are ingesting traffic that is not unique on a per tag bases.
92
+ Fluentd and fluent-bit tail logs from Kubernetes are unique per container.
93
+
94
+ 3. The plugin does not check if the configuration is valid so be careful to not define
95
+ statements like identical `match` statement with negate because the negate rule will never
96
+ be evaluated.
57
97
 
58
98
  ## Examples
59
99
 
60
- ### 1. Route specific `labels` and `namespace` to `@label` and new `tag`
100
+ ### 1. Route specific `labels` and `namespaces` to `@label` and new `tag`
61
101
  Configuration to re-tag and re-label all logs from `default` namespace with label `app=nginx` and `env=dev`.
62
102
  ```
63
103
  <match example.tag**>
64
104
  @type label_router
65
105
  <route>
66
- labels app:nginx,env:dev
67
- namespace default
68
- @label @NGINX
69
- tag new_tag
106
+ @label @NGINX
107
+ tag new_tag
108
+ <match>
109
+ labels app:nginx,env:dev
110
+ namespaces default
111
+ </match>
112
+ </route>
113
+ </match>
114
+ ```
115
+
116
+ ### 2. Exclude specific `labels` and `namespaces`
117
+ Configuration to re-tag and re-label all logs that **not** from `default` namespace **and not** have labels `ap=nginx` and `env=dev`
118
+ ```
119
+ <match example.tag**>
120
+ @type label_router
121
+ <route>
122
+ @label @NGINX
123
+ tag new_tag
124
+ <match>
125
+ negate true
126
+ labels app:nginx,env:dev
127
+ namespaces default
128
+ </match>
70
129
  </route>
71
130
  </match>
72
131
  ```
@@ -91,9 +150,11 @@ Only `labels`
91
150
  <match example.tag**>
92
151
  @type label_router
93
152
  <route>
94
- labels app:nginx
95
- @label @NGINX
96
- tag new_tag
153
+ @label @NGINX
154
+ tag new_tag
155
+ <match>
156
+ labels app:nginx
157
+ </match>
97
158
  </route>
98
159
  </match>
99
160
  ```
@@ -102,9 +163,11 @@ Only `namespace`
102
163
  <match example.tag**>
103
164
  @type label_router
104
165
  <route>
105
- namespace default
106
- @label @NGINX
107
- tag new_tag
166
+ @label @NGINX
167
+ tag new_tag
168
+ <match>
169
+ namespaces default
170
+ </match>
108
171
  </route>
109
172
  </match>
110
173
  ```
@@ -112,16 +175,31 @@ Rewrite all
112
175
  ```
113
176
  <match example.tag**>
114
177
  @type label_router
115
- <route>
116
- @label @NGINX
117
- tag new_tag
118
- </route>
178
+ <match>
179
+ @label @NGINX
180
+ tag new_tag
181
+ </match>
119
182
  </match>
120
183
  ```
121
184
 
122
185
  ### 3. One of `@label` ot `tag` configuration should be specified
123
186
  If you don't rewrite either of them fluent will **likely to crash** because it will reprocess the same messages again.
124
187
 
188
+ ### 4. Default route/tag
189
+
190
+ Use `default_label` and/or `default_tag` to route non matching records.
191
+
192
+ ```
193
+ <match example.tag**>
194
+ @type label_router
195
+ default_route @default_sink
196
+ <route>
197
+ ...
198
+ </route>
199
+ </match>
200
+ ```
201
+
202
+
125
203
  ## Copyright
126
204
 
127
205
  * Copyright(c) 2019- Banzai Cloud
@@ -3,7 +3,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
 
4
4
  Gem::Specification.new do |spec|
5
5
  spec.name = "fluent-plugin-label-router"
6
- spec.version = "0.1.3"
6
+ spec.version = "0.2.4"
7
7
  spec.authors = ["Banzai Cloud"]
8
8
  spec.email = ["info@banzaicloud.com"]
9
9
 
@@ -26,33 +26,77 @@ module Fluent
26
26
  #record_accessor_create("log")
27
27
  #record_accessor_create("$.key1.key2")
28
28
  #record_accessor_create("$['key1'][0]['key2']")
29
+ desc "Emit mode. If `batch`, the plugin will emit events per labels matched."
30
+ config_param :emit_mode, :enum, list: [:record, :batch], default: :batch
31
+ desc "Sticky tags will match only one record from an event stream. The same tag will be treated the same way"
32
+ config_param :sticky_tags, :bool, default: true
33
+ desc "Default label to drain unmatched patterns"
34
+ config_param :default_route, :string, :default => ""
35
+ desc "Default tag to drain unmatched patterns"
36
+ config_param :default_tag, :string, :default => ""
29
37
 
30
38
  config_section :route, param_name: :routes, multi: true do
31
- desc "Label definition to match record. Example: app:nginx. You can specify more values as comma separated list: key1:value1,key2:value2"
32
- config_param :labels, :hash, :default => {}
33
- desc "Namespaces definition to filter the record. Ignored if left empty."
34
- config_param :namespace, :string, :default => ""
35
39
  desc "New @LABEL if selectors matched"
36
40
  config_param :@label, :string, :default => nil
37
41
  desc "New tag if selectors matched"
38
42
  config_param :tag, :string, :default => ""
39
- desc "Emit mode. If `batch`, the plugin will emit events per labels matched."
40
- config_param :emit_mode, :enum, list: [:record, :batch], default: :batch
41
- desc "Sticky tags will match only one record from an event stream. The same tag will be treated the same way"
42
- config_param :sticky_tags, :bool, default: true
43
+
44
+ config_section :match, param_name: :matches, multi: true do
45
+ desc "Label definition to match record. Example: app:nginx. You can specify more values as comma separated list: key1:value1,key2:value2"
46
+ config_param :labels, :hash, :default => {}
47
+ desc "List of namespace definition to filter the record. Ignored if left empty."
48
+ config_param :namespaces, :array, :default => [], value_type: :string
49
+ desc "List of hosts definition to filter the record. Ignored if left empty."
50
+ config_param :hosts, :array, :default => [], value_type: :string
51
+ desc "List of container names definition to filter the record. Ignored if left empty."
52
+ config_param :container_names, :array, :default => [], value_type: :string
53
+ desc "Negate the selection making it an exclude"
54
+ config_param :negate, :bool, :default => false
55
+ end
43
56
  end
44
57
 
45
58
  class Route
46
- def initialize(selector, namespace, tag, router)
59
+ def initialize(matches, tag, router)
47
60
  @router = router
48
- @selector = selector
49
- @namespace = namespace
61
+ @matches = matches
50
62
  @tag = tag
51
63
  end
52
64
 
53
- def match?(labels, namespace)
54
- # Match labels and namespace if defined
55
- return (match_labels(labels, @selector) and (@namespace == "" or namespace == @namespace))
65
+ # Evaluate selectors
66
+ # We evaluate <match> statements in order:
67
+ # 1. If match == true and negate == false -> return true
68
+ # 2. If match == true and negate == true -> return false
69
+ # 3. If match == false and negate == false -> continue
70
+ # 4. If match == false and negate == true -> continue
71
+ # There is no match at all -> return false
72
+ def match?(metadata)
73
+ @matches.each do |match|
74
+ if filter_select(match, metadata) and !match.negate
75
+ return true
76
+ end
77
+ if filter_select(match, metadata) and match.negate
78
+ return false
79
+ end
80
+ end
81
+ false
82
+ end
83
+
84
+ # Returns true if filter passes (filter match)
85
+ def filter_select(match, metadata)
86
+ # Break on container_name mismatch
87
+ unless match.hosts.empty? || match.hosts.include?(metadata[:host])
88
+ return false
89
+ end
90
+ # Break on host mismatch
91
+ unless match.container_names.empty? || match.container_names.include?(metadata[:container])
92
+ return false
93
+ end
94
+ # Break if list of namespaces is not empty and does not include actual namespace
95
+ unless match.namespaces.empty? || match.namespaces.include?(metadata[:namespace])
96
+ return false
97
+ end
98
+
99
+ match_labels(metadata[:labels], match.labels)
56
100
  end
57
101
 
58
102
  def emit(tag, time, record)
@@ -70,29 +114,38 @@ module Fluent
70
114
  @router.emit_stream(@tag, es)
71
115
  end
72
116
  end
117
+
73
118
  def match_labels(input, match)
74
- return (match.to_a - input.to_a).empty?
119
+ (match.to_a - input.to_a).empty?
75
120
  end
76
121
  end
77
122
 
78
123
  def process(tag, es)
79
124
  if @sticky_tags
80
- if @route_map.has_key?(tag)
81
- # We already matched with this tag send events to the routers
82
- @route_map[tag].each do |r|
83
- r.emit_es(tag, es.dup)
125
+ @mutex.synchronize {
126
+ if @route_map.has_key?(tag)
127
+ # We already matched with this tag send events to the routers
128
+ @route_map[tag].each do |r|
129
+ r.emit_es(tag, es.dup)
130
+ end
131
+ return
84
132
  end
85
- return
86
- end
133
+ }
87
134
  end
88
135
  event_stream = Hash.new {|h, k| h[k] = Fluent::MultiEventStream.new }
89
136
  es.each do |time, record|
90
- input_labels = @access_to_labels.call(record).to_h
91
- input_namespace = @access_to_namespace.call(record).to_s
137
+ input_metadata = { labels: @access_to_labels.call(record).to_h,
138
+ namespace: @access_to_namespace.call(record).to_s,
139
+ container: @access_to_container_name.call(record).to_s,
140
+ host: @access_to_host.call(record).to_s}
141
+ orphan_record = true
92
142
  @routers.each do |r|
93
- if r.match?(input_labels, input_namespace)
143
+ if r.match?(input_metadata)
144
+ orphan_record = false
94
145
  if @sticky_tags
95
- @route_map[tag].push(r)
146
+ @mutex.synchronize {
147
+ @route_map[tag].add(r)
148
+ }
96
149
  end
97
150
  if @batch
98
151
  event_stream[r].add(time, record)
@@ -101,25 +154,46 @@ module Fluent
101
154
  end
102
155
  end
103
156
  end
104
- if @batch
105
- event_stream.each do |r, es|
106
- r.emit_es(tag, es.dup)
157
+ if !@default_router.nil? && orphan_record
158
+ if @sticky_tags
159
+ @mutex.synchronize {
160
+ @route_map[tag].add(@default_router)
161
+ }
162
+ end
163
+ if @batch
164
+ event_stream[@default_router].add(time, record)
165
+ else
166
+ @default_router.emit(tag, time, record.dup)
107
167
  end
108
168
  end
109
169
  end
170
+ if @batch
171
+ event_stream.each do |r, es|
172
+ r.emit_es(tag, es.dup)
173
+ end
174
+ end
110
175
  end
111
176
 
112
177
  def configure(conf)
113
178
  super
114
- @route_map = Hash.new { |h, k| h[k] = Array.new }
179
+ @route_map = Hash.new { |h, k| h[k] = Set.new }
180
+ @mutex = Mutex.new
115
181
  @routers = []
182
+ @default_router = nil
116
183
  @routes.each do |rule|
117
184
  route_router = event_emitter_router(rule['@label'])
118
- @routers << Route.new(rule.labels, rule.namespace.to_s, rule.tag.to_s, route_router)
185
+ puts rule
186
+ @routers << Route.new(rule.matches, rule.tag.to_s, route_router)
187
+ end
188
+
189
+ if @default_route != '' or @default_tag != ''
190
+ @default_router = Route.new(nil, @default_tag, event_emitter_router(@default_route))
119
191
  end
120
192
 
121
193
  @access_to_labels = record_accessor_create("$.kubernetes.labels")
122
194
  @access_to_namespace = record_accessor_create("$.kubernetes.namespace_name")
195
+ @access_to_host = record_accessor_create("$.kubernetes.host")
196
+ @access_to_container_name = record_accessor_create("$.kubernetes.container_name")
123
197
 
124
198
  @batch = @emit_mode == :batch
125
199
  end
@@ -34,11 +34,88 @@ class LabelRouterOutputTest < Test::Unit::TestCase
34
34
  d.configure(conf)
35
35
  end
36
36
 
37
+ sub_test_case 'test_routing' do
38
+ test 'basic configuration' do
39
+ routing_conf = %(
40
+ <route>
41
+ <match>
42
+ labels app:app1
43
+ </match>
44
+ <match>
45
+ labels app2:app2
46
+ negate true
47
+ </match>
48
+ tag new_app_tag
49
+ </route>
50
+ <route>
51
+ <match>
52
+ labels app:app1
53
+ namespaces default,test
54
+ </match>
55
+ <match>
56
+ labels app:app2
57
+ namespaces system
58
+ negate true
59
+ </match>
60
+ tag new_app_tag
61
+ </route>
62
+ <route>
63
+ <match>
64
+ labels app:nginx
65
+ namespaces dev,sandbox
66
+ </match>
67
+ </route>
68
+ <route>
69
+ <match>
70
+ labels app:nginx
71
+ namespaces dev,sandbox
72
+ container_names mycontainer
73
+ </match>
74
+ </route>
75
+ )
76
+ d = Fluent::Test::Driver::BaseOwner.new(Fluent::Plugin::LabelRouterOutput)
77
+ d.configure(routing_conf)
78
+
79
+ r1 = Fluent::Plugin::LabelRouterOutput::Route.new(d.instance.routes[0].matches, d.instance.routes[0].tag,nil)
80
+ # Selector matched: GO
81
+ assert_equal(true, r1.match?(labels: { 'app' => 'app1' }, namespace: ''))
82
+ # Exclude match: NO GO
83
+ assert_equal(false, r1.match?(labels: { 'app' => 'app2' }, namespace: ''))
84
+ # Nothing matched: NO GO
85
+ assert_equal(false, r1.match?(labels: { 'app3' => 'app2' }, namespace: ''))
86
+
87
+ r2 = Fluent::Plugin::LabelRouterOutput::Route.new(d.instance.routes[1].matches, d.instance.routes[1].tag,nil)
88
+ # Match selector and namespace: GO
89
+ assert_equal(true, r2.match?(labels: { 'app' => 'app1' }, namespace: 'test'))
90
+ # Exclude via namespace
91
+ assert_equal(false, r2.match?(labels: { 'app' => 'app2' }, namespace: 'system'))
92
+ # Nothing matched: NO GO
93
+ assert_equal(false, r2.match?(labels: { 'app3' => 'app' }, namespace: 'system'))
94
+
95
+ r3 = Fluent::Plugin::LabelRouterOutput::Route.new(d.instance.routes[2].matches, d.instance.routes[2].tag,nil)
96
+ assert_equal(true, r3.match?(labels: { 'app' => 'nginx' }, namespace: 'dev'))
97
+ assert_equal(true, r3.match?(labels: { 'app' => 'nginx' }, namespace: 'sandbox'))
98
+ assert_equal(false, r3.match?(labels: { 'app' => 'nginx2' }, namespace: 'sandbox'))
99
+
100
+ r4 = Fluent::Plugin::LabelRouterOutput::Route.new(d.instance.routes[3].matches, d.instance.routes[3].tag,nil)
101
+ # Matching container name
102
+ assert_equal(true, r4.match?(labels: { 'app' => 'nginx' }, namespace: 'dev', container: 'mycontainer'))
103
+ # Missing container name is equal to wrong container
104
+ assert_equal(false, r4.match?(labels: { 'app' => 'nginx' }, namespace: 'sandbox'))
105
+ # Wrong container name
106
+ assert_equal(false, r4.match?(labels: { 'app' => 'nginx' }, namespace: 'dev', container: 'mycontainer2'))
107
+ # Wrong label but good namespace and container_name
108
+ assert_equal(false, r4.match?(labels: { 'app' => 'nginx2' }, namespace: 'sandbox', container_name: 'mycontainer2'))
109
+ end
110
+ end
111
+
37
112
  sub_test_case 'test_tag' do
38
113
  test 'normal' do
39
114
  CONFIG = %[
40
115
  <route>
41
- labels app:app1
116
+ <match>
117
+ labels app:app1
118
+ </match>
42
119
  tag new_app_tag
43
120
  </route>
44
121
  ]
@@ -56,4 +133,79 @@ class LabelRouterOutputTest < Test::Unit::TestCase
56
133
  assert_equal ["new_app_tag", event_time, {"kubernetes" => {"labels" => {"app" => "app1"} } }], events[0]
57
134
  end
58
135
  end
136
+
137
+
138
+ sub_test_case 'test_multiple_events_batched' do
139
+ test 'normal' do
140
+ conf = %[
141
+ <route>
142
+ <match>
143
+ </match>
144
+ </route>
145
+ ]
146
+ event_time = event_time("2019-07-17 11:11:11 UTC")
147
+ d = create_driver(conf)
148
+ d.run(expect_emits: 1, expect_records: 2) do
149
+ d.feed("test", [
150
+ [event_time, {"kubernetes" => {} } ],
151
+ [event_time, {"kubernetes" => {} } ],
152
+ ])
153
+ end
154
+ end
155
+ end
156
+
157
+ sub_test_case 'test_default_router' do
158
+ test 'normal' do
159
+ CONFIG2 = %[
160
+ <route>
161
+ <match>
162
+ labels app:app1
163
+ </match>
164
+ tag new_app_tag
165
+ </route>
166
+ default_route @default
167
+ default_tag "new_tag"
168
+ ]
169
+ event_time = event_time("2019-07-17 11:11:11 UTC")
170
+ d = create_driver(CONFIG2)
171
+ d.run() do
172
+ d.feed("test", [
173
+ [event_time, {"kubernetes" => {"labels" => {"app" => "app1"} } } ],
174
+ [event_time, {"kubernetes" => {"labels" => {"app" => "app2"} } } ],
175
+ ])
176
+ end
177
+ events = d.events
178
+
179
+ assert_equal(2, events.size)
180
+ assert_equal ["new_app_tag", event_time, {"kubernetes" => {"labels" => {"app" => "app1"} } }], events[0]
181
+ assert_equal ["new_tag", event_time, {"kubernetes" => {"labels" => {"app" => "app2"} } }], events[1]
182
+ end
183
+ end
184
+
185
+ sub_test_case 'test_empty_router' do
186
+ test 'normal' do
187
+ CONFIG3 = %[
188
+ <route>
189
+ tag new_app_tag
190
+ <match>
191
+ labels
192
+ namespaces
193
+ </match>
194
+ </route>
195
+ ]
196
+ event_time = event_time("2019-07-17 11:11:11 UTC")
197
+ d = create_driver(CONFIG3)
198
+ d.run(default_tag: 'test') do
199
+ d.feed(event_time, {"kubernetes" => {"labels" => {"app" => "app1"} } } )
200
+ end
201
+ d.run(default_tag: 'test2') do
202
+ d.feed(event_time, {"kubernetes" => {"labels" => {"app" => "app2"} } } )
203
+ end
204
+ events = d.events
205
+
206
+ assert_equal(2, events.size)
207
+ assert_equal ["new_app_tag", event_time, {"kubernetes" => {"labels" => {"app" => "app1"} } }], events[0]
208
+ assert_equal ["new_app_tag", event_time, {"kubernetes" => {"labels" => {"app" => "app2"} } }], events[1]
209
+ end
210
+ end
59
211
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-label-router
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Banzai Cloud
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-18 00:00:00.000000000 Z
11
+ date: 2020-07-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -107,8 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
107
  - !ruby/object:Gem::Version
108
108
  version: '0'
109
109
  requirements: []
110
- rubyforge_project:
111
- rubygems_version: 2.5.2.3
110
+ rubygems_version: 3.0.3
112
111
  signing_key:
113
112
  specification_version: 4
114
113
  summary: Routing records based on Kubernetes labels.