cl-magic 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.
@@ -0,0 +1,236 @@
1
+ require 'json'
2
+ require 'tty-command'
3
+ require 'tty-prompt'
4
+ require 'cl/magic/common/logging.rb'
5
+ require 'cl/magic/common/parse_and_pick.rb'
6
+
7
+ #
8
+ # Kubectl config commands
9
+ #
10
+
11
+ def get_users_kube_config_cmd_path
12
+ tty_command = TTY::Command.new(printer: :null)
13
+ command = "grep cmd-path ~/.kube/config | head -n1 | cut -d ':' -f2"
14
+ out, err = tty_command.run(command)
15
+ return out.chomp.strip
16
+ end
17
+
18
+ #
19
+ # Kubectl DESCRIBE commands
20
+ #
21
+
22
+ def describe_pod(namespace, pod)
23
+ namespace_option = " --namespace=#{namespace}" if namespace
24
+ command = "kubectl get pod -ojson #{pod.first} #{namespace_option}"
25
+ @logger.wait command
26
+ result = TTY::Command.new(:printer => :null).run(command)
27
+ return JSON.parse(result.out)
28
+ end
29
+
30
+ def describe_deployment(namespace, deployment)
31
+ namespace_option = " --namespace=#{namespace}" if namespace
32
+ command = "kubectl get deployment.apps -ojson #{deployment.first} #{namespace_option}"
33
+ @logger.wait command
34
+ result = TTY::Command.new(:printer => :null).run(command)
35
+ return JSON.parse(result.out)
36
+ end
37
+
38
+ def describe_service(namespace, service)
39
+ namespace_option = " --namespace=#{namespace}" if namespace
40
+ command = "kubectl get service -ojson #{service.first} #{namespace_option}"
41
+ @logger.wait command
42
+ result = TTY::Command.new(:printer => :null).run(command)
43
+ return JSON.parse(result.out)
44
+ end
45
+
46
+ #
47
+ # Kubectl GET commands
48
+ #
49
+
50
+ def get_kubectl_deployments(namespace)
51
+ namespace_option = namespace ? " --namespace=#{namespace}" : " --all-namespaces"
52
+ command = "kubectl get -o=wide deployment.apps #{namespace_option}"
53
+ error_message = "No deployments found"
54
+ return parse_table_results(command, error_message)
55
+ end
56
+
57
+ def get_kubectl_pods(namespace, selector)
58
+ selector_option = " --selector=#{selector}" if selector
59
+ namespace_option = " --namespace=#{namespace}" if namespace
60
+ command = "kubectl get pods #{selector_option} #{namespace_option}"
61
+ error_message = "No pods found"
62
+ return parse_table_results(command, error_message)
63
+ end
64
+
65
+ def get_kubectl_services(namespace)
66
+ namespace_option = " --namespace=#{namespace}" if namespace
67
+ command = "kubectl get -o=wide services #{namespace_option}"
68
+ error_message = "Your kubectl has no services; talk to your team about this. :)"
69
+ return parse_table_results(command, error_message)
70
+ end
71
+
72
+ def get_kubectl_namespaces()
73
+ command = "kubectl get namespaces"
74
+ error_message = "No namespaces found in kubectl context: #{get_current_kubectl_context()}"
75
+ return parse_table_results(command, error_message)
76
+ end
77
+
78
+ def get_kubectl_contexts()
79
+ command = "kubectl config get-contexts"
80
+ error_message = "Your kubectl has no contexts; talk to your team about this. :)"
81
+ return parse_table_results(command, error_message)
82
+ end
83
+
84
+ def get_current_kubectl_context()
85
+ command = "kubectl config current-context"
86
+ return TTY::Command.new(:printer => :null).run(command).out.strip.chomp
87
+ end
88
+
89
+ def get_current_kubectl_namespace()
90
+ command = "kubectl config view --minify --output 'jsonpath={..namespace}'; echo"
91
+ return TTY::Command.new(:printer => :null).run(command).out.strip.chomp
92
+ end
93
+
94
+ def get_containers_on_pod(namespace, pod_name)
95
+ command = "kubectl get pods #{pod_name} -o jsonpath='{.spec.containers[*].name}' --namespace #{namespace}"
96
+ return TTY::Command.new(:printer => :null).run(command).out.strip.chomp.split(' ').collect {|v|[v]}
97
+ end
98
+
99
+ #
100
+ # Kubectl pickers
101
+ #
102
+
103
+ def pick_kubectl_context(selected_context=nil)
104
+
105
+ prev_context = get_current_kubectl_context()
106
+
107
+ # pick content
108
+ if selected_context.nil?
109
+ selected_context = TTY::Prompt.new(interrupt: :exit).select("Which context?", filter: true, per_page: 20) do |menu|
110
+ get_kubectl_contexts().each do |context|
111
+ if context[0]=="*"
112
+ menu.default context[1]
113
+ menu.choice context[1], context[1]
114
+ else
115
+ menu.choice context[0], context[0]
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ # change context
122
+ if selected_context != prev_context
123
+ command = "kubectl config use-context #{selected_context}"
124
+ @logger.wait command
125
+ TTY::Command.new(:printer => :null).run(command)
126
+ end
127
+
128
+ return selected_context
129
+ end
130
+
131
+ def pick_kubectl_deployment(options)
132
+ deploments = get_kubectl_deployments(options[:namespace]).collect {|o| o.values_at(0,-1)}
133
+ deployment = pick_single_result(deploments, "Which deployment?", options[:deployment_name])
134
+ if deployment.nil? or deployment.last == '<none>'
135
+ @logger.error "No deployments found"
136
+ exit(1)
137
+ end
138
+ return deployment
139
+ end
140
+
141
+ def pick_kubectl_namespace(options)
142
+ namespaces = get_kubectl_namespaces().collect {|o| o.values_at(0)}
143
+ current_namespace = get_current_kubectl_namespace()
144
+ return pick_single_result(namespaces, "Which namespace?", options[:namespace], current_namespace)
145
+ end
146
+
147
+ def pick_kubectl_service(options)
148
+ services = get_kubectl_services(options[:namespace]).collect {|o| o.values_at(0,-1)}
149
+ service = pick_single_result(services, "Pick service", options[:service_name])
150
+ if service.nil? or service.last == '<none>'
151
+ @logger.error "No services found"
152
+ exit(1)
153
+ end
154
+ return service
155
+ end
156
+
157
+ def pick_kubectl_pods(deployment, options)
158
+ selector = deployment.last # last field is a selector
159
+ pods = get_kubectl_pods(options[:namespace], selector)
160
+ selected_pods = pick_multiple_result(pods, "Which pods?", options[:pod_names])
161
+
162
+ if selected_pods.count == 0
163
+ @logger.warn "deployment #{deployment.last} has no pods."
164
+ exit()
165
+ end
166
+ return selected_pods
167
+ end
168
+
169
+ def pick_kubectl_pod(service, options)
170
+ selector = service.last # last field is a selector
171
+ pods = get_kubectl_pods(options[:namespace], selector)
172
+ selected_pod = pick_single_result(pods, "Which pods?", options[:pod_name])
173
+
174
+ if selected_pod.nil?
175
+ @logger.error "service #{service.first} has no pod."
176
+ exit(1)
177
+ end
178
+ return selected_pod
179
+ end
180
+
181
+ def pick_kubectl_container(pod, options)
182
+ containers = get_containers_on_pod(options[:namespace], pod.first)
183
+ container = pick_single_result(containers, "Pick container", options[:container_name])
184
+ options[:container_name] = container.first
185
+ end
186
+
187
+ def set_context_namespace_deployment_pod_container_options(options)
188
+ # set: context and namespace
189
+ set_context_and_namespace(options)
190
+
191
+ # pick: deployment and pod
192
+ deployment = pick_kubectl_deployment(options)
193
+ pod = pick_kubectl_pod(deployment, options)
194
+
195
+ # set options
196
+ options[:deployment_name] = deployment.first
197
+ options[:pod_name] = pod.first
198
+
199
+ # set container
200
+ pick_kubectl_container(pod, options)
201
+ end
202
+
203
+ def set_context_and_namespace(options)
204
+ get_ktx(options) if options[:ktx]
205
+
206
+ # set: context and namespace
207
+ if options[:kube_context].nil? and options[:namespace].nil?
208
+ options[:kube_context] = pick_kubectl_context(options[:kube_context]) if options[:kube_context].nil?
209
+ options[:namespace] = pick_kubectl_namespace(options).first if options[:namespace].nil? # just the name
210
+ end
211
+ end
212
+
213
+ #
214
+ # cl ktx
215
+ #
216
+ KTX_FILEPATH = '.cl-ktx.json'
217
+
218
+ def set_ktx(options)
219
+ File.open(KTX_FILEPATH, "w") do |f|
220
+ f.write({
221
+ kube_context: options[:kube_context],
222
+ namespace: options[:namespace],
223
+ }.to_json)
224
+ end
225
+ end
226
+
227
+ def get_ktx(options)
228
+ if options[:ktx]
229
+ if(File.exist?(KTX_FILEPATH))
230
+ file = File.read(KTX_FILEPATH)
231
+ ktx_hash = JSON.parse(file)
232
+ options[:kube_context] = ktx_hash['kube_context']
233
+ options[:namespace] = ktx_hash['namespace']
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,25 @@
1
+ require 'tty-logger'
2
+
3
+ def get_logger
4
+ return TTY::Logger.new do |config|
5
+ config.types = {
6
+ puts: {level: :info},
7
+ }
8
+ config.handlers = [
9
+ [:console, {
10
+ styles: {
11
+ puts: {
12
+ symbol: "",
13
+ label: "",
14
+ color: :black,
15
+ levelpad: 0
16
+ },
17
+ }
18
+ }]
19
+ ]
20
+ end
21
+ end
22
+
23
+ def write_history(message)
24
+ File.open("#{Dir.home}/.cl_history", "a") {|f| f.puts message }
25
+ end
@@ -0,0 +1,81 @@
1
+
2
+ #
3
+ # Parse table results
4
+ #
5
+ # Many commands return
6
+ # * a table of results
7
+ # * the first row being headers
8
+ # * each following row space separated values
9
+ #
10
+ # Lets assume this and parse the results
11
+ #
12
+
13
+ def parse_table_results(command, error_message, num_headers=1, split=nil, working_dir=nil)
14
+ results = []
15
+
16
+ @logger.puts ""
17
+ @logger.wait command
18
+
19
+ # working directory?
20
+ command = "cd #{working_dir} && #{command}" if working_dir
21
+
22
+ # run command
23
+ TTY::Command.new(:printer => :null).run(command).each { |line| results << line.split(split) }
24
+ num_headers.times { results.delete_at(0) } # remove header
25
+
26
+ if not results.any?
27
+ @logger.error error_message
28
+ exit(1)
29
+ end
30
+ return results
31
+ end
32
+
33
+
34
+ #
35
+ # Prompt the user to pick a single result
36
+ #
37
+ # results - and array of arrays
38
+ # prompt_message - the message for our picker
39
+ # exact - if provided, we'll return whatever matches this exactly
40
+ # default - if provided, the picker will default to this item
41
+ #
42
+
43
+ def pick_single_result(results, prompt_message, exact=nil, default=nil, selection_index=0)
44
+
45
+ # exact match?
46
+ results = results.select {|r| r[selection_index] == exact} if exact
47
+
48
+ case results.count
49
+ when 0 # no results?
50
+ return nil
51
+ when 1 # one result, just return it
52
+ return results.first
53
+ end
54
+
55
+ # prompt many results
56
+ selection = TTY::Prompt.new(interrupt: :exit).select(prompt_message, filter: true, per_page: 20) do |menu|
57
+ results.each do |result|
58
+ menu.default result.join(' | ') if result[selection_index] == default
59
+ menu.choice result.join(' | '), result[selection_index]
60
+ end
61
+ end
62
+ return results.select { |r| r[selection_index]==selection }.first
63
+ end
64
+
65
+
66
+ def pick_multiple_result(results, prompt_message, exacts=nil)
67
+
68
+ # exact match?
69
+ return results.select {|r| exacts.split(',').include? r.first} if exacts
70
+
71
+ # 0 or 1 result?
72
+ return results if results.count < 2
73
+
74
+ selections = TTY::Prompt.new(interrupt: :exit).multi_select(prompt_message, filter: true, per_page: 20) do |menu|
75
+ results.each do |result|
76
+ menu.choice result.join(' | '), result.first
77
+ end
78
+ end
79
+
80
+ return results.select {|r| selections.include? r.first}
81
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cl
4
+ module Magic
5
+ VERSION = "0.3.0"
6
+ end
7
+ end
data/lib/cl/magic.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "magic/version"
4
+
5
+ module Cl
6
+ module Magic
7
+ class Error < StandardError; end
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,180 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cl-magic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Don Najd
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-03-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: optparse-subcommand
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: tty-logger
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: tty-command
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: tty-prompt
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pastel
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: byebug
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: ''
112
+ email:
113
+ - dnajd7@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - Gemfile
119
+ - Gemfile.lock
120
+ - README.md
121
+ - bin/cl
122
+ - bin/console
123
+ - bin/setup
124
+ - cl-magic.gemspec
125
+ - lib/cl/magic.rb
126
+ - lib/cl/magic/cl
127
+ - lib/cl/magic/cl-auth
128
+ - lib/cl/magic/cl-aws-okta-auth
129
+ - lib/cl/magic/cl-aws-okta-env
130
+ - lib/cl/magic/cl-dk
131
+ - lib/cl/magic/cl-envkey
132
+ - lib/cl/magic/cl-gc-sql
133
+ - lib/cl/magic/cl-gc-tags
134
+ - lib/cl/magic/cl-glab-commit
135
+ - lib/cl/magic/cl-history
136
+ - lib/cl/magic/cl-kube-cp
137
+ - lib/cl/magic/cl-kube-deployment
138
+ - lib/cl/magic/cl-kube-ktx
139
+ - lib/cl/magic/cl-kube-logs
140
+ - lib/cl/magic/cl-kube-restart
141
+ - lib/cl/magic/cl-kube-search
142
+ - lib/cl/magic/cl-kube-search-all
143
+ - lib/cl/magic/cl-kube-ssh
144
+ - lib/cl/magic/cl-poll
145
+ - lib/cl/magic/cl-sandbox
146
+ - lib/cl/magic/cl-vault
147
+ - lib/cl/magic/common/common_options.rb
148
+ - lib/cl/magic/common/gcloud.rb
149
+ - lib/cl/magic/common/kubectl.rb
150
+ - lib/cl/magic/common/logging.rb
151
+ - lib/cl/magic/common/parse_and_pick.rb
152
+ - lib/cl/magic/version.rb
153
+ homepage: https://gitlab.com/beesbot/cl-magic
154
+ licenses:
155
+ - MIT
156
+ metadata:
157
+ allowed_push_host: https://rubygems.org/
158
+ homepage_uri: https://gitlab.com/beesbot/cl-magic
159
+ source_code_uri: https://gitlab.com/beesbot/cl-magic
160
+ changelog_uri: https://gitlab.com/beesbot/cl-magic
161
+ post_install_message:
162
+ rdoc_options: []
163
+ require_paths:
164
+ - lib
165
+ required_ruby_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: 2.6.0
170
+ required_rubygems_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ requirements: []
176
+ rubygems_version: 3.1.6
177
+ signing_key:
178
+ specification_version: 4
179
+ summary: Magic tools for a turnkey developer experience
180
+ test_files: []