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

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.
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