-
Notifications
You must be signed in to change notification settings - Fork 51
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Backup plugin
- Loading branch information
Showing
5 changed files
with
305 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# Moonshot backup plugin | ||
|
||
Moonshot plugin for backing up config files. | ||
|
||
## Functionality | ||
|
||
The plugin collects and deflates certain files to a single tarball, | ||
and uploads that to a a given S3 bucket. The whole process happens | ||
in memory, nothing is written to disk. The plugin currently supports single files only, | ||
including whole directories in your tarball is not possible yet. | ||
|
||
The plugin uses the Moonshot AWS config, meaning that the bucket must be | ||
present in the same account and region as your deployment. | ||
|
||
## Basic usage | ||
|
||
When instantiating a class, you need to set the following options | ||
in a block, where the object is provided as a block argument: | ||
|
||
- `bucket`: the name of the S3 bucket you wish to upload the tarball | ||
- `files`: an array of relative path names as strings | ||
- `hooks`: which hooks to run the backup logic, works with all valid Moonshot hooks | ||
- `target_name`: tarball archive name, default: `<app_name>_<timestamp>_<user>.tar.gz` | ||
|
||
## Default method | ||
|
||
If you wish to back up only the current template and parameter files, you can simply | ||
use the factory method provided: | ||
|
||
```ruby | ||
plugin(Moonshot::Plugins::Backup.to_bucket('your-bucket-name')) | ||
``` | ||
|
||
## Placeholders | ||
|
||
You can use the following placeholders both in your filenames | ||
and tarball target names (meanings are pretty self explaining): | ||
|
||
- `%{app_name}` | ||
- `%{stack_name}` | ||
- `%{timestamp}` | ||
- `%{user}` | ||
|
||
## Example | ||
|
||
A possible use-case is backing up a CF template and/or | ||
parameter file after create or update. | ||
|
||
```ruby | ||
plugin( | ||
Backup.new do |b| | ||
b.bucket = 'your-bucket-name' | ||
b.files = [ | ||
'cloud_formation/%{app_name}.json', | ||
'cloud_formation/parameters/%{stack_name}.yml' | ||
] | ||
b.hooks = [:post_create, :post_update] | ||
end | ||
) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
require 'rubygems/package' | ||
require 'zlib' | ||
require_relative '../moonshot/creds_helper' | ||
|
||
module Moonshot | ||
module Plugins | ||
# Moonshot plugin class for deflating and uploading files on given hooks | ||
class Backup | ||
include Moonshot::CredsHelper | ||
|
||
attr_accessor :bucket, | ||
:files, | ||
:hooks, | ||
:target_name | ||
|
||
def initialize | ||
yield self if block_given? | ||
raise ArgumentError if @bucket.nil? || @files.nil? || @files.empty? || @hooks.nil? | ||
@target_name ||= '%{app_name}_%{timestamp}_%{user}.tar.gz' | ||
end | ||
|
||
# Factory method to create preconfigured Backup plugins. Uploads current | ||
# template and parameter files. | ||
# @param backup [String] target bucket name | ||
# @return [Backup] configured backup object | ||
def self.to_bucket(bucket) | ||
raise ArgumentError if bucket.nil? || bucket.empty? | ||
Moonshot::Plugins::Backup.new do |b| | ||
b.bucket = bucket | ||
b.files = [ | ||
'cloud_formation/%{app_name}.json', | ||
'cloud_formation/parameters/%{stack_name}.yml' | ||
] | ||
b.hooks = [:post_create, :post_update] | ||
end | ||
end | ||
|
||
# Main worker method, creates a tarball of the given files, and uploads | ||
# to an S3 bucket. | ||
# | ||
# @param resources [Resources] injected Moonshot resources | ||
def backup(resources) # rubocop:disable Metrics/AbcSize | ||
raise ArgumentError if resources.nil? | ||
|
||
@app_name = resources.stack.app_name | ||
@stack_name = resources.stack.name | ||
@target_name = render(@target_name) | ||
|
||
resources.ilog.start("#{log_message} in progress.") do |s| | ||
begin | ||
tar_out = tar(@files) | ||
zip_out = zip(tar_out) | ||
upload(zip_out) | ||
|
||
s.success("#{log_message} succeeded.") | ||
rescue StandardError => e | ||
s.failure("#{log_message} failed: #{e}") | ||
ensure | ||
tar_out.close unless tar_out.nil? | ||
zip_out.close unless zip_out.nil? | ||
end | ||
end | ||
end | ||
|
||
# Dynamically responding to hooks supplied in the constructor | ||
def method_missing(method_name, *args, &block) | ||
@hooks.include?(method_name) ? backup(*args) : super | ||
end | ||
|
||
def respond_to?(method_name, include_private = false) | ||
@hooks.include?(method_name) || super | ||
end | ||
|
||
private | ||
|
||
attr_accessor :app_name, | ||
:stack_name | ||
|
||
# Create a tar archive in memory, returning the IO object pointing at the | ||
# beginning of the archive. | ||
# | ||
# @param target_files [Array<String>] | ||
# @return tar_stream [IO] | ||
def tar(target_files) | ||
tar_stream = StringIO.new | ||
Gem::Package::TarWriter.new(tar_stream) do |writer| | ||
target_files.each do |file| | ||
file = render(file) | ||
|
||
writer.add_file(File.basename(file), 0644) do |io| | ||
File.open(file, 'r') { |f| io.write(f.read) } | ||
end | ||
end | ||
end | ||
tar_stream.seek(0) | ||
tar_stream | ||
end | ||
|
||
# Create a zip archive in memory, returning the IO object pointing at the | ||
# beginning of the zipfile. | ||
# | ||
# @param io_tar [IO] tar stream | ||
# @return zip_stream [IO] IO stream of zipped file | ||
def zip(io_tar) | ||
zip_stream = StringIO.new | ||
Zlib::GzipWriter.wrap(zip_stream) do |gz| | ||
gz.write(io_tar.read) | ||
gz.finish | ||
end | ||
zip_stream.seek(0) | ||
zip_stream | ||
end | ||
|
||
# Uploads an object from the passed IO stream to the specified bucket | ||
# | ||
# @param io_zip [IO] tar stream | ||
def upload(io_zip) | ||
s3_client.put_object( | ||
acl: 'private', | ||
bucket: @bucket, | ||
key: @target_name, | ||
body: io_zip | ||
) | ||
end | ||
|
||
# Renders string with the specified placeholders | ||
# | ||
# @param io_zip [String] raw string with placeholders | ||
# @return [String] rendered string | ||
def render(placeholder) | ||
format( | ||
placeholder, | ||
app_name: @app_name, | ||
stack_name: @stack_name, | ||
timestamp: Time.now.to_i.to_s, | ||
user: ENV['USER'] | ||
) | ||
end | ||
|
||
def log_message | ||
"Uploading '#{@target_name}' to '#{@bucket}'" | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
describe Moonshot::Plugins::Backup do | ||
let(:hooks) do | ||
[ | ||
:pre_create, | ||
:post_create, | ||
:pre_update, | ||
:post_update, | ||
:pre_delete, | ||
:post_delete, | ||
:pre_status, | ||
:post_status, | ||
:pre_doctor, | ||
:post_doctor | ||
] | ||
end | ||
|
||
describe '#new' do | ||
subject { Moonshot::Plugins::Backup } | ||
|
||
it 'should yield self' do | ||
backup = subject.new do |b| | ||
b.bucket = 'test' | ||
b.files = %w(sample files) | ||
b.hooks = [:sample, :hooks] | ||
end | ||
expect(backup.bucket).to eq('test') | ||
expect(backup.files).to eq(%w(sample files)) | ||
expect(backup.hooks).to eq([:sample, :hooks]) | ||
end | ||
|
||
it 'should raise ArgumentError if insufficient parameters are provided' do | ||
expect { subject.new }.to raise_error(ArgumentError) | ||
end | ||
|
||
let(:backup) do | ||
subject.new do |b| | ||
b.bucket = 'testbucket' | ||
b.files = %w(test files) | ||
b.hooks = [:post_create, :post_update] | ||
end | ||
end | ||
it 'should set a default value to target_name if not specified' do | ||
expect(backup.target_name).to eq '%{app_name}_%{timestamp}_%{user}.tar.gz' | ||
end | ||
end | ||
|
||
describe '#to_backup' do | ||
let(:test_bucket_name) { 'test_bucket' } | ||
let(:registered_hooks) { [:post_create, :post_update] } | ||
let(:unregistered_hooks) { hooks - registered_hooks } | ||
|
||
subject { Moonshot::Plugins::Backup.to_bucket(test_bucket_name) } | ||
|
||
it 'should return a Backup object' do | ||
expect(subject).to be_a Moonshot::Plugins::Backup | ||
end | ||
|
||
it 'should raise ArgumentError when no bucket specified' do | ||
end | ||
|
||
it 'should set default config values' do | ||
expect(subject.bucket).to eq test_bucket_name | ||
expect(subject.files).to eq [ | ||
'cloud_formation/%{app_name}.json', | ||
'cloud_formation/parameters/%{stack_name}.yml' | ||
] | ||
expect(subject.hooks).to eq [:post_create, :post_update] | ||
end | ||
|
||
it 'should only respond to the default hooks' do | ||
expect(subject).to respond_to(*registered_hooks) | ||
expect(subject).not_to respond_to(*unregistered_hooks) | ||
end | ||
end | ||
|
||
describe '#backup' do | ||
subject { Moonshot::Plugins::Backup.to_bucket bucket: 'bucket' } | ||
|
||
let(:resources) do | ||
instance_double( | ||
Moonshot::Resources, | ||
stack: instance_double(Moonshot::Stack), | ||
ilog: instance_double(Moonshot::InteractiveLoggerProxy) | ||
) | ||
end | ||
|
||
it 'should raise ArgumentError if resources not injected' do | ||
expect { subject.backup }.to raise_error(ArgumentError) | ||
end | ||
end | ||
end |