flintlock 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []