sfn 0.0.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +107 -0
- data/LICENSE +13 -0
- data/README.md +142 -61
- data/bin/sfn +43 -0
- data/lib/chef/knife/knife_plugin_seed.rb +117 -0
- data/lib/sfn.rb +17 -0
- data/lib/sfn/cache.rb +385 -0
- data/lib/sfn/command.rb +45 -0
- data/lib/sfn/command/create.rb +87 -0
- data/lib/sfn/command/describe.rb +87 -0
- data/lib/sfn/command/destroy.rb +74 -0
- data/lib/sfn/command/events.rb +98 -0
- data/lib/sfn/command/export.rb +103 -0
- data/lib/sfn/command/import.rb +117 -0
- data/lib/sfn/command/inspect.rb +160 -0
- data/lib/sfn/command/list.rb +59 -0
- data/lib/sfn/command/promote.rb +17 -0
- data/lib/sfn/command/update.rb +95 -0
- data/lib/sfn/command/validate.rb +34 -0
- data/lib/sfn/command_module.rb +9 -0
- data/lib/sfn/command_module/base.rb +150 -0
- data/lib/sfn/command_module/stack.rb +166 -0
- data/lib/sfn/command_module/template.rb +147 -0
- data/lib/sfn/config.rb +106 -0
- data/lib/sfn/config/create.rb +35 -0
- data/lib/sfn/config/describe.rb +19 -0
- data/lib/sfn/config/destroy.rb +9 -0
- data/lib/sfn/config/events.rb +25 -0
- data/lib/sfn/config/export.rb +29 -0
- data/lib/sfn/config/import.rb +24 -0
- data/lib/sfn/config/inspect.rb +37 -0
- data/lib/sfn/config/list.rb +25 -0
- data/lib/sfn/config/promote.rb +23 -0
- data/lib/sfn/config/update.rb +20 -0
- data/lib/sfn/config/validate.rb +49 -0
- data/lib/sfn/monkey_patch.rb +8 -0
- data/lib/sfn/monkey_patch/stack.rb +200 -0
- data/lib/sfn/provider.rb +224 -0
- data/lib/sfn/utils.rb +23 -0
- data/lib/sfn/utils/debug.rb +31 -0
- data/lib/sfn/utils/json.rb +37 -0
- data/lib/sfn/utils/object_storage.rb +28 -0
- data/lib/sfn/utils/output.rb +79 -0
- data/lib/sfn/utils/path_selector.rb +99 -0
- data/lib/sfn/utils/ssher.rb +29 -0
- data/lib/sfn/utils/stack_exporter.rb +275 -0
- data/lib/sfn/utils/stack_parameter_scrubber.rb +37 -0
- data/lib/sfn/utils/stack_parameter_validator.rb +124 -0
- data/lib/sfn/version.rb +4 -0
- data/sfn.gemspec +19 -0
- metadata +110 -4
data/lib/sfn/command.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
require 'bogo-cli'
|
3
|
+
|
4
|
+
module Sfn
|
5
|
+
class Command < Bogo::Cli::Command
|
6
|
+
|
7
|
+
autoload :Create, 'sfn/command/create'
|
8
|
+
autoload :Describe, 'sfn/command/describe'
|
9
|
+
autoload :Destroy, 'sfn/command/destroy'
|
10
|
+
autoload :Events, 'sfn/command/events'
|
11
|
+
autoload :Export, 'sfn/command/export'
|
12
|
+
autoload :Import, 'sfn/command/import'
|
13
|
+
autoload :Inspect, 'sfn/command/inspect'
|
14
|
+
autoload :List, 'sfn/command/list'
|
15
|
+
autoload :Promote, 'sfn/command/promote'
|
16
|
+
autoload :Update, 'sfn/command/update'
|
17
|
+
autoload :Validate, 'sfn/command/validate'
|
18
|
+
|
19
|
+
# Override to provide config file searching
|
20
|
+
def initialize(opts, args)
|
21
|
+
unless(opts[:config])
|
22
|
+
opts = opts.to_hash.to_smash(:snake)
|
23
|
+
discover_config(opts)
|
24
|
+
end
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
# Start with current working directory and traverse to root
|
31
|
+
# looking for a `.sfn` configuration file
|
32
|
+
#
|
33
|
+
# @param opts [Smash]
|
34
|
+
# @return [Smash]
|
35
|
+
def discover_config(opts)
|
36
|
+
cwd = Dir.pwd.split(File::SEPARATOR)
|
37
|
+
until(cwd.empty? || File.exists?(cwd.push('.sfn').join(File::SEPARATOR)))
|
38
|
+
cwd.pop(2)
|
39
|
+
end
|
40
|
+
opts[:config] = cwd.join(File::SEPARATOR) unless cwd.empty?
|
41
|
+
opts
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'sparkle_formation'
|
2
|
+
require 'pathname'
|
3
|
+
require 'sfn'
|
4
|
+
|
5
|
+
module Sfn
|
6
|
+
class Command
|
7
|
+
# Cloudformation create command
|
8
|
+
class Create < Command
|
9
|
+
|
10
|
+
include Sfn::CommandModule::Base
|
11
|
+
include Sfn::CommandModule::Template
|
12
|
+
include Sfn::CommandModule::Stack
|
13
|
+
|
14
|
+
# Run the stack creation command
|
15
|
+
def execute!
|
16
|
+
name = name_args.first
|
17
|
+
unless(name)
|
18
|
+
ui.fatal "Formation name must be specified!"
|
19
|
+
exit 1
|
20
|
+
end
|
21
|
+
if(config[:template])
|
22
|
+
file = config[:template]
|
23
|
+
else
|
24
|
+
file = load_template_file
|
25
|
+
nested_stacks_unpack = file.delete('sfn_nested_stack')
|
26
|
+
end
|
27
|
+
ui.info "#{ui.color('Cloud Formation:', :bold)} #{ui.color('create', :green)}"
|
28
|
+
stack_info = "#{ui.color('Name:', :bold)} #{name}"
|
29
|
+
if(config[:path])
|
30
|
+
stack_info << " #{ui.color('Path:', :bold)} #{config[:file]}"
|
31
|
+
end
|
32
|
+
|
33
|
+
unless(config[:print_only])
|
34
|
+
ui.info " -> #{stack_info}"
|
35
|
+
end
|
36
|
+
|
37
|
+
if(nested_stacks_unpack)
|
38
|
+
unpack_nesting(name, file, :create)
|
39
|
+
else
|
40
|
+
|
41
|
+
stack = provider.connection.stacks.build(
|
42
|
+
config[:options].dup.merge(
|
43
|
+
:name => name,
|
44
|
+
:template => file
|
45
|
+
)
|
46
|
+
)
|
47
|
+
|
48
|
+
apply_stacks!(stack)
|
49
|
+
stack.template = Sfn::Utils::StackParameterScrubber.scrub!(stack.template)
|
50
|
+
|
51
|
+
if(config[:print_only])
|
52
|
+
ui.info _format_json(translate_template(stack.template))
|
53
|
+
return
|
54
|
+
end
|
55
|
+
|
56
|
+
populate_parameters!(stack.template)
|
57
|
+
stack.parameters = config[:parameters]
|
58
|
+
|
59
|
+
stack.template = translate_template(stack.template)
|
60
|
+
stack.save
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
if(stack)
|
65
|
+
if(config[:poll])
|
66
|
+
poll_stack(stack.name)
|
67
|
+
stack = provider.connection.stacks.get(name)
|
68
|
+
|
69
|
+
if(stack.reload.success?)
|
70
|
+
ui.info "Stack create complete: #{ui.color('SUCCESS', :green)}"
|
71
|
+
namespace.const_get(:Describe).new({:outputs => true}, [name]).execute!
|
72
|
+
else
|
73
|
+
ui.fatal "Create of new stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
|
74
|
+
ui.info ""
|
75
|
+
namespace.const_get(:Inspect).new({:instance_failure => true}, [name]).execute!
|
76
|
+
raise
|
77
|
+
end
|
78
|
+
else
|
79
|
+
ui.warn 'Stack state polling has been disabled.'
|
80
|
+
ui.info "Stack creation initialized for #{ui.color(name, :green)}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
|
3
|
+
module Sfn
|
4
|
+
class Command
|
5
|
+
# Cloudformation describe command
|
6
|
+
class Describe < Command
|
7
|
+
|
8
|
+
include Sfn::CommandModule::Base
|
9
|
+
|
10
|
+
# information available
|
11
|
+
unless(defined?(AVAILABLE_DISPLAYS))
|
12
|
+
AVAILABLE_DISPLAYS = [:resources, :outputs]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Run the stack describe action
|
16
|
+
def execute!
|
17
|
+
stack_name = name_args.last
|
18
|
+
stack = provider.connection.stacks.get(stack_name)
|
19
|
+
if(stack)
|
20
|
+
display = [].tap do |to_display|
|
21
|
+
AVAILABLE_DISPLAYS.each do |display_option|
|
22
|
+
if(config[display_option])
|
23
|
+
to_display << display_option
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
display = AVAILABLE_DISPLAYS.dup if display.empty?
|
28
|
+
display.each do |display_method|
|
29
|
+
self.send(display_method, stack)
|
30
|
+
end
|
31
|
+
else
|
32
|
+
ui.fatal "Failed to find requested stack: #{ui.color(stack_name, :bold, :red)}"
|
33
|
+
exit -1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Display resources
|
38
|
+
#
|
39
|
+
# @param stack [Miasma::Models::Orchestration::Stack]
|
40
|
+
def resources(stack)
|
41
|
+
stack_resources = stack.resources.all.sort do |x, y|
|
42
|
+
y.updated <=> x.updated
|
43
|
+
end.map do |resource|
|
44
|
+
Smash.new(resource.attributes)
|
45
|
+
end
|
46
|
+
ui.table(self) do
|
47
|
+
table(:border => false) do
|
48
|
+
row(:header => true) do
|
49
|
+
allowed_attributes.each do |attr|
|
50
|
+
column as_title(attr), :width => stack_resources.map{|r| r[attr].to_s.length}.push(as_title(attr).length).max + 2
|
51
|
+
end
|
52
|
+
end
|
53
|
+
stack_resources.each do |resource|
|
54
|
+
row do
|
55
|
+
allowed_attributes.each do |attr|
|
56
|
+
column resource[attr]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end.display
|
62
|
+
end
|
63
|
+
|
64
|
+
# Display outputs
|
65
|
+
#
|
66
|
+
# @param stack [Miasma::Models::Orchestration::Stack]
|
67
|
+
def outputs(stack)
|
68
|
+
ui.info "Outputs for stack: #{ui.color(stack.name, :bold)}"
|
69
|
+
unless(stack.outputs.empty?)
|
70
|
+
stack.outputs.each do |output|
|
71
|
+
key, value = output.key, output.value
|
72
|
+
key = snake(key).to_s.split('_').map(&:capitalize).join(' ')
|
73
|
+
ui.info [' ', ui.color("#{key}:", :bold), value].join(' ')
|
74
|
+
end
|
75
|
+
else
|
76
|
+
ui.info " #{ui.color('No outputs found')}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [Array<String>] default attributes
|
81
|
+
def default_attributes
|
82
|
+
%w(updated logical_id type status status_reason)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
|
3
|
+
module Sfn
|
4
|
+
class Command
|
5
|
+
class Destroy < Command
|
6
|
+
|
7
|
+
include Sfn::CommandModule::Base
|
8
|
+
|
9
|
+
# Run the stack destruction action
|
10
|
+
def execute!
|
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
|
+
stack.destroy
|
33
|
+
ui.info "Destroy request complete for stack: #{ui.color(stack_name, :red)}"
|
34
|
+
else
|
35
|
+
ui.warn "Failed to locate requested stack: #{ui.color(stack_name, :bold)}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
if(config[:poll])
|
39
|
+
if(stacks.size == 1)
|
40
|
+
poll_stack(stacks.first)
|
41
|
+
else
|
42
|
+
ui.error "Stack polling is not available when multiple stack deletion is requested!"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
ui.info " -> Destroyed Cloud Formation#{plural}: #{ui.color(stacks.join(', '), :bold, :red)}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Cleanup persisted templates if nested stack resources are included
|
49
|
+
def nested_stack_cleanup!(stack)
|
50
|
+
nest_stacks = stack.template.fetch('Resources', {}).values.find_all do |resource|
|
51
|
+
resource['Type'] == 'AWS::CloudFormation::Stack'
|
52
|
+
end.each do |resource|
|
53
|
+
url = resource['Properties']['TemplateURL']
|
54
|
+
if(url)
|
55
|
+
_, bucket_name, path = URI.parse(url).path.split('/', 3)
|
56
|
+
bucket = provider.connection.api_for(:storage).buckets.get(bucket_name)
|
57
|
+
if(bucket)
|
58
|
+
file = bucket.files.get(path)
|
59
|
+
if(file)
|
60
|
+
file.destroy
|
61
|
+
ui.info "Deleted nested stack template! (Bucket: #{bucket_name} Template: #{path})"
|
62
|
+
else
|
63
|
+
ui.warn "Failed to locate template file within bucket for deletion! (#{path})"
|
64
|
+
end
|
65
|
+
else
|
66
|
+
ui.warn "Failed to locate bucket containing template file for deletion! (#{bucket_name})"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
|
3
|
+
module Sfn
|
4
|
+
class Command
|
5
|
+
# Events command
|
6
|
+
class Events < Command
|
7
|
+
|
8
|
+
include Sfn::CommandModule::Base
|
9
|
+
|
10
|
+
# @return [Miasma::Models::Orchestration::Stack]
|
11
|
+
attr_reader :stack
|
12
|
+
|
13
|
+
# Run the events list action
|
14
|
+
def execute!
|
15
|
+
name = name_args.first
|
16
|
+
ui.info "Events for Stack: #{ui.color(name, :bold)}\n"
|
17
|
+
@stacks = []
|
18
|
+
@stack = provider.connection.stacks.get(name)
|
19
|
+
@stacks << stack
|
20
|
+
discover_stacks(stack)
|
21
|
+
if(stack)
|
22
|
+
table = ui.table(self) do
|
23
|
+
table(:border => false) do
|
24
|
+
events = get_events
|
25
|
+
row(:header => true) do
|
26
|
+
allowed_attributes.each do |attr|
|
27
|
+
column attr.split('_').map(&:capitalize).join(' '), :width => ((val = events.map{|e| e[attr].to_s.length}.push(attr.length).max + 2) > 70 ? 70 : val)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
events.each do |event|
|
31
|
+
row do
|
32
|
+
allowed_attributes.each do |attr|
|
33
|
+
column event[attr]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end.display
|
39
|
+
if(config[:poll])
|
40
|
+
while(stack.in_progress?)
|
41
|
+
to_wait = config.fetch(:poll_wait_time, 10).to_f
|
42
|
+
while(to_wait > 0)
|
43
|
+
sleep(0.1)
|
44
|
+
to_wait -= 0.1
|
45
|
+
end
|
46
|
+
stack.reload
|
47
|
+
table.display
|
48
|
+
end
|
49
|
+
end
|
50
|
+
else
|
51
|
+
ui.fatal "Failed to locate requested stack: #{ui.color(name, :bold, :red)}"
|
52
|
+
raise "Failed to locate stack: #{name}!"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Fetch events from stack
|
57
|
+
#
|
58
|
+
# @param stack [Miasma::Models::Orchestration::Stack]
|
59
|
+
# @param last_id [String] only return events after this ID
|
60
|
+
# @return [Array<Hash>]
|
61
|
+
def get_events(*args)
|
62
|
+
discover_stacks(stack)
|
63
|
+
stack_events = @stacks.map do |stack|
|
64
|
+
stack.events.all.map do |e|
|
65
|
+
e.attributes.merge(:stack_name => stack.name).to_smash
|
66
|
+
end
|
67
|
+
end.flatten.compact
|
68
|
+
stack_events.sort do |x,y|
|
69
|
+
Time.parse(x[:time].to_s) <=> Time.parse(y[:time].to_s)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def discover_stacks(stack)
|
74
|
+
stack.resources.reload.all.each do |resource|
|
75
|
+
if(resource.type == 'AWS::CloudFormation::Stack')
|
76
|
+
nested_stack = provider.connection.stacks.get(resource.id)
|
77
|
+
@stacks.push(nested_stack).uniq!
|
78
|
+
discover_stacks(nested_stack)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [Array<String>] default attributes for events
|
84
|
+
def default_attributes
|
85
|
+
%w(stack_name time resource_logical_id resource_status resource_status_reason)
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [Array<String>] allowed attributes for events
|
89
|
+
def allowed_attributes
|
90
|
+
result = super
|
91
|
+
unless(@stacks.size > 1)
|
92
|
+
result.delete('stack_name')
|
93
|
+
end
|
94
|
+
result
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
|
3
|
+
module Sfn
|
4
|
+
class Command
|
5
|
+
# Export command
|
6
|
+
class Export < Command
|
7
|
+
|
8
|
+
include Sfn::CommandModule::Base
|
9
|
+
include Sfn::Utils::ObjectStorage
|
10
|
+
|
11
|
+
# Run export action
|
12
|
+
def execute!
|
13
|
+
raise NotImplementedError.new 'Implementation updates required'
|
14
|
+
stack_name = name_args.first
|
15
|
+
ui.info "#{ui.color('Stack Export:', :bold)} #{stack_name}"
|
16
|
+
ui.confirm 'Perform export'
|
17
|
+
stack = provider.stacks.get(stack_name)
|
18
|
+
if(stack)
|
19
|
+
export_options = Smash.new.tap do |opts|
|
20
|
+
[:chef_popsicle, :chef_environment_parameter, :ignore_parameters].each do |key|
|
21
|
+
opts[key] = config[key] unless config[key].nil?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
exporter = Sfn::Utils::StackExporter.new(stack, export_options)
|
25
|
+
result = exporter.export
|
26
|
+
outputs = [
|
27
|
+
write_to_file(result, stack),
|
28
|
+
write_to_bucket(result, stack)
|
29
|
+
].compact
|
30
|
+
if(outputs.empty?)
|
31
|
+
ui.warn 'No persistent output location defined. Printing export:'
|
32
|
+
ui.info _format_json(result)
|
33
|
+
end
|
34
|
+
ui.info "#{ui.color('Stack export', :bold)} (#{name_args.first}): #{ui.color('complete', :green)}"
|
35
|
+
unless(outputs.empty?)
|
36
|
+
outputs.each do |output|
|
37
|
+
ui.info ui.color(" -> #{output}", :blue)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
else
|
41
|
+
ui.fatal "Failed to discover requested stack: #{ui.color(stack_name, :red, :bold)}"
|
42
|
+
exit -1
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Generate file name for stack export JSON contents
|
47
|
+
#
|
48
|
+
# @param stack [Miasma::Models::Orchestration::Stack]
|
49
|
+
# @return [String] file name
|
50
|
+
def export_file_name(stack)
|
51
|
+
name = config[:file]
|
52
|
+
if(name)
|
53
|
+
if(name.respond_to?(:call))
|
54
|
+
name.call(stack)
|
55
|
+
else
|
56
|
+
name.to_s
|
57
|
+
end
|
58
|
+
else
|
59
|
+
"#{stack.stack_name}-#{Time.now.to_i}.json"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Write stack export to local file
|
64
|
+
#
|
65
|
+
# @param payload [Hash] stack export payload
|
66
|
+
# @param stack [Misama::Stack::Orchestration::Stack]
|
67
|
+
# @return [String, NilClass] path to file
|
68
|
+
def write_to_file(payload, stack)
|
69
|
+
raise NotImplementedError
|
70
|
+
if(config[:path])
|
71
|
+
full_path = File.join(
|
72
|
+
config[:path],
|
73
|
+
export_file_name(stack)
|
74
|
+
)
|
75
|
+
_, bucket, path = full_path.split('/', 3)
|
76
|
+
directory = provider.service_for(:storage,
|
77
|
+
:provider => :local,
|
78
|
+
:local_root => '/'
|
79
|
+
).directories.get(bucket)
|
80
|
+
file_store(payload, path, directory)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Write stack export to remote bucket
|
85
|
+
#
|
86
|
+
# @param payload [Hash] stack export payload
|
87
|
+
# @param stack [Miasma::Models::Orchestration::Stack]
|
88
|
+
# @return [String, NilClass] remote bucket key
|
89
|
+
def write_to_bucket(payload, stack)
|
90
|
+
raise NotImplementedError
|
91
|
+
if(bucket = config[:bucket])
|
92
|
+
key_path = File.join(*[
|
93
|
+
bucket_prefix(stack),
|
94
|
+
export_file_name(stack)
|
95
|
+
].compact
|
96
|
+
)
|
97
|
+
file_store(payload, key_path, provider.service_for(:storage).directories.get(bucket))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|