snap_deploy 0.1.3

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.
@@ -0,0 +1,98 @@
1
+ require 'timeout'
2
+ require 'json'
3
+
4
+ class SnapDeploy::Provider::AWS::OpsWorks < Clamp::Command
5
+
6
+ option '--app-id', "APP_ID", "The application ID", :required => true
7
+ option '--instance-ids', "INSTANCE_IDS", "The instance IDs to deploy to", :multivalued => true
8
+ option '--[no-]wait', :flag, 'Wait until (or not) deployed and return the deployment status.', :default => true
9
+ option '--[no-]migrate', :flag, 'If the db should be automatically migrated.', :default => true
10
+
11
+ include SnapDeploy::CLI::DefaultOptions
12
+ include SnapDeploy::Helpers
13
+
14
+ def execute
15
+ require 'aws-sdk'
16
+ Timeout::timeout(600) do
17
+ create_deployment
18
+ end
19
+ rescue ::Timeout::Error
20
+ error 'Timeout: Could not finish deployment in 10 minutes.'
21
+ end
22
+
23
+ private
24
+
25
+ def create_deployment
26
+ data = client.create_deployment(
27
+ {
28
+ stack_id: ops_works_app[:stack_id],
29
+ command: {name: 'deploy'},
30
+ comment: deploy_comment,
31
+ custom_json: custom_json.to_json
32
+ }.merge(deploy_target)
33
+ )
34
+ info "Deployment created: #{data[:deployment_id]}"
35
+ return unless wait?
36
+ print "Deploying "
37
+ deployment = wait_until_deployed(data[:deployment_id])
38
+ print "\n"
39
+ if deployment[:status] == 'successful'
40
+ info "Deployment successful."
41
+ else
42
+ error "Deployment failed."
43
+ raise "Deployment failed."
44
+ end
45
+ end
46
+
47
+ def deploy_target
48
+ target = {app_id: app_id}
49
+ target[:instance_ids] = instance_ids_list if instance_ids_list && !instance_ids_list.empty?
50
+ return target
51
+ end
52
+
53
+ def custom_json
54
+ {
55
+ deploy: {
56
+ ops_works_app[:shortname] => {
57
+ migrate: !!migrate?,
58
+ scm: {
59
+ revision: snap_commit
60
+ }
61
+ }
62
+ }
63
+ }
64
+ end
65
+
66
+ def wait_until_deployed(deployment_id)
67
+ deployment = nil
68
+ loop do
69
+ result = client.describe_deployments(deployment_ids: [deployment_id])
70
+ deployment = result[:deployments].first
71
+ break unless deployment[:status] == "running"
72
+ print "."
73
+ sleep 5
74
+ end
75
+ deployment
76
+ end
77
+
78
+ def ops_works_app
79
+ @ops_works_app ||= fetch_ops_works_app
80
+ end
81
+
82
+ def fetch_ops_works_app
83
+ data = client.describe_apps(app_ids: [app_id])
84
+ unless data[:apps] && data[:apps].count == 1
85
+ raise "App #{app_id} not found."
86
+ end
87
+ data[:apps].first
88
+ end
89
+
90
+ def client
91
+ @client ||= begin
92
+ AWS.config(access_key_id: access_key_id, secret_access_key: secret_access_key, logger: logger, log_formatter: AWS::Core::LogFormatter.colored)
93
+ info "Logging in using Access Key ending with : #{access_key_id[-4..-1]}"
94
+ AWS::OpsWorks.new.client
95
+ end
96
+ end
97
+
98
+ end
@@ -0,0 +1,111 @@
1
+ require 'timeout'
2
+ require 'json'
3
+
4
+ class SnapDeploy::Provider::AWS::S3 < Clamp::Command
5
+
6
+ option '--bucket',
7
+ 'BUCKET_NAME',
8
+ 'S3 Bucket.',
9
+ :required => true
10
+
11
+ option '--region',
12
+ "REGION",
13
+ 'EC2 Region.',
14
+ :default => 'us-east-1'
15
+
16
+ option '--endpoint',
17
+ 'ENDPOINT',
18
+ 'S3 Endpoint.',
19
+ :default => 's3.amazonaws.com'
20
+
21
+ option '--local-dir',
22
+ "LOCAL_DIR",
23
+ 'The local directory. e.g. `~/build/s3` (absolute) or `_site` (relative)',
24
+ :default => Dir.pwd
25
+
26
+ option '--remote-dir',
27
+ 'REMOTE_DIR',
28
+ 'The remote s3 directory to upload to.',
29
+ :default => '/'
30
+
31
+ option '--detect-encoding',
32
+ :flag,
33
+ 'Set HTTP header `Content-Encoding` for files compressed with `gzip` and `compress` utilities.',
34
+ :default => nil
35
+
36
+ option '--cache-control',
37
+ 'CACHE_OPTION',
38
+ 'Set HTTP header `Cache-Control` to suggest that the browser cache the file. Valid options are `no-cache`, `no-store`, `max-age=<seconds>`, `s-maxage=<seconds>`, `no-transform`, `public`, `private`.',
39
+ :default => 'no-cache'
40
+
41
+ option '--expires',
42
+ 'WHEN',
43
+ 'This sets the date and time that the cached object is no longer cacheable. The date must be in the format `YYYY-MM-DD HH:MM:SS -ZONE`',
44
+ :default => nil
45
+
46
+ option '--acl',
47
+ 'ACL',
48
+ 'Sets the access control for the uploaded objects. Valid options are `private`, `public_read`, `public_read_write`, `authenticated_read`, `bucket_owner_read`, `bucket_owner_full_control`.',
49
+ :default => 'private'
50
+
51
+ option '--include-dot-files',
52
+ :flag,
53
+ 'When set, upload files starting a `.`.'
54
+
55
+ option '--index-document',
56
+ 'DOCUMENT_NAME',
57
+ 'Set the index document of a S3 website.'
58
+
59
+ include SnapDeploy::CLI::DefaultOptions
60
+ include SnapDeploy::Helpers
61
+
62
+ def execute
63
+ require 'aws-sdk'
64
+ require 'mime-types'
65
+
66
+ glob_args = ["**/*"]
67
+ glob_args << File::FNM_DOTMATCH if include_dot_files?
68
+
69
+ Dir.chdir(local_dir) do
70
+ Dir.glob(*glob_args) do |filename|
71
+ content_type = MIME::Types.type_for(filename).first.to_s
72
+ opts = { :content_type => content_type }.merge(encoding_option_for(filename))
73
+ opts[:cache_control] = cache_control if cache_control
74
+ opts[:acl] = acl if acl
75
+ opts[:expires] = expires if expires
76
+ unless File.directory?(filename)
77
+ client.buckets[bucket].objects.create(upload_path(filename), File.read(filename), opts)
78
+ end
79
+ end
80
+ end
81
+
82
+ if index_document
83
+ client.buckets[bucket].configure_website do |cfg|
84
+ cfg.index_document_suffix = index_document
85
+ end
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def upload_path(filename)
92
+ [remote_dir, filename].compact.join("/")
93
+ end
94
+
95
+ def client
96
+ @client ||= begin
97
+ AWS.config(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region, logger: logger, log_formatter: AWS::Core::LogFormatter.colored)
98
+ info "Logging in using Access Key ending with : #{access_key_id[-4..-1]}"
99
+ AWS::S3.new(endpoint: endpoint)
100
+ end
101
+ end
102
+
103
+ def encoding_option_for(path)
104
+ if detect_encoding? && encoding_for(path)
105
+ {:content_encoding => encoding_for(path)}
106
+ else
107
+ {}
108
+ end
109
+ end
110
+
111
+ end
@@ -0,0 +1,242 @@
1
+ require 'rake'
2
+ require 'rake/file_utils_ext'
3
+
4
+ class SnapDeploy::Provider::Heroku < Clamp::Command
5
+
6
+ SnapDeploy::CLI.subcommand 'heroku', 'deploy to heroku', self
7
+
8
+ include SnapDeploy::CLI::DefaultOptions
9
+ include SnapDeploy::Helpers
10
+ include Rake::FileUtilsExt
11
+
12
+ option '--app-name',
13
+ 'APP_NAME',
14
+ 'The name of the heroku app to deploy',
15
+ :required => true
16
+
17
+ option '--region',
18
+ 'REGION',
19
+ 'The name of the region',
20
+ :default => 'us'
21
+
22
+ option '--config-var',
23
+ 'KEY=VALUE',
24
+ 'The name of the config variables',
25
+ :multivalued => true
26
+
27
+ option '--buildpack-url',
28
+ 'BUILDPACK_URL',
29
+ 'The url of the heroku buildpack' do |url|
30
+ require 'uri'
31
+ if url =~ URI::regexp(%w(http https git))
32
+ url
33
+ else
34
+ raise 'The buildpack url does not appear to be a url.'
35
+ end
36
+ end
37
+
38
+ option '--stack-name',
39
+ 'STACK_NAME',
40
+ 'The name of the heroku stack',
41
+ :default => 'cedar'
42
+
43
+ option '--[no-]db-migrate',
44
+ :flag,
45
+ 'If the db should be automatically migrated',
46
+ :default => false
47
+
48
+ def initialize(*args)
49
+ super
50
+ require 'snap_deploy/provider/heroku/api'
51
+ require 'netrc'
52
+ require 'ansi'
53
+ require 'rendezvous'
54
+ require 'tempfile'
55
+ end
56
+
57
+ def execute
58
+ check_auth
59
+ maybe_create_app
60
+ setup_configuration
61
+ git_push
62
+ maybe_db_migrate
63
+ end
64
+
65
+ private
66
+ SLEEP_INTERVAL = 0.1
67
+
68
+ def maybe_db_migrate
69
+ return unless db_migrate?
70
+
71
+ log ANSI::Code.ansi("Attempting to run `#{migrate_command}`", :cyan)
72
+ dyno = client.dyno.create(app_name, :command => %Q{#{migrate_command}; echo "Command exited with $?"}, :attach => true)
73
+
74
+ reader, writer = IO.pipe
75
+
76
+ thread = Thread.new do
77
+ Rendezvous.start(:input => StringIO.new, :output => writer, :url => dyno['attach_url'])
78
+ end
79
+
80
+ begin
81
+ exit_code = Timeout.timeout(300) do
82
+ copy_to_stdout(thread, reader, writer)
83
+ end
84
+
85
+ unless exit_code
86
+ error ANSI::Code.ansi('The remote command execution may have failed to return an exit code.', :red)
87
+ exit(-1)
88
+ end
89
+
90
+ if exit_code != 0
91
+ error ANSI::Code.ansi("The remote command exited with status #{exit_code}", :red)
92
+ exit(exit_code)
93
+ end
94
+ rescue Timeout::Error
95
+ raise Timeout::Error('There was no output generated in 300 seconds.')
96
+ end
97
+
98
+ thread.join(SLEEP_INTERVAL)
99
+ end
100
+
101
+ def copy_to_stdout(thread, reader, writer)
102
+ writer.sync = true
103
+ exit_code = nil
104
+ tempfile = Tempfile.new('heroku-console-log')
105
+
106
+ loop do
107
+ nothing_to_read = if IO.select([reader], nil, nil, SLEEP_INTERVAL)
108
+ contents = (reader.readpartial(4096) rescue nil) unless writer.closed?
109
+ copy_output(contents, tempfile)
110
+ !!contents
111
+ else
112
+ true
113
+ end
114
+
115
+ thread_dead = if thread.join(SLEEP_INTERVAL)
116
+ writer.close unless writer.closed?
117
+ true
118
+ end
119
+
120
+ break if thread_dead && nothing_to_read
121
+ end
122
+
123
+ # read everything one last time
124
+ copy_output(reader.read, tempfile)
125
+
126
+ # go back a bit in the console output to check on the exit status, aka poor man's tail(1)
127
+ tempfile.seek([-tempfile.size, -32].max, IO::SEEK_END)
128
+
129
+ if last_line = tempfile.readlines.last
130
+ if match_data = last_line.match(/Command exited with (\d+)/)
131
+ exit_code = match_data[1].to_i
132
+ end
133
+ end
134
+
135
+ exit_code
136
+ end
137
+
138
+ def copy_output(contents, tempfile)
139
+ return unless contents
140
+ tempfile.print contents
141
+ print contents
142
+ end
143
+
144
+ def migrate_command
145
+ 'rake db:migrate --trace'
146
+ end
147
+
148
+ def maybe_create_app
149
+ print ANSI::Code.ansi('Checking to see if app already exists... ', :cyan)
150
+ if app_exists?
151
+ print ANSI::Code.ansi("OK\n", :green)
152
+ else
153
+ print ANSI::Code.ansi("No\n", :yellow)
154
+ create_app
155
+ end
156
+ end
157
+
158
+ def setup_configuration
159
+ print ANSI::Code.ansi('Setting up config vars... ', :cyan)
160
+
161
+ # config_var_list returns a dup
162
+ configs = config_var_list
163
+
164
+ configs << "BUILDPACK_URL=#{buildpack_url}" if buildpack_url
165
+
166
+ if configs.empty?
167
+ print ANSI::Code.ansi("No config vars specified\n", :green)
168
+ return
169
+ end
170
+
171
+ existing_vars = client.config_var.info(app_name)
172
+
173
+ vars = configs.inject({}) do |memo, var|
174
+ key, value = var.split('=', 2)
175
+ if existing_vars[key] != value
176
+ memo[key] = value
177
+ end
178
+ memo
179
+ end
180
+
181
+ if vars.empty?
182
+ print ANSI::Code.ansi("No change required\n", :green)
183
+ else
184
+ print ANSI::Code.ansi("\nUpdating config vars #{vars.keys.join(', ')}... ", :cyan)
185
+ client.config_var.update(app_name, vars)
186
+ print ANSI::Code.ansi("OK\n", :green)
187
+ end
188
+ end
189
+
190
+ def git_push
191
+ if pull_request_number
192
+ print ANSI::Code.ansi("Pushing upstream branch #{snap_upstream_branch} from pull request #{pull_request_number} to heroku.\n", :cyan)
193
+ else
194
+ print ANSI::Code.ansi("Pushing branch #{snap_branch} to heroku.\n", :cyan)
195
+ end
196
+ cmd = "git push https://git.heroku.com/#{app_name}.git HEAD:refs/heads/master -f"
197
+ puts "$ #{ANSI::Code.ansi(cmd, :green)}"
198
+ sh(cmd) do |ok, res|
199
+ raise "Could not push to heroku remote. The exit code was #{res.exitstatus}." unless ok
200
+ end
201
+ end
202
+
203
+ def create_app
204
+ print ANSI::Code.ansi('Creating app on heroku since it does not exist... ', :cyan)
205
+ client.app.create({ name: app_name, region: region, stack: stack_name })
206
+ print ANSI::Code.ansi("Done\n", :green)
207
+ end
208
+
209
+ def app_exists?
210
+ !!client.app.info(app_name)
211
+ rescue Excon::Errors::Unauthorized, Excon::Errors::Forbidden => e
212
+ raise "You are not authorized to check if the app exists, perhaps you don't own that app?. The server returned status code #{e.response[:status]}."
213
+ rescue Excon::Errors::NotFound => ignore
214
+ false
215
+ end
216
+
217
+ def check_auth
218
+ print ANSI::Code.ansi('Checking heroku credentials... ', :cyan)
219
+ client.account.info
220
+ print ANSI::Code.ansi("OK\n", :green)
221
+ rescue Excon::Errors::HTTPStatusError => e
222
+ raise "Could not connect to heroku to check your credentials. The server returned status code #{e.response[:status]}."
223
+ end
224
+
225
+ def client
226
+ SnapDeploy::Provider::Heroku::API.connect_oauth(token)
227
+ end
228
+
229
+ def netrc
230
+ @netrc ||= Netrc.read
231
+ end
232
+
233
+ def token
234
+ @token ||= netrc['api.heroku.com'].password
235
+ end
236
+
237
+ def default_options
238
+ {
239
+ }
240
+ end
241
+
242
+ end