kube_cluster 0.4.1 → 0.4.2

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
2
  SHA256:
3
- metadata.gz: 4262ae7773b1196bab5f8d93d51c30e42c4e917a2fe0eed37c4c8c14d421712e
4
- data.tar.gz: 629f308c318d675cb85e18ece6b2e067d124ad3ac9964416ae40e16e2ea2ec43
3
+ metadata.gz: eac52e8ce0c9a435a78497095c22cdc598ccbde6d617247cd25ed00919261e46
4
+ data.tar.gz: 3e6147a0ab466a2cf0275ded885febb7ea9ec4bc092abdc19c39e2130c2dc517
5
5
  SHA512:
6
- metadata.gz: 61b86597f77ec21690a3395db26e7ec13d365f35a9d5d371677ed28e7170f1cfb094eebdbcc2e8bdf0ca23bbee13c8f7626d0ec7cd00a1b5ff0c62b526a648f1
7
- data.tar.gz: 694c11eae61670878923c8c8445a242f63334e652a4c8d8c6e4549a3a6dbff7dbe3993603b59d9b2e081dde16cd0498188e9d3233c484759c3828a49c13fb95f
6
+ metadata.gz: d84f7ef217ade440d364d368701e6b4e75aaf4da3628c60f65b2d71dabf33f6764d802a239628831e2107c2fa988c03f337d4615b173b5d66b125ea911c31a3e
7
+ data.tar.gz: 320623843a7d59d2e8716264211f9e9004bf7984e4b3eebf33bc03762297f8d64b46e4c20103085098a167199922cadb300ad6a852f3ae39ac6e4427b3a2df76
data/Gemfile.lock CHANGED
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- kube_cluster (0.4.1)
4
+ kube_cluster (0.4.2)
5
5
  kube_kubectl (~> 2.0.9)
6
- kube_schema (~> 1.4.0)
6
+ kube_schema (~> 1.4.1)
7
7
  scampi (~> 0.1)
8
8
 
9
9
  GEM
@@ -38,12 +38,12 @@ GEM
38
38
  rubyshell (~> 1.5)
39
39
  shellwords (~> 0.2.2)
40
40
  string_builder (~> 1.2.2)
41
- kube_schema (1.4.0)
41
+ kube_schema (1.4.1)
42
42
  json_schemer (~> 2.5.0)
43
43
  rubyshell (~> 1.5.0)
44
44
  language_server-protocol (3.17.0.5)
45
45
  lint_roller (1.1.0)
46
- parallel (2.0.1)
46
+ parallel (2.1.0)
47
47
  parser (3.3.11.1)
48
48
  ast (~> 2.4.1)
49
49
  racc
data/kube_cluster.gemspec CHANGED
@@ -33,6 +33,6 @@ Gem::Specification.new do |spec|
33
33
  spec.add_development_dependency "rake", "~> 13.0"
34
34
  spec.add_development_dependency "rubocop", "~> 1.21"
35
35
 
36
- spec.add_dependency "kube_schema", "~> 1.4.0"
36
+ spec.add_dependency "kube_schema", "~> 1.4.1"
37
37
  spec.add_dependency "kube_kubectl", "~> 2.0.9"
38
38
  end
@@ -17,16 +17,16 @@ module Kube
17
17
  # end
18
18
  #
19
19
  class Annotations < Middleware
20
- def initialize(**annotations)
21
- @annotations = annotations.transform_keys(&:to_sym).transform_values(&:to_s)
22
- end
23
-
24
20
  def call(manifest)
21
+ annotations = @opts.transform_keys(&:to_sym).transform_values(&:to_s)
22
+
25
23
  manifest.resources.map! do |resource|
26
- h = resource.to_h
27
- h[:metadata] ||= {}
28
- h[:metadata][:annotations] = @annotations.merge(h[:metadata][:annotations] || {})
29
- resource.rebuild(h)
24
+ filter(resource) do
25
+ h = resource.to_h
26
+ h[:metadata] ||= {}
27
+ h[:metadata][:annotations] = annotations.merge(h[:metadata][:annotations] || {})
28
+ resource.rebuild(h)
29
+ end
30
30
  end
31
31
  end
32
32
  end
@@ -25,63 +25,57 @@ module Kube
25
25
  class HPAForDeployment < Middleware
26
26
  LABEL = :"app.kubernetes.io/autoscale"
27
27
 
28
- def initialize(cpu: 75, memory: 80)
29
- @cpu = cpu
30
- @memory = memory
31
- end
32
-
33
28
  def call(manifest)
34
29
  generated = []
30
+ cpu_target = @opts.fetch(:cpu, 75)
31
+ memory_target = @opts.fetch(:memory, 80)
35
32
 
36
33
  manifest.resources.each do |resource|
37
- next unless resource.pod_bearing?
38
-
39
- value = resource.label(LABEL)
40
- next unless value
41
-
42
- min, max = parse_range(value)
43
-
44
- h = resource.to_h
45
- name = h.dig(:metadata, :name)
46
- namespace = h.dig(:metadata, :namespace)
47
- labels = h.dig(:metadata, :labels) || {}
48
- api_version = h[:apiVersion] || "apps/v1"
49
- resource_kind = resource.kind
50
-
51
- # Capture ivars as locals — the block runs via instance_exec
52
- # on a BlackHoleStruct, so @ivars would resolve on the BHS.
53
- cpu_target = @cpu
54
- memory_target = @memory
55
-
56
- generated << Kube::Cluster["HorizontalPodAutoscaler"].new {
57
- metadata.name = name
58
- metadata.namespace = namespace if namespace
59
- metadata.labels = labels.reject { |k, _| k == LABEL }
60
-
61
- spec.scaleTargetRef = {
62
- apiVersion: api_version,
63
- kind: resource_kind,
64
- name: name,
65
- }
66
- spec.minReplicas = min
67
- spec.maxReplicas = max
68
- spec.metrics = [
69
- {
70
- type: "Resource",
71
- resource: {
72
- name: "cpu",
73
- target: { type: "Utilization", averageUtilization: cpu_target },
34
+ filter(resource) do
35
+ next unless resource.pod_bearing?
36
+
37
+ value = resource.label(LABEL)
38
+ next unless value
39
+
40
+ min, max = parse_range(value)
41
+
42
+ h = resource.to_h
43
+ name = h.dig(:metadata, :name)
44
+ namespace = h.dig(:metadata, :namespace)
45
+ labels = h.dig(:metadata, :labels) || {}
46
+ api_version = h[:apiVersion] || "apps/v1"
47
+ resource_kind = resource.kind
48
+
49
+ generated << Kube::Cluster["HorizontalPodAutoscaler"].new {
50
+ metadata.name = name
51
+ metadata.namespace = namespace if namespace
52
+ metadata.labels = labels.reject { |k, _| k == LABEL }
53
+
54
+ spec.scaleTargetRef = {
55
+ apiVersion: api_version,
56
+ kind: resource_kind,
57
+ name: name,
58
+ }
59
+ spec.minReplicas = min
60
+ spec.maxReplicas = max
61
+ spec.metrics = [
62
+ {
63
+ type: "Resource",
64
+ resource: {
65
+ name: "cpu",
66
+ target: { type: "Utilization", averageUtilization: cpu_target },
67
+ },
74
68
  },
75
- },
76
- {
77
- type: "Resource",
78
- resource: {
79
- name: "memory",
80
- target: { type: "Utilization", averageUtilization: memory_target },
69
+ {
70
+ type: "Resource",
71
+ resource: {
72
+ name: "memory",
73
+ target: { type: "Utilization", averageUtilization: memory_target },
74
+ },
81
75
  },
82
- },
83
- ]
84
- }
76
+ ]
77
+ }
78
+ end
85
79
  end
86
80
 
87
81
  manifest.resources.concat(generated)
@@ -28,62 +28,56 @@ module Kube
28
28
  class IngressForService < Middleware
29
29
  LABEL = :"app.kubernetes.io/expose"
30
30
 
31
- def initialize(issuer: "letsencrypt-prod", ingress_class: "nginx")
32
- @issuer = issuer
33
- @ingress_class = ingress_class
34
- end
35
-
36
31
  def call(manifest)
37
32
  generated = []
33
+ issuer = @opts.fetch(:issuer, "letsencrypt-prod")
34
+ ingress_class = @opts.fetch(:ingress_class, "nginx")
38
35
 
39
36
  manifest.resources.each do |resource|
40
- next unless resource.kind == "Service"
41
-
42
- host = resource.label(LABEL)
43
- next unless host
44
-
45
- h = resource.to_h
46
- name = h.dig(:metadata, :name)
47
- namespace = h.dig(:metadata, :namespace)
48
- labels = h.dig(:metadata, :labels) || {}
49
-
50
- # Find the first port on the service
51
- port_name = Array(h.dig(:spec, :ports)).first&.dig(:name) || "http"
52
-
53
- # Use resource name as hostname fallback if label is just "true"
54
- host = "#{name}.local" if host == "true"
55
-
56
- # Capture ivars as locals — the block runs via instance_exec
57
- # on a BlackHoleStruct, so @ivars would resolve on the BHS.
58
- issuer = @issuer
59
- ingress_class = @ingress_class
60
-
61
- generated << Kube::Cluster["Ingress"].new {
62
- metadata.name = name
63
- metadata.namespace = namespace if namespace
64
- metadata.labels = labels.reject { |k, _| k == LABEL }
65
- metadata.annotations = {
66
- "cert-manager.io/cluster-issuer": issuer,
67
- "nginx.ingress.kubernetes.io/ssl-redirect": "true",
68
- }
69
-
70
- spec.ingressClassName = ingress_class
71
- spec.tls = [
72
- { hosts: [host], secretName: "#{name}-tls" },
73
- ]
74
- spec.rules = [
75
- {
76
- host: host,
77
- http: {
78
- paths: [{
79
- path: "/",
80
- pathType: "Prefix",
81
- backend: { service: { name: name, port: { name: port_name } } },
82
- }],
37
+ filter(resource) do
38
+ next unless resource.kind == "Service"
39
+
40
+ host = resource.label(LABEL)
41
+ next unless host
42
+
43
+ h = resource.to_h
44
+ name = h.dig(:metadata, :name)
45
+ namespace = h.dig(:metadata, :namespace)
46
+ labels = h.dig(:metadata, :labels) || {}
47
+
48
+ # Find the first port on the service
49
+ port_name = Array(h.dig(:spec, :ports)).first&.dig(:name) || "http"
50
+
51
+ # Use resource name as hostname fallback if label is just "true"
52
+ host = "#{name}.local" if host == "true"
53
+
54
+ generated << Kube::Cluster["Ingress"].new {
55
+ metadata.name = name
56
+ metadata.namespace = namespace if namespace
57
+ metadata.labels = labels.reject { |k, _| k == LABEL }
58
+ metadata.annotations = {
59
+ "cert-manager.io/cluster-issuer": issuer,
60
+ "nginx.ingress.kubernetes.io/ssl-redirect": "true",
61
+ }
62
+
63
+ spec.ingressClassName = ingress_class
64
+ spec.tls = [
65
+ { hosts: [host], secretName: "#{name}-tls" },
66
+ ]
67
+ spec.rules = [
68
+ {
69
+ host: host,
70
+ http: {
71
+ paths: [{
72
+ path: "/",
73
+ pathType: "Prefix",
74
+ backend: { service: { name: name, port: { name: port_name } } },
75
+ }],
76
+ },
83
77
  },
84
- },
85
- ]
86
- }
78
+ ]
79
+ }
80
+ end
87
81
  end
88
82
 
89
83
  manifest.resources.concat(generated)
@@ -35,16 +35,16 @@ module Kube
35
35
  managed_by: :"app.kubernetes.io/managed-by",
36
36
  }.freeze
37
37
 
38
- def initialize(**labels)
39
- @labels = normalize(labels)
40
- end
41
-
42
38
  def call(manifest)
39
+ labels = normalize(@opts)
40
+
43
41
  manifest.resources.map! do |resource|
44
- h = resource.to_h
45
- h[:metadata] ||= {}
46
- h[:metadata][:labels] = @labels.merge(h[:metadata][:labels] || {})
47
- resource.rebuild(h)
42
+ filter(resource) do
43
+ h = resource.to_h
44
+ h[:metadata] ||= {}
45
+ h[:metadata][:labels] = labels.merge(h[:metadata][:labels] || {})
46
+ resource.rebuild(h)
47
+ end
48
48
  end
49
49
  end
50
50
 
@@ -14,18 +14,20 @@ module Kube
14
14
  # end
15
15
  #
16
16
  class Namespace < Middleware
17
- def initialize(namespace)
17
+ def initialize(namespace, filter: DEFAULT_FILTER)
18
+ super(filter: filter)
18
19
  @namespace = namespace
19
20
  end
20
21
 
21
22
  def call(manifest)
22
23
  manifest.resources.map! do |resource|
23
- next resource if resource.cluster_scoped?
24
-
25
- h = resource.to_h
26
- h[:metadata] ||= {}
27
- h[:metadata][:namespace] = @namespace
28
- resource.rebuild(h)
24
+ filter(resource) do
25
+ next resource if resource.cluster_scoped?
26
+ h = resource.to_h
27
+ h[:metadata] ||= {}
28
+ h[:metadata][:namespace] = @namespace
29
+ resource.rebuild(h)
30
+ end
29
31
  end
30
32
  end
31
33
  end
@@ -89,18 +91,17 @@ test do
89
91
 
90
92
  it "returns_new_resource_instance" do
91
93
  resource = Kube::Cluster["ConfigMap"].new { metadata.name = "test" }
92
- m = manifest(resource)
93
-
94
- Middleware::Namespace.new("production").call(m)
95
-
96
- resource.equal?(m.resources.first).should.be.false
94
+ manifest(resource).tap do |m|
95
+ Middleware::Namespace.new("production").call(m)
96
+ resource.equal?(m.resources.first).should.be.false
97
+ end
97
98
  end
98
99
 
99
100
  private
100
101
 
101
102
  def manifest(*resources)
102
- m = Kube::Cluster::Manifest.new
103
- resources.each { |r| m << r }
104
- m
103
+ Kube::Cluster::Manifest.new.tap do |manifest|
104
+ resources.each { |x| manifest << x }
105
+ end
105
106
  end
106
107
  end
@@ -22,40 +22,40 @@ module Kube
22
22
  # end
23
23
  #
24
24
  class PodAntiAffinity < Middleware
25
- def initialize(topology_key: "kubernetes.io/hostname", weight: 1)
26
- @topology_key = topology_key
27
- @weight = weight
28
- end
29
-
30
25
  def call(manifest)
26
+ topology_key = @opts.fetch(:topology_key, "kubernetes.io/hostname")
27
+ weight = @opts.fetch(:weight, 1)
28
+
31
29
  manifest.resources.map! do |resource|
32
- next resource unless resource.pod_bearing?
33
-
34
- h = resource.to_h
35
- pod_spec = resource.pod_template(h)
36
- next resource unless pod_spec
37
-
38
- # Don't overwrite existing affinity configuration.
39
- next resource if pod_spec[:affinity]
40
-
41
- match_labels = h.dig(:spec, :selector, :matchLabels)
42
- next resource unless match_labels && !match_labels.empty?
43
-
44
- pod_spec[:affinity] = {
45
- podAntiAffinity: {
46
- preferredDuringSchedulingIgnoredDuringExecution: [
47
- {
48
- weight: @weight,
49
- podAffinityTerm: {
50
- labelSelector: { matchLabels: match_labels },
51
- topologyKey: @topology_key,
30
+ filter(resource) do
31
+ next resource unless resource.pod_bearing?
32
+
33
+ h = resource.to_h
34
+ pod_spec = resource.pod_template(h)
35
+ next resource unless pod_spec
36
+
37
+ # Don't overwrite existing affinity configuration.
38
+ next resource if pod_spec[:affinity]
39
+
40
+ match_labels = h.dig(:spec, :selector, :matchLabels)
41
+ next resource unless match_labels && !match_labels.empty?
42
+
43
+ pod_spec[:affinity] = {
44
+ podAntiAffinity: {
45
+ preferredDuringSchedulingIgnoredDuringExecution: [
46
+ {
47
+ weight: weight,
48
+ podAffinityTerm: {
49
+ labelSelector: { matchLabels: match_labels },
50
+ topologyKey: topology_key,
51
+ },
52
52
  },
53
- },
54
- ],
55
- },
56
- }
53
+ ],
54
+ },
55
+ }
57
56
 
58
- resource.rebuild(h)
57
+ resource.rebuild(h)
58
+ end
59
59
  end
60
60
  end
61
61
  end
@@ -41,24 +41,26 @@ module Kube
41
41
 
42
42
  def call(manifest)
43
43
  manifest.resources.map! do |resource|
44
- size = resource.label(LABEL)
45
- next resource unless size
46
- next resource unless resource.pod_bearing?
44
+ filter(resource) do
45
+ size = resource.label(LABEL)
46
+ next resource unless size
47
+ next resource unless resource.pod_bearing?
47
48
 
48
- preset = PRESETS.fetch(size.to_s) do
49
- raise ArgumentError, "Unknown size preset: #{size.inspect}. " \
50
- "Valid sizes: #{PRESETS.keys.join(', ')}"
51
- end
49
+ preset = PRESETS.fetch(size.to_s) do
50
+ raise ArgumentError, "Unknown size preset: #{size.inspect}. " \
51
+ "Valid sizes: #{PRESETS.keys.join(', ')}"
52
+ end
52
53
 
53
- h = resource.to_h
54
- pod_spec = resource.pod_template(h)
55
- next resource unless pod_spec
54
+ h = resource.to_h
55
+ pod_spec = resource.pod_template(h)
56
+ next resource unless pod_spec
56
57
 
57
- resource.each_container(pod_spec) do |container|
58
- container[:resources] = deep_merge(preset, container[:resources] || {})
59
- end
58
+ resource.each_container(pod_spec) do |container|
59
+ container[:resources] = deep_merge(preset, container[:resources] || {})
60
+ end
60
61
 
61
- resource.rebuild(h)
62
+ resource.rebuild(h)
63
+ end
62
64
  end
63
65
  end
64
66
  end
@@ -54,31 +54,31 @@ module Kube
54
54
  },
55
55
  }.freeze
56
56
 
57
- def initialize(default: :restricted)
58
- @default = default.to_s
59
- end
60
-
61
57
  def call(manifest)
58
+ default = @opts.fetch(:default, :restricted).to_s
59
+
62
60
  manifest.resources.map! do |resource|
63
- next resource unless resource.pod_bearing?
61
+ filter(resource) do
62
+ next resource unless resource.pod_bearing?
64
63
 
65
- profile_name = resource.label(LABEL) || @default
66
- profile = PROFILES.fetch(profile_name.to_s) do
67
- raise ArgumentError, "Unknown security profile: #{profile_name.inspect}. " \
68
- "Valid profiles: #{PROFILES.keys.join(', ')}"
69
- end
64
+ profile_name = resource.label(LABEL) || default
65
+ profile = PROFILES.fetch(profile_name.to_s) do
66
+ raise ArgumentError, "Unknown security profile: #{profile_name.inspect}. " \
67
+ "Valid profiles: #{PROFILES.keys.join(', ')}"
68
+ end
70
69
 
71
- h = resource.to_h
72
- pod_spec = resource.pod_template(h)
73
- next resource unless pod_spec
70
+ h = resource.to_h
71
+ pod_spec = resource.pod_template(h)
72
+ next resource unless pod_spec
74
73
 
75
- pod_spec[:securityContext] = deep_merge(profile[:pod], pod_spec[:securityContext] || {})
74
+ pod_spec[:securityContext] = deep_merge(profile[:pod], pod_spec[:securityContext] || {})
76
75
 
77
- resource.each_container(pod_spec) do |container|
78
- container[:securityContext] = deep_merge(profile[:container], container[:securityContext] || {})
79
- end
76
+ resource.each_container(pod_spec) do |container|
77
+ container[:securityContext] = deep_merge(profile[:container], container[:securityContext] || {})
78
+ end
80
79
 
81
- resource.rebuild(h)
80
+ resource.rebuild(h)
81
+ end
82
82
  end
83
83
  end
84
84
  end
@@ -25,30 +25,32 @@ module Kube
25
25
  generated = []
26
26
 
27
27
  manifest.resources.each do |resource|
28
- next unless resource.pod_bearing?
29
-
30
- h = resource.to_h
31
- ports = extract_ports(resource, h)
32
- next if ports.empty?
33
-
34
- match_labels = h.dig(:spec, :selector, :matchLabels)
35
- next unless match_labels && !match_labels.empty?
36
-
37
- generated << Kube::Cluster["Service"].new {
38
- metadata.name = h.dig(:metadata, :name)
39
- metadata.namespace = h.dig(:metadata, :namespace) if h.dig(:metadata, :namespace)
40
- metadata.labels = h.dig(:metadata, :labels) || {}
41
-
42
- spec.selector = match_labels
43
- spec.ports = ports.map { |p|
44
- {
45
- name: p[:name],
46
- port: p[:containerPort],
47
- targetPort: p[:name],
48
- protocol: p.fetch(:protocol, "TCP"),
28
+ filter(resource) do
29
+ next unless resource.pod_bearing?
30
+
31
+ h = resource.to_h
32
+ ports = extract_ports(resource, h)
33
+ next if ports.empty?
34
+
35
+ match_labels = h.dig(:spec, :selector, :matchLabels)
36
+ next unless match_labels && !match_labels.empty?
37
+
38
+ generated << Kube::Cluster["Service"].new {
39
+ metadata.name = h.dig(:metadata, :name)
40
+ metadata.namespace = h.dig(:metadata, :namespace) if h.dig(:metadata, :namespace)
41
+ metadata.labels = h.dig(:metadata, :labels) || {}
42
+
43
+ spec.selector = match_labels
44
+ spec.ports = ports.map { |p|
45
+ {
46
+ name: p[:name],
47
+ port: p[:containerPort],
48
+ targetPort: p[:name],
49
+ protocol: p.fetch(:protocol, "TCP"),
50
+ }
49
51
  }
50
52
  }
51
- }
53
+ end
52
54
  end
53
55
 
54
56
  manifest.resources.concat(generated)
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler/setup"
4
+ require "scampi"
5
+ require "kube/cluster"
6
+
3
7
  require_relative "middleware/stack"
4
8
  require_relative "middleware/namespace"
5
9
  require_relative "middleware/labels"
@@ -44,10 +48,59 @@ module Kube
44
48
  # end
45
49
  #
46
50
  class Middleware
47
- def initialize(**opts)
51
+ DEFAULT_FILTER = -> (x) {true}
52
+
53
+ def initialize(filter: DEFAULT_FILTER, **opts)
54
+ @filter = filter
48
55
  @opts = opts
49
56
  end
50
57
 
58
+ # Build an anonymous middleware class from a block.
59
+ # The block becomes the +#call+ method and runs via
60
+ # +instance_exec+, so +filter+, +deep_merge+, and
61
+ # +@opts+ are all available inside it.
62
+ #
63
+ # Middleware.build(filter: ->(r) { r.pod_bearing? }) do |manifest|
64
+ # manifest.resources.map! do |resource|
65
+ # filter(resource) do
66
+ # h = resource.to_h
67
+ # h[:metadata][:labels][:custom] = "yes"
68
+ # resource.rebuild(h)
69
+ # end
70
+ # end
71
+ # end
72
+ #
73
+ def self.build(**defaults, &block)
74
+ Class.new(self) do
75
+ define_method(:initialize) do |**overrides|
76
+ super(**defaults, **overrides)
77
+ end
78
+
79
+ define_method(:call, &block)
80
+ end
81
+ end
82
+
83
+ def filter(resource, &block)
84
+ # In case super() wasn't called by the middleware.
85
+ unless @filter.respond_to?(:call)
86
+ @filter = DEFAULT_FILTER
87
+ end
88
+
89
+ # Usage:
90
+ # def call(manifest)
91
+ # manifest.resources.map! do |resource|
92
+ # filter(resource) do
93
+ # # ... middleware code here
94
+ # end
95
+ # end
96
+ # end
97
+ if @filter.call(resource)
98
+ instance_exec(&block)
99
+ else
100
+ resource
101
+ end
102
+ end
103
+
51
104
  # Override in subclasses. Receives the full manifest,
52
105
  # mutates it in place.
53
106
  def call(manifest)
@@ -67,3 +120,297 @@ module Kube
67
120
  end
68
121
  end
69
122
  end
123
+
124
+ test do
125
+ Middleware = Kube::Cluster::Middleware
126
+
127
+ # --- filter helper ----------------------------------------------------------
128
+
129
+ it "filter_runs_block_when_filter_matches" do
130
+ mw = Middleware.new(filter: ->(r) { true })
131
+ resource = Kube::Cluster["ConfigMap"].new { metadata.name = "test" }
132
+
133
+ result = mw.filter(resource) { :ran }
134
+
135
+ result.should == :ran
136
+ end
137
+
138
+ it "filter_returns_resource_when_filter_rejects" do
139
+ mw = Middleware.new(filter: ->(r) { false })
140
+ resource = Kube::Cluster["ConfigMap"].new { metadata.name = "test" }
141
+
142
+ result = mw.filter(resource) { :should_not_run }
143
+
144
+ result.should == resource
145
+ end
146
+
147
+ it "filter_runs_block_by_default" do
148
+ mw = Middleware.new
149
+ resource = Kube::Cluster["ConfigMap"].new { metadata.name = "test" }
150
+
151
+ result = mw.filter(resource) { :ran }
152
+
153
+ result.should == :ran
154
+ end
155
+
156
+ it "filter_passes_resource_to_filter_proc" do
157
+ received = nil
158
+ mw = Middleware.new(filter: ->(r) { received = r; true })
159
+ resource = Kube::Cluster["ConfigMap"].new { metadata.name = "test" }
160
+
161
+ mw.filter(resource) { :ok }
162
+
163
+ received.should == resource
164
+ end
165
+
166
+ it "filter_can_match_on_kind" do
167
+ only_deployments = ->(r) { r.kind == "Deployment" }
168
+
169
+ mw = Middleware.new(filter: only_deployments)
170
+ deploy = Kube::Cluster["Deployment"].new { metadata.name = "web" }
171
+ config = Kube::Cluster["ConfigMap"].new { metadata.name = "cfg" }
172
+
173
+ mw.filter(deploy) { :ran }.should == :ran
174
+ mw.filter(config) { :ran }.should == config
175
+ end
176
+
177
+ it "filter_can_match_on_label" do
178
+ only_labeled = ->(r) { r.label("app.kubernetes.io/name") == "web" }
179
+
180
+ mw = Middleware.new(filter: only_labeled)
181
+ labeled = Kube::Cluster["ConfigMap"].new {
182
+ metadata.name = "cm"
183
+ metadata.labels = { "app.kubernetes.io/name": "web" }
184
+ }
185
+ unlabeled = Kube::Cluster["ConfigMap"].new { metadata.name = "other" }
186
+
187
+ mw.filter(labeled) { :ran }.should == :ran
188
+ mw.filter(unlabeled) { :ran }.should == unlabeled
189
+ end
190
+
191
+ it "filter_can_match_on_pod_bearing" do
192
+ only_pods = ->(r) { r.pod_bearing? }
193
+ mw = Middleware.new(filter: only_pods)
194
+
195
+ deploy = Kube::Cluster["Deployment"].new { metadata.name = "web" }
196
+ config = Kube::Cluster["ConfigMap"].new { metadata.name = "cfg" }
197
+
198
+ mw.filter(deploy) { :ran }.should == :ran
199
+ mw.filter(config) { :ran }.should == config
200
+ end
201
+
202
+ it "filter_block_has_access_to_middleware_instance" do
203
+ mw = Middleware.new
204
+ resource = Kube::Cluster["ConfigMap"].new { metadata.name = "test" }
205
+
206
+ # deep_merge is a private method on Middleware — the block should
207
+ # be able to call it via instance_exec.
208
+ result = mw.filter(resource) {
209
+ deep_merge({ a: 1 }, { b: 2 })
210
+ }
211
+
212
+ result.should == { a: 1, b: 2 }
213
+ end
214
+
215
+ it "filter_recovers_when_super_was_not_called" do
216
+ # Simulate a subclass that overrides initialize without calling super,
217
+ # leaving @filter as nil.
218
+ mw = Middleware.allocate
219
+ resource = Kube::Cluster["ConfigMap"].new { metadata.name = "test" }
220
+
221
+ result = mw.filter(resource) { :ran }
222
+
223
+ result.should == :ran
224
+ end
225
+
226
+ # --- filter in a transform middleware (map! pattern) ------------------------
227
+
228
+ it "filter_with_transform_middleware_skips_non_matching" do
229
+ m = manifest(
230
+ Kube::Cluster["Deployment"].new { metadata.name = "web" },
231
+ Kube::Cluster["ConfigMap"].new { metadata.name = "cfg" },
232
+ )
233
+
234
+ # A middleware that adds a label, but only to Deployments.
235
+ mw = Middleware.new(filter: ->(r) { r.kind == "Deployment" })
236
+
237
+ m.resources.map! do |resource|
238
+ mw.filter(resource) do
239
+ h = resource.to_h
240
+ h[:metadata] ||= {}
241
+ h[:metadata][:labels] = (h[:metadata][:labels] || {}).merge(tagged: "yes")
242
+ resource.rebuild(h)
243
+ end
244
+ end
245
+
246
+ m.resources[0].to_h.dig(:metadata, :labels, :tagged).should == "yes"
247
+ m.resources[1].to_h.dig(:metadata, :labels, :tagged).should.be.nil
248
+ end
249
+
250
+ it "filter_with_transform_middleware_applies_to_all_by_default" do
251
+ m = manifest(
252
+ Kube::Cluster["Deployment"].new { metadata.name = "web" },
253
+ Kube::Cluster["ConfigMap"].new { metadata.name = "cfg" },
254
+ )
255
+
256
+ mw = Middleware.new
257
+
258
+ m.resources.map! do |resource|
259
+ mw.filter(resource) do
260
+ h = resource.to_h
261
+ h[:metadata] ||= {}
262
+ h[:metadata][:labels] = (h[:metadata][:labels] || {}).merge(tagged: "yes")
263
+ resource.rebuild(h)
264
+ end
265
+ end
266
+
267
+ m.resources[0].to_h.dig(:metadata, :labels, :tagged).should == "yes"
268
+ m.resources[1].to_h.dig(:metadata, :labels, :tagged).should == "yes"
269
+ end
270
+
271
+ # --- Middleware.build -------------------------------------------------------
272
+
273
+ it "build_creates_middleware_from_block" do
274
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
275
+
276
+ klass = Middleware.build do |manifest|
277
+ manifest.resources.map! do |resource|
278
+ h = resource.to_h
279
+ h[:metadata][:labels] = { injected: "yes" }
280
+ resource.rebuild(h)
281
+ end
282
+ end
283
+ klass.new.call(m)
284
+
285
+ m.resources.first.to_h.dig(:metadata, :labels, :injected).should == "yes"
286
+ end
287
+
288
+ it "build_bakes_in_filter" do
289
+ m = manifest(
290
+ Kube::Cluster["Deployment"].new { metadata.name = "web" },
291
+ Kube::Cluster["ConfigMap"].new { metadata.name = "cfg" },
292
+ )
293
+
294
+ klass = Middleware.build(filter: ->(r) { r.kind == "Deployment" }) do |manifest|
295
+ manifest.resources.map! do |resource|
296
+ filter(resource) do
297
+ h = resource.to_h
298
+ h[:metadata][:labels] = (h[:metadata][:labels] || {}).merge(tagged: "yes")
299
+ resource.rebuild(h)
300
+ end
301
+ end
302
+ end
303
+ klass.new.call(m)
304
+
305
+ m.resources[0].to_h.dig(:metadata, :labels, :tagged).should == "yes"
306
+ m.resources[1].to_h.dig(:metadata, :labels, :tagged).should.be.nil
307
+ end
308
+
309
+ it "build_has_access_to_deep_merge" do
310
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
311
+
312
+ klass = Middleware.build do |manifest|
313
+ manifest.resources.map! do |resource|
314
+ h = resource.to_h
315
+ h[:metadata][:labels] = deep_merge({ a: 1 }, { b: 2 })
316
+ resource.rebuild(h)
317
+ end
318
+ end
319
+ klass.new.call(m)
320
+
321
+ m.resources.first.to_h.dig(:metadata, :labels).should == { a: 1, b: 2 }
322
+ end
323
+
324
+ it "build_bakes_in_opts" do
325
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
326
+
327
+ klass = Middleware.build(team: "platform") do |manifest|
328
+ manifest.resources.map! do |resource|
329
+ h = resource.to_h
330
+ h[:metadata][:labels] = { team: @opts[:team] }
331
+ resource.rebuild(h)
332
+ end
333
+ end
334
+ klass.new.call(m)
335
+
336
+ m.resources.first.to_h.dig(:metadata, :labels, :team).should == "platform"
337
+ end
338
+
339
+ it "build_works_in_stack" do
340
+ m = manifest(
341
+ Kube::Cluster["Deployment"].new { metadata.name = "web" },
342
+ Kube::Cluster["ConfigMap"].new { metadata.name = "cfg" },
343
+ )
344
+
345
+ stack = Middleware::Stack.new do
346
+ use Middleware.build(filter: ->(r) { r.kind == "Deployment" }) { |manifest|
347
+ manifest.resources.map! do |resource|
348
+ filter(resource) do
349
+ h = resource.to_h
350
+ h[:metadata][:labels] = (h[:metadata][:labels] || {}).merge(custom: "from-build")
351
+ resource.rebuild(h)
352
+ end
353
+ end
354
+ }
355
+ end
356
+ stack.call(m)
357
+
358
+ m.resources[0].to_h.dig(:metadata, :labels, :custom).should == "from-build"
359
+ m.resources[1].to_h.dig(:metadata, :labels, :custom).should.be.nil
360
+ end
361
+
362
+ it "build_returns_a_class" do
363
+ klass = Middleware.build { |manifest| }
364
+
365
+ klass.should.be.kind_of(Class)
366
+ (klass < Middleware).should == true
367
+ end
368
+
369
+ it "build_noop_base_class_without_block" do
370
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
371
+ original = m.resources.first.to_h.dup
372
+
373
+ Middleware.new.call(m)
374
+
375
+ m.resources.first.to_h.should == original
376
+ end
377
+
378
+ # --- filter in a generative middleware (each pattern) -----------------------
379
+
380
+ it "filter_with_generative_middleware_skips_non_matching" do
381
+ m = manifest(
382
+ Kube::Cluster["Deployment"].new {
383
+ metadata.name = "web"
384
+ metadata.labels = { "app.kubernetes.io/name": "web" }
385
+ },
386
+ Kube::Cluster["Deployment"].new {
387
+ metadata.name = "worker"
388
+ metadata.labels = { "app.kubernetes.io/name": "worker" }
389
+ },
390
+ )
391
+
392
+ only_web = ->(r) { r.label("app.kubernetes.io/name") == "web" }
393
+ mw = Middleware.new(filter: only_web)
394
+
395
+ generated = []
396
+ m.resources.each do |resource|
397
+ mw.filter(resource) do
398
+ generated << Kube::Cluster["ConfigMap"].new {
399
+ metadata.name = "generated-for-#{resource.to_h.dig(:metadata, :name)}"
400
+ }
401
+ end
402
+ end
403
+ m.resources.concat(generated)
404
+
405
+ m.resources.size.should == 3
406
+ m.resources.last.to_h.dig(:metadata, :name).should == "generated-for-web"
407
+ end
408
+
409
+ private
410
+
411
+ def manifest(*resources)
412
+ m = Kube::Cluster::Manifest.new
413
+ resources.each { |r| m << r }
414
+ m
415
+ end
416
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kube
4
4
  module Cluster
5
- VERSION = "0.4.1"
5
+ VERSION = "0.4.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kube_cluster
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan K
@@ -57,14 +57,14 @@ dependencies:
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: 1.4.0
60
+ version: 1.4.1
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: 1.4.0
67
+ version: 1.4.1
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: kube_kubectl
70
70
  requirement: !ruby/object:Gem::Requirement