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

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.
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