harpoon 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 937896b519cb54849436445b71d22a9e8d8ba4af
4
+ data.tar.gz: 7ce38434f4b5d2c80aebe8a11fa1b76d44b8822f
5
+ SHA512:
6
+ metadata.gz: 395c2a001da8362adf10d10cb49d7bda934236f00fdcb733dd09631c9b15987347fa90e5d3323939af42a3d09bde1e515b45cdd0bb08a059aefe68cb45caea3e
7
+ data.tar.gz: e58b5de1b132b71b25369087264838717fd52443c580ad109ddcfd1e9ddacc6ff7c8e82988a8350616f2e69c824da676c82d4a9f9677f469d423d8b263506f78
@@ -0,0 +1 @@
1
+ *.gem
@@ -0,0 +1,21 @@
1
+ MIT License (MIT)
2
+
3
+ Copyright (c) 2013-2014 Ryan Quinn
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.
@@ -0,0 +1,4 @@
1
+ # Harpoon
2
+ A static site deployer
3
+
4
+ This is a work in progress, not ready for prime time just yet. Keep your eyes on it though, I'll be releasing it shortly!
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'test'
5
+ end
6
+
7
+ desc "Run tests"
8
+ task :default => :test
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'harpoon'
3
+
4
+
5
+ Harpoon::Client.start
@@ -0,0 +1,18 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'harpoon'
3
+ s.version = '0.0.3'
4
+ s.date = '2014-08-20'
5
+ s.summary = "A single page app deployer for amazon s3"
6
+ s.description = "Deploy small server-less webapps to amazon s3, including buckets, dns and permissions"
7
+ s.authors = ["Ryan Quinn"]
8
+ s.email = 'ryan@mazondo.com'
9
+ s.licenses = ["MIT"]
10
+ s.files = `git ls-files -z`.split("\x0")
11
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
12
+ s.add_dependency "thor", "~> 0.19.1"
13
+ s.add_dependency "netrc", "~> 0.7.7"
14
+ s.add_dependency "aws-sdk", "~> 1.51.0"
15
+ s.add_dependency "public_suffix", "~> 1.4.5"
16
+ s.add_dependency 'colorize', '~> 0.7.3'
17
+ s.homepage = 'http://www.getharpoon.com'
18
+ end
@@ -0,0 +1,11 @@
1
+ module Harpoon
2
+ require_relative "harpoon/auth"
3
+ require_relative "harpoon/client"
4
+ require_relative "harpoon/errors"
5
+ require_relative "harpoon/config"
6
+ require_relative "harpoon/runner"
7
+
8
+ # Services
9
+ require_relative "harpoon/services/test.rb"
10
+ require_relative "harpoon/services/s3.rb"
11
+ end
@@ -0,0 +1,101 @@
1
+ require "netrc"
2
+ require 'fileutils'
3
+
4
+ module Harpoon
5
+ class Auth
6
+ attr_reader :namespace
7
+ def initialize(options = {})
8
+ @logger = options[:logger]
9
+ if options[:namespace]
10
+ @namespace = sanitize_namespace(options[:namespace])
11
+ else
12
+ @namespace = "main"
13
+ end
14
+ end
15
+
16
+ def destroy(key)
17
+ if netrc && netrc[netrc_key(key)]
18
+ netrc.delete(netrc_key(key))
19
+ netrc.save
20
+ end
21
+ end
22
+
23
+ def set(key, value1 = nil, value2 = nil)
24
+ FileUtils.mkdir_p(File.dirname(netrc_path))
25
+ FileUtils.touch(netrc_path)
26
+ unless running_on_windows?
27
+ FileUtils.chmod(0600, netrc_path)
28
+ end
29
+ netrc[netrc_key(key)] = [netrc_nil(value1), netrc_nil(value2)]
30
+ netrc.save
31
+ end
32
+
33
+ def get(key)
34
+ if netrc
35
+ n = netrc[netrc_key(key)]
36
+ n ? n.map {|m| netrc_nil(m)} : n
37
+ end
38
+ end
39
+
40
+ def get_or_ask(key, mes1 = "Private Key", mes2 = "Public Key")
41
+ values = get(key)
42
+ return values if values
43
+ puts "Enter your #{mes1}:"
44
+ val1 = $stdin.gets.to_s.strip
45
+ puts "Enter your #{mes2}:"
46
+ val2 = $stdin.gets.to_s.strip
47
+ set(key, val1, val2)
48
+ return [val1, val2]
49
+ end
50
+
51
+ private
52
+
53
+ #netrc doesn't like nil values
54
+ def netrc_nil(value = nil)
55
+ if value
56
+ if value == "nothing-here"
57
+ return nil
58
+ else
59
+ return value
60
+ end
61
+ else
62
+ return "nothing-here"
63
+ end
64
+ end
65
+
66
+ def netrc_key(key)
67
+ "harpoon-#{@namespace}-#{key}"
68
+ end
69
+
70
+ def netrc_path
71
+ default = Netrc.default_path
72
+ encrypted = default + ".gpg"
73
+ if File.exists?(encrypted)
74
+ encrypted
75
+ else
76
+ default
77
+ end
78
+ end
79
+
80
+ def netrc
81
+ @netrc ||= begin
82
+ File.exists?(netrc_path) && Netrc.read(netrc_path)
83
+ rescue => error
84
+ if error.message =~ /^Permission bits for/
85
+ perm = File.stat(netrc_path).mode & 0777
86
+ abort("Permissions #{perm} for '#{netrc_path}' are too open. You should run `chmod 0600 #{netrc_path}` so that your credentials are NOT accessible by others.")
87
+ else
88
+ raise error
89
+ end
90
+ end
91
+ end
92
+
93
+ def running_on_windows?
94
+ RUBY_PLATFORM =~ /mswin32|mingw32/
95
+ end
96
+
97
+ def sanitize_namespace(n)
98
+ n.gsub(/[^a-zA-Z0-9\-]/, "")
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,51 @@
1
+ require 'fileutils'
2
+ require "thor"
3
+
4
+ module Harpoon
5
+ class Client < Thor
6
+ class_option :config, :type => :string
7
+ class_option :log_level, :type => :string
8
+
9
+ desc "init", "Initializes a config file in current directory"
10
+ def init
11
+ #initialize a config file in the current directory
12
+ begin
13
+ Harpoon::Config.create(Dir.pwd)
14
+ rescue Harpoon::Errors::AlreadyInitialized => e
15
+ puts e.message
16
+ else
17
+ puts "Harpoon has been initialized"
18
+ end
19
+ end
20
+
21
+ desc "setup", "Setup the current app"
22
+ def setup
23
+ runner = Harpoon::Runner.new(options)
24
+ runner.setup
25
+ end
26
+
27
+ desc "deploy", "Deploys the current app"
28
+ def deploy
29
+ runner = Harpoon::Runner.new(options)
30
+ runner.deploy
31
+ end
32
+
33
+ desc "doctor", "Check the health of the current deploy strategy"
34
+ def doctor
35
+ runner = Harpoon::Runner.new(options)
36
+ runner.doctor
37
+ end
38
+
39
+ desc "list", "List available rollbacks"
40
+ def list
41
+ runner = Harpoon::Runner.new(options)
42
+ runner.list
43
+ end
44
+
45
+ desc "rollback", "Rollback to previous release"
46
+ def rollback
47
+ runner = Harpoon::Runner.new(options)
48
+ runner.rollback
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,60 @@
1
+ require "json"
2
+ require "fileutils"
3
+
4
+ module Harpoon
5
+ class Config
6
+ # Checks if a config exists at a given path
7
+ def self.exists?(path)
8
+ path = full_path(path, false)
9
+ File.exists?(path)
10
+ end
11
+
12
+ # Returns the full path of a config given a directory.
13
+ # By default this raises an alert if the config doesn't exist.
14
+ def self.full_path(path, must_exist = true)
15
+ path = File.expand_path(path)
16
+ if File.directory?(path)
17
+ path = File.join(path, "harpoon.json")
18
+ end
19
+ raise Harpoon::Errors::InvalidConfigLocation, "No config located at #{path}" if must_exist && !File.exists?(path)
20
+ path
21
+ end
22
+
23
+ # Create a config at a given path
24
+ def self.create(path)
25
+ path = full_path(path, false)
26
+ if File.exists?(path)
27
+ raise Harpoon::Errors::AlreadyInitialized, "Harpoon has already been initialized, see #{path}"
28
+ end
29
+ FileUtils.copy_file File.join(File.dirname(__FILE__), "templates", "harpoon.json"), path
30
+ end
31
+
32
+ # Load a config file from a given path
33
+ # Returns a new config object
34
+ def self.read(path = nil, logger = nil)
35
+ path = full_path(path)
36
+ if File.exists? path
37
+ data = JSON.parse(IO.read(path))
38
+ directory = [File.dirname(path), data["directory"]].select {|m| m && m != ""}
39
+ data["directory"] = File.join(directory)
40
+ new data, logger
41
+ else
42
+ raise Harpoon::Errors::InvalidConfigLocation, "Specified config doesn't exist, please create one"
43
+ end
44
+ end
45
+
46
+ attr_reader :files
47
+
48
+ # Initialize a new config object with the data loaded
49
+ def initialize(data = {}, logger = nil)
50
+ @config = data
51
+ @logger = logger
52
+ @files = Dir.glob(File.join(@config["directory"], "**", "*")).select {|f| !File.directory?(f) && File.basename(f) != "harpoon.json"} if @config["directory"]
53
+ end
54
+
55
+ # Check for the configuration item
56
+ def method_missing(method, *args)
57
+ @config[method.to_s]
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,8 @@
1
+ module Harpoon
2
+ module Errors
3
+ class InvalidConfigLocation < StandardError;end;
4
+ class AlreadyInitialized < StandardError;end;
5
+ class InvalidConfiguration < StandardError;end;
6
+ class MissingSetup < StandardError;end;
7
+ end
8
+ end
@@ -0,0 +1,79 @@
1
+ require "logger"
2
+ require "colorize"
3
+
4
+ module Harpoon
5
+ class Runner
6
+ def initialize(options)
7
+ @logger = load_logger(options[:log_level])
8
+ @config = Harpoon::Config.read(options[:config] || Dir.pwd, @logger)
9
+ @auth = load_auth
10
+ @service = load_host
11
+ end
12
+
13
+ def method_missing(method, *args)
14
+ #don't know about this here, must be for the service
15
+ @service.send(method, *args)
16
+ end
17
+
18
+ private
19
+
20
+ def load_auth
21
+ if @config.auth_namespace
22
+ return Harpoon::Auth.new({namespace: @config.auth_namespace, logger: @logger})
23
+ else
24
+ return Harpoon::Auth.new({logger: @logger})
25
+ end
26
+ end
27
+
28
+ def load_host
29
+ if @config.hosting
30
+ begin
31
+ return Harpoon::Services.const_get(@config.hosting.capitalize).new(@config, @auth, @logger)
32
+ rescue NameError => e
33
+ raise Harpoon::Errors::InvalidConfiguration, "Unknown Hosting Service: #{@config.hosting}"
34
+ end
35
+ else
36
+ raise Harpoon::Errors::InvalidConfiguration, "Hosting parameter is required"
37
+ end
38
+ end
39
+
40
+ def load_logger(log_level = "info")
41
+ log_level ||= "info"
42
+ logger = Logger.new(STDOUT)
43
+
44
+ #set log level
45
+ case log_level.downcase
46
+ when "debug"
47
+ logger.level = Logger::DEBUG
48
+ when "info"
49
+ logger.level = Logger::INFO
50
+ when "warn"
51
+ logger.level = Logger::WARN
52
+ when "error"
53
+ logger.level = Logger::ERROR
54
+ when "fatal"
55
+ logger.level = Logger::FATAL
56
+ end
57
+
58
+ #set log formatter
59
+ logger.formatter = proc do |severity, datetime, progname, msg|
60
+ case severity.to_s.downcase
61
+ when "debug"
62
+ "DEBUG: #{msg}\n".colorize(:light_blue)
63
+ when "info"
64
+ "#{msg}\n".colorize(:gray)
65
+ when "warn"
66
+ "#{msg}\n".colorize(:yellow)
67
+ when "error"
68
+ "#{msg}\n".colorize(:orange)
69
+ when "fatal"
70
+ "#{msg}\n".colorize(:red)
71
+ else
72
+ "#{severity} - #{msg}\n"
73
+ end
74
+ end
75
+
76
+ logger
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,245 @@
1
+ require "aws-sdk"
2
+ require "uri"
3
+ require "public_suffix"
4
+ require "pathname"
5
+
6
+ module Harpoon
7
+ module Services
8
+ class S3
9
+ def initialize(config, auth, logger)
10
+ # Store what we're given for later
11
+ @config = config
12
+ @auth = auth
13
+ @logger = logger
14
+
15
+ # Ask for the users credentials
16
+ @credentials = @auth.get_or_ask("s3", "Key", "Secret")
17
+
18
+ if config.hosting_options && config.hosting_options["region"]
19
+ region = config.hosting_options["region"]
20
+ else
21
+ region = "us-west-2"
22
+ end
23
+
24
+ AWS.config(access_key_id: @credentials[0], secret_access_key: @credentials[1], region: region)
25
+
26
+ # setup amazon interfaces
27
+ @s3 = AWS::S3.new
28
+ @r53 = AWS::Route53.new
29
+ end
30
+
31
+ def setup
32
+ if @config.domain
33
+ #we have domain info, so let's make sure it's setup for it
34
+ if @config.domain["primary"]
35
+ #primary domain detected, let's make sure it exists
36
+ bucket = setup_bucket(@config.domain["primary"])
37
+ #setup bucket to server webpages
38
+ @logger.info "Setting primary domain as website"
39
+ bucket.configure_website
40
+ #setup ACL
41
+ @logger.info "Setting bucket policy"
42
+ policy = AWS::S3::Policy.new
43
+ policy.allow(
44
+ actions: ['s3:GetObject'],
45
+ resources: [bucket.objects],
46
+ principals: :any
47
+ )
48
+
49
+ @logger.debug policy.to_json
50
+
51
+ bucket.policy = policy
52
+
53
+
54
+ history = setup_bucket(rollback_bucket(@config.domain["primary"]))
55
+ @logger.info "Created rollback bucket"
56
+
57
+ setup_dns_alias(@config.domain["primary"], bucket)
58
+
59
+
60
+ if @config.domain["forwarded"]
61
+ #we also want to forward some domains
62
+ #make sure we have an array
63
+ #TODO : Move all of this nonsense to the config object, it should be validating this stuff
64
+ forwarded = @config.domain["forwarded"].is_a?(Array) ? @config.domain["forwarded"] : [@config.domain["forwarded"]]
65
+ forwarded.each do |f|
66
+ bucket = setup_bucket(f)
67
+ @logger.info "Seting up redirect to primary"
68
+ cw = AWS::S3::WebsiteConfiguration.new({redirect_all_requests_to: {host_name: @config.domain["primary"]}})
69
+ bucket.website_configuration = cw
70
+ setup_dns_alias(f, bucket)
71
+ end
72
+ end
73
+
74
+ # print out DNS settings
75
+ print_dns_settings(@config.domain["primary"])
76
+ end
77
+ end
78
+ end
79
+
80
+ def deploy
81
+ raise Harpoon::Errors::InvalidConfiguration, "Missing list of files" unless @config.files && @config.directory && @config.domain["primary"]
82
+ move_existing_to_history!
83
+ current_bucket = @s3.buckets[@config.domain["primary"]]
84
+ raise Harpoon::Errors::MissingSetup, "Required s3 buckets are not created, consider running harpoon setup first" unless current_bucket.exists?
85
+ @logger.info "Writing files to s3"
86
+ @config.files.each do |f|
87
+ @logger.debug "Path: #{f}"
88
+ relative_path = Pathname.new(f).relative_path_from(Pathname.new(@config.directory)).to_s
89
+ @logger.debug "s3 key: #{relative_path}"
90
+ current_bucket.objects[relative_path].write(Pathname.new(f))
91
+ end
92
+ @logger.info "Deploy complete"
93
+ end
94
+
95
+ def list
96
+ @logger.info "The following rollbacks are available:"
97
+ if @config.domain && @config.domain["primary"]
98
+ tree = @s3.buckets[rollback_bucket(@config.domain["primary"])].as_tree
99
+ rollbacks = tree.children.collect {|i| i.prefix.gsub(/\/$/, "").to_i }
100
+ rollbacks.sort!.reverse!
101
+ rollbacks.each_with_index do |r, index|
102
+ @logger.info Time.at(r).strftime("#{index + 1} - %F %r")
103
+ end
104
+ end
105
+ end
106
+
107
+ def doctor
108
+ # check configuration
109
+ if @config.domain
110
+ if @config.domain["primary"]
111
+ @logger.info "Primary Domain: #{@config.domain["primary"]}"
112
+ else
113
+ @logger.fatal "Missing Primary Domain"
114
+ exit
115
+ end
116
+ else
117
+ @logger.fatal "Missing Domain Configuration"
118
+ exit
119
+ end
120
+ # check IAM permissions
121
+ # check buckets exist
122
+ primary_bucket = @s3.buckets[@config.domain["primary"]]
123
+ if primary_bucket.exists?
124
+ @logger.info "Primary bucket exists"
125
+ else
126
+ @logger.fatal "Missing Primary domain bucket"
127
+ end
128
+ # check domain setup
129
+ # print DNS settings
130
+ print_dns_settings(@config.domain["primary"])
131
+ end
132
+
133
+ def rollback
134
+ @logger.info "Not yet implemented!"
135
+ @logger.info "But don't worry, your rollbacks are safely stored in #{rollback_bucket(@config.domain["primary"])}"
136
+ self.list
137
+ end
138
+
139
+ private
140
+
141
+ def setup_dns_alias(domain, bucket)
142
+ @logger.debug "Setup Domain Alias for #{domain}"
143
+ #extract root domain
144
+ rdomain = root_domain(domain)
145
+ @logger.debug "Root Domain: #{rdomain}"
146
+ #add that dot
147
+ rdomain += "." unless rdomain.end_with?(".")
148
+ domain += "." unless domain.end_with?(".")
149
+ @logger.debug "Post Dot root: #{rdomain}"
150
+ @logger.debug "Post Dot domain: #{domain}"
151
+
152
+
153
+ #ensure we have a hosted zone
154
+ hosted_zone = @r53.hosted_zones.find {|h| h.name == rdomain}
155
+ hosted_zone = @r53.hosted_zones.create(rdomain, {comment: "Created By Harpoon"}) if !hosted_zone
156
+
157
+ record = hosted_zone.rrsets[domain, "A"]
158
+
159
+ dns_alias, zone_id = alias_and_zone_id(bucket.location_constraint)
160
+ @logger.debug "Alias: #{dns_alias}, Zone: #{zone_id}"
161
+
162
+ record = hosted_zone.rrsets.create(domain, "A", alias_target: {dns_name: dns_alias, hosted_zone_id: zone_id, evaluate_target_health: false}) unless record.exists?
163
+ @logger.info "Created Host Record: #{record.name}, #{record.type}"
164
+ end
165
+
166
+ def setup_bucket(bucket_name)
167
+ @logger.info "Creating bucket: #{bucket_name}"
168
+ bucket = @s3.buckets[bucket_name]
169
+ unless bucket.exists?
170
+ bucket = @s3.buckets.create(bucket_name)
171
+ end
172
+ bucket
173
+ end
174
+
175
+ def root_domain(domain)
176
+ #pull out the host if we have a full url
177
+ domain = URI.parse(domain).host if domain.start_with?("http")
178
+ PublicSuffix.parse(domain).domain
179
+ end
180
+
181
+ def alias_and_zone_id(constraint)
182
+ #taken from: http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
183
+ case constraint
184
+ when "us-east-1"
185
+ return "s3-website-us-east-1.amazonaws.com.", "Z3AQBSTGFYJSTF"
186
+ when "us-west-2"
187
+ return "s3-website-us-west-2.amazonaws.com.", "Z3BJ6K6RIION7M"
188
+ when "us-west-1"
189
+ return "s3-website-us-west-1.amazonaws.com.", "Z2F56UZL2M1ACD"
190
+ when "eu-west-1"
191
+ return "s3-website-eu-west-1.amazonaws.com.", "Z1BKCTXD74EZPE"
192
+ when "ap-southeast-1"
193
+ return "s3-website-ap-southeast-1.amazonaws.com.", "Z3O0J2DXBE1FTB"
194
+ when "ap-southeast-2"
195
+ return "s3-website-ap-southeast-2.amazonaws.com.", "Z1WCIGYICN2BYD"
196
+ when "ap-northeast-1"
197
+ return "s3-website-ap-northeast-1.amazonaws.com.", "Z2M4EHUR26P7ZW"
198
+ when "sa-east-1"
199
+ return "s3-website-sa-east-1.amazonaws.com.", "Z7KQH4QJS55SO"
200
+ when "us-gov-west-1"
201
+ return "s3-website-us-gov-west-1.amazonaws.com.", "Z31GFT0UA1I2HV"
202
+ end
203
+ end
204
+
205
+ def print_dns_settings(domain)
206
+ @logger.debug "Print DNS Settings for: #{domain}"
207
+ rdomain = "#{root_domain(domain)}."
208
+ @logger.debug "Root Domain: #{rdomain}"
209
+ @logger.warn "=============================="
210
+ @logger.warn "Please forward to the following DNS:"
211
+ hosted_zone = @r53.hosted_zones.find {|h| h.name == rdomain}
212
+ hosted_zone.rrsets[rdomain, "NS"].resource_records.each do |r|
213
+ @logger.warn r[:value]
214
+ end
215
+ @logger.warn "=============================="
216
+ end
217
+
218
+ def rollback_bucket(domain)
219
+ "#{domain}-history"
220
+ end
221
+
222
+ def move_existing_to_history!
223
+ raise Harpoon::Errors::InvalidConfiguration, "Must have a primary domain defined" unless @config.domain["primary"]
224
+ @logger.info "Moving existing deploy to history"
225
+ current = @s3.buckets[@config.domain["primary"]]
226
+ history = @s3.buckets[rollback_bucket(@config.domain["primary"])]
227
+ raise Harpoon::Errors::MissingSetup, "The expected buckets are not yet created, please try running harpoon setup" unless current.exists? && history.exists?
228
+
229
+ current_date = Time.now.to_i
230
+ #iterate over current bucket objects and prefix them with timestamp, move to history bucket
231
+ current.objects.each do |o|
232
+ s3_key = File.join(current_date.to_s, o.key)
233
+ @logger.debug "Original Key: #{o.key}"
234
+ @logger.debug "History Key: #{s3_key}"
235
+ @logger.debug "Metadata: #{o.metadata.to_h.inspect}"
236
+ history.objects[s3_key].write(o.read, {metadata: o.metadata})
237
+ end
238
+ @logger.debug "Moved to history, deleting files from current bucket"
239
+ #delete the current objects
240
+ current.objects.delete_all
241
+ @logger.debug "Files deleted"
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,18 @@
1
+ module Harpoon
2
+ module Services
3
+ class Test
4
+ attr_accessor :config, :requests
5
+
6
+ def initialize(config = nil, auth = nil, logger = nil)
7
+ @auth = auth
8
+ @config = config
9
+ @requests = []
10
+ @logger = logger
11
+ end
12
+
13
+ def method_missing(method, *args)
14
+ @requests.push [method, args]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ {
2
+ "name" : "appName",
3
+ "hosting" : "s3",
4
+ "auth_namespace" : "main",
5
+ "hosting_options" : {
6
+ "region" : "us-west-2"
7
+ },
8
+ "domain" : {
9
+ "primary" : "www.domain.com",
10
+ "forwarded" : ["domain.com"]
11
+ },
12
+ "directory" : ""
13
+ }
@@ -0,0 +1,2 @@
1
+ require "minitest/autorun"
2
+ require_relative "../lib/harpoon"
@@ -0,0 +1,63 @@
1
+ require "helper"
2
+
3
+ describe "Auth token" do
4
+
5
+ it "Should let me initialize a namespace" do
6
+ auth = Harpoon::Auth.new
7
+ assert_equal "main", auth.namespace, "Should default to main namespace"
8
+
9
+ auth = Harpoon::Auth.new({namespace: "test"})
10
+ assert_equal "test", auth.namespace, "Should have set the namespace"
11
+ end
12
+
13
+ it "should let me store and retrieve namespaced auth params" do
14
+ auth = Harpoon::Auth.new({namespace: "test"})
15
+ auth2 = Harpoon::Auth.new({namespace: "test2"})
16
+
17
+ #delete if already exists
18
+ auth.destroy "test-host"
19
+ auth2.destroy "test-host"
20
+
21
+ assert !auth.get("test-host"), "Should have destroyed values"
22
+ assert !auth2.get("test-host"), "Should have destroyed values"
23
+
24
+ auth.set "test-host", "key", "secret"
25
+ auth2.set "test-host", "key2", "secret2"
26
+
27
+ assert_equal ["key", "secret"], auth.get("test-host"), "Should have set Auth1"
28
+ assert_equal ["key2", "secret2"], auth2.get("test-host"), "Should have set Auth2"
29
+ end
30
+
31
+ it "Should be able to handle single keys" do
32
+ auth = Harpoon::Auth.new({namespace: "test"})
33
+
34
+ # destroy if it exists
35
+ auth.destroy "test-host"
36
+
37
+ auth.set "test-host", "key"
38
+
39
+ assert_equal ["key", nil], auth.get("test-host"), "Should have been able to handle a single key"
40
+ end
41
+
42
+ it "Should understand how to get or ask" do
43
+ auth = Harpoon::Auth.new({namespace: "test"})
44
+ auth.destroy "test-host"
45
+
46
+ assert !auth.get("test-host"), "Should have destroyed values"
47
+
48
+ auth.get_or_ask "test-host", "enter 1", "enter 2"
49
+
50
+ assert_equal 1, auth.get("test-host")[0].to_i, "Should have stored it correctly"
51
+ assert_equal 2, auth.get("test-host")[1].to_i, "Should have stored it correctly"
52
+ end
53
+
54
+ it "Should know how to handle weird namespaces" do
55
+ auth = Harpoon::Auth.new({namespace: "a.Z ?what NOW-2"})
56
+ assert_equal "aZwhatNOW-2", auth.namespace, "Should have cleaned up the name"
57
+ end
58
+
59
+ it "Should be able to be given a logger" do
60
+ auth = Harpoon::Auth.new({logger: Logger.new(STDOUT)})
61
+ assert_equal Logger, auth.instance_eval {@logger.class}, "Should have stored the logger"
62
+ end
63
+ end
@@ -0,0 +1,60 @@
1
+ require "helper"
2
+
3
+ describe "Config File" do
4
+
5
+ it "Should raise an error for missing config" do
6
+ assert_raises Harpoon::Errors::InvalidConfigLocation do
7
+ config = Harpoon::Config.read("test_directory/missing.json")
8
+ end
9
+ end
10
+
11
+ it "Should create a config where I tell it to" do
12
+ temp_file = Harpoon::Config.full_path("test/test_directory/test_create", false)
13
+ #does the temp file exist? if so, delete it
14
+ if Harpoon::Config.exists?(temp_file)
15
+ File.delete(temp_file)
16
+ end
17
+
18
+ assert !File.exists?(temp_file), "Should have deleted old config"
19
+
20
+ #create file
21
+ Harpoon::Config.create(temp_file)
22
+
23
+ assert File.exists?(temp_file), "Should have created config file"
24
+ File.delete(temp_file)
25
+ end
26
+
27
+ it "Should know how to parse a config file" do
28
+ config = Harpoon::Config.read("test/test_directory")
29
+ assert_equal "Ryan", config.name, "Should have read the config file"
30
+ assert_equal nil, config.other_value, "Should not have another value"
31
+ end
32
+
33
+ it "Should be able to tell me that a harpoon config exists" do
34
+ assert Harpoon::Config::exists?("test/test_directory")
35
+ end
36
+
37
+ it "Should let me ask for the full path a config file" do
38
+ assert_equal File.join(Dir.pwd, "test", "test_directory", "harpoon.json"), Harpoon::Config.full_path("test/test_directory"), "Should return the full file path of a config file"
39
+ end
40
+
41
+ it "Should be able to be given a logger" do
42
+ config = Harpoon::Config.read("test/test_directory", Logger.new(STDOUT))
43
+ assert_equal Logger, config.instance_eval {@logger.class}, "Should have stored the logger"
44
+ end
45
+
46
+ it "Should expect and sanitize input" do
47
+ skip "Not implemented"
48
+ end
49
+
50
+ it "Should provide a list of files for the services" do
51
+ nested = Harpoon::Config.read("test/test_directory/nested_files")
52
+ unnested = Harpoon::Config.read("test/test_directory/unnested_files")
53
+
54
+ assert_equal 1, nested.files.length, "Should have only found 1 file"
55
+ assert_equal File.join(Dir.pwd, "test", "test_directory", "nested_files", "nested", "directory", "test3.txt"), nested.files.first, "Should have found the correct file"
56
+
57
+ assert_equal 2, unnested.files.length, "Should have found 2 files"
58
+ assert_equal [File.join(Dir.pwd, "test", "test_directory", "unnested_files", "nested", "test2.txt"), File.join(Dir.pwd, "test", "test_directory", "unnested_files", "test.txt")], unnested.files, "Should have found the right files"
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ {
2
+ "name" : "Ryan"
3
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name" : "appName",
3
+ "hosting" : "s3",
4
+ "auth_namespace" : "main",
5
+ "hosting_options" : {
6
+ "region" : "US Standard"
7
+ },
8
+ "domain" : {
9
+ "primary" : "www.domain.com",
10
+ "forwarded" : ["domain.com"]
11
+ },
12
+ "directory" : "nested/directory"
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name" : "test-app",
3
+ "hosting" : "test",
4
+ "auth_namespace" : "test-namespace",
5
+ "hosting_options" : {
6
+ "option" : "value"
7
+ },
8
+ "domain": {
9
+ "primary" : "www.domain.com",
10
+ "forwarded" : ["domain.com"]
11
+ },
12
+ "directory" : ""
13
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name" : "appName",
3
+ "hosting" : "s3",
4
+ "auth_namespace" : "main",
5
+ "hosting_options" : {
6
+ "region" : "US Standard"
7
+ },
8
+ "domain" : {
9
+ "primary" : "www.domain.com",
10
+ "forwarded" : ["domain.com"]
11
+ }
12
+ }
@@ -0,0 +1,14 @@
1
+ require "helper"
2
+
3
+ describe "Test Hosting Module" do
4
+ before do
5
+ @hosting = Harpoon::Services::Test.new
6
+ end
7
+
8
+ it "Should let me make requests" do
9
+ @hosting.deploy "options", "go", "here"
10
+ assert_equal :deploy, @hosting.requests[0][0]
11
+ assert_equal ["options", "go", "here"], @hosting.requests[0][1]
12
+ end
13
+
14
+ end
@@ -0,0 +1,34 @@
1
+ require "helper"
2
+ describe "Runner" do
3
+ before do
4
+ @runner = Harpoon::Runner.new({config: "test/test_directory/test_client"})
5
+ end
6
+
7
+ it "Should load the config from the config option" do
8
+ assert_equal "test-app", @runner.instance_eval {@config.name}, "Should have loaded the config file"
9
+ end
10
+
11
+ it "Should have loaded the service from the config" do
12
+ assert_equal Harpoon::Services::Test, @runner.instance_eval {@service.class}, "Should have set the service from the config file"
13
+ end
14
+
15
+ it "Should load the auth namespace from the service" do
16
+ assert_equal "test-namespace", @runner.instance_eval {@auth.namespace}, "Should have set the namespace correctly"
17
+ end
18
+
19
+ it "Should be passing in the configuration and auth to the service" do
20
+ assert_equal @runner.instance_eval {@auth}, @runner.instance_eval {@service.instance_eval {@auth}}, "Should have gotten the right auth"
21
+ assert_equal @runner.instance_eval {@config}, @runner.instance_eval {@service.instance_eval {@config}}, "Should have gotten the right config"
22
+ end
23
+
24
+ it "Should pass any unknown commands to the service" do
25
+ @runner.deploy
26
+ assert_equal :deploy, @runner.instance_eval {@service.requests[0][0]}, "Should have run deploy on the service"
27
+ end
28
+
29
+ it "Should pass a default logger to everyone" do
30
+ assert_equal Logger, @runner.instance_eval {@service.instance_eval {@logger.class}}, "Should have passed a logger to the service"
31
+ assert_equal Logger, @runner.instance_eval {@auth.instance_eval {@logger.class}}, "Should have passed a logger to the auth"
32
+ assert_equal Logger, @runner.instance_eval {@config.instance_eval {@logger.class}}, "Should have passed a logger to the config"
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ require "helper"
2
+
3
+ describe "Test all services" do
4
+
5
+ it "Should test all services to make sure they have the minimum" do
6
+ #load all the services we know about and iterate over them, making sure they have the required
7
+ # functions
8
+ Harpoon::Services.constants.each do |c|
9
+ next if c.to_s == "Test" #skip the test
10
+ m = Kernel.const_get("Harpoon").const_get("Services").const_get(c).instance_methods
11
+ assert_includes m, :deploy, "Should include a deploy method"
12
+ assert_includes m, :doctor, "Should include a doctor method"
13
+ assert_includes m, :rollback, "Should include a rollback method"
14
+ assert_includes m, :list, "Should include a list method"
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: harpoon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Ryan Quinn
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 0.19.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 0.19.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: netrc
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 0.7.7
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 0.7.7
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 1.51.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 1.51.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: public_suffix
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 1.4.5
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: 1.4.5
69
+ - !ruby/object:Gem::Dependency
70
+ name: colorize
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: 0.7.3
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: 0.7.3
83
+ description: Deploy small server-less webapps to amazon s3, including buckets, dns
84
+ and permissions
85
+ email: ryan@mazondo.com
86
+ executables:
87
+ - harpoon
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - .gitignore
92
+ - LICENSE.md
93
+ - README.md
94
+ - Rakefile
95
+ - bin/harpoon
96
+ - harpoon-0.0.1.gem
97
+ - harpoon.gemspec
98
+ - lib/harpoon.rb
99
+ - lib/harpoon/auth.rb
100
+ - lib/harpoon/client.rb
101
+ - lib/harpoon/config.rb
102
+ - lib/harpoon/errors.rb
103
+ - lib/harpoon/runner.rb
104
+ - lib/harpoon/services/s3.rb
105
+ - lib/harpoon/services/test.rb
106
+ - lib/harpoon/templates/harpoon.json
107
+ - test/helper.rb
108
+ - test/test_auth.rb
109
+ - test/test_config.rb
110
+ - test/test_directory/harpoon.json
111
+ - test/test_directory/nested_files/harpoon.json
112
+ - test/test_directory/nested_files/nested/directory/test3.txt
113
+ - test/test_directory/nested_files/nested/test2.txt
114
+ - test/test_directory/nested_files/test.txt
115
+ - test/test_directory/test_client/harpoon.json
116
+ - test/test_directory/unnested_files/harpoon.json
117
+ - test/test_directory/unnested_files/nested/test2.txt
118
+ - test/test_directory/unnested_files/test.txt
119
+ - test/test_hosting.rb
120
+ - test/test_runner.rb
121
+ - test/test_services.rb
122
+ homepage: http://www.getharpoon.com
123
+ licenses:
124
+ - MIT
125
+ metadata: {}
126
+ post_install_message:
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - '>='
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - '>='
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubyforge_project:
142
+ rubygems_version: 2.1.11
143
+ signing_key:
144
+ specification_version: 4
145
+ summary: A single page app deployer for amazon s3
146
+ test_files: []