Skip to content

Commit

Permalink
add tfutil.jsonify to make it easier to call out from terraform data.…
Browse files Browse the repository at this point in the history
…external
  • Loading branch information
wr0ngway committed Dec 9, 2019
1 parent 9e6176f commit c9bfdf5
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 2 deletions.
2 changes: 2 additions & 0 deletions lib/simplygenius/atmos/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ def default_color?
Commands::AuthExec
subcommand "container", "Container tools",
Commands::Container
subcommand "tfutil", "Terraform tools",
Commands::TfUtil

subcommand "version", "Display version" do
def execute
Expand Down
129 changes: 129 additions & 0 deletions lib/simplygenius/atmos/commands/tfutil.rb
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
4 changes: 2 additions & 2 deletions lib/simplygenius/atmos/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,6 @@ def save_user_config_file(data, merge_to_existing: true)
File.chmod(0600, user_config_file)
end

private

def config_merge(lhs, rhs, debug_state=[])
result = nil

Expand Down Expand Up @@ -168,6 +166,8 @@ def config_merge(lhs, rhs, debug_state=[])
return result
end

private

def load_config_sources(relative_root, config, *patterns)
patterns.each do |pattern|
logger.debug("Loading atmos config files using pattern: #{pattern}")
Expand Down
90 changes: 90 additions & 0 deletions spec/commands/tfutil_spec.rb
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

0 comments on commit c9bfdf5

Please sign in to comment.