From 5f6048aee2ede5079a6311f0026ddd3a8b35be30 Mon Sep 17 00:00:00 2001 From: Jonas Meyer-Ohle <19151471+jonas-meyer@users.noreply.github.com> Date: Thu, 27 Nov 2025 17:19:54 +0000 Subject: [PATCH] encoding/yaml: add support for YAML tags via `@yaml(tag)` attribute This adds support for encoding YAML tags by specifying them in `@yaml(tag="...")` attributes on CUE fields. The tag value is applied to the encoded YAML node. This enables generating YAML files that use custom tags, such as those required by authentik blueprints (!KeyOf, !Env, !Find, etc.) and other YAML-based tools that rely on tags for special processing. Tags can be applied to scalars, sequences, and mappings. The implementation extracts tags from field attributes in the AST and sets them on the corresponding yaml.Node during encoding. Fixes #2316 Signed-off-by: Jonas Meyer-Ohle <19151471+jonas-meyer@users.noreply.github.com> --- internal/encoding/yaml/encode.go | 30 +++++++++++++ internal/encoding/yaml/encode_test.go | 64 +++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/internal/encoding/yaml/encode.go b/internal/encoding/yaml/encode.go index 155182017b7..05212bf9b80 100644 --- a/internal/encoding/yaml/encode.go +++ b/internal/encoding/yaml/encode.go @@ -234,6 +234,27 @@ func encodeExprs(exprs []ast.Expr) (n *yaml.Node, err error) { return n, nil } +// extractYAMLTag looks for @yaml(tag="...") attribute and returns the tag value. +// Returns an empty string if no @yaml attribute or no tag argument is found. +// Returns an error if the attribute is malformed. +func extractYAMLTag(attrs []*ast.Attribute) (string, error) { + for _, attr := range attrs { + key, body := attr.Split() + if key == "yaml" { + parsed := internal.ParseAttrBody(attr.Pos(), body) + if parsed.Err != nil { + return "", parsed.Err + } + if val, found, err := parsed.Lookup(0, "tag"); err != nil { + return "", err + } else if found { + return val, nil + } + } + } + return "", nil +} + // encodeDecls converts a sequence of declarations to a value. If it encounters // an embedded value, it will return this expression. This is more relaxed for // structs than is currently allowed for CUE, but the expectation is that this @@ -289,6 +310,15 @@ func encodeDecls(decls []ast.Decl) (n *yaml.Node, err error) { if err != nil { return nil, err } + + yamlTag, err := extractYAMLTag(x.Attrs) + if err != nil { + return nil, err + } + if yamlTag != "" { + value.Tag = yamlTag + } + lastHead = label lastFoot = value addDocs(x, label, value) diff --git a/internal/encoding/yaml/encode_test.go b/internal/encoding/yaml/encode_test.go index 10ed40b6bf6..f882c3bbf9c 100644 --- a/internal/encoding/yaml/encode_test.go +++ b/internal/encoding/yaml/encode_test.go @@ -243,6 +243,70 @@ route: receiver: pager group_by: [alertname, cluster] `, + }, { + name: "yaml_tag_scalar", + in: ` + key: "value" @yaml(tag="!Custom") + env: "VAR_NAME" @yaml(tag="!Env") + `, + out: ` +key: !Custom value +env: !Env VAR_NAME + `, + }, { + name: "yaml_tag_sequence", + in: ` + lookup: ["table", ["key", "value"]] @yaml(tag="!Find") + items: [1, 2, 3] @yaml(tag="!Seq") + `, + out: ` +lookup: !Find [table, [key, value]] +items: !Seq [1, 2, 3] + `, + }, { + name: "yaml_tag_mapping", + in: ` + config: { + key: "value" + count: 42 + } @yaml(tag="!Map") + `, + out: ` +config: !Map + key: value + count: 42 + `, + }, { + name: "yaml_tag_mixed", + in: ` + plain: "no-tag" + tagged: "has-tag" @yaml(tag="!Custom") + nested: { + field: "value" @yaml(tag="!Nested") + } + `, + out: ` +plain: no-tag +tagged: !Custom has-tag +nested: + field: !Nested value + `, + }, { + name: "yaml_tag_verbatim", + in: ` + custom: "value" @yaml(tag="!") + `, + out: ` +custom: !%3Ctag:example.com,2000:app/foo%3E value + `, + }, { + name: "yaml_attribute_without_tag", + in: ` + field: "value" @yaml(other="ignored") + `, + out: ` +field: value + `, }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) {