kube_kubectl 0.2.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8d33ec25c06aef9931a7b340e17de2920f4cdb8675507c74516d0d3d152e8f4
4
- data.tar.gz: 2ccfb320c419f49a220781e402b2d146a94436c8376d4945a24cdc09958965ad
3
+ metadata.gz: 7c1f62f75309c836223f481c105768b6e67fd2bfc4533c80e7aab9116da287b9
4
+ data.tar.gz: c0c8d859157bd69147c6b0f56b049a1a344a36d1172cbe090ba7a70f4561d2b4
5
5
  SHA512:
6
- metadata.gz: 7a82e93491a8f911a898b3fd276ac617a37d3d793ca143f27c1a35a6296ff33823e698591a55cf0716fa055a9db13ed5e491431c4f94bc793e6a855a20aeedfa
7
- data.tar.gz: 79ccfcba0a85fda3536b74962dc9cb3bf66c3884e927aa820f7da76caeba815998ddeb703128d543d14683358fddf2aeed5a7fddf5f4a649147fadae77fc36cf
6
+ metadata.gz: 8fbc323f909aa80047d0a125d730703b9908a06f7cfa7dc90002bcdf6c222da123e6c7d8ce55610011357f33ec835b607a10696ddceba0681231d7156becb015
7
+ data.tar.gz: ee25e1d7f70d8029477143e12a7b05b19cc400b2b7fc6a5cf17def7caffe6a5c3966b6962bf0dd8d690b6eeb97fa27e1d2947f9fa06bfcf652543123e2b446b4
@@ -0,0 +1,125 @@
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
+ }
data/kube_ctl.gemspec CHANGED
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.metadata['documentation_uri'] = spec.homepage
24
24
  spec.metadata['rubygems_mfa_required'] = 'true'
25
25
 
26
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|data)/}) }
26
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
27
  spec.bindir = 'exe'
28
28
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
29
  spec.require_paths = ['lib']
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kube
4
+ module Ctl
5
+ class CommandNode
6
+ include Enumerable
7
+
8
+ BLANK_DATA = { commands: [], resource: nil, args: [], flags: {} }.freeze
9
+
10
+ attr_reader :command_data
11
+
12
+ def initialize(current_node:, command_data: BLANK_DATA)
13
+ @current_node = current_node
14
+ @command_data = command_data
15
+ end
16
+
17
+ def flag(key, value = nil)
18
+ self.class.new(
19
+ current_node: @current_node,
20
+ command_data: @command_data.merge(
21
+ flags: @command_data[:flags].merge(key.to_s.tr('_', '-') => value)
22
+ )
23
+ )
24
+ end
25
+
26
+ def each(&block)
27
+ to_a.each(&block)
28
+ end
29
+
30
+ def to_a
31
+ QueryBuilder.new(@command_data).to_a
32
+ end
33
+
34
+ def to_s
35
+ QueryBuilder.new(@command_data).to_s
36
+ end
37
+
38
+ def inspect
39
+ "#<#{self.class.name} #{self}>"
40
+ end
41
+
42
+ def method_missing(name, *args, &block)
43
+ segment = name.to_s.tr('_', '-')
44
+ child = @current_node.children[segment]
45
+
46
+ if child&.command?
47
+ self.class.new(
48
+ current_node: child,
49
+ command_data: @command_data.merge(
50
+ commands: @command_data[:commands] + [segment],
51
+ args: @command_data[:args] + args.map(&:to_s)
52
+ )
53
+ )
54
+ elsif child&.resource?
55
+ self.class.new(
56
+ current_node: child,
57
+ command_data: @command_data.merge(
58
+ resource: (@command_data[:resource] || ResourceSelector.new) + [segment],
59
+ args: @command_data[:args] + args.map(&:to_s)
60
+ )
61
+ )
62
+ elsif @current_node.resource? || @current_node.children.empty?
63
+ # Leaf command or already in resource mode — free-form resource segment
64
+ self.class.new(
65
+ current_node: TreeNode.new(name: segment, type: :resource),
66
+ command_data: @command_data.merge(
67
+ resource: (@command_data[:resource] || ResourceSelector.new) + [segment],
68
+ args: @command_data[:args] + args.map(&:to_s)
69
+ )
70
+ )
71
+ elsif Enumerable.method_defined?(name)
72
+ to_a.public_send(name, *args, &block)
73
+ else
74
+ super
75
+ end
76
+ end
77
+
78
+ def respond_to_missing?(name, include_private = false)
79
+ segment = name.to_s.tr('_', '-')
80
+
81
+ @current_node.children.key?(segment) ||
82
+ @current_node.resource? ||
83
+ @current_node.children.empty? ||
84
+ Enumerable.method_defined?(name) ||
85
+ super
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Kube
6
+ module Ctl
7
+ class Instance
8
+ GEM_ROOT = File.expand_path('../../..', __dir__)
9
+
10
+ COMMAND_TREE = JSON.parse(
11
+ File.read(File.join(GEM_ROOT, 'data', 'kubectl-command-tree-v1-minimal.json'))
12
+ )
13
+
14
+ ROOT = TreeNode.new(
15
+ name: 'kubectl',
16
+ type: :command,
17
+ children: TreeNode.build(COMMAND_TREE)
18
+ )
19
+
20
+ attr_reader :kubeconfig
21
+
22
+ # When kubeconfig is nil, no --kubeconfig flag is passed and
23
+ # kubectl falls back to its own default (~/.kube/config).
24
+ def initialize(kubeconfig: ENV['KUBECONFIG'])
25
+ @kubeconfig = kubeconfig
26
+ end
27
+
28
+ def method_missing(name, *args, &block)
29
+ root_node.public_send(name, *args, &block)
30
+ end
31
+
32
+ def respond_to_missing?(name, include_private = false)
33
+ root_node.respond_to?(name) || super
34
+ end
35
+
36
+ def inspect
37
+ "#<#{self.class.name} kubeconfig=#{@kubeconfig.inspect}>"
38
+ end
39
+
40
+ private
41
+
42
+ def root_node
43
+ CommandNode.new(current_node: ROOT, command_data: initial_command_data)
44
+ end
45
+
46
+ def initial_command_data
47
+ CommandNode::BLANK_DATA.merge(flags: initial_flags)
48
+ end
49
+
50
+ def initial_flags
51
+ @kubeconfig ? { 'kubeconfig' => @kubeconfig } : {}
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Kube
6
+ module Ctl
7
+ class QueryBuilder
8
+ def initialize(command_data)
9
+ @commands = command_data[:commands] || []
10
+ @resource = command_data[:resource]
11
+ @args = command_data[:args] || []
12
+ @flags = command_data[:flags] || {}
13
+ end
14
+
15
+ def to_s
16
+ ['kubectl', *@commands, *resource_arg, *@args, *rendered_flags].join(' ')
17
+ end
18
+
19
+ def to_a
20
+ stdout, _status = Open3.capture2('kubectl', *@commands, *resource_arg, *@args, *rendered_flags)
21
+ stdout.lines.map(&:chomp)
22
+ end
23
+
24
+ private
25
+
26
+ def resource_arg
27
+ @resource ? [@resource.to_s] : []
28
+ end
29
+
30
+ def rendered_flags
31
+ @flags.flat_map { |k, v| v.nil? ? ["--#{k}"] : ["--#{k}", v.to_s] }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kube
4
+ module Ctl
5
+ class ResourceSelector < Array
6
+ def +(other)
7
+ self.class.new(super)
8
+ end
9
+
10
+ def to_s
11
+ join('.')
12
+ end
13
+
14
+ def to_regex
15
+ Regexp.new(map { |s| Regexp.escape(s) }.join('.*\.'))
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kube
4
+ module Ctl
5
+ class TreeNode
6
+ # Commands whose leaf children are resource types.
7
+ # Non-leaf children (those with their own subtree) remain commands.
8
+ RESOURCE_PARENTS = %w[
9
+ create
10
+ create/secret
11
+ create/service
12
+ top
13
+ ].freeze
14
+
15
+ attr_reader :name, :type, :children
16
+
17
+ def initialize(name:, type:, children: {})
18
+ @name = name.freeze
19
+ @type = type
20
+ @children = children.freeze
21
+ end
22
+
23
+ def command? = type == :command
24
+ def resource? = type == :resource
25
+
26
+ # Recursively build a children hash of TreeNodes from the parsed JSON.
27
+ # parent_path tracks position in the tree to determine child types.
28
+ def self.build(hash, parent_path: nil)
29
+ hash.each_with_object({}) do |(key, subtree), children|
30
+ name = key.to_s
31
+ current_path = [parent_path, name].compact.join('/')
32
+ leaf = subtree.empty?
33
+
34
+ # Under a resource parent, leaf nodes are resources, non-leaves are commands.
35
+ # Everywhere else, everything is a command.
36
+ child_type = if RESOURCE_PARENTS.include?(parent_path.to_s) && leaf
37
+ :resource
38
+ else
39
+ :command
40
+ end
41
+
42
+ children[name] = new(
43
+ name: name,
44
+ type: child_type,
45
+ children: build(subtree, parent_path: current_path)
46
+ )
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kube
4
4
  module Ctl
5
- VERSION = "0.2.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
data/lib/kube/ctl.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "ctl/version"
3
+ require_relative 'ctl/version'
4
+ require_relative 'ctl/tree_node'
5
+ require_relative 'ctl/resource_selector'
6
+ require_relative 'ctl/query_builder'
7
+ require_relative 'ctl/command_node'
8
+ require_relative 'ctl/instance'
4
9
 
5
10
  module Kube
6
11
  module Ctl
data/lib/kube.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'kube/ctl'
4
+
5
+ module Kube
6
+ def self.ctl(kubeconfig: ENV['KUBECONFIG'])
7
+ Kube::Ctl::Instance.new(kubeconfig: kubeconfig)
8
+ end
9
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kube_kubectl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan K
@@ -77,11 +77,18 @@ files:
77
77
  - bin/setup
78
78
  - bin/tag-version
79
79
  - bin/test
80
+ - data/kubectl-command-tree-v1-minimal.json
80
81
  - exe/kube_ctl
81
82
  - flake.nix
82
83
  - kube_ctl.gemspec
83
84
  - kube_ctl.gemspec.erb
85
+ - lib/kube.rb
84
86
  - lib/kube/ctl.rb
87
+ - lib/kube/ctl/command_node.rb
88
+ - lib/kube/ctl/instance.rb
89
+ - lib/kube/ctl/query_builder.rb
90
+ - lib/kube/ctl/resource_selector.rb
91
+ - lib/kube/ctl/tree_node.rb
85
92
  - lib/kube/ctl/version.rb
86
93
  - lib/kube/ctl/version.rb.erb
87
94
  homepage: https://github.com/general-intelligence-systems/kube_ctl