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
data/TREE_PLAN.md
DELETED
|
@@ -1,513 +0,0 @@
|
|
|
1
|
-
How will be now create ActiveRecord::Relatino style command objects?
|
|
2
|
-
|
|
3
|
-
10:25
|
|
4
|
-
|
|
5
|
-
Architected chainable, lazy-evaluated kubectl command builder pattern
|
|
6
|
-
Architected chainable, lazy-evaluated kubectl command builder pattern
|
|
7
|
-
Great use case for the tree! The idea is an immutable, chainable builder that only shells out when forced — exactly like ActiveRecord::Relation.
|
|
8
|
-
|
|
9
|
-
ruby
|
|
10
|
-
class Kubectl
|
|
11
|
-
TREE = JSON.parse(
|
|
12
|
-
File.read(File.join(GEM_DIR, 'data', 'kubectl-command-tree-v1-minimal.json')),
|
|
13
|
-
symbolize_names: true
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
def initialize(commands: [], flags: {}, tree: TREE)
|
|
17
|
-
@commands = commands
|
|
18
|
-
@flags = flags
|
|
19
|
-
@tree = tree
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def method_missing(name, *args)
|
|
23
|
-
cmd = name.to_s.tr('_', '-')
|
|
24
|
-
@tree.key?(cmd.to_sym).then do |valid|
|
|
25
|
-
raise ArgumentError, "Unknown kubectl command: #{cmd}" unless valid
|
|
26
|
-
self.class.new(
|
|
27
|
-
commands: @commands + [cmd] + args.map(&:to_s),
|
|
28
|
-
flags: @flags,
|
|
29
|
-
tree: @tree.dig(cmd.to_sym) || {}
|
|
30
|
-
)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def respond_to_missing?(name, include_private = false)
|
|
35
|
-
@tree.key?(name.to_s.tr('_', '-').to_sym) || super
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def flag(key, value = nil)
|
|
39
|
-
self.class.new(
|
|
40
|
-
commands: @commands,
|
|
41
|
-
flags: @flags.merge(key.to_s.tr('_', '-') => value),
|
|
42
|
-
tree: @tree
|
|
43
|
-
)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def to_a
|
|
47
|
-
stdout, = Open3.capture2('kubectl', *@commands, *rendered_flags)
|
|
48
|
-
stdout.lines.map(&:chomp)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def to_s
|
|
52
|
-
['kubectl', *@commands, *rendered_flags].join(' ')
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
def rendered_flags
|
|
58
|
-
@flags.flat_map do |k, v|
|
|
59
|
-
v ? ["--#{k}", v.to_s] : ["--#{k}"]
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
Which gives you a nice chainable API:
|
|
64
|
-
|
|
65
|
-
ruby
|
|
66
|
-
Kubectl.new.get('pods')
|
|
67
|
-
.flag(:namespace, 'production')
|
|
68
|
-
.flag(:output, 'json')
|
|
69
|
-
.to_a
|
|
70
|
-
|
|
71
|
-
Kubectl.new.rollout.status('deployment/my-app')
|
|
72
|
-
.flag(:namespace, 'staging')
|
|
73
|
-
.to_a
|
|
74
|
-
|
|
75
|
-
# Preview without running
|
|
76
|
-
Kubectl.new.get('nodes').flag(:output, 'wide').to_s
|
|
77
|
-
# => "kubectl get nodes --output wide"
|
|
78
|
-
Key design decisions:
|
|
79
|
-
|
|
80
|
-
Immutable — every chain returns a new instance with merged state, nothing mutates
|
|
81
|
-
method_missing walks the tree — invalid commands raise immediately rather than failing at shell time
|
|
82
|
-
to_a is the forcing function — lazy until you call it, just like ActiveRecord::Relation
|
|
83
|
-
to_s for free previewing — useful for debugging or logging the command before firing it
|
|
84
|
-
If Enumerable would respond to the method missing then make the api call..
|
|
85
|
-
|
|
86
|
-
10:28
|
|
87
|
-
|
|
88
|
-
Architected automatic API invocation through Enumerable inclusion
|
|
89
|
-
Architected automatic API invocation through Enumerable inclusion
|
|
90
|
-
Include Enumerable and make each the single forcing function — then all enumerable methods trigger the shell call automatically:
|
|
91
|
-
|
|
92
|
-
ruby
|
|
93
|
-
include Enumerable
|
|
94
|
-
|
|
95
|
-
def each(&block)
|
|
96
|
-
to_a.each(&block)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def method_missing(name, *args, &block)
|
|
100
|
-
cmd = name.to_s.tr('_', '-')
|
|
101
|
-
|
|
102
|
-
if @tree.key?(cmd.to_sym)
|
|
103
|
-
self.class.new(
|
|
104
|
-
commands: @commands + [cmd] + args.map(&:to_s),
|
|
105
|
-
flags: @flags,
|
|
106
|
-
tree: @tree.dig(cmd.to_sym) || {}
|
|
107
|
-
)
|
|
108
|
-
elsif Enumerable.method_defined?(name)
|
|
109
|
-
to_a.public_send(name, *args, &block)
|
|
110
|
-
else
|
|
111
|
-
super
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
Now the API call happens naturally:
|
|
115
|
-
|
|
116
|
-
ruby
|
|
117
|
-
Kubectl.new.get('pods').flag(:namespace, 'production').map { |line| line.split.first }
|
|
118
|
-
Kubectl.new.get('nodes').flag(:output, 'wide').select { |line| line.include?('Ready') }
|
|
119
|
-
Kubectl.new.get('pods').first(3)
|
|
120
|
-
The chain stays lazy until an Enumerable method is hit — at which point each forces to_a which fires the shell command. No explicit .to_a needed.
|
|
121
|
-
|
|
122
|
-
Okay call this object CommandNode and create an additional object called ResourceNode which handles the deployment.v1.apps pattern... assume we have an array of all supported type.version.group sets.
|
|
123
|
-
|
|
124
|
-
10:32
|
|
125
|
-
|
|
126
|
-
Architected nested tree structure for resource type parsing and integration
|
|
127
|
-
Architected nested tree structure for resource type parsing and integration
|
|
128
|
-
ruby
|
|
129
|
-
class ResourceNode
|
|
130
|
-
# e.g. ["deployments.v1.apps", "pods.v1", "replicasets.v1.apps", ...]
|
|
131
|
-
TREE = SUPPORTED_TYPES
|
|
132
|
-
.each_with_object({}) do |type_string, tree|
|
|
133
|
-
type_string
|
|
134
|
-
.split('.')
|
|
135
|
-
.reduce(tree) { |node, segment| node[segment.to_sym] ||= {} }
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def initialize(path: [], tree: TREE)
|
|
139
|
-
@path = path
|
|
140
|
-
@tree = tree
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def method_missing(name, *args, &block)
|
|
144
|
-
segment = name.to_s.tr('_', '-')
|
|
145
|
-
|
|
146
|
-
@tree.key?(segment.to_sym).then do |valid|
|
|
147
|
-
raise ArgumentError, "Unknown resource segment: #{segment}" unless valid
|
|
148
|
-
self.class.new(
|
|
149
|
-
path: @path + [segment],
|
|
150
|
-
tree: @tree[segment.to_sym]
|
|
151
|
-
)
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def respond_to_missing?(name, include_private = false)
|
|
156
|
-
@tree.key?(name.to_s.tr('_', '-').to_sym) || super
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def to_s
|
|
160
|
-
@path.join('.')
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
Which then composes naturally with CommandNode:
|
|
164
|
-
|
|
165
|
-
ruby
|
|
166
|
-
resource = ResourceNode.new.deployments.v1.apps
|
|
167
|
-
|
|
168
|
-
CommandNode.new
|
|
169
|
-
.get(resource.to_s)
|
|
170
|
-
.flag(:namespace, 'production')
|
|
171
|
-
.map { |line| line.split.first }
|
|
172
|
-
Or you could make CommandNode accept ResourceNode directly by checking in rendered_args:
|
|
173
|
-
|
|
174
|
-
ruby
|
|
175
|
-
# in CommandNode
|
|
176
|
-
def method_missing(name, *args, &block)
|
|
177
|
-
cmd = name.to_s.tr('_', '-')
|
|
178
|
-
|
|
179
|
-
if @tree.key?(cmd.to_sym)
|
|
180
|
-
self.class.new(
|
|
181
|
-
commands: @commands + [cmd] + args.map(&:to_s), # to_s coerces ResourceNode naturally
|
|
182
|
-
flags: @flags,
|
|
183
|
-
tree: @tree.dig(cmd.to_sym) || {}
|
|
184
|
-
)
|
|
185
|
-
elsif Enumerable.method_defined?(name)
|
|
186
|
-
to_a.public_send(name, *args, &block)
|
|
187
|
-
else
|
|
188
|
-
super
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
So the full idiomatic API becomes:
|
|
192
|
-
|
|
193
|
-
ruby
|
|
194
|
-
CommandNode.new
|
|
195
|
-
.get(ResourceNode.new.deployments.v1.apps)
|
|
196
|
-
.flag(:namespace, 'production')
|
|
197
|
-
.select { |line| line.include?('my-app') }
|
|
198
|
-
The ResourceNode tree is built once at load time from the flat array — segments become nested keys, so method_missing can walk it exactly like CommandNode walks the kubectl command tree.
|
|
199
|
-
|
|
200
|
-
hmm... this won't work... we need to be able to do Kubectl.get.deployment.v1 etc...
|
|
201
|
-
|
|
202
|
-
What we need to do it iterate over the schema and create some TreeInstances for each key... so each key turns from a string into a TreeInstance.... then when we're going throuhg our commands we know what the next node should be....
|
|
203
|
-
|
|
204
|
-
this is not a simple task... there are a lot of moving parts...
|
|
205
|
-
|
|
206
|
-
**make a plan** to get this right
|
|
207
|
-
|
|
208
|
-
10:36
|
|
209
|
-
|
|
210
|
-
Architected tree-node system grafting commands and resources together
|
|
211
|
-
Architected tree-node system grafting commands and resources together
|
|
212
|
-
The Plan
|
|
213
|
-
The core insight: every step in the chain — whether a kubectl command or a resource segment — is the same thing: a node that knows what its valid children are. So we need one universal building block.
|
|
214
|
-
|
|
215
|
-
1. TreeNode — the universal building block
|
|
216
|
-
TreeNode
|
|
217
|
-
name: string
|
|
218
|
-
children: { name => TreeNode }
|
|
219
|
-
Built at load time. Every key in every tree becomes a TreeNode instance pointing to its children. No method_missing string parsing at runtime — the graph is pre-resolved.
|
|
220
|
-
|
|
221
|
-
2. Build two trees into TreeNode graphs
|
|
222
|
-
Command tree — from the JSON:
|
|
223
|
-
|
|
224
|
-
get -> TreeNode(name: "get", children: {})
|
|
225
|
-
rollout -> TreeNode(name: "rollout", children: {
|
|
226
|
-
status -> TreeNode(...),
|
|
227
|
-
history -> TreeNode(...)
|
|
228
|
-
})
|
|
229
|
-
Resource tree — from the flat array:
|
|
230
|
-
|
|
231
|
-
["deployments.v1.apps", "pods.v1", ...]
|
|
232
|
-
-> deployments -> TreeNode(name: "deployments", children: {
|
|
233
|
-
v1 -> TreeNode(name: "v1", children: {
|
|
234
|
-
apps -> TreeNode(name: "apps", children: {})
|
|
235
|
-
})
|
|
236
|
-
})
|
|
237
|
-
3. Graft the resource tree onto resource-accepting commands
|
|
238
|
-
Commands like get, delete, describe, edit accept a resource as their next argument. Their children should include the resource tree nodes:
|
|
239
|
-
|
|
240
|
-
get.children = resource_tree.children # deployments, pods, nodes...
|
|
241
|
-
This is the key step — it's what lets Kubectl.get.deployments.v1.apps resolve correctly through a single unified method_missing walk.
|
|
242
|
-
|
|
243
|
-
4. CommandContext — the chainable, lazy evaluator
|
|
244
|
-
CommandContext
|
|
245
|
-
current_node: TreeNode # where we are in the graph
|
|
246
|
-
args: [] # accumulated path ["get", "deployments.v1.apps"]
|
|
247
|
-
flags: {} # --namespace production etc
|
|
248
|
-
method_missing — looks up name in current_node.children, returns new CommandContext with updated node + args
|
|
249
|
-
flag(key, value) — returns new CommandContext with merged flags
|
|
250
|
-
include Enumerable + each — forces the shell call, the single lazy boundary
|
|
251
|
-
to_s — renders the command without running it
|
|
252
|
-
5. Entry point
|
|
253
|
-
ruby
|
|
254
|
-
module Kubectl
|
|
255
|
-
def self.method_missing(name, ...)
|
|
256
|
-
CommandContext.new(current_node: ROOT_COMMAND_NODE, args: [], flags: {})
|
|
257
|
-
.public_send(name, ...)
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
Load-time sequence
|
|
261
|
-
1. Parse kubectl-command-tree-v1-minimal.json -> build command TreeNodes
|
|
262
|
-
2. Parse SUPPORTED_TYPES array -> build resource TreeNodes
|
|
263
|
-
3. Graft resource tree onto resource commands -> stitch the two graphs
|
|
264
|
-
4. Assign root -> ROOT_COMMAND_NODE
|
|
265
|
-
Everything after that is just graph traversal. Sound right before we build it?
|
|
266
|
-
|
|
267
|
-
Not quite... the tree node has a type... deployment.v1.app is not 3 tree nodes...
|
|
268
|
-
|
|
269
|
-
10:39
|
|
270
|
-
|
|
271
|
-
Reconsidered tree structure distinguishing resource nodes from command nodes
|
|
272
|
-
Reconsidered tree structure distinguishing resource nodes from command nodes
|
|
273
|
-
You're right. deployments.v1.apps is one thing — a resource type. The dots are qualifiers on that resource, not tree levels. So:
|
|
274
|
-
|
|
275
|
-
Revised Plan
|
|
276
|
-
TreeNode has a type:
|
|
277
|
-
|
|
278
|
-
TreeNode
|
|
279
|
-
name: string
|
|
280
|
-
type: :command | :resource
|
|
281
|
-
children: { name => TreeNode }
|
|
282
|
-
The resource tree is flat — one node per resource name:
|
|
283
|
-
|
|
284
|
-
deployments -> TreeNode(name: "deployments", type: :resource)
|
|
285
|
-
pods -> TreeNode(name: "pods", type: :resource)
|
|
286
|
-
Each ResourceNode holds all its possible version.group qualifiers internally:
|
|
287
|
-
|
|
288
|
-
deployments knows about: ["v1.apps", "v1"]
|
|
289
|
-
pods knows about: ["v1"]
|
|
290
|
-
So .v1.apps is qualification within the resource node, not tree traversal. The ResourceNode accumulates those segments until it resolves to a known type string.
|
|
291
|
-
|
|
292
|
-
The chain then looks like:
|
|
293
|
-
|
|
294
|
-
Kubectl.get # CommandNode -> type: :command
|
|
295
|
-
.deployments # ResourceNode -> type: :resource, resolved: false
|
|
296
|
-
.v1 # still ResourceNode, qualifier accumulated
|
|
297
|
-
.apps # ResourceNode -> resolved: "deployments.v1.apps"
|
|
298
|
-
.flag(...) # back to CommandContext
|
|
299
|
-
.select { ... } # forces shell call
|
|
300
|
-
Does that capture what you're thinking?
|
|
301
|
-
|
|
302
|
-
the Resource nodes don't know anything about any other resource nodes... they only know the command nodes that will come in future... that's how they know they've exited teh resoruce node phase...
|
|
303
|
-
|
|
304
|
-
what the Kubectl object shuold be ding here at everystep is building a hash datastructure...
|
|
305
|
-
which we end up passing like this...
|
|
306
|
-
|
|
307
|
-
def to_a
|
|
308
|
-
QueryBuilder.new(self.command_data).query.to_a
|
|
309
|
-
end
|
|
310
|
-
This means that the primary focus of each stage on method missing is to iterate over the options and return the appropriate node.
|
|
311
|
-
|
|
312
|
-
Show more
|
|
313
|
-
10:42
|
|
314
|
-
|
|
315
|
-
Reconciled node architecture with hash-building data flow pattern
|
|
316
|
-
Reconciled node architecture with hash-building data flow pattern
|
|
317
|
-
Ah, now it's much clearer. Each node's only job is:
|
|
318
|
-
|
|
319
|
-
"given what I know about my valid next steps, what node do I hand back?"
|
|
320
|
-
|
|
321
|
-
And the whole chain is just accumulating a hash.
|
|
322
|
-
|
|
323
|
-
Revised Plan
|
|
324
|
-
The data structure being built:
|
|
325
|
-
|
|
326
|
-
ruby
|
|
327
|
-
{
|
|
328
|
-
commands: ["get"],
|
|
329
|
-
resource: "deployments.v1.apps",
|
|
330
|
-
flags: { namespace: "production" }
|
|
331
|
-
}
|
|
332
|
-
CommandNode — knows its valid subcommands AND the resource names:
|
|
333
|
-
|
|
334
|
-
method_missing:
|
|
335
|
-
name in subcommands? -> return CommandNode (append to commands)
|
|
336
|
-
name in resources? -> return ResourceNode (start accumulating resource)
|
|
337
|
-
Enumerable? -> force shell call
|
|
338
|
-
ResourceNode — knows the supported type strings AND the command nodes:
|
|
339
|
-
|
|
340
|
-
method_missing:
|
|
341
|
-
current_accumulation + segment completes a known type? -> stay ResourceNode
|
|
342
|
-
name in command nodes? -> exit, return CommandNode
|
|
343
|
-
Enumerable? -> force shell call
|
|
344
|
-
The ResourceNode doesn't know other resources — it only knows commands, which is exactly how it detects it's done.
|
|
345
|
-
|
|
346
|
-
Entry/exit at every step is just:
|
|
347
|
-
|
|
348
|
-
ruby
|
|
349
|
-
def method_missing(name, ...)
|
|
350
|
-
next_node(name).new(command_data.merge(...))
|
|
351
|
-
end
|
|
352
|
-
to_a at any point:
|
|
353
|
-
|
|
354
|
-
ruby
|
|
355
|
-
def to_a
|
|
356
|
-
QueryBuilder.new(command_data).query.to_a
|
|
357
|
-
end
|
|
358
|
-
Shall we build it?
|
|
359
|
-
|
|
360
|
-
yes
|
|
361
|
-
|
|
362
|
-
10:43
|
|
363
|
-
|
|
364
|
-
Architected Ruby DSL for kubectl command chaining
|
|
365
|
-
Architected Ruby DSL for kubectl command chaining
|
|
366
|
-
ruby
|
|
367
|
-
require 'json'
|
|
368
|
-
require 'open3'
|
|
369
|
-
|
|
370
|
-
GEM_DIR = File.expand_path('..', __dir__)
|
|
371
|
-
|
|
372
|
-
COMMAND_TREE = JSON.parse(
|
|
373
|
-
File.read(File.join(GEM_DIR, 'data', 'kubectl-command-tree-v1-minimal.json')),
|
|
374
|
-
symbolize_names: true
|
|
375
|
-
)
|
|
376
|
-
|
|
377
|
-
SUPPORTED_TYPES = %w[
|
|
378
|
-
deployments.v1.apps
|
|
379
|
-
pods.v1
|
|
380
|
-
replicasets.v1.apps
|
|
381
|
-
].freeze
|
|
382
|
-
|
|
383
|
-
RESOURCE_NAMES = SUPPORTED_TYPES.map { |t| t.split('.').first }.uniq.freeze
|
|
384
|
-
|
|
385
|
-
class QueryBuilder
|
|
386
|
-
def initialize(command_data)
|
|
387
|
-
@commands = command_data[:commands]
|
|
388
|
-
@resource = command_data[:resource]
|
|
389
|
-
@flags = command_data[:flags]
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
def query
|
|
393
|
-
stdout, = Open3.capture2('kubectl', *@commands, *[@resource].compact, *rendered_flags)
|
|
394
|
-
stdout.lines.map(&:chomp)
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
private
|
|
398
|
-
|
|
399
|
-
def rendered_flags
|
|
400
|
-
@flags.flat_map { |k, v| v ? ["--#{k}", v.to_s] : ["--#{k}"] }
|
|
401
|
-
end
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
class ResourceNode
|
|
405
|
-
include Enumerable
|
|
406
|
-
|
|
407
|
-
COMMAND_NAMES = COMMAND_TREE.keys.map(&:to_s).freeze
|
|
408
|
-
|
|
409
|
-
def initialize(command_data:, accumulated:)
|
|
410
|
-
@command_data = command_data
|
|
411
|
-
@accumulated = accumulated
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
def flag(key, value = nil)
|
|
415
|
-
self.class.new(
|
|
416
|
-
command_data: @command_data.merge(flags: @command_data[:flags].merge(key => value)),
|
|
417
|
-
accumulated: @accumulated
|
|
418
|
-
)
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
def each(&block)
|
|
422
|
-
QueryBuilder.new(@command_data.merge(resource: @accumulated)).query.each(&block)
|
|
423
|
-
end
|
|
424
|
-
|
|
425
|
-
def method_missing(name, *args, &block)
|
|
426
|
-
segment = name.to_s.tr('_', '-')
|
|
427
|
-
candidate = [@accumulated, segment].join('.')
|
|
428
|
-
|
|
429
|
-
if COMMAND_NAMES.include?(segment)
|
|
430
|
-
CommandNode.new(
|
|
431
|
-
command_data: @command_data.merge(resource: @accumulated, commands: @command_data[:commands] + [segment]),
|
|
432
|
-
tree: COMMAND_TREE[name]
|
|
433
|
-
)
|
|
434
|
-
elsif SUPPORTED_TYPES.any? { |t| t == candidate || t.start_with?("#{candidate}.") }
|
|
435
|
-
self.class.new(command_data: @command_data, accumulated: candidate)
|
|
436
|
-
else
|
|
437
|
-
super
|
|
438
|
-
end
|
|
439
|
-
end
|
|
440
|
-
|
|
441
|
-
def respond_to_missing?(name, include_private = false)
|
|
442
|
-
segment = name.to_s.tr('_', '-')
|
|
443
|
-
candidate = [@accumulated, segment].join('.')
|
|
444
|
-
|
|
445
|
-
COMMAND_NAMES.include?(segment) ||
|
|
446
|
-
SUPPORTED_TYPES.any? { |t| t == candidate || t.start_with?("#{candidate}.") } ||
|
|
447
|
-
super
|
|
448
|
-
end
|
|
449
|
-
end
|
|
450
|
-
|
|
451
|
-
class CommandNode
|
|
452
|
-
include Enumerable
|
|
453
|
-
|
|
454
|
-
def initialize(command_data: { commands: [], resource: nil, flags: {} }, tree: COMMAND_TREE)
|
|
455
|
-
@command_data = command_data
|
|
456
|
-
@tree = tree
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
def flag(key, value = nil)
|
|
460
|
-
self.class.new(
|
|
461
|
-
command_data: @command_data.merge(flags: @command_data[:flags].merge(key => value)),
|
|
462
|
-
tree: @tree
|
|
463
|
-
)
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
def each(&block)
|
|
467
|
-
QueryBuilder.new(@command_data).query.each(&block)
|
|
468
|
-
end
|
|
469
|
-
|
|
470
|
-
def method_missing(name, *args, &block)
|
|
471
|
-
segment = name.to_s.tr('_', '-')
|
|
472
|
-
|
|
473
|
-
if @tree.key?(name)
|
|
474
|
-
self.class.new(
|
|
475
|
-
command_data: @command_data.merge(commands: @command_data[:commands] + [segment]),
|
|
476
|
-
tree: @tree[name]
|
|
477
|
-
)
|
|
478
|
-
elsif RESOURCE_NAMES.include?(segment)
|
|
479
|
-
ResourceNode.new(command_data: @command_data, accumulated: segment)
|
|
480
|
-
else
|
|
481
|
-
super
|
|
482
|
-
end
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
def respond_to_missing?(name, include_private = false)
|
|
486
|
-
segment = name.to_s.tr('_', '-')
|
|
487
|
-
@tree.key?(name) || RESOURCE_NAMES.include?(segment) || super
|
|
488
|
-
end
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
module Kubectl
|
|
492
|
-
def self.method_missing(name, *args, &block)
|
|
493
|
-
CommandNode.new.public_send(name, *args, &block)
|
|
494
|
-
end
|
|
495
|
-
|
|
496
|
-
def self.respond_to_missing?(name, include_private = false)
|
|
497
|
-
CommandNode.new.respond_to?(name) || super
|
|
498
|
-
end
|
|
499
|
-
end
|
|
500
|
-
The chain then flows like:
|
|
501
|
-
|
|
502
|
-
ruby
|
|
503
|
-
Kubectl.get.deployments.v1.apps.flag(:namespace, 'production').select { |l| l.include?('my-app') }
|
|
504
|
-
# ^ ^ ^^^ ResourceNode accumulating
|
|
505
|
-
# | | until Enumerable forces QueryBuilder
|
|
506
|
-
# | RESOURCE_NAMES detected -> ResourceNode
|
|
507
|
-
# CommandNode
|
|
508
|
-
Key things to note:
|
|
509
|
-
|
|
510
|
-
ResourceNode only exits back to CommandNode if it sees a known command name — that's the only thing it knows about the outside world
|
|
511
|
-
Both nodes accumulate into command_data hash, which is the only thing QueryBuilder ever sees
|
|
512
|
-
each is the single forcing function on both nodes — Enumerable does the rest for free
|
|
513
|
-
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'open3'
|
|
5
|
-
|
|
6
|
-
def extract(args = [])
|
|
7
|
-
stdout, = Open3.capture2('kubectl', *args, '--help')
|
|
8
|
-
lines = stdout.lines
|
|
9
|
-
|
|
10
|
-
subcommands = lines
|
|
11
|
-
.lazy
|
|
12
|
-
.select { |l| l.match?(/^ \w[\w-]+\s{2,}/) }
|
|
13
|
-
.map { |l| l.strip.split(/\s{2,}/).first }
|
|
14
|
-
.reject { |cmd| cmd == 'help' }
|
|
15
|
-
.map { |cmd| [cmd, extract(args + [cmd])] }
|
|
16
|
-
.to_h
|
|
17
|
-
|
|
18
|
-
flags = lines
|
|
19
|
-
.lazy
|
|
20
|
-
.select { |l| l.match?(/^\s+--?[\w]/) }
|
|
21
|
-
.map { |l| l.strip }
|
|
22
|
-
.to_a
|
|
23
|
-
|
|
24
|
-
{ flags:, subcommands: }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def minimize(tree)
|
|
28
|
-
tree[:subcommands]
|
|
29
|
-
.transform_values { |v| minimize(v) }
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
GEM_DIR = File.expand_path('..', __dir__)
|
|
33
|
-
|
|
34
|
-
extract.then do |tree|
|
|
35
|
-
File.write(
|
|
36
|
-
File.join(GEM_DIR, "data", "kubectl-command-tree-v1.json"),
|
|
37
|
-
JSON.pretty_generate(tree)
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
File.write(
|
|
41
|
-
File.join(GEM_DIR, "data", "kubectl-command-tree-v1-minimal.json"),
|
|
42
|
-
JSON.pretty_generate(minimize(tree))
|
|
43
|
-
)
|
|
44
|
-
end
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"create": {
|
|
3
|
-
"clusterrole": {},
|
|
4
|
-
"clusterrolebinding": {},
|
|
5
|
-
"configmap": {},
|
|
6
|
-
"cronjob": {},
|
|
7
|
-
"deployment": {},
|
|
8
|
-
"ingress": {},
|
|
9
|
-
"job": {},
|
|
10
|
-
"namespace": {},
|
|
11
|
-
"poddisruptionbudget": {},
|
|
12
|
-
"priorityclass": {},
|
|
13
|
-
"quota": {},
|
|
14
|
-
"role": {},
|
|
15
|
-
"rolebinding": {},
|
|
16
|
-
"secret": {
|
|
17
|
-
"docker-registry": {},
|
|
18
|
-
"generic": {},
|
|
19
|
-
"tls": {}
|
|
20
|
-
},
|
|
21
|
-
"service": {
|
|
22
|
-
"clusterip": {},
|
|
23
|
-
"externalname": {},
|
|
24
|
-
"loadbalancer": {},
|
|
25
|
-
"nodeport": {}
|
|
26
|
-
},
|
|
27
|
-
"serviceaccount": {},
|
|
28
|
-
"token": {}
|
|
29
|
-
},
|
|
30
|
-
"expose": {},
|
|
31
|
-
"run": {},
|
|
32
|
-
"set": {
|
|
33
|
-
"env": {},
|
|
34
|
-
"image": {},
|
|
35
|
-
"resources": {},
|
|
36
|
-
"selector": {},
|
|
37
|
-
"serviceaccount": {},
|
|
38
|
-
"subject": {}
|
|
39
|
-
},
|
|
40
|
-
"explain": {},
|
|
41
|
-
"get": {},
|
|
42
|
-
"edit": {},
|
|
43
|
-
"delete": {},
|
|
44
|
-
"rollout": {
|
|
45
|
-
"history": {},
|
|
46
|
-
"pause": {},
|
|
47
|
-
"restart": {},
|
|
48
|
-
"resume": {},
|
|
49
|
-
"status": {},
|
|
50
|
-
"undo": {}
|
|
51
|
-
},
|
|
52
|
-
"scale": {},
|
|
53
|
-
"autoscale": {},
|
|
54
|
-
"certificate": {
|
|
55
|
-
"approve": {},
|
|
56
|
-
"deny": {}
|
|
57
|
-
},
|
|
58
|
-
"cluster-info": {
|
|
59
|
-
"dump": {}
|
|
60
|
-
},
|
|
61
|
-
"top": {
|
|
62
|
-
"node": {},
|
|
63
|
-
"pod": {}
|
|
64
|
-
},
|
|
65
|
-
"cordon": {},
|
|
66
|
-
"uncordon": {},
|
|
67
|
-
"drain": {},
|
|
68
|
-
"taint": {},
|
|
69
|
-
"describe": {},
|
|
70
|
-
"logs": {},
|
|
71
|
-
"attach": {},
|
|
72
|
-
"exec": {},
|
|
73
|
-
"port-forward": {},
|
|
74
|
-
"proxy": {},
|
|
75
|
-
"cp": {},
|
|
76
|
-
"auth": {
|
|
77
|
-
"can-i": {},
|
|
78
|
-
"reconcile": {},
|
|
79
|
-
"whoami": {}
|
|
80
|
-
},
|
|
81
|
-
"debug": {},
|
|
82
|
-
"events": {},
|
|
83
|
-
"diff": {},
|
|
84
|
-
"apply": {
|
|
85
|
-
"edit-last-applied": {},
|
|
86
|
-
"set-last-applied": {},
|
|
87
|
-
"view-last-applied": {}
|
|
88
|
-
},
|
|
89
|
-
"patch": {},
|
|
90
|
-
"replace": {},
|
|
91
|
-
"wait": {},
|
|
92
|
-
"kustomize": {},
|
|
93
|
-
"label": {},
|
|
94
|
-
"annotate": {},
|
|
95
|
-
"completion": {},
|
|
96
|
-
"alpha": {
|
|
97
|
-
"kuberc": {
|
|
98
|
-
"set": {},
|
|
99
|
-
"view": {}
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
"api-resources": {},
|
|
103
|
-
"api-versions": {},
|
|
104
|
-
"config": {
|
|
105
|
-
"current-context": {},
|
|
106
|
-
"delete-cluster": {},
|
|
107
|
-
"delete-context": {},
|
|
108
|
-
"delete-user": {},
|
|
109
|
-
"get-clusters": {},
|
|
110
|
-
"get-contexts": {},
|
|
111
|
-
"get-users": {},
|
|
112
|
-
"rename-context": {},
|
|
113
|
-
"set": {},
|
|
114
|
-
"set-cluster": {},
|
|
115
|
-
"set-context": {},
|
|
116
|
-
"set-credentials": {},
|
|
117
|
-
"unset": {},
|
|
118
|
-
"use-context": {},
|
|
119
|
-
"view": {}
|
|
120
|
-
},
|
|
121
|
-
"plugin": {
|
|
122
|
-
"list": {}
|
|
123
|
-
},
|
|
124
|
-
"version": {}
|
|
125
|
-
}
|