cl-magic 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []