firespring_dev_commands 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|