fluent-plugin-label-router 0.1.3 → 0.2.4

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 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.