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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +83 -0
- data/lib/firespring_dev_commands/audit/report/item.rb +33 -0
- data/lib/firespring_dev_commands/audit/report/levels.rb +36 -0
- data/lib/firespring_dev_commands/audit/report.rb +49 -0
- data/lib/firespring_dev_commands/aws/account/info.rb +15 -0
- data/lib/firespring_dev_commands/aws/account.rb +164 -0
- data/lib/firespring_dev_commands/aws/cloudformation/parameters.rb +26 -0
- data/lib/firespring_dev_commands/aws/cloudformation.rb +188 -0
- data/lib/firespring_dev_commands/aws/codepipeline.rb +96 -0
- data/lib/firespring_dev_commands/aws/credentials.rb +136 -0
- data/lib/firespring_dev_commands/aws/login.rb +131 -0
- data/lib/firespring_dev_commands/aws/parameter.rb +32 -0
- data/lib/firespring_dev_commands/aws/profile.rb +55 -0
- data/lib/firespring_dev_commands/aws/s3.rb +42 -0
- data/lib/firespring_dev_commands/aws.rb +10 -0
- data/lib/firespring_dev_commands/boolean.rb +7 -0
- data/lib/firespring_dev_commands/common.rb +112 -0
- data/lib/firespring_dev_commands/daterange.rb +171 -0
- data/lib/firespring_dev_commands/docker/compose.rb +271 -0
- data/lib/firespring_dev_commands/docker/status.rb +38 -0
- data/lib/firespring_dev_commands/docker.rb +276 -0
- data/lib/firespring_dev_commands/dotenv.rb +6 -0
- data/lib/firespring_dev_commands/env.rb +38 -0
- data/lib/firespring_dev_commands/eol/product_version.rb +86 -0
- data/lib/firespring_dev_commands/eol.rb +58 -0
- data/lib/firespring_dev_commands/git/info.rb +13 -0
- data/lib/firespring_dev_commands/git.rb +420 -0
- data/lib/firespring_dev_commands/jira/issue.rb +33 -0
- data/lib/firespring_dev_commands/jira/project.rb +13 -0
- data/lib/firespring_dev_commands/jira/user/type.rb +20 -0
- data/lib/firespring_dev_commands/jira/user.rb +31 -0
- data/lib/firespring_dev_commands/jira.rb +78 -0
- data/lib/firespring_dev_commands/logger.rb +8 -0
- data/lib/firespring_dev_commands/node/audit.rb +39 -0
- data/lib/firespring_dev_commands/node.rb +107 -0
- data/lib/firespring_dev_commands/php/audit.rb +71 -0
- data/lib/firespring_dev_commands/php.rb +109 -0
- data/lib/firespring_dev_commands/rake.rb +24 -0
- data/lib/firespring_dev_commands/ruby/audit.rb +30 -0
- data/lib/firespring_dev_commands/ruby.rb +113 -0
- data/lib/firespring_dev_commands/second.rb +22 -0
- data/lib/firespring_dev_commands/tar/pax_header.rb +49 -0
- data/lib/firespring_dev_commands/tar/type_flag.rb +49 -0
- data/lib/firespring_dev_commands/tar.rb +149 -0
- data/lib/firespring_dev_commands/templates/aws.rb +84 -0
- data/lib/firespring_dev_commands/templates/base_interface.rb +54 -0
- data/lib/firespring_dev_commands/templates/ci.rb +138 -0
- data/lib/firespring_dev_commands/templates/docker/application.rb +177 -0
- data/lib/firespring_dev_commands/templates/docker/default.rb +200 -0
- data/lib/firespring_dev_commands/templates/docker/node/application.rb +145 -0
- data/lib/firespring_dev_commands/templates/docker/php/application.rb +190 -0
- data/lib/firespring_dev_commands/templates/docker/ruby/application.rb +146 -0
- data/lib/firespring_dev_commands/templates/eol.rb +23 -0
- data/lib/firespring_dev_commands/templates/git.rb +147 -0
- data/lib/firespring_dev_commands/version.rb +11 -0
- data/lib/firespring_dev_commands.rb +21 -0
- 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,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
|