rake_cloudspin 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 718feeca897645f5430cf25838521b1d9f61e70f2dc137e123ad2da60bbc57f9
4
+ data.tar.gz: 5320da339ee454d484ad524634be3de9c16320937dd71dc7f12b267026e5645b
5
+ SHA512:
6
+ metadata.gz: eb5e94a709f07f6d174a6ccf2cc8a750d6728359cb02c3442c92fbe07372b97d43d8c21b4330665756480c4c5eed9273589c29b22507be14632a86b8b29e51a3
7
+ data.tar.gz: bdc760a200da9c72b09953040dcacce32086697fdd0eec6bcb1a9916e29733ac0da9a3d1c92dd662c632e6e81c709d9f692d76c133c2a1e572aae0c8b5a0a640
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.3
5
+ before_install: gem install bundler -v 1.16.2
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at tobyclemson@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,9 @@
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 rake_cloudspin.gemspec
6
+ gemspec
7
+
8
+ gem 'aws_ssh_key', :path => '../aws_ssh_key'
9
+ # gem 'aws_ssh_key', :git => 'https://github.com/cloudspinners/aws_ssh_key.git'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Kief Morris
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.
data/README.md ADDED
@@ -0,0 +1,327 @@
1
+ # RakeCloudspin
2
+
3
+ This library of Rake tasks is a prototype for an infrastructure project build framework. It is intended as a basis for exploring project structures, conventions, and functionality, but is not currently in a stable state. Feel free to copy and use it, but be prepared to extend and modify it in order to make it usable, and be aware that there isn't likely to be a clean path to upgrade your projects as this thing evolves.
4
+
5
+
6
+ ## What's the point of this?
7
+
8
+ Currently, most people and teams managing infrastructure with tools such as Terraform, CloudFormation, etc. define their own project structures, and write their own wrapper scripts to run that tool and associated tasks. Essentially, each project is a unique snowflake.
9
+
10
+ The goal for cloudspin is to evolve a common structure and build tooling for infrastructure projects, focused on the lifecycle of "[stacks](http://infrastructure-as-code.com/patterns/2018/03/28/defining-stacks.html)" - infrastructure elements provisioned on dynamic infrastructure such as IaaS clouds.
11
+
12
+ Our hypothesis is that, with a common project structure and tooling:
13
+
14
+ - Teams will spend less time building and maintaining snowflake build systems,
15
+ - New team members can more quickly get up to speed when joining an infrastructure project,
16
+ - People can create and share tools and scripts that work with the common structure, creating an ecosystem,
17
+ - People can create and share infrastructure code for running various software and services, creating a community library.
18
+
19
+
20
+ ## Philosophy
21
+
22
+ - [Convention over configuration](https://en.wikipedia.org/wiki/Convention_over_configuration).
23
+ -- The tool should discover elements of the project based on folder structure
24
+ -- A given configuration value should be set in a single place
25
+ -- Implies a highly "[opinionated](https://medium.com/@stueccles/the-rise-of-opinionated-software-ca1ba0140d5b)" approach
26
+ - Encourage good agile engineering practices for the infrastructure code
27
+ -- Writing and running tests should be a natural thing
28
+ -- Building and using [infrastructure pipelines](http://infrastructure-as-code.com/book/2017/08/02/environment-pipeline.html) should be a natural thing
29
+ - Support evolutionary architecture
30
+ -- Loose coupling of infrastructure elements
31
+ - Empower developers / users of infrastructure
32
+
33
+
34
+ # Structure of a project
35
+
36
+ Cloudspin is used to manage Terraform projects for AWS infrastructure. It uses Ruby rake. There are some example projects, [simple-stack](https://github.com/cloudspinners/spin-simple-stack) is a simple example.
37
+
38
+ Each cloudspin project represents a **Component**. A Component is a collection of stacks (as defined above) that together provide a useful service of some sort. Each instance of a service provisioned in the cloud is a *Deployment*. You may have Deployments for environments, e.g. a QA deployment, Staging deployment, Production deployment, etc. You might also have multiple production deployments, for example you might provision a deployment for each of your customers.
39
+
40
+ ## Project structure
41
+
42
+ Your component project should have the following basic structure:
43
+
44
+ ```
45
+ COMPONENT-ROOT
46
+ |-- deployment/
47
+ |-- delivery/
48
+ |-- component.yaml
49
+ |-- component-local.yaml
50
+ |-- Rakefile
51
+ └-- go*
52
+ ```
53
+
54
+ ## Deployment stacks
55
+
56
+ The `COMPONENT-ROOT/deployment/` folder has a subfolder for each stack that is provisioned for a deployment of the component.
57
+
58
+ ```
59
+ COMPONENT-ROOT
60
+ └-- deployment/
61
+ |-- networking/
62
+ |-- cluster/
63
+ └-- database/
64
+ ```
65
+
66
+ In this example, we have one stack for networking (VPC, subnets, etc.), one for a cluster (ECS cluster), and a third for a database (RDS instance).
67
+
68
+
69
+ ## Stack folders
70
+
71
+ Each stack has the following structure:
72
+
73
+ ```
74
+ deployment/
75
+ └── networking/
76
+ ├── stack.yaml
77
+ ├── infra/
78
+ │   ├── backend.tf
79
+ │   ├── bastion.tf
80
+ │   ├── dns.tf
81
+ │   ├── outputs.tf
82
+ │   ├── subnets.tf
83
+ │   ├── variables.tf
84
+ │   └── vpc.tf
85
+ └── tests/
86
+ └── inspec/
87
+ ├── controls/
88
+ │   ├── bastion.rb
89
+ │   ├── subnets.rb
90
+ │   └── vpc.rb
91
+ └── inspec.yml
92
+ ```
93
+
94
+ See below for details on the `stack.yaml` file.
95
+
96
+
97
+ ## Delivery stacks
98
+
99
+ The `COMPONENT-ROOT/delivery/` folder can have a number of subfolders, each representing a stack that provisions things needed for delivery. Each of these is typically provisioned only once per component. Examples include pipeline definitions, and artefact repository configurations.
100
+
101
+ ```bash
102
+ delivery/
103
+ └── aws-pipeline
104
+ ├── infra
105
+ │   ├── artefact_bucket.tf
106
+ │   ├── backend.tf
107
+ │   ├── outputs.tf
108
+ │   ├── packaging_codebuild_stage.tf
109
+ │   ├── pipeline.tf
110
+ │   ├── prodapply_codebuild_stage.tf
111
+ │   ├── testapply_codebuild_stage.tf
112
+ │   └── variables.tf
113
+ └── stack.yaml
114
+ ```
115
+
116
+
117
+ # Setting up a cloudspin project
118
+
119
+ These are the steps to set up a new cloudspin infrastructure project:
120
+
121
+ 1. Import the `rake_cloudspin` gem.
122
+ 2. Create component configuration
123
+ 3. Create one or more deployment and delivery stacks
124
+
125
+
126
+ ## Adding the rake_cloudspin gem to your project
127
+
128
+ ### Install the gem
129
+
130
+ Add this line to your application's Gemfile:
131
+
132
+ ```ruby
133
+ gem 'rake_cloudspin', :git => 'https://github.com/cloudspinners/rake_cloudspin.git'
134
+ ```
135
+
136
+ (TODO: Publish releases of this gem properly)
137
+
138
+ And then execute:
139
+
140
+ $ bundle install
141
+
142
+ Or install it yourself as:
143
+
144
+ $ gem install rake_cloudspin
145
+
146
+
147
+ ## Import the library into your Rakefile
148
+
149
+ Here is an example Rakefile:
150
+
151
+ ```ruby
152
+ require 'rake/clean'
153
+ require 'rake_cloudspin'
154
+
155
+ CLEAN.include('build')
156
+ CLEAN.include('work')
157
+ CLEAN.include('dist')
158
+ CLOBBER.include('vendor')
159
+
160
+ task :default => [ :plan ]
161
+
162
+ RakeCloudspin.define_tasks
163
+
164
+ ```
165
+
166
+ Many of our example projects use a `go` script as a wrapper to run rake. This makes sure prerequisites are installed, including the gems. See [example go script](https://raw.githubusercontent.com/cloudspinners/spin-simple-stack/master/go) from the spin-simple-stack project.
167
+
168
+
169
+ ## Component configuration (component.yaml)
170
+
171
+ There are two files used to configure your Component, both of which live at the root of the project, alongside the Rakefile.
172
+
173
+ * `component.yaml` has the default configuration options, and is intended to be checked into source control with the rest of your project.
174
+ * `component-local.yaml` allows you to override configuration options when you run cloudspin locally. This is intended to be excluded from source control, so each person who works on the project can have their own custom options.
175
+
176
+ Here is an example, again from the *simpleweb* project, of a `component.yaml` file.
177
+
178
+
179
+ ```yaml
180
+ ---
181
+ estate: cloudspin
182
+ component: simple
183
+ region: eu-west-1
184
+ ```
185
+
186
+ Some of these configuration variables are used for naming things, others are for configuring infrastructure.
187
+
188
+ - *estate* is an identifier that runs across all components, all deployments. It may be the name of the organisation, division, etc.
189
+ - *component* is the name of this component.
190
+ - *region* is the default region for deploying stacks.
191
+
192
+
193
+ Other variables are used to configure infrastructure, generally passed to Terraform code. The specific variables that are available in your component configuration will depend on your own project code. They will tend to be driven by the `stack.yaml` files for the deployment and delivery stacks in your project.
194
+
195
+
196
+ # Stack configuration (stack.yaml)
197
+
198
+ Each stack in `deployment/*` and `delivery/*` must have a `stack.yaml` file in its root. Otherwise, the cloudspin build won't recognize the stack.
199
+
200
+ Here's another example from simpleweb.
201
+
202
+ ```yaml
203
+ ---
204
+ vars:
205
+ region: "%{hiera('region')}"
206
+ component: "%{hiera('component')}"
207
+ deployment_identifier: "%{hiera('deployment_identifier')}"
208
+ estate: "%{hiera('estate')}"
209
+ service: "%{hiera('service')}"
210
+ base_dns_domain: "%{hiera('domain_name')}"
211
+
212
+ webserver_ssh_public_key_path: "../ssh_keys/webserver_ssh_key.pub"
213
+ bastion_ssh_public_key_path: "../ssh_keys/bastion_ssh_key.pub"
214
+ allowed_cidr: "%{hiera('my_ip')}/32"
215
+
216
+ ssh_keys:
217
+ - webserver_ssh_key
218
+ - bastion_ssh_key
219
+
220
+ state:
221
+ type: s3
222
+ scope: deployment
223
+ ```
224
+
225
+
226
+ ## Terraform variables
227
+
228
+ The `vars:` section of the `stack.yaml` file defines variables that are passed to terraform. See the [terraform configuration documentation](https://www.terraform.io/docs/configuration/variables.html) for how these are used. Cloudspin passes the variables defined in the `stack.yaml` file to the terraform command on the commandline.
229
+
230
+ The values in the configuration file can include values from component variables or other variables set by cloudspin. Cloudspin uses hiera to do this, so the syntax is:
231
+
232
+ ```
233
+ "%{hiera('VARIABLE_NAME')}"
234
+ ```
235
+
236
+ ## SSH keys
237
+
238
+ Some infrastructure needs ssh keys, for example keypairs used by EC2 instances. Cloudspin can manage these for you if your stack.yaml file has an `ssh_key` section as below:
239
+
240
+ ```yaml
241
+ ssh_keys:
242
+ - webserver_ssh_key
243
+ - bastion_ssh_key
244
+ ```
245
+
246
+ Each keyname listed in here represents an ssh public/private key pair required by the stack. When run the first time, cloudspin will generate an ssh key pair, and upload both keys to the AWS SSM Parameter Store as values encrypted with KMS. On later runs, Cloudspin will retrieve the existing keys and use those as appropriate.
247
+
248
+ A separate keypair is used for each deployment of the given stack. So keys are not shared between components, stacks, or environments. They don't need to be checked into version control. Ephemeral test instances of the stack will have keys automatically generated, and these will be destroyed afterwards along with the environment.
249
+
250
+ The keys are written or downloaded to the local filesystem, so they can be passed to Terraform. In the simpleweb example, two keypairs are generated, and the location of their public keys are passed as vars:
251
+
252
+ ```yaml
253
+ vars:
254
+ ...
255
+ webserver_ssh_public_key_path: "../ssh_keys/webserver_ssh_key.pub"
256
+ bastion_ssh_public_key_path: "../ssh_keys/bastion_ssh_key.pub"
257
+ ```
258
+
259
+ TODO: The location of the keyfiles should be set in variables by cloudspin, so you don't need to know the location.
260
+
261
+ If you don't want cloudspin to generate ssh keys for you, don't list the keys under the `ssh_keys` section of the `stack.yaml` file, and simply give the path to the keyfile you want to use in the `vars` section.
262
+
263
+
264
+ # Running cloudspin tasks
265
+
266
+ You run cloudspin either by running `rake`, or using a wrapper like the `go` script. Our examples assume the `go` script is used.
267
+
268
+ ## ${deployment_identifier}
269
+
270
+ You must set a unique `deployment_identifier` value for each unique instance of your component. You can set a default in your `component.yaml` file, although this is dangerous - it will be easy for someone to forget to set the value in some other way, and accidentally make changes to that instance. So if you do this, make sure the named environment is one you don't care about accidentally breaking.
271
+
272
+ Your **production** environment should of course NEVER be the default `deployment_identifier`.
273
+
274
+ It's common for each person to set their own `deployment_identifier` in `component-local.yaml`, so they can run cloudspin locally to create a personal "sandbox" instance to work on. It's useful to have a naming convention for this, so it's easy to manage instances, e.g. to destroy unneeded developer instances.
275
+
276
+ Most non-sandbox instances of the component will be provisioned and managed by the pipeline. In these cases, the pipeline configuration will set the `deployment_identifier` value.
277
+
278
+ The most common way to set the `deployment_identifier` is with an environment variable:
279
+
280
+ ```bash
281
+ DEPLOYMENT_IDENTIFIER=mytest ./go provision
282
+ ```
283
+
284
+ ## Cloudspin tasks
285
+
286
+ You can see the tasks by running `rake -T` or `./go -T`.
287
+
288
+ The main lifecycle tasks are:
289
+
290
+ - plan: Show what Terraform will do to the existing component instance
291
+ - provision: Create or update all deployment stacks in the instance
292
+ - test: Run all component tests against the instance
293
+ - destroy: Completely destroy all deployment stacks in the instance
294
+ - vars: Show the Terraform variables that will be set by cloudspin
295
+
296
+ Each of these commands can be run to affect all deployment stacks in the instance. They will not affect the delivery stacks.
297
+
298
+ It's also possible to run these commands for a specific deployment stack (or delivery stack):
299
+
300
+ ```bash
301
+ rake deployment:simpleweb:plan # Plan deployment-simpleweb using terraform
302
+ rake deployment:simpleweb:provision # Provision deployment-simpleweb using terraform
303
+ rake deployment:simpleweb:test # Run inspec tests
304
+ rake deployment:simpleweb:destroy # Destroy deployment-simpleweb using terraform
305
+ rake deployment:simpleweb:vars # Show terraform variables for stack 'simpleweb'
306
+ ```
307
+
308
+ Replace `simpleweb` with the name of a different stack as appropriate. For delivery stacks, the syntax is to `rake delivery:STACKNAME:task`.
309
+
310
+
311
+ # Runtime details
312
+
313
+ What's the `work` folder about?
314
+
315
+
316
+ # General info
317
+
318
+
319
+ ## Contributing
320
+
321
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rake_cloudspin.
322
+
323
+
324
+ ## Components
325
+
326
+ This is largely based on code from [Infrablocks](https://github.com/infrablocks), and uses some components, including [rake_terraform](https://github.com/infrablocks/rake_terraform).
327
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rake_cloudspin"
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__)
data/bin/setup ADDED
@@ -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
@@ -0,0 +1,4 @@
1
+ module RakeDependencies
2
+ class RequiredParameterUnset < ::StandardError
3
+ end
4
+ end
@@ -0,0 +1,23 @@
1
+ module Paths
2
+ class <<self
3
+ def project_root_directory
4
+ File.expand_path(Rake.application.original_dir)
5
+ end
6
+
7
+ def from_project_root_directory(*segments)
8
+ join_and_expand(project_root_directory, *segments)
9
+ end
10
+
11
+ def join_and_expand(*segments)
12
+ File.expand_path(join(*segments))
13
+ end
14
+
15
+ def join(*segments)
16
+ File.join(*segments.compact)
17
+ end
18
+
19
+ def self_directory
20
+ File.dirname(__FILE__)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+
2
+ module RakeCloudspin
3
+ module Statebucket
4
+ def self.build_bucket_name(estate:, deployment_identifier:, component:)
5
+ [
6
+ 'state',
7
+ estate,
8
+ deployment_identifier,
9
+ component
10
+ ].join('-')
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,98 @@
1
+ require 'rake/tasklib'
2
+ require_relative 'exceptions'
3
+
4
+ module RakeCloudspin
5
+ class TaskLib < ::Rake::TaskLib
6
+ class << self
7
+ def parameter_definitions
8
+ @parameter_definitions ||= {}
9
+ end
10
+
11
+ def parameter(name, options = {})
12
+ parameter_definition = ParameterDefinition.new(
13
+ name, options[:default], options[:required])
14
+ name = parameter_definition.name
15
+
16
+ attr_accessor(name)
17
+
18
+ parameter_definitions[name] = parameter_definition
19
+ end
20
+
21
+ def setup_defaults_for(instance)
22
+ parameter_definitions.values.each do |parameter_definition|
23
+ parameter_definition.apply_default_to(instance)
24
+ end
25
+ end
26
+
27
+ def check_required_for(instance)
28
+ dissatisfied = parameter_definitions.values.reject do |definition|
29
+ definition.satisfied_by?(instance)
30
+ end
31
+ unless dissatisfied.empty?
32
+ names = dissatisfied.map(&:name)
33
+ raise RequiredParameterUnset,
34
+ "Required parameter#{names.length > 1 ? 's' : ''} #{names.join(',')} unset."
35
+ end
36
+ end
37
+ end
38
+
39
+ def initialize(*args, &block)
40
+ setup_defaults
41
+ process_arguments(args)
42
+ process_block(block)
43
+ check_required
44
+ define
45
+ end
46
+
47
+ def setup_defaults
48
+ self.class.setup_defaults_for(self)
49
+ end
50
+
51
+ def process_arguments(_)
52
+ end
53
+
54
+ def process_block(block)
55
+ block.call(self) if block
56
+ end
57
+
58
+ def check_required
59
+ self.class.check_required_for(self)
60
+ end
61
+
62
+ def define
63
+ end
64
+
65
+ private
66
+
67
+ class ParameterDefinition
68
+ attr_reader :name
69
+
70
+ def initialize(name, default = nil, required = false)
71
+ @name = name.to_sym
72
+ @default = default
73
+ @required = required
74
+ end
75
+
76
+ def writer_method
77
+ "#{name}="
78
+ end
79
+
80
+ def reader_method
81
+ name
82
+ end
83
+
84
+ def apply_default_to(instance)
85
+ instance.__send__(writer_method, @default) if @default
86
+ end
87
+
88
+ def dissatisfied_by?(instance)
89
+ value = instance.__send__(reader_method)
90
+ @required && value.nil?
91
+ end
92
+
93
+ def satisfied_by?(instance)
94
+ !dissatisfied_by?(instance)
95
+ end
96
+ end
97
+ end
98
+ end