From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 770C61FF0C0 for ; Wed, 04 Nov 2026 20:07:09 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 2CD112186E; Wed, 04 Nov 2026 20:05:25 +0100 (CET) From: =?UTF-8?q?Michael=20K=C3=B6ppl?= To: pve-devel@lists.proxmox.com Subject: [PATCH e2e-tests 19/38] tests: container: verify application containers from OCI images Date: Wed, 4 Nov 2026 20:03:26 +0100 Message-ID: <20261104190345.2573464-20-m.koeppl@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261104190345.2573464-1-m.koeppl@proxmox.com> References: <20261104190345.2573464-1-m.koeppl@proxmox.com> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793819035072 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.223 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 2HBOQW6BYTF43WT4GUYOLQ7P5UA4B7MN X-Message-ID-Hash: 2HBOQW6BYTF43WT4GUYOLQ7P5UA4B7MN X-MailFrom: m.koeppl@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Create a container from the OCI image built during setup and check that the image's entrypoint, environment, user, working directory and stop signal end up in the config, and that the host manages the network. Then check that init runs the entrypoint with the image's user, working directory and environment, including a variable added through the 'env' property, and that the image's volume path exists. Since 'pct exec' does not get variables that are meant for init only, the test reads them from /proc/1/environ. Finally, shut the container down. The image's init only exits on the image's stop signal, so the shutdown fails if that signal is not taken over from the OCI image. Signed-off-by: Michael Köppl --- tests/pve_ct_oci.pl | 190 ++++++++++++++++++++++++++++++++++++++++++++ tests/testcases.ron | 9 +++ 2 files changed, 199 insertions(+) create mode 100755 tests/pve_ct_oci.pl diff --git a/tests/pve_ct_oci.pl b/tests/pve_ct_oci.pl new file mode 100755 index 0000000..b987a8a --- /dev/null +++ b/tests/pve_ct_oci.pl @@ -0,0 +1,190 @@ +#!/usr/bin/perl + +=head1 NAME + +C - Test an application container created from an OCI image. + +=head1 DESCRIPTION + +This testcase creates an unprivileged container from the minimal OCI image that +the setup plan builds and puts into pve1's local storage. It verifies that the +image's entrypoint and command, environment, user, working directory and stop +signal are taken over into the container config and that the network is managed +by the host, since the image has no network management of its own. + +It then adds an environment variable through the API and starts the container. +It verifies that the image's entrypoint runs as init with the image's user, +working directory and environment, including the added variable, that the +image's volume path exists, and that the container is reachable from the node +at its host-configured address. Finally, it shuts the container down. The +image's init only exits on the image's stop signal. + +=cut + +use strict; +use warnings; + +use Test::More; + +use lib '.'; +use Proxmox::Test::Helper qw(get_pve_instance); +use Proxmox::Test::Testcase; +use Proxmox::Test::Util qw(poll_until); + +my $pve = get_pve_instance('pve1'); +my $node = $pve->get_nodename(); +my $client = $pve->client(); + +my $ct_ip = '10.250.252.10'; +my $node_ip = '10.250.252.1'; + +my $test_container = { + vmid => $client->get('/cluster/nextid'), + node => $node, + ostemplate => 'local:vztmpl/e2e-oci.tar', + rootfs => 'local-lvm:1', + storage => 'local-lvm', + memory => 256, + unprivileged => 1, + hostname => 'ct-oci', + net0 => "name=eth0,bridge=vmbr0,ip=$ct_ip/24", +}; + +my $vmid = $test_container->{vmid}; +my $env_marker_key = 'MARKER_ENV'; +my $env_marker_value = 'test'; + +Proxmox::Test::Testcase->new('pve_ct_oci')->with([{}])->step( + 'create container from OCI image', + sub { + my $upid = $client->post("/nodes/$node/lxc", $test_container); + $pve->task_ok($upid, "created container $vmid from OCI image") + or die "creating container $vmid failed\n"; + }, + sub { + $pve->destroy_guest('lxc', $vmid); + }, +)->step( + 'verify config taken from the image', + sub { + my $conf = $client->get("/nodes/$node/lxc/$vmid/config", {}); + + is( + $conf->{entrypoint}, + '/entrypoint.sh e2e-cmd', + 'entrypoint and command are taken from the image', + ); + is($conf->{cmode}, 'console', 'console mode is used for the custom entrypoint'); + # This is set by the OCI image. + is($conf->{env}, "PATH=/bin\0E2E_IMAGE_ENV=from-image", 'image environment is set'); + like($conf->{net0} // '', qr/\bhost-managed=1\b/, 'network is managed by the host'); + + my %raw = map { $_->[0] => $_->[1] } ($conf->{lxc} // [])->@*; + is($raw{'lxc.init.uid'}, 1000, 'init user is resolved from the image user database'); + is($raw{'lxc.init.gid'}, 1000, 'init group is resolved from the image group database'); + is($raw{'lxc.init.cwd'}, '/home/e2e', 'working directory is taken from the image'); + is($raw{'lxc.signal.halt'}, 'SIGUSR1', 'stop signal is taken from the image'); + }, +)->step( + 'add environment variable', + sub { + my $env = $client->get("/nodes/$node/lxc/$vmid/config", {})->{env}; + $client->put( + "/nodes/$node/lxc/$vmid/config", + { env => "$env\0$env_marker_key=$env_marker_value" }, + ); + + my $conf = $client->get("/nodes/$node/lxc/$vmid/config", {}); + like( + $conf->{env} // '', + qr/\0$env_marker_key=$env_marker_value$/, + 'config has the added variable', + ); + }, +)->step( + 'add node address on vmbr0', + sub { + $pve->run_command("ip addr add $node_ip/24 dev vmbr0"); + }, + sub { + $pve->run_command("ip addr del $node_ip/24 dev vmbr0 2>/dev/null; true"); + }, +)->step( + 'start container', + sub { + $pve->start_guest('lxc', $vmid); + }, +)->step( + 'verify the image entrypoint runs as init', + sub { + is( + $pve->ct_exec($vmid, 'cat /proc/1/cmdline'), + "/bin/sh\0/entrypoint.sh\0e2e-cmd\0", + 'init runs the entrypoint with the command as argument', + ); + + my $status = $pve->ct_exec($vmid, 'cat /proc/1/status'); + like($status, qr/^Uid:\s+1000\s+1000\s+1000\s+1000$/m, 'init runs as the image user'); + like($status, qr/^Gid:\s+1000\s+1000\s+1000\s+1000$/m, + 'init runs with the image group'); + + is( + $pve->ct_exec($vmid, 'readlink /proc/1/cwd'), + "/home/e2e\n", + 'init runs in the working directory', + ); + }, +)->step( + 'verify the environment of init', + sub { + my %env = + map { split(/=/, $_, 2) } split(/\0/, $pve->ct_exec($vmid, 'cat /proc/1/environ')); + + is($env{E2E_IMAGE_ENV}, 'from-image', 'init has the image environment'); + is($env{$env_marker_key}, $env_marker_value, 'init has the added variable'); + }, +)->step( + 'verify the image volume path exists', + sub { + ok( + eval { $pve->ct_exec($vmid, 'test -d /data'); 1 }, + 'directory for the image volume exists', + ); + }, +)->step( + 'verify host-managed network', + sub { + poll_until( + sub { + $pve->ct_exec($vmid, 'ip -4 addr show dev eth0') =~ m/\binet \Q$ct_ip\E\/24\b/; + }, + timeout => 30, + interval => 2, + description => "eth0 in container $vmid to get $ct_ip", + ); + pass("host configured $ct_ip on eth0 in container $vmid"); + + poll_until( + sub { $pve->node_can_ping($ct_ip) }, + timeout => 30, + interval => 3, + description => "container $vmid to be pingable from the node", + ); + ok($pve->node_can_ping($ct_ip), "node can ping container $vmid"); + }, +)->step( + 'shutdown container', + sub { + my $upid = $client->post("/nodes/$node/lxc/$vmid/status/shutdown", { timeout => 30 }); + $pve->task_ok($upid, "shut down container $vmid") + or die "shutting down container $vmid failed\n"; + + is( + $client->get("/nodes/$node/lxc/$vmid/status/current", {})->{status}, + 'stopped', + "container $vmid reports status 'stopped' after shutdown", + ); + }, +)->run(); + +done_testing(); diff --git a/tests/testcases.ron b/tests/testcases.ron index 1d5b7ad..afbaa5b 100644 --- a/tests/testcases.ron +++ b/tests/testcases.ron @@ -178,6 +178,15 @@ TestcaseConfig( ], tag: "extended", ), + Testcase( + name: "pve_ct_oci", + run: "./tests/pve_ct_oci.pl", + timeout: 600, + instances: [ + "pve1", + ], + tag: "extended", + ), Testcase( name: "pve_ha_resource_config", run: "./tests/pve_ha_resource_config.pl", -- 2.47.3