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 <me@dualfroz.com>
This commit is contained in:
dualfroz committed 2026-10-02 16:23:46 +02:00
1 parent 8275178ff1
commit 96e2453797
6 files changed
+83 -3

No files matched your search

+4 -2
View File
@@ -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} }) {
+4 -1
View File
@@ -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
@@ -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
@@ -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
+11
View File
@@ -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
+47
View File
@@ -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();