Skip to content

Let applications contribute template functions - #1117

Merged
joshdrake merged 3 commits into
masterfrom
josh/register-template-func
Aug 27, 2026
Merged

Let applications contribute template functions#1117
joshdrake merged 3 commits into
masterfrom
josh/register-template-func

Conversation

@joshdrake

Copy link
Copy Markdown
Contributor

A certificate template can only call the functions this library provides, and some of what a CA wants to put in a template is knowledge this library doesn't have and shouldn't acquire. There's currently no way to supply one: WithTemplate builds its function map internally, and it's reached through provisioners the CA constructs for itself, so no call path exists along which a function could be passed down.

RegisterTemplateFunc is that path.

x509util.RegisterTemplateFunc("cel", func(expr string, data any) (any, error) {
    // the application's own expression evaluator, over types it knows about
})
{{ cel "device.serial" $ | toJson }}

x509util and sshutil each keep their own registry, so registering for one kind of certificate doesn't silently affect the other, and an application wanting a function in both asks for both.

What it refuses

Shadowing a built-in. A template calling toJson or fail has to get this library's implementation; a registry that allowed the substitution would make a template's meaning depend on which packages a binary happened to link. Duplicate names, non-functions, and names that aren't Go identifiers are refused for the same reason — better an error at start-up than a template that parses and does something else.

The $ convention

A registered function receives only its own arguments, as any template function does. One that needs the data being rendered takes it as a parameter and the template supplies it with $ — which is the value the template was executed with wherever it appears, unlike the dot, which a range block rebinds. There's a test covering exactly that case.

Notes

  • No new dependencies. Two files modified (net −6 lines), four added.
  • Register during start-up, not lazily: text/template resolves function names at parse time, so a template rendered before the function is registered fails to parse. The doc comment says so, and a test demonstrates it.
  • Full go test ./... passes, including -race.

Why this rather than a CEL environment in the library

This replaces #1116, which took the other approach — a public CEL package here, with an extension registry. That put cel-go in this library's public API to serve one embedder's needs. This is the smaller seam: crypto gains a way to accept a function and stays out of the expression business entirely, and the CEL machinery lives with the application that knows what a device is.

🤖 Generated with Claude Code

https://claude.ai/code/session_016N6LAjGgci2KLKYDbdWVcw

A certificate template can only call the functions this library provides,
and some of what a CA wants to put in a template is knowledge this
library does not have and should not acquire. There is currently no way
to supply one: WithTemplate builds its function map internally, and it
is reached through provisioners the CA constructs for itself, so no call
path exists along which a function could be passed down.

RegisterTemplateFunc is that path. x509util and sshutil each keep their
own registry, so registering for one kind of certificate does not
silently affect the other, and an application wanting a function in both
asks for both.

Registration refuses to shadow a built-in. A template calling toJson or
fail has to get this library's implementation; a registry that allowed
the substitution would make a template's meaning depend on which
packages a binary happened to link. Duplicate names, non-functions and
names that are not Go identifiers are refused for the same reason --
better an error at start-up than a template that parses and does
something else.

A registered function receives only its own arguments, as any template
function does. One that needs the data being rendered takes it as a
parameter and the template supplies it with $, which is the value the
template was executed with wherever it appears -- unlike the dot, which
a range block rebinds.

No new dependencies.

Change-Type: feature
Release-Note: yes
Audience: developer
Impact: none
Breaking: false
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6LAjGgci2KLKYDbdWVcw
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Register refuses to shadow a built-in, which is right for the accidental
case and wrong for the intended one. An application may decide its own
implementation of a function is the one its templates should get -- and
if that is the intent, it should be stated rather than left to depend on
the order in which two packages happen to touch the function map.

Replace states it. It validates the name and the value the same way, and
skips only the reserved-name and duplicate checks.

Change-Type: feature
Release-Note: yes
Audience: developer
Impact: none
Breaking: false
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6LAjGgci2KLKYDbdWVcw
@joshdrake
joshdrake marked this pull request as ready for review August 26, 2026 23:07
They were carrying rationale that belongs in the pull request, not the
package. Cut to what a caller needs: what the function does, the
start-up ordering requirement, and the "$" convention -- in the length
and voice the rest of the package uses.

Change-Type: docs
Release-Note: no
Audience: developer
Impact: none
Breaking: false
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6LAjGgci2KLKYDbdWVcw

@maraino maraino left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, replace feels a little overkill

@joshdrake
joshdrake merged commit eae7820 into master Aug 27, 2026
11 of 12 checks passed
@joshdrake
joshdrake deleted the josh/register-template-func branch August 27, 2026 02:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants