deploy_doc 0.1.0

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
+ SHA1:
3
+ metadata.gz: ad636b7c3d04d48cb1348f0a4a51144855b15e6c
4
+ data.tar.gz: 3e2d63ba1d5507ce621a87693ee2316908dfba93
5
+ SHA512:
6
+ metadata.gz: 31ddd23250904ebfed56bce18dc65402115ddcc7a0be2cc310e3bf30b9cb733797c47ff17ba243d59f0c7070bcb25754d6539641ddae684edccba085d96ba218
7
+ data.tar.gz: 912167c2b23c5d4fc57c557ec7f229e125c26fdff5a7d5bf50a43f97e96b33ff6bf1436b24aa603141941b6849f8489c03f17e6555d7bd7464f8ef66c8d8beac
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.12.5
@@ -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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in deploy_doc.gemspec
4
+ gemspec
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
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
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
@@ -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,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "deploy_doc"
5
+ require "deploy_doc/cli"
6
+
7
+ require "optparse"
8
+ DeployDoc::CLI.run!(ARGV)
@@ -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,7 @@
1
+ module DeployDoc
2
+ class Config < Struct.new(:action, :markdown_file, :docker_socket, :docker_image, :data_dir)
3
+ def self.defaults
4
+ Config.new(nil, nil, "unix:///var/run/docker.sock", "ruby:2.3", ".")
5
+ end
6
+ end
7
+ 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,4 @@
1
+ module DeployDoc
2
+ class Error < RuntimeError
3
+ end
4
+ 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
@@ -0,0 +1,3 @@
1
+ module DeployDoc
2
+ VERSION = "0.1.0"
3
+ end
data/lib/deploy_doc.rb ADDED
@@ -0,0 +1,13 @@
1
+ require "yaml"
2
+ require "json"
3
+ require "tempfile"
4
+
5
+ require "deploy_doc/version"
6
+ require "deploy_doc/error"
7
+ require "deploy_doc/config"
8
+ require "deploy_doc/docker"
9
+ require "deploy_doc/test_plan"
10
+
11
+
12
+ module DeployDoc
13
+ end
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: []