Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
open-build-service/src/backend/bs_regpush
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
executable file
937 lines (858 sloc)
30.5 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/perl -w | |
| # | |
| # Copyright (c) 2018 SUSE Inc. | |
| # | |
| # This program is free software; you can redistribute it and/or modify | |
| # it under the terms of the GNU General Public License version 2 as | |
| # published by the Free Software Foundation. | |
| # | |
| # This program is distributed in the hope that it will be useful, | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| # GNU General Public License for more details. | |
| # | |
| # You should have received a copy of the GNU General Public License | |
| # along with this program (see the file COPYING); if not, write to the | |
| # Free Software Foundation, Inc., | |
| # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA | |
| # | |
| ################################################################ | |
| # | |
| # Registry interfacing | |
| # | |
| BEGIN { | |
| my ($wd) = $0 =~ m-(.*)/- ; | |
| $wd ||= '.'; | |
| unshift @INC, "$wd/build"; | |
| unshift @INC, "$wd"; | |
| } | |
| use JSON::XS (); | |
| use Digest::SHA (); | |
| use Data::Dumper; | |
| use Compress::Zlib; | |
| use BSRPC ':https'; | |
| use BSHTTP; | |
| use BSTar; | |
| use BSContar; | |
| use BSUtil; | |
| use BSBearer; | |
| use strict; | |
| my $registry_timeout = 300; | |
| my $dest_creds; | |
| my $use_image_tags; | |
| my $multiarch; | |
| my $digestfile; | |
| my $writeinfofile; | |
| my $delete_mode; | |
| my $delete_except_mode; | |
| my $list_mode; | |
| my $no_cosign_info; | |
| my @tags; | |
| my $blobdir; | |
| my $oci; | |
| my $cosign; | |
| my $cosigncookie; | |
| my $slsaprovenance; | |
| my $sbom; | |
| my $gun; | |
| my @signcmd; | |
| my $pubkeyfile; | |
| my $rekorserver; | |
| my $registryserver; | |
| my $repository; | |
| my @tarfiles; | |
| my $registry_authenticator; | |
| my $keepalive; | |
| my $cosign_cookie_name = 'org.open-build-service.cosign.cookie'; | |
| sub send_layer { | |
| my ($param) = @_; | |
| my $sl = $param->{'send_layer_data'}; # [ $layer_ent, $offset ], | |
| my $chunk = BSTar::extract($sl->[0]->{'file'}, $sl->[0], $sl->[1], 65536); | |
| $sl->[1] += length($chunk); | |
| return $chunk; | |
| } | |
| sub blob_exists { | |
| my ($blobid, $size) = @_; | |
| my $replyheaders; | |
| my $param = { | |
| 'uri' => "$registryserver/v2/$repository/blobs/$blobid", | |
| 'request' => 'HEAD', | |
| 'authenticator' => $registry_authenticator, | |
| 'timeout' => $registry_timeout, | |
| 'replyheaders' => \$replyheaders, | |
| 'maxredirects' => 1, | |
| }; | |
| eval { BSRPC::rpc($param) }; | |
| if ($replyheaders) { | |
| die("size mismatch?\n") if $replyheaders->{'content-length'} && $size && $size != $replyheaders->{'content-length'}; | |
| return 0 if $replyheaders->{'docker-content-digest'} && $blobid ne $replyheaders->{'docker-content-digest'}; | |
| return 1; | |
| } | |
| return 0; | |
| } | |
| sub blob_upload { | |
| my ($blobid, $upload_ent) = @_; | |
| return $blobid if blob_exists($blobid, $upload_ent->{'size'}); | |
| print "uploading layer $blobid... "; | |
| my $replyheaders; | |
| my $param = { | |
| 'uri' => "$registryserver/v2/$repository/blobs/uploads/", | |
| 'request' => 'POST', | |
| 'authenticator' => $registry_authenticator, | |
| 'timeout' => $registry_timeout, | |
| 'replyheaders' => \$replyheaders, | |
| }; | |
| BSRPC::rpc($param); | |
| my $loc = $replyheaders->{'location'}; | |
| my @locextra; | |
| if ($loc =~ s/\?(.*)$//) { | |
| push @locextra, split('&', $1); | |
| s/%([a-fA-F0-9]{2})/chr(hex($1))/ge for @locextra; | |
| } | |
| die("no location in upload reply\n") unless $loc; | |
| $loc = "$registryserver$loc" if $loc =~ /^\//; | |
| $param = { | |
| 'headers' => [ "Content-Length: $upload_ent->{'size'}", "Content-Type: application/octet-stream" ], | |
| 'uri' => $loc, | |
| 'request' => 'PUT', | |
| 'authenticator' => $registry_authenticator, | |
| 'replyheaders' => \$replyheaders, | |
| 'data' => \&send_layer, | |
| 'send_layer_data' => [ $upload_ent, 0 ], | |
| }; | |
| $replyheaders = undef; | |
| BSRPC::rpc($param, undef, @locextra, "digest=$blobid"); | |
| my $id = $replyheaders->{'docker-content-digest'}; | |
| die("server did not return a content id\n") unless $id; | |
| die("server created a different blobid: $blobid $id\n") if $blobid ne $id; | |
| print "done.\n"; | |
| return $blobid; | |
| } | |
| sub blob_upload_content { | |
| my ($blobid, $content) = @_; | |
| return blob_upload($blobid, { 'data' => $content, 'size' => length($content) }); | |
| } | |
| sub blob_download { | |
| my ($blobid, $filename) = @_; | |
| my $stdout_receiver = sub { | |
| while(1) { | |
| my $s = BSHTTP::read_data($_[0], 8192); | |
| return {} if $s eq ''; | |
| print($s) || die("write: $!\n"); | |
| } | |
| }; | |
| my $param = { | |
| 'uri' => "$registryserver/v2/$repository/blobs/$blobid", | |
| 'authenticator' => $registry_authenticator, | |
| 'timeout' => $registry_timeout, | |
| 'receiver' => $filename eq '-' ? $stdout_receiver : \&BSHTTP::file_receiver, | |
| 'filename' => $filename, | |
| 'maxredirects' => 1, | |
| }; | |
| BSRPC::rpc($param); | |
| } | |
| sub blob_fetch { | |
| my ($blobid) = @_; | |
| my $param = { | |
| 'uri' => "$registryserver/v2/$repository/blobs/$blobid", | |
| 'authenticator' => $registry_authenticator, | |
| 'timeout' => $registry_timeout, | |
| 'maxredirects' => 1, | |
| }; | |
| return BSRPC::rpc($param); | |
| } | |
| sub manifest_exists { | |
| my ($manifest, $tag, $content_type) = @_; | |
| $content_type ||= $BSContar::mt_docker_manifest; | |
| my $maniid = 'sha256:'.Digest::SHA::sha256_hex($manifest); | |
| $tag = $maniid unless defined $tag; | |
| my $replyheaders; | |
| my $param = { | |
| 'headers' => [ "Accept: $content_type" ], | |
| 'uri' => "$registryserver/v2/$repository/manifests/$tag", | |
| 'authenticator' => $registry_authenticator, | |
| 'replyheaders' => \$replyheaders, | |
| 'timeout' => $registry_timeout, | |
| 'keepalive' => $keepalive, | |
| }; | |
| my $maniret; | |
| eval { $maniret = BSRPC::rpc($param) }; | |
| if ($maniret) { | |
| my $maniretid = $replyheaders->{'docker-content-digest'}; | |
| $maniretid ||= 'sha256:'.Digest::SHA::sha256_hex($maniret); | |
| return 1 if $maniretid eq $maniid; | |
| } | |
| return 0; | |
| } | |
| sub manifest_upload { | |
| my ($manifest, $tag, $content_type, $quiet) = @_; | |
| my $maniid = 'sha256:'.Digest::SHA::sha256_hex($manifest); | |
| return $maniid if manifest_exists($manifest, $tag, $content_type); | |
| $content_type ||= $BSContar::mt_docker_manifest; | |
| my $fat = ''; | |
| $fat = 'fat ' if $content_type eq $BSContar::mt_docker_manifestlist || $content_type eq $BSContar::mt_oci_index; | |
| if (!$quiet) { | |
| if (defined($tag)) { | |
| print "uploading ${fat}manifest $maniid for tag '$tag'... "; | |
| } else { | |
| print "uploading ${fat}manifest $maniid... "; | |
| } | |
| } | |
| $tag = $maniid unless defined $tag; | |
| my $replyheaders; | |
| my $param = { | |
| 'headers' => [ "Content-Type: $content_type" ], | |
| 'uri' => "$registryserver/v2/$repository/manifests/$tag", | |
| 'request' => 'PUT', | |
| 'authenticator' => $registry_authenticator, | |
| 'replyheaders' => \$replyheaders, | |
| 'data' => $manifest, | |
| }; | |
| BSRPC::rpc($param, undef); | |
| my $id = $replyheaders->{'docker-content-digest'}; | |
| die("server did not return a content id\n") unless $id; | |
| die("server created a different manifest id: $maniid $id\n") if $maniid ne $id; | |
| print "done.\n" unless $quiet; | |
| return $maniid; | |
| } | |
| sub manifest_append { | |
| my ($manifest, $tag) = @_; | |
| my $maniid = 'sha256:'.Digest::SHA::sha256_hex($manifest); | |
| my $str = "$maniid ".length($manifest).(defined($tag) ? " $tag" : '')."\n"; | |
| if ($digestfile ne '-') { | |
| BSUtil::appendstr($digestfile, $str); | |
| } else { | |
| print $str; | |
| } | |
| } | |
| sub manifest_upload_tags { | |
| my ($manifest, $tags, $content_type) = @_; | |
| if (!@{$tags || []}) { | |
| manifest_upload($manifest, undef, $content_type); | |
| manifest_append($manifest, undef) if defined $digestfile; | |
| return; | |
| } | |
| for my $tag (BSUtil::unify(@$tags)) { | |
| manifest_upload($manifest, $tag, $content_type); | |
| manifest_append($manifest, $tag) if defined $digestfile; | |
| } | |
| } | |
| sub cosign_upload { | |
| my ($tag, $config, @layers) = @_; | |
| my $config_blobid = blob_upload_content('sha256:'.Digest::SHA::sha256_hex($config), $config); | |
| my $config_data = { | |
| 'mediaType' => $BSContar::mt_oci_config, | |
| 'size' => length($config), | |
| 'digest' => $config_blobid, | |
| }; | |
| my $mediaType = $BSContar::mt_oci_manifest; | |
| my $mani = { | |
| 'schemaVersion' => 2, | |
| 'mediaType' => $mediaType, | |
| 'config' => $config_data, | |
| 'layers' => [], | |
| }; | |
| while (@layers >= 2) { | |
| my ($payload_layer, $payload) = splice(@layers, 0, 2); | |
| my $payload_blobid = blob_upload_content('sha256:'.Digest::SHA::sha256_hex($payload), $payload); | |
| die unless $payload_blobid eq $payload_layer->{'digest'}; | |
| push @{$mani->{'layers'}}, $payload_layer; | |
| } | |
| my $mani_json = BSContar::create_dist_manifest_list($mani); | |
| return manifest_upload($mani_json, $tag, $mediaType); | |
| } | |
| sub get_all_tags { | |
| my @regtags; | |
| my $replyheaders; | |
| my $param = { | |
| 'uri' => "$registryserver/v2/$repository/tags/list", | |
| 'authenticator' => $registry_authenticator, | |
| 'timeout' => $registry_timeout, | |
| 'replyheaders' => \$replyheaders, | |
| 'keepalive' => $keepalive, | |
| }; | |
| while (1) { | |
| undef $replyheaders; | |
| my $r = BSRPC::rpc($param, \&JSON::XS::decode_json); | |
| push @regtags, @{$r->{'tags'} || []}; | |
| last unless $replyheaders->{'link'}; | |
| die unless $replyheaders->{'link'} =~ /^<(\/v2\/.*)>/; | |
| $param->{'uri'} = "$registryserver$1"; | |
| $param->{'verbatim_uri'} = 1; | |
| } | |
| return BSUtil::unify(@regtags); | |
| } | |
| sub get_all_repositories { | |
| my @repos; | |
| my $replyheaders; | |
| my $param = { | |
| 'uri' => "$registryserver/v2/_catalog", | |
| 'authenticator' => $registry_authenticator, | |
| 'timeout' => $registry_timeout, | |
| 'replyheaders' => \$replyheaders, | |
| }; | |
| while (1) { | |
| undef $replyheaders; | |
| my $r = BSRPC::rpc($param, \&JSON::XS::decode_json); | |
| push @repos, @{$r->{'repositories'}}; | |
| last unless $replyheaders->{'link'}; | |
| die("Bad link: $replyheaders->{'link'}\n") unless $replyheaders->{'link'} =~ /^<(\/v2\/\S+)>/; | |
| $param->{'uri'} = "$registryserver$1"; | |
| $param->{'verbatim_uri'} = 1; | |
| } | |
| return BSUtil::unify(@repos); | |
| } | |
| sub get_manifest_for_tag { | |
| my ($tag) = @_; | |
| my $replyheaders; | |
| my $param = { | |
| 'uri' => "$registryserver/v2/$repository/manifests/$tag", | |
| 'headers' => [ "Accept: $BSContar::mt_docker_manifest, $BSContar::mt_docker_manifestlist, $BSContar::mt_oci_manifest, $BSContar::mt_oci_index" ], | |
| 'authenticator' => $registry_authenticator, | |
| 'replyheaders' => \$replyheaders, | |
| 'timeout' => $registry_timeout, | |
| 'keepalive' => $keepalive, | |
| }; | |
| my $mani_json; | |
| eval { $mani_json = BSRPC::rpc($param); }; | |
| if ($@) { | |
| return () if $@ =~ /^404/; # tag does not exist | |
| die($@); | |
| } | |
| my $mani = JSON::XS::decode_json($mani_json); | |
| my $maniid = $replyheaders->{'docker-content-digest'}; | |
| die("$tag: no docker-content-digest\n") unless $maniid; | |
| return ($mani, $maniid, $mani_json); | |
| } | |
| sub delete_manifest { | |
| my ($maniid) = @_; | |
| die("not a manifest digest: $maniid\n") unless $maniid =~ /^sha256:[0-9a-f]{64}$/; | |
| my ($mani, $maniid2, $mani_json) = get_manifest_for_tag($maniid); | |
| return unless $maniid2; | |
| die("manifest digest mismatch\n") if $maniid2 ne $maniid; | |
| print "deleting manifest $maniid\n"; | |
| my $param = { | |
| 'uri' => "$registryserver/v2/$repository/manifests/$maniid", | |
| 'request' => 'DELETE', | |
| 'authenticator' => $registry_authenticator, | |
| 'timeout' => $registry_timeout, | |
| }; | |
| BSRPC::rpc($param, undef); | |
| } | |
| my $cannot_directly_delete_tags; | |
| sub delete_tag { | |
| my ($tag) = @_; | |
| my ($mani, $maniid, $mani_json) = get_manifest_for_tag($tag); | |
| return unless $maniid; | |
| print "deleting tag $tag [$maniid]\n"; | |
| if (!$cannot_directly_delete_tags) { | |
| my $param = { | |
| 'uri' => "$registryserver/v2/$repository/manifests/$tag", | |
| 'request' => 'DELETE', | |
| 'authenticator' => $registry_authenticator, | |
| 'timeout' => $registry_timeout, | |
| }; | |
| eval { BSRPC::rpc($param, undef); }; | |
| return unless $@; | |
| die($@) unless $@ =~ /^40[05]/; | |
| $cannot_directly_delete_tags = 1; | |
| } | |
| # now mangle and upload so that we get a new unique image id for that tag | |
| my $mani_json_mangled = "\n\n".JSON::XS->new->utf8->canonical->encode($mani)."\n\n"; | |
| my $newmaniid = manifest_upload($mani_json_mangled, $tag, $mani->{'mediaType'}, 1); | |
| # then delete the new unique image id, thus deleting the tag as well | |
| my $param = { | |
| 'uri' => "$registryserver/v2/$repository/manifests/$newmaniid", | |
| 'request' => 'DELETE', | |
| 'authenticator' => $registry_authenticator, | |
| 'timeout' => $registry_timeout, | |
| }; | |
| eval { BSRPC::rpc($param, undef); }; | |
| my $err = $@; | |
| if ($err) { | |
| # delete failed, switch tag back to the old image | |
| eval { manifest_upload($mani_json, $tag, $mani->{'mediaType'}, 1) if $newmaniid ne $maniid; }; | |
| die($err); | |
| } | |
| } | |
| sub list_tag { | |
| my ($tag) = @_; | |
| if ($no_cosign_info && $tag =~ /^[a-z0-9]+-[a-f0-9]+\.(?:sig|att)$/) { | |
| printf "%-20s -\n", $tag; | |
| return; | |
| } | |
| my ($mani, $maniid) = get_manifest_for_tag($tag); | |
| my $extra = ''; | |
| if ($tag =~ /^[a-z0-9]+-[a-f0-9]+\.sig$/ && $mani && $mani->{'mediaType'} && $mani->{'mediaType'} eq $BSContar::mt_oci_manifest) { | |
| if (@{$mani->{'layers'} || []} == 1 && $mani->{'layers'}->[0]->{'mediaType'} eq 'application/vnd.dev.cosign.simplesigning.v1+json') { | |
| my $annotations = $mani->{'layers'}->[0]->{'annotations'} || {}; | |
| my $cookie = $annotations->{$cosign_cookie_name}; | |
| $extra = " cosigncookie=$cookie" if $cookie; | |
| } | |
| } | |
| if ($tag =~ /^[a-z0-9]+-[a-f0-9]+\.att$/ && $mani && $mani->{'mediaType'} && $mani->{'mediaType'} eq $BSContar::mt_oci_manifest) { | |
| if (@{$mani->{'layers'} || []} >= 1 && $mani->{'layers'}->[0]->{'mediaType'} eq 'application/vnd.dsse.envelope.v1+json') { | |
| my $annotations = $mani->{'layers'}->[0]->{'annotations'} || {}; | |
| my $cookie = $annotations->{$cosign_cookie_name}; | |
| $extra = " cosigncookie=$cookie" if $cookie; | |
| } | |
| } | |
| if (!$mani) { | |
| printf "%-20s -\n", $tag; | |
| } elsif (!$mani->{'mediaType'}) { | |
| printf "%-20s %s %s\n", $tag, $maniid, 'v1image'; | |
| } elsif ($mani->{'mediaType'} eq $BSContar::mt_docker_manifestlist) { | |
| printf "%-20s %s %s\n", $tag, $maniid, 'list'; | |
| } elsif ($mani->{'mediaType'} eq $BSContar::mt_docker_manifest) { | |
| printf "%-20s %s %s%s\n", $tag, $maniid, 'image', $extra; | |
| } elsif ($mani->{'mediaType'} eq $BSContar::mt_oci_manifest) { | |
| printf "%-20s %s %s%s\n", $tag, $maniid, 'ociimage', $extra; | |
| } elsif ($mani->{'mediaType'} eq $BSContar::mt_oci_index) { | |
| printf "%-20s %s %s\n", $tag, $maniid, 'ocilist'; | |
| } else { | |
| printf "%-20s %s %s\n", $tag, $maniid, 'unknown'; | |
| } | |
| } | |
| sub tags_from_digestfile { | |
| my ($add_cosign_tags) = @_; | |
| return () unless $digestfile; | |
| my @ret; | |
| local *DIG; | |
| open(DIG, '<', $digestfile) || die("$digestfile: $!\n"); | |
| while (<DIG>) { | |
| chomp; | |
| next if /^#/ || /^\s*$/; | |
| push @ret, "$1-$2.sig", "$1-$2.att" if $add_cosign_tags && /^([a-z0-9]+):([a-f0-9]+) (\d+)/; | |
| next if /^([a-z0-9]+):([a-f0-9]+) (\d+)\s*$/; # ignore anonymous images | |
| die("bad line in digest file\n") unless /^([a-z0-9]+):([a-f0-9]+) (\d+) (.+?)\s*$/; | |
| push @ret, $4; | |
| } | |
| close(DIG); | |
| return @ret; | |
| } | |
| sub construct_container_tar { | |
| my ($containerinfo) = @_; | |
| die("Must specify a blobdir for containerinfos\n") unless $blobdir; | |
| my $manifest = $containerinfo->{'tar_manifest'}; | |
| my $mtime = $containerinfo->{'tar_mtime'}; | |
| my $blobids = $containerinfo->{'tar_blobids'}; | |
| die("containerinfo is incomplete\n") unless $mtime && $manifest && $blobids; | |
| my @tar; | |
| for my $blobid (@$blobids) { | |
| my $fd; | |
| open($fd, '<', "$blobdir/_blob.$blobid") || die("$blobdir/_blob.$blobid: $!\n"); | |
| push @tar, {'name' => $blobid, 'file' => $fd, 'mtime' => $mtime, 'offset' => 0, 'size' => (-s $fd)}; | |
| } | |
| push @tar, {'name' => 'manifest.json', 'data' => $manifest, 'mtime' => $mtime, 'size' => length($manifest)}; | |
| return \@tar; | |
| } | |
| sub open_tarfile { | |
| my ($tarfile) = @_; | |
| my ($tar, $tarfd, $govariant); | |
| if ($tarfile =~ /\.containerinfo$/) { | |
| my $containerinfo_json = readstr($tarfile); | |
| my $containerinfo = JSON::XS::decode_json($containerinfo_json); | |
| $govariant = $containerinfo->{'govariant'}; | |
| $tar = construct_container_tar($containerinfo); | |
| } elsif ($tarfile =~ /\.helminfo$/) { | |
| my $chart = $tarfile; | |
| $chart =~ s/\.helminfo$/.tgz/; | |
| die("$chart: $!\n") unless -e $chart; | |
| my $helminfo_json = readstr($tarfile); | |
| my $helminfo = JSON::XS::decode_json($helminfo_json); | |
| ($tar) = BSContar::container_from_helm($chart, $helminfo->{'config_json'}, $helminfo->{'tags'}); | |
| } else { | |
| open($tarfd, '<', $tarfile) || die("$tarfile: $!\n"); | |
| $tar = BSTar::list($tarfd); | |
| $_->{'file'} = $tarfd for @$tar; | |
| } | |
| return ($tar, $tarfd, $govariant); | |
| } | |
| sub die_with_usage { | |
| die <<'END'; | |
| usage: bs_regpush [options] <registryserver> repository tar [tar...] | |
| bs_regpush [options] -l <registryserver> [repository [tag]] | |
| bs_regpush [options] -D <registryserver> repository | |
| bs_regpush [options] -X <registryserver> repository | |
| MODES: | |
| - upload mode | |
| -l - list mode | |
| -D - delete mode | |
| -X - delete except mode | |
| OPTIONS: | |
| --dest-creds - credentials in form <user>:<password> or "-" to read from STDIN | |
| -T - use image tags | |
| -m - push multiarch image | |
| -t - tag (can be given multiple times) | |
| -F - digestfile, output in upload mode, otherwise input | |
| END | |
| } | |
| $| = 1; | |
| while (@ARGV) { | |
| if ($ARGV[0] eq '--dest-creds') { | |
| $dest_creds = BSBearer::get_credentials($ARGV[1]); | |
| splice(@ARGV, 0, 2); | |
| } elsif ($ARGV[0] eq '-T') { | |
| $use_image_tags = 1; | |
| shift @ARGV; | |
| } elsif ($ARGV[0] eq '-m') { | |
| $multiarch = 1; | |
| shift @ARGV; | |
| } elsif ($ARGV[0] eq '-t') { | |
| push @tags, $ARGV[1]; | |
| splice(@ARGV, 0, 2); | |
| } elsif ($ARGV[0] eq '-F') { | |
| $digestfile = $ARGV[1]; | |
| splice(@ARGV, 0, 2); | |
| } elsif ($ARGV[0] eq '--write-info') { | |
| (undef, $writeinfofile) = splice(@ARGV, 0, 2); | |
| } elsif ($ARGV[0] eq '-D') { | |
| $delete_mode = 1; | |
| shift @ARGV; | |
| } elsif ($ARGV[0] eq '-l') { | |
| $list_mode = 1; | |
| shift @ARGV; | |
| } elsif ($ARGV[0] eq '--no-cosign-info') { | |
| $no_cosign_info = 1; | |
| shift @ARGV; | |
| } elsif ($ARGV[0] eq '-X') { | |
| $delete_except_mode = 1; | |
| shift @ARGV; | |
| } elsif ($ARGV[0] eq '-B') { | |
| $blobdir = $ARGV[1]; | |
| splice(@ARGV, 0, 2); | |
| } elsif ($ARGV[0] eq '--oci') { | |
| $oci = 1; | |
| shift @ARGV; | |
| } elsif ($ARGV[0] eq '--cosign') { | |
| $cosign = 1; | |
| shift @ARGV; | |
| } elsif ($ARGV[0] eq '--cosigncookie') { | |
| (undef, $cosigncookie) = splice(@ARGV, 0, 2); | |
| } elsif ($ARGV[0] eq '--slsaprovenance') { | |
| $slsaprovenance = 1; | |
| shift @ARGV; | |
| } elsif ($ARGV[0] eq '--sbom') { | |
| $sbom = 1; | |
| shift @ARGV; | |
| } elsif ($ARGV[0] eq '--rekor') { | |
| (undef, $rekorserver) = splice(@ARGV, 0, 2); | |
| } elsif ($ARGV[0] eq '-G') { | |
| (undef, $gun) = splice(@ARGV, 0, 2); | |
| } elsif ($ARGV[0] eq '-p') { | |
| (undef, $pubkeyfile) = splice(@ARGV, 0, 2); | |
| } elsif ($ARGV[0] eq '--dest-creds') { | |
| $dest_creds = BSBearer::get_credentials($ARGV[1]); | |
| splice(@ARGV, 0, 2); | |
| } elsif ($ARGV[0] eq '-P' || $ARGV[0] eq '--project' || $ARGV[0] eq '-u' || $ARGV[0] eq '--signtype' || $ARGV[0] eq '-h') { | |
| my @signopts = splice(@ARGV, 0, 2); | |
| push @signcmd, @signopts unless $signopts[0] eq '-h'; | |
| } else { | |
| last; | |
| } | |
| } | |
| $registry_authenticator = BSBearer::generate_authenticator($dest_creds, 'verbose' => (-c STDOUT ? 1 : 0)); | |
| if ($list_mode) { | |
| ($registryserver, $repository) = @ARGV; | |
| if (@ARGV == 1) { | |
| for my $repo (sort(get_all_repositories())) { | |
| print "$repo\n"; | |
| } | |
| } elsif (@ARGV == 2) { | |
| $keepalive = {}; | |
| my %tags = map {$_ => 1} @tags; | |
| $tags{$_} = 1 for tags_from_digestfile(); | |
| %tags = map {$_ => 1} get_all_tags() unless %tags; | |
| list_tag($_) for sort keys %tags; | |
| } elsif (@ARGV == 3) { | |
| my ($mani, $maniid, $mani_json) = get_manifest_for_tag($ARGV[2]); | |
| print "$mani_json\n" if $mani_json; | |
| } elsif (@ARGV == 4) { | |
| if ($ARGV[3] eq 'config' || $ARGV[3] eq 'config.json') { | |
| my ($mani) = get_manifest_for_tag($ARGV[2]); | |
| my $config = blob_fetch($mani->{'config'}->{'digest'}); | |
| $config = JSON::XS->new->utf8->canonical->pretty->encode(JSON::XS::decode_json($config)) if $ARGV[3] eq 'config'; | |
| print $config; | |
| } else { | |
| blob_download($ARGV[2], $ARGV[3]); | |
| } | |
| } else { | |
| die_with_usage(); | |
| } | |
| exit(0); | |
| } | |
| die_with_usage() unless @ARGV >= 2; | |
| ($registryserver, $repository, @tarfiles) = @ARGV; | |
| if ($delete_mode || $delete_except_mode) { | |
| die("cannot do both delete and delete-except\n") if $delete_mode && $delete_except_mode; | |
| my %tags; | |
| $tags{$_} = 1 for @tags; | |
| $tags{$_} = 1 for tags_from_digestfile($delete_except_mode ? 1 : 0); | |
| if ($delete_mode) { | |
| for my $tag (sort keys %tags) { | |
| if ($tag =~ /^sha256:[0-9a-f]{64}$/) { | |
| delete_manifest($tag); | |
| } else { | |
| delete_tag($tag); | |
| } | |
| } | |
| } elsif ($delete_except_mode) { | |
| for my $tag (grep {!$tags{$_}} get_all_tags()) { | |
| delete_tag($tag); | |
| } | |
| } | |
| exit; | |
| } | |
| if ($cosign) { | |
| require BSConfiguration; | |
| require BSConSign; | |
| require BSPGP; | |
| require BSX509 if $rekorserver; | |
| require BSRekor if $rekorserver; | |
| die("need a pubkey for cosign signature creation\n") unless $pubkeyfile; | |
| die("need a gun for cosign signature creation\n") unless $gun; | |
| die("sign program is not configured!\n") unless $BSConfig::sign; | |
| unshift @signcmd, $BSConfig::sign; | |
| } | |
| die("No tar file to upload?\n") if !@tarfiles; | |
| die("more than one tar file specified\n") if @tarfiles > 1 && !$multiarch; | |
| if ($use_image_tags && @tarfiles > 1) { | |
| # make sure all tar files contain the same tags | |
| my $imagetags; | |
| for my $tarfile (@tarfiles) { | |
| my ($tar, $tarfd) = open_tarfile($tarfile); | |
| my %tar = map {$_->{'name'} => $_} @$tar; | |
| my ($manifest_ent, $manifest) = BSContar::get_manifest(\%tar); | |
| my @imagetags = @{$manifest->{'RepoTags'} || []}; | |
| s/.*:// for @imagetags; | |
| my $it = join(', ', sort(BSUtil::unify(@imagetags))); | |
| die("multiarch images contain different tags: $imagetags -- $it\n") if defined($imagetags) && $imagetags ne $it; | |
| if (!defined($imagetags)) { | |
| $imagetags = $it; | |
| push @tags, @imagetags; | |
| } | |
| close $tarfd if $tarfd; | |
| } | |
| $use_image_tags = 0; | |
| } | |
| # use oci types if we have a helm chart | |
| $oci = 1 if grep {/\.helminfo$/} @tarfiles; | |
| my %digests_to_sign; | |
| my @multimanifests; | |
| my %multiplatforms; | |
| my @imginfos; | |
| for my $tarfile (@tarfiles) { | |
| my ($tar, $tarfd, $govariant) = open_tarfile($tarfile); | |
| my %tar = map {$_->{'name'} => $_} @$tar; | |
| my $provenance; | |
| my $spdx_json; | |
| my $cyclonedx_json; | |
| if ($slsaprovenance) { | |
| my $provenancefile = $tarfile; | |
| if ($provenancefile =~ s/\.[^\.]*$/.slsa_provenance.json/) { | |
| $provenance = readstr($provenancefile) if -s $provenancefile; | |
| } | |
| } | |
| if ($sbom) { | |
| my $spdx_file = $tarfile; | |
| if ($spdx_file =~ s/\.[^\.]*$/.spdx.json/) { | |
| $spdx_json = readstr($spdx_file) if -s $spdx_file; | |
| } | |
| my $cyclonedx_file = $tarfile; | |
| if ($cyclonedx_file =~ s/\.[^\.]*$/.cdx.json/) { | |
| $cyclonedx_json = readstr($cyclonedx_file) if -s $cyclonedx_file; | |
| } | |
| } | |
| my ($manifest_ent, $manifest) = BSContar::get_manifest(\%tar); | |
| #print Dumper($manifest); | |
| if ($use_image_tags) { | |
| my @imagetags = @{$manifest->{'RepoTags'} || []}; | |
| s/.*:// for @imagetags; | |
| push @tags, @imagetags if $use_image_tags; | |
| } | |
| my ($config_ent, $config) = BSContar::get_config(\%tar, $manifest); | |
| #print Dumper($config); | |
| my @layers = @{$manifest->{'Layers'} || []}; | |
| die("container has no layers\n") unless @layers; | |
| my $config_layers; | |
| if ($config->{'rootfs'} && $config->{'rootfs'}->{'diff_ids'}) { | |
| $config_layers = $config->{'rootfs'}->{'diff_ids'}; | |
| die("layer number mismatch\n") if @layers != @{$config_layers || []}; | |
| } | |
| my $goarch = $config->{'architecture'} || 'any'; | |
| my $goos = $config->{'os'} || 'any'; | |
| $govariant = $config->{'variant'} if $config->{'variant'}; | |
| if ($multiarch) { | |
| # see if a already have this arch/os combination | |
| my $platformstr = "architecture:$goarch os:$goos"; | |
| $platformstr .= " variant:$govariant" if $govariant; | |
| if ($multiplatforms{$platformstr}) { | |
| print "ignoring $tarfile, already have $platformstr\n"; | |
| close $tarfd if $tarfd; | |
| next; | |
| } | |
| $multiplatforms{$platformstr} = 1; | |
| } | |
| # process config | |
| my $config_blobid = BSContar::blobid_entry($config_ent); | |
| # create layer data | |
| my $config_data = { | |
| 'mediaType' => $config_ent->{'mimetype'} || ($oci ? $BSContar::mt_oci_config : $BSContar::mt_docker_config), | |
| 'size' => $config_ent->{'size'}, | |
| 'digest' => $config_blobid, | |
| }; | |
| # upload to server | |
| blob_upload($config_blobid, $config_ent); | |
| # process layers (compress if necessary) | |
| my %layer_datas; | |
| my @layer_data; | |
| for my $layer_file (@layers) { | |
| if ($layer_datas{$layer_file}) { | |
| # already did that file, just reuse old layer data | |
| push @layer_data, $layer_datas{$layer_file}; | |
| next; | |
| } | |
| my $layer_ent = $tar{$layer_file}; | |
| die("File $layer_file not included in tar\n") unless $layer_ent; | |
| # detect layer compression | |
| my $comp = BSContar::detect_entry_compression($layer_ent); | |
| die("unsupported compression $comp\n") if $comp && $comp ne 'gzip'; | |
| if (!$comp) { | |
| print "compressing $layer_ent->{'name'}... "; | |
| $layer_ent = BSContar::compress_entry($layer_ent); | |
| print "done.\n"; | |
| } | |
| my $blobid = BSContar::blobid_entry($layer_ent); | |
| #print "$layer_file -> $blobid\n"; | |
| # create layer data | |
| my $layer_data = { | |
| 'mediaType' => $layer_ent->{'mimetype'} || ($oci ? $BSContar::mt_oci_layer_gzip : $BSContar::mt_docker_layer_gzip), | |
| 'size' => $layer_ent->{'size'}, | |
| 'digest' => $blobid, | |
| }; | |
| push @layer_data, $layer_data; | |
| $layer_datas{$layer_file} = $layer_data; | |
| # upload to server | |
| blob_upload($blobid, $layer_ent); | |
| } | |
| close $tarfd if $tarfd; | |
| my $mediaType = $oci ? $BSContar::mt_oci_manifest : $BSContar::mt_docker_manifest; | |
| my $mani = { | |
| 'schemaVersion' => 2, | |
| 'mediaType' => $mediaType, | |
| 'config' => $config_data, | |
| 'layers' => \@layer_data, | |
| }; | |
| my $mani_json = BSContar::create_dist_manifest($mani); | |
| my $mani_id = 'sha256:'.Digest::SHA::sha256_hex($mani_json); | |
| $digests_to_sign{$mani_id} = [ $provenance, $spdx_json, $cyclonedx_json ]; | |
| if ($multiarch) { | |
| manifest_upload_tags($mani_json, undef, $mediaType); # upload anonymous image | |
| my $multimani = { | |
| 'mediaType' => $mediaType, | |
| 'size' => length($mani_json), | |
| 'digest' => $mani_id, | |
| 'platform' => {'architecture' => $goarch, 'os' => $goos}, | |
| }; | |
| $multimani->{'platform'}->{'variant'} = $govariant if $govariant; | |
| push @multimanifests, $multimani; | |
| } else { | |
| manifest_upload_tags($mani_json, \@tags, $mediaType); | |
| } | |
| my $imginfo = { | |
| 'file' => $tarfile, | |
| 'imageid' => $config_blobid, | |
| 'goarch' => $goarch, | |
| 'goos' => $goos, | |
| 'distmanifest' => $mani_id, | |
| }; | |
| $imginfo->{'govariant'} = $govariant if $govariant; | |
| my @diff_ids = @{$config_layers || []}; | |
| for (@layer_data) { | |
| my $l = { 'blobid' => $_->{'digest'}, 'blobsize' => $_->{'size'} }; | |
| $l->{'diffid'} = shift @diff_ids if @diff_ids; | |
| push @{$imginfo->{'layers'}}, $l; | |
| } | |
| push @imginfos, $imginfo; | |
| } | |
| my $info = { | |
| 'images' => \@imginfos, | |
| 'tags' => \@tags, | |
| 'distmanifesttype' => 'image', | |
| 'distmanifest' => $imginfos[0]->{'distmanifest'}, | |
| }; | |
| if ($multiarch) { | |
| my $mediaType = $oci ? $BSContar::mt_oci_index : $BSContar::mt_docker_manifestlist; | |
| my $mani = { | |
| 'schemaVersion' => 2, | |
| 'mediaType' => $mediaType, | |
| 'manifests' => \@multimanifests, | |
| }; | |
| my $mani_json = BSContar::create_dist_manifest_list($mani); | |
| my $mani_id = 'sha256:'.Digest::SHA::sha256_hex($mani_json); | |
| $digests_to_sign{$mani_id} = []; | |
| manifest_upload_tags($mani_json, \@tags, $mediaType); | |
| $info->{'distmanifesttype'} = 'list'; | |
| $info->{'distmanifest'} = $mani_id; | |
| } | |
| if ($writeinfofile) { | |
| my $info_json = JSON::XS->new->utf8->canonical->encode($info); | |
| writestr($writeinfofile, undef, $info_json); | |
| } | |
| if ($cosign && %digests_to_sign) { | |
| my $creator = 'OBS'; | |
| my $gpgpubkey = readstr($pubkeyfile); | |
| $cosigncookie ||= BSConSign::createcosigncookie($gpgpubkey, $gun, $creator); | |
| # upload signatures | |
| for my $digest (sort keys %digests_to_sign) { | |
| my $sig_tag = "$digest.sig"; | |
| $sig_tag =~ s/:/-/; | |
| my ($sig_mani, $sig_maniid, $sig_mani_json) = get_manifest_for_tag($sig_tag); | |
| if ($sig_mani && $sig_mani->{'mediaType'} && $sig_mani->{'mediaType'} eq $BSContar::mt_oci_manifest && @{$sig_mani->{'layers'} || []} == 1 && $BSConSign::mt_cosign && $sig_mani->{'layers'}->[0]->{'mediaType'} eq $BSConSign::mt_cosign) { | |
| my $annotations = $sig_mani->{'layers'}->[0]->{'annotations'} || {}; | |
| next if ($annotations->{$cosign_cookie_name} || '') eq $cosigncookie; | |
| } | |
| print "creating cosign signature for $gun $digest\n"; | |
| my $signfunc = sub { BSUtil::xsystem($_[0], @signcmd, '-O', '-h', 'sha256') }; | |
| my $annotations = { $cosign_cookie_name => $cosigncookie }; | |
| my ($config, $payload_layer, $payload, $sig) = BSConSign::createcosign($signfunc, $digest, $gun, $creator, undef, $annotations); | |
| cosign_upload($sig_tag, $config, $payload_layer, $payload); | |
| if ($rekorserver) { | |
| print "uploading cosign signature to $rekorserver\n"; | |
| my $sslpubkey = BSX509::keydata2pubkey(BSPGP::pk2keydata(BSPGP::unarmor($gpgpubkey))); | |
| $sslpubkey = BSASN1::der2pem($sslpubkey, 'PUBLIC KEY'); | |
| BSRekor::upload_hashedrekord($rekorserver, $payload_layer->{'digest'}, $sslpubkey, $sig); | |
| } | |
| } | |
| # upload attestations | |
| for my $digest (sort keys %digests_to_sign) { | |
| my $provenance = $digests_to_sign{$digest}->[0]; | |
| my $spdx_json = $digests_to_sign{$digest}->[1]; | |
| my $cyclonedx_json = $digests_to_sign{$digest}->[2]; | |
| next unless $provenance || $spdx_json || $cyclonedx_json; | |
| my $att_tag = "$digest.att"; | |
| $att_tag =~ s/:/-/; | |
| my ($att_mani, $att_maniid, $att_mani_json) = get_manifest_for_tag($att_tag); | |
| my $numlayers = ($provenance ? 1 : 0) + ($spdx_json ? 1 : 0) + ($cyclonedx_json ? 1 : 0); | |
| if ($att_mani && $att_mani->{'mediaType'} && $att_mani->{'mediaType'} eq $BSContar::mt_oci_manifest && @{$att_mani->{'layers'} || []} == $numlayers && $BSConSign::mt_dsse) { | |
| next unless grep {$_->{'mediaType'} eq $BSConSign::mt_dsse && (($_->{'annotations'} || {})->{$cosign_cookie_name} || '') eq $cosigncookie} @{$att_mani->{'layers'}}; | |
| } | |
| print "creating cosign attestation for $gun $digest\n"; | |
| my $signfunc = sub { BSUtil::xsystem($_[0], @signcmd, '-O', '-h', 'sha256') }; | |
| my $annotations = { $cosign_cookie_name => $cosigncookie }; | |
| my @attestations; | |
| push @attestations, BSConSign::fixup_intoto_attestation($provenance, $signfunc, $digest, $gun) if $provenance; | |
| push @attestations, BSConSign::fixup_intoto_attestation($spdx_json, $signfunc, $digest, $gun) if $spdx_json; | |
| push @attestations, BSConSign::fixup_intoto_attestation($cyclonedx_json, $signfunc, $digest, $gun) if $cyclonedx_json; | |
| my ($config, @attestation_layers) = BSConSign::createcosign_attestation($digest, \@attestations, $annotations); | |
| cosign_upload($att_tag, $config, @attestation_layers); | |
| if ($rekorserver) { | |
| print "uploading cosign attestations to $rekorserver\n"; | |
| my $sslpubkey = BSX509::keydata2pubkey(BSPGP::pk2keydata(BSPGP::unarmor($gpgpubkey))); | |
| $sslpubkey = BSASN1::der2pem($sslpubkey, 'PUBLIC KEY'); | |
| for my $attestation (@attestations) { | |
| BSRekor::upload_intoto($rekorserver, $attestation, $sslpubkey); | |
| } | |
| } | |
| } | |
| } | |