firespring_dev_commands 1.3.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.
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