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.
@@ -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