Skip to content

Allow variable NAMES to be declared by pattern #9

Description

@mikkoi

Allow variable NAMES to be declared by pattern

Against main at Env::Assert 0.016. Line numbers and quoted code are from
the working copy, not the CPAN release -- the released file is perltidied and the
repository copy is not.

Depends on "Allow a variable to be declared permitted but not required".
A name pattern cannot mean required -- you cannot demand the presence of a
variable whose name you do not know -- so this only makes sense once optional
variables exist. Please do not start this one first.

Summary

.envdesc declares one literal variable name per line. Add the ability to
declare a family of names by regular expression, so that variables which are
injected by the environment rather than chosen by the author can be tolerated
without enumerating them.

Why enumeration is not enough

The companion issue makes it possible to say "LANG may be absent". That
solves the handful of variables you can name.

It does not solve the ones you cannot. In a Kubernetes pod, the container runtime
injects a variable family for every Service in the namespace:

MYDB_SERVICE_HOST=10.43.0.31
MYDB_SERVICE_PORT=5432
MYDB_PORT=tcp://10.43.0.31:5432
MYDB_PORT_5432_TCP_ADDR=10.43.0.31
MYDB_PORT_5432_TCP_PORT=5432
MYDB_PORT_5432_TCP_PROTO=tcp
...

You cannot list these in advance, because the list is a property of the
namespace at pod start, not of your application
. Someone deploying an
unrelated Service alongside yours changes it. With --exact on and only literal
declarations available, every such deployment breaks your startup check, and the
maintenance burden lands on whoever is least equipped to understand it.

One rule should be able to cover the family:

## envassert (var: name_is_pattern=1, optional=1)
^[A-Z0-9_]{1,}_PORT_[0-9]{1,}_TCP(_(ADDR|PORT|PROTO)){0,1}$=^.*$

Secondary benefit, not the motivation

Name patterns are also a way to state a rule across a family you do own:

^APP_FEATURE_[A-Z_]{1,}$=^(0|1)$

"every feature flag must be 0 or 1", without listing them. This works whether or
not --exact is on, and may turn out to be the more commonly used half.

Design decisions

These are the maintainer's to make. I have opinions and no stake; the point of
listing them is that a contributor should not have to guess.

1. Syntax

The current parse takes everything before the first = as the name:

# lib/Env/Assert/Functions.pm:244
^ (?<name> [^=]{1,}) = (?<value> .*) $

so a literal name may currently contain almost anything. Options:

  • a tag comment, consistent with the companion issue:
    ## envassert (var: name_is_pattern=1)
  • a sigil: ~^KUBERNETES_.*$=^.*$
  • delimiters: /^KUBERNETES_.*$/=^.*$

The tag comment is the most conservative: it cannot collide with any existing
file, because a line beginning ## is already a comment. A sigil is terser but
theoretically ambiguous, since [^=]{1,} permits a leading ~. In practice
POSIX environment variable names are [A-Za-z_][A-Za-z0-9_]*, so no real
variable starts with ~ or / -- but "in practice" is doing work in that
sentence, and the tag avoids needing it.

2. Anchoring

Decide and document whether a name pattern is anchored implicitly. The value
regexp is applied unanchored today (m/$regexp/msx) and the shipped examples
anchor themselves (^[[:word:]]{1,}$).

If name patterns follow that convention, then _HOST matches SMTP_HOST and
also SMTP_HOSTNAME_OVERRIDE. That will surprise somebody. Either anchor
implicitly, or say loudly in the POD that you must anchor, and prefer the option
that makes the surprising case impossible rather than merely documented.

3. Precedence -- the one that will cause a bug if it is left implicit

SMTP_HOST can match both its own literal line and a broad ^.*_HOST$ rule.
Which value regexp applies?

A literal wins; among patterns, first in file order wins
B literal wins; two matching patterns is a parse-time error
C every matching rule must be satisfied (conjunctive)

I would choose B. Under A, a permissive family rule can silently loosen a
strict check that was deliberately written, and nothing tells you -- which is the
same class of silent-drift failure that --exact exists to prevent. B refuses to
guess. C is elegant and makes a broad tolerate-rule (^.*$) free, but it means
adding one family rule can retroactively constrain unrelated variables, which is
surprising in the other direction.

Whichever is chosen, the error message should name the rule that matched.
"Variable SMTP_HOST has invalid content" is not enough once more than one rule
could be responsible.

4. A pattern rule cannot be required

Marking one required should be a parse-time error, not silently ignored. It
is a statement that cannot be satisfied, and failing at parse time is the only
place it can be reported usefully.

What changes in the code

Two loops in Env::Assert::Functions::assert (line 133), and they change
differently:

  • The presence loop (line 145) iterates keys %{ $vars } -- declared names.
    Pattern rules have no name to look up and must not take part in it: there
    is nothing whose presence they could require.

  • The exact-mode loop (line 166) currently does a hash lookup:

    # lib/Env/Assert/Functions.pm:168
    if( ! exists $vars->{ $var_name } ) {

    This is the line that changes: a variable is "known" if it matches a literal
    name or any name pattern.

  • The value check needs to apply to variables matched by a pattern rule,
    which today it cannot -- they are not in $vars under their own name.

The natural shape is a helper that answers "which rule governs this environment
variable?"
, used by both the exact-mode loop and the value check, with the
precedence decision from (3) living in that one function.

file_to_desc (line 209) needs to keep pattern rules somewhere separate from the
name-keyed variables hash -- they cannot be keyed by name, since their name is
not a name.

Performance

Not a concern worth designing around: a handful of rules against a few dozen
variables, once, at startup. Compile each pattern once with qr// rather than in
the inner loop and that is the end of it.

Acceptance criteria

  • A name pattern in .envdesc matches multiple environment variables, and each
    matched variable has its value checked against that rule's regexp.
  • With --exact, a variable matching a name pattern is not reported as
    missing from the description.
  • With --exact, a variable matching no literal and no pattern is still
    reported.
  • A pattern rule never causes a "missing from environment" error, whatever the
    environment contains.
  • Marking a pattern rule as required is a parse-time error naming the line.
  • The precedence rule chosen in (3) is implemented, documented in the POD, and
    covered by a test where a literal and a pattern both match.
  • Error messages name the matching rule.
  • Existing .envdesc files behave identically. Nothing without the new marker
    changes.

Tests

prove -l, per CONTRIBUTING.md. exact is already exercised in
t/env-assert-private.t and t/env-assert-public-assert.t.

Cases worth writing:

  • one pattern, three matching variables, all valid -> pass
  • one pattern, one matching variable with a bad value -> error naming that
    variable
  • pattern + --exact, matched variables -> not reported as undeclared
  • pattern + --exact, an unmatched variable -> still reported
  • literal and pattern both matching one variable -> the behaviour decided in (3)
  • pattern marked required -> parse-time error
  • a description file with no patterns -> byte-identical behaviour to today
  • a realistic Kubernetes-shaped environment (a KUBERNETES_* set plus one
    Service family) declared by two rules, under --exact -> pass

The last one is the acceptance test for the whole feature: it is the scenario
that motivates it, and if it does not read clearly in the test file the syntax
probably needs another look.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions