opsicle 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +8 -8
  2. data/.gitignore +3 -0
  3. data/.travis.yml +6 -1
  4. data/Gemfile +3 -0
  5. data/Guardfile +5 -0
  6. data/README.markdown +44 -17
  7. data/bin/opsicle +36 -3
  8. data/lib/opsicle.rb +2 -3
  9. data/lib/opsicle/commands.rb +6 -0
  10. data/lib/opsicle/commands/deploy.rb +39 -0
  11. data/lib/opsicle/{list.rb → commands/list.rb} +0 -2
  12. data/lib/opsicle/{ssh.rb → commands/ssh.rb} +0 -3
  13. data/lib/opsicle/commands/ssh_key.rb +40 -0
  14. data/lib/opsicle/config.rb +1 -0
  15. data/lib/opsicle/deployment.rb +59 -0
  16. data/lib/opsicle/deployments.rb +22 -0
  17. data/lib/opsicle/monitor.rb +12 -0
  18. data/lib/opsicle/monitor/app.rb +147 -0
  19. data/lib/opsicle/monitor/panel.rb +98 -0
  20. data/lib/opsicle/monitor/panels/deployments.rb +42 -0
  21. data/lib/opsicle/monitor/panels/header.rb +48 -0
  22. data/lib/opsicle/monitor/panels/help.rb +33 -0
  23. data/lib/opsicle/monitor/screen.rb +83 -0
  24. data/lib/opsicle/monitor/spy/dataspyable.rb +19 -0
  25. data/lib/opsicle/monitor/spy/deployments.rb +53 -0
  26. data/lib/opsicle/monitor/subpanel.rb +55 -0
  27. data/lib/opsicle/monitor/translatable.rb +33 -0
  28. data/lib/opsicle/stack.rb +22 -0
  29. data/lib/opsicle/version.rb +1 -1
  30. data/opsicle.gemspec +1 -1
  31. data/spec/opsicle/client_spec.rb +6 -6
  32. data/spec/opsicle/commands/deploy_spec.rb +50 -0
  33. data/spec/opsicle/{list_spec.rb → commands/list_spec.rb} +7 -6
  34. data/spec/opsicle/commands/ssh_key_spec.rb +75 -0
  35. data/spec/opsicle/{ssh_spec.rb → commands/ssh_spec.rb} +24 -24
  36. data/spec/opsicle/config_spec.rb +12 -11
  37. data/spec/opsicle/monitor/app_spec.rb +63 -0
  38. data/spec/opsicle/monitor/panel_spec.rb +162 -0
  39. data/spec/opsicle/monitor/screen_spec.rb +121 -0
  40. data/spec/opsicle/monitor/spy/deployments_spec.rb +41 -0
  41. data/spec/opsicle/monitor/subpanel_spec.rb +199 -0
  42. data/spec/spec_helper.rb +2 -1
  43. metadata +44 -16
  44. data/Gemfile.lock +0 -75
  45. data/lib/opsicle/deploy.rb +0 -25
  46. data/spec/opsicle/deploy_spec.rb +0 -29
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MWY2NTQzM2MwYjY3NDE2OGEzOGE4NTk2NDQzZjVkMjhjOGVlOThiZQ==
4
+ ZjkzMDgwYWI5OTI4NDQ5Mzc5MmRmZjg1ZTc3YWU3MTgxZWMxNTlmNA==
5
5
  data.tar.gz: !binary |-
6
- MjEyZmMwMDZmODBlYTkxOGY1NzcxZWRhNGM4NWZmMzJiYzNmMDEwNA==
6
+ YmNkNWQxZWFiOTg4MDgyMzU3NjA1ZDk4NTVhYzlhY2IzY2M5YzIwYg==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- MTNiYTM1NDY4MTIwYmE2OTA2ZGViOGM1YzMwZWQyZDY1NTQyM2ZhZmE1NjVh
10
- Mjg5NTg3YzZmYmU3NDcxY2Q5NWE2N2MyYWE0YzkwN2I2YzVkZGVmNmMwMzk3
11
- NWU3ZTdiMTBiMzI3Zjc2ZGUwMTBkYzRjYzMzZTNjYThjNTdlNTY=
9
+ NjgzMjk2ODIwMzRhOTk5ZjY5NmY2MWU1ZmEzYTEyMTZiMjJkYmE4Y2ZhYmJl
10
+ YThjNDNkYzUxMTI4NzlkZDI0MWQ4YWQ5MGJjMThjYjVkNTc1ZDM0YTFmMDBk
11
+ MTU5NDc0MWM4M2M4MWEzYWUwNTYzMzZkNzRhNzQwMTk3NzY4OWU=
12
12
  data.tar.gz: !binary |-
13
- NzQyMTc1ZDI3NjMwNGRiMWQ4NjhlNzVjMjE0MjI4MzU4OWZiNTFiNmZmYWNm
14
- OGZkOTYzYWJiMWQ3NWVkMDY3MTVmYWUzODRiYWJjNzM3YjM3ZDk3Mzc4MmY4
15
- NjhjNjQ3OWY5ZTQ0NDE2N2FhYjE1OWYyNjg3YzQ4ZjUyMGEwN2E=
13
+ NDJhYjQ3Y2IwZDBlYzRjY2I2MWZkNTRhNWRhZjVlNTNlOGY1ZDkzODE2Zjlh
14
+ ZTViNDdmNGYyYmY3ODc0MzdjYzU2ODg2YjQzYTJjNjgxNGRmMDE4YTA4OTIz
15
+ MmU5YTc1MTgxOTQ2MmJjMThlNzQ1ZmNkMzAzNjM3YWIxMjZkZTQ=
data/.gitignore CHANGED
@@ -1,8 +1,11 @@
1
+ # ignore .opsicle since this will never be hosted on OpsWorks
2
+ .opsicle*
1
3
  *.gem
2
4
  *.rbc
3
5
  .bundle
4
6
  .config
5
7
  coverage
8
+ Gemfile.lock
6
9
  InstalledFiles
7
10
  lib/bundler/man
8
11
  pkg
data/.travis.yml CHANGED
@@ -3,5 +3,10 @@ rvm:
3
3
  - 2.0
4
4
  - 2.1
5
5
  - 1.9.3
6
- - jruby-19mode # JRuby in 1.9 mode
7
6
  script: bundle exec rspec
7
+ matrix:
8
+ include:
9
+ - rvm: jruby-19mode # JRuby in 1.9 mode
10
+ env: JRUBY_OPTS="--1.9 -Xcext.enabled=true"
11
+ allow_failures:
12
+ - rvm: jruby-19mode
data/Gemfile CHANGED
@@ -2,3 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in opsicle.gemspec
4
4
  gemspec
5
+
6
+ gem 'curses' if RUBY_VERSION >= '2.1' || RUBY_PLATFORM == 'java'
7
+
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard :rspec, cmd: 'bundle exec rspec', all_on_start: true, all_after_pass: true do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
data/README.markdown CHANGED
@@ -4,28 +4,31 @@ A gem bringing the glory of OpsWorks to your command line.
4
4
  [![Gem Version](https://badge.fury.io/rb/opsicle.png)](http://badge.fury.io/rb/opsicle)
5
5
  [![Build Status](https://travis-ci.org/sportngin/opsicle.png?branch=master)](https://travis-ci.org/sportngin/opsicle)
6
6
 
7
- ##Deployment Commands
7
+ ## Installation
8
+ Add this line to your project's Gemfile:
8
9
 
9
- ```bash
10
- # Run a basic deploy for the current app
11
- opsicle deploy staging
12
-
13
- # Run the deploy for production
14
- opsicle deploy production
15
-
16
- # SSH to a server instance in the given environment stack (ex: staging)
17
- opsicle ssh staging
18
-
19
- # Run other opsworks commands
20
- opsicle update_custom_cookbooks staging
10
+ **For Ruby >=2.1.0**
11
+ ```ruby
12
+ gem 'opsicle'
13
+ gem 'curses'
14
+ ```
21
15
 
22
- # Trigger the setup event
23
- opsicle setup staging
16
+ **For Ruby <2.1.0, 1.9.3**
17
+ ```ruby
18
+ gem 'opsicle'
24
19
  ```
25
20
 
26
- Opsicle accepts a `--verbose` flag to show additional information as commands are run.
21
+ (Alternatively, `gem 'opsicle'; gem 'curses' unless RUBY_VERSION < "2.1.0"`)
27
22
 
28
- ##Set up an Application to use opsicle
23
+ **Why the extra `curses` gem for Ruby 2.1.0+?**
24
+ Opsicle uses [curses](http://en.wikipedia.org/wiki/Curses_(programming_library)).
25
+ Ruby's library to interface with curses was [removed from stdlib in Ruby 2.1.0](https://bugs.ruby-lang.org/issues/8584).
26
+ [The new curses gem](https://github.com/ruby/curses) is not backwards compatible, so in an effort to keep this gem
27
+ friendly with all current Ruby versions we don't list it as a dependency in Opsicle's gemspec - doing so would cause
28
+ errors for Ruby 1.9.3 users.
29
+ Ruby >=2.1.0 will likely be enforced sometime in 2014; [certainly by February 2015](https://www.ruby-lang.org/en/news/2014/01/10/ruby-1-9-3-will-end-on-2015/).
30
+
31
+ ### Set up an Application to use opsicle
29
32
 
30
33
  ```yaml
31
34
  # your_app_root/.opsicle
@@ -49,4 +52,28 @@ production:
49
52
  aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY
50
53
  ```
51
54
 
55
+ ## Using Opsicle
56
+
57
+ Run `opsicle help` for a full list of commands and their uses.
58
+ Some common commands:
59
+
60
+ ```bash
61
+
62
+ # Run a basic deploy for the current app
63
+ opsicle deploy staging
64
+
65
+ # Run the deploy for production
66
+ opsicle deploy production
52
67
 
68
+ # SSH to a server instance in the given environment stack
69
+ opsicle ssh staging
70
+
71
+ # Set your user SSH key (PUBLIC KEY) for OpsWorks
72
+ opsicle ssh-key staging <key-file>
73
+
74
+ # Launch the Opsicle Stack Monitor for the given environment stack
75
+ opsicle monitor staging
76
+
77
+ ```
78
+
79
+ Opsicle accepts a `--verbose` flag to show additional information as commands are run.
data/bin/opsicle CHANGED
@@ -16,7 +16,8 @@ default_command :help
16
16
 
17
17
  command :deploy do |c|
18
18
  c.syntax = "opsicle deploy <environment>"
19
- c.description = "Deploy your current app to the given OpsWorks stack"
19
+ c.description = "Deploy your current app to the given environment stack"
20
+ c.option "--browser", "Open OpsWorks deployments screen instead of Opsicle Stack Monitor"
20
21
  c.action do |args, options|
21
22
  raise ArgumentError, "Environment is required" unless args.first
22
23
  Opsicle::Deploy.new(args.first).execute(options.__hash__)
@@ -25,7 +26,7 @@ end
25
26
 
26
27
  command :list do |c|
27
28
  c.syntax = "opsicle list <environment>"
28
- c.description = "List all apps in the given environment"
29
+ c.description = "List all apps in the given environment stack"
29
30
  c.action do |args, options|
30
31
  raise ArgumentError, "Environment is required" unless args.first
31
32
  Opsicle::List.new(args.first).execute(options.__hash__)
@@ -34,9 +35,41 @@ end
34
35
 
35
36
  command :ssh do |c|
36
37
  c.syntax = "opsicle ssh <environment>"
37
- c.description = "SSH access to instances in the given Opsworks stack"
38
+ c.description = "SSH access to instances in the given environment stack"
38
39
  c.action do |args, options|
39
40
  raise ArgumentError, "Environment is required" unless args.first
40
41
  Opsicle::SSH.new(args.first).execute(options.__hash__)
41
42
  end
42
43
  end
44
+
45
+ command 'ssh-key' do |c|
46
+ c.syntax = "opsicle ssh-key <environment> <key-file>"
47
+ c.description = "Set your user SSH key (PUBLIC KEY) for OpsWorks"
48
+ c.action do |args, options|
49
+ raise ArgumentError, "Environment is required" unless args.first
50
+ raise ArgumentError, "ssh public key-file is required" unless args[1]
51
+ Opsicle::SSHKey.new(*args).execute(options.__hash__)
52
+ end
53
+ end
54
+
55
+ command 'monitor' do |c|
56
+ c.syntax = "opsicle monitor <environment>"
57
+ c.description = "Launch the Opsicle Stack Monitor for the given environment stack"
58
+ c.action do |args, options|
59
+ raise ArgumentError, "Environment is required" unless args.first
60
+
61
+ @monitor = Opsicle::Monitor::App.new(args.first, options.__hash__)
62
+
63
+ begin
64
+ @monitor.start
65
+ rescue => e
66
+ say "<%= color('Uh oh, an error occurred while starting the Opsicle Stack Monitor.', RED) %>"
67
+ say "<%= color('Use --trace to view stack trace.', RED) %>"
68
+
69
+ if options.trace
70
+ raise
71
+ end
72
+ end
73
+ end
74
+ end
75
+ alias_command :'top', :'monitor'
data/lib/opsicle.rb CHANGED
@@ -5,7 +5,6 @@ Signal.trap("INT") do
5
5
  end
6
6
 
7
7
  require "opsicle/version"
8
- require "opsicle/deploy"
9
- require "opsicle/list"
10
- require "opsicle/ssh"
8
+ require "opsicle/commands"
9
+ require "opsicle/monitor"
11
10
 
@@ -0,0 +1,6 @@
1
+ require 'opsicle/client'
2
+
3
+ require "opsicle/commands/deploy"
4
+ require "opsicle/commands/list"
5
+ require "opsicle/commands/ssh"
6
+ require "opsicle/commands/ssh_key"
@@ -0,0 +1,39 @@
1
+ module Opsicle
2
+ class Deploy
3
+ attr_reader :client
4
+
5
+ def initialize(environment)
6
+ @environment = environment
7
+ @client = Client.new(environment)
8
+ end
9
+
10
+ def execute(options={})
11
+ response = client.run_command('deploy')
12
+
13
+ if options[:browser]
14
+ open_deploy(response[:deployment_id])
15
+ else
16
+ @monitor = Opsicle::Monitor::App.new(@environment, options)
17
+
18
+ begin
19
+ @monitor.start
20
+ rescue => e
21
+ say "<%= color('Uh oh, an error occurred while starting the Opsicle Stack Monitor.', RED) %>"
22
+ say "<%= color('Use --trace to view stack trace.', RED) %>"
23
+
24
+ if options.trace
25
+ raise
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ def open_deploy(deployment_id)
32
+ if deployment_id
33
+ exec "open 'https://console.aws.amazon.com/opsworks/home?#/stack/#{client.config.opsworks_config[:stack_id]}/deployments'"
34
+ else
35
+ puts 'deploy failed'
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,6 +1,4 @@
1
- require 'aws-sdk'
2
1
  require 'terminal-table'
3
- require_relative 'client'
4
2
 
5
3
  module Opsicle
6
4
  class List
@@ -1,6 +1,3 @@
1
- require 'aws-sdk'
2
- require_relative 'client'
3
-
4
1
  module Opsicle
5
2
  class SSH
6
3
  attr_reader :client
@@ -0,0 +1,40 @@
1
+ module Opsicle
2
+ class SSHKey
3
+
4
+ def initialize(environment, keyfile)
5
+ @client = Client.new(environment)
6
+ @keyfile = keyfile
7
+ end
8
+
9
+ def execute(options={})
10
+ validate!
11
+ update
12
+ say "ssh-key updated successfully"
13
+ end
14
+
15
+ def validate!
16
+ raise KeyFileNotFound, "No key file could be found" unless File.exists?(@keyfile)
17
+ raise InvalidKeyFile, "Key file is invalid" unless valid_key_file?
18
+ raise InvalidKeyFile, "Key file is a private key" unless public_key?
19
+ end
20
+
21
+ def valid_key_file?
22
+ system("ssh-keygen -l -f #{@keyfile} > /dev/null")
23
+ end
24
+
25
+ def public_key?
26
+ !key.match(/PRIVATE KEY/)
27
+ end
28
+
29
+ def key
30
+ @key ||= File.read(@keyfile)
31
+ end
32
+
33
+ def update
34
+ @client.api_call(:update_my_user_profile, {ssh_public_key: key})
35
+ end
36
+
37
+ end
38
+ KeyFileNotFound = Class.new(StandardError)
39
+ InvalidKeyFile = Class.new(StandardError)
40
+ end
@@ -1,4 +1,5 @@
1
1
  require 'yaml'
2
+ require 'aws-sdk'
2
3
 
3
4
  module Opsicle
4
5
  class Config
@@ -0,0 +1,59 @@
1
+ module Opsicle
2
+ class Deployment
3
+
4
+ def initialize(deployment_id, client)
5
+ @deployment_id = deployment_id
6
+ @client = client
7
+ end
8
+
9
+ def deployment_id
10
+ deployment[:deployment_id]
11
+ end
12
+
13
+ def stack_id
14
+ deployment[:stack_id]
15
+ end
16
+
17
+ def app_id
18
+ deployment[:app_id]
19
+ end
20
+
21
+ def created_at
22
+ deployment[:created_at]
23
+ end
24
+
25
+ def completed_at
26
+ deployment(reload: true)[:completed_at]
27
+ end
28
+
29
+ def duration
30
+ deployment(reload: true)[:duration]
31
+ end
32
+
33
+ def command
34
+ deployment[:command]
35
+ end
36
+
37
+ def status
38
+ deployment(reload: true)[:status]
39
+ end
40
+
41
+ def instance_ids
42
+ deployment[:instance_ids]
43
+ end
44
+
45
+ %w(running successful failed).each do |status_name|
46
+ define_method("#{status_name}?") { status == status_name }
47
+ end
48
+
49
+ def deployment(options={})
50
+ # Only call the API again if you need to
51
+ @deployment = nil if options[:reload]
52
+ @deployment ||= @client.api_call('describe_deployments',
53
+ :deployment_ids => [@deployment_id]
54
+ )[:deployments].first
55
+ end
56
+ private :deployment
57
+
58
+ end
59
+ end
@@ -0,0 +1,22 @@
1
+ module Opsicle
2
+ class Deployments
3
+
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def data
9
+ deployments(reload: true)
10
+ end
11
+
12
+ def deployments(options={})
13
+ # Only call the API again if you need to
14
+ @deployments = nil if options[:reload]
15
+ @deployments ||= @client.api_call('describe_deployments',
16
+ :stack_id => @client.config.opsworks_config[:stack_id]
17
+ )[:deployments]
18
+ end
19
+ private :deployments
20
+
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ require "opsicle/monitor/app"
2
+ require "opsicle/monitor/panel"
3
+ require "opsicle/monitor/subpanel"
4
+ require "opsicle/monitor/screen"
5
+ require "opsicle/monitor/translatable"
6
+
7
+ require "opsicle/monitor/panels/header"
8
+ require "opsicle/monitor/panels/deployments"
9
+ require "opsicle/monitor/panels/help"
10
+
11
+ require "opsicle/monitor/spy/dataspyable"
12
+ require "opsicle/monitor/spy/deployments"
@@ -0,0 +1,147 @@
1
+ # Credit where credit is due:
2
+ # The Monitor module's architecture and many of its classes are heavily based on
3
+ # the work of tiredpixel's sidekiq-spy gem: https://github.com/tiredpixel/sidekiq-spy
4
+ # His help in working with the Ruby curses library has been invaluable - thanks tiredpixel!
5
+
6
+ require 'opsicle/client'
7
+
8
+ module Opsicle
9
+ module Monitor
10
+ class App
11
+ API_POLLING_INTERVAL = 10
12
+ SCREEN_REFRESH_INTERVAL = 5
13
+
14
+ attr_reader :running
15
+ attr_reader :restarting
16
+
17
+ class << self
18
+ attr_accessor :client
19
+ end
20
+
21
+ def initialize(environment, options)
22
+ @running = false
23
+ @restarting = false
24
+ @threads = {}
25
+
26
+ # Make client with correct configuration available to monitor spies
27
+ App.client = Client.new(environment)
28
+ end
29
+
30
+ def start
31
+ begin
32
+ @running = true
33
+
34
+ setup
35
+
36
+ @threads[:command] ||= Thread.new do
37
+ command_loop # listen for commands
38
+ end
39
+
40
+ @threads[:refresh_screen] ||= Thread.new do
41
+ refresh_screen_loop # refresh frequently
42
+ end
43
+
44
+ @threads[:refresh_data] ||= Thread.new do
45
+ refresh_data_loop # refresh not so frequently
46
+ end
47
+
48
+ @threads.each { |tname, t| t.join }
49
+ ensure
50
+ cleanup
51
+ end
52
+ end
53
+
54
+ def stop
55
+ @running = false
56
+ @screen.close
57
+ @screen = nil # Ruby curses lib doesn't have closed?(), so we set to nil, just in case
58
+
59
+ exit 0
60
+ end
61
+
62
+ def restart
63
+ @restarting = true
64
+ end
65
+
66
+ def do_command(key)
67
+ command = { q: :stop,
68
+ h: [:set_screen, :help],
69
+ b: :open_opsworks_browser,
70
+ d: [:set_screen, :deployments] }[key.to_sym]
71
+ command ||= :invalid_input
72
+
73
+ send *command unless command == :invalid_input
74
+
75
+ wakey_wakey # wake threads for immediate response
76
+ end
77
+
78
+ private
79
+
80
+ def set_screen(screen)
81
+ @screen.panel_main = screen
82
+ end
83
+
84
+ def setup
85
+ @screen = Monitor::Screen.new
86
+ end
87
+
88
+ def cleanup
89
+ @screen.close if @screen
90
+ end
91
+
92
+ def wakey_wakey
93
+ @threads.each { |tname, t| t.run if t.status == 'sleep' }
94
+ end
95
+
96
+ def command_loop
97
+ while @running do
98
+ next unless @screen # #refresh_loop might be reattaching screen
99
+
100
+ key = @screen.next_key
101
+
102
+ next unless key # keep listening if timeout
103
+
104
+ do_command(key)
105
+ end
106
+ end
107
+
108
+ def refresh_screen_loop
109
+ while @running do
110
+ next unless @screen # HACK: only certain test scenarios?
111
+
112
+ if @restarting || @screen.missized? # signal(s) or whilst still resizing
113
+ panel_main = @screen.panel_main
114
+
115
+ cleanup
116
+
117
+ setup
118
+
119
+ @screen.panel_main = panel_main
120
+
121
+ @restarting = false
122
+ end
123
+
124
+ @screen.refresh
125
+
126
+ sleep SCREEN_REFRESH_INTERVAL # go to sleep; could be rudely awoken on quit
127
+ end
128
+ end
129
+
130
+ # This loop is specifically separate from the screen loop
131
+ # because we don't want to spam OpWorks with API calls every second.
132
+ def refresh_data_loop
133
+ while @running do
134
+ next unless @screen # HACK: only certain test scenarios?
135
+
136
+ @screen.refresh_spies
137
+
138
+ sleep API_POLLING_INTERVAL
139
+ end
140
+ end
141
+
142
+ def open_opsworks_browser
143
+ %x(open 'https://console.aws.amazon.com/opsworks/home?#/stack/#{App.client.config.opsworks_config[:stack_id]}')
144
+ end
145
+ end
146
+ end
147
+ end