dream-ops 0.3.0 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cb363370cd9c9603a6971dcf9cc367abaec8824b
4
- data.tar.gz: 1b501140522af9ce3f984fe76ea8b498e4b44118
3
+ metadata.gz: a640e2b3d14a77a968e916ceff5699a51a75aa49
4
+ data.tar.gz: 8beff4a4bc985e83caa02d7c537a8595baf6d6e9
5
5
  SHA512:
6
- metadata.gz: b15b31e9d7eed839854be3bd5589d56f4306b42664a9ac5a18bf222583d04bdf32dcab1320d616f941a37cbb1fed4d98a06df2b21fc83940532c100a128e0412
7
- data.tar.gz: 230fc144ae650fbbe0d45cf5b8d56e29d8948786c04bd16ad316bf5b37d2a661b576c7594536dd42cba28a8bbbebcf8127903ac484c5f33743f1cc5e7c506ee3
6
+ metadata.gz: 54546e61709396f02aaab3f8f5d5653e44b1a9b2c25ff88057da04be2c3eaefba888c42084f3d3ddf8397f8be7a07265db51a77b654200ec10e8c3a257082019
7
+ data.tar.gz: c79c27aa79c56bf1ac0c5811d74283d9c3df208f9ab5bd5cab2e378a403850f57da4b93945adecb41ba9e34836d04240697f1fb6b0685170eab25c0ecbbc7837
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
+ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.4.0]
10
+
11
+ ### Changed
12
+ - **BREAKING CHANGE**: Renamed `-s` and `--stacks` options to `-T` and `--targets` respectively
13
+ - Updated to `"berkshelf" ~> 7.0`
14
+ - Updated to `"chef" ~> 13.6`
15
+ - Updated to `"thor" ~> 0.20`
16
+
17
+ ### Added
18
+ - Added `solo` deploy type
19
+ - New `init` command for initializing configuration
20
+ - Added global `ssh_key` option
21
+ - Added global `force_setup` option
22
+ - Better error handling / logging
23
+
24
+ ### Removed
25
+ - Removed `ridley` as a dependency
26
+
27
+ ## [0.3.0]
28
+
29
+ ### Fixes
30
+ - Problem with newer S3 domains
31
+
32
+ ### Added
33
+ - Travis CI integration along with sample unit test
34
+
35
+ ## [0.2.0]
36
+
37
+ ### Added
38
+ - Better error handling / logging
39
+ - Gem version badge to README
40
+ - Usage documentation
41
+
42
+ [Unreleased]: https://github.com/chris-allen/dream-ops/compare/v0.4.0...HEAD
43
+ [0.4.0]: https://github.com/chris-allen/dream-ops/compare/v0.3.0...v0.4.0
44
+ [0.3.0]: https://github.com/chris-allen/dream-ops/compare/v0.2.0...v0.3.0
45
+ [0.2.0]: https://github.com/chris-allen/dream-ops/compare/v0.1.0...v0.2.0
data/README.md CHANGED
@@ -26,12 +26,25 @@ Or install it yourself as:
26
26
 
27
27
  ```bash
28
28
  dream help
29
+ Commands:
30
+ dream deploy [TYPE] -T, --targets=one two three # Deploys to specified targets
31
+ dream help [COMMAND] # Describe available commands or one specific command
32
+ dream init [TYPE] -T, --targets=one two three # Initialize configuration on specified targets
33
+ dream version # Display version
34
+
35
+ Options:
36
+ -F, [--format=FORMAT] # Output format to use.
37
+ # Default: human
38
+ -q, [--quiet], [--no-quiet] # Silence all informational output.
39
+ -d, [--debug], [--no-debug] # Output debug information
40
+ -i, [--ssh-key=SSH_KEY] # Path to SSH key
41
+
29
42
  ```
30
43
 
31
44
  ### Deploying to OpsWorks
32
45
 
33
46
  ```bash
34
- dream deploy opsworks --stacks 08137c03-1e85-4787-b82c-cb825638cdfa
47
+ dream deploy opsworks -T 08137c03-1e85-4787-b82c-cb825638cdfa
35
48
  Stack: nodeapp
36
49
  --- Cookbook: chef-nodeapp
37
50
  --- Apps: ["nodeapp"]
@@ -42,11 +55,63 @@ Stack: nodeapp
42
55
  ...Deploying [stack="nodeapp"] [app="nodeapp"]
43
56
  ```
44
57
 
58
+ ### Deploy Using `chef-solo`
59
+
60
+ ```bash
61
+ dream deploy solo -T ubuntu@example.com -i /path/to/key.pem
62
+ Target: ip-172-31-53-232
63
+ --- Cookbook: chef-nodeapp (outdated)
64
+ ...Building cookbook [chef-nodeapp]
65
+ ...Deploying cookbook [chef-nodeapp]
66
+ ...Syncing [repo="nodeapp" target="ubuntu@example.com"]
67
+ ...Running setup role [target="ubuntu@example.com"]
68
+ ...Running deploy role [target="ubuntu@example.com"]
69
+ ```
70
+
71
+ ### Initialize `chef-solo`
72
+
73
+ ```bash
74
+ dream init solo -T ubuntu@example.com -i /path/to/key.pem
75
+ Target: ip-172-31-53-232
76
+ --- ChefDK Installed: false
77
+ --- Valid chef.json: false
78
+ --- Valid role[setup]: false
79
+ --- Valid role[deploy]: false
80
+ ...Installing ChefDK [target="ubuntu@example.com"]
81
+ ...Creating boilerplate /var/chef/chef.json [target="ubuntu@example.com"]
82
+ ...Creating boilerplate /var/chef/roles/setup.json [target="ubuntu@example.com"]
83
+ ...Creating boilerplate /var/chef/roles/deploy.json [target="ubuntu@example.com"]
84
+ ```
85
+
45
86
  ## Development
46
87
 
47
88
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
48
89
 
49
- 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`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
90
+ To install this gem onto your local machine, run `bundle exec rake install`.
91
+
92
+
93
+ ## Release
94
+ ### switch to `master` branch:
95
+ - Change package version in `version.rb` according to release changes (`major|minor|patch`).
96
+ - Update `CHANGELOG.md`:
97
+ - Rename `[Unreleased]` section to reflect new release version and release date, same format as for all previous releases
98
+ - Create new `[Unreleased]` section on top of file, as it was previously
99
+ - On the bottom of `CHANGELOG.md` file, create comparison reference for current release changes:
100
+ ```
101
+ # was
102
+ [Unreleased]: https://github.com/chris-allen/dream-ops/compare/v0.3.0...HEAD
103
+ [0.3.0]: https://github.com/chris-allen/dream-ops/compare/v0.2.0...v0.3.0
104
+
105
+ # became
106
+ # - "Unreleased" renamed to commit version
107
+ # - new "Unreleased" created, comparing last "0.4.0" commit with "HEAD"
108
+ [Unreleased]: https://github.com/chris-allen/dream-ops/compare/v0.4.0...HEAD
109
+ [0.4.0]: https://github.com/chris-allen/dream-ops/compare/v0.3.0...v0.4.0
110
+ [0.3.0]: https://github.com/chris-allen/dream-ops/compare/v0.2.0...v0.3.0
111
+ ```
112
+ - Commit `CHANGELOG.md` and `version.rb` with message `:rocket: {version}` (where version is your release version)
113
+
114
+ - Then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
50
115
 
51
116
  ## Contributing
52
117
 
@@ -11,10 +11,18 @@ Gem::Specification.new do |spec|
11
11
  spec.required_ruby_version = ">= 2.3.1"
12
12
  spec.required_rubygems_version = ">= 2.0.0"
13
13
 
14
- spec.summary = "This is the summary"
15
- spec.description = "This is the description"
14
+ spec.summary = "CLI for automating the deployment of application cookbooks"
15
+ spec.description = <<-EOF
16
+ This CLI automatically rebuilds and deploys your cookbook
17
+ when changes are detected. Application deployment is
18
+ supported for OpsWorks stacks and ubuntu SSH hosts.
19
+ EOF
16
20
  spec.homepage = "https://github.com/chris-allen/dream-ops"
17
21
  spec.license = "MIT"
22
+ spec.metadata = {
23
+ "source_code_uri" => "https://github.com/chris-allen/dream-ops",
24
+ "changelog_uri" => "https://github.com/chris-allen/dream-ops/blob/master/CHANGELOG.md"
25
+ }
18
26
 
19
27
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
28
  f.match(%r{^(test|spec|features)/})
@@ -28,8 +36,7 @@ Gem::Specification.new do |spec|
28
36
 
29
37
  spec.add_dependency "rubyzip", "~> 1.2"
30
38
  spec.add_dependency "aws-sdk", "~> 2"
31
- spec.add_dependency "berkshelf", "~> 6.2"
32
- spec.add_dependency "ridley", "~> 5.0"
33
- spec.add_dependency "thor", "~> 0.19", "< 0.19.2"
34
- spec.add_dependency "chef", "~> 12.7"
39
+ spec.add_dependency "berkshelf", "~> 7.0"
40
+ spec.add_dependency "thor", "~> 0.20"
41
+ spec.add_dependency "chef", "~> 13.6"
35
42
  end
@@ -29,6 +29,9 @@ module DreamOps
29
29
 
30
30
  autoload :BaseDeployer, "dream-ops/deployment/base"
31
31
  autoload :OpsWorksDeployer, "dream-ops/deployment/opsworks"
32
+ autoload :SoloDeployer, "dream-ops/deployment/solo"
33
+ autoload :BaseInitializer, "dream-ops/init/base"
34
+ autoload :SoloInitializer, "dream-ops/init/solo"
32
35
 
33
36
  class << self
34
37
  include Mixin::Logging
@@ -58,11 +61,38 @@ module DreamOps
58
61
  id = name.to_s.capitalize
59
62
  @formatter = DreamOps.const_get("#{id}Formatter").new
60
63
  end
64
+
65
+ # Get path for the SSH key
66
+ #
67
+ # @return [~String]
68
+ def ssh_key
69
+ @ssh_key ||= nil
70
+ end
71
+
72
+ # Specify path to use for the SSH key
73
+ #
74
+ # @return [~String]
75
+ def set_ssh_key(key)
76
+ @ssh_key = key
77
+ end
78
+
79
+ # Get whether to always run setup
80
+ #
81
+ # @return [~boolean]
82
+ def force_setup
83
+ @force_setup ||= false
84
+ end
85
+
86
+ # Specify whether to always run setup
87
+ #
88
+ # @return [~boolean]
89
+ def set_force_setup(force)
90
+ @force_setup = force
91
+ end
61
92
  end
62
93
  end
63
94
 
64
95
  require_relative "dream-ops/cli"
65
96
  require_relative "dream-ops/logger"
66
97
 
67
- Ridley.logger = DreamOps.logger
68
- DreamOps.logger.level = Logger::WARN
98
+ DreamOps.logger.level = Logger::WARN
@@ -64,6 +64,11 @@ module DreamOps
64
64
  end
65
65
 
66
66
  DreamOps.set_format @options[:format]
67
+
68
+ if @options[:ssh_key]
69
+ DreamOps.set_ssh_key @options[:ssh_key]
70
+ end
71
+
67
72
  @options = options.dup # unfreeze frozen options Hash from Thor
68
73
  end
69
74
 
@@ -90,26 +95,37 @@ module DreamOps
90
95
  desc: "Output debug information",
91
96
  aliases: "-d",
92
97
  default: false
98
+ class_option :ssh_key,
99
+ type: :string,
100
+ desc: "Path to SSH key",
101
+ aliases: "-i",
102
+ default: ""
93
103
 
94
104
  desc "version", "Display version"
95
105
  def version
96
106
  DreamOps.formatter.version
97
107
  end
98
108
 
99
- method_option :stacks,
109
+ method_option :targets,
100
110
  type: :array,
101
- desc: "Only these stack IDs.",
102
- aliases: "-s",
111
+ desc: "Only these targets",
112
+ aliases: "-T",
103
113
  required: true
114
+ method_option :force_setup,
115
+ type: :boolean,
116
+ desc: "Always run setup",
117
+ aliases: "-f"
104
118
  desc "deploy [TYPE]", "Deploys to specified targets"
105
119
  def deploy(type)
106
120
  deployer = nil
121
+ DreamOps.set_force_setup options[:force_setup]
122
+ targets = options[:targets]
123
+ args = [*targets]
107
124
 
108
- args = []
109
125
  if type == 'opsworks'
110
126
  deployer = OpsWorksDeployer.new
111
- stack_ids = options[:stacks]
112
- args = [*stack_ids]
127
+ elsif type == 'solo'
128
+ deployer = SoloDeployer.new
113
129
  else
114
130
  DreamOps.ui.error "Deployment of type '#{type}' is not supported"
115
131
  exit(1)
@@ -119,5 +135,33 @@ module DreamOps
119
135
  deployer.deploy(*args)
120
136
  end
121
137
  end
138
+
139
+ method_option :targets,
140
+ type: :array,
141
+ desc: "Only these targets",
142
+ aliases: "-T",
143
+ required: true
144
+ method_option :dryrun,
145
+ type: :boolean,
146
+ desc: "Only print what actions will be taken",
147
+ aliases: "-x",
148
+ default: false
149
+ desc "init [TYPE]", "Initialize configuration on specified targets"
150
+ def init(type)
151
+ initializer = nil
152
+ targets = options[:targets]
153
+ args = [*targets]
154
+
155
+ if type == 'solo'
156
+ initializer = SoloInitializer.new
157
+ else
158
+ DreamOps.ui.error "Type '#{type}' is not supported"
159
+ exit(1)
160
+ end
161
+
162
+ if !initializer.nil?
163
+ initializer.init(*args, options[:dryrun])
164
+ end
165
+ end
122
166
  end
123
167
  end
@@ -1,4 +1,3 @@
1
- require "berkshelf"
2
1
  require "dream-ops/utils/zip"
3
2
  require 'fileutils'
4
3
 
@@ -31,8 +30,8 @@ module DreamOps
31
30
  # :cookbooks => [
32
31
  # {
33
32
  # :bucket => "chef-app",
34
- # :cookbook_key => "chef-app-dev.zip",
35
- # :sha_key => "chef-app-dev_SHA.txt",
33
+ # :cookbook_filename => "chef-app-dev.zip",
34
+ # :sha_filename => "chef-app-dev_SHA.txt",
36
35
  # :name => "chef-app",
37
36
  # :path => "./chef",
38
37
  # :local_sha => "7bfa19491170563f422a321c144800f4435323b1",
@@ -59,8 +58,19 @@ module DreamOps
59
58
  # a hash containing the deploy target details
60
59
  deployer_method :deploy_target
61
60
 
62
- # It may turn out that cookbook building will be different for different
63
- # deployments, but for now all deployments will build them the same way.
61
+ def __get_cookbook_paths()
62
+ # Treat any directory with a Berksfile as a cookbook
63
+ cookbooks = Dir.glob('./**/Berksfile')
64
+
65
+ return cookbooks.map { |path| path.gsub(/(.*)(\/Berksfile)(.*)/, '\1') }
66
+ end
67
+
68
+ def __bail_with_fatal_error(ex)
69
+ raise ex
70
+ Thread.exit
71
+ end
72
+
73
+ # Collect cookbook dependencies and compress based on file extension
64
74
  def build_cookbook(cookbook)
65
75
  berksfile = Berkshelf::Berksfile.from_file(File.join(cookbook[:path], "Berksfile"))
66
76
  berksfile.vendor("berks-cookbooks")
@@ -69,13 +79,18 @@ module DreamOps
69
79
  file.write("source \"https://supermarket.chef.io\"\n\n")
70
80
  file.write("cookbook \"#{cookbook[:name]}\", path: \"./#{cookbook[:name]}\"")
71
81
  }
72
- zf = ZipFileGenerator.new("berks-cookbooks", cookbook[:cookbook_key])
73
- zf.write
82
+
83
+ if cookbook[:cookbook_filename].end_with? ".zip"
84
+ zf = ZipFileGenerator.new("berks-cookbooks", cookbook[:cookbook_filename])
85
+ zf.write
86
+ elsif cookbook[:cookbook_filename].end_with? ".tar.gz"
87
+ system("tar -czvf #{cookbook[:cookbook_filename]} -C berks-cookbooks . > /dev/null 2>&1")
88
+ end
74
89
  end
75
90
 
76
91
  def cleanup_cookbooks(cookbooks)
77
92
  cookbooks.each do |cookbook|
78
- File.delete(cookbook[:cookbook_key])
93
+ File.delete(cookbook[:cookbook_filename])
79
94
  FileUtils.remove_dir("berks-cookbooks")
80
95
  end
81
96
  end
@@ -113,4 +128,4 @@ module DreamOps
113
128
  exit(1) if !deploy_success
114
129
  end
115
130
  end
116
- end
131
+ end
@@ -1,5 +1,4 @@
1
1
  require "aws-sdk"
2
- require "ridley"
3
2
  require "uri"
4
3
 
5
4
  Aws.use_bundled_cert!
@@ -18,8 +17,8 @@ module DreamOps
18
17
  # :cookbooks => [
19
18
  # {
20
19
  # :bucket => "chef-app",
21
- # :cookbook_key => "chef-app-dev.zip",
22
- # :sha_key => "chef-app-dev_SHA.txt",
20
+ # :cookbook_filename => "chef-app-dev.zip",
21
+ # :sha_filename => "chef-app-dev_SHA.txt",
23
22
  # :name => "chef-app",
24
23
  # :path => "./chef",
25
24
  # :local_sha => "7bfa19491170563f422a321c144800f4435323b1",
@@ -34,8 +33,8 @@ module DreamOps
34
33
  # ],
35
34
  # :cookbook => {
36
35
  # :bucket => "chef-app",
37
- # :cookbook_key => "chef-app-dev.zip",
38
- # :sha_key => "chef-app-dev_SHA.txt",
36
+ # :cookbook_filename => "chef-app-dev.zip",
37
+ # :sha_filename => "chef-app-dev_SHA.txt",
39
38
  # :name => "chef-app",
40
39
  # :path => "./chef",
41
40
  # :local_sha => "7bfa19491170563f422a321c144800f4435323b1",
@@ -81,7 +80,6 @@ module DreamOps
81
80
  # Retrieves stack apps and gets all information about remote/local cookbook
82
81
  def analyze_stack(stack)
83
82
  cookbook = nil
84
- cookbookName = nil
85
83
 
86
84
  DreamOps.ui.info "Stack: #{stack.name}"
87
85
  if !stack.custom_cookbooks_source.nil?
@@ -93,30 +91,40 @@ module DreamOps
93
91
  firstSlash = cookbookPath.index('/')
94
92
  cookbook = {
95
93
  bucket: cookbookPath[0..(firstSlash-1)],
96
- cookbook_key: cookbookPath[(firstSlash+1)..-1],
97
- sha_key: cookbookPath[firstSlash+1..-1].sub('.zip', '_SHA.txt')
94
+ cookbook_filename: cookbookPath[(firstSlash+1)..-1],
95
+ sha_filename: cookbookPath[firstSlash+1..-1].sub('.zip', '_SHA.txt')
98
96
  }
99
97
 
100
- # Treat any directory with a Berksfile as a cookbook
101
- cookbooks = Dir.glob('./**/Berksfile')
98
+ cookbooks = __get_cookbook_paths()
102
99
 
103
100
  # For now we only handle if we find one cookbook
104
101
  if cookbooks.length == 1
105
- metadata = Ridley::Chef::Cookbook::Metadata.from_file(cookbooks[0].sub("Berksfile", "metadata.rb"))
102
+ path = cookbooks[0]
103
+ loader = Chef::Cookbook::CookbookVersionLoader.new(path)
104
+ loader.load_cookbooks
105
+ cookbook_version = loader.cookbook_version
106
+ metadata = cookbook_version.metadata
107
+
106
108
  cookbook[:name] = metadata.name
107
- cookbook[:path] = cookbooks[0].sub('/Berksfile', '')
108
- if cookbook[:cookbook_key].include? cookbook[:name]
109
+ cookbook[:path] = path
110
+ if cookbook[:cookbook_filename].include? cookbook[:name]
109
111
  cookbook[:local_sha] = `git log --pretty=%H -1 #{cookbook[:path]}`.chomp
110
112
 
111
113
  begin
112
- obj = Aws::S3::Object.new(cookbook[:bucket], cookbook[:sha_key])
114
+ obj = Aws::S3::Object.new(cookbook[:bucket], cookbook[:sha_filename])
113
115
  cookbook[:remote_sha] = obj.get.body.string
114
116
  rescue Aws::S3::Errors::NoSuchKey
115
- cookbook[:remote_sha] = ''
117
+ cookbook[:remote_sha] = ""
116
118
  end
117
119
  else
118
- DreamOps.ui.info "Stack cookbook source is '#{cookbook[:cookbook_key]}' but found '#{cookbook[:name]}' locally"
120
+ DreamOps.ui.warn "Stack cookbook source is '#{cookbook[:cookbook_filename]}' but found '#{cookbook[:name]}' locally"
119
121
  end
122
+ elsif cookbooks.length > 1
123
+ DreamOps.ui.warn "Found more than one cookbook at paths #{cookbooks}. Skipping build."
124
+ cookbook[:name] = "???"
125
+ else
126
+ DreamOps.ui.warn "No cookbook found"
127
+ cookbook[:name] = "???"
120
128
  end
121
129
  DreamOps.ui.info "--- Cookbook: #{cookbook[:name]}"
122
130
  end
@@ -135,12 +143,12 @@ module DreamOps
135
143
  # Deploys cookbook to S3
136
144
  def deploy_cookbook(cookbook)
137
145
  begin
138
- archiveFile = File.open(cookbook[:cookbook_key])
139
- remoteCookbook = Aws::S3::Object.new(cookbook[:bucket], cookbook[:cookbook_key])
146
+ archiveFile = File.open(cookbook[:cookbook_filename])
147
+ remoteCookbook = Aws::S3::Object.new(cookbook[:bucket], cookbook[:cookbook_filename])
140
148
  response = remoteCookbook.put({ acl: "private", body: archiveFile })
141
149
  archiveFile.close
142
150
 
143
- remoteSha = Aws::S3::Object.new(cookbook[:bucket], cookbook[:sha_key])
151
+ remoteSha = Aws::S3::Object.new(cookbook[:bucket], cookbook[:sha_filename])
144
152
  response = remoteSha.put({ acl: "private", body: cookbook[:local_sha] })
145
153
  rescue => e
146
154
  DreamOps.ui.error "#{$!}"
@@ -150,8 +158,8 @@ module DreamOps
150
158
  #
151
159
  def deploy_target(target, cookbooks)
152
160
  # If this stack has a new cookbook
153
- if !target[:cookbook].nil?
154
- if __cookbook_in_array(target[:cookbook], cookbooks)
161
+ if !target[:cookbook].nil? || DreamOps.force_setup
162
+ if __cookbook_in_array(target[:cookbook], cookbooks) || DreamOps.force_setup
155
163
  # Grab a fresh copy of the cookbook on all instances in the stack
156
164
  update_custom_cookbooks(target[:stack])
157
165
 
@@ -174,12 +182,12 @@ module DreamOps
174
182
  command: { name: "update_custom_cookbooks" }
175
183
  })
176
184
  rescue Aws::OpsWorks::Errors::ValidationException
177
- bail_with_fatal_error(NoRunningInstancesError.new(stack))
185
+ __bail_with_fatal_error(NoRunningInstancesError.new(stack))
178
186
  end
179
187
 
180
188
  status = wait_for_deployment(response.deployment_id)
181
189
  if status != 'successful'
182
- bail_with_fatal_error(OpsWorksCommandFailedError.new(
190
+ __bail_with_fatal_error(OpsWorksCommandFailedError.new(
183
191
  stack, response.deployment_id, 'update_custom_cookbooks')
184
192
  )
185
193
  end
@@ -194,7 +202,7 @@ module DreamOps
194
202
 
195
203
  status = wait_for_deployment(response.deployment_id)
196
204
  if status != 'successful'
197
- bail_with_fatal_error(OpsWorksCommandFailedError.new(
205
+ __bail_with_fatal_error(OpsWorksCommandFailedError.new(
198
206
  stack, response.deployment_id, 'setup')
199
207
  )
200
208
  end
@@ -209,12 +217,12 @@ module DreamOps
209
217
  command: { name: "deploy" }
210
218
  })
211
219
  rescue Aws::OpsWorks::Errors::ValidationException
212
- bail_with_fatal_error(NoRunningInstancesError.new(stack))
220
+ __bail_with_fatal_error(NoRunningInstancesError.new(stack))
213
221
  end
214
222
 
215
223
  status = wait_for_deployment(response.deployment_id)
216
224
  if status != 'successful'
217
- bail_with_fatal_error(OpsWorksCommandFailedError.new(
225
+ __bail_with_fatal_error(OpsWorksCommandFailedError.new(
218
226
  stack, response.deployment_id, 'deploy')
219
227
  )
220
228
  end
@@ -238,15 +246,10 @@ module DreamOps
238
246
  return status
239
247
  end
240
248
 
241
- def bail_with_fatal_error(ex)
242
- raise ex
243
- Thread.exit
244
- end
245
-
246
249
  def __cookbook_in_array(cb, cookbooks)
247
250
  return cookbooks.any? {|c|
248
- c[:cookbook_key] == cb[:cookbook_key] and c[:bucket] == cb[:bucket]
251
+ c[:cookbook_filename] == cb[:cookbook_filename] and c[:bucket] == cb[:bucket]
249
252
  }
250
253
  end
251
254
  end
252
- end
255
+ end
@@ -0,0 +1,200 @@
1
+ require 'securerandom'
2
+
3
+ module DreamOps
4
+ class SoloDeployer < BaseDeployer
5
+ def __cookbook_in_array(cb, cookbooks)
6
+ return cookbooks.any? {|c| c[:name] == cb[:name]}
7
+ end
8
+
9
+ def __cookbook_was_updated(target, cookbooks)
10
+ return cookbooks.any? {|c| c[:targets].include? target }
11
+ end
12
+
13
+ # Analyze the SSH hosts for deployment
14
+ #
15
+ # @return [Hash]
16
+ # a hash containing cookbooks that need to be built/updated
17
+ # and deploy targets
18
+ #
19
+ # Example:
20
+ # {
21
+ # :cookbooks => [
22
+ # {
23
+ # :bucket => "chef-app",
24
+ # :cookbook_filename => "chef-app-dev.tar.gz",
25
+ # :sha_filename => "chef-app-dev_SHA.txt",
26
+ # :name => "chef-app",
27
+ # :path => "./chef",
28
+ # :local_sha => "7bfa19491170563f422a321c144800f4435323b1",
29
+ # :targets => [ "ubuntu@example.com" ]
30
+ # }
31
+ # ],
32
+ # deploy_targets: [
33
+ # {
34
+ # :host => "ubuntu@example.com"
35
+ # :remote_sha => ""
36
+ # }
37
+ # ]
38
+ # }
39
+ def analyze(targets)
40
+ @ssh_opts = "-i #{DreamOps.ssh_key} -o 'StrictHostKeyChecking no'"
41
+ @q_all = "> /dev/null 2>&1"
42
+ @q_stdout = "> /dev/null"
43
+
44
+ # Collect and print target info
45
+ result = { cookbooks: [], deploy_targets: [] }
46
+ targets.each do |target|
47
+ target_result = analyze_target(target)
48
+ # Add the target to the deploy targets
49
+ result[:deploy_targets] << target_result[:deploy_target]
50
+ # Determine whether the cookbook needs to be built
51
+ cbook = target_result[:cookbook]
52
+ if !cbook.nil? && !cbook[:local_sha].nil?
53
+ # We only build the cookbook if we don't have this version remotely
54
+ if target_result[:deploy_target][:remote_sha] != cbook[:local_sha]
55
+ # Don't build the same destination cookbook more than once
56
+ if !__cookbook_in_array(cbook, result[:cookbooks])
57
+ cbook[:targets] = [target]
58
+ result[:cookbooks] << cbook
59
+ else
60
+ # Add target to be deployed to
61
+ cbook_index = result[:cookbooks].index { |c| c[:name] == cbook[:name] }
62
+ result[:cookbooks][cbook_index][:targets] << target
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ return result
69
+ end
70
+
71
+ def analyze_target(target)
72
+ # Validate SSH creds
73
+ if DreamOps.ssh_key.empty?
74
+ __bail_with_fatal_error(NoSshKeyError.new())
75
+ end
76
+ hostname = `ssh #{@ssh_opts} #{target} sudo hostname 2>&1`.chomp
77
+ if ! $?.success?
78
+ __bail_with_fatal_error(InvalidSshKeyError.new(target, hostname))
79
+ end
80
+
81
+ DreamOps.ui.info "Target: #{hostname}"
82
+
83
+ result = { host: target }
84
+ cookbook = { }
85
+ cookbooks = __get_cookbook_paths()
86
+
87
+ # For now we only handle if we find one cookbook
88
+ if cookbooks.length == 1
89
+ path = cookbooks[0]
90
+ loader = Chef::Cookbook::CookbookVersionLoader.new(path)
91
+ loader.load_cookbooks
92
+ cookbook_version = loader.cookbook_version
93
+ metadata = cookbook_version.metadata
94
+
95
+ cookbook[:sha_filename] = "#{metadata.name}_SHA.txt"
96
+ cookbook[:cookbook_filename] = "#{metadata.name}.tar.gz"
97
+ cookbook[:name] = metadata.name
98
+ cookbook[:path] = path
99
+ cookbook[:local_sha] = `git log --pretty=%H -1 #{cookbook[:path]}`.chomp
100
+
101
+ `ssh #{@ssh_opts} #{target} sudo mkdir -p /var/chef/cookbooks`
102
+
103
+ if system("ssh #{@ssh_opts} #{target} stat /var/chef/#{cookbook[:sha_filename]} #{@q_all}")
104
+ result[:remote_sha] = `ssh #{@ssh_opts} #{target} cat /var/chef/#{cookbook[:sha_filename]}`.chomp
105
+ else
106
+ result[:remote_sha] = ""
107
+ end
108
+ elsif cookbooks.length > 1
109
+ DreamOps.ui.warn "Found more than one cookbook at paths #{cookbooks}. Skipping build."
110
+ cookbook[:name] = "???"
111
+ else
112
+ DreamOps.ui.warn "No cookbook found"
113
+ cookbook[:name] = "???"
114
+ end
115
+
116
+ suffix = if cookbook[:local_sha] != result[:remote_sha] then "(outdated)" else "(up to date)" end
117
+ DreamOps.ui.info "--- Cookbook: #{cookbook[:name]} #{suffix}"
118
+
119
+ return { deploy_target: result, cookbook: cookbook }
120
+ end
121
+
122
+ # Deploys cookbook to server
123
+ def deploy_cookbook(cookbook)
124
+ cookbook[:targets].each do |target|
125
+ if system("scp #{@ssh_opts} #{cookbook[:cookbook_filename]} #{target}:/tmp #{@q_all}")
126
+ `ssh #{@ssh_opts} #{target} sudo rm -rf /var/chef/cookbooks/* #{@q_all}`
127
+ `ssh #{@ssh_opts} #{target} sudo tar -xzvf /tmp/#{cookbook[:cookbook_filename]} -C /var/chef/cookbooks #{@q_all}`
128
+ `ssh #{@ssh_opts} #{target} sudo rm -rf /tmp/#{cookbook[:cookbook_filename]} #{@q_all}`
129
+ else
130
+ DreamOps.ui.error "Failed to copy cookbook to host '#{target}'"
131
+ end
132
+
133
+ if !system(
134
+ "ssh #{@ssh_opts} #{target} "+
135
+ "\"echo '#{cookbook[:local_sha]}' | sudo tee /var/chef/#{cookbook[:sha_filename]}\" #{@q_all}"
136
+ )
137
+ DreamOps.ui.error "Failed to update remote cookbook sha"
138
+ end
139
+ end
140
+ end
141
+
142
+ def run_chef_role(target, role)
143
+ if !system("ssh #{@ssh_opts} #{target[:host]} stat /var/log/chef #{@q_all}")
144
+ `ssh #{@ssh_opts} #{target[:host]} sudo mkdir -p /var/log/chef`
145
+ end
146
+
147
+ uuid = SecureRandom.uuid
148
+
149
+ if ! system(
150
+ "ssh #{@ssh_opts} #{target[:host]} \"" +
151
+ "set -o pipefail && " +
152
+ "sudo chef-solo -j /var/chef/chef.json -o \"role[#{role}]\" 2>&1 | sudo tee /var/log/chef/#{uuid}.log #{@q_all}\""
153
+ )
154
+ __bail_with_fatal_error(ChefSoloFailedError.new(target[:host], "/var/log/chef/#{uuid}.log"))
155
+ end
156
+ end
157
+
158
+ # Deploy latest code
159
+ def deploy_target(target, cookbooks)
160
+ # Sync code to home directory
161
+ repo_path = `git rev-parse --show-toplevel`.chomp
162
+ repo_name = `basename #{repo_path}`.chomp
163
+ DreamOps.ui.info "...Syncing [repo=\"#{repo_name}\" target=\"#{target[:host]}\"]"
164
+ `rsync -r -e "ssh #{@ssh_opts}" . #{target[:host]}:~/#{repo_name}`
165
+
166
+
167
+ # Bail if ChefDK is not installed
168
+ if !system("ssh #{@ssh_opts} #{target[:host]} which chef #{@q_stdout}")
169
+ __bail_with_fatal_error(ChefDKNotInstalledError.new(target[:host]))
170
+ end
171
+
172
+ # Bail if chef.json doesn't exist
173
+ if !system("ssh #{@ssh_opts} #{target[:host]} stat /var/chef/chef.json #{@q_all}")
174
+ __bail_with_fatal_error(ChefJsonNotFoundError.new(target[:host]))
175
+ end
176
+
177
+ # Bail if setup.json doesn't exist
178
+ if !system("ssh #{@ssh_opts} #{target[:host]} stat /var/chef/roles/setup.json #{@q_all}")
179
+ __bail_with_fatal_error(RoleNotFoundError.new(target[:host], "setup"))
180
+ end
181
+
182
+ # Bail if deploy.json doesn't exist
183
+ if !system("ssh #{@ssh_opts} #{target[:host]} stat /var/chef/roles/deploy.json #{@q_all}")
184
+ __bail_with_fatal_error(RoleNotFoundError.new(target[:host], "deploy"))
185
+ end
186
+
187
+ # If this stack has a new cookbook, re-run the setup role
188
+ if (
189
+ __cookbook_was_updated(target[:host], cookbooks) ||
190
+ DreamOps.force_setup
191
+ )
192
+ DreamOps.ui.info "...Running setup role [target=\"#{target[:host]}\"]"
193
+ run_chef_role(target, "setup")
194
+ end
195
+
196
+ DreamOps.ui.info "...Running deploy role [target=\"#{target[:host]}\"]"
197
+ run_chef_role(target, "deploy")
198
+ end
199
+ end
200
+ end
@@ -39,7 +39,113 @@ module DreamOps
39
39
  "https://console.aws.amazon.com/opsworks/home?region=#{@stack.region}#/stack/#{@stack.stack_id}/deployments/#{@deployment_id}",
40
40
  ].join("\n")
41
41
  end
42
+ end
42
43
 
43
- alias_method :message, :to_s
44
+ class NoSshKeyError < DreamOpsError
45
+ set_status_code(12)
46
+
47
+ def to_s
48
+ "You mush specify a SSH key for authentication."
49
+ end
50
+ end
51
+
52
+ class InvalidSshKeyError < DreamOpsError
53
+ set_status_code(13)
54
+
55
+ def initialize(target, output)
56
+ @target = target
57
+ @output = output
58
+ end
59
+
60
+ def to_s
61
+ [
62
+ "Failed to communicate with '#{@target}' over ssh:",
63
+ "",
64
+ @output,
65
+ ].join("\n")
66
+ end
67
+ end
68
+
69
+ class ChefDKNotInstalledError < DreamOpsError
70
+ set_status_code(14)
71
+
72
+ def initialize(target)
73
+ @target = target
74
+ end
75
+
76
+ def to_s
77
+ [
78
+ "ChefDK not installed on target \"#{@target}\". To initialize chef-solo, run:",
79
+ "",
80
+ "dream init solo -t #{@target} -i #{DreamOps.ssh_key}",
81
+ ].join("\n")
82
+ end
83
+ end
84
+
85
+ class ChefDKFailedError < DreamOpsError
86
+ set_status_code(15)
87
+
88
+ def initialize(target, wget_url)
89
+ @target = target
90
+ @wget_url = wget_url
91
+ end
92
+
93
+ def to_s
94
+ [
95
+ "Target \"#{@target}\" failed installing ChefDK from:",
96
+ "",
97
+ @wget_url,
98
+ ].join("\n")
99
+ end
100
+ end
101
+
102
+ class ChefSoloFailedError < DreamOpsError
103
+ set_status_code(16)
104
+
105
+ def initialize(target, log_path)
106
+ @target = target
107
+ @log_path = log_path
108
+ end
109
+
110
+ def to_s
111
+ [
112
+ "Target \"#{@target}\" failed running chef-solo. The failure log is located at:",
113
+ "",
114
+ @log_path,
115
+ ].join("\n")
116
+ end
117
+ end
118
+
119
+ class ChefJsonNotFoundError < DreamOpsError
120
+ set_status_code(17)
121
+
122
+ def initialize(target)
123
+ @target = target
124
+ end
125
+
126
+ def to_s
127
+ [
128
+ "Could not find /var/chef/chef.json \"#{@target}\". To initialize with an empty runlist, run:",
129
+ "",
130
+ "dream init solo -t #{@target} -i #{DreamOps.ssh_key}",
131
+ ].join("\n")
132
+ end
133
+ end
134
+
135
+ class RoleNotFoundError < DreamOpsError
136
+ set_status_code(18)
137
+
138
+ def initialize(target, role)
139
+ @target = target
140
+ @role = role
141
+ end
142
+
143
+ def to_s
144
+ [
145
+ "Could not find /var/chef/roles/#{@role}.json \"#{@target}\". To initialize with an empty runlist, run:",
146
+ "",
147
+ "dream init solo -t #{@target} -i #{DreamOps.ssh_key}",
148
+ ].join("\n")
149
+ end
44
150
  end
45
- end
151
+ end
@@ -40,4 +40,4 @@ module DreamOps
40
40
  # The cleanup hook is defined by subclasses and is called by the CLI.
41
41
  def cleanup_hook; end
42
42
  end
43
- end
43
+ end
@@ -160,4 +160,4 @@ module DreamOps
160
160
  DreamOps.ui.info "DEPRECATED: #{message}"
161
161
  end
162
162
  end
163
- end
163
+ end
@@ -0,0 +1,69 @@
1
+ module DreamOps
2
+ class BaseInitializer
3
+ class << self
4
+ #
5
+ # @macro initiailizer_method
6
+ # @method $1(*args)
7
+ # Create a initiailizer method for the declaration
8
+ #
9
+ def initiailizer_method(name)
10
+ class_eval <<-EOH, __FILE__, __LINE__ + 1
11
+ def #{name}(*args)
12
+ raise AbstractFunction,
13
+ "##{name} must be implemented on \#{self.class.name}!"
14
+ end
15
+ EOH
16
+ end
17
+ end
18
+
19
+ # This method MUST be implemented by subclasses.
20
+ #
21
+ # @return [Array]
22
+ # an array of configuration for each target
23
+ #
24
+ # Example:
25
+ # [
26
+ # {
27
+ # :host => "ubuntu@example.com"
28
+ # :chefdk_installed => false,
29
+ # :solo_json_exists => false,
30
+ # :setup_role_exists => true,
31
+ # :deploy_role_exists => true
32
+ # }
33
+ # ]
34
+ initiailizer_method :analyze
35
+
36
+ # This method MUST be implemented by subclasses.
37
+ #
38
+ # @param [String] target
39
+ # a target host
40
+ initiailizer_method :init_target
41
+
42
+ def __bail_with_fatal_error(ex)
43
+ raise ex
44
+ Thread.exit
45
+ end
46
+
47
+ def init(*args, dryrun)
48
+ targets = analyze(args)
49
+
50
+ # Update cookbooks if needed and deploy to all targets
51
+ deploy_success = true
52
+ deploy_threads = []
53
+ targets.each do |target|
54
+ deploy_threads << Thread.new { init_target(target, dryrun) }
55
+ end
56
+ deploy_threads.each do |t|
57
+ begin
58
+ t.join
59
+ rescue DreamOps::DreamOpsError
60
+ DreamOps.ui.error "#{$!}"
61
+ deploy_success = deploy_success && false
62
+ end
63
+ end
64
+
65
+ # If ANY deploy threads failed, exit with failure
66
+ exit(1) if !deploy_success
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,120 @@
1
+ module DreamOps
2
+ class SoloInitializer < BaseInitializer
3
+ def analyze(targets)
4
+ @ssh_opts = "-i #{DreamOps.ssh_key} -o 'StrictHostKeyChecking no'"
5
+ @q_all = "> /dev/null 2>&1"
6
+ @q_stdout = "> /dev/null"
7
+
8
+ result = []
9
+
10
+ targets.each do |target|
11
+ # Validate SSH creds
12
+ if DreamOps.ssh_key.empty?
13
+ __bail_with_fatal_error(NoSshKeyError.new())
14
+ end
15
+ hostname = `ssh #{@ssh_opts} #{target} sudo hostname 2>&1`.chomp
16
+ if ! $?.success?
17
+ __bail_with_fatal_error(InvalidSshKeyError.new(target, hostname))
18
+ end
19
+
20
+ DreamOps.ui.info "Target: #{hostname}"
21
+
22
+ target_result = { host: target }
23
+
24
+ target_result[:chefdk_installed] = system("ssh #{@ssh_opts} #{target} which chef #{@q_stdout}")
25
+ DreamOps.ui.info "--- ChefDK Installed: #{target_result[:chefdk_installed]}"
26
+
27
+ target_result[:solo_json_exists] = system("ssh #{@ssh_opts} #{target} stat /var/chef/chef.json #{@q_all}")
28
+ DreamOps.ui.info "--- Valid chef.json: #{target_result[:solo_json_exists]}"
29
+
30
+ target_result[:setup_role_exists] = system("ssh #{@ssh_opts} #{target} stat /var/chef/roles/setup.json #{@q_all}")
31
+ DreamOps.ui.info "--- Valid role[setup]: #{target_result[:setup_role_exists]}"
32
+
33
+ target_result[:deploy_role_exists] = system("ssh #{@ssh_opts} #{target} stat /var/chef/roles/deploy.json #{@q_all}")
34
+ DreamOps.ui.info "--- Valid role[deploy]: #{target_result[:deploy_role_exists]}"
35
+
36
+ result << target_result
37
+ end
38
+
39
+ return result
40
+ end
41
+
42
+ def init_target(target, dryrun)
43
+ # Install ChefDK if not already
44
+ if !target[:chefdk_installed]
45
+ if dryrun
46
+ DreamOps.ui.warn "...WOULD Install ChefDK [target=\"#{target[:host]}\"]"
47
+ else
48
+ DreamOps.ui.warn "...Installing ChefDK [target=\"#{target[:host]}\"]"
49
+
50
+ # Get ubuntu version
51
+ ubuntu_ver = `ssh #{@ssh_opts} #{target[:host]} "awk 'BEGIN { FS = \\"=\\" } /DISTRIB_RELEASE/ { print \\$2 }' /etc/lsb-release"`.chomp
52
+
53
+ # Download and install the package
54
+ chefdk_url = "https://packages.chef.io/files/stable/chefdk/3.3.23/ubuntu/#{ubuntu_ver}/chefdk_3.3.23-1_amd64.deb"
55
+ if system("ssh #{@ssh_opts} #{target[:host]} \"wget #{chefdk_url} -P /tmp\" #{@q_all}")
56
+ `ssh #{@ssh_opts} #{target[:host]} "sudo dpkg -i /tmp/chefdk_3.3.23-1_amd64.deb" #{@q_all}`
57
+ `ssh #{@ssh_opts} #{target[:host]} "sudo rm /tmp/chefdk_3.3.23-1_amd64.deb" #{@q_all}`
58
+ else
59
+ __bail_with_fatal_error(ChefDKFailedError.new(target, chefdk_url))
60
+ end
61
+ end
62
+ end
63
+
64
+ if !dryrun
65
+ `ssh #{@ssh_opts} #{target[:host]} sudo mkdir -p /var/chef/roles`
66
+ end
67
+
68
+ # Create empty json file for chef-solo
69
+ if !target[:solo_json_exists]
70
+ json_path = "/var/chef/chef.json"
71
+ if dryrun
72
+ DreamOps.ui.warn "...WOULD Create boilerplate #{json_path} [target=\"#{target[:host]}\"]"
73
+ else
74
+ DreamOps.ui.warn "...Creating boilerplate #{json_path} [target=\"#{target[:host]}\"]"
75
+ `ssh #{@ssh_opts} #{target[:host]} "echo '{\n \\"run_list\\": []\n}' | sudo tee -a #{json_path}"`
76
+ end
77
+ end
78
+
79
+ # Create setup role with empty run_list for chef-solo
80
+ if !target[:setup_role_exists]
81
+ setup_path = "/var/chef/roles/setup.json"
82
+ if dryrun
83
+ DreamOps.ui.warn "...WOULD Create boilerplate #{setup_path} [target=\"#{target[:host]}\"]"
84
+ else
85
+ DreamOps.ui.warn "...Creating boilerplate #{setup_path} [target=\"#{target[:host]}\"]"
86
+ role_contents = [
87
+ '{',
88
+ ' \"name\": \"setup\",',
89
+ ' \"json_class\": \"Chef::Role\",',
90
+ ' \"description\": \"This role is intended for use when cookbook changes are detected\",',
91
+ ' \"chef_type\": \"role\",',
92
+ ' \"run_list\": []',
93
+ '}',
94
+ ].join("\n")
95
+ `ssh #{@ssh_opts} #{target[:host]} "echo '#{role_contents}' | sudo tee -a #{setup_path}"`
96
+ end
97
+ end
98
+
99
+ # Create deploy role with empty run_list for chef-solo
100
+ if !target[:deploy_role_exists]
101
+ deploy_path = "/var/chef/roles/deploy.json"
102
+ if dryrun
103
+ DreamOps.ui.warn "...WOULD Create boilerplate #{deploy_path} [target=\"#{target[:host]}\"]"
104
+ else
105
+ DreamOps.ui.warn "...Creating boilerplate #{deploy_path} [target=\"#{target[:host]}\"]"
106
+ role_contents = [
107
+ '{',
108
+ ' \"name\": \"deploy\",',
109
+ ' \"json_class\": \"Chef::Role\",',
110
+ ' \"description\": \"This role is intended for use when code changes\",',
111
+ ' \"chef_type\": \"role\",',
112
+ ' \"run_list\": []',
113
+ '}',
114
+ ].join("\n")
115
+ `ssh #{@ssh_opts} #{target[:host]} "echo '#{role_contents}' | sudo tee -a #{deploy_path}"`
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -1,5 +1,5 @@
1
1
  module DreamOps
2
- class Logger < Ridley::Logging::Logger
2
+ class Logger < Logger
3
3
  alias_method :fatal, :error
4
4
 
5
5
  def deprecate(message)
@@ -28,4 +28,4 @@ module DreamOps
28
28
  super(message)
29
29
  end
30
30
  end
31
- end
31
+ end
@@ -43,5 +43,4 @@ class ZipFileGenerator
43
43
  end
44
44
  }
45
45
  end
46
-
47
- end
46
+ end
@@ -1,3 +1,3 @@
1
1
  module DreamOps
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dream-ops
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Allen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-08-28 00:00:00.000000000 Z
11
+ date: 2018-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -72,63 +72,46 @@ dependencies:
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '6.2'
75
+ version: '7.0'
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '6.2'
83
- - !ruby/object:Gem::Dependency
84
- name: ridley
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '5.0'
90
- type: :runtime
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: '5.0'
82
+ version: '7.0'
97
83
  - !ruby/object:Gem::Dependency
98
84
  name: thor
99
85
  requirement: !ruby/object:Gem::Requirement
100
86
  requirements:
101
87
  - - "~>"
102
88
  - !ruby/object:Gem::Version
103
- version: '0.19'
104
- - - "<"
105
- - !ruby/object:Gem::Version
106
- version: 0.19.2
89
+ version: '0.20'
107
90
  type: :runtime
108
91
  prerelease: false
109
92
  version_requirements: !ruby/object:Gem::Requirement
110
93
  requirements:
111
94
  - - "~>"
112
95
  - !ruby/object:Gem::Version
113
- version: '0.19'
114
- - - "<"
115
- - !ruby/object:Gem::Version
116
- version: 0.19.2
96
+ version: '0.20'
117
97
  - !ruby/object:Gem::Dependency
118
98
  name: chef
119
99
  requirement: !ruby/object:Gem::Requirement
120
100
  requirements:
121
101
  - - "~>"
122
102
  - !ruby/object:Gem::Version
123
- version: '12.7'
103
+ version: '13.6'
124
104
  type: :runtime
125
105
  prerelease: false
126
106
  version_requirements: !ruby/object:Gem::Requirement
127
107
  requirements:
128
108
  - - "~>"
129
109
  - !ruby/object:Gem::Version
130
- version: '12.7'
131
- description: This is the description
110
+ version: '13.6'
111
+ description: |2
112
+ This CLI automatically rebuilds and deploys your cookbook
113
+ when changes are detected. Application deployment is
114
+ supported for OpsWorks stacks and ubuntu SSH hosts.
132
115
  email:
133
116
  - chris@apaxsoftware.com
134
117
  executables:
@@ -138,6 +121,7 @@ extra_rdoc_files: []
138
121
  files:
139
122
  - ".gitignore"
140
123
  - ".travis.yml"
124
+ - CHANGELOG.md
141
125
  - Gemfile
142
126
  - LICENSE.txt
143
127
  - README.md
@@ -150,9 +134,12 @@ files:
150
134
  - lib/dream-ops/cli.rb
151
135
  - lib/dream-ops/deployment/base.rb
152
136
  - lib/dream-ops/deployment/opsworks.rb
137
+ - lib/dream-ops/deployment/solo.rb
153
138
  - lib/dream-ops/errors.rb
154
139
  - lib/dream-ops/formatters/base.rb
155
140
  - lib/dream-ops/formatters/human.rb
141
+ - lib/dream-ops/init/base.rb
142
+ - lib/dream-ops/init/solo.rb
156
143
  - lib/dream-ops/logger.rb
157
144
  - lib/dream-ops/mixin/logging.rb
158
145
  - lib/dream-ops/shell.rb
@@ -161,7 +148,9 @@ files:
161
148
  homepage: https://github.com/chris-allen/dream-ops
162
149
  licenses:
163
150
  - MIT
164
- metadata: {}
151
+ metadata:
152
+ source_code_uri: https://github.com/chris-allen/dream-ops
153
+ changelog_uri: https://github.com/chris-allen/dream-ops/blob/master/CHANGELOG.md
165
154
  post_install_message:
166
155
  rdoc_options: []
167
156
  require_paths:
@@ -181,5 +170,5 @@ rubyforge_project:
181
170
  rubygems_version: 2.6.14
182
171
  signing_key:
183
172
  specification_version: 4
184
- summary: This is the summary
173
+ summary: CLI for automating the deployment of application cookbooks
185
174
  test_files: []