diff --git a/cmd/node-installer/detect.go b/cmd/node-installer/detect.go new file mode 100644 index 00000000..5643d68a --- /dev/null +++ b/cmd/node-installer/detect.go @@ -0,0 +1,63 @@ +/* + Copyright The SpinKube Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "errors" + "fmt" + "log/slog" + + "github.com/spf13/afero" + "github.com/spinkube/runtime-class-manager/internal/preset" +) + +var containerdConfigLocations = map[string]preset.Settings{ + // Microk8s + "/var/snap/microk8s/current/args/containerd-template.toml": preset.MicroK8s, + // RKE2 + "/var/lib/rancher/rke2/agent/etc/containerd/config.toml": preset.RKE2, + // K3s + "/var/lib/rancher/k3s/agent/etc/containerd/config.toml": preset.K3s, + // K0s + "/etc/k0s/containerd.toml": preset.K0s, + // default + "/etc/containerd/config.toml": preset.Default, +} + +func DetectDistro(config Config, hostFs afero.Fs) (preset.Settings, error) { + if config.Runtime.ConfigPath != "" { + // containerd config path has been set explicitly + if distro, ok := containerdConfigLocations[config.Runtime.ConfigPath]; ok { + return distro, nil + } + slog.Warn("could not determine distro from containerd config, falling back to defaults", "config", config.Runtime.ConfigPath) + return preset.Default.WithConfigPath(config.Runtime.ConfigPath), nil + } + + var errs []error + + for loc, distro := range containerdConfigLocations { + _, err := hostFs.Stat(loc) + if err == nil { + // config file found, return corresponding distro settings + return distro, nil + } + errs = append(errs, err) + } + + return preset.Settings{}, fmt.Errorf("failed to detect containerd config path: %w", errors.Join(errs...)) +} diff --git a/cmd/node-installer/detect_test.go b/cmd/node-installer/detect_test.go new file mode 100644 index 00000000..e86195e6 --- /dev/null +++ b/cmd/node-installer/detect_test.go @@ -0,0 +1,188 @@ +/* + Copyright The SpinKube Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main_test + +import ( + "reflect" + "testing" + + "github.com/spf13/afero" + main "github.com/spinkube/runtime-class-manager/cmd/node-installer" + "github.com/spinkube/runtime-class-manager/internal/preset" + tests "github.com/spinkube/runtime-class-manager/tests/node-installer" + "github.com/stretchr/testify/require" +) + +func Test_DetectDistro(t *testing.T) { + type args struct { + config main.Config + hostFs afero.Fs + } + tests := []struct { + name string + args args + wantErr bool + wantPreset preset.Settings + }{ + { + "config_override", + args{ + main.Config{ + struct { + Name string + ConfigPath string + }{"containerd", preset.MicroK8s.ConfigPath}, + struct { + Path string + AssetPath string + }{"/opt/kwasm", "/assets"}, + struct{ RootPath string }{""}, + }, + tests.FixtureFs("../../testdata/node-installer/distros/default"), + }, + false, + preset.MicroK8s, + }, + { + "config_not_found_fallback_default", + args{ + main.Config{ + struct { + Name string + ConfigPath string + }{"containerd", "/etc/containerd/not_found.toml"}, + struct { + Path string + AssetPath string + }{"/opt/kwasm", "/assets"}, + struct{ RootPath string }{""}, + }, + tests.FixtureFs("../../testdata/node-installer/distros/default"), + }, + false, + preset.Default.WithConfigPath("/etc/containerd/not_found.toml"), + }, + { + "unsupported", + args{ + main.Config{ + struct { + Name string + ConfigPath string + }{"containerd", ""}, + struct { + Path string + AssetPath string + }{"/opt/kwasm", "/assets"}, + struct{ RootPath string }{""}, + }, + tests.FixtureFs("../../testdata/node-installer/distros/unsupported"), + }, + true, + preset.Default, + }, + { + "microk8s", + args{ + main.Config{ + struct { + Name string + ConfigPath string + }{"containerd", ""}, + struct { + Path string + AssetPath string + }{"/opt/kwasm", "/assets"}, + struct{ RootPath string }{""}, + }, + tests.FixtureFs("../../testdata/node-installer/distros/microk8s"), + }, + false, + preset.MicroK8s, + }, + { + "k0s", + args{ + main.Config{ + struct { + Name string + ConfigPath string + }{"containerd", ""}, + struct { + Path string + AssetPath string + }{"/opt/kwasm", "/assets"}, + struct{ RootPath string }{""}, + }, + tests.FixtureFs("../../testdata/node-installer/distros/k0s"), + }, + false, + preset.K0s, + }, + { + "k3s", + args{ + main.Config{ + struct { + Name string + ConfigPath string + }{"containerd", ""}, + struct { + Path string + AssetPath string + }{"/opt/kwasm", "/assets"}, + struct{ RootPath string }{""}, + }, + tests.FixtureFs("../../testdata/node-installer/distros/k3s"), + }, + false, + preset.K3s, + }, + { + "rke2", + args{ + main.Config{ + struct { + Name string + ConfigPath string + }{"containerd", ""}, + struct { + Path string + AssetPath string + }{"/opt/kwasm", "/assets"}, + struct{ RootPath string }{""}, + }, + tests.FixtureFs("../../testdata/node-installer/distros/rke2"), + }, + false, + preset.RKE2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + preset, err := main.DetectDistro(tt.args.config, tt.args.hostFs) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.wantPreset.ConfigPath, preset.ConfigPath) + require.Equal(t, reflect.ValueOf(tt.wantPreset.Setup), reflect.ValueOf(preset.Setup)) + require.Equal(t, reflect.ValueOf(tt.wantPreset.Restarter), reflect.ValueOf(preset.Restarter)) + } + }) + } +} diff --git a/cmd/node-installer/install.go b/cmd/node-installer/install.go index 3ad64184..0aaa3405 100644 --- a/cmd/node-installer/install.go +++ b/cmd/node-installer/install.go @@ -26,6 +26,7 @@ import ( "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spinkube/runtime-class-manager/internal/containerd" + "github.com/spinkube/runtime-class-manager/internal/preset" "github.com/spinkube/runtime-class-manager/internal/shim" ) @@ -36,9 +37,20 @@ var installCmd = &cobra.Command{ Run: func(_ *cobra.Command, _ []string) { rootFs := afero.NewOsFs() hostFs := afero.NewBasePathFs(rootFs, config.Host.RootPath) - restarter := containerd.NewRestarter() - if err := RunInstall(config, rootFs, hostFs, restarter); err != nil { + distro, err := DetectDistro(config, hostFs) + if err != nil { + slog.Error("failed to detect containerd config", "error", err) + os.Exit(1) + } + + config.Runtime.ConfigPath = distro.ConfigPath + if err = distro.Setup(preset.Env{ConfigPath: distro.ConfigPath, HostFs: hostFs}); err != nil { + slog.Error("failed to run distro setup", "error", err) + os.Exit(1) + } + + if err := RunInstall(config, rootFs, hostFs, distro.Restarter); err != nil { slog.Error("failed to install", "error", err) os.Exit(1) } diff --git a/cmd/node-installer/root.go b/cmd/node-installer/root.go index 3efbf469..42b1d8a5 100644 --- a/cmd/node-installer/root.go +++ b/cmd/node-installer/root.go @@ -51,7 +51,7 @@ func Execute() { func init() { rootCmd.PersistentFlags().StringVarP(&config.Runtime.Name, "runtime", "r", "containerd", "Set the container runtime to configure (containerd, cri-o)") - rootCmd.PersistentFlags().StringVarP(&config.Runtime.ConfigPath, "runtime-config", "c", "/etc/containerd/config.toml", "Path to the runtime config file") + rootCmd.PersistentFlags().StringVarP(&config.Runtime.ConfigPath, "runtime-config", "c", "", "Path to the runtime config file. Will try to autodetect if left empty") rootCmd.PersistentFlags().StringVarP(&config.Kwasm.Path, "kwasm-path", "k", "/opt/kwasm", "Working directory for kwasm on the host") rootCmd.PersistentFlags().StringVarP(&config.Host.RootPath, "host-root", "H", "/", "Path to the host root path") } diff --git a/cmd/node-installer/uninstall.go b/cmd/node-installer/uninstall.go index a30bbafd..ec076532 100644 --- a/cmd/node-installer/uninstall.go +++ b/cmd/node-installer/uninstall.go @@ -36,13 +36,18 @@ var uninstallCmd = &cobra.Command{ Run: func(_ *cobra.Command, _ []string) { rootFs := afero.NewOsFs() hostFs := afero.NewBasePathFs(rootFs, config.Host.RootPath) - restarter := containerd.NewRestarter() - if err := RunUninstall(config, rootFs, hostFs, restarter); err != nil { - slog.Error("failed to uninstall shim", "error", err) + distro, err := DetectDistro(config, hostFs) + if err != nil { + slog.Error("failed to detect containerd config", "error", err) + os.Exit(1) + } + + config.Runtime.ConfigPath = distro.ConfigPath - // Exiting with 0 to prevent Kubernetes Jobs from running repetitively - os.Exit(0) + if err := RunUninstall(config, rootFs, hostFs, distro.Restarter); err != nil { + slog.Error("failed to uninstall", "error", err) + os.Exit(1) } }, } diff --git a/internal/preset/preset.go b/internal/preset/preset.go new file mode 100644 index 00000000..c357a610 --- /dev/null +++ b/internal/preset/preset.go @@ -0,0 +1,96 @@ +package preset + +import ( + "errors" + "io" + "os" + "strings" + + "github.com/spf13/afero" + "github.com/spinkube/runtime-class-manager/internal/containerd" +) + +type Settings struct { + ConfigPath string + Setup func(Env) error + Restarter containerd.Restarter +} + +type Env struct { + HostFs afero.Fs + ConfigPath string +} + +var Default = Settings{ + ConfigPath: "/etc/containerd/config.toml", + Setup: func(_ Env) error { return nil }, + Restarter: containerd.NewRestarter(), +} + +func (s Settings) WithConfigPath(path string) Settings { + s.ConfigPath = path + return s +} + +func (s Settings) WithSetup(setup func(env Env) error) Settings { + s.Setup = setup + return s +} + +var MicroK8s = Default.WithConfigPath("/var/snap/microk8s/current/args/containerd-template.toml") + +var RKE2 = Default.WithConfigPath("/var/lib/rancher/rke2/agent/etc/containerd/config.toml.tmpl"). + WithSetup(func(env Env) error { + _, err := env.HostFs.Stat(env.ConfigPath) + if err == nil { + return nil + } + + if errors.Is(err, os.ErrNotExist) { + // Copy base config into .tmpl version + src, _ := strings.CutSuffix(env.ConfigPath, ".tmpl") + in, err := env.HostFs.Open(src) + if err != nil { + return err + } + defer in.Close() + out, err := env.HostFs.Create(env.ConfigPath) + if err != nil { + return err + } + defer func() { + cerr := out.Close() + if err == nil { + err = cerr + } + }() + if _, err = io.Copy(out, in); err != nil { + return err + } + err = out.Sync() + + return nil + } + + return err + }) + +var K3s = RKE2.WithConfigPath("/var/lib/rancher/k3s/agent/etc/containerd/config.toml.tmpl") + +var K0s = Default.WithConfigPath("/etc/k0s/containerd.d/config.toml"). + WithSetup(func(env Env) error { + _, err := env.HostFs.Stat(env.ConfigPath) + if err == nil { + return nil + } + + if errors.Is(err, os.ErrNotExist) { + _, err := env.HostFs.Create(env.ConfigPath) + if err != nil { + return err + } + return nil + } + + return err + }) diff --git a/internal/preset/preset_test.go b/internal/preset/preset_test.go new file mode 100644 index 00000000..a540129c --- /dev/null +++ b/internal/preset/preset_test.go @@ -0,0 +1,97 @@ +package preset_test + +import ( + "testing" + + "github.com/spf13/afero" + "github.com/spinkube/runtime-class-manager/internal/preset" + tests "github.com/spinkube/runtime-class-manager/tests/node-installer" + "github.com/stretchr/testify/require" +) + +func Test_WithSetup(t *testing.T) { + type args struct { + settings preset.Settings + hostFs afero.Fs + } + tests := []struct { + name string + args args + wantErr bool + wantContents string + }{ + { + "rke2_err", + args{ + preset.RKE2, + tests.FixtureFs("../../testdata/node-installer/distros/unsupported"), + }, + true, + "", + }, + { + "rke2_config_exists", + args{ + preset.RKE2, + tests.FixtureFs("../../testdata/node-installer/containerd/rke2-existing-config-tmpl"), + }, + false, + "version = 2\npreexisting-config = true", + }, + { + "rke2_config_is_created", + args{ + preset.RKE2, + tests.FixtureFs("../../testdata/node-installer/distros/rke2"), + }, + false, + "version = 2", + }, + { + "k3s", + args{ + preset.K3s, + tests.FixtureFs("../../testdata/node-installer/distros/k3s"), + }, + false, + "version = 2", + }, + { + "k0s", + args{ + preset.K0s, + tests.FixtureFs("../../testdata/node-installer/distros/k0s"), + }, + false, + "", + }, + { + "microk8s", + args{ + preset.MicroK8s, + tests.FixtureFs("../../testdata/node-installer/distros/microk8s"), + }, + false, + "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.settings.Setup( + preset.Env{ + ConfigPath: tt.args.settings.ConfigPath, + HostFs: tt.args.hostFs, + }, + ) + + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + bytes, err := afero.ReadFile(tt.args.hostFs, tt.args.settings.ConfigPath) + require.NoError(t, err) + require.Equal(t, tt.wantContents, string(bytes)) + } + }) + } +} diff --git a/testdata/node-installer/containerd/rke2-existing-config-tmpl/var/lib/rancher/rke2/agent/etc/containerd/config.toml.tmpl b/testdata/node-installer/containerd/rke2-existing-config-tmpl/var/lib/rancher/rke2/agent/etc/containerd/config.toml.tmpl new file mode 100644 index 00000000..996b643a --- /dev/null +++ b/testdata/node-installer/containerd/rke2-existing-config-tmpl/var/lib/rancher/rke2/agent/etc/containerd/config.toml.tmpl @@ -0,0 +1,2 @@ +version = 2 +preexisting-config = true \ No newline at end of file diff --git a/testdata/node-installer/containerd/rke2-only-base-config-exists/var/lib/rancher/rke2/agent/etc/containerd/config.toml b/testdata/node-installer/containerd/rke2-only-base-config-exists/var/lib/rancher/rke2/agent/etc/containerd/config.toml new file mode 100644 index 00000000..a45a2afd --- /dev/null +++ b/testdata/node-installer/containerd/rke2-only-base-config-exists/var/lib/rancher/rke2/agent/etc/containerd/config.toml @@ -0,0 +1 @@ +version = 2 \ No newline at end of file diff --git a/testdata/node-installer/distros/default/etc/containerd/config.toml b/testdata/node-installer/distros/default/etc/containerd/config.toml new file mode 100644 index 00000000..e69de29b diff --git a/testdata/node-installer/distros/k0s/etc/k0s/containerd.toml b/testdata/node-installer/distros/k0s/etc/k0s/containerd.toml new file mode 100644 index 00000000..e69de29b diff --git a/testdata/node-installer/distros/k3s/var/lib/rancher/k3s/agent/etc/containerd/config.toml b/testdata/node-installer/distros/k3s/var/lib/rancher/k3s/agent/etc/containerd/config.toml new file mode 100644 index 00000000..a45a2afd --- /dev/null +++ b/testdata/node-installer/distros/k3s/var/lib/rancher/k3s/agent/etc/containerd/config.toml @@ -0,0 +1 @@ +version = 2 \ No newline at end of file diff --git a/testdata/node-installer/distros/microk8s/var/snap/microk8s/current/args/containerd-template.toml b/testdata/node-installer/distros/microk8s/var/snap/microk8s/current/args/containerd-template.toml new file mode 100644 index 00000000..e69de29b diff --git a/testdata/node-installer/distros/rke2/var/lib/rancher/rke2/agent/etc/containerd/config.toml b/testdata/node-installer/distros/rke2/var/lib/rancher/rke2/agent/etc/containerd/config.toml new file mode 100644 index 00000000..a45a2afd --- /dev/null +++ b/testdata/node-installer/distros/rke2/var/lib/rancher/rke2/agent/etc/containerd/config.toml @@ -0,0 +1 @@ +version = 2 \ No newline at end of file diff --git a/testdata/node-installer/distros/unsupported/.gitkeep b/testdata/node-installer/distros/unsupported/.gitkeep new file mode 100644 index 00000000..e69de29b