diff --git a/pkg/cloud_config/validate.go b/pkg/cloud_config/validate.go new file mode 100644 index 000000000..748ce259b --- /dev/null +++ b/pkg/cloud_config/validate.go @@ -0,0 +1,28 @@ +// Package cloud_config provides helpers for validating cloud-config content +// before it is served to nodes. +package cloud_config + +import "strings" + +// IsEmptyCloudConfig reports whether content is a text/cloud-config part +// that contains no module directives — i.e. it is only the '#cloud-config' +// header line plus optional blank lines/comments. +// +// Such empty parts cause cloud-init on the node to log warnings and can +// trigger TypeErrors in module handlers that expect a non-None config +// value (see issue #100). +func IsEmptyCloudConfig(contentType, content string) bool { + if !strings.HasPrefix(strings.TrimSpace(contentType), "text/cloud-config") { + return false + } + for _, line := range strings.Split(content, "\n") { + trimmed := strings.TrimSpace(line) + // Skip blank lines and the mandatory #cloud-config marker. + if trimmed == "" || trimmed == "#cloud-config" || strings.HasPrefix(trimmed, "#") { + continue + } + // Found at least one non-comment, non-blank line — not empty. + return false + } + return true +} diff --git a/pkg/cloud_config/validate_test.go b/pkg/cloud_config/validate_test.go new file mode 100644 index 000000000..7fcd7629e --- /dev/null +++ b/pkg/cloud_config/validate_test.go @@ -0,0 +1,52 @@ +package cloud_config + +import "testing" + +func TestIsEmptyCloudConfig(t *testing.T) { + tests := []struct { + name string + contentType string + content string + want bool + }{ + { + name: "empty part - header only", + contentType: "text/cloud-config", + content: "#cloud-config\n", + want: true, + }, + { + name: "empty part - header with blanks", + contentType: "text/cloud-config", + content: "#cloud-config\n\n \n", + want: true, + }, + { + name: "non-empty part - has write_files", + contentType: "text/cloud-config", + content: "#cloud-config\nwrite_files:\n - path: /tmp/foo\n", + want: false, + }, + { + name: "wrong content type", + contentType: "text/x-shellscript", + content: "#cloud-config\n", + want: false, + }, + { + name: "content type with charset param", + contentType: "text/cloud-config; charset=utf-8", + content: "#cloud-config\n", + want: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := IsEmptyCloudConfig(tc.contentType, tc.content) + if got != tc.want { + t.Errorf("IsEmptyCloudConfig(%q, ...) = %v, want %v", tc.contentType, got, tc.want) + } + }) + } +}