internal/cloudinit/append_test.go
Ref: Size: 4.2 KiB History
package cloudinit
import (
"io"
"mime"
"mime/multipart"
"net/mail"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// eitriDoc stands in for the cloud-config the agent's seed appends. Its
// merge_how line is what the ordering below exists to serve.
const eitriDoc = "#cloud-config\nmerge_how: \"list(prepend)+dict(recurse_array,no_replace)+str()\"\nruncmd:\n - [\"sh\", \"-c\", \"resize2fs /dev/vda1\"]\n"
// orderedParts parses an archive into (contentType, body) pairs IN ORDER.
// mimeParts keys by content type, which cannot see the difference between two
// cloud-config parts — and their order is the whole contract here.
func orderedParts(t *testing.T, s string) []struct{ CT, Body string } {
t.Helper()
msg, err := mail.ReadMessage(strings.NewReader(s))
require.NoError(t, err)
mediaType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
require.NoError(t, err)
require.Equal(t, "multipart/mixed", mediaType)
mr := multipart.NewReader(msg.Body, params["boundary"])
var out []struct{ CT, Body string }
for {
p, err := mr.NextPart()
if err != nil {
break
}
body, err := io.ReadAll(p)
require.NoError(t, err)
mt, _, _ := mime.ParseMediaType(p.Header.Get("Content-Type"))
out = append(out, struct{ CT, Body string }{mt, string(body)})
}
return out
}
func TestAppendCloudConfigPutsTheTenantsDocumentFirstAndEitrisLast(t *testing.T) {
// Order is the contract: cloud-init's cloud-config handler merges each part
// into the buffer the earlier parts built, using THAT part's mergers. eitri's
// part must therefore be merged in last, or its merge_how governs nothing.
tenant := "#cloud-config\npackages: [htop]\nruncmd:\n - [\"sh\", \"-c\", \"echo tenant\"]\n"
out, err := AppendCloudConfig(tenant, eitriDoc)
require.NoError(t, err)
parts := orderedParts(t, out)
require.Len(t, parts, 2)
assert.Equal(t, "text/cloud-config", parts[0].CT)
assert.Equal(t, tenant, parts[0].Body, "the tenant's document rides byte-for-byte")
assert.Equal(t, "text/cloud-config", parts[1].CT)
assert.Equal(t, eitriDoc, parts[1].Body, "eitri's document is the last part merged")
}
func TestAppendCloudConfigLeavesANonCloudConfigPayloadIntact(t *testing.T) {
// A tenant script is not something eitri can merge into, so it rides as its
// own part under the type cloud-init dispatches it with, untouched.
script := "#!/bin/bash\necho hello > /tmp/marker\n"
out, err := AppendCloudConfig(script, eitriDoc)
require.NoError(t, err)
parts := orderedParts(t, out)
require.Len(t, parts, 2)
assert.Equal(t, "text/x-shellscript", parts[0].CT)
assert.Equal(t, script, parts[0].Body)
assert.Equal(t, "text/cloud-config", parts[1].CT)
assert.Equal(t, eitriDoc, parts[1].Body)
}
func TestAppendCloudConfigAppendsToAnExistingArchiveRatherThanNesting(t *testing.T) {
existing, err := wrapMultipart([]part{
typedPart("text/cloud-config", "#cloud-config\npackages:\n - git\n"),
typedPart("text/x-shellscript", "#!/bin/bash\necho hi\n"),
})
require.NoError(t, err)
out, err := AppendCloudConfig(existing, eitriDoc)
require.NoError(t, err)
parts := orderedParts(t, out)
require.Len(t, parts, 3)
for _, p := range parts {
assert.NotContains(t, p.CT, "multipart", "must append, not nest")
}
assert.Equal(t, "#cloud-config\npackages:\n - git\n", parts[0].Body, "the tenant's own parts keep their order")
assert.Equal(t, "#!/bin/bash\necho hi\n", parts[1].Body)
assert.Equal(t, eitriDoc, parts[2].Body, "eitri's part is last, after every part the tenant brought")
}
func TestAppendCloudConfigRefusesAPayloadItCannotGiveAPartHeader(t *testing.T) {
// A jinja template's real type is unknown until it renders and a gzip blob is
// opaque, so neither can be labelled honestly. Refusing is the only safe
// answer: composing them wrong would hand the guest a seed whose CA trust
// never lands, which is the failure this whole mechanism exists to prevent.
for _, in := range []string{
"## template: jinja\n#cloud-config\nhostname: {{ v1.local_hostname }}\n",
"\x1f\x8b\x08 gzipped",
"random text that is not user-data",
} {
_, err := AppendCloudConfig(in, eitriDoc)
assert.Error(t, err, "must refuse %q rather than drop eitri's config", in[:min(20, len(in))])
}
}