Add proper support for schemaVersion 2 (including manifest lists) in generate-tag-details.pl

This commit is contained in:
Tianon Gravi 2016-06-13 12:48:27 -07:00
parent a558264f9c
commit 2af56b2774
1 changed files with 260 additions and 41 deletions

View File

@ -8,8 +8,21 @@ use Mojo::UserAgent;
die 'no images specified' unless @ARGV;
my $mediaManifestList = 'application/vnd.docker.distribution.manifest.list.v2+json';
my $mediaManifestV2 = 'application/vnd.docker.distribution.manifest.v2+json';
my $mediaManifestV1 = 'application/vnd.docker.distribution.manifest.v1+json';
my $ua = Mojo::UserAgent->new->max_redirects(10);
$ua->transactor->name('Docker');
$ua->transactor->name(join ' ',
# https://github.com/docker/docker/blob/v1.11.2/dockerversion/useragent.go#L13-L34
'docker/1.11.2',
'go/1.6.2',
'git-commit/v1.11.2',
'kernel/4.4.11',
'os/linux',
'arch/amd64',
# BOGUS USER AGENTS FOR THE BOGUS USER AGENT THRONE
);
my $maxRetries = 3;
sub ua_req {
@ -102,7 +115,13 @@ sub get_manifest {
return $manifests{$image} if $manifests{$image};
my $manifestTx = registry_req(get => $repo => "manifests/$tag" => (
#Accept => 'application/vnd.docker.distribution.manifest.v2+json',
# prefer a "version 2" manifest
# https://docs.docker.com/registry/spec/manifest-v2-2/
Accept => [
$mediaManifestList,
$mediaManifestV2,
$mediaManifestV1,
],
));
return () if $manifestTx->res->code == 404; # tag doesn't exist
die "failed to get manifest for $image" unless $manifestTx->success;
@ -112,6 +131,26 @@ sub get_manifest {
);
}
sub blob_req {
my $method = shift;
my $repo = shift;
my $blob = shift;
my %extHeaders = @_;
return registry_req($method => $repo => "blobs/$blob" => %extHeaders);
}
sub get_blob_json {
my ($repo, $blob) = @_;
my $key = $repo . '@' . $blob;
state %blobs;
return $blobs{$key} if $blobs{$key};
my $tx = blob_req(get => ($repo, $blob) => ());
die "failed to get blob data for $key" unless $tx->success;
return $blobs{$key} = $tx->res->json;
}
sub get_blob_headers {
my ($repo, $blob) = @_;
@ -119,7 +158,7 @@ sub get_blob_headers {
state %headers;
return $headers{$key} if $headers{$key};
my $headersTx = registry_req(head => $repo => "blobs/$blob" => ());
my $headersTx = blob_req(head => ($repo, $blob) => ());
die "failed to get headers for $key" unless $headersTx->success;
return $headers{$key} = $headersTx->res->headers;
}
@ -142,6 +181,181 @@ sub get_layer_data {
return $layers{$id} = $data;
}
sub parse_manifest_v1_data {
my ($repo, $manifest) = @_;
my $data = {
manifestVersion => $mediaManifestV1,
manifest => $manifest,
imageId => undef,
platform => {},
dockerVersion => undef,
entrypoint => undef,
defaultCommand => undef,
layers => [],
commands => [],
};
my %seenBlob;
for my $fsLayer (@{ $manifest->{fsLayers} // [] }) {
my $blob = $fsLayer->{blobSum};
next unless $blob;
next if $seenBlob{$blob};
$seenBlob{$blob} = 1;
push @{ $data->{layers} }, {
digest => $blob,
};
}
for my $history (@{ $manifest->{history} // [] }) {
next unless $history->{v1Compatibility};
my $v1 = Mojo::Util::encode('UTF-8', $history->{v1Compatibility});
my $json = Mojo::JSON::decode_json($v1);
$data->{dockerVersion} //= $json->{docker_version};
$data->{platform}{os} //= $json->{os};
$data->{platform}{architecture} //= $json->{architecture};
$data->{entrypoint} //= $json->{config}{Entrypoint};
$data->{defaultCommand} //= $json->{config}{Cmd};
$data->{imageId} //= $json->{id};
# "history" in v1 is in reverse order (hence "unshift" instead of "push")
unshift @{ $data->{commands} }, {
created => $json->{created},
command => $json->{container_config}{Cmd},
};
}
return $data;
}
sub parse_manifest_v2_data {
my ($repo, $manifest) = @_;
my $configDigest = $manifest->{config}{digest};
my $config = get_blob_json($repo, $configDigest);
return {
manifestVersion => $mediaManifestV2,
manifest => $manifest,
imageId => $configDigest,
config => $config,
platform => {
os => $config->{os},
architecture => $config->{architecture},
},
dockerVersion => $config->{docker_version},
entrypoint => $config->{config}{Entrypoint},
defaultCommand => $config->{config}{Cmd},
layers => $manifest->{layers} // [],
commands => $config->{history} // [],
};
}
sub get_image_data {
my ($image) = @_;
my ($repo, $tag) = split_image_name($image);
my ($digest, $manifest) = get_manifest($repo, $tag);
unless (defined $digest && defined $manifest) {
# tag must not exist!
return;
}
my $data = {
repo => $repo,
tag => $tag,
digest => $digest,
images => [],
};
if ($manifest->{schemaVersion} eq '1') {
# https://docs.docker.com/registry/spec/manifest-v2-1/
push @{$data->{images}}, parse_manifest_v1_data($repo, $manifest);
}
elsif ($manifest->{schemaVersion} eq '2') {
# https://docs.docker.com/registry/spec/manifest-v2-2/
if ($manifest->{mediaType} eq $mediaManifestV2) {
push @{$data->{images}}, parse_manifest_v2_data($repo, $manifest);
}
elsif ($manifest->{mediaType} eq $mediaManifestList) {
$data->{manifest} = $manifest;
$data->{manifestVersion} = $manifest->{mediaType};
for my $sub (@{ $manifest->{manifests} // [] }) {
my $digest = $sub->{digest};
die "sub-manifest missing digest!" unless $digest;
my $subManifest = get_manifest($repo, $digest);
die "manifest $digest does not exist!" unless defined $subManifest;
my $subData;
if ($sub->{mediaType} eq $mediaManifestV1) {
$subData = parse_manifest_v1_data($repo, $subManifest);
}
elsif ($sub->{mediaType} eq $mediaManifestV2) {
$subData = parse_manifest_v2_data($repo, $subManifest);
}
else {
die "unknown mediaType $manifest->{mediaType} for $digest";
}
$subData->{digest} = $digest;
$subData->{platform} = $sub->{platform};
push @{$data->{images}}, $subData;
}
}
else {
die "unknown mediaType $manifest->{mediaType} for schemaVersion 2";
}
}
else {
die "unknown schemaVersion: $manifest->{schemaVersion}";
}
for my $image (@{ $data->{images} }) {
$image->{platform} //= {};
$image->{layers} //= [];
$image->{size} = 0;
for my $layer (@{ $image->{layers} }) {
my $headers = get_blob_headers($repo, $layer->{digest});
$layer->{size} //= $headers->content_length;
$layer->{mediaType} //= $headers->content_type;
$layer->{lastModified} //= $headers->last_modified;
$image->{size} += $layer->{size};
}
$image->{commands} //= [];
for my $command (@{ $image->{commands} }) {
$command->{command} //= [ $command->{created_by} ];
$command->{dockerfile} //= cmd_to_dockerfile($command->{command});
}
}
return $data;
}
sub platform_string {
my $platform = shift;
return (
($platform->{os} // 'linux')
. (defined $platform->{'os.version'} ? ' version ' . $platform->{'os.version'} : '')
. (defined $platform->{'os.features'} ? ' ft. ' . join(', ', @{ $platform->{'os.features'} }) : '')
. '; '
. ($platform->{architecture} // 'amd64')
. (defined $platform->{variant} ? ' variant ' . $platform->{variant} : '')
. (defined $platform->{features} ? ' ft. ' . join(', ', @{ $platform->{features} }) : '')
);
}
sub cmd_to_dockerfile {
my ($cmd) = @_;
@ -224,61 +438,66 @@ sub date {
while (my $image = shift) {
print "\n";
say '## `' . $image . '`';
my ($repo, $tag) = split_image_name($image);
my ($digest, $manifest) = get_manifest($repo, $tag);
my $data = get_image_data($image);
unless (defined $digest && defined $manifest) {
unless ($data) {
# tag must not exist yet!
say "\n", '**does not exist** (yet?)';
next;
}
my $repo = $data->{repo};
$repo =~ s!^library/!!;
print "\n";
say '```console';
say '$ docker pull ' . $repo . '@' . $digest;
say '$ docker pull ' . $repo . '@' . $data->{digest};
say '```';
my %parentChild;
my %totals = (
virtual_size => 0,
blob_content_length => 0,
);
for my $i (0 .. $#{ $manifest->{fsLayers} }) {
my $v1 = Mojo::Util::encode 'UTF-8', $manifest->{history}[$i]{v1Compatibility};
my $data = get_layer_data(
$repo, undef,
$manifest->{fsLayers}[$i]{blobSum},
Mojo::JSON::decode_json($v1),
);
$parentChild{$data->{parent} // ''} = $data->{id};
$totals{$_} += $data->{$_} for keys %totals;
print "\n";
say '- Manifest MIME: `' . $data->{manifestVersion} . '`' if $data->{manifestVersion};
say '- Platforms:';
for my $imageData (@{ $data->{images} }) {
say ' - ' . platform_string($imageData->{platform});
}
for my $imageData (@{ $data->{images} }) {
print "\n";
say "-\t" . 'Total Virtual Size: ' . size($totals{virtual_size}) if $totals{virtual_size};
say "-\t" . 'Total v2 Content-Length: ' . size($totals{blob_content_length});
say '### `' . $image . '` - ' . platform_string($imageData->{platform});
if ($imageData->{digest}) {
print "\n";
say '### Layers (' . scalar(keys %parentChild) . ')';
my $cur = $parentChild{''};
while ($cur) {
print "\n";
say '#### `' . $cur . '`';
my $data = get_layer_data($repo, $cur);
if ($data->{container_command}) {
print "\n";
say '```dockerfile';
say cmd_to_dockerfile($data->{container_command});
say '```console';
say '$ docker pull ' . $repo . '@' . $imageData->{digest};
say '```';
}
print "\n";
say "-\t" . 'Created: ' . date($data->{created}) if $data->{created};
say "-\t" . 'Parent Layer: `' . $data->{parent} . '`' if $data->{parent};
say "-\t" . 'Docker Version: ' . $data->{docker_version} if $data->{docker_version};
say "-\t" . 'Virtual Size: ' . size($data->{virtual_size}) if $totals{virtual_size};
say "-\t" . 'v2 Blob: `' . $data->{blob} . '`';
say "-\t" . 'v2 Content-Length: ' . size($data->{blob_content_length});
say "-\t" . 'v2 Last-Modified: ' . date($data->{blob_last_modified}) if $data->{blob_last_modified};
$cur = $parentChild{$cur};
say '- Docker Version: ' . $imageData->{dockerVersion} if $imageData->{dockerVersion};
say '- Manifest MIME: `' . $imageData->{manifestVersion} . '`' if $imageData->{manifestVersion};
say '- Total Size: **' . size($imageData->{size}) . '** ';
say ' (compressed transfer size, not on-disk size)';
say '- Image ID: `' . $imageData->{imageId} . '`' if $imageData->{imageId};
say '- Entrypoint: `' . Mojo::JSON::encode_json($imageData->{entrypoint}) . '`' if $imageData->{entrypoint} && @{ $imageData->{entrypoint} };
say '- Default Command: `' . Mojo::JSON::encode_json($imageData->{defaultCommand}) . '`' if $imageData->{defaultCommand};
print "\n";
say '```dockerfile';
for my $command (@{ $imageData->{commands} }) {
say '# ' . date($command->{created});
say $command->{dockerfile};
}
say '```';
print "\n";
say '- Layers:';
for my $layer (@{ $imageData->{layers} }) {
say ' - `' . $layer->{digest} . '` ';
say ' Last Modified: ' . date($layer->{lastModified}) . ' ';
say ' Size: ' . size($layer->{size});
}
}
}