hyera
hyera resolves Puppet Hiera
data the way Puppet 8's own lookup does: pip install hyera, import hyera,
and run the hyera command. It reads a Hiera base config, walks the
hierarchy for a given context, and fully resolves values -- %{...}
interpolation, the hiera/lookup/scope/literal/alias functions, and
array/hash/deep-hash merging included.
The reference is Puppet 8.10.0 as measured: for the same hiera.yaml, data
and scope, a lookup finds, misses or fails where Puppet's does and returns
the same value. Error message text, the command's output text and its exit
statuses are hyera's own; where hyera deliberately differs, the list under
Differences from Puppet says how. The
documentation site holds the API
reference and the changelog.
Features
- Hiera 5 configuration -- full
hiera.yamlversion 5 schema validation: a config Puppet rejects fails here too. - Hiera 1-4 configs, too -- a versionless or
version: 3hiera.yaml(Hiera 1, 2 and 3's own dialect) andversion: 4module/environment configs are read the way Puppet reads them. - Global, environment and module layers --
environmentpath/basemodulepath/modulepathreproduce Puppet's own layer stack, including a module'sdefault_hierarchy. - Every lookup form --
lookup/__call__/h[...]/in, plusdig/get/getvarnavigation andexplain(). - Puppet's merge strategies --
first,unique,hashanddeep(withknockout_prefix,sort_merged_arrays,merge_hash_arrays), driven by an explicitmerge=or by data-declaredlookup_options. convert_to-- Puppet'snew()type conversion, including a redactingSensitivewrapper.- YAML, JSON and HOCON backends, plus
eyaml_lookup_key(PKCS7) and asops_databackend Puppet itself does not have. - A CLI that takes
puppet lookup's flags and renders the value ass,jsonoryaml, runnable ashyera,python -m hyera, or as an MCP tool (HYERA_MCP=stdio). Its exit statuses are its own:1for a miss,2for an error. - Bounded, revalidating caches -- lookups reuse resolved locations and parsed data across calls, and pick up changed files without restarting.
- A typed exception hierarchy -- every failure derives from
HieraError, so callers can catch precisely.
Installation
pip install hyera
| Extra | Install | Adds | Needed for |
|---|---|---|---|
cli |
pip install "hyera[cli]" |
duho |
the hyera command / python -m hyera |
hocon |
pip install "hyera[hocon]" |
pyhocon |
hocon_data hierarchy levels |
eyaml |
pip install "hyera[eyaml]" |
cryptography |
eyaml_lookup_key (PKCS7) levels |
The sops_data/sops/sops_<format> backend needs the external
sops binary on PATH, not a Python
extra.
Quick start
version: 5
defaults:
datadir: data
data_hash: yaml_data
hierarchy:
- name: "Per node"
path: "nodes/%{trusted.certname}.yaml"
- name: "Per OS family"
path: "os/%{facts.os.family}.yaml"
- name: "Per role"
path: "roles/%{role}.yaml"
- name: "Common"
path: "common.yaml"
This is examples/hiera.yaml;
its data lives under examples/data/ and a matching fact file sits at
examples/facts.yaml. From the repo root:
>>> from hyera import Hiera, Scope, load_facts
>>> h = Hiera("examples/hiera.yaml", scope=Scope(facts=load_facts("examples/facts.yaml")))
>>> h.lookup("ntp::servers")
['ntp.web.example.com', '0.pool.ntp.org', '1.pool.ntp.org']
>>> h["nginx::workers"]
8
>>> h.dig("users", "alice", "uid")
1001
>>> h.lookup("users")
{'alice': {'shell': '/bin/bash', 'uid': 1001}}
>>> "motd" in h
True
>>> h.keys()
['nginx::workers', 'users', 'packages::manager', 'ntp::servers', 'motd']
>>> h.to_dict()["nginx::workers"]
8
>>> from hyera import lookup
>>> lookup("examples/hiera.yaml", "nginx::workers", facts=load_facts("examples/facts.yaml"))
8
With the cli extra installed:
$ hyera --hiera_config examples/hiera.yaml --facts examples/facts.yaml --node web01.example.com --render-as json ntp::servers
["ntp.web.example.com","0.pool.ntp.org","1.pool.ntp.org"]
puppet lookup takes the same flags, as
examples/README.md
shows.
Command line
Install the cli extra to get the command: pip install "hyera[cli]"
(without it, hyera/python -m hyera print that hint and exit 2).
hyera takes puppet lookup's flags, apart from those listed as not
supported under Hiera coverage:
hyera [options] KEY [KEY ...]
hyera --hiera_config examples/hiera.yaml --facts examples/facts.yaml --node web01.example.com ntp::servers
hyera --hiera_config examples/hiera.yaml --facts examples/facts.yaml --merge deep --knock-out-prefix=-- --render-as json users
hyera --hiera_config examples/hiera.yaml --facts examples/facts.yaml --explain ntp::servers
python -m hyera --hiera_config examples/hiera.yaml --facts examples/facts.yaml ntp::servers
Options, grouped:
- lookup: one or more
KEYs (the first one found wins);--merge first|unique|hash|deep;--knock-out-prefix,--sort-merged-arraysand--merge-hash-arrays(only with--merge deep);--type(asserts the found value and--defaultagainst a Puppet type expression);--default;--explain/--explain-options. - facts and scope:
--facts FILE(.json/.yaml/.yml, or any other name tried as JSON then YAML);--node NAME(used in messages only, sets no fact);--scope NAME=VALUE/-s(repeatable; VALUE is YAML, a dotted NAME builds a hash -- hyera's one flag with nopuppet lookupcounterpart). - settings:
--hiera_config PATH(default./hiera.yamlif present, else Puppet's built-in default configuration);--environment NAME;--environmentpath/--modulepath/--basemodulepath(each a list of paths separated by the OS path separator);--codedir;--strict off|warning|error(defaultwarning). - output:
--render-as s|json|yaml(defaultyaml, orswhile explaining). - logging:
-v/--verbose(repeatable; adds info, then debug),-d/--debug(debug, same as-vv),-q/--quiet(repeatable; drops to error, then critical),--loglevel [NAME:]LEVEL.
Without --merge, the data's lookup_options decides; an explicit
--merge, first included, overrides it. See
Errors and exit codes below for what each exit
status means and what this CLI does not (yet) support.
HYERA_MCP=stdio hyera runs the same command as an MCP server over
stdin/stdout, so an MCP client can drive lookups: it exposes one tool,
hyera, whose arguments are the command-line fields (keys, hiera_config,
facts, scope, merge, ...) and whose result is what the command would
print. An option value of exactly -- (the knock-out prefix --) is passed
through as the value, as on the command line. A call that finds nothing or
fails comes back as an error result whose last line gives the exit status
and its meaning, exit code: 1 (No value found for the key).
An MCP caller that controls the tool's arguments can do what a user at the
command line can: read any file the process can read as a facts file or a
hiera.yaml (an error message can name the keys of a YAML or JSON mapping
it finds there, and shows whether a path exists), and read any hiera data on
disk. A hierarchy level that names sops_data runs the sops binary.
Expose the tool only to a caller you would trust with that access, or bound
it with two environment variables read once when the server starts (ignored
without HYERA_MCP): HYERA_MCP_ROOT=/srv/hiera requires every path
argument (facts, hiera_config, environmentpath, modulepath,
basemodulepath, codedir) to resolve, links resolved, inside that
directory, and turns on confine_locations for every call;
HYERA_MCP_BACKENDS=yaml_data,json_data lets a hierarchy name only those
functions. A refused call exits 2 with one line naming the argument; a
value that is not usable stops the server at start. Neither is a sandbox for
what a backend you allow does. The plain command has no such flags.
API overview
| Module | Purpose | Reference |
|---|---|---|
hyera |
hyera: a Python implementation of Puppet Hiera data lookup. | https://jose-pr.github.io/hyera/api/hyera/ |
hyera.types |
Public Puppet type objects (Integer, Optional, Struct, ...). |
https://jose-pr.github.io/hyera/api/types/ |
hyera.backends |
Data backends: a self-registering Backend registry. |
https://jose-pr.github.io/hyera/api/backends/ |
hyera.testing |
Helpers for testing a backend: run its hooks the way the engine does. | https://jose-pr.github.io/hyera/api/testing/ |
hyera.cli |
Command-line interface for hyera, built on duho. | https://jose-pr.github.io/hyera/api/cli/ |
hyera (command) / python -m hyera |
Runs a lookup from the command line, taking puppet lookup's flags. |
#command-line |
Guide
Configuration
A standard Hiera 5 hiera.yaml works. Each level names a data_hash,
lookup_key or data_dig backend and a source (path, paths, glob,
globs, uri, uris, or mapped_paths):
---
version: 5
defaults:
data_hash: yaml_data
datadir: data
hierarchy:
- name: "Per-node"
path: "nodes/%{trusted.certname}.yaml"
- name: "Per-environment"
path: "environments/%{environment}.yaml"
- name: "Per-role"
mapped_paths: [roles, role, "roles/%{role}.yaml"]
- name: "Modules"
globs:
- "modules/*.yaml"
- name: "Common"
path: "common.yaml"
See Backends below for the registered data_hash/lookup_key
names and the one non-Puppet backend.
Hiera 3 and 4 configs
A hiera.yaml without a version key, or with version: 3 (Hiera 1, 2 and 3
are the same dialect to Puppet), is read and validated against Puppet's own
version 3 schema, then resolved with Puppet's backend-major provider order:
one data source per listed backends name, over the whole hierarchy, in
list order.
---
:backends:
- yaml
:yaml:
:datadir: data
:extension: yaml
:hierarchy:
- "nodes/%{::trusted.certname}"
- common
yaml/json/hocon/eyaml map onto the same YAMLBackend/JSONBackend/
HOCONBackend/EyamlBackend a v5 data_hash: yaml_data etc. would use;
any other name must be a third-party hyera.Backend registered under that
name in the "v3" namespace (NAMES = {"v3": (...)}), or it raises
ConfigError (see Differences from Puppet --
Puppet, with real Hiera 3 installed, would instead skip that backend
silently). merge_behavior/deep_merge_options/logger are validated but
never applied, matching Puppet: only an explicit merge=/--merge changes
how results combine. A relative datadir (including the default,
<codedir>/environments/%{::environment}/hieradata) resolves against the
process's working directory at construction, never the hiera.yaml
directory -- Hiera(..., codedir=...)/hyera --codedir set $codedir
(Puppet's own AIO default per platform otherwise).
hiera.yaml version 4 (backend: yaml|json|hocon instead of data_hash:,
path/paths defaulting to the entry's own name) is accepted in the
environment and module layers only -- Puppet rejects it at the global
layer, after validating its schema (a schema-invalid version 4 file at the
global layer raises its schema error, never the layer one). Its datadir
is joined onto the config root exactly as written, with no interpolation at
all -- unlike every other version. A version 3 (or missing-version)
hiera.yaml at an environment or module root is likewise still fully
schema-validated, then ignored with a warning (or, under
Scope(strict="error"), raised) rather than read -- see
Layers below. hiera3_backend follows the same backend-name
rule as a v3 backends: entry, and is accepted only in the global layer.
A %{lookup()}/%{hiera()}/%{alias()} reached while interpolating a
version 3 global layer's own data stays confined to the global layer --
never reaching an environment or module, even for an otherwise-qualified
key -- unless the current environment has a real version 5 hiera.yaml (an
absent, ignored-version-3, or version 4 environment all count as none).
Layers
hiera.yaml above is the global layer. Puppet also reads an
environment layer and, for a module::key-shaped lookup, a module
layer -- pass environmentpath/basemodulepath/modulepath to
Hiera(...) to enable them:
.
├── hiera.yaml # global
├── data/common.yaml
└── environments/
└── production/
├── hiera.yaml # environment (scope.environment)
├── data/common.yaml
└── modules/
└── mymod/
├── hiera.yaml # module (mymod::* keys only)
└── data/common.yaml
h = Hiera(
"hiera.yaml",
environmentpath="./environments",
basemodulepath="./modules",
)
h.lookup("mymod::setting") # global, then environment, then mymod's own hiera.yaml
The lookup order, at every level, is global then environment then module --
a merge (merge="unique", merge="deep", ...) spans all three. A key not
qualified <module>::... never reaches the module layer at all, and a
module's own data that is not qualified with that module's name is dropped
(with a warning) rather than leaking into another module's namespace.
hiera3_backend is accepted only in the global layer's hiera.yaml. A
version-3 (or missing-version) hiera.yaml at an environment or module
root is silently ignored (with a warning); puppet lookup's own
strict=error raises instead. A version 4 hiera.yaml at an environment or
module root is read normally (it is only the global layer that rejects
it). See the
shipped API header's
"Layers" entry for the full discovery and error rules.
A module's own hiera.yaml may also declare a default_hierarchy
(default_hierarchy is rejected everywhere else -- global or environment
-- with ConfigError):
# modules/mymod/hiera.yaml
version: 5
hierarchy:
- name: "Common"
path: "common.yaml"
default_hierarchy:
- name: "Module defaults"
path: "module_defaults.yaml"
It is consulted only for that module's own mymod::* keys, and only after
every layer (global, environment, the module's own main hierarchy) misses.
The caller's merge= does not apply there -- the merge comes from the
default hierarchy's own lookup_options instead -- while the main
hierarchy's convert_to still applies to whatever value it returns.
Lookups
from hyera import Hiera, Scope
h = Hiera("hiera.yaml", scope=Scope(facts={"os": {"family": "Debian"}}, environment="production"))
# First match wins:
h.lookup("ntp::servers")
# Merge across the whole hierarchy:
h.lookup("classes", merge="unique") # flatten + dedupe arrays
h.lookup("users", merge="deep") # deep hash merge
# Missing keys raise KeyNotFoundError (also a KeyError) unless a default is given:
h.lookup("missing", default_value="fallback")
"some::key" in h
# A Hiera is callable, and h[...] takes lookup()'s own arguments:
h("ntp::servers")
h["classes", None, "unique"]
# Bind a derived scope once and reuse -- a view, sharing config and caches:
prod = h.scoped(environment="production")
prod["ntp::servers"]
Coming from hiera()/hiera_array()/hiera_hash() -- Puppet's legacy
functions always force a merge, ignoring lookup_options; merge="first"
below is that forcing, not merely "the default":
| Puppet | hyera |
|---|---|
hiera('key') |
h.lookup('key', None, 'first') |
hiera('key', 'default') |
h.lookup('key', None, 'first', 'default') |
hiera_array('key') |
h.lookup('key', None, 'unique') |
hiera_hash('key') |
h.lookup('key', None, 'hash') |
hiera_include('key') |
not supported (applies classes to a catalog) |
One-shot lookup
import hyera
hyera.lookup("hiera.yaml", "ntp::servers", facts={"os": {"family": "Debian"}})
hyera.lookup(base_config, name, ..., facts=None, scope=None) builds a
Hiera and returns its lookup; every other argument is Hiera.lookup's
own. It builds a new instance on every call and caches nothing between
calls, so a loop uses the class. facts and scope are exclusive, and no
other Hiera option (layers, backends, cache control) is reachable from it.
Listing keys
h.keys() # every top-level key the data_hash levels hold, in precedence order
h.to_dict() # {key: h.lookup((key,)) for key in h.keys()}
h.to_dict(merge="deep") # one merge strategy for every key
keys() lists what the scope's data_hash levels hold: the global
hierarchy, the environment's, each module's own (only keys in its
namespace), then each module's default_hierarchy. A key appears once;
lookup_options is never listed. A lookup_key or data_dig level cannot
be listed and adds nothing. to_dict() runs one exact-key lookup per key, so
each value is interpolated and converted as lookup returns it, and a key
whose lookup misses is left out.
Navigating values
dig/get/getvar are Puppet's own navigation functions, each ported
onto Hiera:
h.dig("db", "credentials", "user") # None if any step is missing
h.get("db.credentials.user", default_value="admin") # a dotted navigation string
h.getvar("facts.os.family") # reads the bound scope, not the data
dig looks up its first argument, then walks the rest of them into the
result (a list index or a dict key at a time), returning None the
moment a step is missing rather than raising. get takes the same idea as
one dotted string and a default_value, plus an optional block that
receives a walk error (a non-collection or a non-integer list index)
instead of raising. getvar runs get's own navigation over a scope
variable instead of a looked-up key.
Explaining a lookup
print(h.explain("ntp::servers").text())
explain takes exactly lookup's own arguments and returns a
hyera.ExplainResult: .text() is the indented report puppet lookup --explain prints (every hierarchy entry and path consulted, merges and
their results, interpolations, the lookup_options search); .to_hash()
is the same tree, keyed the way --render-as json --explain renders it.
explain_options=True reports only how lookup_options was assembled.
Type-checked lookups
value_type (on lookup/dig/get/explain/()/[]) takes a Puppet
type-expression string, or the equivalent object from hyera.types -- one
isinstance-aware class per Puppet type, never a builtin subclass:
from hyera import types
h.lookup("ntp::servers", types.Array[types.String]) # same as "Array[String]"
h.lookup("retries", types.Integer[1, 10]) # same as "Integer[1, 10]"
isinstance(5, types.Integer) # True
isinstance(5, types.Integer[1, 10]) # True
isinstance(11, types.Integer[1, 10]) # False
types.Integer("42") # 42 (an int) -- Puppet's new(), same as convert_to
types.Array("ab") # ["a", "b"]
A bare class (types.Integer) is the unparameterized type; subscripting
(types.Integer[1, 10]) builds a parameterized one, equal to parsing the
same Puppet text; calling either is Puppet's new(). hyera.types is not
re-exported from top-level hyera except Sensitive, already public there
as the redacting wrapper (types.Sensitive is the same object).
Merging and lookup_options
Pass merge= to lookup() -- one of Puppet's strategy names, a hyera.Merge
member (Merge.DEEP is the same value as "deep", so either spelling works
everywhere merge= is accepted), or a hash of deep options:
h.lookup("classes", merge="unique") # flatten + dedupe arrays
h.lookup("app::name", merge="default") # explicit first-match
h.lookup("conf", merge="deep") # recursive hash merge
h.lookup("conf", merge={"strategy": "deep", # deep-merge options
"knockout_prefix": "--",
"sort_merged_arrays": True,
"merge_hash_arrays": True})
More idiomatically, declare the strategy (and optional convert_to) in the
data under the reserved lookup_options key -- then callers need not pass
merge= at all:
# common.yaml
classes:
- base
lookup_options:
classes: { merge: unique }
"^app::.*": { merge: { strategy: deep } } # regex: must start with ^
port: { convert_to: Integer }
db::password: { convert_to: Sensitive }
A lookup_options key is treated as a regular expression only when it
starts with ^ (Hiera 5's rule); every other key is matched literally, so
a key containing . or other metacharacters cannot shadow unrelated keys.
Patterns use Ruby regex syntax ((?<name>…), \A, \z, \h/\H, a
lookbehind) and match by searching from the start of the key, so ^app::
matches app::ports; they are tried in the merged order, lower-priority
levels' patterns first. An exact key match always wins over a pattern
match. An invalid pattern, or a lookup_options value that is not a hash,
raises HieraLookupError for the whole lookup. An entry that is a string
applies no options and stops the search (a matching pattern for the same
key is never tried); any other non-hash, non-string entry raises.
With layers configured, lookup_options from the global, environment and
module data all apply to the same key -- global wins over environment,
which wins over module -- and a module's own keys/patterns must start with
<module>::.
An explicit merge= argument overrides only the merge lookup_options
would have picked; convert_to always applies. convert_to takes
a Puppet type string (Integer, Optional[Integer]) or [Type, *args]
([Integer, 16], [String, '%x']) and converts with Puppet's new():
Integer, Float, Numeric, String, Boolean, Array, Hash, Tuple, Struct,
Optional, NotUndef and Sensitive (a redacting hyera.Sensitive wrapper). An
invalid type or a failed conversion raises hyera.HieraLookupError. A
String format is one directive (%d, %5.2f, %x, %p, ...) with
Puppet's per-type rules and its own "Illegal format" errors; a format that is
not exactly one directive is an error, never ignored.
Scope and facts
Scope is Puppet's top scope: it holds variables (node parameters),
facts, trusted data, server_facts, environment and strict, and is
what every Hiera lookup runs against. Precedence for a top-scope
variable name: an explicit variables entry wins over a fact of the same
name, which wins over a server_facts entry; $environment defaults to
"production"; $trusted defaults to Puppet's local hash (certname taken
from a clientcert variable/fact, else empty). Facts are also reachable as
a whole through $facts, and server_facts through $server_facts.
from hyera import Hiera, Scope, Strict, load_facts
scope = Scope(facts=load_facts("facts.yaml"), environment="production", strict=Strict.ERROR)
h = Hiera("hiera.yaml", scope=scope)
h.lookup("ntp::servers")
strict= takes a hyera.Strict member (OFF/WARNING/ERROR) or the
plain string it equals ("off"/"warning"/"error"); hyera.Merge,
hyera.FunctionKind, hyera.BackendKind and hyera.RenderAs are the same
kind of str-mixin enum for the other closed-set arguments described below.
load_facts(path) reads a puppet lookup --facts-style file (JSON for
.json, YAML for .yaml/.yml, otherwise JSON then YAML); the result
must be a mapping, and hostname/domain/fqdn/clientcert are
all-or-nothing. facts_from_facter() runs a bare facter -j instead:
from hyera import facts_from_facter
scope = Scope(facts=facts_from_facter())
h.scoped(**derive_args) returns a Hiera view bound to
h.scope.derive(**derive_args), sharing h's config, backends and caches:
variables/facts/server_facts shallow-update the parent scope's own
(new values win, nothing goes stale); environment/strict/trusted/
node_name replace the parent's when given.
strict ("off", "warning" -- the default -- or "error") controls
what happens when an interpolated variable is undefined; see
Differences from Puppet.
Backends
Backends (by data_hash name -- Puppet function names only; see
Differences from Puppet below for the one
exception):
| Backend | data_hash name |
Notes |
|---|---|---|
YAMLBackend |
yaml_data |
parses YAML the way Puppet's Psych does (types, symbols, BOM), on libyaml when available |
JSONBackend |
json_data |
|
HOCONBackend |
hocon_data |
requires pip install "hyera[hocon]" |
SopsBackend |
sops_data (also sops, sops_<yaml|json|ini|dotenv>) |
decrypts via the sops CLI on the fly |
A third-party backend registers itself the same way, by subclassing
hyera.Backend and declaring NAMES; Backend.find/.get/.new/.names
look a backend up by name, and Hiera(backends=[...]) restricts a lookup to
an explicit allow-list of classes.
HOCONBackend resolves include directives exactly as Puppet's own
hocon_data does by default: a plain include "file" contributes
nothing; include file(…) really reads the file (relative to the process
working directory, or absolute); a directive in value position (including
inside a [...] array) is kept as literal text; url(…), classpath(…),
required(…), package(…) and a case-mismatched keyword all raise
BackendError, matching Puppet's own parse/method errors for those forms.
Pass hocon_includes=False to HOCONBackend, or set options: {hocon_includes: false} on a hocon_data hierarchy entry (or in
defaults: {options: ...}; hyera's own extension, which Puppet rejects --
it refuses every options key on hocon_data), to restore the stricter,
pre-fidelity behaviour instead:
every form but a plain quoted include raises, include file(…) included.
In either mode, pyhocon's own include-resolving methods stay wrapped as a
fail-closed backstop, so an undiscovered gap in the text scanner still
cannot read a file or reach the network for a form the active mode does
not intend to resolve.
sops and unattended runs
SopsBackend (data_hash: sops_data) shells out to sops to decrypt a
level on the fly. The format (YAML, JSON, INI or dotenv) is inferred from
the file's extension the same way the sops CLI itself picks it
(.yaml/.yml/.json/.env/.ini, case-sensitive); any other
extension is a clear error, since sops would read that file as binary.
It is hardened so an automated lookup never hangs, dies opaquely, or
leaks a decrypted secret:
- a finite subprocess timeout (
hyera.backends.SOPS_TIMEOUT, default 30 s;SopsBackend(timeout=...)overrides it for one backend); on expirysopsand its child processes are killed and aBackendTimeoutError(aBackendErrorand aTimeoutError) is raised, sopsruns with its standard input closed, so it can never consume the caller's own input,- the last 2,000 characters of its stderr surfaced in a
BackendError, - a clear error when the
sopsbinary is not onPATH, - the data file is passed to
sopsas an absolute path after a literal--, so a level or scope value that starts with-can never be read as asopsoption, - the
sopsfound onPATHis the one executed, by its full resolved path; asops.bat/sops.cmdshim is refused (cmd.exere-parses a batch file's argument line, which a data-derived path could abuse), - a decrypted file that fails to parse reports only the problem and its
line/column -- never the decrypted plaintext; a YAML value shaped to
quote itself into the error message (
!!float,!ruby/object:...) is redacted instead, - an INI file is always decrypted through sops's own JSON view, never ini text -- sops's INI writer can otherwise emit a value that a text parser reads as a different key or an injected section.
eyaml_lookup_key
EyamlBackend (lookup_key: eyaml_lookup_key) decrypts hiera-eyaml's
ENC[PKCS7,...] values, behind the optional eyaml extra
(cryptography). PKCS7 only -- the private key alone is needed, no
certificate:
hierarchy:
- name: "secrets"
lookup_key: eyaml_lookup_key
path: "secrets.eyaml"
options:
pkcs7_private_key: "keys/private_key.pkcs7.pem"
pkcs7_private_key,pkcs7_private_key_env_varandpkcs7_b64_private_key_env_varfollow hiera-eyaml's own precedence (env var beats a plain path, base64-env-var beats both);pkcs7_public_key*options are accepted but never read.- A relative
pkcs7_private_keyresolves against the process's current working directory, exactly like hiera-eyaml itself -- notbase_pathand not the data file's own directory. - Other hiera-eyaml encryptors (GPG and third-party plugins) are not supported; a value using one raises the same "cannot load such file" error Puppet itself gives without that plugin installed.
Writing a backend
A package that ships backends needs no import in the user's code: it names the
module that defines them in the hyera.backends entry-point group, and
hyera imports that module the first time it looks a backend up (never at
import hyera). An entry that fails to import is logged and skipped.
[project.entry-points."hyera.backends"]
my_backend = "my_package.backend"
A hook (data_hash(path, options, context), lookup_key(key, options, context) or data_dig(key_segments, options, context)) that raises an
exception of a class outside hyera is reported as a BackendError naming
the function and the location, with the original as its cause; raise
BackendError yourself for a problem with a source. hyera.testing runs a
hook without a hierarchy (lookup_key, data_dig, data_hash, with
LookupContext.for_testing() as the context) and ships BackendContract,
the checks every backend passes:
import json
from hyera.backends import Backend, BackendError
from hyera.testing import BackendContract
class JSONFileBackend(Backend):
NAMES = {"function": ("json_file_data",)}
def loads(self, text):
try:
return json.loads(text)
except ValueError as e:
raise BackendError("invalid JSON: " + e.msg) from None
class TestJSONFileBackend(BackendContract):
backend = JSONFileBackend
def write_source(self, directory, data):
(directory / "a.json").write_text(json.dumps(data), encoding="utf-8")
return {"path": "a.json"}
Errors and exit codes
What goes wrong at run time derives from HieraError (.path names the
file concerned, where there is one). A caller's bad argument is not one: it
raises a plain TypeError (wrong type) or ValueError (right type, unusable
value), such as h.lookup("k", merge="bogus") or h.lookup("k", "Bogus[").
The same strategy or type in a data file's lookup_options or convert_to
raises the package error. MergeError and InterpolationError are also
ValueError, so test for HieraError or the exact class, not ValueError
alone.
ConfigError--hiera.yamlis missing, unreadable, unparsable, or violates Puppet's version 5 schema..linenames the 1-based line in.paththe problem was found at, when known.BackendError-- a data file could not be read or parsed;.pathnames it.HieraLookupError-- a failure while resolving a key, with subclassesInterpolationError(an unknown interpolation method, a misplaced%{alias(...)}, a recursive lookup, or an undefined variable understrict="error"),MergeError(values that cannot be merged, or an unknown or invalid strategy in a data file'slookup_options), andKeyNotFoundError(also aKeyError) --lookup()'s miss, with no default given.
The CLI's exit codes: 0 found (or --default/--explain printed), 1
the key was not found -- nothing is printed, matching puppet lookup's own
silent miss -- 2 any other error: a usage problem, a missing or unreadable
facts file, a bad config or data file, an unrenderable value, or a reader
that closes the output early or a device that cannot be written (both
silent: nothing on stderr) -- and 130 an interrupt (Ctrl-C), with no
traceback. A 2 is reported as one stderr line
(-v, -d or DUHO_TRACEBACK=1 adds the traceback). puppet lookup
exits 1 for both a miss and an error, and prints Error: Could not run:
and the message for an error; hyera's CLI tells the two apart, and its
message text is its own (see Differences from Puppet).
Caching
Each Hiera (and every .scoped(...) view of it, which shares the same
caches) keeps two scope-keyed caches -- resolved hierarchy locations and
merged lookup_options -- plus an unbounded cache of parsed data files,
bounded like Puppet's own per-environment cache: its size follows the data
tree, not the number of scopes seen. Hiera(..., cache_size=256) bounds
each scope-keyed cache (least-recently-used entries dropped; None for no
bound, 0 to disable); Hiera.clear_cache() drops every cache, including
parsed data files. Hiera(..., revalidate=True) (the default): each
lookup re-checks the data files and glob listings it uses and re-reads one
whose inode, modification time or size changed, as Puppet does between
compilations -- files added or removed at path/paths/mapped_paths
locations and under globbed directories are seen by the next lookup.
revalidate=False keeps every file and glob listing as first read until
clear_cache(). Neither mode re-reads hiera.yaml itself; construct a new
Hiera to pick up a changed base config.
A lookup_key or data_dig result is kept per top-level key. Under
revalidate=True it is dropped when a file the hook read through
context.cached_file_data changes (its inode, modification time or size),
and a hook that read no file through it is called once per lookup, since
hyera cannot know when its source changed. Under revalidate=False every
result is kept until clear_cache(). A miss is never kept, and a hook's own
context.cache is a separate store.
Untrusted input
Like Puppet, hyera trusts what the hierarchy and the data say: a scope value
interpolated into a path can climb out of the datadir with .. or name
an absolute path, a YAML document can expand aliases without bound, a glob
can expand {a,b}{a,b}... exponentially, and a HOCON ${VAR} reads the
process environment. If the scope values or the data come from a caller you
do not control, turn on what applies; nothing is on by default.
Hiera(..., confine_locations=True)treats a data file location outside its level'sdatadir(symbolic links resolved) as absent: never opened, shown byexplain()as a path not found, logged once atWARNING. A HOCONinclude file(...)outside it fails. It covers the files a level reads (path,paths,glob,mapped_paths);urilevels and what alookup_keybackend does with a path are not covered.Hiera(..., limits=hyera.Limits(...))bounds three costs:yaml_alias_nodes, the nodes one YAML document may yield through aliases (the load fails before any node is built);glob_patterns, the patterns oneglobmay expand to through braces (the lookup fails before any directory is walked); andhocon_substitution_size, the largest value one HOCON${...}substitution may insert, in characters for a string and in nodes plus characters for a list or object (the load fails before the value is copied). They bound those costs and not memory in general.options: {hocon_env: false}on ahocon_dataentry, orHOCONBackend(hocon_env=False), stops a HOCON substitution the document does not define from reading the process environment.
One set to start from, a recommendation and not a measured bound:
hiera = hyera.Hiera(
"hiera.yaml",
scope=hyera.Scope(facts=facts_from_the_caller),
confine_locations=True,
limits=hyera.Limits(
yaml_alias_nodes=100_000,
glob_patterns=1_000,
hocon_substitution_size=1_000_000,
),
)
For the MCP tool, see Command line.
Hiera coverage
hiera.yaml keys
| Feature | Status | Notes |
|---|---|---|
version |
Supported | 5 is fully validated; a versionless or 3 config is read as Hiera 3; 4 only in environment/module layers; any other value raises. |
defaults |
Supported | applies to any hierarchy entry lacking its own value; missing/empty/null becomes Puppet's built-in default. |
name |
Supported | required, non-empty, unique per hierarchy. |
path |
Supported | |
paths |
Supported | |
glob |
Partial | matched through hyera's own Ruby Dir.glob port; case-sensitive and byte-sorted on every OS, unlike Ruby on Windows. (id: glob-case-sensitive-byte-order) |
globs |
Partial | same as glob. (id: glob-case-sensitive-byte-order) |
mapped_paths |
Supported | a scope reference iterated as [key, value] pairs; each item is a local scope variable. |
uri |
Supported | validated with Ruby's URI() grammar, passed to the entry's function, never fetched. |
uris |
Supported | same as uri. |
datadir |
Supported | defaults to data, next to hiera.yaml. |
options |
Supported | interpolated, passed to the backend with path/uri. |
data_hash |
Supported | value must be a real Puppet function name (or the one non-Puppet sops_data name -- see Backends below). |
lookup_key |
Supported | called per key and per location with a hyera.LookupContext. |
data_dig |
Supported | same calling convention as lookup_key, plus the requested key segments. |
hiera3_backend |
Partial | global layer only; an unregistered name raises ConfigError where Puppet, with real Hiera 3 installed, silently contributes nothing. (id: v3-ruby-backend-unavailable) |
default_hierarchy |
Supported | module layer only; consulted after every other layer misses. |
plan_hierarchy |
Not supported | schema-validated but never consulted -- hyera does not run Puppet Bolt plans, the only context where Puppet applies it. |
Features
| Feature | Status | Notes |
|---|---|---|
| Hiera 3 and 4 configs | Supported | version 3 (or missing) resolved with Puppet's backend-major provider order; version 4 accepted in the environment/module layers only. |
version 1, 2 and others |
Not supported | a versionless file or 3 is read as Hiera 3; 1, 2 and any other value except 4 (environment/module layers) and 5 raise "This runtime does not support hiera.yaml version N". |
| Global/environment/module layers | Supported | Hiera(..., environmentpath=, basemodulepath=, modulepath=). |
Interpolation variables (%{x}, %{::x}, %{facts.x}, %{trusted.x}) |
Supported | |
Interpolation functions (hiera/lookup/alias/scope/literal) |
Supported | |
Undefined variables (strict) |
Partial | default is "warning" (interpolates as "" and logs); Puppet 8 defaults to "error". (id: strict-default-warning) |
Merge strategies (first/default/unique/hash/deep) |
Supported | |
Deep-merge options (knockout_prefix, sort_merged_arrays, merge_hash_arrays) |
Partial | a knockout_prefix Python's re cannot compile raises, where Ruby accepts it with a warning. (id: knockout-prefix-not-python-regex) |
reverse_deep/unconstrained_deep |
Supported | Hiera-3-era deep-merge variants. |
lookup_options merge (exact and ^ keys) |
Supported | |
convert_to |
Partial | SemVer, SemVerRange, Timespan, Timestamp, Regexp, Binary, URI, Type and Object all raise. (id: convert-to-unsupported-type) |
Lookup forms (name list, value_type, default_value, default_values_hash, override, block) |
Supported | |
| Dotted keys | Supported | |
dig/get/getvar |
Supported | |
explain/explain_options |
Supported | |
| Type expressions | Partial | a type alias other than Data/RichData is unsupported. (id: convert-to-unsupported-type) |
yaml_data |
Supported | |
json_data |
Supported | |
hocon_data |
Partial | include file("*.conf") globs by default, where Puppet's never does, and pyhocon parses a few constructs differently; see Backends and Differences from Puppet. (id: hocon-include-glob) |
eyaml_lookup_key |
Partial | PKCS7 only; other hiera-eyaml encryptors are not supported. (id: eyaml-pkcs7-only) |
sops_data |
Supported | the one backend with no Puppet equivalent. (id: sops-backend) |
puppet lookup CLI flags |
Partial | every flag except --compile/--trusted and the binary --render-as formats. (id: environment-conf-compile-trusted-unsupported) |
Not supported
| Feature | Status | Notes |
|---|---|---|
| Running an arbitrary Ruby Hiera 3 backend | Not supported | a v3/hiera3_backend name must be a Puppet-mapped one or a third-party Python hyera.Backend. |
Encrypted-value convert_to beyond Sensitive |
Not supported | |
| hiera-eyaml encryptors other than PKCS7 | Not supported | GPG and third-party plugins. |
environment.conf's modulepath/environment_data_provider, metadata.json's deprecated data_provider |
Not supported | superseded by the explicit modulepath= keyword. |
--render-as binary|msgpack|console|flat|rich_data_json |
Not supported | only s, json and yaml; an empty --render-as is an error. |
-V |
Not supported | use --version. |
Underscore and hyphen spellings of a flag (--hiera-config, --render_as, --knock_out_prefix) |
Not supported | only the spellings under Command line are accepted, and an option cannot be abbreviated (--expl). |
--render-as json float text |
Not supported | floats print in Python's spelling (1e-05, 1000000000000000.0), where Puppet's Ruby prints 0.00001 and 1e+15; a nil hash key prints "null". |
An empty --environment |
Not supported | Puppet falls back to its default environment; hyera reports an error. |
--compile/--trusted |
Not supported | |
calling_class/calling_module |
Not supported | |
| Facts from PuppetDB or the Puppet server | Not supported | facts come only from --facts/Scope(facts=...). |
puppet.conf discovery |
Not supported | |
Type aliases other than Data/RichData; new() for SemVer, SemVerRange, Timespan, Timestamp, Regexp, Binary, URI, Type, Object |
Not supported | |
hiera()/hiera_array()/hiera_hash()/hiera_include() as methods |
Not supported | use .lookup() -- see the mapping table under Lookups. |
The types Iterable, Iterator, Init and Unit in a type expression |
Not supported | a value_type or convert_to naming one raises HieraLookupError. |
A Float bound written as a string (Float['1', 2]), a float among a Tuple's element types (Tuple[String, 1.0, 2]), and a Hash used as a key by Hash.new |
Not supported | Puppet accepts the two type expressions and builds the Hash; hyera raises (a Python dict cannot hold a dict as a key). |
Differences from Puppet
hyera aims to give the same lookup outcome and value as Puppet 8.10.0's
puppet lookup. Every deliberate difference is listed here, tagged with a
slug; each is also either a recorded conformance-harness deviation (checked
against the real Puppet oracle) or a documented-only difference the harness
cannot record a golden for.
- A missing hiera.yaml raises
ConfigError, not Puppet's built-in fallback.Hiera(path)raises whenpathdoes not exist, and the CLI's--hiera_configbehaves the same way for a named file that is missing; Puppet then falls back to its built-in default configuration. Ask for that explicitly withHiera(None, base_path=...), or omit--hiera_configso./hiera.yaml-if-present is tried first. (id:missing-config-raises) - The directory holding hiera.yaml is used literally. hyera never
interpolates
%{...}inside that absolute base directory, and for glob levels treats any glob metacharacter in it as a literal pattern character; Puppet interpolates%{...}there too. There is no opt-in, since the directory is fixed at construction. (id:config-dir-not-interpolated) - A changed hiera.yaml is not re-read by an existing
Hiera. Puppet re-reads it between compilations; construct a newHierato pick up a changed base config (data files and glob listings are re-checked by default; see Caching). (id:config-not-revalidated) environmentpath=None(the default) means no environment directories at all. Every environment name then resolves with no environment layer and no error; Puppet always has anenvironmentpath, so an environment name it cannot find always raises. Pass a realenvironmentpathto get Puppet's raising behaviour. (id:environmentpath-none-means-no-layer)- An unregistered Hiera 3 backend name raises
ConfigError. Puppet, with real Hiera 3 installed, silently contributes nothing for abackends:/hiera3_backend:name it cannot run; hyera cannot run a Ruby Hiera 3 backend at all, so register a third-party Pythonhyera.Backendunder that name instead, or drop it frombackends:. (id:v3-ruby-backend-unavailable) codedirdefaults to Puppet's AIO system location for the platform (%ALLUSERSPROFILE%\PuppetLabs\codeon Windows,/etc/puppetlabs/codeelsewhere) -- never the per-user~/.puppetlabs/etc/codedefault or a value discovered frompuppet.conf. Passcodedir=/--codedirexplicitly to match a differently-configured Puppet install. (id:codedir-aio-default)sops_data(alsosops, andsops_yaml/sops_json/sops_ini/sops_dotenvto force the format) -- adata_hashbackend with no Puppet equivalent, for decrypting a sops-encrypted data file on the fly. A hierarchy that uses it does not load under real Puppet, and there is no Puppet equivalent to fall back to. (id:sops-backend)hocon_data'sinclude file("*.conf")globs. hyera lets pyhocon's own resolution run for real, which expands a glob in afile(...)argument and includes every match; Puppet's ownhocon_datanever expands such a glob (it contributes nothing). There is no opt-in that reproduces Puppet's non-globbingfile(...)exactly, thoughhocon_includes=False(oroptions: {hocon_includes: false}on the entry or indefaults) is available as a stricter, non-resolving alternative for every include form. Every otherincludeform matches Puppet exactly (see Backends). (id:hocon-include-glob)hocon_datais parsed by pyhocon, not Ruby's hocon gem. Quoted keys,nullinside a concatenation and unicode escapes match Puppet, but these constructs differ (Puppet, then hyera):list = [1]thenlist += 2gives[1,2], then2;enabled = Trueis the string"True", then the booleantrue;label = true xis"true x", then"Truex";mode = 010is8, then10;ratio = 1.0is1, then1.0; a key set first to an object and then to a scalar, a quoted-path key such as"x"."y" = 4, the empty-string key"" = 2and a leading byte-order mark load in Puppet and are parse errors in hyera;[1,, 2]and+1are errors in Puppet and are accepted by hyera; a backslash-slash escape and a unicode escape with non-hex digits are kept as written; an object's keys come back in a different order. (id:hocon-pyhocon-parser)eyaml_lookup_keysupports only the PKCS7 encryptor. hiera-eyaml's other encryptors (GPG, and any third-party plugin) raise the same "cannot load such file" error real Puppet gives without that plugin's gem installed -- this project never adds one, so there is no way to opt into GPG support here. (id:eyaml-pkcs7-only)- Navigating a dotted sub-key into an Integer keeps hyera's own error
text. Puppet crashes with a raw Ruby
NoMethodError("undefined method 'include?' for an instance of Integer") instead of a designed message; hyera raisesHieraLookupError("Data Provider type mismatch: Got Integer when a hash-like object was expected ..."), the same shape it already uses for a String or Array in this position. There is no opt-in. (id:integer-dotted-navigation-error-text) - A deep-merge
knockout_prefixthat Python'sremodule cannot compile raisesMergeError. Ruby accepts a prefix like**(with a warning about a redundant nested repeat operator) and uses it as a regex; choose aknockout_prefixthat is valid in both regex dialects to avoid the difference. (id:knockout-prefix-not-python-regex) convert_to(Puppet'snew()) does not support every type Puppet does. SemVer, SemVerRange, Timespan, Timestamp, Regexp, Binary, URI, Type and Object all raisehyera.HieraLookupError("hyera does not support new() for the Puppet type '...'") instead of converting -- these are types whose values are not plain data. A type alias other thanData/RichDatais also unsupported (parse_typeresolves only the five Puppet static-loader aliases; any other capitalized name becomes an unresolved type reference). There is no opt-in. (id:convert-to-unsupported-type)- Undefined variables default to
strict="warning"(an undefined%{var}/%{scope('var')}interpolates as""and logs a warning); Puppet 8 defaults tostrict="error", which fails the lookup. PassScope(strict="error")(or.scoped(strict="error")) to match Puppet's own default. Hierarchy locations of a version 5hiera.yamlare lenient in every mode, as in Puppet; a version 3 or 4 one fails the lookup understrict="error", as in Puppet. (id:strict-default-warning) - Glob wildcards are case-sensitive and results sort by byte order on
every OS, as on Puppet's Linux servers; Ruby on Windows matches glob
wildcards case-insensitively instead, so a hierarchy authored against a
Windows Puppet server may need adjusting. (id:
glob-case-sensitive-byte-order) --render-as yamlprintsSensitivevalues redacted, as the other formats do; Puppet prints the plaintext. There is no opt-in to print the plaintext here. (id:render-yaml-sensitive-redacted)--render-as yamlreads back as the value looked up, but its quoting need not match Puppet's text. hyera quotes a string that a YAML 1.1 reader would take for a number, date or float (1,000,2001-1-1,.Nan), escapes the line breaks YAML adds to LF and CR, and never writes anchors or aliases.--explain --render-as yamlwrites the explain tree with plain string keys. (id:render-yaml-equivalent-not-identical)--render-as sprints hashes in Ruby 3.2's AIO form ({"a"=>1}), as Puppet 8's own packages do; Puppet on Ruby 3.4 or later renders{"a" => 1}(with spaces around=>) instead. There is no opt-in, since hyera targets the AIO packages' own Ruby version. (id:aio-hash-rendering)--scope NAME=VALUEsets node parameters, whichpuppet lookuptakes from the node classifier instead; use--scopefor every value a real Puppet run would source from the classifier. (id:scope-flag-sets-node-parameters)- Facts come only from
--facts/Scope(facts=...).puppet lookupalso reads the local node's facter facts or PuppetDB-stored facts when--factsis omitted; hyera always requires an explicit facts source (a--factsfile with no facts is rejected, as in Puppet). (id:facts-from-file-only) $server_factsholds onlyserverversion(8.10.0) andenvironment. A real Puppet server populates several more; pass the missing ones throughScope(server_facts=...)directly if a hierarchy needs them. (id:server-facts-minimal)environment.confis not read;--compileand--trustedare not supported. Configureenvironmentpath/modulepath/basemodulepathexplicitly instead of relying onenvironment.confdiscovery, and there is no catalog-compilation mode to fall back to. (id:environment-conf-compile-trusted-unsupported)- Hash keys Python cannot tell apart are an error or one key. Ruby
keeps
1,1.0andtrueas three keys; Python treats them as one. A YAML mapping whose keys collide only that way ({1: a, 1.0: b}) raisesBackendErrornaming them, rather than silently dropping one entry, and--merge deepjoins such keys from different levels into one. Write the keys as strings to avoid it. (id:python-equal-hash-keys) - Data that is not valid UTF-8 is rejected as a whole. hyera reads data
files and
eyamlplaintext as strict UTF-8. Ajson_datafile with one non-UTF-8 byte fails every lookup that reaches it, where Puppet answers the other keys and fails only when it renders that value; aneyamlplaintext that is not UTF-8 raises, where Puppet returns the bytes; a!!binaryvalue whose bytes are not UTF-8 cannot be rendered assorjson. (id:non-utf8-data) - Collections nested more than 500 levels deep are a parse error, in
YAML and JSON files,
--factsfiles and--scopevalues, with the messagenested too deeply. Puppet reads YAML to about 10,000 levels and stops JSON at 100; the bound keeps a hostile file from ending the interpreter, which libyaml's recursive composer does on Python 3.9. (id:nesting-bound) - A few Ruby regex constructs are refused, and POSIX bracket classes are
ASCII-only. A
Pattern/Regexptype, aconvert_to/value_typeexpression or alookup_optionskey using\p{..},\P{..},\R,\X,\G,\K,\g<..>,&&or a nested class inside[...], a negated shorthand (\D \W \S \H) inside[...], a nested repeat such asa**, or (on Python 3.9 and 3.10) a possessive quantifier or an atomic group, raisesHieraLookupErrornaming the construct, where Ruby accepts it.[[:alpha:]]and the other POSIX classes match ASCII letters only, where Ruby's match Unicode. There is no match-time bound, as in Ruby: a pattern with nested quantifiers can take exponential time on a long subject. (id:ruby-regex-constructs) Stringformats cover one directive per value, not Puppet's container options. A format given as a type map ({Integer => '%x'}), the#indenting flag on an Array or Hash, and a precision on%a/%AraiseHieraLookupError, as does converting a Binary, Timestamp, URI or Object value; Puppet accepts all of them. (id:string-format-subset)- A chain of
%{lookup()}or%{alias()}interpolations resolves to about 80 hops. Puppet resolves 100; hyera recurses once per hop and never changes Python's recursion limit, so a longer chain, or a value nested past the limit, raisesInterpolationErrornaming the keys being resolved. (id:interpolation-chain-depth) - Two interpolation shapes differ.
%{::::x}reads as an undefined variable, where Puppet prints the fact it names; and a hash key that interpolates to an Array (%{alias('arr')}) raisesInterpolationError("not hashable"), where Puppet keeps the Array as the key. (id:interpolation-key-shapes) - Every error exits
2wherepuppet lookupexits1, and the error line is hyera's own. A miss exits1in both. For any other errorpuppet lookupexits1and printsError: Could not run:and the message; hyera exits2and logs oneERRORline. See Errors and exit codes. (id:error-exit-status) - Error message text is hyera's own where it differs. The same lookup
fails in both, but the wording need not match: hyera's unknown-function
error ends with
known: ..., a hiera.yamlversion: 4file in the global layer is refused without Puppet's(file: ...)suffix, and--explainof a version 3 config with a relative:datadir:shows absolute paths where Puppet shows them as written. Match on the exception class, not its text. (id:error-message-text) - Schema errors of a version 5
hiera.yamlcarry(line: N). Puppet never prints a line number; hyera ends each mismatch with the line of the node it points at, and puts the first one's inConfigError.line. Both list every mismatch. (id:schema-error-line-suffix) - Some inputs that crash Puppet 8.10 work in hyera.
puppet lookup --type Data k(or any type alias) fails in Puppet and returns the value here; an Integer key inlookup_optionsor in module data fails every Puppet lookup that reads it and is ignored by hyera;Float.new("0")crashes Puppet and returns0.0here. (id:puppet-crashes-hyera-answers) --environmenttakes the name literally.--environment production/is accepted by Puppet and names no environment in hyera, which reports the missing environment. (id:environment-trailing-slash)- A global
hiera.yamlthat cannot be loaded fails at construction, even forlookup_options.Hiera(...)reads the global configuration when it is built, so a global layer in version 4, or one failing the schema, raisesConfigErrorthere; Puppet reads it only for a key that needs it and answers a miss for the reserved keylookup_options. An environment or module layer that cannot be loaded is read by the first lookup that needs it, and the reserved key answers a miss before that, as in Puppet. (id:global-config-error-at-construction) - Three Ruby
Dir.globbehaviours are not matched. A brace group directly after**/(**/{b,a}.yaml) is matched per directory in sorted order by Ruby, where hyera expands it first and keeps the written order, so the files of such agloblevel can be searched in a different order; Ruby keeps a doubled/in a result (a//f.yaml), and hyera collapses it; a brace that expands to an empty pattern ({,a}) also returns the base directory in Ruby, which Puppet then discards as a directory. (id:dir-glob-ruby-quirks) - On a case-insensitive filesystem, a literal glob segment matched by case
folding is returned in the pattern's spelling, where Ruby returns the
on-disk spelling (
Dir.glob('Sub/c.yaml')findssub/c.yamlon APFS). Values are unaffected;--explainandsources()show the pattern's spelling. (id:glob-case-folded-spelling)
Development
python -m venv .venv/3.14-posix-x86_64
.venv/3.14-posix-x86_64/bin/pip install -e ".[dev]"
python -m pytest -q
python -m black --check src/ tests/ benchmarks/ examples/
Windows uses Scripts\python instead of bin/python; venvs are named
<version>-<os>-<arch>. Build the docs with pip install -e ".[docs]",
then python -m mkdocs build --strict. See the
root AGENTS.md
for the conformance recorder and the full contributor guide.
Releasing
This project follows Semantic Versioning and keeps a
CHANGELOG.md.
Pushing a tag matching v* runs release.yml: the test gate, then build
(which checks the tag names the version actually built), then a strict
docs build (docs-gate), then the GitHub release, then publishing to PyPI
through Trusted Publishing. A pre-release tag (v1.0.0-rc.1) is published
as a PyPI pre-release, which pip install hyera skips unless you ask for it
(--pre or an exact version pin); only a final tag also dispatches
docs.yml to redeploy the docs at that tag.
License
MIT, for this project's own code. Several modules port code translated from
Puppet (Apache-2.0), the
deep_merge gem (MIT),
Psych (MIT), Ruby's YAML library, and
Ruby's uri library (2-clause BSDL); those
files carry their own notice. See
NOTICE and
LICENSES/.
Metadata
Release files for hyera 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hyera-0.1.0.tar.gz | 976.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hyera-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.4 MB
Release files / hyera-0.1.0.tar.gz
| Download URL | hyera-0.1.0.tar.gz |
|---|---|
| Size | 976.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4fa5f35eaf5fe3c73b978e7ee3c0e6bb6be8d4ecf53aa5d895714b040c779661
|
|
BLAKE2b-256 checksum How to use checksums |
66fc1452b93a9bd1f233d366e4f92e7c45fd70baa49d20203dc4d90ff5c5b3c9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logRelease files / hyera-0.1.0-py3-none-any.whl
| Download URL | hyera-0.1.0-py3-none-any.whl |
|---|---|
| Size | 397.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fde941483ee05ab077d2c85a4371d26989b6b3c456e473e4fcd1e7b65adaee48
|
|
BLAKE2b-256 checksum How to use checksums |
2b38ddaae9b38bc097a353a313e022cd6558e9495dd5e308adda55bb3fba4c52
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency log