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
@@ -0,0 +1,64 @@
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
+
8
+ require 'MrMurano/Config'
9
+
10
+ global_option('--[no-]color', %(Disable fancy output)) do |value|
11
+ HighLine.use_color = value
12
+ Rainbow.enabled = value
13
+ end
14
+
15
+ global_option('-c', '--config KEY=VALUE', %(Set a single config key)) do |param|
16
+ key, value = param.split('=', 2)
17
+ # a=b :> ["a", "b"]
18
+ # a= :> ["a", ""]
19
+ # a :> ["a"]
20
+ raise "Bad config '#{param}'" if key.nil?
21
+ if value.nil?
22
+ $cfg[key] = 'true'
23
+ else
24
+ $cfg[key] = value
25
+ end
26
+ end
27
+
28
+ global_option('-C', '--configfile FILE', %(Load additional configuration file)) do |file|
29
+ # This is called after all of the top level code in this file.
30
+ $cfg.load_specific(file)
31
+ end
32
+
33
+ global_option('-L', '--curl', %(Print out a curl command for each network call)) do
34
+ $cfg['tool.curldebug'] = true
35
+ end
36
+
37
+ global_option('-n', '--dry', %(Do not run actions that make changes)) do
38
+ $cfg['tool.dry'] = true
39
+ # Running dry implies verbose.
40
+ $cfg['tool.verbose'] = true
41
+ end
42
+
43
+ exclude_help = %(
44
+ Except config values from the specified scope(s).
45
+ SCOPES can be 1 scope or comma-separated list of
46
+ #{MrMurano::Config::CFG_SCOPES.map(&:to_s)}
47
+ ).strip
48
+ global_option('--exclude-scopes SCOPES', Array, exclude_help) do |value|
49
+ $cfg.exclude_scopes = value.map(&:to_sym)
50
+ end
51
+
52
+ # --no-page is handled early on, in bin/murano.
53
+ global_option('--no-page', %(Do not page --help output))
54
+
55
+ global_option('--[no-]plugins', %(Do not load plugins. Good for when one goes bad))
56
+
57
+ global_option('--[no-]progress', %(Disable spinner and progress message)) do |value|
58
+ $cfg['tool.no-progress'] = !value
59
+ end
60
+
61
+ global_option('-V', '--verbose', %(Be chatty)) do
62
+ $cfg['tool.verbose'] = true
63
+ end
64
+
@@ -1,192 +1,414 @@
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 'erb'
9
+ require 'rainbow'
10
+ require 'MrMurano/verbosing'
1
11
  require 'MrMurano/Account'
12
+ require 'MrMurano/Business'
13
+ require 'MrMurano/Config'
2
14
  require 'MrMurano/Config-Migrate'
3
- require 'erb'
15
+ require 'MrMurano/ReCommander'
16
+ require 'MrMurano/Solution-Services'
17
+ require 'MrMurano/SyncRoot'
18
+ require 'MrMurano/commands/business'
19
+ require 'MrMurano/commands/solution'
20
+ require 'MrMurano/commands/sync'
21
+
22
+ def init_cmd_description
23
+ %(
24
+
25
+ The init command helps you create a new Murano project
26
+ ======================================================
27
+
28
+ Example
29
+ -------
30
+
31
+ Create a new project in a new directory:
32
+
33
+ #{MrMurano::EXE_NAME} init my-new-app
34
+
35
+ Example
36
+ -------
37
+
38
+ Create a project in the current directory, or rewrite an existing project:
39
+
40
+ cd project/path
41
+ #{MrMurano::EXE_NAME} init
42
+
43
+ Solutions
44
+ ---------
45
+
46
+ The init command configures two new Solutions for your new Murano project:
47
+
48
+ 1. An Application
49
+
50
+ The Application is what users see.
51
+
52
+ Use the Application to control, monitor, and consume values from products.
53
+
54
+ 2. A Product
55
+
56
+ A Product is something that captures data and reports it to the Application.
57
+
58
+ A Product can be a physical device connected to the Internet. It can be
59
+ a simulator running on your local network. It can be anything that
60
+ triggers events or supplies input data to the Application.
61
+
62
+ How it Works
63
+ ------------
64
+
65
+ You will be asked to log on to your Business account.
66
+
67
+ - To create a new Murano business account, visit:
4
68
 
69
+ #{MrMurano::SIGN_UP_URL}
70
+
71
+ - Once logged on, you can choose to store your logon token so you
72
+ can skip this step when using Murano CLI.
73
+
74
+ After logon, name your Application, and then name your Product.
75
+
76
+ - Please choose names that contain only lowercase letters and numbers.
77
+
78
+ The names are used as variable names in scripts, and as domain names,
79
+ so they cannot contain underscores, dashes, or other punctuation.
80
+
81
+ After creating the two Solutions, they will be linked.
82
+
83
+ - Linking Solutions allows data and events to flow between the two.
84
+
85
+ For example, a Product device generates data that will be consumed
86
+ or processed by the Application.
87
+
88
+ The init command will pull down Product and Application services
89
+ that you can edit.
90
+
91
+ - The services, or event handlers, let you control how data is
92
+ processed and how your application behaves.
93
+
94
+ Take a look at the new directories and files created in your
95
+ Project after running init to see what services are available.
96
+
97
+ There are many other resources that are not downloaded that
98
+ you can also create and edit. Visit our docs site for more!
99
+
100
+ http://docs.exosite.com/
101
+
102
+ ).strip
103
+ end
5
104
 
6
105
  command :init do |c|
7
- c.syntax = %{murano init}
8
- c.summary = %{The easy way to start a project}
9
- c.description = %{}
106
+ c.syntax = %(murano init)
107
+ c.summary = %(The easy way to start a project)
108
+ c.description = init_cmd_description
109
+
110
+ c.example %(
111
+ Initialize Murano project using specific Business and Solutions
112
+ ).strip, 'murano init --business-id 12345 --product-name myprod --application-name myapp'
113
+
114
+ # Let user specify existing business and/or solution IDs and/or names.
115
+ cmd_option_business_pickers(c)
116
+ cmd_option_application_pickers(c)
117
+ cmd_option_product_pickers(c)
118
+
119
+ c.option('--refresh', %(Ignore Business and Solution IDs found in the config))
120
+ c.option('--purge', %(Remove Project directories and files, and recreate anew))
121
+ c.option('--[no-]sync', %(Pull down existing remote files (generally a good thing) (default: true)))
122
+ c.option('--[no-]mkdirs', %(Create default directories))
10
123
 
11
- c.option '--force', %{Override existing business, solution, or product ids}
12
- c.option '--[no-]mkdirs', %{Create default directories}
124
+ # This command can be run without a project config.
125
+ c.project_not_required = true
126
+ # This command should not walk up the directory tree
127
+ # looking for a .murano/config project config file.
128
+ c.restrict_to_cur_dir = true
129
+ # Ask for user password if not found.
130
+ c.prompt_if_logged_off = true
13
131
 
14
132
  c.action do |args, options|
15
- options.default :force=>false, :mkdirs=>true
16
- acc = MrMurano::Account.new
17
- puts ''
133
+ c.verify_arg_count!(args, 1)
134
+ options.default(refresh: false, purge: false, sync: true, mkdirs: true)
18
135
 
19
- if Pathname.new(Dir.pwd).realpath == Pathname.new(Dir.home).realpath then
20
- acc.error "Cannot init a project in your HOME directory."
21
- exit 2
136
+ acc = MrMurano::Account.instance
137
+ validate_dir!(acc, args, options)
138
+
139
+ puts('')
140
+ if $cfg.project_exists
141
+ verbage = 'Rebasing'
142
+ else
143
+ verbage = 'Creating'
22
144
  end
145
+ say("#{verbage} project at #{Rainbow($cfg['location.base'].to_s).underline}")
23
146
 
24
- say "Found project base directory at #{$cfg['location.base'].to_s}"
25
- puts ''
147
+ puts('')
26
148
 
27
- # Try to import a .Solutionfile.secret
149
+ # Try to import a .Solutionfile.secret.
150
+ # NOTE/2017-06-29: .Solutionfile.secret and SolutionFile (see ProjectFile.rb)
151
+ # are old MurCLI constructs; here we just try to migrate from the old format
152
+ # to the new format (where config goes in .murano/config and there's an
153
+ # explicit directory structure; the user cannot specify a different file
154
+ # hierarchy).
28
155
  MrMurano::ConfigMigrate.new.import_secret
29
156
 
30
- # If they have never logged in, then asking for the business.id will also ask
31
- # for their username and password.
32
- say "Using account #{$cfg['user.name']}"
33
- say ''
157
+ # See if the config already specifies a Business ID. If not, see if the
158
+ # config contains a username and password; otherwise, ask for them. With
159
+ # a username and password, get the list of businesses from Murano; if
160
+ # just one found, use that; if more than one found, ask user which one
161
+ # to use; else, if no businesses found, spit out the new-account URL
162
+ # and tell the user to use their browser to create a new Business.
163
+ unless $cfg['user.name'].to_s.empty?
164
+ say("Found User #{Rainbow($cfg['user.name']).underline}")
165
+ puts('')
166
+ end
34
167
 
35
- # 1. Get business id
36
- if not options.force and not $cfg['business.id'].nil? then
37
- say "Using Business ID already set to #{$cfg['business.id']}"
38
- else
39
- bizz = acc.businesses
40
- if bizz.count == 1 then
41
- bizid = bizz.first
42
- say "You are only part of one business; using #{bizid[:name]}"
43
- $cfg.set('businesses.id', bizid[:bizid], :project)
44
-
45
- elsif bizz.count == 0 then
46
- acc.warning "You don't have any businesses; Log into the webUI and create one."
47
- exit 3
48
- else
49
- choose do |menu|
50
- menu.prompt = "Select which Business to use:"
51
- menu.flow = :columns_across
52
- bizz.sort{|a,b| a[:name]<=>b[:name]}.each do |b|
53
- menu.choice(b[:name]) do
54
- $cfg.set('business.id', b[:bizid], :project)
55
- end
56
- end
57
- end
58
- end
168
+ # Find and verify Business by ID (from $cfg) or by name (from --business),
169
+ # or ask user which business to use. If user has not logged on, they will
170
+ # be asked for their username and/or password first.
171
+ biz = business_find_or_ask!(acc, options)
172
+ # Save the 'business.id' and 'business.name' to the project config.
173
+ # ([lb] guessing biz guaranteed to be not nil, but checking anyway.)
174
+ biz.write unless biz.nil?
175
+
176
+ # Verify or ask user to create Solutions.
177
+ sol_opts = {
178
+ create_ok: true,
179
+ update_cfg: true,
180
+ ignore_cfg: options.refresh,
181
+ verbose: true,
182
+ }
183
+ # Get/Create Application ID
184
+ sol_opts[:match_sid] = options.application_id
185
+ sol_opts[:match_name] = options.application_name
186
+ sol_opts[:match_fuzzy] = options.application
187
+ appl = solution_find_or_create(biz: biz, type: :application, **sol_opts)
188
+ # Get/Create Product ID
189
+ sol_opts[:match_sid] = options.product_id
190
+ sol_opts[:match_name] = options.product_name
191
+ sol_opts[:match_fuzzy] = options.product
192
+ prod = solution_find_or_create(biz: biz, type: :product, **sol_opts)
193
+
194
+ # Automatically link solutions.
195
+ link_opts = { verbose: true }
196
+ link_solutions(appl, prod, link_opts)
197
+
198
+ # If no ProjectFile, then write a ProjectFile.
199
+ write_project_file
200
+
201
+ if options.mkdirs
202
+ # Make the directory structure.
203
+ make_directories(purge: options.purge)
204
+
205
+ # For new solutions, Murano creates a few empty and example event handlers.
206
+ # For existing solutions, the user might already have created some files.
207
+ # Grab them now.
208
+ syncdown_new_and_existing if options.sync
59
209
  end
60
- puts '' # blank line
61
210
 
62
- # 2. Get Solution id
63
- if not options.force and not $cfg['solution.id'].nil? then
64
- say "Using Solution ID already set to #{$cfg['solution.id']}"
211
+ blather_success
212
+ end
213
+
214
+ def highlight_id(id)
215
+ Rainbow(id).aliceblue.bright.underline
216
+ end
217
+
218
+ def validate_dir!(acc, args, options)
219
+ # 2017-06-21: You can run init --dry and not have any files touched or
220
+ # any Murano elements changed. But there's not much utility in that.
221
+ # So maybe we should just not let users run a --dry init.
222
+ #if $cfg['tool.dry']
223
+ # acc.error 'Cannot run a --dry init.'
224
+ # exit 2
225
+ #end
226
+
227
+ if args.count > 1
228
+ acc.error('Please only specify 1 path')
229
+ exit(2)
230
+ end
231
+
232
+ if args.empty?
233
+ target_dir = Pathname.new(Dir.pwd)
65
234
  else
66
- solz = acc.solutions.select{|s| s[:type] == 'dataApi'}
67
- if solz.count == 1 then
68
- sol = solz.first
69
- say "You only have one solution; using #{sol[:domain]}"
70
- $cfg.set('solution.id', sol[:apiId], :project)
71
-
72
- elsif solz.count == 0 then
73
- say "You don't have any solutions; lets create one"
74
- solname = ask("Solution Name? ")
75
- ret = acc.new_solution(solname)
76
- if ret.nil? then
77
- acc.error "Create Solution failed"
78
- exit 5
79
- end
80
- if not ret.kind_of?(Hash) and not ret.empty? then
81
- acc.error "Create Solution failed: #{ret.to_s}"
82
- exit 2
235
+ target_dir = Pathname.new(args[0])
236
+ unless Dir.exist?(target_dir.to_path)
237
+ if target_dir.exist?
238
+ acc.error("Target exists but is not a directory: #{target_dir.to_path}")
239
+ exit 1
83
240
  end
241
+ # FIXME/2017-07-02: Add test for this
242
+ target_dir.mkpath
243
+ end
244
+ Dir.chdir target_dir
245
+ end
84
246
 
85
- # create doesn't return anything, so we need to go look for it.
86
- ret = acc.solutions.select do |i|
87
- i[:type] == 'dataApi' and (i[:name] == solname or i[:domain] =~ /#{solname}\./i)
88
- end
89
- sid = (ret.first or {})[:apiId]
90
- if sid.nil? or sid.empty? then
91
- acc.error "Solution didn't find an apiId!!!! #{name} -> #{ret}"
92
- exit 3
93
- end
94
- $cfg.set('solution.id', sid, :project)
247
+ # The home directory already has its own .murano/ folder,
248
+ # so we cannot create a project therein.
249
+ if Pathname.new(Dir.pwd).realpath == Pathname.new(Dir.home).realpath
250
+ acc.error('Cannot init a project in your HOME directory.')
251
+ exit(2)
252
+ end
253
+ # Might as well block root path, too.
254
+ if Pathname.new(Dir.pwd).realpath == File::SEPARATOR
255
+ acc.error('Cannot init a project in your root directory.')
256
+ exit(2)
257
+ end
95
258
 
96
- else
97
- choose do |menu|
98
- menu.prompt = "Select which Solution to use:"
99
- menu.flow = :columns_across
100
- solz.sort{|a,b| a[:domain]<=>b[:domain]}.each do |s|
101
- menu.choice(s[:domain].sub(/\..*$/,'')) do
102
- $cfg.set('solution.id', s[:apiId], :project)
103
- end
259
+ # Only create a new project in an empty directory,
260
+ # or a recognized Murano CLI project.
261
+ unless $cfg.project_exists || options.refresh
262
+ # Get a list of files, ignoring the dot meta entries.
263
+ files = Dir.entries(target_dir.to_path)
264
+ files -= %w[. ..]
265
+ # If there are files (and it's not just .byebug_history which
266
+ # gets created when you develop with byebug), then ask to proceed.
267
+ unless files.empty? || (files.length == 1 && files[0] == '.byebug_history')
268
+ # Check for a .murano/ directory. It might be empty, which
269
+ # is why $cfg.project_exists might have been false.
270
+ unless files.include?(MrMurano::Config::CFG_DIR_NAME)
271
+ acc.warning 'The project directory contains unknown files.'
272
+ confirmed = acc.ask_yes_no('Really init project? [y/N] ', false)
273
+ unless confirmed
274
+ acc.warning('abort!')
275
+ exit 1
104
276
  end
105
277
  end
106
278
  end
107
279
  end
108
- puts '' # blank line
109
280
 
110
- # 3. Get Product id
111
- if not options.force and not $cfg['product.id'].nil? then
112
- say "Using Product ID already set to #{$cfg['product.id']}"
113
- else
114
- podz = acc.products
115
- if podz.count == 1 then
116
- prd = podz.first
117
- say "You only have one product; using #{prd[:label]}"
118
- $cfg.set('product.id', prd[:modelId], :project)
119
-
120
- elsif podz.count == 0 then
121
- say "You don't have any products; lets create one"
122
- podname = ask("Product Name? ")
123
- ret = acc.new_product(podname)
124
- if ret.nil? then
125
- acc.error "Create Product failed"
126
- exit 5
127
- end
128
- if not ret.kind_of?(Hash) and not ret.empty? then
129
- acc.error "Create Product failed: #{ret.to_s}"
130
- exit 2
131
- end
281
+ target_dir
282
+ end
132
283
 
133
- # create doesn't return anything, so we need to go look for it.
134
- ret = acc.products.select{|i| i[:label] == podname}
135
- pid = ret.first[:modelId]
136
- if pid.nil? or pid.empty? then
137
- acc.error "Product didn't find an apiId!!!! #{ret}"
138
- exit 3
139
- end
140
- $cfg.set('product.id', pid, :project)
284
+ def write_project_file
285
+ return if $project.using_projectfile
286
+ tmpl = File.read(
287
+ File.join(
288
+ File.dirname(__FILE__), '..', 'template', 'projectFile.murano.erb'
289
+ )
290
+ )
291
+ tmpl = ERB.new(tmpl)
292
+ res = tmpl.result($project.data_binding)
293
+ pr_file = $project['info.name'] + '.murano'
294
+ say("Writing Project file to #{pr_file}")
295
+ puts('')
296
+ File.open(pr_file, 'w') { |io| io << res }
297
+ end
141
298
 
299
+ def make_directories(purge: false)
300
+ base = $cfg['location.base']
301
+ base = Pathname.new(base) unless base.is_a?(Pathname)
302
+ num_locats = 0
303
+ num_mkpaths = 0
304
+ num_rmpaths = 0
305
+ %w[
306
+ location.files
307
+ location.endpoints
308
+ location.modules
309
+ location.eventhandlers
310
+ location.resources
311
+ ].each do |cfgi|
312
+ num_locats += 1
313
+ n_mkdirs, n_rmdirs = make_directory(cfgi, base, purge)
314
+ num_mkpaths += n_mkdirs
315
+ num_rmpaths += n_rmdirs
316
+ end
317
+ if num_rmpaths > 0
318
+ say('Removed existing directories')
319
+ puts('')
320
+ end
321
+ if num_mkpaths > 0
322
+ if num_mkpaths == num_locats
323
+ say('Created default directories')
142
324
  else
143
- choose do |menu|
144
- menu.prompt = "Select which Product to use:"
145
- menu.flow = :columns_across
146
- podz.sort{|a,b| a[:label]<=>b[:label]}.each do |p|
147
- menu.choice(p[:label]) do
148
- $cfg.set('product.id', p[:modelId], :project)
149
- end
150
- end
151
- end
325
+ say('Created some default directories')
152
326
  end
327
+ else
328
+ say('Default directories already exist')
153
329
  end
330
+ puts('')
331
+ end
154
332
 
155
- puts ''
156
- say "Ok, In business ID: #{$cfg['business.id']} using Solution ID: #{$cfg['solution.id']} with Product ID: #{$cfg['product.id']}"
157
-
158
- # If no ProjectFile, then write a ProjectFile
159
- if not $project.usingProjectfile then
160
- tmpl = File.read(File.join(File.dirname(__FILE__),'..','template','projectFile.murano.erb'))
161
- tmpl = ERB.new(tmpl)
162
- res = tmpl.result($project.data_binding)
163
- prFile = $project['info.name'] + '.murano'
164
- say "Writing an initial Project file: #{prFile}"
165
- File.open(prFile, 'w') {|io| io << res}
166
- end
167
-
168
- if options.mkdirs then
169
- base = $cfg['location.base']
170
- base = Pathname.new(base) unless base.kind_of? Pathname
171
- %w{
172
- location.files
173
- location.endpoints
174
- location.modules
175
- location.eventhandlers
176
- location.specs
177
- }.each do |cfgi|
178
- path = $cfg[cfgi]
179
- path = Pathname.new(path) unless path.kind_of? Pathname
180
- path = base + path
181
- unless path.exist? then
182
- path = path.dirname unless path.extname.empty?
183
- path.mkpath
184
- end
333
+ def make_directory(cfgi, base, purge)
334
+ path = $cfg[cfgi]
335
+ path = Pathname.new(path) unless path.is_a?(Pathname)
336
+ path = base + path
337
+ # The path is generally a directory, but sometimes
338
+ # it's a file (e.g., spec/resources.yaml).
339
+ basedir = path
340
+ basedir = basedir.dirname unless basedir.extname.empty?
341
+ raise 'Unexpected: bad basedir' if basedir.to_s.empty? || basedir == File::SEPARATOR
342
+ found_basedir = false
343
+ basedir.ascend do |ancestor|
344
+ if ancestor == base
345
+ found_basedir = true
346
+ break
185
347
  end
186
- say "Default directories created"
187
348
  end
349
+ unless found_basedir
350
+ say("Please fix your config: location.* values should be a subdir of location.base (#{base})")
351
+ exit 1
352
+ end
353
+ dry = $cfg['tool.dry']
354
+ num_rmdir = 0
355
+ if purge
356
+ MrMurano::Verbose.warning("--dry: Not purging existing directory: #{basedir}") if dry
357
+ files = Dir.glob("#{basedir}/*")
358
+ FileUtils.rm_rf(files, noop: dry)
359
+ FileUtils.rmdir(basedir, noop: dry)
360
+ MrMurano::Verbose.verbose("Removed #{basedir}")
361
+ num_rmdir += 1
362
+ end
363
+ return 0, num_rmdir if path.exist?
364
+ MrMurano::Verbose.warning("--dry: Not creating default directory: #{basedir}") if dry
365
+ FileUtils.mkdir_p(basedir, noop: dry)
366
+ FileUtils.touch(path, noop: dry) if path != basedir
367
+ [1, num_rmdir]
368
+ end
188
369
 
370
+ def syncdown_new_and_existing
371
+ # If the user already has an existing project, grab its files.
372
+ #
373
+ # If Murano creates any default eventhandlers, grab those
374
+ # (e.g., the timer event, tsdb exportJob, and user account
375
+ # are all created by the platform; and our own method,
376
+ # link_solutions, creates a boilerplate event handler that
377
+ # yeti-ui expects to find (you'll have issues in the web UI
378
+ # if this script doesn't exist)).
379
+ #
380
+ # See:
381
+ # sphinx-api/src/views/interface/productService.swagger.json
382
+ num_synced = syncdown_files(delete: false, create: true, update: false)
383
+ if num_synced > 0
384
+ inflection = MrMurano::Verbose.pluralize?('item', num_synced)
385
+ say("Synced #{num_synced} #{inflection}")
386
+ else
387
+ say('Items already synced')
388
+ end
389
+ puts('')
189
390
  end
190
- end
191
391
 
192
- # vim: set ai et sw=2 ts=2 :
392
+ def blather_success
393
+ say('Success!')
394
+ puts('')
395
+ id_postfix = ' ID'
396
+ important_ids = %w[business application product].freeze
397
+ importantest_width = important_ids.map do |id_name|
398
+ cfg_key = id_name + '.id'
399
+ $cfg[cfg_key].length + id_postfix.length
400
+ end.max # Max the map; get the length of the longest ID.
401
+ important_ids.each do |id_name|
402
+ # cfg_key is, e.g., 'business.id', 'product.id', 'application.id'
403
+ cfg_key = id_name + '.id'
404
+ next if $cfg[cfg_key].nil?
405
+ #say "#{id_name.capitalize} ID: #{highlight_id($cfg[cfg_key])}"
406
+ # Right-aligned:
407
+ tmpl = format('%%%ds: %%s', importantest_width)
408
+ # Left-aligned:
409
+ #tmpl = format('%%-%ds: %%s', importantest_width)
410
+ say(format(tmpl, id_name.capitalize + id_postfix, highlight_id($cfg[cfg_key])))
411
+ end
412
+ puts('')
413
+ end
414
+ end