sfn 0.0.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|