harpoon 0.0.3

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.
@@ -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: []