cmds.secrets.CompileTemplates: Fix conf line handling
All checks were successful
CI / Packaging - Kali Linux (pull_request) Successful in 4m23s
CI / Packaging - OpenSUSE Tumbleweed (pull_request) Successful in 5m46s
CI / Packaging test (pull_request) Successful in 0s
CI / Packaging - Kali Linux (push) Successful in 5m30s
CI / Packaging - OpenSUSE Tumbleweed (push) Successful in 5m35s
CI / Packaging test (push) Successful in 0s

File attributes of the compiled output of "jw-pkg secrets
compile-templates" can be specified via the # conf line atop a .jw-tmpl
file, the parsing of that line is buggy, however.

Its mode is parsed as a decimal number unless it carries a leading zero, so
"mode=640" yields 640 decimal, 1200 octal, and the output file ends up
without usable permissions.  The same decimal parse applies to a conf line atop
the .jw-secret of a target, whose attributes are honored the same way, and
the --mode option of compile-templates passes its string value through to
Attrs, whose mode is typed int, so a target without a conf line of its own
crashes in _format_metadata().

Other than that, the # conf line itself should be removed from the compiled
output, but isn't.

This commit makes the conf line handling correct:

- _parse_attributes() parses the mode as octal, with or without a leading
  zero
- _compile_one_template_file() drops the conf line from the compiled
  content
- the --mode option is converted to an octal int by octal_mode() at the
  argument boundary

The integration test writes its conf line modes without leading zeros,
expects the conf line gone from the compiled output, takes one target's
mode from a conf line atop its .jw-secret, and passes a --mode that the
conf lines override.

Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v1.1.0
Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-10-11 18:23:11 +02:00
commit 141dd0ac3c
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61
4 changed files with 38 additions and 22 deletions

View file

@ -1,12 +1,16 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, override from argparse import ArgumentParser, ArgumentTypeError, Namespace
from typing import override
from .Cmd import Cmd, Parent from .Cmd import Cmd, Parent
from .lib.base import Attrs from .lib.base import Attrs
if TYPE_CHECKING: def octal_mode(s: str) -> int:
from argparse import ArgumentParser, Namespace try:
return int(s, 8)
except ValueError:
raise ArgumentTypeError(f'Not an octal mode: {s}')
class CmdCompileTemplates(Cmd): # export class CmdCompileTemplates(Cmd): # export
@ -30,5 +34,9 @@ class CmdCompileTemplates(Cmd): # export
'--group', '-g', default = None, help = 'Default output file group' '--group', '-g', default = None, help = 'Default output file group'
) )
parser.add_argument( parser.add_argument(
'--mode', '-m', default = None, help = 'Default output file mode' '--mode',
'-m',
default = None,
type = octal_mode,
help = 'Default output file mode (octal)',
) )

View file

@ -76,12 +76,10 @@ class FilesContext:
elif key == 'group': elif key == 'group':
ret.group = val or None ret.group = val or None
elif key == 'mode': elif key == 'mode':
# -- octal, with or without a leading zero
if val: if val:
try: try:
if re.fullmatch(r'0[0-7]+', val): ret.mode = int(val, 8)
ret.mode = int(val, 8)
else:
ret.mode = int(val, 0)
except ValueError: except ValueError:
ret.mode = None ret.mode = None
@ -126,6 +124,11 @@ class FilesContext:
attrs = self._parse_attributes(new_content) attrs = self._parse_attributes(new_content)
if attrs is None: if attrs is None:
attrs = default_attrs attrs = default_attrs
else:
# -- A conf line is the attribute spec of the output
# file, not part of its content
lines = new_content.splitlines(keepends = True)
new_content = ''.join(lines[1:])
if attrs is not None: if attrs is not None:
if attrs.owner is not None: if attrs.owner is not None:

View file

@ -846,7 +846,7 @@ options:
-h, --help show this help message and exit -h, --help show this help message and exit
--owner, -o OWNER Default output file owner (default: None) --owner, -o OWNER Default output file owner (default: None)
--group, -g GROUP Default output file group (default: None) --group, -g GROUP Default output file group (default: None)
--mode, -m MODE Default output file mode (default: None) --mode, -m MODE Default output file mode (octal) (default: None)
============= Running: jw-pkg.py -t ../../../.. --log-level info secrets install --help ============= Running: jw-pkg.py -t ../../../.. --log-level info secrets install --help
usage: jw-pkg.py secrets install [-h] [--only-missing] src [packages ...] usage: jw-pkg.py secrets install [-h] [--only-missing] src [packages ...]

View file

@ -175,7 +175,7 @@ def main() -> None:
os.makedirs(target + '/etc') os.makedirs(target + '/etc')
with open(target + '/etc/foo.conf.jw-tmpl', 'w') as f: with open(target + '/etc/foo.conf.jw-tmpl', 'w') as f:
f.write( f.write(
f'# conf: owner={USER}, group={GROUP}, mode=0640\n' f'# conf: owner={USER}, group={GROUP}, mode=640\n'
'host=HOSTNAME\n' 'host=HOSTNAME\n'
'token=TOKEN\n' 'token=TOKEN\n'
) )
@ -297,8 +297,15 @@ def main() -> None:
assert not os.path.exists(baz_secret) assert not os.path.exists(baz_secret)
assert stat.S_IMODE(os.stat(foo_secret).st_mode) == 0o400 assert stat.S_IMODE(os.stat(foo_secret).st_mode) == 0o400
# -- compile-templates: substitute the keys, apply the metadata # -- The file attributes can also be specified atop the
# .jw-secret; bar takes its mode from there
os.unlink(bar_secret)
with open(bar_secret, 'w') as f:
f.write(f'# conf: owner={USER}, group={GROUP}, mode=640\n')
f.write('PLAIN=plain-value\n')
# -- compile-templates: substitute the keys, apply the
# metadata; the conf lines win over the --mode default
out, _ = run_cli( out, _ = run_cli(
distro, [ distro, [
'secrets', 'secrets',
@ -307,6 +314,8 @@ def main() -> None:
USER, USER,
'--group', '--group',
GROUP, GROUP,
'--mode',
'400',
PKG, PKG,
] ]
) )
@ -314,13 +323,10 @@ def main() -> None:
st = os.stat(foo_out) st = os.stat(foo_out)
assert (st.st_uid, stat.S_IMODE(st.st_mode)) == (os.getuid(), 0o640), st assert (st.st_uid, stat.S_IMODE(st.st_mode)) == (os.getuid(), 0o640), st
with open(foo_out) as f: with open(foo_out) as f:
assert f.read() == ( assert f.read() == ('host=jannet\n'
f'# conf: owner={USER}, group={GROUP}, mode=0640\n' 'token=host-token\n')
'host=jannet\n'
'token=host-token\n'
)
st = os.stat(bar_out) st = os.stat(bar_out)
assert (st.st_uid, stat.S_IMODE(st.st_mode)) == (os.getuid(), 0o400), st assert (st.st_uid, stat.S_IMODE(st.st_mode)) == (os.getuid(), 0o640), st
with open(bar_out) as f: with open(bar_out) as f:
assert f.read() == 'plain=plain-value\n' assert f.read() == 'plain=plain-value\n'
@ -334,16 +340,15 @@ def main() -> None:
USER, USER,
'--group', '--group',
GROUP, GROUP,
'--mode',
'400',
PKG, PKG,
] ]
) )
assert out == [], out assert out == [], out
with open(foo_out) as f: with open(foo_out) as f:
assert f.read() == ( assert f.read() == ('host=jannet\n'
f'# conf: owner={USER}, group={GROUP}, mode=0640\n' 'token=host-token\n')
'host=jannet\n'
'token=host-token\n'
)
with open(bar_out) as f: with open(bar_out) as f:
assert f.read() == 'plain=plain-value\n' assert f.read() == 'plain=plain-value\n'