Module: CuffSert
- Defined in:
- lib/cuffsert/presenters.rb,
lib/cuffsert/main.rb,
lib/cuffsert/errors.rb,
lib/cuffsert/actions.rb,
lib/cuffsert/version.rb,
lib/cuffsert/cfstates.rb,
lib/cuffsert/cli_args.rb,
lib/cuffsert/messages.rb,
lib/cuffsert/metadata.rb,
lib/cuffsert/rxcfclient.rb,
lib/cuffsert/rxs3client.rb,
lib/cuffsert/cfarguments.rb,
lib/cuffsert/confirmation.rb
Overview
TODO:
-
propagate timeout here (from config?)
-
creation change-set: cfargs = ‘CREATE’
Defined Under Namespace
Classes: Abort, BaseAction, BasePresenter, BaseRenderer, ChangeSet, CreateStackAction, CuffSertError, Done, JsonRenderer, Message, MessageAction, NoChanges, ProgressbarRenderer, RawPresenter, RecreateStackAction, RendererPresenter, Report, RxCFClient, RxCFError, RxS3Client, StackConfig, Templates, UpdateStackAction
Constant Summary collapse
- VERSION =
'0.14.2'
- INPROGRESS_STATES =
%w[ CREATE_IN_PROGRESS UPDATE_IN_PROGRESS UPDATE_COMPLETE_CLEANUP_IN_PROGRESS UPDATE_ROLLBACK_IN_PROGRESS UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS DELETE_IN_PROGRESS ]
- GOOD_STATES =
%w[ CREATE_COMPLETE ROLLBACK_COMPLETE UPDATE_COMPLETE DELETE_COMPLETE DELETE_SKIPPED ]
- BAD_STATES =
%w[ CREATE_FAILED UPDATE_ROLLBACK_COMPLETE UPDATE_ROLLBACK_FAILED UPDATE_FAILED DELETE_FAILED FAILED ]
- FINAL_STATES =
GOOD_STATES + BAD_STATES
- STACKNAME_RE =
/^[A-Za-z0-9_-]+$/
- ACTION_ORDER =
['Add', 'Modify', 'Replace?', 'Replace!', 'Remove']
- TIMEOUT =
10
Class Method Summary collapse
- .as_cloudformation_args(meta) ⇒ Object
- .as_create_stack_args(meta) ⇒ Object
- .as_delete_stack_args(stack) ⇒ Object
- .as_update_change_set(meta, stack) ⇒ Object
- .ask_confirmation(input = STDIN, output = STDOUT) ⇒ Object
- .build_meta(cli_args) ⇒ Object
- .cli_overrides(meta, cli_args) ⇒ Object
- .confirmation(meta, action, change_set) ⇒ Object
- .determine_action(meta, cfclient, force_replace: false) ⇒ Object
- .load_config(io) ⇒ Object
- .load_template(stack_uri) ⇒ Object
- .make_renderer(cli_args) ⇒ Object
- .meta_defaults(cli_args) ⇒ Object
- .meta_for_path(metadata, path, target = StackConfig.new) ⇒ Object
- .metadata_if_present(cli_args) ⇒ Object
- .need_confirmation(meta, action, desc) ⇒ Object
- .parse_cli_args(argv) ⇒ Object
- .run(argv) ⇒ Object
- .s3_uri_to_https(uri, region) ⇒ Object
- .state_category(state) ⇒ Object
- .symbolize_keys(hash) ⇒ Object
- .template_parameters(meta) ⇒ Object
- .validate_and_urlify(stack_path) ⇒ Object
- .validate_cli_args(cli_args) ⇒ Object
Class Method Details
.as_cloudformation_args(meta) ⇒ Object
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/cuffsert/cfarguments.rb', line 12 def self.as_cloudformation_args() cfargs = { :stack_name => .stackname, :capabilities => %w[ CAPABILITY_AUTO_EXPAND CAPABILITY_IAM CAPABILITY_NAMED_IAM ], } unless .parameters.empty? cfargs[:parameters] = .parameters.map do |k, v| if v.nil? {:parameter_key => k, :use_previous_value => true} else {:parameter_key => k, :parameter_value => v.to_s} end end end unless ..empty? cfargs[:tags] = ..map do |k, v| {:key => k, :value => v.to_s} end end if .stack_uri cfargs.merge!(self.template_parameters()) end cfargs end |
.as_create_stack_args(meta) ⇒ Object
44 45 46 47 48 49 50 51 52 |
# File 'lib/cuffsert/cfarguments.rb', line 44 def self.as_create_stack_args() no_value = .parameters.select {|_, v| v.nil? }.keys raise "Supply value for #{no_value.join(', ')}" unless no_value.empty? cfargs = self.as_cloudformation_args() cfargs[:timeout_in_minutes] = TIMEOUT cfargs[:on_failure] = 'DELETE' cfargs end |
.as_delete_stack_args(stack) ⇒ Object
77 78 79 |
# File 'lib/cuffsert/cfarguments.rb', line 77 def self.as_delete_stack_args(stack) { :stack_name => stack[:stack_id] } end |
.as_update_change_set(meta, stack) ⇒ Object
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/cuffsert/cfarguments.rb', line 54 def self.as_update_change_set(, stack) cfargs = self.as_cloudformation_args() cfargs[:change_set_name] = .stackname cfargs[:change_set_type] = 'UPDATE' if cfargs[:use_previous_template] = .stack_uri.nil? Array(stack[:parameters]).each do |param| key = param[:parameter_key] unless .parameters.include?(key) cfargs[:parameters] ||= [] cfargs[:parameters] << {:parameter_key => key, :use_previous_value => true} end end if !..empty? Array(stack[:tags]).each do |tag| unless ..include?(tag[:key]) cfargs[:tags] << tag end end end end cfargs end |
.ask_confirmation(input = STDIN, output = STDOUT) ⇒ Object
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/cuffsert/confirmation.rb', line 26 def self.ask_confirmation(input = STDIN, output = STDOUT) return false unless input.isatty state = Termios.tcgetattr(input) mystate = state.dup mystate.c_lflag |= Termios::ISIG mystate.c_lflag &= ~Termios::ECHO mystate.c_lflag &= ~Termios::ICANON output.write 'Continue? [yN] ' begin Termios.tcsetattr(input, Termios::TCSANOW, mystate) answer = input.getc.chr.downcase output.write("\n") answer == 'y' rescue Interrupt false ensure Termios.tcsetattr(input, Termios::TCSANOW, state) end end |
.build_meta(cli_args) ⇒ Object
76 77 78 79 80 81 |
# File 'lib/cuffsert/metadata.rb', line 76 def self.(cli_args) default = self.(cli_args) config = self.(cli_args) = CuffSert.(config, cli_args[:selector], default) CuffSert.cli_overrides(, cli_args) end |
.cli_overrides(meta, cli_args) ⇒ Object
107 108 109 110 111 112 113 114 115 |
# File 'lib/cuffsert/metadata.rb', line 107 def self.cli_overrides(, cli_args) .update_from(cli_args[:overrides]) .aws_region = cli_args[:aws_region] || .aws_region .op_mode = cli_args[:op_mode] || .op_mode if (stack_path = (cli_args[:stack_path] || [])[0]) .stack_uri = CuffSert.validate_and_urlify(stack_path) end end |
.confirmation(meta, action, change_set) ⇒ Object
46 47 48 49 50 |
# File 'lib/cuffsert/confirmation.rb', line 46 def self.confirmation(, action, change_set) return false if .op_mode == :dry_run return true unless CuffSert.need_confirmation(, action, change_set) return CuffSert.ask_confirmation(STDIN, STDOUT) end |
.determine_action(meta, cfclient, force_replace: false) ⇒ Object
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# File 'lib/cuffsert/main.rb', line 14 def self.determine_action(, cfclient, force_replace: false) found = cfclient.find_stack_blocking() if found && INPROGRESS_STATES.include?(found[:stack_status]) = Abort.new('Stack operation already in progress') action = MessageAction.new() else if found.nil? action = CreateStackAction.new(, nil) elsif found[:stack_status] == 'ROLLBACK_COMPLETE' || force_replace action = RecreateStackAction.new(, found) else action = UpdateStackAction.new(, found) end yield action end action end |
.load_config(io) ⇒ Object
50 51 52 53 54 55 56 57 |
# File 'lib/cuffsert/metadata.rb', line 50 def self.load_config(io) config = YAML.load(io) raise 'config does not seem to be a YAML hash?' unless config.is_a?(Hash) config = symbolize_keys(config) format = config.delete(:format) raise 'Please include Format: v1' if format.nil? || format.downcase != 'v1' config end |
.load_template(stack_uri) ⇒ Object
88 89 90 91 |
# File 'lib/cuffsert/cfarguments.rb', line 88 def self.load_template(stack_uri) file = stack_uri.to_s.sub(/^file:\/+/, '/') YAML.load(open(file).read) end |
.make_renderer(cli_args) ⇒ Object
33 34 35 36 37 38 39 |
# File 'lib/cuffsert/main.rb', line 33 def self.make_renderer(cli_args) if cli_args[:output] == :json JsonRenderer.new(STDOUT, STDERR, cli_args) else ProgressbarRenderer.new(STDOUT, STDERR, cli_args) end end |
.meta_defaults(cli_args) ⇒ Object
85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/cuffsert/metadata.rb', line 85 def self.(cli_args) stack_path = (cli_args[:stack_path] || [])[0] if stack_path && File.exists?(stack_path) nil_params = CuffBase.empty_from_template(open(stack_path)) else nil_params = {} end default = StackConfig.new default.update_from({:parameters => nil_params}) default.suffix = File.basename(cli_args[:metadata], '.yml') if cli_args[:metadata] default end |
.meta_for_path(metadata, path, target = StackConfig.new) ⇒ Object
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/cuffsert/metadata.rb', line 59 def self.(, path, target = StackConfig.new) target.update_from() candidate, *path = path key = candidate || [:defaultpath] variants = [:variants] if key.nil? raise "No DefaultPath found for #{variants.keys}" unless variants.nil? return target end target.append_path(key) raise "Missing variants section as expected by #{key}" if variants.nil? = variants[key.to_sym] raise "#{key.inspect} not found in variants" if .nil? self.(, path, target) end |
.metadata_if_present(cli_args) ⇒ Object
98 99 100 101 102 103 104 105 |
# File 'lib/cuffsert/metadata.rb', line 98 def self.(cli_args) if cli_args[:metadata] io = open(cli_args[:metadata]) CuffSert.load_config(io) else {} end end |
.need_confirmation(meta, action, desc) ⇒ Object
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# File 'lib/cuffsert/confirmation.rb', line 4 def self.need_confirmation(, action, desc) return true if .op_mode == :always_ask return false if .op_mode == :dangerous_ok case action when :create false when :update change_set = desc change_set[:changes].any? do |change| rc = change[:resource_change] rc[:action] == 'Remove' || ( rc[:action] == 'Modify' && ['Always', 'True', 'Conditional'].include?(rc[:replacement]) ) end when :recreate true else true # safety first end end |
.parse_cli_args(argv) ⇒ Object
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/cuffsert/cli_args.rb', line 8 def self.parse_cli_args(argv) args = { :output => :progressbar, :verbosity => 1, :force_replace => false, :op_mode => nil, :overrides => { :parameters => {}, :tags => {}, } } parser = OptionParser.new do |opts| opts. = "Upsert a CloudFormation template, reading creation options and metadata from a yaml file. Currently, parameter values, stack name and stack tags are read from metadata file. Version #{CuffSert::VERSION}." opts.separator('') opts.separator('Usage:') opts.separator(' cuffsert --name <stackname> stack.json') opts.separator(' cuffsert --name <stackname> {--parameter Name=Value | --tag Name=Value}... [stack.json]') opts.separator(' cuffsert --metadata <metadata.json> --selector <path/in/metadata> stack.json') opts.separator(' cuffsert --metadata <metadata.json> --selector <path/in/metadata> {--parameter Name=Value | --tag Name=Value}... [stack.json]') CuffBase.shared_cli_args(opts, args) opts.on('--metadata path', '-m path', 'Yaml file to read stack metadata from') do |path| path = '/dev/stdin' if path == '-' unless File.exist?(path) raise "--metadata #{path} does not exist" end args[:metadata] = path end opts.on('--selector selector', '-s selector', 'Dash or slash-separated variant names used to navigate the metadata') do |selector| args[:selector] = selector.split(/[-,\/]/) end opts.on('--name stackname', '-n name', 'Alternative stackname (default is to construct the name from the selector)') do |stackname| unless stackname =~ STACKNAME_RE raise "--name #{stackname} is expected to be #{STACKNAME_RE.inspect}" end args[:overrides][:stackname] = stackname end opts.on('--parameter k=v', '-p k=v', 'Set the value of a particular parameter, overriding any file metadata') do |kv| key, val = kv.split(/=/, 2) if val.nil? raise "--parameter #{kv} should be key=value" end if args[:overrides][:parameters].include?(key) raise "cli args include duplicate parameter #{key}" end args[:overrides][:parameters][key] = val end opts.on('--tag k=v', '-t k=v', 'Set a stack tag, overriding any file metadata') do |kv| key, val = kv.split(/=/, 2) if val.nil? raise "--tag #{kv} should be key=value" end if args[:overrides][:tags].include?(key) raise "cli args include duplicate tag #{key}" end args[:overrides][:tags][key] = val end opts.on('--s3-upload-prefix=prefix', 'Templates > 51200 bytes are uploaded here. Format: s3://bucket-name/[pre/fix]') do |prefix| unless prefix.start_with?('s3://') raise "Upload prefix #{prefix} must start with s3://" end args[:s3_upload_prefix] = prefix end opts.on('--json', 'Output events in JSON, no progressbar, colors') do args[:output] = :json end opts.on('--verbose', '-v', 'More detailed output. Once will print all stack events, twice will print debug info') do args[:verbosity] += 1 end opts.on('--quiet', '-q', 'Output only fatal errors') do args[:verbosity] = 0 end opts.on('--replace', 'Re-create the stack if it already exist') do args[:force_replace] = true end opts.on('--ask', 'Always ask for confirmation') do raise 'You can only use one of --yes, --ask and --dry-run' if args[:op_mode] args[:op_mode] = :always_ask end opts.on('--yes', '-y', 'Don\'t ask to replace and delete stack resources') do raise 'You can only use one of --yes, --ask and --dry-run' if args[:op_mode] args[:op_mode] = :dangerous_ok end opts.on('--dry-run', 'Describe what would be done') do raise 'You can only use one of --yes, --ask and --dry-run' if args[:op_mode] args[:op_mode] = :dry_run end opts.on('--help', '-h', 'Produce this message') do abort(opts.to_s) end end if argv.empty? abort(parser.to_s) else args[:stack_path] = parser.parse(argv) args end end |
.run(argv) ⇒ Object
41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/cuffsert/main.rb', line 41 def self.run(argv) cli_args = CuffSert.parse_cli_args(argv) CuffSert.validate_cli_args(cli_args) = CuffSert.(cli_args) cfclient = RxCFClient.new(.aws_region) action = CuffSert.determine_action(, cfclient, force_replace: cli_args[:force_replace]) do |a| a.confirmation = CuffSert.method(:confirmation) a.s3client = RxS3Client.new(cli_args, .aws_region) if cli_args[:s3_upload_prefix] a.cfclient = cfclient end action.validate! renderer = CuffSert.make_renderer(cli_args) RendererPresenter.new(action.as_observable, renderer) end |
.s3_uri_to_https(uri, region) ⇒ Object
81 82 83 84 85 86 |
# File 'lib/cuffsert/cfarguments.rb', line 81 def self.s3_uri_to_https(uri, region) bucket = uri.host key = uri.path host = region == 'us-east-1' ? 's3.amazonaws.com' : "s3-#{region}.amazonaws.com" "https://#{host}/#{bucket}#{key}" end |
.state_category(state) ⇒ Object
30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/cuffsert/cfstates.rb', line 30 def self.state_category(state) if BAD_STATES.include?(state) :bad elsif GOOD_STATES.include?(state) :good elsif INPROGRESS_STATES.include?(state) :progress else raise "Cannot categorize state #{state}" end end |
.symbolize_keys(hash) ⇒ Object
117 118 119 120 121 122 123 124 125 126 |
# File 'lib/cuffsert/metadata.rb', line 117 def self.symbolize_keys(hash) hash.each_with_object({}) do |(k, v), h| k = k.downcase.to_sym if k == :tags || k == :parameters h[k] = v.each_with_object({}) { |e, h| h[e['Name']] = e['Value'] } else h[k] = v.is_a?(Hash) ? symbolize_keys(v) : v end end end |
.template_parameters(meta) ⇒ Object
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/cuffsert/cfarguments.rb', line 95 def self.template_parameters() template_parameters = {} if .stack_uri.scheme == 's3' template_parameters[:template_url] = self.s3_uri_to_https(.stack_uri, .aws_region) elsif .stack_uri.scheme == 'https' if .stack_uri.host.end_with?('amazonaws.com') template_parameters[:template_url] = .stack_uri.to_s else raise 'Only HTTPS URLs pointing to amazonaws.com supported.' end elsif .stack_uri.scheme == 'file' template = CuffSert.load_template(.stack_uri).to_json if template.size <= 51200 template_parameters[:template_body] = template end else raise "Unsupported scheme #{.stack_uri.scheme}" end template_parameters end |
.validate_and_urlify(stack_path) ⇒ Object
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/cuffsert/metadata.rb', line 34 def self.validate_and_urlify(stack_path) if stack_path =~ /^[A-Za-z0-9]+:/ stack_uri = URI.parse(stack_path) else normalized = File.(stack_path) unless File.exist?(normalized) raise "Local file #{normalized} does not exist" end stack_uri = URI.join('file:///', normalized) end unless ['s3', 'file'].include?(stack_uri.scheme) raise "Uri #{stack_uri.scheme} is not supported" end stack_uri end |
.validate_cli_args(cli_args) ⇒ Object
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/cuffsert/cli_args.rb', line 122 def self.validate_cli_args(cli_args) errors = [] if cli_args[:stack_path] != nil && cli_args[:stack_path].size > 1 errors << 'Require at most one template' end if cli_args[:metadata].nil? && cli_args[:overrides][:stackname].nil? errors << 'Without --metadata, you must supply --name to identify stack to update' end if cli_args[:selector] && cli_args[:metadata].nil? errors << 'You cannot use --selector without --metadata' end raise errors.join(', ') unless errors.empty? end |