File Coverage

blib/lib/Container/Buildah/Stage.pm
Criterion Covered Total %
statement 24 257 9.3
branch 0 114 0.0
condition 0 12 0.0
subroutine 8 32 25.0
pod 10 20 50.0
total 42 435 9.6


line stmt bran cond sub pod time code
1             # Container::Buildah::Stage
2             # ABSTRACT: object used by Container::Buildah to track a stage of a multi-stage container build
3             # by Ian Kluft
4              
5             ## no critic (Modules::RequireExplicitPackage)
6             # 'use strict' and 'use warnings' included here
7 1     1   1667 use Modern::Perl qw(2015); # require 5.20.0
  1         3  
  1         9  
8             ## use critic (Modules::RequireExplicitPackage)
9              
10             package Container::Buildah::Stage;
11             $Container::Buildah::Stage::VERSION = '0.2.1';
12 1     1   209 use autodie;
  1         2  
  1         8  
13 1     1   5430 use Carp qw(croak confess);
  1         3  
  1         78  
14 1     1   10 use Cwd;
  1         2  
  1         71  
15 1     1   7 use Readonly;
  1         2  
  1         57  
16 1     1   634 use File::stat;
  1         7185  
  1         5  
17 1     1   683 use FindBin;
  1         1068  
  1         1216  
18              
19             # import from Container::Buildah::Subcommand after BEGIN phase (where 'use' takes place), to avoid conflicts
20             require Container::Buildah;
21             require Container::Buildah::Subcommand;
22             Container::Buildah::Subcommand->import(qw(process_params prog));
23              
24             Readonly::Scalar my $mnt_env_name => "BUILDAHUTIL_MOUNT";
25             Readonly::Array my @auto_accessors => qw(commit consumes depends from func_deps func_exec mnt name produces
26             user user_home);
27             my $accessors_created = 0;
28              
29             # instantiate an object
30             # this should only be called by Container::Buildah
31             # these objects will be passed to each stage's stage->func_*()
32             # private class method
33             sub new {
34 0     0 1   my ($class, @in_args) = @_;
35              
36 0           my $self = { @in_args };
37 0           bless $self, $class;
38              
39             # enforce that only Container::Buildah module can call this method
40 0           my ($package) = caller;
41 0 0         if ($package ne "Container::Buildah") {
42 0           croak __PACKAGE__."->new() can only be called from Container::Buildah";
43             }
44              
45             # initialize accessor methods if not done on a prior call to new()
46 0           generate_read_accessors();
47              
48             # check for required name parameter
49 0 0         if (not exists $self->{name}) {
50 0           croak __PACKAGE__.": cannot instantiate without a name parameter";
51             }
52              
53             # get container mount point, if in the user namespace
54 0 0         if (exists $ENV{$mnt_env_name}) {
55 0           $self->{mnt} = $ENV{$mnt_env_name};
56             }
57              
58             # get ref to stage configuation
59 0           my $config = Container::Buildah->get_config("stages", $self->{name});
60 0 0 0       if ((not defined $config) or (ref $config ne "HASH")) {
61 0           croak __PACKAGE__.": no configuration for stage ".$self->{name};
62             }
63 0           foreach my $key (keys %$config) {
64 0           $self->{$key} = $config->{$key};
65             }
66              
67             # check for missing stage config settings
68 0           my @missing;
69 0           foreach my $key (qw(from func_exec)) {
70 0 0         if (not exists $self->{$key}) {
71 0           push @missing, $key;
72             }
73             }
74              
75             # fail if any required parameters are missing
76 0 0         if (@missing) {
77 0           croak __PACKAGE__.": required parameters missing in stage ".$self->{name}.": ".join(" ", @missing);
78             }
79              
80 0           return $self;
81             }
82              
83             # return entry from stage configuration subset of Container::Buildah configuation
84             # Note: this reads the stage configuration data, not to be confused with buildah's config subcommand
85             # public instance method
86             sub stage_config
87             {
88 0     0 1   my ($self, $key) = @_;
89 0 0         if (exists $self->{$key}) {
90 0 0 0       if (ref $self->{$key} and ref $self->{$key} ne "ARRAY") {
91 0           return $self->{$key};
92             }
93              
94             # if the value is a scalar, perform variable expansion
95 0           return Container::Buildah::expand($self->{$key});
96             }
97 0           return;
98             }
99              
100             # status method forward to Container::Buildah::status()
101             # public instance method
102             sub status
103             {
104 0     0 1   my ($self, @in_args) = @_;
105 0           my $cb = Container::Buildah->instance();
106 0           my @label;
107 0           @label = ('['.$self->container_name().']');
108 0           $cb->status(@label, @in_args);
109 0           return;
110             }
111              
112             # debug method forward to Container::Buildah::debug()
113             # public instance method
114             sub debug
115             {
116 0     0 1   my ($self, @in_args) = @_;
117 0           my $cb = Container::Buildah->instance();
118              
119             # collect debug parameters
120 0           my %params;
121 0 0         if (ref $in_args[0] eq "HASH") {
122 0           my $params_ref = shift @in_args;
123 0           %params = %$params_ref;
124             }
125 0           $params{wrapper} = 1; # tell Container::Buidlah::debug() to skip the stack frame for this wrapper
126              
127             # insert label parameter with container name, if we're in a state where it's defined
128 0 0         if (exists $self->{config}{container_name}) {
129 0           $params{label} = $self->{config}{container_name};
130             }
131              
132             # call the debug method in Container::Buildah
133 0           $cb->debug(\%params, @in_args);
134 0           return;
135             }
136              
137             # accessors - commented out but retained to show why we needed to generate accessor functions
138             #sub get_commit { my $self = shift; return $self->stage_config("commit"); }
139             #sub get_consumes { my $self = shift; return $self->stage_config("consumes"); }
140             #sub get_from { my $self = shift; return $self->stage_config("from"); }
141             #sub get_func_deps { my $self = shift; return $self->stage_config("func_deps"); }
142             #sub get_func_exec { my $self = shift; return $self->stage_config("func_exec"); }
143             #sub get_mnt { my $self = shift; return $self->stage_config("mnt"); }
144             #sub get_name { my $self = shift; return $self->stage_config("name"); }
145             #sub get_produces { my $self = shift; return $self->stage_config("produces"); }
146             #sub get_user_home { my $self = shift; return $self->stage_config("user_home"); }
147             #sub get_user { my $self = shift; return $self->stage_config("user"); }
148              
149             # generate read accessor methods
150             # note: these parameters are set only in new() - there are no write accessors so none are generated
151             # private class function
152             sub generate_read_accessors
153             {
154             # check if accessors have been created
155 0 0   0 0   if ($accessors_created) {
156             # skip if already done
157 0           return;
158             }
159              
160             # create accessor methods
161 0           foreach my $field_name (@auto_accessors) {
162             # for read accessor name, prepend get_ to field name
163 0           my $method_name = "get_".$field_name;
164            
165             # generate accessor method to handle this field
166             my $method_sub = sub {
167 0     0     my $self = shift;
168 0 0         $self->isa(__PACKAGE__)
    0          
    0          
169             or confess "$method_name method (from generate_read_accessors) expects ".__PACKAGE__." object, got "
170             .((defined $self)?((ref $self)?ref $self:"scalar"):"(undef)");
171 0           my $value = $self->stage_config($field_name);
172 0 0         $self->debug({level => 3, name => __PACKAGE__."::".$method_name},
173             (defined $value)?"value=$value":"(undef)");
174 0           return $value;
175 0           };
176              
177             # install and call the newly-generated method
178 1     1   10 no strict 'refs'; ## no critic (ProhibitNoStrict)
  1         2  
  1         3520  
179 0           *{ $method_name } = $method_sub; # install generated method in class symbol table
  0            
180             }
181 0           $accessors_created = 1; # do this only once
182 0           return;
183             }
184              
185             # get container name
186             # generate it the first time
187             # public instance method
188             sub container_name
189             {
190 0     0 1   my $self = shift;
191              
192             # derive container name
193 0 0         if (not exists $self->{container_name}) {
194 0           $self->{container_name} = Container::Buildah->get_config("basename")."_".$self->get_name;
195             }
196 0           return $self->{container_name};
197             }
198              
199             #
200             # buildah subcommand front-end functions
201             # Within Container::Buildah::Stage the object has methods for subcommands which take a container name.
202             # Each method gets container_name from the object. So it is not passed as a separate parameter.
203             #
204             # Other more general subcommands are in Container::Buildah class.
205             #
206              
207             # front-end to "buildah add" subcommand
208             # usage: $self->add( [{[dest => value]. [chown => mode]},] src, [src, ...] )
209             # public instance method
210             sub add
211             {
212 0     0 1   my ($self, @in_args) = @_;
213 0           $self->debug({level => 2}, @in_args);
214 0           my $params = {};
215 0 0         if (ref $in_args[0] eq "HASH") {
216 0           $params = shift @in_args;
217             }
218              
219             # process parameters
220 0           my ($extract, @args) = process_params({name => 'add',
221             extract => [qw(dest)],
222             arg_init => [qw(--add-history)],
223             arg_flag => [qw(quiet)],
224             arg_str => [qw(chown)]
225             }, $params);
226              
227             # get special parameter dest if it exists
228 0           my $dest = $extract->{dest};
229              
230             # run command
231 0           my $cb = Container::Buildah->instance();
232 0 0         $cb->buildah("add", @args, $self->container_name, @in_args, ($dest ? ($dest) : ()));
233 0           return;
234             }
235              
236             # front-end to "buildah commit" subcommand
237             # usage: $self->commit( [{param => value, ...}], image-name )
238             # public instance method
239             sub commit
240             {
241 0     0 1   my ($self, @in_args) = @_;
242 0           $self->debug({level => 2}, @in_args);
243 0           my $params = {};
244 0 0         if (ref $in_args[0] eq "HASH") {
245 0           $params = shift @in_args;
246             }
247 0           my $image_name = shift @in_args;
248              
249             # process parameters
250 0           my ($extract, @args) = process_params({name => 'commit',
251             arg_flag => [qw(disable-compression omit-timestamp quiet rm squash tls-verify)],
252             arg_int => [qw(timestamp)],
253             arg_str => [qw(authfile blob-cache cert-dir creds encryption-key format iidfile
254             reference-time sign-by signature-policy tls-verify omit-timestamp)],
255             arg_array => [qw(encrypt-layer)],
256             }, $params);
257              
258             # do commit
259 0           my $cb = Container::Buildah->instance();
260 0   0       $cb->buildah("commit", @args, $self->container_name, ($image_name // ()));
261 0           return;
262             }
263              
264              
265             # front-end to "buildah config" subcommand
266             # usage: $self->config({ param => value, ...})
267             # Note: this is for the container's configuration, not to be confused with configuration data of this module
268             # public instance method
269             sub config
270             {
271 0     0 1   my ($self, @in_args) = @_;
272 0           $self->debug({level => 2}, @in_args);
273 0           my $params = {};
274 0 0         if (ref $in_args[0] eq "HASH") {
275 0           $params = shift @in_args;
276             }
277              
278             # process parameters
279 0           my ($extract, @args) = process_params({name => 'config',
280             arg_init => [qw(--add-history)],
281             arg_str => [qw(arch author cmd comment created-by domainname healthcheck healthcheck-interval
282             healthcheck-retries healthcheck-start-period healthcheck-timeout history-comment hostname
283             os shell stop-signal user workingdir)],
284             arg_array => [qw(annotation env label onbuild port volume)],
285             arg_list => [qw(entrypoint)],
286             }, $params);
287              
288             # run command
289 0           my $cb = Container::Buildah->instance();
290 0           $cb->buildah("config", @args, $self->container_name);
291 0           return;
292             }
293              
294             # front-end to "buildah copy" subcommand
295             # usage: $self->copy( [{dest => value},] src, [src, ...] )
296             # public instance method
297             sub copy
298             {
299 0     0 1   my ($self, @in_args) = @_;
300 0           $self->debug({level => 2}, @in_args);
301 0           my $params = {};
302 0 0         if (ref $in_args[0] eq "HASH") {
303 0           $params = shift @in_args;
304             }
305              
306             # process parameters
307 0           my ($extract, @args) = process_params({name => 'copy',
308             extract => [qw(dest)],
309             arg_init => [qw(--add-history)],
310             arg_flag => [qw(quiet)],
311             arg_str => [qw(chown)]
312             }, $params);
313              
314             # get special parameter dest if it exists
315 0           my $dest = $extract->{dest};
316              
317             # run command
318 0           my $cb = Container::Buildah->instance();
319 0 0         $cb->buildah("copy", @args, $self->container_name, @in_args, ($dest ? ($dest) : ()));
320 0           return;
321             }
322              
323             # front-end to "buildah run" subcommand
324             # usage: $self->run( [{param => value, ...}], [command], ... )
325             # Command parameter can be an array of strings for one command, or array of arrays of strings for multiple commands.
326             # This applies the same command-line arguments (from %params) to each command. To change parameters for a command,
327             # make a separate call to the function.
328             # public instance method
329             sub run
330             {
331 0     0 1   my ($self, @in_args) = @_;
332 0           $self->debug({level => 2}, @in_args);
333 0           my $params = {};
334 0 0         if (ref $in_args[0] eq "HASH") {
335 0           $params = shift @in_args;
336             }
337              
338             # process parameters
339 0           my ($extract, @args) = process_params({name => 'run',
340             arg_init => ['--add-history'],
341             arg_flag => [qw(no-pivot terminal)],
342             arg_str => [qw(cni-config-dir cni-plugin-path hostname ipc isolation network pid runtime
343             user uts)],
344             arg_array => [qw(cap-add cap-drop mount runtime-flag security-opt volume)],
345             }, $params);
346              
347             # loop through provided commands
348             # build outer array if only one command was provided
349 0 0         my @commands = ref $in_args[0] ? @in_args : [@in_args];
350 0           foreach my $command (@commands) {
351             # if any entries are not arrays, temporarily make them into one
352 0 0         if (not ref $command) {
    0          
353 0           $command = [$command];
354             } elsif (ref $command ne "ARRAY") {
355 0           confess "run: command must be a scalar or array, got ".ref $command;
356             }
357              
358             # run command
359 0           my $cb = Container::Buildah->instance();
360 0           $cb->buildah("run", @args, $self->container_name, '--', @$command);
361             }
362 0           return;
363             }
364              
365             #
366             # private methods - container-stage processing utilities
367             #
368              
369             # remove a container by name if it already exists - we need the name
370             # private instance method
371             sub rmcontainer
372             {
373 0     0 0   my $self = shift;
374 0           my $cb = Container::Buildah->instance();
375              
376             $cb->inspect({
377             suppress_error => 1,
378       0     nonzero => sub {},
379 0     0     zero => sub {$cb->rm($self->container_name);}},
380 0           $self->container_name);
381 0           return;
382             }
383              
384             # get path to the executing script
385             # used for file dependency checks and re-running the script in a container namespace
386             # private class function
387             sub progpath
388             {
389 0     0 0   state $progpath = "$FindBin::Bin/$FindBin::Script";
390 0           return $progpath;
391             }
392              
393             # derive tarball name for stage which produces it
394             # defaults to the current stage
395             # private instance method
396             sub tarball
397             {
398 0     0 0   my $self = shift;
399 0   0       my $stage_name = shift // $self->get_name;
400 0           return Container::Buildah->get_config("basename")."_".$stage_name.".tar.bz2";
401             }
402              
403             # get file modification timestamp
404             # private class function
405             sub ftime
406             {
407 0     0 0   my $file = shift;
408              
409             # follow symlinks, limit to 10 levels in case of loop
410 0           my $count=10;
411 0           my $f_file = $file;
412 0           while ($count > 0) {
413 0 0         if (-l $f_file) {
414 0           $f_file = readlink $f_file;
415             } else {
416 0           last;
417             }
418 0           $count--;
419             }
420 0 0         if ($count <= 0) {
421 0           croak "ftime: apparent symlink loop or more than 10 levels at $file";
422             }
423              
424             # skip if the path doesn't point to a file
425 0 0         if (not -f $f_file ) {
426 0           croak "ftime: not a regular file at $file";
427             }
428              
429             # return the modification time of the file
430 0           my $fstat = stat $f_file;
431 0           return $fstat->mtime;
432             }
433              
434             # check if this script or configuration is newer than a deliverable file, or if the deliverable doesn't exist
435             # private class function
436             sub check_deliverable
437             {
438 0     0 0   my $depfile = shift;
439              
440             # if the deliverable doesn't exist, then it must be built
441 0 0         if (not -e $depfile) {
442 0           return "does not exist";
443             }
444 0 0         if (not -f $depfile) {
445 0           croak "not a file: $depfile";
446             }
447              
448             # if the program has been modified more recently than the deliverable, the deliverable must be rebuilt
449 0 0         if (ftime(progpath()) > ftime($depfile)) {
450 0           return "program modified";
451             }
452              
453             # if the configuration has been modified more recently than the deliverable, the deliverable must be rebuilt
454 0           my $cb = Container::Buildah->instance();
455 0           my $config_files = $cb->get_config('_config_files');
456 0           foreach my $file (@$config_files) {
457 0 0         if (ftime($file) > ftime($depfile)) {
458 0           return "config file modified";
459             }
460             }
461              
462 0           return;
463             }
464              
465             # generic external wrapper function for all stages
466             # mount the container namespace and enter it to run the custom stage build function
467             # private instance method
468             sub launch_namespace
469             {
470 0     0 0   my $self = shift;
471              
472             # check if this stage produces a deliverable to another stage
473 0           my $produces = $self->get_produces;
474 0 0         if (defined $produces) {
475             # generate deliverable file name
476 0           my $tarball_out = $self->tarball;
477              
478             # check if deliverable tarball file already exists
479 0           my $tarball_result = check_deliverable($tarball_out);
480 0 0         if (not $tarball_result) {
481             # skip this stage because the deliverable already exists and is up-to-date
482 0           $self->status("build tarball skipped - deliverable up-to-date $tarball_out");
483 0           return;
484             }
485              
486             # continue with this build stage if tarball missing or program updated more recently than tarball
487 0           $self->status("build tarball ($tarball_result): $tarball_out");
488             }
489              
490             #
491             # run container for this stage
492             # commit it if configured (usually that's only for the final stage)
493             # otherwise a stage is discarded except for its product tarball
494             #
495              
496             # if the container exists, remove it
497 0           $self->rmcontainer;
498              
499             # get the base image
500 0           my $cb = Container::Buildah->instance();
501 0           $cb->from({name => $self->container_name}, $self->get_from);
502              
503             # get copy of @ARGV saved by main() for use here re-launching in namespace
504 0           my $argv_ref = Container::Buildah->get_config("argv");
505 0 0         if (ref $argv_ref ne "ARRAY") {
506 0           confess "wrong type for argv - expected ARRAY ref, got ".(ref $argv_ref);
507             }
508              
509             # run the builder script in the container
510 0           $cb->unshare({container => $self->container_name,
511             envname => $mnt_env_name},
512             progpath(),
513             "--internal=".$self->get_name,
514             @$argv_ref,
515             );
516              
517             # commit the container if configured
518 0           my $commit = $self->get_commit;
519 0           my @tags;
520 0 0         if (defined $commit) {
521 0 0         if (not ref $commit) {
    0          
522 0           @tags = ($commit);
523             } elsif (ref $commit eq "ARRAY") {
524 0           @tags = @$commit;
525             } else {
526 0           confess "reference to ".(ref $commit)." not supported in commit - use scalar or array";
527             }
528             }
529 0           my $image_name = shift @tags;
530 0           $self->commit($image_name);
531 0 0         if (@tags) {
532 0           $cb->tag({image => $image_name}, @tags);
533             }
534 0           return;
535             }
536              
537             # import tarball(s) from other container stages if configured
538             # private instance method
539             sub consume
540             {
541 0     0 0   my $self = shift;
542              
543             # create groups and users before import
544 0           my $user = $self->get_user;
545 0 0         if (defined $self->get_user) {
546 0           my $user_name = $user;
547 0           my ($uid, $group_name, $gid);
548 0 0         if ($user =~ /:/x) {
549 0           ($user_name, $group_name) = split /:/x, $user;
550 0 0         if ($user_name =~ /=/x) {
551 0           ($user_name, $uid) = split /=/x, $user_name;
552             }
553 0 0         if ($group_name =~ /=/x) {
554 0           ($group_name, $gid) = split /=/x, $group_name;
555             }
556             }
557             # TODO: find distro-independent approach instead of assuming Linux Fileystem Standard /usr/sbin paths
558 0 0         if (defined $group_name) {
559 0 0         $self->run(["/usr/sbin/groupadd", ((defined $gid) ? ("--gid=$gid") : ()), $group_name]);
560             }
561 0           my $user_home = $self->get_user_home;
562 0 0         $self->run(
    0          
    0          
563             ["/usr/sbin/useradd", ((defined $uid) ? ("--uid=$uid") : ()),
564             ((defined $group_name) ? ("--gid=$group_name") : ()),
565             ((defined $user_home) ? ("--home-dir=$user_home") : ()), $user_name],
566             );
567             }
568              
569             # import tarballs from each stage we depend upon
570 0           my $consumes = $self->get_consumes;
571 0 0         if (defined $consumes) {
572 0 0         if (ref $consumes eq "ARRAY") {
573 0           my @in_stages = @$consumes;
574 0           my $cwd = getcwd();
575 0           foreach my $in_stage (@in_stages) {
576 0           my $tarball_in = $self->tarball($in_stage);
577 0           $self->debug("in ".$self->get_name." stage before untar; pid=$$ cwd=$cwd tarball=$tarball_in");
578 0 0         (-f $tarball_in) or croak "consume(".join(" ", @in_stages)."): ".$tarball_in." not found";
579 0           $self->add({dest => "/"}, $tarball_in);
580             }
581             } else {
582 0           croak "consume stage->consumes was set but not an array ref";
583             }
584             }
585 0           return;
586             }
587              
588             # drop leading slash from a path
589             # private class function
590             sub dropslash
591             {
592 0     0 0   my $str = shift;
593 0 0         if (substr($str,0,1) eq '/') {
594 0           substr($str,0,1,'');
595             }
596 0           return $str;
597             }
598              
599             # export tarball for availability to other container stages if configured
600             # private instance method
601             sub produce
602             {
603 0     0 0   my $self = shift;
604              
605             # export directories to tarball for product of this stage
606 0           my $produces = $self->get_produces;
607 0 0         if (defined $produces) {
608 0 0         if (ref $produces eq "ARRAY") {
609 0           my $tarball_out = $self->tarball;
610 0           my $cb = Container::Buildah->instance();
611 0           my @product_dirs;
612 0           foreach my $product (@$produces) {
613 0           push @product_dirs, dropslash($product);
614             }
615              
616             # move any existing tarball to backup
617 0 0         if ( -f $tarball_out ) {
618 0           rename $tarball_out, $tarball_out.".bak";
619             }
620              
621             # create the tarball
622 0           my $cwd = getcwd();
623 0           $self->debug("in ".$self->get_name." stage before tar; pid=$$ cwd=$cwd product_dirs="
624             .join(" ", @product_dirs));
625             # ignore tar exit code 1 - appears to be unavoidable and meaningless when building on an overlayfs
626 0 0   0     my $nonzero = sub { my $ret=shift; if ($ret>1) {croak "tar exited with code $ret";}};
  0            
  0            
  0            
627 0           $cb->cmd({name => "tar", nonzero => $nonzero}, "/usr/bin/tar", "--create", "--bzip2",
628             "--preserve-permissions", "--sparse", "--file=".$tarball_out, "--directory=".$self->get_mnt, @product_dirs);
629             } else {
630 0           croak "product: stage->consumes was set but not an array ref";
631             }
632             }
633 0           return;
634             }
635              
636             1;
637              
638             __END__