From b34c2a9410e0fa67926103b772e1cc63f09cd75f Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Sun, 5 Jul 2026 14:51:34 +0200 Subject: [PATCH 01/18] non kube network observer installation --- internal/cmd/skupper/common/flags.go | 13 + .../system/kube/system_network-observer.go | 39 ++ .../system/nonkube/system_network-observer.go | 100 ++++ .../nonkube/system_network-observer_test.go | 156 +++++++ internal/cmd/skupper/system/system.go | 34 ++ internal/cmd/skupper/system/system_test.go | 9 + internal/images/image_utils.go | 24 + internal/images/images.go | 4 +- .../nonkube/network-observer/certificates.go | 52 +++ .../network-observer/config_templates.go | 47 ++ .../nonkube/network-observer/containers.go | 114 +++++ .../nonkube/network-observer/installer.go | 434 ++++++++++++++++++ internal/nonkube/network-observer/password.go | 29 ++ .../nonkube/network-observer/password_test.go | 66 +++ internal/nonkube/network-observer/systemd.go | 342 ++++++++++++++ 15 files changed, 1462 insertions(+), 1 deletion(-) create mode 100644 internal/cmd/skupper/system/kube/system_network-observer.go create mode 100644 internal/cmd/skupper/system/nonkube/system_network-observer.go create mode 100644 internal/cmd/skupper/system/nonkube/system_network-observer_test.go create mode 100644 internal/nonkube/network-observer/certificates.go create mode 100644 internal/nonkube/network-observer/config_templates.go create mode 100644 internal/nonkube/network-observer/containers.go create mode 100644 internal/nonkube/network-observer/installer.go create mode 100644 internal/nonkube/network-observer/password.go create mode 100644 internal/nonkube/network-observer/password_test.go create mode 100644 internal/nonkube/network-observer/systemd.go diff --git a/internal/cmd/skupper/common/flags.go b/internal/cmd/skupper/common/flags.go index f1929880c..af16eabc1 100644 --- a/internal/cmd/skupper/common/flags.go +++ b/internal/cmd/skupper/common/flags.go @@ -92,6 +92,13 @@ for other Kubernetes flavors, loadbalancer is the default.` FlagNameReloadType = "reload-type" FlagDescReloadType = "Specify the type of reload to perform. Choices: manual, auto" + + FlagNameNetworkObserverUsername = "username" + FlagDescNetworkObserverUsername = "Username for HTTP Basic Auth (default: skupper)" + FlagNameNetworkObserverPassword = "password" + FlagDescNetworkObserverPassword = "Password for HTTP Basic Auth (auto-generated if not provided)" + FlagNameNetworkObserverUninstall = "uninstall" + FlagDescNetworkObserverUninstall = "Uninstall Network Observer and related containers" ) type CommandSiteCreateFlags struct { @@ -245,6 +252,12 @@ type CommandListenerGenerateFlags struct { Output string } +type CommandNetworkObserverFlags struct { + Username string + Password string + Uninstall bool +} + type CommandVersionFlags struct { Output string } diff --git a/internal/cmd/skupper/system/kube/system_network-observer.go b/internal/cmd/skupper/system/kube/system_network-observer.go new file mode 100644 index 000000000..c77858c1d --- /dev/null +++ b/internal/cmd/skupper/system/kube/system_network-observer.go @@ -0,0 +1,39 @@ +package kube + +import ( + "fmt" + + "github.com/skupperproject/skupper/internal/cmd/skupper/common" + "github.com/spf13/cobra" +) + +type CmdSystemNetworkObserver struct { + CobraCmd *cobra.Command + Flags *common.CommandNetworkObserverFlags + namespace string + user string + password string +} + +func NewCmdCmdSystemNetworkObserver() *CmdSystemNetworkObserver { + return &CmdSystemNetworkObserver{} +} + +func (cmd *CmdSystemNetworkObserver) NewClient(cobraCommand *cobra.Command, args []string) {} + +func (cmd *CmdSystemNetworkObserver) ValidateInput(args []string) error { + return nil +} + +func (cmd *CmdSystemNetworkObserver) InputToOptions() { + +} + +func (cmd *CmdSystemNetworkObserver) Run() error { + fmt.Println("This command does not support kubernetes platforms.") + return nil +} + +func (cmd *CmdSystemNetworkObserver) WaitUntil() error { + return nil +} diff --git a/internal/cmd/skupper/system/nonkube/system_network-observer.go b/internal/cmd/skupper/system/nonkube/system_network-observer.go new file mode 100644 index 000000000..5a4befb56 --- /dev/null +++ b/internal/cmd/skupper/system/nonkube/system_network-observer.go @@ -0,0 +1,100 @@ +package nonkube + +import ( + "errors" + "fmt" + + "github.com/skupperproject/skupper/internal/cmd/skupper/common" + networkobserver "github.com/skupperproject/skupper/internal/nonkube/network-observer" + "github.com/spf13/cobra" +) + +type CmdSystemNetworkObserver struct { + CobraCmd *cobra.Command + Flags *common.CommandNetworkObserverFlags + namespace string + user string + password string +} + +func NewCmdSystemNetworkObserver() *CmdSystemNetworkObserver { + return &CmdSystemNetworkObserver{} +} + +func (cmd *CmdSystemNetworkObserver) NewClient(cobraCommand *cobra.Command, args []string) { + if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace) != nil { + cmd.namespace = cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String() + } + if cmd.namespace == "" { + cmd.namespace = "default" + } +} + +func (cmd *CmdSystemNetworkObserver) ValidateInput(args []string) error { + var validationErrors []error + + if len(args) > 0 { + validationErrors = append(validationErrors, fmt.Errorf("this command does not accept arguments")) + } + + if cmd.Flags != nil && cmd.Flags.Uninstall { + if cmd.Flags.Password != "" { + validationErrors = append(validationErrors, fmt.Errorf("--%s cannot be used with --%s", common.FlagNameNetworkObserverPassword, common.FlagNameNetworkObserverUninstall)) + } + } + + return errors.Join(validationErrors...) +} + +func (cmd *CmdSystemNetworkObserver) InputToOptions() { + + if cmd.Flags.Username != "" { + cmd.user = cmd.Flags.Username + } + + if cmd.Flags.Password != "" { + cmd.password = cmd.Flags.Password + } + +} + +func (cmd *CmdSystemNetworkObserver) Run() error { + installer, err := networkobserver.NewInstaller(cmd.namespace, cmd.user, cmd.password) + if err != nil { + return fmt.Errorf("failed to create installer: %w", err) + } + + if cmd.Flags.Uninstall { + + if err := installer.ValidatePrerequisitesForUninstall(); err != nil { + return err + } + + if err := installer.Uninstall(); err != nil { + return fmt.Errorf("uninstallation failed: %w", err) + } + + return nil + } + + if err := installer.ValidatePrerequisitesForInstall(); err != nil { + return fmt.Errorf("prerequisite validation failed: %w", err) + } + + result, err := installer.Install() + if err != nil { + return fmt.Errorf("installation failed: %w", err) + } + + fmt.Println("Network observer installed successfully!") + fmt.Printf("\nAccess URL: %s\n", result.URL) + fmt.Printf("Username: %s\n", result.Username) + fmt.Printf("Password: %s\n", result.Password) + fmt.Println("\nNote: Save these credentials securely.") + + return nil +} + +func (cmd *CmdSystemNetworkObserver) WaitUntil() error { + return nil +} diff --git a/internal/cmd/skupper/system/nonkube/system_network-observer_test.go b/internal/cmd/skupper/system/nonkube/system_network-observer_test.go new file mode 100644 index 000000000..8f5a78929 --- /dev/null +++ b/internal/cmd/skupper/system/nonkube/system_network-observer_test.go @@ -0,0 +1,156 @@ +package nonkube + +import ( + "strings" + "testing" + + "github.com/skupperproject/skupper/internal/cmd/skupper/common" + "github.com/spf13/cobra" +) + +func TestCmdSystemNetworkObserverValidateInput(t *testing.T) { + tests := []struct { + name string + args []string + flags *common.CommandNetworkObserverFlags + expectedError []string + }{ + { + name: "rejects arguments", + args: []string{"extra"}, + expectedError: []string{"this command does not accept arguments"}, + }, + { + name: "rejects credentials with uninstall", + flags: &common.CommandNetworkObserverFlags{ + Uninstall: true, + Password: "password", + }, + expectedError: []string{ + "--password cannot be used with --uninstall", + }, + }, + { + name: "allows install credentials", + flags: &common.CommandNetworkObserverFlags{ + Username: "user", + Password: "password", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmd := &CmdSystemNetworkObserver{Flags: test.flags} + + err := cmd.ValidateInput(test.args) + if len(test.expectedError) == 0 { + if err != nil { + t.Fatalf("expected no error, got %q", err.Error()) + } + return + } + + if err == nil { + t.Fatal("expected error") + } + for _, expected := range test.expectedError { + if !strings.Contains(err.Error(), expected) { + t.Fatalf("expected validation error %q, got %q", expected, err.Error()) + } + } + }) + } +} + +func TestCmdSystemNetworkObserverNewClient(t *testing.T) { + tests := []struct { + name string + flagNamespace string + initialNamespace string + expectedNamespace string + }{ + { + name: "uses namespace flag", + flagNamespace: "west", + expectedNamespace: "west", + }, + { + name: "defaults namespace", + expectedNamespace: "default", + }, + { + name: "defaults even when namespace was preset without flag", + initialNamespace: "east", + expectedNamespace: "default", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cobraCmd := &cobra.Command{} + cobraCmd.Flags().String(common.FlagNameNamespace, "", "") + if test.flagNamespace != "" { + if err := cobraCmd.Flags().Set(common.FlagNameNamespace, test.flagNamespace); err != nil { + t.Fatalf("failed to set namespace flag: %v", err) + } + } + + cmd := &CmdSystemNetworkObserver{ + CobraCmd: cobraCmd, + namespace: test.initialNamespace, + } + + cmd.NewClient(cobraCmd, nil) + + if cmd.namespace != test.expectedNamespace { + t.Fatalf("expected namespace %q, got %q", test.expectedNamespace, cmd.namespace) + } + }) + } +} + +func TestCmdSystemNetworkObserverInputToOptions(t *testing.T) { + tests := []struct { + name string + flags *common.CommandNetworkObserverFlags + initialUser string + initialPassword string + expectedUser string + expectedPassword string + }{ + { + name: "copies credentials from flags", + flags: &common.CommandNetworkObserverFlags{Username: "user", Password: "password"}, + expectedUser: "user", + expectedPassword: "password", + }, + { + name: "leaves existing values when flags are empty", + flags: &common.CommandNetworkObserverFlags{}, + initialUser: "existing-user", + initialPassword: "existing-password", + expectedUser: "existing-user", + expectedPassword: "existing-password", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmd := &CmdSystemNetworkObserver{ + Flags: test.flags, + user: test.initialUser, + password: test.initialPassword, + } + + cmd.InputToOptions() + + if cmd.user != test.expectedUser { + t.Fatalf("expected user %q, got %q", test.expectedUser, cmd.user) + } + if cmd.password != test.expectedPassword { + t.Fatalf("expected password %q, got %q", test.expectedPassword, cmd.password) + } + }) + } +} diff --git a/internal/cmd/skupper/system/system.go b/internal/cmd/skupper/system/system.go index c4ef0a0d8..0a10f5ab5 100644 --- a/internal/cmd/skupper/system/system.go +++ b/internal/cmd/skupper/system/system.go @@ -35,6 +35,7 @@ approach, which is based on the new set of Custom Resource Definitions (CRDs).`, cmd.AddCommand(CmdSystemGenerateBundleFactory(platform)) cmd.AddCommand(CmdSystemApplyFactory(platform)) cmd.AddCommand(CmdSystemDeleteFactory(platform)) + cmd.AddCommand(CmdSystemNetworkObserverFactory(platform)) return cmd } @@ -230,3 +231,36 @@ func CmdSystemDeleteFactory(configuredPlatform common.Platform) *cobra.Command { return cmd } + +func CmdSystemNetworkObserverFactory(configuredPlatform common.Platform) *cobra.Command { + + //This implementation will warn the user that the command is not available for Kubernetes environments. + kubeCommand := kube.NewCmdCmdSystemNetworkObserver() + nonKubeCommand := nonkube.NewCmdSystemNetworkObserver() + + cmdDesc := common.SkupperCmdDescription{ + Use: "network-observer", + Short: "Install the network observer", + Long: `Install the Skupper network observer to collect and expose network metrics. +The network observer requires an existing Skupper site and will deploy three containers: +- network-observer: Collects metrics from the router +- prometheus: Stores metrics +- nginx: Provides HTTPS access with HTTP Basic Auth`, + Example: `skupper network-observer --namespace west +skupper network-observer -n west --username admin --password secret`, + } + + cmd := common.ConfigureCobraCommand(configuredPlatform, cmdDesc, nil, nonKubeCommand) + + cmdFlags := common.CommandNetworkObserverFlags{} + cmd.Flags().StringVar(&cmdFlags.Username, common.FlagNameNetworkObserverUsername, "skupper", common.FlagDescNetworkObserverUsername) + cmd.Flags().StringVar(&cmdFlags.Password, common.FlagNameNetworkObserverPassword, "", common.FlagDescNetworkObserverPassword) + cmd.Flags().BoolVar(&cmdFlags.Uninstall, common.FlagNameNetworkObserverUninstall, false, common.FlagDescNetworkObserverUninstall) + + kubeCommand.CobraCmd = cmd + kubeCommand.Flags = &cmdFlags + nonKubeCommand.CobraCmd = cmd + nonKubeCommand.Flags = &cmdFlags + + return cmd +} diff --git a/internal/cmd/skupper/system/system_test.go b/internal/cmd/skupper/system/system_test.go index 0db8bdac2..8d8d8310c 100644 --- a/internal/cmd/skupper/system/system_test.go +++ b/internal/cmd/skupper/system/system_test.go @@ -69,6 +69,15 @@ func TestCmdSystemFactory(t *testing.T) { }, command: CmdSystemDeleteFactory(common.PlatformKubernetes), }, + { + name: "CmdSystemNetworkObserverFactory", + expectedFlagsWithDefaultValue: map[string]interface{}{ + common.FlagNameNetworkObserverUsername: "skupper", + common.FlagNameNetworkObserverPassword: "", + common.FlagNameNetworkObserverUninstall: "false", + }, + command: CmdSystemNetworkObserverFactory(common.PlatformDocker), + }, } for _, test := range testTable { diff --git a/internal/images/image_utils.go b/internal/images/image_utils.go index 62f5b4308..9eb912680 100644 --- a/internal/images/image_utils.go +++ b/internal/images/image_utils.go @@ -26,12 +26,14 @@ const ( CliImageEnvKey string = "SKUPPER_CLI_IMAGE" SystemControllerImageEnvKey string = "SKUPPER_SYSTEM_CONTROLLER_IMAGE" PrometheusServerImageEnvKey string = "PROMETHEUS_SERVER_IMAGE" + NginxImageEnvKey string = "NGINX_IMAGE" OauthProxyImageEnvKey string = "OAUTH_PROXY_IMAGE" RouterPullPolicyEnvKey string = "SKUPPER_ROUTER_IMAGE_PULL_POLICY" KubeAdaptorPullPolicyEnvKey string = "SKUPPER_KUBE_ADAPTOR_IMAGE_PULL_POLICY" OauthProxyPullPolicyEnvKey string = "OAUTH_PROXY_IMAGE_PULL_POLICY" SkupperImageRegistryEnvKey string = "SKUPPER_IMAGE_REGISTRY" PrometheusImageRegistryEnvKey string = "PROMETHEUS_IMAGE_REGISTRY" + NginxImageRegistryEnvKey string = "NGINX_IMAGE_REGISTRY" OauthProxyRegistryEnvKey string = "OAUTH_PROXY_IMAGE_REGISTRY" ) @@ -139,6 +141,20 @@ func GetPrometheusServerImageName() string { } } +func GetPrometheusImageName() string { + return GetPrometheusServerImageName() +} + +func GetNginxImageName() string { + image := os.Getenv(NginxImageEnvKey) + if image == "" { + imageRegistry := GetNginxImageRegistry() + return strings.Join([]string{imageRegistry, NginxImageName}, "/") + } else { + return image + } +} + func GetSystemControllerImageName() string { image := os.Getenv(SystemControllerImageEnvKey) if image == "" { @@ -165,6 +181,14 @@ func GetPrometheusImageRegistry() string { return imageRegistry } +func GetNginxImageRegistry() string { + imageRegistry := os.Getenv(NginxImageRegistryEnvKey) + if imageRegistry == "" { + return NginxImageRegistry + } + return imageRegistry +} + func CreateMapImageDigest(runningPods map[string]string) map[string]string { imagesToRetrieve := map[string]string{ "router": GetRouterImageName(), diff --git a/internal/images/images.go b/internal/images/images.go index 5f268d024..645a7dff6 100644 --- a/internal/images/images.go +++ b/internal/images/images.go @@ -10,7 +10,9 @@ const ( SystemControllerImageName string = "system-controller:v2-dev" PrometheusImageRegistry string = "quay.io/prometheus" - PrometheusServerImageName string = "prometheus:v2.42.0" + PrometheusServerImageName string = "prometheus:v3.11.3" + NginxImageRegistry string = "mirror.gcr.io/nginxinc" + NginxImageName string = "nginx-unprivileged:1.31.0-alpine" OauthProxyImageRegistry string = "quay.io/openshift" OauthProxyImageName string = "origin-oauth-proxy:4.14.0" ) diff --git a/internal/nonkube/network-observer/certificates.go b/internal/nonkube/network-observer/certificates.go new file mode 100644 index 000000000..82e6606e7 --- /dev/null +++ b/internal/nonkube/network-observer/certificates.go @@ -0,0 +1,52 @@ +package networkobserver + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/skupperproject/skupper/internal/certs" + corev1 "k8s.io/api/core/v1" +) + +func GenerateNginxCert(caDir, certDir string) error { + caSecret, err := loadSecretFromDir(caDir) + if err != nil { + return fmt.Errorf("failed to load skupper-local-ca: %w", err) + } + + secret, err := certs.GenerateSecret("skupper-network-observer", "skupper-network-observer", []string{"localhost"}, 0, caSecret) + if err != nil { + return fmt.Errorf("failed to generate nginx certificate: %w", err) + } + + if err := os.MkdirAll(certDir, 0755); err != nil { + return fmt.Errorf("failed to create certificate directory: %w", err) + } + + if err := os.WriteFile(filepath.Join(certDir, "tls.crt"), secret.Data["tls.crt"], 0644); err != nil { + return fmt.Errorf("failed to write tls.crt: %w", err) + } + if err := os.WriteFile(filepath.Join(certDir, "tls.key"), secret.Data["tls.key"], 0600); err != nil { + return fmt.Errorf("failed to write tls.key: %w", err) + } + + return nil +} + +func loadSecretFromDir(dir string) (*corev1.Secret, error) { + crt, err := os.ReadFile(filepath.Join(dir, "tls.crt")) + if err != nil { + return nil, err + } + key, err := os.ReadFile(filepath.Join(dir, "tls.key")) + if err != nil { + return nil, err + } + return &corev1.Secret{ + Data: map[string][]byte{ + "tls.crt": crt, + "tls.key": key, + }, + }, nil +} diff --git a/internal/nonkube/network-observer/config_templates.go b/internal/nonkube/network-observer/config_templates.go new file mode 100644 index 000000000..0016f52d2 --- /dev/null +++ b/internal/nonkube/network-observer/config_templates.go @@ -0,0 +1,47 @@ +package networkobserver + +import "fmt" + +func RenderPrometheusConfig(netobsPort int) string { + return fmt.Sprintf(`global: + scrape_interval: 15s + evaluation_interval: 15s +alerting: + alertmanagers: + - static_configs: + - targets: +scrape_configs: + - job_name: "network-observer-local" + scheme: http + follow_redirects: true + enable_http2: true + static_configs: + - targets: ["localhost:%d"] +`, netobsPort) +} + +func RenderNginxConfig(nginxPort, netobsPort int) string { + return fmt.Sprintf(`ssl_session_cache shared:SSL:10m; +ssl_session_timeout 10m; + +server { + listen %d ssl; + keepalive_timeout 70; + + ssl_certificate /etc/certificates/tls.crt; + ssl_certificate_key /etc/certificates/tls.key; + ssl_protocols TLSv1.3; + add_header Strict-Transport-Security "max-age=63072000" always; + + auth_basic "Skupper"; + auth_basic_user_file /etc/httpusers/htpasswd; + + location /api/ { + proxy_pass http://127.0.0.1:%d; + } + location / { + proxy_pass http://127.0.0.1:%d; + } +} +`, nginxPort, netobsPort, netobsPort) +} diff --git a/internal/nonkube/network-observer/containers.go b/internal/nonkube/network-observer/containers.go new file mode 100644 index 000000000..9b24fec3e --- /dev/null +++ b/internal/nonkube/network-observer/containers.go @@ -0,0 +1,114 @@ +package networkobserver + +import ( + "fmt" + "path/filepath" + + "github.com/skupperproject/skupper/internal/images" + "github.com/skupperproject/skupper/pkg/container" + "github.com/skupperproject/skupper/pkg/nonkube/api" +) + +func GetNetworkObserverContainer(namespace string, p ports) container.Container { + namespacePath := api.GetHostNamespaceHome(namespace) + clientCertsPath := filepath.Join(namespacePath, string(api.CertificatesPath), "skupper-local-client") + + return container.Container{ + Name: fmt.Sprintf("%s-skupper-network-observer", namespace), + Image: images.GetNetworkObserverImageName(), + Command: []string{ + fmt.Sprintf("-listen=127.0.0.1:%d", p.netobs), + fmt.Sprintf("-prometheus-api=http://127.0.0.1:%d", p.prometheus), + "-router-endpoint=amqps://127.0.0.1:5671", + "-router-tls-ca=/etc/messaging/ca.crt", + "-router-tls-cert=/etc/messaging/tls.crt", + "-router-tls-key=/etc/messaging/tls.key", + fmt.Sprintf("-listen-metrics=:%d", p.metrics), + }, + Env: map[string]string{}, + Labels: map[string]string{ + "application": "skupper-v2", + "skupper.io/v2-component": "network-observer", + }, + FileMounts: []container.FileMount{ + { + Source: clientCertsPath, + Destination: "/etc/messaging", + Options: []string{"z"}, + }, + }, + Networks: map[string]container.ContainerNetworkInfo{}, + RestartPolicy: "always", + } +} + +func GetPrometheusContainer(namespace string, p ports) container.Container { + namespacePath := api.GetHostNamespaceHome(namespace) + prometheusDir := filepath.Join(namespacePath, "network-observer", "prometheus") + dataPath := filepath.Join(namespacePath, "network-observer", "prometheus", "data") + + return container.Container{ + Name: fmt.Sprintf("%s-skupper-prometheus", namespace), + Image: images.GetPrometheusImageName(), + Command: []string{ + "--config.file=/etc/prometheus/prometheus.yml", + "--storage.tsdb.path=/prometheus/", + fmt.Sprintf("--web.listen-address=:%d", p.prometheus), + }, + Env: map[string]string{}, + Labels: map[string]string{ + "application": "skupper-v2", + "skupper.io/v2-component": "prometheus", + }, + FileMounts: []container.FileMount{ + { + Source: prometheusDir, + Destination: "/etc/prometheus", + Options: []string{"z"}, + }, + { + Source: dataPath, + Destination: "/prometheus", + Options: []string{"z"}, + }, + }, + Networks: map[string]container.ContainerNetworkInfo{}, + RestartPolicy: "always", + } +} + +func GetNginxContainer(namespace string) container.Container { + namespacePath := api.GetHostNamespaceHome(namespace) + nginxConfDir := filepath.Join(namespacePath, "network-observer", "nginx", "conf.d") + htpasswdDir := filepath.Join(namespacePath, "network-observer", "htpasswd") + certsPath := filepath.Join(namespacePath, "network-observer", "certs") + + return container.Container{ + Name: fmt.Sprintf("%s-skupper-nginx", namespace), + Image: images.GetNginxImageName(), + Env: map[string]string{}, + Labels: map[string]string{ + "application": "skupper-v2", + "skupper.io/v2-component": "nginx-proxy", + }, + FileMounts: []container.FileMount{ + { + Source: nginxConfDir, + Destination: "/etc/nginx/conf.d", + Options: []string{"z"}, + }, + { + Source: certsPath, + Destination: "/etc/certificates", + Options: []string{"z"}, + }, + { + Source: htpasswdDir, + Destination: "/etc/httpusers", + Options: []string{"z"}, + }, + }, + Networks: map[string]container.ContainerNetworkInfo{}, + RestartPolicy: "always", + } +} diff --git a/internal/nonkube/network-observer/installer.go b/internal/nonkube/network-observer/installer.go new file mode 100644 index 000000000..3cb1e7c41 --- /dev/null +++ b/internal/nonkube/network-observer/installer.go @@ -0,0 +1,434 @@ +package networkobserver + +import ( + "context" + "fmt" + "log/slog" + "os" + "os/exec" + "os/user" + "path/filepath" + "strconv" + + "github.com/skupperproject/skupper/api/types" + "github.com/skupperproject/skupper/internal/config" + "github.com/skupperproject/skupper/internal/nonkube/client/compat" + "github.com/skupperproject/skupper/internal/nonkube/client/fs" + "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" +) + +type ports struct { + nginx int + prometheus int + netobs int + metrics int +} + +type Installer struct { + Namespace string + Username string + Password string + Platform string + ports ports + logger *slog.Logger + cli *compat.CompatClient + siteHandler *fs.SiteHandler +} + +type InstallResult struct { + URL string + Username string + Password string +} + +func NewInstaller(namespace string, username string, password string) (*Installer, error) { + selectedPlatform, err := detectPlatform() + if err != nil { + return nil, err + } + containerEndpoint, err := getContainerEndpoint(selectedPlatform) + if err != nil { + return nil, err + } + + compatClient, err := compat.NewCompatClient(containerEndpoint, "") + if err != nil { + return nil, fmt.Errorf("failed to create container client: %v", err) + } + + return &Installer{ + Namespace: namespace, + Username: username, + Password: password, + Platform: selectedPlatform, + logger: slog.Default().With("component", "network.observer.installer"), + siteHandler: fs.NewSiteHandler(namespace), + cli: compatClient, + }, nil +} + +func (i *Installer) ValidatePrerequisitesForInstall() error { + i.logger.Info("Validating prerequisites", slog.String("namespace", i.Namespace)) + namespacePath := api.GetHostNamespaceHome(i.Namespace) + + if _, err := os.Stat(namespacePath); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("namespace %q not found", i.Namespace) + } + return err + } + + containerNames := []string{ + fmt.Sprintf("%s-skupper-prometheus", i.Namespace), + fmt.Sprintf("%s-skupper-network-observer", i.Namespace), + fmt.Sprintf("%s-skupper-nginx", i.Namespace), + } + + for _, containerName := range containerNames { + if i.isContainerRunning(containerName) { + return fmt.Errorf("container %q is already running in %s", containerName, i.Platform) + } + } + + sites, err := i.siteHandler.List(fs.GetOptions{InputOnly: true}) + if err != nil { + return err + } else { + if len(sites) == 0 { + return fmt.Errorf("required site not found") + } + } + + clientCertsPath := filepath.Join(namespacePath, string(api.CertificatesPath), "skupper-local-client") + requiredCerts := []string{"ca.crt", "tls.crt", "tls.key"} + for _, cert := range requiredCerts { + certPath := filepath.Join(clientCertsPath, cert) + if _, err := os.Stat(certPath); os.IsNotExist(err) { + return fmt.Errorf("required certificate not found: %s", certPath) + } + } + + return nil +} + +func (i *Installer) Install() (*InstallResult, error) { + + i.logger.Info("Starting network observer installation", slog.String("namespace", i.Namespace)) + + if err := i.createDirectories(); err != nil { + return nil, fmt.Errorf("failed to create directories: %w", err) + } + + if err := i.generateConfigurations(); err != nil { + return nil, fmt.Errorf("failed to generate configurations: %w", err) + } + + if err := i.generateCertificates(); err != nil { + return nil, fmt.Errorf("failed to generate certificates: %w", err) + } + + systemdGlobal, err := common.NewSystemdGlobal(i.Platform) + if err != nil { + return nil, err + } + + err = systemdGlobal.Enable() + if err != nil { + return nil, err + } + + err = i.installContainer(GetNetworkObserverContainer(i.Namespace, i.ports)) + if err != nil { + return nil, err + } + err = i.installContainer(GetPrometheusContainer(i.Namespace, i.ports)) + if err != nil { + return nil, err + } + err = i.installContainer(GetNginxContainer(i.Namespace)) + if err != nil { + return nil, err + } + + err = i.createSystemdServices() + if err != nil { + return nil, fmt.Errorf("failed to create systemd services: %w", err) + } + + i.logger.Info("Network observer installation completed successfully") + + generatedPassword, err := i.generateHtpasswd() + if err != nil { + return nil, fmt.Errorf("failed to generate htpasswd: %w", err) + } + + return &InstallResult{ + URL: fmt.Sprintf("https://localhost:%d", i.ports.nginx), + Username: i.Username, + Password: generatedPassword, + }, nil +} + +func (i *Installer) ValidatePrerequisitesForUninstall() error { + + containerNames := []string{ + fmt.Sprintf("%s-skupper-prometheus", i.Namespace), + fmt.Sprintf("%s-skupper-network-observer", i.Namespace), + fmt.Sprintf("%s-skupper-nginx", i.Namespace), + } + + containersAreRunning := false + for _, containerName := range containerNames { + if i.isContainerRunning(containerName) { + containersAreRunning = true + } + } + + if !containersAreRunning { + return fmt.Errorf("network observer containers not running in namespace %q, there is nothing to uninstall", i.Namespace) + } + + return nil +} + +func (i *Installer) Uninstall() error { + i.logger.Info("Uninstalling network observer", slog.String("namespace", i.Namespace)) + + manager := NewSystemdServiceManager(i.Namespace, i.Platform, ports{}) + if err := manager.RemoveServices(); err != nil { + i.logger.Warn("Failed to remove systemd services", slog.Any("error", err)) + } + + containerNames := []string{ + fmt.Sprintf("%s-skupper-nginx", i.Namespace), + fmt.Sprintf("%s-skupper-network-observer", i.Namespace), + fmt.Sprintf("%s-skupper-prometheus", i.Namespace), + } + for _, name := range containerNames { + if i.isContainerRunning(name) { + if err := i.cli.ContainerStop(name); err != nil { + i.logger.Warn("Failed to stop container", slog.String("name", name), slog.String("error", err.Error())) + } + } + + if err := i.cli.ContainerRemove(name); err != nil { + i.logger.Warn("Failed to remove container", slog.String("name", name), slog.Any("error", err)) + } + } + + namespacePath := api.GetHostNamespaceHome(i.Namespace) + dataDir := filepath.Join(namespacePath, "network-observer") + if err := os.RemoveAll(dataDir); err != nil { + i.logger.Warn("Failed to remove network-observer data directory", slog.String("path", dataDir), slog.Any("error", err)) + } + + i.logger.Info("Network observer uninstalled successfully") + return nil +} + +func detectPlatform() (string, error) { + platform := config.GetPlatform() + + if platform != types.PlatformDocker && platform != types.PlatformPodman { + return "", fmt.Errorf("unsupported platform %q for network observer", platform) + } + + switch platform { + case "docker": + _, err := exec.LookPath("docker") + if err != nil { + return "", fmt.Errorf("docker not found") + } + + default: + _, err := exec.LookPath("podman") + if err != nil { + return "", fmt.Errorf("podman not found") + } + + } + + return string(platform), nil +} + +func getContainerEndpoint(platform string) (string, error) { + currentUser, err := user.Current() + if err != nil { + return "", fmt.Errorf("Failed to get current user: %v", err) + } + uid := currentUser.Uid + uidInt, _ := strconv.Atoi(uid) + + xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR") + if xdgRuntimeDir == "" { + xdgRuntimeDir = fmt.Sprintf("/run/user/%s", uid) + } + + containerEndpointDefault := os.Getenv("CONTAINER_ENDPOINT") + + if containerEndpointDefault == "" { + + if platform == "docker" { + containerEndpointDefault = "unix:///run/docker.sock" + } else { + + containerEndpointDefault = fmt.Sprintf("unix://%s/podman/podman.sock", xdgRuntimeDir) + + if uidInt == 0 { + if platform == "podman" { + containerEndpointDefault = "unix:///run/podman/podman.sock" + } + } + } + } + + return containerEndpointDefault, nil +} + +func (i *Installer) isContainerRunning(containerName string) bool { + + containers, err := i.cli.ContainerList() + if err != nil { + return false + } + + for _, c := range containers { + if c.Name == containerName { + return c.Running + } + } + + return false +} + +func (i *Installer) createDirectories() error { + namespacePath := api.GetHostNamespaceHome(i.Namespace) + dirs := []string{ + filepath.Join(namespacePath, "network-observer"), + filepath.Join(namespacePath, "network-observer", "prometheus"), + filepath.Join(namespacePath, "network-observer", "nginx"), + filepath.Join(namespacePath, "network-observer", "nginx", "conf.d"), + filepath.Join(namespacePath, "network-observer", "htpasswd"), + filepath.Join(namespacePath, "network-observer", "certs"), + } + + for _, dir := range dirs { + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", dir, err) + } + } + + dataDir := filepath.Join(namespacePath, "network-observer", "prometheus", "data") + if err := os.MkdirAll(dataDir, 0750); err != nil { + return fmt.Errorf("failed to create directory %s: %w", dataDir, err) + } + + return nil +} + +func (i *Installer) generateConfigurations() error { + namespacePath := api.GetHostNamespaceHome(i.Namespace) + + nginxPort, err := utils.TcpPortNextFree(8443) + if err != nil { + return fmt.Errorf("failing to assign port to nginx: %s", err) + } + prometheusPort, err := utils.TcpPortNextFree(9090) + if err != nil { + return fmt.Errorf("failing to assign port to prometheus: %s", err) + } + metricsPort, err := utils.TcpPortNextFree(9000) + if err != nil { + return fmt.Errorf("failing to assign port to prometheus API: %s", err) + } + netobsPort, err := utils.TcpPortNextFree(8080) + if err != nil { + return fmt.Errorf("failing to assign port to network observer: %s", err) + } + + i.ports = ports{ + nginx: nginxPort, + prometheus: prometheusPort, + netobs: netobsPort, + metrics: metricsPort, + } + + i.logger.Info("Assigned ports", + slog.Int("nginx", nginxPort), + slog.Int("prometheus", prometheusPort), + slog.Int("netobs", netobsPort), + slog.Int("metrics", metricsPort), + ) + + prometheusPath := filepath.Join(namespacePath, "network-observer", "prometheus", "prometheus.yml") + if err := os.WriteFile(prometheusPath, []byte(RenderPrometheusConfig(netobsPort)), 0644); err != nil { + return fmt.Errorf("failed to write prometheus config: %w", err) + } + + nginxPath := filepath.Join(namespacePath, "network-observer", "nginx", "conf.d", "default.conf") + if err := os.WriteFile(nginxPath, []byte(RenderNginxConfig(nginxPort, netobsPort)), 0644); err != nil { + return fmt.Errorf("failed to write nginx config: %w", err) + } + + return nil +} + +func (i *Installer) generateCertificates() error { + namespacePath := api.GetHostNamespaceHome(i.Namespace) + caDir := filepath.Join(namespacePath, string(api.IssuersPath), "skupper-local-ca") + certDir := filepath.Join(namespacePath, "network-observer", "certs") + + return GenerateNginxCert(caDir, certDir) +} + +func (i *Installer) generateHtpasswd() (string, error) { + namespacePath := api.GetHostNamespaceHome(i.Namespace) + htpasswdPath := filepath.Join(namespacePath, "network-observer", "htpasswd", "htpasswd") + + username, password, htpasswdContent, err := GenerateHtpasswdCredentials(i.Username, i.Password) + if err != nil { + return "", fmt.Errorf("failed to generate htpasswd credentials: %w", err) + } + + if err := os.WriteFile(htpasswdPath, []byte(htpasswdContent), 0600); err != nil { + return "", fmt.Errorf("failed to write htpasswd file: %w", err) + } + + i.logger.Info("Generated htpasswd credentials", "username", username) + return password, nil +} + +func (i *Installer) installContainer(newContainer container.Container) error { + err := i.cli.ImagePull(context.TODO(), newContainer.Image) + if err != nil { + return fmt.Errorf("failed to pull image: %v", err) + } + fmt.Printf("Pulled image: %s\n", newContainer.Image) + + err = i.cli.ContainerCreate(&newContainer) + if err != nil { + return fmt.Errorf("failed to create container %s: %v", newContainer.Name, err) + } + err = i.cli.ContainerStart(newContainer.Name) + if err != nil { + return fmt.Errorf("failed to start container %s: %v", newContainer.Name, err) + } + + return nil +} + +func (i *Installer) createSystemdServices() error { + i.logger.Info("Creating systemd services", slog.String("namespace", i.Namespace)) + + manager := NewSystemdServiceManager(i.Namespace, i.Platform, i.ports) + err := manager.CreateServices() + if err != nil { + return fmt.Errorf("failed to create systemd services: %w", err) + } + + i.logger.Info("Systemd services created successfully") + return nil +} diff --git a/internal/nonkube/network-observer/password.go b/internal/nonkube/network-observer/password.go new file mode 100644 index 000000000..0fde42a23 --- /dev/null +++ b/internal/nonkube/network-observer/password.go @@ -0,0 +1,29 @@ +package networkobserver + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + + "golang.org/x/crypto/bcrypt" +) + +func GenerateHtpasswdCredentials(username string, password string) (string, string, string, error) { + if password == "" { + secretBytes := [16]byte{} + if _, err := rand.Read(secretBytes[:]); err != nil { + return "", "", "", fmt.Errorf("error generating random password: %w", err) + } + + password = base64.RawStdEncoding.EncodeToString(secretBytes[:]) + } + + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", "", "", fmt.Errorf("error generating htpasswd hash: %w", err) + } + + htpasswdContent := fmt.Sprintf("%s:%s\n", username, string(hash)) + + return username, password, htpasswdContent, nil +} diff --git a/internal/nonkube/network-observer/password_test.go b/internal/nonkube/network-observer/password_test.go new file mode 100644 index 000000000..0a6461829 --- /dev/null +++ b/internal/nonkube/network-observer/password_test.go @@ -0,0 +1,66 @@ +package networkobserver + +import ( + "strings" + "testing" + + "golang.org/x/crypto/bcrypt" +) + +func TestGenerateHtpasswdCredentials_UsesProvidedPasswordAndHashesStoredValue(t *testing.T) { + username, password, htpasswdContent, err := GenerateHtpasswdCredentials("foo", "bar") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if username != "foo" { + t.Fatalf("expected username %q, got %q", "foo", username) + } + if password != "bar" { + t.Fatalf("expected password %q, got %q", "bar", password) + } + if strings.Contains(htpasswdContent, "bar") { + t.Fatalf("expected htpasswd content to avoid plain password, got %q", htpasswdContent) + } + // bcrypt hashes start with $2a$ or $2b$ + if !strings.HasPrefix(htpasswdContent, "foo:$2") { + t.Fatalf("expected bcrypt htpasswd entry, got %q", htpasswdContent) + } + if !strings.HasSuffix(htpasswdContent, "\n") { + t.Fatalf("expected trailing newline, got %q", htpasswdContent) + } + // verify the hash actually matches the password + hash := strings.TrimSuffix(strings.TrimPrefix(htpasswdContent, "foo:"), "\n") + if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte("bar")); err != nil { + t.Fatalf("bcrypt hash does not match password: %v", err) + } +} + +func TestGenerateHtpasswdCredentials_GeneratesPasswordAndHashesStoredValue(t *testing.T) { + username, password, htpasswdContent, err := GenerateHtpasswdCredentials("foo", "") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if username != "foo" { + t.Fatalf("expected username %q, got %q", "foo", username) + } + if password == "" { + t.Fatal("expected generated password") + } + if strings.Contains(htpasswdContent, password) { + t.Fatalf("expected htpasswd content to avoid plain password, got %q", htpasswdContent) + } + // bcrypt hashes start with $2a$ or $2b$ + if !strings.HasPrefix(htpasswdContent, "foo:$2") { + t.Fatalf("expected bcrypt htpasswd entry, got %q", htpasswdContent) + } + if !strings.HasSuffix(htpasswdContent, "\n") { + t.Fatalf("expected trailing newline, got %q", htpasswdContent) + } + // verify the hash actually matches the generated password + hash := strings.TrimSuffix(strings.TrimPrefix(htpasswdContent, "foo:"), "\n") + if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil { + t.Fatalf("bcrypt hash does not match generated password: %v", err) + } +} diff --git a/internal/nonkube/network-observer/systemd.go b/internal/nonkube/network-observer/systemd.go new file mode 100644 index 000000000..cbc9f5bc1 --- /dev/null +++ b/internal/nonkube/network-observer/systemd.go @@ -0,0 +1,342 @@ +package networkobserver + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/skupperproject/skupper/internal/images" + "github.com/skupperproject/skupper/pkg/nonkube/api" +) + +const SystemdServiceTemplate = `[Unit] +Description=Skupper Network Observer - %s +After=network.target +Wants=skupper-network-observer-prometheus-%s.service skupper-network-observer-app-%s.service skupper-network-observer-nginx-%s.service +After=skupper-network-observer-prometheus-%s.service skupper-network-observer-app-%s.service skupper-network-observer-nginx-%s.service + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/bin/true +ExecStop=/bin/true + +[Install] +WantedBy=default.target +` + +const SystemdPrometheusServiceTemplate = `[Unit] +Description=Skupper Network Observer Prometheus - %s +After=network.target +PartOf=skupper-network-observer-%s.service + +[Service] +Type=simple +Restart=always +RestartSec=5 +ExecStartPre=-{{.ContainerEngine}} stop %s-skupper-prometheus +ExecStartPre=-{{.ContainerEngine}} rm %s-skupper-prometheus +ExecStart={{.ContainerEngine}} run --name %s-skupper-prometheus \ + --label application=skupper-v2 \ + --label skupper.io/v2-component=prometheus \ + --user={{.RunAsUser}} \ + --userns=keep-id \ + --network host \ + --restart always \ + -v %s/network-observer/prometheus:/etc/prometheus:z \ + -v %s/network-observer/prometheus/data:/prometheus:z \ + {{.PrometheusImage}} \ + --config.file=/etc/prometheus/prometheus.yml \ + --storage.tsdb.path=/prometheus/ \ + --web.listen-address=:{{.PrometheusPort}} +ExecStop={{.ContainerEngine}} stop %s-skupper-prometheus +ExecStopPost={{.ContainerEngine}} rm %s-skupper-prometheus + +[Install] +WantedBy=skupper-network-observer-%s.service +` + +const SystemdNetworkObserverServiceTemplate = `[Unit] +Description=Skupper Network Observer Application - %s +After=network.target skupper-controller.service skupper-network-observer-prometheus-%s.service +Wants=skupper-controller.service +PartOf=skupper-network-observer-%s.service + +[Service] +Type=simple +Restart=always +RestartSec=5 +ExecStartPre=-{{.ContainerEngine}} stop %s-skupper-network-observer +ExecStartPre=-{{.ContainerEngine}} rm %s-skupper-network-observer +ExecStart={{.ContainerEngine}} run --name %s-skupper-network-observer \ + --label application=skupper-v2 \ + --label skupper.io/v2-component=network-observer \ + --user={{.RunAsUser}} \ + --userns=keep-id \ + --network host \ + --restart always \ + -v %s/runtime/certs/skupper-local-client:/etc/messaging:ro,z \ + {{.NetworkObserverImage}} \ + -listen=127.0.0.1:{{.NetobsPort}} \ + -prometheus-api=http://127.0.0.1:{{.PrometheusPort}} \ + -router-endpoint=amqps://127.0.0.1:5671 \ + -router-tls-ca=/etc/messaging/ca.crt \ + -router-tls-cert=/etc/messaging/tls.crt \ + -router-tls-key=/etc/messaging/tls.key \ + -listen-metrics=:{{.MetricsPort}} +ExecStop={{.ContainerEngine}} stop %s-skupper-network-observer +ExecStopPost={{.ContainerEngine}} rm %s-skupper-network-observer + +[Install] +WantedBy=skupper-network-observer-%s.service +` + +const SystemdNginxServiceTemplate = `[Unit] +Description=Skupper Network Observer Nginx Proxy - %s +After=network.target skupper-network-observer-app-%s.service +PartOf=skupper-network-observer-%s.service + +[Service] +Type=simple +Restart=always +RestartSec=5 +ExecStartPre=-{{.ContainerEngine}} stop %s-skupper-nginx +ExecStartPre=-{{.ContainerEngine}} rm %s-skupper-nginx +ExecStart={{.ContainerEngine}} run --name %s-skupper-nginx \ + --label application=skupper-v2 \ + --label skupper.io/v2-component=nginx-proxy \ + --user={{.RunAsUser}} \ + --userns=keep-id \ + --network host \ + --restart always \ + -v %s/network-observer/nginx/conf.d:/etc/nginx/conf.d:z \ + -v %s/network-observer/certs:/etc/certificates:z \ + -v %s/network-observer/htpasswd:/etc/httpusers:z \ + {{.NginxImage}} +ExecStop={{.ContainerEngine}} stop %s-skupper-nginx +ExecStopPost={{.ContainerEngine}} rm %s-skupper-nginx + +[Install] +WantedBy=skupper-network-observer-%s.service +` + +type SystemdServiceManager struct { + Namespace string + ContainerEngine string + ServiceDir string + RunAsUser string + ports ports +} + +func NewSystemdServiceManager(namespace, containerEngine string, p ports) *SystemdServiceManager { + serviceDir := getSystemdServiceDir() + return &SystemdServiceManager{ + Namespace: namespace, + ContainerEngine: containerEngine, + ServiceDir: serviceDir, + RunAsUser: fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), + ports: p, + } +} + +func getSystemdServiceDir() string { + if os.Getuid() == 0 { + return "/etc/systemd/system" + } + home := os.Getenv("HOME") + return filepath.Join(home, ".config", "systemd", "user") +} + +func (s *SystemdServiceManager) CreateServices() error { + + if err := os.MkdirAll(s.ServiceDir, 0755); err != nil { + return fmt.Errorf("failed to create systemd service directory: %w", err) + } + + namespacePath := api.GetHostNamespaceHome(s.Namespace) + + mainServiceName := fmt.Sprintf("skupper-network-observer-%s.service", s.Namespace) + mainServicePath := filepath.Join(s.ServiceDir, mainServiceName) + mainServiceContent := fmt.Sprintf(SystemdServiceTemplate, + s.Namespace, + s.Namespace, s.Namespace, s.Namespace, + s.Namespace, s.Namespace, s.Namespace) + if err := os.WriteFile(mainServicePath, []byte(mainServiceContent), 0644); err != nil { + return fmt.Errorf("failed to write main service file: %w", err) + } + + prometheusServiceName := fmt.Sprintf("skupper-network-observer-prometheus-%s.service", s.Namespace) + prometheusServicePath := filepath.Join(s.ServiceDir, prometheusServiceName) + prometheusServiceContent := s.renderPrometheusService(namespacePath) + if err := os.WriteFile(prometheusServicePath, []byte(prometheusServiceContent), 0644); err != nil { + return fmt.Errorf("failed to write prometheus service file: %w", err) + } + + appServiceName := fmt.Sprintf("skupper-network-observer-app-%s.service", s.Namespace) + appServicePath := filepath.Join(s.ServiceDir, appServiceName) + appServiceContent := s.renderNetworkObserverService(namespacePath) + if err := os.WriteFile(appServicePath, []byte(appServiceContent), 0644); err != nil { + return fmt.Errorf("failed to write network observer service file: %w", err) + } + + nginxServiceName := fmt.Sprintf("skupper-network-observer-nginx-%s.service", s.Namespace) + nginxServicePath := filepath.Join(s.ServiceDir, nginxServiceName) + nginxServiceContent := s.renderNginxService(namespacePath) + if err := os.WriteFile(nginxServicePath, []byte(nginxServiceContent), 0644); err != nil { + return fmt.Errorf("failed to write nginx service file: %w", err) + } + + if err := s.reloadSystemd(); err != nil { + return fmt.Errorf("failed to reload systemd: %w", err) + } + + for _, svc := range []string{ + fmt.Sprintf("skupper-network-observer-prometheus-%s.service", s.Namespace), + fmt.Sprintf("skupper-network-observer-app-%s.service", s.Namespace), + fmt.Sprintf("skupper-network-observer-nginx-%s.service", s.Namespace), + mainServiceName, + } { + if err := s.enableService(svc); err != nil { + return fmt.Errorf("failed to enable service %s: %w", svc, err) + } + } + + if err := s.startService(mainServiceName); err != nil { + return fmt.Errorf("failed to start service: %w", err) + } + + return nil +} + +func (s *SystemdServiceManager) renderPrometheusService(namespacePath string) string { + content := fmt.Sprintf(SystemdPrometheusServiceTemplate, + s.Namespace, s.Namespace, + s.Namespace, s.Namespace, s.Namespace, + namespacePath, namespacePath, + s.Namespace, s.Namespace, s.Namespace) + content = strings.ReplaceAll(content, "{{.ContainerEngine}}", s.ContainerEngine) + content = strings.ReplaceAll(content, "{{.RunAsUser}}", s.RunAsUser) + content = strings.ReplaceAll(content, "{{.PrometheusImage}}", images.GetPrometheusImageName()) + content = strings.ReplaceAll(content, "{{.PrometheusPort}}", fmt.Sprintf("%d", s.ports.prometheus)) + return content +} + +func (s *SystemdServiceManager) renderNetworkObserverService(namespacePath string) string { + content := fmt.Sprintf(SystemdNetworkObserverServiceTemplate, + s.Namespace, s.Namespace, s.Namespace, + s.Namespace, s.Namespace, s.Namespace, + namespacePath, + s.Namespace, s.Namespace, s.Namespace) + content = strings.ReplaceAll(content, "{{.ContainerEngine}}", s.ContainerEngine) + content = strings.ReplaceAll(content, "{{.RunAsUser}}", s.RunAsUser) + content = strings.ReplaceAll(content, "{{.NetworkObserverImage}}", images.GetNetworkObserverImageName()) + content = strings.ReplaceAll(content, "{{.NetobsPort}}", fmt.Sprintf("%d", s.ports.netobs)) + content = strings.ReplaceAll(content, "{{.PrometheusPort}}", fmt.Sprintf("%d", s.ports.prometheus)) + content = strings.ReplaceAll(content, "{{.MetricsPort}}", fmt.Sprintf("%d", s.ports.metrics)) + return content +} + +func (s *SystemdServiceManager) renderNginxService(namespacePath string) string { + content := fmt.Sprintf(SystemdNginxServiceTemplate, + s.Namespace, s.Namespace, s.Namespace, + s.Namespace, s.Namespace, s.Namespace, + namespacePath, namespacePath, namespacePath, + s.Namespace, s.Namespace, s.Namespace) + content = strings.ReplaceAll(content, "{{.ContainerEngine}}", s.ContainerEngine) + content = strings.ReplaceAll(content, "{{.RunAsUser}}", s.RunAsUser) + content = strings.ReplaceAll(content, "{{.NginxImage}}", images.GetNginxImageName()) + return content +} + +func (s *SystemdServiceManager) reloadSystemd() error { + var cmd *exec.Cmd + if os.Getuid() == 0 { + cmd = exec.Command("systemctl", "daemon-reload") + } else { + cmd = exec.Command("systemctl", "--user", "daemon-reload") + } + return cmd.Run() +} + +func (s *SystemdServiceManager) enableService(serviceName string) error { + var cmd *exec.Cmd + if os.Getuid() == 0 { + cmd = exec.Command("systemctl", "enable", serviceName) + } else { + cmd = exec.Command("systemctl", "--user", "enable", serviceName) + } + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to enable %s: %w", serviceName, err) + } + return nil +} + +func (s *SystemdServiceManager) startService(serviceName string) error { + var cmd *exec.Cmd + if os.Getuid() == 0 { + cmd = exec.Command("systemctl", "start", serviceName) + } else { + cmd = exec.Command("systemctl", "--user", "start", serviceName) + } + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to start %s: %w", serviceName, err) + } + return nil +} + +func (s *SystemdServiceManager) RemoveServices() error { + mainServiceName := fmt.Sprintf("skupper-network-observer-%s.service", s.Namespace) + + // Stop and disable the main service + if err := s.stopAndDisableService(mainServiceName); err != nil { + // Log but don't fail if service doesn't exist + fmt.Printf("Warning: failed to stop service: %v\n", err) + } + + // Remove service files + serviceNames := []string{ + mainServiceName, + fmt.Sprintf("skupper-network-observer-prometheus-%s.service", s.Namespace), + fmt.Sprintf("skupper-network-observer-app-%s.service", s.Namespace), + fmt.Sprintf("skupper-network-observer-nginx-%s.service", s.Namespace), + } + + for _, serviceName := range serviceNames { + servicePath := filepath.Join(s.ServiceDir, serviceName) + if err := os.Remove(servicePath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove service file %s: %w", serviceName, err) + } + } + + // Reload systemd + if err := s.reloadSystemd(); err != nil { + return fmt.Errorf("failed to reload systemd: %w", err) + } + + return nil +} + +func (s *SystemdServiceManager) stopAndDisableService(serviceName string) error { + var stopCmd, disableCmd *exec.Cmd + if os.Getuid() == 0 { + stopCmd = exec.Command("systemctl", "stop", serviceName) + disableCmd = exec.Command("systemctl", "disable", serviceName) + } else { + stopCmd = exec.Command("systemctl", "--user", "stop", serviceName) + disableCmd = exec.Command("systemctl", "--user", "disable", serviceName) + } + + err := stopCmd.Run() + if err != nil { + return err + } + + err = disableCmd.Run() + if err != nil { + return err + } + + return nil +} From 0fb2491eb340aa2eb6fe2c670a76099e8026435c Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Fri, 10 Jul 2026 22:14:14 +0200 Subject: [PATCH 02/18] fix redundancy --- internal/cmd/skupper/common/flags.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/skupper/common/flags.go b/internal/cmd/skupper/common/flags.go index af16eabc1..d46549dd0 100644 --- a/internal/cmd/skupper/common/flags.go +++ b/internal/cmd/skupper/common/flags.go @@ -94,7 +94,7 @@ for other Kubernetes flavors, loadbalancer is the default.` FlagDescReloadType = "Specify the type of reload to perform. Choices: manual, auto" FlagNameNetworkObserverUsername = "username" - FlagDescNetworkObserverUsername = "Username for HTTP Basic Auth (default: skupper)" + FlagDescNetworkObserverUsername = "Username for HTTP Basic Auth" FlagNameNetworkObserverPassword = "password" FlagDescNetworkObserverPassword = "Password for HTTP Basic Auth (auto-generated if not provided)" FlagNameNetworkObserverUninstall = "uninstall" From 9e172bb9b36b181060e06b06f04fdcf5b1a022f7 Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Mon, 13 Jul 2026 20:46:02 +0200 Subject: [PATCH 03/18] add local router address to initiate the network-observer container instead of having it harcoded --- internal/nonkube/network-observer/containers.go | 2 +- internal/nonkube/network-observer/installer.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/nonkube/network-observer/containers.go b/internal/nonkube/network-observer/containers.go index 9b24fec3e..44baf98af 100644 --- a/internal/nonkube/network-observer/containers.go +++ b/internal/nonkube/network-observer/containers.go @@ -19,7 +19,7 @@ func GetNetworkObserverContainer(namespace string, p ports) container.Container Command: []string{ fmt.Sprintf("-listen=127.0.0.1:%d", p.netobs), fmt.Sprintf("-prometheus-api=http://127.0.0.1:%d", p.prometheus), - "-router-endpoint=amqps://127.0.0.1:5671", + fmt.Sprintf("-router-endpoint=%s", p.router), "-router-tls-ca=/etc/messaging/ca.crt", "-router-tls-cert=/etc/messaging/tls.crt", "-router-tls-key=/etc/messaging/tls.key", diff --git a/internal/nonkube/network-observer/installer.go b/internal/nonkube/network-observer/installer.go index 3cb1e7c41..72ff22037 100644 --- a/internal/nonkube/network-observer/installer.go +++ b/internal/nonkube/network-observer/installer.go @@ -14,6 +14,7 @@ import ( "github.com/skupperproject/skupper/internal/config" "github.com/skupperproject/skupper/internal/nonkube/client/compat" "github.com/skupperproject/skupper/internal/nonkube/client/fs" + "github.com/skupperproject/skupper/internal/nonkube/client/runtime" "github.com/skupperproject/skupper/internal/nonkube/common" "github.com/skupperproject/skupper/internal/utils" "github.com/skupperproject/skupper/pkg/container" @@ -25,6 +26,7 @@ type ports struct { prometheus int netobs int metrics int + router string } type Installer struct { @@ -349,11 +351,17 @@ func (i *Installer) generateConfigurations() error { return fmt.Errorf("failing to assign port to network observer: %s", err) } + routerEndpoint, err := runtime.GetLocalRouterAddress(i.Namespace) + if err != nil { + return fmt.Errorf("failed to determine local router address: %w", err) + } + i.ports = ports{ nginx: nginxPort, prometheus: prometheusPort, netobs: netobsPort, metrics: metricsPort, + router: routerEndpoint, } i.logger.Info("Assigned ports", @@ -361,6 +369,7 @@ func (i *Installer) generateConfigurations() error { slog.Int("prometheus", prometheusPort), slog.Int("netobs", netobsPort), slog.Int("metrics", metricsPort), + slog.String("router", routerEndpoint), ) prometheusPath := filepath.Join(namespacePath, "network-observer", "prometheus", "prometheus.yml") From f8e25b0450cbecc159b4ceb5259decdef6356460 Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Mon, 13 Jul 2026 21:27:10 +0200 Subject: [PATCH 04/18] add fallback getting the user home dir --- internal/nonkube/network-observer/systemd.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/nonkube/network-observer/systemd.go b/internal/nonkube/network-observer/systemd.go index cbc9f5bc1..a50663b63 100644 --- a/internal/nonkube/network-observer/systemd.go +++ b/internal/nonkube/network-observer/systemd.go @@ -145,7 +145,10 @@ func getSystemdServiceDir() string { if os.Getuid() == 0 { return "/etc/systemd/system" } - home := os.Getenv("HOME") + home, err := os.UserHomeDir() + if err != nil { + home = fmt.Sprintf("/home/%s", os.Getenv("USER")) + } return filepath.Join(home, ".config", "systemd", "user") } From 303c4dc2054cd8e326dc9790bef33625d0ff0e6e Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Mon, 13 Jul 2026 21:34:28 +0200 Subject: [PATCH 05/18] fix harcoded router endpoint and add podman userns flag if the selected platform is podman --- internal/nonkube/network-observer/systemd.go | 22 +++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/internal/nonkube/network-observer/systemd.go b/internal/nonkube/network-observer/systemd.go index a50663b63..47a8cba7b 100644 --- a/internal/nonkube/network-observer/systemd.go +++ b/internal/nonkube/network-observer/systemd.go @@ -42,8 +42,7 @@ ExecStart={{.ContainerEngine}} run --name %s-skupper-prometheus \ --label application=skupper-v2 \ --label skupper.io/v2-component=prometheus \ --user={{.RunAsUser}} \ - --userns=keep-id \ - --network host \ +{{.UsernsFlag}} --network host \ --restart always \ -v %s/network-observer/prometheus:/etc/prometheus:z \ -v %s/network-observer/prometheus/data:/prometheus:z \ @@ -74,14 +73,13 @@ ExecStart={{.ContainerEngine}} run --name %s-skupper-network-observer \ --label application=skupper-v2 \ --label skupper.io/v2-component=network-observer \ --user={{.RunAsUser}} \ - --userns=keep-id \ - --network host \ +{{.UsernsFlag}} --network host \ --restart always \ -v %s/runtime/certs/skupper-local-client:/etc/messaging:ro,z \ {{.NetworkObserverImage}} \ -listen=127.0.0.1:{{.NetobsPort}} \ -prometheus-api=http://127.0.0.1:{{.PrometheusPort}} \ - -router-endpoint=amqps://127.0.0.1:5671 \ + -router-endpoint={{.RouterEndpoint}} \ -router-tls-ca=/etc/messaging/ca.crt \ -router-tls-cert=/etc/messaging/tls.crt \ -router-tls-key=/etc/messaging/tls.key \ @@ -108,8 +106,7 @@ ExecStart={{.ContainerEngine}} run --name %s-skupper-nginx \ --label application=skupper-v2 \ --label skupper.io/v2-component=nginx-proxy \ --user={{.RunAsUser}} \ - --userns=keep-id \ - --network host \ +{{.UsernsFlag}} --network host \ --restart always \ -v %s/network-observer/nginx/conf.d:/etc/nginx/conf.d:z \ -v %s/network-observer/certs:/etc/certificates:z \ @@ -213,6 +210,13 @@ func (s *SystemdServiceManager) CreateServices() error { return nil } +func (s *SystemdServiceManager) userNsFlag() string { + if s.ContainerEngine == "podman" { + return " --userns=keep-id \\\n" + } + return "" +} + func (s *SystemdServiceManager) renderPrometheusService(namespacePath string) string { content := fmt.Sprintf(SystemdPrometheusServiceTemplate, s.Namespace, s.Namespace, @@ -221,6 +225,7 @@ func (s *SystemdServiceManager) renderPrometheusService(namespacePath string) st s.Namespace, s.Namespace, s.Namespace) content = strings.ReplaceAll(content, "{{.ContainerEngine}}", s.ContainerEngine) content = strings.ReplaceAll(content, "{{.RunAsUser}}", s.RunAsUser) + content = strings.ReplaceAll(content, "{{.UsernsFlag}}", s.userNsFlag()) content = strings.ReplaceAll(content, "{{.PrometheusImage}}", images.GetPrometheusImageName()) content = strings.ReplaceAll(content, "{{.PrometheusPort}}", fmt.Sprintf("%d", s.ports.prometheus)) return content @@ -234,9 +239,11 @@ func (s *SystemdServiceManager) renderNetworkObserverService(namespacePath strin s.Namespace, s.Namespace, s.Namespace) content = strings.ReplaceAll(content, "{{.ContainerEngine}}", s.ContainerEngine) content = strings.ReplaceAll(content, "{{.RunAsUser}}", s.RunAsUser) + content = strings.ReplaceAll(content, "{{.UsernsFlag}}", s.userNsFlag()) content = strings.ReplaceAll(content, "{{.NetworkObserverImage}}", images.GetNetworkObserverImageName()) content = strings.ReplaceAll(content, "{{.NetobsPort}}", fmt.Sprintf("%d", s.ports.netobs)) content = strings.ReplaceAll(content, "{{.PrometheusPort}}", fmt.Sprintf("%d", s.ports.prometheus)) + content = strings.ReplaceAll(content, "{{.RouterEndpoint}}", s.ports.router) content = strings.ReplaceAll(content, "{{.MetricsPort}}", fmt.Sprintf("%d", s.ports.metrics)) return content } @@ -249,6 +256,7 @@ func (s *SystemdServiceManager) renderNginxService(namespacePath string) string s.Namespace, s.Namespace, s.Namespace) content = strings.ReplaceAll(content, "{{.ContainerEngine}}", s.ContainerEngine) content = strings.ReplaceAll(content, "{{.RunAsUser}}", s.RunAsUser) + content = strings.ReplaceAll(content, "{{.UsernsFlag}}", s.userNsFlag()) content = strings.ReplaceAll(content, "{{.NginxImage}}", images.GetNginxImageName()) return content } From 474f7f5b73ba18d11e7e1eb51505caec40a00953 Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Mon, 13 Jul 2026 21:37:51 +0200 Subject: [PATCH 06/18] generate password before installing the nginx container --- internal/nonkube/network-observer/installer.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/nonkube/network-observer/installer.go b/internal/nonkube/network-observer/installer.go index 72ff22037..e459e82d0 100644 --- a/internal/nonkube/network-observer/installer.go +++ b/internal/nonkube/network-observer/installer.go @@ -132,6 +132,11 @@ func (i *Installer) Install() (*InstallResult, error) { return nil, fmt.Errorf("failed to generate certificates: %w", err) } + generatedPassword, err := i.generateHtpasswd() + if err != nil { + return nil, fmt.Errorf("failed to generate htpasswd: %w", err) + } + systemdGlobal, err := common.NewSystemdGlobal(i.Platform) if err != nil { return nil, err @@ -162,11 +167,6 @@ func (i *Installer) Install() (*InstallResult, error) { i.logger.Info("Network observer installation completed successfully") - generatedPassword, err := i.generateHtpasswd() - if err != nil { - return nil, fmt.Errorf("failed to generate htpasswd: %w", err) - } - return &InstallResult{ URL: fmt.Sprintf("https://localhost:%d", i.ports.nginx), Username: i.Username, From f21bde8c712ce58fb4478f03aaaf1e1492f1c614 Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Mon, 13 Jul 2026 21:44:36 +0200 Subject: [PATCH 07/18] add timeout for pulling images --- internal/nonkube/network-observer/installer.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/nonkube/network-observer/installer.go b/internal/nonkube/network-observer/installer.go index e459e82d0..bb7997f40 100644 --- a/internal/nonkube/network-observer/installer.go +++ b/internal/nonkube/network-observer/installer.go @@ -9,6 +9,7 @@ import ( "os/user" "path/filepath" "strconv" + "time" "github.com/skupperproject/skupper/api/types" "github.com/skupperproject/skupper/internal/config" @@ -411,7 +412,9 @@ func (i *Installer) generateHtpasswd() (string, error) { } func (i *Installer) installContainer(newContainer container.Container) error { - err := i.cli.ImagePull(context.TODO(), newContainer.Image) + ctx, cn := context.WithTimeout(context.Background(), time.Minute*10) + defer cn() + err := i.cli.ImagePull(ctx, newContainer.Image) if err != nil { return fmt.Errorf("failed to pull image: %v", err) } From f2e7de5a6c61834e768fae8eb8688d5ddb2f45d5 Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Fri, 24 Jul 2026 17:45:08 +0200 Subject: [PATCH 08/18] remove nginx deployment for network-observer in non-kube environments --- internal/cmd/skupper/common/flags.go | 6 -- .../system/nonkube/system_network-observer.go | 25 +------ .../nonkube/system_network-observer_test.go | 62 ++-------------- internal/cmd/skupper/system/system.go | 10 +-- internal/cmd/skupper/system/system_test.go | 2 - internal/images/image_utils.go | 18 ----- internal/images/images.go | 2 - .../nonkube/network-observer/certificates.go | 52 -------------- .../network-observer/config_templates.go | 26 ------- .../nonkube/network-observer/containers.go | 38 +--------- .../nonkube/network-observer/installer.go | 71 +------------------ internal/nonkube/network-observer/password.go | 29 -------- .../nonkube/network-observer/password_test.go | 66 ----------------- internal/nonkube/network-observer/systemd.go | 60 ++-------------- 14 files changed, 18 insertions(+), 449 deletions(-) delete mode 100644 internal/nonkube/network-observer/certificates.go delete mode 100644 internal/nonkube/network-observer/password.go delete mode 100644 internal/nonkube/network-observer/password_test.go diff --git a/internal/cmd/skupper/common/flags.go b/internal/cmd/skupper/common/flags.go index d46549dd0..636ef4c8f 100644 --- a/internal/cmd/skupper/common/flags.go +++ b/internal/cmd/skupper/common/flags.go @@ -93,10 +93,6 @@ for other Kubernetes flavors, loadbalancer is the default.` FlagNameReloadType = "reload-type" FlagDescReloadType = "Specify the type of reload to perform. Choices: manual, auto" - FlagNameNetworkObserverUsername = "username" - FlagDescNetworkObserverUsername = "Username for HTTP Basic Auth" - FlagNameNetworkObserverPassword = "password" - FlagDescNetworkObserverPassword = "Password for HTTP Basic Auth (auto-generated if not provided)" FlagNameNetworkObserverUninstall = "uninstall" FlagDescNetworkObserverUninstall = "Uninstall Network Observer and related containers" ) @@ -253,8 +249,6 @@ type CommandListenerGenerateFlags struct { } type CommandNetworkObserverFlags struct { - Username string - Password string Uninstall bool } diff --git a/internal/cmd/skupper/system/nonkube/system_network-observer.go b/internal/cmd/skupper/system/nonkube/system_network-observer.go index 5a4befb56..2de97af89 100644 --- a/internal/cmd/skupper/system/nonkube/system_network-observer.go +++ b/internal/cmd/skupper/system/nonkube/system_network-observer.go @@ -13,8 +13,6 @@ type CmdSystemNetworkObserver struct { CobraCmd *cobra.Command Flags *common.CommandNetworkObserverFlags namespace string - user string - password string } func NewCmdSystemNetworkObserver() *CmdSystemNetworkObserver { @@ -37,29 +35,13 @@ func (cmd *CmdSystemNetworkObserver) ValidateInput(args []string) error { validationErrors = append(validationErrors, fmt.Errorf("this command does not accept arguments")) } - if cmd.Flags != nil && cmd.Flags.Uninstall { - if cmd.Flags.Password != "" { - validationErrors = append(validationErrors, fmt.Errorf("--%s cannot be used with --%s", common.FlagNameNetworkObserverPassword, common.FlagNameNetworkObserverUninstall)) - } - } - return errors.Join(validationErrors...) } -func (cmd *CmdSystemNetworkObserver) InputToOptions() { - - if cmd.Flags.Username != "" { - cmd.user = cmd.Flags.Username - } - - if cmd.Flags.Password != "" { - cmd.password = cmd.Flags.Password - } - -} +func (cmd *CmdSystemNetworkObserver) InputToOptions() {} func (cmd *CmdSystemNetworkObserver) Run() error { - installer, err := networkobserver.NewInstaller(cmd.namespace, cmd.user, cmd.password) + installer, err := networkobserver.NewInstaller(cmd.namespace) if err != nil { return fmt.Errorf("failed to create installer: %w", err) } @@ -88,9 +70,6 @@ func (cmd *CmdSystemNetworkObserver) Run() error { fmt.Println("Network observer installed successfully!") fmt.Printf("\nAccess URL: %s\n", result.URL) - fmt.Printf("Username: %s\n", result.Username) - fmt.Printf("Password: %s\n", result.Password) - fmt.Println("\nNote: Save these credentials securely.") return nil } diff --git a/internal/cmd/skupper/system/nonkube/system_network-observer_test.go b/internal/cmd/skupper/system/nonkube/system_network-observer_test.go index 8f5a78929..bcbbfa748 100644 --- a/internal/cmd/skupper/system/nonkube/system_network-observer_test.go +++ b/internal/cmd/skupper/system/nonkube/system_network-observer_test.go @@ -21,21 +21,12 @@ func TestCmdSystemNetworkObserverValidateInput(t *testing.T) { expectedError: []string{"this command does not accept arguments"}, }, { - name: "rejects credentials with uninstall", - flags: &common.CommandNetworkObserverFlags{ - Uninstall: true, - Password: "password", - }, - expectedError: []string{ - "--password cannot be used with --uninstall", - }, + name: "accepts no arguments", + flags: &common.CommandNetworkObserverFlags{}, }, { - name: "allows install credentials", - flags: &common.CommandNetworkObserverFlags{ - Username: "user", - Password: "password", - }, + name: "accepts uninstall flag", + flags: &common.CommandNetworkObserverFlags{Uninstall: true}, }, } @@ -109,48 +100,3 @@ func TestCmdSystemNetworkObserverNewClient(t *testing.T) { }) } } - -func TestCmdSystemNetworkObserverInputToOptions(t *testing.T) { - tests := []struct { - name string - flags *common.CommandNetworkObserverFlags - initialUser string - initialPassword string - expectedUser string - expectedPassword string - }{ - { - name: "copies credentials from flags", - flags: &common.CommandNetworkObserverFlags{Username: "user", Password: "password"}, - expectedUser: "user", - expectedPassword: "password", - }, - { - name: "leaves existing values when flags are empty", - flags: &common.CommandNetworkObserverFlags{}, - initialUser: "existing-user", - initialPassword: "existing-password", - expectedUser: "existing-user", - expectedPassword: "existing-password", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - cmd := &CmdSystemNetworkObserver{ - Flags: test.flags, - user: test.initialUser, - password: test.initialPassword, - } - - cmd.InputToOptions() - - if cmd.user != test.expectedUser { - t.Fatalf("expected user %q, got %q", test.expectedUser, cmd.user) - } - if cmd.password != test.expectedPassword { - t.Fatalf("expected password %q, got %q", test.expectedPassword, cmd.password) - } - }) - } -} diff --git a/internal/cmd/skupper/system/system.go b/internal/cmd/skupper/system/system.go index 0a10f5ab5..be4a8c8ba 100644 --- a/internal/cmd/skupper/system/system.go +++ b/internal/cmd/skupper/system/system.go @@ -242,19 +242,15 @@ func CmdSystemNetworkObserverFactory(configuredPlatform common.Platform) *cobra. Use: "network-observer", Short: "Install the network observer", Long: `Install the Skupper network observer to collect and expose network metrics. -The network observer requires an existing Skupper site and will deploy three containers: +The network observer requires an existing Skupper site and will deploy two containers: - network-observer: Collects metrics from the router -- prometheus: Stores metrics -- nginx: Provides HTTPS access with HTTP Basic Auth`, - Example: `skupper network-observer --namespace west -skupper network-observer -n west --username admin --password secret`, +- prometheus: Stores metrics`, + Example: `skupper network-observer --namespace west`, } cmd := common.ConfigureCobraCommand(configuredPlatform, cmdDesc, nil, nonKubeCommand) cmdFlags := common.CommandNetworkObserverFlags{} - cmd.Flags().StringVar(&cmdFlags.Username, common.FlagNameNetworkObserverUsername, "skupper", common.FlagDescNetworkObserverUsername) - cmd.Flags().StringVar(&cmdFlags.Password, common.FlagNameNetworkObserverPassword, "", common.FlagDescNetworkObserverPassword) cmd.Flags().BoolVar(&cmdFlags.Uninstall, common.FlagNameNetworkObserverUninstall, false, common.FlagDescNetworkObserverUninstall) kubeCommand.CobraCmd = cmd diff --git a/internal/cmd/skupper/system/system_test.go b/internal/cmd/skupper/system/system_test.go index 8d8d8310c..cba6d118b 100644 --- a/internal/cmd/skupper/system/system_test.go +++ b/internal/cmd/skupper/system/system_test.go @@ -72,8 +72,6 @@ func TestCmdSystemFactory(t *testing.T) { { name: "CmdSystemNetworkObserverFactory", expectedFlagsWithDefaultValue: map[string]interface{}{ - common.FlagNameNetworkObserverUsername: "skupper", - common.FlagNameNetworkObserverPassword: "", common.FlagNameNetworkObserverUninstall: "false", }, command: CmdSystemNetworkObserverFactory(common.PlatformDocker), diff --git a/internal/images/image_utils.go b/internal/images/image_utils.go index 9eb912680..ade1f44b5 100644 --- a/internal/images/image_utils.go +++ b/internal/images/image_utils.go @@ -145,16 +145,6 @@ func GetPrometheusImageName() string { return GetPrometheusServerImageName() } -func GetNginxImageName() string { - image := os.Getenv(NginxImageEnvKey) - if image == "" { - imageRegistry := GetNginxImageRegistry() - return strings.Join([]string{imageRegistry, NginxImageName}, "/") - } else { - return image - } -} - func GetSystemControllerImageName() string { image := os.Getenv(SystemControllerImageEnvKey) if image == "" { @@ -181,14 +171,6 @@ func GetPrometheusImageRegistry() string { return imageRegistry } -func GetNginxImageRegistry() string { - imageRegistry := os.Getenv(NginxImageRegistryEnvKey) - if imageRegistry == "" { - return NginxImageRegistry - } - return imageRegistry -} - func CreateMapImageDigest(runningPods map[string]string) map[string]string { imagesToRetrieve := map[string]string{ "router": GetRouterImageName(), diff --git a/internal/images/images.go b/internal/images/images.go index 645a7dff6..4afb3b43e 100644 --- a/internal/images/images.go +++ b/internal/images/images.go @@ -11,8 +11,6 @@ const ( PrometheusImageRegistry string = "quay.io/prometheus" PrometheusServerImageName string = "prometheus:v3.11.3" - NginxImageRegistry string = "mirror.gcr.io/nginxinc" - NginxImageName string = "nginx-unprivileged:1.31.0-alpine" OauthProxyImageRegistry string = "quay.io/openshift" OauthProxyImageName string = "origin-oauth-proxy:4.14.0" ) diff --git a/internal/nonkube/network-observer/certificates.go b/internal/nonkube/network-observer/certificates.go deleted file mode 100644 index 82e6606e7..000000000 --- a/internal/nonkube/network-observer/certificates.go +++ /dev/null @@ -1,52 +0,0 @@ -package networkobserver - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/skupperproject/skupper/internal/certs" - corev1 "k8s.io/api/core/v1" -) - -func GenerateNginxCert(caDir, certDir string) error { - caSecret, err := loadSecretFromDir(caDir) - if err != nil { - return fmt.Errorf("failed to load skupper-local-ca: %w", err) - } - - secret, err := certs.GenerateSecret("skupper-network-observer", "skupper-network-observer", []string{"localhost"}, 0, caSecret) - if err != nil { - return fmt.Errorf("failed to generate nginx certificate: %w", err) - } - - if err := os.MkdirAll(certDir, 0755); err != nil { - return fmt.Errorf("failed to create certificate directory: %w", err) - } - - if err := os.WriteFile(filepath.Join(certDir, "tls.crt"), secret.Data["tls.crt"], 0644); err != nil { - return fmt.Errorf("failed to write tls.crt: %w", err) - } - if err := os.WriteFile(filepath.Join(certDir, "tls.key"), secret.Data["tls.key"], 0600); err != nil { - return fmt.Errorf("failed to write tls.key: %w", err) - } - - return nil -} - -func loadSecretFromDir(dir string) (*corev1.Secret, error) { - crt, err := os.ReadFile(filepath.Join(dir, "tls.crt")) - if err != nil { - return nil, err - } - key, err := os.ReadFile(filepath.Join(dir, "tls.key")) - if err != nil { - return nil, err - } - return &corev1.Secret{ - Data: map[string][]byte{ - "tls.crt": crt, - "tls.key": key, - }, - }, nil -} diff --git a/internal/nonkube/network-observer/config_templates.go b/internal/nonkube/network-observer/config_templates.go index 0016f52d2..10ac3cce6 100644 --- a/internal/nonkube/network-observer/config_templates.go +++ b/internal/nonkube/network-observer/config_templates.go @@ -19,29 +19,3 @@ scrape_configs: - targets: ["localhost:%d"] `, netobsPort) } - -func RenderNginxConfig(nginxPort, netobsPort int) string { - return fmt.Sprintf(`ssl_session_cache shared:SSL:10m; -ssl_session_timeout 10m; - -server { - listen %d ssl; - keepalive_timeout 70; - - ssl_certificate /etc/certificates/tls.crt; - ssl_certificate_key /etc/certificates/tls.key; - ssl_protocols TLSv1.3; - add_header Strict-Transport-Security "max-age=63072000" always; - - auth_basic "Skupper"; - auth_basic_user_file /etc/httpusers/htpasswd; - - location /api/ { - proxy_pass http://127.0.0.1:%d; - } - location / { - proxy_pass http://127.0.0.1:%d; - } -} -`, nginxPort, netobsPort, netobsPort) -} diff --git a/internal/nonkube/network-observer/containers.go b/internal/nonkube/network-observer/containers.go index 44baf98af..b4173539d 100644 --- a/internal/nonkube/network-observer/containers.go +++ b/internal/nonkube/network-observer/containers.go @@ -17,7 +17,7 @@ func GetNetworkObserverContainer(namespace string, p ports) container.Container Name: fmt.Sprintf("%s-skupper-network-observer", namespace), Image: images.GetNetworkObserverImageName(), Command: []string{ - fmt.Sprintf("-listen=127.0.0.1:%d", p.netobs), + fmt.Sprintf("-listen=:%d", p.netobs), fmt.Sprintf("-prometheus-api=http://127.0.0.1:%d", p.prometheus), fmt.Sprintf("-router-endpoint=%s", p.router), "-router-tls-ca=/etc/messaging/ca.crt", @@ -76,39 +76,3 @@ func GetPrometheusContainer(namespace string, p ports) container.Container { RestartPolicy: "always", } } - -func GetNginxContainer(namespace string) container.Container { - namespacePath := api.GetHostNamespaceHome(namespace) - nginxConfDir := filepath.Join(namespacePath, "network-observer", "nginx", "conf.d") - htpasswdDir := filepath.Join(namespacePath, "network-observer", "htpasswd") - certsPath := filepath.Join(namespacePath, "network-observer", "certs") - - return container.Container{ - Name: fmt.Sprintf("%s-skupper-nginx", namespace), - Image: images.GetNginxImageName(), - Env: map[string]string{}, - Labels: map[string]string{ - "application": "skupper-v2", - "skupper.io/v2-component": "nginx-proxy", - }, - FileMounts: []container.FileMount{ - { - Source: nginxConfDir, - Destination: "/etc/nginx/conf.d", - Options: []string{"z"}, - }, - { - Source: certsPath, - Destination: "/etc/certificates", - Options: []string{"z"}, - }, - { - Source: htpasswdDir, - Destination: "/etc/httpusers", - Options: []string{"z"}, - }, - }, - Networks: map[string]container.ContainerNetworkInfo{}, - RestartPolicy: "always", - } -} diff --git a/internal/nonkube/network-observer/installer.go b/internal/nonkube/network-observer/installer.go index bb7997f40..3d8af0726 100644 --- a/internal/nonkube/network-observer/installer.go +++ b/internal/nonkube/network-observer/installer.go @@ -23,7 +23,6 @@ import ( ) type ports struct { - nginx int prometheus int netobs int metrics int @@ -32,8 +31,6 @@ type ports struct { type Installer struct { Namespace string - Username string - Password string Platform string ports ports logger *slog.Logger @@ -42,12 +39,10 @@ type Installer struct { } type InstallResult struct { - URL string - Username string - Password string + URL string } -func NewInstaller(namespace string, username string, password string) (*Installer, error) { +func NewInstaller(namespace string) (*Installer, error) { selectedPlatform, err := detectPlatform() if err != nil { return nil, err @@ -64,8 +59,6 @@ func NewInstaller(namespace string, username string, password string) (*Installe return &Installer{ Namespace: namespace, - Username: username, - Password: password, Platform: selectedPlatform, logger: slog.Default().With("component", "network.observer.installer"), siteHandler: fs.NewSiteHandler(namespace), @@ -87,7 +80,6 @@ func (i *Installer) ValidatePrerequisitesForInstall() error { containerNames := []string{ fmt.Sprintf("%s-skupper-prometheus", i.Namespace), fmt.Sprintf("%s-skupper-network-observer", i.Namespace), - fmt.Sprintf("%s-skupper-nginx", i.Namespace), } for _, containerName := range containerNames { @@ -129,15 +121,6 @@ func (i *Installer) Install() (*InstallResult, error) { return nil, fmt.Errorf("failed to generate configurations: %w", err) } - if err := i.generateCertificates(); err != nil { - return nil, fmt.Errorf("failed to generate certificates: %w", err) - } - - generatedPassword, err := i.generateHtpasswd() - if err != nil { - return nil, fmt.Errorf("failed to generate htpasswd: %w", err) - } - systemdGlobal, err := common.NewSystemdGlobal(i.Platform) if err != nil { return nil, err @@ -156,10 +139,6 @@ func (i *Installer) Install() (*InstallResult, error) { if err != nil { return nil, err } - err = i.installContainer(GetNginxContainer(i.Namespace)) - if err != nil { - return nil, err - } err = i.createSystemdServices() if err != nil { @@ -169,9 +148,7 @@ func (i *Installer) Install() (*InstallResult, error) { i.logger.Info("Network observer installation completed successfully") return &InstallResult{ - URL: fmt.Sprintf("https://localhost:%d", i.ports.nginx), - Username: i.Username, - Password: generatedPassword, + URL: fmt.Sprintf("http://localhost:%d", i.ports.netobs), }, nil } @@ -180,7 +157,6 @@ func (i *Installer) ValidatePrerequisitesForUninstall() error { containerNames := []string{ fmt.Sprintf("%s-skupper-prometheus", i.Namespace), fmt.Sprintf("%s-skupper-network-observer", i.Namespace), - fmt.Sprintf("%s-skupper-nginx", i.Namespace), } containersAreRunning := false @@ -206,7 +182,6 @@ func (i *Installer) Uninstall() error { } containerNames := []string{ - fmt.Sprintf("%s-skupper-nginx", i.Namespace), fmt.Sprintf("%s-skupper-network-observer", i.Namespace), fmt.Sprintf("%s-skupper-prometheus", i.Namespace), } @@ -312,10 +287,6 @@ func (i *Installer) createDirectories() error { dirs := []string{ filepath.Join(namespacePath, "network-observer"), filepath.Join(namespacePath, "network-observer", "prometheus"), - filepath.Join(namespacePath, "network-observer", "nginx"), - filepath.Join(namespacePath, "network-observer", "nginx", "conf.d"), - filepath.Join(namespacePath, "network-observer", "htpasswd"), - filepath.Join(namespacePath, "network-observer", "certs"), } for _, dir := range dirs { @@ -335,10 +306,6 @@ func (i *Installer) createDirectories() error { func (i *Installer) generateConfigurations() error { namespacePath := api.GetHostNamespaceHome(i.Namespace) - nginxPort, err := utils.TcpPortNextFree(8443) - if err != nil { - return fmt.Errorf("failing to assign port to nginx: %s", err) - } prometheusPort, err := utils.TcpPortNextFree(9090) if err != nil { return fmt.Errorf("failing to assign port to prometheus: %s", err) @@ -358,7 +325,6 @@ func (i *Installer) generateConfigurations() error { } i.ports = ports{ - nginx: nginxPort, prometheus: prometheusPort, netobs: netobsPort, metrics: metricsPort, @@ -366,7 +332,6 @@ func (i *Installer) generateConfigurations() error { } i.logger.Info("Assigned ports", - slog.Int("nginx", nginxPort), slog.Int("prometheus", prometheusPort), slog.Int("netobs", netobsPort), slog.Int("metrics", metricsPort), @@ -378,39 +343,9 @@ func (i *Installer) generateConfigurations() error { return fmt.Errorf("failed to write prometheus config: %w", err) } - nginxPath := filepath.Join(namespacePath, "network-observer", "nginx", "conf.d", "default.conf") - if err := os.WriteFile(nginxPath, []byte(RenderNginxConfig(nginxPort, netobsPort)), 0644); err != nil { - return fmt.Errorf("failed to write nginx config: %w", err) - } - return nil } -func (i *Installer) generateCertificates() error { - namespacePath := api.GetHostNamespaceHome(i.Namespace) - caDir := filepath.Join(namespacePath, string(api.IssuersPath), "skupper-local-ca") - certDir := filepath.Join(namespacePath, "network-observer", "certs") - - return GenerateNginxCert(caDir, certDir) -} - -func (i *Installer) generateHtpasswd() (string, error) { - namespacePath := api.GetHostNamespaceHome(i.Namespace) - htpasswdPath := filepath.Join(namespacePath, "network-observer", "htpasswd", "htpasswd") - - username, password, htpasswdContent, err := GenerateHtpasswdCredentials(i.Username, i.Password) - if err != nil { - return "", fmt.Errorf("failed to generate htpasswd credentials: %w", err) - } - - if err := os.WriteFile(htpasswdPath, []byte(htpasswdContent), 0600); err != nil { - return "", fmt.Errorf("failed to write htpasswd file: %w", err) - } - - i.logger.Info("Generated htpasswd credentials", "username", username) - return password, nil -} - func (i *Installer) installContainer(newContainer container.Container) error { ctx, cn := context.WithTimeout(context.Background(), time.Minute*10) defer cn() diff --git a/internal/nonkube/network-observer/password.go b/internal/nonkube/network-observer/password.go deleted file mode 100644 index 0fde42a23..000000000 --- a/internal/nonkube/network-observer/password.go +++ /dev/null @@ -1,29 +0,0 @@ -package networkobserver - -import ( - "crypto/rand" - "encoding/base64" - "fmt" - - "golang.org/x/crypto/bcrypt" -) - -func GenerateHtpasswdCredentials(username string, password string) (string, string, string, error) { - if password == "" { - secretBytes := [16]byte{} - if _, err := rand.Read(secretBytes[:]); err != nil { - return "", "", "", fmt.Errorf("error generating random password: %w", err) - } - - password = base64.RawStdEncoding.EncodeToString(secretBytes[:]) - } - - hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return "", "", "", fmt.Errorf("error generating htpasswd hash: %w", err) - } - - htpasswdContent := fmt.Sprintf("%s:%s\n", username, string(hash)) - - return username, password, htpasswdContent, nil -} diff --git a/internal/nonkube/network-observer/password_test.go b/internal/nonkube/network-observer/password_test.go deleted file mode 100644 index 0a6461829..000000000 --- a/internal/nonkube/network-observer/password_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package networkobserver - -import ( - "strings" - "testing" - - "golang.org/x/crypto/bcrypt" -) - -func TestGenerateHtpasswdCredentials_UsesProvidedPasswordAndHashesStoredValue(t *testing.T) { - username, password, htpasswdContent, err := GenerateHtpasswdCredentials("foo", "bar") - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if username != "foo" { - t.Fatalf("expected username %q, got %q", "foo", username) - } - if password != "bar" { - t.Fatalf("expected password %q, got %q", "bar", password) - } - if strings.Contains(htpasswdContent, "bar") { - t.Fatalf("expected htpasswd content to avoid plain password, got %q", htpasswdContent) - } - // bcrypt hashes start with $2a$ or $2b$ - if !strings.HasPrefix(htpasswdContent, "foo:$2") { - t.Fatalf("expected bcrypt htpasswd entry, got %q", htpasswdContent) - } - if !strings.HasSuffix(htpasswdContent, "\n") { - t.Fatalf("expected trailing newline, got %q", htpasswdContent) - } - // verify the hash actually matches the password - hash := strings.TrimSuffix(strings.TrimPrefix(htpasswdContent, "foo:"), "\n") - if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte("bar")); err != nil { - t.Fatalf("bcrypt hash does not match password: %v", err) - } -} - -func TestGenerateHtpasswdCredentials_GeneratesPasswordAndHashesStoredValue(t *testing.T) { - username, password, htpasswdContent, err := GenerateHtpasswdCredentials("foo", "") - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if username != "foo" { - t.Fatalf("expected username %q, got %q", "foo", username) - } - if password == "" { - t.Fatal("expected generated password") - } - if strings.Contains(htpasswdContent, password) { - t.Fatalf("expected htpasswd content to avoid plain password, got %q", htpasswdContent) - } - // bcrypt hashes start with $2a$ or $2b$ - if !strings.HasPrefix(htpasswdContent, "foo:$2") { - t.Fatalf("expected bcrypt htpasswd entry, got %q", htpasswdContent) - } - if !strings.HasSuffix(htpasswdContent, "\n") { - t.Fatalf("expected trailing newline, got %q", htpasswdContent) - } - // verify the hash actually matches the generated password - hash := strings.TrimSuffix(strings.TrimPrefix(htpasswdContent, "foo:"), "\n") - if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil { - t.Fatalf("bcrypt hash does not match generated password: %v", err) - } -} diff --git a/internal/nonkube/network-observer/systemd.go b/internal/nonkube/network-observer/systemd.go index 47a8cba7b..ab6c46b2f 100644 --- a/internal/nonkube/network-observer/systemd.go +++ b/internal/nonkube/network-observer/systemd.go @@ -14,8 +14,8 @@ import ( const SystemdServiceTemplate = `[Unit] Description=Skupper Network Observer - %s After=network.target -Wants=skupper-network-observer-prometheus-%s.service skupper-network-observer-app-%s.service skupper-network-observer-nginx-%s.service -After=skupper-network-observer-prometheus-%s.service skupper-network-observer-app-%s.service skupper-network-observer-nginx-%s.service +Wants=skupper-network-observer-prometheus-%s.service skupper-network-observer-app-%s.service +After=skupper-network-observer-prometheus-%s.service skupper-network-observer-app-%s.service [Service] Type=oneshot @@ -77,7 +77,7 @@ ExecStart={{.ContainerEngine}} run --name %s-skupper-network-observer \ --restart always \ -v %s/runtime/certs/skupper-local-client:/etc/messaging:ro,z \ {{.NetworkObserverImage}} \ - -listen=127.0.0.1:{{.NetobsPort}} \ + -listen=:{{.NetobsPort}} \ -prometheus-api=http://127.0.0.1:{{.PrometheusPort}} \ -router-endpoint={{.RouterEndpoint}} \ -router-tls-ca=/etc/messaging/ca.crt \ @@ -91,34 +91,6 @@ ExecStopPost={{.ContainerEngine}} rm %s-skupper-network-observer WantedBy=skupper-network-observer-%s.service ` -const SystemdNginxServiceTemplate = `[Unit] -Description=Skupper Network Observer Nginx Proxy - %s -After=network.target skupper-network-observer-app-%s.service -PartOf=skupper-network-observer-%s.service - -[Service] -Type=simple -Restart=always -RestartSec=5 -ExecStartPre=-{{.ContainerEngine}} stop %s-skupper-nginx -ExecStartPre=-{{.ContainerEngine}} rm %s-skupper-nginx -ExecStart={{.ContainerEngine}} run --name %s-skupper-nginx \ - --label application=skupper-v2 \ - --label skupper.io/v2-component=nginx-proxy \ - --user={{.RunAsUser}} \ -{{.UsernsFlag}} --network host \ - --restart always \ - -v %s/network-observer/nginx/conf.d:/etc/nginx/conf.d:z \ - -v %s/network-observer/certs:/etc/certificates:z \ - -v %s/network-observer/htpasswd:/etc/httpusers:z \ - {{.NginxImage}} -ExecStop={{.ContainerEngine}} stop %s-skupper-nginx -ExecStopPost={{.ContainerEngine}} rm %s-skupper-nginx - -[Install] -WantedBy=skupper-network-observer-%s.service -` - type SystemdServiceManager struct { Namespace string ContainerEngine string @@ -161,8 +133,8 @@ func (s *SystemdServiceManager) CreateServices() error { mainServicePath := filepath.Join(s.ServiceDir, mainServiceName) mainServiceContent := fmt.Sprintf(SystemdServiceTemplate, s.Namespace, - s.Namespace, s.Namespace, s.Namespace, - s.Namespace, s.Namespace, s.Namespace) + s.Namespace, s.Namespace, + s.Namespace, s.Namespace) if err := os.WriteFile(mainServicePath, []byte(mainServiceContent), 0644); err != nil { return fmt.Errorf("failed to write main service file: %w", err) } @@ -181,13 +153,6 @@ func (s *SystemdServiceManager) CreateServices() error { return fmt.Errorf("failed to write network observer service file: %w", err) } - nginxServiceName := fmt.Sprintf("skupper-network-observer-nginx-%s.service", s.Namespace) - nginxServicePath := filepath.Join(s.ServiceDir, nginxServiceName) - nginxServiceContent := s.renderNginxService(namespacePath) - if err := os.WriteFile(nginxServicePath, []byte(nginxServiceContent), 0644); err != nil { - return fmt.Errorf("failed to write nginx service file: %w", err) - } - if err := s.reloadSystemd(); err != nil { return fmt.Errorf("failed to reload systemd: %w", err) } @@ -195,7 +160,6 @@ func (s *SystemdServiceManager) CreateServices() error { for _, svc := range []string{ fmt.Sprintf("skupper-network-observer-prometheus-%s.service", s.Namespace), fmt.Sprintf("skupper-network-observer-app-%s.service", s.Namespace), - fmt.Sprintf("skupper-network-observer-nginx-%s.service", s.Namespace), mainServiceName, } { if err := s.enableService(svc); err != nil { @@ -248,19 +212,6 @@ func (s *SystemdServiceManager) renderNetworkObserverService(namespacePath strin return content } -func (s *SystemdServiceManager) renderNginxService(namespacePath string) string { - content := fmt.Sprintf(SystemdNginxServiceTemplate, - s.Namespace, s.Namespace, s.Namespace, - s.Namespace, s.Namespace, s.Namespace, - namespacePath, namespacePath, namespacePath, - s.Namespace, s.Namespace, s.Namespace) - content = strings.ReplaceAll(content, "{{.ContainerEngine}}", s.ContainerEngine) - content = strings.ReplaceAll(content, "{{.RunAsUser}}", s.RunAsUser) - content = strings.ReplaceAll(content, "{{.UsernsFlag}}", s.userNsFlag()) - content = strings.ReplaceAll(content, "{{.NginxImage}}", images.GetNginxImageName()) - return content -} - func (s *SystemdServiceManager) reloadSystemd() error { var cmd *exec.Cmd if os.Getuid() == 0 { @@ -311,7 +262,6 @@ func (s *SystemdServiceManager) RemoveServices() error { mainServiceName, fmt.Sprintf("skupper-network-observer-prometheus-%s.service", s.Namespace), fmt.Sprintf("skupper-network-observer-app-%s.service", s.Namespace), - fmt.Sprintf("skupper-network-observer-nginx-%s.service", s.Namespace), } for _, serviceName := range serviceNames { From 06907199dd7697ef54916a84d53a98abad81e90b Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Fri, 24 Jul 2026 18:09:54 +0200 Subject: [PATCH 09/18] remove network observer if when running system uninstall --force --- .../system/nonkube/system_uninstall.go | 41 +++++++++++-------- .../system/nonkube/system_uninstall_test.go | 28 +++++++++---- .../nonkube/network-observer/installer.go | 11 +++++ 3 files changed, 57 insertions(+), 23 deletions(-) diff --git a/internal/cmd/skupper/system/nonkube/system_uninstall.go b/internal/cmd/skupper/system/nonkube/system_uninstall.go index cd9f54e2e..55264e6a1 100644 --- a/internal/cmd/skupper/system/nonkube/system_uninstall.go +++ b/internal/cmd/skupper/system/nonkube/system_uninstall.go @@ -11,19 +11,21 @@ import ( "github.com/skupperproject/skupper/internal/cmd/skupper/common" "github.com/skupperproject/skupper/internal/config" "github.com/skupperproject/skupper/internal/nonkube/bootstrap" + networkobserver "github.com/skupperproject/skupper/internal/nonkube/network-observer" "github.com/skupperproject/skupper/internal/utils/validator" "github.com/skupperproject/skupper/pkg/nonkube/api" "github.com/spf13/cobra" ) type CmdSystemUninstall struct { - CobraCmd *cobra.Command - Namespace string - SystemUninstall func(string) error - CheckActiveSites func() (bool, error) - Flags *common.CommandSystemUninstallFlags - forceUninstall bool - TearDown func(namespace string) error + CobraCmd *cobra.Command + Namespace string + SystemUninstall func(string) error + CheckActiveSites func() (bool, error) + Flags *common.CommandSystemUninstallFlags + forceUninstall bool + TearDown func(namespace string) error + NetworkObserverUninstall func(namespace string) error } func NewCmdSystemUninstall() *CmdSystemUninstall { @@ -38,6 +40,7 @@ func (cmd *CmdSystemUninstall) NewClient(cobraCommand *cobra.Command, args []str cmd.CheckActiveSites = bootstrap.CheckActiveSites cmd.Namespace = cobraCommand.Flag("namespace").Value.String() cmd.TearDown = bootstrap.Teardown + cmd.NetworkObserverUninstall = networkobserver.UninstallForNamespace } func (cmd *CmdSystemUninstall) ValidateInput(args []string) error { @@ -86,21 +89,27 @@ func (cmd *CmdSystemUninstall) Run() error { for _, entry := range entries { if entry.IsDir() { - runtimeDir := "namespaces/" + entry.Name() + "/runtime/" + namespace := entry.Name() + + if cmd.NetworkObserverUninstall != nil { + if err := cmd.NetworkObserverUninstall(namespace); err != nil { + return fmt.Errorf("failed to uninstall network observer for namespace %q: %s", namespace, err) + } + } + + runtimeDir := "namespaces/" + namespace + "/runtime/" _, err := os.ReadDir(path.Join(api.GetHostDataHome(), runtimeDir)) if err == nil { - fmt.Printf("Removing active site namespace \"%s\"\n", entry.Name()) - err := cmd.TearDown(entry.Name()) - if err != nil { - return fmt.Errorf("failed to remove site \"%s\": %s", entry.Name(), err) + fmt.Printf("Removing active site namespace %q\n", namespace) + if err := cmd.TearDown(namespace); err != nil { + return fmt.Errorf("failed to remove site %q: %s", namespace, err) } } else { // site not active so just remove directory - err := os.RemoveAll(api.GetHostNamespaceHome(entry.Name())) - if err == nil { - fmt.Printf("Namespace \"%s\" has been removed\n", entry.Name()) + if err := os.RemoveAll(api.GetHostNamespaceHome(namespace)); err == nil { + fmt.Printf("Namespace %q has been removed\n", namespace) } else { - return fmt.Errorf("failed to remove site \"%s\": %s", entry.Name(), err) + return fmt.Errorf("failed to remove site %q: %s", namespace, err) } } } diff --git a/internal/cmd/skupper/system/nonkube/system_uninstall_test.go b/internal/cmd/skupper/system/nonkube/system_uninstall_test.go index be1b21e61..a42763f49 100644 --- a/internal/cmd/skupper/system/nonkube/system_uninstall_test.go +++ b/internal/cmd/skupper/system/nonkube/system_uninstall_test.go @@ -131,10 +131,11 @@ func TestCmdSystemUninstall_InputToOptions(t *testing.T) { func TestCmdSystemUninstall_Run(t *testing.T) { type test struct { - name string - flags *common.CommandSystemUninstallFlags - disableSocketFails bool - errorMessage string + name string + flags *common.CommandSystemUninstallFlags + disableSocketFails bool + networkObserverFails bool + errorMessage string } testTable := []test{ @@ -150,6 +151,13 @@ func TestCmdSystemUninstall_Run(t *testing.T) { errorMessage: "Unable to uninstall.\nError: disable socket fails", flags: &common.CommandSystemUninstallFlags{Force: false}, }, + { + name: "network observer uninstall fails", + disableSocketFails: false, + networkObserverFails: true, + errorMessage: `failed to uninstall network observer for namespace "test": network observer uninstall failed`, + flags: &common.CommandSystemUninstallFlags{Force: true}, + }, } //Add a temp file so site exists for uninstall tests @@ -203,6 +211,9 @@ func TestCmdSystemUninstall_Run(t *testing.T) { for _, test := range testTable { command := newCmdSystemUninstallWithMocks(test.disableSocketFails) command.forceUninstall = test.flags.Force + if test.networkObserverFails { + command.NetworkObserverUninstall = mockNetworkObserverUninstallFails + } t.Run(test.name, func(t *testing.T) { @@ -229,9 +240,10 @@ func cleanup() { func newCmdSystemUninstallWithMocks(disableSocketFails bool) *CmdSystemUninstall { cmdMock := &CmdSystemUninstall{ - SystemUninstall: mockCmdSystemUninstall, - CheckActiveSites: mockCmdSystemUninstallNoActiveSites, - TearDown: mockCmdSystemTearDown, + SystemUninstall: mockCmdSystemUninstall, + CheckActiveSites: mockCmdSystemUninstallNoActiveSites, + TearDown: mockCmdSystemTearDown, + NetworkObserverUninstall: mockNetworkObserverUninstall, } if disableSocketFails { @@ -250,3 +262,5 @@ func mockCmdSystemUninstallThereAreStillSites() (bool, error) { return true, func mockCmdSystemUninstallCheckActiveSitesFails() (bool, error) { return false, fmt.Errorf("error") } func mockCmdSystemUninstallNoActiveSites() (bool, error) { return false, nil } func mockCmdSystemTearDown(string) error { return nil } +func mockNetworkObserverUninstall(string) error { return nil } +func mockNetworkObserverUninstallFails(string) error { return fmt.Errorf("network observer uninstall failed") } diff --git a/internal/nonkube/network-observer/installer.go b/internal/nonkube/network-observer/installer.go index 3d8af0726..2784eb595 100644 --- a/internal/nonkube/network-observer/installer.go +++ b/internal/nonkube/network-observer/installer.go @@ -173,6 +173,17 @@ func (i *Installer) ValidatePrerequisitesForUninstall() error { return nil } +func UninstallForNamespace(namespace string) error { + installer, err := NewInstaller(namespace) + if err != nil { + return nil + } + if err := installer.ValidatePrerequisitesForUninstall(); err != nil { + return nil + } + return installer.Uninstall() +} + func (i *Installer) Uninstall() error { i.logger.Info("Uninstalling network observer", slog.String("namespace", i.Namespace)) From fe70e6361e1ff259f5e5ba8b6d16ef088a97136b Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Mon, 27 Jul 2026 21:20:12 +0200 Subject: [PATCH 10/18] attaching containers to systemd services instead of recreating them --- internal/nonkube/network-observer/systemd.go | 101 +++---------------- 1 file changed, 15 insertions(+), 86 deletions(-) diff --git a/internal/nonkube/network-observer/systemd.go b/internal/nonkube/network-observer/systemd.go index ab6c46b2f..67e348153 100644 --- a/internal/nonkube/network-observer/systemd.go +++ b/internal/nonkube/network-observer/systemd.go @@ -5,10 +5,6 @@ import ( "os" "os/exec" "path/filepath" - "strings" - - "github.com/skupperproject/skupper/internal/images" - "github.com/skupperproject/skupper/pkg/nonkube/api" ) const SystemdServiceTemplate = `[Unit] @@ -36,22 +32,8 @@ PartOf=skupper-network-observer-%s.service Type=simple Restart=always RestartSec=5 -ExecStartPre=-{{.ContainerEngine}} stop %s-skupper-prometheus -ExecStartPre=-{{.ContainerEngine}} rm %s-skupper-prometheus -ExecStart={{.ContainerEngine}} run --name %s-skupper-prometheus \ - --label application=skupper-v2 \ - --label skupper.io/v2-component=prometheus \ - --user={{.RunAsUser}} \ -{{.UsernsFlag}} --network host \ - --restart always \ - -v %s/network-observer/prometheus:/etc/prometheus:z \ - -v %s/network-observer/prometheus/data:/prometheus:z \ - {{.PrometheusImage}} \ - --config.file=/etc/prometheus/prometheus.yml \ - --storage.tsdb.path=/prometheus/ \ - --web.listen-address=:{{.PrometheusPort}} -ExecStop={{.ContainerEngine}} stop %s-skupper-prometheus -ExecStopPost={{.ContainerEngine}} rm %s-skupper-prometheus +ExecStart=%s start --attach %s-skupper-prometheus +ExecStop=%s stop %s-skupper-prometheus [Install] WantedBy=skupper-network-observer-%s.service @@ -67,25 +49,8 @@ PartOf=skupper-network-observer-%s.service Type=simple Restart=always RestartSec=5 -ExecStartPre=-{{.ContainerEngine}} stop %s-skupper-network-observer -ExecStartPre=-{{.ContainerEngine}} rm %s-skupper-network-observer -ExecStart={{.ContainerEngine}} run --name %s-skupper-network-observer \ - --label application=skupper-v2 \ - --label skupper.io/v2-component=network-observer \ - --user={{.RunAsUser}} \ -{{.UsernsFlag}} --network host \ - --restart always \ - -v %s/runtime/certs/skupper-local-client:/etc/messaging:ro,z \ - {{.NetworkObserverImage}} \ - -listen=:{{.NetobsPort}} \ - -prometheus-api=http://127.0.0.1:{{.PrometheusPort}} \ - -router-endpoint={{.RouterEndpoint}} \ - -router-tls-ca=/etc/messaging/ca.crt \ - -router-tls-cert=/etc/messaging/tls.crt \ - -router-tls-key=/etc/messaging/tls.key \ - -listen-metrics=:{{.MetricsPort}} -ExecStop={{.ContainerEngine}} stop %s-skupper-network-observer -ExecStopPost={{.ContainerEngine}} rm %s-skupper-network-observer +ExecStart=%s start --attach %s-skupper-network-observer +ExecStop=%s stop %s-skupper-network-observer [Install] WantedBy=skupper-network-observer-%s.service @@ -95,18 +60,14 @@ type SystemdServiceManager struct { Namespace string ContainerEngine string ServiceDir string - RunAsUser string - ports ports } -func NewSystemdServiceManager(namespace, containerEngine string, p ports) *SystemdServiceManager { +func NewSystemdServiceManager(namespace, containerEngine string, _ ports) *SystemdServiceManager { serviceDir := getSystemdServiceDir() return &SystemdServiceManager{ Namespace: namespace, ContainerEngine: containerEngine, ServiceDir: serviceDir, - RunAsUser: fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), - ports: p, } } @@ -127,8 +88,6 @@ func (s *SystemdServiceManager) CreateServices() error { return fmt.Errorf("failed to create systemd service directory: %w", err) } - namespacePath := api.GetHostNamespaceHome(s.Namespace) - mainServiceName := fmt.Sprintf("skupper-network-observer-%s.service", s.Namespace) mainServicePath := filepath.Join(s.ServiceDir, mainServiceName) mainServiceContent := fmt.Sprintf(SystemdServiceTemplate, @@ -141,14 +100,22 @@ func (s *SystemdServiceManager) CreateServices() error { prometheusServiceName := fmt.Sprintf("skupper-network-observer-prometheus-%s.service", s.Namespace) prometheusServicePath := filepath.Join(s.ServiceDir, prometheusServiceName) - prometheusServiceContent := s.renderPrometheusService(namespacePath) + prometheusServiceContent := fmt.Sprintf(SystemdPrometheusServiceTemplate, + s.Namespace, s.Namespace, + s.ContainerEngine, s.Namespace, + s.ContainerEngine, s.Namespace, + s.Namespace) if err := os.WriteFile(prometheusServicePath, []byte(prometheusServiceContent), 0644); err != nil { return fmt.Errorf("failed to write prometheus service file: %w", err) } appServiceName := fmt.Sprintf("skupper-network-observer-app-%s.service", s.Namespace) appServicePath := filepath.Join(s.ServiceDir, appServiceName) - appServiceContent := s.renderNetworkObserverService(namespacePath) + appServiceContent := fmt.Sprintf(SystemdNetworkObserverServiceTemplate, + s.Namespace, s.Namespace, s.Namespace, + s.ContainerEngine, s.Namespace, + s.ContainerEngine, s.Namespace, + s.Namespace) if err := os.WriteFile(appServicePath, []byte(appServiceContent), 0644); err != nil { return fmt.Errorf("failed to write network observer service file: %w", err) } @@ -174,44 +141,6 @@ func (s *SystemdServiceManager) CreateServices() error { return nil } -func (s *SystemdServiceManager) userNsFlag() string { - if s.ContainerEngine == "podman" { - return " --userns=keep-id \\\n" - } - return "" -} - -func (s *SystemdServiceManager) renderPrometheusService(namespacePath string) string { - content := fmt.Sprintf(SystemdPrometheusServiceTemplate, - s.Namespace, s.Namespace, - s.Namespace, s.Namespace, s.Namespace, - namespacePath, namespacePath, - s.Namespace, s.Namespace, s.Namespace) - content = strings.ReplaceAll(content, "{{.ContainerEngine}}", s.ContainerEngine) - content = strings.ReplaceAll(content, "{{.RunAsUser}}", s.RunAsUser) - content = strings.ReplaceAll(content, "{{.UsernsFlag}}", s.userNsFlag()) - content = strings.ReplaceAll(content, "{{.PrometheusImage}}", images.GetPrometheusImageName()) - content = strings.ReplaceAll(content, "{{.PrometheusPort}}", fmt.Sprintf("%d", s.ports.prometheus)) - return content -} - -func (s *SystemdServiceManager) renderNetworkObserverService(namespacePath string) string { - content := fmt.Sprintf(SystemdNetworkObserverServiceTemplate, - s.Namespace, s.Namespace, s.Namespace, - s.Namespace, s.Namespace, s.Namespace, - namespacePath, - s.Namespace, s.Namespace, s.Namespace) - content = strings.ReplaceAll(content, "{{.ContainerEngine}}", s.ContainerEngine) - content = strings.ReplaceAll(content, "{{.RunAsUser}}", s.RunAsUser) - content = strings.ReplaceAll(content, "{{.UsernsFlag}}", s.userNsFlag()) - content = strings.ReplaceAll(content, "{{.NetworkObserverImage}}", images.GetNetworkObserverImageName()) - content = strings.ReplaceAll(content, "{{.NetobsPort}}", fmt.Sprintf("%d", s.ports.netobs)) - content = strings.ReplaceAll(content, "{{.PrometheusPort}}", fmt.Sprintf("%d", s.ports.prometheus)) - content = strings.ReplaceAll(content, "{{.RouterEndpoint}}", s.ports.router) - content = strings.ReplaceAll(content, "{{.MetricsPort}}", fmt.Sprintf("%d", s.ports.metrics)) - return content -} - func (s *SystemdServiceManager) reloadSystemd() error { var cmd *exec.Cmd if os.Getuid() == 0 { From 448f8baa3e9e864e24bb0e17f5c4f2cab095d9b7 Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Mon, 27 Jul 2026 21:27:31 +0200 Subject: [PATCH 11/18] configure localhost only --- internal/nonkube/network-observer/containers.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/nonkube/network-observer/containers.go b/internal/nonkube/network-observer/containers.go index b4173539d..95c9949bc 100644 --- a/internal/nonkube/network-observer/containers.go +++ b/internal/nonkube/network-observer/containers.go @@ -17,13 +17,12 @@ func GetNetworkObserverContainer(namespace string, p ports) container.Container Name: fmt.Sprintf("%s-skupper-network-observer", namespace), Image: images.GetNetworkObserverImageName(), Command: []string{ - fmt.Sprintf("-listen=:%d", p.netobs), + fmt.Sprintf("-listen=127.0.0.1:%d", p.netobs), fmt.Sprintf("-prometheus-api=http://127.0.0.1:%d", p.prometheus), fmt.Sprintf("-router-endpoint=%s", p.router), "-router-tls-ca=/etc/messaging/ca.crt", "-router-tls-cert=/etc/messaging/tls.crt", "-router-tls-key=/etc/messaging/tls.key", - fmt.Sprintf("-listen-metrics=:%d", p.metrics), }, Env: map[string]string{}, Labels: map[string]string{ @@ -53,7 +52,7 @@ func GetPrometheusContainer(namespace string, p ports) container.Container { Command: []string{ "--config.file=/etc/prometheus/prometheus.yml", "--storage.tsdb.path=/prometheus/", - fmt.Sprintf("--web.listen-address=:%d", p.prometheus), + fmt.Sprintf("--web.listen-address=127.0.0.1:%d", p.prometheus), }, Env: map[string]string{}, Labels: map[string]string{ From e96355cf10f3903e6520decf7edf413467edf4c9 Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Mon, 27 Jul 2026 21:30:26 +0200 Subject: [PATCH 12/18] fix example --- internal/cmd/skupper/system/system.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/skupper/system/system.go b/internal/cmd/skupper/system/system.go index be4a8c8ba..9d00307f9 100644 --- a/internal/cmd/skupper/system/system.go +++ b/internal/cmd/skupper/system/system.go @@ -245,7 +245,7 @@ func CmdSystemNetworkObserverFactory(configuredPlatform common.Platform) *cobra. The network observer requires an existing Skupper site and will deploy two containers: - network-observer: Collects metrics from the router - prometheus: Stores metrics`, - Example: `skupper network-observer --namespace west`, + Example: `skupper system network-observer --namespace west`, } cmd := common.ConfigureCobraCommand(configuredPlatform, cmdDesc, nil, nonKubeCommand) From b50afb2f8ff3b9143f1ab82b4096e21b9c5b3cb7 Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Mon, 27 Jul 2026 21:57:29 +0200 Subject: [PATCH 13/18] add log message when skipping network observer uninstall --- internal/nonkube/network-observer/installer.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/nonkube/network-observer/installer.go b/internal/nonkube/network-observer/installer.go index 2784eb595..ef2f2539d 100644 --- a/internal/nonkube/network-observer/installer.go +++ b/internal/nonkube/network-observer/installer.go @@ -176,9 +176,10 @@ func (i *Installer) ValidatePrerequisitesForUninstall() error { func UninstallForNamespace(namespace string) error { installer, err := NewInstaller(namespace) if err != nil { - return nil + return err } if err := installer.ValidatePrerequisitesForUninstall(); err != nil { + installer.logger.Info("Skipping network observer uninstall", slog.String("reason", err.Error())) return nil } return installer.Uninstall() From c14edba83979be9662f1c01ce239e1ced9715031 Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Mon, 27 Jul 2026 22:15:59 +0200 Subject: [PATCH 14/18] improve systemd services enablement and creation --- internal/nonkube/network-observer/systemd.go | 30 +++++++++----------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/internal/nonkube/network-observer/systemd.go b/internal/nonkube/network-observer/systemd.go index 67e348153..809382354 100644 --- a/internal/nonkube/network-observer/systemd.go +++ b/internal/nonkube/network-observer/systemd.go @@ -10,8 +10,6 @@ import ( const SystemdServiceTemplate = `[Unit] Description=Skupper Network Observer - %s After=network.target -Wants=skupper-network-observer-prometheus-%s.service skupper-network-observer-app-%s.service -After=skupper-network-observer-prometheus-%s.service skupper-network-observer-app-%s.service [Service] Type=oneshot @@ -90,10 +88,7 @@ func (s *SystemdServiceManager) CreateServices() error { mainServiceName := fmt.Sprintf("skupper-network-observer-%s.service", s.Namespace) mainServicePath := filepath.Join(s.ServiceDir, mainServiceName) - mainServiceContent := fmt.Sprintf(SystemdServiceTemplate, - s.Namespace, - s.Namespace, s.Namespace, - s.Namespace, s.Namespace) + mainServiceContent := fmt.Sprintf(SystemdServiceTemplate, s.Namespace) if err := os.WriteFile(mainServicePath, []byte(mainServiceContent), 0644); err != nil { return fmt.Errorf("failed to write main service file: %w", err) } @@ -120,13 +115,9 @@ func (s *SystemdServiceManager) CreateServices() error { return fmt.Errorf("failed to write network observer service file: %w", err) } - if err := s.reloadSystemd(); err != nil { - return fmt.Errorf("failed to reload systemd: %w", err) - } - for _, svc := range []string{ - fmt.Sprintf("skupper-network-observer-prometheus-%s.service", s.Namespace), - fmt.Sprintf("skupper-network-observer-app-%s.service", s.Namespace), + prometheusServiceName, + appServiceName, mainServiceName, } { if err := s.enableService(svc); err != nil { @@ -134,6 +125,15 @@ func (s *SystemdServiceManager) CreateServices() error { } } + for _, svc := range []string{ + prometheusServiceName, + appServiceName, + } { + if err := s.startService(svc); err != nil { + return fmt.Errorf("failed to start service %s: %w", svc, err) + } + } + if err := s.startService(mainServiceName); err != nil { return fmt.Errorf("failed to start service: %w", err) } @@ -180,13 +180,12 @@ func (s *SystemdServiceManager) startService(serviceName string) error { func (s *SystemdServiceManager) RemoveServices() error { mainServiceName := fmt.Sprintf("skupper-network-observer-%s.service", s.Namespace) - // Stop and disable the main service + if err := s.stopAndDisableService(mainServiceName); err != nil { - // Log but don't fail if service doesn't exist fmt.Printf("Warning: failed to stop service: %v\n", err) } - // Remove service files + serviceNames := []string{ mainServiceName, fmt.Sprintf("skupper-network-observer-prometheus-%s.service", s.Namespace), @@ -200,7 +199,6 @@ func (s *SystemdServiceManager) RemoveServices() error { } } - // Reload systemd if err := s.reloadSystemd(); err != nil { return fmt.Errorf("failed to reload systemd: %w", err) } From 58f62df64d25957d073b02698c082f1ac33562f8 Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Mon, 27 Jul 2026 22:35:44 +0200 Subject: [PATCH 15/18] fix formatting --- .../system/nonkube/system_uninstall_test.go | 14 ++++++++------ internal/nonkube/network-observer/systemd.go | 2 -- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/cmd/skupper/system/nonkube/system_uninstall_test.go b/internal/cmd/skupper/system/nonkube/system_uninstall_test.go index a42763f49..4d9572964 100644 --- a/internal/cmd/skupper/system/nonkube/system_uninstall_test.go +++ b/internal/cmd/skupper/system/nonkube/system_uninstall_test.go @@ -131,11 +131,11 @@ func TestCmdSystemUninstall_InputToOptions(t *testing.T) { func TestCmdSystemUninstall_Run(t *testing.T) { type test struct { - name string - flags *common.CommandSystemUninstallFlags - disableSocketFails bool - networkObserverFails bool - errorMessage string + name string + flags *common.CommandSystemUninstallFlags + disableSocketFails bool + networkObserverFails bool + errorMessage string } testTable := []test{ @@ -263,4 +263,6 @@ func mockCmdSystemUninstallCheckActiveSitesFails() (bool, error) { return false, func mockCmdSystemUninstallNoActiveSites() (bool, error) { return false, nil } func mockCmdSystemTearDown(string) error { return nil } func mockNetworkObserverUninstall(string) error { return nil } -func mockNetworkObserverUninstallFails(string) error { return fmt.Errorf("network observer uninstall failed") } +func mockNetworkObserverUninstallFails(string) error { + return fmt.Errorf("network observer uninstall failed") +} diff --git a/internal/nonkube/network-observer/systemd.go b/internal/nonkube/network-observer/systemd.go index 809382354..4abfe2cfe 100644 --- a/internal/nonkube/network-observer/systemd.go +++ b/internal/nonkube/network-observer/systemd.go @@ -180,12 +180,10 @@ func (s *SystemdServiceManager) startService(serviceName string) error { func (s *SystemdServiceManager) RemoveServices() error { mainServiceName := fmt.Sprintf("skupper-network-observer-%s.service", s.Namespace) - if err := s.stopAndDisableService(mainServiceName); err != nil { fmt.Printf("Warning: failed to stop service: %v\n", err) } - serviceNames := []string{ mainServiceName, fmt.Sprintf("skupper-network-observer-prometheus-%s.service", s.Namespace), From 409ca57fbd2a12f5ceb79d2ffdcc52d157bda2f6 Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Tue, 28 Jul 2026 15:10:48 +0200 Subject: [PATCH 16/18] uninstall netobs only if it was installed --- internal/nonkube/network-observer/installer.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/nonkube/network-observer/installer.go b/internal/nonkube/network-observer/installer.go index ef2f2539d..37a338cab 100644 --- a/internal/nonkube/network-observer/installer.go +++ b/internal/nonkube/network-observer/installer.go @@ -174,14 +174,16 @@ func (i *Installer) ValidatePrerequisitesForUninstall() error { } func UninstallForNamespace(namespace string) error { + namespacePath := api.GetHostNamespaceHome(namespace) + dataDir := filepath.Join(namespacePath, "network-observer") + if _, err := os.Stat(dataDir); os.IsNotExist(err) { + return nil + } + installer, err := NewInstaller(namespace) if err != nil { return err } - if err := installer.ValidatePrerequisitesForUninstall(); err != nil { - installer.logger.Info("Skipping network observer uninstall", slog.String("reason", err.Error())) - return nil - } return installer.Uninstall() } From f28116b272cac1f2c7da138af607a182458f0f31 Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Sun, 30 Aug 2026 21:19:07 +0200 Subject: [PATCH 17/18] add only a prometheus per host --- internal/cmd/skupper/common/flags.go | 9 +- .../system/kube/system_network-observer.go | 2 +- .../skupper/system/kube/system_prometheus.go | 34 ++++ .../system/kube/system_prometheus_test.go | 46 +++++ .../system/nonkube/system_prometheus.go | 79 ++++++++ .../system/nonkube/system_prometheus_test.go | 128 ++++++++++++ .../system/nonkube/system_uninstall.go | 8 + internal/cmd/skupper/system/system.go | 32 ++- .../network-observer/config_templates.go | 12 +- .../nonkube/network-observer/containers.go | 12 +- .../nonkube/network-observer/installer.go | 107 +++------- .../network-observer/prometheus_installer.go | 185 ++++++++++++++++++ .../prometheus_installer_test.go | 144 ++++++++++++++ .../network-observer/prometheus_state.go | 160 +++++++++++++++ .../network-observer/prometheus_state_test.go | 152 ++++++++++++++ internal/nonkube/network-observer/systemd.go | 177 ++++++++--------- pkg/nonkube/api/environment.go | 8 + 17 files changed, 1109 insertions(+), 186 deletions(-) create mode 100644 internal/cmd/skupper/system/kube/system_prometheus.go create mode 100644 internal/cmd/skupper/system/kube/system_prometheus_test.go create mode 100644 internal/cmd/skupper/system/nonkube/system_prometheus.go create mode 100644 internal/cmd/skupper/system/nonkube/system_prometheus_test.go create mode 100644 internal/nonkube/network-observer/prometheus_installer.go create mode 100644 internal/nonkube/network-observer/prometheus_installer_test.go create mode 100644 internal/nonkube/network-observer/prometheus_state.go create mode 100644 internal/nonkube/network-observer/prometheus_state_test.go diff --git a/internal/cmd/skupper/common/flags.go b/internal/cmd/skupper/common/flags.go index 636ef4c8f..464855880 100644 --- a/internal/cmd/skupper/common/flags.go +++ b/internal/cmd/skupper/common/flags.go @@ -94,7 +94,10 @@ for other Kubernetes flavors, loadbalancer is the default.` FlagDescReloadType = "Specify the type of reload to perform. Choices: manual, auto" FlagNameNetworkObserverUninstall = "uninstall" - FlagDescNetworkObserverUninstall = "Uninstall Network Observer and related containers" + FlagDescNetworkObserverUninstall = "Uninstall Network Observer" + + FlagNamePrometheusUninstall = "uninstall" + FlagDescPrometheusUninstall = "Uninstall Prometheus" ) type CommandSiteCreateFlags struct { @@ -252,6 +255,10 @@ type CommandNetworkObserverFlags struct { Uninstall bool } +type CommandPrometheusFlags struct { + Uninstall bool +} + type CommandVersionFlags struct { Output string } diff --git a/internal/cmd/skupper/system/kube/system_network-observer.go b/internal/cmd/skupper/system/kube/system_network-observer.go index c77858c1d..c1e38a229 100644 --- a/internal/cmd/skupper/system/kube/system_network-observer.go +++ b/internal/cmd/skupper/system/kube/system_network-observer.go @@ -15,7 +15,7 @@ type CmdSystemNetworkObserver struct { password string } -func NewCmdCmdSystemNetworkObserver() *CmdSystemNetworkObserver { +func NewCmdSystemNetworkObserver() *CmdSystemNetworkObserver { return &CmdSystemNetworkObserver{} } diff --git a/internal/cmd/skupper/system/kube/system_prometheus.go b/internal/cmd/skupper/system/kube/system_prometheus.go new file mode 100644 index 000000000..1bd88868b --- /dev/null +++ b/internal/cmd/skupper/system/kube/system_prometheus.go @@ -0,0 +1,34 @@ +package kube + +import ( + "fmt" + + "github.com/skupperproject/skupper/internal/cmd/skupper/common" + "github.com/spf13/cobra" +) + +type CmdSystemPrometheus struct { + CobraCmd *cobra.Command + Flags *common.CommandPrometheusFlags +} + +func NewCmdSystemPrometheus() *CmdSystemPrometheus { + return &CmdSystemPrometheus{} +} + +func (cmd *CmdSystemPrometheus) NewClient(cobraCommand *cobra.Command, args []string) {} + +func (cmd *CmdSystemPrometheus) ValidateInput(args []string) error { + return nil +} + +func (cmd *CmdSystemPrometheus) InputToOptions() {} + +func (cmd *CmdSystemPrometheus) Run() error { + fmt.Println("This command does not support kubernetes platforms.") + return nil +} + +func (cmd *CmdSystemPrometheus) WaitUntil() error { + return nil +} diff --git a/internal/cmd/skupper/system/kube/system_prometheus_test.go b/internal/cmd/skupper/system/kube/system_prometheus_test.go new file mode 100644 index 000000000..379f788e7 --- /dev/null +++ b/internal/cmd/skupper/system/kube/system_prometheus_test.go @@ -0,0 +1,46 @@ +package kube + +import ( + "testing" + + "github.com/skupperproject/skupper/internal/cmd/skupper/common" +) + +func TestCmdSystemPrometheus_ValidateInput(t *testing.T) { + tests := []struct { + name string + args []string + }{ + {name: "no arguments"}, + {name: "arguments are accepted", args: []string{"something"}}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmd := &CmdSystemPrometheus{Flags: &common.CommandPrometheusFlags{}} + if err := cmd.ValidateInput(test.args); err != nil { + t.Fatalf("expected no error, got %q", err) + } + }) + } +} + +func TestCmdSystemPrometheus_Run(t *testing.T) { + // The kube Run prints a not-supported message and always returns nil. + cmd := &CmdSystemPrometheus{Flags: &common.CommandPrometheusFlags{}} + if err := cmd.Run(); err != nil { + t.Fatalf("expected no error, got %q", err) + } +} + +func TestCmdSystemPrometheus_WaitUntil(t *testing.T) { + cmd := &CmdSystemPrometheus{} + if err := cmd.WaitUntil(); err != nil { + t.Fatalf("expected no error, got %q", err) + } +} + +func TestCmdSystemPrometheus_InputToOptions(t *testing.T) { + cmd := &CmdSystemPrometheus{Flags: &common.CommandPrometheusFlags{}} + cmd.InputToOptions() +} diff --git a/internal/cmd/skupper/system/nonkube/system_prometheus.go b/internal/cmd/skupper/system/nonkube/system_prometheus.go new file mode 100644 index 000000000..4673dda9e --- /dev/null +++ b/internal/cmd/skupper/system/nonkube/system_prometheus.go @@ -0,0 +1,79 @@ +package nonkube + +import ( + "errors" + "fmt" + + "github.com/skupperproject/skupper/internal/cmd/skupper/common" + networkobserver "github.com/skupperproject/skupper/internal/nonkube/network-observer" + "github.com/spf13/cobra" +) + +type CmdSystemPrometheus struct { + CobraCmd *cobra.Command + Flags *common.CommandPrometheusFlags + Install func() error + Uninstall func() error +} + +func NewCmdSystemPrometheus() *CmdSystemPrometheus { + return &CmdSystemPrometheus{} +} + +func (cmd *CmdSystemPrometheus) NewClient(cobraCommand *cobra.Command, args []string) { + installer, err := networkobserver.NewPrometheusInstaller() + if err != nil { + return + } + cmd.Install = func() error { + if err := installer.ValidatePrerequisitesForInstall(); err != nil { + return fmt.Errorf("prerequisite validation failed: %w", err) + } + return installer.Install() + } + cmd.Uninstall = func() error { + if err := installer.ValidatePrerequisitesForUninstall(); err != nil { + return err + } + return installer.Uninstall() + } +} + +func (cmd *CmdSystemPrometheus) ValidateInput(args []string) error { + var validationErrors []error + + if len(args) > 0 { + validationErrors = append(validationErrors, fmt.Errorf("this command does not accept arguments")) + } + + return errors.Join(validationErrors...) +} + +func (cmd *CmdSystemPrometheus) InputToOptions() {} + +func (cmd *CmdSystemPrometheus) Run() error { + if cmd.Flags.Uninstall { + if cmd.Uninstall == nil { + return fmt.Errorf("failed to create prometheus installer") + } + if err := cmd.Uninstall(); err != nil { + return fmt.Errorf("uninstallation failed: %w", err) + } + fmt.Println("Prometheus uninstalled successfully!") + return nil + } + + if cmd.Install == nil { + return fmt.Errorf("failed to create prometheus installer") + } + if err := cmd.Install(); err != nil { + return fmt.Errorf("installation failed: %w", err) + } + + fmt.Println("Prometheus installed successfully!") + return nil +} + +func (cmd *CmdSystemPrometheus) WaitUntil() error { + return nil +} diff --git a/internal/cmd/skupper/system/nonkube/system_prometheus_test.go b/internal/cmd/skupper/system/nonkube/system_prometheus_test.go new file mode 100644 index 000000000..a7d5b6533 --- /dev/null +++ b/internal/cmd/skupper/system/nonkube/system_prometheus_test.go @@ -0,0 +1,128 @@ +package nonkube + +import ( + "fmt" + "strings" + "testing" + + "github.com/skupperproject/skupper/internal/cmd/skupper/common" +) + +func TestCmdSystemPrometheus_ValidateInput(t *testing.T) { + tests := []struct { + name string + args []string + expectedError string + }{ + { + name: "no arguments accepted", + args: nil, + }, + { + name: "rejects arguments", + args: []string{"extra"}, + expectedError: "this command does not accept arguments", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmd := &CmdSystemPrometheus{Flags: &common.CommandPrometheusFlags{}} + + err := cmd.ValidateInput(test.args) + if test.expectedError == "" { + if err != nil { + t.Fatalf("expected no error, got %q", err) + } + return + } + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), test.expectedError) { + t.Fatalf("expected error %q, got %q", test.expectedError, err.Error()) + } + }) + } +} + +func TestCmdSystemPrometheus_Run(t *testing.T) { + tests := []struct { + name string + flags *common.CommandPrometheusFlags + install func() error + uninstall func() error + expectedError string + }{ + { + name: "install succeeds", + flags: &common.CommandPrometheusFlags{Uninstall: false}, + install: func() error { return nil }, + }, + { + name: "install fails", + flags: &common.CommandPrometheusFlags{Uninstall: false}, + install: func() error { return fmt.Errorf("disk full") }, + expectedError: "installation failed: disk full", + }, + { + name: "install with nil installer (NewClient failed)", + flags: &common.CommandPrometheusFlags{Uninstall: false}, + install: nil, + expectedError: "failed to create prometheus installer", + }, + { + name: "uninstall succeeds", + flags: &common.CommandPrometheusFlags{Uninstall: true}, + uninstall: func() error { return nil }, + }, + { + name: "uninstall fails", + flags: &common.CommandPrometheusFlags{Uninstall: true}, + uninstall: func() error { return fmt.Errorf("container not found") }, + expectedError: "uninstallation failed: container not found", + }, + { + name: "uninstall with nil installer (NewClient failed)", + flags: &common.CommandPrometheusFlags{Uninstall: true}, + uninstall: nil, + expectedError: "failed to create prometheus installer", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmd := &CmdSystemPrometheus{ + Flags: test.flags, + Install: test.install, + Uninstall: test.uninstall, + } + + err := cmd.Run() + if test.expectedError == "" { + if err != nil { + t.Fatalf("expected no error, got %q", err) + } + return + } + if err == nil { + t.Fatal("expected error, got nil") + } + if err.Error() != test.expectedError { + t.Fatalf("expected error %q, got %q", test.expectedError, err.Error()) + } + }) + } +} + +func TestCmdSystemPrometheus_WaitUntil(t *testing.T) { + cmd := &CmdSystemPrometheus{} + if err := cmd.WaitUntil(); err != nil { + t.Fatalf("expected no error, got %q", err) + } +} + +func TestCmdSystemPrometheus_InputToOptions(t *testing.T) { + cmd := &CmdSystemPrometheus{Flags: &common.CommandPrometheusFlags{}} + cmd.InputToOptions() +} diff --git a/internal/cmd/skupper/system/nonkube/system_uninstall.go b/internal/cmd/skupper/system/nonkube/system_uninstall.go index 55264e6a1..6425c3f82 100644 --- a/internal/cmd/skupper/system/nonkube/system_uninstall.go +++ b/internal/cmd/skupper/system/nonkube/system_uninstall.go @@ -26,6 +26,7 @@ type CmdSystemUninstall struct { forceUninstall bool TearDown func(namespace string) error NetworkObserverUninstall func(namespace string) error + PrometheusUninstall func() error } func NewCmdSystemUninstall() *CmdSystemUninstall { @@ -41,6 +42,7 @@ func (cmd *CmdSystemUninstall) NewClient(cobraCommand *cobra.Command, args []str cmd.Namespace = cobraCommand.Flag("namespace").Value.String() cmd.TearDown = bootstrap.Teardown cmd.NetworkObserverUninstall = networkobserver.UninstallForNamespace + cmd.PrometheusUninstall = networkobserver.UninstallPrometheus } func (cmd *CmdSystemUninstall) ValidateInput(args []string) error { @@ -116,6 +118,12 @@ func (cmd *CmdSystemUninstall) Run() error { } } + if cmd.PrometheusUninstall != nil { + if err := cmd.PrometheusUninstall(); err != nil { + return fmt.Errorf("failed to uninstall prometheus: %s", err) + } + } + err := cmd.SystemUninstall(string(config.GetPlatform())) if err != nil { diff --git a/internal/cmd/skupper/system/system.go b/internal/cmd/skupper/system/system.go index 9d00307f9..9656cd23b 100644 --- a/internal/cmd/skupper/system/system.go +++ b/internal/cmd/skupper/system/system.go @@ -36,6 +36,7 @@ approach, which is based on the new set of Custom Resource Definitions (CRDs).`, cmd.AddCommand(CmdSystemApplyFactory(platform)) cmd.AddCommand(CmdSystemDeleteFactory(platform)) cmd.AddCommand(CmdSystemNetworkObserverFactory(platform)) + cmd.AddCommand(CmdSystemPrometheusFactory(platform)) return cmd } @@ -235,7 +236,7 @@ func CmdSystemDeleteFactory(configuredPlatform common.Platform) *cobra.Command { func CmdSystemNetworkObserverFactory(configuredPlatform common.Platform) *cobra.Command { //This implementation will warn the user that the command is not available for Kubernetes environments. - kubeCommand := kube.NewCmdCmdSystemNetworkObserver() + kubeCommand := kube.NewCmdSystemNetworkObserver() nonKubeCommand := nonkube.NewCmdSystemNetworkObserver() cmdDesc := common.SkupperCmdDescription{ @@ -248,7 +249,7 @@ The network observer requires an existing Skupper site and will deploy two conta Example: `skupper system network-observer --namespace west`, } - cmd := common.ConfigureCobraCommand(configuredPlatform, cmdDesc, nil, nonKubeCommand) + cmd := common.ConfigureCobraCommand(configuredPlatform, cmdDesc, kubeCommand, nonKubeCommand) cmdFlags := common.CommandNetworkObserverFlags{} cmd.Flags().BoolVar(&cmdFlags.Uninstall, common.FlagNameNetworkObserverUninstall, false, common.FlagDescNetworkObserverUninstall) @@ -260,3 +261,30 @@ The network observer requires an existing Skupper site and will deploy two conta return cmd } + +func CmdSystemPrometheusFactory(configuredPlatform common.Platform) *cobra.Command { + + kubeCommand := kube.NewCmdSystemPrometheus() + nonKubeCommand := nonkube.NewCmdSystemPrometheus() + + cmdDesc := common.SkupperCmdDescription{ + Use: "prometheus", + Short: "Install the host-level Prometheus instance", + Long: `Install a single Prometheus container shared by all network observers on this host. +This must be run before installing any network observer.`, + Example: `skupper system prometheus +skupper system prometheus --uninstall`, + } + + cmd := common.ConfigureCobraCommand(configuredPlatform, cmdDesc, kubeCommand, nonKubeCommand) + + cmdFlags := common.CommandPrometheusFlags{} + cmd.Flags().BoolVar(&cmdFlags.Uninstall, common.FlagNamePrometheusUninstall, false, common.FlagDescPrometheusUninstall) + + kubeCommand.CobraCmd = cmd + kubeCommand.Flags = &cmdFlags + nonKubeCommand.CobraCmd = cmd + nonKubeCommand.Flags = &cmdFlags + + return cmd +} diff --git a/internal/nonkube/network-observer/config_templates.go b/internal/nonkube/network-observer/config_templates.go index 10ac3cce6..640cdda5e 100644 --- a/internal/nonkube/network-observer/config_templates.go +++ b/internal/nonkube/network-observer/config_templates.go @@ -2,7 +2,7 @@ package networkobserver import "fmt" -func RenderPrometheusConfig(netobsPort int) string { +func RenderPrometheusConfig(targetsDir string) string { return fmt.Sprintf(`global: scrape_interval: 15s evaluation_interval: 15s @@ -11,11 +11,13 @@ alerting: - static_configs: - targets: scrape_configs: - - job_name: "network-observer-local" + - job_name: "skupper-network-observers" scheme: http follow_redirects: true enable_http2: true - static_configs: - - targets: ["localhost:%d"] -`, netobsPort) + file_sd_configs: + - files: + - "%s/*.json" + refresh_interval: 15s +`, targetsDir) } diff --git a/internal/nonkube/network-observer/containers.go b/internal/nonkube/network-observer/containers.go index 95c9949bc..1c25524db 100644 --- a/internal/nonkube/network-observer/containers.go +++ b/internal/nonkube/network-observer/containers.go @@ -18,6 +18,7 @@ func GetNetworkObserverContainer(namespace string, p ports) container.Container Image: images.GetNetworkObserverImageName(), Command: []string{ fmt.Sprintf("-listen=127.0.0.1:%d", p.netobs), + fmt.Sprintf("-listen-metrics=127.0.0.1:%d", p.metrics), fmt.Sprintf("-prometheus-api=http://127.0.0.1:%d", p.prometheus), fmt.Sprintf("-router-endpoint=%s", p.router), "-router-tls-ca=/etc/messaging/ca.crt", @@ -41,13 +42,12 @@ func GetNetworkObserverContainer(namespace string, p ports) container.Container } } -func GetPrometheusContainer(namespace string, p ports) container.Container { - namespacePath := api.GetHostNamespaceHome(namespace) - prometheusDir := filepath.Join(namespacePath, "network-observer", "prometheus") - dataPath := filepath.Join(namespacePath, "network-observer", "prometheus", "data") +func GetHostPrometheusContainer(p prometheusInstallerPorts) container.Container { + prometheusHome := api.GetHostPrometheusHome() + dataPath := filepath.Join(prometheusHome, "data") return container.Container{ - Name: fmt.Sprintf("%s-skupper-prometheus", namespace), + Name: "skupper-prometheus", Image: images.GetPrometheusImageName(), Command: []string{ "--config.file=/etc/prometheus/prometheus.yml", @@ -61,7 +61,7 @@ func GetPrometheusContainer(namespace string, p ports) container.Container { }, FileMounts: []container.FileMount{ { - Source: prometheusDir, + Source: prometheusHome, Destination: "/etc/prometheus", Options: []string{"z"}, }, diff --git a/internal/nonkube/network-observer/installer.go b/internal/nonkube/network-observer/installer.go index 37a338cab..d6136b9e8 100644 --- a/internal/nonkube/network-observer/installer.go +++ b/internal/nonkube/network-observer/installer.go @@ -77,15 +77,13 @@ func (i *Installer) ValidatePrerequisitesForInstall() error { return err } - containerNames := []string{ - fmt.Sprintf("%s-skupper-prometheus", i.Namespace), - fmt.Sprintf("%s-skupper-network-observer", i.Namespace), + if !IsPrometheusInstalled() { + return fmt.Errorf("prometheus is not installed; run \"skupper system prometheus\" first") } - for _, containerName := range containerNames { - if i.isContainerRunning(containerName) { - return fmt.Errorf("container %q is already running in %s", containerName, i.Platform) - } + netobsContainer := fmt.Sprintf("%s-skupper-network-observer", i.Namespace) + if i.isContainerRunning(netobsContainer) { + return fmt.Errorf("container %q is already running in %s", netobsContainer, i.Platform) } sites, err := i.siteHandler.List(fs.GetOptions{InputOnly: true}) @@ -113,10 +111,6 @@ func (i *Installer) Install() (*InstallResult, error) { i.logger.Info("Starting network observer installation", slog.String("namespace", i.Namespace)) - if err := i.createDirectories(); err != nil { - return nil, fmt.Errorf("failed to create directories: %w", err) - } - if err := i.generateConfigurations(); err != nil { return nil, fmt.Errorf("failed to generate configurations: %w", err) } @@ -135,12 +129,12 @@ func (i *Installer) Install() (*InstallResult, error) { if err != nil { return nil, err } - err = i.installContainer(GetPrometheusContainer(i.Namespace, i.ports)) - if err != nil { - return nil, err + + if err := WriteTargetFile(i.Namespace, i.ports.metrics); err != nil { + return nil, fmt.Errorf("failed to write prometheus target file: %w", err) } - err = i.createSystemdServices() + err = i.createNetObsSystemdService() if err != nil { return nil, fmt.Errorf("failed to create systemd services: %w", err) } @@ -153,30 +147,16 @@ func (i *Installer) Install() (*InstallResult, error) { } func (i *Installer) ValidatePrerequisitesForUninstall() error { - - containerNames := []string{ - fmt.Sprintf("%s-skupper-prometheus", i.Namespace), - fmt.Sprintf("%s-skupper-network-observer", i.Namespace), + netobsContainer := fmt.Sprintf("%s-skupper-network-observer", i.Namespace) + if !i.isContainerRunning(netobsContainer) { + return fmt.Errorf("network observer is not running in namespace %q, there is nothing to uninstall", i.Namespace) } - - containersAreRunning := false - for _, containerName := range containerNames { - if i.isContainerRunning(containerName) { - containersAreRunning = true - } - } - - if !containersAreRunning { - return fmt.Errorf("network observer containers not running in namespace %q, there is nothing to uninstall", i.Namespace) - } - return nil } func UninstallForNamespace(namespace string) error { - namespacePath := api.GetHostNamespaceHome(namespace) - dataDir := filepath.Join(namespacePath, "network-observer") - if _, err := os.Stat(dataDir); os.IsNotExist(err) { + targetFile := filepath.Join(api.GetPrometheusTargetsDir(), namespace+".json") + if _, err := os.Stat(targetFile); os.IsNotExist(err) { return nil } @@ -190,14 +170,17 @@ func UninstallForNamespace(namespace string) error { func (i *Installer) Uninstall() error { i.logger.Info("Uninstalling network observer", slog.String("namespace", i.Namespace)) + if err := RemoveTargetFile(i.Namespace); err != nil { + i.logger.Warn("Failed to remove prometheus target file", slog.Any("error", err)) + } + manager := NewSystemdServiceManager(i.Namespace, i.Platform, ports{}) - if err := manager.RemoveServices(); err != nil { + if err := manager.RemoveNetworkObserverService(); err != nil { i.logger.Warn("Failed to remove systemd services", slog.Any("error", err)) } containerNames := []string{ fmt.Sprintf("%s-skupper-network-observer", i.Namespace), - fmt.Sprintf("%s-skupper-prometheus", i.Namespace), } for _, name := range containerNames { if i.isContainerRunning(name) { @@ -211,12 +194,6 @@ func (i *Installer) Uninstall() error { } } - namespacePath := api.GetHostNamespaceHome(i.Namespace) - dataDir := filepath.Join(namespacePath, "network-observer") - if err := os.RemoveAll(dataDir); err != nil { - i.logger.Warn("Failed to remove network-observer data directory", slog.String("path", dataDir), slog.Any("error", err)) - } - i.logger.Info("Network observer uninstalled successfully") return nil } @@ -296,37 +273,14 @@ func (i *Installer) isContainerRunning(containerName string) bool { return false } -func (i *Installer) createDirectories() error { - namespacePath := api.GetHostNamespaceHome(i.Namespace) - dirs := []string{ - filepath.Join(namespacePath, "network-observer"), - filepath.Join(namespacePath, "network-observer", "prometheus"), - } - - for _, dir := range dirs { - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("failed to create directory %s: %w", dir, err) - } - } - - dataDir := filepath.Join(namespacePath, "network-observer", "prometheus", "data") - if err := os.MkdirAll(dataDir, 0750); err != nil { - return fmt.Errorf("failed to create directory %s: %w", dataDir, err) - } - - return nil -} - func (i *Installer) generateConfigurations() error { - namespacePath := api.GetHostNamespaceHome(i.Namespace) - - prometheusPort, err := utils.TcpPortNextFree(9090) + prometheusPort, err := ReadPrometheusPort() if err != nil { - return fmt.Errorf("failing to assign port to prometheus: %s", err) + return err } - metricsPort, err := utils.TcpPortNextFree(9000) + metricsPort, err := NextFreeMetricsPort(9000) if err != nil { - return fmt.Errorf("failing to assign port to prometheus API: %s", err) + return fmt.Errorf("failing to assign port to metrics: %s", err) } netobsPort, err := utils.TcpPortNextFree(8080) if err != nil { @@ -352,11 +306,6 @@ func (i *Installer) generateConfigurations() error { slog.String("router", routerEndpoint), ) - prometheusPath := filepath.Join(namespacePath, "network-observer", "prometheus", "prometheus.yml") - if err := os.WriteFile(prometheusPath, []byte(RenderPrometheusConfig(netobsPort)), 0644); err != nil { - return fmt.Errorf("failed to write prometheus config: %w", err) - } - return nil } @@ -381,15 +330,15 @@ func (i *Installer) installContainer(newContainer container.Container) error { return nil } -func (i *Installer) createSystemdServices() error { - i.logger.Info("Creating systemd services", slog.String("namespace", i.Namespace)) +func (i *Installer) createNetObsSystemdService() error { + i.logger.Info("Creating systemd service for Network Observer", slog.String("namespace", i.Namespace)) manager := NewSystemdServiceManager(i.Namespace, i.Platform, i.ports) - err := manager.CreateServices() + err := manager.CreateNetworkObserverService() if err != nil { - return fmt.Errorf("failed to create systemd services: %w", err) + return fmt.Errorf("failed to create systemd service: %w", err) } - i.logger.Info("Systemd services created successfully") + i.logger.Info("Systemd service created successfully") return nil } diff --git a/internal/nonkube/network-observer/prometheus_installer.go b/internal/nonkube/network-observer/prometheus_installer.go new file mode 100644 index 000000000..4f5a98d48 --- /dev/null +++ b/internal/nonkube/network-observer/prometheus_installer.go @@ -0,0 +1,185 @@ +package networkobserver + +import ( + "context" + "fmt" + "log/slog" + "os" + "path/filepath" + "time" + + "github.com/skupperproject/skupper/internal/nonkube/client/compat" + "github.com/skupperproject/skupper/internal/utils" + "github.com/skupperproject/skupper/pkg/container" + "github.com/skupperproject/skupper/pkg/nonkube/api" +) + +type prometheusInstallerPorts struct { + prometheus int +} + +type PrometheusInstaller struct { + Platform string + ports prometheusInstallerPorts + logger *slog.Logger + cli *compat.CompatClient +} + +func NewPrometheusInstaller() (*PrometheusInstaller, error) { + selectedPlatform, err := detectPlatform() + if err != nil { + return nil, err + } + containerEndpoint, err := getContainerEndpoint(selectedPlatform) + if err != nil { + return nil, err + } + compatClient, err := compat.NewCompatClient(containerEndpoint, "") + if err != nil { + return nil, fmt.Errorf("failed to create container client: %v", err) + } + return &PrometheusInstaller{ + Platform: selectedPlatform, + logger: slog.Default().With("component", "prometheus.installer"), + cli: compatClient, + }, nil +} + +func (p *PrometheusInstaller) isContainerRunning(name string) bool { + containers, err := p.cli.ContainerList() + if err != nil { + return false + } + for _, c := range containers { + if c.Name == name { + return c.Running + } + } + return false +} + +func (p *PrometheusInstaller) ValidatePrerequisitesForInstall() error { + if p.isContainerRunning("skupper-prometheus") { + return fmt.Errorf("container \"skupper-prometheus\" is already running in %s", p.Platform) + } + return nil +} + +func (p *PrometheusInstaller) Install() error { + p.logger.Info("Starting host-level prometheus installation") + + prometheusHome := api.GetHostPrometheusHome() + targetsDir := api.GetPrometheusTargetsDir() + dataDir := filepath.Join(prometheusHome, "data") + + for _, d := range []struct { + path string + perm os.FileMode + }{ + {prometheusHome, 0755}, + {targetsDir, 0755}, + {dataDir, 0750}, + } { + if err := os.MkdirAll(d.path, d.perm); err != nil { + return fmt.Errorf("failed to create directory %s: %w", d.path, err) + } + } + + prometheusPort, err := utils.TcpPortNextFree(9090) + if err != nil { + return fmt.Errorf("failed to assign port to prometheus: %w", err) + } + p.ports = prometheusInstallerPorts{prometheus: prometheusPort} + p.logger.Info("Assigned prometheus port", slog.Int("port", prometheusPort)) + + configPath := filepath.Join(prometheusHome, "prometheus.yml") + if err := os.WriteFile(configPath, []byte(RenderPrometheusConfig("/etc/prometheus/targets")), 0644); err != nil { + return fmt.Errorf("failed to write prometheus config: %w", err) + } + + if err := p.installContainer(GetHostPrometheusContainer(p.ports)); err != nil { + return err + } + + manager := &SystemdServiceManager{ + ContainerEngine: p.Platform, + ServiceDir: getSystemdServiceDir(), + } + if err := manager.CreatePrometheusService(); err != nil { + return fmt.Errorf("failed to create prometheus systemd service: %w", err) + } + + if err := WritePrometheusState(prometheusPort); err != nil { + return err + } + + p.logger.Info("Host-level prometheus installation completed", slog.Int("port", prometheusPort)) + return nil +} + +func (p *PrometheusInstaller) ValidatePrerequisitesForUninstall() error { + if !p.isContainerRunning("skupper-prometheus") { + return fmt.Errorf("container \"skupper-prometheus\" is not running in %s; nothing to uninstall", p.Platform) + } + if namespaces := installedNetworkObservers(); len(namespaces) > 0 { + return fmt.Errorf("network observers are still installed (%s); run \"skupper system network-observer --uninstall\" for each namespace first", joinStrings(namespaces)) + } + return nil +} + +func (p *PrometheusInstaller) Uninstall() error { + p.logger.Info("Uninstalling host-level prometheus") + + manager := &SystemdServiceManager{ + ContainerEngine: p.Platform, + ServiceDir: getSystemdServiceDir(), + } + if err := manager.RemovePrometheusService(); err != nil { + p.logger.Warn("Failed to remove prometheus systemd service", slog.Any("error", err)) + } + + const containerName = "skupper-prometheus" + if p.isContainerRunning(containerName) { + if err := p.cli.ContainerStop(containerName); err != nil { + p.logger.Warn("Failed to stop container", slog.String("name", containerName), slog.Any("error", err)) + } + } + if err := p.cli.ContainerRemove(containerName); err != nil { + p.logger.Warn("Failed to remove container", slog.String("name", containerName), slog.Any("error", err)) + } + + prometheusHome := api.GetHostPrometheusHome() + if err := os.RemoveAll(prometheusHome); err != nil { + p.logger.Warn("Failed to remove prometheus directory", slog.String("path", prometheusHome), slog.Any("error", err)) + } + + p.logger.Info("Host-level prometheus uninstalled successfully") + return nil +} + +func UninstallPrometheus() error { + if !IsPrometheusInstalled() { + return nil + } + installer, err := NewPrometheusInstaller() + if err != nil { + return err + } + return installer.Uninstall() +} + +func (p *PrometheusInstaller) installContainer(newContainer container.Container) error { + ctx, cn := context.WithTimeout(context.Background(), time.Minute*10) + defer cn() + if err := p.cli.ImagePull(ctx, newContainer.Image); err != nil { + return fmt.Errorf("failed to pull image: %v", err) + } + fmt.Printf("Pulled image: %s\n", newContainer.Image) + if err := p.cli.ContainerCreate(&newContainer); err != nil { + return fmt.Errorf("failed to create container %s: %v", newContainer.Name, err) + } + if err := p.cli.ContainerStart(newContainer.Name); err != nil { + return fmt.Errorf("failed to start container %s: %v", newContainer.Name, err) + } + return nil +} diff --git a/internal/nonkube/network-observer/prometheus_installer_test.go b/internal/nonkube/network-observer/prometheus_installer_test.go new file mode 100644 index 000000000..b7c42297a --- /dev/null +++ b/internal/nonkube/network-observer/prometheus_installer_test.go @@ -0,0 +1,144 @@ +package networkobserver + +import ( + "log/slog" + "os" + "testing" + + "github.com/skupperproject/skupper/internal/nonkube/client/compat" + "github.com/skupperproject/skupper/pkg/container" + "github.com/skupperproject/skupper/pkg/nonkube/api" + "gotest.tools/v3/assert" +) + +func newMockContainer(name string, running bool) *container.Container { + return &container.Container{ + Name: "/" + name, + Running: running, + Labels: map[string]string{"application": container.AppName}, + Image: "mock-image", + } +} + +func newPrometheusInstallerWithMock(containers []*container.Container) *PrometheusInstaller { + return &PrometheusInstaller{ + Platform: "podman", + logger: slog.Default(), + cli: compat.NewCompatClientMock(containers), + } +} + +func TestValidatePrerequisitesForInstall_AlreadyRunning(t *testing.T) { + installer := newPrometheusInstallerWithMock([]*container.Container{ + newMockContainer("skupper-prometheus", true), + }) + + err := installer.ValidatePrerequisitesForInstall() + assert.ErrorContains(t, err, `"skupper-prometheus" is already running`) +} + +func TestValidatePrerequisitesForInstall_NotRunning(t *testing.T) { + installer := newPrometheusInstallerWithMock([]*container.Container{}) + + assert.NilError(t, installer.ValidatePrerequisitesForInstall()) +} + +func TestValidatePrerequisitesForInstall_ContainerExistsButStopped(t *testing.T) { + installer := newPrometheusInstallerWithMock([]*container.Container{ + newMockContainer("skupper-prometheus", false), + }) + + assert.NilError(t, installer.ValidatePrerequisitesForInstall()) +} + +func TestValidatePrerequisitesForUninstall_NotRunning(t *testing.T) { + installer := newPrometheusInstallerWithMock([]*container.Container{}) + + err := installer.ValidatePrerequisitesForUninstall() + assert.ErrorContains(t, err, "is not running") + assert.ErrorContains(t, err, "nothing to uninstall") +} + +func TestValidatePrerequisitesForUninstall_NetworkObserversStillInstalled(t *testing.T) { + setTempPrometheusHome(t) + + assert.NilError(t, WriteTargetFile("west", 9001)) + + installer := newPrometheusInstallerWithMock([]*container.Container{ + newMockContainer("skupper-prometheus", true), + }) + + err := installer.ValidatePrerequisitesForUninstall() + assert.ErrorContains(t, err, "network observers are still installed") + assert.ErrorContains(t, err, "west") +} + +func TestValidatePrerequisitesForUninstall_OK(t *testing.T) { + setTempPrometheusHome(t) + + installer := newPrometheusInstallerWithMock([]*container.Container{ + newMockContainer("skupper-prometheus", true), + }) + + assert.NilError(t, installer.ValidatePrerequisitesForUninstall()) +} + +func TestPrometheusInstaller_isContainerRunning(t *testing.T) { + tests := []struct { + name string + containers []*container.Container + query string + expected bool + }{ + { + name: "running container found", + containers: []*container.Container{newMockContainer("skupper-prometheus", true)}, + query: "skupper-prometheus", + expected: true, + }, + { + name: "stopped container not considered running", + containers: []*container.Container{newMockContainer("skupper-prometheus", false)}, + query: "skupper-prometheus", + expected: false, + }, + { + name: "unknown container name returns false", + containers: []*container.Container{newMockContainer("skupper-prometheus", true)}, + query: "other-container", + expected: false, + }, + { + name: "empty container list returns false", + containers: []*container.Container{}, + query: "skupper-prometheus", + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + installer := newPrometheusInstallerWithMock(tc.containers) + assert.Equal(t, tc.expected, installer.isContainerRunning(tc.query)) + }) + } +} + +func TestPrometheusInstaller_Uninstall_RemovesStateAndDir(t *testing.T) { + setTempPrometheusHome(t) + + prometheusHome := api.GetHostPrometheusHome() + if err := os.MkdirAll(prometheusHome, 0755); err != nil { + t.Fatalf("setup: %v", err) + } + assert.NilError(t, WritePrometheusState(9090)) + + installer := newPrometheusInstallerWithMock([]*container.Container{ + newMockContainer("skupper-prometheus", true), + }) + + assert.NilError(t, installer.Uninstall()) + + _, err := os.Stat(prometheusHome) + assert.Assert(t, os.IsNotExist(err), "expected prometheus home to be removed") +} diff --git a/internal/nonkube/network-observer/prometheus_state.go b/internal/nonkube/network-observer/prometheus_state.go new file mode 100644 index 000000000..e691618ec --- /dev/null +++ b/internal/nonkube/network-observer/prometheus_state.go @@ -0,0 +1,160 @@ +package networkobserver + +import ( + "encoding/json" + "fmt" + "net" + "os" + "path/filepath" + "strconv" + + "github.com/skupperproject/skupper/internal/utils" + "github.com/skupperproject/skupper/pkg/nonkube/api" +) + +type PrometheusState struct { + Port int `json:"port"` +} + +func prometheusStateFile() string { + return filepath.Join(api.GetHostPrometheusHome(), "prometheus.yml.state") +} + +func WritePrometheusState(port int) error { + data, err := json.Marshal(PrometheusState{Port: port}) + if err != nil { + return fmt.Errorf("failed to marshal prometheus state: %w", err) + } + if err := os.WriteFile(prometheusStateFile(), data, 0644); err != nil { + return fmt.Errorf("failed to write prometheus state file: %w", err) + } + return nil +} + +func ReadPrometheusPort() (int, error) { + data, err := os.ReadFile(prometheusStateFile()) + if err != nil { + if os.IsNotExist(err) { + return 0, fmt.Errorf("prometheus is not installed; run \"skupper system prometheus\" first") + } + return 0, fmt.Errorf("failed to read prometheus state file: %w", err) + } + var state PrometheusState + if err := json.Unmarshal(data, &state); err != nil { + return 0, fmt.Errorf("failed to parse prometheus state file: %w", err) + } + return state.Port, nil +} + +func IsPrometheusInstalled() bool { + _, err := ReadPrometheusPort() + return err == nil +} + +func WriteTargetFile(namespace string, metricsPort int) error { + type target struct { + Targets []string `json:"targets"` + Labels map[string]string `json:"labels"` + } + targets := []target{ + { + Targets: []string{fmt.Sprintf("localhost:%d", metricsPort)}, + Labels: map[string]string{"namespace": namespace}, + }, + } + data, err := json.MarshalIndent(targets, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal target file: %w", err) + } + targetsDir := api.GetPrometheusTargetsDir() + if err := os.MkdirAll(targetsDir, 0755); err != nil { + return fmt.Errorf("failed to create targets directory: %w", err) + } + targetFile := filepath.Join(targetsDir, namespace+".json") + if err := os.WriteFile(targetFile, data, 0644); err != nil { + return fmt.Errorf("failed to write target file %s: %w", targetFile, err) + } + return nil +} + +func RemoveTargetFile(namespace string) error { + targetFile := filepath.Join(api.GetPrometheusTargetsDir(), namespace+".json") + if err := os.Remove(targetFile); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove target file %s: %w", targetFile, err) + } + return nil +} + +func claimedMetricsPorts() map[int]bool { + claimed := map[int]bool{} + targetsDir := api.GetPrometheusTargetsDir() + entries, err := os.ReadDir(targetsDir) + if err != nil { + return claimed + } + type targetEntry struct { + Targets []string `json:"targets"` + } + for _, e := range entries { + if e.IsDir() || filepath.Ext(e.Name()) != ".json" { + continue + } + data, err := os.ReadFile(filepath.Join(targetsDir, e.Name())) + if err != nil { + continue + } + var targets []targetEntry + if err := json.Unmarshal(data, &targets); err != nil { + continue + } + for _, t := range targets { + for _, addr := range t.Targets { + if _, portStr, err := net.SplitHostPort(addr); err == nil { + if p, err := strconv.Atoi(portStr); err == nil { + claimed[p] = true + } + } + } + } + } + return claimed +} + +func installedNetworkObservers() []string { + targetsDir := api.GetPrometheusTargetsDir() + entries, err := os.ReadDir(targetsDir) + if err != nil { + return nil + } + var namespaces []string + for _, e := range entries { + if !e.IsDir() && filepath.Ext(e.Name()) == ".json" { + namespaces = append(namespaces, e.Name()[:len(e.Name())-len(".json")]) + } + } + return namespaces +} + +func joinStrings(ss []string) string { + result := "" + for i, s := range ss { + if i > 0 { + result += ", " + } + result += s + } + return result +} + +func NextFreeMetricsPort(start int) (int, error) { + claimed := claimedMetricsPorts() + for port := start; port <= 65535; port++ { + if claimed[port] { + continue + } + if !utils.TcpPortInUse("", port) { + return port, nil + } + } + return 0, fmt.Errorf("no available metrics port found") +} diff --git a/internal/nonkube/network-observer/prometheus_state_test.go b/internal/nonkube/network-observer/prometheus_state_test.go new file mode 100644 index 000000000..f0a9b4d9b --- /dev/null +++ b/internal/nonkube/network-observer/prometheus_state_test.go @@ -0,0 +1,152 @@ +package networkobserver + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/skupperproject/skupper/pkg/nonkube/api" + "gotest.tools/v3/assert" +) + +func setTempPrometheusHome(t *testing.T) { + t.Helper() + if os.Getuid() == 0 { + api.DefaultRootDataHome = t.TempDir() + } else { + t.Setenv("XDG_DATA_HOME", t.TempDir()) + } +} + +func TestWriteAndReadPrometheusState(t *testing.T) { + setTempPrometheusHome(t) + + if err := os.MkdirAll(api.GetHostPrometheusHome(), 0755); err != nil { + t.Fatalf("setup: %v", err) + } + + assert.NilError(t, WritePrometheusState(9090)) + + port, err := ReadPrometheusPort() + assert.NilError(t, err) + assert.Equal(t, 9090, port) +} + +func TestReadPrometheusPort_NotInstalled(t *testing.T) { + setTempPrometheusHome(t) + + _, err := ReadPrometheusPort() + assert.ErrorContains(t, err, "prometheus is not installed") +} + +func TestReadPrometheusPort_Corrupt(t *testing.T) { + setTempPrometheusHome(t) + + if err := os.MkdirAll(api.GetHostPrometheusHome(), 0755); err != nil { + t.Fatalf("setup: %v", err) + } + if err := os.WriteFile(prometheusStateFile(), []byte("not-json"), 0644); err != nil { + t.Fatalf("setup: %v", err) + } + + _, err := ReadPrometheusPort() + assert.ErrorContains(t, err, "failed to parse prometheus state file") +} + +func TestIsPrometheusInstalled(t *testing.T) { + setTempPrometheusHome(t) + + assert.Equal(t, false, IsPrometheusInstalled()) + + if err := os.MkdirAll(api.GetHostPrometheusHome(), 0755); err != nil { + t.Fatalf("setup: %v", err) + } + assert.NilError(t, WritePrometheusState(9091)) + assert.Equal(t, true, IsPrometheusInstalled()) +} + +func TestWriteAndRemoveTargetFile(t *testing.T) { + setTempPrometheusHome(t) + + assert.NilError(t, WriteTargetFile("west", 9001)) + + targetFile := filepath.Join(api.GetPrometheusTargetsDir(), "west.json") + data, err := os.ReadFile(targetFile) + assert.NilError(t, err) + + type entry struct { + Targets []string `json:"targets"` + Labels map[string]string `json:"labels"` + } + var entries []entry + assert.NilError(t, json.Unmarshal(data, &entries)) + assert.Equal(t, 1, len(entries)) + assert.Equal(t, "localhost:9001", entries[0].Targets[0]) + assert.Equal(t, "west", entries[0].Labels["namespace"]) + + assert.NilError(t, RemoveTargetFile("west")) + _, err = os.Stat(targetFile) + assert.Assert(t, os.IsNotExist(err)) + + assert.NilError(t, RemoveTargetFile("west")) +} + +func TestInstalledNetworkObservers(t *testing.T) { + setTempPrometheusHome(t) + + assert.Equal(t, 0, len(installedNetworkObservers())) + + assert.NilError(t, WriteTargetFile("west", 9001)) + assert.NilError(t, WriteTargetFile("east", 9002)) + + namespaces := installedNetworkObservers() + assert.Equal(t, 2, len(namespaces)) + + found := map[string]bool{} + for _, ns := range namespaces { + found[ns] = true + } + assert.Assert(t, found["west"]) + assert.Assert(t, found["east"]) +} + +func TestClaimedMetricsPorts(t *testing.T) { + setTempPrometheusHome(t) + + assert.Equal(t, 0, len(claimedMetricsPorts())) + + assert.NilError(t, WriteTargetFile("west", 9001)) + assert.NilError(t, WriteTargetFile("east", 9002)) + + claimed := claimedMetricsPorts() + assert.Assert(t, claimed[9001]) + assert.Assert(t, claimed[9002]) + assert.Assert(t, !claimed[9003]) +} + +func TestNextFreeMetricsPort(t *testing.T) { + setTempPrometheusHome(t) + + assert.NilError(t, WriteTargetFile("ns0", 9000)) + assert.NilError(t, WriteTargetFile("ns1", 9001)) + + port, err := NextFreeMetricsPort(9000) + assert.NilError(t, err) + assert.Assert(t, port >= 9002, "expected port >= 9002, got %d", port) +} + +func TestJoinStrings(t *testing.T) { + tests := []struct { + input []string + expected string + }{ + {nil, ""}, + {[]string{"a"}, "a"}, + {[]string{"a", "b", "c"}, "a, b, c"}, + } + for _, tc := range tests { + got := joinStrings(tc.input) + assert.Equal(t, tc.expected, got) + } +} diff --git a/internal/nonkube/network-observer/systemd.go b/internal/nonkube/network-observer/systemd.go index 4abfe2cfe..c7ae66824 100644 --- a/internal/nonkube/network-observer/systemd.go +++ b/internal/nonkube/network-observer/systemd.go @@ -7,41 +7,25 @@ import ( "path/filepath" ) -const SystemdServiceTemplate = `[Unit] -Description=Skupper Network Observer - %s -After=network.target - -[Service] -Type=oneshot -RemainAfterExit=yes -ExecStart=/bin/true -ExecStop=/bin/true - -[Install] -WantedBy=default.target -` - -const SystemdPrometheusServiceTemplate = `[Unit] -Description=Skupper Network Observer Prometheus - %s +const SystemdPrometheusHostServiceTemplate = `[Unit] +Description=Skupper Prometheus (host-level) After=network.target -PartOf=skupper-network-observer-%s.service [Service] Type=simple Restart=always RestartSec=5 -ExecStart=%s start --attach %s-skupper-prometheus -ExecStop=%s stop %s-skupper-prometheus +ExecStart=%s start --attach skupper-prometheus +ExecStop=%s stop skupper-prometheus [Install] -WantedBy=skupper-network-observer-%s.service +WantedBy=default.target ` const SystemdNetworkObserverServiceTemplate = `[Unit] -Description=Skupper Network Observer Application - %s -After=network.target skupper-controller.service skupper-network-observer-prometheus-%s.service -Wants=skupper-controller.service -PartOf=skupper-network-observer-%s.service +Description=Skupper Network Observer - %s +After=network.target skupper-controller.service skupper-prometheus.service +Wants=skupper-controller.service skupper-prometheus.service [Service] Type=simple @@ -51,7 +35,7 @@ ExecStart=%s start --attach %s-skupper-network-observer ExecStop=%s stop %s-skupper-network-observer [Install] -WantedBy=skupper-network-observer-%s.service +WantedBy=default.target ` type SystemdServiceManager struct { @@ -80,67 +64,90 @@ func getSystemdServiceDir() string { return filepath.Join(home, ".config", "systemd", "user") } -func (s *SystemdServiceManager) CreateServices() error { +func (s *SystemdServiceManager) CreateNetworkObserverService() error { if err := os.MkdirAll(s.ServiceDir, 0755); err != nil { return fmt.Errorf("failed to create systemd service directory: %w", err) } - mainServiceName := fmt.Sprintf("skupper-network-observer-%s.service", s.Namespace) - mainServicePath := filepath.Join(s.ServiceDir, mainServiceName) - mainServiceContent := fmt.Sprintf(SystemdServiceTemplate, s.Namespace) - if err := os.WriteFile(mainServicePath, []byte(mainServiceContent), 0644); err != nil { - return fmt.Errorf("failed to write main service file: %w", err) + svcName := fmt.Sprintf("skupper-network-observer-%s.service", s.Namespace) + svcPath := filepath.Join(s.ServiceDir, svcName) + svcContent := fmt.Sprintf(SystemdNetworkObserverServiceTemplate, + s.Namespace, + s.ContainerEngine, s.Namespace, + s.ContainerEngine, s.Namespace) + if err := os.WriteFile(svcPath, []byte(svcContent), 0644); err != nil { + return fmt.Errorf("failed to write network observer service file: %w", err) } - prometheusServiceName := fmt.Sprintf("skupper-network-observer-prometheus-%s.service", s.Namespace) - prometheusServicePath := filepath.Join(s.ServiceDir, prometheusServiceName) - prometheusServiceContent := fmt.Sprintf(SystemdPrometheusServiceTemplate, - s.Namespace, s.Namespace, - s.ContainerEngine, s.Namespace, - s.ContainerEngine, s.Namespace, - s.Namespace) - if err := os.WriteFile(prometheusServicePath, []byte(prometheusServiceContent), 0644); err != nil { - return fmt.Errorf("failed to write prometheus service file: %w", err) + if err := s.enableService(svcName); err != nil { + return fmt.Errorf("failed to enable service %s: %w", svcName, err) } - appServiceName := fmt.Sprintf("skupper-network-observer-app-%s.service", s.Namespace) - appServicePath := filepath.Join(s.ServiceDir, appServiceName) - appServiceContent := fmt.Sprintf(SystemdNetworkObserverServiceTemplate, - s.Namespace, s.Namespace, s.Namespace, - s.ContainerEngine, s.Namespace, - s.ContainerEngine, s.Namespace, - s.Namespace) - if err := os.WriteFile(appServicePath, []byte(appServiceContent), 0644); err != nil { - return fmt.Errorf("failed to write network observer service file: %w", err) + if err := s.startService(svcName); err != nil { + return fmt.Errorf("failed to start service %s: %w", svcName, err) } - for _, svc := range []string{ - prometheusServiceName, - appServiceName, - mainServiceName, - } { - if err := s.enableService(svc); err != nil { - return fmt.Errorf("failed to enable service %s: %w", svc, err) - } + return nil +} + +func (s *SystemdServiceManager) RemoveNetworkObserverService() error { + svcName := fmt.Sprintf("skupper-network-observer-%s.service", s.Namespace) + + if err := s.stopAndDisableService(svcName); err != nil { + fmt.Printf("Warning: failed to stop/disable service %s: %v\n", svcName, err) } - for _, svc := range []string{ - prometheusServiceName, - appServiceName, - } { - if err := s.startService(svc); err != nil { - return fmt.Errorf("failed to start service %s: %w", svc, err) - } + svcPath := filepath.Join(s.ServiceDir, svcName) + if err := os.Remove(svcPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove service file %s: %w", svcName, err) } - if err := s.startService(mainServiceName); err != nil { - return fmt.Errorf("failed to start service: %w", err) + if err := s.reloadSystemd(); err != nil { + return fmt.Errorf("failed to reload systemd: %w", err) } return nil } +func (s *SystemdServiceManager) CreatePrometheusService() error { + if err := os.MkdirAll(s.ServiceDir, 0755); err != nil { + return fmt.Errorf("failed to create systemd service directory: %w", err) + } + + svcName := "skupper-prometheus.service" + svcPath := filepath.Join(s.ServiceDir, svcName) + svcContent := fmt.Sprintf(SystemdPrometheusHostServiceTemplate, + s.ContainerEngine, + s.ContainerEngine) + if err := os.WriteFile(svcPath, []byte(svcContent), 0644); err != nil { + return fmt.Errorf("failed to write prometheus service file: %w", err) + } + + if err := s.enableServiceByPath(svcPath); err != nil { + return fmt.Errorf("failed to enable prometheus service: %w", err) + } + if err := s.startService(svcName); err != nil { + return fmt.Errorf("failed to start prometheus service: %w", err) + } + return nil +} + +func (s *SystemdServiceManager) RemovePrometheusService() error { + svcName := "skupper-prometheus.service" + if err := s.stopAndDisableService(svcName); err != nil { + fmt.Printf("Warning: failed to stop prometheus service: %v\n", err) + } + svcPath := filepath.Join(s.ServiceDir, svcName) + if err := os.Remove(svcPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove prometheus service file: %w", err) + } + if err := s.reloadSystemd(); err != nil { + return fmt.Errorf("failed to reload systemd: %w", err) + } + return nil +} + func (s *SystemdServiceManager) reloadSystemd() error { var cmd *exec.Cmd if os.Getuid() == 0 { @@ -164,43 +171,29 @@ func (s *SystemdServiceManager) enableService(serviceName string) error { return nil } -func (s *SystemdServiceManager) startService(serviceName string) error { +func (s *SystemdServiceManager) enableServiceByPath(servicePath string) error { var cmd *exec.Cmd if os.Getuid() == 0 { - cmd = exec.Command("systemctl", "start", serviceName) + cmd = exec.Command("systemctl", "enable", servicePath) } else { - cmd = exec.Command("systemctl", "--user", "start", serviceName) + cmd = exec.Command("systemctl", "--user", "enable", servicePath) } if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to start %s: %w", serviceName, err) + return fmt.Errorf("failed to enable %s: %w", servicePath, err) } return nil } -func (s *SystemdServiceManager) RemoveServices() error { - mainServiceName := fmt.Sprintf("skupper-network-observer-%s.service", s.Namespace) - - if err := s.stopAndDisableService(mainServiceName); err != nil { - fmt.Printf("Warning: failed to stop service: %v\n", err) - } - - serviceNames := []string{ - mainServiceName, - fmt.Sprintf("skupper-network-observer-prometheus-%s.service", s.Namespace), - fmt.Sprintf("skupper-network-observer-app-%s.service", s.Namespace), - } - - for _, serviceName := range serviceNames { - servicePath := filepath.Join(s.ServiceDir, serviceName) - if err := os.Remove(servicePath); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to remove service file %s: %w", serviceName, err) - } +func (s *SystemdServiceManager) startService(serviceName string) error { + var cmd *exec.Cmd + if os.Getuid() == 0 { + cmd = exec.Command("systemctl", "start", serviceName) + } else { + cmd = exec.Command("systemctl", "--user", "start", serviceName) } - - if err := s.reloadSystemd(); err != nil { - return fmt.Errorf("failed to reload systemd: %w", err) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to start %s: %w", serviceName, err) } - return nil } diff --git a/pkg/nonkube/api/environment.go b/pkg/nonkube/api/environment.go index 7f23aed2a..0124c2eda 100644 --- a/pkg/nonkube/api/environment.go +++ b/pkg/nonkube/api/environment.go @@ -195,3 +195,11 @@ func GetSystemControllerPath() string { dataHome := GetHostDataHome() return path.Join(dataHome, "system-controller") } + +func GetHostPrometheusHome() string { + return getHostPath("prometheus") +} + +func GetPrometheusTargetsDir() string { + return path.Join(GetHostPrometheusHome(), "targets") +} From d89c8c1fe65483d6c04b432d7cf2460f3e2b8a8a Mon Sep 17 00:00:00 2001 From: Noe Luaces Date: Mon, 31 Aug 2026 18:59:43 +0200 Subject: [PATCH 18/18] codespell fix --- internal/kube/certificates/mgr_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/kube/certificates/mgr_test.go b/internal/kube/certificates/mgr_test.go index 6eb8c8027..81d4d4656 100644 --- a/internal/kube/certificates/mgr_test.go +++ b/internal/kube/certificates/mgr_test.go @@ -411,7 +411,7 @@ func secretWithOwnerRef(secret *corev1.Secret, ref metav1.OwnerReference) *corev return secret } -// managedWithOwnerHosts sets up a Certificiate with skupper controlled and owner hosts annotations +// managedWithOwnerHosts sets up a Certificate with skupper controlled and owner hosts annotations func managedWithOwnerHosts(t *testing.T, cert *skupperv2alpha1.Certificate, ref metav1.OwnerReference, hosts ...string) *skupperv2alpha1.Certificate { t.Helper() cert.ObjectMeta.OwnerReferences = append(cert.ObjectMeta.OwnerReferences, ref)