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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +43 -0
- data/.github/workflows/tag-gem-version-bump.yml +47 -0
- data/.gitignore +2 -0
- data/Gemfile.lock +48 -52
- data/bin/console +3 -0
- data/bin/dev +4 -0
- data/docker-compose.yml +26 -0
- data/examples/01-basic-redis-pod/manifest.rb +60 -0
- data/examples/02-manifest-with-middleware/manifest.rb +37 -0
- data/examples/02-manifest-with-middleware/middleware/labels.rb +4 -0
- data/examples/02-manifest-with-middleware/middleware/namespace.rb +4 -0
- data/examples/02-manifest-with-middleware/templates/config_map.rb +13 -0
- data/examples/02-manifest-with-middleware/templates/deployment.rb +59 -0
- data/examples/02-manifest-with-middleware/templates/horizontal_pod_autoscaler.rb +30 -0
- data/examples/02-manifest-with-middleware/templates/ingress.rb +38 -0
- data/examples/02-manifest-with-middleware/templates/service.rb +12 -0
- data/examples/03-app-with-database/demo.rb +87 -0
- data/examples/03-app-with-database/helpers.rb +18 -0
- data/examples/03-app-with-database/my_app.rb +45 -0
- data/examples/03-app-with-database/postgresql.rb +81 -0
- data/examples/03-app-with-database/ruby_on_rails.rb +31 -0
- data/flake.lock +3 -3
- data/flake.nix +6 -0
- data/kube_cluster.gemspec +3 -1
- data/lib/kube/cli/cluster.rb +41 -0
- data/lib/kube/cluster/connection.rb +18 -0
- data/lib/kube/cluster/instance.rb +21 -0
- data/lib/kube/cluster/manifest.rb +25 -0
- data/lib/kube/cluster/middleware/annotations.rb +32 -0
- data/lib/kube/cluster/middleware/hpa_for_deployment.rb +111 -0
- data/lib/kube/cluster/middleware/ingress_for_service.rb +91 -0
- data/lib/kube/cluster/middleware/labels.rb +59 -0
- data/lib/kube/cluster/middleware/namespace.rb +31 -0
- data/lib/kube/cluster/middleware/pod_anti_affinity.rb +61 -0
- data/lib/kube/cluster/middleware/resource_preset.rb +64 -0
- data/lib/kube/cluster/middleware/security_context.rb +84 -0
- data/lib/kube/cluster/middleware/service_for_deployment.rb +71 -0
- data/lib/kube/cluster/middleware/stack.rb +43 -0
- data/lib/kube/cluster/middleware.rb +69 -0
- data/lib/kube/cluster/resource/dirty_tracking.rb +113 -0
- data/lib/kube/cluster/resource/persistence.rb +67 -0
- data/lib/kube/cluster/resource.rb +99 -0
- data/lib/kube/cluster/version.rb +1 -1
- data/lib/kube/cluster.rb +34 -7
- data/lib/kube/errors.rb +57 -0
- metadata +69 -17
- data/Rakefile +0 -11
- data/TREE_PLAN.md +0 -513
- data/bin/generate-command-schema-v1 +0 -44
- data/data/kubectl-command-tree-v1-minimal.json +0 -125
- data/data/kubectl-command-tree-v1.json +0 -1469
- data/examples/quick-repl/docker-compose.yml +0 -52
- data/exe/kube_cluster +0 -6
- data/lib/kube/cluster/command_node.rb +0 -89
- data/lib/kube/cluster/ctl.rb +0 -33
- data/lib/kube/cluster/query_builder.rb +0 -35
- data/lib/kube/cluster/resource_selector.rb +0 -19
- 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":
|
|
6
|
-
"narHash": "sha256-
|
|
5
|
+
"lastModified": 1776169885,
|
|
6
|
+
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
|
|
7
7
|
"owner": "NixOS",
|
|
8
8
|
"repo": "nixpkgs",
|
|
9
|
-
"rev": "
|
|
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
|