dream-ops 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8b8aa9f131724802c240a24e1d23dbf37eb8c70e
4
+ data.tar.gz: 513e26443ce943be5ec10c4251a90794364ce536
5
+ SHA512:
6
+ metadata.gz: d95f1f526da0bcc544dfcb06af87576571b46cf4ad75e14d1b68b36085084121ec5f14a1c561a9da5197fad733a9c2ad934d0ea5a6e3f2d73e4ad98c928f0341
7
+ data.tar.gz: 42e9083c9b3d6038548bd5b4897a91c861eb673a84bb2e46d0f675eef5166e74802bc17d7a0fe674082b5e067a7fc08509beed271cb178e59ad70b9644a5465d
@@ -0,0 +1,10 @@
1
+ .DS_Store
2
+ /.bundle/
3
+ /.yardoc
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in dream.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 chris-allen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,39 @@
1
+ # DreamOps
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/dream-ops`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'dream-ops'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install dream-ops
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ 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.
30
+
31
+ 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).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/dream-ops.
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "dream-ops"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "dream-ops/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "dream-ops"
8
+ spec.version = DreamOps::VERSION
9
+ spec.authors = ["Chris Allen"]
10
+ spec.email = ["chris@apaxsoftware.com"]
11
+ spec.required_ruby_version = ">= 2.3.1"
12
+ spec.required_rubygems_version = ">= 2.0.0"
13
+
14
+ spec.summary = "This is the summary"
15
+ spec.description = "This is the description"
16
+ spec.homepage = "https://github.com/chris-allen/dream-ops"
17
+ spec.license = "MIT"
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features)/})
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.15"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+
29
+ spec.add_dependency "rubyzip", "~> 1.2"
30
+ 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"
35
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ $:.push File.expand_path("../../lib", __FILE__)
3
+ require "dream-ops/cli"
4
+
5
+ DreamOps::Cli::Runner.new(ARGV.dup).execute!
@@ -0,0 +1,60 @@
1
+ # XXX: work around logger spam from hashie
2
+ # https://github.com/intridea/hashie/issues/394
3
+ begin
4
+ require "hashie"
5
+ require "hashie/logger"
6
+ Hashie.logger = Logger.new(nil)
7
+ rescue LoadError
8
+ # intentionally left blank
9
+ end
10
+
11
+ require "berkshelf"
12
+ require "thor"
13
+
14
+ Berkshelf.ui.mute!
15
+
16
+ module DreamOps
17
+
18
+ require_relative "dream-ops/version"
19
+ require_relative "dream-ops/errors"
20
+
21
+ autoload :Shell, "dream-ops/shell"
22
+
23
+ autoload :BaseFormatter, "dream-ops/formatters/base"
24
+ autoload :HumanFormatter, "dream-ops/formatters/human"
25
+ # autoload :JsonFormatter, "dream-ops/formatters/json"
26
+ # autoload :NullFormatter, "dream-ops/formatters/null"
27
+
28
+ autoload :BaseDeployer, "dream-ops/deployment/base"
29
+ autoload :OpsWorksDeployer, "dream-ops/deployment/opsworks"
30
+
31
+ class << self
32
+ # @return [DreamOps::Shell]
33
+ def ui
34
+ @ui ||= DreamOps::Shell.new
35
+ end
36
+
37
+ # Get the appropriate Formatter object based on the formatter
38
+ # classes that have been registered.
39
+ #
40
+ # @return [~Formatter]
41
+ def formatter
42
+ @formatter ||= HumanFormatter.new
43
+ end
44
+
45
+ # Specify the format for output
46
+ #
47
+ # @param [#to_sym] format_id
48
+ # the ID of the registered formatter to use
49
+ #
50
+ # @example DreamOps.set_format :json
51
+ #
52
+ # @return [~Formatter]
53
+ def set_format(name)
54
+ id = name.to_s.capitalize
55
+ @formatter = DreamOps.const_get("#{id}Formatter").new
56
+ end
57
+ end
58
+ end
59
+
60
+ require_relative "dream-ops/cli"
@@ -0,0 +1,150 @@
1
+ require "dream-ops"
2
+
3
+ # require_relative "init_generator"
4
+ # require_relative "cookbook_generator"
5
+ # require_relative "commands/shelf"
6
+
7
+
8
+ module DreamOps
9
+ class Cli < Thor
10
+ # This is the main entry point for the CLI. It exposes the method {#execute!} to
11
+ # start the CLI.
12
+ #
13
+ # @note the arity of {#initialize} and {#execute!} are extremely important for testing purposes. It
14
+ # is a requirement to perform in-process testing with Aruba. In process testing is much faster
15
+ # than spawning a new Ruby process for each test.
16
+ class Runner
17
+ def initialize(argv, stdin = STDIN, stdout = STDOUT, stderr = STDERR, kernel = Kernel)
18
+ @argv, @stdin, @stdout, @stderr, @kernel = argv, stdin, stdout, stderr, kernel
19
+ end
20
+
21
+ def execute!
22
+ $stdin = @stdin
23
+ $stdout = @stdout
24
+ $stderr = @stderr
25
+
26
+ DreamOps::Cli.start(@argv)
27
+ @kernel.exit(0)
28
+ rescue DreamOps::DreamOpsError => e
29
+ DreamOps.ui.error e
30
+ DreamOps.ui.error "\t" + e.backtrace.join("\n\t") if ENV["BERKSHELF_DEBUG"]
31
+ @kernel.exit(e.status_code)
32
+ # rescue Ridley::Errors::RidleyError => e
33
+ # DreamOps.ui.error "#{e.class} #{e}"
34
+ # DreamOps.ui.error "\t" + e.backtrace.join("\n\t") if ENV["BERKSHELF_DEBUG"]
35
+ # @kernel.exit(47)
36
+ end
37
+ end
38
+
39
+ class << self
40
+ def dispatch(meth, given_args, given_opts, config)
41
+ if given_args.length > 1 && !(given_args & Thor::HELP_MAPPINGS).empty?
42
+ command = given_args.first
43
+
44
+ if subcommands.include?(command)
45
+ super(meth, [command, "help"].compact, nil, config)
46
+ else
47
+ super(meth, ["help", command].compact, nil, config)
48
+ end
49
+ else
50
+ super
51
+ DreamOps.formatter.cleanup_hook unless config[:current_command].name == "help"
52
+ end
53
+ end
54
+ end
55
+
56
+ def initialize(*args)
57
+ super(*args)
58
+
59
+ # if @options[:config]
60
+ # unless File.exist?(@options[:config])
61
+ # raise ConfigNotFound.new(:berkshelf, @options[:config])
62
+ # end
63
+
64
+ # DreamOps.config = DreamOps::Config.from_file(@options[:config])
65
+ # end
66
+
67
+ if @options[:debug]
68
+ ENV["BERKSHELF_DEBUG"] = "true"
69
+ DreamOps.logger.level = ::Logger::DEBUG
70
+ end
71
+
72
+ if @options[:quiet]
73
+ DreamOps.ui.mute!
74
+ end
75
+
76
+ DreamOps.set_format @options[:format]
77
+ @options = options.dup # unfreeze frozen options Hash from Thor
78
+ end
79
+
80
+ namespace "berkshelf"
81
+
82
+ map "ls" => :list
83
+ map "book" => :cookbook
84
+ map ["ver", "-v", "--version"] => :version
85
+
86
+ default_task :install
87
+
88
+ class_option :config,
89
+ type: :string,
90
+ desc: "Path to DreamOps configuration to use.",
91
+ aliases: "-c",
92
+ banner: "PATH"
93
+ class_option :format,
94
+ type: :string,
95
+ default: "human",
96
+ desc: "Output format to use.",
97
+ aliases: "-F",
98
+ banner: "FORMAT"
99
+ class_option :quiet,
100
+ type: :boolean,
101
+ desc: "Silence all informational output.",
102
+ aliases: "-q",
103
+ default: false
104
+ class_option :debug,
105
+ type: :boolean,
106
+ desc: "Output debug information",
107
+ aliases: "-d",
108
+ default: false
109
+
110
+ desc "version", "Display version"
111
+ def version
112
+ DreamOps.formatter.version
113
+ end
114
+
115
+ method_option :stacks,
116
+ type: :array,
117
+ desc: "Only these stack IDs.",
118
+ aliases: "-s"
119
+ desc "deploy", "Creates deploy"
120
+ def deploy(type)
121
+ deployer = nil
122
+
123
+ args = []
124
+ if type == 'opsworks'
125
+ deployer = OpsWorksDeployer.new
126
+ stack_ids = options[:stacks]
127
+ args = [*stack_ids]
128
+ end
129
+
130
+ if !deployer.nil?
131
+ deployer.deploy(*args)
132
+ end
133
+ end
134
+
135
+ # tasks["cookbook"].options = DreamOps::CookbookGenerator.class_options
136
+
137
+ private
138
+
139
+ # Print a list of the given cookbooks. This is used by various
140
+ # methods like {list} and {contingent}.
141
+ #
142
+ # @param [Array<CachedCookbook>] cookbooks
143
+ #
144
+ def print_list(cookbooks)
145
+ Array(cookbooks).sort.each do |cookbook|
146
+ DreamOps.formatter.msg " * #{cookbook.cookbook_name} (#{cookbook.version})"
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,80 @@
1
+ require "berkshelf"
2
+ require "dream-ops/utils/zip"
3
+ require 'fileutils'
4
+
5
+ module DreamOps
6
+ class BaseDeployer
7
+ class << self
8
+ #
9
+ # @macro deployer_method
10
+ # @method $1(*args)
11
+ # Create a deployer method for the declaration
12
+ #
13
+ def deployer_method(name)
14
+ class_eval <<-EOH, __FILE__, __LINE__ + 1
15
+ def #{name}(*args)
16
+ raise AbstractFunction,
17
+ "##{name} must be implemented on \#{self.class.name}!"
18
+ end
19
+ EOH
20
+ end
21
+ end
22
+
23
+ # These MUST be implemented by subclasses
24
+ deployer_method :analyze
25
+ deployer_method :deploy_cookbook
26
+ deployer_method :deploy_target
27
+
28
+ # It may turn out that cookbook building will be different for different
29
+ # deployments, but for now all deployments build them the same way.
30
+ def build_cookbook(cookbook)
31
+ berksfile = Berkshelf::Berksfile.from_file(File.join(cookbook[:path], "Berksfile"))
32
+ berksfile.vendor("berks-cookbooks")
33
+
34
+ File.open("berks-cookbooks/Berksfile", 'w') { |file|
35
+ file.write("source \"https://supermarket.chef.io\"\n\n")
36
+ file.write("cookbook \"#{cookbook[:name]}\", path: \"./#{cookbook[:name]}\"")
37
+ }
38
+ zf = ZipFileGenerator.new("berks-cookbooks", cookbook[:cookbook_key])
39
+ zf.write
40
+ end
41
+
42
+ def cleanup_cookbooks(cookbooks)
43
+ cookbooks.each do |cookbook|
44
+ File.delete(cookbook[:cookbook_key])
45
+ FileUtils.remove_dir("berks-cookbooks")
46
+ end
47
+ end
48
+
49
+ def deploy(*args)
50
+ # Find unique cookbooks and deploy targets
51
+ result = analyze(args)
52
+
53
+ # Build each unique cookbook once in case it's used by more than one app
54
+ result[:cookbooks].each do |cookbook|
55
+ DreamOps.ui.info "...Building cookbook [#{cookbook[:name]}]"
56
+ build_cookbook(cookbook)
57
+
58
+ DreamOps.ui.info "...Deploying cookbook [#{cookbook[:name]}]"
59
+ deploy_cookbook(cookbook)
60
+ end
61
+ cleanup_cookbooks(result[:cookbooks])
62
+
63
+ # Update cookbooks if needed and deploy to all targets
64
+ deploy_success = true
65
+ deploy_threads = []
66
+ result[:deploy_targets].each do |target|
67
+ deploy_threads << Thread.new { deploy_target(target, result[:cookbooks]) }
68
+ end
69
+ deploy_threads.each do |t|
70
+ begin
71
+ t.join
72
+ rescue DreamOps::FatalDeployError
73
+ deploy_success = deploy_success && false
74
+ end
75
+ end
76
+
77
+ exit(1) if !deploy_success
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,202 @@
1
+ require "aws-sdk"
2
+ require "ridley"
3
+
4
+ Aws.use_bundled_cert!
5
+
6
+ module DreamOps
7
+ class OpsWorksDeployer < BaseDeployer
8
+
9
+ # Output the version of DreamOps
10
+ def analyze(stack_ids)
11
+ begin
12
+ @opsworks = Aws::OpsWorks::Client.new
13
+ stacks = @opsworks.describe_stacks({ stack_ids: stack_ids, }).stacks
14
+ rescue => e
15
+ DreamOps.ui.error "Failed to fetch OpsWorks stacks\n"
16
+ DreamOps.ui.error "#{$!}"
17
+ exit(1)
18
+ end
19
+
20
+ # Collect and print stack info
21
+ result = { cookbooks: [], deploy_targets: [] }
22
+ stacks.each do |stack|
23
+ stack_result = analyze_stack(stack)
24
+ # Add the stack to the deploy targets
25
+ result[:deploy_targets] << {
26
+ stack: stack,
27
+ apps: stack_result[:apps],
28
+ cookbook: stack_result[:cookbook]
29
+ }
30
+ # Determine whether the cookbook needs to be built
31
+ cbook = stack_result[:cookbook]
32
+ if !cbook.nil? && !cbook[:local_sha].nil?
33
+ # We only build the cookbook if we don't have this version on S3
34
+ if cbook[:remote_sha] != cbook[:local_sha]
35
+ # Don't build the same destination cookbook more than once
36
+ if !__cookbook_in_array(cbook, result[:cookbooks])
37
+ result[:cookbooks] << cbook
38
+ end
39
+ end
40
+ end
41
+ end
42
+ return result
43
+ end
44
+
45
+ ################## OpsWorks specific methods ##################
46
+
47
+ # Retrieves stack apps and gets all information about remote/local cookbook
48
+ def analyze_stack(stack)
49
+ cookbook = nil
50
+ cookbookName = nil
51
+
52
+ DreamOps.ui.info "Stack: #{stack.name}"
53
+ if !stack.custom_cookbooks_source.nil?
54
+ source = stack.custom_cookbooks_source
55
+
56
+ # Skip this step if the stack doesn't use S3
57
+ if source.type == 's3'
58
+ cookbookPath = source.url[25..-1]
59
+ firstSlash = cookbookPath.index('/')
60
+ cookbook = {
61
+ bucket: cookbookPath[0..(firstSlash-1)],
62
+ cookbook_key: cookbookPath[(firstSlash+1)..-1],
63
+ sha_key: cookbookPath[firstSlash+1..-1].sub('.zip', '_SHA.txt')
64
+ }
65
+
66
+ # Treat any directory with a Berksfile as a cookbook
67
+ cookbooks = Dir.glob('./**/Berksfile')
68
+
69
+ # For now we only handle if we find one cookbook
70
+ if cookbooks.length == 1
71
+ metadata = Ridley::Chef::Cookbook::Metadata.from_file(cookbooks[0].sub("Berksfile", "metadata.rb"))
72
+ cookbook[:name] = metadata.name
73
+ cookbook[:path] = cookbooks[0].sub('/Berksfile', '')
74
+ if cookbook[:cookbook_key].include? cookbook[:name]
75
+ cookbook[:local_sha] = `git log --pretty=%H -1 #{cookbook[:path]}`.chomp
76
+
77
+ begin
78
+ obj = Aws::S3::Object.new(cookbook[:bucket], cookbook[:sha_key])
79
+ cookbook[:remote_sha] = obj.get.body.string
80
+ rescue Aws::S3::Errors::NoSuchKey
81
+ cookbook[:remote_sha] = ''
82
+ end
83
+ else
84
+ DreamOps.ui.info "Stack cookbook source is '#{cookbook[:cookbook_key]}' but found '#{cookbook[:name]}' locally"
85
+ end
86
+ end
87
+ DreamOps.ui.info "--- Cookbook: #{cookbook[:name]}"
88
+ end
89
+ end
90
+
91
+ apps = @opsworks.describe_apps({ stack_id: stack.stack_id }).apps
92
+ if apps.length == 0
93
+ DreamOps.ui.info "--- Apps: No apps"
94
+ else
95
+ DreamOps.ui.info "--- Apps: #{apps.map{|app| app.name}}"
96
+ end
97
+
98
+ return { apps: apps, cookbook: cookbook }
99
+ end
100
+
101
+ # Deploys cookbook to S3
102
+ def deploy_cookbook(cookbook)
103
+ begin
104
+ archiveFile = File.open(cookbook[:cookbook_key])
105
+ remoteCookbook = Aws::S3::Object.new(cookbook[:bucket], cookbook[:cookbook_key])
106
+ response = remoteCookbook.put({ acl: "private", body: archiveFile })
107
+ archiveFile.close
108
+
109
+ remoteSha = Aws::S3::Object.new(cookbook[:bucket], cookbook[:sha_key])
110
+ response = remoteSha.put({ acl: "private", body: cookbook[:local_sha] })
111
+ rescue => e
112
+ DreamOps.ui.error "#{$!}"
113
+ end
114
+ end
115
+
116
+ #
117
+ def deploy_target(target, cookbooks)
118
+ # If this stack has a new cookbook
119
+ if !target[:cookbook].nil?
120
+ if __cookbook_in_array(target[:cookbook], cookbooks)
121
+ begin
122
+ # Grab a fresh copy of the cookbook on all instances in the stack
123
+ update_custom_cookbooks(target[:stack])
124
+ rescue Aws::OpsWorks::Errors::ValidationException
125
+ DreamOps.ui.error "Stack \"#{target[:stack].name}\" has no running instances."
126
+ __bail_with_fatal_error
127
+ end
128
+
129
+ # Re-run the setup step for all layers
130
+ setup(target[:stack])
131
+ end
132
+ end
133
+
134
+ # Deploy all apps for stack
135
+ target[:apps].each do |app|
136
+ begin
137
+ deploy_app(app, target[:stack])
138
+ rescue Aws::OpsWorks::Errors::ValidationException
139
+ DreamOps.ui.error "Stack \"#{target[:stack].name}\" has no running instances."
140
+ __bail_with_fatal_error
141
+ end
142
+ end
143
+ end
144
+
145
+ def update_custom_cookbooks(stack)
146
+ DreamOps.ui.info "...Updating custom cookbooks [stack=\"#{stack.name}\"]"
147
+ response = @opsworks.create_deployment({
148
+ stack_id: stack.stack_id,
149
+ command: { name: "update_custom_cookbooks" }
150
+ })
151
+ return wait_for_deployment(response.deployment_id)
152
+ end
153
+
154
+ def setup(stack)
155
+ DreamOps.ui.info "...Running setup command [stack=\"#{stack.name}\"]"
156
+ response = @opsworks.create_deployment({
157
+ stack_id: stack.stack_id,
158
+ command: { name: "setup" }
159
+ })
160
+ return wait_for_deployment(response.deployment_id)
161
+ end
162
+
163
+ def deploy_app(app, stack)
164
+ DreamOps.ui.info "...Deploying [stack=\"#{stack.name}\"] [app=\"#{app.name}\"]"
165
+ response = @opsworks.create_deployment({
166
+ stack_id: stack.stack_id,
167
+ app_id: app.app_id,
168
+ command: { name: "deploy" }
169
+ })
170
+ return wait_for_deployment(response.deployment_id)
171
+ end
172
+
173
+ def get_deployment_status(deployment_id)
174
+ response = @opsworks.describe_deployments({ deployment_ids: [deployment_id] })
175
+ if response[:deployments].length == 1
176
+ return response[:deployments][0].status
177
+ end
178
+ return "failed"
179
+ end
180
+
181
+ def wait_for_deployment(deployment_id)
182
+ status = "failed"
183
+ while true
184
+ status = get_deployment_status(deployment_id)
185
+ break if ["successful", "failed"].include? status
186
+ sleep(2)
187
+ end
188
+ return status
189
+ end
190
+
191
+ def __bail_with_fatal_error
192
+ raise FatalDeployError
193
+ Thread.exit
194
+ end
195
+
196
+ def __cookbook_in_array(cb, cookbooks)
197
+ return cookbooks.any? {|c|
198
+ c[:cookbook_key] == cb[:cookbook_key] and c[:bucket] == cb[:bucket]
199
+ }
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,15 @@
1
+ module DreamOps
2
+ class DreamOpsError < StandardError
3
+ class << self
4
+ # @param [Integer] code
5
+ def set_status_code(code)
6
+ define_method(:status_code) { code }
7
+ define_singleton_method(:status_code) { code }
8
+ end
9
+ end
10
+
11
+ alias_method :message, :to_s
12
+ end
13
+
14
+ class FatalDeployError < DreamOpsError; set_status_code(10); end
15
+ end
@@ -0,0 +1,43 @@
1
+ module DreamOps
2
+ class BaseFormatter
3
+ class << self
4
+ #
5
+ # @macro formatter_method
6
+ # @method $1(*args)
7
+ # Create a formatter method for the declaration
8
+ #
9
+ def formatter_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
+ # UI methods
20
+ formatter_method :deprecation
21
+ formatter_method :error
22
+ formatter_method :msg
23
+ formatter_method :warn
24
+
25
+ # Object methods
26
+ formatter_method :fetch
27
+ formatter_method :info
28
+ formatter_method :install
29
+ formatter_method :list
30
+ formatter_method :outdated
31
+ formatter_method :package
32
+ formatter_method :search
33
+ formatter_method :show
34
+ formatter_method :skipping
35
+ formatter_method :uploaded
36
+ formatter_method :use
37
+ formatter_method :vendor
38
+ formatter_method :version
39
+
40
+ # The cleanup hook is defined by subclasses and is called by the CLI.
41
+ def cleanup_hook; end
42
+ end
43
+ end
@@ -0,0 +1,163 @@
1
+ module DreamOps
2
+ class HumanFormatter < BaseFormatter
3
+ # Output the version of DreamOps
4
+ def version
5
+ DreamOps.ui.info DreamOps::VERSION
6
+ end
7
+
8
+ # @param [DreamOps::Dependency] dependency
9
+ def fetch(dependency)
10
+ DreamOps.ui.info "Fetching '#{dependency.name}' from #{dependency.location}"
11
+ end
12
+
13
+ # Output a Cookbook installation message using {DreamOps.ui}
14
+ #
15
+ # @param [Source] source
16
+ # the source the dependency is being downloaded from
17
+ # @param [RemoteCookbook] cookbook
18
+ # the cookbook to be downloaded
19
+ def install(source, cookbook)
20
+ message = "Installing #{cookbook.name} (#{cookbook.version})"
21
+
22
+ if source.type == :chef_repo
23
+ message << " from #{cookbook.location_path}"
24
+ elsif !source.default?
25
+ message << " from #{source}"
26
+ message << " ([#{cookbook.location_type}] #{cookbook.location_path})"
27
+ end
28
+
29
+ DreamOps.ui.info(message)
30
+ end
31
+
32
+ # Output a Cookbook use message using {DreamOps.ui}
33
+ #
34
+ # @param [Dependency] dependency
35
+ def use(dependency)
36
+ message = "Using #{dependency.name} (#{dependency.locked_version})"
37
+ message << " from #{dependency.location}" if dependency.location
38
+ DreamOps.ui.info(message)
39
+ end
40
+
41
+ # Output a Cookbook upload message using {DreamOps.ui}
42
+ #
43
+ # @param [DreamOps::CachedCookbook] cookbook
44
+ # @param [Ridley::Connection] conn
45
+ def uploaded(cookbook, conn)
46
+ DreamOps.ui.info "Uploaded #{cookbook.cookbook_name} (#{cookbook.version}) to: '#{conn.server_url}'"
47
+ end
48
+
49
+ # Output a Cookbook skip message using {DreamOps.ui}
50
+ #
51
+ # @param [DreamOps::CachedCookbook] cookbook
52
+ # @param [Ridley::Connection] conn
53
+ def skipping(cookbook, conn)
54
+ DreamOps.ui.info "Skipping #{cookbook.cookbook_name} (#{cookbook.version}) (frozen)"
55
+ end
56
+
57
+ # Output a list of outdated cookbooks and the most recent version
58
+ # using {DreamOps.ui}
59
+ #
60
+ # @param [Hash] hash
61
+ # the list of outdated cookbooks in the format
62
+ # { 'cookbook' => { 'supermarket.chef.io' => #<Cookbook> } }
63
+ def outdated(hash)
64
+ if hash.empty?
65
+ DreamOps.ui.info("All cookbooks up to date!")
66
+ else
67
+ DreamOps.ui.info("The following cookbooks have newer versions:")
68
+
69
+ hash.each do |name, info|
70
+ info["remote"].each do |remote_source, remote_version|
71
+ out = " * #{name} (#{info['local']} => #{remote_version})"
72
+
73
+ unless remote_source.default?
74
+ out << " [#{remote_source.uri}]"
75
+ end
76
+
77
+ DreamOps.ui.info(out)
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ # Output a Cookbook package message using {DreamOps.ui}
84
+ #
85
+ # @param [String] destination
86
+ def package(destination)
87
+ DreamOps.ui.info "Cookbook(s) packaged to #{destination}"
88
+ end
89
+
90
+ # Output the important information about a cookbook using {DreamOps.ui}.
91
+ #
92
+ # @param [CachedCookbook] cookbook
93
+ def info(cookbook)
94
+ DreamOps.ui.info(cookbook.pretty_print)
95
+ end
96
+
97
+ # Output a list of cookbooks using {DreamOps.ui}
98
+ #
99
+ # @param [Array<Dependency>] list
100
+ def list(dependencies)
101
+ DreamOps.ui.info "Cookbooks installed by your Berksfile:"
102
+ dependencies.each do |dependency|
103
+ out = " * #{dependency}"
104
+ out << " from #{dependency.location}" if dependency.location
105
+ DreamOps.ui.info(out)
106
+ end
107
+ end
108
+
109
+ # Ouput Cookbook search results using {DreamOps.ui}
110
+ #
111
+ # @param [Array<APIClient::RemoteCookbook>] results
112
+ def search(results)
113
+ results.sort_by(&:name).each do |remote_cookbook|
114
+ DreamOps.ui.info "#{remote_cookbook.name} (#{remote_cookbook.version})"
115
+ end
116
+ end
117
+
118
+ # Output Cookbook path using {DreamOps.ui}
119
+ #
120
+ # @param [CachedCookbook] cookbook
121
+ def show(cookbook)
122
+ path = File.expand_path(cookbook.path)
123
+ DreamOps.ui.info(path)
124
+ end
125
+
126
+ # Output Cookbook vendor info message using {DreamOps.ui}
127
+ #
128
+ # @param [CachedCookbook] cookbook
129
+ # @param [String] destination
130
+ def vendor(cookbook, destination)
131
+ cookbook_destination = File.join(destination, cookbook.cookbook_name)
132
+ DreamOps.ui.info "Vendoring #{cookbook.cookbook_name} (#{cookbook.version}) to #{cookbook_destination}"
133
+ end
134
+
135
+ # Output a generic message using {DreamOps.ui}
136
+ #
137
+ # @param [String] message
138
+ def msg(message)
139
+ DreamOps.ui.info message
140
+ end
141
+
142
+ # Output an error message using {DreamOps.ui}
143
+ #
144
+ # @param [String] message
145
+ def error(message)
146
+ DreamOps.ui.error message
147
+ end
148
+
149
+ # Output a warning message using {DreamOps.ui}
150
+ #
151
+ # @param [String] message
152
+ def warn(message)
153
+ DreamOps.ui.warn message
154
+ end
155
+
156
+ # Output a deprecation warning
157
+ #
158
+ # @param [String] message
159
+ def deprecation(message)
160
+ DreamOps.ui.info "DEPRECATED: #{message}"
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,31 @@
1
+ require "thor"
2
+
3
+ module DreamOps
4
+ # Subclass the current shell (which is different based on the OS)
5
+ Shell = Class.new(Thor::Base.shell) do
6
+ # Mute the output of this instance of UI until {#unmute!} is called
7
+ def mute!
8
+ @mute = true
9
+ end
10
+
11
+ # Unmute the output of this instance of UI until {#mute!} is called
12
+ def unmute!
13
+ @mute = false
14
+ end
15
+
16
+ def say(*args)
17
+ return if quiet?
18
+ super(*args)
19
+ end
20
+ alias_method :info, :say
21
+
22
+ def warn(message, color = :yellow)
23
+ say(message, color)
24
+ end
25
+
26
+ def error(message, color = :red)
27
+ message = set_color(message, *color) if color
28
+ super(message)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,47 @@
1
+ require 'zip'
2
+
3
+ # This is a simple example which uses rubyzip to
4
+ # recursively generate a zip file from the contents of
5
+ # a specified directory. The directory itself is not
6
+ # included in the archive, rather just its contents.
7
+ #
8
+ # Usage:
9
+ # directoryToZip = "/tmp/input"
10
+ # outputFile = "/tmp/out.zip"
11
+ # zf = ZipFileGenerator.new(directoryToZip, outputFile)
12
+ # zf.write()
13
+ class ZipFileGenerator
14
+
15
+ # Initialize with the directory to zip and the location of the output archive.
16
+ def initialize(inputDir, outputFile)
17
+ @inputDir = inputDir
18
+ @outputFile = outputFile
19
+ end
20
+
21
+ # Zip the input directory.
22
+ def write()
23
+ entries = Dir.entries(@inputDir); entries.delete("."); entries.delete("..")
24
+ io = Zip::File.open(@outputFile, Zip::File::CREATE);
25
+
26
+ writeEntries(entries, "", io)
27
+ io.close();
28
+ end
29
+
30
+ # A helper method to make the recursion work.
31
+ private
32
+ def writeEntries(entries, path, io)
33
+
34
+ entries.each { |e|
35
+ zipFilePath = path == "" ? e : File.join(path, e)
36
+ diskFilePath = File.join(@inputDir, zipFilePath)
37
+ if File.directory?(diskFilePath)
38
+ io.mkdir(zipFilePath)
39
+ subdir =Dir.entries(diskFilePath); subdir.delete("."); subdir.delete("..")
40
+ writeEntries(subdir, zipFilePath, io)
41
+ else
42
+ io.get_output_stream(zipFilePath) { |f| f.puts(File.open(diskFilePath, "rb").read())}
43
+ end
44
+ }
45
+ end
46
+
47
+ end
@@ -0,0 +1,3 @@
1
+ module DreamOps
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,182 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dream-ops
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Allen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-07-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubyzip
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: aws-sdk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: berkshelf
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '6.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
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'
97
+ - !ruby/object:Gem::Dependency
98
+ name: thor
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.19'
104
+ - - "<"
105
+ - !ruby/object:Gem::Version
106
+ version: 0.19.2
107
+ type: :runtime
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: '0.19'
114
+ - - "<"
115
+ - !ruby/object:Gem::Version
116
+ version: 0.19.2
117
+ - !ruby/object:Gem::Dependency
118
+ name: chef
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '12.7'
124
+ type: :runtime
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '12.7'
131
+ description: This is the description
132
+ email:
133
+ - chris@apaxsoftware.com
134
+ executables:
135
+ - dream
136
+ extensions: []
137
+ extra_rdoc_files: []
138
+ files:
139
+ - ".gitignore"
140
+ - Gemfile
141
+ - LICENSE.txt
142
+ - README.md
143
+ - Rakefile
144
+ - bin/console
145
+ - bin/setup
146
+ - dream-ops.gemspec
147
+ - exe/dream
148
+ - lib/dream-ops.rb
149
+ - lib/dream-ops/cli.rb
150
+ - lib/dream-ops/deployment/base.rb
151
+ - lib/dream-ops/deployment/opsworks.rb
152
+ - lib/dream-ops/errors.rb
153
+ - lib/dream-ops/formatters/base.rb
154
+ - lib/dream-ops/formatters/human.rb
155
+ - lib/dream-ops/shell.rb
156
+ - lib/dream-ops/utils/zip.rb
157
+ - lib/dream-ops/version.rb
158
+ homepage: https://github.com/chris-allen/dream-ops
159
+ licenses:
160
+ - MIT
161
+ metadata: {}
162
+ post_install_message:
163
+ rdoc_options: []
164
+ require_paths:
165
+ - lib
166
+ required_ruby_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: 2.3.1
171
+ required_rubygems_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: 2.0.0
176
+ requirements: []
177
+ rubyforge_project:
178
+ rubygems_version: 2.4.8
179
+ signing_key:
180
+ specification_version: 4
181
+ summary: This is the summary
182
+ test_files: []