MuranoCLI 2.2.4 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.agignore +3 -0
- data/.gitignore +18 -1
- data/.rubocop.yml +222 -0
- data/.trustme.sh +185 -0
- data/.trustme.vim +24 -0
- data/Gemfile +23 -4
- data/LICENSE.txt +1 -1
- data/MuranoCLI.gemspec +43 -8
- data/README.markdown +9 -11
- data/Rakefile +187 -143
- data/TODO.taskpaper +2 -2
- data/bin/murano +51 -52
- data/docs/basic_example.rst +436 -0
- data/docs/completions/murano_completion-bash +3484 -0
- data/docs/demo.md +32 -32
- data/docs/develop.rst +391 -0
- data/lib/MrMurano.rb +21 -7
- data/lib/MrMurano/Account.rb +159 -174
- data/lib/MrMurano/Business.rb +381 -0
- data/lib/MrMurano/Config-Migrate.rb +32 -26
- data/lib/MrMurano/Config.rb +407 -128
- data/lib/MrMurano/Content.rb +191 -0
- data/lib/MrMurano/Gateway.rb +489 -0
- data/lib/MrMurano/Keystore.rb +48 -0
- data/lib/MrMurano/Passwords.rb +103 -0
- data/lib/MrMurano/ProjectFile.rb +121 -79
- data/lib/MrMurano/ReCommander.rb +114 -10
- data/lib/MrMurano/Setting.rb +90 -0
- data/lib/MrMurano/Solution-ServiceConfig.rb +89 -45
- data/lib/MrMurano/Solution-Services.rb +461 -166
- data/lib/MrMurano/Solution-Users.rb +70 -31
- data/lib/MrMurano/Solution.rb +372 -13
- data/lib/MrMurano/SolutionId.rb +73 -0
- data/lib/MrMurano/SyncRoot.rb +137 -0
- data/lib/MrMurano/SyncUpDown.rb +594 -284
- data/lib/MrMurano/Webservice-Cors.rb +71 -0
- data/lib/MrMurano/Webservice-Endpoint.rb +234 -0
- data/lib/MrMurano/Webservice-File.rb +193 -0
- data/lib/MrMurano/Webservice.rb +51 -0
- data/lib/MrMurano/commands.rb +18 -15
- data/lib/MrMurano/commands/business.rb +300 -6
- data/lib/MrMurano/commands/completion-bash.erb +166 -0
- data/lib/MrMurano/commands/{zshcomplete.erb → completion-zsh.erb} +0 -0
- data/lib/MrMurano/commands/completion.rb +76 -39
- data/lib/MrMurano/commands/config.rb +108 -44
- data/lib/MrMurano/commands/content.rb +115 -72
- data/lib/MrMurano/commands/cors.rb +29 -14
- data/lib/MrMurano/commands/devices.rb +286 -0
- data/lib/MrMurano/commands/domain.rb +52 -12
- data/lib/MrMurano/commands/gb.rb +24 -9
- data/lib/MrMurano/commands/globals.rb +64 -0
- data/lib/MrMurano/commands/init.rb +377 -155
- data/lib/MrMurano/commands/keystore.rb +92 -82
- data/lib/MrMurano/commands/link.rb +300 -0
- data/lib/MrMurano/commands/login.rb +74 -11
- data/lib/MrMurano/commands/logs.rb +63 -32
- data/lib/MrMurano/commands/mock.rb +57 -29
- data/lib/MrMurano/commands/password.rb +57 -39
- data/lib/MrMurano/commands/postgresql.rb +127 -94
- data/lib/MrMurano/commands/settings.rb +203 -0
- data/lib/MrMurano/commands/show.rb +79 -38
- data/lib/MrMurano/commands/solution.rb +423 -5
- data/lib/MrMurano/commands/solution_picker.rb +547 -0
- data/lib/MrMurano/commands/status.rb +195 -61
- data/lib/MrMurano/commands/sync.rb +78 -39
- data/lib/MrMurano/commands/timeseries.rb +71 -55
- data/lib/MrMurano/commands/tsdb.rb +113 -87
- data/lib/MrMurano/commands/usage.rb +57 -15
- data/lib/MrMurano/hash.rb +100 -10
- data/lib/MrMurano/http.rb +187 -43
- data/lib/MrMurano/makePretty.rb +16 -14
- data/lib/MrMurano/optparse.rb +2178 -0
- data/lib/MrMurano/progress.rb +138 -0
- data/lib/MrMurano/schema/resource-v1.0.0.yaml +32 -0
- data/lib/MrMurano/template/projectFile.murano.erb +16 -13
- data/lib/MrMurano/verbosing.rb +166 -29
- data/lib/MrMurano/version.rb +30 -1
- data/spec/Account-Passwords_spec.rb +21 -4
- data/spec/Account_spec.rb +69 -146
- data/spec/Business_spec.rb +290 -0
- data/spec/ConfigFile_spec.rb +1 -0
- data/spec/ConfigMigrate_spec.rb +12 -8
- data/spec/Config_spec.rb +40 -34
- data/spec/Content_spec.rb +363 -0
- data/spec/GatewayBase_spec.rb +54 -0
- data/spec/GatewayDevice_spec.rb +321 -0
- data/spec/GatewayResource_spec.rb +266 -0
- data/spec/GatewaySettings_spec.rb +120 -0
- data/spec/Http_spec.rb +18 -8
- data/spec/Mock_spec.rb +2 -2
- data/spec/ProjectFile_spec.rb +25 -14
- data/spec/Setting_spec.rb +110 -0
- data/spec/Solution-ServiceConfig_spec.rb +44 -5
- data/spec/Solution-ServiceEventHandler_spec.rb +23 -14
- data/spec/Solution-ServiceModules_spec.rb +47 -37
- data/spec/Solution-UsersRoles_spec.rb +10 -8
- data/spec/Solution_spec.rb +17 -8
- data/spec/SyncRoot_spec.rb +46 -20
- data/spec/SyncUpDown_spec.rb +437 -201
- data/spec/Verbosing_spec.rb +12 -4
- data/spec/{Solution-Cors_spec.rb → Webservice-Cors_spec.rb} +23 -20
- data/spec/{Solution-Endpoint_spec.rb → Webservice-Endpoint_spec.rb} +43 -41
- data/spec/{Solution-File_spec.rb → Webservice-File_spec.rb} +44 -33
- data/spec/Webservice-Setting_spec.rb +89 -0
- data/spec/_workspace.rb +4 -4
- data/spec/cmd_business_spec.rb +9 -4
- data/spec/cmd_common.rb +44 -1
- data/spec/cmd_content_spec.rb +43 -17
- data/spec/cmd_cors_spec.rb +4 -4
- data/spec/cmd_device_spec.rb +61 -16
- data/spec/cmd_domain_spec.rb +29 -6
- data/spec/cmd_init_spec.rb +281 -126
- data/spec/cmd_keystore_spec.rb +3 -3
- data/spec/cmd_link_spec.rb +98 -0
- data/spec/cmd_password_spec.rb +1 -1
- data/spec/cmd_setting_application_spec.rb +260 -0
- data/spec/cmd_setting_product_spec.rb +220 -0
- data/spec/cmd_status_spec.rb +223 -114
- data/spec/cmd_syncdown_spec.rb +115 -35
- data/spec/cmd_syncup_spec.rb +68 -15
- data/spec/cmd_usage_spec.rb +35 -8
- data/spec/fixtures/dumped_config +6 -4
- data/spec/fixtures/gateway_resource_files/resources.notyaml +12 -0
- data/spec/fixtures/gateway_resource_files/resources.yaml +13 -0
- data/spec/fixtures/gateway_resource_files/resources_invalid.yaml +13 -0
- data/spec/fixtures/mrmuranorc_deleted_bob +0 -2
- data/spec/fixtures/product_spec_files/lightbulb.yaml +20 -13
- data/spec/fixtures/{syncable_content → syncable_conflict}/services/devdata.lua +1 -1
- data/spec/fixtures/{syncable_content → syncable_conflict}/services/timers.lua +0 -0
- data/spec/spec_helper.rb +5 -0
- metadata +262 -171
- data/bin/mr +0 -8
- data/lib/MrMurano/Product-1P-Device.rb +0 -145
- data/lib/MrMurano/Product-Resources.rb +0 -205
- data/lib/MrMurano/Product.rb +0 -358
- data/lib/MrMurano/Solution-Cors.rb +0 -47
- data/lib/MrMurano/Solution-Endpoint.rb +0 -191
- data/lib/MrMurano/Solution-File.rb +0 -166
- data/lib/MrMurano/commands/assign.rb +0 -57
- data/lib/MrMurano/commands/businessList.rb +0 -45
- data/lib/MrMurano/commands/product.rb +0 -14
- data/lib/MrMurano/commands/productCreate.rb +0 -39
- data/lib/MrMurano/commands/productDelete.rb +0 -33
- data/lib/MrMurano/commands/productDevice.rb +0 -87
- data/lib/MrMurano/commands/productDeviceIdCmds.rb +0 -89
- data/lib/MrMurano/commands/productList.rb +0 -45
- data/lib/MrMurano/commands/productWrite.rb +0 -27
- data/lib/MrMurano/commands/solutionCreate.rb +0 -41
- data/lib/MrMurano/commands/solutionDelete.rb +0 -34
- data/lib/MrMurano/commands/solutionList.rb +0 -45
- data/spec/ProductBase_spec.rb +0 -113
- data/spec/ProductContent_spec.rb +0 -162
- data/spec/ProductResources_spec.rb +0 -329
- data/spec/Product_1P_Device_spec.rb +0 -202
- data/spec/Product_1P_RPC_spec.rb +0 -175
- data/spec/Product_spec.rb +0 -153
- data/spec/Solution-ServiceDevice_spec.rb +0 -176
- data/spec/cmd_assign_spec.rb +0 -51
@@ -1,16 +1,32 @@
|
|
1
|
-
|
2
|
-
|
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 '
|
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
|
13
|
-
|
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(
|
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,
|
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?
|
45
|
-
|
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,
|
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?
|
62
|
-
|
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,
|
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.
|
80
|
-
|
81
|
-
warning "Skipping missing #{from
|
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?
|
85
|
-
warning "Cannot read from #{from
|
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
|
-
|
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 <<
|
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, '
|
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 <<
|
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(
|
117
|
-
# TODO figure out APIs for updating users.
|
118
|
-
warning
|
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',
|
166
|
+
#SyncRoot.instance.add('users', User, 'U', false)
|
128
167
|
end
|
129
|
-
|
168
|
+
|
data/lib/MrMurano/Solution.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
18
|
-
|
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
|
56
|
+
def endpoint(path='')
|
57
|
+
super
|
25
58
|
parts = ['https:/', $cfg['net.host'], 'api:1'] + @uriparts
|
26
|
-
s = parts.map
|
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
|
-
|
52
|
-
|
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 :
|