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,47 +1,53 @@
1
- require 'pathname'
1
+ # Last Modified: 2017.08.16 /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
+
2
8
  require 'fileutils'
9
+ require 'pathname'
3
10
  require 'yaml'
4
11
  require 'MrMurano/verbosing'
5
12
  require 'MrMurano/Account'
6
13
  require 'MrMurano/Config'
7
14
 
8
-
9
15
  module MrMurano
10
16
  class ConfigMigrate
11
17
  include Verbose
12
18
 
13
19
  def import_secret
14
20
  solsecret = Pathname.new($cfg['location.base']) + '.Solutionfile.secret'
15
- if solsecret.exist? then
16
- # Is in JSON, which as a subset of YAML, so use YAML parser
17
- solsecret.open do |io|
18
- ss = YAML.load(io)
19
-
20
- pff = $cfg.file_at('passwords', :user)
21
- pwd = MrMurano::Passwords.new(pff)
22
- pwd.load
23
- ps = pwd.get($cfg['net.host'], ss['email'])
24
- if ps.nil? then
21
+ return unless solsecret.exist?
22
+ # Is in JSON, which as a subset of YAML, so use YAML parser
23
+ solsecret.open do |io|
24
+ # rubocop:disable Security/YAMLLoad: Prefer using YAML.safe_load over YAML.load.
25
+ # MAYBE/2017-07-02: Switch to YAML.safe_load.
26
+ ss = YAML.load(io)
27
+
28
+ pff = $cfg.file_at('passwords', :user)
29
+ pwd = MrMurano::Passwords.new(pff)
30
+ pwd.load
31
+ ps = pwd.get($cfg['net.host'], ss['email'])
32
+ if ps.nil?
33
+ pwd.set($cfg['net.host'], ss['email'], ss['password'])
34
+ pwd.save
35
+ elsif ps != ss['password']
36
+ y = ask('A different password for this account already exists. Overwrite? N/y')
37
+ if y =~ /^y/i
25
38
  pwd.set($cfg['net.host'], ss['email'], ss['password'])
26
39
  pwd.save
27
- elsif ps != ss['password'] then
28
- y = ask("A different password for this account already exists. Overwrite? N/y")
29
- if y =~ /^y/i then
30
- pwd.set($cfg['net.host'], ss['email'], ss['password'])
31
- pwd.save
32
- end
33
- else
34
- # already set, nothing to do.
35
40
  end
36
-
37
- $cfg.set('user.name', ss['email'])
38
- $cfg.set('solution.id', ss['solution_id']) if ss.has_key? 'solution_id'
39
- $cfg.set('product.id', ss['product_id']) if ss.has_key? 'product_id'
41
+ # else, already set, nothing to do.
40
42
  end
43
+
44
+ $cfg.set('user.name', ss['email'])
45
+ $project.refresh_user_name
46
+
47
+ $cfg.set('application.id', ss['solution_id']) if ss.key? 'solution_id'
48
+ $cfg.set('product.id', ss['product_id']) if ss.key? 'product_id'
41
49
  end
42
50
  end
43
-
44
51
  end
45
52
  end
46
53
 
47
- # vim: set ai et sw=2 ts=2 :
@@ -1,109 +1,147 @@
1
- require 'pathname'
2
- require 'inifile'
1
+ # Last Modified: 2017.08.18 /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 'highline'
9
+ require 'inifile'
10
+ require 'pathname'
11
+ require 'rainbow'
12
+ require 'MrMurano/verbosing'
13
+ require 'MrMurano/SyncRoot'
4
14
 
5
15
  module MrMurano
6
16
  class Config
7
- #
8
- # internal transient this-run-only things (also -c options)
9
- # specified from --configfile
10
- # env from ENV['MURANO_CONFIGFILE']
11
- # project .murano/config at project dir
12
- # user .murano/config at $HOME
13
- # defaults Internal hardcoded defaults
14
- #
17
+ include Verbose
18
+
19
+ # Config scopes:
20
+ # :internal transient this-run-only things (also -c options)
21
+ # :specified from --configfile
22
+ # :env from ENV['MURANO_CONFIGFILE']
23
+ # :project .murano/config at project dir
24
+ # :user .murano/config at $HOME
25
+ # :defaults Internal hardcoded defaults
26
+ # NOTE: This list is ordered, such that values stored in upper scopes
27
+ # mask values of the same keys in the lower scopes.
28
+ CFG_SCOPES = %i[internal specified env project user defaults].freeze
29
+
15
30
  ConfigFile = Struct.new(:kind, :path, :data) do
16
- def load()
31
+ def load
17
32
  return if kind == :internal
18
33
  return if kind == :defaults
19
- self[:path] = Pathname.new(path) unless path.kind_of? Pathname
20
- self[:data] = IniFile.new(:filename=>path.to_s) if self[:data].nil?
34
+ # DEVs: Uncomment if you're trying to figure where settings are coming from.
35
+ # See also: murano config --locations
36
+ #puts "Loading config at: #{path}"
37
+ self[:path] = Pathname.new(path) unless path.is_a? Pathname
38
+ self[:data] = IniFile.new(filename: path.to_s) if self[:data].nil?
21
39
  self[:data].restore
22
40
  end
23
41
 
24
- def write()
42
+ def write
25
43
  return if kind == :internal
26
44
  return if kind == :defaults
27
- self[:path] = Pathname.new(path) unless path.kind_of? Pathname
28
- self[:data] = IniFile.new(:filename=>path.to_s) if self[:data].nil?
45
+ if defined?($cfg) && !$cfg.nil? && $cfg['tool.dry']
46
+ # $cfg.nil? when run from spec tests that don't load it with:
47
+ # include_context "CI_CMD"
48
+ MrMurano::Verbose.warning('--dry: Not writing config file')
49
+ return
50
+ end
51
+ self[:path] = Pathname.new(path) unless path.is_a?(Pathname)
52
+ # Ensure path to the file exists.
53
+ unless path.dirname.exist?
54
+ path.dirname.mkpath
55
+ MrMurano::Config.fix_modes(path.dirname)
56
+ end
57
+ self[:data] = IniFile.new(filename: path.to_s) if self[:data].nil?
29
58
  self[:data].save
30
- path.chmod(0600)
59
+ path.chmod(0o600)
31
60
  end
32
61
  end
33
62
 
34
- attr :paths
63
+ attr_reader :paths
35
64
  attr_reader :projectDir
65
+ attr_reader :project_exists
66
+ attr_writer :exclude_scopes
67
+ attr_accessor :curlfile_f
36
68
 
37
- CFG_SCOPES=%w{internal specified env project user defaults}.map{|i| i.to_sym}.freeze
69
+ CFG_ENV_NAME = %(MURANO_CONFIGFILE)
70
+ CFG_FILE_NAME = %(.murano/config)
71
+ CFG_DIR_NAME = %(.murano)
38
72
 
39
- CFG_ENV_NAME=%{MURANO_CONFIGFILE}.freeze
40
- CFG_FILE_NAME=%[.murano/config].freeze
41
- CFG_DIR_NAME=%[.murano].freeze
73
+ CFG_OLD_ENV_NAME = %(MR_CONFIGFILE)
74
+ CFG_OLD_DIR_NAME = %(.mrmurano)
75
+ CFG_OLD_FILE_NAME = %(.mrmuranorc)
42
76
 
43
- CFG_OLD_ENV_NAME=%[MR_CONFIGFILE].freeze
44
- CFG_OLD_DIR_NAME=%[.mrmurano].freeze
45
- CFG_OLD_FILE_NAME=%[.mrmuranorc].freeze
77
+ CFG_SOLUTION_ID_KEYS = %w[application.id product.id].freeze
46
78
 
47
- def warning(msg)
48
- $stderr.puts HighLine.color(msg, :yellow)
49
- end
50
- def error(msg)
51
- $stderr.puts HighLine.color(msg, :red)
52
- end
53
-
54
- def migrateOldEnv
55
- unless ENV[CFG_OLD_ENV_NAME].nil? then
56
- warning %{ENV "#{CFG_OLD_ENV_NAME}" is no longer supported. Rename it to "#{CFG_ENV_NAME}"}
57
- unless ENV[CFG_ENV_NAME].nil? then
58
- error %{Both "#{CFG_ENV_NAME}" and "#{CFG_OLD_ENV_NAME}" defined, please remove "#{CFG_OLD_ENV_NAME}".}
59
- end
60
- ENV[CFG_ENV_NAME] = ENV[CFG_OLD_ENV_NAME]
79
+ def migrate_old_env
80
+ return if ENV[CFG_OLD_ENV_NAME].nil?
81
+ warning %(ENV "#{CFG_OLD_ENV_NAME}" is no longer supported. Rename it to "#{CFG_ENV_NAME}")
82
+ unless ENV[CFG_ENV_NAME].nil?
83
+ warning %(Both "#{CFG_ENV_NAME}" and "#{CFG_OLD_ENV_NAME}" defined, please remove "#{CFG_OLD_ENV_NAME}".)
61
84
  end
85
+ ENV[CFG_ENV_NAME] = ENV[CFG_OLD_ENV_NAME]
62
86
  end
63
87
 
64
- def migrateOldConfig(where)
88
+ def migrate_old_config(where)
65
89
  # Check for dir.
66
- if (where + CFG_OLD_DIR_NAME).exist? then
67
- warning %{Moving old directory "#{CFG_OLD_DIR_NAME}" to "#{CFG_DIR_NAME}" in "#{where}"}
90
+ if (where + CFG_OLD_DIR_NAME).exist?
91
+ warning %(Moving old directory "#{CFG_OLD_DIR_NAME}" to "#{CFG_DIR_NAME}" in "#{where}")
68
92
  (where + CFG_OLD_DIR_NAME).rename(where + CFG_DIR_NAME)
69
93
  end
70
94
 
71
- # check for cfg.
72
- if (where + CFG_OLD_FILE_NAME).exist? then
73
- warning %{Moving old config "#{CFG_OLD_FILE_NAME}" to "#{CFG_FILE_NAME}" in "#{where}"}
95
+ # Check for cfg.
96
+ # rubocop:disable Style/GuardClause
97
+ if (where + CFG_OLD_FILE_NAME).exist?
98
+ warning %(Moving old config "#{CFG_OLD_FILE_NAME}" to "#{CFG_FILE_NAME}" in "#{where}")
74
99
  (where + CFG_DIR_NAME).mkpath
75
100
  (where + CFG_OLD_FILE_NAME).rename(where + CFG_FILE_NAME)
76
101
  end
77
102
  end
78
103
 
79
- def initialize
104
+ def initialize(cmd_runner=nil)
105
+ @runner = cmd_runner
106
+ @curlfile_f = nil
107
+
80
108
  @paths = []
81
- @paths << ConfigFile.new(:internal, nil, IniFile.new())
109
+ @paths << ConfigFile.new(:internal, nil, IniFile.new)
82
110
  # :specified --configfile FILE goes here. (see load_specific)
83
111
 
84
- migrateOldEnv
85
- unless ENV[CFG_ENV_NAME].nil? then
112
+ migrate_old_env
113
+ unless ENV[CFG_ENV_NAME].nil?
86
114
  # if it exists, must be a file
87
115
  # if it doesn't exist, that's ok
88
116
  ep = Pathname.new(ENV[CFG_ENV_NAME])
89
- if ep.file? or not ep.exist? then
90
- @paths << ConfigFile.new(:env, ep)
91
- end
117
+ @paths << ConfigFile.new(:env, ep) if ep.file? || !ep.exist?
92
118
  end
93
119
 
94
- @projectDir = findProjectDir()
95
- migrateOldConfig(@projectDir)
96
- @paths << ConfigFile.new(:project, @projectDir + CFG_FILE_NAME)
97
- (@projectDir + CFG_DIR_NAME).mkpath
98
- fixModes(@projectDir + CFG_DIR_NAME)
120
+ @project_dir, @project_exists = find_project_dir
121
+ # For murano init, do not use parent config file as project config.
122
+ if !@runner.nil? && @runner.active_command.restrict_to_cur_dir
123
+ pwd = Pathname.new(Dir.pwd).realpath
124
+ if @project_dir != pwd
125
+ @project_dir = pwd
126
+ @project_exists = false
127
+ end
128
+ end
129
+ @paths << ConfigFile.new(:project, @project_dir + CFG_FILE_NAME)
99
130
 
100
- migrateOldConfig(Pathname.new(Dir.home))
101
131
  @paths << ConfigFile.new(:user, Pathname.new(Dir.home) + CFG_FILE_NAME)
102
- (Pathname.new(Dir.home) + CFG_DIR_NAME).mkpath
103
- fixModes(Pathname.new(Dir.home) + CFG_DIR_NAME)
104
132
 
105
- @paths << ConfigFile.new(:defaults, nil, IniFile.new())
133
+ @paths << ConfigFile.new(:defaults, nil, IniFile.new)
106
134
 
135
+ # The user can exclude certain scopes.
136
+ @exclude_scopes = []
137
+
138
+ set_defaults
139
+ end
140
+
141
+ def set_defaults
142
+ # All these set()'s are against the :defaults config.
143
+ # So no disk writing ensues. And these serve as defaults
144
+ # unless, say, a SolutionFile says otherwise.
107
145
 
108
146
  set('tool.verbose', false, :defaults)
109
147
  set('tool.debug', false, :defaults)
@@ -113,31 +151,85 @@ module MrMurano
113
151
 
114
152
  set('net.host', 'bizapi.hosted.exosite.io', :defaults)
115
153
 
116
- set('location.base', @projectDir, :defaults) unless @projectDir.nil?
154
+ set('location.base', @project_dir, :defaults) unless @project_dir.nil?
117
155
  set('location.files', 'files', :defaults)
118
156
  set('location.endpoints', 'routes', :defaults)
119
157
  set('location.modules', 'modules', :defaults)
120
158
  set('location.eventhandlers', 'services', :defaults)
121
- set('location.specs', 'specs/resources.yaml', :defaults)
159
+ set('location.resources', 'specs/resources.yaml', :defaults)
122
160
  set('location.cors', 'cors.yaml', :defaults)
123
161
 
124
- set('sync.bydefault', SyncRoot.bydefault.join(' '), :defaults) if defined? SyncRoot
162
+ set('sync.bydefault', SyncRoot.instance.bydefault.join(' '), :defaults) if defined? SyncRoot
125
163
 
126
164
  set('files.default_page', 'index.html', :defaults)
127
165
  set('files.searchFor', '**/*', :defaults)
128
166
  set('files.ignoring', '', :defaults)
129
167
 
130
- set('endpoints.searchFor', '{,../endpoints}/*.lua {,../endpoints}s/*/*.lua', :defaults)
131
- set('endpoints.ignoring', '*_test.lua *_spec.lua .*', :defaults)
132
-
133
- set('eventhandler.searchFor', '*.lua */*.lua {../eventhandlers,../event_handler}/*.lua {../eventhandlers,../event_handler}/*/*.lua', :defaults)
134
- set('eventhandler.ignoring', '*_test.lua *_spec.lua .*', :defaults)
135
- set('eventhandler.skiplist', 'websocket webservice device.service_call', :defaults)
136
-
137
- set('modules.searchFor', '*.lua */*.lua', :defaults)
138
- set('modules.ignoring', '*_test.lua *_spec.lua .*', :defaults)
139
-
140
- if Gem.win_platform? then
168
+ set('endpoints.searchFor', %w[
169
+ {,../endpoints}/*.lua
170
+ {,../endpoints}s/*/*.lua
171
+ ].join(' '), :defaults)
172
+ set('endpoints.ignoring', %w[
173
+ *_test.lua
174
+ *_spec.lua
175
+ .*
176
+ ].join(' '), :defaults)
177
+
178
+ set('eventhandler.searchFor', %w[
179
+ *.lua
180
+ */*.lua
181
+ {../eventhandlers,../event_handler}/*.lua
182
+ {../eventhandlers,../event_handler}/*/*.lua
183
+ ].join(' '), :defaults)
184
+ set('eventhandler.ignoring', %w[
185
+ *_test.lua
186
+ *_spec.lua
187
+ .*
188
+ ].join(' '), :defaults)
189
+ # 2017-08-07: device.datapoint is the v1 device event handler.
190
+ # It is deprecated and will be removed in 12 months.
191
+ # So it technically still works, but eventually will not.
192
+ # CAVEAT: One must manually enable the Device V1 service for a Murano 1.1 business.
193
+ # device2.event is the event handler that is created when two solutions
194
+ # are linked (say, an application and a product). Do not delete this.
195
+ # The interface service contains lots of simple device event handlers
196
+ # that we don't want to touch.
197
+ set('eventhandler.skiplist', %w[
198
+ device.service_call
199
+ device2.event
200
+ interface
201
+ webservice
202
+ websocket
203
+ ].join(' '), :defaults)
204
+ # Do not delete boilerplate event handlers.
205
+ set('eventhandler.undeletable', %w[
206
+ *.event
207
+ timer.timer
208
+ tsdb.exportJob
209
+ user.account
210
+ ].join(' '), :defaults)
211
+
212
+ # 2017-07-26: Nested Lua support: Change '*/*.lua' to '**/*.lua'.
213
+ # There are similar changes made to the ProjectFile,
214
+ # to modules.include and modules.exclude, which, if set,
215
+ # override modules.searchFor and modules.ignoring, respectively.
216
+ # NOTE: ** finds files in subdirs in subdirs,
217
+ # e.g., modules/subdir1/subdir2/<here>/and/<here>
218
+ # otherwise, */*.lua just finds files modules/subdir1/<here>.
219
+ set('modules.searchFor', %w[
220
+ *.lua
221
+ **/*.lua
222
+ ].join(' '), :defaults)
223
+
224
+ set('modules.ignoring', %w[
225
+ *_test.lua
226
+ *_spec.lua
227
+ .*
228
+ ].join(' '), :defaults)
229
+
230
+ set('modules.no-nesting', false, :defaults)
231
+
232
+ if Gem.win_platform?
141
233
  set('diff.cmd', 'fc', :defaults)
142
234
  else
143
235
  set('diff.cmd', 'diff -u', :defaults)
@@ -148,46 +240,75 @@ module MrMurano
148
240
 
149
241
  ## Find the root of this project Directory.
150
242
  #
151
- # The Project dir is the directory between PWD and HOME that has one of (in
152
- # order of preference):
243
+ # The Project dir is the directory between PWD and HOME
244
+ # that has one of (in order of preference):
153
245
  # - .murano/config
154
246
  # - .mrmuranorc
155
247
  # - .murano/
156
248
  # - .mrmurano/
157
- def findProjectDir()
158
- result=nil
159
- fileNames=[CFG_FILE_NAME, CFG_OLD_FILE_NAME]
160
- dirNames=[CFG_DIR_NAME, CFG_OLD_DIR_NAME]
249
+ def find_project_dir
250
+ file_names = [CFG_FILE_NAME, CFG_OLD_FILE_NAME]
251
+ dir_names = [CFG_DIR_NAME, CFG_OLD_DIR_NAME]
161
252
  home = Pathname.new(Dir.home).realpath
162
253
  pwd = Pathname.new(Dir.pwd).realpath
163
- return home if home == pwd
164
- pwd.ascend do |i|
165
- break unless result.nil?
166
- break if i == home
167
- fileNames.each do |f|
168
- if (i + f).exist? then
169
- result = i
170
- end
254
+ # The home directory contains the user ~/.murano/config,
255
+ # so we cannot also have a project .murano/ directory.
256
+ return home, false if home == pwd
257
+ pwd.ascend do |path|
258
+ # Don't bother with home or looking above it.
259
+ break if path == home
260
+ file_names.each do |fname|
261
+ return path, true if (path + fname).exist?
171
262
  end
172
- dirNames.each do |f|
173
- if (i + f).directory? then
174
- result = i
175
- end
263
+ dir_names.each do |dname|
264
+ return path, true if (path + dname).directory?
176
265
  end
177
266
  end
178
-
179
267
  # Now if nothing found, assume it will live in pwd.
180
- result = Pathname.new(Dir.pwd) if result.nil?
181
- return result
268
+ result = Pathname.new(Dir.pwd)
269
+ [result, false]
270
+ end
271
+ private :find_project_dir
272
+
273
+ def prompt_if_logged_off
274
+ # MAYBE/2017-07-31: [lb] likes the idea of only prompting for the
275
+ # password for certain commands (e.g., `murano login` and `murano init`),
276
+ # but this might break a user's experience if they don't want to store
277
+ # their password in ~/.murano but instead always want to be prompted.
278
+ #!@runner.nil? && @runner.active_command.prompt_if_logged_off
279
+ # Disabling for now...
280
+ true
182
281
  end
183
- private :findProjectDir
184
282
 
185
- def fixModes(path)
186
- if path.directory? then
187
- path.chmod(0700)
188
- elsif path.file? then
189
- path.chmod(0600)
283
+ def validate_cmd
284
+ # Most commands should be run from within a Murano project (sub-)directory.
285
+ # If user is running a project command not within a project directory,
286
+ # we'll print a message now and exit the app from run_active_command later.
287
+ unless @runner.nil?
288
+ the_cmd = @runner.active_command
289
+ unless the_cmd.name == 'help' || the_cmd.project_not_required || @project_exists
290
+ error %(The "#{the_cmd.name}" command only works in a Murano project.)
291
+ say %(Please change to a project directory, or run `murano init` to create a new project.)
292
+ # Note that commnander-rb uses an at_exit hook, which we hack around.
293
+ @runner.command_exit = 1
294
+ return
295
+ end
190
296
  end
297
+
298
+ migrate_old_config(@project_dir)
299
+ migrate_old_config(Pathname.new(Dir.home))
300
+ end
301
+
302
+ def self.fix_modes(path)
303
+ if path.directory?
304
+ path.chmod(0o700)
305
+ elsif path.file?
306
+ path.chmod(0o600)
307
+ end
308
+ end
309
+
310
+ def fix_modes(path)
311
+ MrMurano::Config.fix_modes(path)
191
312
  end
192
313
 
193
314
  def file_at(name, scope=:project)
@@ -197,7 +318,7 @@ module MrMurano
197
318
  when :specified
198
319
  root = nil
199
320
  when :project
200
- root = @projectDir + CFG_DIR_NAME
321
+ root = @project_dir + CFG_DIR_NAME
201
322
  when :user
202
323
  root = Pathname.new(Dir.home) + CFG_DIR_NAME
203
324
  when :defaults
@@ -209,9 +330,11 @@ module MrMurano
209
330
  end
210
331
 
211
332
  ## Load all of the potential config files
212
- def load()
333
+ def load
213
334
  # - read/write config file in [Project, User, System] (all are optional)
214
- @paths.each { |cfg| cfg.load }
335
+ @paths.each(&:load)
336
+ # If user wants curl commands dumped to a file, open that file.
337
+ init_curl_file
215
338
  end
216
339
 
217
340
  ## Load specified file into the config stack
@@ -222,52 +345,115 @@ module MrMurano
222
345
  @paths.insert(1, spc)
223
346
  end
224
347
 
225
- ## Get a value for key, looking at the specificed scopes
348
+ ## Get a value for key, looking at the specified scopes
226
349
  # key is <section>.<key>
227
350
  def get(key, scope=CFG_SCOPES)
228
- scope = [scope] unless scope.kind_of? Array
229
- paths = @paths.select{|p| scope.include? p.kind}
351
+ scope = [scope] unless scope.is_a? Array
352
+ paths = @paths.select { |p| scope.include? p.kind }
353
+ paths = paths.reject { |p| @exclude_scopes.include? p.kind }
354
+
355
+ section, ikey = key.split('.')
356
+ paths.each do |path|
357
+ next unless path.data.has_section?(section)
358
+ sec = path.data[section]
359
+ return sec if ikey.nil?
360
+ return sec[ikey] if sec.key?(ikey)
361
+ end
362
+ nil
363
+ end
364
+
365
+ ## Get one or more key-values for the specified scopes, honoring '*' wildcard.
366
+ # key is <section>.<key>, <section>.*, or *.<key>
367
+ def get_wild(key, scope=CFG_SCOPES)
368
+ scope = [scope] unless scope.is_a? Array
369
+ paths = @paths.select { |p| scope.include? p.kind }
370
+ paths = paths.reject { |p| @exclude_scopes.include? p.kind }
230
371
 
372
+ seen = {}
373
+
374
+ kvals = []
375
+ is_wild = wild?(key)
231
376
  section, ikey = key.split('.')
232
377
  paths.each do |path|
233
- if path.data.has_section?(section) then
378
+ if section != '*' || !is_wild
379
+ next unless path.data.has_section?(section)
234
380
  sec = path.data[section]
235
- return sec if ikey.nil?
236
- if sec.has_key?(ikey) then
237
- return sec[ikey]
381
+ if ikey != '*' || !is_wild
382
+ # blah.blug query
383
+ if !ikey.nil?
384
+ next unless sec.key?(ikey)
385
+ kvals += [["#{section}.#{ikey}", sec[ikey], path.kind]]
386
+ else
387
+ sec.each do |skey|
388
+ kvals += [["#{section}.#{skey}", sec[skey], path.kind]]
389
+ end
390
+ end
391
+ return kvals
392
+ else
393
+ # blah.* query
394
+ sec.each do |kv|
395
+ kvid = "#{section}.#{kv[0]}"
396
+ next if seen[kvid]
397
+ seen[kvid] = true
398
+ kvals += [[kvid, kv[1].to_s, path.kind]]
399
+ end
400
+ end
401
+ else
402
+ # *.blah query
403
+ path.data.each do |ini|
404
+ path.data[ini].keys.each do |skey|
405
+ next unless ikey == '*' || skey == ikey
406
+ kvid = "#{ini}.#{skey}"
407
+ next if seen[kvid]
408
+ seen[kvid] = true
409
+ kvals += [[kvid, path.data[ini][skey].to_s, path.kind]]
410
+ end
238
411
  end
239
412
  end
240
413
  end
241
- return nil
414
+ kvals.sort_by! { |kv| kv[0] }
415
+ kvals
242
416
  end
243
417
 
244
- ## Dump out a combined config
245
- def dump()
246
- # have a fake, merge all into it, then dump it.
247
- base = IniFile.new()
248
- @paths.reverse.each do |ini|
249
- base.merge! ini.data
250
- end
251
- base.to_s
418
+ def wild?(key)
419
+ section, ikey = key.split('.')
420
+ is_wild = (
421
+ !section.to_s.empty? &&
422
+ !ikey.to_s.empty? &&
423
+ ((section == '*') || (ikey == '*'))
424
+ )
425
+ is_wild
252
426
  end
253
427
 
254
428
  def set(key, value, scope=:project)
255
429
  section, ikey = key.split('.', 2)
256
- raise "Invalid key" if section.nil?
257
- if not section.nil? and ikey.nil? then
430
+ raise 'Invalid key' if section.nil?
431
+ if ikey.nil?
258
432
  # If key isn't dotted, then assume the tool section.
259
433
  ikey = section
260
434
  section = 'tool'
261
435
  end
262
436
 
263
- paths = @paths.select{|p| scope == p.kind}
264
- raise "Unknown scope" if paths.empty?
437
+ paths = @paths.select { |p| scope == p.kind }
438
+ raise "Unknown scope ‘#{scope}’" if paths.empty?
439
+ raise "Too many scopes ‘#{scope}’" if paths.length > 1
440
+
265
441
  cfg = paths.first
266
442
  data = cfg.data
267
443
  tomod = data[section]
268
444
  tomod[ikey] = value unless value.nil?
269
445
  tomod.delete(ikey) if value.nil?
270
446
  data[section] = tomod
447
+ # Remove empty sections to make test results more predictable.
448
+ # Interesting: IniFile.each only returns sections with key-vals,
449
+ # so call IniFile.each_section instead, which includes
450
+ # empty empty section. Here's what "each" looks like:
451
+ # data.each do |sectn, param, val|
452
+ # puts "#{param} = #{val} [in section: #{sectn}]"
453
+ data.each_section do |sectn|
454
+ data.delete_section(sectn) if data[sectn].empty?
455
+ end
456
+
271
457
  cfg.write
272
458
  end
273
459
 
@@ -276,16 +462,109 @@ module MrMurano
276
462
  get(key)
277
463
  end
278
464
 
279
- # For setting internal, this-run-only values
465
+ # For setting internal, this-run-only values.
280
466
  def []=(key, value)
281
467
  set(key, value, :internal)
282
468
  end
283
469
 
470
+ ## Dump out a combined config
471
+ def dump
472
+ # have a fake, merge all into it, then dump it.
473
+ base = IniFile.new
474
+ @paths.reverse.each do |ini|
475
+ base.merge! ini.data
476
+ end
477
+ base.to_s
478
+ end
479
+
480
+ ## Dump out locations of all known configs
481
+ def locations
482
+ locats = ''
483
+ first = true
484
+ puts ''
485
+ CFG_SCOPES.each do |scope|
486
+ locats += "\n" unless first
487
+ first = false
488
+
489
+ cfg_paths = @paths.select { |p| p.kind == scope }
490
+
491
+ msg = "Scope: ‘#{scope}’\n\n"
492
+ locats += Rainbow(msg).bright.underline
493
+
494
+ if !cfg_paths.empty?
495
+ cfg = cfg_paths.first
496
+
497
+ if !cfg.path.nil? && cfg.path.exist?
498
+ path = "Path: #{cfg.path}\n"
499
+ elsif %i[internal defaults].include? cfg.kind
500
+ # cfg.path is nil.
501
+ path = "Path: ‘#{scope}’ config is not saved.\n"
502
+ else
503
+ path = "Path: ‘#{scope}’ config does not exist.\n"
504
+ end
505
+ #locats += Rainbow(path).bright
506
+ locats += path
507
+ locats += "\n"
508
+
509
+ skip_content = false
510
+ if scope == :env
511
+ locats += "Use the environment variable, MURANO_CONFIGFILE, to specify this config file.\n"
512
+ locats += "\n"
513
+ if ENV['MURANO_PASSWORD'].to_s.empty?
514
+ locats += "The MURANO_PASSWORD environ is not set.\n"
515
+ else
516
+ locats += "The MURANO_PASSWORD environ is set and will be used.\n"
517
+ end
518
+ skip_content = !cfg.path.exist?
519
+ end
520
+ next if skip_content
521
+ locats += "\n" if scope == :env
522
+
523
+ base = IniFile.new
524
+ base.merge! cfg.data
525
+ content = base.to_s
526
+ if !content.empty?
527
+ locats += "Config:\n\n"
528
+ #locats += base.to_s
529
+ base.to_s.split("\n").each do |line|
530
+ locats += ' ' + line + "\n"
531
+ end
532
+ else
533
+ msg = "Config: Empty INI file.\n"
534
+ #locats += Rainbow(msg).aqua.bright
535
+ locats += msg
536
+ end
537
+ else
538
+ msg = "No config found for ‘#{scope}’.\n"
539
+ if scope != :specified
540
+ locats += Rainbow(msg).red.bright
541
+ else
542
+ locats += "Path: ‘#{scope}’ config does not exist.\n\n"
543
+ locats += "Use --configfile to specify this config file.\n"
544
+ end
545
+ end
546
+ end
547
+ locats
548
+ end
549
+
550
+ # To capture curl calls when running rspec, write to a file.
551
+ def init_curl_file
552
+ if self['tool.curldebug'] && !self['tool.curlfile'].to_s.strip.empty?
553
+ if @curlfile_f.nil?
554
+ @curlfile_f = File.open(self['tool.curlfile'], 'a')
555
+ # MEH: Call @curlfile_f.close() at some point? Or let Ruby do on exit.
556
+ @curlfile_f << Time.now.to_s + "\n"
557
+ @curlfile_f << "murano #{ARGV.join(' ')}\n"
558
+ @curlfile_f.flush
559
+ end
560
+ elsif !@curlfile_f.nil?
561
+ @curlfile_f.close
562
+ @curlfile_f = nil
563
+ end
564
+ end
284
565
  end
285
566
 
286
567
  class ConfigError < StandardError
287
568
  end
288
-
289
569
  end
290
570
 
291
- # vim: set ai et sw=2 ts=2 :