github.com/containers/podman/v5@v5.1.0-rc1/hack/swagger-check (about) 1 #!/usr/bin/perl 2 # 3 # swagger-check - Look for inconsistencies between swagger and source code 4 # 5 package LibPod::SwaggerCheck; 6 7 use v5.14; 8 use strict; 9 use warnings; 10 11 use File::Find; 12 13 (our $ME = $0) =~ s|.*/||; 14 (our $VERSION = '$Revision: 1.7 $ ') =~ tr/[0-9].//cd; 15 16 # For debugging, show data structures using DumpTree($var) 17 #use Data::TreeDumper; $Data::TreeDumper::Displayaddress = 0; 18 19 ############################################################################### 20 # BEGIN user-customizable section 21 22 our $Default_Dir = 'pkg/api/server'; 23 24 # END user-customizable section 25 ############################################################################### 26 27 ############################################################################### 28 # BEGIN boilerplate args checking, usage messages 29 30 sub usage { 31 print <<"END_USAGE"; 32 Usage: $ME [OPTIONS] DIRECTORY-TO-CHECK 33 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. 39 40 For example, the following would be a correctly-matching pair of lines: 41 42 // swagger:operation GET /images/json compat getImages 43 r.Handle(VersionedPath("/images/json"), s.APIHandler(compat.GetImages)).Methods(http.MethodGet) 44 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. 49 50 The following is an inconsistency as reported by this script: 51 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) 56 57 ...because APIHandler() says 'compat' but the swagger comment 58 says 'libpod'. 59 60 OPTIONS: 61 62 -v, --verbose show verbose progress indicators 63 -n, --dry-run make no actual changes 64 65 --help display this message 66 --version display program name and version 67 END_USAGE 68 69 exit; 70 } 71 72 # Command-line options. Note that this operates directly on @ARGV ! 73 our $debug = 0; 74 our $force = 0; 75 our $verbose = 0; 76 our $NOT = ''; # print "blahing the blah$NOT\n" if $debug 77 sub handle_opts { 78 use Getopt::Long; 79 GetOptions( 80 'debug!' => \$debug, 81 'dry-run|n!' => sub { $NOT = ' [NOT]' }, 82 'force' => \$force, 83 'verbose|v' => \$verbose, 84 85 help => \&usage, 86 man => \&man, 87 version => sub { print "$ME version $VERSION\n"; exit 0 }, 88 ) or die "Try `$ME --help' for help\n"; 89 } 90 91 # END boilerplate args checking, usage messages 92 ############################################################################### 93 94 ############################## CODE BEGINS HERE ############################### 95 96 my $exit_status = 0; 97 98 # The term is "modulino". 99 __PACKAGE__->main() unless caller(); 100 101 # Main code. 102 sub main { 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 107 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; 111 112 # Find and act upon all matching files 113 find { wanted => sub { finder(@_) }, no_chdir => 1 }, $dir; 114 115 exit $exit_status; 116 } 117 118 119 ############ 120 # finder # File::Find action - looks for 'r.Handle' or 'r.HandleFunc' 121 ############ 122 sub finder { 123 my $path = $File::Find::name; 124 return if $path =~ m|/\.|; # skip dotfiles 125 return unless $path =~ /\.go$/; # Only want .go files 126 127 print $path, "\n" if $debug; 128 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"; 134 my @comments; 135 while (my $line = <$in>) { 136 if ($line =~ m!^\s*//!) { 137 push @comments, $line; 138 } 139 else { 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) 143 or $exit_status = 1; 144 } 145 146 # Reset comments 147 @comments = (); 148 } 149 } 150 close $in; 151 } 152 153 154 ################### 155 # handle_handle # Cross-check a 'r.Handle*' declaration against swagger 156 ################### 157 # 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. 160 # 161 sub handle_handle { 162 my $path = shift; # for error messages only 163 my $line = shift; # in: the r.Handle* line 164 my @comments = @_; # in: preceding comment lines 165 166 # Preserve the original line, so we can show it in comments 167 my $line_orig = $line; 168 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"; 172 my $indent = $1; 173 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"; 179 my $endpoint = $1; 180 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\}/; 185 186 # e.g. /auth, /containers/*/rename, /distribution, /monitor, /plugins 187 return 1 if $line =~ /\.UnsupportedHandler/; 188 189 # 190 # Determine the HTTP METHOD (GET, POST, DELETE, HEAD) 191 # 192 my $method; 193 if ($line =~ /generic.VersionHandler/) { 194 $method = 'GET'; 195 } 196 elsif ($line =~ m!\.Methods\((.*)\)!) { 197 my $x = $1; 198 199 if ($x =~ /Method(Post|Get|Delete|Head)/) { 200 $method = uc $1; 201 } 202 elsif ($x =~ /\"(HEAD|GET|POST)"/) { 203 $method = $1; 204 } 205 else { 206 die "$ME: $path:$.: Cannot grok $x\n"; 207 } 208 } 209 else { 210 warn "$ME: $path:$.: No Methods in '$line'\n"; 211 return 1; 212 } 213 214 # 215 # Determine the SWAGGER TAG. Assume 'compat' unless we see libpod; but 216 # this can be overruled (see special case below) 217 # 218 my $tag = ($endpoint =~ /(libpod)/ ? $1 : 'compat'); 219 220 # 221 # Determine the OPERATION. Done in a helper function because there 222 # are a lot of complicated special cases. 223 # 224 my $operation = operation_name($method, $endpoint); 225 226 # Special case: the following endpoints all get a custom tag 227 if ($endpoint =~ m!/(pods|manifests)/!) { 228 $tag = $1; 229 } 230 231 # Special case: anything related to 'events' gets a system tag 232 if ($endpoint =~ m!/events!) { 233 $tag = 'system'; 234 } 235 236 state $previous_path; # Previous path name, to avoid dups 237 238 # 239 # Compare actual swagger comment to what we expect based on Handle call. 240 # 241 my $expect = " // swagger:operation $method $endpoint $tag $operation "; 242 my @actual = grep { /swagger:operation/ } @comments; 243 244 return 1 if !@actual; # No swagger comment in file; oh well 245 246 my $actual = $actual[0]; 247 248 # (Ignore whitespace discrepancies) 249 (my $a_trimmed = $actual) =~ s/\s+/ /g; 250 251 return 1 if $a_trimmed eq $expect; 252 253 # Mismatch. Display it. Start with filename, if different from previous 254 print "\n"; 255 if (!$previous_path || $previous_path ne $path) { 256 print $path, ":\n"; 257 } 258 $previous_path = $path; 259 260 # Show the actual line, prefixed with '-' ... 261 print "- $actual[0]"; 262 # ...then our generated ones, but use '...' as a way to ignore matches 263 print "+ $indent//"; 264 my @actual_split = split ' ', $actual; 265 my @expect_split = split ' ', $expect; 266 for my $i (1 .. $#actual_split) { 267 print " "; 268 if ($actual_split[$i] eq ($expect_split[$i]||'')) { 269 print "." x length($actual_split[$i]); 270 } 271 else { 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; 276 } 277 } 278 print "\n"; 279 280 # Show the r.Handle* code line itself 281 print " ", $line_orig; 282 283 return; 284 } 285 286 287 #################### 288 # operation_name # Given a method + endpoint, return the swagger operation 289 #################### 290 sub operation_name { 291 my ($method, $endpoint) = @_; 292 293 # /libpod/foo/bar -> (libpod, foo, bar) 294 my @endpoints = grep { /\S/ } split '/', $endpoint; 295 296 # /libpod endpoints -> add 'Libpod' to end, e.g. PodStatsLibpod 297 my $Libpod = ''; 298 my $main = shift(@endpoints); 299 if ($main eq 'libpod') { 300 $Libpod = ucfirst($main); 301 $main = shift(@endpoints); 302 } 303 $main =~ s/s$//; # e.g. Volumes -> Volume 304 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'; 312 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'; 317 } 318 319 # All sorts of special cases 320 if ($action eq 'df') { 321 $action = 'dataUsage'; 322 } 323 elsif ($action eq "delete" && $endpoint eq "/libpod/play/kube") { 324 $action = "KubeDown" 325 } 326 # Grrrrrr, this one is annoying: some operations get an extra 'All' 327 elsif ($action =~ /^(delete|get|stats)$/ && $endpoint !~ /\{/) { 328 $action .= "All"; 329 $main .= 's' if $main eq 'container'; 330 } 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'; 334 } 335 # Ping is a special endpoint, and even if /libpod/_ping, no 'Libpod' 336 elsif ($main eq '_ping') { 337 $main = 'system'; 338 $action = 'ping'; 339 $Libpod = ''; 340 } 341 # Top-level compat endpoints 342 elsif ($main =~ /^(build|commit)$/) { 343 $main = 'image'; 344 $action = $1; 345 } 346 # Top-level system endpoints 347 elsif ($main =~ /^(auth|event|info|version)$/) { 348 $main = 'system'; 349 $action = $1; 350 $action .= 's' if $action eq 'event'; 351 } 352 353 return "\u${main}\u${action}$Libpod"; 354 } 355 356 357 1;