├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── bin
├── command-config-generator
├── generate_sfn_docs
└── sfn
├── docs
├── README.md
├── callbacks.md
├── command-config.md
├── commands.md
├── configuration.md
├── images
│ ├── d_create.png
│ ├── d_create_prompt.png
│ ├── d_describe.png
│ ├── d_destroy.png
│ ├── d_diff.png
│ ├── d_events.png
│ ├── d_inspect-attribute.png
│ ├── d_inspect-nodes.png
│ ├── d_list.png
│ ├── d_update.png
│ └── d_validate.png
├── lint.md
├── overview.md
├── sparkle-packs.md
├── usage.md
└── v
│ ├── 0.3.2
│ └── marked.js
│ ├── bootstrap.min.css
│ ├── finalizer.css
│ ├── highlight.min.css
│ ├── highlight.min.js
│ ├── jquery-2.1.3.min.js
│ └── loader.js
├── img
└── sfn.jpg
├── lib
├── sfn.rb
└── sfn
│ ├── api_provider.rb
│ ├── api_provider
│ ├── google.rb
│ └── terraform.rb
│ ├── cache.rb
│ ├── callback.rb
│ ├── callback
│ ├── aws_assume_role.rb
│ ├── aws_mfa.rb
│ └── stack_policy.rb
│ ├── command.rb
│ ├── command
│ ├── conf.rb
│ ├── create.rb
│ ├── describe.rb
│ ├── destroy.rb
│ ├── diff.rb
│ ├── events.rb
│ ├── export.rb
│ ├── graph.rb
│ ├── graph
│ │ ├── aws.rb
│ │ ├── provider.rb
│ │ └── terraform.rb
│ ├── import.rb
│ ├── init.rb
│ ├── inspect.rb
│ ├── lint.rb
│ ├── list.rb
│ ├── plan.rb
│ ├── print.rb
│ ├── promote.rb
│ ├── realize.rb
│ ├── trace.rb
│ ├── update.rb
│ └── validate.rb
│ ├── command_module.rb
│ ├── command_module
│ ├── base.rb
│ ├── callbacks.rb
│ ├── planning.rb
│ ├── stack.rb
│ └── template.rb
│ ├── config.rb
│ ├── config
│ ├── conf.rb
│ ├── create.rb
│ ├── describe.rb
│ ├── destroy.rb
│ ├── diff.rb
│ ├── events.rb
│ ├── export.rb
│ ├── graph.rb
│ ├── import.rb
│ ├── init.rb
│ ├── inspect.rb
│ ├── lint.rb
│ ├── list.rb
│ ├── plan.rb
│ ├── print.rb
│ ├── promote.rb
│ ├── realize.rb
│ ├── trace.rb
│ ├── update.rb
│ └── validate.rb
│ ├── error.rb
│ ├── lint.rb
│ ├── lint
│ ├── definition.rb
│ ├── rule.rb
│ └── rule_set.rb
│ ├── monkey_patch.rb
│ ├── monkey_patch
│ ├── stack.rb
│ └── stack
│ │ ├── azure.rb
│ │ └── google.rb
│ ├── planner.rb
│ ├── planner
│ └── aws.rb
│ ├── provider.rb
│ ├── utils.rb
│ ├── utils
│ ├── debug.rb
│ ├── json.rb
│ ├── object_storage.rb
│ ├── output.rb
│ ├── path_selector.rb
│ ├── ssher.rb
│ ├── stack_exporter.rb
│ ├── stack_parameter_scrubber.rb
│ └── stack_parameter_validator.rb
│ └── version.rb
├── sfn.gemspec
└── test
├── helper.rb
├── rspecs.rb
├── rspecs
├── lib
│ ├── callback
│ │ └── stack_policy_rspec.rb
│ └── command_module
│ │ ├── callbacks_rspec.rb
│ │ └── stack_rspec.rb
└── sfn_rspec.rb
└── specs
├── command
├── create_spec.rb
├── describe_spec.rb
├── lint_spec.rb
├── print_spec.rb
└── sparkleformation
│ ├── dummy.rb
│ ├── dummy_azure.rb
│ ├── dummy_google.rb
│ ├── lint_invalid.rb
│ ├── lint_valid.rb
│ ├── nested_dummy.rb
│ ├── nested_dummy_azure.rb
│ └── nested_dummy_google.rb
├── command_module
├── stack_spec.rb
└── template_spec.rb
├── config
├── fail-auto-json
│ └── .sfn
├── fail-auto-ruby
│ └── .sfn
├── fail-auto-yaml
│ └── .sfn
├── json-ext
│ └── .sfn.json
├── no-ext
│ └── .sfn
├── ruby-ext
│ └── .sfn.rb
└── yaml-ext
│ └── .sfn
├── config_spec.rb
├── planner_spec.rb
├── sfn_bin_spec.rb
└── utils
└── stack_parameter_validator_spec.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | .*
2 | !.gitignore
3 | *.gem
4 | Gemfile.lock
5 | !*.sfn
6 | vendor*
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.3
4 | - 2.4
5 | - 2.5
6 | - 2.6
7 | script: bundle exec rake
8 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Branches
4 |
5 | ### `master` branch
6 |
7 | The master branch is the current stable released version.
8 |
9 | ### `develop` branch
10 |
11 | The develop branch is the current edge of development.
12 |
13 | ## Pull requests
14 |
15 | * https://github.com/sparkleformation/sfn/pulls
16 |
17 | Please base all pull requests off the `develop` branch. Merges to
18 | `master` only occur through the `develop` branch. Pull requests
19 | based on `master` will likely be cherry picked.
20 |
21 | ## Tests
22 |
23 | Add a test to your changes.
24 | Tests can be run with bundler:
25 |
26 | ```
27 | bundle
28 | bundle exec rake
29 | ```
30 |
31 | ## Issues
32 |
33 | Need to report an issue? Use the github issues:
34 |
35 | * https://github.com/sparkleformation/sfn/issues
36 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # SparkleFormation CLI
4 |
5 | SparkleFormation command line interface for interacting
6 | with orchestration APIs.
7 |
8 | ## API Compatibility
9 |
10 | * AWS
11 | * Azure
12 | * Google
13 | * Heat
14 | * OpenStack
15 | * Rackspace
16 | * Terraform
17 |
18 | ## Documentation
19 |
20 | * [User Documentation](http://www.sparkleformation.io/docs/sfn/)
21 | * [sfn API Documentation](http://www.sparkleformation.io/docs/sfn/)
22 |
23 | # Info
24 |
25 | * Repository: https://github.com/sparkleformation/sfn
26 | * Website: http://www.sparkleformation.io/docs/sfn/
27 | * Mailing List: https://groups.google.com/forum/#!forum/sparkleformation
28 | * IRC: [#sparkleformation @ Freenode](https://webchat.freenode.net/?channels=#sparkleformation)
29 | * Gitter: [](https://gitter.im/SparkleFormation/sparkleformation?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
30 |
31 |
32 | [miasma]: http://miasma-rb.github.io/miasma/
33 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/setup'
2 | require 'rake/testtask'
3 | require 'rspec/core/rake_task'
4 |
5 | RSpec::Core::RakeTask.new(:spec) do |task|
6 | task.rspec_opts = "--pattern test/rspecs/**{,/*/**}/*_rspec.rb"
7 | end
8 |
9 | Rake::TestTask.new do |test|
10 | test.pattern = 'test/**/*_spec.rb'
11 | test.verbose = true
12 | end
13 |
14 | namespace :rufo do
15 | desc 'Validate Ruby file formatting'
16 | task :validate => [] do
17 | base_path = File.dirname(__FILE__)
18 | [
19 | File.join(base_path, 'lib'),
20 | File.join(base_path, 'test'),
21 | ].each do |path|
22 | if !system("rufo -c #{path}")
23 | $stderr.puts "Files in #{path} directory require formatting!"
24 | $stderr.puts " - Run `rake rufo:fmt`"
25 | exit -1
26 | end
27 | end
28 | end
29 |
30 | desc 'Format Ruby files in this project'
31 | task :fmt => [] do
32 | base_path = File.dirname(__FILE__)
33 | [
34 | File.join(base_path, 'lib'),
35 | File.join(base_path, 'test'),
36 | ].each do |path|
37 | $stdout.puts "Formatting files in #{path} directory..."
38 | system("rufo #{path}")
39 | if $?.exitstatus != 0 && $?.exitstatus != 3
40 | $stderr.puts "ERROR: Formatting files in #{path} failed!"
41 | exit -1
42 | end
43 | end
44 | $stdout.puts " -> File formatting complete!"
45 | end
46 | end
47 |
48 | desc 'Run all tests'
49 | task :default => [] do
50 | Rake::Task['rufo:validate'].invoke
51 | Rake::Task[:spec].invoke
52 | Rake::Task[:test].invoke
53 | end
54 |
--------------------------------------------------------------------------------
/bin/command-config-generator:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "sfn"
4 |
5 | print "Generating command and configuration page..."
6 |
7 | klasses = Sfn::Config.constants.sort.map do |const|
8 | klass = Sfn::Config.const_get(const)
9 | klass if klass.is_a?(Class) && klass.respond_to?(:attributes)
10 | end.compact
11 |
12 | file = File.open(File.join(File.dirname(__FILE__), "..", "docs", "command-config.md"), "w")
13 |
14 | file.puts <<-EOS
15 | ---
16 | title: "Configuration"
17 | weight: 6
18 | anchors:
19 | EOS
20 |
21 | klasses.each do |klass|
22 | file.puts " - title: \"#{klass.name.split("::").last} Command\""
23 | file.puts " url: \"##{klass.name.split("::").last.downcase}-command\""
24 | end
25 | file.puts "---"
26 | file.puts <<-EOS
27 |
28 | # Command configurations
29 |
30 | This lists commands and the options which the commands accept. It also
31 | includes the valid types which the options will accept. Options can be
32 | set with the `.sfn` configuration file to apply default values which
33 | can be overridden on the CLI. Options defined within the `.sfn` configuration
34 | file are the option name with `-` characters replaced with `_`.
35 |
36 | For example, the option `--apply-mapping` can be defined in the `.sfn`
37 | configuration file as:
38 |
39 | ~~~ruby
40 | Configuration.new do
41 | apply_mapping true
42 | end
43 | ~~~
44 |
45 | EOS
46 |
47 | klasses.each do |klass|
48 | file.puts "## #{klass.name.split("::").last} Command"
49 | file.puts
50 | file.puts "~~~"
51 | file.puts "$ sfn #{klass.name.split("::").last.downcase}"
52 | file.puts "~~~"
53 | file.puts "\n"
54 | file.puts "| Option | Attribute | Value"
55 | file.puts "|--------|-----------|------"
56 | klass.attributes.sort_by(&:first).map do |name, value|
57 | file.puts "| `--#{name.to_s.tr("_", "-")}` | Description | #{value[:description]} |"
58 | file.puts "| | Valid | `#{[value[:type]].flatten.compact.join("`, `")}` |"
59 | file.puts "| | Default | #{value[:default].inspect unless value[:default].nil?}|\n"
60 | end
61 | file.puts
62 | end
63 |
64 | puts "done"
65 |
--------------------------------------------------------------------------------
/bin/generate_sfn_docs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'fileutils'
3 |
4 | unless(system("yardoc"))
5 | $stderr.puts 'ACK: Failed to create docs!'
6 | exit -1
7 | end
8 |
9 | FileUtils.mkdir_p('doc/UserDocs')
10 |
11 | Dir.glob('docs/**/*').each do |path|
12 | next unless File.file?(path)
13 | content = File.read(path)
14 | rel_path = path.sub(/.*?docs\//, '')
15 | new_path = File.join('doc/UserDocs', rel_path)
16 | user_doc_root = (['..'] * rel_path.scan('/').size).join('/')
17 | unless(user_doc_root.to_s.empty?)
18 | user_doc_root << '/'
19 | end
20 | FileUtils.mkdir_p(File.dirname(new_path))
21 | File.open(new_path, 'w') do |file|
22 | file.puts content
23 | end
24 | if(new_path.end_with?('.md'))
25 | File.open(new_path.sub('.md', '.html'), 'w') do |file|
26 | file.print "
SparkleFormation CLI User Documentation"
27 | file.print ""
28 | file.print ""
29 | end
30 | end
31 | end
32 |
33 | File.open('doc/UserDocs/index.html', 'w') do |file|
34 | file.puts ''
35 | end
36 |
37 | FileUtils.mkdir('doc/img')
38 | FileUtils.cp('img/sfn.jpg', 'doc/img/')
39 |
40 | puts 'done.'
41 |
--------------------------------------------------------------------------------
/bin/sfn:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "bogo-cli"
4 | require "sfn"
5 |
6 | if defined?(Bundler)
7 | begin
8 | Bundler.require(:sfn)
9 | rescue Bundler::GemfileNotFound
10 | # ignore load error
11 | end
12 | end
13 |
14 | Bogo::Cli::Setup.define do
15 | on :v, :version, "Print version " do
16 | puts "sfn - SparkleFormation CLI - [Version: #{Sfn::VERSION}]"
17 | exit
18 | end
19 |
20 | Sfn::Config.constants.map do |konst|
21 | const = Sfn::Config.const_get(konst)
22 | if const.is_a?(Class) && const.ancestors.include?(Bogo::Config)
23 | const
24 | end
25 | end.compact.sort_by(&:to_s).each do |klass|
26 | klass_name = klass.name.split("::").last.downcase
27 |
28 | command klass_name do
29 | if klass.const_defined?(:DESCRIPTION)
30 | description klass.const_get(:DESCRIPTION)
31 | end
32 |
33 | Sfn::Config.options_for(klass).each do |name, info|
34 | on_name = info[:boolean] ? info[:long] : "#{info[:long]}="
35 | opts = Hash.new.tap do |o|
36 | o[:default] = info[:default] if info.has_key?(:default)
37 | if info[:multiple]
38 | o[:as] = Array
39 | o[:delimiter] = nil
40 | end
41 | end
42 | if info[:short]
43 | on info[:short], on_name, info[:description], opts
44 | else
45 | on on_name, info[:description], opts
46 | end
47 | end
48 |
49 | run do |opts, args|
50 | command = Bogo::Utility.constantize(klass.to_s.sub("Config", "Command")).new(opts, args)
51 | begin
52 | command.execute!
53 | rescue Bogo::Ui::ConfirmationDeclined
54 | command.ui.error "Confirmation declined!"
55 | exit 1
56 | end
57 | end
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ## Table of Contents
2 |
3 | - [Overview](overview.md)
4 | - [Feature Summary](overview.md#feature-summary)
5 | - [Installation](overview.md#installation)
6 | - [Configuration](configuration.md)
7 | - [sfn based](configuration.md#sfn-based)
8 | - [knife based](configuration.md#knife-based)
9 | - [Usage](usage.md)
10 | - [General Commands](usage.md#commands)
11 | - [Commands List](command-config.md)
12 | - [Callbacks](callbacks.md)
13 | - [Enabling Callbacks](callbacks.md#enabling-callbacks)
14 | - [Builtin Callbacks](callbacks.md#builtin-callbacks)
15 | - [Custom Callbacks](callbacks.md#custom-callbacks)
16 | - [SparklePacks](sparkle-packs.md)
17 |
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Configuration"
3 | weight: 2
4 | anchors:
5 | - title: "sfn-based"
6 | url: "#sfn-based"
7 | - title: "knife-based"
8 | url: "#knife-based"
9 | - title: "Generate sfn configuration"
10 | url: "#generate-sfn-configuration"
11 | - title: "configuration-options"
12 | url: "#configuration-options"
13 | ---
14 |
15 |
16 | ## Configuration
17 |
18 | The configuration location of the `sfn` command is
19 | dependent on the invocation method used. Since the
20 | CLI application can be invoked as a standalone
21 | application, or as a knife subcommand, two styles
22 | of configuration are supported.
23 |
24 | ### `sfn`-based
25 |
26 | Configuration for the `sfn` standalone application
27 | utilizes the bogo-config library. This allows the
28 | configuration file to be defined in multiple formats.
29 | Supported formats:
30 |
31 | * Ruby
32 | * YAML
33 | * JSON
34 | * XML
35 |
36 | The configuration is contained within a file named
37 | `.sfn`.
38 |
39 | #### Generate sfn configuration
40 |
41 | The `sfn` command provides a `conf` subcommand. By
42 | default this command will display current local
43 | configuration values. It can also be used to generate
44 | an initial configuration file which can then be
45 | customized:
46 |
47 | ~~~
48 | $ sfn conf --generate
49 | ~~~
50 |
51 | This will create a new `.sfn` file in the working directory.
52 |
53 | ### `knife`-based
54 |
55 | The `sfn` application includes a plugin for the
56 | [knife][knife] CLI tool. Configuration can be
57 | provided in the `.chef/knife.rb` file and commands
58 | can be accessed via:
59 |
60 | ~~~
61 | $ knife sparkleformation --help
62 | ~~~
63 |
64 | [knife]: https://docs.chef.io/knife.html
65 |
--------------------------------------------------------------------------------
/docs/images/d_create.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sparkleformation/sfn/cc601844a4208613803f784759242e88ae6c3546/docs/images/d_create.png
--------------------------------------------------------------------------------
/docs/images/d_create_prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sparkleformation/sfn/cc601844a4208613803f784759242e88ae6c3546/docs/images/d_create_prompt.png
--------------------------------------------------------------------------------
/docs/images/d_describe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sparkleformation/sfn/cc601844a4208613803f784759242e88ae6c3546/docs/images/d_describe.png
--------------------------------------------------------------------------------
/docs/images/d_destroy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sparkleformation/sfn/cc601844a4208613803f784759242e88ae6c3546/docs/images/d_destroy.png
--------------------------------------------------------------------------------
/docs/images/d_diff.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sparkleformation/sfn/cc601844a4208613803f784759242e88ae6c3546/docs/images/d_diff.png
--------------------------------------------------------------------------------
/docs/images/d_events.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sparkleformation/sfn/cc601844a4208613803f784759242e88ae6c3546/docs/images/d_events.png
--------------------------------------------------------------------------------
/docs/images/d_inspect-attribute.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sparkleformation/sfn/cc601844a4208613803f784759242e88ae6c3546/docs/images/d_inspect-attribute.png
--------------------------------------------------------------------------------
/docs/images/d_inspect-nodes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sparkleformation/sfn/cc601844a4208613803f784759242e88ae6c3546/docs/images/d_inspect-nodes.png
--------------------------------------------------------------------------------
/docs/images/d_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sparkleformation/sfn/cc601844a4208613803f784759242e88ae6c3546/docs/images/d_list.png
--------------------------------------------------------------------------------
/docs/images/d_update.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sparkleformation/sfn/cc601844a4208613803f784759242e88ae6c3546/docs/images/d_update.png
--------------------------------------------------------------------------------
/docs/images/d_validate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sparkleformation/sfn/cc601844a4208613803f784759242e88ae6c3546/docs/images/d_validate.png
--------------------------------------------------------------------------------
/docs/lint.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Lint"
3 | weight: 8
4 | anchors:
5 | - title: "Lint Framework"
6 | url: "#lint-framework"
7 | - title: "Composition"
8 | url: "#composition"
9 | - title: "Usage"
10 | url: "#usage"
11 | ---
12 |
13 | ## Lint
14 |
15 | The lint framework built within the sfn tool utilizes the [JMESPath][jmespath] query language
16 | for identifying patterns and apply validation rules.
17 |
18 | ### Lint Framework
19 |
20 | A rule set is a named collection of rules to be applied to a template. Each rule
21 | is composed of one or more definitions. A rule passes only if _all_ definitions
22 | can be successfully applied. Linting related classes:
23 |
24 | * `Sfn::Lint::RuleSet`
25 | * `Sfn::Lint::Rule`
26 | * `Sfn::Lint::Definition`
27 |
28 | ### Composition
29 |
30 | #### Long Form
31 |
32 | ##### `Sfn::Lint::Definition`
33 |
34 | Definitions define a search expression to be applied to a given template. The search
35 | expression is a [JMESPath compatible query expression][jmespath-expr]. The matches
36 | of the search expression are then processed. If the results are valid, a `true` result
37 | is expected. If the results are invalid, a `false` value is expected, or an `Array`
38 | value is expected which provides the list of invalid items.
39 |
40 | ~~~ruby
41 | Sfn::Lint::Definition.new('Resources.[*][0][*].Type') do |matches, template|
42 | unless(search.nil?)
43 | result = search.find_all{|i| !i.start_with?('AWS')}
44 | result.empty? ? true : result
45 | else
46 | true
47 | end
48 | end
49 | ~~~
50 |
51 | The processing block is provided two arguments. The first is the match result of the
52 | search expression. The second is the full template `Hash` that is being processed.
53 |
54 | ##### `Sfn::Lint::Rule`
55 |
56 | Rules are composed of definitions. When a rule is applied to a template it will only
57 | pass if _all_ definitions are successfully applied. A rule also includes a failure
58 | message to provide user context of the failure.
59 |
60 | ~~~ruby
61 | definition = Sfn::Lint::Definition.new('Resources.[*][0][*].Type') do |matches, template|
62 | unless(search.nil?)
63 | result = search.find_all{|i| !i.start_with?('AWS')}
64 | result.empty? ? true : result
65 | else
66 | true
67 | end
68 | end
69 |
70 | Sfn::Lint::Rule.new(
71 | :aws_resources_only,
72 | [definition],
73 | 'All types must be within AWS root namespace'
74 | )
75 | ~~~
76 |
77 | ##### `Sfn::Lint::RuleSet`
78 |
79 | A rule set is a named collection of rules. It allows logically grouping related
80 | rules together. Rule sets are the entry point of linting actions on templates. Once
81 | a rule set has been created, it must then be registered to be made available.
82 |
83 | ~~~ruby
84 | definition = Sfn::Lint::Definition.new('Resources.[*][0][*].Type') do |matches, template|
85 | unless(search.nil?)
86 | result = search.find_all{|i| !i.start_with?('AWS')}
87 | result.empty? ? true : result
88 | else
89 | true
90 | end
91 | end
92 |
93 | rule = Sfn::Lint::Rule.new(
94 | :aws_resources_only,
95 | [definition],
96 | 'All types must be within AWS root namespace'
97 | )
98 |
99 | rule_set = Sfn::Lint::RuleSet.new(:aws_rules, [rule])
100 | Sfn::Lint::RuleSet.register(rule_set)
101 | ~~~
102 |
103 | #### Short Form
104 |
105 | Rule sets can also be created using a generator which reduces the amount of effort required
106 | for composition. The same rule set defined above can be created using the `RuleSet.build`
107 | generator:
108 |
109 | ~~~ruby
110 | rule_set = Sfn::Lint::RuleSet.build(:aws_rules) do
111 | rule :aws_resources_only do
112 | definition 'Resources.[*][0][*].Type' do |search|
113 | unless(search.nil?)
114 | result = search.find_all{|i| !i.start_with?('AWS')}
115 | result.empty? ? true : result
116 | else
117 | true
118 | end
119 | end
120 |
121 | fail_message 'All types must be within AWS root namespace'
122 | end
123 | end
124 |
125 | Sfn::Lint::RuleSet.register(rule_set)
126 | ~~~
127 |
128 | ### Usage
129 |
130 | Linting functionality is available via the `lint` command. The only requirement of the `lint`
131 | command is a template provided by the `--file` flag:
132 |
133 | ~~~
134 | $ sfn lint --file my-template
135 | ~~~
136 |
137 | By default all registered rule sets applicable to the template will be applied. Rule sets can
138 | be disabled by name to prevent them from being applied:
139 |
140 | ~~~
141 | $ sfn lint --file my-template --disable-rule-set aws_rules
142 | ~~~
143 |
144 | or you can explicitly specify what rule sets should be applied:
145 |
146 | ~~~
147 | $ sfn lint --file my-template --enable-rule-set aws_rules
148 | ~~~
149 |
150 | #### Local rule sets
151 |
152 | Rule sets can be created for a local project. These rule sets are kept within a directory, and
153 | should be defined as a single rule set per file. For example, having a directory `tests/lint`
154 | the rule set can be created:
155 |
156 | ~~~ruby
157 | # tests/lint/resource_type_check.rb
158 | RuleSet.build(:aws_rules) do
159 | rule :aws_resources_only do
160 | definition 'Resources.[*][0][*].Type' do |search|
161 | unless(search.nil?)
162 | result = search.find_all{|i| !i.start_with?('AWS')}
163 | result.empty? ? true : result
164 | else
165 | true
166 | end
167 | end
168 |
169 | fail_message 'All types must be within AWS root namespace'
170 | end
171 | end
172 | ~~~
173 |
174 | To include the local rule sets the target directory must be provided:
175 |
176 | ~~~
177 | $ sfn lint --file my-template --lint-directory tests/lint
178 | ~~~
179 |
180 | and if _only_ local rule sets should be applied, it is possible to disable all registered
181 | rule sets:
182 |
183 | ~~~
184 | $ sfn lint --file my-template --lint-directory tests/lint --local-rule-sets-only
185 | ~~~
186 |
187 | [jmespath]: http://jmespath.org/
188 | [jmespath-expr]: http://jmespath.org/specification.html
--------------------------------------------------------------------------------
/docs/overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Overview"
3 | weight: 1
4 | ---
5 |
6 | # Overview
7 |
8 | The SparkleFormation CLI (`sfn`) is a Ruby based command line interface
9 | for interacting with remote orchestration API. It is an application
10 | implementation of the SparkleFormation library and provides access to
11 | all the underlying features provided by the SparkleFormation library.
12 |
13 | ## Feature Summary
14 |
15 | Notable features available via the SparkleFormation CLI:
16 |
17 | - SparkleFormation template processing
18 | - Template processing helpers
19 | - Custom callback support
20 | - Remote orchestration API support
21 | - AWS CloudFormation
22 | - Eucalyptus
23 | - Rackspace Orchestration
24 | - OpenStack Heat
25 | - Google Cloud Deployment Manager
26 | - Chef `knife` plugin support
27 | - Deep resource inspection
28 |
29 | ## Installation
30 |
31 | The SparkleFormation CLI is available from [Ruby Gems](https://rubygems.org/gems/sfn). To install, simply execute:
32 |
33 | ~~~sh
34 | $ gem install sfn
35 | ~~~
36 |
37 | or, if you use [Bundler](http://bundler.io/), add the following to your Gemfile:
38 |
39 | ~~~sh
40 | gem 'sfn'
41 | ~~~
42 |
43 | See [Configuration](configuration.md) and [Usage](usage.md) for further instructions.
44 |
--------------------------------------------------------------------------------
/docs/sparkle-packs.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "SparklePacks"
3 | weight: 6
4 | anchors:
5 | - title: "Enabling SparklePacks"
6 | url: "#enabling-sparklepacks"
7 | ---
8 |
9 | ## What is a SparklePack?
10 |
11 | SparklePacks are implemented as a feature of the SparkleFormation library,
12 | providing a means to package SparkleFormation building blocks
13 | and templates as reusable, redistributable software artifacts.
14 | A SparklePack may package up any combination of SparkleFormation
15 | [building blocks](http://www.sparkleformation.io/docs/sparkle_formation/building-blocks.html) and templates.
16 |
17 | sfn supports loading SparklePacks distributed as [Ruby gems](http://www.sparkleformation.io/docs/sparkle_formation/sparkle-packs.html#distribution).
18 | You can find published SparklePacks on the RubyGems site by
19 | [searching for the sparkle-pack prefix](https://rubygems.org/search?query=sparkle-pack).
20 |
21 | ### Enabling SparklePacks
22 |
23 | The following examples use the [sparkle-pack-aws-availability-zones](https://rubygems.org/gems/sparkle-pack-aws-availability-zones) gem.
24 | In reviewing [the source code of that project on Github](https://github.com/hw-labs/sparkle-pack-aws-availability-zones),
25 | note that it provides a [`zones` registry](https://github.com/hw-labs/sparkle-pack-aws-availability-zones/blob/v0.1.2/lib/sparkleformation/registry/get_azs.rb)
26 | which uses the aws-sdk-core library to return an array of available AZs.
27 |
28 | When using sfn with Bundler, we'll add any SparklePacks we
29 | want to enable to the `sfn` group in our Gemfile:
30 |
31 | ~~~ruby
32 | # Gemfile
33 | source 'https://rubygems.org'
34 |
35 | gem 'sfn'
36 |
37 | group :sfn do
38 | gem 'sparkle-pack-aws-availability-zones'
39 | end
40 | ~~~
41 |
42 | After running `bundle`, the SparklePack is installed but not yet enabled:
43 |
44 | ~~~
45 | $ cat sparkleformation/zones_test.rb
46 | SparkleFormation.new(:zones_test) do
47 | zones registry!(:zones)
48 | end
49 |
50 | $ bundle exec sfn print --file zones_test
51 | ERROR: SparkleFormation::Error::NotFound::Registry: Failed to locate item named: `zones`
52 | ~~~
53 |
54 | Adding the gem to an array of `sparkle_packs` in
55 | the `.sfn` configuration file will activate it for use:
56 |
57 | ~~~ruby
58 | Configuration.new do
59 | sparkle_pack [ 'sparkle-pack-aws-availability-zones' ]
60 | end
61 | ~~~
62 |
63 | Invoking `zones` registry in the template is now functional:
64 |
65 | ~~~
66 | $ bundle exec sfn print --file zones_test
67 | {
68 | "Zones": [
69 | "us-east-1a",
70 | "us-east-1b",
71 | "us-east-1c",
72 | "us-east-1e"
73 | ]
74 | }
75 | ~~~
--------------------------------------------------------------------------------
/docs/usage.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Usage"
3 | weight: 3
4 | anchors:
5 | - title: "Directory Structure"
6 | url: "#directory-structure"
7 | - title: "Template Commands"
8 | url: "#template-commands"
9 | - title: "Stack Commands"
10 | url: "#stack-commands"
11 | ---
12 |
13 | ## Usage
14 |
15 | The `sfn` command can be invoked in two ways. The
16 | first is directly:
17 |
18 | ~~~
19 | $ sfn --help
20 | ~~~
21 |
22 | The second is via the [knife][knife] plugin:
23 |
24 | ~~~
25 | $ knife sparkleformation --help
26 | ~~~
27 |
28 | Both invocations will generate the same result. The
29 | direct `sfn` command can be preferable as it does not
30 | require loading outside libraries nor does it traverse
31 | the filesystem loading plugins. Because of this, the
32 | direct command will generally be faster than the knife
33 | plugin.
34 |
35 | ### Directory Structure
36 |
37 | The `sfn` command utilizes the [SparkleFormation][sparkle_formation]
38 | library and supports template compilation. To use SparkleFormation,
39 | just create the directory structure within the local project
40 | working directory:
41 |
42 | ~~~
43 | > tree
44 | .
45 | |____sparkleformation
46 | | |____dynamics
47 | | |____components
48 | | |____registry
49 | ~~~
50 |
51 | ### Commands
52 |
53 | #### Template Commands
54 |
55 | These are the commands that support an orchestration template.
56 | By default, `sfn` does not enable the [SparkleFormation][sparkle_formation]
57 | integration. This means that any innvocation when a template is
58 | required _must_ provide a path to the serialized document using
59 | the `--file` option.
60 |
61 | To enable the [SparkleFormation][sparkle_formation] integration
62 | simply include the `---processing` flag, or enable it via the
63 | configuration file:
64 |
65 | ~~~ruby
66 | Configuration.new do
67 | processing true
68 | end
69 | ~~~
70 |
71 | When processing is enabled and no path is provided via the `--file`
72 | option, `sfn` will prompt for template selection allowing the user
73 | to choose from local templates, as well as any templates distributed
74 | in loaded [SparklePacks][sparkle_packs].
75 |
76 | Available template related commands:
77 |
78 | * `sfn create`
79 | * `sfn plan`
80 | * `sfn update`
81 | * `sfn validate`
82 |
83 | The `sfn` command supports the advanced nesting functionality provided
84 | by the [SparkleFormation][sparkle_formation] library. There are two
85 | styles of nesting functionality available: shallow and deep. The required
86 | style can be set via the configuration file:
87 |
88 | ~~~ruby
89 | Configuration.new do
90 | apply_nesting 'deep'
91 | end
92 | ~~~
93 |
94 | The default nesting functionality is `"deep"`. To learn more about
95 | the nesting functionality please refer to the [SparkleFormation nested
96 | stacks][nested_stacks] documentation.
97 |
98 | When using nested stacks, a bucket is required for storage of the
99 | nested stack templates. `sfn` will automatically store nested templates
100 | into the defined bucket, but the bucket name _must_ be provided and
101 | the bucket _must_ exist. The bucket name can be defined within the
102 | configuration:
103 |
104 | ~~~ruby
105 | Configuration.new do
106 | nesting_bucket 'my-nested-templates'
107 | end
108 | ~~~
109 |
110 | #### Stack Commands
111 |
112 | These commands are used for inspection or removal of existing stacks:
113 |
114 | * `sfn describe`
115 | * `sfn inspect`
116 | * `sfn diff`
117 | * `sfn events`
118 | * `sfn destroy`
119 |
120 | While the `describe` command is good for an overview of a stack contents
121 | (resources and outputs), the `inspect` command allows for deeper inspection
122 | of a given stack. The `--attribute` option allows access to the underlying
123 | data model that represents the given resource and can be inspected for
124 | information. The data modeling is provided by the [miasma][miasma] cloud
125 | library which can be referenced for supported methods available. As an
126 | example, given an AWS CloudFormation stack with a single EC2 resource,
127 | the `inspect` command can be used to provide all addresses associated
128 | with the instance:
129 |
130 | ~~~
131 | $ sfn inspect my-stack --attribute 'resources.all.at(0).expand.addresses'
132 | ~~~
133 |
134 | [knife]: https://docs.chef.io/knife.html
135 | [sparkle_formation]: https://github.com/sparkleformation/sparkle_formation
136 | [nested_stacks]: http://www.sparkleformation.io/docs/sparkle_formation/nested-stacks.html
137 | [sparkle_packs]: https://sparkleformation.github.io/sparkle_formation/UserDocs/sparkle-packs.html
138 | [miasma]: https://github.com/miasma-rb/miasma
139 |
--------------------------------------------------------------------------------
/docs/v/finalizer.css:
--------------------------------------------------------------------------------
1 | .markdown-body {
2 | min-width: 200px;
3 | max-width: 790px;
4 | margin: 0 auto;
5 | padding-bottom: 30px;
6 | }
7 |
--------------------------------------------------------------------------------
/docs/v/highlight.min.css:
--------------------------------------------------------------------------------
1 | .hljs{display:block;overflow-x:auto;padding:0.5em;background:#f0f0f0;-webkit-text-size-adjust:none}.hljs,.hljs-subst,.hljs-tag .hljs-title,.nginx .hljs-title{color:black}.hljs-string,.hljs-title,.hljs-constant,.hljs-parent,.hljs-tag .hljs-value,.hljs-rules .hljs-value,.hljs-preprocessor,.hljs-pragma,.haml .hljs-symbol,.ruby .hljs-symbol,.ruby .hljs-symbol .hljs-string,.hljs-template_tag,.django .hljs-variable,.smalltalk .hljs-class,.hljs-addition,.hljs-flow,.hljs-stream,.bash .hljs-variable,.apache .hljs-tag,.apache .hljs-cbracket,.tex .hljs-command,.tex .hljs-special,.erlang_repl .hljs-function_or_atom,.asciidoc .hljs-header,.markdown .hljs-header,.coffeescript .hljs-attribute{color:#800}.smartquote,.hljs-comment,.hljs-annotation,.diff .hljs-header,.hljs-chunk,.asciidoc .hljs-blockquote,.markdown .hljs-blockquote{color:#888}.hljs-number,.hljs-date,.hljs-regexp,.hljs-literal,.hljs-hexcolor,.smalltalk .hljs-symbol,.smalltalk .hljs-char,.go .hljs-constant,.hljs-change,.lasso .hljs-variable,.makefile .hljs-variable,.asciidoc .hljs-bullet,.markdown .hljs-bullet,.asciidoc .hljs-link_url,.markdown .hljs-link_url{color:#080}.hljs-label,.hljs-javadoc,.ruby .hljs-string,.hljs-decorator,.hljs-filter .hljs-argument,.hljs-localvars,.hljs-array,.hljs-attr_selector,.hljs-important,.hljs-pseudo,.hljs-pi,.haml .hljs-bullet,.hljs-doctype,.hljs-deletion,.hljs-envvar,.hljs-shebang,.apache .hljs-sqbracket,.nginx .hljs-built_in,.tex .hljs-formula,.erlang_repl .hljs-reserved,.hljs-prompt,.asciidoc .hljs-link_label,.markdown .hljs-link_label,.vhdl .hljs-attribute,.clojure .hljs-attribute,.asciidoc .hljs-attribute,.lasso .hljs-attribute,.coffeescript .hljs-property,.hljs-phony{color:#88f}.hljs-keyword,.hljs-id,.hljs-title,.hljs-built_in,.css .hljs-tag,.hljs-javadoctag,.hljs-phpdoc,.hljs-dartdoc,.hljs-yardoctag,.smalltalk .hljs-class,.hljs-winutils,.bash .hljs-variable,.apache .hljs-tag,.hljs-type,.hljs-typename,.tex .hljs-command,.asciidoc .hljs-strong,.markdown .hljs-strong,.hljs-request,.hljs-status{font-weight:bold}.asciidoc .hljs-emphasis,.markdown .hljs-emphasis{font-style:italic}.nginx .hljs-built_in{font-weight:normal}.coffeescript .javascript,.javascript .xml,.lasso .markup,.tex .hljs-formula,.xml .javascript,.xml .vbscript,.xml .css,.xml .hljs-cdata{opacity:0.5}
--------------------------------------------------------------------------------
/docs/v/loader.js:
--------------------------------------------------------------------------------
1 | function load_page(){
2 | $.get(
3 | window.location.pathname.replace('.html', '.md'),
4 | function(data){
5 | display_page(data)
6 | },
7 | 'text'
8 | );
9 | }
10 |
11 | function display_page(content){
12 | content = content.replace(/.md/g, '.html');
13 | renderer = new marked.Renderer();
14 | renderer.heading = function(text, level){
15 | escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');
16 |
17 | return '' +
22 | text + '';
23 | }
24 | $('#content').html($('#content').html() + marked(content, {renderer: renderer}));
25 | hljs.initHighlighting();
26 | if(window.location.hash){
27 | $('html, body').scrollTop(
28 | $("a[name='"+ window.location.hash.replace('#', '') + "']").offset().top
29 | );
30 | }
31 | }
32 |
33 | $(document).ready(load_page);
34 | $(document).on('page:load', load_page);
--------------------------------------------------------------------------------
/img/sfn.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sparkleformation/sfn/cc601844a4208613803f784759242e88ae6c3546/img/sfn.jpg
--------------------------------------------------------------------------------
/lib/sfn.rb:
--------------------------------------------------------------------------------
1 | require "sfn/version"
2 | require "miasma"
3 | require "bogo"
4 | require "sparkle_formation"
5 |
6 | module Sfn
7 | autoload :ApiProvider, "sfn/api_provider"
8 | autoload :Callback, "sfn/callback"
9 | autoload :Error, "sfn/error"
10 | autoload :Provider, "sfn/provider"
11 | autoload :Cache, "sfn/cache"
12 | autoload :Config, "sfn/config"
13 | autoload :Export, "sfn/export"
14 | autoload :Utils, "sfn/utils"
15 | autoload :MonkeyPatch, "sfn/monkey_patch"
16 | autoload :Knife, "sfn/knife"
17 | autoload :Command, "sfn/command"
18 | autoload :CommandModule, "sfn/command_module"
19 | autoload :Planner, "sfn/planner"
20 | autoload :Lint, "sfn/lint"
21 | end
22 |
--------------------------------------------------------------------------------
/lib/sfn/api_provider.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module ApiProvider
5 | autoload :Google, "sfn/api_provider/google"
6 | autoload :Terraform, "sfn/api_provider/terraform"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/sfn/api_provider/google.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module ApiProvider
5 | module Google
6 |
7 | # Disable remote template storage
8 | def store_template(*_)
9 | end
10 |
11 | # No formatting required on stack results
12 | def format_nested_stack_results(*_)
13 | {}
14 | end
15 |
16 | # Extract current parameters from parent template
17 | #
18 | # @param stack [SparkleFormation]
19 | # @param stack_name [String]
20 | # @param c_stack [Miasma::Models::Orchestration::Stack]
21 | # @return [Hash]
22 | def extract_current_nested_template_parameters(stack, stack_name, c_stack)
23 | if c_stack && c_stack.data[:parent_stack]
24 | c_stack.data[:parent_stack].sparkleish_template(:remove_wrapper).fetch(
25 | :resources, stack_name, :properties, :parameters, Smash.new
26 | )
27 | elsif stack.parent
28 | val = stack.parent.compile.resources.set!(stack_name).properties
29 | val.nil? ? Smash.new : val._dump
30 | else
31 | Smash.new
32 | end
33 | end
34 |
35 | # Disable parameter validate as we can't adjust them without template modifications
36 | def validate_stack_parameter(*_)
37 | true
38 | end
39 |
40 | # Determine if parameter was set via intrinsic function
41 | #
42 | # @param val [Object]
43 | # @return [TrueClass, FalseClass]
44 | def function_set_parameter?(val)
45 | if val
46 | val.start_with?("$(") || val.start_with?("{{")
47 | end
48 | end
49 |
50 | # Set parameters into parent resource properites
51 | def populate_parameters!(template, opts = {})
52 | result = super
53 | result.each_pair do |key, value|
54 | if template.parent
55 | template.parent.compile.resources.set!(template.name).properties.set!(key, value)
56 | else
57 | template.compile.resources.set!(template.name).properties.set!(key, value)
58 | end
59 | end
60 | {}
61 | end
62 |
63 | # Override requirement of nesting bucket
64 | def validate_nesting_bucket!
65 | true
66 | end
67 |
68 | # Override template content extraction to disable scrub behavior
69 | #
70 | # @param thing [SparkleFormation, Hash]
71 | # @return [Hash]
72 | def template_content(thing, *_)
73 | if thing.is_a?(SparkleFormation)
74 | config[:sparkle_dump] ? thing.sparkle_dump : thing.dump
75 | else
76 | thing
77 | end
78 | end
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/lib/sfn/api_provider/terraform.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module ApiProvider
5 | module Terraform
6 |
7 | # Disable remote template storage
8 | def store_template(*_)
9 | end
10 |
11 | # No formatting required on stack results
12 | def format_nested_stack_results(*_)
13 | {}
14 | end
15 |
16 | # Extract current parameters from parent template
17 | #
18 | # @param stack [SparkleFormation]
19 | # @param stack_name [String]
20 | # @param c_stack [Miasma::Models::Orchestration::Stack]
21 | # @return [Hash]
22 | def extract_current_nested_template_parameters(stack, stack_name, c_stack)
23 | if c_stack && c_stack.data[:parent_stack]
24 | c_stack.data[:parent_stack].sparkleish_template(:remove_wrapper).fetch(
25 | :resources, stack_name, :properties, :parameters, Smash.new
26 | )
27 | elsif stack.parent
28 | val = stack.parent.compile.resources.set!(stack_name).properties
29 | val.nil? ? Smash.new : val._dump
30 | else
31 | Smash.new
32 | end
33 | end
34 |
35 | # Disable parameter validate as we can't adjust them without template modifications
36 | def validate_stack_parameter(*_)
37 | true
38 | end
39 |
40 | # Determine if parameter was set via intrinsic function
41 | #
42 | # @param val [Object]
43 | # @return [TrueClass, FalseClass]
44 | def function_set_parameter?(val)
45 | if val
46 | val.start_with?("${")
47 | end
48 | end
49 |
50 | # Override requirement of nesting bucket
51 | def validate_nesting_bucket!
52 | true
53 | end
54 |
55 | # Override template content extraction to disable scrub behavior
56 | #
57 | # @param thing [SparkleFormation, Hash]
58 | # @return [Hash]
59 | def template_content(thing, *_)
60 | if thing.is_a?(SparkleFormation)
61 | config[:sparkle_dump] ? thing.sparkle_dump : thing.dump
62 | else
63 | thing
64 | end
65 | end
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/sfn/callback.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | # Interface for injecting custom functionality
5 | class Callback
6 | autoload :AwsAssumeRole, "sfn/callback/aws_assume_role"
7 | autoload :AwsMfa, "sfn/callback/aws_mfa"
8 | autoload :StackPolicy, "sfn/callback/stack_policy"
9 |
10 | # @return [Bogo::Ui]
11 | attr_reader :ui
12 | # @return [Smash]
13 | attr_reader :config
14 | # @return [Array] CLI arguments
15 | attr_reader :arguments
16 | # @return [Miasma::Models::Orchestration] remote API
17 | attr_reader :api
18 |
19 | # Create a new callback instance
20 | #
21 | # @param ui [Bogo::Ui]
22 | # @param config [Smash] configuration hash
23 | # @param arguments [Array] arguments from the CLI
24 | # @param api [Provider] API connection
25 | #
26 | # @return [self]
27 | def initialize(ui, config, arguments, api)
28 | @ui = ui
29 | @config = config
30 | @arguments = arguments
31 | @api = api
32 | end
33 |
34 | # Wrap action within status text
35 | #
36 | # @param msg [String] action text
37 | # @yieldblock action to perform
38 | # @return [Object] result of yield
39 | def run_action(msg)
40 | ui.info("#{msg}... ", :nonewline)
41 | begin
42 | result = yield
43 | ui.puts ui.color("complete!", :green, :bold)
44 | result
45 | rescue => e
46 | ui.puts ui.color("error!", :red, :bold)
47 | ui.error "Reason - #{e}"
48 | raise
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/sfn/callback/aws_assume_role.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Callback
5 | # Support for AWS STS role credential caching
6 | class AwsAssumeRole < Callback
7 |
8 | # Items to cache in local file
9 | STS_STORE_ITEMS = [
10 | :aws_sts_token,
11 | :aws_sts_access_key_id,
12 | :aws_sts_secret_access_key,
13 | :aws_sts_token_expires,
14 | ]
15 |
16 | # Prevent callback output to user
17 | def quiet
18 | true
19 | end
20 |
21 | # Inject STS related configuration into
22 | # API provider credentials
23 | def after_config(*_)
24 | if enabled? && config.fetch(:credentials, :aws_sts_role_arn)
25 | load_stored_session
26 | end
27 | end
28 |
29 | # Store session token if available for
30 | # later use
31 | def after(*_)
32 | if enabled?
33 | if api.connection.aws_sts_role_arn && api.connection.aws_sts_token
34 | path = config.fetch(:aws_assume_role, :cache_file, ".sfn-aws")
35 | FileUtils.touch(path)
36 | File.chmod(0600, path)
37 | values = load_stored_values(path)
38 | STS_STORE_ITEMS.map do |key|
39 | values[key] = api.connection.data[key]
40 | end
41 | File.open(path, "w") do |file|
42 | file.puts MultiJson.dump(values)
43 | end
44 | end
45 | end
46 | end
47 |
48 | # @return [TrueClass, FalseClass]
49 | def enabled?
50 | config.fetch(:aws_assume_role, :status, "enabled").to_s == "enabled"
51 | end
52 |
53 | # Load stored configuration data into the api connection
54 | #
55 | # @return [TrueClass, FalseClass]
56 | def load_stored_session
57 | path = config.fetch(:aws_assume_role, :cache_file, ".sfn-aws")
58 | if File.exists?(path)
59 | values = load_stored_values(path)
60 | STS_STORE_ITEMS.each do |key|
61 | api.connection.data[key] = values[key]
62 | end
63 | if values[:aws_sts_token_expires]
64 | begin
65 | api.connection.data[:aws_sts_token_expires] = Time.parse(values[:aws_sts_token_expires])
66 | rescue
67 | end
68 | end
69 | true
70 | else
71 | false
72 | end
73 | end
74 |
75 | # Load stored values
76 | #
77 | # @param path [String]
78 | # @return [Hash]
79 | def load_stored_values(path)
80 | begin
81 | if File.exists?(path)
82 | MultiJson.load(File.read(path)).to_smash
83 | else
84 | Smash.new
85 | end
86 | rescue MultiJson::ParseError
87 | Smash.new
88 | end
89 | end
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/lib/sfn/callback/aws_mfa.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Callback
5 | # Support for AWS MFA
6 | class AwsMfa < Callback
7 |
8 | # Items to cache in local file
9 | SESSION_STORE_ITEMS = [
10 | :aws_sts_session_token,
11 | :aws_sts_session_access_key_id,
12 | :aws_sts_session_secret_access_key,
13 | :aws_sts_session_token_expires,
14 | ]
15 |
16 | # Prevent callback output to user
17 | def quiet
18 | true
19 | end
20 |
21 | # Inject MFA related configuration into
22 | # API provider credentials
23 | def after_config(*_)
24 | if enabled?
25 | load_stored_session
26 | api.connection.aws_sts_session_token_code = method(:prompt_for_code)
27 | end
28 | end
29 |
30 | # Store session token if available for
31 | # later use
32 | def after(*_)
33 | if enabled?
34 | if api.connection.aws_sts_session_token
35 | path = config.fetch(:aws_mfa, :cache_file, ".sfn-aws")
36 | FileUtils.touch(path)
37 | File.chmod(0600, path)
38 | values = load_stored_values(path)
39 | SESSION_STORE_ITEMS.map do |key|
40 | values[key] = api.connection.data[key]
41 | end
42 | File.open(path, "w") do |file|
43 | file.puts MultiJson.dump(values)
44 | end
45 | end
46 | end
47 | end
48 |
49 | alias_method :failed, :after
50 |
51 | # @return [TrueClass, FalseClass]
52 | def enabled?
53 | config.fetch(:aws_mfa, :status, "enabled").to_s == "enabled"
54 | end
55 |
56 | # Load stored configuration data into the api connection
57 | #
58 | # @return [TrueClass, FalseClass]
59 | def load_stored_session
60 | path = config.fetch(:aws_mfa, :cache_file, ".sfn-aws")
61 | if File.exists?(path)
62 | values = load_stored_values(path)
63 | SESSION_STORE_ITEMS.each do |key|
64 | api.connection.data[key] = values[key]
65 | end
66 | if values[:aws_sts_session_token_expires]
67 | begin
68 | api.connection.data[:aws_sts_session_token_expires] = Time.parse(values[:aws_sts_session_token_expires])
69 | rescue
70 | end
71 | end
72 | true
73 | else
74 | false
75 | end
76 | end
77 |
78 | # Load stored values
79 | #
80 | # @param path [String]
81 | # @return [Hash]
82 | def load_stored_values(path)
83 | begin
84 | if File.exists?(path)
85 | MultiJson.load(File.read(path)).to_smash
86 | else
87 | Smash.new
88 | end
89 | rescue MultiJson::ParseError
90 | Smash.new
91 | end
92 | end
93 |
94 | # Request MFA code from user
95 | #
96 | # @return [String]
97 | def prompt_for_code
98 | result = ui.ask "AWS MFA code", :valid => /^\d{6}$/
99 | result.strip
100 | end
101 | end
102 | end
103 | end
104 |
--------------------------------------------------------------------------------
/lib/sfn/callback/stack_policy.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Callback
5 | class StackPolicy < Callback
6 |
7 | # Policy to apply prior to stack deletion
8 | DEFENSELESS_POLICY = {
9 | "Statement" => [{
10 | "Effect" => "Allow",
11 | "Action" => "Update:*",
12 | "Resource" => "*",
13 | "Principal" => "*",
14 | }],
15 | }
16 |
17 | # @return [Smash] cached policies
18 | attr_reader :policies
19 |
20 | # Overload to init policy cache
21 | #
22 | # @return [self]
23 | def initialize(*args)
24 | super
25 | @policies = Smash.new
26 | end
27 |
28 | # Submit all cached policies
29 | #
30 | # @param args [Hash]
31 | def submit_policy(args)
32 | ui.info "Submitting stack policy documents"
33 | stack = args[:api_stack]
34 | ([stack] + stack.nested_stacks).compact.each do |p_stack|
35 | run_action "Applying stack policy to #{ui.color(p_stack.name, :yellow)}" do
36 | save_stack_policy(p_stack)
37 | end
38 | end
39 | ui.info "Stack policy documents successfully submitted!"
40 | end
41 |
42 | alias_method :after_create, :submit_policy
43 | alias_method :after_update, :submit_policy
44 |
45 | # Disable all existing policies prior to update
46 | #
47 | # @param args [Hash]
48 | def before_update(args)
49 | if config.get(:stack_policy, :update).to_s == "defenseless"
50 | ui.warn "Disabling all stack policies for update."
51 | stack = args[:api_stack]
52 | ([stack] + stack.nested_stacks).compact.each do |p_stack|
53 | @policies[p_stack.name] = DEFENSELESS_POLICY
54 | run_action "Disabling stack policy for #{ui.color(p_stack.name, :yellow)}" do
55 | save_stack_policy(p_stack)
56 | end
57 | end
58 | end
59 | end
60 |
61 | # Generate stack policy for stack and cache for the after hook
62 | # to handle
63 | #
64 | # @param info [Hash]
65 | def template(info)
66 | if info[:sparkle_stack]
67 | @policies.set(info.fetch(:stack_name, "unknown"),
68 | info[:sparkle_stack].generate_policy)
69 | end
70 | end
71 |
72 | # Save the cached policy for the given stack
73 | #
74 | # @param p_stack [Miasma::Models::Orchestration::Stack]
75 | # @return [NilClass]
76 | def save_stack_policy(p_stack)
77 | valid_logical_ids = p_stack.resources.reload.all.map(&:logical_id)
78 | stack_policy = @policies.fetch(p_stack.id,
79 | @policies.fetch(p_stack.data[:logical_id]),
80 | @policies[p_stack.name]).to_smash
81 | if stack_policy
82 | stack_policy[:Statement].delete_if do |policy_item|
83 | policy_match = policy_item[:Resource].to_s.match(
84 | %r{LogicalResourceId/(?.+)$}
85 | )
86 | if policy_match
87 | !valid_logical_ids.include?(policy_match["logical_id"])
88 | end
89 | end
90 | end
91 | result = p_stack.api.request(
92 | :path => "/",
93 | :method => :post,
94 | :form => Smash.new(
95 | "Action" => "SetStackPolicy",
96 | "StackName" => p_stack.id,
97 | "StackPolicyBody" => MultiJson.dump(stack_policy),
98 | ),
99 | )
100 | end
101 | end
102 | end
103 | end
104 |
--------------------------------------------------------------------------------
/lib/sfn/command.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 | require "bogo-cli"
3 |
4 | module Sfn
5 | class Command < Bogo::Cli::Command
6 | include CommandModule::Callbacks
7 |
8 | autoload :Conf, "sfn/command/conf"
9 | autoload :Create, "sfn/command/create"
10 | autoload :Describe, "sfn/command/describe"
11 | autoload :Destroy, "sfn/command/destroy"
12 | autoload :Diff, "sfn/command/diff"
13 | autoload :Events, "sfn/command/events"
14 | autoload :Export, "sfn/command/export"
15 | autoload :Graph, "sfn/command/graph"
16 | autoload :Import, "sfn/command/import"
17 | autoload :Init, "sfn/command/init"
18 | autoload :Inspect, "sfn/command/inspect"
19 | autoload :Lint, "sfn/command/lint"
20 | autoload :List, "sfn/command/list"
21 | autoload :Plan, "sfn/command/plan"
22 | autoload :Print, "sfn/command/print"
23 | autoload :Promote, "sfn/command/promote"
24 | autoload :Realize, "sfn/command/realize"
25 | autoload :Trace, "sfn/command/trace"
26 | autoload :Update, "sfn/command/update"
27 | autoload :Validate, "sfn/command/validate"
28 |
29 | # Base name of configuration file
30 | CONFIG_BASE_NAME = ".sfn"
31 |
32 | # Supported configuration file extensions
33 | VALID_CONFIG_EXTENSIONS = [
34 | "",
35 | ".rb",
36 | ".json",
37 | ".yaml",
38 | ".yml",
39 | ".xml",
40 | ]
41 |
42 | # Override to provide config file searching
43 | def initialize(cli_opts, args)
44 | unless cli_opts["config"]
45 | discover_config(cli_opts)
46 | end
47 | unless ENV["DEBUG"]
48 | ENV["DEBUG"] = "true" if cli_opts[:debug]
49 | end
50 | super(cli_opts, args)
51 | load_api_provider_extensions!
52 | run_callbacks_for(:after_config)
53 | run_callbacks_for("after_config_#{Bogo::Utility.snake(self.class.name.split("::").last)}")
54 | end
55 |
56 | # @return [Smash]
57 | def config
58 | memoize(:config) do
59 | super
60 | end
61 | end
62 |
63 | protected
64 |
65 | # Load API provider specific overrides to customize behavior
66 | #
67 | # @return [TrueClass, FalseClass]
68 | def load_api_provider_extensions!
69 | if config.get(:credentials, :provider)
70 | base_ext = Bogo::Utility.camel(config.get(:credentials, :provider)).to_sym
71 | targ_ext = self.class.name.split("::").last
72 | if ApiProvider.constants.include?(base_ext)
73 | base_module = ApiProvider.const_get(base_ext)
74 | ui.debug "Loading core provider extensions via `#{base_module}`"
75 | extend base_module
76 | if base_module.constants.include?(targ_ext)
77 | targ_module = base_module.const_get(targ_ext)
78 | ui.debug "Loading targeted provider extensions via `#{targ_module}`"
79 | extend targ_module
80 | end
81 | true
82 | end
83 | end
84 | end
85 |
86 | # Start with current working directory and traverse to root
87 | # looking for a `.sfn` configuration file
88 | #
89 | # @param opts [Slop]
90 | # @return [Slop]
91 | def discover_config(opts)
92 | cwd = Dir.pwd.split(File::SEPARATOR)
93 | detected_path = ""
94 | until cwd.empty? || File.exists?(detected_path.to_s)
95 | detected_path = Dir.glob(
96 | (cwd + ["#{CONFIG_BASE_NAME}{#{VALID_CONFIG_EXTENSIONS.join(",")}}"]).join(
97 | File::SEPARATOR
98 | )
99 | ).first
100 | cwd.pop
101 | end
102 | if opts.respond_to?(:fetch_option)
103 | opts.fetch_option("config").value = detected_path if detected_path
104 | else
105 | opts["config"] = detected_path if detected_path
106 | end
107 | opts
108 | end
109 |
110 | # @return [Class] attempt to return customized configuration class
111 | def config_class
112 | klass_name = self.class.name.split("::").last
113 | if Sfn::Config.const_defined?(klass_name)
114 | Sfn::Config.const_get(klass_name)
115 | else
116 | super
117 | end
118 | end
119 | end
120 | end
121 |
--------------------------------------------------------------------------------
/lib/sfn/command/conf.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Command
5 | # Config command
6 | class Conf < Command
7 | include Sfn::CommandModule::Base
8 |
9 | # Run the list command
10 | def execute!
11 | ui.info ui.color("Current configuration state:")
12 | Config::Conf.attributes.sort_by(&:first).each do |k, val|
13 | if config.has_key?(k)
14 | ui.print " #{ui.color(k, :bold, :green)}: "
15 | format_value(config[k], " ")
16 | end
17 | end
18 | if config[:generate]
19 | ui.puts
20 | ui.info "Generating .sfn configuration file.."
21 | generate_config!
22 | ui.info "Generation of .sfn configuration file #{ui.color("complete!", :green, :bold)}"
23 | end
24 | end
25 |
26 | def generate_config!
27 | if File.exists?(".sfn")
28 | ui.warn "Existing .sfn configuration file detected!"
29 | ui.confirm "Overwrite current .sfn configuration file?"
30 | end
31 | run_action "Writing .sfn file" do
32 | File.open(".sfn", "w") do |file|
33 | file.write SFN_CONFIG_CONTENTS
34 | end
35 | nil
36 | end
37 | end
38 |
39 | def format_value(value, indent = "")
40 | if value.is_a?(Hash)
41 | ui.puts
42 | value.sort_by(&:first).each do |k, v|
43 | ui.print "#{indent} #{ui.color(k, :bold)}: "
44 | format_value(v, indent + " ")
45 | end
46 | elsif value.is_a?(Array)
47 | ui.puts
48 | value.map(&:to_s).sort.each do |v|
49 | ui.print "#{indent} "
50 | format_value(v, indent + " ")
51 | end
52 | else
53 | ui.puts value.to_s
54 | end
55 | end
56 |
57 | SFN_CONFIG_CONTENTS = <<-EOF
58 | # This is an auto-generated configuration file for
59 | # the sfn CLI. To view all available configuration
60 | # options, please see:
61 | # http://www.sparkleformation.io/docs/sfn/configuration.html
62 | Configuration.new do
63 | # Set style of stack nesting
64 | apply_nesting 'deep'
65 | # Enable processing of SparkleFormation templates
66 | processing true
67 | # Provider specific options used when creating
68 | # new stacks. Options defined here are AWS specific.
69 | options do
70 | on_failure 'nothing'
71 | notification_topics []
72 | capabilities ['CAPABILITY_IAM']
73 | tags do
74 | creator ENV['USER']
75 | end
76 | end
77 | # Name of bucket in object store to hold nested
78 | # stack templates
79 | # nesting_bucket 'BUCKET_NAME'
80 | # Prefix used on generated template path prior to storage
81 | # in the object store
82 | # nesting_prefix 'nested-templates'
83 | # Remote provider credentials
84 | credentials do
85 | # Remote provider name (:aws, :azure, :google, :open_stack, :rackspace, :terraform)
86 | provider :aws
87 | # AWS credentials information
88 | aws_access_key_id ENV['AWS_ACCESS_KEY_ID']
89 | aws_secret_access_key ENV['AWS_SECRET_ACCESS_KEY']
90 | aws_region ENV['AWS_REGION']
91 | aws_bucket_region ENV['AWS_REGION']
92 | # aws_sts_role_arn ENV['AWS_STS_ROLE_ARN']
93 | # Eucalyptus related additions
94 | # api_endpoint ENV['EUCA_ENDPOINT']
95 | # euca_compat 'path'
96 | # ssl_enabled false
97 | # Azure credentials information
98 | azure_tenant_id ENV['AZURE_TENANT_ID']
99 | azure_client_id ENV['AZURE_CLIENT_ID']
100 | azure_subscription_id ENV['AZURE_SUBSCRIPTION_ID']
101 | azure_client_secret ENV['AZURE_CLIENT_SECRET']
102 | azure_region ENV['AZURE_REGION']
103 | azure_blob_account_name ENV['AZURE_BLOB_ACCOUNT_NAME']
104 | azure_blob_secret_key ENV['AZURE_BLOB_SECRET_KEY']
105 | # Defaults to "miasma-orchestration-templates"
106 | azure_root_orchestration_container ENV['AZURE_ROOT_ORCHESTRATION_CONTAINER']
107 | # OpenStack credentials information
108 | open_stack_identity_url ENV['OPENSTACK_IDENTITY_URL']
109 | open_stack_username ENV['OPENSTACK_USERNAME']
110 | open_stack_user_id ENV['OPENSTACK_USER_ID']
111 | open_stack_password ENV['OPENSTACK_PASSWORD']
112 | open_stack_token ENV['OPENSTACK_TOKEN']
113 | open_stack_region ENV['OPENSTACK_REGION']
114 | open_stack_tenant_name ENV['OPENSTACK_TENANT_NAME']
115 | open_stack_domain ENV['OPENSTACK_DOMAIN']
116 | open_stack_project ENV['OPENSTACK_PROJECT']
117 | # Rackspace credentials information
118 | rackspace_api_key ENV['RACKSPACE_API_KEY']
119 | rackspace_username ENV['RACKSPACE_USERNAME']
120 | rackspace_region ENV['RACKSPACE_REGION']
121 | # Google Cloud Deployment Manager credentials
122 | google_service_account_email ENV['GOOGLE_SERVICE_ACCOUNT_EMAIL']
123 | google_service_account_private_key ENV['GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY']
124 | google_project ENV['GOOGLE_PROJECT']
125 | # Terraform credentials information
126 | # Valid driver names: :tfe, :boule, :local
127 | terraform_driver :local
128 | terraform_tfe_endpoint ENV['TFE_URL']
129 | terraform_tfe_token ENV['TFE_TOKEN']
130 | terraform_boule_endpoint ENV['BOULE_URL']
131 | terraform_local_directory './terraform-stacks'
132 | terraform_local_scrub_destroyed false
133 | end
134 | end
135 | EOF
136 | end
137 | end
138 | end
139 |
--------------------------------------------------------------------------------
/lib/sfn/command/create.rb:
--------------------------------------------------------------------------------
1 | require "sparkle_formation"
2 | require "sfn"
3 |
4 | module Sfn
5 | class Command
6 | # Create command
7 | class Create < Command
8 | include Sfn::CommandModule::Base
9 | include Sfn::CommandModule::Template
10 | include Sfn::CommandModule::Stack
11 |
12 | # Run the stack creation command
13 | def execute!
14 | name_required!
15 | name = name_args.first
16 |
17 | # NOTE: Always disable plans on create
18 | config[:plan] = false
19 |
20 | if config[:template]
21 | file = config[:template]
22 | else
23 | file = load_template_file
24 | end
25 |
26 | unless config[:print_only]
27 | ui.info "#{ui.color("SparkleFormation:", :bold)} #{ui.color("create", :green)}"
28 | end
29 |
30 | stack_info = "#{ui.color("Name:", :bold)} #{name}"
31 | if config[:path]
32 | stack_info << " #{ui.color("Path:", :bold)} #{config[:file]}"
33 | end
34 |
35 | if config[:print_only]
36 | ui.puts format_json(parameter_scrub!(template_content(file)))
37 | return
38 | else
39 | ui.info " -> #{stack_info}"
40 | end
41 |
42 | stack = provider.connection.stacks.build(
43 | config.fetch(:options, Smash.new).dup.merge(
44 | :name => name,
45 | :template => template_content(file),
46 | :parameters => Smash.new,
47 | )
48 | )
49 |
50 | apply_stacks!(stack)
51 | populate_parameters!(file, :current_parameters => stack.parameters)
52 |
53 | stack.parameters = config_root_parameters
54 |
55 | if config[:upload_root_template]
56 | upload_result = store_template(name, file, Smash.new)
57 | stack.template_url = upload_result[:url]
58 | else
59 | stack.template = parameter_scrub!(template_content(file, :scrub))
60 | end
61 |
62 | api_action!(:api_stack => stack) do
63 | stack.save
64 | if config[:poll]
65 | poll_stack(stack.name)
66 | stack = provider.stack(name)
67 |
68 | if stack.reload.state == :create_complete
69 | ui.info "Stack create complete: #{ui.color("SUCCESS", :green)}"
70 | namespace.const_get(:Describe).new({:outputs => true}, [name]).execute!
71 | else
72 | ui.fatal "Create of new stack #{ui.color(name, :bold)}: #{ui.color("FAILED", :red, :bold)}"
73 | raise "Stack did not reach a successful completion state."
74 | end
75 | else
76 | ui.warn "Stack state polling has been disabled."
77 | ui.info "Stack creation initialized for #{ui.color(name, :green)}"
78 | end
79 | end
80 | end
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/lib/sfn/command/describe.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Command
5 | # Cloudformation describe command
6 | class Describe < Command
7 | include Sfn::CommandModule::Base
8 |
9 | # information available
10 | unless defined?(AVAILABLE_DISPLAYS)
11 | AVAILABLE_DISPLAYS = [:resources, :outputs, :tags]
12 | end
13 |
14 | # Run the stack describe action
15 | def execute!
16 | name_required!
17 | stack_name = name_args.last
18 | root_stack = api_action! do
19 | provider.stack(stack_name)
20 | end
21 | if root_stack
22 | ([root_stack] + root_stack.nested_stacks).compact.each do |stack|
23 | ui.info "Stack description of #{ui.color(stack.name, :bold)}:"
24 | display = [].tap do |to_display|
25 | AVAILABLE_DISPLAYS.each do |display_option|
26 | if config[display_option]
27 | to_display << display_option
28 | end
29 | end
30 | end
31 | display = AVAILABLE_DISPLAYS.dup if display.empty?
32 | display.each do |display_method|
33 | self.send(display_method, stack)
34 | end
35 | ui.puts
36 | end
37 | else
38 | ui.fatal "Failed to find requested stack: #{ui.color(stack_name, :bold, :red)}"
39 | raise "Requested stack not found: #{stack_name}"
40 | end
41 | end
42 |
43 | # Display resources
44 | #
45 | # @param stack [Miasma::Models::Orchestration::Stack]
46 | def resources(stack)
47 | stack_resources = stack.resources.all.sort do |x, y|
48 | y.updated <=> x.updated
49 | end.map do |resource|
50 | Smash.new(resource.attributes)
51 | end
52 | ui.table(self) do
53 | table(:border => false) do
54 | row(:header => true) do
55 | allowed_attributes.each do |attr|
56 | column as_title(attr), :width => stack_resources.map { |r| r[attr].to_s.length }.push(as_title(attr).length).max + 2
57 | end
58 | end
59 | stack_resources.each do |resource|
60 | row do
61 | allowed_attributes.each do |attr|
62 | column resource[attr]
63 | end
64 | end
65 | end
66 | end
67 | end.display
68 | end
69 |
70 | # Display outputs
71 | #
72 | # @param stack [Miasma::Models::Orchestration::Stack]
73 | def outputs(stack)
74 | ui.info "Outputs for stack: #{ui.color(stack.name, :bold)}"
75 | unless stack.outputs.nil? || stack.outputs.empty?
76 | stack.outputs.each do |output|
77 | key, value = output.key, output.value
78 | key = snake(key).to_s.split("_").map(&:capitalize).join(" ")
79 | ui.info [" ", ui.color("#{key}:", :bold), value].join(" ")
80 | end
81 | else
82 | ui.info " #{ui.color("No outputs found")}"
83 | end
84 | end
85 |
86 | # Display tags
87 | #
88 | # @param stack [Miasma::Models::Orchestration::Stack]
89 | def tags(stack)
90 | ui.info "Tags for stack: #{ui.color(stack.name, :bold)}"
91 | if stack.tags && !stack.tags.empty?
92 | stack.tags.each do |key, value|
93 | ui.info [" ", ui.color("#{key}:", :bold), value].join(" ")
94 | end
95 | else
96 | ui.info " #{ui.color("No tags found")}"
97 | end
98 | end
99 |
100 | # @return [Array] default attributes
101 | def default_attributes
102 | %w(updated logical_id type status status_reason)
103 | end
104 | end
105 | end
106 | end
107 |
--------------------------------------------------------------------------------
/lib/sfn/command/destroy.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Command
5 | class Destroy < Command
6 | include Sfn::CommandModule::Base
7 |
8 | # Run the stack destruction action
9 | def execute!
10 | name_required!
11 | stacks = name_args.sort
12 | plural = "s" if stacks.size > 1
13 | globs = stacks.find_all do |s|
14 | s !~ /^[a-zA-Z0-9-]+$/
15 | end
16 | unless globs.empty?
17 | glob_stacks = provider.connection.stacks.all.find_all do |remote_stack|
18 | globs.detect do |glob|
19 | File.fnmatch(glob, remote_stack.name)
20 | end
21 | end
22 | stacks += glob_stacks.map(&:name)
23 | stacks -= globs
24 | stacks.sort!
25 | end
26 | ui.warn "Destroying Stack#{plural}: #{ui.color(stacks.join(", "), :bold)}"
27 | ui.confirm "Destroy listed stack#{plural}?"
28 | stacks.each do |stack_name|
29 | stack = provider.connection.stacks.get(stack_name)
30 | if stack
31 | nested_stack_cleanup!(stack)
32 | begin
33 | api_action!(:api_stack => stack) do
34 | stack.destroy
35 | end
36 | rescue Miasma::Error::ApiError::RequestError => error
37 | raise unless error.response.code == 404
38 | # if stack is already gone, disable polling
39 | config[:poll] = false
40 | end
41 | ui.info "Destroy request complete for stack: #{ui.color(stack_name, :red)}"
42 | else
43 | ui.warn "Failed to locate requested stack: #{ui.color(stack_name, :bold)}"
44 | end
45 | end
46 | if config[:poll]
47 | if stacks.size == 1
48 | pstack = stacks.first
49 | begin
50 | poll_stack(pstack)
51 | stack = provider.connection.stacks.get(pstack)
52 | stack.reload
53 | if stack.state.to_s.end_with?("failed")
54 | ui.error("Stack #{ui.color(pstack, :bold)} still exists after polling complete.")
55 | raise "Failed to successfully destroy stack!"
56 | end
57 | rescue Miasma::Error::ApiError::RequestError => error
58 | # Ignore if stack cannot be reloaded
59 | end
60 | else
61 | ui.error "Stack polling is not available when multiple stack deletion is requested!"
62 | end
63 | end
64 | ui.info " -> Destroyed SparkleFormation#{plural}: #{ui.color(stacks.join(", "), :bold, :red)}"
65 | end
66 |
67 | # Cleanup persisted templates if nested stack resources are included
68 | def nested_stack_cleanup!(stack)
69 | stack.nested_stacks.each do |n_stack|
70 | nested_stack_cleanup!(n_stack)
71 | end
72 | nest_stacks = stack.template.fetch("Resources", {}).values.find_all do |resource|
73 | provider.connection.data[:stack_types].include?(resource["Type"])
74 | end.each do |resource|
75 | url = resource["Properties"]["TemplateURL"]
76 | if url && url.is_a?(String)
77 | _, bucket_name, path = URI.parse(url).path.split("/", 3)
78 | bucket = provider.connection.api_for(:storage).buckets.get(bucket_name)
79 | if bucket
80 | file = bucket.files.get(path)
81 | if file
82 | file.destroy
83 | ui.info "Deleted nested stack template! (Bucket: #{bucket_name} Template: #{path})"
84 | else
85 | ui.warn "Failed to locate template file within bucket for deletion! (#{path})"
86 | end
87 | else
88 | ui.warn "Failed to locate bucket containing template file for deletion! (#{bucket_name})"
89 | end
90 | end
91 | end
92 | end
93 | end
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/lib/sfn/command/diff.rb:
--------------------------------------------------------------------------------
1 | require "sparkle_formation"
2 | require "sfn"
3 | require "hashdiff"
4 |
5 | module Sfn
6 | class Command
7 | # Diff command
8 | class Diff < Command
9 | include Sfn::CommandModule::Base
10 | include Sfn::CommandModule::Template
11 | include Sfn::CommandModule::Stack
12 |
13 | # Diff the stack with existing stack
14 | def execute!
15 | name_required!
16 | name = name_args.first
17 |
18 | begin
19 | stack = provider.stack(name)
20 | rescue Miasma::Error::ApiError::RequestError
21 | stack = nil
22 | end
23 |
24 | if stack
25 | config[:print_only] = true
26 | file = load_template_file
27 | file = parameter_scrub!(file.dump)
28 |
29 | ui.info "#{ui.color("SparkleFormation:", :bold)} #{ui.color("diff", :blue)} - #{name}"
30 | ui.puts
31 |
32 | diff_stack(stack, MultiJson.load(MultiJson.dump(file)).to_smash)
33 | else
34 | ui.fatal "Failed to locate requested stack: #{ui.color(name, :red, :bold)}"
35 | raise "Failed to locate stack: #{name}"
36 | end
37 | end
38 |
39 | # @todo needs updates for better provider compat
40 | def diff_stack(stack, file, parent_names = [])
41 | stack_template = stack.template
42 | nested_stacks = Hash[
43 | file.fetch("Resources", file.fetch("resources", {})).find_all do |name, value|
44 | value.fetch("Properties", {})["Stack"]
45 | end
46 | ]
47 | nested_stacks.each do |name, value|
48 | n_stack = stack.nested_stacks(false).detect do |ns|
49 | ns.data[:logical_id] == name
50 | end
51 | if n_stack
52 | diff_stack(n_stack, value["Properties"]["Stack"], [*parent_names, stack.data.fetch(:logical_id, stack.name)].compact)
53 | end
54 | file["Resources"][name]["Properties"].delete("Stack")
55 | end
56 |
57 | ui.info "#{ui.color("Stack diff:", :bold)} #{ui.color((parent_names + [stack.data.fetch(:logical_id, stack.name)]).compact.join(" > "), :blue)}"
58 |
59 | stack_diff = HashDiff.diff(stack.template, file)
60 |
61 | if config[:raw_diff]
62 | ui.info "Dumping raw template diff:"
63 | require "pp"
64 | pp stack_diff
65 | else
66 | added_resources = stack_diff.find_all do |item|
67 | item.first == "+" && item[1].match(/Resources\.[^.]+$/)
68 | end
69 | removed_resources = stack_diff.find_all do |item|
70 | item.first == "-" && item[1].match(/Resources\.[^.]+$/)
71 | end
72 | modified_resources = stack_diff.find_all do |item|
73 | item[1].start_with?("Resources.") &&
74 | !item[1].end_with?("TemplateURL") &&
75 | !item[1].include?("Properties.Parameters")
76 | end - added_resources - removed_resources
77 |
78 | if added_resources.empty? && removed_resources.empty? && modified_resources.empty?
79 | ui.info "No changes detected"
80 | ui.puts
81 | else
82 | unless added_resources.empty?
83 | ui.info ui.color("Added Resources:", :green, :bold)
84 | added_resources.each do |item|
85 | ui.print ui.color(" -> #{item[1].split(".").last}", :green)
86 | ui.puts " [#{item[2]["Type"]}]"
87 | end
88 | ui.puts
89 | end
90 |
91 | unless modified_resources.empty?
92 | ui.info ui.color("Modified Resources:", :yellow, :bold)
93 | m_resources = Hash.new.tap do |hash|
94 | modified_resources.each do |item|
95 | _, key, path = item[1].split(".", 3)
96 | hash[key] ||= {}
97 | prefix, a_key = path.split(".", 2)
98 | hash[key][prefix] ||= []
99 | matched = hash[key][prefix].detect do |i|
100 | i[:path] == a_key
101 | end
102 | if matched
103 | if item.first == "-"
104 | matched[:original] = item[2]
105 | else
106 | matched[:new] = item[2]
107 | end
108 | else
109 | hash[key][prefix] << Hash.new.tap do |info|
110 | info[:path] = a_key
111 | case item.first
112 | when "~"
113 | info[:original] = item[2]
114 | info[:new] = item[3]
115 | when "+"
116 | info[:new] = item[2]
117 | else
118 | info[:original] = item[2]
119 | end
120 | end
121 | end
122 | end
123 | end.to_smash(:sorted).each do |key, value|
124 | ui.puts ui.color(" - #{key}", :yellow) + " [#{stack.template["Resources"][key]["Type"]}]"
125 | value.each do |prefix, items|
126 | ui.puts ui.color(" #{prefix}:", :bold)
127 | items.each do |item|
128 | original = item[:original].nil? ? ui.color("(none)", :yellow) : ui.color(item[:original].inspect, :red)
129 | new_val = item[:new].nil? ? ui.color("(deleted)", :red) : ui.color(item[:new].inspect, :green)
130 | ui.puts " #{item[:path]}: #{original} -> #{new_val}"
131 | end
132 | end
133 | end
134 | ui.puts
135 | end
136 |
137 | unless removed_resources.empty?
138 | ui.info ui.color("Removed Resources:", :red, :bold)
139 | removed_resources.each do |item|
140 | ui.print ui.color(" <- #{item[1].split(".").last}", :red)
141 | ui.puts " [#{item[2]["Type"]}]"
142 | end
143 | ui.puts
144 | end
145 |
146 | run_callbacks_for(:after_stack_diff,
147 | :diff => stack_diff,
148 | :diff_info => {
149 | :added => added_resources,
150 | :modified => modified_resources,
151 | :removed => removed_resources,
152 | },
153 | :api_stack => stack,
154 | :new_template => file)
155 | end
156 | end
157 | end
158 | end
159 | end
160 | end
161 |
--------------------------------------------------------------------------------
/lib/sfn/command/events.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Command
5 | # Events command
6 | class Events < Command
7 | include Sfn::CommandModule::Base
8 |
9 | # @return [Miasma::Models::Orchestration::Stack]
10 | attr_reader :stack
11 |
12 | # Run the events list action
13 | def execute!
14 | name_required!
15 | name = name_args.first
16 | ui.info "Events for Stack: #{ui.color(name, :bold)}\n"
17 | @seen_events = []
18 | @stack = provider.stack(name)
19 | if stack
20 | api_action!(:api_stack => stack) do
21 | table = ui.table(self) do
22 | table(:border => false) do
23 | events = get_events
24 | row(:header => true) do
25 | allowed_attributes.each do |attr|
26 | width_val = events.map { |e| e[attr].to_s.length }.push(attr.length).max + 2
27 | width_val = width_val > 70 ? 70 : width_val < 20 ? 20 : width_val
28 | column attr.split("_").map(&:capitalize).join(" "), :width => width_val
29 | end
30 | end
31 | events.each do |event|
32 | row do
33 | allowed_attributes.each do |attr|
34 | column event[attr]
35 | end
36 | end
37 | end
38 | end
39 | end.display
40 | if config[:poll]
41 | while (stack.reload.in_progress?)
42 | to_wait = config.fetch(:poll_wait_time, 10).to_f
43 | while (to_wait > 0)
44 | sleep(0.1)
45 | to_wait -= 0.1
46 | end
47 | stack.resources.reload
48 | table.display
49 | end
50 | end
51 | end
52 | else
53 | ui.fatal "Failed to locate requested stack: #{ui.color(name, :bold, :red)}"
54 | raise "Failed to locate stack: #{name}!"
55 | end
56 | end
57 |
58 | # Fetch events from stack
59 | #
60 | # @param stack [Miasma::Models::Orchestration::Stack]
61 | # @param last_id [String] only return events after this ID
62 | # @return [Array]
63 | def get_events(*args)
64 | stack_events = discover_stacks(stack).map do |i_stack|
65 | i_events = []
66 | begin
67 | if @initial_complete && i_stack.in_progress?
68 | i_events = i_stack.events.update!
69 | else
70 | i_events = i_stack.events.all
71 | end
72 | rescue => e
73 | if e.class.to_s.start_with?("Errno")
74 | ui.warn "Connection error encountered: #{e.message} (retrying)"
75 | ui.debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
76 | else
77 | ui.error "Unexpected error received fetching events: #{e.message}"
78 | ui.debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
79 | end
80 | sleep(5)
81 | retry
82 | end
83 | if i_events
84 | i_events.map do |e|
85 | e.attributes.merge(:stack_name => i_stack.name).to_smash
86 | end
87 | end
88 | end.flatten.compact.find_all { |e| e[:time] }.reverse
89 | stack_events.delete_if { |evt| @seen_events.include?(evt) }
90 | @seen_events.concat(stack_events)
91 | unless @initial_complete
92 | stack_events = stack_events.sort_by { |e| e[:time] }
93 | unless config[:all_events]
94 | start_index = stack_events.rindex do |item|
95 | item[:stack_name] == stack.name &&
96 | item[:resource_state].to_s.end_with?("in_progress") &&
97 | item[:resource_status_reason].to_s.downcase.include?("user init")
98 | end
99 | if start_index
100 | stack_events.slice!(0, start_index)
101 | end
102 | end
103 | @initial_complete = true
104 | end
105 | stack_events
106 | end
107 |
108 | # Discover stacks defined within the resources of given stack
109 | #
110 | # @param stack [Miasma::Models::Orchestration::Stack]
111 | def discover_stacks(stack)
112 | @stacks = [stack] + stack.nested_stacks.reverse
113 | end
114 |
115 | # @return [Array] default attributes for events
116 | def default_attributes
117 | %w(stack_name time resource_logical_id resource_status resource_status_reason)
118 | end
119 |
120 | # @return [Array] allowed attributes for events
121 | def allowed_attributes
122 | result = super
123 | unless @stacks.size > 1
124 | result.delete("stack_name")
125 | end
126 | result
127 | end
128 | end
129 | end
130 | end
131 |
--------------------------------------------------------------------------------
/lib/sfn/command/export.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Command
5 | # Export command
6 | class Export < Command
7 | include Sfn::CommandModule::Base
8 | include Sfn::Utils::ObjectStorage
9 |
10 | # Run export action
11 | def execute!
12 | raise NotImplementedError.new "Implementation updates required"
13 | stack_name = name_args.first
14 | ui.info "#{ui.color("Stack Export:", :bold)} #{stack_name}"
15 | ui.confirm "Perform export"
16 | stack = provider.stacks.get(stack_name)
17 | if stack
18 | export_options = Smash.new.tap do |opts|
19 | [:chef_popsicle, :chef_environment_parameter, :ignore_parameters].each do |key|
20 | opts[key] = config[key] unless config[key].nil?
21 | end
22 | end
23 | exporter = Sfn::Utils::StackExporter.new(stack, export_options)
24 | result = exporter.export
25 | outputs = [
26 | write_to_file(result, stack),
27 | write_to_bucket(result, stack),
28 | ].compact
29 | if outputs.empty?
30 | ui.warn "No persistent output location defined. Printing export:"
31 | ui.info _format_json(result)
32 | end
33 | ui.info "#{ui.color("Stack export", :bold)} (#{name_args.first}): #{ui.color("complete", :green)}"
34 | unless outputs.empty?
35 | outputs.each do |output|
36 | ui.info ui.color(" -> #{output}", :blue)
37 | end
38 | end
39 | else
40 | ui.fatal "Failed to discover requested stack: #{ui.color(stack_name, :red, :bold)}"
41 | exit -1
42 | end
43 | end
44 |
45 | # Generate file name for stack export JSON contents
46 | #
47 | # @param stack [Miasma::Models::Orchestration::Stack]
48 | # @return [String] file name
49 | def export_file_name(stack)
50 | name = config[:file]
51 | if name
52 | if name.respond_to?(:call)
53 | name.call(stack)
54 | else
55 | name.to_s
56 | end
57 | else
58 | "#{stack.stack_name}-#{Time.now.to_i}.json"
59 | end
60 | end
61 |
62 | # Write stack export to local file
63 | #
64 | # @param payload [Hash] stack export payload
65 | # @param stack [Misama::Stack::Orchestration::Stack]
66 | # @return [String, NilClass] path to file
67 | def write_to_file(payload, stack)
68 | raise NotImplementedError
69 | if config[:path]
70 | full_path = File.join(
71 | config[:path],
72 | export_file_name(stack)
73 | )
74 | _, bucket, path = full_path.split("/", 3)
75 | directory = provider.service_for(:storage,
76 | :provider => :local,
77 | :local_root => "/").directories.get(bucket)
78 | file_store(payload, path, directory)
79 | end
80 | end
81 |
82 | # Write stack export to remote bucket
83 | #
84 | # @param payload [Hash] stack export payload
85 | # @param stack [Miasma::Models::Orchestration::Stack]
86 | # @return [String, NilClass] remote bucket key
87 | def write_to_bucket(payload, stack)
88 | raise NotImplementedError
89 | if bucket = config[:bucket]
90 | key_path = File.join(*[
91 | bucket_prefix(stack),
92 | export_file_name(stack),
93 | ].compact)
94 | file_store(payload, key_path, provider.service_for(:storage).directories.get(bucket))
95 | end
96 | end
97 | end
98 | end
99 | end
100 |
--------------------------------------------------------------------------------
/lib/sfn/command/graph.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 | require "graph"
3 |
4 | module Sfn
5 | class Command
6 | # Graph command
7 | class Graph < Command
8 | autoload :Provider, "sfn/command/graph/provider"
9 |
10 | include Sfn::CommandModule::Base
11 | include Sfn::CommandModule::Template
12 | include Sfn::CommandModule::Stack
13 |
14 | # Valid graph styles
15 | GRAPH_STYLES = [
16 | "creation",
17 | "dependency",
18 | ]
19 |
20 | # Generate graph
21 | def execute!
22 | config[:print_only] = true
23 | validate_graph_style!
24 | file = load_template_file
25 | provider = Bogo::Utility.camel(file.provider).to_sym
26 | if Provider.constants.include?(provider)
27 | graph_const = Provider.const_get(provider)
28 | ui.debug "Loading provider graph implementation - #{graph_const}"
29 | extend graph_const
30 | @outputs = Smash.new
31 | ui.info "Template resource graph generation - Style: #{ui.color(config[:graph_style], :bold)}"
32 | if config[:file]
33 | ui.puts " -> path: #{config[:file]}"
34 | end
35 | template_dump = file.compile.sparkle_dump!.to_smash
36 | run_action "Pre-processing template for graphing" do
37 | output_discovery(template_dump, @outputs, nil, nil)
38 | ui.debug "Output remapping results from pre-processing:"
39 | @outputs.each_pair do |o_key, o_value|
40 | ui.debug "#{o_key} -> #{o_value}"
41 | end
42 | nil
43 | end
44 | graph = nil
45 | run_action "Generating resource graph" do
46 | graph = generate_graph(template_dump)
47 | nil
48 | end
49 | run_action "Writing graph result" do
50 | FileUtils.mkdir_p(File.dirname(config[:output_file]))
51 | if config[:output_type] == "dot"
52 | File.open("#{config[:output_file]}.dot", "w") do |o_file|
53 | o_file.puts graph.to_s
54 | end
55 | else
56 | graph.save config[:output_file], config[:output_type]
57 | end
58 | nil
59 | end
60 | else
61 | valid_providers = Provider.constants.sort.map { |provider|
62 | Bogo::Utility.snake(provider)
63 | }.join("`, `")
64 | ui.error "Graphing for provider `#{file.provider}` not currently supported."
65 | ui.error "Currently supported providers: `#{valid_providers}`."
66 | end
67 | end
68 |
69 | def generate_graph(template, args = {})
70 | graph = ::Graph.new
71 | @root_graph = graph unless @root_graph
72 | graph.graph_attribs << ::Graph::Attribute.new("overlap = false")
73 | graph.graph_attribs << ::Graph::Attribute.new("splines = true")
74 | graph.graph_attribs << ::Graph::Attribute.new("pack = true")
75 | graph.graph_attribs << ::Graph::Attribute.new('start = "random"')
76 | if args[:name]
77 | graph.name = "cluster_#{args[:name]}"
78 | labelnode_key = "cluster_#{args[:name]}"
79 | graph.plaintext << graph.node(labelnode_key)
80 | graph.node(labelnode_key).label args[:name]
81 | else
82 | graph.name = "root"
83 | end
84 | edge_detection(template, graph, args[:name].to_s.sub("cluster_", ""), args.fetch(:resource_names, []))
85 | graph
86 | end
87 |
88 | def colorize(string)
89 | hash = string.chars.inject(0) do |memo, chr|
90 | if memo + chr.ord > 127
91 | (memo - chr.ord).abs
92 | else
93 | memo + chr.ord
94 | end
95 | end
96 | color = "#"
97 | 3.times do |i|
98 | color << (255 ^ hash).to_s(16)
99 | new_val = hash + (hash * (1 / (i + 1.to_f))).to_i
100 | if hash * (i + 1) < 127
101 | hash = new_val
102 | else
103 | hash = hash / (i + 1)
104 | end
105 | end
106 | color
107 | end
108 |
109 | def validate_graph_style!
110 | if config[:luckymike]
111 | ui.warn "Detected luckymike power override. Forcing `dependency` style!"
112 | config[:graph_style] = "dependency"
113 | end
114 | config[:graph_style] = config[:graph_style].to_s
115 | unless GRAPH_STYLES.include?(config[:graph_style])
116 | raise ArgumentError.new "Invalid graph style provided `#{config[:graph_style]}`. Valid: `#{GRAPH_STYLES.join("`, `")}`"
117 | end
118 | end
119 | end
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/lib/sfn/command/graph/provider.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Command
5 | # Graph command
6 | class Graph < Command
7 | module Provider
8 | autoload :Aws, "sfn/command/graph/aws"
9 | autoload :Terraform, "sfn/command/graph/terraform"
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/sfn/command/import.rb:
--------------------------------------------------------------------------------
1 | require "stringio"
2 | require "sfn"
3 |
4 | module Sfn
5 | class Command
6 | # Import command
7 | class Import < Command
8 | include Sfn::CommandModule::Base
9 | include Sfn::Utils::JSON
10 | include Sfn::Utils::ObjectStorage
11 | include Sfn::Utils::PathSelector
12 |
13 | # Run the import action
14 | def execute!
15 | raise NotImplementedError.new "Implementation updates required"
16 | stack_name, json_file = name_args
17 | ui.info "#{ui.color("Stack Import:", :bold)} #{stack_name}"
18 | unless json_file
19 | entries = [].tap do |_entries|
20 | _entries.push("s3") if config[:bucket]
21 | _entries.push("fs") if config[:path]
22 | end
23 | if entries.size > 1
24 | valid = false
25 | until valid
26 | if config[:interactive_parameters]
27 | answer = ui.ask_question(
28 | "Import via file system (fs) or remote bucket (remote)?",
29 | :default => "remote",
30 | )
31 | else
32 | answer = "remote"
33 | end
34 | valid = true if %w(remote fs).include?(answer)
35 | entries = [answer]
36 | end
37 | elsif entries.size < 1
38 | ui.fatal "No path or bucket set. Unable to perform dynamic lookup!"
39 | exit 1
40 | end
41 | case entries.first
42 | when "remote"
43 | json_file = remote_discovery
44 | else
45 | json_file = local_discovery
46 | end
47 | end
48 | if File.exists?(json_file) || json_file.is_a?(IO)
49 | content = json_file.is_a?(IO) ? json_file.read : File.read(json_file)
50 | export = Mash.new(_from_json(content))
51 | begin
52 | creator = namespace.const_val(:Create).new(
53 | Smash.new(
54 | :template => _from_json(export[:stack][:template]),
55 | :options => _from_json(export[:stack][:options]),
56 | ),
57 | [stack_name]
58 | )
59 | ui.info " - Starting creation of import"
60 | creator.execute!
61 | ui.info "#{ui.color("Stack Import", :bold)} (#{json_file}): #{ui.color("complete", :green)}"
62 | rescue => e
63 | ui.fatal "Failed to import stack: #{e}"
64 | debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
65 | raise
66 | end
67 | else
68 | ui.fatal "Failed to locate JSON export file (#{json_file})"
69 | raise
70 | end
71 | end
72 |
73 | # Generate bucket prefix
74 | #
75 | # @return [String, NilClass]
76 | def bucket_prefix
77 | if prefix = config[:bucket_prefix]
78 | if prefix.respond_to?(:call)
79 | prefix.call
80 | else
81 | prefix.to_s
82 | end
83 | end
84 | end
85 |
86 | # Discover remote file
87 | #
88 | # @return [IO] stack export IO
89 | def remote_discovery
90 | storage = provider.service_for(:storage)
91 | directory = storage.directories.get(config[:bucket])
92 | file = prompt_for_file(
93 | directory,
94 | :directories_name => "Collections",
95 | :files_names => "Exports",
96 | :filter_prefix => bucket_prefix,
97 | )
98 | if file
99 | remote_file = storage.files.get(file)
100 | StringIO.new(remote_file.body)
101 | end
102 | end
103 |
104 | # Discover remote file
105 | #
106 | # @return [IO] stack export IO
107 | def local_discovery
108 | _, bucket = config[:path].split("/", 2)
109 | storage = provider.service_for(:storage,
110 | :provider => :local,
111 | :local_root => "/")
112 | directory = storage.directories.get(bucket)
113 | prompt_for_file(
114 | directory,
115 | :directories_name => "Collections",
116 | :files_names => "Exports",
117 | )
118 | end
119 | end
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/lib/sfn/command/init.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 | require "fileutils"
3 |
4 | module Sfn
5 | class Command
6 | # Init command
7 | class Init < Command
8 | include Sfn::CommandModule::Base
9 |
10 | INIT_DIRECTORIES = [
11 | "sparkleformation/dynamics",
12 | "sparkleformation/components",
13 | "sparkleformation/registry",
14 | ]
15 |
16 | # Run the init command to initialize new project
17 | def execute!
18 | unless name_args.size == 1
19 | raise ArgumentError.new "Please provide path argument only for project initialization"
20 | else
21 | path = name_args.first
22 | end
23 | if File.file?(path)
24 | raise "Cannot create project directory. Given path is a file. (`#{path}`)"
25 | end
26 | if File.directory?(path)
27 | ui.warn "Project directory already exists at given path. (`#{path}`)"
28 | ui.confirm "Overwrite existing files?"
29 | end
30 | run_action "Creating base project directories" do
31 | INIT_DIRECTORIES.each do |new_dir|
32 | FileUtils.mkdir_p(File.join(path, new_dir))
33 | end
34 | nil
35 | end
36 | run_action "Creating project bundle" do
37 | File.open(File.join(path, "Gemfile"), "w") do |file|
38 | file.puts "source 'https://rubygems.org'\n\ngem 'sfn'"
39 | end
40 | nil
41 | end
42 | ui.info "Generating .sfn configuration file"
43 | Dir.chdir(path) do
44 | Conf.new({:generate => true}, []).execute!
45 | end
46 | ui.info "Installing project bundle"
47 | Dir.chdir(path) do
48 | if defined?(Bundler)
49 | Bundler.clean_system("bundle install")
50 | else
51 | system("bundle install")
52 | end
53 | end
54 | ui.info "Project initialization complete!"
55 | ui.puts " Project path -> #{File.expand_path(path)}"
56 | end
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/sfn/command/lint.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Command
5 | # Lint command
6 | class Lint < Command
7 | include Sfn::CommandModule::Base
8 | include Sfn::CommandModule::Template
9 |
10 | # Perform linting
11 | def execute!
12 | print_only_original = config[:print_only]
13 | config[:print_only] = true
14 | file = load_template_file
15 | ui.info "#{ui.color("Template Linting (#{provider.connection.provider}): ", :bold)} #{config[:file].sub(Dir.pwd, "").sub(%r{^/}, "")}"
16 | config[:print_only] = print_only_original
17 |
18 | raw_template = parameter_scrub!(template_content(file))
19 |
20 | if config[:print_only]
21 | ui.puts raw_template
22 | else
23 | result = lint_template(raw_template)
24 | if result == true
25 | ui.info ui.color(" -> VALID", :green, :bold)
26 | else
27 | ui.info ui.color(" -> INVALID", :red, :bold)
28 | result.each do |failure|
29 | ui.error "Result Set: #{ui.color(failure[:rule_set].name, :red, :bold)}"
30 | failure[:failures].each do |f_msg|
31 | ui.fatal f_msg
32 | end
33 | end
34 | raise "Linting failure"
35 | end
36 | end
37 | end
38 |
39 | # Apply linting to given template
40 | #
41 | # @param template [Hash]
42 | # @return [TrueClass, Array]
43 | def lint_template(template)
44 | results = rule_sets.map do |set|
45 | result = set.apply(template)
46 | unless result == true
47 | Smash.new(:rule_set => set, :failures => result)
48 | end
49 | end.compact
50 | results.empty? ? true : results
51 | end
52 |
53 | # @return [Array]
54 | def rule_sets
55 | sets = [config[:lint_directory]].flatten.compact.map do |directory|
56 | if File.directory?(directory)
57 | files = Dir.glob(File.join(directory, "**", "**", "*.rb"))
58 | files.map do |path|
59 | begin
60 | Sfn::Lint.class_eval(
61 | IO.read(path), path, 1
62 | )
63 | rescue
64 | ui.warn "Failed to load detected file: #{path}"
65 | nil
66 | end
67 | end
68 | end
69 | end.flatten.compact.find_all { |rs| rs.provider == provider.connection.provider }
70 | unless config[:local_rule_sets_only]
71 | sets += Sfn::Lint::RuleSet.get_all(provider.connection.provider)
72 | end
73 | if config[:disabled_rule_set]
74 | disabled = [config[:disabled_rule_set]].flatten.compact
75 | sets.delete_if { |i| disabled.include?(i.name.to_s) }
76 | end
77 | if config[:enabled_rule_set]
78 | enabled = [config[:enabled_rule_set]].flatten.compact
79 | sets.delete_if { |i| enabled.include?(i.name.to_s) }
80 | end
81 | sets
82 | end
83 | end
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/lib/sfn/command/list.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Command
5 | # List command
6 | class List < Command
7 | include Sfn::CommandModule::Base
8 |
9 | # Run the list command
10 | def execute!
11 | ui.table(self) do
12 | table(:border => false) do
13 | stacks = api_action! { get_stacks }
14 | row(:header => true) do
15 | allowed_attributes.each do |attr|
16 | width_val = stacks.map { |e| e[attr].to_s.length }.push(attr.length).max + 2
17 | width_val = width_val > 70 ? 70 : width_val < 20 ? 20 : width_val
18 | column attr.split("_").map(&:capitalize).join(" "), :width => width_val
19 | end
20 | end
21 | get_stacks.each do |stack|
22 | row do
23 | allowed_attributes.each do |attr|
24 | column stack[attr]
25 | end
26 | end
27 | end
28 | end
29 | end.display
30 | end
31 |
32 | # Get the list of stacks to display
33 | #
34 | # @return [Array]
35 | def get_stacks
36 | provider.stacks.all.map do |stack|
37 | Smash.new(stack.attributes)
38 | end.sort do |x, y|
39 | if y[:created].to_s.empty?
40 | -1
41 | elsif x[:created].to_s.empty?
42 | 1
43 | else
44 | Time.parse(x[:created].to_s) <=> Time.parse(y[:created].to_s)
45 | end
46 | end
47 | end
48 |
49 | # @return [Array] default attributes to display
50 | def default_attributes
51 | if provider.connection.provider == :aws
52 | %w(name created updated status template_description)
53 | else
54 | %w(name created updated status description)
55 | end
56 | end
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/sfn/command/print.rb:
--------------------------------------------------------------------------------
1 | require "sparkle_formation"
2 | require "sfn"
3 |
4 | module Sfn
5 | class Command
6 | # Print command
7 | class Print < Command
8 | include Sfn::CommandModule::Base
9 | include Sfn::CommandModule::Template
10 | include Sfn::CommandModule::Stack
11 |
12 | # Print the requested template
13 | def execute!
14 | config[:print_only] = true
15 | file = load_template_file
16 |
17 | output_content = parameter_scrub!(template_content(file))
18 | if config[:yaml]
19 | require "yaml"
20 | output_content = YAML.dump(output_content)
21 | else
22 | output_content = format_json(output_content)
23 | end
24 |
25 | if config[:write_to_file]
26 | unless File.directory?(File.dirname(config[:write_to_file]))
27 | run_action "Creating parent directory" do
28 | FileUtils.mkdir_p(File.dirname(config[:write_to_file]))
29 | nil
30 | end
31 | end
32 | run_action "Writing template to file - #{config[:write_to_file]}" do
33 | File.write(config[:write_to_file], output_content)
34 | nil
35 | end
36 | else
37 | ui.puts output_content
38 | end
39 | end
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/sfn/command/promote.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Command
5 | # Promote command
6 | class Promote < Command
7 | include Sfn::CommandModule::Base
8 |
9 | def execute!
10 | raise NotImplementedError.new "Implementation updates required"
11 | stack_name, destination = name_args
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/sfn/command/realize.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Command
5 | # Realize command
6 | class Realize < Command
7 | include Sfn::CommandModule::Base
8 | include Sfn::CommandModule::Planning
9 |
10 | # Run the stack realize command
11 | def execute!
12 | name_required!
13 | name = name_args.first
14 |
15 | stack_info = "#{ui.color("Name:", :bold)} #{name}"
16 | begin
17 | stack = provider.stacks.get(name)
18 | rescue Miasma::Error::ApiError::RequestError
19 | raise Error::StackNotFound,
20 | "Failed to locate stack: #{name}"
21 | end
22 |
23 | if config[:plan_name]
24 | ui.debug "Setting custom plan name - #{config[:plan_name]}"
25 | # ensure custom attribute is dirty so we can modify
26 | stack.custom = stack.custom.dup
27 | stack.custom[:plan_name] = config[:plan_name]
28 | end
29 |
30 | ui.info " -> Loading plan information..."
31 |
32 | plan = stack.plan
33 | if plan.nil?
34 | raise Error::StackPlanNotFound,
35 | "Failed to locate plan for stack `#{name}`"
36 | end
37 |
38 | display_plan_information(plan)
39 |
40 | return if config[:plan_only]
41 |
42 | if config[:merge_api_options]
43 | config.fetch(:options, Smash.new).each_pair do |key, value|
44 | if stack.respond_to?("#{key}=")
45 | stack.send("#{key}=", value)
46 | end
47 | end
48 | end
49 |
50 | begin
51 | api_action!(:api_stack => stack) do
52 | stack.plan_execute
53 | if config[:poll]
54 | poll_stack(stack.name)
55 | if [:update_complete, :create_complete].
56 | include?(stack.reload.state)
57 | ui.info "Stack plan apply complete: " \
58 | "#{ui.color("SUCCESS", :green)}"
59 | namespace.const_get(:Describe).
60 | new({:outputs => true}, [name]).execute!
61 | else
62 | ui.fatal "Update of stack #{ui.color(name, :bold)}: " \
63 | "#{ui.color("FAILED", :red, :bold)}"
64 | raise Error::StackStateIncomplete
65 | end
66 | else
67 | ui.warn "Stack state polling has been disabled."
68 | ui.info "Stack plan apply initialized for " \
69 | "#{ui.color(name, :green)}"
70 | end
71 | end
72 | rescue Miasma::Error::ApiError::RequestError => e
73 | if e.message.downcase.include?("no updates")
74 | ui.warn "No changes detected for stack (#{stack.name})"
75 | else
76 | raise
77 | end
78 | end
79 | end
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/lib/sfn/command/trace.rb:
--------------------------------------------------------------------------------
1 | require "sparkle_formation"
2 | require "sfn"
3 |
4 | module Sfn
5 | class Command
6 | # Trace command
7 | class Trace < Command
8 | include Sfn::CommandModule::Base
9 | include Sfn::CommandModule::Template
10 | include Sfn::CommandModule::Stack
11 |
12 | # Print the requested template
13 | def execute!
14 | config[:sparkle_dump] = true
15 | config[:print_only] = true
16 | file = load_template_file
17 |
18 | if !file.is_a?(SparkleFormation)
19 | raise "Cannot trace non-SparkleFormation template"
20 | else
21 | writer = proc do |audit_log, indent = ""|
22 | audit_log.each do |record|
23 | header = "#{indent}-> "
24 | header << ui.color(record.type.to_s.capitalize, :bold)
25 | header << " - #{record.name}"
26 | source = "#{indent} | source: "
27 | if record.location.line > 0
28 | source << "#{record.location.path} @ #{record.location.line}"
29 | else
30 | source << ui.color(record.location.path, :yellow)
31 | end
32 | origin = "#{indent} | caller: "
33 | if record.caller.line > 0
34 | origin << "#{record.caller.path} @ #{record.caller.line}"
35 | else
36 | origin << ui.color(record.caller.path, :yellow)
37 | end
38 | duration = "#{indent} | duration: "
39 | if record.compile_duration
40 | duration << Kernel.sprintf("%0.4f", record.compile_duration)
41 | duration << "s"
42 | else
43 | duration < "N/A"
44 | end
45 | ui.info header
46 | ui.info source
47 | ui.info origin
48 | ui.info duration
49 | if record.audit_log.count > 0
50 | writer.call(record.audit_log, indent + " |")
51 | end
52 | end
53 | end
54 | ui.info ui.color("Trace information:", :bold)
55 | writer.call(file.audit_log)
56 | end
57 | end
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/sfn/command/update.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Command
5 | # Update command
6 | class Update < Command
7 | include Sfn::CommandModule::Base
8 | include Sfn::CommandModule::Template
9 | include Sfn::CommandModule::Stack
10 | include Sfn::CommandModule::Planning
11 |
12 | # Run the stack update command
13 | def execute!
14 | name_required!
15 | name = name_args.first
16 |
17 | stack_info = "#{ui.color("Name:", :bold)} #{name}"
18 | begin
19 | stack = provider.stacks.get(name)
20 | rescue Miasma::Error::ApiError::RequestError
21 | stack = nil
22 | end
23 |
24 | config[:compile_parameters] ||= Smash.new
25 |
26 | if config[:file]
27 | s_name = [name]
28 |
29 | c_setter = lambda do |c_stack|
30 | if c_stack.outputs
31 | compile_params = c_stack.outputs.detect do |output|
32 | output.key == "CompileState"
33 | end
34 | end
35 | if compile_params
36 | compile_params = MultiJson.load(compile_params.value)
37 | c_current = config[:compile_parameters].fetch(s_name.join("__"), Smash.new)
38 | config[:compile_parameters][s_name.join("__")] = compile_params.merge(c_current)
39 | end
40 | c_stack.nested_stacks(false).each do |n_stack|
41 | s_name.push(n_stack.data.fetch(:logical_id, n_stack.name))
42 | c_setter.call(n_stack)
43 | s_name.pop
44 | end
45 | end
46 |
47 | if stack
48 | c_setter.call(stack)
49 | end
50 |
51 | ui.debug "Compile parameters - #{config[:compile_parameters]}"
52 | file = load_template_file(:stack => stack)
53 | stack_info << " #{ui.color("Path:", :bold)} #{config[:file]}"
54 | else
55 | file = stack.template.dup if config[:plan]
56 | end
57 |
58 | unless stack
59 | ui.fatal "Failed to locate requested stack: #{ui.color(name, :red, :bold)}"
60 | raise "Failed to locate stack: #{name}"
61 | end
62 |
63 | unless config[:print_only]
64 | ui.info "#{ui.color("SparkleFormation:", :bold)} #{ui.color("update", :green)}"
65 | end
66 |
67 | unless file
68 | if config[:template]
69 | file = config[:template]
70 | stack_info << " #{ui.color("(template provided)", :green)}"
71 | else
72 | stack_info << " #{ui.color("(no template update)", :yellow)}"
73 | end
74 | end
75 | unless config[:print_only]
76 | ui.info " -> #{stack_info}"
77 | end
78 | if file
79 | if config[:print_only]
80 | ui.puts format_json(parameter_scrub!(template_content(file)))
81 | return
82 | end
83 |
84 | original_template = stack.template
85 | original_parameters = stack.parameters
86 |
87 | apply_stacks!(stack)
88 |
89 | populate_parameters!(file, :current_parameters => stack.root_parameters)
90 | update_template = stack.template
91 |
92 | if config[:plan]
93 | begin
94 | stack.template = original_template
95 | stack.parameters = original_parameters
96 | plan = build_planner(stack)
97 | if plan
98 | result = plan.generate_plan(
99 | file.respond_to?(:dump) ? file.dump : file,
100 | config_root_parameters
101 | )
102 | display_plan_information(result)
103 | end
104 | rescue => e
105 | unless e.message.include?("Confirmation declined")
106 | ui.error "Unexpected error when generating plan information: #{e.class} - #{e}"
107 | ui.debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
108 | ui.confirm "Continue with stack update?" unless config[:plan_only]
109 | else
110 | raise
111 | end
112 | end
113 | if config[:plan_only]
114 | ui.info "Plan only mode requested. Exiting."
115 | return
116 | end
117 | end
118 | stack.parameters = config_root_parameters
119 |
120 | if config[:upload_root_template]
121 | upload_result = store_template(name, file, Smash.new)
122 | stack.template_url = upload_result[:url]
123 | else
124 | stack.template = parameter_scrub!(template_content(file, :scrub))
125 | end
126 | else
127 | apply_stacks!(stack)
128 | original_parameters = stack.parameters
129 | populate_parameters!(stack.template, :current_parameters => stack.root_parameters)
130 | stack.parameters = config_root_parameters
131 | end
132 |
133 | # Set options defined within config into stack instance for update request
134 | if config[:merge_api_options]
135 | config.fetch(:options, Smash.new).each_pair do |key, value|
136 | if stack.respond_to?("#{key}=")
137 | stack.send("#{key}=", value)
138 | end
139 | end
140 | end
141 |
142 | begin
143 | api_action!(:api_stack => stack) do
144 | stack.save
145 | if config[:poll]
146 | poll_stack(stack.name)
147 | if stack.reload.state == :update_complete
148 | ui.info "Stack update complete: #{ui.color("SUCCESS", :green)}"
149 | namespace.const_get(:Describe).new({:outputs => true}, [name]).execute!
150 | else
151 | ui.fatal "Update of stack #{ui.color(name, :bold)}: #{ui.color("FAILED", :red, :bold)}"
152 | raise "Stack did not reach a successful update completion state."
153 | end
154 | else
155 | ui.warn "Stack state polling has been disabled."
156 | ui.info "Stack update initialized for #{ui.color(name, :green)}"
157 | end
158 | end
159 | rescue Miasma::Error::ApiError::RequestError => e
160 | if e.message.downcase.include?("no updates")
161 | ui.warn "No updates detected for stack (#{stack.name})"
162 | else
163 | raise
164 | end
165 | end
166 | end
167 | end
168 | end
169 | end
170 |
--------------------------------------------------------------------------------
/lib/sfn/command/validate.rb:
--------------------------------------------------------------------------------
1 | require "sparkle_formation"
2 | require "sfn"
3 |
4 | module Sfn
5 | class Command
6 | # Validate command
7 | class Validate < Command
8 | include Sfn::CommandModule::Base
9 | include Sfn::CommandModule::Template
10 | include Sfn::CommandModule::Stack
11 |
12 | def execute!
13 | if config[:all]
14 | validate_templates = sparkle_collection.templates[sparkle_collection.provider].keys
15 | elsif config[:group]
16 | validate_templates = sparkle_collection.templates[sparkle_collection.provider].keys.select do |template|
17 | template.split("__").first == config[:group]
18 | end
19 | else
20 | validate_templates = [config[:file]]
21 | end
22 |
23 | if validate_templates.empty?
24 | load_template_file
25 | validate_templates.push(config[:file])
26 | end
27 |
28 | validate_templates.each do |template|
29 | config[:file] = template
30 | print_only_original = config[:print_only]
31 | config[:print_only] = true
32 | ui.info "#{ui.color("Template Validation (#{provider.connection.provider}): ", :bold)} #{config[:file].sub(Dir.pwd, "").sub(%r{^/}, "")}"
33 | file = load_template_file
34 | config[:print_only] = print_only_original
35 | raw_template = _format_json(parameter_scrub!(template_content(file)))
36 |
37 | if config[:print_only]
38 | ui.puts raw_template
39 | else
40 | validate_stack(
41 | file.respond_to?(:dump) ? file.dump : file,
42 | if config[:processing]
43 | sparkle_collection.get(:template, config[:file])[:name]
44 | else
45 | config[:file]
46 | end
47 | )
48 | end
49 | end
50 | end
51 |
52 | # Validate template with remote API and unpack nested templates if required
53 | #
54 | # @param template [Hash] template data structure
55 | # @param name [String] name of template
56 | # @return [TrueClass]
57 | def validate_stack(template, name)
58 | resources = template.fetch("Resources", {})
59 | nested_stacks = resources.find_all do |r_name, r_value|
60 | r_value.is_a?(Hash) &&
61 | provider.connection.data[:stack_types].include?(r_value["Type"])
62 | end
63 | nested_stacks.each do |n_name, n_resource|
64 | validate_stack(n_resource.fetch("Properties", {}).fetch("Stack", {}), "#{name} > #{n_name}")
65 | n_resource["Properties"].delete("Stack")
66 | end
67 | begin
68 | ui.info "Validating: #{ui.color(name, :bold)}"
69 | if config[:upload_root_template]
70 | upload_result = store_template("validation-stack", template, Smash.new)
71 | stack = provider.connection.stacks.build(
72 | :name => "validation-stack",
73 | :template_url => upload_result[:url],
74 | )
75 | else
76 | stack = provider.connection.stacks.build(
77 | :name => "validation-stack",
78 | :template => parameter_scrub!(template),
79 | )
80 | end
81 | result = api_action!(:api_stack => stack) do
82 | stack.validate
83 | end
84 | ui.info ui.color(" -> VALID", :bold, :green)
85 | true
86 | rescue => e
87 | ui.info ui.color(" -> INVALID", :bold, :red)
88 | ui.fatal e.message
89 | raise e
90 | end
91 | # Clear Compile Time Parameters from Config
92 | config[:compile_parameters] = {}
93 | end
94 | end
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/lib/sfn/command_module.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module CommandModule
5 | autoload :Base, "sfn/command_module/base"
6 | autoload :Callbacks, "sfn/command_module/callbacks"
7 | autoload :Planning, "sfn/command_module/planning"
8 | autoload :Stack, "sfn/command_module/stack"
9 | autoload :Template, "sfn/command_module/template"
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/sfn/command_module/callbacks.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 | require "sparkle_formation"
3 |
4 | module Sfn
5 | module CommandModule
6 | # Callback processor helpers
7 | module Callbacks
8 | include Bogo::Memoization
9 |
10 | # Run expected callbacks around action
11 | #
12 | # @yieldblock api action to run
13 | # @yieldresult [Object] result from call
14 | # @return [Object] result of yield block
15 | def api_action!(*args)
16 | type = self.class.name.split("::").last.downcase
17 | run_callbacks_for(["before_#{type}", :before], *args)
18 | result = nil
19 | begin
20 | result = yield if block_given?
21 | run_callbacks_for(["after_#{type}", :after], *args)
22 | result
23 | rescue => err
24 | run_callbacks_for(["failed_#{type}", :failed], *(args + [err]))
25 | raise
26 | end
27 | end
28 |
29 | # Process requested callbacks
30 | #
31 | # @param type [Symbol, String] name of callback type
32 | # @return [NilClass]
33 | def run_callbacks_for(type, *args)
34 | types = [type].flatten.compact
35 | type = types.first
36 | clbks = types.map do |c_type|
37 | callbacks_for(c_type)
38 | end.flatten(1).compact.uniq.each do |item|
39 | callback_name, callback, quiet = item
40 | quiet = true if config[:print_only]
41 | ui.info "Callback #{ui.color(type.to_s, :bold)} #{callback_name}: #{ui.color("starting", :yellow)}" unless quiet
42 | if args.empty?
43 | callback.call
44 | else
45 | callback.call(*args)
46 | end
47 | ui.info "Callback #{ui.color(type.to_s, :bold)} #{callback_name}: #{ui.color("complete", :green)}" unless quiet
48 | end
49 | nil
50 | end
51 |
52 | # Fetch valid callbacks for given type
53 | #
54 | # @param type [Symbol, String] name of callback type
55 | # @param responder [Array] matching response methods
56 | # @return [Array]
57 | def callbacks_for(type)
58 | ([config.fetch(:callbacks, type, [])].flatten.compact + [config.fetch(:callbacks, :default, [])].flatten.compact).map do |c_name|
59 | instance = memoize(c_name) do
60 | begin
61 | klass = Sfn::Callback.const_get(Bogo::Utility.camel(c_name.to_s))
62 | klass.new(ui, config, arguments, provider)
63 | rescue NameError => e
64 | ui.debug "Callback type lookup error: #{e.class} - #{e}"
65 | raise NameError.new("Unknown #{type} callback requested: #{c_name} (not found)")
66 | end
67 | end
68 | if instance.respond_to?(type)
69 | [c_name, instance.method(type), instance.respond_to?(:quiet) ? instance.quiet : false]
70 | end
71 | end.compact
72 | end
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/lib/sfn/command_module/planning.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 | require "sparkle_formation"
3 |
4 | module Sfn
5 | module CommandModule
6 | # Planning helpers
7 | module Planning
8 | # Create a new planner instance
9 | #
10 | # @param [Miasma::Models::Orchestration::Stack]
11 | # @return [Sfn::Planner]
12 | def build_planner(stack)
13 | klass_name = stack.api.class.to_s.split("::").last
14 | if Planner.const_defined?(klass_name)
15 | Planner.const_get(klass_name).new(ui, config, arguments, stack)
16 | else
17 | warn "Failed to build planner for current provider. No provider implemented. (`#{klass_name}`)"
18 | nil
19 | end
20 | end
21 |
22 | # Display plan result on the UI
23 | #
24 | # @param result [Miasma::Models::Orchestration::Stack::Plan]
25 | def display_plan_information(result)
26 | ui.info ui.color("Pre-update resource planning report:", :bold)
27 | unless print_plan_result(result, [result.name])
28 | ui.info "No resources life cycle changes detected in this update!"
29 | end
30 | if config[:plan_apply]
31 | return ui.info "Realizing this stack plan..."
32 | elsif config[:plan_only]
33 | return
34 | end
35 | ui.confirm "Realize this stack plan?"
36 | end
37 |
38 | # Print plan information to the UI
39 | #
40 | # @param info [Miasma::Models::Orchestration::Stack::Plan]
41 | # @param names [Array] nested names
42 | def print_plan_result(info, names = [])
43 | said_any_things = false
44 | unless Array(info.stacks).empty?
45 | info.stacks.each do |s_name, s_info|
46 | result = print_plan_result(s_info, [*names, s_name].compact)
47 | said_any_things ||= result
48 | end
49 | end
50 | if !names.flatten.compact.empty? || info.name
51 | said_things = false
52 | output_name = names.empty? ? info.name : names.join(" > ")
53 | ui.puts
54 | ui.puts " #{ui.color("Update plan for:", :bold)} #{ui.color(names.join(" > "), :blue)}"
55 | unless Array(info.unknown).empty?
56 | ui.puts " #{ui.color("!!! Unknown update effect:", :red, :bold)}"
57 | print_plan_items(info, :unknown, :red)
58 | ui.puts
59 | said_any_things = said_things = true
60 | end
61 | unless Array(info.unavailable).empty?
62 | ui.puts " #{ui.color("Update request not allowed:", :red, :bold)}"
63 | print_plan_items(info, :unavailable, :red)
64 | ui.puts
65 | said_any_things = said_things = true
66 | end
67 | unless Array(info.replace).empty?
68 | ui.puts " #{ui.color("Resources to be replaced:", :red, :bold)}"
69 | print_plan_items(info, :replace, :red)
70 | ui.puts
71 | said_any_things = said_things = true
72 | end
73 | unless Array(info.interrupt).empty?
74 | ui.puts " #{ui.color("Resources to be interrupted:", :yellow, :bold)}"
75 | print_plan_items(info, :interrupt, :yellow)
76 | ui.puts
77 | said_any_things = said_things = true
78 | end
79 | unless Array(info.remove).empty?
80 | ui.puts " #{ui.color("Resources to be removed:", :red, :bold)}"
81 | print_plan_items(info, :remove, :red)
82 | ui.puts
83 | said_any_things = said_things = true
84 | end
85 | unless Array(info.add).empty?
86 | ui.puts " #{ui.color("Resources to be added:", :green, :bold)}"
87 | print_plan_items(info, :add, :green)
88 | ui.puts
89 | said_any_things = said_things = true
90 | end
91 | unless said_things
92 | ui.puts " #{ui.color("No resource lifecycle changes detected!", :green)}"
93 | ui.puts
94 | said_any_things = true
95 | end
96 | end
97 | said_any_things
98 | end
99 |
100 | # Print planning items
101 | #
102 | # @param info [Miasma::Models::Orchestration::Stack::Plan] plan
103 | # @param key [Symbol] key of items
104 | # @param color [Symbol] color to flag
105 | def print_plan_items(info, key, color)
106 | collection = info.send(key)
107 | max_name = collection.map(&:name).map(&:size).max
108 | max_type = collection.map(&:type).map(&:size).max
109 | max_p = collection.map(&:diffs).flatten(1).map(&:name).map(&:to_s).map(&:size).max
110 | max_o = collection.map(&:diffs).flatten(1).map(&:current).map(&:to_s).map(&:size).max
111 | collection.each do |val|
112 | name = val.name
113 | ui.print " " * 6
114 | ui.print ui.color("[#{val.type}]", color)
115 | ui.print " " * (max_type - val.type.size)
116 | ui.print " " * 4
117 | ui.print ui.color(name, :bold)
118 | properties = Array(val.diffs).map(&:name)
119 | unless properties.empty?
120 | ui.print " " * (max_name - name.size)
121 | ui.print " " * 4
122 | ui.print "Reason: `#{properties.join("`, `")}`"
123 | end
124 | ui.puts
125 | if config[:diffs]
126 | unless val.diffs.empty?
127 | p_name = nil
128 | val.diffs.each do |diff|
129 | if !diff.proposed.nil? || !diff.current.nil?
130 | p_name = diff.name
131 | ui.print " " * 8
132 | ui.print "#{p_name}: "
133 | ui.print " " * (max_p - p_name.size)
134 | ui.print ui.color("-#{diff.current}", :red) if diff.current
135 | ui.print " " * (max_o - diff.current.to_s.size)
136 | ui.print " "
137 | if diff.proposed == Sfn::Planner::RUNTIME_MODIFIED
138 | ui.puts ui.color("+#{diff.current} ", :green)
139 | else
140 | if diff.proposed.nil?
141 | ui.puts
142 | else
143 | ui.puts ui.color("+#{diff.proposed.to_s.gsub("__MODIFIED_REFERENCE_VALUE__", "")}", :green)
144 | end
145 | end
146 | end
147 | end
148 | ui.puts if p_name
149 | end
150 | end
151 | end
152 | end
153 | end
154 | end
155 | end
156 |
--------------------------------------------------------------------------------
/lib/sfn/config.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 | require "bogo-config"
3 |
4 | module Sfn
5 |
6 | # Top level configuration
7 | class Config < Bogo::Config
8 |
9 | # Override attribute helper to detect Hash types and automatically
10 | # add type conversion for CLI provided values + description update
11 | #
12 | # @param name [String, Symbol] name of attribute
13 | # @param type [Class, Array] valid types
14 | # @param info [Hash] attribute information
15 | # @return [Hash]
16 | def self.attribute(name, type, info = Smash.new)
17 | if [type].flatten.any? { |t| t.ancestors.include?(Hash) }
18 | unless info[:coerce]
19 | info[:coerce] = lambda do |v|
20 | case v
21 | when String
22 | Smash[
23 | v.split(/,(?=[^,]*:)/).map do |item_pair|
24 | item_pair.split(/[=:]/, 2)
25 | end
26 | ]
27 | when Hash
28 | v.to_smash
29 | else
30 | v
31 | end
32 | end
33 | info[:description] ||= ""
34 | info[:description] << " (Key:Value[,Key:Value,...])"
35 | end
36 | end
37 | super(name, type, info)
38 | end
39 |
40 | # Only values allowed designating bool type
41 | BOOLEAN = BOOLEAN_VALUES = [TrueClass, FalseClass].freeze
42 | # Boolean type with nil included
43 | TRISTATE_BOOLEAN = (BOOLEAN + [NilClass]).freeze
44 |
45 | autoload :Conf, "sfn/config/conf"
46 | autoload :Create, "sfn/config/create"
47 | autoload :Describe, "sfn/config/describe"
48 | autoload :Destroy, "sfn/config/destroy"
49 | autoload :Describe, "sfn/config/describe"
50 | autoload :Diff, "sfn/config/diff"
51 | autoload :Events, "sfn/config/events"
52 | autoload :Export, "sfn/config/export"
53 | autoload :Graph, "sfn/config/graph"
54 | autoload :Import, "sfn/config/import"
55 | autoload :Init, "sfn/config/init"
56 | autoload :Inspect, "sfn/config/inspect"
57 | autoload :Lint, "sfn/config/lint"
58 | autoload :List, "sfn/config/list"
59 | autoload :Plan, "sfn/config/plan"
60 | autoload :Print, "sfn/config/print"
61 | autoload :Promote, "sfn/config/promote"
62 | autoload :Realize, "sfn/config/realize"
63 | autoload :Trace, "sfn/config/trace"
64 | autoload :Update, "sfn/config/update"
65 | autoload :Validate, "sfn/config/validate"
66 |
67 | attribute(
68 | :config, String,
69 | :description => "Configuration file path",
70 | :short_flag => "c",
71 | )
72 |
73 | attribute(
74 | :credentials, Smash,
75 | :description => "Provider credentials",
76 | :short_flag => "C",
77 | )
78 | attribute(
79 | :ignore_parameters, String,
80 | :multiple => true,
81 | :description => "Parameters to ignore during modifications",
82 | :short_flag => "i",
83 | )
84 | attribute(
85 | :interactive_parameters, [TrueClass, FalseClass],
86 | :default => true,
87 | :description => "Prompt for template parameters",
88 | :short_flag => "I",
89 | )
90 | attribute(
91 | :poll, [TrueClass, FalseClass],
92 | :default => true,
93 | :description => "Poll stack events on modification actions",
94 | :short_flag => "p",
95 | )
96 | attribute(
97 | :defaults, [TrueClass, FalseClass],
98 | :description => "Automatically accept default values",
99 | :short_flag => "d",
100 | )
101 | attribute(
102 | :yes, [TrueClass, FalseClass],
103 | :description => "Automatically accept any requests for confirmation",
104 | :short_flag => "y",
105 | )
106 | attribute(
107 | :debug, [TrueClass, FalseClass],
108 | :description => "Enable debug output",
109 | :short_flag => "u",
110 | )
111 | attribute(
112 | :log, String,
113 | :description => "Enable logging with given level",
114 | :short_flag => "G",
115 | )
116 | attribute(
117 | :colors, [TrueClass, FalseClass],
118 | :description => "Enable colorized output",
119 | :default => true,
120 | )
121 |
122 | attribute :conf, Conf, :coerce => proc { |v| Conf.new(v) }
123 | attribute :create, Create, :coerce => proc { |v| Create.new(v) }
124 | attribute :update, Update, :coerce => proc { |v| Update.new(v) }
125 | attribute :destroy, Destroy, :coerce => proc { |v| Destroy.new(v) }
126 | attribute :events, Events, :coerce => proc { |v| Events.new(v) }
127 | attribute :export, Export, :coerce => proc { |v| Export.new(v) }
128 | attribute :import, Import, :coerce => proc { |v| Import.new(v) }
129 | attribute :inspect, Inspect, :coerce => proc { |v| Inpsect.new(v) }
130 | attribute :describe, Describe, :coerce => proc { |v| Describe.new(v) }
131 | attribute :list, List, :coerce => proc { |v| List.new(v) }
132 | attribute :promote, Promote, :coerce => proc { |v| Promote.new(v) }
133 | attribute :validate, Validate, :coerce => proc { |v| Validate.new(v) }
134 |
135 | # Provide all options for config class (includes global configs)
136 | #
137 | # @param klass [Class]
138 | # @return [Smash]
139 | def self.options_for(klass)
140 | shorts = ["h"] # always reserve `-h` for help
141 | _options_for(klass, shorts)
142 | end
143 |
144 | # Provide options for config class
145 | #
146 | # @param klass [Class]
147 | # @param shorts [Array]
148 | # @return [Smash]
149 | def self._options_for(klass, shorts)
150 | opts = Smash[
151 | [klass].map do |a|
152 | if a.ancestors.include?(Bogo::Config) && !a.attributes.empty?
153 | a.attributes
154 | end
155 | end.compact.reverse.inject(Smash.new) { |m, n| m.deep_merge(n) }.map do |name, info|
156 | next unless info[:description]
157 | short = info[:short_flag]
158 | if !short.to_s.empty? && shorts.include?(short)
159 | raise ArgumentError.new "Short flag already in use! (`#{short}` not available for `#{klass}`)"
160 | end
161 | unless short.to_s.empty?
162 | shorts << short
163 | info[:short] = short
164 | end
165 | info[:long] = name.tr("_", "-")
166 | info[:boolean] = [info[:type]].compact.flatten.all? { |t| BOOLEAN_VALUES.include?(t) }
167 | [name, info]
168 | end.compact
169 | ]
170 | opts
171 | end
172 | end
173 | end
174 |
--------------------------------------------------------------------------------
/lib/sfn/config/conf.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Config command configuration (subclass create to get all the configs)
6 | class Conf < Create
7 | attribute(
8 | :generate, [TrueClass, FalseClass],
9 | :description => "Generate a basic configuration file",
10 | :short_flag => "g",
11 | )
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/sfn/config/create.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Create command configuration
6 | class Create < Update
7 | attribute(
8 | :timeout, Integer,
9 | :coerce => proc { |v| v.to_i },
10 | :description => "Seconds to wait for stack to complete",
11 | :short_flag => "M",
12 | )
13 | attribute(
14 | :rollback, [TrueClass, FalseClass],
15 | :description => "Rollback stack on failure",
16 | :short_flag => "O",
17 | )
18 | attribute(
19 | :options, Smash,
20 | :description => "Extra options to apply to the API call",
21 | :short_flag => "S",
22 | )
23 | attribute(
24 | :notification_topics, String,
25 | :multiple => true,
26 | :description => "Notification endpoints for stack events",
27 | :short_flag => "z",
28 | )
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/sfn/config/describe.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | class Describe < Config
6 | attribute(
7 | :resources, [TrueClass, FalseClass],
8 | :description => "Display stack resource list",
9 | :short_flag => "r",
10 | )
11 |
12 | attribute(
13 | :outputs, [TrueClass, FalseClass],
14 | :description => "Display stack outputs",
15 | :short_flag => "o",
16 | )
17 |
18 | attribute(
19 | :tags, [TrueClass, FalseClass],
20 | :description => "Display stack tags",
21 | :short_flag => "t",
22 | )
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/sfn/config/destroy.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Destroy command configuration
6 | class Destroy < Config
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/sfn/config/diff.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Diff new template with existing stack template
6 | class Diff < Update
7 | attribute(
8 | :raw_diff, [TrueClass, FalseClass],
9 | :default => false,
10 | :description => "Display raw diff information",
11 | :short_flag => "w",
12 | )
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/sfn/config/events.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Events command configuration
6 | class Events < Config
7 | attribute(
8 | :attribute, String,
9 | :multiple => true,
10 | :description => "Event attribute to display",
11 | :short_flag => "a",
12 | )
13 | attribute(
14 | :poll_delay, Integer,
15 | :default => 20,
16 | :description => "Seconds to pause between each event poll",
17 | :coerce => lambda { |v| v.to_i },
18 | :short_flag => "P",
19 | )
20 | attribute(
21 | :all_attributes, [TrueClass, FalseClass],
22 | :description => "Display all event attributes",
23 | :short_flag => "A",
24 | )
25 | attribute(
26 | :all_events, [TrueClass, FalseClass],
27 | :description => "Display all available events",
28 | :short_flag => "L",
29 | )
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/sfn/config/export.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 |
6 | # Export command configuration
7 | class Export < Config
8 | attribute(
9 | :name, String,
10 | :description => "Export file base name",
11 | )
12 | attribute(
13 | :path, String,
14 | :description => "Local path prefix for dump file",
15 | )
16 | attribute(
17 | :bucket, String,
18 | :description => "Remote storage bucket",
19 | )
20 | attribute(
21 | :bucket_prefix, String,
22 | :description => "Remote key prefix within bucket for dump file",
23 | )
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/sfn/config/graph.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Generate graph
6 | class Graph < Validate
7 | attribute(
8 | :output_file, String,
9 | :description => "Directory to write graph files",
10 | :short_flag => "O",
11 | :default => File.join(Dir.pwd, "sfn-graph"),
12 | )
13 |
14 | attribute(
15 | :output_type, String,
16 | :description => "File output type (Requires graphviz package for non-dot types)",
17 | :short_flag => "e",
18 | :default => "dot",
19 | )
20 |
21 | attribute(
22 | :graph_style, String,
23 | :description => "Style of graph (`dependency`, `creation`)",
24 | :default => "creation",
25 | )
26 |
27 | attribute(
28 | :luckymike, [TrueClass, FalseClass],
29 | :description => "Force `dependency` style graph",
30 | :default => false,
31 | )
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/sfn/config/import.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 |
6 | # Import command configuration
7 | class Import < Config
8 | attribute(
9 | :path, String,
10 | :description => "Directory path JSON export files are located",
11 | )
12 | attribute(
13 | :bucket, String,
14 | :description => "Remote storage bucket JSON export files are located",
15 | )
16 | attribute(
17 | :bucket_prefix, String,
18 | :description => "Remote key prefix within bucket for dump file",
19 | )
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/sfn/config/init.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Init command configuration
6 | class Init < Config
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/sfn/config/inspect.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Inspect command configuration
6 | class Inspect < Config
7 | attribute(
8 | :attribute, String,
9 | :multiple => true,
10 | :description => "Dot delimited attribute to view",
11 | :short_flag => "a",
12 | )
13 | attribute(
14 | :nodes, [TrueClass, FalseClass],
15 | :description => "Locate all instances and display addresses",
16 | :short_flag => "n",
17 | )
18 | attribute(
19 | :load_balancers, [TrueClass, FalseClass],
20 | :description => "Locate all load balancers, display addresses and server states",
21 | :short_flag => "l",
22 | )
23 | attribute(
24 | :instance_failure, [TrueClass, FalseClass],
25 | :description => "Display log file error from failed not if possible",
26 | :short_flag => "N",
27 | )
28 | attribute(
29 | :failure_log_path, String,
30 | :description => "Path to remote log file for display on failure",
31 | :default => "/var/log/chef/client.log",
32 | :short_flag => "f",
33 | )
34 | attribute(
35 | :identity_file, String,
36 | :description => "SSH identity file for authentication",
37 | :short_flag => "D",
38 | )
39 | attribute(
40 | :ssh_user, String,
41 | :description => "SSH username for inspection connect",
42 | :short_flag => "s",
43 | )
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/sfn/config/lint.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Lint command configuration
6 | class Lint < Validate
7 | attribute(
8 | :lint_directory, String,
9 | :description => "Directory containing lint rule sets",
10 | :multiple => true,
11 | )
12 | attribute(
13 | :disabled_rule_set, String,
14 | :description => "Disable rule set from being applied",
15 | :multiple => true,
16 | )
17 | attribute(
18 | :enabled_rule_set, String,
19 | :description => "Only apply this rule set",
20 | :multiple => true,
21 | )
22 | attribute(
23 | :local_rule_sets_only, [TrueClass, FalseClass],
24 | :description => "Only apply rule sets provided by lint directory",
25 | :default => false,
26 | )
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/sfn/config/list.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # List command configuration
6 | class List < Sfn::Config
7 | attribute(
8 | :attribute, String,
9 | :multiple => true,
10 | :description => "Attribute of stack to print",
11 | :short_flag => "a",
12 | )
13 | attribute(
14 | :all_attributes, [TrueClass, FalseClass],
15 | :description => "Print all available attributes",
16 | :short_flag => "A",
17 | )
18 | attribute(
19 | :status, String,
20 | :multiple => true,
21 | :description => 'Match stacks with given status. Use "none" to disable.',
22 | :short_flag => "s",
23 | )
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/sfn/config/plan.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Plan command configuration
6 | class Plan < Create
7 | # Remove the plan option. Command specific options will
8 | # cause a conflict if same option name as command is used.
9 | # Also, since this is a plan command, we are always running
10 | # a plan, because that's the command.
11 | attributes.delete(:plan)
12 | # Default diffs to be enabled
13 | attributes.set(:diffs, :default, true)
14 |
15 | attribute(
16 | :plan_name, String,
17 | :description => "Custom plan name or ID (not applicable to all providers)",
18 | )
19 |
20 | attribute(
21 | :load_existing, TRISTATE_BOOLEAN,
22 | :description => "Load existing plan if exists",
23 | :default => nil,
24 | )
25 |
26 | attribute(
27 | :auto_destroy_stack, TRISTATE_BOOLEAN,
28 | :description => "Automatically destroy empty stack",
29 | :default => nil,
30 | )
31 |
32 | attribute(
33 | :auto_destroy_plan, TRISTATE_BOOLEAN,
34 | :description => "Automatically destroy generated plan",
35 | :default => nil,
36 | )
37 |
38 | attribute(
39 | :list, BOOLEAN,
40 | :description => "List all available plans for stack",
41 | :default => false,
42 | :short_flag => "l",
43 | )
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/sfn/config/print.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Print command configuration
6 | class Print < Validate
7 | attribute(
8 | :write_to_file, String,
9 | :description => "Write compiled SparkleFormation template to path provided",
10 | :short_flag => "w",
11 | )
12 |
13 | attribute(
14 | :sparkle_dump, [TrueClass, FalseClass],
15 | :description => "Do not use provider customized dump behavior",
16 | )
17 |
18 | attribute(
19 | :yaml, [TrueClass, FalseClass],
20 | :description => "Output template content in YAML format",
21 | )
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/sfn/config/promote.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Promote command configuration
6 | class Promote < Config
7 | attribute(
8 | :accounts, String,
9 | :description => "JSON accounts file path",
10 | )
11 | attribute(
12 | :bucket, String,
13 | :description => "Bucket name containing the exports",
14 | )
15 | attribute(
16 | :bucket_prefix, String,
17 | :description => "Key prefix within remote bucket",
18 | )
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/sfn/config/realize.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Realize command configuration
6 | class Realize < Config
7 | attribute(
8 | :plan_name, String,
9 | :description => "Custom plan name or ID",
10 | )
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/sfn/config/trace.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Print command configurationUpdate command configuration
6 | class Trace < Validate
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/sfn/config/update.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Update command configuration
6 | class Update < Validate
7 | attribute(
8 | :apply_stack, String,
9 | :multiple => true,
10 | :description => "Apply outputs from stack to input parameters",
11 | :short_flag => "A",
12 | )
13 | attribute(
14 | :apply_mapping, Smash,
15 | :description => "Customize apply stack mapping as [StackName__]OutputName:ParameterName",
16 | )
17 | attribute(
18 | :parameter, Smash,
19 | :multiple => true,
20 | :description => "[DEPRECATED - use `parameters`] Pass template parameters directly (ParamName:ParamValue)",
21 | :coerce => lambda { |v, inst|
22 | result = inst.data[:parameter] || Array.new
23 | case v
24 | when String
25 | v.split(",").each do |item|
26 | result.push(Smash[*item.split(/[=:]/, 2)])
27 | end
28 | else
29 | result.push(v.to_smash)
30 | end
31 | {:bogo_multiple => result}
32 | },
33 | :short_flag => "R",
34 | )
35 | attribute(
36 | :parameters, Smash,
37 | :description => "Pass template parameters directly",
38 | :short_flag => "m",
39 | )
40 | attribute(
41 | :plan, [TrueClass, FalseClass],
42 | :default => true,
43 | :description => "Provide planning information prior to update",
44 | :short_flag => "l",
45 | )
46 | attribute(
47 | :plan_only, [TrueClass, FalseClass],
48 | :default => false,
49 | :description => "Exit after plan display",
50 | )
51 | attribute(
52 | :diffs, [TrueClass, FalseClass],
53 | :description => "Show planner content diff",
54 | :short_flag => "D",
55 | )
56 | attribute(
57 | :merge_api_options, [TrueClass, FalseClass],
58 | :description => "Merge API options defined within configuration on update",
59 | :default => false,
60 | )
61 | attribute(
62 | :parameter_validation, String,
63 | :allowed => ["default", "none", "current", "expected"],
64 | :description => "Stack parameter validation behavior",
65 | :default => "default",
66 | )
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/lib/sfn/config/validate.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | class Config
5 | # Validate command configuration
6 | class Validate < Config
7 | attribute(
8 | :processing, [TrueClass, FalseClass],
9 | :description => "Call the unicorns and explode the glitter bombs",
10 | :default => true,
11 | :short_flag => "P",
12 | )
13 | attribute(
14 | :file, String,
15 | :description => "Path to template file",
16 | :default => nil,
17 | :short_flag => "f",
18 | )
19 | attribute(
20 | :file_path_prompt, [TrueClass, FalseClass],
21 | :default => true,
22 | :description => "Enable interactive prompt for template path discovery",
23 | :short_flag => "F",
24 | )
25 | attribute(
26 | :base_directory, String,
27 | :description => "Path to root of of templates directory",
28 | :short_flag => "b",
29 | )
30 | attribute(
31 | :no_base_directory, [TrueClass, FalseClass],
32 | :description => "Unset any value used for the template root directory path",
33 | :short_flag => "n",
34 | )
35 | attribute(
36 | :translate, String,
37 | :description => "Translate generated template to given provider",
38 | :short_flag => "t",
39 | )
40 | attribute(
41 | :translate_chunk, Integer,
42 | :description => "Chunk length for serialization",
43 | :coerce => lambda { |v| v.to_i },
44 | :short_flag => "T",
45 | )
46 | attribute(
47 | :apply_nesting, [String, Symbol],
48 | :default => "deep",
49 | :description => "Apply stack nesting",
50 | :short_flag => "a",
51 | )
52 | attribute(
53 | :nesting_bucket, String,
54 | :description => "Bucket to use for storing nested stack templates",
55 | :short_flag => "N",
56 | )
57 | attribute(
58 | :nesting_prefix, String,
59 | :description => "File name prefix for storing template in bucket",
60 | :short_flag => "Y",
61 | )
62 | attribute(
63 | :print_only, [TrueClass, FalseClass],
64 | :description => "Print the resulting stack template",
65 | :short_flag => "r",
66 | )
67 | attribute(
68 | :sparkle_pack, String,
69 | :multiple => true,
70 | :description => "Load SparklePack gem",
71 | :coerce => lambda { |s| s.to_s },
72 | :short_flag => "s",
73 | )
74 | attribute(
75 | :compile_parameters, Smash,
76 | :description => "Pass template compile time parameters directly",
77 | :short_flag => "o",
78 | :coerce => lambda { |v|
79 | case v
80 | when String
81 | result = Smash.new
82 | v.split(",").each do |item_pair|
83 | key, value = item_pair.split(/[=:]/, 2)
84 | if !result[key]
85 | result[key] = value
86 | next
87 | end
88 | if !result.is_a?(Array)
89 | result[key] = [result[key]]
90 | end
91 | result[key] << value
92 | end
93 | result
94 | when Hash
95 | result = Smash.new
96 | extractor = lambda do |data, prefix|
97 | data.each_pair do |key, value|
98 | local_key = "#{prefix}__#{key}"
99 | if value.is_a?(Hash)
100 | extractor.call(value, local_key)
101 | else
102 | result[local_key] = data
103 | end
104 | end
105 | end
106 | result
107 | else
108 | v
109 | end
110 | },
111 | )
112 | attribute(
113 | :upload_root_template, [TrueClass, FalseClass],
114 | :description => "Upload root template to storage bucket",
115 | )
116 | attribute(
117 | :all, [TrueClass, FalseClass],
118 | :description => "Validate all templates",
119 | )
120 | attribute(
121 | :group, String,
122 | :description => "Validate templates with the specified group prefix",
123 | )
124 | end
125 | end
126 | end
127 |
--------------------------------------------------------------------------------
/lib/sfn/error.rb:
--------------------------------------------------------------------------------
1 | module Sfn
2 | class Error < StandardError
3 | # @return [Integer] exit code to report
4 | attr_reader :exit_code
5 | # @return [Exception, nil] original exception
6 | attr_reader :original
7 |
8 | # Exit code used when no custom code provided
9 | DEFAULT_EXIT_CODE = 1
10 |
11 | def self.exit_code(c = nil)
12 | if c || !defined?(@exit_code)
13 | @exit_code = c.to_i != 0 ? c : DEFAULT_EXIT_CODE
14 | end
15 | @exit_code
16 | end
17 |
18 | def self.error_msg(m = nil)
19 | if m || !defined?(@error_msg)
20 | @error_msg = m
21 | end
22 | @error_msg
23 | end
24 |
25 | def initialize(*args)
26 | opts = args.detect { |a| a.is_a?(Hash) } || {}
27 | opts = opts.to_smash
28 | msg = args.first.is_a?(String) ? args.first : self.class.error_msg
29 | super(msg)
30 | @exit_code = opts.fetch(:exit_code, self.class.exit_code).to_i
31 | if opts[:original]
32 | if opts[:original].is_a?(Exception)
33 | @original = opts[:original]
34 | else
35 | raise TypeError.new "Expected `Exception` type in `:original` " \
36 | "option but received `#{opts[:original].class}`"
37 | end
38 | end
39 | end
40 |
41 | class InteractionDisabled < Error
42 | error_msg "Interactive prompting is disabled"
43 | exit_code 2
44 | end
45 |
46 | class StackNotFound < Error
47 | error_msg "Failed to locate requested stack"
48 | exit_code 3
49 | end
50 |
51 | class StackPlanNotFound < Error
52 | error_msg "Failed to locate requested stack plan"
53 | exit_code 4
54 | end
55 |
56 | class StackStateIncomplete < Error
57 | error_msg "Stack did not reach a successful completion state"
58 | exit_code 5
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/sfn/lint.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 | require "jmespath"
3 |
4 | module Sfn
5 | module Lint
6 | autoload :Definition, "sfn/lint/definition"
7 | autoload :Rule, "sfn/lint/rule"
8 | autoload :RuleSet, "sfn/lint/rule_set"
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/sfn/lint/definition.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module Lint
5 | # Lint defition
6 | class Definition
7 |
8 | # @return [String] search expression used for matching
9 | attr_reader :search_expression
10 | # @return [Proc-ish] must respond to #call
11 | attr_reader :evaluator
12 | # @return [Symbol] target provider
13 | attr_reader :provider
14 |
15 | # Create a new definition
16 | #
17 | # @param expr [String] search expression used for matching
18 | # @param provider [String, Symbol] target provider
19 | # @param evaluator [Proc] logic used to handle match
20 | # @return [self]
21 | def initialize(expr, provider = :aws, evaluator = nil, &block)
22 | if evaluator && block
23 | raise ArgumentError.new "Only evaluator or block can be provided, not both."
24 | end
25 | @provider = Bogo::Utility.snake(provider).to_sym
26 | @search_expression = expr
27 | @evaluator = evaluator || block
28 | end
29 |
30 | # Apply definition to template
31 | #
32 | # @param template [Hash] template being processed
33 | # @return [TrueClass, Array] true if passed. List of string results that failed
34 | def apply(template)
35 | result = JMESPath.search(search_expression, template)
36 | run(result, template)
37 | end
38 |
39 | protected
40 |
41 | # Check result of search expression
42 | #
43 | # @param result [Object] result(s) of search expression
44 | # @param template [Hash] full template
45 | # @return [TrueClass, Array] true if passed. List of string results that failed
46 | # @note override this method when subclassing
47 | def run(result, template)
48 | unless evaluator
49 | raise NotImplementedError.new "No evaluator has been defined for this definition!"
50 | end
51 | evaluator.call(result, template)
52 | end
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/sfn/lint/rule.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module Lint
5 | # Composition of definitions
6 | class Rule
7 |
8 | # @return [Symbol] name of rule
9 | attr_reader :name
10 | # @return [Array] definitions composing rule
11 | attr_reader :definitions
12 | # @return [String] message describing failure
13 | attr_reader :fail_message
14 | # @return [Symbol] target provider
15 | attr_reader :provider
16 |
17 | # Create a new rule
18 | #
19 | # @param name [String, Symbol] name of rule
20 | # @param definitions [Array] definitions composing rule
21 | # @param fail_message [String] message to describe failure
22 | # @param provider [String, Symbol] target provider
23 | # @return [self]
24 | def initialize(name, definitions, fail_message, provider = :aws)
25 | @name = name.to_sym
26 | @definitions = definitions.dup.uniq.freeze
27 | @fail_message = fail_message
28 | @provider = Bogo::Utility.snake(provider).to_sym
29 | validate_definitions!
30 | end
31 |
32 | # Generate the failure message for this rule with given failure
33 | # result set.
34 | def generate_fail_message(results)
35 | msg = fail_message.dup
36 | unless results.empty?
37 | failed_items = results.map do |item|
38 | f_item = item[:failures]
39 | next if f_item.nil? || f_item == true || f_item == false
40 | f_item
41 | end.flatten.compact.map(&:to_s)
42 | unless failed_items.empty?
43 | msg = "#{msg} (failures: `#{failed_items.join("`, `")}`)"
44 | end
45 | end
46 | msg
47 | end
48 |
49 | # Apply all definitions to template
50 | #
51 | # @param template [Hash]
52 | # @return [TrueClass, Array] true if passed. Definition failures if failed.
53 | def apply(template)
54 | results = definitions.map do |definition|
55 | result = definition.apply(template)
56 | result == true ? result : Smash.new(:definition => definition, :failures => result)
57 | end
58 | if results.all? { |item| item == true }
59 | true
60 | else
61 | results.delete_if { |item| item == true }
62 | results
63 | end
64 | end
65 |
66 | # Check if template passes this rule
67 | #
68 | # @param template [Hash]
69 | # @return [TrueClass, FalseClass]
70 | def pass?(template)
71 | apply(template) == true
72 | end
73 |
74 | # Check if template fails this rule
75 | #
76 | # @param template [Hash]
77 | # @return [TrueClass, FalseClass]
78 | def fail?(template)
79 | !pass?(template)
80 | end
81 |
82 | # Add a new definition to the collection
83 | #
84 | # @param definition [Definition] new definition to add
85 | # @return [self]
86 | def add_definition(definition)
87 | new_defs = definitions.dup
88 | new_defs << definition
89 | @definitions = new_defs.uniq.freeze
90 | validate_definitions!
91 | self
92 | end
93 |
94 | # Remove a definition from the collection
95 | #
96 | # @param definition [Definition] definition to remove
97 | # @return [self]
98 | def remove_definition(definition)
99 | new_defs = definitions.dup
100 | new_defs.delete(definition)
101 | @definitions = new_defs.uniq.freeze
102 | self
103 | end
104 |
105 | # Check that provided definitions provider match rule defined provider
106 | def validate_definitions!
107 | non_match = definitions.find_all do |definition|
108 | definition.provider != provider
109 | end
110 | unless non_match.empty?
111 | raise ArgumentError.new "Rule defines `#{provider}` as provider but includes definitions for " \
112 | "non matching providers. (#{non_match.map(&:provider).map(&:to_s).uniq.sort.join(", ")})"
113 | end
114 | end
115 | end
116 | end
117 | end
118 |
--------------------------------------------------------------------------------
/lib/sfn/lint/rule_set.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module Lint
5 | # Named collection of rules
6 | class RuleSet
7 |
8 | # Helper class for ruleset generation
9 | class Creator
10 | attr_reader :items, :provider
11 |
12 | def initialize(provider)
13 | @provider = provider
14 | @items = []
15 | end
16 |
17 | class RuleSet < Creator
18 | def rule(name, &block)
19 | r = Rule.new(provider)
20 | r.instance_exec(&block)
21 | items << Sfn::Lint::Rule.new(name, r.items, r.fail_message, provider)
22 | end
23 | end
24 |
25 | class Rule < Creator
26 | def definition(expr, evaluator = nil, &block)
27 | items << Sfn::Lint::Definition.new(expr, provider, evaluator, &block)
28 | end
29 |
30 | def fail_message(val = nil)
31 | unless val.nil?
32 | @fail_message = val
33 | end
34 | @fail_message
35 | end
36 | end
37 | end
38 |
39 | class << self
40 | @@_rule_set_registry = Smash.new
41 |
42 | # RuleSet generator helper for quickly building simple rule sets
43 | #
44 | # @param name [String] name of rule set
45 | # @param provider [String, Symbol] target provider
46 | # @yieldblock rule set content
47 | def build(name, provider = :aws, &block)
48 | provider = Bogo::Utility.snake(provider).to_sym
49 | rs = Creator::RuleSet.new(provider)
50 | rs.instance_exec(&block)
51 | self.new(name, provider, rs.items)
52 | end
53 |
54 | # Register a rule set
55 | #
56 | # @param rule_set [RuleSet]
57 | # @return [TrueClass]
58 | def register(rule_set)
59 | @@_rule_set_registry.set(rule_set.provider, rule_set.name, rule_set)
60 | true
61 | end
62 |
63 | # Get registered rule set
64 | #
65 | # @param name [String] name of rule set
66 | # @param provider [String] target provider
67 | # @return [RuleSet, NilClass]
68 | def get(name, provider = :aws)
69 | provider = Bogo::Utility.snake(provider)
70 | @@_rule_set_registry.get(provider, name)
71 | end
72 |
73 | # Get all rule sets for specified provider
74 | #
75 | # @param provider [String] target provider
76 | # @return [Array]
77 | def get_all(provider = :aws)
78 | @@_rule_set_registry.fetch(provider, {}).values
79 | end
80 | end
81 |
82 | include Bogo::Memoization
83 |
84 | # @return [Symbol] name
85 | attr_reader :name
86 | # @return [Symbol] target provider
87 | attr_reader :provider
88 | # @return [Array] rules of set
89 | attr_reader :rules
90 |
91 | # Create new rule set
92 | #
93 | # @param name [String, Symbol] name of rule set
94 | # @param provider [String, Symbol] name of target provider
95 | # @param rules [Array] list of rules defining this set
96 | # @return [self]
97 | def initialize(name, provider = :aws, rules = [])
98 | @name = name.to_sym
99 | @provider = Bogo::Utility.snake(provider).to_sym
100 | @rules = rules.dup.uniq.freeze
101 | validate_rules!
102 | end
103 |
104 | # Add a new rule to the collection
105 | #
106 | # @param rule [Rule] new rule to add
107 | # @return [self]
108 | def add_rule(rule)
109 | new_rules = rules.dup
110 | new_rules << rule
111 | @rules = new_rules.uniq.freeze
112 | validate_rules!
113 | self
114 | end
115 |
116 | # Remove a rule from the collection
117 | #
118 | # @param rule [Rule] rule to remove
119 | # @return [self]
120 | def remove_rule(rule)
121 | new_rules = rules.dup
122 | new_rules.delete(rule)
123 | @rules = new_rules.uniq.freeze
124 | self
125 | end
126 |
127 | # Apply rule set to template.
128 | #
129 | # @param template [Hash]
130 | # @return [TrueClass, Array] true on success, list failure messages on failure
131 | def apply(template)
132 | failures = collect_failures(template)
133 | if failures.empty?
134 | true
135 | else
136 | failures.map do |failure|
137 | failure[:rule].generate_fail_message(failure[:result])
138 | end
139 | end
140 | end
141 |
142 | # Process template through rules defined in this set and
143 | # store failure information
144 | #
145 | # @param template [Hash]
146 | # @return [Array] list of failures
147 | def collect_failures(template)
148 | results = rules.map do |rule|
149 | result = rule.apply(template)
150 | result == true ? true : Smash.new(:rule => rule, :result => result)
151 | end
152 | results.delete_if { |i| i == true }
153 | results
154 | end
155 |
156 | # Check that provided rules provider match rule set defined provider
157 | def validate_rules!
158 | non_match = rules.find_all do |rule|
159 | rule.provider != provider
160 | end
161 | unless non_match.empty?
162 | raise ArgumentError.new "Rule set defines `#{provider}` as provider but includes rules for " \
163 | "non matching providers. (#{non_match.map(&:provider).map(&:to_s).uniq.sort.join(", ")})"
164 | end
165 | end
166 | end
167 | end
168 | end
169 |
--------------------------------------------------------------------------------
/lib/sfn/monkey_patch.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | # Container for monkey patches
5 | module MonkeyPatch
6 | autoload :Stack, "sfn/monkey_patch/stack"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/sfn/monkey_patch/stack/azure.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module MonkeyPatch
5 | module Stack
6 | # Azure specific monkey patch implementations
7 | module Azure
8 |
9 | # @return [Hash] restructured azure template
10 | # @note Will return #template if name collision encountered within resources
11 | def sparkleish_template_azure
12 | new_template = template.to_smash
13 | resources = new_template.delete(:resources)
14 | resources.each do |resource|
15 | new_template.set(:resources, resource.delete(:name), resource)
16 | end
17 | resources.size == new_template[:resources].size ? new_template : template
18 | end
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/sfn/monkey_patch/stack/google.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module MonkeyPatch
5 | module Stack
6 | # Google specific monkey patch implementations
7 | module Google
8 |
9 | # Helper module to allow nested stack behavior to function as expected
10 | # internally within sfn
11 | module PretendStack
12 |
13 | # disable reload
14 | def reload
15 | self
16 | end
17 |
18 | # disable template load
19 | def perform_template_load
20 | Smash.new
21 | end
22 |
23 | # only show resources associated to this stack
24 | def resources
25 | collection = Miasma::Models::Orchestration::Stack::Resources.new(self)
26 | collection.define_singleton_method(:perform_population) do
27 | valid = stack.sparkleish_template.fetch(:resources, {}).keys
28 | stack.custom[:resources].find_all { |r| valid.include?(r[:name]) }.map do |attrs|
29 | Miasma::Models::Orchestration::Stack::Resource.new(stack, attrs).valid_state
30 | end
31 | end
32 | collection
33 | end
34 |
35 | # Sub-stacks never provide events
36 | def events
37 | collection = Miasma::Models::Orchestration::Stack::Events.new(self)
38 | collection.define_singleton_method(:perform_population) { [] }
39 | collection
40 | end
41 | end
42 |
43 | # Return all stacks contained within this stack
44 | #
45 | # @param recurse [TrueClass, FalseClass] recurse to fetch _all_ stacks
46 | # @return [Array]
47 | def nested_stacks_google(recurse = true)
48 | my_template = sparkleish_template
49 | if my_template[:resources][name]
50 | my_template = my_template.get(:resources, name, :properties, :stack)
51 | end
52 | n_stacks = my_template[:resources].map do |s_name, content|
53 | if content[:type] == "sparkleformation.stack"
54 | n_stack = self.class.new(api)
55 | n_stack.extend PretendStack
56 | n_layout = custom.fetch(:layout, {}).fetch(:resources, []).detect { |r| r[:name] == name }
57 | n_layout = (n_layout || custom.fetch(:layout, {})).fetch(:resources, []).detect { |r| r[:name] == s_name } || Smash.new
58 | n_stack.load_data(
59 | :name => s_name,
60 | :id => s_name,
61 | :template => content.get(:properties, :stack),
62 | :outputs => n_layout.fetch("outputs", []).map { |o_val| Smash.new(:key => o_val[:name], :value => o_val["finalValue"]) },
63 | :custom => {
64 | :resources => resources.all.map(&:attributes),
65 | :layout => n_layout,
66 | },
67 | ).valid_state
68 | n_stack.data[:logical_id] = s_name
69 | n_stack.data[:parent_stack] = self
70 | n_stack
71 | end
72 | end.compact
73 | if recurse
74 | (n_stacks + n_stacks.map(&:nested_stacks)).flatten.compact
75 | else
76 | n_stacks
77 | end
78 | end
79 |
80 | # @return [Hash] restructured google template
81 | def sparkleish_template_google(*args)
82 | copy_template = template.to_smash
83 | deref = lambda do |template|
84 | result = template.to_smash
85 | (result.delete(:resources) || []).each do |t_resource|
86 | t_name = t_resource.delete(:name)
87 | if t_resource[:type].to_s.end_with?(".jinja")
88 | schema = copy_template.fetch(:config, :content, :imports, []).delete("#{t_resource[:type]}.schema")
89 | schema_content = copy_template.fetch(:imports, []).detect do |s_item|
90 | s_item[:name] == schema
91 | end
92 | if schema_content
93 | t_resource.set(:parameters, schema_content.get(:content, :properties))
94 | end
95 | n_template = copy_template.fetch(:imports, []).detect do |s_item|
96 | s_item[:name] == t_resource[:type]
97 | end
98 | if n_template
99 | t_resource[:type] = "sparkleformation.stack"
100 | current_properties = t_resource.delete(:properties)
101 | t_resource.set(:properties, :parameters, current_properties) if current_properties
102 | t_resource.set(:properties, :stack, deref.call(n_template[:content]))
103 | end
104 | end
105 | result.set(:resources, t_name, t_resource)
106 | end
107 | result
108 | end
109 | s_template = deref.call(Smash.new(:resources => copy_template.get(:config, :content, :resources)))
110 | if s_template.empty?
111 | template.to_smash
112 | else
113 | layout = custom.fetch(:layout, {}).to_smash
114 | (layout.delete(:resources) || []).each do |l_resource|
115 | layout.set(:resources, l_resource.delete(:name), l_resource)
116 | end
117 | args.include?(:remove_wrapper) ? s_template.get(:resources, name, :properties, :stack) : s_template
118 | end
119 | end
120 |
121 | # @return [Hash]
122 | def root_parameters_google
123 | sparkleish_template.fetch(:resources, name, :properties, :parameters, Smash.new)
124 | end
125 | end
126 | end
127 | end
128 | end
129 |
--------------------------------------------------------------------------------
/lib/sfn/planner.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | # Interface for generating plan report
5 | class Planner
6 | autoload :Aws, "sfn/planner/aws"
7 |
8 | # Value to flag runtime modification
9 | RUNTIME_MODIFIED = "__MODIFIED_REFERENCE_VALUE__"
10 |
11 | # @return [Bogo::Ui]
12 | attr_reader :ui
13 | # @return [Smash]
14 | attr_reader :config
15 | # @return [Array] CLI arguments
16 | attr_reader :arguments
17 | # @return [Miasma::Models::Orchestration::Stack] existing remote stack
18 | attr_reader :origin_stack
19 | # @return [Hash] custom options
20 | attr_reader :options
21 |
22 | # Create a new planner instance
23 | #
24 | # @param ui [Bogo::Ui]
25 | # @param config [Smash]
26 | # @param arguments [Array]
27 | # @param stack [Miasma::Models::Orchestration::Stack]
28 | # @param opts [Hash]
29 | #
30 | # @return [self]
31 | def initialize(ui, config, arguments, stack, opts = {})
32 | @ui = ui
33 | @config = config
34 | @arguments = arguments
35 | @origin_stack = stack
36 | @options = opts
37 | end
38 |
39 | # Generate update report
40 | #
41 | # @param template [Hash] updated template
42 | # @param parameters [Hash] runtime parameters for update
43 | #
44 | # @return [Miasma::Models::Orchestration::Stack::Plan] report
45 | def generate_plan(template, parameters)
46 | raise NotImplementedError
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/sfn/utils.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | # Utility classes and modules
5 | module Utils
6 | autoload :Output, "sfn/utils/output"
7 | autoload :StackParameterValidator, "sfn/utils/stack_parameter_validator"
8 | autoload :StackParameterScrubber, "sfn/utils/stack_parameter_scrubber"
9 | autoload :StackExporter, "sfn/utils/stack_exporter"
10 | autoload :Debug, "sfn/utils/debug"
11 | autoload :JSON, "sfn/utils/json"
12 | autoload :Ssher, "sfn/utils/ssher"
13 | autoload :ObjectStorage, "sfn/utils/object_storage"
14 | autoload :PathSelector, "sfn/utils/path_selector"
15 |
16 | # Provide methods directly from module for previous version compatibility
17 | extend JSON
18 | extend ObjectStorage
19 | extend Bogo::AnimalStrings
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/sfn/utils/debug.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module Utils
5 | # Debug helpers
6 | module Debug
7 | # Output helpers
8 | module Output
9 | # Write debug message
10 | #
11 | # @param msg [String]
12 | def debug(msg)
13 | if ENV["DEBUG"] || (respond_to?(:config) && config[:debug])
14 | puts ": #{msg}"
15 | end
16 | end
17 | end
18 |
19 | class << self
20 | # Load module into class
21 | #
22 | # @param klass [Class]
23 | def included(klass)
24 | klass.class_eval do
25 | include Output
26 | extend Output
27 | end
28 | end
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/sfn/utils/json.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module Utils
5 |
6 | # JSON helper methods
7 | module JSON
8 |
9 | # Convert to JSON
10 | #
11 | # @param thing [Object]
12 | # @return [String]
13 | def _to_json(thing)
14 | MultiJson.dump(thing)
15 | end
16 |
17 | alias_method :dump_json, :_to_json
18 |
19 | # Load JSON data
20 | #
21 | # @param thing [String]
22 | # @return [Object]
23 | def _from_json(thing)
24 | MultiJson.load(thing)
25 | end
26 |
27 | alias_method :load_json, :_from_json
28 |
29 | # Format object into pretty JSON
30 | #
31 | # @param thing [Object]
32 | # @return [String]
33 | def _format_json(thing)
34 | thing = _from_json(thing) if thing.is_a?(String)
35 | MultiJson.dump(thing, :pretty => true)
36 | end
37 |
38 | alias_method :format_json, :_format_json
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/sfn/utils/object_storage.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module Utils
5 |
6 | # Storage helpers
7 | module ObjectStorage
8 |
9 | # Write to file
10 | #
11 | # @param object [Object]
12 | # @param path [String] path to write object
13 | # @param directory [Miasma::Models::Storage::Directory]
14 | # @return [String] file path
15 | def file_store(object, path, directory)
16 | raise NotImplementedError.new "Internal updated required! :("
17 | content = object.is_a?(String) ? object : Utils._format_json(object)
18 | directory.files.create(
19 | :identity => path,
20 | :body => content,
21 | )
22 | loc = directory.service.service.name.split("::").last.downcase
23 | "#{loc}://#{directory.identity}/#{path}"
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/sfn/utils/output.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module Utils
5 | # Output Helpers
6 | module Output
7 |
8 | # Process things and return items
9 | #
10 | # @param things [Array] items to process
11 | # @param args [Hash] options
12 | # @option args [TrueClass, FalseClass] :flat flatten result array
13 | # @option args [Array] :attributes attributes to extract
14 | # @todo this was extracted from events and needs to be cleaned up
15 | def process(things, args = {})
16 | @event_ids ||= []
17 | processed = things.reverse.map do |thing|
18 | next if @event_ids.include?(thing["id"])
19 | @event_ids.push(thing["id"]).compact!
20 | if args[:attributes]
21 | args[:attributes].map do |key|
22 | thing[key].to_s
23 | end
24 | else
25 | thing.values
26 | end
27 | end
28 | args[:flat] ? processed.flatten : processed
29 | end
30 |
31 | # Generate formatted titles
32 | #
33 | # @param thing [Object] thing being processed
34 | # @param args [Hash]
35 | # @option args [Array] :attributes
36 | # @return [Array] formatted titles
37 | def get_titles(thing, args = {})
38 | attrs = args[:attributes] || []
39 | if attrs.empty?
40 | hash = thing.is_a?(Array) ? thing.first : thing
41 | hash ||= {}
42 | attrs = hash.keys
43 | end
44 | titles = attrs.map do |key|
45 | camel(key).gsub(/([a-z])([A-Z])/, '\1 \2')
46 | end.compact
47 | if args[:format]
48 | titles.map { |s| @ui.color(s, :bold) }
49 | else
50 | titles
51 | end
52 | end
53 |
54 | # Output stack related things in nice format
55 | #
56 | # @param stack [String] name of stack
57 | # @param things [Array] things to display
58 | # @param what [String] description of things for output
59 | # @param args [Symbol] options (:ignore_empty_output)
60 | def things_output(stack, things, what, *args)
61 | unless args.include?(:no_title)
62 | output = get_titles(things, :format => true, :attributes => allowed_attributes)
63 | else
64 | output = []
65 | end
66 | columns = allowed_attributes.size
67 | output += process(things, :flat => true, :attributes => allowed_attributes)
68 | output.compact!
69 | if output.empty?
70 | ui.warn "No information found" unless args.include?(:ignore_empty_output)
71 | else
72 | ui.info "#{what.to_s.capitalize} for stack: #{ui.color(stack, :bold)}" if stack
73 | ui.info "#{ui.list(output, :uneven_columns_across, columns)}"
74 | end
75 | end
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/lib/sfn/utils/path_selector.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 | require "pathname"
3 |
4 | module Sfn
5 | module Utils
6 |
7 | # Helper methods for path selection
8 | module PathSelector
9 |
10 | # Humanize the base name of path
11 | #
12 | # @param path [String]
13 | # @return [String]
14 | def humanize_path_basename(path)
15 | File.basename(path).sub(
16 | File.extname(path), ""
17 | ).split(/[-_]/).map(&:capitalize).join(" ")
18 | end
19 |
20 | # Prompt user for file selection
21 | #
22 | # @param directory [String] path to directory
23 | # @param opts [Hash] options
24 | # @option opts [Array] :ignore_directories directory names
25 | # @option opts [String] :directories_name title for directories
26 | # @option opts [String] :files_name title for files
27 | # @option opts [String] :filter_prefix only return results matching filter
28 | # @return [String] file path
29 | def prompt_for_file(directory, opts = {})
30 | file_list = Dir.glob(File.join(directory, "**", "**", "*")).find_all do |file|
31 | File.file?(file)
32 | end
33 | if opts[:filter_prefix]
34 | file_list = file_list.find_all do |file|
35 | file.start_with?(options[:filter_prefix])
36 | end
37 | end
38 | directories = file_list.map do |file|
39 | File.dirname(file)
40 | end.uniq
41 | files = file_list.find_all do |path|
42 | path.sub(directory, "").split("/").size == 2
43 | end
44 | if opts[:ignore_directories]
45 | directories.delete_if do |dir|
46 | opts[:ignore_directories].include?(File.basename(dir))
47 | end
48 | end
49 | if directories.empty? && files.empty?
50 | ui.fatal "No formation paths discoverable!"
51 | else
52 | output = ["Please select an entry"]
53 | output << "(or directory to list):" unless directories.empty?
54 | ui.info output.join(" ")
55 | output.clear
56 | idx = 1
57 | valid = {}
58 | unless directories.empty?
59 | output << ui.color("#{opts.fetch(:directories_name, "Directories")}:", :bold)
60 | directories.each do |dir|
61 | valid[idx] = {:path => dir, :type => :directory}
62 | output << [idx, humanize_path_basename(dir)]
63 | idx += 1
64 | end
65 | end
66 | unless files.empty?
67 | output << ui.color("#{opts.fetch(:files_name, "Files")}:", :bold)
68 | files.each do |file|
69 | valid[idx] = {:path => file, :type => :file}
70 | output << [idx, humanize_path_basename(file)]
71 | idx += 1
72 | end
73 | end
74 | max = idx.to_s.length
75 | output.map! do |o|
76 | if o.is_a?(Array)
77 | " #{o.first}.#{" " * (max - o.first.to_s.length)} #{o.last}"
78 | else
79 | o
80 | end
81 | end
82 | ui.info "#{output.join("\n")}\n"
83 | response = ui.ask_question("Enter selection: ").to_i
84 | unless valid[response]
85 | ui.fatal "How about using a real value"
86 | exit 1
87 | else
88 | entry = valid[response.to_i]
89 | if entry[:type] == :directory
90 | prompt_for_file(entry[:path], opts)
91 | elsif Pathname(entry[:path]).absolute?
92 | entry[:path]
93 | else
94 | "/#{entry[:path]}"
95 | end
96 | end
97 | end
98 | end
99 | end
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/lib/sfn/utils/ssher.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module Utils
5 |
6 | # Helper methods for SSH interactions
7 | module Ssher
8 |
9 | # Retrieve file from remote node
10 | #
11 | # @param address [String]
12 | # @param user [String]
13 | # @param path [String] remote file path
14 | # @param ssh_opts [Hash]
15 | # @return [String, NilClass]
16 | def remote_file_contents(address, user, path, ssh_opts = {})
17 | if path.to_s.strip.empty?
18 | raise ArgumentError.new "No file path provided!"
19 | end
20 | require "net/ssh"
21 | content = ""
22 | ssh_session = Net::SSH.start(address, user, ssh_opts)
23 | content = ssh_session.exec!("sudo cat #{path}")
24 | content.empty? ? nil : content
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/sfn/utils/stack_parameter_scrubber.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
3 | module Sfn
4 | module Utils
5 | # Helper for scrubbing stack parameters
6 | module StackParameterScrubber
7 |
8 | # Validate attributes within Parameter blocks
9 | ALLOWED_PARAMETER_ATTRIBUTES = [
10 | "Type", "Default", "NoEcho", "AllowedValues", "AllowedPattern",
11 | "MaxLength", "MinLength", "MaxValue", "MinValue", "Description",
12 | "ConstraintDescription",
13 | ]
14 |
15 | # Clean the parameters of the template
16 | #
17 | # @param template [Hash]
18 | # @return [Hash] template
19 | def parameter_scrub!(template)
20 | parameters = template["Parameters"]
21 | if parameters
22 | parameters.each do |name, options|
23 | options.delete_if do |attribute, value|
24 | !ALLOWED_PARAMETER_ATTRIBUTES.include?(attribute)
25 | end
26 | end
27 | template["Parameters"] = parameters
28 | end
29 | template
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/sfn/version.rb:
--------------------------------------------------------------------------------
1 | module Sfn
2 | # Current library version
3 | VERSION = Gem::Version.new("3.1.9")
4 | end
5 |
--------------------------------------------------------------------------------
/sfn.gemspec:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + "/lib/"
2 | require "sfn/version"
3 | Gem::Specification.new do |s|
4 | s.name = "sfn"
5 | s.version = Sfn::VERSION.version
6 | s.summary = "SparkleFormation CLI"
7 | s.author = "Chris Roberts"
8 | s.email = "code@chrisroberts.org"
9 | s.homepage = "http://github.com/sparkleformation/sfn"
10 | s.description = "SparkleFormation CLI"
11 | s.license = "Apache-2.0"
12 | s.require_path = "lib"
13 | s.add_runtime_dependency "bogo-cli", ">= 0.2.5", "< 0.4"
14 | s.add_runtime_dependency "bogo-ui", ">= 0.1.28", "< 0.4"
15 | s.add_runtime_dependency "miasma", ">= 0.3.3", "< 0.4"
16 | s.add_runtime_dependency "miasma-aws", ">= 0.3.15", "< 0.4"
17 | s.add_runtime_dependency "miasma-azure", ">= 0.1.0", "< 0.3"
18 | s.add_runtime_dependency "miasma-open-stack", ">= 0.1.0", "< 0.3"
19 | s.add_runtime_dependency "miasma-rackspace", ">= 0.1.0", "< 0.3"
20 | s.add_runtime_dependency "miasma-google", ">= 0.1.0", "< 0.3"
21 | s.add_runtime_dependency "miasma-terraform", ">= 0.1.0", "< 0.2.0"
22 | s.add_runtime_dependency "jmespath"
23 | s.add_runtime_dependency "net-ssh"
24 | s.add_runtime_dependency "sparkle_formation", ">= 3.0.39", "< 4"
25 | s.add_runtime_dependency "hashdiff", "~> 0.2.2"
26 | s.add_runtime_dependency "graph", "~> 2.8.1"
27 | s.add_development_dependency "rake", "~> 10"
28 | s.add_development_dependency "minitest"
29 | s.add_development_dependency "rspec", "~> 3.5"
30 | s.add_development_dependency "rufo", "~> 0.3.0"
31 | s.add_development_dependency "mocha"
32 | s.add_development_dependency "yard"
33 | s.executables << "sfn"
34 | s.files = Dir["{lib,bin,docs}/**/*"] + %w(sfn.gemspec README.md CHANGELOG.md LICENSE)
35 | end
36 |
--------------------------------------------------------------------------------
/test/helper.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 | require "bogo-ui"
3 | require "minitest/autorun"
4 | require "mocha/mini_test"
5 | require "tempfile"
6 | require "openssl"
7 |
8 | # Stub out HTTP so we can easily intercept remote calls
9 | require "http"
10 |
11 | module HTTP
12 | class << self
13 | HTTP::Request::METHODS.each do |h_method|
14 | define_method(h_method) do |*args|
15 | $mock.send(h_method, *args)
16 | end
17 | end
18 | end
19 |
20 | class Client
21 | HTTP::Request::METHODS.each do |h_method|
22 | define_method(h_method) do |*args|
23 | $mock.send(h_method, *args)
24 | end
25 | end
26 | end
27 | end
28 |
29 | module SfnHttpMock
30 | def setup
31 | $mock = Mocha::Mock.new(Mocha::Mockery.instance)
32 | end
33 |
34 | def teardown
35 | $mock = nil
36 | @ui = nil
37 | @stream = nil
38 | if @google_key && File.exist?(@google_key)
39 | File.delete(@google_key)
40 | end
41 | end
42 |
43 | def stream
44 | @stream ||= StringIO.new("")
45 | end
46 |
47 | def ui
48 | @ui ||= Bogo::Ui.new(
49 | :app_name => "TestUi",
50 | :output_to => stream,
51 | :colors => false,
52 | )
53 | end
54 |
55 | def aws_creds
56 | Smash[
57 | %w(aws_access_key_id aws_secret_access_key aws_region).map do |key|
58 | [key, key.upcase]
59 | end
60 | ].merge(:provider => :aws)
61 | end
62 |
63 | def azure_creds
64 | Smash[
65 | %w(azure_tenant_id azure_client_id azure_subscription_id azure_client_secret
66 | azure_region azure_blob_account_name azure_blob_secret_key).map do |key|
67 | [key, key.upcase]
68 | end
69 | ].merge(:provider => :azure)
70 | end
71 |
72 | def google_creds
73 | key_file = Tempfile.new("sfn-test")
74 | key_path = key_file.path
75 | key_file.delete
76 | key_file = File.open(key_path, "w")
77 | key_file.puts OpenSSL::PKey::RSA.new(2048).to_pem
78 | key_file.close
79 | @google_key = key_file.path
80 | Smash[
81 | %w(google_service_account_email google_auth_scope google_project).map do |key|
82 | [key, key.upcase]
83 | end
84 | ].merge(
85 | :provider => :google,
86 | :google_service_account_private_key => @google_key,
87 | )
88 | end
89 |
90 | def heat_creds
91 | end
92 |
93 | def rackspace_creds
94 | Smash[
95 | %w(rackspace_api_key rackspace_username rackspace_region).map do |key|
96 | [key, key.upcase]
97 | end
98 | ].merge(:provider => :rackspace)
99 | end
100 |
101 | def http_response(opts = {})
102 | opts[:version] ||= "1.1"
103 | opts[:status] ||= 200
104 | opts[:body] ||= "{}"
105 | HTTP::Response.new(opts)
106 | end
107 | end
108 |
109 | class MiniTest::Test
110 | include SfnHttpMock
111 | end
112 |
--------------------------------------------------------------------------------
/test/rspecs.rb:
--------------------------------------------------------------------------------
1 | require "sfn"
2 |
--------------------------------------------------------------------------------
/test/rspecs/lib/callback/stack_policy_rspec.rb:
--------------------------------------------------------------------------------
1 | require_relative "../../../rspecs"
2 |
3 | RSpec.describe Sfn::Callback::StackPolicy do
4 | let(:ui) { double(:ui) }
5 | let(:config) { double(:config) }
6 | let(:arguments) { double(:arguments) }
7 | let(:api) { double(:api) }
8 |
9 | let(:instance) { subject.new(ui, config, arguments, api) }
10 |
11 | context "with no stack polciies defined" do
12 | before do
13 | end
14 | it "should not error if no policies are defined" do
15 | end
16 | end
17 |
18 | it "should fail" do
19 | pending
20 | expect(true).to be false
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/test/rspecs/lib/command_module/callbacks_rspec.rb:
--------------------------------------------------------------------------------
1 | require_relative "../../../rspecs"
2 |
3 | RSpec.describe Sfn::CommandModule::Callbacks do
4 | let(:ui) { double(:ui) }
5 | let(:callbacks) { [] }
6 | let(:config) { double(:config) }
7 | let(:arguments) { double(:arguments) }
8 | let(:provider) { double(:provider) }
9 | let(:instance) { klass.new(config, ui, arguments, provider) }
10 | let(:klass) {
11 | Class.new {
12 | include Sfn::CommandModule::Callbacks
13 | attr_reader :config, :ui, :arguments, :provider
14 |
15 | def initialize(c, u, a, p)
16 | @config = c
17 | @ui = u
18 | @arguments = a
19 | @provider = p
20 | end
21 |
22 | def self.name
23 | "Sfn::Callback::Status"
24 | end
25 | }
26 | }
27 |
28 | before do
29 | allow(Sfn::Callback).to receive(:const_get).and_return(klass)
30 | allow(config).to receive(:fetch).with(:callbacks, any_args).and_return(callbacks)
31 | allow(ui).to receive(:debug)
32 | allow(ui).to receive(:info)
33 | allow(config).to receive(:[])
34 | allow(ui).to receive(:color)
35 | end
36 |
37 | describe "#api_action!" do
38 | before { allow(instance).to receive(:run_callbacks_for) }
39 |
40 | it "should run specific and general before and after callbacks" do
41 | expect(instance).to receive(:run_callbacks_for).with(["before_status", :before], any_args)
42 | expect(instance).to receive(:run_callbacks_for).with(["after_status", :after], any_args)
43 | instance.api_action!
44 | end
45 |
46 | it "should run failed callbacks on error" do
47 | expect(instance).to receive(:run_callbacks_for).with(["before_status", :before], any_args)
48 | expect(instance).to receive(:run_callbacks_for).with(["failed_status", :failed], any_args)
49 | expect { instance.api_action! { raise "error" } }.to raise_error(RuntimeError)
50 | end
51 |
52 | it "should provide exception to callbacks on error" do
53 | expect(instance).to receive(:run_callbacks_for).with(["failed_status", :failed], instance_of(RuntimeError))
54 | expect { instance.api_action! { raise "error" } }.to raise_error(RuntimeError)
55 | end
56 | end
57 |
58 | describe "#run_callbacks_for" do
59 | let(:callbacks) { ["status"] }
60 |
61 | it "should run the callback" do
62 | expect_any_instance_of(klass).to receive(:before)
63 | instance.run_callbacks_for(:before)
64 | end
65 | end
66 |
67 | describe "#callbacks_for" do
68 | it "should load callbacks defined within configuration" do
69 | expect(config).to receive(:fetch).with(:callbacks, :before, []).and_return([])
70 | expect(config).to receive(:fetch).with(:callbacks, :default, []).and_return([])
71 | expect(instance.callbacks_for(:before)).to eq([])
72 | end
73 |
74 | context "callback name configured" do
75 | let(:callbacks) { ["status"] }
76 |
77 | it "should lookup callbacks within namespace" do
78 | expect(Sfn::Callback).to receive(:const_get).with("Status").and_return(klass)
79 | expect(instance.callbacks_for(:before)).to be_a(Array)
80 | end
81 |
82 | it "should raise error when class not found" do
83 | expect(Sfn::Callback).to receive(:const_get).and_call_original
84 | expect { instance.callbacks_for(:before) }.to raise_error(NameError)
85 | end
86 | end
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/test/rspecs/lib/command_module/stack_rspec.rb:
--------------------------------------------------------------------------------
1 | require_relative "../../../rspecs"
2 |
3 | RSpec.describe Sfn::CommandModule::Stack::InstanceMethods do
4 | let(:subject) do
5 | Class.tap { |c| c.include(described_class) }.new
6 | end
7 |
8 | describe "#generate_custom_apply_mappings" do
9 | let(:config) { {apply_mapping: mappings} }
10 | let(:mappings) { nil }
11 | let(:provider_stack) { double("provider_stack", api: api, name: stack_name) }
12 | let(:stack_name) { nil }
13 | let(:api) { double("api", data: api_data) }
14 | let(:api_data) { {} }
15 |
16 | before { allow(subject).to receive(:config).and_return(config) }
17 |
18 | it "should return nil by default" do
19 | expect(subject.generate_custom_apply_mappings(provider_stack)).
20 | to be_nil
21 | end
22 |
23 | context "when mappings are set" do
24 | let(:mappings) {
25 | {"test-stack__OriginKey" => "DestKey",
26 | "other-stack__StartKey" => "EndKey"}
27 | }
28 |
29 | it "should return empty hash" do
30 | expect(subject.generate_custom_apply_mappings(provider_stack)).
31 | to eq({})
32 | end
33 |
34 | context "when stack name is set" do
35 | let(:stack_name) { "test-stack" }
36 |
37 | it "should return hash with single item" do
38 | expect(subject.generate_custom_apply_mappings(provider_stack).size).
39 | to eq(1)
40 | end
41 |
42 | it "should include the origin key" do
43 | expect(subject.generate_custom_apply_mappings(provider_stack).keys).
44 | to include("OriginKey")
45 | end
46 |
47 | it "should include the dest key value" do
48 | expect(subject.generate_custom_apply_mappings(provider_stack).values).
49 | to include("DestKey")
50 | end
51 | end
52 |
53 | context "when mappings are plain" do
54 | let(:mappings) {
55 | {"OriginKey" => "DestKey",
56 | "other-stack__StartKey" => "EndKey"}
57 | }
58 |
59 | it "should return hash with single item" do
60 | expect(subject.generate_custom_apply_mappings(provider_stack).size).
61 | to eq(1)
62 | end
63 |
64 | it "should include the origin key" do
65 | expect(subject.generate_custom_apply_mappings(provider_stack).keys).
66 | to include("OriginKey")
67 | end
68 |
69 | it "should include the dest key value" do
70 | expect(subject.generate_custom_apply_mappings(provider_stack).values).
71 | to include("DestKey")
72 | end
73 | end
74 |
75 | context "when mapping includes remote location" do
76 | let(:mappings) {
77 | {"remote_provider__test-stack__OriginKey" => "DestKey",
78 | "other-stack__StartKey" => "EndKey"}
79 | }
80 | let(:stack_name) { "test-stack" }
81 |
82 | it "should return empty hash" do
83 | expect(subject.generate_custom_apply_mappings(provider_stack)).
84 | to eq({})
85 | end
86 |
87 | context "with location set in api data" do
88 | let(:api_data) {
89 | {location: "remote_provider"}
90 | }
91 |
92 | it "should return hash with single item" do
93 | expect(subject.generate_custom_apply_mappings(provider_stack).size).
94 | to eq(1)
95 | end
96 |
97 | it "should include the origin key" do
98 | expect(subject.generate_custom_apply_mappings(provider_stack).keys).
99 | to include("OriginKey")
100 | end
101 |
102 | it "should include the dest key value" do
103 | expect(subject.generate_custom_apply_mappings(provider_stack).values).
104 | to include("DestKey")
105 | end
106 | end
107 |
108 | context "when mapping format is invalid" do
109 | let(:mappings) {
110 | {"invalid__provider__stack__OriginKey" => "DestKey"}
111 | }
112 |
113 | it "should raise an error" do
114 | expect {
115 | subject.generate_custom_apply_mappings(provider_stack)
116 | }.to raise_error(ArgumentError)
117 | end
118 | end
119 | end
120 | end
121 | end
122 | end
123 |
--------------------------------------------------------------------------------
/test/rspecs/sfn_rspec.rb:
--------------------------------------------------------------------------------
1 | require_relative "../rspecs"
2 |
3 | RSpec.describe Sfn do
4 | end
5 |
--------------------------------------------------------------------------------
/test/specs/command/lint_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative "../../helper"
2 |
3 | describe Sfn::Command::Lint do
4 | let(:creds) { aws_creds }
5 |
6 | before do
7 | rs = Sfn::Lint::RuleSet.build(:resource_check) do
8 | rule :aws_resources_only do
9 | definition "Resources.[*][0][*].Type" do |search|
10 | unless search.nil?
11 | result = search.find_all { |i| !i.start_with?("AWS") }
12 | result.empty? ? true : result
13 | else
14 | true
15 | end
16 | end
17 |
18 | fail_message "All types must be within AWS root namespace"
19 | end
20 | end
21 | Sfn::Lint::RuleSet.register(rs)
22 | end
23 |
24 | it "should successfully run on valid template" do
25 | instance = Sfn::Command::Lint.new(
26 | Smash.new(
27 | :ui => ui,
28 | :base_directory => File.join(File.dirname(__FILE__), "sparkleformation"),
29 | :credentials => aws_creds,
30 | :file => "lint_valid",
31 | ),
32 | []
33 | )
34 | instance.execute!
35 | stream.rewind
36 | stream.read.must_include "VALID"
37 | end
38 |
39 | it "should fail on invalid template" do
40 | instance = Sfn::Command::Lint.new(
41 | Smash.new(
42 | :ui => ui,
43 | :base_directory => File.join(File.dirname(__FILE__), "sparkleformation"),
44 | :credentials => aws_creds,
45 | :file => "lint_invalid",
46 | ),
47 | []
48 | )
49 | -> { instance.execute! }.must_raise RuntimeError
50 | stream.rewind
51 | stream.read.must_include "INVALID"
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/test/specs/command/print_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative "../../helper"
2 |
3 | describe Sfn::Command::Print do
4 | it "should print the template only" do
5 | instance = Sfn::Command::Print.new(
6 | Smash.new(
7 | :ui => ui,
8 | :base_directory => File.join(File.dirname(__FILE__), "sparkleformation"),
9 | :credentials => aws_creds,
10 | :file => "lint_valid",
11 | ),
12 | []
13 | )
14 | instance.execute!
15 | stream.rewind
16 | stream.read.start_with?("{").must_equal true
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/specs/command/sparkleformation/dummy.rb:
--------------------------------------------------------------------------------
1 | SparkleFormation.new(:dummy) do
2 | value true
3 | end
4 |
--------------------------------------------------------------------------------
/test/specs/command/sparkleformation/dummy_azure.rb:
--------------------------------------------------------------------------------
1 | SparkleFormation.new(:dummy_azure, :provider => :azure) do
2 | value true
3 | end
4 |
--------------------------------------------------------------------------------
/test/specs/command/sparkleformation/dummy_google.rb:
--------------------------------------------------------------------------------
1 | SparkleFormation.new(:dummy_google, :provider => :google) do
2 | value true
3 | end
4 |
--------------------------------------------------------------------------------
/test/specs/command/sparkleformation/lint_invalid.rb:
--------------------------------------------------------------------------------
1 | SparkleFormation.new(:lint_invalid) do
2 | dynamic!(:s3_bucket, :test)
3 | resources.bad_resource.type "Invalid::Resource"
4 | end
5 |
--------------------------------------------------------------------------------
/test/specs/command/sparkleformation/lint_valid.rb:
--------------------------------------------------------------------------------
1 | SparkleFormation.new(:lint_valid) do
2 | dynamic!(:s3_bucket, :test)
3 | end
4 |
--------------------------------------------------------------------------------
/test/specs/command/sparkleformation/nested_dummy.rb:
--------------------------------------------------------------------------------
1 | SparkleFormation.new(:nested_dummy) do
2 | nest!(:dummy)
3 | end
4 |
--------------------------------------------------------------------------------
/test/specs/command/sparkleformation/nested_dummy_azure.rb:
--------------------------------------------------------------------------------
1 | SparkleFormation.new(:nested_dummy_azure, :provider => :azure) do
2 | nest!(:dummy_azure)
3 | end
4 |
--------------------------------------------------------------------------------
/test/specs/command/sparkleformation/nested_dummy_google.rb:
--------------------------------------------------------------------------------
1 | SparkleFormation.new(:nested_dummy_google, :provider => :google) do
2 | nest!(:dummy_google)
3 | end
4 |
--------------------------------------------------------------------------------
/test/specs/command_module/stack_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative "../../helper"
2 |
3 | describe Sfn::CommandModule::Stack do
4 | before do
5 | @stack = Class.new do
6 | def initialize
7 | @config = Smash.new
8 | @ui = Class.new do
9 | def debug(*_)
10 | end
11 | end.new
12 | end
13 |
14 | attr_reader :config, :ui
15 | end
16 | @stack.include Sfn::CommandModule::Stack
17 | end
18 |
19 | let(:instance) { @instance ||= @stack.new }
20 |
21 | describe "Parameter population helpers" do
22 | describe "Config parameters location" do
23 | before do
24 | instance.config.set(:parameters, :nested__StackItem__MyParameter, "value")
25 | instance.config.set(:parameters, :NotNestedParameter, "value")
26 | instance.config.set(:parameters, :snake_cased_parameter, "value")
27 | end
28 |
29 | it "should match camel cased parameter" do
30 | instance.locate_config_parameter_key([], "NotNestedParameter", "root").must_equal "NotNestedParameter"
31 | instance.config.get(:parameters, "NotNestedParameter").must_equal "value"
32 | end
33 |
34 | it "should match snake cased parameter when camel cased" do
35 | instance.locate_config_parameter_key([], "not_nested_parameter", "root").must_equal "not_nested_parameter"
36 | instance.config.get(:parameters, "NotNestedParameter").must_be_nil
37 | instance.config.get(:parameters, :not_nested_parameter).must_equal "value"
38 | end
39 |
40 | it "should match snake cased parameter" do
41 | instance.locate_config_parameter_key([], "snake_cased_parameter", "root").must_equal "snake_cased_parameter"
42 | instance.config.get(:parameters, :snake_cased_parameter).must_equal "value"
43 | end
44 |
45 | it "should match camel cased parameter when snake cased" do
46 | instance.locate_config_parameter_key([], "SnakeCasedParameter", "root").must_equal "SnakeCasedParameter"
47 | instance.config.get(:parameters, :snake_cased_parameter).must_be_nil
48 | instance.config.get(:parameters, "SnakeCasedParameter").must_equal "value"
49 | end
50 |
51 | it "should match camel cased nested stack parameter" do
52 | instance.locate_config_parameter_key(["nested", "StackItem"], "MyParameter", "root").must_equal "nested__StackItem__MyParameter"
53 | instance.config.get(:parameters, "nested__StackItem__MyParameter").must_equal "value"
54 | end
55 |
56 | it "should match snake cased nested stack parameter when camel cased" do
57 | instance.locate_config_parameter_key(["nested", "stack_item"], "my_parameter", "root").must_equal "nested__stack_item__my_parameter"
58 | instance.config.get(:parameters, "nested__StackItem__MyParameter").must_be_nil
59 | instance.config.get(:parameters, "nested__stack_item__my_parameter").must_equal "value"
60 | end
61 |
62 | it "should return composite key via arg values when not found" do
63 | instance.locate_config_parameter_key(["nested", "StackItem"], "Unknown", "root").must_equal "nested__StackItem__Unknown"
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/test/specs/command_module/template_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative "../../helper"
2 |
3 | describe Sfn::CommandModule::Template do
4 | before do
5 | @template = Class.new do
6 | def initialize
7 | @config = Smash.new
8 | @arguments = []
9 | @ui = AttributeStruct.new
10 | end
11 |
12 | attr_reader :config, :arguments, :ui
13 | end
14 | @template.include Sfn::CommandModule::Template
15 | end
16 |
17 | let(:instance) { @instance ||= @template.new }
18 |
19 | describe "Compile time parameter merging" do
20 | it "should automatically merge items when stack name is not used" do
21 | instance.arguments << "stack-name"
22 | instance.config.set(:compile_parameters, "stack-name__Fubar", "key1", "value")
23 | instance.config.set(:compile_parameters, "Fubar", "key2", "value2")
24 | result = instance.merge_compile_time_parameters
25 | result.get("stack-name__Fubar", "key1").must_equal "value"
26 | result.get("stack-name__Fubar", "key2").must_equal "value2"
27 | result.get("Fubar").must_be_nil
28 | end
29 |
30 | it "should automatically prefix stack name when not provided" do
31 | instance.arguments << "stack-name"
32 | instance.config.set(:compile_parameters, "Fubar", "key2", "value2")
33 | result = instance.merge_compile_time_parameters
34 | result.get("stack-name__Fubar", "key2").must_equal "value2"
35 | result.get("Fubar").must_be_nil
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/test/specs/config/fail-auto-json/.sfn:
--------------------------------------------------------------------------------
1 | {
2 | "processing": [}
3 | }
--------------------------------------------------------------------------------
/test/specs/config/fail-auto-ruby/.sfn:
--------------------------------------------------------------------------------
1 | Configuration.new do
2 | processing [}
3 | end
--------------------------------------------------------------------------------
/test/specs/config/fail-auto-yaml/.sfn:
--------------------------------------------------------------------------------
1 | ---
2 | :processing: [}
--------------------------------------------------------------------------------
/test/specs/config/json-ext/.sfn.json:
--------------------------------------------------------------------------------
1 | {
2 | "processing": false
3 | }
4 |
--------------------------------------------------------------------------------
/test/specs/config/no-ext/.sfn:
--------------------------------------------------------------------------------
1 | Configuration.new do
2 | processing false
3 | end
--------------------------------------------------------------------------------
/test/specs/config/ruby-ext/.sfn.rb:
--------------------------------------------------------------------------------
1 | Configuration.new do
2 | processing false
3 | end
4 |
--------------------------------------------------------------------------------
/test/specs/config/yaml-ext/.sfn:
--------------------------------------------------------------------------------
1 | ---
2 | :processing: false
--------------------------------------------------------------------------------
/test/specs/config_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative "../helper"
2 |
3 | describe Sfn::Config do
4 | describe "Core configuration" do
5 | it "should properly coerce string value to hash" do
6 | config = Sfn::Config.new(:credentials => "key1:value1,key2:value2")
7 | config[:credentials].must_equal "key1" => "value1", "key2" => "value2"
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/test/specs/sfn_bin_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative "../helper"
2 |
3 | describe "sfn" do
4 | def be_sh(command, options = {})
5 | result = `#{command} 2>&1`
6 | unless $?.success?
7 | unless options[:fail] == true || options[:fail] == $?.exitstatus
8 | raise "Command Failed `#{command}` - #{result}"
9 | end
10 | end
11 | result
12 | end
13 |
14 | it "shows help without arguments" do
15 | be_sh("sfn", :fail => true).must_include "Available commands:"
16 | end
17 |
18 | it "shows help when asking for help" do
19 | be_sh("sfn --help").must_include "Available commands:"
20 | end
21 |
22 | it "shows version" do
23 | be_sh("sfn --version").must_include "SparkleFormation CLI - [Version:"
24 | end
25 |
26 | it "errors on unknown flags" do
27 | -> { be_sh("sfn create --fubar") }.must_raise StandardError
28 | be_sh("sfn create --fubar", :fail => true).must_include "--fubar"
29 | end
30 |
31 | it "should include stack trace when debug flag provided" do
32 | be_sh("sfn create --fubar --debug", :fail => true).must_include "in `validate_arguments!"
33 | end
34 |
35 | describe "configuration file" do
36 | let(:config_dir) { File.join(File.dirname(__FILE__), "config") }
37 |
38 | it "should load configuration file with no extension" do
39 | result = Dir.chdir(File.join(config_dir, "no-ext")) do
40 | be_sh("sfn conf")
41 | end
42 | result.must_match /processing.*?:.*?false/
43 | end
44 |
45 | it "should load Ruby configuration file with extension" do
46 | result = Dir.chdir(File.join(config_dir, "ruby-ext")) do
47 | be_sh("sfn conf")
48 | end
49 | result.must_match /processing.*?:.*?false/
50 | end
51 |
52 | it "should load YAML configuration file with extension" do
53 | result = Dir.chdir(File.join(config_dir, "yaml-ext")) do
54 | be_sh("sfn conf")
55 | end
56 | result.must_match /processing.*?:.*?false/
57 | end
58 |
59 | it "should load JSON configuration file with extension" do
60 | result = Dir.chdir(File.join(config_dir, "json-ext")) do
61 | be_sh("sfn conf")
62 | end
63 | result.must_match /processing.*?:.*?false/
64 | end
65 |
66 | it "should display JSON specific load error when JSON load fails" do
67 | result = Dir.chdir(File.join(config_dir, "fail-auto-json")) do
68 | be_sh("sfn conf", :fail => true)
69 | end
70 | result.must_include "unexpected token"
71 | end
72 |
73 | it "should display Ruby specific load error when Ruby load fails" do
74 | result = Dir.chdir(File.join(config_dir, "fail-auto-ruby")) do
75 | be_sh("sfn conf", :fail => true)
76 | end
77 | result.must_include "syntax error"
78 | end
79 |
80 | it "should display YAML specific load error when YAML load fails" do
81 | result = Dir.chdir(File.join(config_dir, "fail-auto-yaml")) do
82 | be_sh("sfn conf", :fail => true)
83 | end
84 | result.must_include "did not find expected node"
85 | end
86 |
87 | it "should display stacktrace in debug mode on load error" do
88 | result = Dir.chdir(File.join(config_dir, "fail-auto-yaml")) do
89 | be_sh("sfn conf --debug", :fail => true)
90 | end
91 | result.must_include "Stacktrace"
92 | result.must_include "SyntaxError"
93 | end
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/test/specs/utils/stack_parameter_validator_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative "../../helper"
2 |
3 | describe Sfn::Utils::StackParameterValidator do
4 | let(:validator) do
5 | klass = Class.new
6 | klass.include Sfn::Utils::StackParameterValidator
7 | klass.new
8 | end
9 |
10 | it "should detect list types" do
11 | validator.list_type?("CommaDelimitedString").must_equal true
12 | validator.list_type?("comma_delimited_string").must_equal true
13 | validator.list_type?("List").must_equal true
14 | validator.list_type?("List").must_equal true
15 | end
16 |
17 | it "should not detect non-list types" do
18 | validator.list_type?("Number").must_equal false
19 | validator.list_type?("json").must_equal false
20 | validator.list_type?("AWS::EC2::Image::Id").must_equal false
21 | end
22 |
23 | it "should reject value under min value" do
24 | validator.min_value(2, 3).wont_equal true
25 | end
26 |
27 | it "should accept value above min value" do
28 | validator.min_value(4, 3).must_equal true
29 | end
30 |
31 | it "should accept value equal to min value" do
32 | validator.min_value(3, 3).must_equal true
33 | end
34 |
35 | it "should reject value over max value" do
36 | validator.max_value(4, 3).wont_equal true
37 | end
38 |
39 | it "should accept value below max value" do
40 | validator.max_value(3, 4).must_equal true
41 | end
42 |
43 | it "should accept value equal to max value" do
44 | validator.max_value(3, 3).must_equal true
45 | end
46 |
47 | it "should reject value under min length" do
48 | validator.min_length("fubar", 6).wont_equal true
49 | end
50 |
51 | it "should accept value over min length" do
52 | validator.min_length("fubar", 4).must_equal true
53 | end
54 |
55 | it "should accept value equal to min length" do
56 | validator.min_length("fubar", 5).must_equal true
57 | end
58 |
59 | it "should reject value over max length" do
60 | validator.max_length("fubar", 4).wont_equal true
61 | end
62 |
63 | it "should accept value under max length" do
64 | validator.max_length("fubar", 6).must_equal true
65 | end
66 |
67 | it "should accept value equal to max length" do
68 | validator.max_length("fubar", 5).must_equal true
69 | end
70 |
71 | it "should reject value not matching pattern" do
72 | validator.allowed_pattern("invalid-ami-9999", '^ami-\d+$').wont_equal true
73 | end
74 |
75 | it "should accept value matching pattern" do
76 | validator.allowed_pattern("ami-9999", '^ami-\d+$').must_equal true
77 | end
78 |
79 | it "should reject value not defined within allowed values" do
80 | validator.allowed_values("ack", ["valid1", "valid2"]).wont_equal true
81 | end
82 |
83 | it "should accept value defined within allowed values" do
84 | validator.allowed_values("ack", ["valid1", "ack", "valid2"]).must_equal true
85 | end
86 |
87 | describe "Definition validation" do
88 | it "should reject value as too long" do
89 | result = validator.validate_parameter("fubar",
90 | "MaxLength" => 4)
91 | result.wont_equal true
92 | result.size.must_equal 1
93 | result = result.first
94 | result.first.must_equal "max_length"
95 | result.last.must_be_kind_of String
96 | end
97 |
98 | it "should process multiple values when list" do
99 | result = validator.validate_parameter("fubar,ack",
100 | "Type" => "CommaDelimitedList",
101 | "MaxLength" => 4)
102 | result.wont_equal true
103 | result.size.must_equal 1
104 | result = result.first
105 | result.first.must_equal "max_length"
106 | result.last.must_be_kind_of String
107 | end
108 |
109 | it "should process multiple values when list and reject all" do
110 | results = validator.validate_parameter("fubar,ack",
111 | "Type" => "CommaDelimitedList",
112 | "MaxLength" => 2)
113 | results.wont_equal true
114 | results.size.must_equal 2
115 | results.each do |result|
116 | result.first.must_equal "max_length"
117 | result.last.must_be_kind_of String
118 | end
119 | end
120 |
121 | it "should process CFN List type" do
122 | results = validator.validate_parameter("fubar,ack",
123 | "Type" => "List",
124 | "MaxLength" => 2)
125 | results.wont_equal true
126 | results.size.must_equal 2
127 | results.each do |result|
128 | result.first.must_equal "max_length"
129 | result.last.must_be_kind_of String
130 | end
131 | end
132 |
133 | it "should process HOT list type" do
134 | results = validator.validate_parameter("fubar,ack",
135 | "type" => "comma_delimited_string",
136 | "max_length" => 2)
137 | results.wont_equal true
138 | results.size.must_equal 2
139 | results.each do |result|
140 | result.first.must_equal "max_length"
141 | result.last.must_be_kind_of String
142 | end
143 | end
144 | end
145 | end
146 |
--------------------------------------------------------------------------------