ccli 0.1.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,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-command'
4
+ require_relative './cluster_secret_adapter'
5
+
6
+ class K8SAdapter < ClusterSecretAdapter
7
+ private
8
+
9
+ def client
10
+ 'kubectl'
11
+ end
12
+
13
+ def client_missing_error
14
+ KubernetesClientMissingError
15
+ end
16
+
17
+ def client_not_logged_in_error
18
+ KubernetesClientNotLoggedInError
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-command'
4
+ require_relative './cluster_secret_adapter'
5
+
6
+ class OSEAdapter < ClusterSecretAdapter
7
+ private
8
+
9
+ def client
10
+ 'oc'
11
+ end
12
+
13
+ def client_missing_error
14
+ OpenshiftClientMissingError
15
+ end
16
+
17
+ def client_not_logged_in_error
18
+ OpenshiftClientNotLoggedInError
19
+ end
20
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'singleton'
5
+ require 'yaml'
6
+ require 'fileutils'
7
+ require 'psych'
8
+
9
+ class SessionAdapter
10
+
11
+ FILE_LOCATION = '~/.ccli/session'
12
+
13
+ def update_session(session)
14
+ session.merge!(session_data) { |_key, input| input } if session_file_exists?
15
+
16
+ FileUtils.mkdir_p ccli_directory_path unless ccli_directory_exists?
17
+ File.open(session_file_path, 'w') do |file|
18
+ session.merge!(extracted_token(session[:encoded_token])) { |_key, _v1, v2| v2 }
19
+ session.delete(:encoded_token)
20
+ file.write session.to_yaml
21
+ end
22
+ end
23
+
24
+ def session_data
25
+ raise SessionMissingError unless session_file_exists?
26
+
27
+ @session_data ||= Psych.load_file(session_file_path)
28
+ end
29
+
30
+ def clear_session
31
+ return unless ccli_directory_exists?
32
+
33
+ FileUtils.rm_r(ccli_directory_path)
34
+ end
35
+
36
+ def selected_folder
37
+ @selected_folder ||= Folder.find(selected_folder_id)
38
+ end
39
+
40
+ private
41
+
42
+ def selected_folder_id
43
+ raise NoFolderSelectedError if session_data[:folder].nil?
44
+
45
+ session_data[:folder]
46
+ end
47
+
48
+ def extracted_token(token)
49
+ return {} unless token
50
+
51
+ decoded_token = Base64.decode64(token)
52
+ attrs = decoded_token.split(':')
53
+ {
54
+ username: attrs[0],
55
+ token: attrs[1]
56
+ }
57
+ end
58
+
59
+ def session_file_path
60
+ File.expand_path(FILE_LOCATION)
61
+ end
62
+
63
+ def ccli_directory_path
64
+ File.dirname(session_file_path)
65
+ end
66
+
67
+ def session_file_exists?
68
+ File.exist?(session_file_path)
69
+ end
70
+
71
+ def ccli_directory_exists?
72
+ Dir.exist?(ccli_directory_path)
73
+ end
74
+ end
@@ -0,0 +1,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require 'commander'
5
+ require 'tty-exit'
6
+ require 'tty-logger'
7
+
8
+ Dir[File.join(__dir__, '**', '*.rb')].sort.each { |file| require file }
9
+
10
+ # rubocop:disable Metrics/ClassLength
11
+ class CLI
12
+ include Commander::Methods
13
+
14
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metric/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength
15
+ def run
16
+ program :name, 'cry - cryptopus cli'
17
+ program :version, '1.0.0'
18
+ program :description, 'CLI tool to manage Openshift Secrets via Cryptopus'
19
+ program :help, 'Source Code', 'https://www.github.com/puzzle/ccli'
20
+ program :help, 'Usage', 'cry [flags]'
21
+
22
+ command :login do |c|
23
+ c.syntax = 'cry login <credentials>'
24
+ c.description = 'Logs in to the ccli'
25
+
26
+ c.action do |args|
27
+ token, url = extract_login_args(args)
28
+ execute_action do
29
+ session_adapter.update_session({ encoded_token: token, url: url })
30
+ renew_auth_token
31
+
32
+ # Test authentification by calling teams endpoint
33
+ Team.all
34
+
35
+ log_success 'Successfully logged in'
36
+ end
37
+ end
38
+ end
39
+
40
+ command :logout do |c|
41
+ c.syntax = 'cry logout'
42
+ c.description = 'Logs out of the ccli'
43
+
44
+ c.action do
45
+ execute_action do
46
+ session_adapter.clear_session
47
+ log_success 'Successfully logged out'
48
+ end
49
+ end
50
+ end
51
+
52
+ command :account do |c|
53
+ c.syntax = 'cry account <id> [options]'
54
+ c.description = 'Fetches an account by the given id'
55
+ c.option '--username', String, 'Only show the username of the user'
56
+ c.option '--password', String, 'Only show the password of the user'
57
+
58
+ c.action do |args, options|
59
+ exit_with_error(:usage_error, 'id missing') if args.empty?
60
+ execute_action do
61
+ logger.info 'Fetching account...'
62
+ account = Account.find(args.first)
63
+ out = account.username if options.username
64
+ out = account.password if options.password
65
+ puts out || account.to_yaml
66
+ end
67
+ end
68
+ end
69
+
70
+ command :folder do |c|
71
+ c.syntax = 'cry folder <id>'
72
+ c.description = 'Selects the Cryptopus folder by id'
73
+
74
+ c.action do |args|
75
+ id = args.first
76
+ exit_with_error(:usage_error, 'id missing') unless id
77
+ exit_with_error(:usage_error, 'id invalid') unless id.match?(/(^\d{1,10}$)/)
78
+
79
+ execute_action do
80
+ session_adapter.update_session({ folder: id })
81
+
82
+ log_success "Selected Folder with id: #{id}"
83
+ end
84
+ end
85
+ end
86
+
87
+ command :'ose-secret-pull' do |c|
88
+ c.syntax = 'cry ose-secret-pull <secret-name>'
89
+ c.summary = 'Pulls secret from Openshift to Cryptopus'
90
+ c.description = "Pulls the Secret from Openshift and pushes them to Cryptopus.\n" \
91
+ 'If a Cryptopus Account in the selected folder using the name ' \
92
+ "of the given secret is already present, it will be updated accordingly.\n" \
93
+ 'If no name is given, it will pull all secrets inside the selected project.'
94
+
95
+ c.action do |args|
96
+ if args.length > 1
97
+ exit_with_error(:usage_error,
98
+ 'Only a single or no arguments are allowed')
99
+ end
100
+
101
+ execute_action({ secret_name: args.first }) do
102
+ if args.empty?
103
+ logger.info 'Fetching secrets...'
104
+ OSESecret.all.each do |secret|
105
+ logger.info "Saving secret #{secret.name}..."
106
+ cryptopus_adapter.save_secret(secret)
107
+ log_success "Saved secret #{secret.name} in Cryptopus"
108
+ end
109
+ elsif args.length == 1
110
+ logger.info "Saving secret #{args.first}..."
111
+ cryptopus_adapter.save_secret(OSESecret.find_by_name(args.first))
112
+ log_success "Saved secret #{args.first} in Cryptopus"
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ command :'ose-secret-push' do |c|
119
+ c.syntax = 'cry ose-secret-push <secret-name>'
120
+ c.summary = 'Pushes secret from Cryptopus to Openshift'
121
+ c.description = 'Pushes the Secret to Openshift by retrieving it from Cryptopus first. ' \
122
+ 'If a Secret in the selected Openshift project using the name ' \
123
+ 'of the given accountname is already present, it will be updated accordingly.'
124
+
125
+ c.action do |args|
126
+ secret_name = args.first
127
+ exit_with_error(:usage_error, 'Only one secret can be pushed') if args.length > 1
128
+ execute_action({ secret_name: secret_name }) do
129
+ secret_accounts = if secret_name.nil?
130
+ logger.info 'Fetching all accounts in folder...'
131
+ session_adapter.selected_folder.accounts
132
+ else
133
+ logger.info "Fetching account #{secret_name}..."
134
+ [cryptopus_adapter.find_account_by_name(secret_name)]
135
+ end
136
+ secret_accounts.each do |account|
137
+ logger.info "Fetching secret #{account.accountname}..."
138
+ secret_account = Account.find(account.id)
139
+ logger.info "Inserting secret #{account.accountname}..."
140
+ ose_adapter.insert_secret(secret_account.to_osesecret)
141
+ log_success "Secret #{secret_account.accountname} was successfully applied"
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ command :'k8s-secret-pull' do |c|
148
+ c.syntax = 'cry k8s-secret-pull <secret-name>'
149
+ c.summary = 'Pulls secret from Kubectl to Cryptopus'
150
+ c.description = "Pulls the Secret from Kubectl and pushes them to Cryptopus.\n" \
151
+ 'If a Cryptopus Account in the selected folder using the name ' \
152
+ "of the given secret is already present, it will be updated accordingly.\n" \
153
+ 'If no name is given, it will pull all secrets inside the selected project.'
154
+
155
+ c.action do |args|
156
+ if args.length > 1
157
+ TTY::Exit.exit_with(:usage_error,
158
+ 'Only a single or no arguments are allowed')
159
+ end
160
+
161
+ execute_action({ secret_name: args.first }) do
162
+ if args.empty?
163
+ logger.info 'Fetching secrets...'
164
+ K8SSecret.all.each do |secret|
165
+ logger.info "Saving secret #{secret.name}..."
166
+ cryptopus_adapter.save_secret(secret)
167
+ log_success "Saved secret #{secret.name} in Cryptopus"
168
+ end
169
+ elsif args.length == 1
170
+ logger.info "Saving secret #{args.first}..."
171
+ cryptopus_adapter.save_secret(K8SSecret.find_by_name(args.first))
172
+ log_success "Saved secret #{args.first} in Cryptopus"
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ command :'k8s-secret-push' do |c|
179
+ c.syntax = 'cry k8s-secret-push <secret-name>'
180
+ c.summary = 'Pushes secret from Cryptopus to Kubectl'
181
+ c.description = 'Pushes the Secret to Kubectl by retrieving it from Cryptopus first. ' \
182
+ 'If a Secret in the selected Kubectl project using the name ' \
183
+ 'of the given accountname is already present, it will be updated accordingly.'
184
+
185
+ c.action do |args|
186
+ secret_name = args.first
187
+ exit_with_error(:usage_error, 'Only one secret can be pushed') if args.length > 1
188
+ execute_action({ secret_name: secret_name }) do
189
+ secret_accounts = if secret_name.nil?
190
+ logger.info 'Fetching all accounts in folder...'
191
+ session_adapter.selected_folder.accounts
192
+ else
193
+ logger.info "Fetching account #{secret_name}..."
194
+ [cryptopus_adapter.find_account_by_name(secret_name)]
195
+ end
196
+ secret_accounts.each do |account|
197
+ logger.info "Fetching secret #{account.accountname}..."
198
+ secret_account = Account.find(account.id)
199
+ logger.info "Inserting secret #{account.accountname}..."
200
+ k8s_adapter.insert_secret(secret_account.to_osesecret)
201
+ log_success "Secret #{secret_account.accountname} was successfully applied"
202
+ end
203
+ end
204
+ end
205
+ end
206
+
207
+ command :teams do |c|
208
+ c.syntax = 'cry teams'
209
+ c.description = 'Lists all available teams'
210
+
211
+ c.action do
212
+ execute_action do
213
+ logger.info 'Fetching teams...'
214
+ teams = Team.all
215
+ output = teams.map(&:render_list).join("\n")
216
+ puts output
217
+ end
218
+ end
219
+ end
220
+
221
+ command :use do |c|
222
+ c.syntax = 'cry use <team/folder>'
223
+ c.description = 'Select the current folder'
224
+
225
+ c.action do |args|
226
+ team_name, folder_name = extract_use_args(args)
227
+ execute_action({ team_name: team_name, folder_name: folder_name }) do
228
+ logger.info "Looking for team #{team_name}..."
229
+ selected_team = Team.find_by_name(team_name)
230
+ raise TeamNotFoundError unless selected_team
231
+
232
+ logger.info "Looking for folder #{folder_name}..."
233
+ selected_folder = selected_team.folder_by_name(folder_name)
234
+ raise FolderNotFoundError unless selected_folder
235
+
236
+ session_adapter.update_session({ folder: selected_folder.id })
237
+ log_success "Selected folder #{folder_name.downcase} in team #{team_name.downcase}"
238
+ end
239
+ end
240
+ end
241
+
242
+ run!
243
+ end
244
+
245
+ private
246
+
247
+ def execute_action(options = {})
248
+ yield if block_given?
249
+ rescue SessionMissingError
250
+ exit_with_error(:usage_error, 'Not logged in')
251
+ rescue UnauthorizedError
252
+ exit_with_error(:usage_error, 'Authorization failed')
253
+ rescue ForbiddenError
254
+ exit_with_error(:usage_error, 'Access denied')
255
+ rescue SocketError
256
+ exit_with_error(:usage_error, 'Could not connect')
257
+ rescue NoFolderSelectedError
258
+ exit_with_error(:usage_error, 'Folder must be selected using cry folder <id>')
259
+ rescue OpenshiftClientMissingError
260
+ exit_with_error(:usage_error, 'oc is not installed')
261
+ rescue OpenshiftClientNotLoggedInError
262
+ exit_with_error(:usage_error, 'oc is not logged in')
263
+ rescue KubernetesClientMissingError
264
+ exit_with_error(:usage_error, 'kubectl is not installed')
265
+ rescue KubernetesClientNotLoggedInError
266
+ exit_with_error(:usage_error, 'kubectl is not logged in')
267
+ rescue CryptopusAccountNotFoundError
268
+ exit_with_error(:usage_error, 'Secret with the given name ' \
269
+ "#{options[:secret_name]} was not found")
270
+ rescue OpenshiftSecretNotFoundError
271
+ exit_with_error(:usage_error, 'Secret with the given name ' \
272
+ "#{options[:secret_name]} was not found")
273
+ rescue TeamNotFoundError
274
+ exit_with_error(:usage_error, 'Team with the given name ' \
275
+ "#{options[:team_name]} was not found")
276
+ rescue FolderNotFoundError
277
+ exit_with_error(:usage_error, 'Folder with the given name ' \
278
+ "#{options[:folder_name]} was not found")
279
+ end
280
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metric/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength
281
+
282
+
283
+ def extract_login_args(args)
284
+ exit_with_error(:usage_error, 'Credentials missing') if args.empty?
285
+ token, url = args.first.split('@')
286
+ exit_with_error(:usage_error, 'URL missing') unless url
287
+ exit_with_error(:usage_error, 'Token missing') if token.empty?
288
+ [token, url]
289
+ end
290
+
291
+ def extract_use_args(args)
292
+ usage_info = 'Usage: cry use <team/folder>'
293
+
294
+ exit_with_error(:usage_error, "Arguments missing\n#{usage_info}") unless args.length >= 1
295
+ team_name, folder_name = args.first.split('/').map(&:downcase)
296
+ exit_with_error(:usage_error, "Team name is missing\n#{usage_info}") if team_name.empty?
297
+ exit_with_error(:usage_error, "Folder name is missing\n#{usage_info}") unless folder_name
298
+ [team_name, folder_name]
299
+ end
300
+
301
+ def exit_with_error(error, msg)
302
+ logger = TTY::Logger.new do |config|
303
+ config.output = $stderr
304
+ end
305
+ logger.error(msg)
306
+ TTY::Exit.exit_with(error)
307
+ end
308
+
309
+ def log_success(msg)
310
+ logger = TTY::Logger.new do |config|
311
+ config.output = $stdout
312
+ end
313
+ logger.success msg
314
+ end
315
+
316
+ def logger
317
+ @logger ||= TTY::Logger.new
318
+ end
319
+
320
+ def ose_adapter
321
+ @ose_adapter ||= OSEAdapter.new
322
+ end
323
+
324
+ def cryptopus_adapter
325
+ @cryptopus_adapter ||= CryptopusAdapter.new
326
+ end
327
+
328
+ def session_adapter
329
+ @session_adapter ||= SessionAdapter.new
330
+ end
331
+
332
+ def k8s_adapter
333
+ @k8s_adapter ||= K8SAdapter.new
334
+ end
335
+
336
+ def renew_auth_token
337
+ session_adapter.update_session({ token: cryptopus_adapter.renewed_auth_token })
338
+ end
339
+ end
340
+ # rubocop:enable Metrics/ClassLength
341
+
342
+ CLI.new.run if $PROGRAM_NAME == __FILE__