MuranoCLI 3.0.1 → 3.0.2
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 +4 -4
- data/.agignore +1 -0
- data/.rubocop.yml +67 -5
- data/Gemfile +6 -3
- data/MuranoCLI.gemspec +14 -10
- data/README.markdown +299 -126
- data/Rakefile +6 -1
- data/bin/murano +2 -2
- data/docs/completions/murano_completion-bash +93 -0
- data/lib/MrMurano.rb +19 -2
- data/lib/MrMurano/Business.rb +22 -19
- data/lib/MrMurano/Config.rb +19 -9
- data/lib/MrMurano/Content.rb +4 -4
- data/lib/MrMurano/Exchange-Element.rb +99 -0
- data/lib/MrMurano/Exchange.rb +137 -0
- data/lib/MrMurano/Gateway.rb +9 -9
- data/lib/MrMurano/Keystore.rb +4 -2
- data/lib/MrMurano/ReCommander.rb +3 -5
- data/lib/MrMurano/Solution-ServiceConfig.rb +12 -12
- data/lib/MrMurano/Solution-Services.rb +15 -14
- data/lib/MrMurano/Solution-Users.rb +2 -2
- data/lib/MrMurano/Solution.rb +43 -49
- data/lib/MrMurano/SolutionId.rb +28 -28
- data/lib/MrMurano/SyncUpDown.rb +32 -22
- data/lib/MrMurano/Webservice-Endpoint.rb +2 -1
- data/lib/MrMurano/Webservice.rb +5 -5
- data/lib/MrMurano/commands.rb +2 -1
- data/lib/MrMurano/commands/business.rb +21 -19
- data/lib/MrMurano/commands/domain.rb +16 -2
- data/lib/MrMurano/commands/exchange.rb +272 -0
- data/lib/MrMurano/commands/globals.rb +17 -1
- data/lib/MrMurano/commands/init.rb +3 -3
- data/lib/MrMurano/commands/link.rb +16 -16
- data/lib/MrMurano/commands/postgresql.rb +2 -2
- data/lib/MrMurano/commands/show.rb +13 -7
- data/lib/MrMurano/commands/solution.rb +23 -17
- data/lib/MrMurano/commands/solution_picker.rb +49 -44
- data/lib/MrMurano/commands/sync.rb +2 -1
- data/lib/MrMurano/commands/timeseries.rb +2 -2
- data/lib/MrMurano/commands/tsdb.rb +2 -2
- data/lib/MrMurano/hash.rb +19 -7
- data/lib/MrMurano/http.rb +12 -2
- data/lib/MrMurano/orderedhash.rb +200 -0
- data/lib/MrMurano/spec_commander.rb +98 -0
- data/lib/MrMurano/verbosing.rb +2 -2
- data/lib/MrMurano/version.rb +2 -2
- data/spec/Business_spec.rb +8 -6
- data/spec/Solution-ServiceConfig_spec.rb +1 -1
- data/spec/SyncUpDown_spec.rb +6 -6
- data/spec/_workspace.rb +9 -4
- data/spec/cmd_business_spec.rb +8 -2
- data/spec/cmd_common.rb +266 -25
- data/spec/cmd_exchange_spec.rb +118 -0
- data/spec/cmd_help_spec.rb +54 -13
- data/spec/cmd_init_spec.rb +1 -12
- data/spec/cmd_link_spec.rb +94 -72
- data/spec/spec_helper.rb +11 -16
- metadata +23 -17
data/lib/MrMurano/SolutionId.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Last Modified: 2017.
|
1
|
+
# Last Modified: 2017.09.11 /coding: utf-8
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
# Copyright © 2016-2017 Exosite LLC.
|
@@ -9,68 +9,68 @@ require 'MrMurano/verbosing'
|
|
9
9
|
|
10
10
|
module MrMurano
|
11
11
|
module SolutionId
|
12
|
-
|
12
|
+
INVALID_API_ID = '-1'
|
13
13
|
UNEXPECTED_TYPE_OR_ERROR_MSG = (
|
14
14
|
'Unexpected result type or error: assuming empty instead'
|
15
15
|
)
|
16
16
|
|
17
|
+
attr_reader :api_id
|
18
|
+
attr_reader :valid_api_id
|
17
19
|
attr_reader :sid
|
18
|
-
attr_reader :valid_sid
|
19
20
|
|
20
|
-
def
|
21
|
-
@
|
21
|
+
def init_api_id!(api_id=nil)
|
22
|
+
@valid_api_id = false
|
22
23
|
unless defined?(@solntype) && @solntype
|
23
24
|
# Note that 'solution.id' isn't an actual config setting;
|
24
25
|
# see instead 'application.id' and 'product.id'. We just
|
25
26
|
# use 'solution.id' to indicate that the caller specified
|
26
27
|
# a solution ID explicitly (i.e., it's not from the $cfg).
|
27
|
-
raise 'Missing
|
28
|
+
raise 'Missing api_id or class @solntype!?' if api_id.to_s.empty?
|
28
29
|
@solntype = 'solution.id'
|
29
30
|
end
|
30
|
-
if
|
31
|
-
self.
|
31
|
+
if api_id
|
32
|
+
self.api_id = api_id
|
32
33
|
else
|
33
34
|
# Get the application.id or product.id.
|
34
|
-
self.
|
35
|
+
self.api_id = $cfg[@solntype]
|
35
36
|
end
|
36
37
|
# Maybe raise 'No application!' or 'No product!'.
|
37
|
-
return unless @
|
38
|
+
return unless @api_id.to_s.empty?
|
38
39
|
raise MrMurano::ConfigError.new("No #{/(.*).id/.match(@solntype)[1]} ID!")
|
39
40
|
end
|
40
41
|
|
41
|
-
def
|
42
|
-
# The @
|
43
|
-
@
|
42
|
+
def api_id?
|
43
|
+
# The @api_id should never be nil or empty, but let's at least check.
|
44
|
+
@api_id != INVALID_API_ID && !@api_id.to_s.empty?
|
44
45
|
end
|
45
46
|
|
46
|
-
def
|
47
|
-
|
48
|
-
if
|
49
|
-
@
|
47
|
+
def api_id=(api_id)
|
48
|
+
api_id = INVALID_API_ID if api_id.nil? || !api_id.is_a?(String) || api_id.empty?
|
49
|
+
if api_id.to_s.empty? || api_id == INVALID_API_ID || (defined?(@api_id) && api_id != @api_id)
|
50
|
+
@valid_api_id = false
|
50
51
|
end
|
51
|
-
@
|
52
|
-
# MAGIC_NUMBER: The 2nd element is the solution ID, e.g., solution/<
|
53
|
-
raise "Unexpected @
|
52
|
+
@api_id = api_id
|
53
|
+
# MAGIC_NUMBER: The 2nd element is the solution ID, e.g., solution/<api_id>/...
|
54
|
+
raise "Unexpected @uriparts_apidex #{@uriparts_apidex}" unless @uriparts_apidex == 1
|
54
55
|
# We're called on initialize before @uriparts is built, so don't always do this.
|
55
|
-
@uriparts[@
|
56
|
+
@uriparts[@uriparts_apidex] = @api_id if defined?(@uriparts)
|
56
57
|
end
|
57
58
|
|
58
59
|
def affirm_valid
|
59
|
-
@
|
60
|
+
@valid_api_id = true
|
60
61
|
end
|
61
62
|
|
62
|
-
def
|
63
|
-
@
|
63
|
+
def valid_api_id?
|
64
|
+
@valid_api_id
|
64
65
|
end
|
65
66
|
|
66
|
-
|
67
|
-
|
68
|
-
@sid
|
67
|
+
def api_id
|
68
|
+
@api_id
|
69
69
|
end
|
70
70
|
|
71
71
|
def endpoint(_path='')
|
72
72
|
# This is hopefully just a DEV error, and not something user will ever see!
|
73
|
-
return unless @uriparts[@
|
73
|
+
return unless @uriparts[@uriparts_apidex] == INVALID_API_ID
|
74
74
|
error("Solution ID missing! Invalid #{MrMurano::Verbose.fancy_ticks(@solntype)}")
|
75
75
|
exit 2
|
76
76
|
end
|
data/lib/MrMurano/SyncUpDown.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Last Modified: 2017.
|
1
|
+
# Last Modified: 2017.09.11 /coding: utf-8
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
# Copyright © 2016-2017 Exosite LLC.
|
@@ -295,17 +295,26 @@ module MrMurano
|
|
295
295
|
#
|
296
296
|
# @param local [Pathname] Full path of where to download to
|
297
297
|
# @param item [Item] The item to download
|
298
|
-
def download(local, item)
|
298
|
+
def download(local, item, options={})
|
299
299
|
#if item[:bundled]
|
300
300
|
# warning "Not downloading into bundled item #{synckey(item)}"
|
301
301
|
# return
|
302
302
|
#end
|
303
303
|
id = item[@itemkey.to_sym]
|
304
|
-
if id.
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
304
|
+
if id.to_s.empty?
|
305
|
+
# 2017-09-05: MRMUR-156: User seeing this.
|
306
|
+
if @itemkey.to_sym != :id
|
307
|
+
debug "!!! Missing '#{@itemkey}', trying :id instead"
|
308
|
+
id = item[:id]
|
309
|
+
end
|
310
|
+
if id.to_s.empty?
|
311
|
+
debug %(Remote item "#{item[:name]}" missing :id / local: #{local} / item: #{item})
|
312
|
+
return if options[:ignore_errors]
|
313
|
+
error %(Remote item missing :id => #{local})
|
314
|
+
print %(You can ignore this error using --ignore-errors)
|
315
|
+
exit 1
|
316
|
+
end
|
317
|
+
debug ":id => #{id}"
|
309
318
|
end
|
310
319
|
|
311
320
|
relpath = local.relative_path_from(Pathname.pwd).to_s
|
@@ -400,14 +409,14 @@ module MrMurano
|
|
400
409
|
end
|
401
410
|
|
402
411
|
def syncup_before
|
403
|
-
|
412
|
+
syncable_validate_api_id
|
404
413
|
end
|
405
414
|
|
406
415
|
def syncup_after
|
407
416
|
end
|
408
417
|
|
409
418
|
def syncdown_before
|
410
|
-
|
419
|
+
syncable_validate_api_id
|
411
420
|
end
|
412
421
|
|
413
422
|
def syncdown_after(_local)
|
@@ -738,13 +747,13 @@ module MrMurano
|
|
738
747
|
end
|
739
748
|
toadd.each do |item|
|
740
749
|
syncdown_item(item, into, options, :create, 'Adding') do |dest, aitem|
|
741
|
-
download(dest, aitem)
|
750
|
+
download(dest, aitem, options)
|
742
751
|
num_synced += 1
|
743
752
|
end
|
744
753
|
end
|
745
754
|
tomod.each do |item|
|
746
755
|
syncdown_item(item, into, options, :update, 'Updating') do |dest, aitem|
|
747
|
-
download(dest, aitem)
|
756
|
+
download(dest, aitem, options)
|
748
757
|
num_synced += 1
|
749
758
|
end
|
750
759
|
end
|
@@ -908,9 +917,10 @@ module MrMurano
|
|
908
917
|
# Get the solution name from the config.
|
909
918
|
# Convert, e.g., application.id => application.name
|
910
919
|
soln_name = $cfg[@solntype.gsub(/(.*)\.id/, '\1.name')]
|
911
|
-
# Skip this syncable if the
|
920
|
+
# Skip this syncable if the api_id is not set, or if user wants to skip
|
921
|
+
# by solution.
|
912
922
|
skip_sol = false
|
913
|
-
if !
|
923
|
+
if !api_id? ||
|
914
924
|
(options[:type] == :application && @solntype != 'application.id') ||
|
915
925
|
(options[:type] == :product && @solntype != 'product.id')
|
916
926
|
skip_sol = true
|
@@ -918,17 +928,17 @@ module MrMurano
|
|
918
928
|
tested = false
|
919
929
|
passed = false
|
920
930
|
if @solntype == 'application.id'
|
921
|
-
# elevate_hash
|
922
|
-
# on unknown keys, so preface with a key? guard.
|
931
|
+
# elevate_hash makes the hash return false rather than
|
932
|
+
# nil on unknown keys, so preface with a key? guard.
|
923
933
|
if options.key?(:application) && !options[:application].to_s.empty?
|
924
934
|
if soln_name =~ /#{Regexp.escape(options[:application])}/i ||
|
925
|
-
|
935
|
+
api_id =~ /#{Regexp.escape(options[:application])}/i
|
926
936
|
passed = true
|
927
937
|
end
|
928
938
|
tested = true
|
929
939
|
end
|
930
940
|
if options.key?(:application_id) && !options[:application_id].to_s.empty?
|
931
|
-
passed = true if options[:application_id] ==
|
941
|
+
passed = true if options[:application_id] == api_id
|
932
942
|
tested = true
|
933
943
|
end
|
934
944
|
if options.key?(:application_name) && !options[:application_name].to_s.empty?
|
@@ -938,13 +948,13 @@ module MrMurano
|
|
938
948
|
elsif @solntype == 'product.id'
|
939
949
|
if options.key?(:product) && !options[:product].to_s.empty?
|
940
950
|
if soln_name =~ /#{Regexp.escape(options[:product])}/i ||
|
941
|
-
|
951
|
+
api_id =~ /#{Regexp.escape(options[:product])}/i
|
942
952
|
passed = true
|
943
953
|
end
|
944
954
|
tested = true
|
945
955
|
end
|
946
956
|
if options.key?(:product_id) && !options[:product_id].to_s.empty?
|
947
|
-
passed = true if options[:product_id] ==
|
957
|
+
passed = true if options[:product_id] == api_id
|
948
958
|
tested = true
|
949
959
|
end
|
950
960
|
if options.key?(:product_name) && !options[:product_name].to_s.empty?
|
@@ -960,13 +970,13 @@ module MrMurano
|
|
960
970
|
ret
|
961
971
|
end
|
962
972
|
|
963
|
-
def
|
973
|
+
def syncable_validate_api_id
|
964
974
|
# 2017-07-02: Now that there are multiple solution types, and because
|
965
975
|
# SyncRoot.add is called on different classes that go with either or
|
966
976
|
# both products and applications, if a user only created one solution,
|
967
|
-
# then some syncables will have their
|
977
|
+
# then some syncables will have their api_id set to -1, because there's
|
968
978
|
# not a corresponding solution in Murano.
|
969
|
-
raise 'Syncable missing
|
979
|
+
raise 'Syncable missing api_id or not valid_api_id??!' unless api_id?
|
970
980
|
end
|
971
981
|
|
972
982
|
def items_lists(options, selected)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Last Modified: 2017.
|
1
|
+
# Last Modified: 2017.09.07 /coding: utf-8
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
# Copyright © 2016-2017 Exosite LLC.
|
@@ -108,6 +108,7 @@ module MrMurano
|
|
108
108
|
# @param modify [Boolean] True if item exists already and this is changing it
|
109
109
|
def upload(local, remote, _modify)
|
110
110
|
local = Pathname.new(local) unless local.is_a? Pathname
|
111
|
+
# MAYBE/2017-09-07: Honor options.ignore_errors here?
|
111
112
|
raise 'no file' unless local.exist?
|
112
113
|
# we assume these are small enough to slurp.
|
113
114
|
if remote.script.nil?
|
data/lib/MrMurano/Webservice.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Last Modified: 2017.
|
1
|
+
# Last Modified: 2017.09.11 /coding: utf-8
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
# Copyright © 2016-2017 Exosite LLC.
|
@@ -22,11 +22,11 @@ module MrMurano
|
|
22
22
|
|
23
23
|
def initialize
|
24
24
|
@solntype = 'application.id'
|
25
|
-
@
|
26
|
-
|
25
|
+
@uriparts_apidex = 1
|
26
|
+
init_api_id!
|
27
27
|
# FIXME/2017-06-05/MRMUR-XXXX: Update to new endpoint.
|
28
|
-
#@uriparts = [:service, @
|
29
|
-
@uriparts = [:solution, @
|
28
|
+
#@uriparts = [:service, @api_id, :webservice]
|
29
|
+
@uriparts = [:solution, @api_id]
|
30
30
|
@itemkey = :id
|
31
31
|
#@locationbase = $cfg['location.base']
|
32
32
|
@location = nil
|
data/lib/MrMurano/commands.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Last Modified: 2017.08.
|
1
|
+
# Last Modified: 2017.08.29 /coding: utf-8
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
# Copyright © 2016-2017 Exosite LLC.
|
@@ -17,6 +17,7 @@ require 'MrMurano/commands/content'
|
|
17
17
|
require 'MrMurano/commands/cors'
|
18
18
|
require 'MrMurano/commands/devices'
|
19
19
|
require 'MrMurano/commands/domain'
|
20
|
+
require 'MrMurano/commands/exchange'
|
20
21
|
require 'MrMurano/commands/globals'
|
21
22
|
require 'MrMurano/commands/keystore'
|
22
23
|
require 'MrMurano/commands/init'
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Last Modified: 2017.
|
1
|
+
# Last Modified: 2017.09.11 /coding: utf-8
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
# Copyright © 2016-2017 Exosite LLC.
|
@@ -12,8 +12,8 @@ require 'MrMurano/ReCommander'
|
|
12
12
|
|
13
13
|
MSG_BUSINESSES_NONE_FOUND = 'No businesses found' unless defined? MSG_BUSINESSES_NONE_FOUND
|
14
14
|
|
15
|
-
# *** Base business command help
|
16
|
-
#
|
15
|
+
# *** Base business command help
|
16
|
+
# ------------------------------
|
17
17
|
|
18
18
|
command :business do |c|
|
19
19
|
c.syntax = %(murano business)
|
@@ -31,16 +31,14 @@ Commands for working with businesses.
|
|
31
31
|
end
|
32
32
|
alias_command 'businesses', 'business'
|
33
33
|
|
34
|
-
# *** Common business command options
|
35
|
-
#
|
34
|
+
# *** Common business command options
|
35
|
+
# -----------------------------------
|
36
36
|
|
37
|
-
def
|
37
|
+
def cmd_table_output_add_options(c)
|
38
38
|
# MAYBE/2017-08-15: Rename to --id-only.
|
39
39
|
c.option '--idonly', 'Only return the IDs'
|
40
|
-
c.option '--[no-]brief', 'Show fewer fields: only
|
41
|
-
#
|
42
|
-
# easier to use --json/--yaml/--output any file, without
|
43
|
-
# having to use obscure `-c tool.outformat=json`).
|
40
|
+
c.option '--[no-]brief', 'Show fewer fields: show only IDs and names'
|
41
|
+
# MAYBE/2017-08-17: Move -o option to globals.rb and apply to all commands.
|
44
42
|
c.option '-o', '--output FILE', 'Download to file instead of STDOUT'
|
45
43
|
end
|
46
44
|
|
@@ -83,8 +81,8 @@ def any_business_pickers?(options)
|
|
83
81
|
num_ways > 0
|
84
82
|
end
|
85
83
|
|
86
|
-
# *** Business commands: list and find
|
87
|
-
#
|
84
|
+
# *** Business commands: list and find
|
85
|
+
# ------------------------------------
|
88
86
|
|
89
87
|
command 'business list' do |c|
|
90
88
|
c.syntax = %(murano business list [--options])
|
@@ -94,7 +92,7 @@ List businesses.
|
|
94
92
|
).strip
|
95
93
|
c.project_not_required = true
|
96
94
|
|
97
|
-
|
95
|
+
cmd_table_output_add_options(c)
|
98
96
|
|
99
97
|
c.action do |args, options|
|
100
98
|
c.verify_arg_count!(args)
|
@@ -111,7 +109,7 @@ Find business by name or ID.
|
|
111
109
|
).strip
|
112
110
|
c.project_not_required = true
|
113
111
|
|
114
|
-
|
112
|
+
cmd_table_output_add_options(c)
|
115
113
|
|
116
114
|
# Add --business/-id/-name options.
|
117
115
|
cmd_option_business_pickers(c)
|
@@ -130,8 +128,8 @@ Find business by name or ID.
|
|
130
128
|
end
|
131
129
|
end
|
132
130
|
|
133
|
-
# *** Business actions helpers
|
134
|
-
#
|
131
|
+
# *** Business actions helpers
|
132
|
+
# ----------------------------
|
135
133
|
|
136
134
|
def business_find_or_ask!(acc, options)
|
137
135
|
#any_business_pickers?(options)
|
@@ -263,9 +261,9 @@ def cmd_business_find_businesses(acc, args, options)
|
|
263
261
|
bizz
|
264
262
|
end
|
265
263
|
|
266
|
-
def
|
264
|
+
def cmd_business_header_and_bizz(bizz, options)
|
267
265
|
if options.idonly
|
268
|
-
headers = [
|
266
|
+
headers = %i[bizid]
|
269
267
|
bizz = bizz.map(&:bizid)
|
270
268
|
elsif options.brief
|
271
269
|
#headers = %i[bizid role name]
|
@@ -274,7 +272,7 @@ def cmd_business_output_businesses(acc, bizz, options)
|
|
274
272
|
bizz = bizz.map { |biz| [biz.bizid, biz.name] }
|
275
273
|
else
|
276
274
|
# 2017-08-16: There are only 3 keys: bizid, role, and name.
|
277
|
-
headers = bizz[0].meta.keys
|
275
|
+
headers = (bizz[0] && bizz[0].meta.keys) || []
|
278
276
|
headers.sort_by! do |hdr|
|
279
277
|
case hdr
|
280
278
|
when :bizid
|
@@ -289,7 +287,11 @@ def cmd_business_output_businesses(acc, bizz, options)
|
|
289
287
|
end
|
290
288
|
bizz = bizz.map { |biz| headers.map { |key| biz.meta[key] } }
|
291
289
|
end
|
290
|
+
[headers, bizz]
|
291
|
+
end
|
292
292
|
|
293
|
+
def cmd_business_output_businesses(acc, bizz, options)
|
294
|
+
headers, bizz = cmd_business_header_and_bizz(bizz, options)
|
293
295
|
io = File.open(options.output, 'w') if options.output
|
294
296
|
acc.outf(bizz, io) do |dd, ios|
|
295
297
|
if options.idonly
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Last Modified: 2017.
|
1
|
+
# Last Modified: 2017.09.11 /coding: utf-8
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
# Copyright © 2016-2017 Exosite LLC.
|
@@ -41,7 +41,21 @@ Print the domain for this solution.
|
|
41
41
|
|
42
42
|
solz.each do |sol|
|
43
43
|
if !options.brief
|
44
|
-
|
44
|
+
if $cfg['tool.outformat'] == 'best'
|
45
|
+
say(sol.pretty_desc(add_type: true, raw_url: options.raw))
|
46
|
+
else
|
47
|
+
dobj = {}
|
48
|
+
dobj[:type] = sol.type.to_s.capitalize
|
49
|
+
dobj[:name] = sol.name || ''
|
50
|
+
dobj[:api_id] = sol.api_id || ''
|
51
|
+
dobj[:sid] = sol.sid || ''
|
52
|
+
dobj[:domain] = ''
|
53
|
+
if sol.domain
|
54
|
+
dobj[:domain] += 'https://' unless options.raw
|
55
|
+
dobj[:domain] += sol.domain
|
56
|
+
end
|
57
|
+
sol.outf(dobj)
|
58
|
+
end
|
45
59
|
elsif options.raw
|
46
60
|
say(sol.domain)
|
47
61
|
else
|
@@ -0,0 +1,272 @@
|
|
1
|
+
# Last Modified: 2017.08.31 /coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Copyright © 2016-2017 Exosite LLC.
|
5
|
+
# License: MIT. See LICENSE.txt.
|
6
|
+
# vim:tw=0:ts=2:sw=2:et:ai
|
7
|
+
|
8
|
+
require 'highline'
|
9
|
+
require 'MrMurano/verbosing'
|
10
|
+
require 'MrMurano/Exchange'
|
11
|
+
require 'MrMurano/ReCommander'
|
12
|
+
require 'MrMurano/commands/business'
|
13
|
+
|
14
|
+
# *** Business commands: Exchange Elements
|
15
|
+
# ----------------------------------------
|
16
|
+
|
17
|
+
command :exchange do |c|
|
18
|
+
c.syntax = %(murano exchange)
|
19
|
+
c.summary = %(About IOT Exchange)
|
20
|
+
c.description = %(
|
21
|
+
Commands for working with IOT Exchange.
|
22
|
+
).strip
|
23
|
+
c.project_not_required = true
|
24
|
+
c.subcmdgrouphelp = true
|
25
|
+
|
26
|
+
c.action do |_args, _options|
|
27
|
+
::Commander::UI.enable_paging unless $cfg['tool.no-page']
|
28
|
+
say MrMurano::SubCmdGroupHelp.new(c).get_help
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
command 'exchange list' do |c|
|
33
|
+
c.syntax = %(murano exchange list [--options] [<name-or-ID>])
|
34
|
+
c.summary = %(List Exchange Elements)
|
35
|
+
c.description = %(
|
36
|
+
List Exchange Elements, either all of them, or those that are purchased or available.
|
37
|
+
|
38
|
+
Each Exchange Element is identified by an Element ID and a name.
|
39
|
+
|
40
|
+
Element status:
|
41
|
+
|
42
|
+
- added An Element that has been added to and enabled for your Business
|
43
|
+
|
44
|
+
- available An Element that can be added to and enabled for your Business
|
45
|
+
|
46
|
+
- available* An Element that you can use if you upgrade your Business tier
|
47
|
+
|
48
|
+
).strip
|
49
|
+
c.project_not_required = true
|
50
|
+
|
51
|
+
cmd_table_output_add_options(c)
|
52
|
+
|
53
|
+
c.option '--[no-]added', 'Only show Elements that have been added to the Application'
|
54
|
+
c.option '--[no-]full', 'Show all fields'
|
55
|
+
c.option '--[no-]other', 'Show other fields: like type, tiers, tags, and action, and apiServiceName'
|
56
|
+
|
57
|
+
# Add --id and --name options.
|
58
|
+
cmd_options_add_id_and_name(c)
|
59
|
+
|
60
|
+
c.action do |args, options|
|
61
|
+
c.verify_arg_count!(args, 1)
|
62
|
+
cmd_defaults_id_and_name(options)
|
63
|
+
|
64
|
+
xchg = MrMurano::Exchange.new
|
65
|
+
|
66
|
+
elems, available, purchased = find_elements(xchg, options, args[0])
|
67
|
+
if options.added.nil?
|
68
|
+
show = elems
|
69
|
+
elsif options.added
|
70
|
+
show = purchased
|
71
|
+
else
|
72
|
+
show = available
|
73
|
+
end
|
74
|
+
|
75
|
+
headers, pruned = cmd_exchange_header_and_elems(show, options)
|
76
|
+
|
77
|
+
io = File.open(options.output, 'w') if options.output
|
78
|
+
xchg.outf(pruned, io) do |item, ios|
|
79
|
+
if options.idonly
|
80
|
+
ios.puts item
|
81
|
+
else
|
82
|
+
ios.puts "Found #{pruned.length} elements."
|
83
|
+
xchg.tabularize(
|
84
|
+
{
|
85
|
+
headers: headers.map(&:to_s),
|
86
|
+
rows: item,
|
87
|
+
},
|
88
|
+
ios,
|
89
|
+
)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
io.close unless io.nil?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
alias_command 'exchange list available', 'exchange list', '--no-added'
|
96
|
+
alias_command 'exchange list purchased', 'exchange list', '--added'
|
97
|
+
|
98
|
+
def find_elements(xchg, options, term)
|
99
|
+
filter_id = nil
|
100
|
+
filter_name = nil
|
101
|
+
filter_fuzzy = nil
|
102
|
+
if term
|
103
|
+
if options.id
|
104
|
+
filter_id = term
|
105
|
+
elsif options.name
|
106
|
+
filter_name = term
|
107
|
+
else
|
108
|
+
filter_fuzzy = term
|
109
|
+
end
|
110
|
+
end
|
111
|
+
xchg.elements(filter_id: filter_id, filter_name: filter_name, filter_fuzzy: filter_fuzzy)
|
112
|
+
end
|
113
|
+
|
114
|
+
def cmd_exchange_header_and_elems(elems, options)
|
115
|
+
# MAYBE/2017-08-31: If you `-c outformat=json`, each Element is a
|
116
|
+
# list of values, rather than a dictionary. Wouldn't the JSON be
|
117
|
+
# easier to consume if each Element was a dict, rather than list?
|
118
|
+
if options.idonly
|
119
|
+
headers = %i[elementId]
|
120
|
+
elems = elems.map(&:elementId)
|
121
|
+
elsif options.brief
|
122
|
+
headers = %i[elementId name]
|
123
|
+
headers += [:status] unless options.added
|
124
|
+
#elems = elems.map { |elem| [elem.elementId, elem.name] }
|
125
|
+
elems = elems.map { |elem| headers.map { |key| elem.send(key) } }
|
126
|
+
elsif options.full
|
127
|
+
headers = %i[elementId name status type apiServiceName tiers tags actions markdown]
|
128
|
+
all_hdrs = (elems[0] && elems[0].meta.keys) || []
|
129
|
+
all_hdrs.each do |chk|
|
130
|
+
headers.push(chk) unless headers.include?(chk)
|
131
|
+
end
|
132
|
+
#elems = elems.map { |elem| headers.map { |key| elem.meta[key] } }
|
133
|
+
elems = elems.map { |elem| headers.map { |key| elem.send(key) || '' } }
|
134
|
+
elsif options.other
|
135
|
+
# NOTE: Showing columns not displayed when --other not specified,
|
136
|
+
# except not showing :markdown, ever.
|
137
|
+
headers = %i[elementId type apiServiceName tiers tags actions]
|
138
|
+
#headers = %i[elementId type apiServiceName tiers tags actions markdown]
|
139
|
+
#elems = elems.map { |elem| headers.map { |key| elem.send(key) } }
|
140
|
+
elems = elems.map do |elem|
|
141
|
+
[
|
142
|
+
elem.elementId,
|
143
|
+
elem.type,
|
144
|
+
elem.apiServiceName,
|
145
|
+
#elem.tiers,
|
146
|
+
#elem.tiers.join(' | '),
|
147
|
+
elem.tiers.join("\n"),
|
148
|
+
#elem.tags,
|
149
|
+
#elem.tags.join(' | '),
|
150
|
+
elem.tags.join("\n"),
|
151
|
+
#elem.actions,
|
152
|
+
elem.actions.map { |actn| actn.map { |key, val| "#{key}: #{val}" }.join("\n") }.join("\n"),
|
153
|
+
#elem.markdown.gsub("\n", '\\n'),
|
154
|
+
]
|
155
|
+
end
|
156
|
+
else
|
157
|
+
# 2017-08-28: There are 9 keys, and one of them -- :markdown -- is a
|
158
|
+
# lot of text, so rather than, e.g., elems[0].meta.keys, be selective.
|
159
|
+
headers = %i[elementId name]
|
160
|
+
headers += [:status] unless options.added
|
161
|
+
headers += [:description]
|
162
|
+
if $stdout.tty?
|
163
|
+
# Calculate how much room (how many characters) are left for the
|
164
|
+
# description column.
|
165
|
+
width_taken = 0
|
166
|
+
# rubocop:disable Performance/FixedSize
|
167
|
+
# "Do not compute the size of statically sized objects."
|
168
|
+
width_taken += '| '.length
|
169
|
+
# Calculate the width of each column except the last (:description).
|
170
|
+
headers[0..-2].each do |key|
|
171
|
+
elem_with_max = elems.max { |a, b| a.send(key).length <=> b.send(key).length }
|
172
|
+
width_taken += elem_with_max.send(key).length
|
173
|
+
width_taken += ' | '.length
|
174
|
+
end
|
175
|
+
width_taken += ' | '.length
|
176
|
+
term_width, _rows = HighLine::SystemExtensions.terminal_size
|
177
|
+
width_avail = term_width - width_taken
|
178
|
+
# MAGIC_NUMBER: Tweak/change this if you want. 20 char min feels
|
179
|
+
# about right: don't wrap if column would be narrow or negative.
|
180
|
+
width_avail = nil if width_avail < 20
|
181
|
+
else
|
182
|
+
width_avail = nil
|
183
|
+
end
|
184
|
+
#elems = elems.map { |elem| headers.map { |key| elem.send(key) } }
|
185
|
+
elems = elems.map do |elem|
|
186
|
+
headers.map do |key|
|
187
|
+
if !width_avail.nil? && key == :description
|
188
|
+
#elem.meta[key].scan(/.{1,#{width_avail}}/).join("\n")
|
189
|
+
full = elem.send(key)
|
190
|
+
parts = []
|
191
|
+
until full.empty?
|
192
|
+
# Split the description on a space before the max width.
|
193
|
+
# FIXME/2017-08-28: Need to test really long desc with no space.
|
194
|
+
part = full[0..width_avail]
|
195
|
+
full = full[(width_avail + 1)..-1] || ''
|
196
|
+
leftover = ''
|
197
|
+
part, _space, leftover = part.rpartition(' ') unless full.empty?
|
198
|
+
if part.empty?
|
199
|
+
part = leftover.to_s
|
200
|
+
leftover = ''
|
201
|
+
else
|
202
|
+
full = leftover.to_s + full
|
203
|
+
full = full
|
204
|
+
end
|
205
|
+
parts.push(part.strip)
|
206
|
+
end
|
207
|
+
parts.join("\n")
|
208
|
+
else
|
209
|
+
elem.send(key)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
[headers, elems]
|
215
|
+
end
|
216
|
+
|
217
|
+
command 'exchange purchase' do |c|
|
218
|
+
c.syntax = %(murano exchange purchase [--options] <name-or-ID>)
|
219
|
+
c.summary = %(Add an Exchange Element to your Business)
|
220
|
+
c.description = %(
|
221
|
+
Add an Exchange Element to your Business.
|
222
|
+
).strip
|
223
|
+
# It feels a little weird to not require a project, but all
|
224
|
+
# we need is the Business ID; this action does not apply to
|
225
|
+
# solutions.
|
226
|
+
c.project_not_required = true
|
227
|
+
|
228
|
+
# Add --id and --name options.
|
229
|
+
cmd_options_add_id_and_name(c)
|
230
|
+
|
231
|
+
c.action do |args, options|
|
232
|
+
c.verify_arg_count!(args, 1, ['Missing Element name or ID'])
|
233
|
+
cmd_defaults_id_and_name(options)
|
234
|
+
|
235
|
+
xchg = MrMurano::Exchange.new
|
236
|
+
|
237
|
+
# If the user specifies filter_id, we could try to fetch that Element
|
238
|
+
# directly (e.g., by calling exchange/<bizId>/element/<elemId>),
|
239
|
+
# but the response doesn't specify if the Element is purchased or not.
|
240
|
+
# So we grab everything from /element/ and /purchase/.
|
241
|
+
|
242
|
+
elems, _available, purchased = find_elements(xchg, options, args[0])
|
243
|
+
if elems.length > 1
|
244
|
+
idents = elems.map { |elem| "#{xchg.fancy_ticks(elem.name)} (#{elem.elementId})" }
|
245
|
+
idents[-1] = 'and ' + idents[-1]
|
246
|
+
xchg.warning(
|
247
|
+
'Please be more specific: More than one matching element was found: ' \
|
248
|
+
"#{idents.join(', ')}"
|
249
|
+
)
|
250
|
+
exit 2
|
251
|
+
elsif elems.empty?
|
252
|
+
xchg.warning('No matching element was found.')
|
253
|
+
exit 2
|
254
|
+
elsif purchased.length == 1
|
255
|
+
# I.e., elems.status == :added
|
256
|
+
xchg.warning(
|
257
|
+
'The specified element has already been purchased: ' \
|
258
|
+
"#{xchg.fancy_ticks(purchased[0].name)} (#{purchased[0].elementId})"
|
259
|
+
)
|
260
|
+
exit 2
|
261
|
+
elsif elems.first.status == :upgrade
|
262
|
+
xchg.warning('Please upgrade your Business to add this Element. Visit:')
|
263
|
+
xchg.warning(' https://www.exosite.io/business/settings/upgrade')
|
264
|
+
exit 2
|
265
|
+
end
|
266
|
+
|
267
|
+
xchg.purchase(elems.first.elementId)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
alias_command 'exchange add', 'exchange purchase'
|
271
|
+
alias_command 'exchange buy', 'exchange purchase'
|
272
|
+
|