deploy_doc 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +188 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/deploy_doc.gemspec +25 -0
- data/doc/fig/architecture.dot +69 -0
- data/doc/fig/architecture.png +0 -0
- data/exe/deploy_doc +8 -0
- data/lib/deploy_doc/cli.rb +109 -0
- data/lib/deploy_doc/config.rb +7 -0
- data/lib/deploy_doc/docker.rb +36 -0
- data/lib/deploy_doc/error.rb +4 -0
- data/lib/deploy_doc/test_plan/annotator_parser.rb +120 -0
- data/lib/deploy_doc/test_plan.rb +99 -0
- data/lib/deploy_doc/version.rb +3 -0
- data/lib/deploy_doc.rb +13 -0
- data/runner/runner.rb +143 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ad636b7c3d04d48cb1348f0a4a51144855b15e6c
|
4
|
+
data.tar.gz: 3e2d63ba1d5507ce621a87693ee2316908dfba93
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 31ddd23250904ebfed56bce18dc65402115ddcc7a0be2cc310e3bf30b9cb733797c47ff17ba243d59f0c7070bcb25754d6539641ddae684edccba085d96ba218
|
7
|
+
data.tar.gz: 912167c2b23c5d4fc57c557ec7f229e125c26fdff5a7d5bf50a43f97e96b33ff6bf1436b24aa603141941b6849f8489c03f17e6555d7bd7464f8ef66c8d8beac
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, and in the interest of
|
4
|
+
fostering an open and welcoming community, we pledge to respect all people who
|
5
|
+
contribute through reporting issues, posting feature requests, updating
|
6
|
+
documentation, submitting pull requests or patches, and other activities.
|
7
|
+
|
8
|
+
We are committed to making participation in this project a harassment-free
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
11
|
+
body size, race, ethnicity, age, religion, or nationality.
|
12
|
+
|
13
|
+
Examples of unacceptable behavior by participants include:
|
14
|
+
|
15
|
+
* The use of sexualized language or imagery
|
16
|
+
* Personal attacks
|
17
|
+
* Trolling or insulting/derogatory comments
|
18
|
+
* Public or private harassment
|
19
|
+
* Publishing other's private information, such as physical or electronic
|
20
|
+
addresses, without explicit permission
|
21
|
+
* Other unethical or unprofessional conduct
|
22
|
+
|
23
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
24
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
25
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
26
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
27
|
+
threatening, offensive, or harmful.
|
28
|
+
|
29
|
+
By adopting this Code of Conduct, project maintainers commit themselves to
|
30
|
+
fairly and consistently applying these principles to every aspect of managing
|
31
|
+
this project. Project maintainers who do not follow or enforce the Code of
|
32
|
+
Conduct may be permanently removed from the project team.
|
33
|
+
|
34
|
+
This code of conduct applies both within project spaces and in public spaces
|
35
|
+
when an individual is representing the project or its community.
|
36
|
+
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
38
|
+
reported by contacting a project maintainer at maarten@moretea.nl. All
|
39
|
+
complaints will be reviewed and investigated and will result in a response that
|
40
|
+
is deemed necessary and appropriate to the circumstances. Maintainers are
|
41
|
+
obligated to maintain confidentiality with regard to the reporter of an
|
42
|
+
incident.
|
43
|
+
|
44
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
45
|
+
version 1.3.0, available at
|
46
|
+
[http://contributor-covenant.org/version/1/3/0/][version]
|
47
|
+
|
48
|
+
[homepage]: http://contributor-covenant.org
|
49
|
+
[version]: http://contributor-covenant.org/version/1/3/0/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Maarten Hoogendoorn
|
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,188 @@
|
|
1
|
+
# DeployDoc
|
2
|
+
|
3
|
+
## Synopsis
|
4
|
+
With DeployDoc, you can test the deployment of your microservice constellation to multiple
|
5
|
+
platforms, based on your end user documentation.
|
6
|
+
Usually, a project will have documented the required steps to deploy the microservices.
|
7
|
+
This documentation typically consists of a text with these steps embedded in it.
|
8
|
+
We realized that by adding some annotations, these steps could be parsed by a computer and
|
9
|
+
run in a CI pipeline. See the [syntax](#syntax) section below for more information about
|
10
|
+
how to write these Markdown files.
|
11
|
+
|
12
|
+
## Architecture
|
13
|
+
- As a user, you must provide a **Markdown file** that contains the steps to set up the
|
14
|
+
infrastructure your app requires, deploy the app to this infrastructure, and finally destroy the
|
15
|
+
created infrastructure.
|
16
|
+
- Optionally, you can provide an alternative **Docker Image**. It defaults to the `ruby` image.
|
17
|
+
- The **`deploy_doc`** tool parses, extracts and converts the Markdown file into a JSON file which
|
18
|
+
contains all the steps that need to be run. (See the [syntax](#syntax) section below on
|
19
|
+
how to write these Markdown files).
|
20
|
+
- The `deploy_doc` tool creates a docker container, and mounts (a) the JSON file containing the
|
21
|
+
steps that need to be run, (b) the runner script that interprets the JSON file and (c) the
|
22
|
+
data dir to `/deploy_doc/data`. The runner returns an appropriate status code to communicate
|
23
|
+
the result of the test run to the CI environment (see the [usage](#usage) section below).
|
24
|
+
- Based on the value of the test output, the CI environment can generate and publish the
|
25
|
+
human readable and verified version version of the documentation.
|
26
|
+
|
27
|
+
|
28
|
+
<img src="doc/fig/architecture.png">
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
Install the `deploy_doc` gem, see the [installation instructions](#installation) below.
|
32
|
+
|
33
|
+
```
|
34
|
+
USAGE: deploy_doc <markdown_file> [options]
|
35
|
+
-h, --help Print help
|
36
|
+
-r, --run-test Run tests
|
37
|
+
-j, --dump-json Just parse the Markdown file and dump the steps to JSON
|
38
|
+
-c, --shell-in-container Start a (temporary) docker container with the same environment as the tests will run in
|
39
|
+
-i, --docker-image=IMG_NAME Use this docker image instead of the default 'ruby:2.3'
|
40
|
+
-d, --data-dir=DIR Set the data directory, instead of the default '.'
|
41
|
+
|
42
|
+
EXIT STATUS:
|
43
|
+
If all test passed, and the infrastructure has been cleaned up, 0 is returned.
|
44
|
+
If something went wrong, but there the infrastructure has been destroyed correctly,
|
45
|
+
a 1 error status is returned, but if the destruction of the infrastructure did not complete,
|
46
|
+
the process returns an error status 2. These cases will probably require manual cleanup steps.
|
47
|
+
Finally, a error status 3 is returned if the program is aborted. We cannot give any guarantees in this case!
|
48
|
+
```
|
49
|
+
|
50
|
+
NOTE: If you use a custom docker image, be sure that it contains a Ruby implementation with version >= 2.1
|
51
|
+
|
52
|
+
## Phases in a DeployDoc
|
53
|
+
The test runner has four phases:
|
54
|
+
|
55
|
+
- `pre-install`, in which required additional software should be installed, such as Terraform.
|
56
|
+
If this phase fails, the tests just aborts, without attempting to destroy the infrastructure.
|
57
|
+
*NOTE*: This implies that it is very important to also install any the tool that is required
|
58
|
+
to tear down the infrastructure.
|
59
|
+
- `create-infrastructure`, in which the cloud resources are created. If this phase fails,
|
60
|
+
the tests are skipped, but the `destroy-infrastructure` phase is executed.
|
61
|
+
- `run-tests`, which should contain instructions that validate that the infrastructure is up and
|
62
|
+
running and that the microservices are deployed correctly.
|
63
|
+
- `destroy-infrastructure`, which must tear down the infrastructure.
|
64
|
+
|
65
|
+
### Secrets phase
|
66
|
+
There is an additional `require-env` phase, that can be used to pass in environmental variables to
|
67
|
+
the steps. If they are not present, testing the documentation will result in an error message that
|
68
|
+
these variables are not present.
|
69
|
+
|
70
|
+
|
71
|
+
## Syntax
|
72
|
+
The expected syntax is compatible with Jekyll.
|
73
|
+
We expect the Markdown file to start with a YAML section, enclosed in a `---` delimiter.
|
74
|
+
|
75
|
+
### Defining phases and steps
|
76
|
+
Multiple shell snippets (steps) can be added to a phase.
|
77
|
+
The phases are run in the order give in the previous section, after which each snippet is executed
|
78
|
+
in the order of declaration in the file.
|
79
|
+
|
80
|
+
The following syntaxes are supported to add snippets:
|
81
|
+
|
82
|
+
1. Single line annotation, not visible in Markdown output
|
83
|
+
|
84
|
+
<!-- deploy-doc PHASE [VALUE]* -->
|
85
|
+
|
86
|
+
2. Hidden multi-line annotation
|
87
|
+
|
88
|
+
<!-- deploy-doc-start PHASE [VALUE]*
|
89
|
+
CONTENT
|
90
|
+
-->
|
91
|
+
|
92
|
+
3. Visible multi-line annotation
|
93
|
+
|
94
|
+
<!-- deploy-doc-start PHASE [VALUE]* -->
|
95
|
+
CONTENT
|
96
|
+
<!-- deploy-doc-end -->
|
97
|
+
|
98
|
+
Note that due to current technical limitations of the test runner, each step is executed in a
|
99
|
+
separate shell. This implies that *setting* an environmental variable in one step, will not be
|
100
|
+
available in a next step. A work-around is to load/store this information in files.
|
101
|
+
The externally required environmental variables to store secrets are available in all steps.
|
102
|
+
|
103
|
+
|
104
|
+
### Example DeployDoc
|
105
|
+
|
106
|
+
```
|
107
|
+
---
|
108
|
+
deployDoc: true
|
109
|
+
---
|
110
|
+
# Hello World
|
111
|
+
<!-- deploy-doc require-env ENV_A ENV_B -->
|
112
|
+
|
113
|
+
<!-- deploy-doc-start pre-install -->
|
114
|
+
|
115
|
+
apt-get update
|
116
|
+
apt-get install -yq jq terraform
|
117
|
+
|
118
|
+
<!-- deploy-doc-end -->
|
119
|
+
|
120
|
+
<!-- deploy-doc-hidden pre-install
|
121
|
+
|
122
|
+
curl https://nixos.org/nix/install | sh
|
123
|
+
nix-env -i nixops
|
124
|
+
|
125
|
+
-->
|
126
|
+
|
127
|
+
<!-- deploy-doc-start create-infrastructure -->
|
128
|
+
|
129
|
+
# This both creates the infrastructure, and
|
130
|
+
# deploys a distributed application to it.
|
131
|
+
nixops create -d test_deployment ./deployment.nix
|
132
|
+
nixops deploy -d test_deployment
|
133
|
+
|
134
|
+
<!-- deploy-doc-end -->
|
135
|
+
|
136
|
+
<!-- deploy-doc-start run-tests -->
|
137
|
+
|
138
|
+
# Get the public endpoint
|
139
|
+
WEBAPP_HOST=`nixops export -d test_deployment | jq ' .[].resources["load-balancer"].publicIpv4' -r`
|
140
|
+
|
141
|
+
# Test that the webapp is up.
|
142
|
+
curl $WEBAPP_HOST
|
143
|
+
|
144
|
+
<!-- deploy-doc-end -->
|
145
|
+
|
146
|
+
<!-- deploy-doc-start destroy-infrastructure -->
|
147
|
+
|
148
|
+
nixops destroy -d test_deployment
|
149
|
+
|
150
|
+
<!-- deploy-doc-end -->
|
151
|
+
```
|
152
|
+
|
153
|
+
## Installation
|
154
|
+
|
155
|
+
Add this line to your application's Gemfile:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
gem 'deploy_doc'
|
159
|
+
```
|
160
|
+
|
161
|
+
And then execute:
|
162
|
+
|
163
|
+
$ bundle
|
164
|
+
|
165
|
+
Or install it yourself as:
|
166
|
+
|
167
|
+
$ gem install deploy_doc
|
168
|
+
|
169
|
+
## Development of this gem
|
170
|
+
|
171
|
+
After checking out the repo, run `bundle install` to install dependencies.
|
172
|
+
Then, run `rake test` to run the tests.
|
173
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
174
|
+
|
175
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
176
|
+
To release a new version, update the version number in `version.rb`, and then run
|
177
|
+
`bundle exec rake release`, which will create a git tag for the version, push git commits and tags,
|
178
|
+
and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
179
|
+
|
180
|
+
## Contributing
|
181
|
+
|
182
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/microservices-demo/deploy_doc.
|
183
|
+
This project is intended to be a safe, welcoming space for collaboration, and contributors are
|
184
|
+
expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
185
|
+
|
186
|
+
## License
|
187
|
+
|
188
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "deploy_doc"
|
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
|
data/deploy_doc.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'deploy_doc/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "deploy_doc"
|
8
|
+
spec.version = DeployDoc::VERSION
|
9
|
+
spec.authors = ["Maarten Hoogendoorn"]
|
10
|
+
spec.email = ["maarten@moretea.nl"]
|
11
|
+
|
12
|
+
spec.summary = %q{Test system for your deploment documentation for your application}
|
13
|
+
spec.description = %q{Using DeployDoc's, you can make your documentation executable}
|
14
|
+
spec.homepage = "https://github.com/microservices-demo/deploy_doc"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.12"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
25
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
digraph deploy_doc_architecture {
|
2
|
+
edge[minlen=1.8];
|
3
|
+
subgraph cluster_user_provided {
|
4
|
+
label= "User provided";
|
5
|
+
fontsize=10
|
6
|
+
node[shape="note"];
|
7
|
+
base_image[label="Docker Base Image"];
|
8
|
+
deploy_doc[label="documentation.md",fontname="mono"];
|
9
|
+
};
|
10
|
+
|
11
|
+
subgraph cluster_doc {
|
12
|
+
label="E.g. Yekyll";
|
13
|
+
labeljust="r";
|
14
|
+
fontsize=10;
|
15
|
+
|
16
|
+
generate_doc[label="Generate\ndocumentation"];
|
17
|
+
}
|
18
|
+
|
19
|
+
|
20
|
+
subgraph cluster_deploydoc {
|
21
|
+
label = "DeployDoc";
|
22
|
+
labeljust="l";
|
23
|
+
fontsize=10
|
24
|
+
script_generator[label="deploy_doc",fontname="mono"];
|
25
|
+
generated_script[label="Test steps\n(JSON file)", shape=note];
|
26
|
+
}
|
27
|
+
|
28
|
+
subgraph cluster_container{
|
29
|
+
label="Within container";
|
30
|
+
labeljust="l";
|
31
|
+
fontsize=10;
|
32
|
+
|
33
|
+
run_kubectl[label="Deploy to k8s\nwith kubectl"];
|
34
|
+
run_terraform[label="Terraform\ninfrastructure"];
|
35
|
+
run_test_script[shape="circle", label="Run tests"]
|
36
|
+
run_test_script -> run_tests[dir=both];
|
37
|
+
run_test_script -> run_terraform [dir=both];
|
38
|
+
run_test_script -> run_kubectl[dir=both];
|
39
|
+
|
40
|
+
run_tests[label="Run sanity\ntests"];
|
41
|
+
}
|
42
|
+
|
43
|
+
|
44
|
+
script_generator -> generated_script;
|
45
|
+
generated_script -> run_test_script;
|
46
|
+
|
47
|
+
deploy_doc -> script_generator;
|
48
|
+
base_image -> run_test_script;
|
49
|
+
deploy_doc -> generate_doc;
|
50
|
+
|
51
|
+
doc_output[shape="note",label="Documentation"];
|
52
|
+
subgraph cluster_outputs {
|
53
|
+
style=invis;
|
54
|
+
node[shape=note];
|
55
|
+
test_output[label="Test output"];
|
56
|
+
|
57
|
+
is_test_ok[shape="diamond", label="Test passed?"];
|
58
|
+
test_output->is_test_ok;
|
59
|
+
is_test_ok -> test_bad [label="No"];
|
60
|
+
is_test_ok -> publish_doc[label="yes"];
|
61
|
+
test_bad[shape="rect", label="Fail CI build"];
|
62
|
+
publish_doc[shape="default", label="Publish correct docs"];
|
63
|
+
}
|
64
|
+
|
65
|
+
doc_output -> publish_doc;
|
66
|
+
|
67
|
+
run_test_script -> test_output;
|
68
|
+
generate_doc -> doc_output;
|
69
|
+
}
|
Binary file
|
data/exe/deploy_doc
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
module DeployDoc
|
2
|
+
module CLI
|
3
|
+
def self.run!(arguments)
|
4
|
+
configuration = parse_arguments(arguments)
|
5
|
+
|
6
|
+
test_plan = TestPlan.from_file(configuration.markdown_file)
|
7
|
+
|
8
|
+
case configuration.action
|
9
|
+
when :run then self.do_run(test_plan, configuration)
|
10
|
+
when :dump_json then self.dump_json(test_plan, configuration)
|
11
|
+
when :just_docker_env then self.just_docker_env(test_plan, configuration)
|
12
|
+
else raise RuntimeError.new("No such action '#{configuration.action}'")
|
13
|
+
end
|
14
|
+
rescue DeployDoc::Error => e
|
15
|
+
puts "Runtime error: #{e.message}"
|
16
|
+
exit 1
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.parse_arguments(arguments)
|
20
|
+
configuration = Config.defaults
|
21
|
+
|
22
|
+
opt_parser = OptionParser.new do |opts|
|
23
|
+
opts.banner = "USAGE: deploy_doc <markdown_file> [options]"
|
24
|
+
|
25
|
+
opts.on("-h", "--help", "Print help") do
|
26
|
+
puts opts
|
27
|
+
puts
|
28
|
+
puts "EXIT STATUS:"
|
29
|
+
puts " If all test passed, and the infrastructure has been cleaned up, 0 is returned."
|
30
|
+
puts " If something went wrong, but there the infrastructure has been destroyed correctly,"
|
31
|
+
puts " a 1 error status is returned, but if the destruction of the infrastructure did not complete,"
|
32
|
+
puts " the process returns an error status 2. These cases will probably require manual cleanup steps."
|
33
|
+
puts " Finally, a error status 3 is returned if the program is aborted. We cannot give any guarrantees in this case!"
|
34
|
+
exit
|
35
|
+
end
|
36
|
+
|
37
|
+
opts.on("-r", "--run-test", "Run tests") do
|
38
|
+
configuration.action = :run
|
39
|
+
end
|
40
|
+
|
41
|
+
opts.on("-j", "--dump-json", "Just parse the Markdown file and dump the steps to JSON") do
|
42
|
+
configuration.action = :dump_json
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on("-c", "--shell-in-container", "Start a (temporary) docker container with the same environment as the tests will run in") do
|
46
|
+
configuration.action = :just_docker_env
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on("-iIMG_NAME", "--docker-image=IMG_NAME", "Use this docker image instead of the default '#{configuration.docker_image}'") do |image|
|
50
|
+
configuration.docker_image = image
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.on("-dDIR", "--data-dir=DIR", "Set the data directory, instead of the default '#{configuration.data_dir}'")do |data_dir|
|
54
|
+
configuration.data_dir = data_dir
|
55
|
+
end
|
56
|
+
end
|
57
|
+
opt_parser.parse!(arguments)
|
58
|
+
configuration.markdown_file = arguments.shift
|
59
|
+
|
60
|
+
if configuration.markdown_file.nil?
|
61
|
+
raise DeployDoc::Error.new("No markdown file provided! Run `deploy_doc -h' to learn more about how to use this tool.")
|
62
|
+
end
|
63
|
+
|
64
|
+
if configuration.action.nil?
|
65
|
+
raise DeployDoc::Error.new("No action given. Either --run-tests, --dump-json or --shell-in-container must be specified")
|
66
|
+
end
|
67
|
+
|
68
|
+
configuration
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.do_run(test_plan, configuration)
|
72
|
+
status = nil
|
73
|
+
|
74
|
+
test_instructions_file = Tempfile.new("deploydoc", "/tmp")
|
75
|
+
test_instructions_file.write test_plan.to_json
|
76
|
+
test_instructions_file.close
|
77
|
+
runner_path = File.expand_path("../../../runner/runner.rb", __FILE__)
|
78
|
+
extra_opts = [
|
79
|
+
"-v#{test_instructions_file.path}:/deploy_doc/steps.json",
|
80
|
+
"-v#{runner_path}:/deploy_doc/runner",
|
81
|
+
]
|
82
|
+
system Docker.cmd(configuration, check_and_find_envs(test_plan), "ruby /deploy_doc/runner /deploy_doc/steps.json", extra_opts)
|
83
|
+
status = $?
|
84
|
+
test_instructions_file.close
|
85
|
+
test_instructions_file.unlink
|
86
|
+
exit status.exitstatus
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.dump_json(test_plan, configuration)
|
90
|
+
puts test_plan.to_json
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.just_docker_env(test_plan, configuration)
|
94
|
+
Kernel.exec Docker.cmd(configuration, check_and_find_envs(test_plan), "sh", ["-ti"])
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.check_and_find_envs(test_plan)
|
98
|
+
test_plan.required_env_vars.map do |name|
|
99
|
+
val = ENV[name]
|
100
|
+
if val.nil?
|
101
|
+
$stderr.puts "Required environmental variable #{name} is not set"
|
102
|
+
exit 1
|
103
|
+
else
|
104
|
+
"-e#{name}=#{val}"
|
105
|
+
end
|
106
|
+
end.join(" ")
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module DeployDoc
|
2
|
+
module Docker
|
3
|
+
def self.cmd(configuration, envs, cmd, extra_opts = [])
|
4
|
+
data_dir = if configuration.data_dir == "."
|
5
|
+
Dir.pwd
|
6
|
+
else
|
7
|
+
configuration.data_dir
|
8
|
+
end
|
9
|
+
|
10
|
+
docker_cmd = [
|
11
|
+
"docker",
|
12
|
+
"run",
|
13
|
+
"-it",
|
14
|
+
"--rm",
|
15
|
+
envs,
|
16
|
+
"-v#{data_dir}:/deploy_doc/data/",
|
17
|
+
"-w/deploy_doc/data/"
|
18
|
+
]
|
19
|
+
|
20
|
+
# Expose the host host docker daemon in the child docker container.
|
21
|
+
docker_socket_protocol, docker_socket_address = configuration.docker_socket.split("://",2)
|
22
|
+
case docker_socket_protocol
|
23
|
+
when "unix"
|
24
|
+
docker_cmd.push "-v#{docker_socket_address}:/var/run/docker.sock"
|
25
|
+
when "tcp"
|
26
|
+
docker_cmd.push "-e DOCKER_HOST='#{configuration.docker_socket}'"
|
27
|
+
else
|
28
|
+
raise DeployDocError.new("Unkown docker socket protocol '#{docker_socket_protocol}'")
|
29
|
+
end
|
30
|
+
|
31
|
+
docker_cmd += extra_opts
|
32
|
+
docker_cmd += [configuration.docker_image, cmd]
|
33
|
+
docker_cmd.join(" ")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module DeployDoc
|
2
|
+
class TestPlan
|
3
|
+
class AnnotationParser
|
4
|
+
Annotation = Struct.new(:source_name, :line_span, :kind, :params, :content)
|
5
|
+
|
6
|
+
def self.parse(markdown, path="<unknown>")
|
7
|
+
AnnotationParser.new(markdown, path).parse!
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(markdown, source_name="<unknown>")
|
11
|
+
@source_name = source_name
|
12
|
+
@markdown_lines = markdown.split("\n")
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse!
|
16
|
+
@annotations = []
|
17
|
+
@parse_idx = 0
|
18
|
+
|
19
|
+
while !eof_reached?
|
20
|
+
parse_block || parse_inline || parse_single_line || parse_text
|
21
|
+
end
|
22
|
+
|
23
|
+
@annotations
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def parse_text
|
29
|
+
inc_line # ignore text, line by line
|
30
|
+
end
|
31
|
+
|
32
|
+
BLOCK_START = /^<!-- deploy-doc-start (?<kind>[-\w]+)( )?(?<params>.*) -->/
|
33
|
+
BLOCK_END = /^<!-- deploy-doc-end -->/
|
34
|
+
|
35
|
+
INLINE_START = /^<!-- deploy-doc-hidden (?<kind>[-\w]+)( )?(?<params>.*)/
|
36
|
+
INLINE_END = /^-->/
|
37
|
+
|
38
|
+
SINGLE_LINE = /^<!-- deploy-doc (?<kind>[-\w]+)( )?(?<params>.*) -->/
|
39
|
+
|
40
|
+
def parse_block
|
41
|
+
if (match = BLOCK_START.match(current_line)).nil?
|
42
|
+
false
|
43
|
+
else
|
44
|
+
inc_line
|
45
|
+
start_line = current_line_nr
|
46
|
+
kind = match["kind"]
|
47
|
+
params = match["params"].split(/\s+/)
|
48
|
+
content = ""
|
49
|
+
loop do
|
50
|
+
if eof_reached?
|
51
|
+
raise("Unexpected end of file; --> not closed? started on #{@source_name}:#{start_line}")
|
52
|
+
elsif !(BLOCK_END.match(current_line).nil?)
|
53
|
+
end_line = current_line_nr() -1
|
54
|
+
@annotations << Annotation.new(@source_name, [start_line, end_line], kind, params, content)
|
55
|
+
return true
|
56
|
+
else
|
57
|
+
content += current_line + "\n"
|
58
|
+
end
|
59
|
+
inc_line
|
60
|
+
end
|
61
|
+
true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse_inline
|
66
|
+
if (match = INLINE_START.match(current_line)).nil?
|
67
|
+
false
|
68
|
+
else
|
69
|
+
inc_line
|
70
|
+
start_line = current_line_nr
|
71
|
+
kind = match["kind"]
|
72
|
+
params = match["params"].split(/\s+/)
|
73
|
+
content = ""
|
74
|
+
loop do
|
75
|
+
if eof_reached?
|
76
|
+
raise("Unexpected end of file; --> not closed? started on #{@source_name}:#{start_line}")
|
77
|
+
elsif !(INLINE_END.match(current_line).nil?)
|
78
|
+
end_line = current_line_nr() -1
|
79
|
+
@annotations << Annotation.new(@source_name, [start_line, end_line], kind, params, content)
|
80
|
+
return true
|
81
|
+
else
|
82
|
+
content += current_line + "\n"
|
83
|
+
end
|
84
|
+
inc_line
|
85
|
+
end
|
86
|
+
true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def parse_single_line
|
91
|
+
if (match = SINGLE_LINE.match(current_line)).nil?
|
92
|
+
false
|
93
|
+
else
|
94
|
+
kind = match["kind"]
|
95
|
+
params = match["params"].split(/\s+/)
|
96
|
+
@annotations << Annotation.new(@source_name, [current_line_nr, current_line_nr], kind, params, nil)
|
97
|
+
inc_line
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
##### Helper functions ####
|
102
|
+
|
103
|
+
def eof_reached?
|
104
|
+
@parse_idx >= @markdown_lines.length
|
105
|
+
end
|
106
|
+
|
107
|
+
def current_line
|
108
|
+
@markdown_lines[@parse_idx]
|
109
|
+
end
|
110
|
+
|
111
|
+
def current_line_nr
|
112
|
+
@parse_idx + 1
|
113
|
+
end
|
114
|
+
|
115
|
+
def inc_line
|
116
|
+
@parse_idx+=1
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module DeployDoc
|
2
|
+
class TestPlan
|
3
|
+
require_relative "test_plan/annotator_parser"
|
4
|
+
|
5
|
+
PHASES = ["pre-install", "create-infrastructure", "run-tests", "destroy-infrastructure"]
|
6
|
+
|
7
|
+
class Step < Struct.new(:source_name, :line_span, :shell)
|
8
|
+
def full_name
|
9
|
+
"#{source_name}:#{line_span.inspect}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.from_file(file_name)
|
14
|
+
content = File.read(file_name)
|
15
|
+
self.from_str(content, file_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.from_str(content, file_name="<unknown file>")
|
19
|
+
metadata = self.parse_metadata(content, file_name)
|
20
|
+
|
21
|
+
if metadata["deployDoc"] != true
|
22
|
+
raise DeployDoc::Error.new("Markdown file #{file_name} does not have a 'deployDoc:true' metadatum")
|
23
|
+
end
|
24
|
+
|
25
|
+
annotations = AnnotationParser.parse(content, file_name)
|
26
|
+
required_env_vars = (annotations.select { |a| a.kind == "require-env" }).map { |a| a.params }.flatten
|
27
|
+
phases = self.phases_from_annotations(annotations)
|
28
|
+
TestPlan.new(metadata, required_env_vars, phases)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.parse_metadata(content, file_name)
|
32
|
+
metadata = content.split("---",3)[1]
|
33
|
+
YAML.load(metadata)
|
34
|
+
rescue
|
35
|
+
raise Error.new("Could not parse metadata in #{file_name}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.phases_from_annotations(annotations)
|
39
|
+
phases = {}
|
40
|
+
|
41
|
+
PHASES.each do |phase|
|
42
|
+
phases[phase] = (annotations.select { |a| a.kind == phase }).map { |a| Step.new(a.source_name, a.line_span, a.content) }
|
43
|
+
end
|
44
|
+
|
45
|
+
phases
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :metadata
|
49
|
+
attr_reader :required_env_vars
|
50
|
+
attr_reader :steps_in_phases
|
51
|
+
|
52
|
+
def initialize(metadata, required_env_vars, steps_in_phases)
|
53
|
+
@metadata = metadata
|
54
|
+
@required_env_vars = required_env_vars
|
55
|
+
@steps_in_phases = steps_in_phases
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
parts = []
|
60
|
+
parts << "Deployment test plan:"
|
61
|
+
parts << ""
|
62
|
+
parts << "Required environment parameters"
|
63
|
+
|
64
|
+
@required_env_vars.each do |e|
|
65
|
+
parts << " - #{e}"
|
66
|
+
end
|
67
|
+
|
68
|
+
PHASES.each do |phase|
|
69
|
+
parts << "Steps in phase #{phase}:"
|
70
|
+
@steps_in_phases[phase].each do |step|
|
71
|
+
parts << "- #{step.source_name}:#{step.line_span.inspect}"
|
72
|
+
parts << step.shell
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
parts.join("\n")
|
77
|
+
end
|
78
|
+
|
79
|
+
def missing_env_vars
|
80
|
+
@required_env_vars.select { |e| ENV[e].nil? }
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_json
|
84
|
+
json = {}
|
85
|
+
|
86
|
+
PHASES.each do |phase_name|
|
87
|
+
json[phase_name] = []
|
88
|
+
@steps_in_phases[phase_name].each do |step|
|
89
|
+
json[phase_name].push({
|
90
|
+
line_span: step.line_span.inspect,
|
91
|
+
shell: step.shell.strip
|
92
|
+
})
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
JSON.pretty_generate(json)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/deploy_doc.rb
ADDED
data/runner/runner.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'open3'
|
3
|
+
$stdout.sync = true
|
4
|
+
$stderr.sync = true
|
5
|
+
|
6
|
+
class StepFailed < RuntimeError
|
7
|
+
def initialize(phase, location)
|
8
|
+
super("on lines #{location} in phase #{phase}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
all_steps = JSON.load(File.read(ARGV.first))
|
13
|
+
|
14
|
+
def report_error(e)
|
15
|
+
puts
|
16
|
+
puts red { "#{e.class}: #{e}" }
|
17
|
+
puts
|
18
|
+
$all_passed = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def run_phase(all_steps, name)
|
22
|
+
puts(bold { underline { name}})
|
23
|
+
steps = all_steps[name]
|
24
|
+
if steps.empty?
|
25
|
+
puts yellow { ' - No steps in this phase'}
|
26
|
+
else
|
27
|
+
steps.each do |step|
|
28
|
+
puts blue { " + Running step #{step["line_span"]}" }
|
29
|
+
|
30
|
+
shellcode = "set +e\n" + step["shell"]
|
31
|
+
begin
|
32
|
+
Open3.popen2e(shellcode) do |stdin, stdout_stderr, wait_exit_code_of_child|
|
33
|
+
$current_io_thread = wait_exit_code_of_child
|
34
|
+
buffer = ""
|
35
|
+
|
36
|
+
loop do
|
37
|
+
begin
|
38
|
+
read = stdout_stderr.read_nonblock(1024)
|
39
|
+
rescue IO::EAGAINWaitReadable
|
40
|
+
IO.select([stdout_stderr])
|
41
|
+
retry
|
42
|
+
rescue EOFError
|
43
|
+
if buffer.length > 0
|
44
|
+
lines = buffer.split("\n")
|
45
|
+
lines.each do |line|
|
46
|
+
print_line(line)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
break
|
50
|
+
end
|
51
|
+
buffer += read
|
52
|
+
line, new_buffer = buffer.split("\n", 2)
|
53
|
+
if new_buffer != nil
|
54
|
+
print_line(line)
|
55
|
+
buffer = new_buffer
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
if wait_exit_code_of_child.value != 0
|
60
|
+
raise StepFailed.new(name, step["line_span"])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
ensure
|
64
|
+
$current_io_thread = nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
ensure
|
69
|
+
puts
|
70
|
+
end
|
71
|
+
|
72
|
+
def print_line(line)
|
73
|
+
puts((blue { " | "}) + line)
|
74
|
+
end
|
75
|
+
|
76
|
+
def bold
|
77
|
+
"\033[1m" + yield + "\033[0m"
|
78
|
+
end
|
79
|
+
|
80
|
+
def underline
|
81
|
+
"\033[4m" + yield + "\033[0m"
|
82
|
+
end
|
83
|
+
|
84
|
+
def red
|
85
|
+
"\033[31m" + yield + "\033[0m"
|
86
|
+
end
|
87
|
+
|
88
|
+
def yellow
|
89
|
+
"\033[33m" + yield + "\033[0m"
|
90
|
+
end
|
91
|
+
|
92
|
+
def blue
|
93
|
+
"\033[34m" + yield + "\033[0m"
|
94
|
+
end
|
95
|
+
|
96
|
+
begin
|
97
|
+
begin
|
98
|
+
run_phase(all_steps, "pre-install")
|
99
|
+
rescue StepFailed => e
|
100
|
+
report_error(e)
|
101
|
+
exit 1
|
102
|
+
end
|
103
|
+
|
104
|
+
all_passed = true
|
105
|
+
begin
|
106
|
+
begin
|
107
|
+
run_phase(all_steps, "create-infrastructure")
|
108
|
+
run_phase(all_steps, "run-tests")
|
109
|
+
rescue Exception => e
|
110
|
+
report_error(e)
|
111
|
+
puts "Skipping next steps, jump to cleaning up\n"
|
112
|
+
all_passed = false
|
113
|
+
end
|
114
|
+
|
115
|
+
begin
|
116
|
+
run_phase(all_steps, "destroy-infrastructure")
|
117
|
+
rescue Exception => e
|
118
|
+
report_error(e)
|
119
|
+
puts " " + bold { red { "#" * 46 } }
|
120
|
+
puts " " + bold { red { "# " } } + bold { red { underline { "Failed to clean up the infrastructure!" } } } + bold { red { " #"}}
|
121
|
+
puts " " + bold { red { "#" * 46 } }
|
122
|
+
exit 2
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
if all_passed
|
127
|
+
exit 0
|
128
|
+
else
|
129
|
+
exit 1
|
130
|
+
end
|
131
|
+
rescue Interrupt
|
132
|
+
puts bold { red { "Interrupted!" } }
|
133
|
+
if $current_io_thread
|
134
|
+
puts "Killing child process #{$current_io_thread.pid}"
|
135
|
+
begin
|
136
|
+
Process.kill("KILL",$current_io_thread.pid)
|
137
|
+
rescue
|
138
|
+
# Don't care if the PID doesn't exist anymore.
|
139
|
+
# All bets are off in any case.
|
140
|
+
end
|
141
|
+
end
|
142
|
+
exit 3
|
143
|
+
end
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: deploy_doc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Maarten Hoogendoorn
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-12-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.12'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.12'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
description: Using DeployDoc's, you can make your documentation executable
|
56
|
+
email:
|
57
|
+
- maarten@moretea.nl
|
58
|
+
executables:
|
59
|
+
- deploy_doc
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- ".gitignore"
|
64
|
+
- ".travis.yml"
|
65
|
+
- CODE_OF_CONDUCT.md
|
66
|
+
- Gemfile
|
67
|
+
- LICENSE.txt
|
68
|
+
- README.md
|
69
|
+
- Rakefile
|
70
|
+
- bin/console
|
71
|
+
- deploy_doc.gemspec
|
72
|
+
- doc/fig/architecture.dot
|
73
|
+
- doc/fig/architecture.png
|
74
|
+
- exe/deploy_doc
|
75
|
+
- lib/deploy_doc.rb
|
76
|
+
- lib/deploy_doc/cli.rb
|
77
|
+
- lib/deploy_doc/config.rb
|
78
|
+
- lib/deploy_doc/docker.rb
|
79
|
+
- lib/deploy_doc/error.rb
|
80
|
+
- lib/deploy_doc/test_plan.rb
|
81
|
+
- lib/deploy_doc/test_plan/annotator_parser.rb
|
82
|
+
- lib/deploy_doc/version.rb
|
83
|
+
- runner/runner.rb
|
84
|
+
homepage: https://github.com/microservices-demo/deploy_doc
|
85
|
+
licenses:
|
86
|
+
- MIT
|
87
|
+
metadata: {}
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project:
|
104
|
+
rubygems_version: 2.5.1
|
105
|
+
signing_key:
|
106
|
+
specification_version: 4
|
107
|
+
summary: Test system for your deploment documentation for your application
|
108
|
+
test_files: []
|