├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── bukelatta.gemspec ├── exe └── bukelatta ├── lib ├── bukelatta.rb └── bukelatta │ ├── client.rb │ ├── driver.rb │ ├── dsl.rb │ ├── dsl │ ├── context.rb │ ├── converter.rb │ └── template_helper.rb │ ├── exporter.rb │ ├── ext │ ├── aws_s3_bucket_ext.rb │ ├── hash_ext.rb │ └── string_ext.rb │ ├── logger.rb │ ├── utils.rb │ └── version.rb └── spec ├── bukelatta_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | test.rb 11 | PolicyFile 12 | *.policy 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.4 4 | before_install: gem install bundler -v 1.11.2 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in bukelatta.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 winebarrel 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bukelatta 2 | 3 | Bukelatta is a tool to manage S3 Bucket Policy. 4 | 5 | It defines the state of S3 Bucket Policy using DSL, and updates S3 Bucket Policy according to DSL. 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'bukelatta' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install bukelatta 22 | 23 | ## Usage 24 | 25 | ```sh 26 | export AWS_ACCESS_KEY_ID='...' 27 | export AWS_SECRET_ACCESS_KEY='...' 28 | bukelatta -e -o Policyfile # export Bucket Policy 29 | vi Policyfile 30 | bukelatta -a --dry-run 31 | bukelatta -a # apply `Policyfile` 32 | ``` 33 | 34 | ## Help 35 | 36 | ``` 37 | Usage: bukelatta [options] 38 | -k, --access-key ACCESS_KEY 39 | -s, --secret-key SECRET_KEY 40 | -r, --region REGION 41 | --profile PROFILE 42 | --credentials-path PATH 43 | -a, --apply 44 | -f, --file FILE 45 | --dry-run 46 | -e, --export 47 | -o, --output FILE 48 | --split 49 | --target REGEXP 50 | --no-color 51 | --debug 52 | --request-concurrency N 53 | ``` 54 | 55 | ## Policyfile example 56 | 57 | ```ruby 58 | require 'other/policyfile' 59 | 60 | bucket "foo-bucket" do 61 | {"Version"=>"2012-10-17", 62 | "Id"=>"AWSConsole-AccessLogs-Policy-XXX", 63 | "Statement"=> 64 | [{"Sid"=>"AWSConsoleStmt-XXX", 65 | "Effect"=>"Allow", 66 | "Principal"=>{"AWS"=>"arn:aws:iam::XXX:root"}, 67 | "Action"=>"s3:PutObject", 68 | "Resource"=> 69 | "arn:aws:s3:::foo-bucket/AWSLogs/XXX/*"}]} 70 | end 71 | 72 | bucket "bar-bucket" do 73 | {"Version"=>"2012-10-17", 74 | "Statement"=> 75 | [{"Sid"=>"AddPerm", 76 | "Effect"=>"Allow", 77 | "Principal"=>"*", 78 | "Action"=>"s3:GetObject", 79 | "Resource"=>"arn:aws:s3:::bar-bucket/*"}]} 80 | end 81 | ``` 82 | 83 | ## Similar tools 84 | * [Codenize.tools](http://codenize.tools/) 85 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "bukelatta" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /bukelatta.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'bukelatta/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'bukelatta' 8 | spec.version = Bukelatta::VERSION 9 | spec.authors = ['winebarrel'] 10 | spec.email = ['sgwr_dts@yahoo.co.jp'] 11 | 12 | spec.summary = %q{Bukelatta is a tool to manage S3 Bucket Policy.} 13 | spec.description = %q{Bukelatta is a tool to manage S3 Bucket Policy.} 14 | spec.homepage = 'https://github.com/winebarrel/bukelatta' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = 'exe' 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ['lib'] 21 | 22 | spec.add_dependency 'aws-sdk', '>= 2.3.0' 23 | spec.add_dependency 'diffy' 24 | spec.add_dependency 'hashie' 25 | spec.add_dependency 'multi_xml' 26 | spec.add_dependency 'parallel' 27 | spec.add_dependency 'term-ansicolor' 28 | 29 | spec.add_development_dependency 'bundler' 30 | spec.add_development_dependency 'rake' 31 | spec.add_development_dependency 'rspec', '~> 3.0' 32 | end 33 | -------------------------------------------------------------------------------- /exe/bukelatta: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.expand_path('../../lib', __FILE__) 3 | 4 | require 'bukelatta' 5 | require 'optparse' 6 | 7 | Version = Bukelatta::VERSION 8 | 9 | OLD_DEFAULT_FILENAME = 'PolicyFile' 10 | DEFAULT_FILENAME = 'Policyfile' 11 | 12 | MAGIC_COMMENT = <<-EOS 13 | # -*- mode: ruby -*- 14 | # vi: set ft=ruby : 15 | EOS 16 | 17 | def parse_options(argv) 18 | options = { 19 | file: DEFAULT_FILENAME, 20 | output: '-', 21 | dry_run: false, 22 | color: true, 23 | request_concurrency: 8, 24 | aws: { ssl_verify_peer: false }, 25 | } 26 | 27 | opt = OptionParser.new 28 | opt.on('-k', '--access-key ACCESS_KEY') {|v| options[:aws][:access_key_id] = v } 29 | opt.on('-s', '--secret-key SECRET_KEY') {|v| options[:aws][:secret_access_key] = v } 30 | opt.on('-r', '--region REGION') {|v| options[:aws][:region] = v } 31 | 32 | opt.on('', '--profile PROFILE') do |v| 33 | options[:aws][:credentials] ||= {} 34 | options[:aws][:credentials][:profile_name] = v 35 | end 36 | 37 | opt.on('', '--credentials-path PATH') do |v| 38 | options[:aws][:credentials] ||= {} 39 | options[:aws][:credentials][:path] = v 40 | end 41 | 42 | opt.on('-a', '--apply') { options[:mode] = :apply } 43 | opt.on('-f', '--file FILE') {|v| options[:file] = v } 44 | opt.on('' , '--dry-run') { options[:dry_run] = true } 45 | opt.on('-e', '--export') { options[:mode] = :export } 46 | opt.on('-o', '--output FILE') {|v| options[:output] = v } 47 | opt.on('' , '--split') { options[:split] = :true } 48 | opt.on('' , '--target REGEXP') {|v| options[:target] = Regexp.new(v) } 49 | opt.on('' , '--no-color') { options[:color] = false } 50 | opt.on('' , '--debug') { options[:debug] = true } 51 | 52 | opt.on('' , '--request-concurrency N', Integer) do |v| 53 | options[:request_concurrency] = v 54 | end 55 | 56 | opt.parse!(argv) 57 | 58 | unless options[:mode] 59 | puts opt.help 60 | exit 1 61 | end 62 | 63 | if options[:aws][:credentials] 64 | credentials = Aws::SharedCredentials.new(options[:aws][:credentials]) 65 | options[:aws][:credentials] = credentials 66 | end 67 | 68 | Aws.config.update(options[:aws]) 69 | String.colorize = options[:color] 70 | 71 | if options[:debug] 72 | Bukelatta::Logger.instance.set_debug(options[:debug]) 73 | 74 | Aws.config.update( 75 | :http_wire_trace => true, 76 | :logger => Bukelatta::Logger.instance 77 | ) 78 | end 79 | 80 | options 81 | rescue => e 82 | $stderr.puts("[ERROR] #{e.message}") 83 | exit 1 84 | end 85 | 86 | def main(argv) 87 | options = parse_options(argv) 88 | client = Bukelatta::Client.new(options) 89 | logger = Bukelatta::Logger.instance 90 | 91 | case options[:mode] 92 | when :export 93 | exported = client.export 94 | output = options[:output] 95 | 96 | if options[:split] 97 | logger.info('Export Bucket Policy') 98 | 99 | output = DEFAULT_FILENAME if output == '-' 100 | dir = File.dirname(output) 101 | FileUtils.mkdir_p(dir) 102 | requires = [] 103 | 104 | exported.each do |bucket_name, policy| 105 | next unless policy 106 | 107 | filename = "#{bucket_name}.policy" 108 | requires << filename 109 | policy_file = File.join(dir, filename) 110 | 111 | logger.info(" write `#{policy_file}`") 112 | 113 | dsl = Bukelatta::DSL.convert({bucket_name => policy}, options) 114 | 115 | open(policy_file, 'wb') do |f| 116 | f.puts MAGIC_COMMENT 117 | f.puts dsl 118 | end 119 | end 120 | 121 | logger.info(" write `#{output}`") 122 | 123 | open(output, 'wb') do |f| 124 | f.puts MAGIC_COMMENT 125 | 126 | requires.each do |policy_file| 127 | f.puts "require '#{policy_file}'" 128 | end 129 | end 130 | else 131 | dsl = Bukelatta::DSL.convert(exported, options) 132 | 133 | if output == '-' 134 | logger.info('# Export Bucket Policy') 135 | puts dsl 136 | else 137 | logger.info("Export Bucket Policy to `#{output}`") 138 | open(output, 'wb') do |f| 139 | f.puts MAGIC_COMMENT 140 | f.puts dsl 141 | end 142 | end 143 | end 144 | when :apply 145 | file = options[:file] 146 | 147 | # TODO: Remove OLD_DEFAULT_FILENAME support 148 | if File.exist?(file) 149 | if file == DEFAULT_FILENAME and Dir.glob(DEFAULT_FILENAME).first == OLD_DEFAULT_FILENAME 150 | file = OLD_DEFAULT_FILENAME 151 | logger.warn(%!"#{OLD_DEFAULT_FILENAME}" is deprecated. Please use "#{DEFAULT_FILENAME}"!.yellow) 152 | end 153 | else 154 | if file == DEFAULT_FILENAME and File.exist?(OLD_DEFAULT_FILENAME) 155 | file = OLD_DEFAULT_FILENAME 156 | logger.warn(%!"#{OLD_DEFAULT_FILENAME}" is deprecated. Please use "#{DEFAULT_FILENAME}"!.yellow) 157 | else 158 | raise "No PolicyFile found (looking for: #{file})" 159 | end 160 | end 161 | 162 | message = "Apply `#{file}` to Bucket Policy" 163 | message << ' (dry-run)' if options[:dry_run] 164 | logger.info(message) 165 | 166 | updated = client.apply(file) 167 | 168 | logger.info('No change'.intense_blue) unless updated 169 | else 170 | raise "Unknown mode: #{options[:mode]}" 171 | end 172 | rescue => e 173 | if options[:debug] 174 | raise e 175 | else 176 | $stderr.puts("[ERROR] #{e.message}".red) 177 | exit 1 178 | end 179 | end 180 | 181 | main(ARGV) 182 | -------------------------------------------------------------------------------- /lib/bukelatta.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk' 2 | require 'diffy' 3 | require 'hashie' 4 | require 'json' 5 | require 'logger' 6 | require 'multi_xml' 7 | require 'parallel' 8 | require 'pp' 9 | require 'singleton' 10 | require 'term/ansicolor' 11 | 12 | require 'bukelatta/version' 13 | require 'bukelatta/ext/aws_s3_bucket_ext' 14 | require 'bukelatta/ext/hash_ext' 15 | require 'bukelatta/ext/string_ext' 16 | require 'bukelatta/logger' 17 | require 'bukelatta/utils' 18 | 19 | require 'bukelatta/client' 20 | require 'bukelatta/driver' 21 | require 'bukelatta/dsl' 22 | require 'bukelatta/dsl/template_helper' 23 | require 'bukelatta/dsl/context' 24 | require 'bukelatta/dsl/converter' 25 | require 'bukelatta/exporter' 26 | -------------------------------------------------------------------------------- /lib/bukelatta/client.rb: -------------------------------------------------------------------------------- 1 | class Bukelatta::Client 2 | include Bukelatta::Utils::Helper 3 | include Bukelatta::Logger::Helper 4 | 5 | def initialize(options = {}) 6 | @options = options 7 | @client = @options[:client] || Aws::S3::Client.new 8 | @resource = Aws::S3::Resource.new(client: @client) 9 | @driver = Bukelatta::Driver.new(@client, @options) 10 | @exporter = Bukelatta::Exporter.new(@client, @options) 11 | end 12 | 13 | def export 14 | @exporter.export 15 | end 16 | 17 | def apply(file) 18 | walk(file) 19 | end 20 | 21 | private 22 | 23 | def walk(file) 24 | expected = load_file(file) 25 | actual = @exporter.export 26 | 27 | updated = walk_buckets(expected, actual) 28 | 29 | if @options[:dry_run] 30 | false 31 | else 32 | updated 33 | end 34 | end 35 | 36 | def walk_buckets(expected, actual) 37 | updated = false 38 | 39 | expected.each do |bucket_name, expected_policy| 40 | next unless matched?(bucket_name) 41 | 42 | actual_policy = actual[bucket_name] 43 | 44 | if actual_policy 45 | updated = walk_policy(bucket_name, expected_policy, actual_policy) || updated 46 | elsif actual.has_key?(bucket_name) 47 | actual.delete(bucket_name) 48 | 49 | if expected_policy 50 | @driver.create_policy(bucket_name, expected_policy) 51 | updated = true 52 | end 53 | else 54 | log(:warn, "No such bucket: #{bucket_name}") 55 | end 56 | end 57 | 58 | updated 59 | end 60 | 61 | def walk_policy(bucket_name, expected_policy, actual_policy) 62 | updated = false 63 | 64 | if expected_policy 65 | if expected_policy != actual_policy 66 | @driver.update_policy(bucket_name, expected_policy, actual_policy) 67 | updated = true 68 | end 69 | else 70 | @driver.delete_policy(bucket_name) 71 | updated = true 72 | end 73 | 74 | updated 75 | end 76 | 77 | def load_file(file) 78 | if file.kind_of?(String) 79 | open(file) do |f| 80 | Bukelatta::DSL.parse(f.read, file) 81 | end 82 | elsif file.respond_to?(:read) 83 | Bukelatta::DSL.parse(file.read, file.path) 84 | else 85 | raise TypeError, "can't convert #{file} into File" 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/bukelatta/driver.rb: -------------------------------------------------------------------------------- 1 | class Bukelatta::Driver 2 | include Bukelatta::Utils::Helper 3 | include Bukelatta::Logger::Helper 4 | 5 | def initialize(client, options = {}) 6 | @client = client 7 | @resource = Aws::S3::Resource.new(client: @client) 8 | @options = options 9 | end 10 | 11 | def create_policy(bucket_name, policy) 12 | log(:info, "Create Bucket `#{bucket_name}` Policy", color: :cyan) 13 | 14 | unless @options[:dry_run] 15 | bucket = @resource.bucket(bucket_name) 16 | 17 | bucket.auto_redirect do |b| 18 | b.policy.put(policy: JSON.dump(policy)) 19 | end 20 | end 21 | end 22 | 23 | def delete_policy(bucket_name) 24 | log(:info, "Delete Bucket `#{bucket_name}` Policy", color: :red) 25 | 26 | unless @options[:dry_run] 27 | bucket = @resource.bucket(bucket_name) 28 | 29 | bucket.auto_redirect do |b| 30 | b.policy.delete 31 | end 32 | end 33 | end 34 | 35 | def update_policy(bucket_name, policy, old_policy) 36 | log(:info, "Update Bucket `#{bucket_name}` Policy", color: :green) 37 | log(:info, diff(old_policy, policy, color: @options[:color]), color: false) 38 | 39 | unless @options[:dry_run] 40 | bucket = @resource.bucket(bucket_name) 41 | 42 | bucket.auto_redirect do |b| 43 | b.policy.put(policy: JSON.dump(policy)) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/bukelatta/dsl.rb: -------------------------------------------------------------------------------- 1 | class Bukelatta::DSL 2 | class << self 3 | def convert(exported, options = {}) 4 | Bukelatta::DSL::Converter.convert(exported, options) 5 | end 6 | 7 | def parse(dsl, path, options = {}) 8 | Bukelatta::DSL::Context.eval(dsl, path, options).result 9 | end 10 | end # of class methods 11 | end 12 | -------------------------------------------------------------------------------- /lib/bukelatta/dsl/context.rb: -------------------------------------------------------------------------------- 1 | class Bukelatta::DSL::Context 2 | include Bukelatta::DSL::TemplateHelper 3 | 4 | def self.eval(dsl, path, options = {}) 5 | self.new(path, options) { 6 | eval(dsl, binding, path) 7 | } 8 | end 9 | 10 | def result 11 | expand_leaf_array! 12 | @result.sort_array! 13 | end 14 | 15 | def initialize(path, options = {}, &block) 16 | @path = path 17 | @options = options 18 | @result = {} 19 | 20 | @context = Hashie::Mash.new( 21 | :path => path, 22 | :options => options, 23 | :templates => {} 24 | ) 25 | 26 | instance_eval(&block) 27 | end 28 | 29 | def template(name, &block) 30 | @context.templates[name.to_s] = block 31 | end 32 | 33 | private 34 | 35 | def require(file) 36 | policyfile = (file =~ %r|\A/|) ? file : File.expand_path(File.join(File.dirname(@path), file)) 37 | 38 | if File.exist?(policyfile) 39 | instance_eval(File.read(policyfile), policyfile) 40 | elsif File.exist?(policyfile + '.rb') 41 | instance_eval(File.read(policyfile + '.rb'), policyfile + '.rb') 42 | else 43 | Kernel.require(file) 44 | end 45 | end 46 | 47 | def bucket(name) 48 | name = name.to_s 49 | 50 | if @result[name] 51 | raise "Bucket `#{name}` is already defined" 52 | end 53 | 54 | @result[name] = yield 55 | end 56 | 57 | def expand_leaf_array! 58 | @result = expand_leaf_array(@result) 59 | end 60 | 61 | def expand_leaf_array(obj) 62 | case obj 63 | when Array 64 | if obj[0].instance_of?(Array) || obj[0].instance_of?(Hash) 65 | return obj.map do |o| 66 | expand_leaf_array(o) 67 | end 68 | end 69 | # Leaf 70 | if obj.length == 1 71 | return obj[0] 72 | end 73 | return obj 74 | when Hash 75 | h = {} 76 | obj.each do |k, v| 77 | h[k] = expand_leaf_array(v) 78 | end 79 | return h 80 | end 81 | return obj 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/bukelatta/dsl/converter.rb: -------------------------------------------------------------------------------- 1 | class Bukelatta::DSL::Converter 2 | include Bukelatta::Utils::Helper 3 | 4 | def self.convert(exported, options = {}) 5 | self.new(exported, options).convert 6 | end 7 | 8 | def initialize(exported, options = {}) 9 | @exported = exported 10 | @options = options 11 | end 12 | 13 | def convert 14 | output_buckets(@exported) 15 | end 16 | 17 | private 18 | 19 | def output_buckets(policy_by_bucket) 20 | buckets = [] 21 | 22 | policy_by_bucket.sort_by(&:first).each do |bucket_name, policy| 23 | if not policy or not matched?(bucket_name) 24 | next 25 | end 26 | 27 | buckets << output_bucket(bucket_name, policy) 28 | end 29 | 30 | buckets.join("\n") 31 | end 32 | 33 | def output_bucket(bucket_name, policy) 34 | policy = policy.pretty_inspect.gsub(/^/, ' ').strip 35 | 36 | <<-EOS 37 | bucket #{bucket_name.inspect} do 38 | #{policy} 39 | end 40 | EOS 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/bukelatta/dsl/template_helper.rb: -------------------------------------------------------------------------------- 1 | module Bukelatta::DSL::TemplateHelper 2 | def include_template(template_name, context = {}) 3 | tmplt = @context.templates[template_name.to_s] 4 | 5 | unless tmplt 6 | raise "Template `#{template_name}` is not defined" 7 | end 8 | 9 | context_orig = @context 10 | @context = @context.merge(context) 11 | instance_eval(&tmplt) 12 | @context = context_orig 13 | end 14 | 15 | def context 16 | @context 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/bukelatta/exporter.rb: -------------------------------------------------------------------------------- 1 | class Bukelatta::Exporter 2 | include Bukelatta::Utils::Helper 3 | 4 | def initialize(client, options = {}) 5 | @client = client 6 | @options = options 7 | @resource = Aws::S3::Resource.new(client: @client) 8 | end 9 | 10 | def export 11 | export_buckets 12 | end 13 | 14 | private 15 | 16 | def export_buckets 17 | result = {} 18 | buckets = @resource.buckets 19 | concurrency = @options[:request_concurrency] 20 | 21 | Parallel.each(buckets, in_threads: concurrency) do |bucket| 22 | next unless matched?(bucket.name) 23 | 24 | policy = export_bucket_policy(bucket) 25 | result[bucket.name] = policy 26 | end 27 | 28 | result.sort_array! 29 | end 30 | 31 | def export_bucket_policy(bucket) 32 | bucket.auto_redirect do |b| 33 | JSON.parse(b.policy.policy.string) 34 | end 35 | rescue Aws::S3::Errors::NoSuchBucketPolicy 36 | nil 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/bukelatta/ext/aws_s3_bucket_ext.rb: -------------------------------------------------------------------------------- 1 | module Bukelatta::Ext 2 | module AwsS3BucketExt 3 | def auto_redirect 4 | ret = nil 5 | bckt = self 6 | 7 | begin 8 | ret = yield(bckt) 9 | rescue Aws::S3::Errors::PermanentRedirect => e 10 | res_body = MultiXml.parse(e.context.http_response.body.read) 11 | edpnt = res_body['Error']['Endpoint'] 12 | clnt = Aws::S3::Client.new(endpoint: "https://#{edpnt}") 13 | rsrc = Aws::S3::Resource.new(client: clnt) 14 | bckt = rsrc.bucket(bckt.name) 15 | retry 16 | end 17 | 18 | ret 19 | end 20 | end 21 | end 22 | 23 | Aws::S3::Bucket.include(Bukelatta::Ext::AwsS3BucketExt) 24 | -------------------------------------------------------------------------------- /lib/bukelatta/ext/hash_ext.rb: -------------------------------------------------------------------------------- 1 | module Bukelatta::Ext 2 | module HashExt 3 | def sort_array! 4 | keys.each do |key| 5 | value = self[key] 6 | self[key] = sort_array0(value) 7 | end 8 | 9 | self 10 | end 11 | 12 | private 13 | 14 | def sort_array0(value) 15 | case value 16 | when Hash 17 | new_value = {} 18 | 19 | value.each do |k, v| 20 | new_value[k] = sort_array0(v) 21 | end 22 | 23 | new_value 24 | when Array 25 | value.map {|v| sort_array0(v) }.sort_by(&:to_s) 26 | else 27 | value 28 | end 29 | end 30 | end 31 | end 32 | 33 | Hash.include(Bukelatta::Ext::HashExt) 34 | -------------------------------------------------------------------------------- /lib/bukelatta/ext/string_ext.rb: -------------------------------------------------------------------------------- 1 | module Bukelatta::Ext 2 | module StringExt 3 | module ClassMethods 4 | def colorize=(value) 5 | @colorize = value 6 | end 7 | 8 | def colorize 9 | @colorize 10 | end 11 | end # ClassMethods 12 | 13 | Term::ANSIColor::Attribute.named_attributes.each do |attribute| 14 | class_eval(<<-EOS, __FILE__, __LINE__ + 1) 15 | def #{attribute.name} 16 | if String.colorize 17 | Term::ANSIColor.send(#{attribute.name.inspect}, self) 18 | else 19 | self 20 | end 21 | end 22 | EOS 23 | end 24 | end 25 | end 26 | 27 | String.include(Bukelatta::Ext::StringExt) 28 | String.extend(Bukelatta::Ext::StringExt::ClassMethods) 29 | -------------------------------------------------------------------------------- /lib/bukelatta/logger.rb: -------------------------------------------------------------------------------- 1 | class Bukelatta::Logger < ::Logger 2 | include Singleton 3 | 4 | def initialize 5 | super($stdout) 6 | 7 | self.formatter = proc do |severity, datetime, progname, msg| 8 | "#{msg}\n" 9 | end 10 | 11 | self.level = Logger::INFO 12 | end 13 | 14 | def set_debug(value) 15 | self.level = value ? Logger::DEBUG : Logger::INFO 16 | end 17 | 18 | module Helper 19 | def log(level, message, log_options = {}) 20 | global_option = @options || {} 21 | message = "[#{level.to_s.upcase}] #{message}" unless level == :info 22 | message << ' (dry-run)' if global_option[:dry_run] 23 | message = message.send(log_options[:color]) if log_options[:color] 24 | logger = global_option[:logger] || Bukelatta::Logger.instance 25 | logger.send(level, message) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/bukelatta/utils.rb: -------------------------------------------------------------------------------- 1 | class Bukelatta::Utils 2 | module Helper 3 | def matched?(name) 4 | if @options[:target] 5 | @options[:target] =~ name 6 | else 7 | true 8 | end 9 | end 10 | 11 | def diff(obj1, obj2, options = {}) 12 | diffy = Diffy::Diff.new( 13 | obj1.pretty_inspect, 14 | obj2.pretty_inspect, 15 | :diff => '-u' 16 | ) 17 | 18 | out = diffy.to_s(options[:color] ? :color : :text).gsub(/\s+\z/m, '') 19 | out.gsub!(/^/, options[:indent]) if options[:indent] 20 | out 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/bukelatta/version.rb: -------------------------------------------------------------------------------- 1 | module Bukelatta 2 | VERSION = '0.1.8' 3 | end 4 | -------------------------------------------------------------------------------- /spec/bukelatta_spec.rb: -------------------------------------------------------------------------------- 1 | describe Bukelatta do 2 | it 'has a version number' do 3 | expect(Bukelatta::VERSION).not_to be nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'bukelatta' 3 | --------------------------------------------------------------------------------