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