3
# swagger-check - Look for inconsistencies between swagger and source code
5
package LibPod::SwaggerCheck;
13
(our $ME = $0) =~ s|.*/||;
14
(our $VERSION = '$Revision: 1.7 $ ') =~ tr/[0-9].//cd;
16
# For debugging, show data structures using DumpTree($var)
17
#use Data::TreeDumper; $Data::TreeDumper::Displayaddress = 0;
19
###############################################################################
20
# BEGIN user-customizable section
22
our $Default_Dir = 'pkg/api/server';
24
# END user-customizable section
25
###############################################################################
27
###############################################################################
28
# BEGIN boilerplate args checking, usage messages
32
Usage: $ME [OPTIONS] DIRECTORY-TO-CHECK
34
$ME scans all .go files under the given DIRECTORY-TO-CHECK
35
(default: $Default_Dir), looking for lines of the form 'r.Handle(...)'
36
or 'r.HandleFunc(...)'. For each such line, we check for a preceding
37
swagger comment line and verify that the comment line matches the
38
declarations in the r.Handle() invocation.
40
For example, the following would be a correctly-matching pair of lines:
42
// swagger:operation GET /images/json compat getImages
43
r.Handle(VersionedPath("/images/json"), s.APIHandler(compat.GetImages)).Methods(http.MethodGet)
45
...because http.MethodGet matches GET in the comment, the endpoint
46
is /images/json in both cases, the APIHandler() says "compat" so
47
that's the swagger tag, and the swagger operation name is the
48
same as the APIHandler but with a lower-case first letter.
50
The following is an inconsistency as reported by this script:
52
pkg/api/server/register_info.go:
53
- // swagger:operation GET /info libpod libpodGetInfo
54
+ // ................. ... ..... compat
55
r.Handle(VersionedPath("/info"), s.APIHandler(compat.GetInfo)).Methods(http.MethodGet)
57
...because APIHandler() says 'compat' but the swagger comment
62
-v, --verbose show verbose progress indicators
63
-n, --dry-run make no actual changes
65
--help display this message
66
--version display program name and version
72
# Command-line options. Note that this operates directly on @ARGV !
76
our $NOT = ''; # print "blahing the blah$NOT\n" if $debug
81
'dry-run|n!' => sub { $NOT = ' [NOT]' },
83
'verbose|v' => \$verbose,
87
version => sub { print "$ME version $VERSION\n"; exit 0 },
88
) or die "Try `$ME --help' for help\n";
91
# END boilerplate args checking, usage messages
92
###############################################################################
94
############################## CODE BEGINS HERE ###############################
98
# The term is "modulino".
99
__PACKAGE__->main() unless caller();
103
# Note that we operate directly on @ARGV, not on function parameters.
104
# This is deliberate: it's because Getopt::Long only operates on @ARGV
105
# and there's no clean way to make it use @_.
106
handle_opts(); # will set package globals
108
# Fetch command-line arguments. Barf if too many.
109
my $dir = shift(@ARGV) || $Default_Dir;
110
die "$ME: Too many arguments; try $ME --help\n" if @ARGV;
112
# Find and act upon all matching files
113
find { wanted => sub { finder(@_) }, no_chdir => 1 }, $dir;
120
# finder # File::Find action - looks for 'r.Handle' or 'r.HandleFunc'
123
my $path = $File::Find::name;
124
return if $path =~ m|/\.|; # skip dotfiles
125
return unless $path =~ /\.go$/; # Only want .go files
127
print $path, "\n" if $debug;
129
# Read each .go file. Keep a running tally of all '// comment' lines;
130
# if we see a 'r.Handle()' or 'r.HandleFunc()' line, pass it + comments
131
# to analysis function.
132
open my $in, '<', $path
133
or die "$ME: Cannot read $path: $!\n";
135
while (my $line = <$in>) {
136
if ($line =~ m!^\s*//!) {
137
push @comments, $line;
140
# Not a comment line. If it's an r.Handle*() one, process it.
141
if ($line =~ m!^\s*r\.Handle(Func)?\(!) {
142
handle_handle($path, $line, @comments)
155
# handle_handle # Cross-check a 'r.Handle*' declaration against swagger
158
# Returns false if swagger comment is inconsistent with function call,
159
# true if it matches or if there simply isn't a swagger comment.
162
my $path = shift; # for error messages only
163
my $line = shift; # in: the r.Handle* line
164
my @comments = @_; # in: preceding comment lines
166
# Preserve the original line, so we can show it in comments
167
my $line_orig = $line;
169
# Strip off the 'r.Handle*(' and leading whitespace; preserve the latter
170
$line =~ s!^(\s*)r\.Handle(Func)?\(!!
171
or die "$ME: INTERNAL ERROR! Got '$line'!\n";
174
# Some have VersionedPath, some don't. Doesn't seem to make a difference
175
# in terms of swagger, so let's just ignore it.
176
$line =~ s!^VersionedPath\(([^\)]+)\)!$1!;
177
$line =~ m!^"(/[^"]+)",!
178
or die "$ME: $path:$.: Cannot grok '$line'\n";
181
# Some function declarations require an argument of the form '{name:.*}'
182
# but the swagger (which gets derived from the comments) should not
183
# include them. Normalize all such args to just '{name}'.
184
$endpoint =~ s/\{name:\.\*\}/\{name\}/;
186
# e.g. /auth, /containers/*/rename, /distribution, /monitor, /plugins
187
return 1 if $line =~ /\.UnsupportedHandler/;
190
# Determine the HTTP METHOD (GET, POST, DELETE, HEAD)
193
if ($line =~ /generic.VersionHandler/) {
196
elsif ($line =~ m!\.Methods\((.*)\)!) {
199
if ($x =~ /Method(Post|Get|Delete|Head)/) {
202
elsif ($x =~ /\"(HEAD|GET|POST)"/) {
206
die "$ME: $path:$.: Cannot grok $x\n";
210
warn "$ME: $path:$.: No Methods in '$line'\n";
215
# Determine the SWAGGER TAG. Assume 'compat' unless we see libpod; but
216
# this can be overruled (see special case below)
218
my $tag = ($endpoint =~ /(libpod)/ ? $1 : 'compat');
221
# Determine the OPERATION. Done in a helper function because there
222
# are a lot of complicated special cases.
224
my $operation = operation_name($method, $endpoint);
226
# Special case: the following endpoints all get a custom tag
227
if ($endpoint =~ m!/(pods|manifests)/!) {
231
# Special case: anything related to 'events' gets a system tag
232
if ($endpoint =~ m!/events!) {
236
state $previous_path; # Previous path name, to avoid dups
239
# Compare actual swagger comment to what we expect based on Handle call.
241
my $expect = " // swagger:operation $method $endpoint $tag $operation ";
242
my @actual = grep { /swagger:operation/ } @comments;
244
return 1 if !@actual; # No swagger comment in file; oh well
246
my $actual = $actual[0];
248
# (Ignore whitespace discrepancies)
249
(my $a_trimmed = $actual) =~ s/\s+/ /g;
251
return 1 if $a_trimmed eq $expect;
253
# Mismatch. Display it. Start with filename, if different from previous
255
if (!$previous_path || $previous_path ne $path) {
258
$previous_path = $path;
260
# Show the actual line, prefixed with '-' ...
261
print "- $actual[0]";
262
# ...then our generated ones, but use '...' as a way to ignore matches
264
my @actual_split = split ' ', $actual;
265
my @expect_split = split ' ', $expect;
266
for my $i (1 .. $#actual_split) {
268
if ($actual_split[$i] eq ($expect_split[$i]||'')) {
269
print "." x length($actual_split[$i]);
272
# Show the difference. Use terminal highlights if available.
273
print "\e[1;37m" if -t *STDOUT;
274
print $expect_split[$i];
275
print "\e[m" if -t *STDOUT;
280
# Show the r.Handle* code line itself
281
print " ", $line_orig;
288
# operation_name # Given a method + endpoint, return the swagger operation
291
my ($method, $endpoint) = @_;
293
# /libpod/foo/bar -> (libpod, foo, bar)
294
my @endpoints = grep { /\S/ } split '/', $endpoint;
296
# /libpod endpoints -> add 'Libpod' to end, e.g. PodStatsLibpod
298
my $main = shift(@endpoints);
299
if ($main eq 'libpod') {
300
$Libpod = ucfirst($main);
301
$main = shift(@endpoints);
303
$main =~ s/s$//; # e.g. Volumes -> Volume
305
# Next path component is an optional action:
306
# GET /containers/json -> ContainerList
307
# DELETE /libpod/containers/{name} -> ContainerDelete
308
# GET /libpod/containers/{name}/logs -> ContainerLogsLibpod
309
my $action = shift(@endpoints) || 'list';
310
$action = 'list' if $action eq 'json';
311
$action = 'delete' if $method eq 'DELETE';
313
# Anything with {id}, {name}, {name:..} may have a following component
314
if ($action =~ m!\{.*\}!) {
315
$action = shift(@endpoints) || 'inspect';
316
$action = 'inspect' if $action eq 'json';
319
# All sorts of special cases
320
if ($action eq 'df') {
321
$action = 'dataUsage';
323
elsif ($action eq "delete" && $endpoint eq "/libpod/play/kube") {
326
# Grrrrrr, this one is annoying: some operations get an extra 'All'
327
elsif ($action =~ /^(delete|get|stats)$/ && $endpoint !~ /\{/) {
329
$main .= 's' if $main eq 'container';
331
# No real way to used MixedCase in an endpoint, so we have to hack it here
332
elsif ($action eq 'showmounted') {
333
$action = 'showMounted';
335
# Ping is a special endpoint, and even if /libpod/_ping, no 'Libpod'
336
elsif ($main eq '_ping') {
341
# Top-level compat endpoints
342
elsif ($main =~ /^(build|commit)$/) {
346
# Top-level system endpoints
347
elsif ($main =~ /^(auth|event|info|version)$/) {
350
$action .= 's' if $action eq 'event';
353
return "\u${main}\u${action}$Libpod";