358 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			358 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
#!/usr/bin/perl
 | 
						|
#
 | 
						|
# swagger-check - Look for inconsistencies between swagger and source code
 | 
						|
#
 | 
						|
package LibPod::SwaggerCheck;
 | 
						|
 | 
						|
use v5.14;
 | 
						|
use strict;
 | 
						|
use warnings;
 | 
						|
 | 
						|
use File::Find;
 | 
						|
 | 
						|
(our $ME = $0) =~ s|.*/||;
 | 
						|
(our $VERSION = '$Revision: 1.7 $ ') =~ tr/[0-9].//cd;
 | 
						|
 | 
						|
# For debugging, show data structures using DumpTree($var)
 | 
						|
#use Data::TreeDumper; $Data::TreeDumper::Displayaddress = 0;
 | 
						|
 | 
						|
###############################################################################
 | 
						|
# BEGIN user-customizable section
 | 
						|
 | 
						|
our $Default_Dir =  'pkg/api/server';
 | 
						|
 | 
						|
# END   user-customizable section
 | 
						|
###############################################################################
 | 
						|
 | 
						|
###############################################################################
 | 
						|
# BEGIN boilerplate args checking, usage messages
 | 
						|
 | 
						|
sub usage {
 | 
						|
    print  <<"END_USAGE";
 | 
						|
Usage: $ME [OPTIONS] DIRECTORY-TO-CHECK
 | 
						|
 | 
						|
$ME scans all .go files under the given DIRECTORY-TO-CHECK
 | 
						|
(default: $Default_Dir), looking for lines of the form 'r.Handle(...)'
 | 
						|
or 'r.HandleFunc(...)'. For each such line, we check for a preceding
 | 
						|
swagger comment line and verify that the comment line matches the
 | 
						|
declarations in the r.Handle() invocation.
 | 
						|
 | 
						|
For example, the following would be a correctly-matching pair of lines:
 | 
						|
 | 
						|
    // swagger:operation GET /images/json compat getImages
 | 
						|
    r.Handle(VersionedPath("/images/json"), s.APIHandler(compat.GetImages)).Methods(http.MethodGet)
 | 
						|
 | 
						|
...because http.MethodGet matches GET in the comment, the endpoint
 | 
						|
is /images/json in both cases, the APIHandler() says "compat" so
 | 
						|
that's the swagger tag, and the swagger operation name is the
 | 
						|
same as the APIHandler but with a lower-case first letter.
 | 
						|
 | 
						|
The following is an inconsistency as reported by this script:
 | 
						|
 | 
						|
pkg/api/server/register_info.go:
 | 
						|
-       // swagger:operation GET /info libpod libpodGetInfo
 | 
						|
+       // ................. ... ..... compat
 | 
						|
        r.Handle(VersionedPath("/info"), s.APIHandler(compat.GetInfo)).Methods(http.MethodGet)
 | 
						|
 | 
						|
...because APIHandler() says 'compat' but the swagger comment
 | 
						|
says 'libpod'.
 | 
						|
 | 
						|
OPTIONS:
 | 
						|
 | 
						|
  -v, --verbose  show verbose progress indicators
 | 
						|
  -n, --dry-run  make no actual changes
 | 
						|
 | 
						|
  --help         display this message
 | 
						|
  --version      display program name and version
 | 
						|
END_USAGE
 | 
						|
 | 
						|
    exit;
 | 
						|
}
 | 
						|
 | 
						|
# Command-line options.  Note that this operates directly on @ARGV !
 | 
						|
our $debug   = 0;
 | 
						|
our $force   = 0;
 | 
						|
our $verbose = 0;
 | 
						|
our $NOT     = '';              # print "blahing the blah$NOT\n" if $debug
 | 
						|
sub handle_opts {
 | 
						|
    use Getopt::Long;
 | 
						|
    GetOptions(
 | 
						|
        'debug!'     => \$debug,
 | 
						|
        'dry-run|n!' => sub { $NOT = ' [NOT]' },
 | 
						|
        'force'      => \$force,
 | 
						|
        'verbose|v'  => \$verbose,
 | 
						|
 | 
						|
        help         => \&usage,
 | 
						|
        man          => \&man,
 | 
						|
        version      => sub { print "$ME version $VERSION\n"; exit 0 },
 | 
						|
    ) or die "Try `$ME --help' for help\n";
 | 
						|
}
 | 
						|
 | 
						|
# END   boilerplate args checking, usage messages
 | 
						|
###############################################################################
 | 
						|
 | 
						|
############################## CODE BEGINS HERE ###############################
 | 
						|
 | 
						|
my $exit_status = 0;
 | 
						|
 | 
						|
# The term is "modulino".
 | 
						|
__PACKAGE__->main()                                     unless caller();
 | 
						|
 | 
						|
# Main code.
 | 
						|
sub main {
 | 
						|
    # Note that we operate directly on @ARGV, not on function parameters.
 | 
						|
    # This is deliberate: it's because Getopt::Long only operates on @ARGV
 | 
						|
    # and there's no clean way to make it use @_.
 | 
						|
    handle_opts();                      # will set package globals
 | 
						|
 | 
						|
    # Fetch command-line arguments.  Barf if too many.
 | 
						|
    my $dir = shift(@ARGV) || $Default_Dir;
 | 
						|
    die "$ME: Too many arguments; try $ME --help\n"                 if @ARGV;
 | 
						|
 | 
						|
    # Find and act upon all matching files
 | 
						|
    find { wanted => sub { finder(@_) }, no_chdir => 1 }, $dir;
 | 
						|
 | 
						|
    exit $exit_status;
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
############
 | 
						|
#  finder  #  File::Find action - looks for 'r.Handle' or 'r.HandleFunc'
 | 
						|
############
 | 
						|
sub finder {
 | 
						|
    my $path = $File::Find::name;
 | 
						|
    return if     $path =~ m|/\.|;              # skip dotfiles
 | 
						|
    return unless $path =~ /\.go$/;             # Only want .go files
 | 
						|
 | 
						|
    print $path, "\n"                           if $debug;
 | 
						|
 | 
						|
    # Read each .go file. Keep a running tally of all '// comment' lines;
 | 
						|
    # if we see a 'r.Handle()' or 'r.HandleFunc()' line, pass it + comments
 | 
						|
    # to analysis function.
 | 
						|
    open my $in, '<', $path
 | 
						|
        or die "$ME: Cannot read $path: $!\n";
 | 
						|
    my @comments;
 | 
						|
    while (my $line = <$in>) {
 | 
						|
        if ($line =~ m!^\s*//!) {
 | 
						|
            push @comments, $line;
 | 
						|
        }
 | 
						|
        else {
 | 
						|
            # Not a comment line. If it's an r.Handle*() one, process it.
 | 
						|
            if ($line =~ m!^\s*r\.Handle(Func)?\(!) {
 | 
						|
                handle_handle($path, $line, @comments)
 | 
						|
                    or $exit_status = 1;
 | 
						|
            }
 | 
						|
 | 
						|
            # Reset comments
 | 
						|
            @comments = ();
 | 
						|
        }
 | 
						|
    }
 | 
						|
    close $in;
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
###################
 | 
						|
#  handle_handle  #  Cross-check a 'r.Handle*' declaration against swagger
 | 
						|
###################
 | 
						|
#
 | 
						|
# Returns false if swagger comment is inconsistent with function call,
 | 
						|
# true if it matches or if there simply isn't a swagger comment.
 | 
						|
#
 | 
						|
sub handle_handle {
 | 
						|
    my $path     = shift;               # for error messages only
 | 
						|
    my $line     = shift;               # in: the r.Handle* line
 | 
						|
    my @comments = @_;                  # in: preceding comment lines
 | 
						|
 | 
						|
    # Preserve the original line, so we can show it in comments
 | 
						|
    my $line_orig = $line;
 | 
						|
 | 
						|
    # Strip off the 'r.Handle*(' and leading whitespace; preserve the latter
 | 
						|
    $line =~ s!^(\s*)r\.Handle(Func)?\(!!
 | 
						|
        or die "$ME: INTERNAL ERROR! Got '$line'!\n";
 | 
						|
    my $indent = $1;
 | 
						|
 | 
						|
    # Some have VersionedPath, some don't. Doesn't seem to make a difference
 | 
						|
    # in terms of swagger, so let's just ignore it.
 | 
						|
    $line =~ s!^VersionedPath\(([^\)]+)\)!$1!;
 | 
						|
    $line =~ m!^"(/[^"]+)",!
 | 
						|
        or die "$ME: $path:$.: Cannot grok '$line'\n";
 | 
						|
    my $endpoint = $1;
 | 
						|
 | 
						|
    # Some function declarations require an argument of the form '{name:.*}'
 | 
						|
    # but the swagger (which gets derived from the comments) should not
 | 
						|
    # include them. Normalize all such args to just '{name}'.
 | 
						|
    $endpoint =~ s/\{name:\.\*\}/\{name\}/;
 | 
						|
 | 
						|
    # e.g. /auth, /containers/*/rename, /distribution, /monitor, /plugins
 | 
						|
    return 1 if $line =~ /\.UnsupportedHandler/;
 | 
						|
 | 
						|
    #
 | 
						|
    # Determine the HTTP METHOD (GET, POST, DELETE, HEAD)
 | 
						|
    #
 | 
						|
    my $method;
 | 
						|
    if ($line =~ /generic.VersionHandler/) {
 | 
						|
        $method = 'GET';
 | 
						|
    }
 | 
						|
    elsif ($line =~ m!\.Methods\((.*)\)!) {
 | 
						|
        my $x = $1;
 | 
						|
 | 
						|
        if ($x =~ /Method(Post|Get|Delete|Head)/) {
 | 
						|
            $method = uc $1;
 | 
						|
        }
 | 
						|
        elsif ($x =~ /\"(HEAD|GET|POST)"/) {
 | 
						|
            $method = $1;
 | 
						|
        }
 | 
						|
        else {
 | 
						|
            die "$ME: $path:$.: Cannot grok $x\n";
 | 
						|
        }
 | 
						|
    }
 | 
						|
    else {
 | 
						|
        warn "$ME: $path:$.: No Methods in '$line'\n";
 | 
						|
        return 1;
 | 
						|
    }
 | 
						|
 | 
						|
    #
 | 
						|
    # Determine the SWAGGER TAG. Assume 'compat' unless we see libpod; but
 | 
						|
    # this can be overruled (see special case below)
 | 
						|
    #
 | 
						|
    my $tag = ($endpoint =~ /(libpod)/ ? $1 : 'compat');
 | 
						|
 | 
						|
    #
 | 
						|
    # Determine the OPERATION. Done in a helper function because there
 | 
						|
    # are a lot of complicated special cases.
 | 
						|
    #
 | 
						|
    my $operation = operation_name($method, $endpoint);
 | 
						|
 | 
						|
    # Special case: the following endpoints all get a custom tag
 | 
						|
    if ($endpoint =~ m!/(pods|manifests)/!) {
 | 
						|
        $tag = $1;
 | 
						|
    }
 | 
						|
 | 
						|
    # Special case: anything related to 'events' gets a system tag
 | 
						|
    if ($endpoint =~ m!/events!) {
 | 
						|
        $tag = 'system';
 | 
						|
    }
 | 
						|
 | 
						|
    state $previous_path;                # Previous path name, to avoid dups
 | 
						|
 | 
						|
    #
 | 
						|
    # Compare actual swagger comment to what we expect based on Handle call.
 | 
						|
    #
 | 
						|
    my $expect = " // swagger:operation $method $endpoint $tag $operation ";
 | 
						|
    my @actual = grep { /swagger:operation/ } @comments;
 | 
						|
 | 
						|
    return 1 if !@actual;         # No swagger comment in file; oh well
 | 
						|
 | 
						|
    my $actual = $actual[0];
 | 
						|
 | 
						|
    # (Ignore whitespace discrepancies)
 | 
						|
    (my $a_trimmed = $actual) =~ s/\s+/ /g;
 | 
						|
 | 
						|
    return 1 if $a_trimmed eq $expect;
 | 
						|
 | 
						|
    # Mismatch. Display it. Start with filename, if different from previous
 | 
						|
    print "\n";
 | 
						|
    if (!$previous_path || $previous_path ne $path) {
 | 
						|
        print $path, ":\n";
 | 
						|
    }
 | 
						|
    $previous_path = $path;
 | 
						|
 | 
						|
    # Show the actual line, prefixed with '-' ...
 | 
						|
    print "- $actual[0]";
 | 
						|
    # ...then our generated ones, but use '...' as a way to ignore matches
 | 
						|
    print "+ $indent//";
 | 
						|
    my @actual_split = split ' ', $actual;
 | 
						|
    my @expect_split = split ' ', $expect;
 | 
						|
    for my $i (1 .. $#actual_split) {
 | 
						|
        print " ";
 | 
						|
        if ($actual_split[$i] eq ($expect_split[$i]||'')) {
 | 
						|
            print "." x length($actual_split[$i]);
 | 
						|
        }
 | 
						|
        else {
 | 
						|
            # Show the difference. Use terminal highlights if available.
 | 
						|
            print "\e[1;37m"            if -t *STDOUT;
 | 
						|
            print $expect_split[$i];
 | 
						|
            print "\e[m"                if -t *STDOUT;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    print "\n";
 | 
						|
 | 
						|
    # Show the r.Handle* code line itself
 | 
						|
    print "  ", $line_orig;
 | 
						|
 | 
						|
    return;
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
####################
 | 
						|
#  operation_name  #  Given a method + endpoint, return the swagger operation
 | 
						|
####################
 | 
						|
sub operation_name {
 | 
						|
    my ($method, $endpoint) = @_;
 | 
						|
 | 
						|
    # /libpod/foo/bar -> (libpod, foo, bar)
 | 
						|
    my @endpoints = grep { /\S/ } split '/', $endpoint;
 | 
						|
 | 
						|
    # /libpod endpoints -> add 'Libpod' to end, e.g. PodStatsLibpod
 | 
						|
    my $Libpod = '';
 | 
						|
    my $main = shift(@endpoints);
 | 
						|
    if ($main eq 'libpod') {
 | 
						|
        $Libpod = ucfirst($main);
 | 
						|
        $main = shift(@endpoints);
 | 
						|
    }
 | 
						|
    $main =~ s/s$//;         # e.g. Volumes -> Volume
 | 
						|
 | 
						|
    # Next path component is an optional action:
 | 
						|
    #    GET    /containers/json               -> ContainerList
 | 
						|
    #    DELETE /libpod/containers/{name}      -> ContainerDelete
 | 
						|
    #    GET    /libpod/containers/{name}/logs -> ContainerLogsLibpod
 | 
						|
    my $action = shift(@endpoints) || 'list';
 | 
						|
    $action = 'list'   if $action eq 'json';
 | 
						|
    $action = 'delete' if $method eq 'DELETE';
 | 
						|
 | 
						|
    # Anything with {id}, {name}, {name:..} may have a following component
 | 
						|
    if ($action =~ m!\{.*\}!) {
 | 
						|
        $action = shift(@endpoints) || 'inspect';
 | 
						|
        $action = 'inspect' if $action eq 'json';
 | 
						|
    }
 | 
						|
 | 
						|
    # All sorts of special cases
 | 
						|
    if ($action eq 'df') {
 | 
						|
        $action = 'dataUsage';
 | 
						|
    }
 | 
						|
    elsif ($action eq "delete" && $endpoint eq "/libpod/play/kube") {
 | 
						|
        $action = "KubeDown"
 | 
						|
    }
 | 
						|
    # Grrrrrr, this one is annoying: some operations get an extra 'All'
 | 
						|
    elsif ($action =~ /^(delete|get|stats)$/ && $endpoint !~ /\{/) {
 | 
						|
        $action .= "All";
 | 
						|
        $main .= 's' if $main eq 'container';
 | 
						|
    }
 | 
						|
    # No real way to used MixedCase in an endpoint, so we have to hack it here
 | 
						|
    elsif ($action eq 'showmounted') {
 | 
						|
        $action = 'showMounted';
 | 
						|
    }
 | 
						|
    # Ping is a special endpoint, and even if /libpod/_ping, no 'Libpod'
 | 
						|
    elsif ($main eq '_ping') {
 | 
						|
        $main = 'system';
 | 
						|
        $action = 'ping';
 | 
						|
        $Libpod = '';
 | 
						|
    }
 | 
						|
    # Top-level compat endpoints
 | 
						|
    elsif ($main =~ /^(build|commit)$/) {
 | 
						|
        $main   = 'image';
 | 
						|
        $action = $1;
 | 
						|
    }
 | 
						|
    # Top-level system endpoints
 | 
						|
    elsif ($main =~ /^(auth|event|info|version)$/) {
 | 
						|
        $main   = 'system';
 | 
						|
        $action = $1;
 | 
						|
        $action .= 's' if $action eq 'event';
 | 
						|
    }
 | 
						|
 | 
						|
    return "\u${main}\u${action}$Libpod";
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
1;
 |