File Coverage

blib/lib/Container/Buildah.pm
Criterion Covered Total %
statement 140 316 44.3
branch 25 128 19.5
condition 6 44 13.6
subroutine 24 33 72.7
pod 8 14 57.1
total 203 535 37.9


line stmt bran cond sub pod time code
1             # Container::Buildah
2             # ABSTRACT: wrapper around containers/buildah tool for multi-stage builds of OCI/Docker-compatible Linux containers
3             # by Ian Kluft
4              
5             ## no critic (Modules::RequireExplicitPackage)
6             # 'use strict' and 'use warnings' included here
7 6     6   479736 use Modern::Perl qw(2015); # require 5.20.0
  6         61416  
  6         43  
8             ## use critic (Modules::RequireExplicitPackage)
9              
10             package Container::Buildah;
11             $Container::Buildah::VERSION = '0.2.1';
12 6     6   2363 use autodie;
  6         16184  
  6         59  
13 6     6   35713 use Carp qw(croak confess);
  6         18  
  6         377  
14 6     6   37 use Exporter;
  6         24  
  6         271  
15 6     6   3821 use Readonly;
  6         25763  
  6         333  
16 6     6   7093 use Getopt::Long;
  6         77039  
  6         40  
17 6     6   4140 use Data::Dumper;
  6         36071  
  6         379  
18 6     6   52 use IO::Handle;
  6         12  
  6         229  
19 6     6   3520 use File::Slurp;
  6         135964  
  6         481  
20 6     6   3036 use File::Sync qw(sync);
  6         17326  
  6         364  
21 6     6   3395 use Algorithm::Dependency;
  6         36637  
  6         213  
22 6     6   2940 use Algorithm::Dependency::Source::HoA;
  6         2904  
  6         216  
23 6     6   2780 use YAML::XS;
  6         17357  
  6         330  
24 6     6   3263 use Template;
  6         117448  
  6         236  
25 6     6   48 use parent qw(Class::Singleton);
  6         11  
  6         65  
26              
27             # import from Container::Buildah::Subcommand after BEGIN phase (where 'use' takes place), to avoid conflicts
28             require Container::Buildah::Subcommand;
29             Container::Buildah::Subcommand->import(qw(process_params prog));
30              
31             # methods delegated to Container::Buildah::Subcommand that need to be imported into this class' symbol table
32             # (methods should not be handled by Exporter - we are doing the same thing but keeping it private to the class)
33             Readonly::Array my @subcommand_methods => qw(cmd buildah bud containers from images info inspect
34             mount pull push_image rename rm rmi tag umount unshare version);
35              
36             # aliases to de-conflict wrapper methods that have same name as Perl builtins
37             Readonly::Hash my %subcommand_aliases => (push => "push_image", rename => "rename_image");
38              
39             #
40             # initialize environment
41             #
42              
43             # globals
44             my $debug=0;
45             my %template_config = (
46             INTERPOLATE => 1,
47             POST_CHOMP => 1,
48             RECURSION => 1,
49             EVAL_PERL => 0,
50             PRE_CHOMP => 2,
51             POST_CHOMP => 2,
52             );
53             my %init_config;
54              
55             # initialization on the singleton instance
56             # see parent Class::Singleton
57             # private class method - required by parent Class::Singleton
58             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines, Miscellanea::ProhibitUnrestrictedNoCritic))
59             sub _new_instance
60             {
61 5     5   1889 my ($class, %params) = @_;
62 5         18 my $self = bless { }, $class;
63              
64             # debugging isn't established yet so just be verbose about startup parameters if debug is specified
65 5 50       26 if (exists $params{debug})
66             {
67 0         0 print STDERR "debug: _new_instance: params=".Dumper(\%params);
68             }
69              
70             # set up config hash - use yaml_config file if provided, then add all entries from config parameter
71             # note: YAML is the preferred location to keep configuration that changes frequently such as software versions
72 5 50       62 if (not exists $self->{config}) {
73 5         19 $self->{config} = {};
74 5         18 $self->{config}{_config_files} = [];
75             }
76 5 50       23 if (exists $params{yaml_config}) {
77 0         0 my $in_config = YAML::XS::LoadFile($params{yaml_config});
78 0         0 push @{$self->{config}{_config_files}}, $params{yaml_config}; # save a record of config files used
  0         0  
79 0 0 0     0 if (ref $in_config eq "HASH") {
    0          
80 0         0 $self->{config} = $in_config;
81             } elsif (ref $in_config eq "ARRAY" and ref $in_config->[0] eq "HASH") {
82 0         0 $self->{config} = $in_config->[0];
83             } else {
84 0         0 confess __PACKAGE__.": can't find associative array for configuration data";
85             }
86             }
87 5         23 foreach my $key (keys %init_config) {
88 13         37 $self->{config}{$key} = $init_config{$key};
89             }
90              
91             # process container basename for this instance
92 5 50       27 if (exists $self->{config}{basename}) {
93             # timestamp string for log file names
94             # set environment for child processes, or use it if established by parent
95 5         29 my $timestamp_envname = uc($self->{config}{basename}."_TIMESTAMP_STR");
96 5 50       21 if (exists $ENV{$timestamp_envname}) {
97 0         0 $self->{config}{timestamp_str} = $ENV{$timestamp_envname};
98             } else {
99 5         283 my @timestamp = localtime;
100 5         19 $timestamp[4]++; # fix month from 0-base
101 5         18 $timestamp[5] += 1900; # fix year from 1900 base
102 5         49 $self->{config}{timestamp_str} = sprintf "%04d-%02d-%02d-%02d-%02d-%02d",
103             $timestamp[5], $timestamp[4], $timestamp[3],
104             $timestamp[2], $timestamp[1],$timestamp[0];
105             ## no critic (Variables::RequireLocalizedPunctuationVars, Miscellanea::ProhibitUnrestrictedNoCritic)
106 5         57 $ENV{$timestamp_envname} = $self->{config}{timestamp_str};
107             }
108             } else {
109 0         0 croak __PACKAGE__.": required basename initialization parameter not found";
110             }
111              
112             # delegate subcommand wrapper functions to Container::Buildah::Subcommand
113 5         43 require Container::Buildah::Subcommand;
114 5         50 foreach my $methodname (@subcommand_methods) {
115 6     6   8923 no strict 'refs'; ## no critic (ProhibitNoStrict)
  6         22  
  6         619  
116 90         469 *{$methodname} = \&{"Container::Buildah::Subcommand::$methodname"};
  90         974  
  90         188  
117             }
118              
119             # handle subcommand wrapper aliases to avoid functions with names of Perl builtins
120 5         66 foreach my $aliasname (keys %subcommand_aliases) {
121 6     6   53 no strict 'refs'; ## no critic (ProhibitNoStrict)
  6         13  
  6         14907  
122 10         124 *{$aliasname} = \&{"Container::Buildah::Subcommand::".$subcommand_aliases{$aliasname}};
  10         130  
  10         41  
123             }
124              
125             # Template setup
126 5         80 $self->{template} = Template->new(\%template_config);
127              
128             # redirect STDIN from /dev/null so subprocesses run automated and can't prompt STDIN
129 5 50       121850 open STDIN, "<", "/dev/null"
130             or croak "failed to redirect STDIN";
131              
132             # save STDOUT and STDERR so they can be restored after redirects
133 5 50       13955 open($self->{oldstdout}, '>&STDOUT')
134             or croak "Can't dup STDOUT: $!";
135 5 50       499 open($self->{oldstderr}, '>&STDERR')
136             or croak "Can't dup STDERR: $!";
137              
138 5         460 return $self;
139             }
140             ## use critic (Subroutines::ProhibitUnusedPrivateSubroutines, Miscellanea::ProhibitUnrestrictedNoCritic))
141              
142             #
143             # configuration/utility functions
144             #
145              
146             # initialize configuration
147             # public class function
148             sub init_config
149             {
150 5     5 1 3050 %init_config = @_;
151 5         16 return;
152             }
153              
154             # print status messages
155             # public class function
156             sub status
157             {
158             # get Container::Buildah ref from method-call parameter or class singleton instance
159 0     0 1 0 my @in_args = @_;
160 0 0 0     0 my $cb = ((ref $in_args[0]) and (ref $in_args[0] eq "Container::Buildah")) ? shift @in_args
161             : Container::Buildah->instance();
162              
163             # print status message
164 0 0       0 if ($debug > 0) {
165 0         0 say STDOUT "=== status: ".join(" ", @in_args);
166 0 0 0     0 if ((exists $cb->{oldstdout}) and ($cb->{oldstdout}->fileno != fileno(STDERR))) {
167 0         0 $cb->{oldstdout}->print("=== status: ".join(" ", @in_args)."\n");
168             }
169             }
170 0         0 return;
171             }
172              
173             # print debug messages
174             # public class function
175             sub debug
176             {
177 27     27 1 69 my ($cb, @in_args) = @_;
178 27 50       81 if (ref $cb ne __PACKAGE__) {
179 0         0 confess "debug must be called as a class method";
180             }
181              
182             # collect debug parameters
183 27         44 my %params;
184 27 50       64 if (ref $in_args[0] eq "HASH") {
185 27         77 my $params_ref = shift @in_args;
186 27         136 %params = %$params_ref;
187             }
188              
189             # print debugging statement if enabled
190 27   50     77 my $level = $params{level} // 1;
191 27 50       87 if ($debug >= $level) {
192 0   0     0 my $wrapper = $params{wrapper} // 0; # skip stack frame if called from debug wrapper function
193              
194             # debug label: get caller name (default to function name from Perl call stack) and any label string
195 0         0 my @label;
196 0 0 0     0 if (exists $params{name} and defined $params{name}) {
197 0         0 push @label, $params{name};
198             } else {
199 0         0 my $caller = (caller(1+$wrapper))[3];
200 0 0       0 if ($caller eq "(eval)") {
201 0         0 push @label, (caller(2+$wrapper))[3], "eval";
202             } else {
203 0         0 push @label, $caller;
204             }
205             }
206 0 0 0     0 if (exists $params{label} and defined $params{label}) {
207 0         0 push @label, $params{label};
208             }
209              
210             # print debug message
211 0 0       0 my $msg = "--- debug [".(join "/", @label)."]: ".join(" ", map {(defined $_) ? $_ : "(undef)"} @in_args);
  0         0  
212 0         0 say STDERR $msg;
213 0 0 0     0 if ((exists $cb->{oldstderr}) and ($cb->{oldstderr}->fileno != fileno(STDERR))) {
214 0         0 $cb->{oldstderr}->print($msg."\n");
215             }
216             }
217 27         72 return;
218             }
219              
220             # template and variable expansion
221             # private class function
222             sub expand
223             {
224 10     10 0 17 my $value = shift;
225 10         54 my $cb = Container::Buildah->instance();
226              
227             # process array values sequentially
228 10 100       103 if (ref $value eq "ARRAY") {
229 2         5 my @result;
230 2         5 foreach my $subvalue (@$value) {
231 3         38 push @result, expand($subvalue);
232             }
233 2         31 $cb->debug({level => 4}, "expand: $value -> [".join(" ", @result)."]");
234 2         8 return \@result;
235             }
236              
237             # process scalar value
238 8         11 my $output;
239 8         43 $cb->{template}->process(\$value, $cb->{config}, \$output);
240 8         42237 $cb->debug({level => 4}, "expand: $value -> $output");
241              
242             # expand templates as long as any remain, up to 10 iterations
243 8         19 my $count=0;
244 8   66     36 while ($output =~ / \[% .* %\] /x and $count++ < 10) {
245 1         3 $value = $output;
246 1         3 $output = ""; # clear because template concatenates to it
247 1         6 $cb->{template}->process(\$value, $cb->{config}, \$output);
248 1         1423 $cb->debug({level => 4}, "expand ($count): $value -> $output");
249             }
250 8         24 return $output;
251             }
252              
253             # get configuration value
254             # public class method
255             sub get_config
256             {
257 7     7 1 28399 my ($class_or_obj, @path) = @_;
258 7 50       27 my $cb = (ref $class_or_obj) ? $class_or_obj : $class_or_obj->instance();
259              
260             # special case for empty path: return config tree root
261 7 50       28 if (not @path) {
262 0         0 $cb->debug({level => 3}, "get_config: retrieved root node");
263 0         0 return $cb->{config};
264             }
265              
266             # navigate down config tree
267 7         15 my $key = pop @path; # last entry of path is target node
268 7         26 my $orig_path = join("/", @path)."->".$key; # for error reporting
269 7         14 my $node = $cb->{config};
270 7         22 while (@path) {
271 6         12 my $subnode = shift @path;
272 6 50 33     30 if (exists $node->{$subnode} and ref $node->{$subnode} eq "HASH") {
273 6         16 $node = $node->{$subnode};
274             } else {
275 0         0 confess "get_config: ($subnode) not found in search for $orig_path";
276             }
277             }
278              
279             # return configuration
280 7 50       17 if (exists $node->{$key}) {
281 7 50 66     30 if (ref $node->{$key} and ref $node->{$key} ne "ARRAY") {
282 0         0 $cb->debug({level => 3}, "get_config: $key -> $node->{$key}");
283 0         0 return $node->{$key};
284             }
285              
286             # if the value is scalar or array, perform variable expansion
287 7         24 my $result = expand($node->{$key});
288 7 100       29 if (ref $node->{$key} eq "ARRAY") {
289 2         9 $cb->debug({level => 3}, "get_config: $key -> [".join(" ", @{$node->{$key}})."]");
  2         18  
290             } else {
291 5         22 $cb->debug({level => 3}, "get_config: $key -> $result");
292             }
293 7         30 return $result;
294             }
295 0         0 $cb->debug({level => 3}, "get_config: not found ($orig_path)");
296 0         0 return;
297             }
298              
299             # allow caller to enforce its required configuration
300             # public class method
301             sub required_config
302             {
303 0     0 1 0 my ($class_or_obj, @in_args) = @_;
304 0 0       0 my $cb = (ref $class_or_obj) ? $class_or_obj : $class_or_obj->instance();
305              
306             # check for missing config parameters required by program
307 0         0 my @missing;
308 0         0 foreach my $key (@in_args) {
309 0 0       0 if (not exists $cb->{config}{$key}) {
310 0         0 push @missing, $key;
311             }
312             }
313              
314             # fail if any required parameters are missing
315 0 0       0 if (@missing) {
316 0         0 croak __PACKAGE__.": required configuration parameters missing: ".join(" ", @missing);
317             }
318             }
319              
320             # get debug mode value
321             # public class function
322             sub get_debug
323             {
324 0     0 1 0 return $debug;
325             }
326              
327             # set debug mode/level
328             # public class function
329             sub set_debug
330             {
331 0     0 1 0 $debug = int shift; # save integer debug level
332 0         0 return;
333             }
334              
335             # get OCI-recognized CPU architecture string for this system
336             # includes tweak to add v7 to armv7
337             # private class method
338             sub get_arch
339             {
340 0     0 0 0 my $cb = shift;
341 0         0 my $arch = $cb->info({format => q({{.host.arch}})});
342 0 0       0 if ($arch eq 'arm') {
343 0         0 my $cpuinfo = File::Slurp::read_file('/proc/cpuinfo', err_mode => "croak");
344 0 0       0 if (/ ^ CPU \s architecture \s* : \s* (.*) $ /x) {
345 0 0       0 if ($1 eq "7") {
346 0         0 $arch='armv7';
347             }
348             }
349             }
350 0         0 $cb->debug({level => 1}, "get_arch => $arch");
351 0         0 return $arch;
352             }
353              
354             # check array to verify all entries are defined, otherwise throw an exception
355             # private class function
356             sub disallow_undef
357             {
358 10     10 0 60 my $array_ref = shift;
359 10         23 my $got_type = ref $array_ref;
360 10 50       31 if ($got_type ne "ARRAY") {
361 0 0       0 confess "disallow_undef: improper usage - requires ARRAY ref, got ".($got_type ? $got_type : "undef");
362             }
363 10         53 for (my $i=0; $i < scalar @$array_ref; $i++) {
364 16 100       54 if (not defined $array_ref->[$i]) {
365 1 100       22 confess "disallow_undef: found undefined value in parameter list item $i: ".join(" ", map {(defined $_) ? $_ : "(undef)"} @$array_ref);
  2         179  
366             }
367             }
368 9         24 return;
369             }
370              
371             #
372             # exception handling
373             #
374              
375             # handle exceptions from eval blocks
376             # private class function
377             sub exception_handler
378             {
379 6     6   56 no autodie;
  6         18  
  6         63  
380 0     0 0   my $xc = shift;
381 0 0         if ($xc) {
382 0 0         if (ref $xc eq "autodie::exception") {
    0          
383 0           say STDERR "exception(".$xc->function."): ".$xc->eval_error." at ".$xc->file." line ".$xc->line;
384             } elsif (ref $xc) {
385 0           say STDERR "exception(".(ref $xc)."): ".$xc
386             } else {
387 0           say STDERR "exception: ".$xc;
388             }
389 0           my $cb = Container::Buildah->instance();
390 0           open(STDOUT, '>&', $cb->{oldstdout});
391 0           open(STDERR, '>&', $cb->{oldstderr});
392              
393             # report status if possible and exit
394 0   0       my $basename = $cb->{config}{basename} // "unnamed container";
395 0           croak $basename." failed";
396             }
397             }
398              
399             #
400             # build stage management functions
401             #
402              
403             # compute container build order from dependencies
404             # private class method
405             sub build_order_deps
406             {
407 0     0 0   my $cb = shift;
408 0           my %deps; # dependencies in a hash of arrays, to be fed to Algorithm::Dependency::Source::HoA
409 0           my $stages = $cb->get_config("stages");
410 0 0         if (ref $stages ne "HASH") {
411 0 0         croak "stages confguration must be a hash, got ".((ref $stages) ? ref $stages : "scalar");
412             }
413              
414             # collect dependency data from each stage's configuration
415 0           my @stages = keys %$stages;
416 0           foreach my $stage (@stages) {
417 0           my @stage_deps;
418              
419             # use consumes or depends parameters in each stage for dependency data
420             # if consumes parameter exists, it declares stages which provide a tarball to this stage
421             # if depends parameter exists, it declares stages which have any other dependency
422 0           foreach my $param (qw(consumes depends)) {
423 0 0         if (exists $stages->{$stage}{$param}) {
424 0 0         if (ref $stages->{$stage}{$param} ne "ARRAY") {
425             croak "stage $stage '$param' entry must be an array, got "
426 0 0         .((ref $stages->{$stage}{$param}) ? ref $stages->{$stage}{$param} : "scalar");
427             }
428 0           push @stage_deps, @{$stages->{$stage}{$param}};
  0            
429             }
430             }
431              
432             # save the dependency list, even if empty
433 0           $deps{$stage} = \@stage_deps;
434             }
435              
436             # compute build order from dependencies using Algorithm::Dependency, using hash-of-arrays input format
437 0           my $Source = Algorithm::Dependency::Source::HoA->new( \%deps );
438 0           my $algdep = Algorithm::Dependency->new(source => $Source);
439 0           my $order = $algdep->schedule_all;
440 0           $cb->debug({level => 1}, "build order (computed): ".join(" ", @$order));
441 0           $cb->{order} = {};
442 0           for (my $i=0; $i < scalar @$order; $i++) {
443 0           $cb->{order}{$order->[$i]} = $i;
444             }
445 0           $cb->debug({level => 1}, "build order (data): ".join(" ", map {$_."=>".$cb->{order}{$_}} keys %{$cb->{order}}));
  0            
  0            
446 0           return;
447             }
448              
449             # run a container-build stage
450             # private class method
451             sub stage
452             {
453 0     0 0   my ($cb, $name, %opt) = @_;
454              
455             # get flag: are we internal to the user namespace for container setup
456 0   0       my $is_internal = $opt{internal} // 0;
457              
458             # instantiate the Container::Buildah::Stage object for this stage's container
459 0           require Container::Buildah::Stage;
460 0           my $stage = Container::Buildah::Stage->new(name => $name);
461              
462             # create timestamped log directory if it doesn't exist
463 0           my $logdir_top = "log-".Container::Buildah->get_config("basename");
464 0           my $logdir_time = "$logdir_top/".Container::Buildah->get_config("timestamp_str");
465 0           foreach my $dir ($logdir_top, $logdir_time) {
466 0 0         if (not -d $dir) {
467 0           mkdir $dir, 02770;
468             }
469             }
470 0 0         if (-l "$logdir_top/current") {
471 0           unlink "$logdir_top/current";
472             }
473 0           symlink Container::Buildah->get_config("timestamp_str"), $logdir_top."/current";
474              
475             # redirect STDOUT and STDERR to log file pipe for container stage
476             ## no critic (InputOutput::RequireBriefOpen, Miscellanea::ProhibitUnrestrictedNoCritic)
477 0           my $stagelog;
478 0 0         open($stagelog, '>>', $logdir_time."/".$name.($is_internal ? "-internal" : ""));
479 0           $stagelog->autoflush(1);
480 0           open(STDOUT, '>&', $stagelog);
481 0           open(STDERR, '>&', $stagelog);
482              
483             # generate container and run this stage's function in it
484 0 0         $stage->status("begin (".($is_internal ? "internal" : "external").")");
485 0 0         eval {
486 0 0         if ($is_internal) {
487             #
488             # run the internal stage function since we're within the mounted container namespace
489             #
490              
491             # retrieve the internal stage functions
492 0           my $func_deps = $stage->get_func_deps;
493 0           my $func_exec = $stage->get_func_exec;
494              
495             # enforce required func_exec configuration (func_deps is optional)
496 0 0         if (not defined $func_exec) {
497 0           croak "stage $name internal: func_exec not configured";
498             }
499 0 0         if (ref $func_exec ne "CODE") {
500 0           confess "stage $name internal: func_exec is not a code reference - got "
501             .(ref $func_exec);
502             }
503              
504             # run deps & exec functions for the stage, process consumed or produced tarballs
505 0 0 0       if ((defined $func_deps) and (ref $func_deps eq "CODE")) {
506 0           $stage->status("func_deps");
507 0           $func_deps->($stage);
508             } else {
509 0           $stage->status("func_deps - skipped, not configured");
510             }
511 0           $stage->status("consume");
512 0           $stage->consume; # import tarball(s) from other stage(s), if configured
513 0           $stage->status("func_exec");
514 0           $func_exec->($stage);
515 0           $stage->status("produce");
516 0           $stage->produce; # export tarball for another stage to use, if configured
517             } else {
518             # run the external stage wrapper which will mount the container namespace and call the internal stage in it
519 0           $stage->status("launch_namespace");
520 0           $stage->launch_namespace;
521             }
522 0           1;
523             } or exception_handler $@;
524 0 0         $stage->status("end (".($is_internal ? "internal" : "external").")");
525              
526             # close output pipe
527 0           close $stagelog;
528 0           open(STDOUT, '>&', $cb->{oldstdout});
529 0           open(STDERR, '>&', $cb->{oldstderr});
530 0           return;
531             }
532              
533             #
534             # process mainline
535             #
536              
537             # process each defined stage of the container production pipeline
538             # public class function
539             sub main
540             {
541             # save a copy of the command line for re-launching the script in a container namespace
542 0     0 1   my @argv_copy = @ARGV;
543              
544             # process command line
545 0           my %cmd_opts;
546             my @added_opts = (exists $init_config{added_opts} and ref $init_config{added_opts} eq "ARRAY")
547 0 0 0       ? @{$init_config{added_opts}} : ();
  0            
548 0           GetOptions(\%cmd_opts, "debug:i", "config:s", "internal:s", @added_opts);
549 0 0         if (exists $cmd_opts{debug}) {
550 0           set_debug($cmd_opts{debug});
551             }
552              
553             # instantiate Container::Buildah object
554 0           my @do_yaml;
555 0 0         if (not exists $init_config{testing_skip_yaml}) {
556 0           my $yaml_config = $cmd_opts{config};
557 0 0         if (not defined $yaml_config) {
558 0           foreach my $suffix (qw(yml yaml)) {
559 0 0         if (-f $init_config{basename}.".".$suffix) {
560 0           $yaml_config = $init_config{basename}.".".$suffix;
561 0           last;
562             }
563             }
564 0 0         if (not defined $yaml_config) {
565 0           croak "YAML configuration required to set software versions";
566             }
567             }
568 0           @do_yaml = (yaml_config => $yaml_config);
569             }
570 0           my $cb = Container::Buildah->instance(@do_yaml);
571              
572             # process config
573 0           $cb->{config}{argv} = \@argv_copy;
574 0           $cb->{config}{opts} = \%cmd_opts;
575 0           $cb->{config}{arch} = $cb->get_arch();
576 0 0 0       if (exists $init_config{required_config}
577             and ref $init_config{required_config} eq "ARRAY")
578             {
579 0           $cb->required_config(@{$init_config{required_config}});
  0            
580             }
581              
582 0 0         if (exists $cmd_opts{internal}) {
583             # run an internal stage inside a container user namespace if --internal=stage was specified
584 0           $cb->stage($cmd_opts{internal}, internal => 1);
585             } else {
586             # compute container build order from dependencies
587 0           $cb->build_order_deps;
588              
589             # external (outside the user namespaces) loop to run each stage
590 0           foreach my $stage (sort {$cb->{order}{$a} <=> $cb->{order}{$b}}
  0            
591 0           keys %{$cb->{config}{stages}})
592             {
593 0           $cb->stage($stage);
594             }
595              
596             # if we get here, we're done
597 0           $cb->status(Container::Buildah->get_config("basename")." complete");
598             }
599 0           return 0;
600             }
601              
602             1;
603              
604             __END__