MuranoCLI 2.2.4 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/.agignore +3 -0
  3. data/.gitignore +18 -1
  4. data/.rubocop.yml +222 -0
  5. data/.trustme.sh +185 -0
  6. data/.trustme.vim +24 -0
  7. data/Gemfile +23 -4
  8. data/LICENSE.txt +1 -1
  9. data/MuranoCLI.gemspec +43 -8
  10. data/README.markdown +9 -11
  11. data/Rakefile +187 -143
  12. data/TODO.taskpaper +2 -2
  13. data/bin/murano +51 -52
  14. data/docs/basic_example.rst +436 -0
  15. data/docs/completions/murano_completion-bash +3484 -0
  16. data/docs/demo.md +32 -32
  17. data/docs/develop.rst +391 -0
  18. data/lib/MrMurano.rb +21 -7
  19. data/lib/MrMurano/Account.rb +159 -174
  20. data/lib/MrMurano/Business.rb +381 -0
  21. data/lib/MrMurano/Config-Migrate.rb +32 -26
  22. data/lib/MrMurano/Config.rb +407 -128
  23. data/lib/MrMurano/Content.rb +191 -0
  24. data/lib/MrMurano/Gateway.rb +489 -0
  25. data/lib/MrMurano/Keystore.rb +48 -0
  26. data/lib/MrMurano/Passwords.rb +103 -0
  27. data/lib/MrMurano/ProjectFile.rb +121 -79
  28. data/lib/MrMurano/ReCommander.rb +114 -10
  29. data/lib/MrMurano/Setting.rb +90 -0
  30. data/lib/MrMurano/Solution-ServiceConfig.rb +89 -45
  31. data/lib/MrMurano/Solution-Services.rb +461 -166
  32. data/lib/MrMurano/Solution-Users.rb +70 -31
  33. data/lib/MrMurano/Solution.rb +372 -13
  34. data/lib/MrMurano/SolutionId.rb +73 -0
  35. data/lib/MrMurano/SyncRoot.rb +137 -0
  36. data/lib/MrMurano/SyncUpDown.rb +594 -284
  37. data/lib/MrMurano/Webservice-Cors.rb +71 -0
  38. data/lib/MrMurano/Webservice-Endpoint.rb +234 -0
  39. data/lib/MrMurano/Webservice-File.rb +193 -0
  40. data/lib/MrMurano/Webservice.rb +51 -0
  41. data/lib/MrMurano/commands.rb +18 -15
  42. data/lib/MrMurano/commands/business.rb +300 -6
  43. data/lib/MrMurano/commands/completion-bash.erb +166 -0
  44. data/lib/MrMurano/commands/{zshcomplete.erb → completion-zsh.erb} +0 -0
  45. data/lib/MrMurano/commands/completion.rb +76 -39
  46. data/lib/MrMurano/commands/config.rb +108 -44
  47. data/lib/MrMurano/commands/content.rb +115 -72
  48. data/lib/MrMurano/commands/cors.rb +29 -14
  49. data/lib/MrMurano/commands/devices.rb +286 -0
  50. data/lib/MrMurano/commands/domain.rb +52 -12
  51. data/lib/MrMurano/commands/gb.rb +24 -9
  52. data/lib/MrMurano/commands/globals.rb +64 -0
  53. data/lib/MrMurano/commands/init.rb +377 -155
  54. data/lib/MrMurano/commands/keystore.rb +92 -82
  55. data/lib/MrMurano/commands/link.rb +300 -0
  56. data/lib/MrMurano/commands/login.rb +74 -11
  57. data/lib/MrMurano/commands/logs.rb +63 -32
  58. data/lib/MrMurano/commands/mock.rb +57 -29
  59. data/lib/MrMurano/commands/password.rb +57 -39
  60. data/lib/MrMurano/commands/postgresql.rb +127 -94
  61. data/lib/MrMurano/commands/settings.rb +203 -0
  62. data/lib/MrMurano/commands/show.rb +79 -38
  63. data/lib/MrMurano/commands/solution.rb +423 -5
  64. data/lib/MrMurano/commands/solution_picker.rb +547 -0
  65. data/lib/MrMurano/commands/status.rb +195 -61
  66. data/lib/MrMurano/commands/sync.rb +78 -39
  67. data/lib/MrMurano/commands/timeseries.rb +71 -55
  68. data/lib/MrMurano/commands/tsdb.rb +113 -87
  69. data/lib/MrMurano/commands/usage.rb +57 -15
  70. data/lib/MrMurano/hash.rb +100 -10
  71. data/lib/MrMurano/http.rb +187 -43
  72. data/lib/MrMurano/makePretty.rb +16 -14
  73. data/lib/MrMurano/optparse.rb +2178 -0
  74. data/lib/MrMurano/progress.rb +138 -0
  75. data/lib/MrMurano/schema/resource-v1.0.0.yaml +32 -0
  76. data/lib/MrMurano/template/projectFile.murano.erb +16 -13
  77. data/lib/MrMurano/verbosing.rb +166 -29
  78. data/lib/MrMurano/version.rb +30 -1
  79. data/spec/Account-Passwords_spec.rb +21 -4
  80. data/spec/Account_spec.rb +69 -146
  81. data/spec/Business_spec.rb +290 -0
  82. data/spec/ConfigFile_spec.rb +1 -0
  83. data/spec/ConfigMigrate_spec.rb +12 -8
  84. data/spec/Config_spec.rb +40 -34
  85. data/spec/Content_spec.rb +363 -0
  86. data/spec/GatewayBase_spec.rb +54 -0
  87. data/spec/GatewayDevice_spec.rb +321 -0
  88. data/spec/GatewayResource_spec.rb +266 -0
  89. data/spec/GatewaySettings_spec.rb +120 -0
  90. data/spec/Http_spec.rb +18 -8
  91. data/spec/Mock_spec.rb +2 -2
  92. data/spec/ProjectFile_spec.rb +25 -14
  93. data/spec/Setting_spec.rb +110 -0
  94. data/spec/Solution-ServiceConfig_spec.rb +44 -5
  95. data/spec/Solution-ServiceEventHandler_spec.rb +23 -14
  96. data/spec/Solution-ServiceModules_spec.rb +47 -37
  97. data/spec/Solution-UsersRoles_spec.rb +10 -8
  98. data/spec/Solution_spec.rb +17 -8
  99. data/spec/SyncRoot_spec.rb +46 -20
  100. data/spec/SyncUpDown_spec.rb +437 -201
  101. data/spec/Verbosing_spec.rb +12 -4
  102. data/spec/{Solution-Cors_spec.rb → Webservice-Cors_spec.rb} +23 -20
  103. data/spec/{Solution-Endpoint_spec.rb → Webservice-Endpoint_spec.rb} +43 -41
  104. data/spec/{Solution-File_spec.rb → Webservice-File_spec.rb} +44 -33
  105. data/spec/Webservice-Setting_spec.rb +89 -0
  106. data/spec/_workspace.rb +4 -4
  107. data/spec/cmd_business_spec.rb +9 -4
  108. data/spec/cmd_common.rb +44 -1
  109. data/spec/cmd_content_spec.rb +43 -17
  110. data/spec/cmd_cors_spec.rb +4 -4
  111. data/spec/cmd_device_spec.rb +61 -16
  112. data/spec/cmd_domain_spec.rb +29 -6
  113. data/spec/cmd_init_spec.rb +281 -126
  114. data/spec/cmd_keystore_spec.rb +3 -3
  115. data/spec/cmd_link_spec.rb +98 -0
  116. data/spec/cmd_password_spec.rb +1 -1
  117. data/spec/cmd_setting_application_spec.rb +260 -0
  118. data/spec/cmd_setting_product_spec.rb +220 -0
  119. data/spec/cmd_status_spec.rb +223 -114
  120. data/spec/cmd_syncdown_spec.rb +115 -35
  121. data/spec/cmd_syncup_spec.rb +68 -15
  122. data/spec/cmd_usage_spec.rb +35 -8
  123. data/spec/fixtures/dumped_config +6 -4
  124. data/spec/fixtures/gateway_resource_files/resources.notyaml +12 -0
  125. data/spec/fixtures/gateway_resource_files/resources.yaml +13 -0
  126. data/spec/fixtures/gateway_resource_files/resources_invalid.yaml +13 -0
  127. data/spec/fixtures/mrmuranorc_deleted_bob +0 -2
  128. data/spec/fixtures/product_spec_files/lightbulb.yaml +20 -13
  129. data/spec/fixtures/{syncable_content → syncable_conflict}/services/devdata.lua +1 -1
  130. data/spec/fixtures/{syncable_content → syncable_conflict}/services/timers.lua +0 -0
  131. data/spec/spec_helper.rb +5 -0
  132. metadata +262 -171
  133. data/bin/mr +0 -8
  134. data/lib/MrMurano/Product-1P-Device.rb +0 -145
  135. data/lib/MrMurano/Product-Resources.rb +0 -205
  136. data/lib/MrMurano/Product.rb +0 -358
  137. data/lib/MrMurano/Solution-Cors.rb +0 -47
  138. data/lib/MrMurano/Solution-Endpoint.rb +0 -191
  139. data/lib/MrMurano/Solution-File.rb +0 -166
  140. data/lib/MrMurano/commands/assign.rb +0 -57
  141. data/lib/MrMurano/commands/businessList.rb +0 -45
  142. data/lib/MrMurano/commands/product.rb +0 -14
  143. data/lib/MrMurano/commands/productCreate.rb +0 -39
  144. data/lib/MrMurano/commands/productDelete.rb +0 -33
  145. data/lib/MrMurano/commands/productDevice.rb +0 -87
  146. data/lib/MrMurano/commands/productDeviceIdCmds.rb +0 -89
  147. data/lib/MrMurano/commands/productList.rb +0 -45
  148. data/lib/MrMurano/commands/productWrite.rb +0 -27
  149. data/lib/MrMurano/commands/solutionCreate.rb +0 -41
  150. data/lib/MrMurano/commands/solutionDelete.rb +0 -34
  151. data/lib/MrMurano/commands/solutionList.rb +0 -45
  152. data/spec/ProductBase_spec.rb +0 -113
  153. data/spec/ProductContent_spec.rb +0 -162
  154. data/spec/ProductResources_spec.rb +0 -329
  155. data/spec/Product_1P_Device_spec.rb +0 -202
  156. data/spec/Product_1P_RPC_spec.rb +0 -175
  157. data/spec/Product_spec.rb +0 -153
  158. data/spec/Solution-ServiceDevice_spec.rb +0 -176
  159. data/spec/cmd_assign_spec.rb +0 -51
@@ -1,16 +1,32 @@
1
- require 'uri'
2
- require 'net/http'
1
+ # Last Modified: 2017.08.17 /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
+
3
8
  require 'json'
4
- require 'yaml'
9
+ require 'net/http'
5
10
  require 'pp'
11
+ require 'uri'
12
+ require 'yaml'
6
13
  require 'MrMurano/Solution'
14
+ #require 'MrMurano/SyncRoot'
7
15
 
8
16
  module MrMurano
9
17
  ##
10
18
  # User Management common things
11
19
  class UserBase < SolutionBase
12
- def list()
13
- get()
20
+ def initialize
21
+ super
22
+ end
23
+
24
+ def list
25
+ get
26
+ # MAYBE/2017-08-17:
27
+ # ret = get
28
+ # return [] unless ret.is_a?(Array)
29
+ # sort_by_name(ret)
14
30
  end
15
31
 
16
32
  def fetch(id)
@@ -22,18 +38,19 @@ module MrMurano
22
38
  end
23
39
 
24
40
  # @param modify Bool: True if item exists already and this is changing it
25
- def upload(local, remote, modify)
41
+ def upload(_local, remote, _modify)
26
42
  # Roles cannot be modified, so must delete and post.
27
43
  delete('/' + remote[@itemkey]) do |request, http|
28
44
  response = http.request(request)
29
45
  case response
46
+ # rubocop:disable Lint/EmptyWhen: Avoid when branches without a body.
30
47
  when Net::HTTPSuccess
31
48
  when Net::HTTPNotFound
32
49
  else
33
50
  showHttpError(request, response)
34
51
  end
35
52
  end
36
- remote.reject!{|k,v| k==:synckey or k==:bundled}
53
+ remote.reject! { |k, _v| %i[bundles synckey synctype].include? k }
37
54
  post('/', remote)
38
55
  end
39
56
 
@@ -41,16 +58,19 @@ module MrMurano
41
58
  # needs to append/merge with file
42
59
  # for now, we'll read, modify, write
43
60
  here = []
44
- if local.exist? then
45
- local.open('rb') {|io| here = YAML.load(io)}
61
+ if local.exist?
62
+ # FIXME/2017-07-18: Security/YAMLLoad: Prefer using YAML.safe_load over YAML.load.
63
+ # Disabling [rubo]cop for now.
64
+ # rubocop:disable Security/YAMLLoad
65
+ local.open('rb') { |io| here = YAML.load(io) }
46
66
  here = [] if here == false
47
67
  end
48
68
  here.delete_if do |i|
49
69
  Hash.transform_keys_to_symbols(i)[@itemkey] == item[@itemkey]
50
70
  end
51
- here << item.reject{|k,v| k==:synckey}
71
+ here << item.reject { |k, _v| %i[synckey synctype].include? k }
52
72
  local.open('wb') do |io|
53
- io.write here.map{|i| Hash.transform_keys_to_strings(i)}.to_yaml
73
+ io.write here.map { |i| Hash.transform_keys_to_strings(i) }.to_yaml
54
74
  end
55
75
  end
56
76
 
@@ -58,64 +78,83 @@ module MrMurano
58
78
  # needs to append/merge with file
59
79
  # for now, we'll read, modify, write
60
80
  here = []
61
- if local.exist? then
62
- local.open('rb') {|io| here = YAML.load(io)}
81
+ if local.exist?
82
+ # FIXME/2017-07-18: Security/YAMLLoad: Prefer using YAML.safe_load over YAML.load.
83
+ local.open('rb') { |io| here = YAML.load(io) }
63
84
  here = [] if here == false
64
85
  end
65
86
  key = @itemkey.to_sym
66
87
  here.delete_if do |it|
67
88
  Hash.transform_keys_to_symbols(it)[key] == item[key]
68
89
  end
69
- local.open('wb') do|io|
70
- io.write here.map{|i| Hash.transform_keys_to_strings(i)}.to_yaml
90
+ local.open('wb') do |io|
91
+ io.write here.map { |i| Hash.transform_keys_to_strings(i) }.to_yaml
71
92
  end
72
93
  end
73
94
 
74
- def tolocalpath(into, item)
95
+ def tolocalpath(into, _item)
75
96
  into
76
97
  end
77
98
 
78
99
  def localitems(from)
79
- from = Pathname.new(from) unless from.kind_of? Pathname
80
- if not from.exist? then
81
- warning "Skipping missing #{from.to_s}"
100
+ from = Pathname.new(from) unless from.is_a? Pathname
101
+ unless from.exist?
102
+ warning "Skipping missing #{from}"
82
103
  return []
83
104
  end
84
- unless from.file? then
85
- warning "Cannot read from #{from.to_s}"
105
+ unless from.file?
106
+ warning "Cannot read from #{from}"
86
107
  return []
87
108
  end
88
109
 
110
+ # MAYBE/2017-07-03: Do we care if there are duplicate keys in the yaml? See dup_count.
89
111
  here = []
90
- from.open {|io| here = YAML.load(io) }
112
+ # FIXME/2017-07-18: Security/YAMLLoad: Prefer using YAML.safe_load over YAML.load.
113
+ from.open { |io| here = YAML.load(io) }
91
114
  here = [] if here == false
92
115
 
93
- here.map{|i| Hash.transform_keys_to_symbols(i)}
116
+ here.map! { |i| Hash.transform_keys_to_symbols(i) }
117
+
118
+ sort_by_name(here)
94
119
  end
95
120
  end
96
121
 
97
122
  # …/role
98
123
  class Role < UserBase
99
124
  def initialize
125
+ @solntype = 'application.id'
126
+ #@solntype = 'product.id'
100
127
  super
101
- @uriparts << 'role'
128
+ @uriparts << :role
102
129
  @itemkey = :role_id
103
130
  end
131
+
132
+ def self.description
133
+ %(Role)
134
+ end
104
135
  end
105
- #SyncRoot.add('roles', Role, 'R', %{Roles})
136
+ #SyncRoot.instance.add('roles', Role, 'G', false)
106
137
 
107
138
  # …/user
108
139
  # :nocov:
109
140
  class User < UserBase
110
141
  def initialize
142
+ # 2017-07-03: [lb] tried 'product.id' and got 403 Forbidden;
143
+ # And I tried 'application.id' and get() returned an empty [].
144
+ @solntype = 'application.id'
145
+ #@solntype = 'product.id'
111
146
  super
112
- @uriparts << 'user'
147
+ @uriparts << :user
148
+ end
149
+
150
+ def self.description
151
+ %(User)
113
152
  end
114
153
 
115
154
  # @param modify Bool: True if item exists already and this is changing it
116
- def upload(local, remote, modify)
117
- # TODO figure out APIs for updating users.
118
- warning "Updating Users isn't working currently."
155
+ def upload(_local, _remote, _modify)
156
+ # TODO: figure out APIs for updating users.
157
+ warning %(Updating Users is not yet implemented.)
119
158
  # post does work if the :password field is set.
120
159
  end
121
160
 
@@ -124,6 +163,6 @@ module MrMurano
124
163
  end
125
164
  end
126
165
  # :nocov:
127
- #SyncRoot.add('users', User, 'U', %{Users})
166
+ #SyncRoot.instance.add('users', User, 'U', false)
128
167
  end
129
- # vim: set ai et sw=2 ts=2 :
168
+
@@ -1,59 +1,418 @@
1
+ # Last Modified: 2017.08.17 /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 'rainbow'
1
9
  require 'uri'
2
- require 'MrMurano/Config'
3
10
  require 'MrMurano/http'
4
11
  require 'MrMurano/verbosing'
12
+ require 'MrMurano/Config'
13
+ require 'MrMurano/SolutionId'
5
14
  require 'MrMurano/SyncUpDown'
6
15
 
7
16
  module MrMurano
8
17
  class SolutionBase
9
- def initialize
10
- @sid = $cfg['solution.id']
11
- raise MrMurano::ConfigError.new("No solution!") if @sid.nil?
18
+ include Http
19
+ include Verbose
20
+ include SolutionId
21
+
22
+ def initialize(from=nil)
23
+ @uriparts_sidex = 1
24
+ # Introspection. Feels hacky.
25
+ if from.is_a? MrMurano::Solution
26
+ init_sid!(from.sid)
27
+ @valid_sid = from.valid_sid
28
+ # We shouldn't need to worry about other things...
29
+ #@token = from.token
30
+ #@http = from.http
31
+ #@json_opts = from.json_opts
32
+ else
33
+ init_sid!(from)
34
+ end
12
35
  @uriparts = [:solution, @sid]
13
36
  @itemkey = :id
14
- @project_section = nil
37
+ @project_section = nil unless defined?(@project_section)
15
38
  end
16
39
 
17
- include Http
18
- include Verbose
40
+ def ==(other)
41
+ other.class == self.class && other.state == state
42
+ end
43
+
44
+ protected
45
+
46
+ def state
47
+ [@sid, @valid_sid, @uriparts, @solntype, @itemkey, @project_section]
48
+ end
49
+
50
+ public
19
51
 
20
52
  ## Generate an endpoint in Murano
21
53
  # Uses the uriparts and path
22
54
  # @param path String: any additional parts for the URI
23
55
  # @return URI: The full URI for this enpoint.
24
- def endPoint(path='')
56
+ def endpoint(path='')
57
+ super
25
58
  parts = ['https:/', $cfg['net.host'], 'api:1'] + @uriparts
26
- s = parts.map{|v| v.to_s}.join('/')
59
+ s = parts.map(&:to_s).join('/')
27
60
  URI(s + path.to_s)
28
61
  end
29
62
  # …
30
63
 
64
+ def get(path='', query=nil, &block)
65
+ aggregate = nil
66
+ total = nil
67
+ remaining = -1
68
+ orig_query = (query || []).dup
69
+ while remaining != 0
70
+ ret = super
71
+ if ret.nil? && !@suppress_error
72
+ warning "No solution with ID: #{@sid}"
73
+ whirly_interject { say 'Run `murano show` to see the business and list of solutions.' }
74
+ MrMurano::SolutionBase.warn_configfile_env_maybe
75
+ exit 1
76
+ end
77
+ return nil if ret.nil?
78
+ # Pagination: Check if more data.
79
+ if ret.is_a?(Hash) && ret.key?(:total) && ret.key?(:items)
80
+ query = orig_query.dup
81
+ if total.nil?
82
+ total = ret[:total]
83
+ remaining = total - ret[:items].length
84
+ # The response also includes a hint of how to get the next page.
85
+ # ret[:next] == "/api/v1/eventhandler?query={\
86
+ # \"solution_id\":\"XXXXXXXXXXXXXXXX\"}&limit=20&offset=20"
87
+ # But note that the URL we use is a little different
88
+ # https://bizapi.hosted.exosite.io/api:1/solution/XXXXXXXXXXXXXXXXX/eventhandler
89
+ else
90
+ if total != ret[:total]
91
+ warning "Unexpected: subsequence :total not total: #{ret[:total]} != #{total}"
92
+ end
93
+ remaining -= ret[:items].length
94
+ end
95
+ if remaining > 0
96
+ #query.push ['limit', 20]
97
+ query.push ['offset', total - remaining]
98
+ elsif remaining != 0
99
+ warning "Unexpected: negative remaining: ‘#{total}’"
100
+ remaining = 0
101
+ end
102
+ if aggregate.nil?
103
+ aggregate = ret
104
+ else
105
+ aggregate[:items].concat ret[:items]
106
+ end
107
+ else
108
+ # ret is not a hash, or it's missing :total or :items.
109
+ warning "Unexpected: aggregate set: #{aggregate} / ret: #{ret}" unless aggregate.nil?
110
+ aggregate = ret
111
+ remaining = 0
112
+ end
113
+ end
114
+ aggregate
115
+ end
116
+
117
+ # This at least works for EventHandler and ServiceConfig.
118
+ # - ServiceConfig overrides to fetch also 'script_key'.
119
+ def search(svc_name, path=nil)
120
+ # NOTE: You can ask the server to filter the list.
121
+ # E.g., the web UI filters with:
122
+ # ?select=service,id,solution_id,script_key,alias
123
+ # NOTE: ServiceConfig has 'script_key', but EventHandler does not.
124
+ # So a default filter would exclude 'script_key'.
125
+ # HOWEVER: As of 2017-06-28, there is no discernible change in
126
+ # processing time, so no real reason to ask server to filter
127
+ # the results.
128
+ #path = path || '?select=id,service'
129
+ matches = list(path)
130
+ matches.select { |match| match[:service] == svc_name }
131
+ end
132
+
133
+ def self.warn_configfile_env_maybe
134
+ if !$cfg.get('business.id', :env).to_s.empty? &&
135
+ !$cfg.get('business.id', :project).to_s.empty? &&
136
+ $cfg.get('business.id', :env) != $cfg.get('business.id', :project)
137
+ MrMurano::Verbose.warning(
138
+ 'NOTE: MURANO_CONFIGFILE specifies a different business.id than the local project file'
139
+ )
140
+ end
141
+ end
142
+
31
143
  include SyncUpDown
32
144
  end
33
145
 
34
146
  class Solution < SolutionBase
147
+ def initialize(sid=nil)
148
+ # Does it matter if we use :sid or :apiId?
149
+ meta = sid if sid.is_a?(Hash)
150
+ sid = sid[:sid] if sid.is_a?(Hash)
151
+ super(sid)
152
+ set_name
153
+ @meta = {}
154
+ @valid = false
155
+ self.meta = meta unless meta.nil?
156
+ end
157
+
158
+ # The Solution @name.
159
+ attr_reader :name
160
+
161
+ # A reference to the business account object.
162
+ attr_accessor :biz
163
+
164
+ attr_reader :meta
165
+
166
+ protected
167
+
168
+ def state
169
+ parts = super
170
+ parts + [@name, @meta, @valid]
171
+ end
172
+
173
+ public
174
+
175
+ # *** Network calls
176
+
35
177
  def version
36
178
  get('/version')
37
179
  end
38
180
 
39
181
  def info
40
- get()
182
+ get
183
+ end
184
+
185
+ def info_safe
186
+ @suppress_error = true
187
+ resp = get
188
+ if resp.is_a?(Hash) && !resp.key?(:error)
189
+ self.meta = resp
190
+ @valid_sid = true
191
+ else
192
+ self.meta = {}
193
+ @valid_sid = false
194
+ end
195
+ @suppress_error = false
41
196
  end
42
197
 
43
198
  def list
44
199
  get('/')
200
+ # MAYBE/2017-08-17:
201
+ # ret = get('/')
202
+ # return [] unless ret.is_a?(Array)
203
+ # sort_by_name(ret)
204
+ end
205
+
206
+ def usage
207
+ get('/usage')
45
208
  end
46
209
 
47
210
  def log
48
211
  get('/logs')
49
212
  end
50
213
 
51
- def usage
52
- get('/usage')
214
+ # *** Solution utils
215
+
216
+ def cfg_key_id
217
+ "#{type}.id"
53
218
  end
54
219
 
220
+ def cfg_key_name
221
+ "#{type}.name"
222
+ end
223
+
224
+ # meta is from the list of solutions fetched from business/<bizid>/solution/,
225
+ # e.g., from a call to solutions(), applications(), or products(); or it's
226
+ # from a call to info.
227
+ def meta=(data)
228
+ @meta = data
229
+ # Verify the solution ID.
230
+ # NOTE: The Solution info fetched from business/<bizid>/solutions endpoint
231
+ # includes the keys, :name, :sid, and :domain (see calls to solutions()).
232
+ # The solution details fetched from a call to Solution.get() include the
233
+ # keys, :name, :id, and :domain, among others.
234
+ # Note that the info() response does not include :type.
235
+ # Also, :apiId is indicated by solutions(), too.
236
+ sid = @meta[:sid] || @meta[:id] || nil
237
+ unless @meta[:apiId].to_s.empty?
238
+ if @meta[:apiId] != @meta[:sid]
239
+ warning "Unexpected: apiId != sid: #{@meta[:apiId]} != #{@meta[:sid]}"
240
+ end
241
+ end
242
+ unless @sid.to_s.empty? || sid.to_s == @sid.to_s
243
+ warning "#{type_name} ID mismatch. Server says ‘#{sid}’, but config says ‘#{@sid}’."
244
+ end
245
+ self.sid = sid
246
+ # Verify/set the name.
247
+ unless @name.to_s.empty? || @meta[:name].to_s == @name.to_s
248
+ warning "Name mismatch. Server says ‘#{@meta[:name]}’, but config says ‘#{@name}’."
249
+ end
250
+ if !@meta[:name].to_s.empty?
251
+ set_name(@meta[:name])
252
+ unless @valid_name || type == :solution
253
+ warning "Unexpected: Server returned invalid name: ‘#{@meta[:name]}’"
254
+ end
255
+ elsif @meta[:domain]
256
+ # This could be a pre-ADC/pre-Murano business.
257
+ warning "Unexpected: Server returned no name for domain: ‘#{@meta[:domain]}’"
258
+ else
259
+ warning "Unexpected: Server returned no name for solution: ‘#{@meta}’"
260
+ end
261
+ end
262
+
263
+ def domain
264
+ @meta[:domain]
265
+ end
266
+
267
+ def pretty_desc(add_type: false, raw_url: false)
268
+ # [lb] would normally put presentation code elsewhere (i.e., model
269
+ # classes should not be formatting output), but this seems okay.
270
+ desc = ''
271
+ desc += "#{type.to_s.capitalize}: " if add_type
272
+ name = self.name || '~Unnamed~'
273
+ sid = self.sid || '~No-ID~'
274
+ desc += "#{Rainbow(name).underline} <#{sid}>"
275
+ if domain
276
+ desc += ' '
277
+ desc += 'https://' unless raw_url
278
+ desc += domain
279
+ end
280
+ desc
281
+ end
282
+
283
+ def type
284
+ # info() doesn't return :type. So get from class name, e.g.,
285
+ # if soln.class == 'MrMurano::Product', type is :product.
286
+ #self.class.to_s.gsub(/^.*::/, '')
287
+ #raise 'Not implemented'
288
+ # Return, e.g., :application or :product.
289
+ self.class.to_s.gsub(/^.*::/, '').downcase.to_sym
290
+ end
291
+
292
+ def type_name
293
+ type.to_s.capitalize
294
+ end
295
+
296
+ # FIXME/Rubocop/2017-07-02: Style/AccessorMethodName
297
+ # Rename set_name, perhaps to apply_name?
298
+ # rubocop:disable Style/AccessorMethodName
299
+ def set_name(name=nil)
300
+ # Use duck typing instead of `is_a? String` to be more duck-like.
301
+ if name.respond_to?(:to_str) && name != ''
302
+ @name = name
303
+ # FIXME/Rubocop/2017-07-02: Double-negation
304
+ @valid_name = !@name.match(name_validate_regex).nil?
305
+ else
306
+ @name = ''
307
+ @valid_name = false
308
+ end
309
+ end
310
+
311
+ # FIXME/Rubocop/2017-07-02: Style/AccessorMethodName
312
+ # Or maybe no. Cannot create method `def name!=(name)` and I [lb]
313
+ # kinda like the bang!. You could call it apply_name!, perhaps.
314
+ def set_name!(name)
315
+ raise 'Expecting name, not nothing' unless name && name != ''
316
+ raise MrMurano::ConfigError.new(name_validate_help) unless name.match(name_validate_regex)
317
+ @name = name
318
+ @valid_name = true
319
+ end
320
+
321
+ def quoted_name
322
+ if @name.to_s.empty?
323
+ ''
324
+ else
325
+ "‘#{@name}’"
326
+ end
327
+ end
328
+
329
+ def valid?
330
+ @valid_sid && @valid_name
331
+ end
332
+
333
+ def valid_name?
334
+ @valid_name
335
+ end
336
+
337
+ def name_validate_regex
338
+ /^$/
339
+ end
340
+
341
+ def name_validate_help
342
+ ''
343
+ end
344
+ end
345
+
346
+ class Application < Solution
347
+ def initialize(sid=nil)
348
+ @solntype = 'application.id'
349
+ super
350
+ end
351
+
352
+ # FIXME/2017-07-02: Test long names:
353
+ # Murano Appl:
354
+ # /^[a-zA-Z0-9-\s]{1,63}$/
355
+ # E.g., longest acceptable name:
356
+ # #ABCdefGHIjklMNOpqrSTUvwxYZAbcdefghijklmnopqrstuvwxyz34567890123
357
+ # 99-Party-TIME-XXX-YOU-BETCHA-letsallridebikes4ever-and-4ever111
358
+ # Murano Prod:
359
+ # /^(?![0-9])[a-zA-Z0-9]{2,63}$/
360
+ # Yassssssssssssssssssss11111111111111111111111111111111111111111
361
+ # Either (should be too long):
362
+ # abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz345678901234567890123456789
363
+
364
+ # FIXME/2017-06-28: Test uppercase characters again.
365
+
366
+ # SYNC_ME: See regex in bizapi: lib/api/route/business/solution.js
367
+ def name_validate_regex
368
+ /^[a-zA-Z0-9\-\s]{1,63}$/
369
+ end
370
+
371
+ def name_validate_help
372
+ %(
373
+ The Application name may only contain letters, numbers, and dashes.
374
+ The name must contain at least 1 character and no more than 63.
375
+ ).strip
376
+ end
55
377
  end
56
378
 
379
+ class Product < Solution
380
+ def initialize(sid=nil)
381
+ # Code path for `murano domain`.
382
+ @solntype = 'product.id'
383
+ super
384
+ end
385
+
386
+ # SYNC_ME: See regex in bizapi: lib/api/route/business/solution.js
387
+ def name_validate_regex
388
+ /^(?![0-9])[a-zA-Z0-9]{2,63}$/
389
+ end
390
+
391
+ def name_validate_help
392
+ %(
393
+ The Product name may contain only letters and numbers, and the name may
394
+ not start with a number. The name must contain at least 3 characters and
395
+ no more than 63.
396
+ ).strip
397
+ end
398
+ end
399
+ end
400
+
401
+ def solution_factory_reset(sol)
402
+ new_sol = nil
403
+ if sol.is_a? MrMurano::Solution
404
+ unless sol.meta[:template].to_s.empty?
405
+ begin
406
+ clazz = Object.const_get("MrMurano::#{sol.meta[:template].capitalize}")
407
+ new_sol = clazz.new(sol)
408
+ new_sol.meta = sol.meta
409
+ rescue NameError => _err
410
+ MrMurano::Verbose.warning(
411
+ "Unrecognized solution :template value: #{sol.meta[:template]}"
412
+ )
413
+ end
414
+ end
415
+ end
416
+ new_sol || sol
57
417
  end
58
418
 
59
- # vim: set ai et sw=2 ts=2 :