molder 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d2480986a1941cf8389155f51644915554997ce09b92437f3eb24c1b4bd6fbe9
4
+ data.tar.gz: 3001d82af80718bc6f4db8ade5bef766e27d199f16fac8136d7a30a5924a036e
5
+ SHA512:
6
+ metadata.gz: f94f91528b905aea08c623e7a50fa575304f6dfbd9ef8d8c8c821252ee1e3c7919237eff400c3398f0d78d868a7034524a6b63ec55d8eafc04cd487656ef3cb5
7
+ data.tar.gz: 13f261c86a9ea2892d26790ef2b5bf17d76b4235d05e22e909df334f599821cfa777fad1079a78fc71ab6f3f868552c90399fbe807db13a353d33349b930fa4f
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ InstalledFiles
7
+ _yardoc
8
+ Gemfile.lock
9
+ coverage
10
+ pkg
11
+ doc
12
+ spec/reports
13
+ tmp
14
+ vendor/
15
+ **/.DS_Store
16
+ .idea/
17
+ log/
18
+ .ruby-version
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format progress
2
+ --color
@@ -0,0 +1,4 @@
1
+ example_id | status | run_time |
2
+ -------------------------- | ------ | --------------- |
3
+ ./spec/molder_spec.rb[1:1] | passed | 0.00067 seconds |
4
+ ./spec/molder_spec.rb[1:2] | failed | 0.00965 seconds |
@@ -0,0 +1,26 @@
1
+ language: ruby
2
+ env:
3
+ global:
4
+ - CODECLIMATE_REPO_TOKEN=49ca8c0e2d56ab70e7eabc237692e21cc8fe43ef13ed5d411fa20bf1e8997a44
5
+ rvm:
6
+ - 2.2.9
7
+ - 2.3.6
8
+ - 2.4.3
9
+ - 2.5.0
10
+ cache:
11
+ - bundler
12
+ before_script:
13
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
14
+ - chmod +x ./cc-test-reporter
15
+ - ./cc-test-reporter before-build
16
+ after_script:
17
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
18
+ script: bundle exec rspec
19
+ notifications:
20
+ slack:
21
+ rooms:
22
+ email:
23
+ recipients:
24
+ - kigster@gmail.com
25
+ on_success: change
26
+ on_failure: always
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in molder.gemspec
6
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Konstantin Gredeskoul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,209 @@
1
+ [![Build Status](https://travis-ci.org/kigster/molder.svg?branch=master)](https://travis-ci.org/kigster/molder)
2
+
3
+ # Molder
4
+
5
+ Molder is a command line tool for generating and running (in parallel) a set of related but similar commands. A key
6
+ use-case is auto-generation of the host provisioning commands for an arbitrary cloud environment. The gem is not constrained to any particular cloud tool or even a command, and can be used to generate a consistent set of commands based on several customizable dimensions.
7
+
8
+ For example, you could generate 600 provisioning commands for hosts in EC2, numbered from 1 to 100, constrained to the dimensions "zone-id" (values: ["a", "b", "c"]) and the data center "dc" (values: ['us-west2', 'us-east1' ]).
9
+
10
+ ## Usage
11
+
12
+ Molder works in the following way:
13
+
14
+ * It reads the configuration YAML file, described further below
15
+
16
+ * It parses command line arguments, and extracts the **command name** and **template name(s)**
17
+
18
+ * It matches the command name with one of the commands specified in the YAML file
19
+
20
+ * It matches the template name(s) with those in the YAML file
21
+
22
+ * It then uses [Liquid template language](https://shopify.github.io/liquid/) to substitute tokens in the command template, possibly enumerating over provided numbers.
23
+
24
+ * Tokens are taken from the template definition, and two more tokens are added: `formatted_number` and `number`.
25
+
26
+ ### YAML Configuration File
27
+
28
+ Here is the semi-minimal YAML file that demonstrates all features.
29
+
30
+ First, at the top, we define the global section. The only arguments there are `log_dir` and `index_format`, the latter is the `sprintf` pattern applied to the numeric index.
31
+
32
+ ```yaml
33
+ global:
34
+ log_dir: ./log
35
+ index_format: '%03.3d'
36
+ ```
37
+
38
+ Second, we have a `configuration` section that is not really used by the library, but is used within this YAML file to define the templates with the minimum repetition. Note, how we are using `? role[base]` to indicate a hash key with a nil value. These can be merged down below, and `molder` converts all hashes with nil values into an array of keys.
39
+
40
+ Note how in the name of the instances we are using Liquid markup to reference `formatted_number` and `zone_id`. The former is provided by the gem, and the latter is provided by the template itself.
41
+
42
+ ```yaml
43
+ configuration:
44
+ ec2-base: &ec2-base
45
+ image: ami-f9u98f
46
+ flavor: c5.4xlarge
47
+ security_group_id: ssg-f8987987
48
+ ssh_user: ubuntu
49
+ ssh_key: ubuntu_key
50
+ identity_file: '~/.ssh/ec2.pem'
51
+ run_list: &run_list_base
52
+ ? role[base]
53
+
54
+ web: &ec2-web
55
+ <<: *ec2-base
56
+ name: web{{ formatted_number }}-{{ zone_id }}
57
+ run_list: &run_list_web
58
+ ? role[web]
59
+
60
+ job: &ec2-job
61
+ <<: *ec2-base
62
+ flavor: c5.2xlarge
63
+ name: job{{ formatted_number }}-{{ zone_id }}
64
+ run_list: &run_list_job
65
+ ? role[job]
66
+
67
+ us-east1-a: &us-east1-a
68
+ subnet: subnet-ff09898
69
+ zone: us-east1-a
70
+ zone_id: a
71
+ run_list: &run_list_zone_a
72
+ ? role[zone-a]
73
+
74
+ us-east1-b: &us-east1-b
75
+ subnet: subnet-f909809
76
+ zone: us-east1-b
77
+ zone_id: b
78
+ run_list: &run_list_zone_b
79
+ ? role[zone-b]
80
+ ```
81
+
82
+ Next, we define the actual templates. These are composed of several YAML entries defined above.
83
+
84
+ Note that we define both zone-specific instances of two types (web and job), as well as an array of web instances (comprised of both zones a and b), and job instances.
85
+
86
+
87
+ ```yaml
88
+ templates:
89
+ web-a: &web-a
90
+ <<: *us-east1-a
91
+ <<: *ec2-web
92
+ run_list:
93
+ <<: *run_list_base
94
+ <<: *run_list_web
95
+ <<: *run_list_zone_a
96
+
97
+ web-b: &web-b
98
+ <<: *ec2-web
99
+ <<: *us-east1-b
100
+ run_list:
101
+ <<: *run_list_base
102
+ <<: *run_list_web
103
+ <<: *run_list_zone_b
104
+
105
+ job-a: &job-a
106
+ <<: *ec2-job
107
+ <<: *us-east1-a
108
+ run_list:
109
+ <<: *run_list_base
110
+ <<: *run_list_job
111
+ <<: *run_list_zone_a
112
+
113
+ job-b: &job-b
114
+ <<: *ec2-job
115
+ <<: *us-east1-b
116
+ run_list:
117
+ <<: *run_list_base
118
+ <<: *run_list_job
119
+ <<: *run_list_zone_b
120
+
121
+ web:
122
+ - <<: *web-a
123
+ - <<: *web-b
124
+
125
+ job:
126
+ - <<: *job-a
127
+ - <<: *job-b
128
+ ```
129
+
130
+ The final section defines commands that we can generate using this YAML file. We are only including one command here, called `provision`, which comes with a description and args. Args include extensive set of liquid tokens that are pulled from the template attributes applied to this command.
131
+
132
+ ```yaml
133
+ commands:
134
+ provision:
135
+ desc: Provision hosts on AWS EC2 using knife ec2 plugin.
136
+ args: |
137
+ echo knife ec2 server create
138
+ -N {{ name }}
139
+ -I {{ image }}
140
+ -Z {{ zone }}
141
+ -f {{ flavor }}
142
+ --environment {{ environment }}
143
+ --subnet {{ subnet }}
144
+ -g {{ security_group_id }}
145
+ -r {{ run_list }}
146
+ -S {{ ssh_key }}
147
+ -i {{ identity_file }}
148
+ --ssh-user {{ ssh_user }}; sleep 2
149
+ ```
150
+
151
+ So how would we use molder to generate commands that provision a bunch of ec2 hosts for us?
152
+
153
+ ```bash
154
+ $ molder provision web[1..5]/job[1,4,6] -a environment=production -c config/molder.yml
155
+ ```
156
+
157
+ This is the output we would see:
158
+
159
+ ![output](docs/molder.png)
160
+
161
+ Let's understand this command:
162
+
163
+ * first argument is `provision` — it has to match one of the commands in the YAML file.
164
+
165
+ * second argument consists of template names, followed by the numbers in square brackets. Either comma-separated numbers are supported, or a range (not that a range must also be included in square brackets)
166
+
167
+ * multiple template names can be separated by a slash, as seen here.
168
+
169
+ * next we pass `-a environment=production` — notice that our provision command defined in the template uses `{{ environment }}` token, even though no such attribute is defined in any of the templates. if we do not supply this argument, the value of environment in the command line would be blank.
170
+
171
+ * Note that you can pass multiple attributes, separated by a slash, like so: `-a environment=production/flavor=c5.4xlarge`
172
+
173
+ * The final argument is the template file. The default location is `config/molder.yml` — so if you place the file in that folder you don't need to pass `-c` argument.
174
+
175
+ ### Complete Options List
176
+
177
+ If you run `molder -h` you would see the following:
178
+
179
+ ![cli](docs/molder-cli.png)
180
+
181
+ ## Installation
182
+
183
+ Add this line to your application's Gemfile:
184
+
185
+ ```ruby
186
+ gem 'molder'
187
+ ```
188
+
189
+ And then execute:
190
+
191
+ $ bundle
192
+
193
+ Or install it yourself as:
194
+
195
+ $ gem install molder
196
+
197
+ ## Development
198
+
199
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
200
+
201
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
202
+
203
+ ## Contributing
204
+
205
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kigster/molder.
206
+
207
+ ## License
208
+
209
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,35 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'yard'
4
+
5
+ def shell(*args)
6
+ puts "running: #{args.join(' ')}"
7
+ system(args.join(' '))
8
+ end
9
+
10
+ task :clean do
11
+ shell 'rm -rf log/ pkg/ tmp/ coverage/ doc/'
12
+ end
13
+
14
+ task :gem => [:build] do
15
+ shell('gem install pkg/*')
16
+ end
17
+
18
+ task :permissions => [ :clean ] do
19
+ shell("chmod -v o+r,g+r * */* */*/* */*/*/* */*/*/*/* */*/*/*/*/*")
20
+ shell("find . -type d -exec chmod o+x,g+x {} \\;")
21
+ end
22
+
23
+ task :build => :permissions
24
+
25
+ YARD::Rake::YardocTask.new(:doc) do |t|
26
+ t.files = %w(lib/**/*.rb exe/*.rb - README.md LICENSE.txt)
27
+ t.options.unshift('--title','Molder - command generator based on templates')
28
+ t.after = ->() { exec('open doc/index.html') }
29
+ end
30
+
31
+ RSpec::Core::RakeTask.new(:spec)
32
+
33
+ task :default => :spec
34
+
35
+
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "molder"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
Binary file
Binary file
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ require 'rubygems'
4
+ require 'molder'
5
+
6
+ Molder::CLI.new(ARGV).execute!
@@ -0,0 +1,9 @@
1
+ require 'molder/version'
2
+ require 'require_dir'
3
+ module Molder
4
+ RequireDir.enable_require_dir!(self, __FILE__)
5
+
6
+ dir_r 'molder'
7
+ end
8
+
9
+
@@ -0,0 +1,81 @@
1
+ require 'hashie/mash'
2
+ require 'hashie/extensions/mash/symbolize_keys'
3
+ require 'molder/errors'
4
+ require 'parallel'
5
+ require 'fileutils'
6
+ module Molder
7
+ class App
8
+
9
+ attr_accessor :config, :options, :command, :command_name, :commands, :templates, :log_dir
10
+
11
+ def initialize(config:, options:, command_name:)
12
+ self.config = config
13
+ self.options = options
14
+ self.command_name = command_name
15
+ self.commands = []
16
+ self.log_dir = options[:log_dir] || config.global.log_dir || './log'
17
+
18
+ resolve_command!
19
+
20
+ resolve_templates!
21
+ end
22
+
23
+ def execute!
24
+ colors = %i(yellow blue red green magenta cyan white)
25
+
26
+ FileUtils.mkdir_p(log_dir)
27
+ puts "Executing #{commands.size} commands using a pool of up to #{options.max_processes} processes:\n".bold.cyan.underlined
28
+ ::Parallel.each((1..commands.size),
29
+ :in_processes => options.max_processes) do |i|
30
+
31
+ color = colors[(i - 1) % colors.size]
32
+ cmd = commands[i - 1]
33
+
34
+ printf('%s', "Worker: #{Parallel.worker_number}, command #{i}\n".send(color)) if options.verbose
35
+ puts "#{cmd}\n".send(color)
36
+
37
+ system %Q(( #{cmd} ) > #{log_dir}/#{command_name}.#{i}.log) unless options.dry_run
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def resolve_templates!
44
+ self.templates ||= []
45
+ options.names.each_pair do |name, indexes|
46
+ if config.templates[name]
47
+ template_array = config.templates[name].is_a?(Array) ?
48
+ config.templates[name] :
49
+ [config.templates[name]]
50
+
51
+ template_array.flatten.each do |attrs|
52
+ attributes = attrs.dup
53
+ attributes.merge!(options.override) if options.override
54
+ self.templates << ::Molder::Template.new(config: config,
55
+ name: name,
56
+ indexes: indexes,
57
+ command: command,
58
+ attributes: attributes)
59
+ end
60
+ else
61
+ raise ::Molder::InvalidTemplateName, "Template name #{name} is not valid."
62
+ end
63
+ end
64
+
65
+ self.templates.each do |t|
66
+ t.each_command do |cmd|
67
+ self.commands << cmd
68
+ end
69
+ end
70
+ end
71
+
72
+ def resolve_command!
73
+ unless config.commands.include?(command_name)
74
+ raise(::Molder::InvalidCommandError, "Command #{command_name} is not defined in the configuration file #{options[:config]}")
75
+ end
76
+
77
+ command_hash = Hashie::Extensions::SymbolizeKeys.symbolize_keys(config.commands[command_name].to_h)
78
+ self.command = Molder::Command.new(name: command_name, config: config, **command_hash)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,188 @@
1
+ require 'hashie/mash'
2
+ require 'colored2'
3
+ require 'optionparser'
4
+
5
+ require 'etc'
6
+
7
+ module Molder
8
+ class CLI
9
+ attr_accessor :argv, :original_argv, :options, :config, :command
10
+
11
+ def initialize(argv, stdin = STDIN, stdout = STDOUT, stderr = STDERR, kernel = Kernel)
12
+ @argv, @stdin, @stdout, @stderr, @kernel = argv, stdin, stdout, stderr, kernel
13
+
14
+ self.options = Hashie::Mash.new
15
+ self.options[:max_processes] = Etc.nprocessors - 2
16
+ self.argv = argv.dup
17
+ self.original_argv = argv.dup
18
+
19
+ self.argv << '-h' if argv.empty?
20
+
21
+ parser.parse!(self.argv)
22
+ exit(0) if options.help
23
+
24
+ pre_parse!
25
+
26
+ if options.indexes
27
+ override = {}
28
+ options.names.each_pair do |name, values|
29
+ if values.nil?
30
+ override[name] = option.indexes
31
+ end
32
+ end
33
+ options.names.merge!(override)
34
+ end
35
+
36
+ self.config = if options.config
37
+ if File.exist?(options.config)
38
+ Configuration.load(options.config)
39
+ else
40
+ report_error(message: "file #{options.config} does not exist.")
41
+ end
42
+ else
43
+ Configuration.default
44
+ end
45
+ end
46
+
47
+ def execute!
48
+ exit_code = begin
49
+ $stderr = @stderr
50
+ $stdin = @stdin
51
+ $stdout = @stdout
52
+
53
+ App.new(config: config, options: options, command_name: command).execute!
54
+
55
+ 0
56
+ rescue StandardError => e
57
+ report_error(exception: e)
58
+ 1
59
+ rescue SystemExit => e
60
+ e.status
61
+ ensure
62
+ $stderr = STDERR
63
+ $stdin = STDIN
64
+ $stdout = STDOUT
65
+ end
66
+ @kernel.exit(exit_code)
67
+ end
68
+
69
+ private
70
+
71
+ def pre_parse!
72
+ if argv[0] && !argv[0].start_with?('-')
73
+ self.command = argv.shift
74
+ end
75
+
76
+ if self.argv[0] && !self.argv[0].start_with?('-')
77
+ options[:names] = Hashie::Mash.new
78
+ self.argv.shift.split('/').each { |arg| parse_templates(arg) }
79
+ end
80
+ end
81
+
82
+ def parser
83
+ OptionParser.new(nil, 35) do |opts|
84
+ opts.separator 'OPTIONS:'.bold.yellow
85
+
86
+ opts.on('-c', '--config [file]',
87
+ 'Main YAML configuration file') { |config| options[:config] = config }
88
+
89
+ opts.on('-t', '--template [n1/n2/..]',
90
+ 'Names of the templates to use') do |value|
91
+ options[:names] ||= Hashie::Mash.new
92
+ value.split('/').each { |arg| parse_templates(arg) }
93
+ end
94
+
95
+ opts.on('-i', '--index [range/array]',
96
+ 'Numbers to use in generating commands',
97
+ 'Can be a comma-separated list of values,',
98
+ 'or a range, eg "1..5"') do |value|
99
+ options[:indexes] = index_expression_to_array(value)
100
+ end
101
+
102
+ opts.on('-a', '--attrs [k1=v1/k2=v2/...]',
103
+ 'Provide additional attributes, or override existing ones') do |value|
104
+ h = {}
105
+ value.split('/').each do |pair|
106
+ key, value = pair.split('=')
107
+ h[key] = value
108
+ end
109
+ options[:override] = h
110
+ end
111
+
112
+ opts.on('-m', '--max-processes [number]',
113
+ 'Do not start more than this many processes at once') { |value| options[:max_processes] = value.to_i }
114
+
115
+ opts.on('-l', '--log-dir [dir]',
116
+ 'Directory where STDOUT of running commands is saved') { |value| options[:log_dir] = value }
117
+
118
+ opts.on('-n', '--dry-run',
119
+ 'Don\'t actually run commands, just print them') { |_value| options[:dry_run] = true }
120
+
121
+ opts.on('-v', '--verbose',
122
+ 'Print more output') { |_value| options[:verbose] = true }
123
+
124
+ opts.on('-b', '--backtrace',
125
+ 'Show error stack trace if available') { |_value| options[:backtrace] = true }
126
+
127
+ opts.on('-h', '--help',
128
+ 'Show help') do
129
+ @stdout.puts opts
130
+ options[:help] = true
131
+ end
132
+
133
+ end.tap do |p|
134
+ p.banner = <<-eof
135
+ #{'DESCRIPTION'.bold.yellow}
136
+ Molder is a template based command generator and runner for cases where you need to
137
+ generate many similar and yet somewhat different commands, defined in the
138
+ YAML template. Please read #{'https://github.com/kigster/molder'.bold.blue.underlined} for
139
+ a detailed explanation of the config file structure.
140
+
141
+ Note that the default configuration file is #{Molder::Configuration::DEFAULT_CONFIG.bold.green}.
142
+
143
+ #{'USAGE'.bold.yellow}
144
+ #{'molder [-c config.yml] command template1[n1..n2]/template2[n1,n2,..]/... [options]'.bold.blue}
145
+ #{'molder [-c config.yml] command -t template -i index [options]'.blue.bold}
146
+
147
+ #{'EXAMPLES'.bold.yellow}
148
+ #{'# The following commands assume YAML file is in the default location:'.bold.black}
149
+ #{'molder provision web[1,3,5]'.bold.blue}
150
+
151
+ #{'# -n flag means dry run — so instead of running commands, just print them:'.bold.black}
152
+ #{'molder provision web[1..4]/job[1..4] -n'.bold.blue}
153
+
154
+ eof
155
+ end
156
+ end
157
+
158
+ def report_error(message: nil, exception: nil)
159
+ if options[:backtrace] && exception.backtrace
160
+ @stderr.puts exception.backtrace.reverse.join("\n").yellow.italic
161
+ end
162
+ @stderr.puts "Error: #{exception.to_s.bold.red}" if exception
163
+ @stderr.puts "Error: #{message.bold.red}" if message
164
+ @kernel.exit(1)
165
+ end
166
+
167
+ def parse_templates(arg)
168
+ options[:names] ||= Hashie::Mash.new
169
+ templates = arg.split('/')
170
+ templates.each do |t|
171
+ name, indexes = parse_name(t)
172
+ options[:names][name] = indexes
173
+ end
174
+ end
175
+
176
+ def parse_name(t)
177
+ name, values = t.split('[')
178
+ values.gsub!(/\]/, '') if values
179
+ [name, index_expression_to_array(values)]
180
+ end
181
+
182
+ def index_expression_to_array(value = nil)
183
+ return nil if value.nil?
184
+ value.include?('..') ? eval("(#{value}).to_a") : eval("[#{value}]")
185
+ end
186
+ end
187
+ end
188
+
@@ -0,0 +1,16 @@
1
+ module Molder
2
+ class Command
3
+ attr_accessor :name, :config, :desc, :supervise, :concurrent, :examples, :args
4
+
5
+ def initialize(name:, config:, desc:, supervise: true, concurrent: true, examples: [], args:)
6
+ self.name = name
7
+ self.config = config
8
+ self.desc = desc
9
+ self.supervise = supervise
10
+ self.concurrent = concurrent
11
+ self.examples = examples
12
+ self.args = args
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ require 'yaml'
2
+ require 'hashie/mash'
3
+ require 'hashie/extensions/parsers/yaml_erb_parser'
4
+
5
+ module Molder
6
+ class Configuration < Hashie::Mash
7
+ DEFAULT_CONFIG = 'config/molder.yml'.freeze
8
+ class << self
9
+ def default_config
10
+ DEFAULT_CONFIG
11
+ end
12
+
13
+ def default
14
+ if File.exist?(default_config)
15
+ load(default_config)
16
+ else
17
+ raise ::Molder::ConfigNotFound, "Default file #{default_config} was not found"
18
+ end
19
+
20
+ end
21
+
22
+ def load(file)
23
+ self.new(YAML.load(File.read(file)))
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ module Molder
2
+ class MolderError < StandardError; end
3
+ class InvalidCommandError < MolderError; end
4
+ class InvalidTemplateName < MolderError; end
5
+ class ConfigNotFound < MolderError; end
6
+ class LiquidTemplateError < MolderError; end
7
+ end
@@ -0,0 +1,109 @@
1
+ require 'fileutils'
2
+ require 'liquid'
3
+ require 'hashie'
4
+ require 'colored2'
5
+
6
+ class Symbol
7
+ def to_liquid
8
+ to_i
9
+ end
10
+ end
11
+
12
+ module Molder
13
+ #
14
+ # == Usage
15
+ #
16
+ # Generally you first generate a set of parameters (a nested hash) that
17
+ # represents configuration of your application. As was mentioned above, the
18
+ # parameter hash can be self-referential – it will be automatically expanded.
19
+ #
20
+ # Once you create a Renderer instance with a given parameter set, you can then
21
+ # use the +#render+ method to convert content with Renderer placeholers into a
22
+ # fully resolved string.
23
+ #
24
+ # == Example
25
+ #/
26
+ # require 'molder/renderer'
27
+ #
28
+ # params = { 'invitee' => 'Adam',
29
+ # 'extra' => 'Eve',
30
+ # 'salutation' => 'Dear {{ invitee }} & {{ extra }}',
31
+ # 'from' => 'Jesus'
32
+ # }
33
+ # @Renderer = ::Molder::Renderer.new(params)
34
+ # ⤷ #<Molder::Renderer:0x007fb90b9c32d8>
35
+ # content = '{{ salutation }}, please attend my birthday. Sincerely, {{ from }}.'
36
+ # ⤷ {{ salutation }}, please attend my birthday. Sincerely, {{ from }}.
37
+ # @Renderer.render(content)
38
+ # ⤷ "Dear Adam & Eve, please attend my birthday. Sincerely, Jesus."
39
+ #
40
+ # == Troubleshooting
41
+ #
42
+ # See errors documented under this class.
43
+ #
44
+ class Renderer
45
+
46
+ # When the parameter hash contains a circular reference, this
47
+ # error will be thrown. It is thrown after the params hash is attempted
48
+ # to be expanded MAX_RECURSIONS times.
49
+ class TooManyRecursionsError < StandardError;
50
+ end
51
+
52
+ # When a Renderer (or params) contain a reference that can not be resolved
53
+ # this error is raised.
54
+ class UnresolvedReferenceError < ArgumentError;
55
+ end
56
+
57
+ # During parameter resolution phase (constructor) this error indicates that
58
+ # internal representation of the params hash (YAML) no longer compiles after
59
+ # some parameters have been resolved. This would be an internal error that
60
+ # should be coded around and fixed as a bug if it ever to occur.
61
+ class SyntaxError < StandardError;
62
+ end
63
+
64
+ MAX_RECURSIONS = 100
65
+
66
+ attr_accessor :template
67
+
68
+ # Create Renderer object, while storing and auto-expanding params.
69
+ def initialize(template)
70
+ self.template = template
71
+ end
72
+
73
+ # Render given content using expanded params.
74
+ def render(params)
75
+ attributes = expand_arguments(Hashie.stringify_keys(params.to_h))
76
+ liquid_template = Liquid::Template.parse(template)
77
+ liquid_template.render(attributes, { strict_variables: true }).tap do
78
+ unless liquid_template.errors.empty?
79
+ raise LiquidTemplateError, "#{liquid_template.errors.map(&:message).join("\n")}"
80
+ end
81
+ end.gsub(/\n/, ' ').gsub(/\s{2,}/, ' ').strip
82
+ rescue ArgumentError => e
83
+ raise UnresolvedReferenceError.new(e)
84
+ end
85
+
86
+ private
87
+
88
+ def expand_arguments(params)
89
+ current = YAML.dump(params)
90
+ recursions = 0
91
+
92
+ while current =~ %r[{{\s*[a-z_]+\s*}}]
93
+ recursions += 1
94
+ raise TooManyRecursionsError.new if recursions > MAX_RECURSIONS
95
+ previous = current
96
+ current = ::Liquid::Template.parse(previous).render(params)
97
+ end
98
+
99
+ begin
100
+ Hashie::Mash.new(YAML.load(current))
101
+ rescue Psych::SyntaxError => e
102
+ STDERR.puts "Error parsing YAML Renderer:\n" +
103
+ e.message.red +
104
+ "\n#{current}"
105
+ raise SyntaxError.new(e)
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,35 @@
1
+ require 'molder/renderer'
2
+ module Molder
3
+ class Template
4
+ attr_accessor :config, :name, :attributes, :indexes, :command
5
+
6
+ def initialize(config:, name:, indexes:, attributes: {}, command:)
7
+ self.config = config
8
+ self.name = name
9
+ self.indexes = indexes
10
+ self.command = command
11
+ self.attributes = self.class.normalize(attributes)
12
+ end
13
+
14
+ def each_command
15
+ indexes.map do |i|
16
+ self.attributes[:number] = i
17
+ self.attributes[:formatted_number] = sprintf(config.global.index_format, i)
18
+ ::Molder::Renderer.new(command.args).render(attributes.dup).tap do |cmd|
19
+ yield(cmd) if block_given?
20
+ end
21
+ end
22
+ end
23
+
24
+ def self.normalize(attrs)
25
+ override = {}
26
+ attrs.each_pair do |key, value|
27
+ if value.is_a?(Hash) && value.values.compact.empty?
28
+ override[key] = value.keys.to_a.join(',')
29
+ end
30
+ end
31
+ attrs.merge!(override)
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,7 @@
1
+ module Molder
2
+ VERSION = '0.1.4'.freeze
3
+ DESCRIPTION = <<-eof
4
+ Molder is a command line tool for generating and running (in parallel, across a configurable number of processes) a set of related but similar commands that are generated based on a merge of a template with a set of attributes. A key use-case is auto-generation of the host provisioning commands for an arbitrary cloud environment. The gem is not constrained to any particular cloud tool or even a command, and can be used to generate a consistent set of commands based on several customizable dimensions. For example, you could generate 600 provisioning commands for hosts in EC2, numbered from 1 to 100, constrained to the dimensions "zone-id" (values: ["a", "b", "c"]) and the data center "dc" (values: ['us-west2', 'us-east1' ]).
5
+ eof
6
+ .gsub(/\s{2,}/, ' ')
7
+ end
@@ -0,0 +1,35 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'molder/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'molder'
7
+ spec.version = ::Molder::VERSION
8
+ spec.authors = ['Konstantin Gredeskoul']
9
+ spec.email = ['kigster@gmail.com']
10
+
11
+ spec.summary = Molder::DESCRIPTION
12
+ spec.description = Molder::DESCRIPTION
13
+ spec.homepage = 'https://github.com/kigster/molder'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_dependency 'liquid'
24
+ spec.add_dependency 'hashie'
25
+ spec.add_dependency 'colored2'
26
+ spec.add_dependency 'parallel'
27
+ spec.add_dependency 'require_dir', '~> 2'
28
+
29
+ spec.add_development_dependency 'simplecov'
30
+ spec.add_development_dependency 'awesome_print'
31
+ spec.add_development_dependency 'rake'
32
+ spec.add_development_dependency 'yard'
33
+ spec.add_development_dependency 'rspec', '~> 3'
34
+ spec.add_development_dependency 'rspec-its'
35
+ end
metadata ADDED
@@ -0,0 +1,240 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: molder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.4
5
+ platform: ruby
6
+ authors:
7
+ - Konstantin Gredeskoul
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-04-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: liquid
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: hashie
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: colored2
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: parallel
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: require_dir
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: awesome_print
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: yard
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rspec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '3'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '3'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rspec-its
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ description: 'Molder is a command line tool for generating and running (in parallel,
168
+ across a configurable number of processes) a set of related but similar commands
169
+ that are generated based on a merge of a template with a set of attributes. A key
170
+ use-case is auto-generation of the host provisioning commands for an arbitrary cloud
171
+ environment. The gem is not constrained to any particular cloud tool or even a command,
172
+ and can be used to generate a consistent set of commands based on several customizable
173
+ dimensions. For example, you could generate 600 provisioning commands for hosts
174
+ in EC2, numbered from 1 to 100, constrained to the dimensions "zone-id" (values:
175
+ ["a", "b", "c"]) and the data center "dc" (values: [''us-west2'', ''us-east1'' ]).
176
+
177
+ '
178
+ email:
179
+ - kigster@gmail.com
180
+ executables:
181
+ - molder
182
+ extensions: []
183
+ extra_rdoc_files: []
184
+ files:
185
+ - ".gitignore"
186
+ - ".rspec"
187
+ - ".rspec_status"
188
+ - ".travis.yml"
189
+ - Gemfile
190
+ - LICENSE.txt
191
+ - README.md
192
+ - Rakefile
193
+ - bin/console
194
+ - bin/setup
195
+ - docs/molder-cli.png
196
+ - docs/molder.png
197
+ - exe/molder
198
+ - lib/molder.rb
199
+ - lib/molder/app.rb
200
+ - lib/molder/cli.rb
201
+ - lib/molder/command.rb
202
+ - lib/molder/configuration.rb
203
+ - lib/molder/errors.rb
204
+ - lib/molder/renderer.rb
205
+ - lib/molder/template.rb
206
+ - lib/molder/version.rb
207
+ - molder.gemspec
208
+ homepage: https://github.com/kigster/molder
209
+ licenses:
210
+ - MIT
211
+ metadata: {}
212
+ post_install_message:
213
+ rdoc_options: []
214
+ require_paths:
215
+ - lib
216
+ required_ruby_version: !ruby/object:Gem::Requirement
217
+ requirements:
218
+ - - ">="
219
+ - !ruby/object:Gem::Version
220
+ version: '0'
221
+ required_rubygems_version: !ruby/object:Gem::Requirement
222
+ requirements:
223
+ - - ">="
224
+ - !ruby/object:Gem::Version
225
+ version: '0'
226
+ requirements: []
227
+ rubyforge_project:
228
+ rubygems_version: 2.7.6
229
+ signing_key:
230
+ specification_version: 4
231
+ summary: 'Molder is a command line tool for generating and running (in parallel, across
232
+ a configurable number of processes) a set of related but similar commands that are
233
+ generated based on a merge of a template with a set of attributes. A key use-case
234
+ is auto-generation of the host provisioning commands for an arbitrary cloud environment.
235
+ The gem is not constrained to any particular cloud tool or even a command, and can
236
+ be used to generate a consistent set of commands based on several customizable dimensions.
237
+ For example, you could generate 600 provisioning commands for hosts in EC2, numbered
238
+ from 1 to 100, constrained to the dimensions "zone-id" (values: ["a", "b", "c"])
239
+ and the data center "dc" (values: [''us-west2'', ''us-east1'' ]).'
240
+ test_files: []