kube_cluster 0.2.0 → 0.2.1
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/database/manifest.rb +238 -0
- data/examples/version2/demo.rb +87 -0
- data/examples/version2/helpers.rb +18 -0
- data/examples/version2/my_app.rb +45 -0
- data/examples/version2/postgresql.rb +81 -0
- data/examples/version2/ruby_on_rails.rb +31 -0
- data/examples/web-app/manifest.rb +215 -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/middleware/annotations.rb +32 -0
- data/lib/kube/cluster/manifest/middleware/hpa_for_deployment.rb +109 -0
- data/lib/kube/cluster/manifest/middleware/ingress_for_service.rb +89 -0
- data/lib/kube/cluster/manifest/middleware/labels.rb +59 -0
- data/lib/kube/cluster/manifest/middleware/namespace.rb +31 -0
- data/lib/kube/cluster/manifest/middleware/pod_anti_affinity.rb +61 -0
- data/lib/kube/cluster/manifest/middleware/resource_preset.rb +64 -0
- data/lib/kube/cluster/manifest/middleware/security_context.rb +84 -0
- data/lib/kube/cluster/manifest/middleware/service_for_deployment.rb +69 -0
- data/lib/kube/cluster/manifest/middleware.rb +178 -0
- data/lib/kube/cluster/manifest/stack.rb +56 -0
- data/lib/kube/cluster/manifest.rb +76 -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 +21 -0
- data/lib/kube/cluster/version.rb +1 -1
- data/lib/kube/cluster.rb +13 -7
- data/lib/kube/errors.rb +57 -0
- metadata +63 -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,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module App
|
|
4
|
+
module Helpers
|
|
5
|
+
def match_labels(name:, instance:, component: nil)
|
|
6
|
+
labels = {
|
|
7
|
+
"app.kubernetes.io/name": name,
|
|
8
|
+
"app.kubernetes.io/instance": instance,
|
|
9
|
+
}
|
|
10
|
+
labels[:"app.kubernetes.io/component"] = component if component
|
|
11
|
+
labels
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def base64(str)
|
|
15
|
+
[str].pack("m0")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -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::Schema["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::Schema["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::Schema["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::Schema["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::Schema["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
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "kube/schema"
|
|
5
|
+
|
|
6
|
+
MATCH_LABELS = STANDARD_LABELS.slice(
|
|
7
|
+
:"app.kubernetes.io/name",
|
|
8
|
+
:"app.kubernetes.io/instance",
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
REGISTRY = "docker.io"
|
|
12
|
+
REPOSITORY = "nginx"
|
|
13
|
+
TAG = "1.27.3-alpine"
|
|
14
|
+
IMAGE = "#{REGISTRY}/#{REPOSITORY}:#{TAG}"
|
|
15
|
+
|
|
16
|
+
RESOURCE_PRESETS = {
|
|
17
|
+
"nano": { requests: { cpu: "100m", memory: "128Mi" }, limits: { cpu: "150m", memory: "192Mi" } },
|
|
18
|
+
"micro": { requests: { cpu: "250m", memory: "256Mi" }, limits: { cpu: "375m", memory: "384Mi" } },
|
|
19
|
+
"small": { requests: { cpu: "500m", memory: "512Mi" }, limits: { cpu: "750m", memory: "768Mi" } },
|
|
20
|
+
"medium": { requests: { cpu: "500m", memory: "1024Mi" }, limits: { cpu: "750m", memory: "1536Mi" } },
|
|
21
|
+
"large": { requests: { cpu: "1.0", memory: "2048Mi" }, limits: { cpu: "1.5", memory: "3072Mi" } },
|
|
22
|
+
"xlarge": { requests: { cpu: "1.0", memory: "3072Mi" }, limits: { cpu: "3.0", memory: "6144Mi" } },
|
|
23
|
+
"2xlarge": { requests: { cpu: "1.0", memory: "3072Mi" }, limits: { cpu: "6.0", memory: "12288Mi" } },
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module AutoConfig
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class MyApp < Kube::Schema::Manifest
|
|
30
|
+
include Helpers
|
|
31
|
+
include AutoConfig
|
|
32
|
+
|
|
33
|
+
def initialize(namespace: DEFAULT_NAMESPACE, labels: STANDARD_LABELS)
|
|
34
|
+
@app_name = "web-app"
|
|
35
|
+
@fullname = "#{RELEASE_NAME}-#{APP_NAME}"[0, 63].chomp("-")
|
|
36
|
+
@namespace = namespace
|
|
37
|
+
|
|
38
|
+
@labels = {
|
|
39
|
+
"app.kubernetes.io/name": @app_name
|
|
40
|
+
"app.kubernetes.io/managed-by": "kube_cluster",
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
stack do
|
|
45
|
+
use Middleware::Namespace
|
|
46
|
+
use Middleware::Labels
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class Namespace < Kube::Schema["Namespace"]
|
|
50
|
+
def initialize(namespace:)
|
|
51
|
+
build {
|
|
52
|
+
metadata.name = namespace
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class ConfigMap < Kube::Schema["ConfigMap"]
|
|
58
|
+
def initialize(namespace:)
|
|
59
|
+
build {
|
|
60
|
+
metadata.name = "#{namespace}-config"
|
|
61
|
+
spec.data = {
|
|
62
|
+
RAILS_ENV: "production",
|
|
63
|
+
LOG_LEVEL: "info",
|
|
64
|
+
WORKERS: "4",
|
|
65
|
+
PORT: "3000",
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class Deployment < Kube::Schema["Deployment"]
|
|
72
|
+
def initialize(namespace:)
|
|
73
|
+
build {
|
|
74
|
+
metadata.name = namespace
|
|
75
|
+
|
|
76
|
+
spec.replicas = 3
|
|
77
|
+
spec.selector.matchLabels = MATCH_LABELS
|
|
78
|
+
|
|
79
|
+
spec.template.metadata.labels = STANDARD_LABELS
|
|
80
|
+
spec.template.metadata.annotations = {
|
|
81
|
+
# Checksum pattern from _utils.tpl -- triggers rolling restart on config change
|
|
82
|
+
"checksum/config": "{{ sha256sum of configmap data }}",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
spec.template.spec.containers = [
|
|
86
|
+
{
|
|
87
|
+
name: APP_NAME,
|
|
88
|
+
image: IMAGE,
|
|
89
|
+
ports: [{ name: "http", containerPort: 3000, protocol: "TCP" }],
|
|
90
|
+
resources: RESOURCES,
|
|
91
|
+
env: [
|
|
92
|
+
{ name: "PORT", value: "3000" },
|
|
93
|
+
],
|
|
94
|
+
envFrom: [
|
|
95
|
+
{ configMapRef: { name: "#{FULLNAME}-config" } },
|
|
96
|
+
],
|
|
97
|
+
livenessProbe: {
|
|
98
|
+
httpGet: { path: "/healthz", port: "http" },
|
|
99
|
+
initialDelaySeconds: 15,
|
|
100
|
+
periodSeconds: 10,
|
|
101
|
+
},
|
|
102
|
+
readinessProbe: {
|
|
103
|
+
httpGet: { path: "/readyz", port: "http" },
|
|
104
|
+
initialDelaySeconds: 5,
|
|
105
|
+
periodSeconds: 5,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
# Pod anti-affinity (from _affinities.tpl)
|
|
111
|
+
# Soft anti-affinity: prefer spreading pods across nodes but don't enforce it
|
|
112
|
+
spec.template.spec.affinity = {
|
|
113
|
+
podAntiAffinity: {
|
|
114
|
+
preferredDuringSchedulingIgnoredDuringExecution: [
|
|
115
|
+
{
|
|
116
|
+
weight: 1,
|
|
117
|
+
podAffinityTerm: {
|
|
118
|
+
labelSelector: {
|
|
119
|
+
matchLabels: MATCH_LABELS,
|
|
120
|
+
},
|
|
121
|
+
topologyKey: "kubernetes.io/hostname",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class Service < Kube::Schema["Service"]
|
|
132
|
+
def initialize(namespace:)
|
|
133
|
+
build {
|
|
134
|
+
metadata.name = namespace
|
|
135
|
+
|
|
136
|
+
spec.selector = MATCH_LABELS
|
|
137
|
+
spec.ports = [
|
|
138
|
+
{ name: "http", port: 80, targetPort: "http", protocol: "TCP" },
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
class Ingress < Kube::Schema["Ingress"]
|
|
145
|
+
def initialize(namespace:)
|
|
146
|
+
build {
|
|
147
|
+
metadata.name = namespace
|
|
148
|
+
metadata.annotations = {
|
|
149
|
+
"cert-manager.io/cluster-issuer": "letsencrypt-prod",
|
|
150
|
+
"nginx.ingress.kubernetes.io/ssl-redirect": "true",
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
spec.ingressClassName = "nginx"
|
|
154
|
+
spec.tls = [
|
|
155
|
+
{
|
|
156
|
+
hosts: ["app.example.com"],
|
|
157
|
+
secretName: "#{namespace}-tls",
|
|
158
|
+
},
|
|
159
|
+
]
|
|
160
|
+
spec.rules = [
|
|
161
|
+
{
|
|
162
|
+
host: "app.example.com",
|
|
163
|
+
http: {
|
|
164
|
+
paths: [
|
|
165
|
+
{
|
|
166
|
+
path: "/",
|
|
167
|
+
pathType: "Prefix",
|
|
168
|
+
backend: {
|
|
169
|
+
service: {
|
|
170
|
+
name: FULLNAME,
|
|
171
|
+
port: { name: "http" },
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
class HorizontalPodAutoscaler < Kube::Schema["HorizontalPodAutoscaler"]
|
|
184
|
+
def initialize(namespace:)
|
|
185
|
+
build {
|
|
186
|
+
metadata.name = namespace
|
|
187
|
+
|
|
188
|
+
spec.scaleTargetRef.apiVersion = "apps/v1"
|
|
189
|
+
spec.scaleTargetRef.kind = "Deployment"
|
|
190
|
+
spec.scaleTargetRef.name = namespace
|
|
191
|
+
|
|
192
|
+
spec.minReplicas = 3
|
|
193
|
+
spec.maxReplicas = 10
|
|
194
|
+
spec.metrics = [
|
|
195
|
+
{
|
|
196
|
+
type: "Resource",
|
|
197
|
+
resource: {
|
|
198
|
+
name: "cpu",
|
|
199
|
+
target: { type: "Utilization", averageUtilization: 75 },
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
type: "Resource",
|
|
204
|
+
resource: {
|
|
205
|
+
name: "memory",
|
|
206
|
+
target: { type: "Utilization", averageUtilization: 80 },
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
puts MyApp.new.to_yaml
|
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,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kube
|
|
4
|
+
module Cluster
|
|
5
|
+
class Manifest < Kube::Schema::Manifest
|
|
6
|
+
class Middleware
|
|
7
|
+
# Merges annotations into +metadata.annotations+ on every resource.
|
|
8
|
+
# Existing annotations are preserved; the supplied annotations act
|
|
9
|
+
# as defaults that can be overridden per-resource.
|
|
10
|
+
#
|
|
11
|
+
# stack do
|
|
12
|
+
# use Middleware::Annotations,
|
|
13
|
+
# "prometheus.io/scrape": "true",
|
|
14
|
+
# "prometheus.io/port": "9090"
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
class Annotations < Middleware
|
|
18
|
+
def initialize(**annotations)
|
|
19
|
+
@annotations = annotations.transform_keys(&:to_sym).transform_values(&:to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(resource)
|
|
23
|
+
h = resource.to_h
|
|
24
|
+
h[:metadata] ||= {}
|
|
25
|
+
h[:metadata][:annotations] = @annotations.merge(h[:metadata][:annotations] || {})
|
|
26
|
+
rebuild(resource, h)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kube
|
|
4
|
+
module Cluster
|
|
5
|
+
class Manifest < Kube::Schema::Manifest
|
|
6
|
+
class Middleware
|
|
7
|
+
# Generates a HorizontalPodAutoscaler for every pod-bearing
|
|
8
|
+
# resource that carries the +app.kubernetes.io/autoscale+ label.
|
|
9
|
+
#
|
|
10
|
+
# The label value encodes the min and max replicas as "min-max":
|
|
11
|
+
#
|
|
12
|
+
# metadata.labels = { "app.kubernetes.io/autoscale": "1-5" }
|
|
13
|
+
#
|
|
14
|
+
# Options:
|
|
15
|
+
# cpu: — target CPU utilization percentage (default: 75)
|
|
16
|
+
# memory: — target memory utilization percentage (default: 80)
|
|
17
|
+
#
|
|
18
|
+
# stack do
|
|
19
|
+
# use Middleware::HPAForDeployment
|
|
20
|
+
# use Middleware::HPAForDeployment, cpu: 60, memory: 70
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
class HPAForDeployment < Middleware
|
|
24
|
+
LABEL = :"app.kubernetes.io/autoscale"
|
|
25
|
+
|
|
26
|
+
def initialize(cpu: 75, memory: 80)
|
|
27
|
+
@cpu = cpu
|
|
28
|
+
@memory = memory
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call(resource)
|
|
32
|
+
return resource unless pod_bearing?(resource)
|
|
33
|
+
|
|
34
|
+
value = label(resource, LABEL)
|
|
35
|
+
return resource unless value
|
|
36
|
+
|
|
37
|
+
min, max = parse_range(value)
|
|
38
|
+
|
|
39
|
+
h = resource.to_h
|
|
40
|
+
name = h.dig(:metadata, :name)
|
|
41
|
+
namespace = h.dig(:metadata, :namespace)
|
|
42
|
+
labels = h.dig(:metadata, :labels) || {}
|
|
43
|
+
api_version = h[:apiVersion] || "apps/v1"
|
|
44
|
+
resource_kind = kind(resource)
|
|
45
|
+
|
|
46
|
+
# Capture ivars as locals — the block runs via instance_exec
|
|
47
|
+
# on a BlackHoleStruct, so @ivars would resolve on the BHS.
|
|
48
|
+
cpu_target = @cpu
|
|
49
|
+
memory_target = @memory
|
|
50
|
+
|
|
51
|
+
hpa = Kube::Schema["HorizontalPodAutoscaler"].new {
|
|
52
|
+
metadata.name = name
|
|
53
|
+
metadata.namespace = namespace if namespace
|
|
54
|
+
metadata.labels = labels.reject { |k, _| k == LABEL }
|
|
55
|
+
|
|
56
|
+
spec.scaleTargetRef = {
|
|
57
|
+
apiVersion: api_version,
|
|
58
|
+
kind: resource_kind,
|
|
59
|
+
name: name,
|
|
60
|
+
}
|
|
61
|
+
spec.minReplicas = min
|
|
62
|
+
spec.maxReplicas = max
|
|
63
|
+
spec.metrics = [
|
|
64
|
+
{
|
|
65
|
+
type: "Resource",
|
|
66
|
+
resource: {
|
|
67
|
+
name: "cpu",
|
|
68
|
+
target: { type: "Utilization", averageUtilization: cpu_target },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: "Resource",
|
|
73
|
+
resource: {
|
|
74
|
+
name: "memory",
|
|
75
|
+
target: { type: "Utilization", averageUtilization: memory_target },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[resource, hpa]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def parse_range(value)
|
|
87
|
+
parts = value.to_s.split("-", 2)
|
|
88
|
+
|
|
89
|
+
unless parts.length == 2
|
|
90
|
+
raise ArgumentError,
|
|
91
|
+
"Invalid autoscale label: #{value.inspect}. Expected format: \"min-max\" (e.g. \"1-5\")"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
min = Integer(parts[0])
|
|
95
|
+
max = Integer(parts[1])
|
|
96
|
+
|
|
97
|
+
unless min > 0 && max >= min
|
|
98
|
+
raise ArgumentError,
|
|
99
|
+
"Invalid autoscale range: min=#{min}, max=#{max}. " \
|
|
100
|
+
"min must be > 0 and max must be >= min."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
[min, max]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|