diff --git a/src/python/jw/pkg/cmds/secrets/CmdCompileTemplates.py b/src/python/jw/pkg/cmds/secrets/CmdCompileTemplates.py index 0ce4cfb6..6bd8a4fb 100644 --- a/src/python/jw/pkg/cmds/secrets/CmdCompileTemplates.py +++ b/src/python/jw/pkg/cmds/secrets/CmdCompileTemplates.py @@ -1,12 +1,16 @@ 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 .lib.base import Attrs -if TYPE_CHECKING: - from argparse import ArgumentParser, Namespace +def octal_mode(s: str) -> int: + try: + return int(s, 8) + except ValueError: + raise ArgumentTypeError(f'Not an octal mode: {s}') class CmdCompileTemplates(Cmd): # export @@ -30,5 +34,9 @@ class CmdCompileTemplates(Cmd): # export '--group', '-g', default = None, help = 'Default output file group' ) 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)', ) diff --git a/src/python/jw/pkg/cmds/secrets/lib/FilesContext.py b/src/python/jw/pkg/cmds/secrets/lib/FilesContext.py index 29464597..065934f6 100644 --- a/src/python/jw/pkg/cmds/secrets/lib/FilesContext.py +++ b/src/python/jw/pkg/cmds/secrets/lib/FilesContext.py @@ -76,12 +76,10 @@ class FilesContext: elif key == 'group': ret.group = val or None elif key == 'mode': + # -- octal, with or without a leading zero if val: try: - if re.fullmatch(r'0[0-7]+', val): - ret.mode = int(val, 8) - else: - ret.mode = int(val, 0) + ret.mode = int(val, 8) except ValueError: ret.mode = None @@ -126,6 +124,11 @@ class FilesContext: attrs = self._parse_attributes(new_content) if attrs is None: 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.owner is not None: diff --git a/test/integration/jw-pkg/help/test-expected.txt b/test/integration/jw-pkg/help/test-expected.txt index 9dd91555..b0363da8 100644 --- a/test/integration/jw-pkg/help/test-expected.txt +++ b/test/integration/jw-pkg/help/test-expected.txt @@ -846,7 +846,7 @@ options: -h, --help show this help message and exit --owner, -o OWNER Default output file owner (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 usage: jw-pkg.py secrets install [-h] [--only-missing] src [packages ...] diff --git a/test/integration/jw-pkg/secrets/runme.py b/test/integration/jw-pkg/secrets/runme.py index 777498d1..5d82f88d 100644 --- a/test/integration/jw-pkg/secrets/runme.py +++ b/test/integration/jw-pkg/secrets/runme.py @@ -175,7 +175,7 @@ def main() -> None: os.makedirs(target + '/etc') with open(target + '/etc/foo.conf.jw-tmpl', 'w') as f: f.write( - f'# conf: owner={USER}, group={GROUP}, mode=0640\n' + f'# conf: owner={USER}, group={GROUP}, mode=640\n' 'host=HOSTNAME\n' 'token=TOKEN\n' ) @@ -297,8 +297,15 @@ def main() -> None: assert not os.path.exists(baz_secret) 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( distro, [ 'secrets', @@ -307,6 +314,8 @@ def main() -> None: USER, '--group', GROUP, + '--mode', + '400', PKG, ] ) @@ -314,13 +323,10 @@ def main() -> None: st = os.stat(foo_out) assert (st.st_uid, stat.S_IMODE(st.st_mode)) == (os.getuid(), 0o640), st with open(foo_out) as f: - assert f.read() == ( - f'# conf: owner={USER}, group={GROUP}, mode=0640\n' - 'host=jannet\n' - 'token=host-token\n' - ) + assert f.read() == ('host=jannet\n' + 'token=host-token\n') 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: assert f.read() == 'plain=plain-value\n' @@ -334,16 +340,15 @@ def main() -> None: USER, '--group', GROUP, + '--mode', + '400', PKG, ] ) assert out == [], out with open(foo_out) as f: - assert f.read() == ( - f'# conf: owner={USER}, group={GROUP}, mode=0640\n' - 'host=jannet\n' - 'token=host-token\n' - ) + assert f.read() == ('host=jannet\n' + 'token=host-token\n') with open(bar_out) as f: assert f.read() == 'plain=plain-value\n'