rzo 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6974bf3026de895a982abdcd218564cfb9fadd87
4
- data.tar.gz: c949443d5fff2c64a36033aad8af9f1b7412f1c3
3
+ metadata.gz: 57197ea4ec41e6e0b5ae935a754b415ee3ef7ebc
4
+ data.tar.gz: ae8140e57a89faf648963cb66f96f45f1fdfa8e5
5
5
  SHA512:
6
- metadata.gz: 7c43d4433d78e5f0af754a9c3001a3303bf3013c8ed2388ada4f648ff89e0538e707cb60db37271bc34d827d156dab783bf1c1e0bbbbb503bf248a04ad480be9
7
- data.tar.gz: 6447d475c5c42783b201f14ec9c6ed1102fa7511054c5316c53b22f71e1fe076c948e56930322e2bee5273a8445b9581829ebf186888e740e2c4b0f21043a165
6
+ metadata.gz: 627ff40899c739564bd9cd0a5a8bdb0d5995ab76a650e11293a51cf322c456e7310709940b016d539e66c8a23984c17b2ccd67eaef52aadc4a2a8777affde89b
7
+ data.tar.gz: eaecfbd7e222a971b4c1737b0451b30e24a8dbd0a402ff46ffca2ba0c11895a98914e3618f4b49a21dd6205a166d14d68c9f3e0741eeba900e07c743e0f63eb6
data/.rubocop.yml CHANGED
@@ -7,8 +7,11 @@ AllCops:
7
7
  - pkg/**/*
8
8
  - spec/fixtures/**/*
9
9
  - lib/rzo/trollop.rb
10
+ - Vagrantfile
10
11
 
11
12
  # Cop's to ignore
13
+ Style/RedundantReturn:
14
+ Enabled: false
12
15
 
13
16
  Lint/UnneededDisable:
14
17
  Enabled: false
@@ -16,7 +19,13 @@ Lint/UnneededDisable:
16
19
  Layout/MultilineOperationIndentation:
17
20
  Enabled: false
18
21
 
22
+ # if / else / end is more clear than conditional assignment
23
+ Style/ConditionalAssignment:
24
+ Enabled: false
25
+
19
26
  # With this enabled it suggests a change that will break the Gemfile
27
+ # Also, if name = hsh['name'] is really useful to avoid Unknown method called on
28
+ # nil object errors.
20
29
  Lint/AssignmentInCondition:
21
30
  Enabled: false
22
31
 
data/.travis.yml CHANGED
@@ -12,3 +12,4 @@ rvm:
12
12
  script:
13
13
  - 'bundle exec rake validate rubocop'
14
14
  - 'bundle exec rspec'
15
+ - 'scripts/functional_gem_behavior.sh'
data/CHANGELOG.md CHANGED
@@ -1,7 +1,24 @@
1
1
  # Change Log
2
2
 
3
- ## [v0.3.0](https://github.com/ghoneycutt/rizzo/tree/v0.3.0)
3
+ ## [v0.4.0](https://github.com/ghoneycutt/rizzo/tree/v0.4.0)
4
4
 
5
+ [Full Changelog](https://github.com/ghoneycutt/rizzo/compare/v0.3.0...v0.4.0)
6
+
7
+ **Closed issues:**
8
+
9
+ - Add Safety Checking [\#16](https://github.com/ghoneycutt/rizzo/issues/16)
10
+ - Fix uninitialized constant Rzo::App::Subcommand::Pathname \(NameError\) [\#14](https://github.com/ghoneycutt/rizzo/issues/14)
11
+ - Add `rzo roles' command to list the roles [\#10](https://github.com/ghoneycutt/rizzo/issues/10)
12
+ - Convert into a ruby gem [\#5](https://github.com/ghoneycutt/rizzo/issues/5)
13
+
14
+ **Merged pull requests:**
15
+
16
+ - \(\#16\) Add Directory Safety Checks [\#20](https://github.com/ghoneycutt/rizzo/pull/20) ([jeffmccune](https://github.com/jeffmccune))
17
+ - \(\#14\) Fix uninitialized constant Pathname \(NameError\) [\#15](https://github.com/ghoneycutt/rizzo/pull/15) ([jeffmccune](https://github.com/jeffmccune))
18
+ - Add functional testing [\#13](https://github.com/ghoneycutt/rizzo/pull/13) ([jeffmccune](https://github.com/jeffmccune))
19
+ - \(\#10\) Add roles subcommand [\#12](https://github.com/ghoneycutt/rizzo/pull/12) ([jeffmccune](https://github.com/jeffmccune))
20
+
21
+ ## [v0.3.0](https://github.com/ghoneycutt/rizzo/tree/v0.3.0) (2017-08-24)
5
22
  [Full Changelog](https://github.com/ghoneycutt/rizzo/compare/v0.2.0...v0.3.0)
6
23
 
7
24
  **Merged pull requests:**
data/Guardfile CHANGED
@@ -1,18 +1,14 @@
1
- # Note: The cmd option is now required due to the increasing number of ways
2
- # rspec may be run, below are examples of the most common uses.
3
- # * bundler: 'bundle exec rspec'
4
- # * bundler binstubs: 'bin/rspec'
5
- # * spring: 'bin/rspec' (This will use spring if running and you have
6
- # installed the spring binstubs per the docs)
7
- # * zeus: 'zeus rspec' (requires the server to be started separately)
8
- # * 'just' rspec: 'rspec'
9
- rspec_results = File.expand_path('.rspec_status')
10
- guard 'yard', server: false do
11
- watch(%r{app\/.+\.rb})
12
- watch(%r{lib\/.+\.rb})
13
- watch(%r{ext\/.+\.c})
1
+ # Convert a lib path to a spec path
2
+ def to_spec(path)
3
+ path.sub('lib/', 'spec/').sub(/\.rb$/, '_spec.rb')
14
4
  end
15
5
 
6
+ # guard 'yard', server: false do
7
+ # watch(%r{app\/.+\.rb})
8
+ # watch(%r{lib\/.+\.rb})
9
+ # watch(%r{ext\/.+\.c})
10
+ # end
11
+
16
12
  guard :rubocop do
17
13
  watch(/.+\.rb$/)
18
14
  watch('Gemfile')
@@ -22,20 +18,39 @@ guard :rubocop do
22
18
  watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
23
19
  end
24
20
 
25
- guard :rspec, cmd: 'bundle exec rspec', results_file: rspec_results do
21
+ guard :shell do
26
22
  require 'guard/rspec/dsl'
23
+ require 'pathname'
27
24
  dsl = Guard::RSpec::Dsl.new(self)
28
25
 
26
+ runner = proc do |p|
27
+ if system("rspec -fd #{p}")
28
+ n 'Spec tests pass', 'rspec', :success
29
+ else
30
+ n 'Spec tests fail', 'rspec', :failed
31
+ end
32
+ nil
33
+ end
34
+
29
35
  # Feel free to open issues for suggestions and improvements
30
36
 
31
37
  # RSpec files
32
38
  rspec = dsl.rspec
33
- watch(rspec.spec_helper) { rspec.spec_dir }
34
- watch(rspec.spec_support) { rspec.spec_dir }
35
- watch(rspec.spec_files)
39
+ watch(rspec.spec_helper) do |m|
40
+ runner.call(m[0])
41
+ end
42
+ watch(rspec.spec_support) do
43
+ runner.call(m[0])
44
+ end
45
+ watch rspec.spec_files do |m|
46
+ runner.call(m[0])
47
+ end
36
48
 
37
49
  # Ruby files
38
50
  ruby = dsl.ruby
39
- dsl.watch_spec_files_for(ruby.lib_files)
51
+ watch(ruby.lib_files) do |m|
52
+ spec = Pathname.new(to_spec(m[0]))
53
+ runner.call(spec.to_s) if spec.readable?
54
+ end
40
55
  end
41
56
  # vim:ft=ruby
data/README.md CHANGED
@@ -4,6 +4,12 @@ Rizzo is a heavily customized Vagrant configuration and work flow with a
4
4
  role based focus. It is meant to make working with Vagrant easier and
5
5
  purpose built for layered Puppet control repositories.
6
6
 
7
+ Rizzo loads a personal configuration file, `~/.rizzo.json` by default, which
8
+ lists one or more control repositories. Rizzo then looks for a loads a
9
+ `.rizzo.json` configuration file located at the root of the top level control
10
+ repository. The top level control repository is the first listed in
11
+ the array of control repositories in the personal configuration file.
12
+
7
13
  There should be at least one node for every role that is managed by a
8
14
  control repo. This information is stored in `.rizzo.json` under the
9
15
  control repo. This makes it apparent what roles are available and aids
@@ -90,7 +96,11 @@ repo.
90
96
 
91
97
  ## `~/.rizzo.json`
92
98
 
93
- Change the paths to your git repos
99
+ The personal configuration file is loaded first from `~/.rizzo.json` by default.
100
+ The global `--config` option allows the end user to specific a different path to
101
+ the personal configuration file.
102
+
103
+ Using this example, change the paths to your git repos:
94
104
 
95
105
  ```json
96
106
  {
@@ -98,6 +108,7 @@ Change the paths to your git repos
98
108
  "bootstrap_repo_path": "/Users/gh/git/bootstrap"
99
109
  },
100
110
  "control_repos": [
111
+ "/Users/gh/git/puppet-control-myteam",
101
112
  "/Users/gh/git/puppetdata",
102
113
  "/Users/gh/git/puppet-modules"
103
114
  ],
@@ -124,11 +135,16 @@ Change the paths to your git repos
124
135
  }
125
136
  ```
126
137
 
127
- Once you have `~/.rizzo.json`, change to your top level control repository and generate your `Vagrantfile`.
138
+ Once you have a personal config file, `~/.rizzo.json`, change directories to
139
+ your top level control repository and generate your `Vagrantfile`:
128
140
 
129
141
  ```shell
142
+ cd ~/git/puppet-control-myteam
130
143
  bundle exec rizzo generate
131
144
  ```
145
+
146
+ Expected output:
147
+
132
148
  ```
133
149
  Wrote vagrant config to Vagrantfile
134
150
  ```
@@ -155,8 +171,8 @@ VM, run `vagrant status NAME`.
155
171
  ### defaults
156
172
 
157
173
  The defaults hash is merged with each node entries hash. Put user
158
- specific entries in `~/.rizzo.json` and project specific entries in
159
- `${PATH_TO_CONTROL_REPO}/.rizzo.json`.
174
+ specific entries in the personal configuration file at `~/.rizzo.json` and
175
+ control repo specific entries in `${PATH_TO_CONTROL_REPO}/.rizzo.json`.
160
176
 
161
177
  ### control_repos
162
178
 
data/lib/rzo/app.rb CHANGED
@@ -3,6 +3,7 @@ require 'rzo/logging'
3
3
  require 'rzo/option_parsing'
4
4
  require 'rzo/app/config'
5
5
  require 'rzo/app/generate'
6
+ require 'rzo/app/roles'
6
7
  require 'json'
7
8
 
8
9
  module Rzo
@@ -24,9 +25,11 @@ module Rzo
24
25
  # method in the app controller
25
26
  class ErrorAndExit < StandardError
26
27
  attr_accessor :exit_status
28
+ attr_accessor :log_fatal
27
29
  def initialize(message = nil, exit_status = 1)
28
30
  super(message)
29
31
  self.exit_status = exit_status
32
+ self.log_fatal = []
30
33
  end
31
34
  end
32
35
 
@@ -59,6 +62,12 @@ module Rzo
59
62
  @generate ||= Generate.new(opts, @stdout, @stderr)
60
63
  end
61
64
 
65
+ ##
66
+ # Accessor to Subcommand::Config
67
+ def config
68
+ @config ||= Config.new(opts, @stdout, @stderr)
69
+ end
70
+
62
71
  ##
63
72
  # Override this later to allow trollop to write to an intercepted file
64
73
  # descriptor for testing. This will avoid trollop's behavior of calling
@@ -76,14 +85,17 @@ module Rzo
76
85
  def run
77
86
  case opts[:subcommand]
78
87
  when 'config'
79
- Config.new(opts, @stdout, @stderr).run
88
+ config.run
80
89
  when 'generate'
81
90
  generate.run
91
+ when 'roles'
92
+ Roles.new(opts, @stdout, @stderr).run
82
93
  else
83
94
  educate
84
95
  end
85
96
  rescue ErrorAndExit => e
86
- log.error e.message
97
+ log.fatal e.message
98
+ e.log_fatal.each { |m| log.fatal(m) }
87
99
  e.backtrace.each { |l| log.debug(l) }
88
100
  e.exit_status
89
101
  end
@@ -0,0 +1,306 @@
1
+ require 'rzo/app'
2
+ require 'pathname'
3
+ require 'json-schema'
4
+ # rubocop:disable Style/GuardClause
5
+ module Rzo
6
+ class App
7
+ ##
8
+ # Mix-in module providing configuration validation methods and safety
9
+ # checking. The goal is to provide useful feedback to the end user in the
10
+ # situation where ~/.rizzo.json is configured to point at directories which
11
+ # do not exist, have missing keys, etc...
12
+ # rubocop:disable Metrics/ModuleLength
13
+ module ConfigValidation
14
+ ## Rizzo configuration schema for the personal configuration file at
15
+ # ~/.rizzo.json. Minimum necessary to load the complete configuration from
16
+ # all control repositories.
17
+ RZO_PERSONAL_CONFIG_SCHEMA = {
18
+ '$schema' => 'http://json-schema.org/draft-06/schema#',
19
+ title: 'Personal Configuration',
20
+ description: 'Rizzo personal configuration file',
21
+ type: 'object',
22
+ properties: {
23
+ defaults: {
24
+ type: 'object',
25
+ },
26
+ control_repos: {
27
+ type: 'array',
28
+ items: { type: 'string' },
29
+ uniqueItems: true,
30
+ },
31
+ },
32
+ required: ['control_repos'],
33
+ }.freeze
34
+ ## Rizzo complete configuration schema. This should move to a JSON file outside
35
+ # the code.
36
+ RZO_REPO_CONFIG_SCHEMA = {
37
+ type: 'object',
38
+ required: %w[defaults control_repos puppetmaster],
39
+ properties: {
40
+ defaults: {
41
+ type: 'object',
42
+ required: ['bootstrap_repo_path'],
43
+ properties: {
44
+ bootstrap_repo_path: {
45
+ type: 'string',
46
+ pattern: '^([a-zA-Z]:){0,1}(/[^/]+)+$',
47
+ },
48
+ },
49
+ },
50
+ puppetmaster: {
51
+ type: 'object',
52
+ required: %w[name modulepath synced_folders],
53
+ properties: {
54
+ name: {
55
+ type: 'array',
56
+ items: { type: 'string' },
57
+ },
58
+ modulepath: {
59
+ type: 'array',
60
+ items: { type: 'string' },
61
+ },
62
+ synced_folders: {
63
+ '$schema' => 'http://json-schema.org/draft-06/schema#',
64
+ type: 'object',
65
+ properties: {
66
+ '/' => {},
67
+ patternProperties: {
68
+ '^(/[^/]+)+$' => {},
69
+ },
70
+ additionalProperties: false,
71
+ required: ['/'],
72
+ }
73
+ },
74
+ },
75
+ },
76
+ control_repos: {
77
+ type: 'array',
78
+ items: { type: 'string' },
79
+ uniqueItems: true,
80
+ },
81
+ nodes: {
82
+ type: 'array',
83
+ items: {
84
+ type: 'object',
85
+ required: %w[name hostname ip],
86
+ properties: {
87
+ name: { type: 'string' },
88
+ hostname: { type: 'string' },
89
+ ip: { type: 'string' },
90
+ memory: {
91
+ type: 'string',
92
+ pattern: '^[0-9]+$'
93
+ },
94
+ forwarded_ports: {
95
+ type: 'array',
96
+ items: {
97
+ type: 'object',
98
+ required: %w[guest host],
99
+ properties: {
100
+ guest: {
101
+ type: 'string',
102
+ pattern: '^[0-9]+$',
103
+ },
104
+ host: {
105
+ type: 'string',
106
+ pattern: '^[0-9]+$',
107
+ },
108
+ },
109
+ },
110
+ },
111
+ },
112
+ },
113
+ uniqueItems: true,
114
+ }
115
+ },
116
+ }.freeze
117
+ # The checks to execute, in order. Each method must return nil if there
118
+ # are no issues found. Otherwise, the check should return either one, or
119
+ # an array of Issue instances.
120
+ CHECKS_PERSONAL_CONFIG = %i[validate_personal_schema validate_control_repos].freeze
121
+ CHECKS_REPO_CONFIG = %i[validate_schema validate_defaults_key validate_control_repos].freeze
122
+
123
+ ##
124
+ # Class to model an issue found during validation
125
+ class Issue
126
+ attr_accessor :message
127
+
128
+ def initialize(msg)
129
+ self.message = msg
130
+ end
131
+
132
+ def to_s
133
+ message
134
+ end
135
+ end
136
+
137
+ ##
138
+ # Compute Issues given a config map (base or complete), and an Array of
139
+ # methods to execute.
140
+ #
141
+ # @param [Array<Symbol>] checks the method identifiers to execute, passing
142
+ # config. These methods must return nil (no issue found), an Issue
143
+ # instance, or Array<Instance> for multiple issues found.
144
+ #
145
+ # @param [Hash] config the config hash, either a base configuration or a
146
+ # fully merged configuration.
147
+ #
148
+ # @return [Array<Issue>] Array of issue instances, or an empty array if no
149
+ # issues found with the config.
150
+ def compute_issues(checks, config)
151
+ ctx = self
152
+ checks.each_with_object([]) do |mth, ary|
153
+ debug "Checking config for #{mth} issues"
154
+ if issue = ctx.send(mth, config)
155
+ # May get back an Array<Issue> or one Issue
156
+ ary.concat([*issue])
157
+ end
158
+ end
159
+ end
160
+
161
+ ##
162
+ # Validate a personal configuration, typically originating from
163
+ # ~/.rizzo.json. This configuration is necessary to build a complete
164
+ # control repo configuration using the top level control repo. This
165
+ # validation focuses on the minimum necessary configuration to bootstrap
166
+ # the complete configuration, primarily the repo locations and existence.
167
+ def validate_personal_config!(config)
168
+ issues = compute_issues(CHECKS_PERSONAL_CONFIG, config)
169
+ if issues.empty?
170
+ debug 'No issues detected with the personal configuration.'
171
+ else
172
+ validate_inform!(issues)
173
+ end
174
+ end
175
+
176
+ ##
177
+ # Validate a complete loaded configuration. This is distinct from a base
178
+ # configuration in that the JSON files in each control repository have
179
+ # already been merged, in order, on top of the base configuration
180
+ # originating at ~/.rizzo.json. This implements safety checking. These
181
+ # methods are expected to execute within the context of a
182
+ # Rzo::App::Subcommand instance, therefore log methods and the parsed
183
+ # configuration are assumed to be available.
184
+ #
185
+ # The approach is to collect an Array of Issue instances. If issues are
186
+ # found, control is handed off to validate_inform! to inform the user of
187
+ # the issues and potentially abort the program.
188
+ #
189
+ # @param [Hash] config the config hash, fully merged by load_config!
190
+ def validate_complete_config!(config)
191
+ issues = compute_issues(CHECKS_REPO_CONFIG, config)
192
+ if issues.empty?
193
+ debug 'No issues detected with the complete, merged configuration.'
194
+ else
195
+ validate_inform!(issues)
196
+ end
197
+ end
198
+
199
+ ##
200
+ # Validate using
201
+ # [json-schema](https://github.com/ruby-json-schema/json-schema)
202
+ #
203
+ # @return [Issue,nil] Issue found, or nil if no issues found.
204
+ def validate_schema(config)
205
+ if JSON::Validator.validate(RZO_REPO_CONFIG_SCHEMA, config)
206
+ debug 'No schema violations found in loaded config.'
207
+ return nil
208
+ else
209
+ err_msgs = JSON::Validator.fully_validate(RZO_REPO_CONFIG_SCHEMA, config)
210
+ return err_msgs.map { |msg| Issue.new("Schema violation: #{msg}") }
211
+ end
212
+ end
213
+
214
+ ##
215
+ # Validate the configuration has a top level key named "defaults" and the
216
+ # value is a Hash map.
217
+ # rubocop:disable Metrics/MethodLength
218
+ #
219
+ # @return [Issue,nil] Issue found, or nil if no issues found.
220
+ def validate_defaults_key(config)
221
+ if defaults = config['defaults']
222
+ return Issue.new('Top level key "defaults" must have a Hash value') unless defaults.is_a? Hash
223
+ else
224
+ return Issue.new('Configuration does not contain top level "defaults" key')
225
+ end
226
+ if pth = defaults['bootstrap_repo_path']
227
+ return Issue.new('#/defaults/bootstrap_repo_path is not a String') unless pth.is_a? String
228
+ else
229
+ return Issue.new 'Configuration "defaults" value does not contain a '\
230
+ '"bootstrap_repo_path" key. For example, '\
231
+ '{"defaults":{"bootstrap_repo_path":"/tmp/foo"}}'
232
+ end
233
+ validate_existence(pth, '#/defaults/bootstrap_repo_path value of ')
234
+ end
235
+ # rubocop:enable Metrics/MethodLength
236
+
237
+ ##
238
+ # Validate the top level "control_repos" key, which should have a value of
239
+ # Array<String> where each string value is a fully qualified path.
240
+ #
241
+ # @return [Issue,nil] Issue found, or nil if no issues found.
242
+ def validate_control_repos(config)
243
+ if repos = config['control_repos']
244
+ return Issue.new('Top level key "control_repos" must have an Array value') unless repos.is_a? Array
245
+ else
246
+ return Issue.new('Top level key "control_repos" is not specified. It must be an Array of paths to your control repos.')
247
+ end
248
+ repos.each_with_object([]) do |pth, ary|
249
+ if issue = validate_existence(pth, '#/control_repos')
250
+ ary << issue
251
+ end
252
+ end
253
+ end
254
+
255
+ ##
256
+ # Given a string, validate it's a fully qualified path, readable, and a
257
+ # git directory.
258
+ #
259
+ # @return [Issue,Array<Issue>,nil] nil if no issues found, or one or more
260
+ # Issue instances.
261
+ def validate_existence(path, prefix = '')
262
+ pn = Pathname.new(path)
263
+ git = pn + '.git'
264
+ return Issue.new("#{prefix}#{pn} is not an absolute path. It must be fully qualified, not relative") unless pn.absolute?
265
+ return Issue.new("#{prefix}#{pn} is not a directory. Has it been cloned?") unless pn.directory?
266
+ return Issue.new("#{prefix}#{pn} is not readable. Are permissions correct?") unless pn.readable?
267
+ return Issue.new("#{prefix}#{git} does not exist. Has #{git.dirname} been cloned properly?") unless git.directory?
268
+ end
269
+
270
+ ##
271
+ # Validate the personal configuration, focus on ensuring the rest of the
272
+ # configuration can load properly.
273
+ #
274
+ # @return [Issue,Array<Issue>,nil] nil if no issues found, or one or more
275
+ # Issue instances.
276
+ def validate_personal_schema(config)
277
+ if JSON::Validator.validate(RZO_PERSONAL_CONFIG_SCHEMA, config)
278
+ debug 'No schema violations found in personal configuration file.'
279
+ return nil
280
+ else
281
+ err_msgs = JSON::Validator.fully_validate(RZO_PERSONAL_CONFIG_SCHEMA, config)
282
+ return err_msgs.map { |msg| Issue.new("Personal config problem: #{msg}") }
283
+ end
284
+ end
285
+
286
+ # Inform the user about issues found and exit the program. The top level
287
+ # exception handler is not expected to display much information on
288
+ # validation errors. This method is expected to provide the helpful
289
+ # guidance.
290
+ #
291
+ # @param [Array<Issue>] issues Array of issues. Each hash must have at
292
+ # least a key named `:message`
293
+ def validate_inform!(issues)
294
+ if opts[:validate]
295
+ msg = "Validation issues found with #{opts[:config]}"
296
+ exc = ErrorAndExit.new(msg, 2)
297
+ exc.log_fatal = issues.each_with_object([]) { |i, a| a << i.to_s }
298
+ raise exc
299
+ else
300
+ issues.each { |i| log.warn(i.to_s) }
301
+ end
302
+ end
303
+ end
304
+ # rubocop:enable Metrics/ModuleLength
305
+ end
306
+ end
@@ -0,0 +1,31 @@
1
+ require 'rzo/app/subcommand'
2
+ module Rzo
3
+ class App
4
+ ##
5
+ # Load all rizzo config files and print the roles
6
+ class Roles < Subcommand
7
+ attr_reader :config
8
+
9
+ ##
10
+ # Map the combined config to a list of roles. No effort is made to sort
11
+ # them.
12
+ #
13
+ # @return [Array<String>] array of strings identifying each Puppet role
14
+ # name. This is the same as the name of the VM.
15
+ def roles
16
+ return [] unless nodes = config['nodes']
17
+ nodes.each_with_object([]) do |node, a|
18
+ next unless node['name']
19
+ a << node['name']
20
+ end
21
+ end
22
+
23
+ def run
24
+ exit_status = 0
25
+ load_config!
26
+ write_file(opts[:output]) { |fd| fd.puts(roles) }
27
+ exit_status
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,9 +1,12 @@
1
+ require 'pathname'
1
2
  require 'rzo/logging'
2
3
  require 'deep_merge'
4
+ require 'rzo/app/config_validation'
3
5
  module Rzo
4
6
  class App
5
7
  # The base class for subcommands
6
8
  class Subcommand
9
+ include ConfigValidation
7
10
  include Logging
8
11
  extend Logging
9
12
  # The options hash injected from the application controller via the
@@ -55,12 +58,14 @@ module Rzo
55
58
  # at first match and merge on top of local, defaults (~/.rizzo.json)
56
59
  def load_config!
57
60
  config = load_rizzo_config(opts[:config])
58
- validate_config(config)
61
+ validate_personal_config!(config)
59
62
  repos = config['control_repos']
60
63
  @config = load_repo_configs(config, repos)
61
64
  debug "Merged configuration: \n#{JSON.pretty_generate(@config)}"
62
- validate_forwarded_ports(@config)
63
- validate_ip_addresses(@config)
65
+ # TODO: Move these validations to an instance method?
66
+ validate_complete_config!(@config)
67
+ # validate_forwarded_ports(@config)
68
+ # validate_ip_addresses(@config)
64
69
  @config
65
70
  end
66
71
 
@@ -87,17 +92,6 @@ module Rzo
87
92
  end
88
93
  end
89
94
 
90
- ##
91
- # Basic validation of the configuration file content.
92
- #
93
- # @param [Hash] config the configuration map
94
- def validate_config(config)
95
- errors = []
96
- errors.push 'control_repos key is not an Array' unless config['control_repos'].is_a?(Array)
97
- errors.each { |l| log.error l }
98
- raise ErrorAndExit, 'Errors found in config file. Cannot proceed.' unless errors.empty?
99
- end
100
-
101
95
  ##
102
96
  # Check for duplicate forwarded host ports across all hosts and exit
103
97
  # non-zero with an error message if found.
@@ -47,7 +47,7 @@ module Rzo
47
47
  # name.
48
48
  #
49
49
  # @return [Hash<Symbol, String>] Global options
50
- # rubocop:disable Metrics/MethodLength
50
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
51
51
  def parse_global_options!(argv, env)
52
52
  semver = Rzo::VERSION
53
53
  prog_name = NAME
@@ -58,9 +58,13 @@ module Rzo
58
58
  log_msg = 'Log file to write to or keywords '\
59
59
  'STDOUT, STDERR {RZO_LOGTO}'
60
60
  opt :logto, log_msg, default: env['RZO_LOGTO'] || 'STDERR'
61
+ opt :validate, 'Check the configuration for common issues {RZO_VALIDATE="false"}',
62
+ default: env['RZO_VALIDATE'] == 'false' ? false : true
61
63
  opt :syslog, 'Log to syslog', default: false, conflicts: :logto
62
- opt :verbose, 'Set log level to INFO'
63
- opt :debug, 'Set log level to DEBUG'
64
+ opt :verbose, 'Set log level to INFO {RZO_VERBOSE="true"}',
65
+ default: env['RZO_VERBOSE'] == 'true'
66
+ opt :debug, 'Set log level to DEBUG {RZO_DEBUG="true"}',
67
+ default: env['RZO_DEBUG'] == 'true'
64
68
  opt :config, 'Rizzo config file {RZO_CONFIG}',
65
69
  default: env['RZO_CONFIG'] || '~/.rizzo.json'
66
70
  end
@@ -86,7 +90,7 @@ module Rzo
86
90
  # parsed.
87
91
  #
88
92
  # @return [Hash<Symbol, String>] Subcommand specific options hash
89
- # rubocop:disable Metrics/MethodLength
93
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
90
94
  def parse_subcommand_options!(subcommand, argv, env)
91
95
  prog_name = NAME
92
96
  case subcommand
@@ -100,11 +104,16 @@ module Rzo
100
104
  banner "#{prog_name} #{subcommand} options:"
101
105
  opt :vagrantfile, 'Output Vagrantfile', short: 'o', default: env['RZO_VAGRANTFILE'] || 'Vagrantfile'
102
106
  end
107
+ when 'roles'
108
+ Rzo::Trollop.options(argv) do
109
+ banner "#{prog_name} #{subcommand} options:"
110
+ opt :output, 'Roles output', short: 'o', default: env['RZO_OUTPUT'] || 'STDOUT'
111
+ end
103
112
  else
104
113
  Rzo::Trollop.die "Unknown subcommand: #{subcommand.inspect}"
105
114
  end
106
115
  end
107
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
116
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
108
117
 
109
118
  # The name of the executable, could be `rizzo` or `rzo`
110
119
  NAME = File.basename($PROGRAM_NAME).freeze
@@ -116,6 +125,7 @@ Sub Commands:
116
125
 
117
126
  config Print out the combined rizzo json config
118
127
  generate Initialize Vagrantfile in top control repo
128
+ roles Output all roles defined in the combined config
119
129
 
120
130
  Global options: (Note, command line arguments supersede ENV vars in {}'s)
121
131
  EOBANNER
data/lib/rzo/version.rb CHANGED
@@ -5,7 +5,7 @@ module Rzo
5
5
  # The authoritative location of the rzo version. It should be possible to
6
6
  # `require 'rizzo/version'` and access `Rizzo::VERSION` from third party
7
7
  # libraries and the gemspec. The version is defined as a Semantic Version.
8
- VERSION = '0.3.0'.freeze
8
+ VERSION = '0.4.0'.freeze
9
9
 
10
10
  ##
11
11
  # Return the SemVer string, e.g. `"0.1.0"`
data/rzo.gemspec CHANGED
@@ -42,5 +42,6 @@ Gem::Specification.new do |spec|
42
42
  spec.add_development_dependency 'guard-shell', '~> 0.7'
43
43
  spec.add_development_dependency 'simplecov', '~> 0.14'
44
44
  spec.add_dependency 'json', '~> 2.1'
45
+ spec.add_dependency 'json-schema', '~> 2.8'
45
46
  spec.add_dependency 'deep_merge', '~> 1.1'
46
47
  end
@@ -0,0 +1,343 @@
1
+ #! /bin/bash
2
+ #
3
+ # Test to ensure the gem actually builds. Pre-requisite step to verify the gem
4
+ # installs. Run this script from the repository root.
5
+ # can
6
+
7
+ # Turn off validation for testing purposes. We'll turn it back on for explicit
8
+ # testing of the validation behavior
9
+ export RZO_VALIDATE='false'
10
+
11
+ set -eu
12
+
13
+ STAMP=$(date +%s)
14
+
15
+ if [[ -z "${NO_COLOR:-}" ]]; then
16
+ NC='\033[0m' # No Color
17
+ RED='\033[0;31m'
18
+ GREEN='\033[0;32m'
19
+ YELLOW='\033[0;33m'
20
+ BLUE='\033[0;34m'
21
+ else
22
+ NC=''
23
+ RED=''
24
+ GREEN=''
25
+ YELLOW=''
26
+ BLUE=''
27
+ fi
28
+
29
+ # Describe the test with a nice heading
30
+ desc() {
31
+ msg="$1"
32
+ echo
33
+ echo -e "${GREEN}${msg}${NC}"
34
+ echo "${msg//?/=}"
35
+ return 0
36
+ }
37
+
38
+ testcase() {
39
+ msg="$1"
40
+ echo -en " * ${NC}${msg}:${NC} "
41
+ return 0
42
+ }
43
+
44
+ # Look for a string in some output, e.g. stderr or stdout.
45
+ match() {
46
+ msg="$1" # descriptive message
47
+ expected="$2" # a file
48
+ actual="$3" # extended regexp
49
+ testcase "$msg"
50
+ if grep -qE "$expected" "$actual"; then
51
+ pass "It does."
52
+ return 0
53
+ else
54
+ echo "Expected $actual to contain '${expected}', but it did not." >&2
55
+ echo "Expected:"
56
+ cat "$expected"
57
+ echo
58
+ echo "Actual:"
59
+ cat "$actual"
60
+ fail "Did not find '$expected' in $actual"
61
+ return 1
62
+ fi
63
+ }
64
+
65
+ pass() {
66
+ msg="$1"
67
+ echo -e "${GREEN}PASS:${NC} ${msg}" >&2
68
+ return 0
69
+ }
70
+
71
+ fail() {
72
+ msg="$1"
73
+ echo -e "${RED}FAIL:${NC} $msg" >&2
74
+ return 1
75
+ }
76
+
77
+ warn() {
78
+ msg="$1"
79
+ echo -e "${YELLOW}Warning:${NC} $msg" >&2
80
+ }
81
+
82
+ err() {
83
+ msg="$1"
84
+ echo -e "${RED}Error:${NC} $msg" >&2
85
+ }
86
+
87
+ debug() {
88
+ [[ -z "${DEBUG:-}" ]] && return 0
89
+ msg="$1"
90
+ echo -e "${BLUE}Debug:${NC} $msg" >&2
91
+ }
92
+
93
+ # Move ~/.rizzo.json out of the way if it exists
94
+ if [[ -e ~/.rizzo.json ]]; then
95
+ debug "Moving ~/.rizzo.json to ~/.rizzo.json.$STAMP"
96
+ mv -f ~/.rizzo.json ~/.rizzo.json.$STAMP
97
+ fi
98
+
99
+ # Clean up our temp directory
100
+ scratch=$(mktemp -d)
101
+ export TMPDIR="$scratch"
102
+ finish() {
103
+ if [[ -e ~/.rizzo.json.$STAMP ]]; then
104
+ mv -f ~/.rizzo.json.$STAMP ~/.rizzo.json
105
+ debug "Moved ~/.rizzo.json.$STAMP to ~/.rizzo.json"
106
+ fi
107
+ if [[ -d "$scratch" ]]; then
108
+ rm -rf "$scratch"
109
+ debug "Removed $scratch"
110
+ fi
111
+ }
112
+ trap finish EXIT
113
+
114
+ [ -d pkg ] && rm -rf pkg
115
+ mkdir pkg
116
+ bundle exec rake build
117
+
118
+ # Load RVM into a shell session *as a function*. This is necessary to switch
119
+ # gemsets. This script should still operate without rvm, but it would pollute
120
+ # GEM_HOME, so must be explicitly enabled by the user using RZO_GEM_INSTALL
121
+ for i in "$HOME/.rvm/scripts/rvm" "/usr/local/rvm/scripts/rvm"; do
122
+ if [[ -s "$i" ]]; then
123
+ set +u # RVM uses unbound variables, which makes me very sad. :(
124
+ source "$i"
125
+ RZO_GEM_INSTALL=yes
126
+ break
127
+ fi
128
+ done
129
+
130
+ # We use a custom gemset to ensure the build dependency bundle is isolated from
131
+ # the functional testing phase.
132
+ if [[ -z ${RZO_GEM_INSTALL:-} ]]; then
133
+ warn "rvm not found, GEM_HOME will be tainted by the tests."
134
+ warn "To proceed anyway, run: INSTALL_TO_GEM_HOME=true $0"
135
+ [[ -z ${INSTALL_TO_GEM_HOME:-} ]] && exit 1
136
+ else
137
+ rvm gemset create cleanroom
138
+ rvm gemset use cleanroom
139
+ fi
140
+
141
+ desc "The gem environment used for functional testing"
142
+ gem env
143
+
144
+ desc "There should be minimal gems installed initially"
145
+ gem list
146
+
147
+ desc "Install the gem"
148
+ gem install pkg/*.gem
149
+
150
+ desc "The gem and dependencies should be installed"
151
+ gem list
152
+
153
+ desc "The executable should be in the path"
154
+ which rzo
155
+
156
+ desc "rzo --help should contain usage"
157
+ stdout=$(mktemp -t XXXXXX.stdout)
158
+ rzo --help | tee $stdout
159
+ expected='usage: .*GLOBAL OPTIONS.*SUBCOMMAND.*ARGS'
160
+ if ! grep -qE "$expected" $stdout; then
161
+ fail "rzo --help STDOUT does not contain '$expected'"
162
+ fi
163
+
164
+ desc "rzo --version should output a semantic version string"
165
+ stdout=$(mktemp -t XXXXXX.stdout)
166
+ rzo --version | tee $stdout
167
+ grep -qE '[0-9]+\.[0-9]+\.[0-9]' $stdout
168
+
169
+ desc "rzo bare (no arguments) should match --help"
170
+ bare_output=$(mktemp -t XXXXXX.rzo_bare)
171
+ help_output=$(mktemp -t XXXXXX.rzo_help)
172
+ rzo > $bare_output
173
+ rzo --help > $help_output
174
+ if diff -U2 $help_output $bare_output; then
175
+ pass "It does."
176
+ else
177
+ fail "rzo is not the same as rzo --help"
178
+ fi
179
+
180
+ desc "rzo config with no config should be helpful"
181
+ stdout=$(mktemp -t XXXXXX.stdout)
182
+ stderr=$(mktemp -t XXXXXX.stderr)
183
+ rzo config 2> $stderr | tee $stdout
184
+ expected="Cannot read config file"
185
+ if ! grep -E "$expected" $stderr; then
186
+ echo "STDERR:"
187
+ cat $stderr >&2
188
+ fail "rzo config STDERR does not contain '$expected'"
189
+ else
190
+ pass "Looks good."
191
+ fi
192
+
193
+ # Puppet data repositories used by rizzo
194
+ PUPPETDATA="${TMPDIR}/git/puppetdata"
195
+
196
+ desc "With a valid ~/.rizzo.json file looking like:"
197
+ cat > ~/.rizzo.json <<EOCONFIG
198
+ {
199
+ "defaults": { "bootstrap_repo_path": "${HOME}/git/bootstrap" },
200
+ "control_repos": [ "${PUPPETDATA}", "${HOME}/git/ghoneycutt-modules" ],
201
+ "puppetmaster": {
202
+ "name": [ "puppetca", "puppet" ],
203
+ "modulepath": [ "./modules", "./puppetdata/modules", "./ghoneycutt/modules" ],
204
+ "synced_folders": {
205
+ "/repos/puppetdata": { "local": "${PUPPETDATA}", "owner": "root", "group": "root" },
206
+ "/repos/ghoneycutt": { "local": "${HOME}/git/ghoneycutt-modules", "owner": "root", "group": "root" }
207
+ }
208
+ }
209
+ }
210
+ EOCONFIG
211
+ # Print out the config
212
+ expected=$(mktemp -t XXXXXX.rizzo.json)
213
+ ruby -rjson -e 'puts JSON.pretty_generate(JSON.parse(ARGF.read))' ~/.rizzo.json | tee $expected
214
+
215
+ desc "rzo config is expected to pretty generate the JSON config"
216
+ stdout=$(mktemp -t XXXXXX.stdout)
217
+ stderr=$(mktemp -t XXXXXX.stderr)
218
+ rzo config > $stdout 2> $stderr
219
+ if diff -U2 $expected $stdout; then
220
+ pass "It does."
221
+ else
222
+ fail "rzo config STDOUT differs from expected pretty generated config"
223
+ fi
224
+
225
+ desc "rzo generate produces a minimal Vagrantfile"
226
+
227
+ expected=$(mktemp -t XXXXXXX.vagrantfile)
228
+ actual=$(mktemp -t XXXXXXX.vagrantfile)
229
+ # NOTE: The first line is omitted because it contains a timestamp
230
+ cat > $expected <<VAGRANTFILE
231
+ # https://github.com/ghoneycutt/rizzo
232
+ Vagrant.configure(2) do |config|
233
+ # use 'vagrant plugin install vagrant-proxyconf' to install
234
+ if Vagrant.has_plugin?('vagrant-proxyconf')
235
+ config.proxy.http = ENV['HTTP_PROXY'] if ENV['HTTP_PROXY']
236
+ config.proxy.https = ENV['HTTPS_PROXY'] if ENV['HTTPS_PROXY']
237
+ end
238
+ end
239
+ # -*- mode: ruby -*-
240
+ # vim:ft=ruby
241
+ VAGRANTFILE
242
+
243
+ stdout=$(mktemp -t XXXXXX.stdout)
244
+ stderr=$(mktemp -t XXXXXX.stderr)
245
+ rzo generate 2> $stderr
246
+ tail -n+2 Vagrantfile > $actual
247
+ if diff -U2 $expected $actual; then
248
+ pass "It does."
249
+ else
250
+ fail "rzo generate produced a Vagrantfile different than expected"
251
+ fi
252
+
253
+ expected="Wrote vagrant config to Vagrantfile"
254
+ desc "rzo generate STDERR is expected to match '$expected'"
255
+ if ! grep -qE "$expected" $stderr; then
256
+ echo "Expected:"
257
+ echo "$expected"
258
+ echo
259
+ echo "Actual:"
260
+ cat $stderr
261
+ echo
262
+ fail "Expected STDOUT of rzo generate does not match actual output"
263
+ else
264
+ pass "It does."
265
+ fi
266
+
267
+ desc "rzo roles with no personal .rizzo.json is expected to warn"
268
+ stdout=$(mktemp -t XXXXXX.stdout)
269
+ stderr=$(mktemp -t XXXXXX.stderr)
270
+ rzo roles 2> $stderr > $stdout
271
+
272
+ match "warns about puppetdata not being a directory" 'WARN .*puppetdata is not a directory' $stderr
273
+ match "warns about ghoneycutt-modules not being a directory" 'WARN .*ghoneycutt-modules is not a directory' $stderr
274
+ match "warns about bootstrap not being a directory" 'WARN .*bootstrap is not a directory' $stderr
275
+
276
+ desc "with a single puppetca role, rzo roles outputs the role name"
277
+ if ! [[ -d "$PUPPETDATA" ]]; then
278
+ mkdir -p "$PUPPETDATA"
279
+ debug "Created $PUPPETDATA"
280
+ fi
281
+ echo '{"nodes":[{"name":"puppetca"}]}' > ${PUPPETDATA}/.rizzo.json
282
+
283
+ stdout=$(mktemp -t XXXXXX.stdout)
284
+ stderr=$(mktemp -t XXXXXX.stderr)
285
+ rzo roles 2> $stderr > $stdout
286
+ expected='puppetca'
287
+ if grep -qxE "$expected" $stdout; then
288
+ pass "rzo roles STDOUT, expected: '$expected' got: '$(cat $stdout)'"
289
+ else
290
+ fail "rzo roles STDOUT, expected: '$expected' got: '$(cat $stdout)'"
291
+ fi
292
+
293
+ desc "rzo generate is expected to produce a Vagrantfile with one VM defined"
294
+ expected=$(mktemp -t XXXXXX.vagrantfile.expected)
295
+ stdout=$(mktemp -t XXXXXX.stdout)
296
+ stderr=$(mktemp -t XXXXXX.stderr)
297
+ cat <<VAGRANTFILE > $expected
298
+ # https://github.com/ghoneycutt/rizzo
299
+ Vagrant.configure(2) do |config|
300
+ # use 'vagrant plugin install vagrant-proxyconf' to install
301
+ if Vagrant.has_plugin?('vagrant-proxyconf')
302
+ config.proxy.http = ENV['HTTP_PROXY'] if ENV['HTTP_PROXY']
303
+ config.proxy.https = ENV['HTTPS_PROXY'] if ENV['HTTPS_PROXY']
304
+ end
305
+
306
+ config.vm.define "puppetca", autostart: false do |cfg|
307
+ cfg.vm.box = nil
308
+ cfg.vm.box_url = nil
309
+ cfg.vm.box_download_checksum = nil
310
+ cfg.vm.box_download_checksum_type = nil
311
+ cfg.vm.provider :virtualbox do |vb|
312
+ vb.customize ['modifyvm', :id, '--memory', nil]
313
+ end
314
+ cfg.vm.hostname = nil
315
+ cfg.vm.network 'private_network',
316
+ ip: nil,
317
+ netmask: nil
318
+ cfg.vm.synced_folder "${PUPPETDATA}", "/repos/puppetdata",
319
+ owner: "root", group: "root"
320
+ cfg.vm.synced_folder "${HOME}/git/ghoneycutt-modules", "/repos/ghoneycutt",
321
+ owner: "root", group: "root"
322
+ config.vm.synced_folder "${HOME}/git/bootstrap",
323
+ nil,
324
+ owner: 'vagrant', group: 'root'
325
+ config.vm.provision 'shell', inline: "echo 'modulepath = ./modules:./puppetdata/modules:./ghoneycutt/modules' > /environment.conf"
326
+ config.vm.provision 'shell', inline: "/bin/bash / "
327
+ end
328
+ end
329
+ # -*- mode: ruby -*-
330
+ # vim:ft=ruby
331
+ VAGRANTFILE
332
+ rzo generate 2> $stderr > $stdout
333
+ actual=$(mktemp -t XXXXXX.vagrantfile.actual)
334
+ tail -n+2 Vagrantfile > $actual
335
+ if diff -U2 $expected $actual; then
336
+ cat Vagrantfile
337
+ pass "It does."
338
+ else
339
+ fail "actual Vagrantfile does not match expected file"
340
+ fi
341
+
342
+ desc "END of functional testing"
343
+ pass "Functional testing completed successfully."
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rzo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Garrett Honeycutt
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2017-08-24 00:00:00.000000000 Z
12
+ date: 2017-09-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -193,6 +193,20 @@ dependencies:
193
193
  - - "~>"
194
194
  - !ruby/object:Gem::Version
195
195
  version: '2.1'
196
+ - !ruby/object:Gem::Dependency
197
+ name: json-schema
198
+ requirement: !ruby/object:Gem::Requirement
199
+ requirements:
200
+ - - "~>"
201
+ - !ruby/object:Gem::Version
202
+ version: '2.8'
203
+ type: :runtime
204
+ prerelease: false
205
+ version_requirements: !ruby/object:Gem::Requirement
206
+ requirements:
207
+ - - "~>"
208
+ - !ruby/object:Gem::Version
209
+ version: '2.8'
196
210
  - !ruby/object:Gem::Dependency
197
211
  name: deep_merge
198
212
  requirement: !ruby/object:Gem::Requirement
@@ -237,7 +251,9 @@ files:
237
251
  - lib/rzo.rb
238
252
  - lib/rzo/app.rb
239
253
  - lib/rzo/app/config.rb
254
+ - lib/rzo/app/config_validation.rb
240
255
  - lib/rzo/app/generate.rb
256
+ - lib/rzo/app/roles.rb
241
257
  - lib/rzo/app/subcommand.rb
242
258
  - lib/rzo/app/templates/Vagrantfile.erb
243
259
  - lib/rzo/logging.rb
@@ -245,6 +261,7 @@ files:
245
261
  - lib/rzo/trollop.rb
246
262
  - lib/rzo/version.rb
247
263
  - rzo.gemspec
264
+ - scripts/functional_gem_behavior.sh
248
265
  homepage: https://github.com/ghoneycutt/rizzo
249
266
  licenses:
250
267
  - Apache-2.0