environmate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # Environmate
2
+
3
+ Environmate is a ruby sinatra web application to deploy
4
+ Puppet code to a Puppet master.
5
+
6
+ This web application will completely take control of your
7
+ environments directory and purge every other environment already in there
8
+ or which is deployed by other means.
9
+
10
+ Currently it supports webhook events from Gitlab, but this may be easily
11
+ extended in the future to more GIT web frontends.
12
+
13
+ ## Installation
14
+
15
+ Environmate comes as ruby gem. You can install it on
16
+ your puppet master by running:
17
+
18
+ $ /opt/puppetlabs/puppet/bin/gem install environmate
19
+
20
+ ## Usage
21
+
22
+ Environmate comes with puma included and no additional setup
23
+ is required. Simply run environmate from the console:
24
+
25
+ $ /opt/puppetlabs/puppet/bin/environmate --help
26
+
27
+ ## Environment Management
28
+
29
+ Environmate provides two different ways to deploy code which represent two
30
+ different use cases and are explained in the following section.
31
+
32
+ ### Dynamic Environments
33
+
34
+ Dynamic environments are puppet environments which are derived from a
35
+ GIT branch which starts with a defined prefix. The default prefix is
36
+ 'env/'. All other branches will simply be ignored.
37
+
38
+ To deploy such branches as environments a web hook has to registered in Gitlab
39
+ with the API endpoint '/gitlab_push' on push events. The web application will
40
+ examine each push event and deploy the specified revision.
41
+
42
+ The typical use case for this type of deployment is for development purposes:
43
+
44
+ Start a new branch on a local development machine and push it to your GIT
45
+ server:
46
+
47
+ $ git checkout -b env/mynewfeature
48
+ $ vim modules/foo/manifests/init.pp
49
+ $ git commit -a -m "my awesome new feature"
50
+ $ git push -u origin env/mynewfeature
51
+
52
+ Environmate will now automatically create a new environment with the name
53
+ 'mynewfeature' which you can test on any node with:
54
+
55
+ $ puppet agent --test --environment mynewfeature
56
+
57
+ If you delete the branch the environment will be removed again.
58
+
59
+ ### Static Environments
60
+
61
+ Static environments are predefined environments which can be deployed with
62
+ a different API endpoint '/deploy'. The main purpose is to deploy arbitrary
63
+ revisions from a deployment pipeline outside the version control system.
64
+ The push request to this endpoint has to contain the following data:
65
+
66
+ {
67
+ "environment": "name_of_the_puppet_environment",
68
+ "token": "secret_token",
69
+ "revision": "git_commit_revision_(sha1)"
70
+ }
71
+
72
+ Those environments and their tokens have to be preconfigured in order to work.
73
+ Dynamic environments can not have the same name as a static environment and will
74
+ fail to deploy if created.
75
+
76
+ ## Features
77
+
78
+ ### Atomic Deployments
79
+
80
+ Environmate deploys the code for the revisions in directories with the name
81
+ of the SHA1 reference. Only after the environment is completely deployed it will add
82
+ a link with the puppet environment name or switch an existing name in an atomic operation
83
+ which guarantees that there is never an invalid environment.
84
+
85
+ If the deployment of a revision fails for some reason, the link will remain on the old
86
+ revision and the puppet master stays unaffected.
87
+
88
+ ### Notifications
89
+
90
+ Since the Gitlab hook provides information about the users email it is possible to
91
+ configure a mapping of email to jabber accounts to inform the user in a timely
92
+ fashion about the progress of the deployment.
93
+
94
+ However this will only work for ruby > 1.9.3 since xmpp4r uses classes not
95
+ available in previous ruby versions.
96
+
97
+ ## Configuration
98
+
99
+ Environmate will attempt to load the configuration in the following order.
100
+ It will use the first yaml file it finds:
101
+
102
+ - '/etc/environmate.yml'
103
+ - '~/.environmate.yml'
104
+
105
+ Additionally you can provide a configuration file when starting the environmate
106
+ service:
107
+
108
+ $ /opt/puppetlabs/puppet/bin/environmate --config /path/to/my/conf.yml
109
+
110
+ Here is a complete example config:
111
+
112
+ production:
113
+ environment_path: '/etc/puppetlabs/code/environments'
114
+
115
+ lockfile_path: '/var/run/lock/puppet_deploy_lock'
116
+ lockfile_options:
117
+ timeout: 300
118
+
119
+ logfile: '/var/log/environmate.log'
120
+ loglevel: 'INFO'
121
+
122
+ master_repository: 'http://gitlab.exmple.com/puppet/control'
123
+ master_branch: 'origin/master'
124
+ master_path: '/etc/puppetlabs/code/environmate_master'
125
+
126
+ dynamic_environments_prefix: 'env/'
127
+
128
+ static_environments:
129
+ nonprod:
130
+ token: 'abc123'
131
+ prod:
132
+ token: '123abc'
133
+
134
+ xmpp:
135
+ username: 'foo@jabber.example.com'
136
+ password: 'foofoofoo'
137
+ users:
138
+ - bob@example.com: 'bob@jabber.example.com'
139
+ - alice@example.com: 'alice@jabber.example.com'
140
+
141
+ server_settings:
142
+ :Port: 4443
143
+ :Bind: '0.0.0.0'
144
+ :SSLEnable: true
145
+ :SSLCertificate: '/path/to/your/cert.pem'
146
+ :SSLPrivateKey: '/path/to/your/key.pem'
147
+
148
+ ## Internals
149
+
150
+ ### Locking
151
+
152
+ To prevent deployment processes from interfering with each other, only one deployment
153
+ can happen at a certain time. Environmate will halt additional deployment requests
154
+ until the deployment in progress is finished in which case the next deployment waiting will
155
+ be started automatically.
156
+
157
+ ### Master Repository
158
+
159
+ The master repository is only there to work as a starting point for new branches, so we
160
+ don't have to clone the whole environment from scratch each time. Environmate
161
+ will try to find the shortest way to deploy an environment from the already deployed
162
+ revisions. If no good starting point can be evaluated it will default to the master.
163
+
164
+ ## Debugging
165
+
166
+ To easily debug environmate you can start it manually with the foreground flag to get all
167
+ the log output to the console. This way you don't have to adjust the config:
168
+
169
+ $ /opt/puppetlabs/puppet/bin/environmate --foreground --verbosity DEBUG
170
+
171
+ If you want to see stacktraces add the trace flag:
172
+
173
+ $ /opt/puppetlabs/puppet/bin/environmate --foreground --verbosity DEBUG --trace
174
+
175
+ ## Contributing
176
+
177
+ Bug reports and pull requests are welcome on GitHub at https://github.com/puzzle/environmate.
178
+ This project is intended to be a safe, welcoming space for collaboration, and contributors are
179
+ expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
180
+
181
+ ## License
182
+
183
+ The gem is available as open source under the terms of the GNU General Public License 3.0
184
+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
185
+
186
+ ## Code of Conduct
187
+
188
+ Everyone interacting in the Environmate project’s codebases, issue trackers, chat rooms
189
+ and mailing lists is expected to follow the
190
+ [code of conduct](https://github.com/puzzle/environmate/blob/master/CODE_OF_CONDUCT.md).
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 'environmate'
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,39 @@
1
+
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'environmate/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'environmate'
8
+ spec.version = Environmate::VERSION
9
+ spec.authors = ['Andreas Zuber']
10
+ spec.email = ['zuber@puzzle.ch']
11
+
12
+ spec.summary = 'Manage Puppet environments with a GIT workflow'
13
+ spec.description = 'Environmate is a Webhook receiver for various GIT '\
14
+ 'web frontends for deloying Puppet environments to'\
15
+ 'the master/server'
16
+ spec.homepage = 'https://github.com/puzzle/environmate'
17
+ spec.license = 'GPL-3.0'
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features)/})
21
+ end
22
+ spec.bindir = 'exe'
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ['lib']
25
+
26
+ spec.add_dependency 'sinatra', '~> 2'
27
+ spec.add_dependency 'lockfile', '~> 2'
28
+
29
+ # TODO: make this dependencies optional
30
+ spec.add_dependency 'xmpp4r', '~> 0'
31
+ spec.add_dependency 'librarian-puppet', '~> 3'
32
+
33
+ spec.add_development_dependency 'bundler', '~> 1.16'
34
+ spec.add_development_dependency 'rake', '~> 10.0'
35
+ spec.add_development_dependency 'rspec', '~> 3.0'
36
+ spec.add_development_dependency 'rack-test', '~> 0.8'
37
+ spec.add_development_dependency 'simplecov', '~> 0.15'
38
+ spec.add_development_dependency 'rubocop', '~> 0.51'
39
+ end
data/exe/environmate ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.push File.join(File.dirname(__FILE__), '..', 'lib')
4
+
5
+ require 'optparse'
6
+ require 'environmate/app'
7
+
8
+ options = {
9
+ verbosity: nil,
10
+ foreground: false,
11
+ trace: false,
12
+ }
13
+
14
+ parser = OptionParser.new do|opts|
15
+ opts.banner = "Usage: environmate [options]"
16
+
17
+ opts.on('-c', '--config CONFIGFILE.YML', 'Location of the configuration file') do |config_file|
18
+ options[:config_file] = config_file
19
+ end
20
+
21
+ opts.on('-e', '--rack_env RAILS_ENV', 'Rack environment') do |rack_env|
22
+ ENV['RACK_ENV'] = rack_env
23
+ end
24
+
25
+ opts.on('-v', '--verbosity VERBOSITY', 'Log Verbosity: ERROR, WARN, INFO, DEBUG') do |verbosity|
26
+ options[:verbosity] = verbosity
27
+ end
28
+
29
+ opts.on('-f', '--foreground', 'Log to console instead of logfile and don\'t daemonize') do
30
+ options[:foreground] = true
31
+ end
32
+
33
+ opts.on('-t', '--trace', 'Print backtrace on error') do
34
+ options[:trace] = true
35
+ end
36
+
37
+ opts.on('-h', '--help', 'Displays Help') do
38
+ puts opts
39
+ exit
40
+ end
41
+ end
42
+
43
+ parser.parse!
44
+
45
+ begin
46
+ Environmate::App.run!(options)
47
+ rescue => e
48
+ puts e.message
49
+ puts e.backtrace if options[:trace]
50
+ exit(-1)
51
+ end
@@ -0,0 +1,14 @@
1
+ require 'environmate/version'
2
+ require 'environmate/errors'
3
+ require 'environmate/configuration'
4
+ require 'environmate/log'
5
+ require 'environmate/user'
6
+ require 'environmate/xmpp'
7
+ require 'environmate/command'
8
+ require 'environmate/git_repository'
9
+ require 'environmate/deployment'
10
+ require 'environmate/environment'
11
+ require 'environmate/environment_manager'
12
+
13
+ module Environmate
14
+ end
@@ -0,0 +1,83 @@
1
+ #
2
+ # This is the main application class
3
+ #
4
+ require 'json'
5
+ require 'sinatra/base'
6
+ require 'environmate'
7
+
8
+ module Environmate
9
+ class App < Sinatra::Base
10
+
11
+ def self.run!(options = {})
12
+ Environmate.load_configuration(settings.environment.to_s, options[:config_file])
13
+ configuration = Environmate.configuration
14
+
15
+ logfile = options[:foreground] ? STDOUT : configuration['logfile']
16
+ loglevel = options[:verbosity] || configuration['loglevel']
17
+ Environmate.logger = Logger.new(logfile)
18
+ Environmate.log.level = Logger.const_get(loglevel.upcase)
19
+
20
+ Environmate::Xmpp.init
21
+
22
+ server_settings = configuration['server_settings']
23
+
24
+ if server_settings[:SSLEnable]
25
+ require 'webrick/https'
26
+
27
+ ssl_cert = server_settings[:SSLCertificate] || ""
28
+ ssl_key = server_settings[:SSLPrivateKey] || ""
29
+ # replace cert filename with content
30
+ if File.exists?(ssl_cert)
31
+ server_settings[:SSLCertificate] = OpenSSL::X509::Certificate.new(File.open(ssl_cert).read)
32
+ end
33
+ if File.exists?(ssl_key)
34
+ server_settings[:SSLPrivateKey] = OpenSSL::PKey::RSA.new(File.open(ssl_key).read)
35
+ end
36
+ end
37
+
38
+ Rack::Handler::WEBrick.run(self, server_settings)
39
+ end
40
+
41
+ #
42
+ # push hook for deployment of dynamic environments
43
+ # from gitlab
44
+ #
45
+ post '/gitlab_push' do
46
+ data = JSON.parse(request.body.read)
47
+ user = Environmate::User.new(data['user_email'])
48
+ branch = data['ref'].gsub('refs/heads/', '')
49
+ old_revision = data['before'].to_s
50
+ new_revision = data['after'].to_s
51
+
52
+ # gitlab uses a rev with only 0 to signal branch removal
53
+ new_revision = nil if new_revision[/^0+$/]
54
+ puppet_env = Environmate::EnvironmentManager.env_from_branch(branch)
55
+
56
+ unless puppet_env.nil?
57
+ deployment = Environmate::Deployment.new(user, puppet_env, new_revision, old_revision)
58
+ deployment.deploy_dynamic
59
+ end
60
+
61
+ content_type :json
62
+ user.response.to_json
63
+ end
64
+
65
+ #
66
+ # Deploy hook for static environments
67
+ #
68
+ post '/deploy' do
69
+ data = JSON.parse(request.body.read)
70
+ user = Environmate::User.new
71
+ puppet_env = data['environment']
72
+ revision = data['revision']
73
+ token = data['token']
74
+
75
+ deployment = Environmate::Deployment.new(user, puppet_env, revision)
76
+ deployment.deploy_static(token)
77
+
78
+ content_type :json
79
+ user.response.to_json
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,20 @@
1
+ require 'open3'
2
+
3
+ module Environmate
4
+ module Command
5
+
6
+ def command(cmd)
7
+ stdout, stderr, status = Open3.capture3(cmd)
8
+ unless status.success?
9
+ message = []
10
+ message << "Command '#{cmd}' failed"
11
+ message << 'Status:' + status.exitstatus.to_s
12
+ message << "Stdout:\n" + stdout.strip
13
+ message << "Stderr:\n" + stderr.strip
14
+ raise Environmate::DeployError, message.join("\n")
15
+ end
16
+ return stdout
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,52 @@
1
+ #
2
+ # Simple configuration loader from yaml
3
+ #
4
+ require 'yaml'
5
+
6
+ module Environmate
7
+
8
+ def self.load_configuration(environment, config_file = nil)
9
+ config_file ||= config_location
10
+ if config_file.nil?
11
+ raise "No configuration file was provided"
12
+ end
13
+ unless File.exists?(config_file)
14
+ raise "Configuration file #{config_file} does not exist"
15
+ end
16
+ config = YAML.load_file(config_file)[environment]
17
+ @configuration = config_defaults.merge(config)
18
+ end
19
+
20
+ def self.configuration
21
+ @configuration
22
+ end
23
+
24
+ private
25
+
26
+ def self.config_defaults
27
+ {
28
+ 'logfile' => '/var/log/enviromate.log',
29
+ 'loglevel' => 'WARN',
30
+ 'environment_path' => '/etc/puppetlabs/code/environments',
31
+ 'lockfile_path' => '/var/run/lock/environmate',
32
+ 'lockfile_options' => {
33
+ 'timeout' => 300
34
+ },
35
+ 'master_repository' => 'http://gitlab.example.com/puppet/control',
36
+ 'master_branch' => 'origin/master',
37
+ 'master_path' => '/etc/puppetlabs/code/environmate',
38
+ 'dynamic_environments_prefix' => 'env/',
39
+ 'static_environments' => {},
40
+ 'install_modules_command' => 'librarian-puppet install --destructive',
41
+ 'server_settings' => {},
42
+ }
43
+ end
44
+
45
+ def self.config_location
46
+ [
47
+ '/etc/environmate.yml',
48
+ File.expand_path('~/.environmate.yml'),
49
+ ].find{|c| File.exist?(c)}
50
+ end
51
+
52
+ end