buckler 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +66 -0
- data/.ruby-version +1 -0
- data/.travis.yml +8 -0
- data/.yardopts +4 -0
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +2 -0
- data/LICENSE.md +9 -0
- data/README.md +96 -0
- data/Rakefile +16 -0
- data/bin/bucket +18 -0
- data/buckler.gemspec +31 -0
- data/lib/buckler.rb +29 -0
- data/lib/buckler/actions.rb +90 -0
- data/lib/buckler/actions/create_bucket.rb +53 -0
- data/lib/buckler/actions/destroy_bucket.rb +22 -0
- data/lib/buckler/actions/empty_bucket.rb +16 -0
- data/lib/buckler/actions/get_bucket.rb +0 -0
- data/lib/buckler/actions/list_buckets.rb +21 -0
- data/lib/buckler/actions/list_regions.rb +23 -0
- data/lib/buckler/actions/sync_buckets.rb +106 -0
- data/lib/buckler/aws.rb +81 -0
- data/lib/buckler/commands.rb +271 -0
- data/lib/buckler/heroku.rb +41 -0
- data/lib/buckler/logging.rb +19 -0
- data/lib/buckler/monkey_patches.rb +19 -0
- data/lib/buckler/regions.rb +23 -0
- data/lib/buckler/strings.rb +24 -0
- data/lib/buckler/thread_dispatch.rb +48 -0
- data/lib/buckler/version.rb +14 -0
- data/test/buckler_test.rb +80 -0
- data/test/integration/bad_options_test.rb +25 -0
- data/test/integration/create_bucket_test.rb +27 -0
- data/test/integration/destroy_bucket_test.rb +21 -0
- data/test/integration/empty_bucket_test.rb +25 -0
- data/test/integration/list_buckets_test.rb +20 -0
- data/test/integration/list_regions_test.rb +15 -0
- data/test/integration/sync_buckets_test.rb +30 -0
- data/test/run.rb +23 -0
- metadata +192 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
module Buckler
|
2
|
+
|
3
|
+
# Returns the Ruby executable on the user’s $PATH
|
4
|
+
def self.ruby_cmd
|
5
|
+
@ruby_cmd ||= find_executable0("ruby")
|
6
|
+
end
|
7
|
+
|
8
|
+
# Returns the Heroku executable on the user’s $PATH
|
9
|
+
def self.heroku_cmd
|
10
|
+
@heroku_cmd ||= find_executable0("heroku")
|
11
|
+
end
|
12
|
+
|
13
|
+
# True if the user has a Heroku and Ruby executable
|
14
|
+
def self.heroku_available?
|
15
|
+
ruby_cmd.present? && heroku_cmd.present?
|
16
|
+
end
|
17
|
+
|
18
|
+
# Fetches the given environment `variable_name` from the user’s Heroku project
|
19
|
+
def self.heroku_config_get(variable_name)
|
20
|
+
|
21
|
+
command_output, command_intake = IO.pipe
|
22
|
+
|
23
|
+
pid = Kernel.spawn(
|
24
|
+
"#{ruby_cmd} #{heroku_cmd} config:get #{variable_name}",
|
25
|
+
STDOUT => command_intake,
|
26
|
+
STDERR => command_intake
|
27
|
+
)
|
28
|
+
|
29
|
+
command_intake.close
|
30
|
+
|
31
|
+
_, status = Process.wait2(pid)
|
32
|
+
|
33
|
+
if status.exitstatus == 0
|
34
|
+
return command_output.read.to_s
|
35
|
+
else
|
36
|
+
return false
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Buckler::Logging
|
2
|
+
|
3
|
+
def log(message)
|
4
|
+
STDOUT.print("#{message}\n")
|
5
|
+
end
|
6
|
+
|
7
|
+
def alert(message)
|
8
|
+
STDERR.print("#{message.dangerize}\n")
|
9
|
+
end
|
10
|
+
|
11
|
+
def verbose(message)
|
12
|
+
STDERR.print("#{message}\n") if $buckler_verbose_mode
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
module Buckler
|
18
|
+
extend Buckler::Logging
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# When you call MakeMakefile methods, they write information to ./mkmf.conf.
|
2
|
+
# Please don’t do that, MakeMakefile.
|
3
|
+
module MakeMakefile::Logging
|
4
|
+
@logfile = File::NULL
|
5
|
+
end
|
6
|
+
|
7
|
+
# The Aws gem complains on STDOUT when you’re redirected to the right region.
|
8
|
+
# Silence warnings about region redirects, they’re unavoidable with this tool.
|
9
|
+
module Aws
|
10
|
+
module Plugins
|
11
|
+
class S3RequestSigner < Seahorse::Client::Plugin
|
12
|
+
class BucketRegionErrorHandler < Handler
|
13
|
+
def log_warning(*args)
|
14
|
+
# Intentional no-op
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Buckler
|
2
|
+
|
3
|
+
S3_BUCKET_REGIONS = {
|
4
|
+
"us-east-1" => "🇺🇸 US East (Virginia)",
|
5
|
+
"us-west-1" => "🇺🇸 US West (California)",
|
6
|
+
"us-west-2" => "🇺🇸 US West (Oregon)",
|
7
|
+
"sa-east-1" => "🇧🇷 South America (São Paulo)",
|
8
|
+
"eu-central-1" => "🇩🇪 Europe (Frankfurt)",
|
9
|
+
"eu-west-1" => "🇮🇪 Europe (Ireland)",
|
10
|
+
"ap-northeast-1" => "🇯🇵 Asia Pacific (Tokyo)",
|
11
|
+
"ap-northeast-2" => "🇰🇷 Asia Pacific (Seoul)",
|
12
|
+
"ap-south-1" => "🇮🇳 Asia Pacific (Mumbai)",
|
13
|
+
"ap-southeast-1" => "🇸🇬 Asia Pacific (Singapore)",
|
14
|
+
"ap-southeast-2" => "🇦🇺 Asia Pacific (Sydney)",
|
15
|
+
"cn-north-1" => "🇨🇳 China (Beijing)",
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
# True if the given name is a valid AWS region.
|
19
|
+
def self.valid_region?(name)
|
20
|
+
S3_BUCKET_REGIONS.keys.include?(name)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class String
|
2
|
+
|
3
|
+
# Returns a copy of this string with terminal escapes to bold it
|
4
|
+
def bold
|
5
|
+
"\033[1m#{self}\e[0m"
|
6
|
+
end
|
7
|
+
|
8
|
+
# Returns a copy of this string with terminal escapes to make it red
|
9
|
+
def dangerize
|
10
|
+
"\e[38;5;196m#{self}\e[0m"
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns a copy of this string with terminal escapes to make it a color
|
14
|
+
# Options: `:orange` or `:pink`
|
15
|
+
def bucketize(color = :orange)
|
16
|
+
case color
|
17
|
+
when :pink
|
18
|
+
"\e[38;5;206m#{self}\e[0m"
|
19
|
+
else
|
20
|
+
"\e[38;5;208m#{self}\e[0m"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Buckler
|
2
|
+
class ThreadDispatch
|
3
|
+
|
4
|
+
include Buckler::Logging
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@lambda_pool = []
|
8
|
+
@thread_pool = []
|
9
|
+
@max_threads = Etc.nprocessors * 2 # Twice the number of CPU cores available to Ruby
|
10
|
+
end
|
11
|
+
|
12
|
+
def queue(λ)
|
13
|
+
@lambda_pool << λ
|
14
|
+
end
|
15
|
+
|
16
|
+
def any_running_threads?
|
17
|
+
@thread_pool.any?{|s| s.status == "run"}
|
18
|
+
end
|
19
|
+
|
20
|
+
def running_thread_count
|
21
|
+
@thread_pool.select{|s| s.status == "run"}.count
|
22
|
+
end
|
23
|
+
|
24
|
+
def perform_and_wait
|
25
|
+
|
26
|
+
Thread.abort_on_exception = true
|
27
|
+
|
28
|
+
start_time = Time.now
|
29
|
+
|
30
|
+
@lambda_pool.each do |λ|
|
31
|
+
while running_thread_count >= @max_threads
|
32
|
+
verbose "Sleeping due to worker limit. #{running_thread_count} currently running."
|
33
|
+
sleep 0.2
|
34
|
+
end
|
35
|
+
@thread_pool << Thread.new(&λ)
|
36
|
+
end
|
37
|
+
|
38
|
+
verbose "All workers spawned, waiting for workers to finish"
|
39
|
+
while any_running_threads? do
|
40
|
+
sleep 0.2
|
41
|
+
end
|
42
|
+
|
43
|
+
return (Time.now - start_time).round(2)
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Buckler
|
2
|
+
|
3
|
+
# Returns Buckler’s version number
|
4
|
+
def self.version
|
5
|
+
Gem::Version.new("1.0.0")
|
6
|
+
end
|
7
|
+
|
8
|
+
# Contains Buckler’s version number
|
9
|
+
module VERSION
|
10
|
+
MAJOR, MINOR, TINY, PRE = Buckler.version.segments
|
11
|
+
STRING = Buckler.version.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
class BucklerTest < Minitest::Test
|
2
|
+
|
3
|
+
# An S3 client for checking/cleaning the results of actions
|
4
|
+
# that the bucket executable should have performed
|
5
|
+
|
6
|
+
def initialize(*args)
|
7
|
+
@s3 = Aws::S3::Client.new(
|
8
|
+
region: "us-east-1",
|
9
|
+
access_key_id: ENV.fetch("AWS_ACCESS_KEY_ID"),
|
10
|
+
secret_access_key: ENV.fetch("AWS_SECRET_ACCESS_KEY"),
|
11
|
+
)
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
# Create a test bucket and return it
|
16
|
+
|
17
|
+
def create_test_bucket
|
18
|
+
test_name = "buckler-#{SecureRandom.hex(6)}"
|
19
|
+
bucket = Aws::S3::Bucket.new(test_name, client:@s3)
|
20
|
+
bucket.create
|
21
|
+
bucket.wait_until_exists
|
22
|
+
return [bucket, test_name]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Fills a bucket with random, but testable objects
|
26
|
+
|
27
|
+
def load_bucket_with_objects(bucket)
|
28
|
+
50.times do
|
29
|
+
bucket.put_object({
|
30
|
+
acl: "private",
|
31
|
+
body: SecureRandom.hex,
|
32
|
+
key: "#{SecureRandom.uuid}.txt",
|
33
|
+
content_type: "text/plain",
|
34
|
+
cache_control: "no-cache",
|
35
|
+
})
|
36
|
+
end
|
37
|
+
fail unless bucket.objects.to_a.many?
|
38
|
+
return true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Runs the given buckler `command`
|
42
|
+
# Returns two values: the text that the process printed to STDOUT
|
43
|
+
# and the text the process printed to STDERR
|
44
|
+
|
45
|
+
def run_buckler_command(command)
|
46
|
+
|
47
|
+
command_stdout_ouput, command_stdout_intake = IO.pipe
|
48
|
+
command_stderr_ouput, command_stderr_intake = IO.pipe
|
49
|
+
|
50
|
+
environment = {
|
51
|
+
"AWS_ACCESS_KEY_ID" => ENV.fetch("AWS_ACCESS_KEY_ID"),
|
52
|
+
"AWS_SECRET_ACCESS_KEY" => ENV.fetch("AWS_SECRET_ACCESS_KEY"),
|
53
|
+
}
|
54
|
+
|
55
|
+
pid = Kernel.spawn(
|
56
|
+
environment,
|
57
|
+
"#{BUCKLER_EXECUTABLE} #{command}",
|
58
|
+
STDOUT => command_stdout_intake,
|
59
|
+
STDERR => command_stderr_intake,
|
60
|
+
)
|
61
|
+
|
62
|
+
command_stdout_intake.close
|
63
|
+
command_stderr_intake.close
|
64
|
+
|
65
|
+
Process.wait2(pid)
|
66
|
+
|
67
|
+
stdout_results = command_stdout_ouput.read
|
68
|
+
stderr_results = command_stderr_ouput.read
|
69
|
+
|
70
|
+
command_stdout_ouput.close
|
71
|
+
command_stderr_ouput.close
|
72
|
+
|
73
|
+
return [
|
74
|
+
stdout_results,
|
75
|
+
stderr_results
|
76
|
+
]
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class BadOptionsTest < BucklerTest
|
2
|
+
|
3
|
+
def test_bad_credentials
|
4
|
+
|
5
|
+
# Errors about bad credentials
|
6
|
+
stdout, stderr = run_buckler_command("list --id CUTE_CATS --secret BUT_ALSO_DOGGIES")
|
7
|
+
assert stderr.include?("Invalid AWS Access Key"), "Error message should be printed"
|
8
|
+
assert stderr.include?("CUTE_CATS"), "Should repeat Key ID"
|
9
|
+
|
10
|
+
# Errors when the ID is correct but the secret is wrong
|
11
|
+
stdout, stderr = run_buckler_command("list --id #{ENV.fetch("AWS_ACCESS_KEY_ID")} --secret BUT_ALSO_DOGGIES")
|
12
|
+
assert stderr.include?("Invalid AWS Secret Access Key"), "Error mesage should be printed"
|
13
|
+
assert stderr.include?(ENV.fetch("AWS_ACCESS_KEY_ID")), "Should repeat Key ID"
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_invaid_options
|
18
|
+
|
19
|
+
# Passing invalid options should not work
|
20
|
+
stdout, stderr = run_buckler_command("list --fake FAKE --uhh")
|
21
|
+
assert stderr.include?("illegal option"), "Error message should be printed"
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class CreateBucketTest < BucklerTest
|
2
|
+
|
3
|
+
def test_create_bucket
|
4
|
+
|
5
|
+
test_name = "buckler-#{SecureRandom.hex(6)}"
|
6
|
+
bucket = Aws::S3::Bucket.new(test_name, client:@s3)
|
7
|
+
|
8
|
+
# Test that a bucket was indeed created
|
9
|
+
|
10
|
+
stdout, stderr = run_buckler_command("create #{test_name}")
|
11
|
+
assert stdout.include?("available for use"), "“availble for use” text should be printed"
|
12
|
+
assert stdout.include?(test_name), "Bucket name should be repeated to user"
|
13
|
+
assert bucket.exists?, "Bucket should be created"
|
14
|
+
|
15
|
+
# Can’t create a bucket that already exisits
|
16
|
+
|
17
|
+
stdout, stderr = run_buckler_command("create #{test_name}")
|
18
|
+
assert stderr.include?("already exists"), "“already exists” text not printed"
|
19
|
+
assert stderr.include?(test_name), "Bucket name not repeated to user"
|
20
|
+
|
21
|
+
# Cleanup
|
22
|
+
|
23
|
+
bucket.delete! if bucket.exists?
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class DestroyBucketTest < BucklerTest
|
2
|
+
|
3
|
+
def test_destroy_bucket
|
4
|
+
|
5
|
+
bucket, test_name = create_test_bucket
|
6
|
+
|
7
|
+
# Test that a bucket was indeed destroyed
|
8
|
+
|
9
|
+
stdout, stderr = run_buckler_command("destroy #{test_name} --confirm #{test_name}")
|
10
|
+
assert stdout.include?("destroyed"), "Destruction confirmation should be printed"
|
11
|
+
assert stdout.include?(test_name), "Bucket name should be repeated"
|
12
|
+
|
13
|
+
# Can’t destroy a bucket that doesn’t exist
|
14
|
+
|
15
|
+
stdout, stderr = run_buckler_command("destroy #{test_name}")
|
16
|
+
assert stderr.include?("No such bucket"), "Rejection message should be printed"
|
17
|
+
assert stderr.include?(test_name), "Bucket name should be repeated to user"
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class EmptyBucketTest < BucklerTest
|
2
|
+
|
3
|
+
def test_empty_bucket
|
4
|
+
|
5
|
+
# Fill a bucket and empty it
|
6
|
+
|
7
|
+
bucket, test_name = create_test_bucket
|
8
|
+
load_bucket_with_objects(bucket)
|
9
|
+
stdout, stderr = run_buckler_command("empty #{test_name} --confirm #{test_name}")
|
10
|
+
assert bucket.objects.none?, "The bucket should be emptied"
|
11
|
+
|
12
|
+
# Can’t empty a non-existant bucket
|
13
|
+
|
14
|
+
test_name = "buckler-#{SecureRandom.hex(6)}"
|
15
|
+
stdout, stderr = run_buckler_command("empty #{test_name}")
|
16
|
+
assert stderr.include?("No such"), "Error message should be printed"
|
17
|
+
assert stderr.include?(test_name), "Bucket name should be repeated"
|
18
|
+
|
19
|
+
# Cleanup
|
20
|
+
|
21
|
+
bucket.delete! if bucket.exists?
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class ListBucketsTest < BucklerTest
|
2
|
+
|
3
|
+
def test_list_buckets
|
4
|
+
|
5
|
+
bucket, test_name = create_test_bucket
|
6
|
+
|
7
|
+
# Test that a bucket is listed
|
8
|
+
|
9
|
+
stdout, stderr = run_buckler_command("list")
|
10
|
+
assert stdout.include?(test_name), "Bucket name should be repeated"
|
11
|
+
assert stdout.include?("us-east-1"), "Bucket region should be repeated"
|
12
|
+
assert stdout.include?("NAME"), "Header should be printed"
|
13
|
+
|
14
|
+
# Cleanup
|
15
|
+
|
16
|
+
bucket.delete! if bucket.exists?
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class ListRegionsTest < BucklerTest
|
2
|
+
|
3
|
+
def test_list_regions
|
4
|
+
|
5
|
+
stdout, stderr = run_buckler_command("regions")
|
6
|
+
assert stdout.include?("REGION"), "Header should be printed"
|
7
|
+
assert stdout.include?("us-west-1"), "Other rows should be printed"
|
8
|
+
assert stdout.include?("eu-central-1"), "Other rows should be printed"
|
9
|
+
assert stdout.include?("🇰🇷"), "Other rows should be printed"
|
10
|
+
assert stdout.include?("China"), "Other rows should be printed"
|
11
|
+
assert stdout.include?("Tokyo"), "Other rows should be printed"
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class SyncBucketsTest < BucklerTest
|
2
|
+
|
3
|
+
def test_sync_buckets
|
4
|
+
|
5
|
+
# Fill a bucket and sync it
|
6
|
+
|
7
|
+
source_bucket, source_name = create_test_bucket
|
8
|
+
target_bucket, target_name = create_test_bucket
|
9
|
+
load_bucket_with_objects(source_bucket)
|
10
|
+
|
11
|
+
stdout, stderr = run_buckler_command("sync #{source_name} #{target_name} --confirm #{target_name}")
|
12
|
+
assert target_bucket.objects.to_a.many?, "The target bucket should be filled with goodies"
|
13
|
+
assert target_bucket.objects.first.object.content_type.include?("text/plain"), "Content-Type headers should have synced"
|
14
|
+
assert target_bucket.objects.first.object.cache_control.include?("no-cache"), "Cache-Control headers should have synced"
|
15
|
+
|
16
|
+
# Can’t sync bad buckets
|
17
|
+
|
18
|
+
bad_name = "bucker-#{SecureRandom.hex(6)}"
|
19
|
+
stdout, stderr = run_buckler_command("sync #{source_name} #{bad_name} --confirm #{bad_name}")
|
20
|
+
assert stderr.include?("No such bucket"), "“No such” message should be printed"
|
21
|
+
assert stderr.include?(bad_name), "Bucket name should be repeated"
|
22
|
+
|
23
|
+
# Cleanup
|
24
|
+
|
25
|
+
source_bucket.delete! if source_bucket.exists?
|
26
|
+
target_bucket.delete! if target_bucket.exists?
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|