capistrano-s3_archive 0.5.4 → 0.9.9
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/README.md +83 -30
- data/capistrano-s3_archive.gemspec +5 -4
- data/img/s3_archive-rsync.png +0 -0
- data/legacy_README.md +97 -0
- data/lib/capistrano/legacy_s3_archive.rb +249 -0
- data/lib/capistrano/s3_archive.rb +1 -247
- data/lib/capistrano/s3_archive/version.rb +1 -1
- data/lib/capistrano/scm/s3_archive.rb +308 -0
- data/lib/capistrano/scm/tasks/s3_archive.rake +57 -0
- data/lib/capistrano/tasks/{s3_archive.rake → legacy_s3_archive.rake} +0 -0
- metadata +34 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 97cf12d50fcd21483d0594609a57b4867f898e47
|
4
|
+
data.tar.gz: 7109636be3cb0841c520546ca318991c5a6bbf28
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f0953e0106a31337751b0e0c9eb045ce845581d313ad040a96ab529308a2911c4fc127b182be47ff9d3da80d94a614974163521862e40d2fb0acefef23b24903
|
7
|
+
data.tar.gz: 243cc2aa9fd85e1977a08705827ae26f3a2d10be857bdd1430c6cc47406cbf39c09775c28baff8c5bc298fd37650364d72b786efe5e9b331df24f1e6fe5bfad2
|
data/README.md
CHANGED
@@ -1,16 +1,19 @@
|
|
1
1
|
# Capistrano::S3Archive
|
2
2
|
|
3
|
-
Capistrano::S3Archive is an extention of [Capistrano](http://www.capistranorb.com/)
|
3
|
+
Capistrano::S3Archive is an extention of [Capistrano](http://www.capistranorb.com/).
|
4
4
|
|
5
5
|
This behaves like the [capistrano-rsync](https://github.com/moll/capistrano-rsync) except downloading sources from S3 instead of GIT by default.
|
6
6
|
|
7
|
+

|
8
|
+
|
9
|
+
**CAUTION!!** Document for VERSION < 0.9 is [legacy_README](legacy_README.md)
|
7
10
|
|
8
11
|
## Installation
|
9
12
|
|
10
13
|
Add this line to your application's Gemfile:
|
11
14
|
|
12
15
|
```ruby
|
13
|
-
gem 'capistrano-s3_archive'
|
16
|
+
gem 'capistrano-s3_archive', '>= 0.9'
|
14
17
|
```
|
15
18
|
|
16
19
|
And then execute:
|
@@ -23,9 +26,16 @@ And then execute:
|
|
23
26
|
|
24
27
|
## Usage
|
25
28
|
|
26
|
-
|
29
|
+
### Quick Start
|
30
|
+
|
31
|
+
In Capfile:
|
32
|
+
|
33
|
+
```
|
34
|
+
require "capistrano/scm/s3_archive"
|
35
|
+
install_plugin Capistrano::SCM::S3Archive
|
36
|
+
```
|
27
37
|
|
28
|
-
|
38
|
+
And set a S3 path containing source archives to `:repo_url` and the parameters to access Amazon S3 to `:s3_archive_client_options`, For example, if you has following tree,
|
29
39
|
|
30
40
|
s3://yourbucket/somedirectory/
|
31
41
|
|- 201506011200.zip
|
@@ -34,15 +44,15 @@ Set a S3 path containing source archives to `repo_url`. For example, if you has
|
|
34
44
|
|- 201506020100.zip
|
35
45
|
`- 201506030100.zip
|
36
46
|
|
37
|
-
then `
|
47
|
+
then your `config/deploy.rb` would be:
|
38
48
|
|
39
|
-
Set parameters to access Amazon S3:
|
40
|
-
|
41
|
-
```ruby
|
42
|
-
set :s3_client_options, { region: 'ap-northeast-1', credentials: somecredentials }
|
43
49
|
```
|
50
|
+
set :repo_url, 's3://yourbucket/somedirectory/'
|
51
|
+
set :s3_archive_client_options, { region: 'ap-northeast-1', credentials: somecredentials }
|
52
|
+
```
|
53
|
+
|
54
|
+
To deploy staging:
|
44
55
|
|
45
|
-
And set regular capistrano options. To deploy staging:
|
46
56
|
```
|
47
57
|
$ bundle exec cap staging deploy
|
48
58
|
```
|
@@ -54,26 +64,69 @@ $ bundle exec cap staging deploy_only
|
|
54
64
|
|
55
65
|
|
56
66
|
### Configuration
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
67
|
+
|
68
|
+
Available configurations are followings (key, default).
|
69
|
+
|
70
|
+
:repo_url, nil
|
71
|
+
:branch, :latest
|
72
|
+
:s3_archive_client_options, nil
|
73
|
+
:s3_archive_sort_proc, ->(new, old) { old.key <=> new.key }
|
74
|
+
:s3_archive_object_version_id, nil
|
75
|
+
:s3_archive_local_download_dir, "tmp/archives"
|
76
|
+
:s3_archive_local_cache_dir, "tmp/deploy"
|
77
|
+
:s3_archive_remote_rsync_options, ['-az', '--delete']
|
78
|
+
:s3_archive_remote_rsync_ssh_options, []
|
79
|
+
:s3_archive_remote_rsync_runner_options, {}
|
80
|
+
:s3_archive_rsync_cache_dir, "shared/deploy"
|
81
|
+
:s3_archive_hardlink_release, false
|
82
|
+
|
83
|
+
**`repo_url` (required)**
|
84
|
+
|
85
|
+
The S3 bucket and prefix where the archives are stored. e.g. 's3://yourbucket/somedirectory/'.
|
86
|
+
|
87
|
+
**`branch`**
|
88
|
+
|
89
|
+
Basename of archive object to deploy. In the previous example at Quick Start section, you can use `'201506011500.zip'`, `'201506020100.zip'`, etc. And `:latest` is a special symbol to select latest object automatically by `:s3_archive_sort_proc`.
|
90
|
+
|
91
|
+
**`s3_archive_client_options` (required)**
|
92
|
+
|
93
|
+
Options passed to `Aws::S3::Client.new(options)` to fetch archives.
|
94
|
+
|
95
|
+
**`s3_archive_sort_proc`**
|
96
|
+
|
97
|
+
Sort algorithm used to detect basename of `:latest` object. It should be proc object for `new,old` as `Aws::S3::Object` comparing.
|
98
|
+
|
99
|
+
**`:s3_archive_object_version_id`**
|
100
|
+
|
101
|
+
Version ID of version-controlled S3 object. It should use with `:branch`. e.g. `set :branch, 'myapp.zip'; set :version_id, 'qawsedrftgyhujikolq'`
|
102
|
+
|
103
|
+
**`:s3_archive_local_download_dir`**
|
104
|
+
|
105
|
+
Path where to download source archives. Can use both relative or absolute.
|
106
|
+
|
107
|
+
**`:s3_archive_local_cache_dir`**
|
108
|
+
|
109
|
+
Path where to extruct your archive on local for staging and rsyncing. Can use both relative or absolute.
|
110
|
+
|
111
|
+
**`:s3_archive_remote_rsync_options`**
|
112
|
+
|
113
|
+
Options used to rsync to remote cache dir.
|
114
|
+
|
115
|
+
**`:s3_archive_remote_rsync_ssh_options`**
|
116
|
+
|
117
|
+
Options used in `rsync -e 'ssh OPTIONS'`.
|
118
|
+
|
119
|
+
**`:s3_archive_remote_rsync_runner_options`**
|
120
|
+
|
121
|
+
Runner options of a task to rsync to remote cache, this options are passed to `on release_roles(:all), options` in the rsyncing task. It's useful when to reduce the overload of the machine running Capistrano. e.g. `set :s3_archive_remote_rsync_runner_options, { in: :groups, limit: 10 }`.
|
122
|
+
|
123
|
+
**`:s3_archive_rsync_cache_dir`**
|
124
|
+
|
125
|
+
Path where to cache your sources on the remote server to avoid rsyncing from scratch each time. Can use both relative or absolute from `deploy_to` path.
|
126
|
+
|
127
|
+
**`:s3_archive_hardlink_release`**
|
128
|
+
|
129
|
+
Enable `--link-dest` option when creating release directory by remote rsyncing. It could speed deployment up.
|
77
130
|
|
78
131
|
## Development
|
79
132
|
|
@@ -19,9 +19,10 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
21
|
spec.required_ruby_version = '>= 2.0.0'
|
22
|
-
spec.add_dependency 'capistrano', '~> 3.
|
23
|
-
spec.add_dependency 'aws-sdk
|
22
|
+
spec.add_dependency 'capistrano', '~> 3.0'
|
23
|
+
spec.add_dependency 'aws-sdk', '~> 2.0'
|
24
24
|
|
25
|
-
spec.add_development_dependency "bundler"
|
26
|
-
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "bundler"
|
26
|
+
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "rubocop"
|
27
28
|
end
|
Binary file
|
data/legacy_README.md
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# Capistrano::S3Archive (README for legacy version)
|
2
|
+
|
3
|
+
**CAUTION**
|
4
|
+
Capistrano plugin system has been renewed at version 3.7. This legacy_README is the document for old version.
|
5
|
+
|
6
|
+
https://github.com/capistrano/capistrano/blob/master/UPGRADING-3.7.md
|
7
|
+
|
8
|
+
================
|
9
|
+
|
10
|
+
Capistrano::S3Archive is an extention of [Capistrano](http://www.capistranorb.com/) which enables to `set :scm, :s3_archive`.
|
11
|
+
|
12
|
+
This behaves like the [capistrano-rsync](https://github.com/moll/capistrano-rsync) except downloading sources from S3 instead of GIT by default.
|
13
|
+
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'capistrano-s3_archive'
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
<!-- Or install it yourself as: -->
|
28
|
+
|
29
|
+
<!-- $ gem install capistrano-s3_archive -->
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
`set :scm, :s3_archive` in your config file.
|
34
|
+
|
35
|
+
Set a S3 path containing source archives to `repo_url`. For example, if you has following tree,
|
36
|
+
|
37
|
+
s3://yourbucket/somedirectory/
|
38
|
+
|- 201506011200.zip
|
39
|
+
|- 201506011500.zip
|
40
|
+
...
|
41
|
+
|- 201506020100.zip
|
42
|
+
`- 201506030100.zip
|
43
|
+
|
44
|
+
then `set :repo_url, 's3://yourbucket/somedirectory'`.
|
45
|
+
|
46
|
+
Set parameters to access Amazon S3:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
set :s3_client_options, { region: 'ap-northeast-1', credentials: somecredentials }
|
50
|
+
```
|
51
|
+
|
52
|
+
And set regular capistrano options. To deploy staging:
|
53
|
+
```
|
54
|
+
$ bundle exec cap staging deploy
|
55
|
+
```
|
56
|
+
|
57
|
+
Or to skip download & extruct archive and deploy local files:
|
58
|
+
```
|
59
|
+
$ bundle exec cap staging deploy_only
|
60
|
+
```
|
61
|
+
|
62
|
+
|
63
|
+
### Configuration
|
64
|
+
Set parameters with `set :key, value`.
|
65
|
+
|
66
|
+
#### Rsync Strategy (default)
|
67
|
+
|
68
|
+
Key | Default | Description
|
69
|
+
--------------|---------|------------
|
70
|
+
branch | `latest` | The S3 Object basename to download. Support `:latest` or such as `'201506011500.zip'`.
|
71
|
+
version_id | nil | Version ID of version-controlled S3 object. It should use with `:branch`. e.g. `set :branch, 'myapp.zip'; set :version_id, 'qawsedrftgyhujikolq'`
|
72
|
+
sort_proc | `->(a,b) { b.key <=> a.key }` | Sort algorithm used to detect `:latest` object basename. It should be proc object for `a,b` as `Aws::S3::Object` comparing.
|
73
|
+
rsync_options | `['-az']` | Options used to rsync.
|
74
|
+
local_cache | `tmp/deploy` | Path where to extruct your archive on local for staging and rsyncing. Can be both relative or absolute.
|
75
|
+
rsync_cache | `shared/deploy` | Path where to cache your repository on the server to avoid rsyncing from scratch each time. Can be both relative or absolute.<br> Set to `nil` if you want to disable the cache.
|
76
|
+
s3_archive | `tmp/archives` | Path where to download source archives. Can be both relative or absolute.
|
77
|
+
archive_release_runner_options | { in: :groups, limit: fetch(:archive_release_runner_concurrency) } | Runner options on creating releases.
|
78
|
+
(archive_release_runner_concurrency) | 20 | Default value of runner concurrency option.
|
79
|
+
|
80
|
+
##### Experimental configration
|
81
|
+
Key | Default | Description
|
82
|
+
--------------|---------|------------
|
83
|
+
hardlink | nil | Enable `--link-dest` option when remote rsyncing. It could speed deployment up in the case rsync_cache enabled.
|
84
|
+
|
85
|
+
## Development
|
86
|
+
|
87
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
88
|
+
|
89
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
90
|
+
|
91
|
+
## Contributing
|
92
|
+
|
93
|
+
1. Fork it ( https://github.com/[my-github-username]/capistrano-s3_archive/fork )
|
94
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
95
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
96
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
97
|
+
5. Create a new Pull Request
|
@@ -0,0 +1,249 @@
|
|
1
|
+
$stderr.puts "DEPRECATION WARNING: `set :scm, :s3_archive` is deprecated. see https://github.com/capistrano/capistrano/blob/master/UPGRADING-3.7.md to update to the plugin architecture."
|
2
|
+
|
3
|
+
require 'aws-sdk-core'
|
4
|
+
|
5
|
+
load File.expand_path("../tasks/legacy_s3_archive.rake", __FILE__)
|
6
|
+
|
7
|
+
require "capistrano/s3_archive/version"
|
8
|
+
require 'capistrano/scm'
|
9
|
+
|
10
|
+
set_if_empty :rsync_options, ['-az --delete']
|
11
|
+
set_if_empty :rsync_ssh_options, []
|
12
|
+
set_if_empty :rsync_copy, "rsync --archive --acls --xattrs"
|
13
|
+
set_if_empty :rsync_cache, "shared/deploy"
|
14
|
+
set_if_empty :local_cache, "tmp/deploy"
|
15
|
+
set_if_empty :s3_archive, "tmp/archives"
|
16
|
+
set_if_empty :sort_proc, ->(a,b) { b.key <=> a.key }
|
17
|
+
set_if_empty :archive_release_runner_concurrency, 20
|
18
|
+
set_if_empty :archive_release_runner_options, { in: :groups, limit: fetch(:archive_release_runner_concurrency) }
|
19
|
+
|
20
|
+
module Capistrano
|
21
|
+
module S3Archive
|
22
|
+
class SCM < Capistrano::SCM
|
23
|
+
include FileUtils
|
24
|
+
attr_accessor :bucket, :object_prefix
|
25
|
+
|
26
|
+
def initialize(*args)
|
27
|
+
super
|
28
|
+
@bucket, @object_prefix = parse_s3_uri(repo_url)
|
29
|
+
set :local_cache_dir, "#{fetch(:local_cache)}/#{fetch(:stage)}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_object(target)
|
33
|
+
opts = { bucket: bucket, key: archive_object_key }
|
34
|
+
opts[:version_id] = fetch(:version_id) if fetch(:version_id)
|
35
|
+
s3_client.get_object(opts, target: target)
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_object_metadata
|
39
|
+
s3_client.list_object_versions(bucket: bucket, prefix: archive_object_key).versions.find do |v|
|
40
|
+
if fetch(:version_id) then v.version_id == fetch(:version_id)
|
41
|
+
else v.is_latest
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def list_objects(all_page = true)
|
47
|
+
response = s3_client.list_objects(bucket: bucket, prefix: object_prefix)
|
48
|
+
if all_page
|
49
|
+
response.inject([]) do |objects, page|
|
50
|
+
objects += page.contents
|
51
|
+
end
|
52
|
+
else
|
53
|
+
response
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def archive_object_key
|
58
|
+
@archive_object_key ||=
|
59
|
+
case fetch(:branch).to_sym
|
60
|
+
when :master, :latest, nil
|
61
|
+
latest_object_key
|
62
|
+
else
|
63
|
+
object_prefix + fetch(:branch)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
def s3_client
|
69
|
+
@s3_client ||= Aws::S3::Client.new(fetch(:s3_client_options))
|
70
|
+
end
|
71
|
+
|
72
|
+
def latest_object_key
|
73
|
+
list_objects.sort(&fetch(:sort_proc)).first.key
|
74
|
+
end
|
75
|
+
|
76
|
+
def parse_s3_uri(uri)
|
77
|
+
pathes = uri.split('://')[1].split('/')
|
78
|
+
[pathes.first, pathes.drop(1).push('').join('/')]
|
79
|
+
end
|
80
|
+
|
81
|
+
### Default strategy
|
82
|
+
private
|
83
|
+
module RsyncStrategy
|
84
|
+
class MissingSSHKyesError < StandardError; end
|
85
|
+
class ResourceBusyError < StandardError; end
|
86
|
+
|
87
|
+
def local_check
|
88
|
+
list_objects(false)
|
89
|
+
end
|
90
|
+
|
91
|
+
def check
|
92
|
+
return if context.class == SSHKit::Backend::Local
|
93
|
+
ssh_key = ssh_key_for(context.host)
|
94
|
+
if ssh_key.nil?
|
95
|
+
fail MissingSSHKyesError, "#{RsyncStrategy} only supports publickey authentication. Please set #{context.host.hostname}.keys or ssh_options."
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def stage
|
100
|
+
stage_lock do
|
101
|
+
archive_dir = File.join(fetch(:s3_archive), fetch(:stage).to_s)
|
102
|
+
archive_file = File.join(archive_dir, File.basename(archive_object_key))
|
103
|
+
tmp_file = "#{archive_file}.part"
|
104
|
+
etag_file = File.join(archive_dir, ".#{File.basename(archive_object_key)}.etag")
|
105
|
+
fail "#{tmp_file} is found. Another process is running?" if File.exist?(tmp_file)
|
106
|
+
|
107
|
+
etag = get_object_metadata.tap { |it| fail "No such object: #{current_revision}" if it.nil? }.etag
|
108
|
+
if [archive_file, etag_file].all? { |f| File.exist?(f) } && File.read(etag_file) == etag
|
109
|
+
context.info "#{archive_file} (etag:#{etag}) is found. download skipped."
|
110
|
+
else
|
111
|
+
context.info "Download #{current_revision} to #{archive_file}"
|
112
|
+
mkdir_p(File.dirname(archive_file))
|
113
|
+
File.open(tmp_file, 'w') do |f|
|
114
|
+
get_object(f)
|
115
|
+
end
|
116
|
+
move(tmp_file, archive_file)
|
117
|
+
File.write(etag_file, etag)
|
118
|
+
end
|
119
|
+
|
120
|
+
remove_entry_secure(fetch(:local_cache_dir)) if File.exist? fetch(:local_cache_dir)
|
121
|
+
mkdir_p(fetch(:local_cache_dir))
|
122
|
+
case archive_file
|
123
|
+
when /\.zip\Z/
|
124
|
+
cmd = "unzip -q -d #{fetch(:local_cache_dir)} #{archive_file}"
|
125
|
+
when /\.tar\.gz\Z|\.tar\.bz2\Z|\.tgz\Z/
|
126
|
+
cmd = "tar xf #{archive_file} -C #{fetch(:local_cache_dir)}"
|
127
|
+
end
|
128
|
+
|
129
|
+
release_lock(true) do
|
130
|
+
run_locally do
|
131
|
+
execute cmd
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def cleanup
|
138
|
+
run_locally do
|
139
|
+
archives_dir = File.join(fetch(:s3_archive), fetch(:stage).to_s)
|
140
|
+
archives = capture(:ls, '-xtr', archives_dir).split
|
141
|
+
if archives.count >= fetch(:keep_releases)
|
142
|
+
tobe_removes = (archives - archives.last(fetch(:keep_releases)))
|
143
|
+
if tobe_removes.any?
|
144
|
+
tobe_removes_str = tobe_removes.map do |file|
|
145
|
+
File.join(archives_dir, file)
|
146
|
+
end.join(' ')
|
147
|
+
execute :rm, tobe_removes_str
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def release(server = context.host)
|
154
|
+
unless context.class == SSHKit::Backend::Local
|
155
|
+
user = user_for(server) + '@' unless user_for(server).nil?
|
156
|
+
key = ssh_key_for(server)
|
157
|
+
ssh_port_option = server.port.nil? ? '' : "-p #{server.port}"
|
158
|
+
end
|
159
|
+
rsync = ['rsync']
|
160
|
+
rsync.concat fetch(:rsync_options)
|
161
|
+
rsync << fetch(:local_cache_dir) + '/'
|
162
|
+
unless context.class == SSHKit::Backend::Local
|
163
|
+
rsync << "-e 'ssh -i #{key} #{ssh_port_option} #{fetch(:rsync_ssh_options).join(' ')}'"
|
164
|
+
rsync << "#{user}#{server.hostname}:#{rsync_cache || release_path}"
|
165
|
+
else
|
166
|
+
rsync << '--no-compress'
|
167
|
+
rsync << "#{rsync_cache || release_path}"
|
168
|
+
end
|
169
|
+
release_lock do
|
170
|
+
run_locally do
|
171
|
+
execute *rsync
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
unless fetch(:rsync_cache).nil?
|
176
|
+
cache = rsync_cache
|
177
|
+
link_option = if fetch(:hardlink) && test!("[ `readlink #{current_path}` != #{release_path} ]")
|
178
|
+
"--link-dest `readlink #{current_path}`"
|
179
|
+
end
|
180
|
+
copy = %(#{fetch(:rsync_copy)} #{link_option} "#{cache}/" "#{release_path}/")
|
181
|
+
context.execute copy
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def current_revision
|
186
|
+
fetch(:version_id) ? "#{archive_object_key}?versionid=#{fetch(:version_id)}" : archive_object_key
|
187
|
+
end
|
188
|
+
|
189
|
+
def ssh_key_for(host)
|
190
|
+
if not host.keys.empty?
|
191
|
+
host.keys.first
|
192
|
+
elsif host.ssh_options && host.ssh_options.has_key?(:keys)
|
193
|
+
Array(host.ssh_options[:keys]).first
|
194
|
+
elsif fetch(:ssh_options, nil) && fetch(:ssh_options).has_key?(:keys)
|
195
|
+
fetch(:ssh_options)[:keys].first
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def user_for(host)
|
200
|
+
if host.user
|
201
|
+
host.user
|
202
|
+
elsif host.ssh_options && host.ssh_options.has_key?(:user)
|
203
|
+
host.ssh_options[:user]
|
204
|
+
elsif fetch(:ssh_options, nil) && fetch(:ssh_options).has_key?(:user)
|
205
|
+
fetch(:ssh_options)[:user]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
def rsync_cache
|
211
|
+
cache = fetch(:rsync_cache)
|
212
|
+
cache = deploy_to + "/" + cache if cache && cache !~ /^\//
|
213
|
+
cache
|
214
|
+
end
|
215
|
+
|
216
|
+
def stage_lock(&block)
|
217
|
+
mkdir_p(File.dirname(fetch(:local_cache)))
|
218
|
+
lockfile = "#{fetch(:local_cache)}.#{fetch(:stage)}.lock"
|
219
|
+
File.open(lockfile, "w") do |f|
|
220
|
+
if f.flock(File::LOCK_EX | File::LOCK_NB)
|
221
|
+
block.call
|
222
|
+
else
|
223
|
+
fail ResourceBusyError, "Could not get #{lockfile}"
|
224
|
+
end
|
225
|
+
end
|
226
|
+
ensure
|
227
|
+
rm lockfile if File.exist? lockfile
|
228
|
+
end
|
229
|
+
|
230
|
+
def release_lock(exclusive = false, &block)
|
231
|
+
mkdir_p(File.dirname(fetch(:local_cache)))
|
232
|
+
lockfile = "#{fetch(:local_cache)}.#{fetch(:stage)}.release.lock"
|
233
|
+
File.open(lockfile, File::RDONLY|File::CREAT) do |f|
|
234
|
+
mode = if exclusive
|
235
|
+
File::LOCK_EX | File::LOCK_NB
|
236
|
+
else
|
237
|
+
File::LOCK_SH
|
238
|
+
end
|
239
|
+
if f.flock(mode)
|
240
|
+
block.call
|
241
|
+
else
|
242
|
+
fail ResourceBusyError, "Could not get #{fetch(:lockfile)}"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -1,247 +1 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
load File.expand_path("../tasks/s3_archive.rake", __FILE__)
|
4
|
-
|
5
|
-
require "capistrano/s3_archive/version"
|
6
|
-
require 'capistrano/scm'
|
7
|
-
|
8
|
-
set_if_empty :rsync_options, ['-az --delete']
|
9
|
-
set_if_empty :rsync_ssh_options, []
|
10
|
-
set_if_empty :rsync_copy, "rsync --archive --acls --xattrs"
|
11
|
-
set_if_empty :rsync_cache, "shared/deploy"
|
12
|
-
set_if_empty :local_cache, "tmp/deploy"
|
13
|
-
set_if_empty :s3_archive, "tmp/archives"
|
14
|
-
set_if_empty :sort_proc, ->(a,b) { b.key <=> a.key }
|
15
|
-
set_if_empty :archive_release_runner_concurrency, 20
|
16
|
-
set_if_empty :archive_release_runner_options, { in: :groups, limit: fetch(:archive_release_runner_concurrency) }
|
17
|
-
|
18
|
-
module Capistrano
|
19
|
-
module S3Archive
|
20
|
-
class SCM < Capistrano::SCM
|
21
|
-
include FileUtils
|
22
|
-
attr_accessor :bucket, :object_prefix
|
23
|
-
|
24
|
-
def initialize(*args)
|
25
|
-
super
|
26
|
-
@bucket, @object_prefix = parse_s3_uri(repo_url)
|
27
|
-
set :local_cache_dir, "#{fetch(:local_cache)}/#{fetch(:stage)}"
|
28
|
-
end
|
29
|
-
|
30
|
-
def get_object(target)
|
31
|
-
opts = { bucket: bucket, key: archive_object_key }
|
32
|
-
opts[:version_id] = fetch(:version_id) if fetch(:version_id)
|
33
|
-
s3_client.get_object(opts, target: target)
|
34
|
-
end
|
35
|
-
|
36
|
-
def get_object_metadata
|
37
|
-
s3_client.list_object_versions(bucket: bucket, prefix: archive_object_key).versions.find do |v|
|
38
|
-
if fetch(:version_id) then v.version_id == fetch(:version_id)
|
39
|
-
else v.is_latest
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def list_objects(all_page = true)
|
45
|
-
response = s3_client.list_objects(bucket: bucket, prefix: object_prefix)
|
46
|
-
if all_page
|
47
|
-
response.inject([]) do |objects, page|
|
48
|
-
objects += page.contents
|
49
|
-
end
|
50
|
-
else
|
51
|
-
response
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def archive_object_key
|
56
|
-
@archive_object_key ||=
|
57
|
-
case fetch(:branch).to_sym
|
58
|
-
when :master, :latest, nil
|
59
|
-
latest_object_key
|
60
|
-
else
|
61
|
-
object_prefix + fetch(:branch)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
private
|
66
|
-
def s3_client
|
67
|
-
@s3_client ||= Aws::S3::Client.new(fetch(:s3_client_options))
|
68
|
-
end
|
69
|
-
|
70
|
-
def latest_object_key
|
71
|
-
list_objects.sort(&fetch(:sort_proc)).first.key
|
72
|
-
end
|
73
|
-
|
74
|
-
def parse_s3_uri(uri)
|
75
|
-
pathes = uri.split('://')[1].split('/')
|
76
|
-
[pathes.first, pathes.drop(1).push('').join('/')]
|
77
|
-
end
|
78
|
-
|
79
|
-
### Default strategy
|
80
|
-
private
|
81
|
-
module RsyncStrategy
|
82
|
-
class MissingSSHKyesError < StandardError; end
|
83
|
-
class ResourceBusyError < StandardError; end
|
84
|
-
|
85
|
-
def local_check
|
86
|
-
list_objects(false)
|
87
|
-
end
|
88
|
-
|
89
|
-
def check
|
90
|
-
return if context.class == SSHKit::Backend::Local
|
91
|
-
ssh_key = ssh_key_for(context.host)
|
92
|
-
if ssh_key.nil?
|
93
|
-
fail MissingSSHKyesError, "#{RsyncStrategy} only supports publickey authentication. Please set #{context.host.hostname}.keys or ssh_options."
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
def stage
|
98
|
-
stage_lock do
|
99
|
-
archive_dir = File.join(fetch(:s3_archive), fetch(:stage).to_s)
|
100
|
-
archive_file = File.join(archive_dir, File.basename(archive_object_key))
|
101
|
-
tmp_file = "#{archive_file}.part"
|
102
|
-
etag_file = File.join(archive_dir, ".#{File.basename(archive_object_key)}.etag")
|
103
|
-
fail "#{tmp_file} is found. Another process is running?" if File.exist?(tmp_file)
|
104
|
-
|
105
|
-
etag = get_object_metadata.tap { |it| fail "No such object: #{current_revision}" if it.nil? }.etag
|
106
|
-
if [archive_file, etag_file].all? { |f| File.exist?(f) } && File.read(etag_file) == etag
|
107
|
-
context.info "#{archive_file} (etag:#{etag}) is found. download skipped."
|
108
|
-
else
|
109
|
-
context.info "Download #{current_revision} to #{archive_file}"
|
110
|
-
mkdir_p(File.dirname(archive_file))
|
111
|
-
File.open(tmp_file, 'w') do |f|
|
112
|
-
get_object(f)
|
113
|
-
end
|
114
|
-
move(tmp_file, archive_file)
|
115
|
-
File.write(etag_file, etag)
|
116
|
-
end
|
117
|
-
|
118
|
-
remove_entry_secure(fetch(:local_cache_dir)) if File.exist? fetch(:local_cache_dir)
|
119
|
-
mkdir_p(fetch(:local_cache_dir))
|
120
|
-
case archive_file
|
121
|
-
when /\.zip\Z/
|
122
|
-
cmd = "unzip -q -d #{fetch(:local_cache_dir)} #{archive_file}"
|
123
|
-
when /\.tar\.gz\Z|\.tar\.bz2\Z|\.tgz\Z/
|
124
|
-
cmd = "tar xf #{archive_file} -C #{fetch(:local_cache_dir)}"
|
125
|
-
end
|
126
|
-
|
127
|
-
release_lock(true) do
|
128
|
-
run_locally do
|
129
|
-
execute cmd
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
def cleanup
|
136
|
-
run_locally do
|
137
|
-
archives_dir = File.join(fetch(:s3_archive), fetch(:stage).to_s)
|
138
|
-
archives = capture(:ls, '-xtr', archives_dir).split
|
139
|
-
if archives.count >= fetch(:keep_releases)
|
140
|
-
tobe_removes = (archives - archives.last(fetch(:keep_releases)))
|
141
|
-
if tobe_removes.any?
|
142
|
-
tobe_removes_str = tobe_removes.map do |file|
|
143
|
-
File.join(archives_dir, file)
|
144
|
-
end.join(' ')
|
145
|
-
execute :rm, tobe_removes_str
|
146
|
-
end
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
def release(server = context.host)
|
152
|
-
unless context.class == SSHKit::Backend::Local
|
153
|
-
user = user_for(server) + '@' unless user_for(server).nil?
|
154
|
-
key = ssh_key_for(server)
|
155
|
-
ssh_port_option = server.port.nil? ? '' : "-p #{server.port}"
|
156
|
-
end
|
157
|
-
rsync = ['rsync']
|
158
|
-
rsync.concat fetch(:rsync_options)
|
159
|
-
rsync << fetch(:local_cache_dir) + '/'
|
160
|
-
unless context.class == SSHKit::Backend::Local
|
161
|
-
rsync << "-e 'ssh -i #{key} #{ssh_port_option} #{fetch(:rsync_ssh_options).join(' ')}'"
|
162
|
-
rsync << "#{user}#{server.hostname}:#{rsync_cache || release_path}"
|
163
|
-
else
|
164
|
-
rsync << '--no-compress'
|
165
|
-
rsync << "#{rsync_cache || release_path}"
|
166
|
-
end
|
167
|
-
release_lock do
|
168
|
-
run_locally do
|
169
|
-
execute *rsync
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
unless fetch(:rsync_cache).nil?
|
174
|
-
cache = rsync_cache
|
175
|
-
link_option = if fetch(:hardlink) && test!("[ `readlink #{current_path}` != #{release_path} ]")
|
176
|
-
"--link-dest `readlink #{current_path}`"
|
177
|
-
end
|
178
|
-
copy = %(#{fetch(:rsync_copy)} #{link_option} "#{cache}/" "#{release_path}/")
|
179
|
-
context.execute copy
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
def current_revision
|
184
|
-
fetch(:version_id) ? "#{archive_object_key}?versionid=#{fetch(:version_id)}" : archive_object_key
|
185
|
-
end
|
186
|
-
|
187
|
-
def ssh_key_for(host)
|
188
|
-
if not host.keys.empty?
|
189
|
-
host.keys.first
|
190
|
-
elsif host.ssh_options && host.ssh_options.has_key?(:keys)
|
191
|
-
Array(host.ssh_options[:keys]).first
|
192
|
-
elsif fetch(:ssh_options, nil) && fetch(:ssh_options).has_key?(:keys)
|
193
|
-
fetch(:ssh_options)[:keys].first
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
def user_for(host)
|
198
|
-
if host.user
|
199
|
-
host.user
|
200
|
-
elsif host.ssh_options && host.ssh_options.has_key?(:user)
|
201
|
-
host.ssh_options[:user]
|
202
|
-
elsif fetch(:ssh_options, nil) && fetch(:ssh_options).has_key?(:user)
|
203
|
-
fetch(:ssh_options)[:user]
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
private
|
208
|
-
def rsync_cache
|
209
|
-
cache = fetch(:rsync_cache)
|
210
|
-
cache = deploy_to + "/" + cache if cache && cache !~ /^\//
|
211
|
-
cache
|
212
|
-
end
|
213
|
-
|
214
|
-
def stage_lock(&block)
|
215
|
-
mkdir_p(File.dirname(fetch(:local_cache)))
|
216
|
-
lockfile = "#{fetch(:local_cache)}.#{fetch(:stage)}.lock"
|
217
|
-
File.open(lockfile, "w") do |f|
|
218
|
-
if f.flock(File::LOCK_EX | File::LOCK_NB)
|
219
|
-
block.call
|
220
|
-
else
|
221
|
-
fail ResourceBusyError, "Could not get #{lockfile}"
|
222
|
-
end
|
223
|
-
end
|
224
|
-
ensure
|
225
|
-
rm lockfile if File.exist? lockfile
|
226
|
-
end
|
227
|
-
|
228
|
-
def release_lock(exclusive = false, &block)
|
229
|
-
mkdir_p(File.dirname(fetch(:local_cache)))
|
230
|
-
lockfile = "#{fetch(:local_cache)}.#{fetch(:stage)}.release.lock"
|
231
|
-
File.open(lockfile, File::RDONLY|File::CREAT) do |f|
|
232
|
-
mode = if exclusive
|
233
|
-
File::LOCK_EX | File::LOCK_NB
|
234
|
-
else
|
235
|
-
File::LOCK_SH
|
236
|
-
end
|
237
|
-
if f.flock(mode)
|
238
|
-
block.call
|
239
|
-
else
|
240
|
-
fail ResourceBusyError, "Could not get #{fetch(:lockfile)}"
|
241
|
-
end
|
242
|
-
end
|
243
|
-
end
|
244
|
-
end
|
245
|
-
end
|
246
|
-
end
|
247
|
-
end
|
1
|
+
load File.expand_path("../legacy_s3_archive.rb", __FILE__)
|
@@ -0,0 +1,308 @@
|
|
1
|
+
require "capistrano/scm/plugin"
|
2
|
+
require "aws-sdk"
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
module Capistrano
|
6
|
+
class SCM
|
7
|
+
class S3Archive < Capistrano::SCM::Plugin
|
8
|
+
attr_reader :extractor
|
9
|
+
include FileUtils
|
10
|
+
|
11
|
+
class ResourceBusyError < StandardError; end
|
12
|
+
|
13
|
+
def set_defaults
|
14
|
+
set_if_empty :s3_archive_client_options, {}
|
15
|
+
set_if_empty :s3_archive_extract_to, :local # :local or :remote
|
16
|
+
set_if_empty(:s3_archive_sort_proc, ->(new, old) { old.key <=> new.key })
|
17
|
+
set_if_empty :s3_archive_object_version_id, nil
|
18
|
+
set_if_empty :s3_archive_local_download_dir, "tmp/archives"
|
19
|
+
set_if_empty :s3_archive_local_cache_dir, "tmp/deploy"
|
20
|
+
set_if_empty :s3_archive_remote_rsync_options, ['-az', '--delete']
|
21
|
+
set_if_empty :s3_archive_remote_rsync_ssh_options, []
|
22
|
+
set_if_empty :s3_archive_remote_rsync_runner_options, {}
|
23
|
+
set_if_empty :s3_archive_rsync_cache_dir, "shared/deploy"
|
24
|
+
set_if_empty :s3_archive_hardlink_release, false
|
25
|
+
# internal use
|
26
|
+
set_if_empty :s3_archive_rsync_copy, "rsync --archive --acls --xattrs"
|
27
|
+
end
|
28
|
+
|
29
|
+
def define_tasks
|
30
|
+
eval_rakefile File.expand_path("../tasks/s3_archive.rake", __FILE__)
|
31
|
+
end
|
32
|
+
|
33
|
+
def register_hooks
|
34
|
+
after "deploy:new_release_path", "s3_archive:create_release"
|
35
|
+
before "deploy:check", "s3_archive:check"
|
36
|
+
before "deploy:set_current_revision", "s3_archive:set_current_revision"
|
37
|
+
end
|
38
|
+
|
39
|
+
def local_check
|
40
|
+
s3_client.list_objects(bucket: s3params.bucket, prefix: s3params.object_prefix)
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_object(target)
|
44
|
+
opts = { bucket: s3params.bucket, key: archive_object_key }
|
45
|
+
opts[:version_id] = fetch(:s3_archive_object_version_id) if fetch(:s3_archive_object_version_id)
|
46
|
+
s3_client.get_object(opts, target: target)
|
47
|
+
end
|
48
|
+
|
49
|
+
def remote_check
|
50
|
+
backend.execute :echo, 'check ssh'
|
51
|
+
end
|
52
|
+
|
53
|
+
def stage
|
54
|
+
stage_lock do
|
55
|
+
archive_dir = File.join(fetch(:s3_archive_local_download_dir), fetch(:stage).to_s)
|
56
|
+
archive_file = File.join(archive_dir, File.basename(archive_object_key))
|
57
|
+
tmp_file = "#{archive_file}.part"
|
58
|
+
etag_file = File.join(archive_dir, ".#{File.basename(archive_object_key)}.etag")
|
59
|
+
fail "#{tmp_file} is found. Another process is running?" if File.exist?(tmp_file)
|
60
|
+
etag = get_object_metadata.tap { |it| fail "No such object: #{current_revision}" if it.nil? }.etag
|
61
|
+
|
62
|
+
|
63
|
+
if [archive_file, etag_file].all? { |f| File.exist?(f) } && File.read(etag_file) == etag
|
64
|
+
backend.info "#{archive_file} (etag:#{etag}) is found. download skipped."
|
65
|
+
else
|
66
|
+
backend.info "Download #{current_revision} to #{archive_file}"
|
67
|
+
mkdir_p(File.dirname(archive_file))
|
68
|
+
File.open(tmp_file, 'w') do |f|
|
69
|
+
get_object(f)
|
70
|
+
end
|
71
|
+
move(tmp_file, archive_file)
|
72
|
+
File.write(etag_file, etag)
|
73
|
+
end
|
74
|
+
|
75
|
+
remove_entry_secure(fetch(:s3_archive_local_cache_dir)) if File.exist? fetch(:s3_archive_local_cache_dir)
|
76
|
+
mkdir_p(fetch(:s3_archive_local_cache_dir))
|
77
|
+
case archive_file
|
78
|
+
when /\.zip\Z/
|
79
|
+
cmd = "unzip -q -d #{fetch(:s3_archive_local_cache_dir)} #{archive_file}"
|
80
|
+
when /\.tar\.gz\Z|\.tar\.bz2\Z|\.tgz\Z/
|
81
|
+
cmd = "tar xf #{archive_file} -C #{fetch(:s3_archive_local_cache_dir)}"
|
82
|
+
end
|
83
|
+
|
84
|
+
release_lock_on_stage do
|
85
|
+
run_locally do
|
86
|
+
execute cmd
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def cleanup_stage_dir
|
93
|
+
run_locally do
|
94
|
+
archives_dir = File.join(fetch(:s3_archive_local_download_dir), fetch(:stage).to_s)
|
95
|
+
archives = capture(:ls, '-xtr', archives_dir).split
|
96
|
+
if archives.count >= fetch(:keep_releases)
|
97
|
+
to_be_removes = (archives - archives.last(fetch(:keep_releases)))
|
98
|
+
if to_be_removes.any?
|
99
|
+
to_be_removes_str = to_be_removes.map do |file|
|
100
|
+
File.join(archives_dir, file)
|
101
|
+
end.join(' ')
|
102
|
+
execute :rm, to_be_removes_str
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def transfer_sources(dest)
|
109
|
+
fail "#{__method__} must be called in run_locally" unless backend.is_a?(SSHKit::Backend::Local)
|
110
|
+
|
111
|
+
rsync = ['rsync']
|
112
|
+
rsync.concat fetch(:s3_archive_remote_rsync_options, [])
|
113
|
+
rsync << (fetch(:s3_archive_local_cache_dir) + '/')
|
114
|
+
|
115
|
+
if dest.local?
|
116
|
+
rsync << ('--no-compress')
|
117
|
+
rsync << rsync_cache_dir
|
118
|
+
else
|
119
|
+
rsync << "-e 'ssh #{dest.ssh_key_option} #{fetch(:s3_archive_remote_rsync_ssh_options).join(' ')}'"
|
120
|
+
rsync << "#{dest.login_user_at}#{dest.hostname}:#{rsync_cache_dir}"
|
121
|
+
end
|
122
|
+
|
123
|
+
release_lock_on_create do
|
124
|
+
backend.execute(*rsync)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def release
|
129
|
+
link_option = if fetch(:s3_archive_hardlink_release) && backend.test("[ `readlink #{current_path}` != #{release_path} ]")
|
130
|
+
"--link-dest `readlink #{current_path}`"
|
131
|
+
end
|
132
|
+
create_release = %[#{fetch(:s3_archive_rsync_copy)} #{link_option} "#{rsync_cache_dir}/" "#{release_path}/"]
|
133
|
+
backend.execute create_release
|
134
|
+
end
|
135
|
+
|
136
|
+
def current_revision
|
137
|
+
if fetch(:s3_archive_object_version_id)
|
138
|
+
"#{archive_object_key}?versionid=#{fetch(:s3_archive_object_version_id)}"
|
139
|
+
else
|
140
|
+
archive_object_key
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def archive_object_key
|
145
|
+
@archive_object_key ||=
|
146
|
+
case fetch(:branch, :latest).to_sym
|
147
|
+
when :master, :latest
|
148
|
+
latest_object_key
|
149
|
+
else
|
150
|
+
s3params.object_prefix + fetch(:branch).to_s
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def rsync_cache_dir
|
155
|
+
File.join(deploy_to, fetch(:s3_archive_rsync_cache_dir))
|
156
|
+
end
|
157
|
+
|
158
|
+
def s3params
|
159
|
+
@s3params ||= S3Params.new(fetch(:repo_url))
|
160
|
+
end
|
161
|
+
|
162
|
+
def get_object_metadata
|
163
|
+
s3_client.list_object_versions(bucket: s3params.bucket, prefix: archive_object_key).versions.find do |v|
|
164
|
+
if fetch(:s3_archive_object_version_id) then v.version_id == fetch(:s3_archive_object_version_id)
|
165
|
+
else v.is_latest
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def list_all_objects
|
171
|
+
response = s3_client.list_objects(bucket: s3params.bucket, prefix: s3params.object_prefix)
|
172
|
+
response.inject([]) do |objects, page|
|
173
|
+
objects + page.contents
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def latest_object_key
|
178
|
+
list_all_objects.sort(&fetch(:s3_archive_sort_proc)).first.key
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
def release_lock_on_stage(&block)
|
184
|
+
release_lock((File::LOCK_EX | File::LOCK_NB), &block) # exclusive lock
|
185
|
+
end
|
186
|
+
|
187
|
+
def release_lock_on_create(&block)
|
188
|
+
release_lock(File::LOCK_SH, &block)
|
189
|
+
end
|
190
|
+
|
191
|
+
def release_lock(lock_mode, &block)
|
192
|
+
mkdir_p(File.dirname(fetch(:s3_archive_local_cache_dir)))
|
193
|
+
lockfile = "#{fetch(:s3_archive_local_cache_dir)}.#{fetch(:stage)}.release.lock"
|
194
|
+
File.open(lockfile, File::RDONLY | File::CREAT) do |file|
|
195
|
+
if file.flock(lock_mode)
|
196
|
+
block.call
|
197
|
+
else
|
198
|
+
fail ResourceBusyError, "Could not get #{lockfile}"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def stage_lock(&block)
|
204
|
+
mkdir_p(File.dirname(fetch(:s3_archive_local_cache_dir)))
|
205
|
+
lockfile = "#{fetch(:s3_archive_local_cache_dir)}.#{fetch(:stage)}.lock"
|
206
|
+
File.open(lockfile, "w") do |file|
|
207
|
+
fail ResourceBusyError, "Could not get #{lockfile}" unless file.flock(File::LOCK_EX | File::LOCK_NB)
|
208
|
+
block.call
|
209
|
+
end
|
210
|
+
ensure
|
211
|
+
rm lockfile if File.exist? lockfile
|
212
|
+
end
|
213
|
+
|
214
|
+
def s3_client
|
215
|
+
@s3_client ||= Aws::S3::Client.new(fetch(:s3_archive_client_options))
|
216
|
+
end
|
217
|
+
|
218
|
+
class LocalExtractor
|
219
|
+
# class ResourceBusyError < StandardError; end
|
220
|
+
|
221
|
+
# include FileUtils
|
222
|
+
|
223
|
+
def stage
|
224
|
+
stage_lock do
|
225
|
+
archive_dir = File.join(fetch(:s3_archive_local_download_dir), fetch(:stage).to_s)
|
226
|
+
archive_file = File.join(archive_dir, File.basename(archive_object_key))
|
227
|
+
tmp_file = "#{archive_file}.part"
|
228
|
+
etag_file = File.join(archive_dir, ".#{File.basename(archive_object_key)}.etag")
|
229
|
+
fail "#{tmp_file} is found. Another process is running?" if File.exist?(tmp_file)
|
230
|
+
etag = get_object_metadata.tap { |it| fail "No such object: #{current_revision}" if it.nil? }.etag
|
231
|
+
|
232
|
+
|
233
|
+
if [archive_file, etag_file].all? { |f| File.exist?(f) } && File.read(etag_file) == etag
|
234
|
+
context.info "#{archive_file} (etag:#{etag}) is found. download skipped."
|
235
|
+
else
|
236
|
+
context.info "Download #{current_revision} to #{archive_file}"
|
237
|
+
mkdir_p(File.dirname(archive_file))
|
238
|
+
File.open(tmp_file, 'w') do |f|
|
239
|
+
get_object(f)
|
240
|
+
end
|
241
|
+
move(tmp_file, archive_file)
|
242
|
+
File.write(etag_file, etag)
|
243
|
+
end
|
244
|
+
|
245
|
+
remove_entry_secure(fetch(:s3_archive_local_cache_dir)) if File.exist? fetch(:s3_archive_local_cache_dir)
|
246
|
+
mkdir_p(fetch(:s3_archive_local_cache_dir))
|
247
|
+
case archive_file
|
248
|
+
when /\.zip\Z/
|
249
|
+
cmd = "unzip -q -d #{fetch(:s3_archive_local_cache_dir)} #{archive_file}"
|
250
|
+
when /\.tar\.gz\Z|\.tar\.bz2\Z|\.tgz\Z/
|
251
|
+
cmd = "tar xf #{archive_file} -C #{fetch(:s3_archive_local_cache_dir)}"
|
252
|
+
end
|
253
|
+
|
254
|
+
release_lock_on_stage do
|
255
|
+
run_locally do
|
256
|
+
execute cmd
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def stage_lock(&block)
|
263
|
+
mkdir_p(File.dirname(fetch(:s3_archive_local_cache_dir)))
|
264
|
+
lockfile = "#{fetch(:s3_archive_local_cache_dir)}.#{fetch(:stage)}.lock"
|
265
|
+
begin
|
266
|
+
File.open(lockfile, "w") do |file|
|
267
|
+
fail ResourceBusyError, "Could not get #{lockfile}" unless file.flock(File::LOCK_EX | File::LOCK_NB)
|
268
|
+
block.call
|
269
|
+
end
|
270
|
+
ensure
|
271
|
+
rm lockfile if File.exist? lockfile
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
class RemoteExtractor
|
277
|
+
end
|
278
|
+
|
279
|
+
class S3Params
|
280
|
+
attr_reader :bucket, :object_prefix
|
281
|
+
|
282
|
+
def initialize(repo_url)
|
283
|
+
uri = URI.parse(repo_url)
|
284
|
+
@bucket = uri.host
|
285
|
+
@object_prefix = uri.path.sub(/\/?\Z/, '/').slice(1..-1) # normalize path
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
class Configuration
|
292
|
+
class Server
|
293
|
+
def login_user_at
|
294
|
+
user = [user, ssh_options[:user]].compact.first
|
295
|
+
user ? "#{user}@" : ''
|
296
|
+
end
|
297
|
+
|
298
|
+
def ssh_key_option
|
299
|
+
key = [keys, ssh_options[:keys]].flatten.compact.first
|
300
|
+
key ? "-i #{key}" : ''
|
301
|
+
end
|
302
|
+
|
303
|
+
def ssh_port_option
|
304
|
+
port ? "-p #{port}" : ''
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
plugin = self
|
2
|
+
|
3
|
+
namespace :s3_archive do
|
4
|
+
desc 'Check that the S3 buckets are reachable'
|
5
|
+
task :check do
|
6
|
+
run_locally do
|
7
|
+
plugin.local_check
|
8
|
+
end
|
9
|
+
|
10
|
+
on release_roles :all do
|
11
|
+
plugin.remote_check
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
desc 'Extruct and stage the S3 archive in a stage directory'
|
16
|
+
task :stage do
|
17
|
+
if fetch(:skip_staging, false)
|
18
|
+
info "Skip extracting and staging."
|
19
|
+
next
|
20
|
+
end
|
21
|
+
|
22
|
+
run_locally do
|
23
|
+
plugin.stage
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
after :stage, :cleanup_stage_dir do
|
28
|
+
run_locally do
|
29
|
+
plugin.cleanup_stage_dir
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
desc 'Copy repo to releases'
|
34
|
+
task create_release: :stage do
|
35
|
+
on release_roles(:all), fetch(:s3_archive_remote_rsync_runner_options) do |server|
|
36
|
+
test "[ -e #{plugin.rsync_cache_dir} ]" # implicit initialize for 'server'
|
37
|
+
run_locally do
|
38
|
+
plugin.transfer_sources(server)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
on release_roles(:all) do
|
43
|
+
execute :mkdir, '-p', release_path
|
44
|
+
plugin.release
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
desc 'Determine the revision that will be deployed'
|
49
|
+
task :set_current_revision do
|
50
|
+
set :current_revision, plugin.current_revision
|
51
|
+
end
|
52
|
+
end unless Rake::Task.task_defined?("s3_archive:check")
|
53
|
+
|
54
|
+
task :deploy_only do
|
55
|
+
set :skip_staging, true
|
56
|
+
invoke :deploy
|
57
|
+
end
|
File without changes
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: capistrano-s3_archive
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Takuto Komazaki
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-07-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: capistrano
|
@@ -16,16 +16,16 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 3.
|
19
|
+
version: '3.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 3.
|
26
|
+
version: '3.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name: aws-sdk
|
28
|
+
name: aws-sdk
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
@@ -42,30 +42,44 @@ dependencies:
|
|
42
42
|
name: bundler
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rake
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
67
81
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
82
|
+
version: '0'
|
69
83
|
description: Capistrano deployment from an archive on Amazon S3.
|
70
84
|
email:
|
71
85
|
- komazarari@gmail.com
|
@@ -82,9 +96,14 @@ files:
|
|
82
96
|
- bin/console
|
83
97
|
- bin/setup
|
84
98
|
- capistrano-s3_archive.gemspec
|
99
|
+
- img/s3_archive-rsync.png
|
100
|
+
- legacy_README.md
|
101
|
+
- lib/capistrano/legacy_s3_archive.rb
|
85
102
|
- lib/capistrano/s3_archive.rb
|
86
103
|
- lib/capistrano/s3_archive/version.rb
|
87
|
-
- lib/capistrano/
|
104
|
+
- lib/capistrano/scm/s3_archive.rb
|
105
|
+
- lib/capistrano/scm/tasks/s3_archive.rake
|
106
|
+
- lib/capistrano/tasks/legacy_s3_archive.rake
|
88
107
|
- vagrant_example/.insecure_private_key
|
89
108
|
- vagrant_example/Capfile
|
90
109
|
- vagrant_example/Gemfile
|
@@ -109,7 +128,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
128
|
version: '0'
|
110
129
|
requirements: []
|
111
130
|
rubyforge_project:
|
112
|
-
rubygems_version: 2.6.
|
131
|
+
rubygems_version: 2.6.11
|
113
132
|
signing_key:
|
114
133
|
specification_version: 4
|
115
134
|
summary: Capistrano deployment from an archive on Amazon S3.
|