push.pl 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. #!/usr/bin/env perl
  2. use strict;
  3. use warnings;
  4. use 5.010;
  5. use open ':encoding(utf8)';
  6. use File::Basename qw(basename fileparse);
  7. use File::Temp;
  8. use Getopt::Long;
  9. use Mojo::UserAgent;
  10. use Mojo::Util qw(decode encode slurp spurt trim);
  11. use Term::UI;
  12. use Term::ReadLine;
  13. my $hubLengthLimit = 25_000;
  14. my $githubBase = 'https://github.com/docker-library/docs/tree/master'; # TODO point this at the correct "dist-xxx" branch based on "namespace"
  15. my $username;
  16. my $password;
  17. my $batchmode;
  18. my $namespace;
  19. GetOptions(
  20. 'u|username=s' => \$username,
  21. 'p|password=s' => \$password,
  22. 'batchmode!' => \$batchmode,
  23. 'namespace=s' => \$namespace,
  24. ) or die 'bad args';
  25. die 'no repos specified' unless @ARGV;
  26. my $ua = Mojo::UserAgent->new->max_redirects(10);
  27. $ua->transactor->name('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36');
  28. my $term = Term::ReadLine->new('docker-library-docs-push');
  29. unless (defined $username) {
  30. $username = $term->get_reply(prompt => 'Hub Username');
  31. }
  32. unless (defined $password) {
  33. $password = $term->get_reply(prompt => 'Hub Password'); # TODO hide the input? O:)
  34. }
  35. my $login = $ua->post('https://hub.docker.com/v2/users/login/' => {} => json => { username => $username, password => $password });
  36. die 'login failed' unless $login->success;
  37. my $token = $login->res->json->{token};
  38. my $csrf;
  39. for my $cookie (@{ $login->res->cookies }) {
  40. if ($cookie->name eq 'csrftoken') {
  41. $csrf = $cookie->value;
  42. last;
  43. }
  44. }
  45. die 'missing CSRF token' unless defined $csrf;
  46. my $attemptLogin = $ua->post('https://hub.docker.com/attempt-login/' => {} => json => { jwt => $token });
  47. die 'attempt-login failed' unless $attemptLogin->success;
  48. my $authorizationHeader = {
  49. Authorization => "JWT $token",
  50. 'X-CSRFToken' => $csrf,
  51. };
  52. my $userData = $ua->get('https://hub.docker.com/v2/user/' => $authorizationHeader);
  53. die 'user failed' unless $userData->success;
  54. $userData = $userData->res->json;
  55. sub prompt_for_edit {
  56. my $currentText = shift;
  57. my $proposedFile = shift;
  58. my $lengthLimit = shift // 0;
  59. my $proposedText = slurp $proposedFile or warn 'missing ' . $proposedFile;
  60. $proposedText = trim(decode('UTF-8', $proposedText));
  61. # remove our warning about generated files (Hub doesn't support HTML comments in Markdown)
  62. $proposedText =~ s% ^ <!-- .*? --> \s* %%sx;
  63. if ($lengthLimit > 0 && length($proposedText) > $lengthLimit) {
  64. # TODO https://github.com/docker/hub-beta-feedback/issues/238
  65. my $fullUrl = "$githubBase/$proposedFile";
  66. my $tagsNote = "**Note:** the description for this image is longer than the Hub length limit of $lengthLimit, so the \"Supported tags\" list has been trimmed to compensate. The full list can be found at [$fullUrl]($fullUrl#supported-tags-and-respective-dockerfile-links). See [docker/hub-beta-feedback#238](https://github.com/docker/hub-beta-feedback/issues/238) for more information.\n\n";
  67. my $genericNote = "**Note:** the description for this image is longer than the Hub length limit of $lengthLimit, so has been trimmed. The full description can be found at [$fullUrl]($fullUrl). See [docker/hub-beta-feedback#238](https://github.com/docker/hub-beta-feedback/issues/238) for more information.\n\n";
  68. my $trimmedText = $proposedText;
  69. # if our text is too long for the Hub length limit, let's first try removing the "Supported tags" list and add $tagsNote and see if that's enough to let us put the full image documentation
  70. $trimmedText =~ s%^(# Supported tags and respective `Dockerfile` links\n\n).*?\n(?=# |\[)%$1$tagsNote%ms;
  71. # (we scrape until the next "h1" or a line starting with a link which is likely a build status badge for an architecture-namespace)
  72. if (length($trimmedText) > $lengthLimit) {
  73. # ... if that doesn't do the trick, then do our older naïve description trimming
  74. $trimmedText = $genericNote . substr $proposedText, 0, ($lengthLimit - length($genericNote));
  75. }
  76. $proposedText = $trimmedText;
  77. }
  78. return $currentText if $currentText eq $proposedText;
  79. my @proposedFileBits = fileparse($proposedFile, qr!\.[^.]*!);
  80. my $file = File::Temp->new(SUFFIX => '-' . basename($proposedFileBits[1]) . '-current' . $proposedFileBits[2]);
  81. my $filename = $file->filename;
  82. spurt encode('UTF-8', $currentText . "\n"), $filename;
  83. my $tempProposedFile = File::Temp->new(SUFFIX => '-' . basename($proposedFileBits[1]) . '-proposed' . $proposedFileBits[2]);
  84. my $tempProposedFilename = $tempProposedFile->filename;
  85. spurt encode('UTF-8', $proposedText . "\n"), $tempProposedFilename;
  86. system(qw(git --no-pager diff --no-index), $filename, $tempProposedFilename);
  87. my $reply;
  88. if ($batchmode) {
  89. $reply = 'yes';
  90. }
  91. else {
  92. $reply = $term->get_reply(
  93. prompt => 'Apply changes?',
  94. choices => [ qw( yes vimdiff no quit ) ],
  95. default => 'yes',
  96. );
  97. }
  98. if ($reply eq 'quit') {
  99. say 'quitting, as requested';
  100. exit;
  101. }
  102. if ($reply eq 'yes') {
  103. return $proposedText;
  104. }
  105. if ($reply eq 'vimdiff') {
  106. system('vimdiff', $tempProposedFilename, $filename) == 0 or die "vimdiff on $filename and $proposedFile failed";
  107. return trim(decode('UTF-8', slurp($filename)));
  108. }
  109. return $currentText;
  110. }
  111. while (my $repo = shift) { # 'library/hylang', 'tianon/perl', etc
  112. $repo =~ s!^/+|/+$!!; # trim extra slashes (from "*/" globbing, for example)
  113. $repo = $namespace . '/' . $repo if $namespace; # ./push.pl --namespace xxx ...
  114. $repo = 'library/' . $repo unless $repo =~ m!/!; # "hylang" -> "library/hylang"
  115. my $repoName = $repo;
  116. $repoName =~ s!^.*/!!; # 'hylang', 'perl', etc
  117. my $repoUrl = 'https://hub.docker.com/v2/repositories/' . $repo . '/';
  118. my $repoTx = $ua->get($repoUrl => $authorizationHeader);
  119. warn 'warning: failed to get: ' . $repoUrl . ' (skipping)' and next unless $repoTx->success;
  120. my $repoDetails = $repoTx->res->json;
  121. $repoDetails->{description} //= '';
  122. $repoDetails->{full_description} //= '';
  123. my $hubShort = prompt_for_edit($repoDetails->{description}, $repoName . '/README-short.txt');
  124. my $hubLong = prompt_for_edit($repoDetails->{full_description}, $repoName . '/README.md', $hubLengthLimit);
  125. say 'no change to ' . $repoName . '; skipping' and next if $repoDetails->{description} eq $hubShort and $repoDetails->{full_description} eq $hubLong;
  126. say 'updating ' . $repoName;
  127. my $repoPatch = $ua->patch($repoUrl => $authorizationHeader => json => {
  128. description => $hubShort,
  129. full_description => $hubLong,
  130. });
  131. warn 'patch to ' . $repoUrl . ' failed: ' . $repoPatch->res->text and next unless $repoPatch->success;
  132. }