opsicle 0.1.0 → 0.2.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.
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