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