firespring_dev_commands 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +83 -0
  4. data/lib/firespring_dev_commands/audit/report/item.rb +33 -0
  5. data/lib/firespring_dev_commands/audit/report/levels.rb +36 -0
  6. data/lib/firespring_dev_commands/audit/report.rb +49 -0
  7. data/lib/firespring_dev_commands/aws/account/info.rb +15 -0
  8. data/lib/firespring_dev_commands/aws/account.rb +164 -0
  9. data/lib/firespring_dev_commands/aws/cloudformation/parameters.rb +26 -0
  10. data/lib/firespring_dev_commands/aws/cloudformation.rb +188 -0
  11. data/lib/firespring_dev_commands/aws/codepipeline.rb +96 -0
  12. data/lib/firespring_dev_commands/aws/credentials.rb +136 -0
  13. data/lib/firespring_dev_commands/aws/login.rb +131 -0
  14. data/lib/firespring_dev_commands/aws/parameter.rb +32 -0
  15. data/lib/firespring_dev_commands/aws/profile.rb +55 -0
  16. data/lib/firespring_dev_commands/aws/s3.rb +42 -0
  17. data/lib/firespring_dev_commands/aws.rb +10 -0
  18. data/lib/firespring_dev_commands/boolean.rb +7 -0
  19. data/lib/firespring_dev_commands/common.rb +112 -0
  20. data/lib/firespring_dev_commands/daterange.rb +171 -0
  21. data/lib/firespring_dev_commands/docker/compose.rb +271 -0
  22. data/lib/firespring_dev_commands/docker/status.rb +38 -0
  23. data/lib/firespring_dev_commands/docker.rb +276 -0
  24. data/lib/firespring_dev_commands/dotenv.rb +6 -0
  25. data/lib/firespring_dev_commands/env.rb +38 -0
  26. data/lib/firespring_dev_commands/eol/product_version.rb +86 -0
  27. data/lib/firespring_dev_commands/eol.rb +58 -0
  28. data/lib/firespring_dev_commands/git/info.rb +13 -0
  29. data/lib/firespring_dev_commands/git.rb +420 -0
  30. data/lib/firespring_dev_commands/jira/issue.rb +33 -0
  31. data/lib/firespring_dev_commands/jira/project.rb +13 -0
  32. data/lib/firespring_dev_commands/jira/user/type.rb +20 -0
  33. data/lib/firespring_dev_commands/jira/user.rb +31 -0
  34. data/lib/firespring_dev_commands/jira.rb +78 -0
  35. data/lib/firespring_dev_commands/logger.rb +8 -0
  36. data/lib/firespring_dev_commands/node/audit.rb +39 -0
  37. data/lib/firespring_dev_commands/node.rb +107 -0
  38. data/lib/firespring_dev_commands/php/audit.rb +71 -0
  39. data/lib/firespring_dev_commands/php.rb +109 -0
  40. data/lib/firespring_dev_commands/rake.rb +24 -0
  41. data/lib/firespring_dev_commands/ruby/audit.rb +30 -0
  42. data/lib/firespring_dev_commands/ruby.rb +113 -0
  43. data/lib/firespring_dev_commands/second.rb +22 -0
  44. data/lib/firespring_dev_commands/tar/pax_header.rb +49 -0
  45. data/lib/firespring_dev_commands/tar/type_flag.rb +49 -0
  46. data/lib/firespring_dev_commands/tar.rb +149 -0
  47. data/lib/firespring_dev_commands/templates/aws.rb +84 -0
  48. data/lib/firespring_dev_commands/templates/base_interface.rb +54 -0
  49. data/lib/firespring_dev_commands/templates/ci.rb +138 -0
  50. data/lib/firespring_dev_commands/templates/docker/application.rb +177 -0
  51. data/lib/firespring_dev_commands/templates/docker/default.rb +200 -0
  52. data/lib/firespring_dev_commands/templates/docker/node/application.rb +145 -0
  53. data/lib/firespring_dev_commands/templates/docker/php/application.rb +190 -0
  54. data/lib/firespring_dev_commands/templates/docker/ruby/application.rb +146 -0
  55. data/lib/firespring_dev_commands/templates/eol.rb +23 -0
  56. data/lib/firespring_dev_commands/templates/git.rb +147 -0
  57. data/lib/firespring_dev_commands/version.rb +11 -0
  58. data/lib/firespring_dev_commands.rb +21 -0
  59. metadata +436 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d6f07bb537b2a77642593e6f28ad14791810c0395b56eb73b7dfe062f414491c
4
+ data.tar.gz: 6fd0bb605fb7fa4fe17f5baf565003a960479774b0f0d83a4c5c3b6cf1ceacd9
5
+ SHA512:
6
+ metadata.gz: a8f58eca8af848da446bd775e0fb7a329cf94188abe44ae6a6ce4b7f074cf09a6d7a8598bbd4a8e1cdd69031dae586380d543f28b398821c5f63ac4c53fbe13d
7
+ data.tar.gz: 2edd0e114d231fbc3019d4c4235a51629df99e91dcd49c4ca4b0d03ee40cbf7ee8d2639670e4f5613f0027cfdb89b6b3ad204cd5856e9896099381fbbad2b2d7
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Firespring
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # Firespring Dev Commands
2
+ This project is for maintaining your local development environment using a Firespring supported library of commands
3
+
4
+ ### Usage
5
+ * To use a released version of the library, add the following to your Gemfile
6
+ ```
7
+ gem 'firespring_dev_commands', '~> 0.0.1'
8
+ ```
9
+
10
+ * To use a local version of the library, add the following to your Gemfile
11
+ * This is not common
12
+ * It is mostly used for testing local changes before the gem is released
13
+ ```
14
+ gem 'firespring_dev_commands', path: '/path/to/firespring/dev-commands-ruby'
15
+ ```
16
+
17
+ * Add the following to your Rakefile
18
+ ```
19
+ require 'rubygems'
20
+ require 'bundler/setup'
21
+ require 'firespring_dev_commands'
22
+ ```
23
+
24
+ * (optional) Add any firespring_dev_command templates you wish to use
25
+ ```
26
+ # Create default tasks
27
+ Dev::Template::Docker::Default.new
28
+ Dev::Template::Docker::Application.new('foo')
29
+ Dev::Template::Docker::Node::Application.new('foo')
30
+ ```
31
+ * If you run `rake -T` now, you should have base rake commands and application rake commands for an app called `foo`
32
+
33
+ * (optinoal) Add AWS login template commands
34
+ ```
35
+ # Configure AWS accounts and create tasks
36
+ Dev::Aws::Account::configure do |c|
37
+ c.root = Dev::Aws::Account::Info.new('Foo Root', '1234')
38
+ c.children = [Dev::Aws::Account::Info.new('Foo Dev', '5678')]
39
+ end
40
+ Dev::Template::Aws.new
41
+ ```
42
+ * Now you should be able to log in to the 1234 account and switch to your personal role in the 5678 account
43
+ * If you specify a "registry" id in the Account configure you will be logged in ECR inside the system docker so you can pull and push images
44
+
45
+ ### Development
46
+ * Clone the repo
47
+ * Change code
48
+ * Build test image
49
+ * `rake build`
50
+ * Connect to the test image
51
+ * `rake app:sh`
52
+ * Ensure ruby lints pass
53
+ * `rake app:ruby:lint` or `rake app:ruby:lint:fix`
54
+ * Ensure tests pass
55
+ * `rake app:ruby:test`
56
+ * Update the gem version appropriately
57
+ * We use semantic versioning
58
+ * https://semver.org/
59
+ * Open a pull request and add reviewers
60
+
61
+ ### Publishing
62
+ * After your changes have been approved, run the `rake release` command
63
+ * You will receive an error if you try to re-publish an existing version of the gem
64
+ * Theoretically you could yank an existing version and re-publish if necessary
65
+
66
+ # Concepts
67
+ ### Config
68
+ * Many of the classes have a configure singleton which can be used to set global configs
69
+ * These configs should then be the default used when instantiating the object
70
+ * The configs should always be over-writable when instantiating the object
71
+
72
+ ### Templates
73
+ * The templates should have as little code/logic in them as possible
74
+ * This is to help with re-usability
75
+ * Instead, create the bulk of the logic in the ruby files so that if a user wants to modify it they can re-use those ruby methods in a task of their own making
76
+ * Naming of the templates generally follows `rake <thing>:<language (optional)>:action:<modifier (optional)`
77
+ * e.g. `rake build`, `rake app:up`, `rake app:php:test:unit`
78
+
79
+ ### TODOs
80
+ * Consider publishing a docker image which you can run the commands in
81
+ * So you don't need ruby on your local system
82
+ * Add LOTS of tests to get code coverage to 100%
83
+
@@ -0,0 +1,33 @@
1
+ module Dev
2
+ class Audit
3
+ class Report
4
+ # This class contains audit report items and their associated data
5
+ class Item
6
+ attr_accessor :id, :name, :title, :url, :severity, :version
7
+
8
+ def initialize(id:, name:, title:, url:, severity:, version:)
9
+ @id = id
10
+ @name = name
11
+ @title = title
12
+ @url = url
13
+ @severity = severity
14
+ @version = version
15
+ end
16
+
17
+ # Returns a string representation of this audit report item
18
+ def to_s
19
+ [
20
+ '+-------------------+----------------------------------------------------------------------------------+',
21
+ format('| %s | %-80s |', format('%-17s', 'Severity').green, severity),
22
+ format('| %s | %-80s |', format('%-17s', 'Package').green, name),
23
+ format('| %s | %-80s |', format('%-17s', 'Id').green, id),
24
+ format('| %s | %-80s |', format('%-17s', 'Title').green, title),
25
+ format('| %s | %-80s |', format('%-17s', 'URL').green, url),
26
+ format('| %s | %-80s |', format('%-17s', 'Affected versions').green, version),
27
+ '+-------------------+----------------------------------------------------------------------------------+'
28
+ ].join("\n")
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ module Dev
2
+ class Audit
3
+ class Report
4
+ # Contains constants representing different audit report severity levels
5
+ class Level
6
+ # "info" severity level
7
+ INFO = 'info'.freeze
8
+
9
+ # "low" severity level
10
+ LOW = 'low'.freeze
11
+
12
+ # "moderate" severity level
13
+ MODERATE = 'moderate'.freeze
14
+
15
+ # "high" severity level
16
+ HIGH = 'high'.freeze
17
+
18
+ # "critical" severity level
19
+ CRITICAL = 'critical'.freeze
20
+
21
+ # "unknown" severity level
22
+ UNKNOWN = 'unknown'.freeze
23
+ end
24
+
25
+ # All supported audit report levels in ascending order of severity
26
+ LEVELS = [
27
+ Level::INFO,
28
+ Level::LOW,
29
+ Level::MODERATE,
30
+ Level::HIGH,
31
+ Level::CRITICAL,
32
+ Level::UNKNOWN
33
+ ].freeze
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,49 @@
1
+ module Dev
2
+ # Class containing security audit information
3
+ class Audit
4
+ # The class containing standardized information about an audit report
5
+ class Report
6
+ attr_accessor :items, :min_severity, :ignorelist, :filtered_items
7
+
8
+ def initialize(
9
+ items,
10
+ min_severity: ENV.fetch('MIN_SEVERITY', nil),
11
+ ignorelist: ENV['IGNORELIST'].to_s.split(/\s*,\s*/)
12
+ )
13
+ # Items should be an array of Item objects
14
+ @items = Array(items)
15
+ raise 'items must all be report items' unless @items.all?(Dev::Audit::Report::Item)
16
+
17
+ @min_severity = min_severity || Level::HIGH
18
+ @ignorelist = Array(ignorelist).compact
19
+ end
20
+
21
+ # Get all severities greater than or equal to the minimum severity
22
+ def desired_severities
23
+ LEVELS.slice(LEVELS.find_index(min_severity)..-1)
24
+ end
25
+
26
+ # Run the filters against the report items and filter out any which should be excluded
27
+ def filtered_items
28
+ @filtered_items ||= items.select { |it| desired_severities.include?(it.severity) }.select { |it| ignorelist.none?(it.id) }
29
+ end
30
+
31
+ # Output the text of the filtered report items
32
+ # Exit with a non-zero status if any vulnerabilities were found
33
+ def check
34
+ puts(to_s)
35
+ exit(1) unless filtered_items.empty?
36
+ end
37
+
38
+ # Returns a string representation of this audit report
39
+ def to_s
40
+ return 'No security vulnerabilities found'.green if filtered_items.empty?
41
+
42
+ [].tap do |ary|
43
+ ary << "Found #{filtered_items.length} security vulnerabilities:".white.on_red
44
+ filtered_items.each { |item| ary << item.to_s }
45
+ end.join("\n")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,15 @@
1
+ module Dev
2
+ class Aws
3
+ class Account
4
+ # Class which contains information about the Aws account
5
+ class Info
6
+ attr_accessor :name, :id
7
+
8
+ def initialize(name, id)
9
+ @name = name
10
+ @id = id
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,164 @@
1
+ module Dev
2
+ class Aws
3
+ # Class containing useful methods for interacting with the Aws account
4
+ class Account
5
+ # Config object for setting top level Aws account config options
6
+ Config = Struct.new(:root, :children, :default, :registry)
7
+
8
+ # Instantiates a new top level config object if one hasn't already been created
9
+ # Yields that config object to any given block
10
+ # Returns the resulting config object
11
+ def self.config
12
+ @config ||= Config.new
13
+ yield(@config) if block_given?
14
+ @config
15
+ end
16
+
17
+ # Alias the config method to configure for a slightly clearer access syntax
18
+ class << self
19
+ alias_method :configure, :config
20
+ end
21
+
22
+ # The name of the file containing the Aws settings
23
+ CONFIG_FILE = "#{Dev::Aws::CONFIG_DIR}/config".freeze
24
+
25
+ attr_accessor :root, :children, :default, :registry
26
+
27
+ # Instantiate an account object
28
+ # Requires that root account and at least one child account have been configured
29
+ # All accounts must be of type Dev::Aws::Account::Info
30
+ # If a registry is configured then the user will be logged in to ECR when they log in to the account
31
+ def initialize
32
+ raise 'Root account must be configured' unless self.class.config.root.is_a?(Dev::Aws::Account::Info)
33
+ raise 'Child accounts must be configured' if self.class.config.children.empty? || !self.class.config.children.all?(Dev::Aws::Account::Info)
34
+
35
+ @root = self.class.config.root
36
+ @children = self.class.config.children
37
+ @default = self.class.config.default
38
+ @registry = self.class.config.registry
39
+ end
40
+
41
+ # Returns all configured account information objects
42
+ def all
43
+ @all ||= ([root] + children).sort_by(&:name)
44
+ end
45
+
46
+ # Returns the name portion of all configured account information objects
47
+ def all_names
48
+ @all_names ||= all.map(&:name)
49
+ end
50
+
51
+ # Returns the id portion of all configured account information objects
52
+ def all_accounts
53
+ @all_accounts ||= all.map(&:id)
54
+ end
55
+
56
+ # Look up the account name for the given account id
57
+ def name_by_account(account)
58
+ all.find { |it| it.id == account }.name
59
+ end
60
+
61
+ # Setup base Aws settings
62
+ def base_setup!
63
+ # Make the base config directory
64
+ FileUtils.mkdir_p(Dev::Aws::CONFIG_DIR)
65
+
66
+ puts
67
+ puts 'Configuring default login values'
68
+
69
+ # Write region and mfa serial to config file
70
+ cfgini = IniFile.new(filename: "#{Dev::Aws::CONFIG_DIR}/config", default: 'default')
71
+ defaultini = cfgini['default']
72
+
73
+ region_default = defaultini['region'] || ENV['AWS_DEFAULT_REGION'] || Dev::Aws::DEFAULT_REGION
74
+ defaultini['region'] = Dev::Common.new.ask('Default region name', region_default)
75
+
76
+ mfa_default = defaultini['mfa_serial'] || ENV['AWS_MFA_ARN'] || "arn:aws:iam::#{root}:mfa/#{ENV.fetch('USERNAME', nil)}"
77
+ defaultini['mfa_serial'] = Dev::Common.new.ask('Default mfa arn', mfa_default)
78
+
79
+ session_name_default = defaultini['role_session_name'] || "#{ENV.fetch('USERNAME', nil)}_cli"
80
+ defaultini['role_session_name'] = Dev::Common.new.ask('Default session name', session_name_default)
81
+
82
+ duration_default = defaultini['session_duration'] || 36_000
83
+ defaultini['session_duration'] = Dev::Common.new.ask('Default session duration in seconds', duration_default)
84
+
85
+ cfgini.write
86
+ end
87
+
88
+ # Setup Aws account specific settings
89
+ def setup!(account)
90
+ # Run base setup if it doesn't exist
91
+ Rake::Task['aws:configure:default'].invoke unless File.exist?(CONFIG_FILE)
92
+
93
+ puts
94
+ puts "Configuring #{account} login values"
95
+
96
+ write!(account)
97
+ puts
98
+ end
99
+
100
+ # Write Aws account specific settings to the config file
101
+ def write!(account)
102
+ raise 'Configure default account settings first (rake aws:configure:default)' unless File.exist?(CONFIG_FILE)
103
+
104
+ # Parse the ini file and load values
105
+ cfgini = IniFile.new(filename: CONFIG_FILE, default: 'default')
106
+ defaultini = cfgini['default']
107
+ profileini = cfgini["profile #{account}"]
108
+
109
+ profileini['source_profile'] = account
110
+
111
+ region_default = profileini['region'] || defaultini['region'] || ENV['AWS_DEFAULT_REGION'] || Dev::Aws::DEFAULT_REGION
112
+ profileini['region'] = Dev::Common.new.ask('Default region name', region_default)
113
+
114
+ role_default = profileini['role_arn'] || "arn:aws:iam::#{account}:role/ReadonlyAccessRole"
115
+ profileini['role_arn'] = Dev::Common.new.ask('Default role arn', role_default)
116
+
117
+ cfgini.write
118
+ end
119
+
120
+ # Menu to select one of the Aws child accounts
121
+ def select
122
+ # If there is only one child account, use that
123
+ return children.first.id if children.length == 1
124
+
125
+ # Output a list for the user to select from
126
+ puts 'Account Selection:'
127
+ children.each_with_index do |account, i|
128
+ printf " %2s) %-20s %s\n", i + 1, account.name, account.id
129
+ end
130
+ selection = Dev::Common.new.ask('Enter the number of the account you wish to log in to', select_default)
131
+ number = selection.to_i
132
+ raise "Invalid selection: #{selection}" if number < 1
133
+
134
+ # If the selection is 3 characters or more, assume they entered the full account number
135
+ if selection.length > 3
136
+ raise "Invalid selection: #{selection}" unless all_accounts.include?(selection)
137
+
138
+ return selection
139
+ end
140
+
141
+ # Otherwise they probably entered the number of the account to use
142
+ # Use the number as the index for lookup in accounts array and then get the value of that number
143
+ raise "Invalid selection: #{selection}" unless children.length >= number
144
+
145
+ children[number - 1].id
146
+ end
147
+
148
+ # Method of determining what the appropriate default account is for the select menu
149
+ # Filters out the account you are currently logged in to if it's no longer
150
+ # an option on the project you are currently trying to login on
151
+ def select_default
152
+ # If we are currently logged in to one of the configured accounts, use it as the default
153
+ account_id = Dev::Env.new(Dev::Aws::Profile::CONFIG_FILE).get(Dev::Aws::Profile::IDENTIFIER)
154
+ return account_id if all_accounts.include?(account_id)
155
+
156
+ # Otherwise, if a default is configured, use that
157
+ return default if default
158
+
159
+ # Otherwise, just return the first account
160
+ children.first.id
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,26 @@
1
+ module Dev
2
+ class Aws
3
+ class Cloudformation
4
+ # Class which contains Parameters for a Aws cloudformation stack
5
+ class Parameters
6
+ attr_accessor :parameters
7
+
8
+ def initialize(parameters = {})
9
+ raise 'parameters should be a hash' unless parameters.is_a?(Hash)
10
+
11
+ @parameters = parameters
12
+ end
13
+
14
+ # Returns the given parameters in their default format. Can be passed to a create or update command
15
+ def default
16
+ parameters.map { |k, v| {parameter_key: k, parameter_value: v} }
17
+ end
18
+
19
+ # Returns the given parameters all set to use the previous values specified in their templates
20
+ def preserve
21
+ parameters.map { |k, _| {parameter_key: k, use_previous_value: true} }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,188 @@
1
+ require 'securerandom'
2
+ require 'aws-sdk-s3'
3
+ require 'aws-sdk-cloudformation'
4
+
5
+ module Dev
6
+ class Aws
7
+ # Class for performing cloudformation functions
8
+ class Cloudformation
9
+ # Not Started status
10
+ NOT_STARTED = :not_started
11
+
12
+ # Started status
13
+ STARTED = :started
14
+
15
+ # No Changes status
16
+ NO_CHANGES = :no_changes
17
+
18
+ # Failed status
19
+ FAILED = :failed
20
+
21
+ # Finished status
22
+ FINISHED = :finished
23
+
24
+ attr_accessor :client, :name, :template_filename, :parameters, :capabilities, :failure_behavior, :state
25
+
26
+ def initialize(name, template_filename, parameters: Dev::Aws::Cloudformation::Parameters.new, capabilities: [], failure_behavior: 'ROLLBACK')
27
+ raise 'parameters must be an intsance of parameters' unless parameters.is_a?(Dev::Aws::Cloudformation::Parameters)
28
+
29
+ @client = nil
30
+ @name = name
31
+ @template_filename = template_filename
32
+ @parameters = parameters
33
+ @capabilities = capabilities
34
+ @failure_behavior = failure_behavior
35
+ @state = NOT_STARTED
36
+ end
37
+
38
+ # Create/set a new client if none is present
39
+ # Return the client
40
+ def client
41
+ @client ||= ::Aws::CloudFormation::Client.new
42
+ end
43
+
44
+ # Create the cloudformation stack
45
+ def create(should_wait: true)
46
+ # Call upload function to get the s3 url
47
+ template_url = upload(template_filename)
48
+
49
+ # Create the cloudformation stack
50
+ client.create_stack(
51
+ stack_name: name,
52
+ template_url: template_url,
53
+ parameters: parameters.default,
54
+ capabilities: capabilities,
55
+ on_failure: failure_behavior
56
+ )
57
+ @state = STARTED
58
+ LOG.info "#{name} stack create started at #{Time.now.to_s.light_yellow}"
59
+
60
+ # return if we aren't waiting here
61
+ return unless should_wait
62
+
63
+ # Wait if we are supposed to wait
64
+ create_wait
65
+ @state = FINISHED
66
+ LOG.info "#{name} stack create finished at #{Time.now.to_s.light_yellow}"
67
+ rescue => e
68
+ LOG.error "Error creating stack: #{e.message}"
69
+ @state = FAILED
70
+ end
71
+
72
+ # Update the cloudformation stack
73
+ def update(should_wait: true)
74
+ # Call upload function to get the s3 url
75
+ template_url = upload(template_filename)
76
+
77
+ # Update the cloudformation stack
78
+ client.update_stack(
79
+ stack_name: name,
80
+ template_url: template_url,
81
+ parameters: parameters.preserve,
82
+ capabilities: capabilities
83
+ )
84
+ @state = STARTED
85
+ LOG.info "#{name} stack update started at #{Time.now.to_s.light_yellow}"
86
+
87
+ # return if we aren't waiting here
88
+ return unless should_wait
89
+
90
+ # Wait if we are supposed to wait
91
+ update_wait
92
+ @state = FINISHED
93
+ LOG.info "#{name} stack update finished at #{Time.now.to_s.light_yellow}"
94
+ rescue => e
95
+ if /no updates/i.match?(e.message)
96
+ LOG.info "No updates to needed on #{name}".light_yellow
97
+ @state = NO_CHANGES
98
+ else
99
+
100
+ LOG.error "Error updating stack: #{e.message}"
101
+ @state = FAILED
102
+ end
103
+ end
104
+
105
+ # Delete the cloudformation stack
106
+ def delete(should_wait: true)
107
+ # Delete the cloudformation stack
108
+ client.delete_stack(stack_name: name)
109
+ @state = STARTED
110
+ LOG.info "#{name} stack delete started at #{Time.now.to_s.light_yellow}"
111
+
112
+ # Return if we aren't waiting here
113
+ return unless should_wait
114
+
115
+ # Wait if we are supposed to wait
116
+ delete_wait
117
+ @state = FINISHED
118
+ LOG.info "#{name} stack delete finished at #{Time.now.to_s.light_yellow}"
119
+ rescue => e
120
+ LOG.error "Error deleting stack: #{e.message}"
121
+ @state = FAILED
122
+ end
123
+
124
+ # Wait for create complete
125
+ def create_wait
126
+ wait(name, :create_complete)
127
+ end
128
+
129
+ # Wait for update complete
130
+ def update_wait
131
+ wait(name, :update_complete)
132
+ end
133
+
134
+ # Wait for delete complete
135
+ def delete_wait
136
+ wait(name, :delete_complete)
137
+ end
138
+
139
+ # Wait for the stack name to complete the specified type of action
140
+ # Defaults to exists
141
+ def wait(stack_name, type = 'exists', max_attempts: 360, delay: 5)
142
+ # Don't wait if there's nothing to wait for
143
+ return if no_changes? || finished?
144
+
145
+ client.wait_until(
146
+ :"stack_#{type}",
147
+ {stack_name: stack_name},
148
+ {max_attempts: max_attempts, delay: delay}
149
+ )
150
+ rescue ::Aws::Waiters::Errors::WaiterFailed => e
151
+ raise "Action failed to complete: #{e.message}"
152
+ end
153
+
154
+ # State matches the not started state
155
+ def not_started?
156
+ state == NOT_STARTED
157
+ end
158
+
159
+ # State matches the started state
160
+ def started?
161
+ state == STARTED
162
+ end
163
+
164
+ # State matches the no_changes state
165
+ def no_changes?
166
+ state == NO_CHANGES
167
+ end
168
+
169
+ # State matches the failed state
170
+ def failed?
171
+ state == FAILED
172
+ end
173
+
174
+ # State matches the finished state
175
+ def finished?
176
+ state == FINISHED
177
+ end
178
+
179
+ # Uploads the filename to the cloudformation templates bucket and returns the url of the file
180
+ private def upload(filename)
181
+ s3 = Dev::Aws::S3.new
182
+ template_bucket = s3.cf_bucket.name
183
+ key = "#{File.basename(filename)}/#{SecureRandom.uuid}"
184
+ s3.put(bucket: template_bucket, key: key, filename: filename)
185
+ end
186
+ end
187
+ end
188
+ end