MuranoCLI 3.0.1 → 3.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|