Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ releases
.env

server.dev.yaml
.gocache/*
38 changes: 38 additions & 0 deletions cmd/client/config_path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"os"
"path/filepath"

"github.com/openmined/syftbox/internal/client/config"
"github.com/openmined/syftbox/internal/utils"
"github.com/spf13/cobra"
)

// resolveConfigPath determines which config file path to use, honoring (in order):
// 1) An explicitly set --config flag
// 2) SYFTBOX_CONFIG_PATH environment variable
// 3) Existing config files in common locations
// 4) The default path
func resolveConfigPath(cmd *cobra.Command) string {
if cfgFlag := cmd.Flag("config"); cfgFlag != nil && cfgFlag.Changed {
return cfgFlag.Value.String()
}

if envPath := os.Getenv("SYFTBOX_CONFIG_PATH"); envPath != "" {
return envPath
}

candidates := []string{
config.DefaultConfigPath,
filepath.Join(home, ".config", "syftbox", "config.json"),
}

for _, candidate := range candidates {
if utils.FileExists(candidate) {
return candidate
}
}

return config.DefaultConfigPath
}
67 changes: 67 additions & 0 deletions cmd/client/config_path_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package main

import (
"os"
"path/filepath"
"testing"

"github.com/openmined/syftbox/internal/client/config"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)

func newTestCmd() *cobra.Command {
cmd := &cobra.Command{}
cmd.PersistentFlags().StringP("config", "c", config.DefaultConfigPath, "path to config file")
return cmd
}

func TestResolveConfigPathFlagBeatsEnv(t *testing.T) {
cmd := newTestCmd()
flagPath := "/tmp/flag/config.json"
envPath := "/tmp/env/config.json"

t.Setenv("SYFTBOX_CONFIG_PATH", envPath)
err := cmd.PersistentFlags().Set("config", flagPath)
assert.NoError(t, err)

resolved := resolveConfigPath(cmd)
assert.Equal(t, flagPath, resolved)
}

func TestResolveConfigPathUsesEnvWhenNoFlag(t *testing.T) {
cmd := newTestCmd()
envPath := "/tmp/env/config.json"

t.Setenv("SYFTBOX_CONFIG_PATH", envPath)

resolved := resolveConfigPath(cmd)
assert.Equal(t, envPath, resolved)
}

func TestResolveConfigPathFindsExistingFile(t *testing.T) {
// Point the helper's home to an isolated temp dir so we don't touch real files.
oldHome := home
tempHome := t.TempDir()
home = tempHome
t.Cleanup(func() { home = oldHome })

cmd := newTestCmd()
t.Setenv("SYFTBOX_CONFIG_PATH", "")

existing := filepath.Join(home, ".config", "syftbox", "config.json")
err := os.MkdirAll(filepath.Dir(existing), 0o755)
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(existing, []byte("{}"), 0o644))

resolved := resolveConfigPath(cmd)
assert.Equal(t, existing, resolved)
}

func TestResolveConfigPathFallsBackToDefault(t *testing.T) {
cmd := newTestCmd()
t.Setenv("SYFTBOX_CONFIG_PATH", "")

resolved := resolveConfigPath(cmd)
assert.Equal(t, config.DefaultConfigPath, resolved)
}
4 changes: 4 additions & 0 deletions cmd/client/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ func newDaemonCmd() *cobra.Command {

slog.Info("syftbox", "version", version.Version, "revision", version.Revision, "build", version.BuildDate)

configPath := resolveConfigPath(cmd)
slog.Info("daemon using config", "path", configPath)

daemon, err := client.NewClientDaemon(&controlplane.CPServerConfig{
Addr: addr,
AuthToken: authToken,
EnableSwagger: enableSwagger,
ConfigPath: configPath,
})
if err != nil {
return err
Expand Down
135 changes: 135 additions & 0 deletions cmd/client/daemon_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package main

import (
"os"
"path/filepath"
"testing"

"github.com/openmined/syftbox/internal/client/config"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDaemonConfigPath(t *testing.T) {
// Helper function to create a test daemon command
createTestDaemonCmd := func() *cobra.Command {
var addr string
var authToken string
var enableSwagger bool

cmd := &cobra.Command{
Use: "daemon",
Short: "Test daemon command",
RunE: func(cmd *cobra.Command, args []string) error {
cmd.Annotations = map[string]string{
"resolved_config": resolveConfigPath(cmd),
}
return nil
},
}

cmd.Flags().StringVarP(&addr, "http-addr", "a", "localhost:7938", "Address to bind")
cmd.Flags().StringVarP(&authToken, "http-token", "t", "", "Access token")
cmd.Flags().BoolVarP(&enableSwagger, "http-swagger", "s", true, "Enable Swagger")
cmd.PersistentFlags().StringP("config", "c", config.DefaultConfigPath, "path to config file")

return cmd
}

t.Run("uses SYFTBOX_CONFIG_PATH environment variable", func(t *testing.T) {
// Set environment variable
testPath := "/custom/env/path/config.json"
os.Setenv("SYFTBOX_CONFIG_PATH", testPath)
defer os.Unsetenv("SYFTBOX_CONFIG_PATH")

cmd := createTestDaemonCmd()
cmd.SetArgs([]string{})

err := cmd.Execute()
require.NoError(t, err)

// Check resolved config path
assert.Equal(t, testPath, cmd.Annotations["resolved_config"])
})

t.Run("flag overrides environment variable", func(t *testing.T) {
// Set environment variable
envPath := "/env/path/config.json"
os.Setenv("SYFTBOX_CONFIG_PATH", envPath)
defer os.Unsetenv("SYFTBOX_CONFIG_PATH")

flagPath := "/flag/path/config.json"
cmd := createTestDaemonCmd()
cmd.SetArgs([]string{"--config", flagPath})

err := cmd.Execute()
require.NoError(t, err)

// Flag should override env var
assert.Equal(t, flagPath, cmd.Annotations["resolved_config"])
})

t.Run("uses default when no env or flag", func(t *testing.T) {
// Make sure env var is not set
os.Unsetenv("SYFTBOX_CONFIG_PATH")

cmd := createTestDaemonCmd()
cmd.SetArgs([]string{})

err := cmd.Execute()
require.NoError(t, err)

// Should use default
home, _ := os.UserHomeDir()
expectedDefault := filepath.Join(home, ".syftbox", "config.json")
assert.Equal(t, expectedDefault, cmd.Annotations["resolved_config"])
})

t.Run("short flag -c works", func(t *testing.T) {
os.Unsetenv("SYFTBOX_CONFIG_PATH")

flagPath := "/short/flag/config.json"
cmd := createTestDaemonCmd()
cmd.SetArgs([]string{"-c", flagPath})

err := cmd.Execute()
require.NoError(t, err)

assert.Equal(t, flagPath, cmd.Annotations["resolved_config"])
})

t.Run("priority order: flag > env > default", func(t *testing.T) {
// This test documents the expected priority order
envPath := "/env/config.json"
flagPath := "/flag/config.json"

// Test 1: Only env var set
os.Setenv("SYFTBOX_CONFIG_PATH", envPath)
cmd1 := createTestDaemonCmd()
cmd1.SetArgs([]string{})
err := cmd1.Execute()
require.NoError(t, err)
assert.Equal(t, envPath, cmd1.Annotations["resolved_config"], "Should use env var when no flag")

// Test 2: Both env var and flag set
cmd2 := createTestDaemonCmd()
cmd2.SetArgs([]string{"--config", flagPath})
err = cmd2.Execute()
require.NoError(t, err)
assert.Equal(t, flagPath, cmd2.Annotations["resolved_config"], "Flag should override env var")

// Test 3: Neither set
os.Unsetenv("SYFTBOX_CONFIG_PATH")
cmd3 := createTestDaemonCmd()
cmd3.SetArgs([]string{})
err = cmd3.Execute()
require.NoError(t, err)
home, _ := os.UserHomeDir()
expectedDefault := filepath.Join(home, ".syftbox", "config.json")
assert.Equal(t, expectedDefault, cmd3.Annotations["resolved_config"], "Should use default when nothing set")

// Clean up
os.Unsetenv("SYFTBOX_CONFIG_PATH")
})
}
37 changes: 37 additions & 0 deletions cmd/client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/mattn/go-isatty"
"github.com/openmined/syftbox/internal/client"
"github.com/openmined/syftbox/internal/client/config"
"github.com/openmined/syftbox/internal/client/controlplane"
"github.com/openmined/syftbox/internal/utils"
"github.com/openmined/syftbox/internal/version"
"github.com/spf13/cobra"
Expand All @@ -30,6 +31,38 @@ var rootCmd = &cobra.Command{
Short: "SyftBox CLI",
Version: version.Detailed(),
Run: func(cmd *cobra.Command, args []string) {
enableControlPlane, _ := cmd.Flags().GetBool("control-plane")
if enableControlPlane {
cmd.SilenceUsage = true

configPath := resolveConfigPath(cmd)
httpAddr, _ := cmd.Flags().GetString("http-addr")
httpToken, _ := cmd.Flags().GetString("http-token")
enableSwagger, _ := cmd.Flags().GetBool("http-swagger")

showSyftBoxHeader()
slog.Info("syftbox", "version", version.Version, "revision", version.Revision, "build", version.BuildDate)
slog.Info("control plane mode", "addr", httpAddr, "config", configPath)

daemon, err := client.NewClientDaemon(&controlplane.CPServerConfig{
Addr: httpAddr,
AuthToken: httpToken,
EnableSwagger: enableSwagger,
ConfigPath: configPath,
})
if err != nil {
slog.Error("syftbox control plane", "error", err)
os.Exit(1)
}

defer slog.Info("Bye!")
if err := daemon.Start(cmd.Context()); err != nil && !errors.Is(err, context.Canceled) {
slog.Error("syftbox control plane start", "error", err)
os.Exit(1)
}
return
}

cfg, err := loadConfig(cmd)
if err != nil {
slog.Error("syftbox config", "error", err)
Expand Down Expand Up @@ -76,6 +109,10 @@ func init() {
rootCmd.Flags().StringP("datadir", "d", config.DefaultDataDir, "data directory where the syftbox workspace is stored")
rootCmd.Flags().StringP("server", "s", config.DefaultServerURL, "url of the syftbox server")
rootCmd.PersistentFlags().StringP("config", "c", config.DefaultConfigPath, "path to config file")
rootCmd.Flags().Bool("control-plane", false, "start the local control plane HTTP server")
rootCmd.Flags().String("http-addr", "localhost:7938", "address to bind the local control plane http server")
rootCmd.Flags().String("http-token", "", "access token for the local control plane http server")
rootCmd.Flags().Bool("http-swagger", true, "enable Swagger for the local control plane http server")
}

func main() {
Expand Down
1 change: 1 addition & 0 deletions internal/client/controlplane/controlplane_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ type CPServerConfig struct {
Addr string // Address to bind the control plane server
AuthToken string // Access token for the control plane server
EnableSwagger bool // EnableSwagger enables Swagger documentation
ConfigPath string // Path to the config file
}
4 changes: 4 additions & 0 deletions internal/client/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ type ClientDaemon struct {

func NewClientDaemon(config *controlplane.CPServerConfig) (*ClientDaemon, error) {
mgr := datasitemgr.New()
// Pass the config path to the datasite manager
if config.ConfigPath != "" {
mgr.SetConfigPath(config.ConfigPath)
}
cps, err := controlplane.NewControlPlaneServer(config, mgr)
if err != nil {
return nil, err
Expand Down
33 changes: 29 additions & 4 deletions internal/client/datasitemgr/datasite_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type DatasiteManager struct {
status DatasiteStatus
runtimeCfg *RuntimeConfig
datasiteErr error
configPath string
mu sync.RWMutex
}

Expand All @@ -47,16 +48,26 @@ func (d *DatasiteManager) SetRuntimeConfig(cfg *RuntimeConfig) {
d.runtimeCfg = cfg
}

func (d *DatasiteManager) SetConfigPath(path string) {
d.mu.Lock()
defer d.mu.Unlock()

d.configPath = path
}

func (d *DatasiteManager) Start(ctx context.Context) error {
slog.Info("datasite manager start")

if !d.defaultConfigExists() {
slog.Info("default config not found. waiting to be provisioned.")
// Use the configured path or fall back to default
configPath := d.getConfigPath()

if !d.configExists(configPath) {
slog.Info("config not found. waiting to be provisioned.", "path", configPath)
return nil
}

slog.Info("default config found. provisioning datasite.")
cfg, err := config.LoadFromFile(config.DefaultConfigPath)
slog.Info("config found. provisioning datasite.", "path", configPath)
cfg, err := config.LoadFromFile(configPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
Expand Down Expand Up @@ -154,6 +165,20 @@ func (d *DatasiteManager) newDatasite(ctx context.Context, cfg *config.Config) e
return nil
}

func (d *DatasiteManager) getConfigPath() string {
d.mu.RLock()
defer d.mu.RUnlock()

if d.configPath != "" {
return d.configPath
}
return config.DefaultConfigPath
}

func (d *DatasiteManager) configExists(path string) bool {
return utils.FileExists(path)
}

func (d *DatasiteManager) defaultConfigExists() bool {
return utils.FileExists(config.DefaultConfigPath)
}
Loading