diff --git a/sdk/clusterplugin/clusterplugin_suite_test.go b/sdk/clusterplugin/clusterplugin_suite_test.go new file mode 100644 index 0000000000..e7765e5a95 --- /dev/null +++ b/sdk/clusterplugin/clusterplugin_suite_test.go @@ -0,0 +1,13 @@ +package clusterplugin + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestClusterPlugin(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ClusterPlugin Suite") +} diff --git a/sdk/clusterplugin/plugin.go b/sdk/clusterplugin/plugin.go index 9b9cab4921..addb744887 100644 --- a/sdk/clusterplugin/plugin.go +++ b/sdk/clusterplugin/plugin.go @@ -4,8 +4,10 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "github.com/kairos-io/kairos/v4/sdk/bus" + fsutils "github.com/kairos-io/kairos/v4/sdk/utils/fs" "github.com/mudler/go-pluggable" yip "github.com/mudler/yip/pkg/schema" "gopkg.in/yaml.v3" @@ -55,6 +57,15 @@ func (p ClusterPlugin) onBoot(event *pluggable.Event) pluggable.EventResponse { configFilePath = config.Cluster.ClusterConfigPath } + // On a UKI install or reset, this runs in the initramfs stage, before + // anything else has created the cloud-config directory (that normally + // happens as a side effect of the first RunStage call, post-pivot). Create + // it here rather than rely on that ordering. + if err := fsutils.MkdirAll(filesystem, filepath.Dir(configFilePath), 0755); err != nil { + response.Error = fmt.Sprintf("failed to create cloud config directory: %s", err.Error()) + return response + } + // open our cloud configuration file for writing f, err := filesystem.OpenFile(configFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) if err != nil { diff --git a/sdk/clusterplugin/plugin_test.go b/sdk/clusterplugin/plugin_test.go new file mode 100644 index 0000000000..8cfe12925d --- /dev/null +++ b/sdk/clusterplugin/plugin_test.go @@ -0,0 +1,78 @@ +package clusterplugin + +import ( + "encoding/json" + + "github.com/kairos-io/kairos/v4/sdk/bus" + "github.com/mudler/go-pluggable" + yip "github.com/mudler/yip/pkg/schema" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/twpayne/go-vfs/v4/vfst" + "gopkg.in/yaml.v3" +) + +var _ = Describe("ClusterPlugin.onBoot", func() { + var plugin ClusterPlugin + var cleanup func() + + BeforeEach(func() { + var testFS *vfst.TestFS + var err error + testFS, cleanup, err = vfst.NewTestFS(map[string]interface{}{}) + Expect(err).ToNot(HaveOccurred()) + filesystem = testFS + + plugin = ClusterPlugin{ + Provider: func(cluster Cluster) yip.YipConfig { + return yip.YipConfig{Name: "test"} + }, + } + }) + + AfterEach(func() { + cleanup() + }) + + newBootEvent := func(cluster *Cluster) *pluggable.Event { + cfg := Config{Cluster: cluster} + cfgYAML, err := yaml.Marshal(cfg) + Expect(err).ToNot(HaveOccurred()) + + payload, err := json.Marshal(bus.EventPayload{Config: string(cfgYAML)}) + Expect(err).ToNot(HaveOccurred()) + + return &pluggable.Event{Name: bus.EventBoot, Data: string(payload)} + } + + // On a UKI install or reset, onBoot runs before anything else has created + // /usr/local/cloud-config: it normally comes into being as a side effect of + // the first RunStage call, which only happens post-pivot. Without creating + // the directory here, OpenFile fails with ENOENT. + It("creates the cloud-config directory when it does not exist yet", func() { + response := plugin.onBoot(newBootEvent(&Cluster{Role: RoleControlPlane})) + Expect(response.Errored()).To(BeFalse(), response.Error) + + content, err := filesystem.ReadFile(clusterProviderCloudConfigFile) + Expect(err).ToNot(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("#cloud-config")) + }) + + It("creates the parent directory of a custom ClusterConfigPath", func() { + customPath := "/oem/custom/cluster-config.yaml" + response := plugin.onBoot(newBootEvent(&Cluster{Role: RoleControlPlane, ClusterConfigPath: customPath})) + Expect(response.Errored()).To(BeFalse(), response.Error) + + content, err := filesystem.ReadFile(customPath) + Expect(err).ToNot(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("#cloud-config")) + }) + + It("is a no-op when config.Cluster is nil", func() { + response := plugin.onBoot(newBootEvent(nil)) + Expect(response.Errored()).To(BeFalse(), response.Error) + + _, err := filesystem.ReadFile(clusterProviderCloudConfigFile) + Expect(err).To(HaveOccurred()) + }) +})