snap_deploy 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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