Skip to content

Commit

Permalink
Merge pull request #76 from sportngin/update
Browse files Browse the repository at this point in the history
Adding update command.
  • Loading branch information
bbergstrom committed Mar 30, 2015
2 parents 9e8f957 + aadcb36 commit 92f4930
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 6 deletions.
19 changes: 15 additions & 4 deletions README.markdown
Expand Up @@ -57,7 +57,11 @@ production:

## Using Opsicle

Run `opsicle help` for a full list of commands and their uses.
Run `opsicle help` for a full list of commands and their uses.

Opsicle accepts a `--verbose` flag or the VERBOSE environment variable to show additional information as commands are run.
Opsicle accepts a DEBUG environment variable to show additional logging such as stack traces for failed commands.

Some common commands:

### Deployments
Expand Down Expand Up @@ -99,7 +103,14 @@ opsicle monitor staging
This command accepts a --path argument to the directory of cookbooks to upload. It defaults to 'cookbooks'.
It also accepts a --bucket-name for the base s3 bucket. This flag is required.

### Update
Update an OpsWorks resource like a stack, layer or app with a given set of property values.
The values can be passed as inline JSON or as a path to a YAML file.
Naming and value format needs to follow what is defined for the [AWS Ruby SDK](http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/OpsWorks/Client.html).
Upon successful execution a table of the resulting changes is printed to stdout.

Opsicle accepts a `--verbose` flag or the VERBOSE environment variable to show additional information as commands are run.

Opsicle accepts a DEBUG environment variable to show additional logging such as stack traces for failed commands.
For example:
```
opsicle --debug update staging stack -j '{"use_opsworks_security_groups":false, "custom_json":"{\"foo\":5}"}'
opsicle --debug update staging app -y app.yml
```
27 changes: 27 additions & 0 deletions bin/opsicle
Expand Up @@ -2,6 +2,8 @@
require 'rubygems'
require 'gli'
require 'opsicle'
require 'json'
require 'yaml'

include GLI::App

Expand Down Expand Up @@ -181,4 +183,29 @@ command :instances do |c|
end
end


desc "Update properties on a OpsWorks resource."
arg_name '<environment> <type>'
command 'update' do |c|
valid_types = %w[app elastic_ip instance layer my_user_profile rds_db_instance stack user_profile volume]
c.flag [:j, :json], :desc => 'JSON of values to update.', :type => String
c.flag [:y, :yaml_file], :desc => 'YAML file of values to update.', :type => String

c.action do |global_options, options, args|
raise ArgumentError, "Environment is required" unless args.first
raise ArgumentError, "Resource type is required" unless args[1]
raise ArgumentError, "Invalid type: #{args[1]}. Valid types: #{valid_types}" unless valid_types.include?(args[1])
if options[:json]
values = JSON.parse(options[:json])
elsif options[:yaml_file]
yaml_file = File.expand_path(options[:yaml_file])
values = YAML.load_file(yaml_file)
else
raise ArgumentError, "Values required in JSON or YAML flag."
end

Opsicle::Update.new(*args).execute(values, global_options.merge(options))
end
end

exit run(ARGV)
1 change: 1 addition & 0 deletions lib/opsicle/commands.rb
Expand Up @@ -5,6 +5,7 @@
require "opsicle/commands/execute_recipes"
require "opsicle/commands/list"
require "opsicle/commands/list_instances"
require "opsicle/commands/update"
require "opsicle/commands/ssh"
require "opsicle/commands/ssh_key"
require "opsicle/commands/ssh_clean_keys"
57 changes: 57 additions & 0 deletions lib/opsicle/commands/update.rb
@@ -0,0 +1,57 @@
require 'hashdiff'
require 'opsicle/output'

module Opsicle
class Update
attr_reader :client, :type

def initialize(environment, type)
@client = Client.new(environment)
@type = type
end

def execute(values, options)
before = describe
update(values)
after = describe
print(before, after)
end

def describe
api_method = "describe_#{@type}s"
api_opts = {
:"#{@type}_ids" => [client.config.opsworks_config[:"#{@type}_id"]]
}
client.api_call(api_method, api_opts)[:"#{@type}s"][0]
end

def update(values)
api_method = "update_#{@type}"
api_opts = values.merge(:"#{@type}_id" => client.config.opsworks_config[:"#{@type}_id"])
client.api_call(api_method, api_opts)
end

def print(before, after)
diff = HashDiff.diff(before, after)
Output.say("Changes: #{diff.size}")
Output.terminal.say(Terminal::Table.new headings: %w[Change Key Before After], rows: format_diff(diff)) if diff.size > 0
end

def format_diff(diff)
diff.map { |change|
case change[0]
when '-'
change.insert(3, nil)
change.map! { |i| Output.format(i, :removal) }
when '+'
change.insert(2, nil)
change.map! { |i| Output.format(i, :addition) }
when '~'
change.map! { |i| Output.format(i, :modification) }
end
change
}
end

end
end
11 changes: 9 additions & 2 deletions lib/opsicle/output.rb
Expand Up @@ -15,14 +15,21 @@ def self.color_scheme
:verbose => [:bold, :magenta],
:debug => [:bold, :cyan],
:success => [:bold, :green],
:addition => [:bold, :green],
:removal => [:bold, :red],
:modification => [:bold, :yellow],
)
end

def self.say(msg, log_style=:normal)
terminal.say format(msg, log_style)
end

def self.format(msg, log_style=:normal)
if $color
terminal.say "<%= color('#{msg}', '#{log_style}') %>"
terminal.color(msg.to_s, log_style)
else
terminal.say msg
msg
end
end

Expand Down
1 change: 1 addition & 0 deletions opsicle.gemspec
Expand Up @@ -24,6 +24,7 @@ Gem::Specification.new do |spec|
spec.add_dependency "highline", "~> 1.6"
spec.add_dependency "terminal-table", "~> 1.4"
spec.add_dependency "minitar", "~> 0.5"
spec.add_dependency "hashdiff", "~> 0.2"

spec.add_development_dependency "bundler", "~> 1.3"
spec.add_development_dependency "rake", "~> 10.1"
Expand Down
71 changes: 71 additions & 0 deletions spec/opsicle/commands/update_spec.rb
@@ -0,0 +1,71 @@
require "spec_helper"
require "opsicle"

module Opsicle
describe Update do
subject { Update.new("env", "type") }
let(:client) { double }
let(:env) { "env" }
let(:type) { "type" }
let(:values) { { :foo => "bar" } }

before do
allow(Client).to receive(:new).with('env') { client }
allow(client).to receive_message_chain("config.opsworks_config.[]") { 123 }
end

context "#execute" do
it "should update and print results" do
allow(subject).to receive(:describe) { "tacos" }
expect(subject).to receive(:print).with("tacos", "tacos")
api_opts = values.merge(:type_id => 123)
expect(client).to receive(:api_call).with("update_type", api_opts)
subject.execute(values, api_opts)
end
end

context "#describe" do
it "should return data for type" do
expect(client).to receive(:api_call).with("describe_types", :type_ids => [123]).and_return(:types => [])
subject.describe
end
end

context "#update" do
it "should update values for type" do
api_opts = values.merge(:type_id => 123)
expect(client).to receive(:api_call).with("update_type", api_opts)
subject.update(values)
end
end

context "#print" do
it "should print no changes without table" do
allow(HashDiff).to receive(:diff) { [] }
expect(Output).to receive(:say).with("Changes: 0") { nil }
expect(Output).to_not receive(:terminal)
subject.print(nil, nil)
end
it "should print changes with table" do
allow(HashDiff).to receive(:diff) { [%w[- nyan 1], %w[+ cat 2],%w[~ taco 3 4]] }
expect(Output).to receive(:say).with("Changes: 3") { nil }
expect(Output).to receive_message_chain("terminal.say")
subject.print(nil, nil)
end
end

context "#format_diff" do
let(:diff) { [%w[- nyan 1], %w[+ cat 2],%w[~ taco 3 4]] }
let(:formatted_diff) {
[%w[r- rnyan r1 r], %w[a+ acat a a2], %w[m~ mtaco m3 m4]]
}

it "should align columns and colorize" do
allow(Output).to receive(:format).with(anything, :removal) { |arg| "r#{arg}"}
allow(Output).to receive(:format).with(anything, :addition) { |arg| "a#{arg}"}
allow(Output).to receive(:format).with(anything, :modification) { |arg| "m#{arg}"}
expect(subject.format_diff(diff)).to eq(formatted_diff)
end
end
end
end
61 changes: 61 additions & 0 deletions spec/opsicle/output_spec.rb
@@ -0,0 +1,61 @@
require "spec_helper"
require "opsicle"

module Opsicle
describe Output do
subject { Output }
let(:terminal) { double(:say => nil, :color => nil) }
let(:msg) { "message" }
let(:colored_msg) { "COLOURmessageCOLOUR" }

before do
allow(subject).to receive(:terminal).and_return(terminal)
$color = true
$verbose = false
end

context "#say" do
it "should say a formatted message" do
allow(terminal).to receive(:color).and_return(colored_msg)
expect(terminal).to receive(:say).with(colored_msg)
subject.say(msg)
end
it "should say a message without color" do
$color = false
expect(terminal).to receive(:say).with(msg)
subject.say(msg)
end
end

context "#format" do
it "should color message" do
allow(terminal).to receive(:color).and_return(colored_msg)
expect(subject.format(msg)).to eq(colored_msg)
end
it "should not color message" do
$color = false
expect(subject.format(msg)).to eq(msg)
end
end

context "#say_verbose" do
it "should not say a verbose message" do
expect(terminal).to_not receive(:say)
subject.say_verbose(msg)
end
it "should say a verbose message" do
$verbose = true
expect(terminal).to receive(:say)
subject.say_verbose(msg)
end
end

context "#ask" do
it "should ask" do
expect(terminal).to receive(:ask)
subject.ask
end
end

end
end

0 comments on commit 92f4930

Please sign in to comment.