From bcc098c32197247f9536dd04ab9065e513ae08cb Mon Sep 17 00:00:00 2001 From: Karl Rister Date: Mon, 4 May 2026 16:00:22 -0500 Subject: [PATCH] feat: remove workshop.pl and consolidate on workshop.py Remove the legacy Perl implementation. The Python version has been the default since rickshaw PR #805 and is what bin/_main always invokes. Update CI workflows and documentation to reference workshop.py only. Perl dependencies in workshop.json remain for the 4 legacy supported releases (2025.1-2025.4) that still use workshop.pl at runtime. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/crucible-merged.yaml | 1 - .github/workflows/workshop-ci.yaml | 58 +- CLAUDE.md | 20 +- README.md | 18 +- workshop.pl | 2208 ------------------------ 5 files changed, 8 insertions(+), 2297 deletions(-) delete mode 100755 workshop.pl diff --git a/.github/workflows/crucible-merged.yaml b/.github/workflows/crucible-merged.yaml index 16aa83b..ce3947a 100644 --- a/.github/workflows/crucible-merged.yaml +++ b/.github/workflows/crucible-merged.yaml @@ -6,7 +6,6 @@ on: branches: [ master ] paths: - .github/workflows/crucible-merged.yaml - - workshop.pl - workshop.py - schema.json - registries-schema.json diff --git a/.github/workflows/workshop-ci.yaml b/.github/workflows/workshop-ci.yaml index af0091c..8913fbf 100644 --- a/.github/workflows/workshop-ci.yaml +++ b/.github/workflows/workshop-ci.yaml @@ -37,62 +37,6 @@ jobs: - name: Display changes run: echo '${{ toJSON(steps.filter.outputs) }}' | jq . - workshop-pl: - needs: changes - if: ${{ github.event.pull_request.head.repo.fork != true && (github.event_name == 'workflow_dispatch' || needs.changes.outputs.only-docs != 'true') }} - runs-on: [ self-hosted, aws-cloud-1 ] - - steps: - - uses: actions/checkout@v4 - - - name: Install git - run: | - sudo dnf install -y git - - - name: Install container dependencies - run: | - sudo dnf install -y skopeo podman buildah - - - name: Install Perl dependencies - run: | - sudo dnf install -y perl-JSON perl-JSON-XS perl-JSON-Validator perl-Data-UUID perl-Digest-SHA perl-Archive-Extract-xz-IO-Uncompress-UnXz.noarch - - - name: Install Toolbox - run: | - if [ -d ~/toolbox ]; then - sudo rm -Rf ~/toolbox - fi - pushd ~/ - git clone https://github.com/perftool-incubator/toolbox.git - - - name: Run workshop - dump-config - run: | - export TOOLBOX_HOME=~/toolbox - echo - sudo --preserve-env=TOOLBOX_HOME ./workshop.pl --log-level verbose --label workshop-ci-dump-config --userenv ./userenvs/fedora-ci.json --dump-config true - - - name: Run workshop - dump-config - force-build-policy missing - run: | - export TOOLBOX_HOME=~/toolbox - echo - sudo --preserve-env=TOOLBOX_HOME ./workshop.pl --log-level verbose --label workshop-ci-dump-config --userenv ./userenvs/fedora-ci.json --dump-config true --force-build-policy missing - - - name: Run workshop - dump-files - run: | - export TOOLBOX_HOME=~/toolbox - echo - sudo --preserve-env=TOOLBOX_HOME ./workshop.pl --log-level verbose --label workshop-ci-dump-files --userenv ./userenvs/fedora-ci.json --dump-files true - - - name: Run workshop - run: | - export TOOLBOX_HOME=~/toolbox - echo - sudo --preserve-env=TOOLBOX_HOME ./workshop.pl --log-level verbose --label workshop-ci --userenv ./userenvs/fedora-ci.json - - - name: Cleanup toolbox - run: | - sudo rm -Rf ~/toolbox - workshop-py: needs: changes if: ${{ github.event.pull_request.head.repo.fork != true && (github.event_name == 'workflow_dispatch' || needs.changes.outputs.only-docs != 'true') }} @@ -158,7 +102,7 @@ jobs: - run: echo "faux-workshop-ci complete" workshop-ci: - needs: [ workshop-pl, workshop-py, faux-workshop-ci ] + needs: [ workshop-py, faux-workshop-ci ] if: always() runs-on: ubuntu-latest steps: diff --git a/CLAUDE.md b/CLAUDE.md index c57f4ac..d9d099a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,14 +4,9 @@ Workshop builds container images using buildah. It combines a userenv (base image spec) with requirement definitions (software to install) to produce reproducible container images. Part of the [perftool-incubator](https://github.com/perftool-incubator) project. -## Implementations +## Implementation -There are two coexisting implementations: - -- **`workshop.pl`** -- Original Perl implementation (~2200 lines) -- **`workshop.py`** -- Python 3 reimplementation (~1800 lines) - -Both should be kept functionally equivalent. Changes to build logic should be applied to both implementations unless otherwise specified. +- **`workshop.py`** -- Python 3 implementation (~1800 lines) ## Key Files @@ -23,10 +18,6 @@ Both should be kept functionally equivalent. Changes to build logic should be ap ## Dependencies -### Perl -- Requires modules: JSON, JSON::Validator, Getopt::Long, Digest::SHA, Data::UUID, etc. -- Uses `toolbox::json` and `toolbox::logging` from TOOLBOX_HOME/perl/ - ### Python - Uses `invoke` (for `invoke.run()` shell commands) and `jsonschema` (via toolbox) - Uses `toolbox.json` (load_json_file, validate_schema) from TOOLBOX_HOME/python/ @@ -38,17 +29,16 @@ Both should be kept functionally equivalent. Changes to build logic should be ap ## Code Style -- 4-space indentation, no tabs (both Perl and Python) -- Both files include modeline headers for editor configuration +- 4-space indentation, no tabs +- Modeline headers for editor configuration - Python uses a custom `VERBOSE` logging level (15) between DEBUG (10) and INFO (20) - SHA-256 checksums use canonical/sorted JSON encoding ## CI Workflows -- `workshop-ci.yaml` -- Tests both Perl and Python implementations on PR +- `workshop-ci.yaml` -- Tests workshop.py on PR - `crucible-ci.yaml` -- Crucible integration CI on PR - `crucible-merged.yaml` -- Post-merge crucible CI (pull_request_target: closed) -- Workflow trigger paths must include both `workshop.pl` and `workshop.py` ## Git Conventions diff --git a/README.md b/README.md index 23e6d5b..a0298f5 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,7 @@ Workshop is a tool for building container images based on specified userenv and - [skopeo](https://github.com/containers/skopeo) -- remote registry inspection - curl -### Perl Modules (for workshop.pl) - -- JSON -- JSON::Validator -- Getopt::Long -- Digest::SHA -- Data::UUID -- Scalar::Util -- File::Basename - -### Python Packages (for workshop.py) +### Python Packages - invoke - jsonschema (provided via toolbox) @@ -36,14 +26,11 @@ Workshop requires the [toolbox](https://github.com/perftool-incubator/toolbox) p export TOOLBOX_HOME=/path/to/toolbox ``` -Workshop expects `$TOOLBOX_HOME/perl/` to exist and contain the `toolbox::json` and `toolbox::logging` modules (for `workshop.pl`), and `$TOOLBOX_HOME/python/` for the `toolbox.json` module (for `workshop.py`). +Workshop expects `$TOOLBOX_HOME/python/` to exist and contain the `toolbox.json` module. ## Usage -There are two functionally equivalent implementations: `workshop.pl` (Perl) and `workshop.py` (Python). Currently `workshop.pl` is the stable, production version. `workshop.py` is a reimplementation that is intended to eventually replace it. Both are tested in CI and accept the same arguments: - ``` -workshop.pl --userenv [--requirements ...] [options] workshop.py --userenv [--requirements ...] [options] ``` @@ -134,7 +121,6 @@ The `--param key=value` flag performs string substitution across userenv, requir This is useful for injecting paths or other values that vary between environments. For example: ``` -workshop.pl --userenv my-userenv.json --requirements my-reqs.json --param BASEDIR=/opt/myapp workshop.py --userenv my-userenv.json --requirements my-reqs.json --param BASEDIR=/opt/myapp ``` diff --git a/workshop.pl b/workshop.pl deleted file mode 100755 index 27360e2..0000000 --- a/workshop.pl +++ /dev/null @@ -1,2208 +0,0 @@ -#!/usr/bin/perl -# -*- mode: perl; indent-tabs-mode: nil; perl-indent-level: 4 -*- -# vim: autoindent tabstop=4 shiftwidth=4 expandtab softtabstop=4 filetype=perl - -use strict; -use warnings; - -use Getopt::Long; -use JSON; -use Scalar::Util qw(looks_like_number); -use File::Basename; -use Digest::SHA qw(sha256_hex); -use JSON::Validator; - -use Data::UUID; -my $uuid = Data::UUID->new; - -use Data::Dumper; -$Data::Dumper::Sortkeys = 1; -$Data::Dumper::Pair = ' : '; -$Data::Dumper::Useqq = 1; -$Data::Dumper::Indent = 1; - -BEGIN { - if (!(exists $ENV{'TOOLBOX_HOME'} && -d "$ENV{'TOOLBOX_HOME'}/perl")) { - print "This script requires libraries that are provided by the toolbox project.\n"; - print "Toolbox can be acquired from https://github.com/perftool-incubator/toolbox and\n"; - print "then use 'export TOOLBOX_HOME=/path/to/toolbox' so that it can be located.\n"; - exit 1; - } -} -use lib "$ENV{'TOOLBOX_HOME'}/perl"; -use toolbox::json; -use toolbox::logging; - -# disable output buffering -$|++; - -my $indent = " "; - -my %args; -$args{'log-level'} = 'info'; -$args{'skip-update'} = 'false'; -$args{'force'} = 'false'; -$args{'dump-config'} = 'false'; -$args{'dump-files'} = 'false'; -$args{'param'} = {}; -$args{'reg-tls-verify'} = 'true'; - -my @cli_args = ( '--log-level', '--requirements', '--skip-update', '--userenv', '--force', '--config', '--dump-config', '--dump-files', '--force-build-policy', '--registries-json' ); -my %log_levels = ( 'info' => 1, 'verbose' => 1, 'debug' => 1 ); -my %update_options = ( 'true' => 1, 'false' => 1 ); -my %force_options = ( 'true' => 1, 'false' => 1 ); -my %dump_config_options = ( 'true' => 1, 'false' => 1); -my %dump_files_options = ( 'true' => 1, 'false' => 1); -my %reg_tls_verify_options = ( 'true' => 1, 'false' => 1); -my %force_build_policy_options = ( 'missing' => 1, 'ifnewer' => 1 ); - -my @virtual_fs = ('dev', 'proc', 'sys'); - -my $command_logger_fmt = "################################################################################\n" . - "COMMAND: %s\n" . - "RETURN CODE: %d\n" . - "COMMAND OUTPUT:\n\n%s\n" . - "********************************************************************************\n"; - -my $command; -my $command_output; -my $dirname; -my $schema_location; -my $registries_schema_location; -my $userenv_json; -my $rc; -my $distro_installs; -my $req_counter; -my $update_cmd = ""; -my $clean_cmd = ""; -my $getsrc_cmd; -my $volume_opt = ""; -if (-e "/run/secrets" ) { - $volume_opt = "--volume /run/secrets:/run/secrets" -} - -sub get_exit_code { - my ($exit_reason) = @_; - - my %reasons = ( - 'success' => 0, - 'no_userenv' => 1, - 'config_set_cmd' => 2, - 'schema_not_found' => 3, - 'userenv_failed_validation' => 4, - 'failed_opening_userenv' => 5, - 'requirement_failed_validation' => 6, - 'failed_opening_requirement' => 7, - 'userenv_missing' => 8, - 'duplicate_requirements' => 9, - 'requirement_conflict' => 10, - 'image_query' => 11, - 'image_origin_pull' => 12, - 'old_container_cleanup' => 13, - 'remove_existing_container' => 14, - 'create_container' => 15, - 'unsupported_package_manager' => 16, - 'update_failed' => 17, - 'update_cleanup' => 18, - 'container_mount' => 19, - 'virtual_fs_mount' => 20, - 'resolve.conf_backup' => 21, - 'resolv.conf_update' => 22, - 'package_install' => 23, - 'group_install' => 24, - 'build_failed' => 25, - 'chdir_failed' => 26, - 'unpack_failed' => 27, - 'unpack_dir_not_found' => 28, - 'download_failed' => 29, - 'command_run_failed' => 30, - 'install_cleanup' => 31, - 'chroot_escape_1' => 32, - 'chroot_escape_2' => 33, - 'chroot_escape_3' => 34, - 'chroot_failed' => 35, - 'directory_reference' => 36, - 'local-copy_failed' => 37, - 'copy_dst_missing' => 38, - 'config_failed_validation' => 39, - 'copy_type' => 40, - 'virtual_fs_umount' => 41, - 'resolve.conf_remove' => 42, - 'resolve.conf_restore' => 43, - 'container_umount' => 44, - 'image_create' => 45, - 'new_container_cleanup' => 46, - 'config_annotate_fail' => 47, - 'get_config_version' => 48, - 'ENTRY_AVAILABLE' => 49, - 'failed_opening_config' => 50, - 'config_set_entrypoint' => 51, - 'config_set_author' => 52, - 'config_set_annotation' => 53, - 'config_set_env' => 54, - 'config_set_port' => 55, - 'config_set_label' => 56, - 'schema_invalid_json' => 57, - 'userenv_invalid_json' => 58, - 'requirement_invalid_json' => 59, - 'config_invalid_json' => 60, - 'userenv_not_found' => 61, - 'requirement_not_found' => 62, - 'config_not_found' => 63, - 'requirement_missing' => 64, - 'config_missing' => 65, - 'cpanm_install_failed' => 70, - 'python3_install_failed' => 80, - 'npm_install_failed' => 90, - 'requirement_definition_missing' => 91, - 'no_label' => 92, - 'package_remove' => 93, - 'group_remove' => 94, - 'architecture_query_failed' => 95, - 'unsupported_platform_architecture' => 96, - 'skopeo_inspect_failed' => 97, - 'skopeo_digest_missing' => 98, - 'registries_json_failed_validation' => 99, - 'registries_json_invalid_json' => 100, - 'registries_json_missing' => 101, - 'failed_opening_registries_json' => 102, - 'registries_json_unknown_loading_error' => 103, - 'userenv_unknown_loading_error' => 104, - 'config_unknown_load_error' => 105, - 'pull_token_not_found' => 106, - 'set_default_user' => 107 - ); - - if (exists($reasons{$exit_reason})) { - return($reasons{$exit_reason}); - } else { - logger('info', "Unknown exit code requested [$exit_reason]\n"); - return(-1); - } -} - -sub compare_requirement_definition { - my ($a, $b) = @_; - - if (ref($a) eq 'HASH') { - if (ref($b) ne 'HASH') { - return(1); - } - - if (scalar(keys(%{$a})) != scalar(keys(%{$b}))) { - return(1); - } - - foreach my $key (keys %{$a}) { - if (!exists $b->{$key}) { - return(1); - } - - my $ret_val = compare_requirement_definition($a->{$key}, $b->{$key}); - if ($ret_val) { - return($ret_val); - } - } - } elsif (ref($a) eq 'ARRAY') { - if (ref($b) ne 'ARRAY') { - return(1); - } - - if (scalar(@{$a}) != scalar(@{$b})) { - return(1); - } - - for (my $i=0; $i[" . ref($a) . "]->[" . ref(\$a) . "]->[$a]\n"); - logger('debug', "B->[" . ref($b) . "]->[" . ref(\$b) . "]->[$b]\n"); - return(1); - } - - return 0; -} - -sub param_replacement { - my ($input, $indent) = @_; - - my $str = $input; - - foreach my $key (keys %{$args{'param'}}) { - logger('verbose', "checking for presence of '" . $key . "' in '" . $str . "'\n"); - if ($str =~ m/$key/) { - logger('info', "replacing '" . $key . "' with '" . $args{'param'}{$key} . "' in '" . $str . "'\n", $indent); - $str =~ s/$key/$args{'param'}{$key}/; - } - } - - return ($str); -} - -sub run_command { - my ($command) = @_; - - $command .= " 2>&1"; - my $command_output = `. /etc/profile; $command`; - my $rc = $? >> 8; - - return ($command, $command_output, $rc); -} - -sub filter_output { - my ($output) = @_; - - my @lines = split(/\n/, $output); - - $output = ""; - for (my $i=0; $i []\n"); - logger("info", "\n"); - logger("info", "Required arguments:\n"); - logger("info", "\n"); - logger("info", "--userenv User environment file\n"); - logger("info", "\n"); - logger("info", "Optional arguments: (* denotes default)\n"); - logger("info", "\n"); - logger("info", "--requirements Requirements file (can be used multiple times)\n"); - logger("info", "--label Label to apply to container image\n"); - logger("info", "--config Container config file\n"); - logger("info", "--log-level Control logging output\n"); - logger("info", "--skip-update Should the container run it's distro update function\n"); - logger("info", "--force Force the container build\n"); - logger("info", "--dump-config Dump the config instead of building the container\n"); - logger("info", "--dump-files Dump the files that are being manually handled\n"); - logger("info", "--param = When is found in the userenv and/or requirements file, substitute for it\n"); - logger("info", "--reg-tls-verify Use TLS for remote registry actions\n"); - logger("info", "--force-build-policy Override the userenv's specified build policy\n"); - logger("info", "--registries-json A configuration file (most likely from Crucible) with details on how to access various image registries\n"); - logger("info", "\n"); -} - -sub arg_handler { - my ($opt_name, $opt_value) = @_; - - if ($opt_name eq "help") { - usage(); - exit(); - } elsif ($opt_name eq "completions") { - $args{$opt_name} = 1; - - if ($opt_value eq 'all') { - for (my $i=0; $i]: " . $opt_value; - } - } elsif ($opt_name eq "config") { - $args{'config'} = $opt_value; - } elsif ($opt_name eq "userenv") { - $args{'userenv'} = $opt_value; - } elsif ($opt_name eq "requirements") { - if (! exists $args{'reqs'}) { - $args{'reqs'} = (); - } - push(@{$args{'reqs'}}, $opt_value); - } elsif ($opt_name eq "skip-update") { - if (exists ($update_options{$opt_value})) { - $args{'skip-update'} = $opt_value; - } else { - die("--skip-update must be one of 'true' or 'false' [not '$opt_value']"); - } - } elsif ($opt_name eq "force") { - if (exists ($force_options{$opt_value})) { - $args{'force'} = $opt_value; - } else { - die("--force must be one of 'true' or 'false' [not '$opt_value']"); - } - } elsif ($opt_name eq "log-level") { - if (exists($log_levels{$opt_value})) { - $args{'log-level'} = $opt_value; - } else { - my $msg = ""; - my @levels = (keys %log_levels); - for (my $i=0; $i= parameter following it [not '$opt_value']"); - } else { - $args{'param'}{$key} = $value; - } - } elsif ($opt_name eq "reg-tls-verify") { - if (exists ($reg_tls_verify_options{$opt_value})) { - $args{'reg-tls-verify'} = $opt_value; - } else { - die("--reg-tls-verify must be one of 'true' or 'false' [not '$opt_value']"); - } - } elsif ($opt_name eq "force-build-policy") { - if (exists ($force_build_policy_options{$opt_value})) { - $args{'force-build-policy'} = $opt_value; - } else { - die("--force-build-policy must be one of 'missing' or 'ifnewer' [not '$opt_value']"); - } - } elsif ($opt_name eq "registries-json") { - $args{'registries-json'} = $opt_value; - } else { - die("I'm confused, how did I get here [$opt_name]?"); - } -} - -sub delete_proto { - my $image = shift; - $image =~ s/^(\w+:\/)//; - return $image; -} - -sub install_manual { - my $req = shift; - my $output_offset = shift; - - logger('info', "installing package via manually provided commands...\n", $output_offset); - - my $install_cmd_log = ""; - my $command, my $command_output, my $rc; - foreach my $cmd (@{$req->{'manual_info'}{'commands'}}) { - logger('info', "executing '$cmd'...\n", $output_offset+1); - ($command, $command_output, $rc) = run_command($cmd); - $install_cmd_log .= sprintf($command_logger_fmt, $command, $rc, $command_output); - if ($rc != 0) { - logger('info', "failed [rc=$rc]\n", $output_offset+2); - logger('error', $install_cmd_log); - logger('error', "Failed to run command '$cmd'\n"); - exit(get_exit_code('command_run_failed')); - } - } - logger('info', "succeeded\n", $output_offset+2); - logger('verbose', $install_cmd_log); -} - -sub install_cpan { - my $req = shift; - my $output_offset = shift; - - logger('info', "installing package via cpan...\n", $output_offset); - - my $cpan_install_log = ""; - my $command, my $command_output, my $rc; - foreach my $cpan_package (@{$req->{'cpan_info'}{'packages'}}) { - logger('info', "cpan installing '$cpan_package'...\n", $output_offset+1); - ($command, $command_output, $rc) = run_command("cpanm $cpan_package"); - $cpan_install_log .= sprintf($command_logger_fmt, $command, $rc, $command_output); - if ($rc != 0){ - logger('info', "failed [rc=$rc]\n", $output_offset+2); - logger('error', $cpan_install_log); - logger('error', "Failed to cpan install perl package '$cpan_package'\n"); - exit(get_exit_code('cpanm_install_failed')); - } - } - logger('info', "succeeded\n", $output_offset+2); - logger('verbose', $cpan_install_log); -} - -sub install_node { - my $req = shift; - my $output_offset = shift; - - logger('info', "installing package via npm install...\n", $output_offset); - - my $npm_install_log = ""; - my $command, my $command_output, my $rc; - foreach my $node_package (@{$req->{'node_info'}{'packages'}}) { - logger('info', "npm installing '$node_package'...\n", $output_offset+1); - ($command, $command_output, $rc) = run_command("npm install $node_package"); - $npm_install_log .= sprintf($command_logger_fmt, $command, $rc, $command_output); - if ($rc != 0){ - logger('info', "failed [rc=$rc]\n", $output_offset+2); - logger('error', $npm_install_log); - logger('error', "Failed to npm install node package '$node_package'\n"); - exit(get_exit_code('npm_install_failed')); - } - } - logger('info', "succeeded\n", $output_offset+2); - logger('verbose', $npm_install_log); -} - -sub install_python { - my $req = shift; - my $output_offset = shift; - - logger('info', "installing package via python3 pip...\n", $output_offset); - - my $python3_install_log = ""; - my $command, my $command_output, my $rc; - foreach my $python3_package (@{$req->{'python3_info'}{'packages'}}) { - logger('info', "python3 pip installing '$python3_package'...\n", $output_offset+1); - ($command, $command_output, $rc) = run_command("/usr/bin/python3 -m pip install $python3_package"); - $python3_install_log .= sprintf($command_logger_fmt, $command, $rc, $command_output); - if ($rc != 0){ - logger('info', "failed [rc=$rc]\n", $output_offset+2); - logger('error', $python3_install_log); - logger('error', "Failed to python3 pip install python3 package '$python3_package'\n"); - exit(get_exit_code('python3_install_failed')); - } - } - logger('info', "succeeded\n", $output_offset+2); - logger('verbose', $python3_install_log); -} - -sub install_source { - my $req = shift; - my $output_offset = shift; - - logger('info', "building package '$req->{'name'}' from source for installation...\n", $output_offset); - - my $build_cmd_log = ""; - logger('info', "downloading...\n", $output_offset+1); - my $max_download_attempts = 3; - my $download_attempts = 1; - my $rc = 1; - my $command; - my $command_output; - while (($download_attempts <= $max_download_attempts) && - ($rc != 0)) { - ($command, $command_output, $rc) = run_command("curl --fail --url $req->{'source_info'}{'url'} --output $req->{'source_info'}{'filename'} --location"); - $build_cmd_log .= sprintf($command_logger_fmt, $command, $rc, $command_output); - $download_attempts++; - if ($rc != 0) { - sleep 1; - } - } - if ($rc == 0) { - logger('info', "getting directory...\n", $output_offset+1); - ($command, $command_output, $rc) = run_command("$req->{'source_info'}{'commands'}{'get_dir'}"); - $build_cmd_log .= sprintf($command_logger_fmt, $command, $rc, $command_output); - $command_output = filter_output($command_output); - my $get_dir = $command_output; - chomp($get_dir); - if ($rc == 0) { - logger('info', "unpacking...\n", $output_offset+1); - ($command, $command_output, $rc) = run_command("$req->{'source_info'}{'commands'}{'unpack'}"); - $build_cmd_log .= sprintf($command_logger_fmt, $command, $rc, $command_output); - if ($rc == 0) { - if (chdir($get_dir)) { - logger('info', "building...\n", $output_offset+1); - foreach my $build_cmd (@{$req->{'source_info'}{'commands'}{'commands'}}) { - logger('info', "executing '$build_cmd'...\n", $output_offset+2); - ($command, $command_output, $rc) = run_command("$build_cmd"); - $build_cmd_log .= sprintf($command_logger_fmt, $command, $rc, $command_output); - if ($rc != 0) { - logger('info', "failed\n", $output_offset+1); - logger('error', $build_cmd_log); - logger('error', "Build failed on command '$build_cmd'!\n"); - exit(get_exit_code('build_failed')); - } - } - logger('info', "succeeded\n", $output_offset+2); - logger('verbose', $build_cmd_log); - } else { - logger('info', "failed\n", $output_offset+2); - logger('error', $build_cmd_log); - logger('error', "Could not chdir to '$get_dir'!\n"); - exit(get_exit_code('chdir_failed')); - } - } else { - logger('info', "failed\n", $output_offset+2); - logger('error', $build_cmd_log); - logger('error', "Could not unpack source package!\n"); - exit(get_exit_code('unpack_failed')); - } - } else { - logger('info', "failed\n", $output_offset+2); - logger('error', $build_cmd_log); - logger('error', "Could not get unpack directory!\n"); - exit(get_exit_code('unpack_dir_not_found')); - } - } else { - logger('info', "failed\n", $output_offset+2); - logger('error', $build_cmd_log); - logger('error', "Could not download $req->{'source_info'}{'url'}!\n"); - exit(get_exit_code('download_failed')); - } -} - -sub install_files { - my $req = shift; - my $container = shift; - my $output_offset = shift; - - foreach my $file (@{$req->{'files_info'}{'files'}}) { - $file->{'src'} = param_replacement($file->{'src'}, 2); - if (exists($file->{'dst'})) { - $file->{'dst'} = param_replacement($file->{'dst'}, 2); - } - logger('info', "copying '$file->{'src'}'...\n", $output_offset); - - my $command, my $command_output, my $rc; - if (exists($file->{'dst'})) { - ($command, $command_output, $rc) = run_command("buildah add $container $file->{'src'} $file->{'dst'}"); - if ($rc != 0) { - logger('info', "failed\n", $output_offset+1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to copy '$file->{'src'}' to the temporary container!\n"); - #$return_channel->put(get_exit_code('local-copy_failed')); - } else { - logger('info', "succeeded\n", $output_offset+1); - command_logger('verbose', $command, $rc, $command_output); - } - } else { - logger('info', "failed\n", $output_offset+1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Destination '$file->{'dst'}' not defined!\n"); - } - } -} - -sub install_distro_manual { - my $req = shift; - my $container = shift; - my $output_offset = shift; - - logger('info', "performing manual distro package installation...\n", $output_offset); - - if (chdir('/root')) { - foreach my $pkg (@{$req->{'distro-manual_info'}{'packages'}}) { - my $install_cmd_log = ""; - my $download_filename = "distro-manual-package"; - - logger('info', "package '$pkg'...\n", $output_offset+1); - - logger('info', "downloading...\n", $output_offset+2); - my $max_download_attempts = 3; - my $download_attempts = 1; - my $rc = 1; - my $command; - my $command_output; - while (($download_attempts <= $max_download_attempts) && - ($rc != 0)) { - ($command, $command_output, $rc) = run_command("buildah run " . $volume_opt . " --isolation chroot $container -- curl --fail --url $pkg --output $download_filename --location"); - $install_cmd_log .= sprintf($command_logger_fmt, $command, $rc, $command_output); - $download_attempts++; - if ($rc != 0) { - sleep 1; - } else { - my $operation_cmd; - if ($userenv_json->{'userenv'}{'properties'}{'packages'}{'type'} eq 'rpm') { - $operation_cmd = "rpm --install --verbose --test " . $download_filename; - } elsif ($userenv_json->{'userenv'}{'properties'}{'packages'}{'type'} eq 'pkg') { - $operation_cmd = ""; - } else { - logger('info', "failed validation\n", $output_offset+3); - logger('error', "Unsupported userenv package type encountered [$userenv_json->{'userenv'}{'properties'}{'packages'}{'type'}]\n"); - exit(get_exit_code('unsupported_package_manager')); - } - - if ($operation_cmd ne '') { - ($command, $command_output, $rc) = run_command("buildah run " . $volume_opt . " --isolation chroot $container -- $operation_cmd"); - $install_cmd_log .= sprintf($command_logger_fmt, $command, $rc, $command_output); - if ($rc != 0) { - sleep 1; - } - } - } - } - if ($rc == 0) { - logger('info', "succeeded\n", $output_offset+3); - - logger('info', "installing...\n", $output_offset+2); - - my $operation_cmd = ""; - if ($userenv_json->{'userenv'}{'properties'}{'packages'}{'type'} eq 'rpm') { - $operation_cmd = "rpm --install --verbose " . $download_filename; - } elsif ($userenv_json->{'userenv'}{'properties'}{'packages'}{'type'} eq 'pkg') { - $operation_cmd = "dpkg --install " . $download_filename; - } else { - logger('info', "failed\n", $output_offset+3); - logger('error', "Unsupported userenv package type encountered [$userenv_json->{'userenv'}{'properties'}{'packages'}{'type'}]\n"); - exit(get_exit_code('unsupported_package_manager')); - } - - ($command, $command_output, $rc) = run_command("buildah run " . $volume_opt . " --isolation chroot $container -- $operation_cmd"); - $install_cmd_log .= sprintf($command_logger_fmt, $command, $rc, $command_output); - if ($rc == 0) { - logger('info', "succeeded\n", $output_offset+3); - - logger('info', "cleaning up...\n", $output_offset+2); - ($command, $command_output, $rc) = run_command("buildah run " . $volume_opt . " --isolation chroot $container -- rm -v $download_filename"); - $install_cmd_log .= sprintf($command_logger_fmt, $command, $rc, $command_output); - if ($rc == 0) { - logger('info', "succeeded\n", $output_offset+3); - logger('verbose', $install_cmd_log); - } else { - logger('info', "failed [rc=$rc]\n", $output_offset+3); - logger('error', $install_cmd_log); - logger('error', "Failed to cleanup package '$pkg'\n"); - exit(get_exit_code("install_cleanup")); - } - } else { - logger('info', "failed [rc=$rc]\n", $output_offset+3); - logger('error', $install_cmd_log); - logger('error', "Failed to install package '$pkg'\n"); - exit(get_exit_code("package_install")); - } - } else { - logger('info', "failed\n", $output_offset+3); - logger('error', $install_cmd_log); - logger('error', "Could not download $pkg!\n"); - exit(get_exit_code('download_failed')); - } - } - } else { - logger('info', "failed\n", $output_offset+1); - logger('error', "Could not chdir to /root!\n"); - exit(get_exit_code('chdir_failed')); - } -} - -sub install_distro { - my $req = shift; - my $container = shift; - my $output_offset = shift; - - $distro_installs = 1; - - # default to 'install' operation in case it is not specified in the requirement - my $operation = "install"; - if (exists($req->{'distro_info'}{'operation'})) { - $operation = $req->{'distro_info'}{'operation'}; - } - - my $environment = ""; - if (exists($req->{'distro_info'}{'environment'})) { - $environment = "env "; - foreach my $key (keys %{$req->{'distro_info'}{'environment'}}) { - $environment .= $key . "=" . $req->{'distro_info'}{'environment'}{$key} . " "; - } - } - - logger('info', "performing distro package $operation...\n", $output_offset); - - if (exists($req->{'distro_info'}{'packages'})) { - foreach my $pkg (@{$req->{'distro_info'}{'packages'}}) { - logger('info', "package '$pkg'...\n", $output_offset+1); - - my $operation_cmd = "$environment"; - if ($userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'} eq 'dnf') { - if ($operation eq 'install') { - $operation_cmd .= "dnf install --allowerasing --assumeyes " . $pkg; - } elsif ($operation eq 'remove') { - $operation_cmd .= "dnf remove --assumeyes " . $pkg; - } - } elsif ($userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'} eq 'yum') { - if ($operation eq 'install') { - $operation_cmd .= "yum install --assumeyes " . $pkg; - } elsif ($operation eq 'remove') { - $operation_cmd .= "yum remove --assumeyes " . $pkg; - } - } elsif ($userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'} eq 'apt') { - if ($operation eq 'install') { - $operation_cmd .= "apt-get install -y " . $pkg; - } elsif ($operation eq 'remove') { - $operation_cmd .= "apt-get remove -y " . $pkg; - } - } elsif ($userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'} eq 'zypper') { - if ($operation eq 'install') { - $operation_cmd .= "zypper install -y " . $pkg; - } elsif ($operation eq 'remove') { - $operation_cmd .= "zypper remove -y " . $pkg; - } - } else { - logger('info', "failed\n", $output_offset+2); - logger('error', "Unsupported userenv package manager encountered [$userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'}]\n"); - exit(get_exit_code('unsupported_package_manager')); - } - - (my $command, my $command_output, my $rc) = run_command("buildah run " . $volume_opt . " --isolation chroot $container -- $operation_cmd"); - if ($rc == 0) { - logger('info', "succeeded\n", $output_offset+2); - command_logger('verbose', $command, $rc, $command_output); - } else { - logger('info', "failed [rc=$rc]\n", $output_offset+2); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to $operation package '$pkg'\n"); - exit(get_exit_code("package_" . $operation)); - } - } - } - - if (exists($req->{'distro_info'}{'groups'})) { - foreach my $grp (@{$req->{'distro_info'}{'groups'}}) { - logger('info', "group '$grp'...\n", $output_offset+1); - - my $operation_cmd = "$environment"; - if ($userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'} eq 'dnf') { - if ($operation eq 'install') { - $operation_cmd .= "dnf groupinstall --allowerasing --assumeyes " . $grp; - } elsif ($operation eq 'remove') { - $operation_cmd .= "dnf groupremove --assumeyes " . $grp; - } - } elsif ($userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'} eq 'yum') { - if ($operation eq 'install') { - $operation_cmd .= "yum groupinstall --assumeyes " . $grp; - } elsif ($operation eq 'remove') { - $operation_cmd .= "yum groupremove --assumeyes " . $grp; - } - } elsif ($userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'} eq 'apt') { - if ($operation eq 'install') { - # The equivalent of 'groupinstall' is just meta-packages for apt, so no special option needed - $operation_cmd .= "apt-get install -y --assumeyes " . $grp; - } elsif ($operation eq 'remove') { - # The equivalent of 'groupremove' is just meta-packages for apt, so no special option needed - $operation_cmd .= "apt-get remove -y --assumeyes " . $grp; - } - } elsif ($userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'} eq 'zypper') { - if ($operation eq 'install') { - $operation_cmd .= "zypper install -y -t pattern " . $grp; - } elsif ($operation eq 'remove') { - $operation_cmd .= "zypper remove -y -t pattern " . $grp; - } - } else { - logger('info', "failed\n", $output_offset+2); - logger('error', "Unsupported userenv package manager encountered [$userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'}]\n"); - exit(get_exit_code('unsupported_package_manager')); - } - - ($command, $command_output, $rc) = run_command("buildah run " . $volume_opt . " --isolation chroot $container -- $operation_cmd"); - if ($rc == 0) { - logger('info', "succeeded\n", $output_offset+2); - command_logger('verbose', $command, $rc, $command_output); - } else { - logger('info', "failed [rc=$rc]\n", $output_offset+2); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to $operation group '$grp'\n"); - exit(get_exit_code('group_' . $operation)); - } - } - } -} - -sub add_chroot { - my $container = shift; - my $output_offset = shift; - my $mount_point; - - # mount the container image - logger('info', "Mounting the temporary container's fileystem...\n", $output_offset); - (my $command, my $command_output, my $rc) = run_command("buildah mount $container"); - if ($rc != 0) { - logger('info', "failed\n", $output_offset+1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to mount the temporary container's filesystem!\n"); - exit(get_exit_code('container_mount')); - } else { - logger('info', "succeeded\n", $output_offset+1); - command_logger('verbose', $command, $rc, $command_output); - $command_output = filter_output($command_output); - chomp($command_output); - $mount_point = $command_output; - } - - # bind mount virtual file systems that may be needed - logger('info', "Bind mounting /dev, /proc/, and /sys into the temporary container's filesystem...\n", $output_offset); - foreach my $fs (@virtual_fs) { - logger('info', "mounting '/$fs'...\n", $output_offset+1); - ($command, $command_output, $rc) = run_command("mount --verbose --options bind /$fs $mount_point/$fs"); - if ($rc != 0) { - logger('info', "failed\n", $output_offset+2); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to mount virtual filesystem '/$fs'!\n"); - exit(get_exit_code('virtual_fs_mount')); - } else { - logger('info', "succeeded\n", $output_offset+2); - command_logger('verbose', $command, $rc, $command_output); - } - } - - if (-e $mount_point . "/etc/resolv.conf") { - logger('info', "Backing up the temporary container's /etc/resolv.conf...\n", $output_offset); - my $command_output_log = ""; - ($command, $command_output, $rc) = run_command("/bin/cp --verbose --force " . $mount_point . "/etc/resolv.conf " . $mount_point . "/etc/resolv.conf.workshop"); - $command_output_log .= sprintf($command_logger_fmt, $command, $rc, $command_output); - if ($rc == 0) { - ($command, $command_output, $rc) = run_command("/bin/rm --verbose --force " . $mount_point . "/etc/resolv.conf"); - $command_output_log .= sprintf($command_logger_fmt, $command, $rc, $command_output); - } - if ($rc != 0) { - logger('info', "failed\n", $output_offset+1); - logger('error', $command_output_log); - logger('error', "Failed to backup the temporary container's /etc/resolv.conf!\n"); - exit(get_exit_code('resolve.conf_backup')); - } - logger('info', "succeeded\n", $output_offset+1); - logger('verbose', $command_output_log); - } - - logger('info', "Temporarily copying the host's /etc/resolv.conf to the temporary container...\n", $output_offset); - ($command, $command_output, $rc) = run_command("/bin/cp --verbose /etc/resolv.conf " . $mount_point . "/etc/resolv.conf"); - if ($rc != 0) { - logger('info', "failed\n", $output_offset+1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to copy /etc/resolv.conf to the temporary container!\n"); - exit(get_exit_code('resolv.conf_update')); - } - logger('info', "succeeded\n", $output_offset+1); - command_logger('verbose', $command, $rc, $command_output); - - return $mount_point; -} - -sub remove_chroot { - my $container = shift; - my $container_mount_point = shift; - my $output_offset = shift; - - # unmount virtual file systems that are bind mounted - logger('info', "Unmounting /dev, /proc/, and /sys from the temporary container's filesystem...\n", $output_offset); - my $umount_cmd_log = ""; - foreach my $fs (@virtual_fs) { - logger('info', "unmounting '/$fs'...\n", $output_offset+1); - (my $command, my $command_output, my $rc) = run_command("umount --verbose $container_mount_point/$fs"); - if ($rc != 0) { - logger('info', "failed\n", $output_offset+2); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to unmount virtual filesystem '/$fs'!\n"); - exit(get_exit_code('virtual_fs_umount')); - } else { - logger('info', "succeeded\n", $output_offset+2); - command_logger('verbose', $command, $rc, $command_output); - } - } - - logger('info', "Removing the temporarily assigned /etc/resolv.conf from the temporary container...\n", $output_offset); - ($command, $command_output, $rc) = run_command("/bin/rm --verbose --force " . $container_mount_point . "/etc/resolv.conf"); - if ($rc != 0) { - logger('info', "failed\n", $output_offset+1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to remove /etc/resolv.conf from the temporary container!\n"); - exit(get_exit_code('resolve.conf_remove')); - } - logger('info', "succeeded\n", $output_offset+1); - command_logger('verbose', $command, $rc, $command_output); - - if (-e $container_mount_point . "/etc/resolv.conf.workshop") { - logger('info', "Restoring the backup of the temporary container's /etc/resolv.conf...\n", $output_offset); - ($command, $command_output, $rc) = run_command("/bin/cp --verbose --force " . $container_mount_point . "/etc/resolv.conf.workshop " . $container_mount_point . "/etc/resolv.conf"); - if ($rc != 0) { - logger('info', "failed\n", $output_offset+1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to restore the temporary container's /etc/resolv.conf!\n"); - exit(get_exit_code('resolve.conf_restore')); - } - logger('info', "succeeded\n", $output_offset+1); - command_logger('verbose', $command, $rc, $command_output); - } - - # unmount the container image - logger('info', "Unmounting the temporary container's filesystem...\n", $output_offset); - ($command, $command_output, $rc) = run_command("buildah unmount $container"); - if ($rc != 0) { - logger('info', "failed\n", $output_offset+1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to unmount the temporary container's filesystem [$container_mount_point]!\n"); - exit(get_exit_code('container_umount')); - } else { - logger('info', "succeeded\n", $output_offset+1); - command_logger('verbose', $command, $rc, $command_output); - } -} - -sub set_update_clean_cmds { - if ($userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'} eq "dnf") { - $update_cmd = "dnf update --assumeyes --allowerasing --nobest"; - $clean_cmd = "dnf clean all"; - } elsif ($userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'} eq "yum") { - $update_cmd = "yum update --assumeyes"; - $clean_cmd = "yum clean all"; - } elsif ($userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'} eq "apt") { - $getsrc_cmd = "apt-get update -y"; - $update_cmd = "apt-get dist-upgrade -y"; - $clean_cmd = "apt-get clean"; - } elsif ($userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'} eq "zypper") { - $update_cmd = "zypper update -y"; - $clean_cmd = "zypper clean"; - } else { - logger('error', "Unsupported userenv package manager encountered [$userenv_json->{'userenv'}{'properties'}{'packages'}{'manager'}]\n"); - exit(get_exit_code('unsupported_package_manager')); - } -} - -sub update_container_pkgs { - my $container = shift; - - if ($args{'skip-update'} eq 'false') { - if (defined $getsrc_cmd) { - # get package-manager files list - logger('info', "Getting package-manager sources for the temporary container...\n"); - ($command, $command_output, $rc) = run_command("buildah run " . $volume_opt . " --isolation chroot $container -- $getsrc_cmd"); - if ($rc != 0) { - logger('info', "failed\n", 1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Updating the temporary container '$container' failed!\n"); - exit(get_exit_code('update_failed')); - } else { - logger('info', "succeeded\n", 1); - command_logger('verbose', $command, $rc, $command_output); - } - } - - # update the container's existing content - logger('info', "Updating the temporary container...\n"); - (my $command, my $command_output, my $rc) = run_command("buildah run " . $volume_opt . " --isolation chroot $container -- $update_cmd"); - if ($rc != 0) { - logger('info', "failed\n", 1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Updating the temporary container '$container' failed!\n"); - exit(get_exit_code('update_failed')); - } else { - logger('info', "succeeded\n", 1); - command_logger('verbose', $command, $rc, $command_output); - } - - logger('info', "Cleaning up after the update...\n"); - ($command, $command_output, $rc) = run_command("buildah run " . $volume_opt . " --isolation chroot $container -- $clean_cmd"); - if ($rc != 0) { - logger('info', "failed\n", 1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Updating the temporary container '$container' failed because it could not clean up after itself!\n"); - exit(get_exit_code('update_cleanup')); - } else { - logger('info', "succeeded\n", 1); - command_logger('verbose', $command, $rc, $command_output); - } - } else { - logger('info', "Skipping update due to --skip-update\n"); - } -} - - -if (!GetOptions("completions=s" => \&arg_handler, - "config=s" => \&arg_handler, - "log-level=s" => \&arg_handler, - "requirements=s" => \&arg_handler, - "skip-update=s" => \&arg_handler, - "force=s" => \&arg_handler, - "userenv=s" => \&arg_handler, - "label=s" => \&arg_handler, - "tag=s" => \&arg_handler, - "proj=s" => \&arg_handler, - "help" => \&arg_handler, - "param=s" => \&arg_handler, - "dump-config=s" => \&arg_handler, - "dump-files=s" => \&arg_handler, - "reg-tls-verify=s" => \&arg_handler, - "force-build-policy=s" => \&arg_handler, - "registries-json=s" => \&arg_handler)) { - usage(); - die("Error in command line arguments"); -} - -logger('debug', "Argument Hash:\n"); -logger('debug', Dumper(\%args)); - -if (exists($args{'completions'})) { - exit(get_exit_code('success')); -} - -if (! exists $args{'userenv'}) { - logger('error', "You must provide --userenv!\n"); - usage(); - exit(get_exit_code('no_userenv')); -} - -$dirname = dirname(__FILE__); -$schema_location = $dirname . "/schema.json"; -$registries_schema_location = $dirname . "/registries-schema.json"; - -logger('info', "Using '$schema_location' for JSON input file schema validation\n"); -logger('info', "Loading userenv definition from '$args{'userenv'}'...\n"); -logger('info', "importing JSON...\n", 1); - -($rc, $userenv_json) = get_json_file($args{'userenv'}, $schema_location); -if ($rc == 0 and defined $userenv_json) { - logger('info', "succeeded\n", 2); -} else { - logger('info', "failed\n", 2); - if ($rc == 2) { - logger('error', "Schema file $schema_location not found!\n"); - exit(get_exit_code('schema_not_found')); - } elsif ($rc == 3) { - logger('error', "Cannot open schema file: $schema_location\n"); - exit(get_exit_code('failed_opening_schema')); - } elsif ($rc == 4) { - logger('error', "Schema $schema_location is invalid JSON!\n"); - exit(get_exit_code('schema_invalid_json')); - } elsif ($rc == 5) { - logger('error', "Schema validation for userenv $args{'userenv'} using schema '$schema_location' failed!\n"); - exit(get_exit_code('userenv_failed_validation')); - } elsif ($rc == 6) { - logger('error', "Userenv $args{'userenv'} is invalid JSON!\n"); - exit(get_exit_code('userenv_invalid_json')); - } elsif ($rc == 8) { - logger('error', "Userenv file $args{'userenv'} not found\n"); - exit(get_exit_code('userenv_missing')); - } elsif ($rc == 9) { - logger('error', "Userenv file $args{'userenv'} open failed\n"); - exit(get_exit_code('failed_opening_userenv')); - } else { - logger('error', "Unknown error: $rc'\n"); - exit(get_exit_code('userenv_unknown_loading_error')); - } -} - -if (! exists $userenv_json->{'userenv'}{'origin'}{'requires-pull-token'}) { - # if the loaded json does not include requires-pull-token then - # default to "false" which results in the same behavior we have - # always had - $userenv_json->{'userenv'}{'origin'}{'requires-pull-token'} = "false"; -} - -if (! exists $userenv_json->{'userenv'}{'origin'}{'build-policy'}) { - # if the loaded json does not include an build policy then - # default to "missing" which results in the same behavior we have - # always had - $userenv_json->{'userenv'}{'origin'}{'build-policy'} = "missing"; -} - -if (exists $userenv_json->{'userenv'}{'properties'}{'platform'}) { - # the userenv has platform information that indicates what type of - # system architecture(s) it supports so validate that what we are - # about to build is supported - my $my_architecture; - logger('info', "performing userenv platform validation...\n", 1); - ($command, $command_output, $rc) = run_command("uname -m"); - if ($rc == 0) { - command_logger('verbose', $command, $rc, $command_output); - $command_output = filter_output($command_output); - chomp($command_output); - $my_architecture = $command_output; - logger('info', "found current system architecture is " . $my_architecture . "\n", 2); - } else { - logger('info', "failed\n", 2); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to obtain the current system architecture!\n"); - exit(get_exit_code('architecture_query_failed')); - } - - my $supported = 0; - # loop through the supported platforms and see if our architecture matches one of them - foreach my $platform (@{$userenv_json->{'userenv'}{'properties'}{'platform'}}) { - if ($platform->{'architecture'} eq $my_architecture) { - $supported = 1; - } - } - - if ($supported) { - logger('info', "succeeded...the userenv is supported for my architecture.\n", 2); - } else { - logger('info', "failed...the userenv is not supported for my architecture.\n", 2); - logger('error', "The userenv is not supported for my architecure.\n"); - exit(get_exit_code('unsupported_platform_architecture')); - } -} - -if (!defined $args{'proj'}) { - if (defined $args{'label'}) { - # Support default behavior before --proj was introduced - $args{'host'} = "localhost"; - $args{'proj'} = "workshop"; - $args{'label'} = $userenv_json->{'userenv'}{'name'} . "_" . $args{'label'}; - } else { - logger('error', "You must provide --label!\n"); - usage(); - exit(get_exit_code('no_label')); - } -} else { - if (!defined $args{'label'}) { - logger('error', "You must provide --label!\n"); - usage(); - exit(get_exit_code('no_label')); - } -} -if (!defined $args{'tag'}) { - $args{'tag'} = "latest"; -} - -logger('info', "calculating sha256...\n", 1); -$userenv_json->{'sha256'} = sha256_hex(JSON->new->canonical->encode($userenv_json)); -logger('info', "succeeded\n", 2); - -logger('debug', "Userenv Hash:\n"); -logger('debug', Dumper($userenv_json)); - -my @all_requirements; -my %active_requirements; -my @checksums; - -push(@checksums, $userenv_json->{'sha256'}); - -my $registries_json; -if (exists($args{'registries-json'})) { - logger('info', "Loading registries JSON...\n"); - logger('info', "'$args{'registries-json'}...\n", 1); - - (my $rc, $registries_json) = get_json_file($args{'registries-json'}, $registries_schema_location); - if ($rc == 0) { - logger('info', "succeeded\n", 2); - } else { - logger('info', "failed\n", 2); - if ($rc == 2) { - logger('error', "Schema file $registries_schema_location not found!\n"); - exit(get_exit_code('schema_not_found')); - } elsif ($rc == 3) { - logger('error', "Cannot open schema file: $registries_schema_location\n"); - exit(get_exit_code('failed_opening_schema')); - } elsif ($rc == 4) { - logger('error', "Schema $registries_schema_location is invalid JSON!\n"); - exit(get_exit_code('schema_invalid_json')); - } elsif ($rc == 5) { - logger('error', "Schema validation for registries JSON userenv $args{'registries-json'} using schema '$registries_schema_location' failed!\n"); - exit(get_exit_code('registries_json_failed_validation')); - } elsif ($rc == 6) { - logger('error', "Registries JSON $args{'registries-json'} is invalid JSON!\n"); - exit(get_exit_code('registries_json_invalid_json')); - } elsif ($rc == 8) { - logger('error', "Registries JSON file $args{'registries-json'} not found\n"); - exit(get_exit_code('registries_json_missing')); - } elsif ($rc == 9) { - logger('error', "Registries JSON file $args{'registries-json'} open failed\n"); - exit(get_exit_code('failed_opening_registries_json')); - } else { - logger('error', "Unkown error: $rc'\n"); - exit(get_exit_code('registries_json_unknown_loading_error')); - } - } - - logger('debug', "Registries JSON:\n"); - logger('debug', Dumper($registries_json)) -} - -logger('info', "Loading requested requirements...\n"); - -logger('info', "'$args{'userenv'}'...\n", 1); -my $userenv_reqs = { 'filename' => $args{'userenv'}, - 'json' => { 'userenvs' => [ - { - "name" => $userenv_json->{'userenv'}{'name'}, - "requirements" => [] - } - ], - 'requirements' => $userenv_json->{'requirements'} - } }; -foreach my $req (@{$userenv_reqs->{'json'}{'requirements'}}) { - push(@{$userenv_reqs->{'json'}{'userenvs'}[0]{'requirements'}}, $req->{'name'}); -} -logger('info', "succeeded\n", 2); -push(@all_requirements, $userenv_reqs); - -foreach my $req (@{$args{'reqs'}}) { - logger('info', "'$req'...\n", 1); - - my $tmp_req = {}; - logger('info', "importing JSON...\n", 2); - (my $rc, $tmp_req->{'json'}) = get_json_file($req, $schema_location); - - if ($rc == 0 and defined $tmp_req->{'json'}) { - $tmp_req->{'filename'} = $req; - logger('info', "succeeded\n", 3); - } else { - logger('info', "failed\n", 3); - #logger('error', "Failed to load requirement file '$req'!\n"); - if ($rc == 2) { - logger('error', "Schema file '$schema_location' not found!\n"); - exit(get_exit_code('schema_not_found')); - } elsif ($rc == 3) { - logger('error', "Cannot open schema file: '$schema_location'\n"); - exit(get_exit_code('failed_opening_schema')); - } elsif ($rc == 4) { - logger('error', "Schema '$schema_location' is invalid JSON!\n"); - exit(get_exit_code('schema_invalid_json')); - } elsif ($rc == 5) { - logger('error', "Schema validation for '$req' using schema '$schema_location' failed!\n"); - exit(get_exit_code('requirement_failed_validation')); - } elsif ($rc == 6) { - logger('error', "Requirement '$req' is invalid JSON!\n"); - exit(get_exit_code('requirement_invalid_json')); - } elsif ($rc == 8) { - logger('error', "Requirement file '$req' not found\n"); - exit(get_exit_code('requirement_missing')); - } else { - logger('error', "Unknown error'\n"); - exit(get_exit_code('failed_opening_requirement')); - } - } - - push(@all_requirements, $tmp_req); -} - -$active_requirements{'hash'} = (); -$active_requirements{'array'} = []; - -logger('info', "Finding active requirements...\n"); -foreach my $tmp_req (@all_requirements) { - logger('info', "processing requirements from '$tmp_req->{'filename'}'...\n", 1); - - my $userenv_idx = -1; - my $userenv_default_idx = -1; - my %userenvs; - - for (my $i=0; $i{'json'}{'userenvs'}}); $i++) { - if (ref($tmp_req->{'json'}{'userenvs'}[$i]{'name'}) eq 'ARRAY') { - for (my $x=0; $x{'json'}{'userenvs'}[$i]{'name'}}); $x++) { - if (exists($userenvs{$tmp_req->{'json'}{'userenvs'}[$i]{'name'}[$x]})) { - logger('info', "failed\n", 2); - logger('error', "Found duplicate userenv definition for '$tmp_req->{'json'}{'userenv'}[$i]{'name'}[$x]' in requirements '$tmp_req->{'filename'}'!\n"); - } else { - $userenvs{$tmp_req->{'json'}{'userenvs'}[$i]{'name'}[$x]} = 1; - } - - if ($tmp_req->{'json'}{'userenvs'}[$i]{'name'}[$x] eq $userenv_json->{'userenv'}{'name'}) { - $userenv_idx = $i; - } - } - } else { - if (exists($userenvs{$tmp_req->{'json'}{'userenvs'}[$i]{'name'}})) { - logger('info', "failed\n", 2); - logger('error', "Found duplicate userenv definition for '$tmp_req->{'json'}{'userenv'}[$i]{'name'}' in requirements '$tmp_req->{'filename'}'!\n"); - } else { - $userenvs{$tmp_req->{'json'}{'userenvs'}[$i]{'name'}} = 1; - } - - if ($tmp_req->{'json'}{'userenvs'}[$i]{'name'} eq $userenv_json->{'userenv'}{'name'}) { - $userenv_idx = $i; - } - } - - if ($tmp_req->{'json'}{'userenvs'}[$i]{'name'} eq 'default') { - $userenv_default_idx = $i; - } - } - - if ($userenv_idx == -1) { - if ($userenv_default_idx == -1) { - logger('info', "failed\n", 2); - logger('error', "Could not find appropriate userenv match in requirements '$tmp_req->{'filename'}' for '$userenv_json->{'userenv'}{'label'}'!\n"); - exit(get_exit_code('userenv_missing')); - } else { - $userenv_idx = $userenv_default_idx; - } - } - - foreach my $req (@{$tmp_req->{'json'}{'userenvs'}[$userenv_idx]{'requirements'}}) { - my %local_requirements; - my $found_req = 0; - - for (my $i=0; $i{'json'}{'requirements'}}); $i++) { - if (exists($local_requirements{$tmp_req->{'json'}{'requirements'}[$i]{'name'}})) { - logger('info', "failed\n", 2); - logger('error', "Found multiple requirement definitions for '$tmp_req->{'json'}{'requirements'}[$i]{'name'}' in '$tmp_req->{'filename'}'!\n"); - exit(get_exit_code('duplicate_requirements')); - } else { - $local_requirements{$tmp_req->{'json'}{'requirements'}[$i]{'name'}} = 1; - } - - if ($req eq $tmp_req->{'json'}{'requirements'}[$i]{'name'}) { - $found_req = 1; - - if (exists($active_requirements{'hash'}{$tmp_req->{'json'}{'requirements'}[$i]{'name'}})) { - if (compare_requirement_definition($tmp_req->{'json'}{'requirements'}[$i], - $active_requirements{'array'}[$active_requirements{'hash'}{$tmp_req->{'json'}{'requirements'}[$i]{'name'}}{'array_index'}])) { - logger('info', "failed\n", 2); - my $conflicts = ""; - my @tmp_array = (@{$active_requirements{'hash'}{$tmp_req->{'json'}{'requirements'}[$i]{'name'}}{'sources'}}); - for (my $x=0; $x 2) { - $conflicts = ', and ' . $tmp_array[scalar(@tmp_array) - 1]; - for (my $x=scalar(@tmp_array)-2; $x>1; $x--) { - $conflicts .= ',' . $tmp_array[$x] . $conflicts; - } - $conflicts = $tmp_array[0] . $conflicts; - } - logger('error', "Discovered a conflict between '$tmp_req->{'filename'}' and $conflicts for requirement '$req'!\n"); - logger('debug', "'" . $tmp_req->{'filename'} . "':\n" . Dumper($tmp_req->{'json'}{'requirements'}[$i]) . "\n"); - logger('debug', $conflicts . ":\n" . Dumper($active_requirements{'array'}[$active_requirements{'hash'}{$tmp_req->{'json'}{'requirements'}[$i]{'name'}}{'array_index'}]) . "\n"); - exit(get_exit_code('requirement_conflict')); - } else { - push(@{$active_requirements{'hash'}{$tmp_req->{'json'}{'requirements'}[$i]{'name'}}{'sources'}}, $tmp_req->{'filename'}); - } - } else { - my $insert_index = push(@{$active_requirements{'array'}}, $tmp_req->{'json'}{'requirements'}[$i]) - 1; - - $active_requirements{'hash'}{$tmp_req->{'json'}{'requirements'}[$i]{'name'}} = { 'sources' => [ - $tmp_req->{'filename'} - ], - 'array_index' => $insert_index }; - } - } - } - - if ($found_req == 0) { - logger('info', "failed\n", 2); - logger('error', "Could not find requirement definition '$req' for userenv '$tmp_req->{'json'}{'userenvs'}[$userenv_idx]{'name'}'!\n"); - exit(get_exit_code('requirement_definition_missing')); - } - } - - # requirements processing end - logger('info', "succeeded\n", 2); -} - -my $files_requirements_present = 0; - -for (my $i=0; $inew->canonical->encode($active_requirements{'array'}[$i])); - - $active_requirements{'array'}[$i]{'sha256'} = $digest; - $active_requirements{'array'}[$i]{'index'} = $i; - push(@checksums, $digest); -} - -logger('debug', "All Requirements Hash:\n"); -logger('debug', Dumper(\@all_requirements)); -logger('debug', "Active Requirements Hash:\n"); -logger('debug', Dumper(\%active_requirements)); - -my $config_json; - -if (exists($args{'config'})) { - logger('info', "Loading config definition from '$args{'config'}'...\n"); - - logger('info', "importing JSON...\n", 1); - (my $rc, $config_json) = get_json_file($args{'config'}, $schema_location); - if ($rc == 0 and defined $config_json) { - logger('info', "succeeded\n", 2); - } else { - logger('info', "failed\n", 2); - if ($rc == 2) { - logger('error', "Schema file '$schema_location' not found!\n"); - exit(get_exit_code('schema_not_found')); - } elsif ($rc == 3) { - logger('error', "Cannot open schema file: '$schema_location'\n"); - exit(get_exit_code('failed_opening_schema')); - } elsif ($rc == 4) { - logger('error', "Schema '$schema_location' is invalid JSON!\n"); - exit(get_exit_code('schema_invalid_json')); - } elsif ($rc == 5) { - logger('error', "Schema validation for userenv '$args{'config'}' using schema '$schema_location' failed!\n"); - exit(get_exit_code('config_failed_validation')); - } elsif ($rc == 6) { - logger('error', "Config '$args{'config'}' is invalid JSON!\n"); - exit(get_exit_code('config_invalid_json')); - } elsif ($rc == 8) { - logger('error', "Config file '$args{'userenv'}' not found\n"); - exit(get_exit_code('config_missing')); - } elsif ($rc == 9) { - logger('error', "Config file '$args{'userenv'} open failed'\n"); - exit(get_exit_code('failed_opening_config')); - } else { - logger('error', "Unknown error'\n"); - exit(get_exit_code('config_unknown_load_error')); - } - } - - logger('info', "calculating sha256...\n", 1); - $config_json->{'sha256'} = sha256_hex(JSON->new->canonical->encode($config_json)); - logger('info', "succeeded\n", 2); - - logger('debug', "Config Hash:\n"); - logger('debug', Dumper($config_json)); - - push(@checksums, $config_json->{'sha256'}); -} - -my $tls_verify = $args{'reg-tls-verify'}; -my $authfile_arg = ""; -if ($userenv_json->{'userenv'}{'origin'}{'requires-pull-token'} eq "true") { - logger('info', "Checking registries JSON for a pull token...\n"); - - my $found_pull_token = 0; - - if (exists($registries_json->{'engines'}{'private'})) { - if ($registries_json->{'engines'}{'private'}{'url'} eq $userenv_json->{'userenv'}{'origin'}{'image'}) { - $found_pull_token = 1; - logger('info', "found " . $registries_json->{'engines'}{'private'}{'tokens'}{'pull'} . " for " . $registries_json->{'engines'}{'private'}{'url'} . " private engines repository\n", 1); - - $authfile_arg = "--authfile=" . $registries_json->{'engines'}{'private'}{'tokens'}{'pull'}; - - if (exists($registries_json->{'engines'}{'private'}{'tls-verify'})) { - $tls_verify = $registries_json->{'engines'}{'private'}{'tls-verify'}; - } - } else { - logger('debug', "does not match " . $registries_json->{'engines'}{'private'}{'url'} . "\n", 1); - } - } - - if (($found_pull_token == 0) && exists($registries_json->{'userenvs'})) { - foreach my $userenv (@{$registries_json->{'userenvs'}}) { - if ($userenv->{'url'} eq $userenv_json->{'userenv'}{'origin'}{'image'}) { - $found_pull_token = 1; - logger('info', "found " . $userenv->{'pull-token'} . " for " . $userenv->{'url'} . "\n", 1); - - $authfile_arg = "--authfile=" . $userenv->{'pull-token'}; - - if (exists($userenv->{'tls-verify'})) { - $tls_verify = $userenv->{'tls-verify'}; - } - } else { - logger('debug', "does not match " . $userenv->{'url'} . "\n", 1); - } - } - } - - if ($found_pull_token == 0) { - logger('info', "not found\n", 1); - logger('error', "Failed to locate a pull token for a userenv that requires one!\n"); - exit(get_exit_code('pull_token_not_found')); - } -} - -if ($args{'dump-config'} eq 'true') { - my %config_dump = (); - - my $include_digest = 0; - if (exists ($args{'force-build-policy'})) { - if ($args{'force-build-policy'} eq 'ifnewer') { - $include_digest = 1; - } elsif ($args{'force-build-policy'} eq 'missing') { - $include_digest = 0; - } - } elsif ($userenv_json->{'userenv'}{'origin'}{'build-policy'} eq 'ifnewer') { - $include_digest = 1; - } elsif ($userenv_json->{'userenv'}{'origin'}{'build-policy'} eq 'missing') { - $include_digest = 0; - } else { - die("I'm confused, how did I get here [include_digest]?") - } - - if ($include_digest == 1) { - # get the userenv digest to include in the config dump -- this - # can be used by callers that are analyzing the dumped config - # to know whether or not the origin image has changed - my $image_id = $userenv_json->{'userenv'}{'origin'}{'image'} . ":" . $userenv_json->{'userenv'}{'origin'}{'tag'}; - my $skopeo_url; - if (($image_id =~ /^dir:/) || ($image_id =~ /^docker:\/\//)) { - $skopeo_url = $image_id; - } else { - $skopeo_url = "docker://" . $image_id; - } - logger('info', "Querying for origin image digest...\n", 1); - ($command, $command_output, $rc) = run_command("skopeo inspect --no-tags " . $authfile_arg . " " . $skopeo_url); - if ($rc == 0) { - logger('info', "succeeded\n", 2); - command_logger('verbose', $command, $rc, $command_output); - - $command_output = filter_output($command_output); - my $skopeo_json = decode_json($command_output); - if (exists ($skopeo_json->{'Digest'})) { - $userenv_json->{'userenv'}{'origin'}{'digest'} = $skopeo_json->{'Digest'}; - } else { - logger('error', "Query results do not contain a digest"); - exit(get_exit_code('skopeo_digest_missing')); - } - } else { - logger('info', "failed\n", 2); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to query " . $skopeo_url); - exit(get_exit_code('skopeo_inspect_failed')); - } - } - - # consolidate information to be dumped - $config_dump{'userenv'} = $userenv_json; - $config_dump{'requirements'} = $active_requirements{'array'}; - $config_dump{'config'} = $config_json; - - # remove internal variables - delete $config_dump{'userenv'}{'sha256'}; - delete $config_dump{'config'}{'sha256'}; - for (my $i=0; $i<@{$config_dump{'requirements'}}; $i++) { - delete $config_dump{'requirements'}[$i]{'index'}; - delete $config_dump{'requirements'}[$i]{'sha256'}; - } - - logger('info', "Config dump:\n"); - logger('info', Data::Dumper->Dump([\%config_dump], [qw(*config_dump)])); - - exit() -} - -if ($args{'dump-files'} eq 'true') { - logger('info', "Files dump:\n"); - - foreach my $req (@{$active_requirements{'array'}}) { - if ($req->{'type'} eq 'files') { - foreach my $file (@{$req->{'files_info'}{'files'}}) { - log_print param_replacement($file->{'src'}, 0) . "\n"; - } - } - } - - exit() -} - -my $container_mount_point; -my $origin_image_id; - -# acquire the userenv from the origin -logger('info', "Attempting to download the latest version of $userenv_json->{'userenv'}{'origin'}{'image'}:$userenv_json->{'userenv'}{'origin'}{'tag'}...\n"); -($command, $command_output, $rc) = run_command("buildah pull --quiet --policy=ifnewer --tls-verify=$tls_verify $authfile_arg $userenv_json->{'userenv'}{'origin'}{'image'}:$userenv_json->{'userenv'}{'origin'}{'tag'}"); -if ($rc == 0) { - logger('info', "succeeded\n", 1); - command_logger('verbose', $command, $rc, $command_output); - - $command_output = filter_output($command_output); - chomp($command_output); - $origin_image_id = $command_output; - - logger('info', "Querying for information about the image...\n"); - ($command, $command_output, $rc) = run_command("buildah images --json " . $origin_image_id); - if ($rc == 0) { - logger('info', "succeeded\n", 1); - command_logger('verbose', $command, $rc, $command_output); - $command_output = filter_output($command_output); - $userenv_json->{'userenv'}{'origin'}{'local_details'} = decode_json($command_output); - } else { - logger('info', "failed\n", 1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to query $userenv_json->{'userenv'}{'origin'}{'image'}:$userenv_json->{'userenv'}{'origin'}{'tag'} ($origin_image_id)!\n"); - exit(get_exit_code('image_query')); - } -} else { - logger('info', "failed\n", 1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to download $userenv_json->{'userenv'}{'origin'}{'image'}:$userenv_json->{'userenv'}{'origin'}{'tag'}!\n"); - exit(get_exit_code('image_origin_pull')); -} - -logger('debug', "Userenv JSON:\n"); -logger('debug', Dumper($userenv_json)); - -my $config_checksum = ""; - -logger('info', "Building image checksum...\n"); - -if ($args{'skip-update'} eq 'false') { - # since we can't really know what happens when we do a distro - # update, add a checksum to the list that cannot be reproduced. - # this will cause an image rebuild to always happen when images - # that have distro updates are being considered. - - logger('info', "obtaining update checksum...\n", 1); - my $digest = sha256_hex($uuid->create_str()); - push(@checksums, $digest); - logger('info', "succeeded\n", 2); - logger('debug', "The sha256 for updating the image is '$digest'\n"); -} - -# add the base image checksum to the list of checksums -my $digest = $userenv_json->{'userenv'}{'origin'}{'local_details'}[0]{'digest'}; -$digest =~ s/^sha256://; -push(@checksums, $digest); - -logger('info', "creating image checksum...\n", 1); -$config_checksum = sha256_hex(join(' ', @checksums)); -logger('info', "succeeded\n", 2); -logger('debug', "The sha256 for the image configuration is '$config_checksum'\n"); - -logger('debug', "Checksum Array:\n"); -logger('debug', Dumper(\@checksums)); - -my $tmp_container = $args{'host'} . "/" . $args{'proj'} . "/" . $args{'label'} . ":" . $args{'tag'}; -my $remove_image = 0; - -# cleanup an existing container image that we are going to replace, if it exists -logger('info', "Checking if container image already exists...\n"); -($command, $command_output, $rc) = run_command("buildah images --json $tmp_container"); -if ($rc == 0) { - logger('info', "found\n", 1); - command_logger('verbose', $command, $rc, $command_output); - - logger('info', "Checking if the existing container image config version is a match...\n"); - logger('info', "getting config version from image...\n", 1); - ($command, $command_output, $rc) = run_command("buildah inspect --type image --format '{{.ImageAnnotations.Workshop_Config_Version}}' $tmp_container"); - if ($rc != 0) { - logger('info', "failed\n", 2); - command_logger('error', $command, $rc, $command_output); - logger('error', "Could not obtain container config version information from container image '$tmp_container'!\n"); - exit(get_exit_code('get_config_version')); - } else { - logger('info', "succeeded\n", 2); - command_logger('verbose', $command, $rc, $command_output); - $command_output = filter_output($command_output); - - chomp($command_output); - - logger('info', "comparing config versions...\n", 1); - if ($command_output eq $config_checksum) { - logger('info', "match found\n", 2); - if ($args{'force'} eq 'false') { - logger('info', "Exiting due to config version match -- the container image is already ready\n"); - logger('info', "To force rebuild of the container image, rerun with '--force true'.\n"); - exit(get_exit_code('success')); - } else { - logger('info', "Force rebuild requested, ignoring config version match\n"); - } - } else { - logger('info', "match not found\n", 2); - } - } - - $remove_image = 1; -} else { - logger('info', "not found\n", 1); - command_logger('verbose', $command, $rc, $command_output); -} - -# make sure there isn't an old container hanging around -logger('info', "Checking for stale container presence...\n"); -($command, $command_output, $rc) = run_command("buildah containers --filter name=$tmp_container --json"); -if ($command_output !~ /null/) { - $command_output = filter_output($command_output); - my $tmp_json = decode_json($command_output); - - my $found = 0; - foreach my $container (@{$tmp_json}) { - if ($container->{'containername'} eq $tmp_container) { - $found = 1; - last; - } - } - - if ($found) { - logger('info', "found\n", 1); - command_logger('verbose', $command, $rc, $command_output); - - # need to clean up an old container - logger('info', "Cleaning up old container...\n"); - ($command, $command_output, $rc) = run_command("buildah rm $tmp_container"); - if ($rc != 0) { - logger('info', "failed\n", 1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Could not clean up old container '$tmp_container'!\n"); - exit(get_exit_code('old_container_cleanup')); - } else { - logger('info', "succeeded\n", 1); - command_logger('verbose', $command, $rc, $command_output); - } - } else { - logger('info', "not found\n", 1); - command_logger('verbose', $command, $rc, $command_output); - } -} else { - logger('info', "not found\n", 1); - command_logger('verbose', $command, $rc, $command_output); -} - -if ($remove_image) { - logger('info', "Removing existing container image that I am about to replace [$tmp_container]...\n"); - ($command, $command_output, $rc) = run_command("buildah rmi $tmp_container"); - if ($rc != 0) { - logger('info', "failed\n", 1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Could not remove existing container image '$tmp_container'!\n"); - exit(get_exit_code('remove_existing_container')); - } else { - logger('info', "succeeded\n", 1); - command_logger('verbose', $command, $rc, $command_output); - } -} - -# create a new container based on the userenv source -logger('info', "Creating temporary container...\n"); -($command, $command_output, $rc) = run_command("buildah from --name $tmp_container $origin_image_id"); -if ($rc != 0) { - logger('info', "failed\n", 1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Could not create new container '$tmp_container' from '$userenv_json->{'origin'}{'origin'}{'image'}:$userenv_json->{'userenv'}{'origin'}{'tag'}' ($origin_image_id)!\n"); - exit(get_exit_code('create_container')); -} else { - logger('info', "succeeded\n", 1); - command_logger('verbose', $command, $rc, $command_output); -} - -# ensure that root is the default user -logger('info', "Setting default user to root for the container...\n"); -($command, $command_output, $rc) = run_command("buildah config --user root $tmp_container"); -if ($rc != 0) { - logger('info', "failed\n", 1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Could not set default user to root for the temporary container '$tmp_container'!\n"); - exit(get_exit_code('set_default_user')); -} else { - logger('info', "succeeded\n", 1); - command_logger('verbose', $command, $rc, $command_output); -} - -set_update_clean_cmds(); -update_container_pkgs($tmp_container); - -logger('info', "Installing Requirements\n"); - -$distro_installs = 0; -$req_counter = 0; -foreach my $req (@{$active_requirements{'array'}}) { - $req_counter += 1; - logger('info', "(" . $req_counter . "/" . scalar(@{$active_requirements{'array'}}) . ") Processing '$req->{'name'}'...\n", 1); - - if ($req->{'type'} eq 'files') { - install_files($req, $tmp_container, 2); - } elsif ($req->{'type'} eq 'distro-manual') { - install_distro_manual($req, $tmp_container, 2); - } elsif ($req->{'type'} eq 'distro') { - install_distro($req, $tmp_container, 2); - } else { - logger('info', "Preparing Environment\n", 2); - - my $container_mount_point = add_chroot($tmp_container, 3); - - # The following requirement types require a chroot in order - # to issue multiple commands and preserve state (like PWD) and to - # capture error codes from individual commands. - - # Capture the current path/pwd and a reference to '/' - my $pwd; - if (opendir(NORMAL_ROOT, "/")) { - ($command, $pwd, $rc) = run_command("pwd"); - chomp($pwd); - } else { - logger('info', "failed\n", 3); - logger('error', "Could not get directory reference to '/'!\n"); - exit(get_exit_code('directory_reference')); - } - - # Jump into the container image - if (not chroot($container_mount_point)) { - logger('info', "failed\n", 3); - logger('error', "Could not chroot to " . $container_mount_point . "!\n"); - exit(get_exit_code('chroot_failed')); - } - - if (not -e "/opt") { - mkdir("/opt") - } - - if (not chdir("/opt")) { - logger('info', "failed\n", 3); - logger('error', "Could not chdir to /opt!\n"); - exit(get_exit_code('chdir_failed')); - } - - # Install the requirment that needs chroot - if ($req->{'type'} eq 'source') { - install_source($req, 2); - } elsif ($req->{'type'} eq 'python3') { - install_python($req, 2); - } elsif ($req->{'type'} eq 'node') { - install_node($req, 2); - } elsif ($req->{'type'} eq 'manual') { - install_manual($req, 2); - } elsif ($req->{'type'} eq 'cpan') { - install_cpan($req, 2); - } - - logger('info', "Cleaning up Environment\n", 2); - - # Break out of the chroot and return to the old path/pwd - if (not chdir(*NORMAL_ROOT)) { - logger('error', "Could not chdir to escape the chroot!\n"); - exit(get_exit_code('chroot_escape_3')); - } - - if (not chroot(".")) { - logger('error', "Could not chroot out of the chroot!\n"); - exit(get_exit_code('chroot_escape_2')); - } - - if (not chdir($pwd)) { - logger('error', "Could not chdir back to the original path/pwd!\n"); - exit(get_exit_code('chroot_escape_1')); - } - - closedir(NORMAL_ROOT); - remove_chroot($tmp_container, $container_mount_point, 3); - } -} # foreach my $req - -if ($distro_installs) { - logger('info', "Cleaning up after performing distro package installations...\n"); - ($command, $command_output, $rc) = run_command("buildah run " . $volume_opt . " --isolation chroot $tmp_container -- $clean_cmd"); - if ($rc != 0) { - logger('info', "failed\n", 1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Cleaning up after distro package installation failed!\n"); - exit(get_exit_code('install_cleanup')); - } else { - logger('info', "succeeded\n", 1); - command_logger('verbose', $command, $rc, $command_output); - } -} - -# add version information to new container image -logger('info', "Adding config version information to the temporary container...\n"); -($command, $command_output, $rc) = run_command("buildah config --annotation Workshop_Config_Version=$config_checksum $tmp_container"); -if ($rc != 0) { - logger('info', "failed\n", 1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to add version information to the temporary container '$tmp_container' using the config checksum '$config_checksum'!\n"); - exit(get_exit_code('config_annotate_fail')); -} else { - logger('info', "succeeded\n", 1); - command_logger('verbose', $command, $rc, $command_output); -} - -if (exists($args{'config'})) { - logger('info', "Adding requested config information to the temporary container...\n"); - - if (exists($config_json->{'config'}{'cmd'})) { - logger('info', "setting cmd...\n", 1); - ($command, $command_output, $rc) = run_command("buildah config --cmd '$config_json->{'config'}{'cmd'}' $tmp_container"); - if ($rc != 0) { - logger('info', "failed\n", 2); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to add requested config cmd to the temporary container '$tmp_container'!\n"); - exit(get_exit_code('config_set_cmd')); - } else { - logger('info', "succeeded\n", 2); - command_logger('verbose', $command, $rc, $command_output); - } - } - - if (exists($config_json->{'config'}{'entrypoint'})) { - logger('info', "setting entrypoint...\n", 1); - my $entrypoint = ""; - foreach my $entrypoint_arg (@{$config_json->{'config'}{'entrypoint'}}) { - $entrypoint .= '"' . $entrypoint_arg . '",'; - } - $entrypoint =~ s/,$//; - ($command, $command_output, $rc) = run_command("buildah config --entrypoint '[$entrypoint]' $tmp_container"); - if ($rc != 0) { - logger('info', "failed\n", 2); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to add requested config entrypoint to the temporary container '$tmp_container'!\n"); - exit(get_exit_code('config_set_entrypoint')); - } else { - logger('info', "succeeded\n", 2); - command_logger('verbose', $command, $rc, $command_output); - } - } - - if (exists($config_json->{'config'}{'author'})) { - logger('info', "setting author...\n", 1); - ($command, $command_output, $rc) = run_command("buildah config --author '$config_json->{'config'}{'author'}' $tmp_container"); - if ($rc != 0) { - logger('info', "failed\n", 2); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to add requested config author to the temporary container '$tmp_container'!\n"); - exit(get_exit_code('config_set_author')); - } else { - logger('info', "succeeded\n", 2); - command_logger('verbose', $command, $rc, $command_output); - } - } - - if (exists($config_json->{'config'}{'annotations'})) { - logger('info', "setting annotation(s)...\n", 1); - - for my $annotation (@{$config_json->{'config'}{'annotations'}}) { - logger('info', "'$annotation'...\n", 2); - ($command, $command_output, $rc) = run_command("buildah config --annotation '$annotation' $tmp_container"); - if ($rc != 0) { - logger('info', "failed\n", 3); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to add requested config annotation to the temporary container '$tmp_container'!\n"); - exit(get_exit_code('config_set_annotation')); - } else { - logger('info', "succeeded\n", 3); - command_logger('verbose', $command, $rc, $command_output); - } - } - } - - if (exists($config_json->{'config'}{'envs'})) { - logger('info', "setting environment variable(s)...\n", 1); - - for my $env (@{$config_json->{'config'}{'envs'}}) { - logger('info', "'$env'...\n", 2); - ($command, $command_output, $rc) = run_command("buildah config --env '$env' $tmp_container"); - if ($rc != 0) { - logger('info', "failed\n", 3); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to add requested config environment variable to the temporary container '$tmp_container'!\n"); - exit(get_exit_code('config_set_env')); - } else { - logger('info', "succeeded\n", 3); - command_logger('verbose', $command, $rc, $command_output); - } - } - } - - if (exists($config_json->{'config'}{'ports'})) { - logger('info', "setting port(s)...\n", 1); - - for my $port (@{$config_json->{'config'}{'ports'}}) { - logger('info', "'$port'...\n", 2); - ($command, $command_output, $rc) = run_command("buildah config --port $port $tmp_container"); - if ($rc != 0) { - logger('info', "failed\n", 3); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to add requested config port to the temporary container '$tmp_container'!\n"); - exit(get_exit_code('config_set_port')); - } else { - logger('info', "succeeded\n", 3); - command_logger('verbose', $command, $rc, $command_output); - } - } - } - - if (exists($config_json->{'config'}{'labels'})) { - logger('info', "setting label(s)...\n", 1); - - for my $label (@{$config_json->{'config'}{'labels'}}) { - logger('info', "'$label'...\n", 2); - ($command, $command_output, $rc) = run_command("buildah config --label $label $tmp_container"); - if ($rc != 0) { - logger('info', "failed\n", 3); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to add requested config label to the temporary container '$tmp_container'!\n"); - exit(get_exit_code('config_set_label')); - } else { - logger('info', "succeeded\n", 3); - command_logger('verbose', $command, $rc, $command_output); - } - } - } -} - -# create the new container image -logger('info', "Creating new container image...\n"); -($command, $command_output, $rc) = run_command("buildah commit --quiet $tmp_container $tmp_container"); -if ($rc != 0) { - logger('info', "failed\n", 1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to create new container image '$tmp_container'!\n"); - exit(get_exit_code('image_create')); -} else { - logger('info', "succeeded\n", 1); - command_logger('verbose', $command, $rc, $command_output); -} - -# clean up the temporary container -logger('info', "Cleaning up the temporary container...\n"); -($command, $command_output, $rc) = run_command("buildah rm $tmp_container"); -if ($rc != 0) { - logger('info', "failed\n", 1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Failed to cleanup temporary container '$tmp_container'!\n"); - exit(get_exit_code('new_container_cleanup')); -} else { - logger('info', "succeeded\n", 1); - command_logger('verbose', $command, $rc, $command_output); -} - -# give the user information about the new container image -logger('info', "Creation of container image '$tmp_container' is complete. Retrieving some details about your new image...\n"); -($command, $command_output, $rc) = run_command("buildah images --json $tmp_container"); -if ($rc == 0) { - logger('info', "succeeded\n", 1); - logger('info', "\n$command_output\n"); - exit(get_exit_code('success')); -} else { - logger('info', "failed\n", 1); - command_logger('error', $command, $rc, $command_output); - logger('error', "Could not get container image information for '$tmp_container'! Something must have gone wrong that I don't understand.\n"); - exit(get_exit_code('image_query')); -}