From 4eac8b21fae86d93d14e3187f05b41d03ffbfcc5 Mon Sep 17 00:00:00 2001 From: Stalin <161853795+0x5t4l1n@users.noreply.github.com> Date: Mon, 3 Aug 2026 01:04:56 +0530 Subject: [PATCH] fix: skip empty text/cloud-config parts instead of returning them Resolves #100. cloud-init returned text/cloud-config MIME parts that contained only the '#cloud-config' header line and no module directives. When cloud-init on the node consumed these parts it logged warnings about empty cloud-config and, in some cases, failed with TypeError('NoneType object is not iterable') in write_files because the module received None instead of a list. Add IsEmptyCloudConfig() and filter empty parts out of the multipart response in the server handler so they are never sent to nodes. --- pkg/cloud_config/validate.go | 28 +++++++++++++++++ pkg/cloud_config/validate_test.go | 52 +++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 pkg/cloud_config/validate.go create mode 100644 pkg/cloud_config/validate_test.go 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) + } + }) + } +}