From 96e2453797e72430db3b6681cd1c5402ce4badd2 Mon Sep 17 00:00:00 2001 From: Michal Fox Date: Fri, 2 Oct 2026 16:23:46 +0200 Subject: [PATCH] fix #7173: config: drop empty tag list when writing When the last tag of a container gets removed, for example in the web UI, the API gets an empty 'tags' value, which then ends up as an empty 'tags:' line in the config and in the output of 'pct config'. As the config parser reads that line back as a single space, it also gets written out again on every following config change. Skip a tag list without any tag when writing the config, which also cleans up existing configs with such a line on their next change. Keep it in the pending section, where it is still needed for removing all tags with a pending change. Add a basic test for parsing and writing container configs, similar to the one in qemu-server, to cover this. Signed-off-by: Michal Fox --- src/PVE/LXC/Config.pm | 6 ++- src/test/Makefile | 5 +- .../parse-config-expected/empty-tags.conf | 8 ++++ src/test/parse-config-input/empty-tags.conf | 9 ++++ src/test/parse-config-input/plain.conf | 11 +++++ src/test/run_parse_config_tests.pl | 47 +++++++++++++++++++ 6 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 src/test/parse-config-expected/empty-tags.conf create mode 100644 src/test/parse-config-input/empty-tags.conf create mode 100644 src/test/parse-config-input/plain.conf create mode 100644 src/test/run_parse_config_tests.pl diff --git a/src/PVE/LXC/Config.pm b/src/PVE/LXC/Config.pm index 3eb1905..420c94a 100644 --- a/src/PVE/LXC/Config.pm +++ b/src/PVE/LXC/Config.pm @@ -1243,7 +1243,7 @@ sub write_pct_config { } my $generate_raw_config = sub { - my ($conf) = @_; + my ($conf, $pending) = @_; my $raw = ''; @@ -1265,6 +1265,8 @@ sub write_pct_config { my $value = $conf->{$key}; die "detected invalid newline or carriage return inside property '$key'\n" if $value =~ m/[\r\n]/; + # an empty tag list is only needed as pending change for removing all tags + next if $key eq 'tags' && !$pending && $value =~ m/^\s*$/; $raw .= "$key: $value\n"; } @@ -1293,7 +1295,7 @@ sub write_pct_config { if (scalar(keys %{ $conf->{pending} })) { $raw .= "\n[pve:pending]\n"; - $raw .= &$generate_raw_config($conf->{pending}); + $raw .= &$generate_raw_config($conf->{pending}, 1); } foreach my $snapname (sort keys %{ $conf->{snapshots} }) { diff --git a/src/test/Makefile b/src/test/Makefile index 91ae6ff..f269a93 100644 --- a/src/test/Makefile +++ b/src/test/Makefile @@ -2,7 +2,7 @@ RUN_USERNS := lxc-usernsexec -m "u:0:`id -u`:1" -m "g:0:`id -g`:1" -- all: test -test: test_setup test_snapshot test_bindmount test_idmap +test: test_setup test_snapshot test_bindmount test_idmap test_parse_config test_setup: run_setup_tests.pl if test -e /run/lock/sbuild; then \ @@ -24,5 +24,8 @@ test_bindmount: bindmount_test.pl test_idmap: run_idmap_tests.pl ./run_idmap_tests.pl +test_parse_config: run_parse_config_tests.pl + ./run_parse_config_tests.pl + clean: rm -rf tmprootfs diff --git a/src/test/parse-config-expected/empty-tags.conf b/src/test/parse-config-expected/empty-tags.conf new file mode 100644 index 0000000..de3cf87 --- /dev/null +++ b/src/test/parse-config-expected/empty-tags.conf @@ -0,0 +1,8 @@ +# empty tag list +arch: amd64 +cores: 1 +hostname: notags +memory: 512 +ostype: debian +rootfs: local-lvm:vm-8006-disk-0,size=8G +unprivileged: 1 diff --git a/src/test/parse-config-input/empty-tags.conf b/src/test/parse-config-input/empty-tags.conf new file mode 100644 index 0000000..417d847 --- /dev/null +++ b/src/test/parse-config-input/empty-tags.conf @@ -0,0 +1,9 @@ +# empty tag list +arch: amd64 +cores: 1 +hostname: notags +memory: 512 +ostype: debian +rootfs: local-lvm:vm-8006-disk-0,size=8G +tags: +unprivileged: 1 diff --git a/src/test/parse-config-input/plain.conf b/src/test/parse-config-input/plain.conf new file mode 100644 index 0000000..e2f9713 --- /dev/null +++ b/src/test/parse-config-input/plain.conf @@ -0,0 +1,11 @@ +# plain container +arch: amd64 +cores: 1 +hostname: plain +memory: 512 +net0: name=eth0,bridge=vmbr0,hwaddr=BC:24:11:2C:69:EC,ip=dhcp,type=veth +ostype: debian +rootfs: local-lvm:vm-8006-disk-0,size=8G +swap: 512 +tags: bar;foo +unprivileged: 1 diff --git a/src/test/run_parse_config_tests.pl b/src/test/run_parse_config_tests.pl new file mode 100644 index 0000000..1640b75 --- /dev/null +++ b/src/test/run_parse_config_tests.pl @@ -0,0 +1,47 @@ +#!/usr/bin/perl + +# Tests parsing and writing container configuration files. +# +# If no expected file exists, the input is assumed to be equal to the expected output. + +use strict; +use warnings; + +use lib qw(..); + +use Test::More; + +use PVE::LXC::Config; +use PVE::Tools; + +my $INPUT_DIR = './parse-config-input'; +my $EXPECTED_DIR = './parse-config-expected'; + +PVE::Tools::dir_glob_foreach( + $INPUT_DIR, + '.*\.conf', + sub { + my ($file) = @_; + + my $fake_config_fn = "$file/lxc/8006.conf"; + my $input = PVE::Tools::file_get_contents("${INPUT_DIR}/${file}"); + + my $output = eval { + my $conf = PVE::LXC::Config::parse_pct_config($fake_config_fn, $input, 1); + PVE::LXC::Config::write_pct_config($fake_config_fn, $conf); + }; + if (my $err = $@) { + note("got unexpected error '$err'"); + fail($file); + return; + } + + my $expected_file = "${EXPECTED_DIR}/${file}"; + my $expected = + -f $expected_file ? PVE::Tools::file_get_contents($expected_file) : $input; + + is($output, $expected, $file); + }, +); + +done_testing();