-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add tfutil.jsonify to make it easier to call out from terraform data.…
…external
- Loading branch information
Showing
4 changed files
with
223 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
require_relative 'base_command' | ||
require 'json' | ||
require 'open3' | ||
require 'clipboard' | ||
|
||
module SimplyGenius | ||
module Atmos | ||
module Commands | ||
|
||
class TfUtil < BaseCommand | ||
|
||
def self.description | ||
"Useful utilities when calling out from terraform with data.external" | ||
end | ||
|
||
subcommand "jsonify", "Manages json on stdin/out to conform\nto use in terraform data.external" do | ||
|
||
banner "Ensures json output only contains a single level Hash with string values (e.g. when execing curl returns a deep json hash of mixed values)" | ||
|
||
option ["-a", "--atmos_config"], | ||
:flag, "Includes the atmos config in the\nhash from parsing json on stdin" | ||
|
||
option ["-c", "--clipboard"], | ||
:flag, "Copies the actual command used\nto the clipboard to allow external debugging" | ||
|
||
option ["-j", "--json"], | ||
:flag, "The command output is parsed as json" | ||
|
||
option ["-x", "--[no-]exit"], | ||
:flag, "Exit with the command's exit code\non failure (or not)", default: true | ||
|
||
parameter "COMMAND ...", | ||
"The command to call", :attribute_name => :command | ||
|
||
# Recursively converts all values to strings as required by terraform data.external | ||
def stringify(obj) | ||
case obj | ||
when Hash | ||
Hash[obj.collect {|k, v| [k, stringify(v)] }] | ||
when Array | ||
obj.collect {|v| stringify(v) } | ||
else | ||
obj.to_s | ||
end | ||
end | ||
|
||
# Makes a hash have only a single level as required by terraform data.external | ||
def flatten(obj) | ||
result = {} | ||
|
||
if obj.is_a? Hash | ||
obj.each do |k, v| | ||
ev = case v | ||
when String | ||
v | ||
when Hash, Array | ||
JSON.generate(v) | ||
else | ||
v.to_s | ||
end | ||
result[k] = ev | ||
end | ||
else | ||
result["data"] = JSON.generate(result) | ||
end | ||
|
||
return result | ||
end | ||
|
||
def maybe_read_stdin | ||
data = nil | ||
begin | ||
chunk = $stdin.read_nonblock(1) | ||
data = chunk + $stdin.read | ||
logger.debug("Received stdin: " + data) | ||
rescue Errno::EAGAIN | ||
data = nil | ||
logger.debug("No stdin") | ||
end | ||
return data | ||
end | ||
|
||
def execute | ||
params = JSON.parse(maybe_read_stdin || '{}') | ||
params = SettingsHash.new(params) | ||
params.enable_expansion = true | ||
if atmos_config? | ||
params = Atmos.config.config_merge(SettingsHash.new(Atmos.config.to_h), params) | ||
end | ||
expanded_command = command.collect {|c| params.expand_string(c) } | ||
|
||
begin | ||
formatted_command = expanded_command.collect {|a| "'#{a}'" }.join(" ") | ||
logger.debug("Running command: #{formatted_command}") | ||
Clipboard.copy(formatted_command) if clipboard? | ||
|
||
cmd_opts = {} | ||
cmd_opts[:stdin_data] = params[:stdin] if params.key?(:stdin) | ||
stdout, stderr, status = Open3.capture3(*expanded_command, **cmd_opts) | ||
result = {stdout: stdout, stderr: stderr, exitcode: status.exitstatus.to_s} | ||
logger.debug("Command result: #{result.inspect}") | ||
|
||
if exit? && status.exitstatus != 0 | ||
$stderr.puts stdout | ||
$stderr.puts stderr | ||
exit status.exitstatus | ||
end | ||
|
||
if json? | ||
result = result.merge(flatten(stringify(JSON.parse(stdout)))) | ||
end | ||
|
||
logger.debug("Json output: #{result.inspect}") | ||
$stdout.puts JSON.generate(result) | ||
|
||
rescue => e | ||
$stderr.puts("#{e.class}: #{e.message}") | ||
exit 1 | ||
end | ||
|
||
end | ||
|
||
end | ||
|
||
end | ||
|
||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
require "simplygenius/atmos/commands/tfutil" | ||
|
||
module SimplyGenius | ||
module Atmos | ||
module Commands | ||
|
||
describe TfUtil do | ||
|
||
let(:cli) { described_class.new("") } | ||
let(:okstatus) { double(Process::Status, exitstatus: 0) } | ||
let(:failstatus) { double(Process::Status, exitstatus: 1) } | ||
|
||
describe "jsonify" do | ||
|
||
it "runs the given command" do | ||
cmd = %w(foo bar) | ||
expect(Clipboard).to_not receive(:copy) | ||
expect(Open3).to receive(:capture3).with(*cmd, {}).and_return(["so", "se", okstatus]) | ||
expect{cli.run(["jsonify", *cmd])}.to output(JSON.generate(stdout: "so", stderr: "se", exitcode: "0") + "\n").to_stdout | ||
end | ||
|
||
it "parses stdin as json and makes it available for interpolation in the given command" do | ||
cmd = %w(foo #{bar}) | ||
expect(Open3).to receive(:capture3).with("foo", "bum", {}).and_return(["so", "se", okstatus]) | ||
expect { | ||
simulate_stdin(JSON.generate(bar: "bum")) { | ||
cli.run(["jsonify", *cmd]) | ||
} | ||
}.to output(JSON.generate(stdout: "so", stderr: "se", exitcode: "0") + "\n").to_stdout | ||
end | ||
|
||
it "extracts stdin key from json params and gives it as stdin to the command" do | ||
cmd = %w(foo bar) | ||
expect(Open3).to receive(:capture3).with(*cmd, stdin_data: "hello").and_return(["so", "se", okstatus]) | ||
expect { | ||
simulate_stdin(JSON.generate(stdin: "hello")) { | ||
cli.run(["jsonify", *cmd]) | ||
} | ||
}.to output(JSON.generate(stdout: "so", stderr: "se", exitcode: "0") + "\n").to_stdout | ||
end | ||
|
||
it "includes atmos config in params hash" do | ||
cmd = %w(foo #{atmos_env}) | ||
begin | ||
Atmos.config = Config.new("ops") | ||
expect(Open3).to receive(:capture3).with("foo", "ops", {}).and_return(["so", "se", okstatus]) | ||
expect{cli.run(["jsonify", "-a", *cmd])}.to output.to_stdout | ||
ensure | ||
Atmos.config = nil | ||
end | ||
end | ||
|
||
it "provides command output within json" do | ||
cmd = %w(foo bar) | ||
expect(Open3).to receive(:capture3).with(*cmd, {}).and_return(['{"hum":"dum"}', "", okstatus]) | ||
expect{cli.run(["jsonify", *cmd])}.to output("{\"stdout\":\"{\\\"hum\\\":\\\"dum\\\"}\",\"stderr\":\"\",\"exitcode\":\"0\"}\n").to_stdout | ||
end | ||
|
||
it "provides command output as json" do | ||
cmd = %w(foo bar) | ||
expect(Open3).to receive(:capture3).with(*cmd, {}).and_return(['{"hum":"dum"}', "", okstatus]) | ||
expect{cli.run(["jsonify", "-j", *cmd])}.to output("{\"stdout\":\"{\\\"hum\\\":\\\"dum\\\"}\",\"stderr\":\"\",\"exitcode\":\"0\",\"hum\":\"dum\"}\n").to_stdout | ||
end | ||
|
||
it "exits on error by default" do | ||
cmd = %w(foo bar) | ||
expect(Open3).to receive(:capture3).with(*cmd, {}).and_return(["", "", failstatus]) | ||
expect{cli.run(["jsonify", *cmd])}.to raise_error(SystemExit) | ||
end | ||
|
||
it "disables exits on error" do | ||
cmd = %w(foo bar) | ||
expect(Open3).to receive(:capture3).with(*cmd, {}).and_return(["", "", failstatus]) | ||
expect{cli.run(["jsonify", "--no-exit", *cmd])}.to output.to_stdout | ||
end | ||
|
||
it "can copy command to clipboard" do | ||
cmd = %w(foo bar) | ||
expect(Clipboard).to receive(:copy).with("'foo' 'bar'") | ||
expect(Open3).to receive(:capture3).with(*cmd, {}).and_return(["", "", okstatus]) | ||
expect{cli.run(["jsonify", "-c", *cmd])}.to output.to_stdout | ||
end | ||
|
||
end | ||
|
||
end | ||
|
||
end | ||
end | ||
end |