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,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awshark
|
4
|
+
module CloudFormation
|
5
|
+
class Parameters
|
6
|
+
include FileLoading
|
7
|
+
|
8
|
+
attr_reader :stage
|
9
|
+
|
10
|
+
def initialize(path:, stage: nil)
|
11
|
+
@filepath = Dir.glob("#{path}/parameters.*").detect do |f|
|
12
|
+
%w[.json .yml .yaml].include?(File.extname(f))
|
13
|
+
end
|
14
|
+
@stage = stage
|
15
|
+
end
|
16
|
+
|
17
|
+
def params
|
18
|
+
@params ||= load_parameters(@filepath)
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_hash
|
22
|
+
params
|
23
|
+
end
|
24
|
+
|
25
|
+
def stack_parameters
|
26
|
+
params.each.map do |k, v|
|
27
|
+
{
|
28
|
+
parameter_key: k,
|
29
|
+
parameter_value: v
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def load_parameters(filepath)
|
37
|
+
data = load_file(filepath) || {}
|
38
|
+
|
39
|
+
data[stage] || data
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
#
|
6
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CloudFormation/Types/Stack.html
|
7
|
+
#
|
8
|
+
module Awshark
|
9
|
+
module CloudFormation
|
10
|
+
class Stack
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
attr_reader :name
|
14
|
+
|
15
|
+
def_delegators :@stack,
|
16
|
+
:stack_id,
|
17
|
+
:stack_name,
|
18
|
+
:description,
|
19
|
+
:parameters,
|
20
|
+
:creation_time,
|
21
|
+
:deletion_time,
|
22
|
+
:last_updated_time,
|
23
|
+
:stack_status,
|
24
|
+
:stack_status_reason,
|
25
|
+
:notification_arns,
|
26
|
+
:capabilities,
|
27
|
+
:outputs,
|
28
|
+
:role_arn,
|
29
|
+
:tags
|
30
|
+
|
31
|
+
def initialize(name:)
|
32
|
+
@name = name
|
33
|
+
@stack = get_stack(name)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Boolean]
|
37
|
+
def exists?
|
38
|
+
@stack.present?
|
39
|
+
end
|
40
|
+
|
41
|
+
def events
|
42
|
+
response = client.describe_stack_events(stack_name: stack_id)
|
43
|
+
response.stack_events
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_or_update(params)
|
47
|
+
if exists?
|
48
|
+
client.update_stack(params)
|
49
|
+
else
|
50
|
+
client.create_stack(params)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def reload
|
55
|
+
@stack = get_stack(name)
|
56
|
+
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Hash]
|
61
|
+
def template
|
62
|
+
return nil unless exists?
|
63
|
+
return @template if @template.present?
|
64
|
+
|
65
|
+
response = client.get_template(stack_name: stack_id)
|
66
|
+
@template = response.template_body
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def client
|
72
|
+
return Awshark.config.cloud_formation.client if Awshark.config.cloud_formation.client
|
73
|
+
|
74
|
+
region = Aws.config[:region]
|
75
|
+
@client ||= Aws::CloudFormation::Client.new(region: region)
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_stack(stack_name)
|
79
|
+
response = begin
|
80
|
+
client.describe_stacks(stack_name: stack_name)
|
81
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
82
|
+
@stack = nil
|
83
|
+
return
|
84
|
+
end
|
85
|
+
|
86
|
+
if response.stacks.length > 1
|
87
|
+
raise ArgumentError, "Found too many stacks with name #{stack_name}. There should only be one."
|
88
|
+
end
|
89
|
+
|
90
|
+
response.stacks[0]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awshark
|
4
|
+
module CloudFormation
|
5
|
+
class StackEvents
|
6
|
+
attr_reader :stack
|
7
|
+
attr_reader :start_time, :last_event
|
8
|
+
|
9
|
+
def initialize(stack)
|
10
|
+
@stack = stack
|
11
|
+
@start_time = Time.now - 10
|
12
|
+
end
|
13
|
+
|
14
|
+
def done?
|
15
|
+
return false if last_event.nil?
|
16
|
+
return false if last_event.resource_type != 'AWS::CloudFormation::Stack'
|
17
|
+
|
18
|
+
last_event.resource_status.match(/IN_PROGRESS/).nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
def new_events
|
22
|
+
events = stack.events
|
23
|
+
|
24
|
+
return [] if events.blank?
|
25
|
+
|
26
|
+
if last_event.nil?
|
27
|
+
@last_event = events.first
|
28
|
+
new_events = events.select { |e| e.timestamp > start_time }
|
29
|
+
return new_events
|
30
|
+
end
|
31
|
+
|
32
|
+
return [] unless new_events?(events)
|
33
|
+
|
34
|
+
last_event_index = events.index { |e| e.event_id == last_event.event_id }
|
35
|
+
new_events = events[0..last_event_index - 1]
|
36
|
+
@last_event = new_events.first
|
37
|
+
|
38
|
+
new_events
|
39
|
+
end
|
40
|
+
|
41
|
+
def new_events?(events)
|
42
|
+
events.first.event_id != last_event.event_id
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awshark
|
4
|
+
module CloudFormation
|
5
|
+
class Template
|
6
|
+
include FileLoading
|
7
|
+
|
8
|
+
attr_reader :path
|
9
|
+
attr_reader :bucket, :name, :stage
|
10
|
+
|
11
|
+
def initialize(path, options = {})
|
12
|
+
@path = path
|
13
|
+
|
14
|
+
@bucket = options[:bucket]
|
15
|
+
@name = options[:name]
|
16
|
+
@stage = options[:stage]
|
17
|
+
end
|
18
|
+
|
19
|
+
# @returns [Hash]
|
20
|
+
def as_json
|
21
|
+
load_file(template_path, context)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @returns [String]
|
25
|
+
def body
|
26
|
+
JSON.pretty_generate(as_json)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @returns [Hash]
|
30
|
+
def context
|
31
|
+
@context ||= begin
|
32
|
+
context = load_file(context_path) || {}
|
33
|
+
context = context[stage] if context.key?(stage)
|
34
|
+
|
35
|
+
{
|
36
|
+
context: RecursiveOpenStruct.new(context),
|
37
|
+
aws_account_id: Awshark.config.aws_account_id,
|
38
|
+
stage: stage
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @returns [Integer]
|
44
|
+
def size
|
45
|
+
body.size
|
46
|
+
end
|
47
|
+
|
48
|
+
# @returns [Boolean]
|
49
|
+
def uploaded?
|
50
|
+
@uploaded == true
|
51
|
+
end
|
52
|
+
|
53
|
+
# @returns [String]
|
54
|
+
def url
|
55
|
+
upload unless uploaded?
|
56
|
+
|
57
|
+
"https://#{bucket}.s3.#{region}.amazonaws.com/#{s3_key}"
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def region
|
63
|
+
Aws.config[:region] || 'eu-central-1'
|
64
|
+
end
|
65
|
+
|
66
|
+
def s3
|
67
|
+
return Awshark.config.s3.client if Awshark.config.s3.client
|
68
|
+
|
69
|
+
@s3 ||= Aws::S3::Client.new(region: region, signature_version: 'v4')
|
70
|
+
end
|
71
|
+
|
72
|
+
def s3_key
|
73
|
+
# https://apidock.com/ruby/Time/strftime
|
74
|
+
@s3_key ||= "awshark/#{name}/#{Time.now.strftime('%Y-%m-%d')}.json"
|
75
|
+
end
|
76
|
+
|
77
|
+
def upload
|
78
|
+
raise ArgumentError, 'Bucket for template upload to S3 is missing' if bucket.blank?
|
79
|
+
|
80
|
+
Awshark.logger.debug "[awshark] Uploading CF template to #{bucket}"
|
81
|
+
|
82
|
+
s3.put_object(bucket: bucket, key: s3_key, body: body)
|
83
|
+
end
|
84
|
+
|
85
|
+
def context_path
|
86
|
+
Dir.glob("#{path}/context.*").detect do |f|
|
87
|
+
%w[.json .yml .yaml].include?(File.extname(f))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def template_path
|
92
|
+
@template_path ||= if File.directory?(path)
|
93
|
+
Dir.glob("#{path}/template.*").detect do |f|
|
94
|
+
%w[.json .yml .yaml].include?(File.extname(f))
|
95
|
+
end
|
96
|
+
else
|
97
|
+
path
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awshark
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :cloud_formation, :s3
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@cloud_formation = OpenStruct.new(
|
9
|
+
client: nil,
|
10
|
+
event_polling: 3
|
11
|
+
)
|
12
|
+
|
13
|
+
@s3 = OpenStruct.new(
|
14
|
+
client: nil
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def aws_account_id
|
19
|
+
return @aws_account_id if defined?(@aws_account_id)
|
20
|
+
|
21
|
+
response = sts_client.get_caller_identity
|
22
|
+
|
23
|
+
@aws_account_id = response.account
|
24
|
+
end
|
25
|
+
|
26
|
+
def sts_client
|
27
|
+
@sts_client ||= Aws::STS::Client.new
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_writer :sts_client
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awshark
|
4
|
+
module EC2
|
5
|
+
class Instance
|
6
|
+
attr_reader :id
|
7
|
+
attr_reader :type
|
8
|
+
attr_reader :state
|
9
|
+
attr_reader :private_ip_address
|
10
|
+
attr_reader :public_ip_address
|
11
|
+
attr_reader :public_dns_name
|
12
|
+
attr_reader :vpc_id
|
13
|
+
attr_reader :subnet_id
|
14
|
+
attr_reader :tags
|
15
|
+
|
16
|
+
def initialize(instance)
|
17
|
+
@id = instance.instance_id
|
18
|
+
@type = instance.instance_type
|
19
|
+
@state = instance.state.name
|
20
|
+
@private_ip_address = instance.private_ip_address
|
21
|
+
@public_ip_address = instance.public_ip_address
|
22
|
+
@public_dns_name = instance.public_dns_name
|
23
|
+
@vpc_id = instance.vpc_id
|
24
|
+
@subnet_id = instance.subnet_id
|
25
|
+
@tags = instance.tags
|
26
|
+
end
|
27
|
+
|
28
|
+
def name
|
29
|
+
tag_value(tags, 'Name').split(' - ').last
|
30
|
+
rescue StandardError
|
31
|
+
'null'
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def tag_value(tags, key)
|
37
|
+
return nil if tags.empty?
|
38
|
+
|
39
|
+
tag = tags.detect { |t| t.key == key }
|
40
|
+
return tag.value if tag
|
41
|
+
|
42
|
+
'null'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awshark
|
4
|
+
module EC2
|
5
|
+
class Manager
|
6
|
+
def all_instances
|
7
|
+
return @all_instances if defined?(@all_instances)
|
8
|
+
|
9
|
+
@all_instances = []
|
10
|
+
response = client.describe_instances
|
11
|
+
|
12
|
+
response.each_page do |page|
|
13
|
+
page.reservations.each do |reservation|
|
14
|
+
reservation.instances.each do |instance|
|
15
|
+
@all_instances << Instance.new(instance)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
@all_instances
|
21
|
+
end
|
22
|
+
|
23
|
+
%i[running stopped terminated].each do |state|
|
24
|
+
define_method "#{state}_instances" do
|
25
|
+
all_instances.select { |i| i.state == state.to_s }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def client
|
32
|
+
@client ||= Aws::EC2::Client.new(region: region)
|
33
|
+
end
|
34
|
+
|
35
|
+
def region
|
36
|
+
Aws.config[:region] || 'eu-central-1'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awshark
|
4
|
+
class ProfileResolver
|
5
|
+
attr_reader :region
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@profile = options[:profile] || ENV['AWS_PROFILE']
|
9
|
+
@shared_config = ::Aws::SharedConfig.new(
|
10
|
+
profile_name: @profile,
|
11
|
+
config_enabled: true
|
12
|
+
)
|
13
|
+
@region = options[:region] || @shared_config.region || 'eu-central-1'
|
14
|
+
end
|
15
|
+
|
16
|
+
def credentials
|
17
|
+
user_credentials || role_credentials
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns Aws credentials for configuration with
|
21
|
+
# [profile]
|
22
|
+
# aws_access_key_id=AWS_ACCESS_KEY_ID
|
23
|
+
# aws_secret_access_key=AWS_SECRET_ACCESS_KEY
|
24
|
+
#
|
25
|
+
# Returns nil for configuration with
|
26
|
+
# [profile]
|
27
|
+
# role_arn = ROLE_ARN
|
28
|
+
# source_profile = SOURCE_PROFILE
|
29
|
+
#
|
30
|
+
# @returns [Aws::Credentials]
|
31
|
+
def user_credentials
|
32
|
+
@shared_config.credentials
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns Aws credentials for configuration with
|
36
|
+
# [profile]
|
37
|
+
# role_arn = ROLE_ARN
|
38
|
+
# source_profile = SOURCE_PROFILE
|
39
|
+
#
|
40
|
+
# @throws [Aws::STS::Errors::AccessDenied] if MultiFactorAuthentication failed
|
41
|
+
# @returns [Aws::Credentials]
|
42
|
+
def role_credentials
|
43
|
+
@shared_config.assume_role_credentials_from_config(
|
44
|
+
region: region,
|
45
|
+
token_code: mfa_token
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def mfa_token
|
50
|
+
print %(Please enter MFA token for AWS account #{@profile}: )
|
51
|
+
$stdin.gets.strip
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|