razor-client 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/NEWS.md +13 -0
- data/bin/razor +3 -2
- data/lib/razor/cli/command.rb +41 -36
- data/lib/razor/cli/format.rb +28 -13
- data/lib/razor/cli/navigate.rb +9 -1
- data/lib/razor/cli/parse.rb +42 -21
- data/lib/razor/cli/table_format.rb +1 -1
- data/lib/razor/cli/version.rb +1 -1
- data/spec/cli/command_spec.rb +96 -0
- data/spec/cli/navigate_spec.rb +25 -0
- data/spec/cli/parse_spec.rb +11 -0
- data/spec/fixtures/vcr/Razor_CLI_Navigate/argument_formatting/should_allow_single-dash_with_single_character_flag.yml +203 -0
- data/spec/fixtures/vcr/Razor_CLI_Navigate/argument_formatting/should_not_allow_double-dash_with_single_character_flag.yml +133 -0
- data/spec/fixtures/vcr/Razor_CLI_Navigate/argument_formatting/should_not_allow_single-dash_with_multiple_character_flag.yml +133 -0
- data/spec/fixtures/vcr/Razor_CLI_Navigate/positional_arguments/should_allow_the_use_of_positional_arguments.yml +681 -0
- data/spec/fixtures/vcr/Razor_CLI_Navigate/positional_arguments/should_fail_with_too_many_positional_arguments.yml +133 -0
- metadata +12 -2
data/NEWS.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
# Razor Client Release Notes
|
2
2
|
|
3
|
+
## 1.2.0 - 2016-03-08
|
4
|
+
|
5
|
+
* BUGFIX: Razor client version will be reported even if the Razor server is
|
6
|
+
unreachable.
|
7
|
+
* BUGFIX: Fixed insecure flag when supplied in addition to a server URL.
|
8
|
+
* NEW: Added positional argument support, when supplied by the server. See
|
9
|
+
`razor <command> --help` for details on usage.
|
10
|
+
* NEW: Added USAGE section to the command's help, which will include positional
|
11
|
+
arguments, if any exist.
|
12
|
+
* IMPROVEMENT: Proper short form argument style is now followed.
|
13
|
+
Single-character arguments now require a single dash, e.g. `-c`.
|
14
|
+
* IMPROVEMENT: Error messaging for SSL issues has been improved.
|
15
|
+
|
3
16
|
## 1.1.0 - 2015-11-12
|
4
17
|
|
5
18
|
* IMPROVEMENT: By default, `razor` will point to port 8150.
|
data/bin/razor
CHANGED
data/lib/razor/cli/command.rb
CHANGED
@@ -1,54 +1,60 @@
|
|
1
1
|
class Razor::CLI::Command
|
2
|
-
def initialize(parse, navigate,
|
3
|
-
@
|
2
|
+
def initialize(parse, navigate, command, segments, cmd_url)
|
3
|
+
@dump_response = parse && parse.dump_response?
|
4
|
+
@show_command_help = parse && parse.show_command_help?
|
4
5
|
@navigate = navigate
|
5
|
-
@
|
6
|
+
@command = command
|
7
|
+
@cmd_schema = command ? command['schema'] : nil
|
8
|
+
@cmd_url = cmd_url
|
6
9
|
@segments = segments
|
7
10
|
end
|
8
11
|
|
9
12
|
def run
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
# Ensure that we copy authentication data from our previous URL.
|
14
|
-
url = URI.parse(cmd["id"])
|
15
|
-
if @doc_resource
|
16
|
-
url = URI.parse(url.to_s)
|
17
|
-
end
|
18
|
-
|
19
|
-
if @parse.show_command_help?
|
20
|
-
@navigate.json_get(url)
|
13
|
+
body = extract_command
|
14
|
+
if @show_command_help
|
15
|
+
@command
|
21
16
|
else
|
22
17
|
if body.empty?
|
23
18
|
raise Razor::CLI::Error,
|
24
19
|
"No arguments for command (did you forget --json ?)"
|
25
20
|
end
|
26
|
-
result = @navigate.json_post(
|
21
|
+
result = @navigate.json_post(@cmd_url, body)
|
27
22
|
# Get actual object from the id.
|
28
23
|
result = result.merge(@navigate.json_get(URI.parse(result['id']))) if result['id']
|
29
24
|
result
|
30
25
|
end
|
31
26
|
end
|
32
27
|
|
33
|
-
def command(name)
|
34
|
-
@command ||= @commands.find { |coll| coll["name"] == name }
|
35
|
-
end
|
36
|
-
|
37
28
|
def extract_command
|
38
|
-
cmd = command(@segments.shift)
|
39
|
-
@cmd_url = URI.parse(cmd['id'])
|
40
|
-
@cmd_schema = cmd_schema(@cmd_url)
|
41
29
|
body = {}
|
30
|
+
pos_index = 0
|
42
31
|
until @segments.empty?
|
43
32
|
argument = @segments.shift
|
44
|
-
if argument =~ /\A--([a-z-]
|
45
|
-
|
46
|
-
arg
|
33
|
+
if argument =~ /\A--([a-z-]{2,})(=(.+))?\Z/ or
|
34
|
+
argument =~ /\A-([a-z])(=(.+))?\Z/
|
35
|
+
# `--arg=value`/`--arg value`
|
36
|
+
# `-a=value`/`-a value`
|
37
|
+
arg_name, value = [$1, $3]
|
47
38
|
value = @segments.shift if value.nil? && @segments[0] !~ /^--/
|
48
|
-
|
49
|
-
body[
|
39
|
+
arg_name = self.class.resolve_alias(arg_name, @cmd_schema)
|
40
|
+
body[arg_name] = self.class.convert_arg(arg_name, value, body[arg_name], @cmd_schema)
|
41
|
+
elsif argument =~ /\A-([a-z-]{2,})(=(.+))?\Z/ and
|
42
|
+
@cmd_schema[self.class.resolve_alias($1, @cmd_schema)]
|
43
|
+
# Short form, should be long; offer suggestion
|
44
|
+
raise ArgumentError, "Unexpected argument #{argument} (did you mean --#{$1}?)"
|
45
|
+
elsif argument =~ /\A--([a-z])(=(.+))?\Z/ and
|
46
|
+
@cmd_schema[self.class.resolve_alias($1, @cmd_schema)]
|
47
|
+
# Long form, should be short; offer suggestion
|
48
|
+
raise ArgumentError, "Unexpected argument #{argument} (did you mean -#{$1}?)"
|
50
49
|
else
|
51
|
-
|
50
|
+
# This may be a positional argument.
|
51
|
+
arg_name = positional_argument(@cmd_schema, pos_index)
|
52
|
+
if arg_name
|
53
|
+
body[arg_name] = self.class.convert_arg(arg_name, argument, body[arg_name], @cmd_schema)
|
54
|
+
pos_index += 1
|
55
|
+
else
|
56
|
+
raise ArgumentError, "Unexpected argument #{argument}"
|
57
|
+
end
|
52
58
|
end
|
53
59
|
end
|
54
60
|
|
@@ -62,15 +68,14 @@ class Razor::CLI::Command
|
|
62
68
|
raise Razor::CLI::Error,
|
63
69
|
"Permission to read file #{body["json"]} denied"
|
64
70
|
end
|
65
|
-
|
71
|
+
body
|
66
72
|
end
|
67
73
|
|
68
|
-
def cmd_schema
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
end
|
74
|
+
def positional_argument(cmd_schema, pos_index)
|
75
|
+
# Find a matching position and return its argument name.
|
76
|
+
cmd_schema && cmd_schema.select do |_, schema|
|
77
|
+
schema['position'] == pos_index
|
78
|
+
end.keys.first
|
74
79
|
end
|
75
80
|
|
76
81
|
def self.arg_type(arg_name, cmd_schema)
|
@@ -94,7 +99,7 @@ class Razor::CLI::Command
|
|
94
99
|
argument_type = arg_type(arg_name, cmd_schema)
|
95
100
|
|
96
101
|
# This might be helpful, since there's no other method for debug-level logging on the client.
|
97
|
-
puts "Formatting argument #{arg_name} with value #{value} as #{argument_type}\n" if @
|
102
|
+
puts "Formatting argument #{arg_name} with value #{value} as #{argument_type}\n" if @dump_response
|
98
103
|
|
99
104
|
case argument_type
|
100
105
|
when "array"
|
data/lib/razor/cli/format.rb
CHANGED
@@ -65,33 +65,48 @@ module Razor::CLI
|
|
65
65
|
def format_command_help(doc, show_api_help)
|
66
66
|
item = doc.items.first
|
67
67
|
raise Razor::CLI::Error, 'Could not find help for that entry' unless item.has_key?('help')
|
68
|
-
if
|
69
|
-
|
70
|
-
|
71
|
-
|
68
|
+
if item['help'].has_key?('examples')
|
69
|
+
if show_api_help && item['help']['examples'].has_key?('api')
|
70
|
+
format_composed_help(item, item['help']['examples']['api']).chomp
|
71
|
+
else
|
72
|
+
format_composed_help(item).chomp
|
73
|
+
end
|
72
74
|
else
|
73
75
|
format_full_help(item['help']).chomp
|
74
76
|
end
|
75
77
|
end
|
76
78
|
|
77
|
-
def
|
79
|
+
def positional_args_usage(object)
|
80
|
+
object['schema'].map do |k, v|
|
81
|
+
[v['position'], k] if v.has_key?('position')
|
82
|
+
end.compact.sort.map(&:last).
|
83
|
+
map {|attr| "[#{attr.gsub('_', '-')}] " }.join.strip
|
84
|
+
end
|
85
|
+
def format_composed_help(object, examples = object['help']['examples']['cli'])
|
86
|
+
help_obj = object['help']
|
78
87
|
ret = ''
|
79
|
-
ret = ret + <<-
|
88
|
+
ret = ret + <<-USAGE
|
89
|
+
# USAGE
|
90
|
+
|
91
|
+
razor #{object['name']} #{positional_args_usage(object)} <flags>
|
92
|
+
|
93
|
+
USAGE
|
94
|
+
ret = ret + <<-SYNOPSIS if help_obj.has_key?('summary')
|
80
95
|
# SYNOPSIS
|
81
|
-
#{
|
96
|
+
#{help_obj['summary']}
|
82
97
|
|
83
98
|
SYNOPSIS
|
84
|
-
ret = ret + <<-DESCRIPTION if
|
99
|
+
ret = ret + <<-DESCRIPTION if help_obj.has_key?('description')
|
85
100
|
# DESCRIPTION
|
86
|
-
#{
|
101
|
+
#{help_obj['description']}
|
87
102
|
|
88
|
-
#{
|
103
|
+
#{help_obj['schema']}
|
89
104
|
DESCRIPTION
|
90
|
-
ret = ret + <<-RETURNS if
|
105
|
+
ret = ret + <<-RETURNS if help_obj.has_key?('returns')
|
91
106
|
# RETURNS
|
92
|
-
#{
|
107
|
+
#{help_obj['returns'].gsub(/^/, ' ')}
|
93
108
|
RETURNS
|
94
|
-
ret = ret + <<-EXAMPLES if
|
109
|
+
ret = ret + <<-EXAMPLES if examples
|
95
110
|
# EXAMPLES
|
96
111
|
|
97
112
|
#{examples.gsub(/^/, ' ')}
|
data/lib/razor/cli/navigate.rb
CHANGED
@@ -93,7 +93,15 @@ module Razor::CLI
|
|
93
93
|
elsif query?
|
94
94
|
Razor::CLI::Query.new(@parse, self, collections, @segments).run
|
95
95
|
elsif command?
|
96
|
-
|
96
|
+
cmd = @segments.shift
|
97
|
+
command = commands.find { |coll| coll["name"] == cmd }
|
98
|
+
cmd_url = URI.parse(command['id'])
|
99
|
+
# Ensure that we copy authentication data from our previous URL.
|
100
|
+
if @doc_resource
|
101
|
+
cmd_url = URI.parse(cmd_url.to_s)
|
102
|
+
end
|
103
|
+
command = json_get(cmd_url)
|
104
|
+
Razor::CLI::Command.new(@parse, self, command, @segments, cmd_url).run
|
97
105
|
else
|
98
106
|
raise NavigationError.new(@doc_resource, @segments, @doc)
|
99
107
|
end
|
data/lib/razor/cli/parse.rb
CHANGED
@@ -63,22 +63,19 @@ module Razor::CLI
|
|
63
63
|
end
|
64
64
|
|
65
65
|
def version
|
66
|
+
server_version = '(unknown)'
|
67
|
+
error = ''
|
66
68
|
begin
|
67
|
-
|
68
|
-
Razor Server version: #{navigate.server_version}
|
69
|
-
Razor Client version: #{Razor::CLI::VERSION}
|
70
|
-
VERSION
|
69
|
+
server_version = navigate.server_version
|
71
70
|
rescue RestClient::Unauthorized
|
72
|
-
|
73
|
-
Error: Credentials are required to connect to the server at #{@api_url}"
|
74
|
-
UNAUTH
|
75
|
-
exit 1
|
71
|
+
error = "Error: Credentials are required to connect to the server at #{@api_url}."
|
76
72
|
rescue
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
73
|
+
error = "Error: Could not connect to the server at #{@api_url}."
|
74
|
+
ensure
|
75
|
+
return [(<<-OUTPUT + "\n" + error).rstrip, error != '' ? 1 : 0]
|
76
|
+
Razor Server version: #{server_version}
|
77
|
+
Razor Client version: #{Razor::CLI::VERSION}
|
78
|
+
OUTPUT
|
82
79
|
end
|
83
80
|
end
|
84
81
|
|
@@ -105,10 +102,23 @@ HELP
|
|
105
102
|
Error: Credentials are required to connect to the server at #{@api_url}"
|
106
103
|
UNAUTH
|
107
104
|
exit = 1
|
108
|
-
rescue
|
105
|
+
rescue SocketError, Errno::ECONNREFUSED => e
|
106
|
+
puts "Error: Could not connect to the server at #{@api_url}"
|
107
|
+
puts " #{e}\n"
|
108
|
+
die
|
109
|
+
rescue RestClient::SSLCertificateNotVerified
|
110
|
+
puts "Error: SSL certificate could not be verified against known CA certificates."
|
111
|
+
puts " To turn off verification, use the -k or --insecure option."
|
112
|
+
die
|
113
|
+
rescue OpenSSL::SSL::SSLError => e
|
114
|
+
# Occurs in case of e.g. certificate mismatch (FQDN vs. hostname)
|
115
|
+
puts "Error: SSL certificate error from server at #{@api_url}"
|
116
|
+
puts " #{e}"
|
117
|
+
die
|
118
|
+
rescue => e
|
109
119
|
output << <<-ERR
|
110
|
-
Error:
|
111
|
-
|
120
|
+
Error: Unknown error occurred while connecting to server at #{@api_url}:
|
121
|
+
#{e}
|
112
122
|
ERR
|
113
123
|
exit = 1
|
114
124
|
end
|
@@ -143,23 +153,34 @@ ERR
|
|
143
153
|
# The format can be determined from later segments.
|
144
154
|
attr_accessor :format, :stripped_args, :ssl_ca_file
|
145
155
|
|
156
|
+
LINUX_PEM_FILE = '/etc/puppetlabs/puppet/ssl/certs/ca.pem'
|
157
|
+
WIN_PEM_FILE = 'C:\ProgramData\PuppetLabs\puppet\etc\ssl\certs\ca.pem'
|
146
158
|
def initialize(args)
|
147
159
|
parse_and_set_api_url(ENV["RAZOR_API"] || DEFAULT_RAZOR_API, :env)
|
148
160
|
@args = args.dup
|
149
161
|
# To be populated externally.
|
150
162
|
@stripped_args = []
|
151
163
|
@format = 'short'
|
164
|
+
@verify_ssl = true
|
165
|
+
env_pem_file = ENV['RAZOR_CA_FILE']
|
152
166
|
# If this is set, it should actually exist.
|
153
|
-
if
|
154
|
-
raise Razor::CLI::InvalidCAFileError.new(
|
167
|
+
if env_pem_file && !File.exists?(env_pem_file)
|
168
|
+
raise Razor::CLI::InvalidCAFileError.new(env_pem_file)
|
169
|
+
end
|
170
|
+
pem_file_locations = [env_pem_file, LINUX_PEM_FILE, WIN_PEM_FILE]
|
171
|
+
pem_file_locations.each do |file|
|
172
|
+
if file && File.exists?(file)
|
173
|
+
@ssl_ca_file = file
|
174
|
+
break
|
175
|
+
end
|
155
176
|
end
|
156
|
-
ca_file = ENV["RAZOR_CA_FILE"]
|
157
|
-
@ssl_ca_file = ca_file if ca_file && File.exists?(ca_file)
|
158
177
|
@args = get_optparse.order(@args)
|
159
178
|
|
160
179
|
# Localhost won't match the server's certificate; no verification required.
|
161
180
|
# This needs to happen after get_optparse so `-k` and `-u` can take effect.
|
162
|
-
|
181
|
+
if @api_url.hostname == 'localhost'
|
182
|
+
@verify_ssl = false
|
183
|
+
end
|
163
184
|
|
164
185
|
@args = set_help_vars(@args)
|
165
186
|
if @args == ['version'] or @show_version
|
@@ -54,7 +54,7 @@ class Razor::CLI::TableFormat
|
|
54
54
|
def average_width(headings)
|
55
55
|
# The 3 here = 2 for width gap + 1 for the column separator.
|
56
56
|
# The 1 is for the last separator.
|
57
|
-
console_width = `stty size | cut -d ' ' -f 2`
|
57
|
+
console_width = `stty size | cut -d ' ' -f 2 2>/dev/null`
|
58
58
|
if console_width.nil? || console_width.to_i <= 0
|
59
59
|
console_width = 80
|
60
60
|
end
|
data/lib/razor/cli/version.rb
CHANGED
@@ -18,7 +18,7 @@ module Razor
|
|
18
18
|
#
|
19
19
|
# The next line is the one that our packaging tools modify, so please make
|
20
20
|
# sure that any change to it is discussed and agreed first.
|
21
|
-
version = '1.
|
21
|
+
version = '1.2.0'
|
22
22
|
|
23
23
|
if version == "DEVELOPMENT"
|
24
24
|
root = File.expand_path("../../..", File.dirname(__FILE__))
|
data/spec/cli/command_spec.rb
CHANGED
@@ -63,4 +63,100 @@ describe Razor::CLI::Command do
|
|
63
63
|
result.should == 'abc'
|
64
64
|
end
|
65
65
|
end
|
66
|
+
|
67
|
+
context "extract_command" do
|
68
|
+
|
69
|
+
def extract(schema, run_array)
|
70
|
+
c = Razor::CLI::Command.new(nil, nil, schema, run_array, nil)
|
71
|
+
c.extract_command
|
72
|
+
end
|
73
|
+
context "flag length" do
|
74
|
+
|
75
|
+
it "fails with a single dash for long flags" do
|
76
|
+
expect{extract({'schema' => {'name' => {'type' => 'array'}}}, ['-name', 'abc'])}.
|
77
|
+
to raise_error(ArgumentError, 'Unexpected argument -name (did you mean --name?)')
|
78
|
+
end
|
79
|
+
it "fails with a double dash for short flags" do
|
80
|
+
expect{extract({'schema' => {'n' => {'type' => 'array'}}}, ['--n', 'abc'])}.
|
81
|
+
to raise_error(ArgumentError, 'Unexpected argument --n (did you mean -n?)')
|
82
|
+
end
|
83
|
+
it "fails with a double dash for short flags if argument does not exist" do
|
84
|
+
c = Razor::CLI::Command.new(nil, nil, {'schema' => {}},
|
85
|
+
['--n', 'abc'], '/foobar')
|
86
|
+
expect{extract({'schema' => {}}, ['--n', 'abc'])}.
|
87
|
+
to raise_error(ArgumentError, 'Unexpected argument --n')
|
88
|
+
end
|
89
|
+
it "succeeds with a double dash for long flags" do
|
90
|
+
extract({'schema' => {'name' => {'type' => 'array'}}},
|
91
|
+
['--name', 'abc'])['name'].should == ['abc']
|
92
|
+
end
|
93
|
+
it "succeeds with a single dash for short flags" do
|
94
|
+
c = Razor::CLI::Command.new(nil, nil, {'schema' => {'n' => {'type' => 'array'}}},
|
95
|
+
['-n', 'abc'], nil)
|
96
|
+
extract({'schema' => {'n' => {'type' => 'array'}}}, ['-n', 'abc'])['n'].should == ['abc']
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context "positional arguments" do
|
101
|
+
let(:schema) do
|
102
|
+
{'schema' => {'n' => {'position' => 1},
|
103
|
+
'o' => {'position' => 0}}}
|
104
|
+
end
|
105
|
+
it "fails without a command schema" do
|
106
|
+
expect{extract(nil, ['123'])}.
|
107
|
+
to raise_error(ArgumentError, 'Unexpected argument 123')
|
108
|
+
end
|
109
|
+
it "fails if no positional arguments exist for a command" do
|
110
|
+
expect{extract({'schema' => {'n' => {}}}, ['abc'])}.
|
111
|
+
to raise_error(ArgumentError, 'Unexpected argument abc')
|
112
|
+
end
|
113
|
+
it "succeeds if no position is supplied" do
|
114
|
+
extract({'schema' => {'n' => {'position' => 0}}}, ['-n', '123'])['n'].
|
115
|
+
should == '123'
|
116
|
+
end
|
117
|
+
it "succeeds if position exists and is supplied" do
|
118
|
+
extract({'schema' => {'n' => {'position' => 0}}}, ['123'])['n'].
|
119
|
+
should == '123'
|
120
|
+
end
|
121
|
+
it "succeeds if multiple positions exist and are supplied" do
|
122
|
+
body = extract(schema, ['123', '456'])
|
123
|
+
body['o'].should == '123'
|
124
|
+
body['n'].should == '456'
|
125
|
+
end
|
126
|
+
it "fails if too many positions are supplied" do
|
127
|
+
expect{extract(schema, ['123', '456', '789'])}.
|
128
|
+
to raise_error(ArgumentError, 'Unexpected argument 789')
|
129
|
+
end
|
130
|
+
it "succeeds if multiple positions exist and one is supplied" do
|
131
|
+
body = extract(schema, ['123'])
|
132
|
+
body['o'].should == '123'
|
133
|
+
body['n'].should == nil
|
134
|
+
end
|
135
|
+
it "succeeds with a combination of positional and flags" do
|
136
|
+
body = extract(schema, ['123', '-n', '456'])
|
137
|
+
body['o'].should == '123'
|
138
|
+
body['n'].should == '456'
|
139
|
+
end
|
140
|
+
it "prefers the later between positional and flags" do
|
141
|
+
body = extract(schema, ['123', '-o', '456'])
|
142
|
+
body['o'].should == '456'
|
143
|
+
body = extract(schema, ['-o', '456', '123'])
|
144
|
+
body['o'].should == '123'
|
145
|
+
end
|
146
|
+
it "correctly sets datatypes" do
|
147
|
+
schema =
|
148
|
+
{'schema' => {'n' => {'type' => 'array', 'position' => 0},
|
149
|
+
'o' => {'type' => 'number', 'position' => 1},
|
150
|
+
'w' => {'type' => 'boolean', 'position' => 2},
|
151
|
+
'a' => {'type' => 'object', 'position' => 3},
|
152
|
+
'i' => {'type' => 'object', 'position' => 4}}}
|
153
|
+
body = extract(schema, ['arr', '123', 'true', '{}', 'abc=123'])
|
154
|
+
body['n'].should == ['arr']
|
155
|
+
body['o'].should == 123
|
156
|
+
body['w'].should == true
|
157
|
+
body['a'].should == {}
|
158
|
+
body['i'].should == {'abc' => '123'}
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
66
162
|
end
|