Artem Chernyshev 0cdb5a58c8
feat: support raw bytes in the inline fields for manifests/patches
Now inline supports all three variants:
- a single inline map (backward compatibility for config patches).
- a list of inline maps
- raw bytes, that can also contain multiple documents.

`omnictl cluster template export` command was updated to export config
patches/manifests as raw bytes to ensure that multiple values are
properly supported.

Fixes: https://github.com/siderolabs/omni/issues/2683

Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
2026-04-30 19:07:46 +03:00

274 lines
5.1 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package models_test
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v4"
"github.com/siderolabs/omni/client/pkg/template/internal/models"
)
type config struct {
Inline models.InlineContent `yaml:"inline"`
}
func TestInlineContent_UnmarshalSingleMap(t *testing.T) {
t.Parallel()
src := `
inline:
machine:
network:
kubespan:
enabled: true
`
var got config
require.NoError(t, yaml.Unmarshal([]byte(src), &got))
raw, err := got.Inline.Bytes()
require.NoError(t, err)
assert.Contains(t, string(raw), "kubespan:")
assert.Contains(t, string(raw), "enabled: true")
}
func TestInlineContent_UnmarshalSliceOfMaps(t *testing.T) {
t.Parallel()
src := `
inline:
- apiVersion: v1
kind: ConfigMap
metadata:
name: a
- apiVersion: v1
kind: ConfigMap
metadata:
name: b
`
var got config
require.NoError(t, yaml.Unmarshal([]byte(src), &got))
raw, err := got.Inline.Bytes()
require.NoError(t, err)
docs := splitYAMLDocs(t, raw)
assert.Len(t, docs, 2)
assert.Contains(t, docs[0], "name: a")
assert.Contains(t, docs[1], "name: b")
}
func TestInlineContent_UnmarshalRawBytes(t *testing.T) {
t.Parallel()
src := `
inline: |
machine:
network:
kubespan:
enabled: true
`
var got config
require.NoError(t, yaml.Unmarshal([]byte(src), &got))
raw, err := got.Inline.Bytes()
require.NoError(t, err)
expected := `machine:
network:
kubespan:
enabled: true
`
assert.Equal(t, expected, string(raw))
}
func TestInlineContent_UnmarshalRawBytesMultiDoc(t *testing.T) {
t.Parallel()
src := `inline: |
apiVersion: v1
kind: ConfigMap
metadata:
name: a
---
apiVersion: v1
kind: ConfigMap
metadata:
name: b
`
var got config
require.NoError(t, yaml.Unmarshal([]byte(src), &got))
raw, err := got.Inline.Bytes()
require.NoError(t, err)
assert.Contains(t, string(raw), "name: a")
assert.Contains(t, string(raw), "name: b")
assert.Contains(t, string(raw), "---")
}
func TestInlineContent_RoundtripSingleMap(t *testing.T) {
t.Parallel()
src := `inline:
machine:
network:
kubespan:
enabled: true
`
var got config
require.NoError(t, yaml.Unmarshal([]byte(src), &got))
out, err := yaml.Marshal(got)
require.NoError(t, err)
// re-parse and compare structurally
var roundtrip config
require.NoError(t, yaml.Unmarshal(out, &roundtrip))
rawIn, err := got.Inline.Bytes()
require.NoError(t, err)
rawOut, err := roundtrip.Inline.Bytes()
require.NoError(t, err)
assert.Equal(t, string(rawIn), string(rawOut))
}
func TestInlineContent_RoundtripSliceOfMaps(t *testing.T) {
t.Parallel()
src := `inline:
- apiVersion: v1
kind: ConfigMap
metadata:
name: a
- apiVersion: v1
kind: ConfigMap
metadata:
name: b
`
var got config
require.NoError(t, yaml.Unmarshal([]byte(src), &got))
out, err := yaml.Marshal(&got)
require.NoError(t, err)
var roundtrip config
require.NoError(t, yaml.Unmarshal(out, &roundtrip))
rawIn, err := got.Inline.Bytes()
require.NoError(t, err)
rawOut, err := roundtrip.Inline.Bytes()
require.NoError(t, err)
assert.Equal(t, string(rawIn), string(rawOut))
}
func TestInlineContent_RoundtripRawBytes(t *testing.T) {
t.Parallel()
src := `inline: |
machine:
network:
kubespan:
enabled: true
`
var got config
require.NoError(t, yaml.Unmarshal([]byte(src), &got))
out, err := yaml.Marshal(&got)
require.NoError(t, err)
// raw bytes are re-emitted as a literal block scalar
assert.True(t, strings.HasPrefix(string(out), "inline: |"))
assert.Contains(t, string(out), "kubespan:")
}
func TestInlineContent_OmitEmpty(t *testing.T) {
t.Parallel()
type wrapper struct {
Name string `yaml:"name"`
Inline models.InlineContent `yaml:"inline,omitempty"`
}
out, err := yaml.Marshal(wrapper{Name: "alpha"})
require.NoError(t, err)
assert.Equal(t, "name: alpha\n", string(out))
}
func TestInlineContent_Constructors(t *testing.T) {
t.Parallel()
mapInline := models.NewInlineContent(map[string]any{"a": 1})
mapBytes, err := mapInline.Bytes()
require.NoError(t, err)
assert.Contains(t, string(mapBytes), "a: 1")
listInline := models.NewInlineContent(map[string]any{"a": 1}, map[string]any{"b": 2})
listBytes, err := listInline.Bytes()
require.NoError(t, err)
assert.Contains(t, string(listBytes), "a: 1")
assert.Contains(t, string(listBytes), "b: 2")
rawInline := models.NewInlineContentBytes([]byte("a: 1\n"))
rawBytes, err := rawInline.Bytes()
require.NoError(t, err)
assert.Equal(t, "a: 1\n", string(rawBytes))
}
func splitYAMLDocs(t *testing.T, raw []byte) []string {
t.Helper()
dec := yaml.NewDecoder(strings.NewReader(string(raw)))
var docs []string
for {
var node yaml.Node
err := dec.Decode(&node)
if err != nil {
break
}
out, marshalErr := yaml.Marshal(&node)
require.NoError(t, marshalErr)
docs = append(docs, string(out))
}
return docs
}