diff --git a/api/types/client.go b/api/types/client.go index 18edc7b3d..b2b4a0037 100644 --- a/api/types/client.go +++ b/api/types/client.go @@ -18,7 +18,11 @@ import ( ) const ( - ENV_PLATFORM = "SKUPPER_PLATFORM" + ENV_PLATFORM = "SKUPPER_PLATFORM" + ENV_SYSTEM_AUTO_RELOAD = "SKUPPER_SYSTEM_RELOAD_TYPE" + + SystemReloadTypeAuto string = "auto" + SystemReloadTypeManual string = "manual" ) type ConnectorCreateOptions struct { diff --git a/internal/nonkube/bootstrap/bootstrap.go b/internal/nonkube/bootstrap/bootstrap.go index eb13329f9..785276a1a 100644 --- a/internal/nonkube/bootstrap/bootstrap.go +++ b/internal/nonkube/bootstrap/bootstrap.go @@ -109,7 +109,7 @@ func Bootstrap(config *Config) (*api.SiteState, error) { if err != nil { return nil, fmt.Errorf("failed to load site state: %v", err) } - // if sources are being consume from namespace sources, they must be properly set + // if sources are being consumed from namespace sources, they must be properly set crNamespace := siteState.GetNamespace() targetNamespace := utils.DefaultStr(config.Namespace, "default") if config.InputPath == sourcesPath { diff --git a/internal/nonkube/bootstrap/install.go b/internal/nonkube/bootstrap/install.go index c516ac399..ffe9ad568 100644 --- a/internal/nonkube/bootstrap/install.go +++ b/internal/nonkube/bootstrap/install.go @@ -14,6 +14,7 @@ import ( "github.com/skupperproject/skupper/internal/nonkube/bootstrap/controller" internalclient "github.com/skupperproject/skupper/internal/nonkube/client/compat" "github.com/skupperproject/skupper/internal/nonkube/common" + "github.com/skupperproject/skupper/internal/utils" "github.com/skupperproject/skupper/pkg/container" "github.com/skupperproject/skupper/pkg/nonkube/api" ) @@ -75,6 +76,8 @@ func Install(platform string) error { "CONTAINER_ENDPOINT": config.containerEndpoint, "SKUPPER_OUTPUT_PATH": config.hostDataHome, "CONTAINER_ENGINE": config.containerEngine, + "SKUPPER_SYSTEM_RELOAD_TYPE": utils.DefaultStr(os.Getenv(types.ENV_SYSTEM_AUTO_RELOAD), + types.SystemReloadTypeManual), } //To mount a volume as a bind, the host path must be specified in the Name field diff --git a/internal/nonkube/compat/site_state_renderer.go b/internal/nonkube/compat/site_state_renderer.go index a61280a01..ddc06658d 100644 --- a/internal/nonkube/compat/site_state_renderer.go +++ b/internal/nonkube/compat/site_state_renderer.go @@ -38,10 +38,19 @@ func (s *SiteStateRenderer) Render(loadedSiteState *api.SiteState, reload bool) } s.loadedSiteState = loadedSiteState endpoint := os.Getenv("CONTAINER_ENDPOINT") - if endpoint == "" { - endpoint = fmt.Sprintf("unix://%s/podman/podman.sock", api.GetRuntimeDir()) + + // the container endpoint is mapped to the podman socket inside the container + if api.IsRunningInContainer() { + endpoint = "unix:///var/run/podman.sock" if s.Platform == "docker" { - endpoint = "unix:///run/docker.sock" + endpoint = "unix:///var/run/docker.sock" + } + } else { + if endpoint == "" { + endpoint = fmt.Sprintf("unix://%s/podman/podman.sock", api.GetRuntimeDir()) + if s.Platform == "docker" { + endpoint = "unix:///run/docker.sock" + } } } s.cli, err = internalclient.NewCompatClient(endpoint, "") diff --git a/internal/nonkube/controller/input_resource_handler.go b/internal/nonkube/controller/input_resource_handler.go new file mode 100644 index 000000000..2e2b7630d --- /dev/null +++ b/internal/nonkube/controller/input_resource_handler.go @@ -0,0 +1,116 @@ +package controller + +import ( + "fmt" + "log/slog" + "os" + "strings" + "sync" + + "github.com/skupperproject/skupper/api/types" + "github.com/skupperproject/skupper/internal/cmd/skupper/common" + "github.com/skupperproject/skupper/internal/nonkube/bootstrap" + "github.com/skupperproject/skupper/internal/utils" + "github.com/skupperproject/skupper/pkg/nonkube/api" +) + +// This feature is responsible for handling the creation of input resources and +// execute the start/reload of the site configuration automatically. +type InputResourceHandler struct { + logger *slog.Logger + namespace string + inputPath string + Bootstrap func(config *bootstrap.Config) (*api.SiteState, error) + PostExec func(config *bootstrap.Config, siteState *api.SiteState) + ConfigBootstrap bootstrap.Config + lock sync.Mutex +} + +func NewInputResourceHandler(namespace string, inputPath string, bStrap func(config *bootstrap.Config) (*api.SiteState, error), postBootStrap func(config *bootstrap.Config, siteState *api.SiteState)) *InputResourceHandler { + + systemReloadType := utils.DefaultStr(os.Getenv(types.ENV_SYSTEM_AUTO_RELOAD), + types.SystemReloadTypeManual) + + if systemReloadType == types.SystemReloadTypeManual { + slog.Default().Debug("Automatic reloading is not configured.") + return nil + } + + handler := &InputResourceHandler{ + namespace: namespace, + inputPath: inputPath, + } + + handler.Bootstrap = bStrap + handler.PostExec = postBootStrap + + var binary string + + platform := types.Platform(utils.DefaultStr(os.Getenv("CONTAINER_ENGINE"), + string(types.PlatformPodman))) + + // TODO: add support for linux platform + switch common.Platform(platform) { + case common.PlatformDocker: + binary = "docker" + case common.PlatformPodman: + binary = "podman" + case common.PlatformLinux: + slog.Default().Error("Linux platform is not supported yet") + return nil + default: + slog.Default().Error("This platform value is not supported: ", slog.String("platform", string(platform))) + return nil + } + + handler.ConfigBootstrap = bootstrap.Config{ + Namespace: namespace, + InputPath: inputPath, + Platform: platform, + Binary: binary, + } + + handler.logger = slog.Default().With("component", "input.resource.handler", "namespace", namespace) + return handler +} + +func (h *InputResourceHandler) OnCreate(name string) { + h.logger.Info(fmt.Sprintf("Resource has been created: %s", name)) + err := h.processInputFile() + if err != nil { + h.logger.Error(err.Error()) + } +} + +// This function does not need to be implemented, given that when a file is updated, +// the event OnCreate is triggered anyway. Having it implemented would cause +// the resources to be reloaded multiple times, stopping and starting a router pod. +// (issue: the router pod is still active while going to be deleted, and the controller +// tries to create a new router pod, failing on this) +func (h *InputResourceHandler) OnUpdate(name string) {} +func (h *InputResourceHandler) OnRemove(name string) { + h.logger.Info(fmt.Sprintf("Resource has been deleted: %s", name)) + err := h.processInputFile() + if err != nil { + h.logger.Error(err.Error()) + } +} +func (h *InputResourceHandler) Filter(name string) bool { + return strings.HasSuffix(name, ".json") || strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml") +} + +func (h *InputResourceHandler) OnBasePathAdded(basePath string) {} + +func (h *InputResourceHandler) processInputFile() error { + h.lock.Lock() + defer h.lock.Unlock() + + siteState, err := h.Bootstrap(&h.ConfigBootstrap) + if err != nil { + return fmt.Errorf("Failed to bootstrap: %s", err) + } + + h.PostExec(&h.ConfigBootstrap, siteState) + + return nil +} diff --git a/internal/nonkube/controller/input_resource_handler_test.go b/internal/nonkube/controller/input_resource_handler_test.go new file mode 100644 index 000000000..fbbd53113 --- /dev/null +++ b/internal/nonkube/controller/input_resource_handler_test.go @@ -0,0 +1,149 @@ +package controller + +import ( + "fmt" + "log/slog" + "testing" + + "github.com/skupperproject/skupper/api/types" + "github.com/skupperproject/skupper/internal/nonkube/bootstrap" + "github.com/skupperproject/skupper/pkg/nonkube/api" + "gotest.tools/v3/assert" +) + +func TestInputResourceHandler(t *testing.T) { + + t.Run("handler created for docker platform", func(t *testing.T) { + t.Setenv(types.ENV_SYSTEM_AUTO_RELOAD, "auto") + t.Setenv("CONTAINER_ENGINE", "docker") + inputResourceHandler := NewInputResourceHandler("test_namespace", "test_inputPath", mockBootstrap, mockPostExec) + expectedConfigBootstrap := bootstrap.Config{ + Namespace: "test_namespace", + InputPath: "test_inputPath", + Platform: "docker", + Binary: "docker", + } + + assert.Assert(t, inputResourceHandler != nil) + assert.Assert(t, inputResourceHandler.inputPath == "test_inputPath") + assert.Assert(t, inputResourceHandler.ConfigBootstrap == expectedConfigBootstrap) + }) + + t.Run("handler created for podman platform", func(t *testing.T) { + t.Setenv(types.ENV_SYSTEM_AUTO_RELOAD, "auto") + t.Setenv("CONTAINER_ENGINE", "podman") + inputResourceHandler := NewInputResourceHandler("test_namespace", "test_inputPath", mockBootstrap, mockPostExec) + expectedConfigBootstrap := bootstrap.Config{ + Namespace: "test_namespace", + InputPath: "test_inputPath", + Platform: "podman", + Binary: "podman", + } + + assert.Assert(t, inputResourceHandler != nil) + assert.Assert(t, inputResourceHandler.inputPath == "test_inputPath") + assert.Assert(t, inputResourceHandler.ConfigBootstrap == expectedConfigBootstrap) + }) + + t.Run("handler not created for linux platform", func(t *testing.T) { + t.Setenv(types.ENV_SYSTEM_AUTO_RELOAD, "auto") + t.Setenv("CONTAINER_ENGINE", "linux") + inputResourceHandler := NewInputResourceHandler("test_namespace", "test_inputPath", mockBootstrap, mockPostExec) + + assert.Assert(t, inputResourceHandler == nil) + + }) + + t.Run("handler not created because the system reload is configured to be manual", func(t *testing.T) { + t.Setenv(types.ENV_SYSTEM_AUTO_RELOAD, "manual") + inputResourceHandler := NewInputResourceHandler("test_namespace", "test_inputPath", mockBootstrap, mockPostExec) + + assert.Assert(t, inputResourceHandler == nil) + + }) + + t.Run("handler not created for unknown platform", func(t *testing.T) { + t.Setenv("CONTAINER_ENGINE", "unknown") + inputResourceHandler := NewInputResourceHandler("test_namespace", "test_inputPath", mockBootstrap, mockPostExec) + + assert.Assert(t, inputResourceHandler == nil) + + }) + + t.Run("resource file created or updated", func(t *testing.T) { + t.Setenv(types.ENV_SYSTEM_AUTO_RELOAD, "auto") + namespace := "test-file-created-ns" + inputPath := "test-file-created-input-path" + + handler := NewInputResourceHandler(namespace, inputPath, mockBootstrap, mockPostExec) + + logSpy := &testLogHandler{ + handler: slog.Default().Handler(), + } + handler.logger = slog.New(logSpy) + + resourceName := "site.yaml" + handler.OnCreate(resourceName) + + expectedMsg := fmt.Sprintf("Resource has been created: %s", resourceName) + if count := logSpy.Count(expectedMsg); count != 1 { + t.Errorf("Expected log '%s' to be present, but found count: %d", expectedMsg, count) + } + + }) + + t.Run("resource file removed", func(t *testing.T) { + t.Setenv(types.ENV_SYSTEM_AUTO_RELOAD, "auto") + namespace := "test-file-ns" + inputPath := "test-file-input-path" + + handler := NewInputResourceHandler(namespace, inputPath, mockBootstrap, mockPostExec) + + logSpy := &testLogHandler{ + handler: slog.Default().Handler(), + } + handler.logger = slog.New(logSpy) + + resourceName := "site.yaml" + handler.OnRemove(resourceName) + + expectedMsg := fmt.Sprintf("Resource has been deleted: %s", resourceName) + if count := logSpy.Count(expectedMsg); count != 1 { + t.Errorf("Expected log '%s' to be present, but found count: %d", expectedMsg, count) + } + }) + + t.Run("resource file created or updated but the reload fails", func(t *testing.T) { + t.Setenv(types.ENV_SYSTEM_AUTO_RELOAD, "auto") + namespace := "test-file-created-ns" + inputPath := "test-file-created-input-path" + + handler := NewInputResourceHandler(namespace, inputPath, mockBootstrapFailed, mockPostExec) + + logSpy := &testLogHandler{ + handler: slog.Default().Handler(), + } + handler.logger = slog.New(logSpy) + + resourceName := "site.yaml" + handler.OnCreate(resourceName) + + expectedMsg := fmt.Sprintf("Failed to bootstrap: failed to bootstrap") + if count := logSpy.Count(expectedMsg); count != 1 { + t.Errorf("Expected log '%s' to be present, but found count: %d", expectedMsg, count) + } + + }) + +} + +func mockBootstrap(config *bootstrap.Config) (*api.SiteState, error) { + return api.NewSiteState(false), nil +} +func mockPostExec(config *bootstrap.Config, siteState *api.SiteState) { + fmt.Println("post bootstrap execution completed") +} + +func mockBootstrapFailed(config *bootstrap.Config) (*api.SiteState, error) { + return nil, fmt.Errorf("failed to bootstrap") +} diff --git a/internal/nonkube/controller/namespace_controller.go b/internal/nonkube/controller/namespace_controller.go index d8c483b0d..ff00c1423 100644 --- a/internal/nonkube/controller/namespace_controller.go +++ b/internal/nonkube/controller/namespace_controller.go @@ -5,21 +5,27 @@ import ( "log/slog" "github.com/skupperproject/skupper/internal/filesystem" + "github.com/skupperproject/skupper/internal/nonkube/bootstrap" + "github.com/skupperproject/skupper/internal/nonkube/client/fs" "github.com/skupperproject/skupper/pkg/nonkube/api" ) type NamespaceController struct { - ns string - stopCh chan struct{} - logger *slog.Logger - watcher *filesystem.FileWatcher - prepare func() + ns string + stopCh chan struct{} + logger *slog.Logger + watcher *filesystem.FileWatcher + prepare func() + pathProvider fs.PathProvider } func NewNamespaceController(namespace string) (*NamespaceController, error) { nsw := &NamespaceController{ ns: namespace, stopCh: make(chan struct{}), + pathProvider: fs.PathProvider{ + Namespace: namespace, + }, } watcher, err := filesystem.NewWatcher(slog.String("namespace", namespace)) if err != nil { @@ -39,8 +45,14 @@ func (w *NamespaceController) Start() { routerConfigHandler.AddCallback(routerStateHandler) collectorLifecycleHandler := NewCollectorLifecycleHandler(w.ns) routerStateHandler.SetCallback(collectorLifecycleHandler) + inputResourceHandler := NewInputResourceHandler(w.ns, w.pathProvider.GetNamespace(), bootstrap.Bootstrap, bootstrap.PostBootstrap) + w.watcher.Add(api.GetInternalOutputPath(w.ns, api.RouterConfigPath), routerConfigHandler) w.watcher.Add(api.GetInternalOutputPath(w.ns, api.RuntimeSiteStatePath), NewNetworkStatusHandler(w.ns)) + + if inputResourceHandler != nil { + w.watcher.Add(w.pathProvider.GetNamespace(), inputResourceHandler) + } } else { w.prepare() }