awshark 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 +10 -0
- data/.rspec +4 -0
- data/.rubocop.yml +130 -0
- data/CHANGELOG.md +4 -0
- data/README.md +62 -0
- data/awshark.gemspec +47 -0
- data/exe/awshark +7 -0
- data/gems.rb +6 -0
- data/lib/awshark.rb +35 -0
- data/lib/awshark/cli.rb +45 -0
- data/lib/awshark/cloud_formation/file_loading.rb +23 -0
- data/lib/awshark/cloud_formation/inferrer.rb +24 -0
- data/lib/awshark/cloud_formation/manager.rb +93 -0
- data/lib/awshark/cloud_formation/parameters.rb +43 -0
- data/lib/awshark/cloud_formation/stack.rb +94 -0
- data/lib/awshark/cloud_formation/stack_events.rb +46 -0
- data/lib/awshark/cloud_formation/template.rb +102 -0
- data/lib/awshark/configuration.rb +34 -0
- data/lib/awshark/ec2/instance.rb +46 -0
- data/lib/awshark/ec2/manager.rb +40 -0
- data/lib/awshark/profile_resolver.rb +54 -0
- data/lib/awshark/rds/check_reservations.rb +59 -0
- data/lib/awshark/rds/manager.rb +62 -0
- data/lib/awshark/s3/artifact.rb +38 -0
- data/lib/awshark/s3/bucket.rb +59 -0
- data/lib/awshark/s3/manager.rb +64 -0
- data/lib/awshark/subcommands/class_options.rb +28 -0
- data/lib/awshark/subcommands/cloud_formation.rb +81 -0
- data/lib/awshark/subcommands/ec2.rb +38 -0
- data/lib/awshark/subcommands/ecs.rb +38 -0
- data/lib/awshark/subcommands/rds.rb +88 -0
- data/lib/awshark/subcommands/s3.rb +93 -0
- data/lib/awshark/version.rb +5 -0
- metadata +277 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awshark
|
4
|
+
module Rds
|
5
|
+
class CheckReservations
|
6
|
+
attr_reader :instances, :reservations
|
7
|
+
|
8
|
+
def initialize(instances:, reservations:)
|
9
|
+
@instances = instances
|
10
|
+
@reservations = reservations
|
11
|
+
end
|
12
|
+
|
13
|
+
def type_permutations
|
14
|
+
type_permutations_hash = {}
|
15
|
+
|
16
|
+
(reservations + instances).each do |instance|
|
17
|
+
key = "#{instance.type}+#{instance.multi_az}"
|
18
|
+
type_permutations_hash[key] = {
|
19
|
+
type: instance.type,
|
20
|
+
multi_az: instance.multi_az
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
type_permutations_hash.values
|
25
|
+
end
|
26
|
+
|
27
|
+
def check
|
28
|
+
type_permutations.map do |permutation|
|
29
|
+
type = permutation[:type]
|
30
|
+
multi_az = permutation[:multi_az]
|
31
|
+
|
32
|
+
instance_count = count_instances(type, multi_az)
|
33
|
+
reserved_count = count_reservations(type, multi_az)
|
34
|
+
|
35
|
+
OpenStruct.new(
|
36
|
+
type: type,
|
37
|
+
multi_az: multi_az,
|
38
|
+
instance_count: instance_count,
|
39
|
+
reserved_count: reserved_count
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def count_instances(type, multi_az)
|
45
|
+
instances.select do |i|
|
46
|
+
i.type == type && i.multi_az == multi_az
|
47
|
+
end.size
|
48
|
+
end
|
49
|
+
|
50
|
+
def count_reservations(type, multi_az)
|
51
|
+
reservation = reservations.detect do |r|
|
52
|
+
r.type == type && r.multi_az == multi_az
|
53
|
+
end
|
54
|
+
|
55
|
+
reservation ? reservation.count : 0
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# TODO: also work for DB clusters
|
4
|
+
|
5
|
+
module Awshark
|
6
|
+
module Rds
|
7
|
+
class Manager
|
8
|
+
def instances
|
9
|
+
return @instances if defined?(@instances)
|
10
|
+
|
11
|
+
@instances = []
|
12
|
+
response = client.describe_db_instances
|
13
|
+
|
14
|
+
response[:db_instances].each do |instance|
|
15
|
+
@instances << OpenStruct.new(
|
16
|
+
name: instance[:db_instance_identifier],
|
17
|
+
type: instance[:db_instance_class],
|
18
|
+
state: instance[:db_instance_status],
|
19
|
+
multi_az: instance[:multi_az],
|
20
|
+
engine: instance[:engine],
|
21
|
+
engine_version: instance[:engine_version],
|
22
|
+
encrypted: instance[:storage_encrypted],
|
23
|
+
storage_type: instance[:storage_type]
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
@instances
|
28
|
+
end
|
29
|
+
|
30
|
+
def reservations
|
31
|
+
return @reservations if defined?(@reservations)
|
32
|
+
|
33
|
+
response = client.describe_reserved_db_instances
|
34
|
+
|
35
|
+
@reservations = response[:reserved_db_instances].map do |instance|
|
36
|
+
OpenStruct.new(
|
37
|
+
count: instance[:db_instance_count],
|
38
|
+
type: instance[:db_instance_class],
|
39
|
+
multi_az: instance[:multi_az],
|
40
|
+
state: instance[:state],
|
41
|
+
offering_type: instance[:offering_type]
|
42
|
+
)
|
43
|
+
end
|
44
|
+
@reservations.select! { |ri| ri[:state] == 'active' }
|
45
|
+
|
46
|
+
@reservations
|
47
|
+
end
|
48
|
+
|
49
|
+
def check_reservations
|
50
|
+
CheckReservations.new(instances: instances, reservations: reservations).check
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def client
|
56
|
+
@client ||= Aws::RDS::Client.new(
|
57
|
+
region: Aws.config[:region] || 'eu-central-1'
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awshark
|
4
|
+
module S3
|
5
|
+
class Artifact
|
6
|
+
attr_reader :key
|
7
|
+
|
8
|
+
def initialize(key)
|
9
|
+
@key = key
|
10
|
+
end
|
11
|
+
|
12
|
+
def cache_control
|
13
|
+
return "public, max-age=#{1.year.to_i}, s-maxage=#{1.year.to_i}" if fingerprint?
|
14
|
+
|
15
|
+
'public, max-age=0, s-maxage=120, must-revalidate'
|
16
|
+
end
|
17
|
+
|
18
|
+
def fingerprint?
|
19
|
+
return false if key.blank?
|
20
|
+
|
21
|
+
basename.match(/\.([0-9a-f]{20}|[0-9a-f]{32})\./).present?
|
22
|
+
end
|
23
|
+
|
24
|
+
def content_type
|
25
|
+
mime = MiniMime.lookup_by_filename(basename)
|
26
|
+
if mime
|
27
|
+
mime.content_type
|
28
|
+
else
|
29
|
+
'application/octet-stream'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def basename
|
34
|
+
::File.basename(key)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awshark
|
4
|
+
module S3
|
5
|
+
class Bucket
|
6
|
+
attr_reader :name, :creation_date, :region
|
7
|
+
|
8
|
+
def initialize(attributes)
|
9
|
+
@name = attributes.name
|
10
|
+
@creation_date = attributes.creation_date
|
11
|
+
@region = attributes.region
|
12
|
+
|
13
|
+
# fixes S3 quirks
|
14
|
+
@region = 'eu-west-1' if @region == 'EU'
|
15
|
+
end
|
16
|
+
|
17
|
+
def byte_size
|
18
|
+
metric_value(metric_name: 'BucketSizeBytes', storage_type: 'StandardStorage')
|
19
|
+
end
|
20
|
+
|
21
|
+
def number_of_objects
|
22
|
+
metric_value(metric_name: 'NumberOfObjects', storage_type: 'AllStorageTypes')
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def cloudwatch
|
28
|
+
@cloudwatch ||= Aws::CloudWatch::Client.new(region: region)
|
29
|
+
end
|
30
|
+
|
31
|
+
def metric_value(metric_name:, storage_type:)
|
32
|
+
return 0 unless region.present?
|
33
|
+
|
34
|
+
response = cloudwatch.get_metric_statistics(
|
35
|
+
namespace: 'AWS/S3',
|
36
|
+
metric_name: metric_name,
|
37
|
+
dimensions: [
|
38
|
+
{
|
39
|
+
name: 'BucketName',
|
40
|
+
value: name
|
41
|
+
},
|
42
|
+
{
|
43
|
+
name: 'StorageType',
|
44
|
+
value: storage_type
|
45
|
+
}
|
46
|
+
],
|
47
|
+
start_time: Time.now - 7.days,
|
48
|
+
end_time: Time.now,
|
49
|
+
period: 86_400,
|
50
|
+
statistics: ['Average']
|
51
|
+
)
|
52
|
+
return 0 if response.datapoints.empty?
|
53
|
+
|
54
|
+
sorted_datapoints = response.datapoints.sort_by(&:timestamp)
|
55
|
+
sorted_datapoints.last.average
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awshark
|
4
|
+
module S3
|
5
|
+
class Manager
|
6
|
+
def list_buckets
|
7
|
+
response = client.list_buckets
|
8
|
+
response.buckets.map do |bucket|
|
9
|
+
attributes = OpenStruct.new(bucket.to_hash)
|
10
|
+
location = client.get_bucket_location(bucket: bucket.name)
|
11
|
+
attributes.region = location.location_constraint
|
12
|
+
Awshark::S3::Bucket.new(attributes)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def list_objects(bucket:, prefix: nil)
|
17
|
+
objects = []
|
18
|
+
response = client.list_objects_v2(bucket: bucket, prefix: prefix)
|
19
|
+
objects.concat(response.contents)
|
20
|
+
|
21
|
+
while response.next_continuation_token
|
22
|
+
response = client.list_objects_v2(
|
23
|
+
bucket: bucket,
|
24
|
+
prefix: prefix,
|
25
|
+
continuation_token: response.next_continuation_token
|
26
|
+
)
|
27
|
+
objects.concat(response.contents)
|
28
|
+
end
|
29
|
+
|
30
|
+
objects.select { |o| o.size.positive? }
|
31
|
+
end
|
32
|
+
|
33
|
+
def update_object_metadata(bucket, key, options = {})
|
34
|
+
raise ArgumentError, 'meta=acl:STRING is missing' if options[:acl].blank?
|
35
|
+
|
36
|
+
object = client.get_object(bucket: bucket, key: key)
|
37
|
+
metadata = object.metadata.merge(options[:metadata] || {})
|
38
|
+
artifact = Artifact.new(key)
|
39
|
+
|
40
|
+
# copy object in place to update metadata
|
41
|
+
client.copy_object(
|
42
|
+
acl: options[:acl] || 'private',
|
43
|
+
bucket: bucket,
|
44
|
+
copy_source: "/#{bucket}/#{key}",
|
45
|
+
key: key,
|
46
|
+
cache_control: options[:cache_control] || object.cache_control,
|
47
|
+
content_type: artifact.content_type,
|
48
|
+
metadata: metadata.stringify_keys,
|
49
|
+
metadata_directive: 'REPLACE',
|
50
|
+
server_side_encryption: 'AES256'
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def client
|
57
|
+
@client ||= Aws::S3::Client.new(
|
58
|
+
region: Aws.config[:region] || 'eu-central-1',
|
59
|
+
signature_version: 'v4'
|
60
|
+
)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awshark
|
4
|
+
module Subcommands
|
5
|
+
module ClassOptions
|
6
|
+
def process_class_options
|
7
|
+
command = current_command_chain.last
|
8
|
+
cli_options = options.merge(parent_options || {}).symbolize_keys
|
9
|
+
|
10
|
+
if cli_options[:help]
|
11
|
+
respond_to?(command) ? help(command) : help
|
12
|
+
exit(0)
|
13
|
+
end
|
14
|
+
|
15
|
+
setup_aws_credentials(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def setup_aws_credentials(options)
|
21
|
+
profile_resolver = ProfileResolver.new(options)
|
22
|
+
|
23
|
+
::Aws.config[:region] = profile_resolver.region
|
24
|
+
::Aws.config[:credentials] = profile_resolver.credentials
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aws-sdk-cloudformation'
|
4
|
+
require 'diffy'
|
5
|
+
require 'recursive-open-struct'
|
6
|
+
|
7
|
+
require 'awshark/cloud_formation/file_loading'
|
8
|
+
require 'awshark/cloud_formation/inferrer'
|
9
|
+
require 'awshark/cloud_formation/manager'
|
10
|
+
require 'awshark/cloud_formation/parameters'
|
11
|
+
require 'awshark/cloud_formation/stack'
|
12
|
+
require 'awshark/cloud_formation/stack_events'
|
13
|
+
require 'awshark/cloud_formation/template'
|
14
|
+
|
15
|
+
module Awshark
|
16
|
+
module Subcommands
|
17
|
+
class CloudFormation < Thor
|
18
|
+
include Awshark::Subcommands::ClassOptions
|
19
|
+
|
20
|
+
class_option :bucket, type: :string, desc: 'S3 bucket for template'
|
21
|
+
class_option :iam, type: :boolean, desc: 'Needs IAM capabilities'
|
22
|
+
class_option :stage, type: :string, desc: 'Stage of the configuration'
|
23
|
+
|
24
|
+
desc 'deploy', 'Updates or creates an AWS CloudFormation stack'
|
25
|
+
long_desc <<-LONGDESC
|
26
|
+
Updates or creates a CloudFormation stack on AWS.
|
27
|
+
|
28
|
+
awshark cf deploy TEMPLATE_PATH CAPABILITIES
|
29
|
+
|
30
|
+
Examples:
|
31
|
+
|
32
|
+
awshark cf deploy foo_template
|
33
|
+
|
34
|
+
awshark cf deploy iam_template IAM
|
35
|
+
LONGDESC
|
36
|
+
def deploy(template_path)
|
37
|
+
process_class_options
|
38
|
+
|
39
|
+
manager = create_manager(template_path)
|
40
|
+
print_stack_information(manager.stack)
|
41
|
+
|
42
|
+
manager.update_stack
|
43
|
+
sleep(2)
|
44
|
+
manager.tail_stack_events
|
45
|
+
rescue GracefulFail => e
|
46
|
+
puts e.message
|
47
|
+
end
|
48
|
+
|
49
|
+
desc 'diff', 'Show diff between local stack template and AWS CloudFormation'
|
50
|
+
long_desc <<-LONGDESC
|
51
|
+
Shows colored diff between local stack template and AWS CloudFormation
|
52
|
+
|
53
|
+
Example: `awshark cf diff TEMPLATE_PATH`
|
54
|
+
LONGDESC
|
55
|
+
def diff(template_path)
|
56
|
+
process_class_options
|
57
|
+
|
58
|
+
manager = create_manager(template_path)
|
59
|
+
print_stack_information(manager.stack)
|
60
|
+
|
61
|
+
diff = manager.diff_stack_template
|
62
|
+
puts diff
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def create_manager(template_path)
|
68
|
+
Awshark::CloudFormation::Manager.new(template_path, options.symbolize_keys)
|
69
|
+
end
|
70
|
+
|
71
|
+
def print_stack_information(stack)
|
72
|
+
if stack.exists?
|
73
|
+
args = { name: stack.name, created_at: stack.creation_time }
|
74
|
+
printf "Stack: %-20<name>s (created at %<created_at>s)\n\n", args
|
75
|
+
else
|
76
|
+
printf "Stack: %<name>s\n\n", name: stack.name
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aws-sdk-ec2'
|
4
|
+
|
5
|
+
require 'awshark/ec2/instance'
|
6
|
+
require 'awshark/ec2/manager'
|
7
|
+
|
8
|
+
module Awshark
|
9
|
+
module Subcommands
|
10
|
+
class EC2 < Thor
|
11
|
+
include Awshark::Subcommands::ClassOptions
|
12
|
+
|
13
|
+
desc 'list', 'List all EC2 instances'
|
14
|
+
long_desc <<-LONGDESC
|
15
|
+
List all EC2 instances in a region
|
16
|
+
|
17
|
+
Example: `awshark ec2 list STATE`
|
18
|
+
LONGDESC
|
19
|
+
def list(state = 'running')
|
20
|
+
process_class_options
|
21
|
+
|
22
|
+
instances = manager.public_send("#{state}_instances")
|
23
|
+
instances = instances.sort_by(&:name)
|
24
|
+
|
25
|
+
instances.each do |i|
|
26
|
+
args = { name: i.name, type: i.type, public_dns: i.public_dns_name, state: i.state }
|
27
|
+
printf "%-40<name>s %-12<type>s %-60<public_dns>s %<state>s\n", args
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def manager
|
34
|
+
@manager ||= Awshark::EC2::Manager.new
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aws-sdk-ecr'
|
4
|
+
|
5
|
+
module Awshark
|
6
|
+
module Subcommands
|
7
|
+
class Ecs < Thor
|
8
|
+
include Awshark::Subcommands::ClassOptions
|
9
|
+
|
10
|
+
desc 'login', 'docker login to AWS ECR'
|
11
|
+
long_desc <<-LONGDESC
|
12
|
+
Use docker login with AWS credentials to Elastic Container Registry
|
13
|
+
|
14
|
+
Example: `awshark ecs login`
|
15
|
+
LONGDESC
|
16
|
+
def login
|
17
|
+
response = client.get_authorization_token
|
18
|
+
token = Base64.decode64(response.authorization_data.first.authorization_token)
|
19
|
+
|
20
|
+
user_name = token.split(':').first
|
21
|
+
password = token.split(':').last
|
22
|
+
url = "https://#{Awshark.config.aws_account_id}.dkr.ecr.eu-central-1.amazonaws.com"
|
23
|
+
|
24
|
+
`docker login -u #{user_name} -p #{password} #{url}`
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def client
|
30
|
+
@client ||= Aws::ECR::Client.new(region: region)
|
31
|
+
end
|
32
|
+
|
33
|
+
def region
|
34
|
+
Aws.config[:region] || 'eu-central-1'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|