feat(engine): add zstd, lz4, snappy, and brotli template functions

Signed-off-by: owan <owan.io1992@gmail.com>
pull/31746/head
owan 2 weeks ago
parent 576a77be5a
commit d4bb18e68c
No known key found for this signature in database
GPG Key ID: D3A0026BA12CE5E4

@ -53,6 +53,12 @@ require (
sigs.k8s.io/yaml v1.6.0
)
require (
github.com/andybalholm/brotli v1.2.0
github.com/klauspost/compress v1.18.0
github.com/pierrec/lz4/v4 v4.1.25
)
require (
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
@ -100,7 +106,6 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect

@ -26,6 +26,8 @@ github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBi
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -242,6 +244,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
@ -313,6 +317,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=

@ -30,6 +30,10 @@ import (
"github.com/BurntSushi/toml"
"github.com/Masterminds/sprig/v3"
"github.com/andybalholm/brotli"
"github.com/klauspost/compress/snappy"
"github.com/klauspost/compress/zstd"
"github.com/pierrec/lz4/v4"
"sigs.k8s.io/yaml"
goYaml "sigs.k8s.io/yaml/goyaml.v3"
)
@ -67,6 +71,14 @@ func funcMap() template.FuncMap {
"fromJsonArray": fromJSONArray,
"gzip": gzipFunc,
"ungzip": ungzipFunc,
"zstd": zstdFunc,
"unzstd": unzstdFunc,
"lz4": lz4Func,
"unlz4": unlz4Func,
"snappy": snappyFunc,
"unsnappy": unsnappyFunc,
"brotli": brotliFunc,
"unbrotli": unbrotliFunc,
// This is a placeholder for the "include" function, which is
// late-bound to a template. By declaring it here, we preserve the
@ -309,3 +321,256 @@ func ungzipFunc(str string) (string, error) {
return string(b), nil
}
// zstdFunc compresses a string with zstd.
func zstdFunc(str string) (string, error) {
// Kubernetes limit for Secret/ConfigMap is 1MB.
const maxLimit = 1048576
encoder, err := zstd.NewWriter(nil)
if err != nil {
return "", err
}
compressed := encoder.EncodeAll([]byte(str), nil)
encoded := base64.StdEncoding.EncodeToString(compressed)
if len(encoded) > maxLimit {
return "", fmt.Errorf("zstd: output size %d exceeds limit of %d bytes", len(encoded), maxLimit)
}
return encoded, nil
}
// unzstdFunc decodes a base64 encoded and zstd-compressed string.
//
// It enforces a size limit (1MB) on the input and output to prevent abuse.
//
// This is designed to be called from a template.
func unzstdFunc(str string) (string, error) {
// Kubernetes limit for Secret/ConfigMap is 1MB.
const maxLimit = 1048576
if len(str) > maxLimit {
return "", fmt.Errorf("unzstd: input size %d exceeds limit of %d bytes", len(str), maxLimit)
}
decoded, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return "", fmt.Errorf("unzstd: base64 decode failed: %w", err)
}
decoder, err := zstd.NewReader(nil)
if err != nil {
return "", err
}
defer decoder.Close()
// Enforce a size limit on the decompressed content.
const maxLimitOutput = 1048576
// Ideally we would use LimitReader, but zstd.Decoder doesn't use io.Reader directly for simple DecodeAll
// But `zstd.NewReader` returns a decoder that implements io.Reader? Yes.
// But `EncodeAll`/`DecodeAll` are stateless faster paths.
// Let's use DecodeAll for simplicity and valid memory check if possible, OR standard io.Copy with limit.
// zstd's DecodeAll allocates. To be safe against bombs, we should probably check size *if possible* or use the reader.
// Let's use the Reader interface to be consistent with ungzip and reusable limit logic.
// However, zstd.DecodeAll is highly optimized.
// Let's try DecodeAll but pre-check if we can? No, we don't know decompressed size easily.
// Let's stick to Reader for safety.
// Re-creating reader:
// decoder, _ := zstd.NewReader(bytes.NewReader(decoded))
// But zstd.NewReader takes io.Reader.
reader, err := zstd.NewReader(bytes.NewReader(decoded))
if err != nil {
return "", err
}
defer reader.Close()
limitR := io.LimitReader(reader, maxLimitOutput+1)
b, err := io.ReadAll(limitR)
if err != nil {
return "", err
}
if len(b) > maxLimitOutput {
return "", fmt.Errorf("unzstd: decompressed content exceeds size limit of %d bytes", maxLimitOutput)
}
if !utf8.Valid(b) {
return "", fmt.Errorf("unzstd: content is not valid UTF-8 text")
}
return string(b), nil
}
// lz4Func compresses a string with lz4.
func lz4Func(str string) (string, error) {
const maxLimit = 1048576
var buf bytes.Buffer
writer := lz4.NewWriter(&buf)
if _, err := writer.Write([]byte(str)); err != nil {
return "", err
}
if err := writer.Close(); err != nil {
return "", err
}
encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
if len(encoded) > maxLimit {
return "", fmt.Errorf("lz4: output size %d exceeds limit of %d bytes", len(encoded), maxLimit)
}
return encoded, nil
}
// unlz4Func decodes a base64 encoded and lz4-compressed string.
//
// It enforces a size limit (1MB) on the input and output to prevent abuse.
//
// This is designed to be called from a template.
func unlz4Func(str string) (string, error) {
const maxLimit = 1048576
if len(str) > maxLimit {
return "", fmt.Errorf("unlz4: input size %d exceeds limit of %d bytes", len(str), maxLimit)
}
decoded, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return "", fmt.Errorf("unlz4: base64 decode failed: %w", err)
}
reader := lz4.NewReader(bytes.NewReader(decoded))
// Enforce a size limit on the decompressed content.
const maxLimitOutput = 1048576
limitR := io.LimitReader(reader, maxLimitOutput+1)
b, err := io.ReadAll(limitR)
if err != nil {
return "", err
}
if len(b) > maxLimitOutput {
return "", fmt.Errorf("unlz4: decompressed content exceeds size limit of %d bytes", maxLimitOutput)
}
if !utf8.Valid(b) {
return "", fmt.Errorf("unlz4: content is not valid UTF-8 text")
}
return string(b), nil
}
// snappyFunc compresses a string with snappy.
func snappyFunc(str string) (string, error) {
const maxLimit = 1048576
// Snappy Encode returns byte slice
compressed := snappy.Encode(nil, []byte(str))
encoded := base64.StdEncoding.EncodeToString(compressed)
if len(encoded) > maxLimit {
return "", fmt.Errorf("snappy: output size %d exceeds limit of %d bytes", len(encoded), maxLimit)
}
return encoded, nil
}
// unsnappyFunc decodes a base64 encoded and snappy-compressed string.
//
// It enforces a size limit (1MB) on the input and output to prevent abuse.
//
// This is designed to be called from a template.
func unsnappyFunc(str string) (string, error) {
const maxLimit = 1048576
if len(str) > maxLimit {
return "", fmt.Errorf("unsnappy: input size %d exceeds limit of %d bytes", len(str), maxLimit)
}
decoded, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return "", fmt.Errorf("unsnappy: base64 decode failed: %w", err)
}
// Snappy DecodedLen is available, use it for safety check before allocation if possible
decodedLen, err := snappy.DecodedLen(decoded)
if err != nil {
return "", err
}
const maxLimitOutput = 1048576
if decodedLen > maxLimitOutput {
return "", fmt.Errorf("unsnappy: decompressed content exceeds size limit of %d bytes", maxLimitOutput)
}
b, err := snappy.Decode(nil, decoded)
if err != nil {
return "", err
}
if !utf8.Valid(b) {
return "", fmt.Errorf("unsnappy: content is not valid UTF-8 text")
}
return string(b), nil
}
// brotliFunc compresses a string with brotli.
func brotliFunc(str string) (string, error) {
const maxLimit = 1048576
var buf bytes.Buffer
writer := brotli.NewWriter(&buf)
if _, err := writer.Write([]byte(str)); err != nil {
return "", err
}
if err := writer.Close(); err != nil {
return "", err
}
encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
if len(encoded) > maxLimit {
return "", fmt.Errorf("brotli: output size %d exceeds limit of %d bytes", len(encoded), maxLimit)
}
return encoded, nil
}
// unbrotliFunc decodes a base64 encoded and brotli-compressed string.
//
// It enforces a size limit (1MB) on the input and output to prevent abuse.
//
// This is designed to be called from a template.
func unbrotliFunc(str string) (string, error) {
const maxLimit = 1048576
if len(str) > maxLimit {
return "", fmt.Errorf("unbrotli: input size %d exceeds limit of %d bytes", len(str), maxLimit)
}
decoded, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return "", fmt.Errorf("unbrotli: base64 decode failed: %w", err)
}
reader := brotli.NewReader(bytes.NewReader(decoded))
const maxLimitOutput = 1048576
limitR := io.LimitReader(reader, maxLimitOutput+1)
b, err := io.ReadAll(limitR)
if err != nil {
return "", err
}
if len(b) > maxLimitOutput {
return "", fmt.Errorf("unbrotli: decompressed content exceeds size limit of %d bytes", maxLimitOutput)
}
if !utf8.Valid(b) {
return "", fmt.Errorf("unbrotli: content is not valid UTF-8 text")
}
return string(b), nil
}

@ -135,6 +135,42 @@ keyInElement1 = "valueInElement1"`,
tpl: `{{ "H4sIAAAAAAAA/8pIzcnJVyjPL8pJAQQAAP//hRFKDQsAAAA=" | ungzip }}`,
expect: `hello world`,
vars: nil,
}, {
// Test Zstd
tpl: `{{ "Hello world!" | zstd }}`,
expect: `KLUv/QQAYQAASGVsbG8gd29ybGQhsn39fw==`,
vars: nil,
}, {
tpl: `{{ "KLUv/QQAYQAASGVsbG8gd29ybGQhsn39fw==" | unzstd }}`,
expect: "Hello world!",
vars: nil,
}, {
// Test LZ4
tpl: `{{ "Hello world!" | lz4 }}`,
expect: `BCJNGGRwuQwAAIBIZWxsbyB3b3JsZCEAAAAAcXerxA==`,
vars: nil,
}, {
tpl: `{{ "BCJNGGRwuQwAAIBIZWxsbyB3b3JsZCEAAAAAcXerxA==" | unlz4 }}`,
expect: "Hello world!",
vars: nil,
}, {
// Test Snappy
tpl: `{{ "Hello world!" | snappy }}`,
expect: `DCxIZWxsbyB3b3JsZCE=`,
vars: nil,
}, {
tpl: `{{ "DCxIZWxsbyB3b3JsZCE=" | unsnappy }}`,
expect: "Hello world!",
vars: nil,
}, {
// Test Brotli
tpl: `{{ "Hello world!" | brotli }}`,
expect: `GwsAACRBQpBKkUVq+pw1CQ==`,
vars: nil,
}, {
tpl: `{{ "GwsAACRBQpBKkUVq+pw1CQ==" | unbrotli }}`,
expect: "Hello world!",
vars: nil,
}}
for _, tt := range tests {

Loading…
Cancel
Save