internal/cloudinit/cloudinit_test.go
Ref: Size: 4.9 KiB History
package cloudinit
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
const key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyForTests user@host"
// parse re-parses merged output so assertions test SEMANTICS, not text
// formatting (which reflows through the YAML round-trip by design).
func parse(t *testing.T, s string) map[string]any {
t.Helper()
require.True(t, strings.HasPrefix(s, "#cloud-config\n"), "merged output must keep the #cloud-config header, got: %q", s[:min(20, len(s))])
var m map[string]any
require.NoError(t, yaml.Unmarshal([]byte(s), &m))
return m
}
func authKeys(t *testing.T, m map[string]any) []string {
t.Helper()
raw, ok := m["ssh_authorized_keys"]
require.True(t, ok, "merged doc must have top-level ssh_authorized_keys")
list, ok := raw.([]any)
require.True(t, ok, "ssh_authorized_keys must be a list, got %T", raw)
out := make([]string, len(list))
for i, v := range list {
out[i] = v.(string)
}
return out
}
func TestMergeAddsKeyWhenAbsent(t *testing.T) {
in := "#cloud-config\npackages:\n - htop\n"
out, err := mergeSSHKey(in, key)
require.NoError(t, err)
m := parse(t, out)
assert.Equal(t, []string{key}, authKeys(t, m))
// Unrelated content survives.
assert.Contains(t, m, "packages")
}
func TestMergeAppendsToExistingList(t *testing.T) {
existing := "ssh-rsa AAAAexisting other@host"
in := "#cloud-config\nssh_authorized_keys:\n - " + existing + "\n"
out, err := mergeSSHKey(in, key)
require.NoError(t, err)
assert.Equal(t, []string{existing, key}, authKeys(t, parse(t, out)))
}
func TestMergeIsIdempotentDedup(t *testing.T) {
in := "#cloud-config\nssh_authorized_keys:\n - " + key + "\n"
out, err := mergeSSHKey(in, key)
require.NoError(t, err)
assert.Equal(t, []string{key}, authKeys(t, parse(t, out)), "an already-present key must not be duplicated")
}
func TestMergeNormalizesScalarKey(t *testing.T) {
// cloud-init accepts ssh_authorized_keys as a single scalar; normalize to a
// list and append rather than clobbering the user's existing key.
existing := "ssh-rsa AAAAexisting other@host"
in := "#cloud-config\nssh_authorized_keys: " + existing + "\n"
out, err := mergeSSHKey(in, key)
require.NoError(t, err)
assert.Equal(t, []string{existing, key}, authKeys(t, parse(t, out)))
}
func TestMergeHeaderWithLeadingBlankLines(t *testing.T) {
in := "\n\n#cloud-config\nruncmd:\n - echo hi\n"
out, err := mergeSSHKey(in, key)
require.NoError(t, err)
assert.Equal(t, []string{key}, authKeys(t, parse(t, out)))
}
func TestMergeRejectsNonCloudConfig(t *testing.T) {
for _, in := range []string{
"#!/bin/bash\necho hi\n", // shell script user-data
"## template: jinja\n#cloud-config\n", // jinja-templated (can't safely edit)
"just some text", // not user-data at all
} {
_, err := mergeSSHKey(in, key)
assert.Error(t, err, "must refuse to merge into non-cloud-config: %q", in)
}
}
func TestMergeRejectsMalformedYAML(t *testing.T) {
_, err := mergeSSHKey("#cloud-config\n bad: : : indent\n\t- x\n", key)
assert.Error(t, err)
}
func TestMergeRejectsSSHAuthorizedKeysWrongType(t *testing.T) {
// A mapping where we expect a scalar/sequence — don't silently drop it.
in := "#cloud-config\nssh_authorized_keys:\n nested: value\n"
_, err := mergeSSHKey(in, key)
assert.Error(t, err)
}
func TestMergePreservesScalarFidelity(t *testing.T) {
// The merge must NOT coerce unquoted scalar VALUES elsewhere in the doc:
// an in-place node edit preserves them; a map[string]any round-trip mangles
// permissions 0644->420, 1.10->1.1, dates, etc. This is the common path.
in := "#cloud-config\n" +
"write_files:\n" +
" - path: /etc/x\n" +
" permissions: 0644\n" +
"version: 1.10\n" +
"stamp: 2020-01-02\n"
out, err := mergeSSHKey(in, key)
require.NoError(t, err)
assert.Contains(t, out, "0644", "octal permissions must survive verbatim, not become 420")
assert.Contains(t, out, "1.10", "trailing zero must survive")
assert.Contains(t, out, "2020-01-02", "date must not be normalized to RFC3339")
// And the key still landed.
assert.Contains(t, out, key)
}
func TestMergePreservesComments(t *testing.T) {
in := "#cloud-config\n# keep me\npackages:\n - htop # inline\n"
out, err := mergeSSHKey(in, key)
require.NoError(t, err)
assert.Contains(t, out, "# keep me", "top-level comment must survive an in-place edit")
}
func TestMergeRejectsMultiDocYAML(t *testing.T) {
// cloud-init rejects multi-document cloud-config; accepting-and-truncating
// would mask an error the guest would raise. Reject it too.
in := "#cloud-config\npackages: [htop]\n---\nruncmd:\n - echo hi\n"
_, err := mergeSSHKey(in, key)
assert.Error(t, err)
}
func TestMergeEmptyBodyGetsKey(t *testing.T) {
// A bare "#cloud-config" with no body is valid; the merge seeds the key.
out, err := mergeSSHKey("#cloud-config\n", key)
require.NoError(t, err)
assert.Equal(t, []string{key}, authKeys(t, parse(t, out)))
}