flintlock 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/CHANGES.md ADDED
@@ -0,0 +1,4 @@
1
+
2
+ # Version 0.1.0
3
+
4
+ - Initial release.
data/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2013, Jon McKenzie
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ * The names of the contributors to this project may not be used to endorse
15
+ or promote products derived from this software without specific prior
16
+ written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
21
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
23
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
24
+ OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
25
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # flintlock
2
+
3
+ ``flintlock`` is a simple application deployer inspired by Heroku's buildpacks.
4
+
5
+ At its core, it's a simple scripting API which allows developers/ops the ability
6
+ to create re-usable application deployments. In ``flintlock``, these deployments
7
+ are called "modules".
8
+
9
+ ## Installation
10
+
11
+ The latest release of ``flintlock`` will be published to ``rubygems.org``. To install,
12
+ just run:
13
+
14
+ ```console
15
+ $ gem install flintlock
16
+ ```
17
+
18
+ If you're running on a RHEL/CentOS 6 machine (or derivative), you might be able to
19
+ make use of the ``flintlock`` RPM spec file located in the git repository
20
+ (``flintlock.spec``) to build RPM packages. Assuming all of the dependencies are
21
+ installed, this should just be a matter of running:
22
+
23
+ ```console
24
+ $ rpmbuild -ba flintlock.spec --define "scl ruby193"
25
+ ```
26
+
27
+ ## Tutorial
28
+
29
+ Let's deploy a sample ``redis`` module I've written. This tutorial assumes you
30
+ are running on a CentOS 6 machine with access to the EPEL package repository.
31
+ You'll also need ``git``.
32
+
33
+ After installing ``flintlock``, run the following:
34
+
35
+ ```console
36
+ flintlock deploy git://github.com/jcmcken/flintlock-redis.git /some/empty/directory
37
+ ```
38
+
39
+ In this case, the ``deploy`` command will recognize that you want to deploy from ``git``.
40
+ It will clone the remote repository, stage it, and then begin deploying the necessary
41
+ files and directories to ``/some/empty/directory``. Let's see what happens:
42
+
43
+
44
+ ```console
45
+ $ flintlock deploy git://github.com/jcmcken/flintlock-redis.git /some/empty/directory
46
+ run fetching module
47
+ info deploying jcmcken/redis (0.0.1) to '/some/empty/directory'
48
+ create creating deploy directory
49
+ run installing and configuring dependencies
50
+ create staging application files
51
+ run launching the application
52
+ run altering application runtime environment
53
+ info complete!
54
+ $
55
+ ```
56
+
57
+ Assuming the module was written well enough, these messages should indicate that our
58
+ ``redis`` server is running. Let's verify:
59
+
60
+ ```console
61
+ $ ps -ef | grep redis
62
+ jcmcken 24846 1 0 17:41 ? 00:00:00 /usr/sbin/redis-server /some/empty/directory/etc/redis.conf
63
+ jcmcken 24865 19343 0 17:41 pts/1 00:00:00 grep redis
64
+ ```
65
+
66
+ Let's take a look at the deploy directory, ``/some/empty/directory``:
67
+
68
+ ```console
69
+ $ tree /some/empty/directory
70
+ /some/empty/directory
71
+ |-- bin
72
+ | `-- redis
73
+ |-- data
74
+ |-- etc
75
+ | `-- redis.conf
76
+ |-- log
77
+ | |-- redis.log
78
+ | |-- stderr.log
79
+ | `-- stdout.log
80
+ `-- run
81
+ `-- redis.pid
82
+ ```
83
+
84
+ You'll notice that everything for this ``redis`` server is self-contained within our deploy
85
+ directory. This is a central tenet of ``flintlock``:
86
+
87
+ **An application deployment is always self-contained within a single directory**
88
+
89
+ How well an application adheres to this philosophy depends on the application. For instance,
90
+ some applications may not have configurable ``/tmp`` directories. For transient data, this
91
+ is usually acceptable. But all of the important files should really be located together.
92
+
93
+ ## Supported Formats
94
+
95
+ Currently ``flintlock`` can install modules from a number of sources. In addition to
96
+ local directories, ``flintlock`` supports the following protocols/formats:
97
+
98
+ * ``git``
99
+ * ``tar`` or ``tar.gz`` over ``http``/``https``
100
+
101
+ Attempting to install any other way will throw an error message similar to the following:
102
+
103
+ ```console
104
+ run fetching module
105
+ error don't know how to download 'https://github.com'!
106
+ ```
107
+
108
+ ## Writing a Module
109
+
110
+ ### Introduction
111
+
112
+ ``flintlock`` modules are simply a bunch of scripts which follow a certain convention.
113
+
114
+ At its heart, a module has the following minimal layout:
115
+
116
+ ```text
117
+ sample-app-1
118
+ |-- bin
119
+ | |-- defaults
120
+ | |-- modify
121
+ | |-- prepare
122
+ | |-- stage
123
+ | |-- start
124
+ | `-- stop
125
+ `-- metadata.json
126
+ ```
127
+
128
+ Running ``flintlock new`` in an empty directory of your choosing will automatically
129
+ generate this structure.
130
+
131
+ The top-level directory (in this case, ``sample-app-1``) can be called anything.
132
+
133
+ The files under ``bin`` are executable scripts (using any language you care to use). All
134
+ of these scripts must exist, but they need not do anything. More on these later.
135
+
136
+ The ``metadata.json`` file contains metadata about the module. This metadata looks as follows:
137
+
138
+ ```json
139
+ {
140
+ "author": "jcmcken",
141
+ "name": "sample-app-1",
142
+ "version": "0.0.1"
143
+ }
144
+ ```
145
+
146
+ All three keys (``author``, ``name``, ``version``) are required, but can be any value. These
147
+ metadata are merely used to namespace the module.
148
+
149
+ ``flintlock`` developers can choose to include more files in their modules if needed.
150
+
151
+ ### Stages
152
+
153
+ ``flintlock`` has different "stages" of execution that occur in a specific order every time
154
+ you run a deployment.
155
+
156
+ These stages correspond directly to the scripts under ``bin/``.
157
+
158
+ The most important stages, and their purpose, are as follows. (The stages occur in the order listed below)
159
+
160
+ * ``prepare``: Install or compile any required dependencies. This script takes no arguments.
161
+ * ``stage``: Stage the application directories and files. This script takes a single argument,
162
+ which is the directory where your app will be deployed. This directory need not exist, but if
163
+ it does, it must be empty.
164
+ * ``start``: Start the application. This script takes the same argument passed to ``stage``.
165
+ * ``modify``: Once the application is started, perform some runtime modifications. For instance,
166
+ if you've just started a MySQL server, you may want to remove the default tables or add a
167
+ password to the database superuser. This script takes the same argument passed to ``stage``
168
+ and ``start``.
169
+
170
+ The API between these scripts and ``flintlock`` is as follows:
171
+
172
+ * If the script exits with a return code of ``0``, ``flintlock`` will think that the script
173
+ succeeded.
174
+ * If the script exits with a return code of ``1``, ``flintlock`` will think that the script
175
+ has failed.
176
+ * Any other return code, and ``flintlock`` will think that some sort of internal error has
177
+ occurred. In other words, something outside of the script's control failed.
178
+
179
+ When ``flintlock`` encounters a non-zero exit code, it will halt execution and display an
180
+ error.
181
+
182
+ ### Configuration Defaults
183
+
184
+ A ``flintlock`` module may also choose to utilize the ``bin/defaults`` script to set
185
+ configuration defaults.
186
+
187
+ By default, ``flintlock`` will source this script prior to executing any of the other
188
+ stages.
189
+
190
+ A user can choose to override these defaults at the command line. For example, if your
191
+ ``defaults`` script looks like:
192
+
193
+ ```bash
194
+ PORT=80
195
+ ```
196
+
197
+ The user can override this at the command line by running:
198
+
199
+ ```console
200
+ PORT=8080 flintlock deploy <module> <deploy_dir>
201
+ ```
202
+
203
+ ``flintlock`` will transparently override the default ``PORT`` with the env var passed at the
204
+ command line.
205
+
206
+ ### Examples
207
+
208
+ An example ``flintlock`` module can be found @ http://github.com/jcmcken/flintlock-redis.git.
data/bin/flintlock ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'flintlock/cli'
4
+
5
+ Flintlock::Cli.start
@@ -0,0 +1,71 @@
1
+ require 'thor'
2
+ require 'flintlock/module'
3
+
4
+ module Flintlock
5
+ class Cli < Thor
6
+ include Thor::Actions
7
+
8
+ desc "deploy MODULE DIRECTORY", "Deploy a flintlock module MODULE to DIRECTORY"
9
+ method_option :debug, :type => :boolean, :description => "enable debug output", :default => false
10
+ def deploy(uri, app_dir)
11
+ say_status "run", "fetching module", :magenta
12
+ mod = get_module(uri, options)
13
+ say_status "info", "deploying #{mod.full_name} to '#{app_dir}'", :blue
14
+ say_status "create", "creating deploy directory"
15
+ mod.create_app_dir(app_dir) rescue abort("deploy directory is not empty")
16
+
17
+ begin
18
+ say_status "run", "installing and configuring dependencies", :magenta
19
+ mod.prepare
20
+ say_status "create", "staging application files"
21
+ mod.stage(app_dir)
22
+ say_status "run", "launching the application", :magenta
23
+ mod.start(app_dir)
24
+ say_status "run", "altering application runtime environment", :magenta
25
+ mod.modify(app_dir)
26
+ say_status "info", "complete!", :blue
27
+ rescue Errno::EACCES => e
28
+ abort("#{e.message.gsub(/Permission denied/, 'permission denied')}")
29
+ rescue RunFailure
30
+ abort('stage failed!')
31
+ end
32
+ end
33
+
34
+ desc "new [DIRECTORY]", "Generate a new, minimal flintlock module"
35
+ def new(directory = Dir.pwd)
36
+ abort("directory isn't empty!") if ! Util.empty_directory?(directory)
37
+ inside(directory) do
38
+ empty_directory "bin"
39
+ inside("bin") do
40
+ Module.script_names.each do |script|
41
+ create_file script
42
+ end
43
+ end
44
+ create_file(Metadata.filename, Metadata.empty)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def get_module(uri, options={})
51
+ begin
52
+ Flintlock::Module.new(uri, options)
53
+ rescue InvalidModule => e
54
+ abort("invalid flintlock module '#{e}'")
55
+ rescue UnsupportedModuleURI => e
56
+ abort("don't know how to download '#{e}'!")
57
+ rescue ModuleDownloadError => e
58
+ abort("failed to download '#{e}'")
59
+ end
60
+ end
61
+
62
+ def error(message)
63
+ say_status "error", message, :red
64
+ end
65
+
66
+ def abort(message)
67
+ error(message)
68
+ exit(1)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,13 @@
1
+ require 'logger'
2
+
3
+ module Flintlock
4
+ class Logger < ::Logger
5
+ def silence!
6
+ @saved_logdev, @logdev = @logdev, nil
7
+ end
8
+
9
+ def unsilence!
10
+ @logdev, @saved_logdev = @saved_logdev, nil if @saved_logdev
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,53 @@
1
+ require 'json'
2
+
3
+ module Flintlock
4
+ class Metadata
5
+ attr_reader :filename
6
+
7
+ def initialize(filename = nil)
8
+ @filename = filename || default_metadata_file
9
+ @data = Metadata.load(@filename)
10
+ end
11
+
12
+ def self.filename
13
+ 'metadata.json'
14
+ end
15
+
16
+ def valid?
17
+ begin
18
+ result = ! [author, version, name].map(&:empty?).any?
19
+ rescue
20
+ result = false
21
+ end
22
+ return result
23
+ end
24
+
25
+ def default_metadata_file
26
+ File.join(Dir.pwd, Metadata.filename)
27
+ end
28
+
29
+ def self.load(filename)
30
+ JSON.load(File.read(filename))
31
+ end
32
+
33
+ def author
34
+ @data.fetch('author')
35
+ end
36
+
37
+ def version
38
+ @data.fetch('version')
39
+ end
40
+
41
+ def name
42
+ @data.fetch('name')
43
+ end
44
+
45
+ def full_name
46
+ "#{author}/#{name} (#{version})"
47
+ end
48
+
49
+ def self.empty
50
+ {"author" => "", "version" => "", "name" => ""}.to_json
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,208 @@
1
+ require 'flintlock/metadata'
2
+ require 'flintlock/logger'
3
+ require 'flintlock/util'
4
+ require 'open3'
5
+ require 'fileutils'
6
+ require 'logger'
7
+ require 'shellwords'
8
+ require 'uri'
9
+ require 'tmpdir'
10
+ require 'tempfile'
11
+ require 'open-uri'
12
+
13
+ module Flintlock
14
+ class InvalidModule < RuntimeError; end
15
+ class UnsupportedModuleURI < RuntimeError; end
16
+ class ModuleDownloadError < RuntimeError; end
17
+ class RunFailure < RuntimeError; end
18
+
19
+ class Module
20
+ attr_reader :uri, :metadata
21
+
22
+ def initialize(uri = nil, options={})
23
+ # track temporary files and directories for deletion
24
+ @tmpfiles = []
25
+
26
+ # destroy tmp files on exit
27
+ at_exit { handle_exit }
28
+
29
+ @debug = !!options[:debug]
30
+ @uri = uri || Dir.pwd
31
+ @log = load_logger
32
+ @root_dir = download_from_uri(@uri)
33
+ @metadata = load_metadata
34
+
35
+ load_scripts!
36
+ validate
37
+
38
+ @env = load_env(@defaults_script)
39
+
40
+ end
41
+
42
+ def download_from_uri(uri)
43
+ case URI.parse(uri).scheme
44
+ when nil # no scheme == local file
45
+ uri
46
+ when 'git'
47
+ handle_git_uri(uri)
48
+ when 'http', 'https'
49
+ raise UnsupportedModuleURI.new(uri) if ! Util.supported_archive?(uri)
50
+ # over these protocols, we're getting an archive
51
+ handle_archive(handle_http_uri(uri))
52
+ else
53
+ raise UnsupportedModuleURI, uri
54
+ end
55
+ end
56
+
57
+ def handle_exit
58
+ @tmpfiles.each { |x| FileUtils.rm_rf(x, :secure => true) }
59
+ end
60
+
61
+ def handle_git_uri(uri)
62
+ root_dir = Dir.mktmpdir
63
+ @tmpfiles << root_dir
64
+ command = Shellwords.join(['git', 'clone', uri, root_dir])
65
+ stdout, stderr, status = Open3.capture3(command)
66
+ raise ModuleDownloadError, uri if status.exitstatus != 0
67
+ root_dir
68
+ end
69
+
70
+ def handle_http_uri(uri, buffer=8192)
71
+ tmpfile = Tempfile.new(['flintlock', Util.full_extname(uri)]).path
72
+ @tmpfiles << tmpfile
73
+ open(uri) do |input|
74
+ open(tmpfile, 'wb') do |output|
75
+ while ( buf = input.read(buffer))
76
+ output.write buf
77
+ end
78
+ end
79
+ end
80
+ tmpfile
81
+ rescue OpenURI::HTTPError
82
+ raise ModuleDownloadError, uri
83
+ end
84
+
85
+ def handle_archive(filename)
86
+ tmpdir = Dir.mktmpdir
87
+ @tmpfiles << tmpdir
88
+ case filename
89
+ when /\.tar\.gz$/
90
+ command = ['tar', 'xfz', filename, '-C', tmpdir]
91
+ when /\.tar$/
92
+ command = ['tar', 'xf', filename, '-C', tmpdir]
93
+ else
94
+ raise UnsupportedModuleURI, filename
95
+ end
96
+ _, _, status = Open3.capture3(Shellwords.join(command))
97
+ raise ModuleDownloadError if status.exitstatus != 0
98
+ tmpdir
99
+ end
100
+
101
+ def full_name
102
+ @metadata.full_name
103
+ end
104
+
105
+ def self.script_names
106
+ ['defaults', 'modify', 'prepare', 'stage', 'start', 'stop']
107
+ end
108
+
109
+ def scripts
110
+ [@modify_script, @prepare_script, @stage_script, @start_script, @stop_script, @defaults_script]
111
+ end
112
+
113
+ def scripts_exist?
114
+ scripts.map { |x| File.file?(x) }.all?
115
+ end
116
+
117
+ def valid?
118
+ @metadata.valid? && scripts_exist?
119
+ end
120
+
121
+ def prepare
122
+ @log.info("running prepare stage: #{@prepare_script}")
123
+ run_script(@prepare_script)
124
+ end
125
+
126
+ def stage(app_dir)
127
+ @log.info("running stage stage: #{@stage_script}")
128
+ run_script(@stage_script, app_dir)
129
+ end
130
+
131
+ def modify(app_dir)
132
+ @log.info("running modify stage: #{@modify_script}")
133
+ run_script(@modify_script, app_dir)
134
+ end
135
+
136
+ def start(app_dir)
137
+ @log.info("running start stage: #{@start_script}")
138
+ run_script(@start_script, app_dir)
139
+ end
140
+
141
+ def stop(app_dir)
142
+ @log.info("running stop stage: #{@stop_script}")
143
+ run_script(@stop_script, app_dir)
144
+ end
145
+
146
+ def current_env
147
+ Hash[ENV.to_a] # get rid of ENV obj
148
+ end
149
+
150
+ def load_env(defaults_script)
151
+ # hokey, but seems to work
152
+ env_data = %x{set -a && source #{defaults_script} && env}.split.map{ |x| x.split('=', 2) }
153
+ env = Hash[env_data]
154
+ @log.debug("defaults script is #{defaults_script}")
155
+ @log.debug("defaults env is #{env.inspect}")
156
+ env = env.merge(current_env)
157
+ @log.debug("merged env is #{env.inspect}")
158
+ env
159
+ end
160
+
161
+ def create_app_dir(app_dir)
162
+ FileUtils.mkdir_p(app_dir)
163
+ raise if ! Util.empty_directory?(app_dir)
164
+ end
165
+
166
+ private
167
+
168
+ def load_scripts!
169
+ Module.script_names.map do |x|
170
+ instance_variable_set("@#{x}_script".to_sym, File.join(@root_dir, 'bin', x))
171
+ end
172
+ end
173
+
174
+ def validate
175
+ raise InvalidModule.new(@uri) if ! valid?
176
+ end
177
+
178
+ def load_logger
179
+ log = Logger.new(STDOUT)
180
+ log.silence! if ! @debug
181
+ log
182
+ end
183
+
184
+ def load_metadata
185
+ begin
186
+ Metadata.new(File.join(@root_dir, Metadata.filename))
187
+ rescue Errno::ENOENT
188
+ raise InvalidModule, uri
189
+ end
190
+ end
191
+
192
+ def run(command)
193
+ handle_run(*Open3.capture3(@env, command))
194
+ end
195
+
196
+ def run_script(script, *args)
197
+ run(Shellwords.join([script, *args]))
198
+ end
199
+
200
+ def handle_run(stdout, stderr, status)
201
+ stdout.lines.each { |x| @log.info(x) }
202
+ if status.exitstatus != 0
203
+ stderr.lines.each { |x| @log.error(x) }
204
+ raise RunFailure
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,27 @@
1
+ module Flintlock
2
+ class Util
3
+ def self.empty_directory?(directory)
4
+ Dir[File.join(directory, '*')].empty?
5
+ end
6
+
7
+ def self.supported_archives
8
+ ['.tar.gz', '.tar']
9
+ end
10
+
11
+ def self.supported_archive?(filename)
12
+ Util.supported_archives.include?(full_extname(filename))
13
+ end
14
+
15
+ def self.full_extname(filename)
16
+ data = []
17
+ current_filename = filename.dup
18
+ while true
19
+ ext = File.extname(current_filename)
20
+ break if ext.empty?
21
+ current_filename = current_filename.gsub(/#{ext}$/, '')
22
+ data << ext
23
+ end
24
+ data.reverse.join
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module Flintlock
2
+ VERSION = '0.1.0'
3
+ end
data/lib/flintlock.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ require 'flintlock/metadata'
3
+ require 'flintlock/module'
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flintlock
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jon McKenzie
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-05-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: thor
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: A simple application deployer inspired by Heroku's buildpacks
47
+ email: jcmcken@gmail.com
48
+ executables:
49
+ - flintlock
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - lib/flintlock.rb
54
+ - lib/flintlock/module.rb
55
+ - lib/flintlock/metadata.rb
56
+ - lib/flintlock/util.rb
57
+ - lib/flintlock/cli.rb
58
+ - lib/flintlock/logger.rb
59
+ - lib/flintlock/version.rb
60
+ - LICENSE
61
+ - README.md
62
+ - CHANGES.md
63
+ - bin/flintlock
64
+ homepage: https://github.com/jcmcken/flintlock
65
+ licenses:
66
+ - MIT
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: 1.9.3
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 1.8.23
86
+ signing_key:
87
+ specification_version: 3
88
+ summary: A simple application deployer
89
+ test_files: []