awshark 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 +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
|