kube_cluster 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +43 -0
  3. data/.github/workflows/tag-gem-version-bump.yml +47 -0
  4. data/.gitignore +2 -0
  5. data/Gemfile.lock +48 -52
  6. data/bin/console +3 -0
  7. data/bin/dev +4 -0
  8. data/docker-compose.yml +26 -0
  9. data/examples/01-basic-redis-pod/manifest.rb +60 -0
  10. data/examples/02-manifest-with-middleware/manifest.rb +37 -0
  11. data/examples/02-manifest-with-middleware/middleware/labels.rb +4 -0
  12. data/examples/02-manifest-with-middleware/middleware/namespace.rb +4 -0
  13. data/examples/02-manifest-with-middleware/templates/config_map.rb +13 -0
  14. data/examples/02-manifest-with-middleware/templates/deployment.rb +59 -0
  15. data/examples/02-manifest-with-middleware/templates/horizontal_pod_autoscaler.rb +30 -0
  16. data/examples/02-manifest-with-middleware/templates/ingress.rb +38 -0
  17. data/examples/02-manifest-with-middleware/templates/service.rb +12 -0
  18. data/examples/03-app-with-database/demo.rb +87 -0
  19. data/examples/03-app-with-database/helpers.rb +18 -0
  20. data/examples/03-app-with-database/my_app.rb +45 -0
  21. data/examples/03-app-with-database/postgresql.rb +81 -0
  22. data/examples/03-app-with-database/ruby_on_rails.rb +31 -0
  23. data/flake.lock +3 -3
  24. data/flake.nix +6 -0
  25. data/kube_cluster.gemspec +3 -1
  26. data/lib/kube/cli/cluster.rb +41 -0
  27. data/lib/kube/cluster/connection.rb +18 -0
  28. data/lib/kube/cluster/instance.rb +21 -0
  29. data/lib/kube/cluster/manifest.rb +25 -0
  30. data/lib/kube/cluster/middleware/annotations.rb +32 -0
  31. data/lib/kube/cluster/middleware/hpa_for_deployment.rb +111 -0
  32. data/lib/kube/cluster/middleware/ingress_for_service.rb +91 -0
  33. data/lib/kube/cluster/middleware/labels.rb +59 -0
  34. data/lib/kube/cluster/middleware/namespace.rb +31 -0
  35. data/lib/kube/cluster/middleware/pod_anti_affinity.rb +61 -0
  36. data/lib/kube/cluster/middleware/resource_preset.rb +64 -0
  37. data/lib/kube/cluster/middleware/security_context.rb +84 -0
  38. data/lib/kube/cluster/middleware/service_for_deployment.rb +71 -0
  39. data/lib/kube/cluster/middleware/stack.rb +43 -0
  40. data/lib/kube/cluster/middleware.rb +69 -0
  41. data/lib/kube/cluster/resource/dirty_tracking.rb +113 -0
  42. data/lib/kube/cluster/resource/persistence.rb +67 -0
  43. data/lib/kube/cluster/resource.rb +99 -0
  44. data/lib/kube/cluster/version.rb +1 -1
  45. data/lib/kube/cluster.rb +34 -7
  46. data/lib/kube/errors.rb +57 -0
  47. metadata +69 -17
  48. data/Rakefile +0 -11
  49. data/TREE_PLAN.md +0 -513
  50. data/bin/generate-command-schema-v1 +0 -44
  51. data/data/kubectl-command-tree-v1-minimal.json +0 -125
  52. data/data/kubectl-command-tree-v1.json +0 -1469
  53. data/examples/quick-repl/docker-compose.yml +0 -52
  54. data/exe/kube_cluster +0 -6
  55. data/lib/kube/cluster/command_node.rb +0 -89
  56. data/lib/kube/cluster/ctl.rb +0 -33
  57. data/lib/kube/cluster/query_builder.rb +0 -35
  58. data/lib/kube/cluster/resource_selector.rb +0 -19
  59. data/lib/kube/cluster/tree_node.rb +0 -51
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helpers"
4
+
5
+ class MyApp < Kube::Cluster::Manifest
6
+ include Helpers
7
+
8
+ Middleware = Kube::Cluster::Manifest::Middleware
9
+
10
+ stack do
11
+ # Generative — produce new resources from existing ones
12
+ use Middleware::ServiceForDeployment
13
+ use Middleware::IngressForService
14
+ use Middleware::HPAForDeployment
15
+
16
+ # Transforms — apply to everything, including generated resources
17
+ use Middleware::Labels, managed_by: "kube_cluster"
18
+ use Middleware::ResourcePreset
19
+ use Middleware::SecurityContext
20
+ use Middleware::PodAntiAffinity
21
+ end
22
+
23
+ attr_reader :domain, :db_domain, :rails_domain, :size
24
+
25
+ def initialize(domain, size: :small, &block)
26
+ super()
27
+ @domain = domain
28
+ @db_domain = "db.#{domain}"
29
+ @rails_domain = "app.#{domain}"
30
+ @size = size
31
+ block.call(self) if block
32
+ end
33
+
34
+ # Labels that encode the manifest-level abstractions.
35
+ # Middleware reads these to apply sizing, security, etc.
36
+ def app_labels(name:, instance:, component: nil)
37
+ labels = {
38
+ "app.kubernetes.io/name": name,
39
+ "app.kubernetes.io/instance": instance,
40
+ "app.kubernetes.io/size": @size.to_s,
41
+ }
42
+ labels[:"app.kubernetes.io/component"] = component if component
43
+ labels
44
+ end
45
+ end
@@ -0,0 +1,81 @@
1
+ class Postgresql < Kube::Cluster::Manifest
2
+
3
+ def initialize(namespace: nil)
4
+ super
5
+ @namespace = namespace
6
+
7
+ self << Namespace.new
8
+ self << StatefulSet.new
9
+ self << Service.new
10
+ self << Secret.new
11
+ end
12
+
13
+ class StatefulSet < Kube::Cluster["StatefulSet"]
14
+ metadata.name = db_name
15
+ metadata.namespace = db_ns
16
+ metadata.labels = db_labels
17
+ spec.serviceName = "#{db_name}-headless"
18
+ spec.replicas = 1
19
+ spec.selector.matchLabels = db_match
20
+ spec.template.metadata.labels = db_labels
21
+ spec.template.spec.containers = [
22
+ {
23
+ name: "postgres",
24
+ image: "docker.io/postgres:16.4-alpine",
25
+ ports: [{ name: "tcp-postgresql", containerPort: 5432 }],
26
+ env: [
27
+ { name: "POSTGRES_PASSWORD", valueFrom: { secretKeyRef: { name: db_name, key: "postgres-password" } } },
28
+ { name: "PGDATA", value: "/var/lib/postgresql/data/pgdata" },
29
+ ],
30
+ volumeMounts: [{ name: "data", mountPath: "/var/lib/postgresql/data" }],
31
+ livenessProbe: {
32
+ exec: { command: ["pg_isready", "-U", "postgres"] },
33
+ initialDelaySeconds: 30,
34
+ periodSeconds: 10,
35
+ timeoutSeconds: 5,
36
+ failureThreshold: 6,
37
+ },
38
+ readinessProbe: {
39
+ exec: { command: ["pg_isready", "-U", "postgres"] },
40
+ initialDelaySeconds: 5,
41
+ periodSeconds: 10,
42
+ timeoutSeconds: 5,
43
+ failureThreshold: 6,
44
+ },
45
+ },
46
+ ]
47
+ spec.volumeClaimTemplates = [
48
+ {
49
+ metadata: { name: "data" },
50
+ spec: {
51
+ accessModes: ["ReadWriteOnce"],
52
+ resources: { requests: { storage: "10Gi" } },
53
+ },
54
+ },
55
+ ]
56
+ end
57
+
58
+ class Namespace < Kube::Cluster["Namespace"]
59
+ metadata.name = db_ns
60
+ metadata.labels = db_labels.reject { |k, _| k == :"app.kubernetes.io/component" }
61
+ end
62
+
63
+ class Secret < Kube::Cluster["Secret"]
64
+ metadata.name = db_name
65
+ metadata.namespace = db_ns
66
+ metadata.labels = db_labels
67
+ self.type = "Opaque"
68
+ self.data = { "postgres-password": m.base64(pg_password) }
69
+ end
70
+
71
+ # Headless service for StatefulSet DNS — explicit because the
72
+ # middleware-generated Service is a regular ClusterIP service.
73
+ class Service < Kube::Cluster["Service"]
74
+ metadata.name = "#{db_name}-headless"
75
+ metadata.namespace = db_ns
76
+ metadata.labels = db_labels
77
+ spec.clusterIP = "None"
78
+ spec.selector = db_match
79
+ spec.ports = [{ name: "tcp-postgresql", port: 5432, targetPort: "tcp-postgresql" }]
80
+ end
81
+ end
@@ -0,0 +1,31 @@
1
+ class RubyOnRails < Kube::Cluster["Deployment"]
2
+ default do
3
+ metadata.name = name
4
+ metadata.namespace = ns
5
+ metadata.labels = labels.merge(
6
+ "app.kubernetes.io/expose": "app.example.com",
7
+ "app.kubernetes.io/autoscale": "1-5",
8
+ )
9
+ spec.replicas = 1
10
+ spec.selector.matchLabels = m.match_labels(name: name, instance: name)
11
+ spec.template.metadata.labels = labels
12
+ spec.template.spec.containers = [
13
+ {
14
+ name: name,
15
+ image: "ghcr.io/acme/rails-app:2.0",
16
+ ports: [{ name: "http", containerPort: 3000, protocol: "TCP" }],
17
+ envFrom: [{ configMapRef: { name: "#{name}-config" } }],
18
+ livenessProbe: {
19
+ httpGet: { path: "/healthz", port: "http" },
20
+ initialDelaySeconds: 15,
21
+ periodSeconds: 10,
22
+ },
23
+ readinessProbe: {
24
+ httpGet: { path: "/readyz", port: "http" },
25
+ initialDelaySeconds: 5,
26
+ periodSeconds: 5,
27
+ },
28
+ },
29
+ ]
30
+ end
31
+ end
data/flake.lock CHANGED
@@ -2,11 +2,11 @@
2
2
  "nodes": {
3
3
  "nixpkgs": {
4
4
  "locked": {
5
- "lastModified": 1773821835,
6
- "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=",
5
+ "lastModified": 1776169885,
6
+ "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
7
7
  "owner": "NixOS",
8
8
  "repo": "nixpkgs",
9
- "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0",
9
+ "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
10
10
  "type": "github"
11
11
  },
12
12
  "original": {
data/flake.nix CHANGED
@@ -10,6 +10,10 @@
10
10
  let
11
11
  pkgs = nixpkgs.legacyPackages.${system};
12
12
  ruby = pkgs.ruby_3_4; # Specify version
13
+ kubectlWithKubeconfig = pkgs.writeShellScriptBin "kubectl" ''
14
+ #!${pkgs.bash}/bin/bash
15
+ KUBECONFIG="$PWD/kubeconfig.yaml" ${pkgs.kubectl}/bin/kubectl "$@"
16
+ '';
13
17
  in
14
18
  {
15
19
  devShells.default = pkgs.mkShell {
@@ -21,6 +25,7 @@
21
25
  ruby
22
26
  pkgs.libyaml # psych gem
23
27
  pkgs.openssl # openssl gem
28
+ kubectlWithKubeconfig
24
29
  ];
25
30
 
26
31
  shellHook = ''
@@ -29,6 +34,7 @@
29
34
  export PATH="$GEM_HOME/bin:$PATH"
30
35
  export BUNDLE_PATH="$GEM_HOME"
31
36
  export BUNDLE_BIN="$GEM_HOME/bin"
37
+ export KUBECONFIG="$PWD/kubeconfig.yaml"
32
38
  '';
33
39
  };
34
40
  }
data/kube_cluster.gemspec CHANGED
@@ -32,5 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency "rake", "~> 13.0"
33
33
  spec.add_development_dependency "rubocop", "~> 1.21"
34
34
 
35
- spec.add_dependency "kube_schema", "~> 1.0"
35
+ spec.add_dependency "kube_schema", "~> 1.2.0"
36
+ spec.add_dependency "kube_kit", "> 0"
37
+ spec.add_dependency "kube_kubectl", "~> 2.0.0"
36
38
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kube/cli"
4
+ require "kube/cluster"
5
+
6
+ Kube::CLI.register "cluster", ->(argv) {
7
+ subcommand = argv.shift
8
+
9
+ case subcommand
10
+ when "connect", nil
11
+ kubeconfig = ENV["KUBECONFIG"]
12
+
13
+ # Parse --kubeconfig flag
14
+ if (idx = argv.index("--kubeconfig"))
15
+ kubeconfig = argv[idx + 1]
16
+ argv.slice!(idx, 2)
17
+ elsif (flag = argv.find { |a| a.start_with?("--kubeconfig=") })
18
+ kubeconfig = flag.split("=", 2).last
19
+ argv.delete(flag)
20
+ end
21
+
22
+ if kubeconfig.nil?
23
+ $stderr.puts "kube cluster connect: missing --kubeconfig or KUBECONFIG env var"
24
+ exit 1
25
+ end
26
+
27
+ instance = Kube::Cluster.connect(kubeconfig: kubeconfig)
28
+ puts instance.inspect
29
+
30
+ when "help", "--help", "-h"
31
+ puts "Usage: kube cluster <subcommand> [options]"
32
+ puts
33
+ puts "Subcommands:"
34
+ puts " connect Connect to a cluster (--kubeconfig=PATH or KUBECONFIG env)"
35
+ puts
36
+
37
+ else
38
+ $stderr.puts "kube cluster: unknown subcommand '#{subcommand}'"
39
+ exit 1
40
+ end
41
+ }, description: "Manage cluster connections"
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kube
4
+ module Cluster
5
+ class Connection
6
+ attr_reader :kubeconfig, :ctl
7
+
8
+ def initialize(kubeconfig:)
9
+ @kubeconfig = kubeconfig
10
+ @ctl = Kube::Ctl::Instance.new(kubeconfig: kubeconfig)
11
+ end
12
+
13
+ def inspect
14
+ "#<#{self.class.name} kubeconfig=#{@kubeconfig.inspect}>"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kube
4
+ module Cluster
5
+ class Instance
6
+ attr_reader :kubeconfig
7
+
8
+ def initialize(kubeconfig:)
9
+ @kubeconfig = kubeconfig
10
+ end
11
+
12
+ def connection
13
+ @connection ||= Connection.new(kubeconfig: @kubeconfig)
14
+ end
15
+
16
+ def inspect
17
+ "#<#{self.class.name} kubeconfig=#{@kubeconfig.inspect}>"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kube
4
+ module Cluster
5
+ # A flat, ordered collection of Kubernetes resources.
6
+ #
7
+ # Manifest is a pure resource collection. Middleware is applied
8
+ # separately via Kube::Cluster::Middleware::Stack.
9
+ #
10
+ # manifest = Kube::Cluster::Manifest.new
11
+ # manifest << Kube::Cluster["Deployment"].new { ... }
12
+ #
13
+ # stack = Kube::Cluster::Middleware::Stack.new do
14
+ # use Middleware::Namespace, "production"
15
+ # use Middleware::Labels, app: "web-app"
16
+ # end
17
+ #
18
+ # stack.call(manifest)
19
+ # manifest.to_yaml
20
+ #
21
+ class Manifest < Kube::Schema::Manifest
22
+ attr_reader :resources
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kube
4
+ module Cluster
5
+ class Middleware
6
+ # Merges annotations into +metadata.annotations+ on every resource.
7
+ # Existing annotations are preserved; the supplied annotations act
8
+ # as defaults that can be overridden per-resource.
9
+ #
10
+ # stack do
11
+ # use Middleware::Annotations,
12
+ # "prometheus.io/scrape": "true",
13
+ # "prometheus.io/port": "9090"
14
+ # end
15
+ #
16
+ class Annotations < Middleware
17
+ def initialize(**annotations)
18
+ @annotations = annotations.transform_keys(&:to_sym).transform_values(&:to_s)
19
+ end
20
+
21
+ def call(manifest)
22
+ manifest.resources.map! do |resource|
23
+ h = resource.to_h
24
+ h[:metadata] ||= {}
25
+ h[:metadata][:annotations] = @annotations.merge(h[:metadata][:annotations] || {})
26
+ resource.rebuild(h)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kube
4
+ module Cluster
5
+ class Middleware
6
+ # Generates a HorizontalPodAutoscaler for every pod-bearing
7
+ # resource that carries the +app.kubernetes.io/autoscale+ label.
8
+ #
9
+ # The label value encodes the min and max replicas as "min-max":
10
+ #
11
+ # metadata.labels = { "app.kubernetes.io/autoscale": "1-5" }
12
+ #
13
+ # Options:
14
+ # cpu: — target CPU utilization percentage (default: 75)
15
+ # memory: — target memory utilization percentage (default: 80)
16
+ #
17
+ # stack do
18
+ # use Middleware::HPAForDeployment
19
+ # use Middleware::HPAForDeployment, cpu: 60, memory: 70
20
+ # end
21
+ #
22
+ class HPAForDeployment < Middleware
23
+ LABEL = :"app.kubernetes.io/autoscale"
24
+
25
+ def initialize(cpu: 75, memory: 80)
26
+ @cpu = cpu
27
+ @memory = memory
28
+ end
29
+
30
+ def call(manifest)
31
+ generated = []
32
+
33
+ manifest.resources.each do |resource|
34
+ next unless resource.pod_bearing?
35
+
36
+ value = resource.label(LABEL)
37
+ next unless value
38
+
39
+ min, max = parse_range(value)
40
+
41
+ h = resource.to_h
42
+ name = h.dig(:metadata, :name)
43
+ namespace = h.dig(:metadata, :namespace)
44
+ labels = h.dig(:metadata, :labels) || {}
45
+ api_version = h[:apiVersion] || "apps/v1"
46
+ resource_kind = resource.kind
47
+
48
+ # Capture ivars as locals — the block runs via instance_exec
49
+ # on a BlackHoleStruct, so @ivars would resolve on the BHS.
50
+ cpu_target = @cpu
51
+ memory_target = @memory
52
+
53
+ generated << Kube::Cluster["HorizontalPodAutoscaler"].new {
54
+ metadata.name = name
55
+ metadata.namespace = namespace if namespace
56
+ metadata.labels = labels.reject { |k, _| k == LABEL }
57
+
58
+ spec.scaleTargetRef = {
59
+ apiVersion: api_version,
60
+ kind: resource_kind,
61
+ name: name,
62
+ }
63
+ spec.minReplicas = min
64
+ spec.maxReplicas = max
65
+ spec.metrics = [
66
+ {
67
+ type: "Resource",
68
+ resource: {
69
+ name: "cpu",
70
+ target: { type: "Utilization", averageUtilization: cpu_target },
71
+ },
72
+ },
73
+ {
74
+ type: "Resource",
75
+ resource: {
76
+ name: "memory",
77
+ target: { type: "Utilization", averageUtilization: memory_target },
78
+ },
79
+ },
80
+ ]
81
+ }
82
+ end
83
+
84
+ manifest.resources.concat(generated)
85
+ end
86
+
87
+ private
88
+
89
+ def parse_range(value)
90
+ parts = value.to_s.split("-", 2)
91
+
92
+ unless parts.length == 2
93
+ raise ArgumentError,
94
+ "Invalid autoscale label: #{value.inspect}. Expected format: \"min-max\" (e.g. \"1-5\")"
95
+ end
96
+
97
+ min = Integer(parts[0])
98
+ max = Integer(parts[1])
99
+
100
+ unless min > 0 && max >= min
101
+ raise ArgumentError,
102
+ "Invalid autoscale range: min=#{min}, max=#{max}. " \
103
+ "min must be > 0 and max must be >= min."
104
+ end
105
+
106
+ [min, max]
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kube
4
+ module Cluster
5
+ class Middleware
6
+ # Generates an Ingress for every Service whose source resource
7
+ # carries the +app.kubernetes.io/expose+ label.
8
+ #
9
+ # The label value is the hostname:
10
+ #
11
+ # metadata.labels = { "app.kubernetes.io/expose": "app.example.com" }
12
+ #
13
+ # Set to +"true"+ to use the resource name as a hostname placeholder
14
+ # (useful when a later middleware or the manifest class resolves it).
15
+ #
16
+ # Options:
17
+ # issuer: — cert-manager ClusterIssuer name (default: "letsencrypt-prod")
18
+ # ingress_class: — IngressClassName (default: "nginx")
19
+ #
20
+ # stack do
21
+ # use Middleware::IngressForService
22
+ # use Middleware::IngressForService, issuer: "letsencrypt-staging"
23
+ # end
24
+ #
25
+ class IngressForService < Middleware
26
+ LABEL = :"app.kubernetes.io/expose"
27
+
28
+ def initialize(issuer: "letsencrypt-prod", ingress_class: "nginx")
29
+ @issuer = issuer
30
+ @ingress_class = ingress_class
31
+ end
32
+
33
+ def call(manifest)
34
+ generated = []
35
+
36
+ manifest.resources.each do |resource|
37
+ next unless resource.kind == "Service"
38
+
39
+ host = resource.label(LABEL)
40
+ next unless host
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
+
47
+ # Find the first port on the service
48
+ port_name = Array(h.dig(:spec, :ports)).first&.dig(:name) || "http"
49
+
50
+ # Use resource name as hostname fallback if label is just "true"
51
+ host = "#{name}.local" if host == "true"
52
+
53
+ # Capture ivars as locals — the block runs via instance_exec
54
+ # on a BlackHoleStruct, so @ivars would resolve on the BHS.
55
+ issuer = @issuer
56
+ ingress_class = @ingress_class
57
+
58
+ generated << Kube::Cluster["Ingress"].new {
59
+ metadata.name = name
60
+ metadata.namespace = namespace if namespace
61
+ metadata.labels = labels.reject { |k, _| k == LABEL }
62
+ metadata.annotations = {
63
+ "cert-manager.io/cluster-issuer": issuer,
64
+ "nginx.ingress.kubernetes.io/ssl-redirect": "true",
65
+ }
66
+
67
+ spec.ingressClassName = ingress_class
68
+ spec.tls = [
69
+ { hosts: [host], secretName: "#{name}-tls" },
70
+ ]
71
+ spec.rules = [
72
+ {
73
+ host: host,
74
+ http: {
75
+ paths: [{
76
+ path: "/",
77
+ pathType: "Prefix",
78
+ backend: { service: { name: name, port: { name: port_name } } },
79
+ }],
80
+ },
81
+ },
82
+ ]
83
+ }
84
+ end
85
+
86
+ manifest.resources.concat(generated)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kube
4
+ module Cluster
5
+ class Middleware
6
+ # Merges labels into +metadata.labels+ on every resource.
7
+ # Existing labels are preserved; the supplied labels act as defaults
8
+ # that can be overridden per-resource.
9
+ #
10
+ # stack do
11
+ # use Middleware::Labels, app: "web-app", managed_by: "kube_cluster"
12
+ # end
13
+ #
14
+ # The keyword arguments are converted to standard label keys:
15
+ #
16
+ # app: -> "app.kubernetes.io/name"
17
+ # instance: -> "app.kubernetes.io/instance"
18
+ # version: -> "app.kubernetes.io/version"
19
+ # component: -> "app.kubernetes.io/component"
20
+ # part_of: -> "app.kubernetes.io/part-of"
21
+ # managed_by: -> "app.kubernetes.io/managed-by"
22
+ #
23
+ # Any unrecognized keys are passed through as-is (string or symbol).
24
+ #
25
+ class Labels < Middleware
26
+ STANDARD_KEYS = {
27
+ app: :"app.kubernetes.io/name",
28
+ instance: :"app.kubernetes.io/instance",
29
+ version: :"app.kubernetes.io/version",
30
+ component: :"app.kubernetes.io/component",
31
+ part_of: :"app.kubernetes.io/part-of",
32
+ managed_by: :"app.kubernetes.io/managed-by",
33
+ }.freeze
34
+
35
+ def initialize(**labels)
36
+ @labels = normalize(labels)
37
+ end
38
+
39
+ def call(manifest)
40
+ manifest.resources.map! do |resource|
41
+ h = resource.to_h
42
+ h[:metadata] ||= {}
43
+ h[:metadata][:labels] = @labels.merge(h[:metadata][:labels] || {})
44
+ resource.rebuild(h)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def normalize(labels)
51
+ labels.each_with_object({}) do |(key, value), result|
52
+ normalized_key = STANDARD_KEYS.fetch(key, key)
53
+ result[normalized_key] = value.to_s
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kube
4
+ module Cluster
5
+ class Middleware
6
+ # Sets +metadata.namespace+ on all namespace-scoped resources.
7
+ # Cluster-scoped kinds (Namespace, ClusterRole, etc.) are skipped.
8
+ #
9
+ # stack do
10
+ # use Middleware::Namespace, "production"
11
+ # end
12
+ #
13
+ class Namespace < Middleware
14
+ def initialize(namespace)
15
+ @namespace = namespace
16
+ end
17
+
18
+ def call(manifest)
19
+ manifest.resources.map! do |resource|
20
+ next resource if resource.cluster_scoped?
21
+
22
+ h = resource.to_h
23
+ h[:metadata] ||= {}
24
+ h[:metadata][:namespace] = @namespace
25
+ resource.rebuild(h)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end