mirror of
https://github.com/netbirdio/netbird.git
synced 2025-08-10 23:55:47 +02:00
[client] Add env and status flags for netbird service command (#3975)
This commit is contained in:
10
.github/workflows/golang-test-linux.yml
vendored
10
.github/workflows/golang-test-linux.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
outputs:
|
outputs:
|
||||||
management: ${{ steps.filter.outputs.management }}
|
management: ${{ steps.filter.outputs.management }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
@ -24,8 +24,8 @@ jobs:
|
|||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
management:
|
management:
|
||||||
- 'management/**'
|
- 'management/**'
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
@ -148,7 +148,7 @@ jobs:
|
|||||||
|
|
||||||
test_client_on_docker:
|
test_client_on_docker:
|
||||||
name: "Client (Docker) / Unit"
|
name: "Client (Docker) / Unit"
|
||||||
needs: [build-cache]
|
needs: [ build-cache ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
@ -181,6 +181,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
HOST_GOCACHE: ${{ steps.go-env.outputs.cache_dir }}
|
HOST_GOCACHE: ${{ steps.go-env.outputs.cache_dir }}
|
||||||
HOST_GOMODCACHE: ${{ steps.go-env.outputs.modcache_dir }}
|
HOST_GOMODCACHE: ${{ steps.go-env.outputs.modcache_dir }}
|
||||||
|
CONTAINER: "true"
|
||||||
run: |
|
run: |
|
||||||
CONTAINER_GOCACHE="/root/.cache/go-build"
|
CONTAINER_GOCACHE="/root/.cache/go-build"
|
||||||
CONTAINER_GOMODCACHE="/go/pkg/mod"
|
CONTAINER_GOMODCACHE="/go/pkg/mod"
|
||||||
@ -198,6 +199,7 @@ jobs:
|
|||||||
-e GOARCH=${GOARCH_TARGET} \
|
-e GOARCH=${GOARCH_TARGET} \
|
||||||
-e GOCACHE=${CONTAINER_GOCACHE} \
|
-e GOCACHE=${CONTAINER_GOCACHE} \
|
||||||
-e GOMODCACHE=${CONTAINER_GOMODCACHE} \
|
-e GOMODCACHE=${CONTAINER_GOMODCACHE} \
|
||||||
|
-e CONTAINER=${CONTAINER} \
|
||||||
golang:1.23-alpine \
|
golang:1.23-alpine \
|
||||||
sh -c ' \
|
sh -c ' \
|
||||||
apk update; apk add --no-cache \
|
apk update; apk add --no-cache \
|
||||||
|
@ -67,7 +67,6 @@ var (
|
|||||||
interfaceName string
|
interfaceName string
|
||||||
wireguardPort uint16
|
wireguardPort uint16
|
||||||
networkMonitor bool
|
networkMonitor bool
|
||||||
serviceName string
|
|
||||||
autoConnectDisabled bool
|
autoConnectDisabled bool
|
||||||
extraIFaceBlackList []string
|
extraIFaceBlackList []string
|
||||||
anonymizeFlag bool
|
anonymizeFlag bool
|
||||||
@ -116,15 +115,9 @@ func init() {
|
|||||||
defaultDaemonAddr = "tcp://127.0.0.1:41731"
|
defaultDaemonAddr = "tcp://127.0.0.1:41731"
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultServiceName := "netbird"
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
defaultServiceName = "Netbird"
|
|
||||||
}
|
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
|
rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
|
||||||
rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultManagementURL))
|
rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultManagementURL))
|
||||||
rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL))
|
rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL))
|
||||||
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location")
|
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location")
|
||||||
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level")
|
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level")
|
||||||
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout. If syslog is specified the log will be sent to syslog daemon.")
|
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout. If syslog is specified the log will be sent to syslog daemon.")
|
||||||
@ -135,7 +128,6 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device")
|
rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&anonymizeFlag, "anonymize", "A", false, "anonymize IP addresses and non-netbird.io domains in logs and status output")
|
rootCmd.PersistentFlags().BoolVarP(&anonymizeFlag, "anonymize", "A", false, "anonymize IP addresses and non-netbird.io domains in logs and status output")
|
||||||
|
|
||||||
rootCmd.AddCommand(serviceCmd)
|
|
||||||
rootCmd.AddCommand(upCmd)
|
rootCmd.AddCommand(upCmd)
|
||||||
rootCmd.AddCommand(downCmd)
|
rootCmd.AddCommand(downCmd)
|
||||||
rootCmd.AddCommand(statusCmd)
|
rootCmd.AddCommand(statusCmd)
|
||||||
@ -146,9 +138,6 @@ func init() {
|
|||||||
rootCmd.AddCommand(forwardingRulesCmd)
|
rootCmd.AddCommand(forwardingRulesCmd)
|
||||||
rootCmd.AddCommand(debugCmd)
|
rootCmd.AddCommand(debugCmd)
|
||||||
|
|
||||||
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
|
|
||||||
serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service
|
|
||||||
|
|
||||||
networksCMD.AddCommand(routesListCmd)
|
networksCMD.AddCommand(routesListCmd)
|
||||||
networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd)
|
networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd)
|
||||||
|
|
||||||
@ -186,14 +175,13 @@ func SetupCloseHandler(ctx context.Context, cancel context.CancelFunc) {
|
|||||||
termCh := make(chan os.Signal, 1)
|
termCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(termCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(termCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||||
go func() {
|
go func() {
|
||||||
done := ctx.Done()
|
defer cancel()
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-ctx.Done():
|
||||||
case <-termCh:
|
case <-termCh:
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("shutdown signal received")
|
log.Info("shutdown signal received")
|
||||||
cancel()
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
|
//go:build !ios && !android
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
@ -14,6 +17,16 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/server"
|
"github.com/netbirdio/netbird/client/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var serviceCmd = &cobra.Command{
|
||||||
|
Use: "service",
|
||||||
|
Short: "manages Netbird service",
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
serviceName string
|
||||||
|
serviceEnvVars []string
|
||||||
|
)
|
||||||
|
|
||||||
type program struct {
|
type program struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@ -22,12 +35,31 @@ type program struct {
|
|||||||
serverInstanceMu sync.Mutex
|
serverInstanceMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
defaultServiceName := "netbird"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
defaultServiceName = "Netbird"
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd)
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
|
||||||
|
serviceEnvDesc := `Sets extra environment variables for the service. ` +
|
||||||
|
`You can specify a comma-separated list of KEY=VALUE pairs. ` +
|
||||||
|
`E.g. --service-env LOG_LEVEL=debug,CUSTOM_VAR=value`
|
||||||
|
|
||||||
|
installCmd.Flags().StringSliceVar(&serviceEnvVars, "service-env", nil, serviceEnvDesc)
|
||||||
|
reconfigureCmd.Flags().StringSliceVar(&serviceEnvVars, "service-env", nil, serviceEnvDesc)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(serviceCmd)
|
||||||
|
}
|
||||||
|
|
||||||
func newProgram(ctx context.Context, cancel context.CancelFunc) *program {
|
func newProgram(ctx context.Context, cancel context.CancelFunc) *program {
|
||||||
ctx = internal.CtxInitState(ctx)
|
ctx = internal.CtxInitState(ctx)
|
||||||
return &program{ctx: ctx, cancel: cancel}
|
return &program{ctx: ctx, cancel: cancel}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSVCConfig() *service.Config {
|
func newSVCConfig() (*service.Config, error) {
|
||||||
config := &service.Config{
|
config := &service.Config{
|
||||||
Name: serviceName,
|
Name: serviceName,
|
||||||
DisplayName: "Netbird",
|
DisplayName: "Netbird",
|
||||||
@ -36,23 +68,47 @@ func newSVCConfig() *service.Config {
|
|||||||
EnvVars: make(map[string]string),
|
EnvVars: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(serviceEnvVars) > 0 {
|
||||||
|
extraEnvs, err := parseServiceEnvVars(serviceEnvVars)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse service environment variables: %w", err)
|
||||||
|
}
|
||||||
|
config.EnvVars = extraEnvs
|
||||||
|
}
|
||||||
|
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" {
|
||||||
config.EnvVars["SYSTEMD_UNIT"] = serviceName
|
config.EnvVars["SYSTEMD_UNIT"] = serviceName
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSVC(prg *program, conf *service.Config) (service.Service, error) {
|
func newSVC(prg *program, conf *service.Config) (service.Service, error) {
|
||||||
s, err := service.New(prg, conf)
|
return service.New(prg, conf)
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var serviceCmd = &cobra.Command{
|
func parseServiceEnvVars(envVars []string) (map[string]string, error) {
|
||||||
Use: "service",
|
envMap := make(map[string]string)
|
||||||
Short: "manages Netbird service",
|
|
||||||
|
for _, env := range envVars {
|
||||||
|
if env == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(env, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, fmt.Errorf("invalid environment variable format: %s (expected KEY=VALUE)", env)
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
if key == "" {
|
||||||
|
return nil, fmt.Errorf("empty environment variable key in: %s", env)
|
||||||
|
}
|
||||||
|
|
||||||
|
envMap[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return envMap, nil
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//go:build !ios && !android
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -47,14 +49,13 @@ func (p *program) Start(svc service.Service) error {
|
|||||||
|
|
||||||
listen, err := net.Listen(split[0], split[1])
|
listen, err := net.Listen(split[0], split[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to listen daemon interface: %w", err)
|
return fmt.Errorf("listen daemon interface: %w", err)
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
defer listen.Close()
|
defer listen.Close()
|
||||||
|
|
||||||
if split[0] == "unix" {
|
if split[0] == "unix" {
|
||||||
err = os.Chmod(split[1], 0666)
|
if err := os.Chmod(split[1], 0666); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed setting daemon permissions: %v", split[1])
|
log.Errorf("failed setting daemon permissions: %v", split[1])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -100,37 +101,49 @@ func (p *program) Stop(srv service.Service) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Common setup for service control commands
|
||||||
|
func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel context.CancelFunc) (service.Service, error) {
|
||||||
|
SetFlagsFromEnvVars(rootCmd)
|
||||||
|
SetFlagsFromEnvVars(serviceCmd)
|
||||||
|
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
if err := handleRebrand(cmd); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := util.InitLog(logLevel, logFile); err != nil {
|
||||||
|
return nil, fmt.Errorf("init log: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := newSVCConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create service config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctx, cancel), cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
var runCmd = &cobra.Command{
|
var runCmd = &cobra.Command{
|
||||||
Use: "run",
|
Use: "run",
|
||||||
Short: "runs Netbird as service",
|
Short: "runs Netbird as service",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
SetFlagsFromEnvVars(rootCmd)
|
|
||||||
|
|
||||||
cmd.SetOut(cmd.OutOrStdout())
|
|
||||||
|
|
||||||
err := handleRebrand(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = util.InitLog(logLevel, logFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed initializing log %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
|
||||||
SetupCloseHandler(ctx, cancel)
|
SetupCloseHandler(ctx, cancel)
|
||||||
SetupDebugHandler(ctx, nil, nil, nil, logFile)
|
SetupDebugHandler(ctx, nil, nil, nil, logFile)
|
||||||
|
|
||||||
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
|
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = s.Run()
|
|
||||||
if err != nil {
|
return s.Run()
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,31 +151,14 @@ var startCmd = &cobra.Command{
|
|||||||
Use: "start",
|
Use: "start",
|
||||||
Short: "starts Netbird service",
|
Short: "starts Netbird service",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
SetFlagsFromEnvVars(rootCmd)
|
|
||||||
|
|
||||||
cmd.SetOut(cmd.OutOrStdout())
|
|
||||||
|
|
||||||
err := handleRebrand(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = util.InitLog(logLevel, logFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
||||||
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cmd.PrintErrln(err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = s.Start()
|
|
||||||
if err != nil {
|
if err := s.Start(); err != nil {
|
||||||
cmd.PrintErrln(err)
|
return fmt.Errorf("start service: %w", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
cmd.Println("Netbird service has been started")
|
cmd.Println("Netbird service has been started")
|
||||||
return nil
|
return nil
|
||||||
@ -173,29 +169,14 @@ var stopCmd = &cobra.Command{
|
|||||||
Use: "stop",
|
Use: "stop",
|
||||||
Short: "stops Netbird service",
|
Short: "stops Netbird service",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
SetFlagsFromEnvVars(rootCmd)
|
|
||||||
|
|
||||||
cmd.SetOut(cmd.OutOrStdout())
|
|
||||||
|
|
||||||
err := handleRebrand(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = util.InitLog(logLevel, logFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed initializing log %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
||||||
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = s.Stop()
|
|
||||||
if err != nil {
|
if err := s.Stop(); err != nil {
|
||||||
return err
|
return fmt.Errorf("stop service: %w", err)
|
||||||
}
|
}
|
||||||
cmd.Println("Netbird service has been stopped")
|
cmd.Println("Netbird service has been stopped")
|
||||||
return nil
|
return nil
|
||||||
@ -206,31 +187,48 @@ var restartCmd = &cobra.Command{
|
|||||||
Use: "restart",
|
Use: "restart",
|
||||||
Short: "restarts Netbird service",
|
Short: "restarts Netbird service",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
SetFlagsFromEnvVars(rootCmd)
|
|
||||||
|
|
||||||
cmd.SetOut(cmd.OutOrStdout())
|
|
||||||
|
|
||||||
err := handleRebrand(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = util.InitLog(logLevel, logFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed initializing log %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
||||||
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = s.Restart()
|
|
||||||
if err != nil {
|
if err := s.Restart(); err != nil {
|
||||||
return err
|
return fmt.Errorf("restart service: %w", err)
|
||||||
}
|
}
|
||||||
cmd.Println("Netbird service has been restarted")
|
cmd.Println("Netbird service has been restarted")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var svcStatusCmd = &cobra.Command{
|
||||||
|
Use: "status",
|
||||||
|
Short: "shows Netbird service status",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := s.Status()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get service status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusText string
|
||||||
|
switch status {
|
||||||
|
case service.StatusRunning:
|
||||||
|
statusText = "Running"
|
||||||
|
case service.StatusStopped:
|
||||||
|
statusText = "Stopped"
|
||||||
|
case service.StatusUnknown:
|
||||||
|
statusText = "Unknown"
|
||||||
|
default:
|
||||||
|
statusText = fmt.Sprintf("Unknown (%d)", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Printf("Netbird service status: %s\n", statusText)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -1,87 +1,121 @@
|
|||||||
|
//go:build !ios && !android
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrGetServiceStatus = fmt.Errorf("failed to get service status")
|
||||||
|
|
||||||
|
// Common service command setup
|
||||||
|
func setupServiceCommand(cmd *cobra.Command) error {
|
||||||
|
SetFlagsFromEnvVars(rootCmd)
|
||||||
|
SetFlagsFromEnvVars(serviceCmd)
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
return handleRebrand(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build service arguments for install/reconfigure
|
||||||
|
func buildServiceArguments() []string {
|
||||||
|
args := []string{
|
||||||
|
"service",
|
||||||
|
"run",
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--log-level",
|
||||||
|
logLevel,
|
||||||
|
"--daemon-addr",
|
||||||
|
daemonAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
if managementURL != "" {
|
||||||
|
args = append(args, "--management-url", managementURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if logFile != "" {
|
||||||
|
args = append(args, "--log-file", logFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure platform-specific service settings
|
||||||
|
func configurePlatformSpecificSettings(svcConfig *service.Config) error {
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
// Respected only by systemd systems
|
||||||
|
svcConfig.Dependencies = []string{"After=network.target syslog.target"}
|
||||||
|
|
||||||
|
if logFile != "console" {
|
||||||
|
setStdLogPath := true
|
||||||
|
dir := filepath.Dir(logFile)
|
||||||
|
|
||||||
|
if _, err := os.Stat(dir); err != nil {
|
||||||
|
if err = os.MkdirAll(dir, 0750); err != nil {
|
||||||
|
setStdLogPath = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if setStdLogPath {
|
||||||
|
svcConfig.Option["LogOutput"] = true
|
||||||
|
svcConfig.Option["LogDirectory"] = dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
svcConfig.Option["OnFailure"] = "restart"
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create fully configured service config for install/reconfigure
|
||||||
|
func createServiceConfigForInstall() (*service.Config, error) {
|
||||||
|
svcConfig, err := newSVCConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create service config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
svcConfig.Arguments = buildServiceArguments()
|
||||||
|
if err = configurePlatformSpecificSettings(svcConfig); err != nil {
|
||||||
|
return nil, fmt.Errorf("configure platform-specific settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return svcConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
var installCmd = &cobra.Command{
|
var installCmd = &cobra.Command{
|
||||||
Use: "install",
|
Use: "install",
|
||||||
Short: "installs Netbird service",
|
Short: "installs Netbird service",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
SetFlagsFromEnvVars(rootCmd)
|
if err := setupServiceCommand(cmd); err != nil {
|
||||||
|
|
||||||
cmd.SetOut(cmd.OutOrStdout())
|
|
||||||
|
|
||||||
err := handleRebrand(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
svcConfig := newSVCConfig()
|
svcConfig, err := createServiceConfigForInstall()
|
||||||
|
if err != nil {
|
||||||
svcConfig.Arguments = []string{
|
return err
|
||||||
"service",
|
|
||||||
"run",
|
|
||||||
"--config",
|
|
||||||
configPath,
|
|
||||||
"--log-level",
|
|
||||||
logLevel,
|
|
||||||
"--daemon-addr",
|
|
||||||
daemonAddr,
|
|
||||||
}
|
|
||||||
|
|
||||||
if managementURL != "" {
|
|
||||||
svcConfig.Arguments = append(svcConfig.Arguments, "--management-url", managementURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
if logFile != "" {
|
|
||||||
svcConfig.Arguments = append(svcConfig.Arguments, "--log-file", logFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
if runtime.GOOS == "linux" {
|
|
||||||
// Respected only by systemd systems
|
|
||||||
svcConfig.Dependencies = []string{"After=network.target syslog.target"}
|
|
||||||
|
|
||||||
if logFile != "console" {
|
|
||||||
setStdLogPath := true
|
|
||||||
dir := filepath.Dir(logFile)
|
|
||||||
|
|
||||||
_, err := os.Stat(dir)
|
|
||||||
if err != nil {
|
|
||||||
err = os.MkdirAll(dir, 0750)
|
|
||||||
if err != nil {
|
|
||||||
setStdLogPath = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if setStdLogPath {
|
|
||||||
svcConfig.Option["LogOutput"] = true
|
|
||||||
svcConfig.Option["LogDirectory"] = dir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
svcConfig.Option["OnFailure"] = "restart"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
s, err := newSVC(newProgram(ctx, cancel), svcConfig)
|
s, err := newSVC(newProgram(ctx, cancel), svcConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cmd.PrintErrln(err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.Install()
|
if err := s.Install(); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("install service: %w", err)
|
||||||
cmd.PrintErrln(err)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Println("Netbird service has been installed")
|
cmd.Println("Netbird service has been installed")
|
||||||
@ -93,27 +127,109 @@ var uninstallCmd = &cobra.Command{
|
|||||||
Use: "uninstall",
|
Use: "uninstall",
|
||||||
Short: "uninstalls Netbird service from system",
|
Short: "uninstalls Netbird service from system",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
SetFlagsFromEnvVars(rootCmd)
|
if err := setupServiceCommand(cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
cmd.SetOut(cmd.OutOrStdout())
|
cfg, err := newSVCConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create service config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
err := handleRebrand(cmd)
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctx, cancel), cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Uninstall(); err != nil {
|
||||||
|
return fmt.Errorf("uninstall service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Netbird service has been uninstalled")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var reconfigureCmd = &cobra.Command{
|
||||||
|
Use: "reconfigure",
|
||||||
|
Short: "reconfigures Netbird service with new settings",
|
||||||
|
Long: `Reconfigures the Netbird service with new settings without manual uninstall/install.
|
||||||
|
This command will temporarily stop the service, update its configuration, and restart it if it was running.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := setupServiceCommand(cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wasRunning, err := isServiceRunning()
|
||||||
|
if err != nil && !errors.Is(err, ErrGetServiceStatus) {
|
||||||
|
return fmt.Errorf("check service status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
svcConfig, err := createServiceConfigForInstall()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
|
s, err := newSVC(newProgram(ctx, cancel), svcConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("create service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.Uninstall()
|
if wasRunning {
|
||||||
if err != nil {
|
cmd.Println("Stopping Netbird service...")
|
||||||
return err
|
if err := s.Stop(); err != nil {
|
||||||
|
cmd.Printf("Warning: failed to stop service: %v\n", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cmd.Println("Netbird service has been uninstalled")
|
|
||||||
|
cmd.Println("Removing existing service configuration...")
|
||||||
|
if err := s.Uninstall(); err != nil {
|
||||||
|
return fmt.Errorf("uninstall existing service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Installing service with new configuration...")
|
||||||
|
if err := s.Install(); err != nil {
|
||||||
|
return fmt.Errorf("install service with new config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wasRunning {
|
||||||
|
cmd.Println("Starting Netbird service...")
|
||||||
|
if err := s.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start service after reconfigure: %w", err)
|
||||||
|
}
|
||||||
|
cmd.Println("Netbird service has been reconfigured and started")
|
||||||
|
} else {
|
||||||
|
cmd.Println("Netbird service has been reconfigured")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isServiceRunning() (bool, error) {
|
||||||
|
cfg, err := newSVCConfig()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctx, cancel), cfg)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := s.Status()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("%w: %w", ErrGetServiceStatus, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return status == service.StatusRunning, nil
|
||||||
|
}
|
||||||
|
263
client/cmd/service_test.go
Normal file
263
client/cmd/service_test.go
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
serviceStartTimeout = 10 * time.Second
|
||||||
|
serviceStopTimeout = 5 * time.Second
|
||||||
|
statusPollInterval = 500 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
// waitForServiceStatus waits for service to reach expected status with timeout
|
||||||
|
func waitForServiceStatus(expectedStatus service.Status, timeout time.Duration) (bool, error) {
|
||||||
|
cfg, err := newSVCConfig()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxSvc, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, timeoutCancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer timeoutCancel()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(statusPollInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return false, fmt.Errorf("timeout waiting for service status %v", expectedStatus)
|
||||||
|
case <-ticker.C:
|
||||||
|
status, err := s.Status()
|
||||||
|
if err != nil {
|
||||||
|
// Continue polling on transient errors
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if status == expectedStatus {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServiceLifecycle tests the complete service lifecycle
|
||||||
|
func TestServiceLifecycle(t *testing.T) {
|
||||||
|
// TODO: Add support for Windows and macOS
|
||||||
|
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
|
||||||
|
t.Skipf("Skipping service lifecycle test on unsupported OS: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("CONTAINER") == "true" {
|
||||||
|
t.Skip("Skipping service lifecycle test in container environment")
|
||||||
|
}
|
||||||
|
|
||||||
|
originalServiceName := serviceName
|
||||||
|
serviceName = "netbirdtest" + fmt.Sprintf("%d", time.Now().Unix())
|
||||||
|
defer func() {
|
||||||
|
serviceName = originalServiceName
|
||||||
|
}()
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
configPath = fmt.Sprintf("%s/netbird-test-config.json", tempDir)
|
||||||
|
logLevel = "info"
|
||||||
|
daemonAddr = fmt.Sprintf("unix://%s/netbird-test.sock", tempDir)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("Install", func(t *testing.T) {
|
||||||
|
installCmd.SetContext(ctx)
|
||||||
|
err := installCmd.RunE(installCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cfg, err := newSVCConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctxSvc, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
status, err := s.Status()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEqual(t, service.StatusUnknown, status)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Start", func(t *testing.T) {
|
||||||
|
startCmd.SetContext(ctx)
|
||||||
|
err := startCmd.RunE(startCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, running)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Restart", func(t *testing.T) {
|
||||||
|
restartCmd.SetContext(ctx)
|
||||||
|
err := restartCmd.RunE(restartCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, running)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reconfigure", func(t *testing.T) {
|
||||||
|
originalLogLevel := logLevel
|
||||||
|
logLevel = "debug"
|
||||||
|
defer func() {
|
||||||
|
logLevel = originalLogLevel
|
||||||
|
}()
|
||||||
|
|
||||||
|
reconfigureCmd.SetContext(ctx)
|
||||||
|
err := reconfigureCmd.RunE(reconfigureCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, running)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Stop", func(t *testing.T) {
|
||||||
|
stopCmd.SetContext(ctx)
|
||||||
|
err := stopCmd.RunE(stopCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
stopped, err := waitForServiceStatus(service.StatusStopped, serviceStopTimeout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, stopped)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Uninstall", func(t *testing.T) {
|
||||||
|
uninstallCmd.SetContext(ctx)
|
||||||
|
err := uninstallCmd.RunE(uninstallCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cfg, err := newSVCConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctxSvc, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = s.Status()
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServiceEnvVars tests environment variable parsing
|
||||||
|
func TestServiceEnvVars(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
envVars []string
|
||||||
|
expected map[string]string
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid single env var",
|
||||||
|
envVars: []string{"LOG_LEVEL=debug"},
|
||||||
|
expected: map[string]string{
|
||||||
|
"LOG_LEVEL": "debug",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid multiple env vars",
|
||||||
|
envVars: []string{"LOG_LEVEL=debug", "CUSTOM_VAR=value"},
|
||||||
|
expected: map[string]string{
|
||||||
|
"LOG_LEVEL": "debug",
|
||||||
|
"CUSTOM_VAR": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Env var with spaces",
|
||||||
|
envVars: []string{" KEY = value "},
|
||||||
|
expected: map[string]string{
|
||||||
|
"KEY": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid format - no equals",
|
||||||
|
envVars: []string{"INVALID"},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid format - empty key",
|
||||||
|
envVars: []string{"=value"},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty value is valid",
|
||||||
|
envVars: []string{"KEY="},
|
||||||
|
expected: map[string]string{
|
||||||
|
"KEY": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty slice",
|
||||||
|
envVars: []string{},
|
||||||
|
expected: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty string in slice",
|
||||||
|
envVars: []string{"", "KEY=value", ""},
|
||||||
|
expected: map[string]string{"KEY": "value"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := parseServiceEnvVars(tt.envVars)
|
||||||
|
|
||||||
|
if tt.expectErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServiceConfigWithEnvVars tests service config creation with env vars
|
||||||
|
func TestServiceConfigWithEnvVars(t *testing.T) {
|
||||||
|
originalServiceName := serviceName
|
||||||
|
originalServiceEnvVars := serviceEnvVars
|
||||||
|
defer func() {
|
||||||
|
serviceName = originalServiceName
|
||||||
|
serviceEnvVars = originalServiceEnvVars
|
||||||
|
}()
|
||||||
|
|
||||||
|
serviceName = "test-service"
|
||||||
|
serviceEnvVars = []string{"TEST_VAR=test_value", "ANOTHER_VAR=another_value"}
|
||||||
|
|
||||||
|
cfg, err := newSVCConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "test-service", cfg.Name)
|
||||||
|
assert.Equal(t, "test_value", cfg.EnvVars["TEST_VAR"])
|
||||||
|
assert.Equal(t, "another_value", cfg.EnvVars["ANOTHER_VAR"])
|
||||||
|
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
assert.Equal(t, "test-service", cfg.EnvVars["SYSTEMD_UNIT"])
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user