require "base64" require "sfn" module Sfn module MonkeyPatch # Expand stack model functionality module Stack autoload :Azure, "sfn/monkey_patch/stack/azure" autoload :Google, "sfn/monkey_patch/stack/google" include Bogo::AnimalStrings include Azure include Google ## Status helpers # Check for state suffix # # @param args [String, Symbol] state suffix to check for (multiple allowed) # @return [TrueClass, FalseClass] true if any matches found in argument list def status_ends_with?(*args) stat = status.to_s.downcase !!args.map(&:to_s).map(&:downcase).detect do |suffix| stat.end_with?(suffix) || state.to_s.end_with?(suffix) end end # Check for state prefix # # @param args [String, Symbol] state prefix to check for (multiple allowed) # @return [TrueClass, FalseClass] true if any matches found in argument list def status_starts_with?(*args) stat = status.to_s.downcase !!args.map(&:to_s).map(&:downcase).detect do |prefix| stat.start_with?(prefix) || state.to_s.start_with?(prefix) end end # Check for state inclusion # # @param args [String, Symbol] state string to check for (multiple allowed) # @return [TrueClass, FalseClass] true if any matches found in argument list def status_includes?(*args) stat = status.to_s.downcase !!args.map(&:to_s).map(&:downcase).detect do |string| stat.include?(string) end end # @return [TrueClass, FalseClass] stack is in progress def in_progress? status_ends_with?(:in_progress) end # @return [TrueClass, FalseClass] stack is in complete state def complete? status_ends_with?(:complete, :failed) end # @return [TrueClass, FalseClass] stack is failed state def failed? status_ends_with?(:failed) || (status_includes?(:rollback) && status_ends_with?(:complete)) end # @return [TrueClass, FalseClass] stack is in success state def success? !failed? && complete? end # @return [TrueClass, FalseClass] stack is creating def creating? in_progress? && status_starts_with?(:create) end # @return [TrueClass, FalseClass] stack is deleting def deleting? in_progress? && status_starts_with?(:delete) end # @return [TrueClass, FalseClass] stack is updating def updating? in_progress? && status_starts_with?(:update) end # @return [TrueClass, FalseClass] stack is rolling back def rollbacking? in_progress? && status_starts_with?(:rollback) end # @return [String] action currently being performed def performing if in_progress? status.to_s.downcase.split("_").first.to_sym end end ### Color coders # @return [TrueClass, FalseClass] stack is in red state def red? failed? || deleting? end # @return [TrueClass, FalseClass] stack is in green state def green? success? end # @return [TrueClass, FalseClass] stack is in yellow state def yellow? !red? && !green? end # Provides color of stack state. Red is an error state, yellow # is a warning state and green is a success state # # @return [Symbol] color of state (:red, :yellow, :green) def color_state red? ? :red : green? ? :green : :yellow end # Provides text of stack state. Danger is an error state, warning # is a warning state and success is a success state # # @return [Symbol] color of state (:danger, :warning, :success) def text_state red? ? :danger : green? ? :success : :warning end # @return [String] URL safe encoded stack id def encoded_id Base64.urlsafe_encode64(id) end # Whole number representation of current completion # # @param min [Integer] lowest allowed return value (defaults 5) # @return [Integer] percent complete (0..100) def percent_complete(min = 5) if self.respond_to?("percent_complete_#{api.provider}") self.send("percent_complete_#{api.provider}", min) else if in_progress? total_resources = template.fetch("Resources", []).size total_complete = resources.all.find_all do |resource| resource.status.downcase.end_with?("complete") end.size result = ((total_complete.to_f / total_resources) * 100).to_i result > min.to_i ? result : min else 100 end end end # Apply stack outputs to current stack parameters # # @param opts [Hash] # @option opts [String] :parameter_key key used for parameters block # @option opts [String] :default_key key used within parameter for default value # @option opts [Hash] :mapping custom output -> parameter name mapping # @param remote_stack [Miasma::Orchestration::Stack] # @return [self] # @note setting `DisableApply` within parameter hash will # prevent parameters being overridden def apply_stack(remote_stack, opts = {}, ignore_params = nil) if self.respond_to?("apply_stack_#{api.provider}") self.send("apply_stack_#{api.provider}", remote_stack, opts, ignore_params) else unless opts[:mapping] opts[:mapping] = {} end if opts[:parameter_key] stack_parameters = template[opts[:parameter_key]] default_key = opts.fetch( :default_key, opts[:parameter_key].to_s[0, 1].match(/[a-z]/) ? "default" : "Default" ) else if template["Parameters"] default_key = "Default" stack_parameters = template["Parameters"] else default_key = "default" stack_parameters = template["parameters"] end end if stack_parameters valid_parameters = stack_parameters.find_all do |key, val| !val["DisableApply"] && !val["disable_apply"] end.map(&:first) if ignore_params valid_parameters.reject! do |key| ignore_params.include?(key) end end remote_stack.outputs.each do |output| o_key = output.key.downcase.tr("_", "") p_key = valid_parameters.detect do |v_param| v_param.downcase.tr("_", "") == o_key end unless p_key map_key = opts[:mapping].keys.detect do |map_key| map_key.downcase.tr("_", "") == o_key end if map_key p_key = valid_parameters.detect do |v_param| v_param.downcase.tr("_", "") == opts[:mapping][map_key].downcase.tr("_", "") end end end if p_key self.parameters = parameters.merge(p_key => output.value) end end end end self end # Return all stacks contained within this stack # # @param recurse [TrueClass, FalseClass] recurse to fetch _all_ stacks # @return [Array<Miasma::Models::Orchestration::Stack>] def nested_stacks(recurse = true) if self.respond_to?("nested_stacks_#{api.provider}") self.send("nested_stacks_#{api.provider}", recurse) else resources.reload.all.map do |resource| if api.data.fetch(:stack_types, []).include?(resource.type) # Custom remote load support if resource.type == "Custom::JackalStack" location, stack_id = resource.id.to_s.split("-", 2) if l_conf = api.data[:locations][location] n_stack = Miasma.api( :type => :orchestration, :provider => l_conf[:provider], :credentials => l_conf, ).stacks.get(stack_id) end else n_stack = resource.expand end if n_stack n_stack.data[:logical_id] = resource.name n_stack.data[:parent_stack] = self n_stack.api.data[:stack_types] = api.data[:stack_types] if recurse [n_stack] + n_stack.nested_stacks(recurse) else n_stack end end end end.flatten.compact end end # @return [TrueClass, FalseClass] stack contains nested stacks def nested? if self.respond_to?("nested_#{api.provider}?") self.send("nested_#{api.provider}?") else !!resources.detect do |resource| api.data.fetch(:stack_types, []).include?(resource.type) end end end # Return stack policy if available # # @return [Smash, NilClass] def policy if self.respond_to?("policy_#{api.provider}") self.send("policy_#{api.provider}") else if (self.api.provider == :aws) # cause this is the only one begin result = self.api.request( :path => "/", :form => Smash.new( "Action" => "GetStackPolicy", "StackName" => self.id, ), ) serialized_policy = result.get(:body, "GetStackPolicyResult", "StackPolicyBody") MultiJson.load(serialized_policy).to_smash rescue Miasma::Error::ApiError::RequestError => e if e.response.code == 404 nil else raise end end end end end # Detect the nesting style in use by the stack # # @return [Symbol, NilClass] style of nesting (:shallow, :deep) # or `nil` if no nesting detected # @note in shallow nesting style, stack resources will not # contain any direct values for parameters (which is what we # are testing for) def nesting_style if self.respond_to?("nesting_style_#{api.provider}") self.send("nesting_style_#{api.provider}") else if nested? self.template["Resources"].find_all do |t_resource| t_resource["Type"] == self.api.class.const_get(:RESOURCE_MAPPING).key(self.class) end.detect do |t_resource| t_resource["Properties"].fetch("Parameters", {}).values.detect do |t_value| !t_value.is_a?(Hash) end end ? :deep : :shallow end end end # Reformat template data structure to SparkleFormation style structure # # @return [Hash] def sparkleish_template(*args) if self.respond_to?("sparkleish_template_#{api.provider}") self.send("sparkleish_template_#{api.provider}", *args) else template end end # Provide easy parameters override # # @return [Hash] def root_parameters if self.respond_to?("root_parameters_#{api.provider}") self.send("root_parameters_#{api.provider}") else parameters end end end end end # Infect miasma Miasma::Models::Orchestration::Stack.send(:include, Sfn::MonkeyPatch::Stack)