dpl 2.0.0.alpha.2 → 2.0.0.alpha.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -1
  3. data/Gemfile.lock +13 -8
  4. data/NOTES.md +1 -74
  5. data/README.md +464 -193
  6. data/lib/dpl/assets/convox/install +11 -0
  7. data/lib/dpl/assets/dpl/README.erb.md +4 -0
  8. data/lib/dpl/cli.rb +54 -18
  9. data/lib/dpl/ctx/test.rb +7 -3
  10. data/lib/dpl/helper/env.rb +67 -18
  11. data/lib/dpl/helper/wrap.rb +9 -0
  12. data/lib/dpl/provider.rb +11 -9
  13. data/lib/dpl/provider/dsl.rb +3 -1
  14. data/lib/dpl/provider/status.rb +6 -6
  15. data/lib/dpl/providers.rb +3 -1
  16. data/lib/dpl/providers/anynines.rb +5 -3
  17. data/lib/dpl/providers/azure_web_apps.rb +1 -1
  18. data/lib/dpl/providers/bintray.rb +2 -0
  19. data/lib/dpl/providers/bluemixcloudfoundry.rb +5 -3
  20. data/lib/dpl/providers/boxfuse.rb +1 -1
  21. data/lib/dpl/providers/cargo.rb +10 -1
  22. data/lib/dpl/providers/chef_supermarket.rb +3 -1
  23. data/lib/dpl/providers/cloud66.rb +2 -0
  24. data/lib/dpl/providers/cloudfiles.rb +2 -0
  25. data/lib/dpl/providers/cloudformation.rb +278 -0
  26. data/lib/dpl/providers/cloudfoundry.rb +6 -4
  27. data/lib/dpl/providers/codedeploy.rb +5 -5
  28. data/lib/dpl/providers/convox.rb +121 -0
  29. data/lib/dpl/providers/datica.rb +1 -1
  30. data/lib/dpl/providers/engineyard.rb +2 -0
  31. data/lib/dpl/providers/gae.rb +6 -7
  32. data/lib/dpl/providers/gcs.rb +5 -3
  33. data/lib/dpl/providers/gleis.rb +70 -0
  34. data/lib/dpl/providers/hackage.rb +2 -0
  35. data/lib/dpl/providers/hephy.rb +3 -1
  36. data/lib/dpl/providers/heroku.rb +4 -8
  37. data/lib/dpl/providers/heroku/api.rb +4 -2
  38. data/lib/dpl/providers/heroku/git.rb +3 -1
  39. data/lib/dpl/providers/lambda.rb +4 -4
  40. data/lib/dpl/providers/launchpad.rb +3 -1
  41. data/lib/dpl/providers/netlify.rb +2 -0
  42. data/lib/dpl/providers/npm.rb +2 -0
  43. data/lib/dpl/providers/openshift.rb +2 -0
  44. data/lib/dpl/providers/opsworks.rb +1 -1
  45. data/lib/dpl/providers/packagecloud.rb +2 -0
  46. data/lib/dpl/providers/pages.rb +4 -7
  47. data/lib/dpl/providers/pages/api.rb +16 -12
  48. data/lib/dpl/providers/pages/git.rb +16 -12
  49. data/lib/dpl/providers/puppetforge.rb +2 -0
  50. data/lib/dpl/providers/pypi.rb +2 -0
  51. data/lib/dpl/providers/releases.rb +8 -6
  52. data/lib/dpl/providers/rubygems.rb +3 -1
  53. data/lib/dpl/providers/s3.rb +7 -7
  54. data/lib/dpl/providers/scalingo.rb +2 -0
  55. data/lib/dpl/providers/testfairy.rb +2 -0
  56. data/lib/dpl/providers/transifex.rb +2 -0
  57. data/lib/dpl/version.rb +1 -1
  58. metadata +7 -3
  59. data/lib/dpl/providers/atlas.rb +0 -49
@@ -22,10 +22,12 @@ module Dpl
22
22
  gem 'net-telnet', '~> 0.1.0' if ruby_pre?('2.3')
23
23
  gem 'rack'
24
24
 
25
+ env :chef
26
+
25
27
  opt '--user_id ID', 'Chef Supermarket user name', required: true
26
- opt '--client_key KEY', 'Client API key file name', required: true
27
28
  opt '--name NAME', 'Cookbook name', note: 'defaults to the name given in metadata.json or metadata.rb', alias: :cookbook_name, deprecated: :cookbook_name
28
29
  opt '--category CAT', 'Cookbook category in Supermarket', required: true, see: 'https://docs.getchef.com/knife_cookbook_site.html#id12', alias: :cookbook_category, deprecated: :cookbook_category
30
+ opt '--client_key KEY', 'Client API key file name', default: 'client.pem'
29
31
  opt '--dir DIR', 'Directory containing the cookbook', default: '.'
30
32
 
31
33
  URL = "https://supermarket.chef.io/api/v1/cookbooks"
@@ -7,6 +7,8 @@ module Dpl
7
7
  tbd
8
8
  str
9
9
 
10
+ env :cloud66
11
+
10
12
  opt '--redeployment_hook URL', 'The redeployment hook URL', required: true, secret: true
11
13
 
12
14
  msgs failed: 'Redeployment failed (%s)'
@@ -13,6 +13,8 @@ module Dpl
13
13
  gem 'fog-core', '= 2.1.0', require: 'fog/core'
14
14
  gem 'fog-rackspace', '~> 0.1.6', require: 'fog/rackspace'
15
15
 
16
+ env :cloudfiles
17
+
16
18
  opt '--username USER', 'Rackspace username', required: true
17
19
  opt '--api_key KEY', 'Rackspace API key', required: true, secret: true
18
20
  opt '--region REGION', 'Cloudfiles region', required: true, enum: %w(ord dfw syd iad hkg)
@@ -0,0 +1,278 @@
1
+ module Dpl
2
+ module Providers
3
+ class Cloudformation < Provider
4
+ status :dev
5
+
6
+ full_name 'AWS CloudFormation'
7
+
8
+ description sq(<<-str)
9
+ tbd
10
+ str
11
+
12
+ gem 'aws-sdk-cloudformation', '~> 1.0'
13
+
14
+ env :aws, :cloudformation
15
+ config '~/.aws/credentials', prefix: 'aws'
16
+
17
+ opt '--access_key_id ID', 'AWS Access Key ID', required: true, secret: true
18
+ opt '--secret_access_key KEY', 'AWS Secret Key', required: true, secret: true
19
+ opt '--region REGION', 'AWS Region to deploy to', default: 'us-east-1'
20
+ opt '--template STR', 'CloudFormation template file', required: true, note: 'can be either a local path or an S3 URL'
21
+ opt '--stack_name NAME', 'CloudFormation Stack Name.', required: true
22
+ opt '--stack_name_prefix STR', 'CloudFormation Stack Name Prefix.'
23
+ opt '--promote', 'Deploy changes', default: true, note: 'otherwise a change set is created'
24
+ opt '--role_arn ARN', 'AWS Role ARN'
25
+ opt '--sts_assume_role ARN', 'AWS Role ARN for cross account deployments (assumed by travis using given AWS credentials).'
26
+ opt '--capabilities STR', 'CloudFormation allowed capabilities', type: :array, enum: %w(CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND), sep: ',', see: 'https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStack.html'
27
+ opt '--wait', 'Wait for CloutFormation to finish the stack creation and update', default: true
28
+ opt '--wait_timeout SEC', 'How many seconds to wait for stack creation and update.', type: :integer, default: 3600
29
+ opt '--create_timeout SEC', 'How many seconds to wait before the stack status becomes CREATE_FAILED', type: :integer, default: 3600, note: 'valid only when creating a stack'
30
+ # if passing a session_token is not recommended in CI/CD why do we add it to dpl?
31
+ opt '--session_token STR', 'AWS Session Access Token if using STS assume role', note: 'Not recommended on CI/CD'
32
+ opt '--parameters STR', 'key=value pairs or ENV var names', type: :array, sep: ',', eg: 'one=1 or ENV_VAR_TWO'
33
+ opt '--output_file PATH', 'Path to output file to store CloudFormation outputs to'
34
+
35
+ msgs login: 'Using Access Key: %{access_key_id}',
36
+ create_stack: 'Creating stack ...',
37
+ promote_stack: 'Promoting stack ...',
38
+ create_change_set: 'Creating change set ...',
39
+ stack_up_to_date: 'Stack already up to date.',
40
+ delete_change_set: 'No changes in stack. Removing changeset.',
41
+ done: 'Done.',
42
+ missing_template: 'File does not exist: %{template}',
43
+ invalid_creds: 'Invalid credentials'
44
+
45
+ strs change_set_name: 'travis-ci-build-%{build_number}-%{now}',
46
+ change_set_desc: 'Changeset created by Travis CI job for build #%{build_number} (%{git_sha})'
47
+
48
+ def login
49
+ info :login
50
+ end
51
+
52
+ def deploy
53
+ stack_exists? ? update : create
54
+ store_events if output_file?
55
+ rescue Aws::CloudFormation::Errors::InvalidAccessKeyId
56
+ error :invalid_creds
57
+ end
58
+
59
+ private
60
+
61
+ def update
62
+ promote? ? promote : create_change_set(:update)
63
+ rescue Aws::CloudFormation::Errors::ValidationError => e
64
+ raise e unless e.message.start_with?('No updates are to be performed')
65
+ info :stack_up_to_date
66
+ end
67
+
68
+ def promote
69
+ info :promote_stack
70
+ client.update_stack(common_params)
71
+ stream_events(stack_name, :stack_update_complete) if wait?
72
+ info :done
73
+ end
74
+
75
+ def create
76
+ promote? ? create_stack : create_change_set(:create)
77
+ end
78
+
79
+ def create_stack
80
+ info :create_stack
81
+ params = { timeout_in_minutes: create_timeout, on_failure: 'ROLLBACK' }
82
+ client.create_stack(common_params.merge(params))
83
+ stream_events(stack_name, :stack_create_complete) if wait?
84
+ info :done
85
+ end
86
+
87
+ def create_change_set(type)
88
+ info :create_change_set
89
+ set = client.create_change_set(common_params.merge(change_set_params(type)))
90
+ wait_for(:change_set_create_complete, change_set_name: set.id) if wait? && !test?
91
+ info :done
92
+ rescue Aws::Waiters::Errors::FailureStateError => e
93
+ raise e unless change_set_contains_changes?(set)
94
+ info :delete_change_set
95
+ client.delete_change_set(change_set_name: set.id)
96
+ end
97
+
98
+ def change_set_params(type)
99
+ {
100
+ change_set_type: type.to_s.upcase,
101
+ change_set_name: interpolate(str(:change_set_name)),
102
+ description: interpolate(str(:change_set_desc))
103
+ }
104
+ end
105
+
106
+ def change_set_contains_changes?(change_set)
107
+ data = client.describe_change_set(change_set_name: change_set.id)
108
+ data.status_reason.start_with?(%(The submitted information didn't contain changes))
109
+ end
110
+
111
+ def stack_exists?
112
+ stack = last_stack
113
+ stack && stack.stack_status != 'REVIEW_IN_PROGRESS'
114
+ rescue Aws::CloudFormation::Errors::ValidationError => e
115
+ raise e unless e.message.include?('does not exist')
116
+ false
117
+ end
118
+
119
+ def stream_events(stack_name, condition)
120
+ stream = EventStream.new(client, stack_name, method(:info))
121
+ wait_for(condition, stack_name: stack_name) unless test? # hmm.
122
+ ensure
123
+ stream.stop unless stream.nil?
124
+ end
125
+
126
+ def wait_for(cond, params)
127
+ started_at = Time.now
128
+ timeout = lambda { |*| throw :failure if Time.now - started_at > wait_timeout }
129
+ # params = params.merge(max_attempts: nil, delay: 5, before_wait: timeout)
130
+ client.wait_until(cond, params) { |w| w.before_wait(&timeout) }
131
+ end
132
+
133
+ def store_events
134
+ logs = last_stack.outputs || {}
135
+ logs = logs.map { |log| "#{log[:output_key]}=#{log[:output_value]}" }
136
+ File.write(output_file, logs.join("\n"))
137
+ end
138
+
139
+ def last_stack
140
+ client.describe_stacks(stack_name: stack_name)[:stacks].first
141
+ end
142
+
143
+ def common_params
144
+ params = {
145
+ stack_name: stack_name,
146
+ role_arn: role_arn,
147
+ capabilities: capabilities,
148
+ parameters: parameters
149
+ }
150
+ params.merge!(template_param)
151
+ @common_params ||= compact(params)
152
+ end
153
+
154
+ def parameters
155
+ @parameters ||= Array(super).map do |str|
156
+ key, value = str.split('=', 2)
157
+ { parameter_key: key, parameter_value: value || ENV[key] }
158
+ end
159
+ end
160
+
161
+ def create_timeout
162
+ super / 60
163
+ end
164
+
165
+ def stack_name
166
+ @stack_name ||= "#{stack_name_prefix}#{super}"
167
+ end
168
+
169
+ def template_param
170
+ str = template
171
+ return { template_url: str } if url?(str)
172
+ return { template_body: read(str) } if file?(str)
173
+ error(:missing_template)
174
+ end
175
+
176
+ def client
177
+ @client ||= Aws::CloudFormation::Client.new(client_options)
178
+ end
179
+
180
+ def client_options
181
+ params = { region: region, credentials: credentials }
182
+ params = params.merge(credentials: assume_role(params)) if sts_assume_role?
183
+ params
184
+ end
185
+
186
+ def credentials
187
+ Aws::Credentials.new(access_key_id, secret_access_key, session_token)
188
+ end
189
+
190
+ def assume_role(params)
191
+ assumed_role = Aws::STS::Client.new(params).assume_role(
192
+ role_arn: sts_assume_role,
193
+ role_session_name: "travis-build-#{build_number}"
194
+ )
195
+ Aws::Credentials.new(
196
+ assumed_role.credentials.access_key_id,
197
+ assumed_role.credentials.secret_access_key,
198
+ assumed_role.credentials.session_token
199
+ )
200
+ end
201
+
202
+ def now
203
+ Time.now.strftime('%Y-%m-%dT%H:%M:%S')
204
+ end
205
+
206
+ def url?(str)
207
+ str =~ %r(^https?://)
208
+ end
209
+
210
+ class EventStream < Struct.new(:client, :stack_name, :handler)
211
+ attr_reader :thread
212
+
213
+ def initialize(*)
214
+ super
215
+ @event = describe_stack_events.stack_events.first
216
+ @thread = Thread.new(&method(:process))
217
+ end
218
+
219
+ def stop
220
+ mutex.synchronize { @stop = true }
221
+ thread.join
222
+ end
223
+
224
+ private
225
+
226
+ def process
227
+ until mutex.synchronize { @stop }
228
+ @event, events = events_since(@event)
229
+ events.each { |e| handler.call(format_event(e)) }
230
+ sleep 5 unless ENV['ENV'] == 'test'
231
+ end
232
+ end
233
+
234
+ # source: https://github.com/rvedotrc/cfn-events/blob/master/lib/cfn-events/runner.rb
235
+ def events_since(event)
236
+ described_stack = describe_stack_events
237
+ stack_events = described_stack.stack_events
238
+ return [event, []] if stack_events.first.event_id == event.event_id
239
+
240
+ events = []
241
+ described_stack.each_page do |page|
242
+
243
+
244
+ if (oldest_new = page.stack_events.index { |e| e.event_id == event.event_id })
245
+ events.concat(page.stack_events[0..oldest_new - 1])
246
+ return [events.first, events.reverse]
247
+ end
248
+ events.concat(page.stack_events)
249
+ end
250
+
251
+ warn %(Last-seen stack event is no longer returned by AWS. Please raise this as a provider's bug.)
252
+ [events.first, events.reverse]
253
+ end
254
+
255
+ def describe_stack_events
256
+ client.describe_stack_events(stack_name: stack_name)
257
+ end
258
+
259
+ def mutex
260
+ @mutex ||= Mutex.new
261
+ end
262
+
263
+ EVENT_KEYS = %i(timestamp resource_type resource_status logical_resource_id
264
+ physical_resource_id resource_status_reason)
265
+
266
+ def format_event(event)
267
+ parts = EVENT_KEYS.map { |key| event.send(key) }
268
+ parts[0] = format_timestamp(parts[0])
269
+ parts.join(' ')
270
+ end
271
+
272
+ def format_timestamp(timestamp)
273
+ timestamp.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
274
+ end
275
+ end
276
+ end
277
+ end
278
+ end
@@ -9,13 +9,15 @@ module Dpl
9
9
  tbd
10
10
  str
11
11
 
12
+ env :cloudfoundry
13
+
12
14
  opt '--username USER', 'Cloud Foundry username', required: true
13
15
  opt '--password PASS', 'Cloud Foundry password', required: true, secret: true
14
- opt '--organization ORG', 'Cloud Foundry target organization', required: true
15
- opt '--space SPACE', 'Cloud Foundry target space', required: true
16
- opt '--api URL', 'Cloud Foundry api URL', required: true
16
+ opt '--organization ORG', 'Cloud Foundry organization', required: true
17
+ opt '--space SPACE', 'Cloud Foundry space', required: true
18
+ opt '--api URL', 'Cloud Foundry api URL', default: 'https://api.run.pivotal.io'
17
19
  opt '--app_name APP', 'Application name'
18
- opt '--buildpack PACK', 'Custom buildpack name or Git URL'
20
+ opt '--buildpack PACK', 'Buildpack name or Git URL'
19
21
  opt '--manifest FILE', 'Path to the manifest'
20
22
  opt '--skip_ssl_validation', 'Skip SSL validation'
21
23
  opt '--v3', 'Use the v3 API version to push the application'
@@ -14,7 +14,7 @@ module Dpl
14
14
  gem 'aws-sdk-codedeploy', '~> 1.0'
15
15
  gem 'aws-sdk-s3', '~> 1.0'
16
16
 
17
- env :aws
17
+ env :aws, :codedeploy
18
18
  config '~/.aws/credentials', '~/.aws/config', prefix: 'aws'
19
19
 
20
20
  opt '--access_key_id ID', 'AWS access key', required: true, secret: true
@@ -28,10 +28,10 @@ module Dpl
28
28
  opt '--region REGION', 'AWS availability zone', default: 'us-east-1'
29
29
  opt '--file_exists_behavior STR', 'How to handle files that already exist in a deployment target location', enum: %w(disallow overwrite retain), default: 'disallow'
30
30
  opt '--wait_until_deployed', 'Wait until the deployment has finished'
31
- opt '--bundle_type TYPE'
32
- opt '--endpoint ENDPOINT'
33
- opt '--key KEY'
34
- opt '--description DESCR'
31
+ opt '--bundle_type TYPE', 'Bundle type of the revision'
32
+ opt '--key KEY', 'S3 bucket key of the revision'
33
+ opt '--description DESCR', 'Description of the revision'
34
+ opt '--endpoint ENDPOINT', 'S3 endpoint url'
35
35
 
36
36
  msgs login: 'Using Access Key: %{access_key_id}',
37
37
  deploy_triggered: 'Deployment triggered: %s',
@@ -0,0 +1,121 @@
1
+ module Dpl
2
+ module Providers
3
+ class Convox < Provider
4
+ status :dev
5
+
6
+ description sq(<<-str)
7
+ tbd
8
+ str
9
+
10
+ gem 'json'
11
+
12
+ env :convox
13
+
14
+ # needs descriptions
15
+ opt '--host HOST', default: 'console.convox.com'
16
+ opt '--app APP', required: true
17
+ opt '--rack RACK', required: true
18
+ opt '--password PASS', required: true
19
+ opt '--install_url URL', default: 'https://convox.com/cli/linux/convox'
20
+ opt '--update_cli'
21
+ opt '--create'
22
+ opt '--promote', default: true
23
+ opt '--env VARS', type: :array, sep: ','
24
+ opt '--env_file FILE'
25
+ opt '--description STR'
26
+ opt '--generation NUM', type: :int, default: '2'
27
+
28
+ # if app and rack are exported to the env, do they need to be passed to these commands?
29
+ cmds login: 'convox version --rack %{rack}',
30
+ validate: 'convox apps info --rack %{rack} --app %{app}',
31
+ create: 'convox apps create %{app} --generation %{generation} --rack %{rack} --wait',
32
+ update: 'convox update',
33
+ set_env: 'convox env set %{env} --rack %{rack} --app %{app} --replace',
34
+ build: 'convox build --rack %{rack} --app %{app} --id --description %{escaped_description}',
35
+ deploy: 'convox deploy --rack %{rack} --app %{app} --wait --id --description %{escaped_description}'
36
+
37
+ msgs create: 'Application %{app} does not exist on rack %{rack}. Creating it ...',
38
+ missing: 'Application %{app} does not exist on rack %{rack}.',
39
+ env_file: 'The given env_file does not exist.',
40
+ deploy: 'Building and promoting application ...',
41
+ build: 'Building application ...'
42
+
43
+ errs login: 'Login failed.'
44
+
45
+ def install
46
+ script :install
47
+ shell :update if update_cli?
48
+ export
49
+ end
50
+
51
+ def login
52
+ shell :login
53
+ end
54
+
55
+ def validate
56
+ shell :validate, assert: false and return
57
+ error :missing unless create?
58
+ shell :create
59
+ end
60
+
61
+ def deploy
62
+ shell :set_env, echo: false unless env.empty?
63
+ shell promote ? :deploy : :build, echo: false
64
+ end
65
+
66
+ # not sure about this api. i like that there is an api for people to include
67
+ # env vars from the current build env, but maybe it would be better to expose
68
+ # FOO=$FOO? is mapping a bare env key to a key/value pair a concept in convox?
69
+ #
70
+ # def env
71
+ # env = env_file.concat(super || []) # TODO Cl should return an empty array, shouldn't it?
72
+ # env = env.map { |str| str.include?('=') ? str : "#{str}=#{ENV[str]}" }
73
+ # env.map { |str| escape(str) }.join(' ')
74
+ # end
75
+
76
+ # here's an alternative implementation that would expose FOO=$FOO:
77
+ gem 'sh_vars', '~> 1.0.2'
78
+
79
+ def env
80
+ env = env_file.concat(super || [])
81
+ env = env.map { |str| ShVars.parse(str).to_h }.inject(&:merge) || {}
82
+ env.map { |key, value| "#{key}=#{value.inspect}" }.join(' ')
83
+ end
84
+
85
+ def env_file
86
+ return [] unless env_file?
87
+ error :env_file unless file?(super)
88
+ lines = read(super).split("\n").map(&:strip)
89
+ lines.reject(&:empty?)
90
+ end
91
+
92
+ def description
93
+ description? ? super : JSON.dump(
94
+ repo_slug: repo_slug,
95
+ git_commit_sha: git_sha,
96
+ git_commit_message: git_commit_msg,
97
+ git_commit_author: git_author_name,
98
+ git_tag: git_tag,
99
+ branch: git_branch,
100
+ travis_build_id: ENV['TRAVIS_BUILD_ID'],
101
+ travis_build_number: ENV['TRAVIS_BUILD_NUMBER'],
102
+ pull_request: ENV['TRAVIS_PULL_REQUEST']
103
+ )
104
+ end
105
+
106
+ def export
107
+ env_vars.each { |key, value| ENV[key.to_s] = value.to_s }
108
+ end
109
+
110
+ def env_vars
111
+ {
112
+ CONVOX_HOST: host,
113
+ CONVOX_PASSWORD: password,
114
+ CONVOX_APP: app,
115
+ CONVOX_RACK: rack,
116
+ CONVOX_CLI: 'convox'
117
+ }
118
+ end
119
+ end
120
+ end
121
+ end