MuranoCLI 3.2.0.beta.1 → 3.2.0.beta.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/.trustme.plugin +137 -0
  4. data/.trustme.sh +217 -117
  5. data/.trustme.vim +9 -3
  6. data/Gemfile +9 -3
  7. data/MuranoCLI.gemspec +8 -5
  8. data/Rakefile +1 -0
  9. data/dockers/Dockerfile.2.2.9 +6 -3
  10. data/dockers/Dockerfile.2.3.6 +6 -3
  11. data/dockers/Dockerfile.2.4.3 +6 -3
  12. data/dockers/Dockerfile.2.5.0 +6 -3
  13. data/dockers/Dockerfile.GemRelease +10 -8
  14. data/dockers/Dockerfile.m4 +23 -5
  15. data/dockers/docker-test.sh +65 -28
  16. data/docs/completions/murano_completion-bash +751 -57
  17. data/docs/develop.rst +10 -9
  18. data/lib/MrMurano/AccountBase.rb +95 -6
  19. data/lib/MrMurano/Commander-Entry.rb +9 -4
  20. data/lib/MrMurano/Config-Migrate.rb +2 -0
  21. data/lib/MrMurano/Config.rb +94 -26
  22. data/lib/MrMurano/Content.rb +1 -1
  23. data/lib/MrMurano/Exchange.rb +77 -42
  24. data/lib/MrMurano/Gateway.rb +1 -1
  25. data/lib/MrMurano/HttpAuthed.rb +20 -7
  26. data/lib/MrMurano/Logs.rb +10 -1
  27. data/lib/MrMurano/ProjectFile.rb +1 -1
  28. data/lib/MrMurano/ReCommander.rb +129 -73
  29. data/lib/MrMurano/Solution-ServiceConfig.rb +18 -11
  30. data/lib/MrMurano/Solution-Services.rb +78 -50
  31. data/lib/MrMurano/Solution-Users.rb +1 -1
  32. data/lib/MrMurano/Solution.rb +13 -63
  33. data/lib/MrMurano/SyncUpDown-Core.rb +185 -77
  34. data/lib/MrMurano/SyncUpDown-Item.rb +29 -4
  35. data/lib/MrMurano/SyncUpDown.rb +11 -11
  36. data/lib/MrMurano/Webservice-Cors.rb +1 -1
  37. data/lib/MrMurano/Webservice-Endpoint.rb +28 -17
  38. data/lib/MrMurano/Webservice-File.rb +103 -43
  39. data/lib/MrMurano/commands/domain.rb +1 -0
  40. data/lib/MrMurano/commands/element.rb +585 -0
  41. data/lib/MrMurano/commands/exchange.rb +211 -204
  42. data/lib/MrMurano/commands/gb.rb +1 -0
  43. data/lib/MrMurano/commands/globals.rb +17 -7
  44. data/lib/MrMurano/commands/init.rb +115 -101
  45. data/lib/MrMurano/commands/keystore.rb +1 -1
  46. data/lib/MrMurano/commands/logs.rb +2 -1
  47. data/lib/MrMurano/commands/postgresql.rb +17 -7
  48. data/lib/MrMurano/commands/service.rb +572 -0
  49. data/lib/MrMurano/commands/show.rb +7 -3
  50. data/lib/MrMurano/commands/solution.rb +2 -1
  51. data/lib/MrMurano/commands/solution_picker.rb +31 -15
  52. data/lib/MrMurano/commands/status.rb +205 -169
  53. data/lib/MrMurano/commands/sync.rb +70 -38
  54. data/lib/MrMurano/commands/token.rb +59 -14
  55. data/lib/MrMurano/commands/usage.rb +1 -0
  56. data/lib/MrMurano/commands.rb +2 -0
  57. data/lib/MrMurano/hash.rb +91 -0
  58. data/lib/MrMurano/http.rb +55 -6
  59. data/lib/MrMurano/makePretty.rb +47 -0
  60. data/lib/MrMurano/optparse.rb +60 -45
  61. data/lib/MrMurano/variegated/TruthyFalsey.rb +48 -0
  62. data/lib/MrMurano/variegated/ruby_dig.rb +64 -0
  63. data/lib/MrMurano/verbosing.rb +113 -3
  64. data/lib/MrMurano/version.rb +1 -1
  65. data/spec/Account_spec.rb +34 -20
  66. data/spec/Business_spec.rb +12 -9
  67. data/spec/Config_spec.rb +7 -1
  68. data/spec/Content_spec.rb +17 -1
  69. data/spec/GatewayBase_spec.rb +5 -2
  70. data/spec/GatewayDevice_spec.rb +4 -2
  71. data/spec/GatewayResource_spec.rb +4 -1
  72. data/spec/GatewaySettings_spec.rb +4 -1
  73. data/spec/HttpAuthed_spec.rb +73 -0
  74. data/spec/Http_spec.rb +32 -35
  75. data/spec/ProjectFile_spec.rb +1 -1
  76. data/spec/Solution-ServiceConfig_spec.rb +4 -1
  77. data/spec/Solution-ServiceEventHandler_spec.rb +6 -3
  78. data/spec/Solution-ServiceModules_spec.rb +4 -1
  79. data/spec/Solution-UsersRoles_spec.rb +4 -1
  80. data/spec/Solution_spec.rb +4 -1
  81. data/spec/SyncUpDown_spec.rb +1 -1
  82. data/spec/Webservice-Cors_spec.rb +4 -1
  83. data/spec/Webservice-Endpoint_spec.rb +9 -6
  84. data/spec/Webservice-File_spec.rb +17 -4
  85. data/spec/Webservice-Setting_spec.rb +6 -2
  86. data/spec/_workspace.rb +2 -0
  87. data/spec/cmd_common.rb +42 -13
  88. data/spec/cmd_content_spec.rb +17 -7
  89. data/spec/cmd_device_spec.rb +1 -1
  90. data/spec/cmd_domain_spec.rb +2 -2
  91. data/spec/cmd_element_spec.rb +400 -0
  92. data/spec/cmd_exchange_spec.rb +2 -2
  93. data/spec/cmd_init_spec.rb +59 -25
  94. data/spec/cmd_keystore_spec.rb +6 -3
  95. data/spec/cmd_link_spec.rb +10 -5
  96. data/spec/cmd_logs_spec.rb +1 -1
  97. data/spec/cmd_setting_application_spec.rb +18 -15
  98. data/spec/cmd_setting_product_spec.rb +7 -7
  99. data/spec/cmd_status_spec.rb +27 -17
  100. data/spec/cmd_syncdown_application_spec.rb +30 -3
  101. data/spec/cmd_syncdown_both_spec.rb +72 -18
  102. data/spec/cmd_syncup_spec.rb +71 -5
  103. data/spec/cmd_token_spec.rb +2 -2
  104. data/spec/cmd_usage_spec.rb +2 -2
  105. data/spec/dry_run_formatter.rb +27 -0
  106. data/spec/fixtures/dumped_config +8 -0
  107. data/spec/fixtures/exchange_element/element-show.json +1 -0
  108. data/spec/fixtures/exchange_element/swagger-mur-6407__10k.yaml +282 -0
  109. data/spec/fixtures/exchange_element/swagger-mur-6407__20k.yaml +588 -0
  110. data/spec/variegated_TruthyFalsey_spec.rb +29 -0
  111. metadata +51 -25
@@ -0,0 +1,572 @@
1
+ # Copyright © 2016-2017 Exosite LLC. All Rights Reserved
2
+ # License: PROPRIETARY. See LICENSE.txt.
3
+ # frozen_string_literal: true
4
+
5
+ # vim:tw=0:ts=2:sw=2:et:ai
6
+ # Unauthorized copying of this file is strictly prohibited.
7
+
8
+ require 'ostruct'
9
+ require 'time'
10
+ require 'MrMurano/makePretty'
11
+ require 'MrMurano/verbosing'
12
+ require 'MrMurano/Exchange'
13
+ require 'MrMurano/ReCommander'
14
+ require 'MrMurano/Solution-ServiceConfig'
15
+ require 'MrMurano/SubCmdGroupContext'
16
+
17
+ # *** A `service` command helper class.
18
+
19
+ class ServiceConsolidator
20
+ include MrMurano::Verbose
21
+
22
+ def initialize(options)
23
+ @include_all = options.show_all
24
+ @abbrev_desc = options.abbrev_desc
25
+ @include_type = options.show_type
26
+ @exclude_added = options.hide_added
27
+ @exclude_avail = options.only_added
28
+ end
29
+
30
+ # SYNC_ME: See Yeti UI's removeServiceIconBlacklist.
31
+ SERVICE_BLACKLIST = {
32
+ asset: true,
33
+ device: true,
34
+ device2: true,
35
+ gateway: true,
36
+ products: true,
37
+ }.freeze
38
+
39
+ # NOTE:
40
+ # svc_cfgs is from /solution/<sid>/serviceconfig/
41
+ # svc_defs is from /solution/<sid>/service/
42
+ # purchased is from /exchange/<bid>/purchased/
43
+ def reconcile_services(svc_cfgs, svc_defs, purchased)
44
+ # The 3 API calls have 3 distinct responses, which we transform into 1.
45
+ sol_services = {}
46
+ # Add Exchange purchases to the list first.
47
+ add_simplify_svc_exchange(sol_services, purchased)
48
+ # Next add Platforms services, and possibly update sol_services.
49
+ svc_standard = services_from_definitions(sol_services, svc_defs)
50
+ add_simplify_svc_definitions(sol_services, svc_standard)
51
+ # Combine the active service data with the Exchange and Platform lists.
52
+ services_combine_with_active(sol_services, svc_cfgs)
53
+ end
54
+
55
+ def add_simplify_svc_exchange(sol_services, elements)
56
+ elements.each do |elem|
57
+ svc_alias = elem[:element][:source][:name]
58
+ svc_meta = {
59
+ name: elem[:element][:name],
60
+ alias: svc_alias,
61
+ description: elem[:element][:description],
62
+ status: '',
63
+ state: '',
64
+ source: 'Exchange',
65
+ }
66
+ sol_services[svc_alias] = svc_meta
67
+ # (lb): Just curious:
68
+ if svc_meta[:approval] != 'approved'
69
+ debug %(Unexpected: Exchange element !approved: #{svc_meta})
70
+ end
71
+ debug %(Unexpected: Exchange element !active: #{svc_meta}) unless svc_meta[:active]
72
+ end
73
+ end
74
+
75
+ def add_simplify_svc_definitions(sol_services, svc_standard)
76
+ svc_standard.each do |defn|
77
+ svc_alias = defn[:alias]
78
+ svc_meta = {
79
+ name: defn[:name],
80
+ alias: svc_alias,
81
+ description: defn[:description],
82
+ status: '',
83
+ state: defn[:status],
84
+ source: 'Platform',
85
+ }
86
+ svc_meta[:state] = add_type(svc_meta[:state], defn[:type])
87
+ unless sol_services[svc_alias].nil?
88
+ warning %(Unexpectedly overwriting entry with /service/: #{svc_alias})
89
+ end
90
+ sol_services[svc_alias] = svc_meta
91
+ end
92
+ end
93
+
94
+ def add_simplify_svc_configuration(sol_services, scfg)
95
+ svc_alias = scfg[:service]
96
+ svc_meta = {
97
+ name: scfg[:name],
98
+ alias: svc_alias,
99
+ description: '',
100
+ status: scfg[:status],
101
+ state: '',
102
+ source: 'Solution',
103
+ }
104
+ svc_meta[:status] = add_type(svc_meta[:status], scfg[:type])
105
+ unless sol_services[svc_alias].nil?
106
+ warning %(Unexpectedly overwriting entry with /serviceconfig/: #{svc_alias})
107
+ end
108
+ sol_services[svc_alias] = svc_meta
109
+ end
110
+
111
+ def add_type(state, type)
112
+ return state if !@include_type || type.to_s.empty?
113
+ %(#{state} (#{type}))
114
+ end
115
+
116
+ def service_blacklisted?(svc_alias)
117
+ !@include_all && !SERVICE_BLACKLIST[svc_alias.to_sym].nil?
118
+ end
119
+
120
+ def services_from_definitions(sol_services, svc_defs)
121
+ svc_defs.select do |svc|
122
+ # (lb): Sanity checking.
123
+ # Check if service blacklisted/!!whitelisted.
124
+ if service_blacklisted?(svc[:alias])
125
+ sol_services.delete(svc[:alias])
126
+ next(false)
127
+ end
128
+ # Check if service (from Exchange) already in the list we are building.
129
+ sxch = sol_services[svc[:alias]]
130
+ # The /exchange/ and /service/ endpoints do not use same name or description,
131
+ # e.g., Exchange name might be "Websocket Service" and /service/ name might
132
+ # be "WebSocket Gateway Service".
133
+ unless sxch.nil?
134
+ if sxch[:description] != svc[:description]
135
+ if !@abbrev_desc
136
+ sxch[:description] += ' / ' + svc[:description]
137
+ elsif sxch[:description].to_s.empty?
138
+ sxch[:description] = svc[:description]
139
+ end
140
+ end
141
+ sxch[:state] = svc[:status]
142
+ sxch[:state] = add_type(sxch[:state], sxch[:type])
143
+ sxch[:source] = 'Platform/' + sxch[:source]
144
+ next(false)
145
+ end
146
+ true
147
+ end
148
+ end
149
+
150
+ def services_combine_with_active(sol_services, svc_cfgs)
151
+ # Fill in status for active services.
152
+ svc_cfgs.each do |svc|
153
+ # (lb): Sanity check. Just clarifying my understanding of APIs and data.
154
+ if svc[:service] != svc[:script_key] && svc[:type] != 'P'
155
+ debug("MISMATCH: :service != :scriptkey / #{svc}")
156
+ end
157
+ if @exclude_added || service_blacklisted?(svc[:service])
158
+ sol_services.delete(svc[:service])
159
+ next
160
+ end
161
+ # The :alias is <sid>_<service>.
162
+ # Did we learn about this service from Exchange or the Platform?
163
+ svc_meta = sol_services[svc[:service]]
164
+ if !svc_meta.nil?
165
+ # Check if 'available', meaning... not 'hidden'.
166
+ # Note that /service/ calls it :status, but so as not to conflict
167
+ # with attr from /seviceconfig/ of same name, we called it :state.
168
+ if !@include_all && svc_meta[:state] == 'hidden'
169
+ sol_services.delete(svc[:service])
170
+ next
171
+ end
172
+ warning('Status not null?') unless svc_meta[:status] == ''
173
+ svc_meta[:status] = svc[:status]
174
+ svc_meta[:source] = add_type(svc_meta[:source], svc[:type])
175
+ # The /serviceconfig/ is generally personalized for the solution, e.g.,
176
+ # /service/ might have name "Timer Service" but /serviceconfig/ will
177
+ # have "<App Name> Timer and Scheduling Service".
178
+ svc_meta[:name] = svc[:name] unless svc[:name].to_s.empty?
179
+ else
180
+ # Make sure the service is not blacklisted/!!whitelisted.
181
+ add_simplify_svc_configuration(sol_services, svc)
182
+ end
183
+ end
184
+ if @exclude_avail
185
+ sol_services.reject! { |_svc_alias, svc| svc[:status].to_s.empty? }
186
+ end
187
+ # This was a weird bit of plumbing. And now we're done.
188
+ sol_services.values
189
+ end
190
+ end
191
+
192
+ # *** The `service` command class.
193
+
194
+ class ServiceCmd
195
+ include MrMurano::Verbose
196
+
197
+ def command_service(cmd)
198
+ cmd.syntax = %(murano service)
199
+ cmd.summary = %(About service)
200
+ cmd.description = %(
201
+ Commands for working with Solution Services.
202
+ ).strip
203
+ cmd.project_not_required = true
204
+ cmd.subcmdgrouphelp = true
205
+
206
+ cmd.action do |_args, _options|
207
+ ::Commander::UI.enable_paging unless $cfg['tool.no-page']
208
+ say MrMurano::SubCmdGroupHelp.new(cmd).get_help
209
+ end
210
+ end
211
+
212
+ # *** `service add` command and helpers.
213
+
214
+ def command_service_add(cmd)
215
+ cmd.syntax = %(murano service add [--options] <service-name>)
216
+ cmd.summary = %(Add a service to a solution)
217
+ cmd.description = %(
218
+ Add a service to a solution.
219
+ ).strip
220
+ cmd.project_not_required = true
221
+
222
+ # FIXME/2018-03-21: See comment under command_service_delete:
223
+ # We need to add multi-solution support so by default this
224
+ # command works on a single solution, rather than both the
225
+ # application and product (unless user specifies sol'n --type).
226
+ cmd_add_solntype_pickers(cmd)
227
+
228
+ cmd.action do |args, options|
229
+ cmd.verify_arg_count!(args, 1, ['Missing argument: Service alias or ID'])
230
+ cmd_defaults_solntype_pickers(options)
231
+ alias_or_id = args[0]
232
+ solz = must_fetch_solutions!(options)
233
+ cmd_service_add_sol_services(alias_or_id, solz)
234
+ end
235
+ end
236
+
237
+ def cmd_service_add_sol_services(alias_or_id, solz)
238
+ solz.each do |sol|
239
+ fancy_alias = fancy_ticks(alias_or_id)
240
+ fancy_name = fancy_ticks(sol.name)
241
+ MrMurano::Verbose.whirly_start %(
242
+ Adding #{fancy_alias} serviceconfig to #{fancy_name}...
243
+ ).strip
244
+ suc = cmd_service_add_sol_service(alias_or_id, sol)
245
+ warning %(Unable to add service #{fancy_alias} to #{fancy_name}.) unless suc
246
+ end
247
+ MrMurano::Verbose.whirly_stop
248
+ end
249
+
250
+ def cmd_service_add_sol_service(alias_or_id, sol)
251
+ svc_cfg = MrMurano::ServiceConfig.new(sol)
252
+ svc_cfg.create(alias_or_id) do |request, http|
253
+ response = http.request(request)
254
+ cmd_service_add_blather_result(response, alias_or_id, sol)
255
+ case response
256
+ when Net::HTTPOK
257
+ true
258
+ when Net::HTTPConflict
259
+ true
260
+ when Net::HTTPPaymentRequired
261
+ false
262
+ else
263
+ svc_cfg.showHttpError(request, response)
264
+ false
265
+ end
266
+ end
267
+ end
268
+
269
+ def cmd_service_add_blather_result(response, alias_or_id, sol)
270
+ fancy_alias = fancy_ticks(alias_or_id)
271
+ fancy_name = fancy_ticks(sol.name)
272
+ case response
273
+ when Net::HTTPOK
274
+ verbose %(Added service #{fancy_alias} to #{fancy_name}.)
275
+ when Net::HTTPConflict
276
+ verbose %(Service #{fancy_alias} was already added to #{fancy_name}.)
277
+ when Net::HTTPPaymentRequired
278
+ warning %(Use exchange to add the service to your business first.)
279
+ else
280
+ warning %(Unexpected response: #{response})
281
+ end
282
+ end
283
+
284
+ # *** `service delete` command and helpers.
285
+
286
+ def command_service_delete(cmd)
287
+ cmd.syntax = %(murano service delete [--options] <service-name>)
288
+ cmd.summary = %(Delete service from solution)
289
+ cmd.description = %(
290
+ Delete service from solution.
291
+ ).strip
292
+ cmd.project_not_required = true
293
+
294
+ # Add flag: --type [application|product|all].
295
+ # FIXME/2018-03-21: The way the CLI works, it chooses all solutions
296
+ # unless told otherwise; but we probably do not want to delete the
297
+ # same service from all solutions! So we really need multi-solution
298
+ # support! I.e., one solution active at a time, not 2!
299
+ cmd_add_solntype_pickers(cmd)
300
+
301
+ cmd.action do |args, options|
302
+ cmd.verify_arg_count!(args, 1, ['Missing argument: Service alias or ID'])
303
+ cmd_defaults_solntype_pickers(options)
304
+ alias_or_id = args[0]
305
+ solz = must_fetch_solutions!(options)
306
+ cmd_service_delete_sol_services(alias_or_id, solz)
307
+ end
308
+ end
309
+
310
+ def cmd_service_delete_sol_services(alias_or_id, solz)
311
+ solz.each do |sol|
312
+ fancy_alias = fancy_ticks(alias_or_id)
313
+ fancy_name = fancy_ticks(sol.name)
314
+ MrMurano::Verbose.whirly_start %(
315
+ Deleting #{fancy_alias} serviceconfig from #{fancy_name}...
316
+ ).strip
317
+ suc = cmd_service_delete_sol_service(alias_or_id, sol)
318
+ warning %(Service #{fancy_alias} not found for #{fancy_name}.) unless suc
319
+ end
320
+ MrMurano::Verbose.whirly_stop
321
+ end
322
+
323
+ def cmd_service_delete_sol_service(alias_or_id, sol)
324
+ svc_cfg = MrMurano::ServiceConfig.new(sol)
325
+ svc_cfg.remove(alias_or_id) do |request, http|
326
+ response = http.request(request)
327
+ cmd_service_delete_blather_result(response, alias_or_id, sol)
328
+ case response
329
+ when Net::HTTPNoContent
330
+ true
331
+ when Net::HTTPNotFound
332
+ false
333
+ else
334
+ svc_cfg.showHttpError(request, response)
335
+ false
336
+ end
337
+ end
338
+ end
339
+
340
+ def cmd_service_delete_blather_result(response, alias_or_id, sol)
341
+ fancy_alias = fancy_ticks(alias_or_id)
342
+ fancy_name = fancy_ticks(sol.name)
343
+ case response
344
+ when Net::HTTPNoContent
345
+ verbose %(Deleted service #{fancy_alias} from #{fancy_name}.)
346
+ when Net::HTTPNotFound
347
+ verbose %(Service #{fancy_alias} was already removed from #{fancy_name}.)
348
+ else
349
+ warning %(Unexpected response: #{response})
350
+ end
351
+ end
352
+
353
+ # *** `service get` command and helpers.
354
+
355
+ def command_service_get(cmd)
356
+ cmd.syntax = %(murano service get [--options] <service-name>)
357
+ cmd.summary = %(Get information about an added service)
358
+ cmd.description = %(
359
+ Get information about an added service.
360
+ ).strip
361
+ cmd.project_not_required = true
362
+
363
+ # Add flag: --type [application|product|all].
364
+ cmd_add_solntype_pickers(cmd)
365
+
366
+ cmd.action do |args, options|
367
+ cmd.verify_arg_count!(args, 1, ['Missing argument: Service alias or ID'])
368
+ cmd_defaults_solntype_pickers(options)
369
+ alias_or_id = args[0]
370
+ solz = must_fetch_solutions!(options)
371
+ sol_infos = cmd_service_get_sol_infos(alias_or_id, solz)
372
+ cmd_service_get_sol_info_report(sol_infos, solz.first)
373
+ end
374
+ end
375
+
376
+ def cmd_service_get_sol_infos(alias_or_id, solz)
377
+ sol_infos = []
378
+ solz.each do |sol|
379
+ MrMurano::Verbose.whirly_start %(
380
+ Fetching #{fancy_ticks(alias_or_id)} serviceconfig for #{fancy_ticks(sol.name)}...
381
+ ).strip
382
+ svc_info = cmd_service_get_sol_info(alias_or_id, sol)
383
+ svc_usage = svc_info['usage']
384
+ svc_usage = '<none reported>' if svc_usage.empty?
385
+ sol_infos += [[sol.sid, sol.name, svc_usage]]
386
+ end
387
+ MrMurano::Verbose.whirly_stop
388
+ sol_infos
389
+ end
390
+
391
+ def cmd_service_get_sol_info(alias_or_id, sol)
392
+ svc_cfg = MrMurano::ServiceConfig.new(sol)
393
+ svc_info = svc_cfg.info(alias_or_id) do |request, http|
394
+ response = http.request(request)
395
+ case response
396
+ when Net::HTTPSuccess
397
+ JSON.parse(response.body)
398
+ when Net::HTTPNotFound
399
+ # Info returns very basic info, e.g., { "usage": {} }
400
+ { 'usage' => '<no such service>' }
401
+ else
402
+ svc_cfg.showHttpError(request, response)
403
+ false
404
+ end
405
+ end
406
+ svc_info
407
+ end
408
+
409
+ def cmd_service_get_sol_info_report(sol_infos, any_sol)
410
+ headers = ['solution id', 'solution name', 'service usage']
411
+ any_sol.outf(sol_infos) do |dd, ios|
412
+ MrMurano::Verbose.tabularize(
413
+ { headers: headers, rows: dd },
414
+ ios
415
+ )
416
+ end
417
+ end
418
+
419
+ # *** `service list` command and helpers.
420
+
421
+ def cmd_service_fetch_solution_services(solz, purchased, options)
422
+ sol_svcs = []
423
+ MrMurano::Verbose.whirly_start('Fetching solution services...')
424
+ solz.each do |sol|
425
+ MrMurano::Verbose.whirly_start('Fetching solution serviceconfig list...')
426
+ # We could get everything without a query, e.g.,
427
+ # svc_cfgs = sol.serviceconfig()
428
+ # but maybe it's faster to only ask for what we need.
429
+ svc_cfgs = sol.serviceconfig(
430
+ select: 'id,service,name,alias,status,type,script_key'
431
+ )
432
+
433
+ MrMurano::Verbose.whirly_start('Fetching solution service list...')
434
+ svc_defs = sol.service
435
+
436
+ MrMurano::Verbose.whirly_start('Assembling solution service list...')
437
+ svc_reducer = ServiceConsolidator.new(options)
438
+ sol_svcs += [
439
+ [
440
+ sol,
441
+ svc_reducer.reconcile_services(
442
+ svc_cfgs[:items],
443
+ svc_defs[:items],
444
+ purchased,
445
+ ),
446
+ ],
447
+ ]
448
+ end
449
+ MrMurano::Verbose.whirly_stop
450
+ sol_svcs
451
+ end
452
+
453
+ def sol_svc_rowify(dd, headers, width_avail)
454
+ rows = []
455
+ # Sort by status, then alias. (Yeti orders Exchange elements first.)
456
+ sorted = dd.sort do |lhs, rhs|
457
+ next(lhs[:status] <=> rhs[:status]) unless lhs[:status] == rhs[:status]
458
+ lhs[:alias] <=> rhs[:alias]
459
+ end
460
+ sorted.each do |svc_cfg|
461
+ row = headers.map do |key|
462
+ full = svc_cfg[key]
463
+ next(full) if full.to_s.empty?
464
+ # Apparently descriptions have unpredictable whitespace and HTML tags.
465
+ full = full.tr("\n", ' ')
466
+ next(full) if width_avail.nil? || key != :description
467
+ MrMurano::Pretties.split_text_on_whitespace(full, width_avail)
468
+ end
469
+ rows << row
470
+ end
471
+ rows
472
+ end
473
+
474
+ def cmd_service_output_solution_services(sols_and_svcs, options)
475
+ headers = %i[name alias status]
476
+ headers += [:id] if options.show_id
477
+ headers += [:state] if options.show_state
478
+ headers += [:source] if options.show_source
479
+ headers += [:description] if options.show_desc || options.abbrev_desc
480
+
481
+ sol_svcs = sols_and_svcs.map { |_sol, svcs| svcs.map { |svc| OpenStruct.new(svc) } }
482
+ width_avail = MrMurano::Pretties.width_last_column(headers, sol_svcs.flatten)
483
+ sols_and_svcs.each do |sol, sol_svc|
484
+ sol.outf(sol_svc) do |dd, ios|
485
+ ios.puts(sol.pretty_desc(add_type: true)) if options.header
486
+ rows = sol_svc_rowify(dd, headers, width_avail)
487
+ sol.tabularize(
488
+ { headers: headers, rows: rows },
489
+ ios
490
+ )
491
+ end
492
+ end
493
+ end
494
+
495
+ def command_service_list(cmd)
496
+ cmd.syntax = %(murano service list)
497
+ cmd.summary = %(List all services previously added to a solution)
498
+ cmd.description = %(
499
+ List all services previously added to a solution.
500
+ ).strip
501
+ cmd.project_not_required = true
502
+
503
+ # Add flag: --type [application|product|all].
504
+ cmd_add_solntype_pickers(cmd)
505
+
506
+ cmd.option(
507
+ '--[no-]header', %(Output solution descriptions (default: true))
508
+ )
509
+
510
+ cmd.option '--show-all', 'Show all services, including hidden'
511
+ cmd.option '--show-id', 'Show service IDs'
512
+ cmd.option '--show-type', 'Show service types (Core, Product, Application, External)'
513
+ cmd.option '--show-state', 'Show service definition status'
514
+ cmd.option '--show-source', 'Show service source'
515
+ cmd.option '--show-desc', 'Show service descriptions'
516
+ cmd.option '--abbrev-desc', 'Show abbreviated service descriptions'
517
+
518
+ cmd.option '--hide-added', 'Show only services available to add to Solution'
519
+ cmd.option '--only-added', 'Show only services already added to Solution'
520
+
521
+ cmd.action do |args, options|
522
+ cmd.verify_arg_count!(args)
523
+ options.default(
524
+ all: false,
525
+ header: true,
526
+ show_all: false,
527
+ show_id: false,
528
+ show_type: false,
529
+ show_state: false,
530
+ show_source: false,
531
+ show_desc: false,
532
+ abbrev_desc: false,
533
+ hide_added: false,
534
+ only_added: false,
535
+ )
536
+ cmd_defaults_solntype_pickers(options)
537
+
538
+ solz = must_fetch_solutions!(options)
539
+ # Prepare the list of purchased Exchange services.
540
+ biz_exg = MrMurano::Exchange.new
541
+ MrMurano::Verbose.whirly_start('Fetching Exchange purchases list...')
542
+ purchased = biz_exg.fetch_purchased(services_only: true)
543
+ sols_and_svcs = cmd_service_fetch_solution_services(solz, purchased, options)
544
+
545
+ cmd_service_output_solution_services(sols_and_svcs, options)
546
+ end
547
+ end
548
+ end
549
+
550
+ def wire_cmd_service
551
+ service_cmd = ServiceCmd.new
552
+
553
+ command(:service) { |cmd| service_cmd.command_service(cmd) }
554
+
555
+ command('service add') { |cmd| service_cmd.command_service_add(cmd) }
556
+ alias_command 'service create', 'service add'
557
+
558
+ command('service delete') { |cmd| service_cmd.command_service_delete(cmd) }
559
+ alias_command 'service remove', 'service delete'
560
+
561
+ command('service get') { |cmd| service_cmd.command_service_get(cmd) }
562
+
563
+ command('service list') { |cmd| service_cmd.command_service_list(cmd) }
564
+ alias_command 'services list', 'service list'
565
+ alias_command 'service list available', 'service list', '--hide-added'
566
+ alias_command 'service list all', 'service list', '--show-all'
567
+ alias_command 'service list full', 'service list', '--show-all', '--show-type', '--show-state', '--show-source', '--show-desc'
568
+ alias_command 'service status', 'service list', '--only-added'
569
+ end
570
+
571
+ wire_cmd_service
572
+
@@ -6,9 +6,9 @@
6
6
  # Unauthorized copying of this file is strictly prohibited.
7
7
 
8
8
  require 'MrMurano/verbosing'
9
+ require 'MrMurano/AccountBase'
9
10
  require 'MrMurano/Business'
10
11
  require 'MrMurano/ReCommander'
11
- require 'MrMurano/Solution'
12
12
 
13
13
  command :show do |c|
14
14
  c.syntax = %(murano show)
@@ -111,7 +111,7 @@ Show readable information about the current configuration.
111
111
  puts 'product ID not found in config'
112
112
  end
113
113
 
114
- MrMurano::SolutionBase.warn_configfile_env_maybe if id_not_in_biz
114
+ MrMurano::AccountBase.warn_configfile_env_maybe if id_not_in_biz
115
115
  end
116
116
  end
117
117
 
@@ -126,7 +126,11 @@ Show readable information about the current configuration.
126
126
  c.action do |args, _options|
127
127
  c.verify_arg_count!(args)
128
128
  puts %(base: #{$cfg['location.base']})
129
- $cfg['location'].each { |key, value| puts %(#{key}: #{$cfg['location.base']}/#{value}) }
129
+ $cfg['location'].keys.sort.each do |key|
130
+ next if key == 'base'
131
+ value = $cfg['location'][key]
132
+ puts %(#{key}: #{$cfg['location.base']}/#{value})
133
+ end
130
134
  end
131
135
  end
132
136
 
@@ -6,7 +6,7 @@
6
6
  # Unauthorized copying of this file is strictly prohibited.
7
7
 
8
8
  require 'MrMurano/verbosing'
9
- require 'MrMurano/Account'
9
+ require 'MrMurano/Business'
10
10
  require 'MrMurano/ReCommander'
11
11
  require 'MrMurano/SubCmdGroupContext'
12
12
  require 'MrMurano/commands/business'
@@ -53,6 +53,7 @@ Create a new solution in the current business.
53
53
  if args.count.zero?
54
54
  sol = biz.solution_from_type!(options.type)
55
55
  name = solution_ask_for_name(sol)
56
+ next if name.nil?
56
57
  names = [name]
57
58
  else
58
59
  names = args
@@ -5,10 +5,12 @@
5
5
  # vim:tw=0:ts=2:sw=2:et:ai
6
6
  # Unauthorized copying of this file is strictly prohibited.
7
7
 
8
+ require 'rainbow'
8
9
  require 'MrMurano/verbosing'
9
10
  require 'MrMurano/Business'
10
11
  require 'MrMurano/Config'
11
12
  require 'MrMurano/Solution'
13
+ require 'MrMurano/variegated/TruthyFalsey'
12
14
 
13
15
  MSG_SOLUTIONS_NONE_FOUND = 'No solutions found' unless defined? MSG_SOLUTIONS_NONE_FOUND
14
16
 
@@ -252,29 +254,32 @@ def solution_get_solutions(biz, type, api_id: nil, name: nil, fuzzy: nil)
252
254
  solz
253
255
  end
254
256
 
255
- def solution_ask_for_name(sol)
256
- asking = true
257
- while asking
258
- solname = ask("Please enter the #{sol.type_name} name: ")
257
+ def solution_ask_for_name(sol_model)
258
+ solname = ''
259
+ while solname == ''
260
+ solname = ask("Enter the #{sol_model.type_name} name [leave blank to skip]: ")
259
261
  puts ''
260
262
  if solname == ''
261
- confirmed = ask("\nReally skip the #{sol.type_name}? [Y/n] ", true)
262
- if confirmed
263
+ confirmed = ask(
264
+ Rainbow("Really skip the #{sol_model.type_name}").bright.underline + '? [Y/n] '
265
+ )
266
+ if confirmed == '' || TruthyFalsey.new(confirmed).truthy?
263
267
  puts ''
264
- return '', '', false
268
+ solname = nil
265
269
  end
266
270
  else
267
- #unless sol.name.match(sol.name_validate_regex) { say ... }
271
+ # Try setting the name on the dummy solution; warn if fails, and keep looping.
268
272
  begin
269
- sol.set_name!(solname)
270
- break
273
+ # Test sol.name_validate_regex.
274
+ sol_model.set_name!(solname)
271
275
  rescue MrMurano::ConfigError => _err
272
- say(sol.name_validate_help)
276
+ say(sol_model.name_validate_help)
273
277
  # keep looping
278
+ solname = ''
274
279
  end
275
280
  end
276
281
  end
277
- sol.name
282
+ solname
278
283
  rescue EOFError
279
284
  # E.g., the user pressed Ctrl-D.
280
285
  # "error: The input stream is exhausted."
@@ -491,13 +496,24 @@ module MrMurano
491
496
  if solname.nil?
492
497
  #say "You do not have any #{type}s. Let's create one."
493
498
  if @verbose
494
- say("This business does not have any #{Inflecto.pluralize(sol.type.to_s)}. Let's create one")
499
+ plural = Inflecto.pluralize(sol.type.to_s)
500
+ say("This business does not have any #{plural}. Let's create one")
495
501
  puts ''
496
502
  end
497
- solution_ask_for_name(sol)
498
- else
503
+ solname = solution_ask_for_name(sol)
504
+ end
505
+ return nil if solname.nil?
506
+
507
+ # The solution name, if we asked for it, will have been verified;
508
+ # otherwise, if supplied as --option but an invalid solution name,
509
+ # raise a fuss.
510
+ begin
499
511
  sol.set_name!(solname)
512
+ rescue MrMurano::ConfigError => _err
513
+ MrMurano::Verbose.error(sol.name_validate_help)
514
+ exit 1
500
515
  end
516
+
501
517
  # MAYBE/2017-07-20: Detect if Business is ADC enabled. If not,
502
518
  # creating a solution fails, e.g.,
503
519
  # Request Failed: 409: [409] upgrade