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.
- checksums.yaml +7 -0
- data/.rubocop.yml +121 -0
- data/.travis.yml +9 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +75 -0
- data/README.md +63 -0
- data/bin/cry +6 -0
- data/ccli.gemspec +24 -0
- data/lib/adapters/cluster_secret_adapter.rb +70 -0
- data/lib/adapters/cryptopus_adapter.rb +100 -0
- data/lib/adapters/k8s_adapter.rb +20 -0
- data/lib/adapters/ose_adapter.rb +20 -0
- data/lib/adapters/session_adapter.rb +74 -0
- data/lib/cli.rb +342 -0
- data/lib/errors.rb +40 -0
- data/lib/models/account.rb +44 -0
- data/lib/models/folder.rb +25 -0
- data/lib/models/k8s_secret.rb +11 -0
- data/lib/models/ose_secret.rb +28 -0
- data/lib/models/team.rb +45 -0
- data/lib/presenters/team_presenter.rb +18 -0
- data/lib/serializers/account_serializer.rb +57 -0
- data/lib/serializers/folder_serializer.rb +12 -0
- data/lib/serializers/ose_secret_serializer.rb +16 -0
- data/lib/serializers/team_serializer.rb +18 -0
- metadata +129 -0
@@ -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
|
data/lib/cli.rb
ADDED
@@ -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__
|