File Coverage

blib/lib/Docker/Construct.pm
Criterion Covered Total %
statement 26 112 23.2
branch 0 72 0.0
condition n/a
subroutine 9 14 64.2
pod 1 1 100.0
total 36 199 18.0


line stmt bran cond sub pod time code
1             package Docker::Construct;
2              
3 1     1   67793 use 5.012;
  1         4  
4 1     1   6 use strict;
  1         2  
  1         21  
5 1     1   5 use warnings;
  1         1  
  1         68  
6              
7             =head1 NAME
8              
9             Docker::Construct - Construct the filesystem of an exported docker image.
10              
11             =cut
12              
13             our $VERSION = '1.0';
14              
15             =head1 SYNOPSIS
16              
17             This is the backend module for the L command-line tool. For
18             basic usage, refer to its documentation instead.
19              
20             use Docker::Construct qw(construct);
21              
22             # Minimal usage
23             construct('path/to/image.tar', 'path/to/output/dir');
24              
25             # With options
26             construct(
27             image => 'path/to/image.tar',
28             dir => 'path/to/output.dir',
29             quiet => 1,
30             include_config => 1
31             )
32              
33             =cut
34              
35 1     1   7 use Exporter;
  1         2  
  1         75  
36             our @ISA = qw(Exporter);
37             our @EXPORT_OK = qw(construct);
38              
39 1     1   8 use Carp;
  1         11  
  1         60  
40 1     1   727 use JSON;
  1         12464  
  1         6  
41 1     1   155 use Scalar::Util qw(openhandle);
  1         3  
  1         65  
42 1     1   509 use File::Spec::Functions qw(splitpath catfile);
  1         739  
  1         69  
43 1     1   7 use File::Path qw(remove_tree);
  1         2  
  1         1037  
44              
45             =head2 construct()
46              
47             Reconstruct the the filesystem of the specified tarball (output from
48             the C command) inside the specified directory. If only two
49             arguments are given, they are interpreted as the paths to the input tarball
50             and output directory respectively. If more arguments are given, the arguments
51             are interpreted as a hash. A hash allows you specify additional options and the
52             input tarball and output directory are specified with the C and C
53             keys respectively.
54              
55             =head2 Options
56              
57             =over 4
58              
59             =item * image I<(required)>
60              
61             Path to the input tarball
62              
63             =item * dir I<(required)>
64              
65             Path to the output directory (must exist already)
66              
67             =item * quiet
68              
69             If true, progress will not be reported on stderr.
70              
71             =item * include_config
72              
73             If true, include the image's config json file as F in the
74             root of the extracted filesystem.
75              
76             =back
77             =cut
78              
79             sub construct {
80             # Parse parameters
81 0     0 1   my %params;
82 0 0         if ( @_ == 2 ) {
83 0           ( $params{image}, $params{dir} ) = @_;
84             }
85             else {
86 0           %params = @_;
87             }
88              
89 0 0         croak "must specify input image tarball 'image'" unless $params{image};
90 0 0         croak "must specify output directory 'dir'" unless $params{dir};
91 0           my $image = $params{image};
92 0           my $dir = $params{dir};
93 0 0         croak "file not found: $image" unless -f $image;
94 0 0         croak "directory not found: $dir" unless -d $dir;
95              
96             # Get list of files in initial image
97 0           my @imagefiles = _read_file_list($image);
98              
99             croak "this does not seem to be a docker image (missing manifest.json)"
100 0 0         unless grep {$_ eq 'manifest.json'} @imagefiles;
  0            
101              
102             # Extract image manifest.
103             my %manifest = %{
104 0           decode_json(
  0            
105             _read_file_from_tar($image, 'manifest.json')
106             )->[0]
107             };
108              
109             # We're gonna create a list of the whiteout files in the image
110             # (keyed by layer). The whiteout files indicate files from
111             # previous layers to be deleted and are named after the files
112             # they delete but prefixed with `.wh.`
113 0           my %wh;
114 0           for my $layer ( @{$manifest{Layers}} ) {
  0            
115 0           my $layer_abbrev = substr($layer,0,12);
116 0 0         print STDERR "reading layer: $layer_abbrev...\n" unless $params{quiet};
117              
118 0           $wh{$layer} = [];
119              
120 0           my $layer_fh = _stream_file_from_tar($image, $layer);
121 0           my $filelist = _exec_tar($layer_fh, '-t');
122              
123 0           while (<$filelist>) {
124 0           chomp;
125 0           my ($dirname, $basename) = (splitpath $_)[1,2];
126 0 0         if ($basename =~ /^\.wh\./) {
127 0           my $to_delete = catfile $dirname, ( $basename =~ s/^\.wh\.//r );
128 0           push @{ $wh{$layer} }, $to_delete;
  0            
129             }
130             }
131              
132 0 0         close $filelist or croak $! ? "could not close pipe: $!"
    0          
133             : "exit code $? from tar";
134 0 0         close $layer_fh or croak $! ? "could not close pipe: $!"
    0          
135             : "exit code $? from tar";
136              
137             }
138              
139             # Extract each layer, ignoring the whiteout files and then removing
140             # the files that are meant to be deleted after each layer.
141 0           for my $layer ( @{$manifest{Layers}} ) {
  0            
142 0           my $layer_abbrev = substr $layer, 0, 12;
143 0 0         print STDERR "extracting layer: $layer_abbrev...\n" unless $params{quiet};
144              
145 0           my $layer_fh = _stream_file_from_tar($image, $layer);
146 0           my $extract_fh = _exec_tar($layer_fh, '-C', $dir, qw'-x --exclude .wh.*');
147              
148 0 0         close $extract_fh or croak $! ? "could not close pipe: $!"
    0          
149             : "exit code $? from tar";
150 0 0         close $layer_fh or croak $! ? "could not close pipe: $!"
    0          
151             : "exit code $? from tar";
152              
153 0           for my $file ( @{ $wh{$layer} }) {
  0            
154 0           my $path = catfile $dir, $file;
155 0 0         if (-f $path) {
    0          
156 0 0         unlink $path or carp "failed to remove file: $path";
157             }
158             elsif (-d $path) {
159 0           remove_tree $path;
160              
161             }
162             }
163             }
164              
165 0 0         if ($params{include_config}) {
166 0           my $config = $manifest{Config};
167 0 0         carp "wanted to include config json but couldn't find it in manifest." unless defined $config;
168              
169 0 0         print STDERR "extracting config: $config...\n" unless $params{quiet};
170              
171 0           my $outfile = catfile $dir, 'config.json';
172 0 0         open(my $config_write, '>', $outfile) or croak "could not open $outfile: $!";
173              
174 0           my $config_read = _exec_tar($image, '-xO', $config);
175 0           while(<$config_read>) {
176 0           print $config_write $_;
177             }
178              
179 0 0         close $config_write or croak "could not close $outfile: $!";
180 0 0         close $config_read or croak $! ? "could not close pipe: $!"
    0          
181             : "exit code $? from tar";
182              
183             }
184              
185 0 0         print STDERR "done.\n" unless $params{quiet};
186              
187             }
188              
189             # Take a tar input (either a filename or a filehandle to one)
190             # and return the list of files in the archive.
191             sub _read_file_list {
192 0     0     my $fh = _exec_tar(shift, '-t');
193              
194 0           my @filelist = <$fh>;
195 0           chomp @filelist;
196              
197 0 0         close $fh or croak $! ? "could not close pipe: $!"
    0          
198             : "exit code $? from tar";
199              
200 0           return @filelist;
201             }
202              
203             # Take a tar input (either a filename or a filehandle to one)
204             # and the name of a file in the archive and return the file's text.
205             sub _read_file_from_tar {
206 0     0     my $fh = _stream_file_from_tar(@_);
207 0           my $content;
208             {
209 0           local $/ = undef;
  0            
210 0           $content = <$fh>;
211             }
212 0 0         close $fh
    0          
213             or croak $! ? "could not close pipe: $!"
214             : "exit code $? from tar";
215 0           return $content;
216             }
217              
218             # Take a tar input (either a filename or a filehandle to one)
219             # and the name of a file in the archive and return an open
220             # filehandle that streams the file's text.
221             sub _stream_file_from_tar {
222 0     0     my $input = shift;
223 0           my $path = shift;
224              
225 0           return _exec_tar($input, '-xO', $path);
226             }
227              
228             # Takes as its first argument, either the filename for a tar archive
229             # or an open filehandle that a tar archive can be read from. The remaining
230             # arguments are used as arguments to `tar`. Starts executing the command
231             # and the returns a filehandle that streams the command's stdout.
232             sub _exec_tar {
233 0     0     my $input = shift;
234 0           my @args = @_;
235              
236 0           my $read_fh;
237 0 0         if (openhandle $input) {
238             # If input is a filehandle, then we fork and pipe input
239             # through the command to the output handle.
240 0           my @command = ('tar', @args);
241 0           my $pid = open($read_fh, '-|');
242 0 0         croak "could not fork" unless defined $pid;
243 0 0         do { open(STDIN, '<&', $input); exec @command; } unless $pid;
  0            
  0            
244             }
245             else {
246             # Otherwise, we assume input is a filename and just exec
247             # tar on it.
248 0           my @command = ('tar', '-f', $input, @args);
249 0 0         open ($read_fh, '-|', @command) or croak "could not exec tar";
250             }
251 0           return $read_fh;
252             }
253              
254             =head1 AUTHOR
255              
256             Cameron Tauxe, C<< >>
257              
258             =head1 LICENSE AND COPYRIGHT
259              
260             This software is copyright (c) 2020 by Cameron Tauxe.
261              
262             This is free software; you can redistribute it and/or modify it under
263             the same terms as the Perl 5 programming language system itself.
264              
265             =cut
266              
267             1;