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,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
|