buckler 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|