Skip to content
Merged
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
6 changes: 6 additions & 0 deletions docs/config-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ spec:
|-------|------|---------|-------------|
| `engine` | `string` | — | Sync engine (currently only `syncthing`) |
| `paths` | `[]string` | — | Mappings in `local:remote` format (max 1 entry) |
| `remoteIgnore` | `[]string` | — | Syncthing ignore patterns written to the remote `.stignore` before sync starts |
| `syncthing.version` | `string` | `v1.29.7` | Local Syncthing binary version |
| `syncthing.autoInstall` | `bool` | `true` | Auto-install local Syncthing |
| `syncthing.image` | `string` | `ghcr.io/acmore/okdev:<version>` | Sidecar image (fallback: `edge`) |
Expand All @@ -245,6 +246,8 @@ spec:

Local ignore rules come from the synced workspace's `.stignore`. `okdev init` writes a starter `.stignore` for built-in templates, and `okdev up` creates one with default patterns if the local sync root does not already have one. Editing `.stignore` takes effect automatically as Syncthing notices the change, but it does not remove files that were already synced to the remote workspace. For faster initial syncs, consider ignoring large generated build outputs or local test artifacts such as `debug/`, `release/`, caches, and dataset directories when they do not need to exist remotely.

Use `remoteIgnore` for paths that should remain local-only after you copy or sync them down from a session. okdev writes these patterns to `.stignore` in the remote sync root before configuring Syncthing, so the remote side will not index or pull matching files from your local workspace on the next start. The patterns use Syncthing `.stignore` syntax.

The `syncthing.version` field controls the local binary on your machine. The Syncthing binary inside the sidecar comes from `spec.sidecar.image`.

```yaml
Expand All @@ -259,6 +262,9 @@ spec:
compression: false
paths:
- .:/workspace
remoteIgnore:
- profiles/
- "*.prof"
```

---
Expand Down
23 changes: 23 additions & 0 deletions internal/cli/syncthing.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ func runSyncthingSync(cmd *cobra.Command, opts *Options, cfg *config.DevEnvironm
if _, err := execInSyncthingContainer(ctx, k, namespace, pod, fmt.Sprintf("mkdir -p %s", syncengine.ShellEscape(pair.Remote))); err != nil {
return err
}
if err := writeRemoteSTIgnoreInPod(ctx, k, namespace, pod, pair.Remote, cfg.Spec.Sync.RemoteIgnore); err != nil {
return err
}
localHome, err := localSyncthingHome(sessionName)
if err != nil {
return err
Expand Down Expand Up @@ -486,6 +489,26 @@ func writeLocalSTIgnore(localPath string) error {
return writeSTIgnore(localPath, defaultSyncExcludes)
}

func writeRemoteSTIgnoreInPod(ctx context.Context, k interface {
ExecShInContainer(context.Context, string, string, string, string) ([]byte, error)
}, namespace, pod, remotePath string, excludes []string) error {
content, ok := buildSTIgnoreContent(excludes)
if !ok {
return nil
}
remotePath = strings.TrimRight(strings.TrimSpace(remotePath), "/")
if remotePath == "" || remotePath == "." || remotePath == "/" {
return fmt.Errorf("refusing to write remote .stignore at unsafe sync root %q", remotePath)
}
stignorePath := path.Join(remotePath, ".stignore")
esc := syncengine.ShellEscape
script := fmt.Sprintf("mkdir -p %s && printf %%s %s > %s", esc(remotePath), esc(content), esc(stignorePath))
if _, err := execInSyncthingContainer(ctx, k, namespace, pod, script); err != nil {
return fmt.Errorf("write remote .stignore: %w", err)
}
return nil
}

func buildSTIgnoreContent(excludes []string) (string, bool) {
if len(excludes) == 0 {
return "", false
Expand Down
38 changes: 38 additions & 0 deletions internal/cli/syncthing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,44 @@ func TestWriteLocalSTIgnorePreservesExistingFileWhenDefaultsImplicit(t *testing.
}
}

func TestWriteRemoteSTIgnoreInPodWritesConfiguredPatterns(t *testing.T) {
rec := &syncthingExecRecorder{}
err := writeRemoteSTIgnoreInPod(context.Background(), rec, "default", "pod-a", "/workspace", []string{"profiles/", "*.prof"})
if err != nil {
t.Fatal(err)
}
if rec.namespace != "default" || rec.pod != "pod-a" || rec.container != syncthingContainerName {
t.Fatalf("unexpected exec target namespace=%q pod=%q container=%q", rec.namespace, rec.pod, rec.container)
}
for _, want := range []string{
"mkdir -p '/workspace'",
"printf %s 'profiles/\n*.prof\n'",
"> '/workspace/.stignore'",
} {
if !strings.Contains(rec.script, want) {
t.Fatalf("expected remote .stignore script to contain %q, got %q", want, rec.script)
}
}
}

func TestWriteRemoteSTIgnoreInPodSkipsEmptyPatterns(t *testing.T) {
rec := &syncthingExecRecorder{}
if err := writeRemoteSTIgnoreInPod(context.Background(), rec, "default", "pod-a", "/workspace", nil); err != nil {
t.Fatal(err)
}
if rec.script != "" {
t.Fatalf("expected no remote .stignore write, got %q", rec.script)
}
}

func TestWriteRemoteSTIgnoreInPodRejectsUnsafeRoot(t *testing.T) {
rec := &syncthingExecRecorder{}
err := writeRemoteSTIgnoreInPod(context.Background(), rec, "default", "pod-a", "/", []string{"profiles/"})
if err == nil || !strings.Contains(err.Error(), "unsafe sync root") {
t.Fatalf("expected unsafe root error, got %v", err)
}
}

func TestStopLocalSyncthingForHomeStopsRecordedPID(t *testing.T) {
home := t.TempDir()
cmd := exec.Command("sleep", "30")
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ type MetadataMap struct {
type SyncSpec struct {
Paths []string `yaml:"paths"`
PreservePaths []string `yaml:"preservePaths"`
RemoteIgnore []string `yaml:"remoteIgnore,omitempty"`
Engine string `yaml:"engine"`
Syncthing SyncthingSpec `yaml:"syncthing"`
}
Expand Down
6 changes: 5 additions & 1 deletion internal/config/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ func Load(configPath string) (*DevEnvironment, string, error) {
return nil, "", fmt.Errorf("read config %q: %w", path, err)
}
if removed := removedSyncIgnoreField(raw); removed != "" {
return nil, "", fmt.Errorf("validate config %q: %w", path, &MigrationEligibleError{Err: fmt.Errorf("%s is removed; manage local ignores with .stignore in the synced local workspace instead", removed)})
msg := fmt.Sprintf("%s is removed; manage local ignores with .stignore in the synced local workspace instead", removed)
if removed == "spec.sync.remoteExclude" {
msg = "spec.sync.remoteExclude is removed; use spec.sync.remoteIgnore for managed remote .stignore patterns"
}
return nil, "", fmt.Errorf("validate config %q: %w", path, &MigrationEligibleError{Err: errors.New(msg)})
}

var cfg DevEnvironment
Expand Down
29 changes: 29 additions & 0 deletions internal/config/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,35 @@ spec:
}
}

func TestLoadAcceptsSyncRemoteIgnore(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, DefaultFile)
raw := []byte(`
apiVersion: okdev.io/v1alpha1
kind: DevEnvironment
metadata:
name: test
spec:
sync:
engine: syncthing
paths: [".:/workspace"]
remoteIgnore:
- profiles/
- "*.prof"
`)
if err := os.WriteFile(path, raw, 0o644); err != nil {
t.Fatalf("write config: %v", err)
}

cfg, _, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
if got := cfg.Spec.Sync.RemoteIgnore; len(got) != 2 || got[0] != "profiles/" || got[1] != "*.prof" {
t.Fatalf("unexpected remoteIgnore %+v", got)
}
}

func TestLoadRejectsRemovedSyncRemoteExclude(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, DefaultFile)
Expand Down
44 changes: 44 additions & 0 deletions scripts/e2e_kind_smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ fi
replace_all_in_file "$CFG_PATH" 'persistentSession: true' 'persistentSession: false'
insert_after_line_once "$CFG_PATH" ' ssh:' ' persistentSession: false'
insert_after_line_once "$CFG_PATH" ' ssh:' ' forwardAgent: true'
python3 - "$CFG_PATH" <<'PY'
import pathlib
import sys

path = pathlib.Path(sys.argv[1])
text = path.read_text()
block = ' remoteIgnore:\n - profiles/\n - "*.prof"\n'
if "remoteIgnore:" not in text:
marker = "\n ports:\n"
if marker not in text:
raise SystemExit("ports block not found")
text = text.replace(marker, "\n" + block + " ports:\n", 1)
path.write_text(text)
PY

echo "Generated config:"
cat "$CFG_PATH"
Expand Down Expand Up @@ -194,6 +208,36 @@ if [[ "$SYNC_OK" != "true" ]]; then
exit 1
fi

echo "Verifying remoteIgnore writes remote .stignore and keeps profiling data local-only"
REMOTE_STIGNORE=$("$OKDEV_BIN" --config "$CFG_PATH" --session "$SESSION_NAME" exec --no-tty --cmd 'cat /workspace/.stignore 2>/dev/null || true')
if [[ "$REMOTE_STIGNORE" != *"profiles/"* || "$REMOTE_STIGNORE" != *"*.prof"* ]]; then
echo "ERROR: expected remote .stignore to contain remoteIgnore patterns" >&2
echo "$REMOTE_STIGNORE" >&2
exit 1
fi
mkdir -p "$SYNC_DIR/profiles"
echo "local profile payload" >"$SYNC_DIR/profiles/run.prof"
echo "remote ignore control" >"$SYNC_DIR/remote-ignore-control.txt"
CONTROL_SYNCED=false
for i in $(seq 1 30); do
REMOTE_CONTROL=$("$OKDEV_BIN" --config "$CFG_PATH" --session "$SESSION_NAME" exec --no-tty --cmd 'if [ -f /workspace/remote-ignore-control.txt ]; then cat /workspace/remote-ignore-control.txt; fi' || true)
if [[ "$REMOTE_CONTROL" == "remote ignore control" ]]; then
CONTROL_SYNCED=true
break
fi
sleep 2
done
if [[ "$CONTROL_SYNCED" != "true" ]]; then
echo "ERROR: expected non-ignored control file to sync while testing remoteIgnore" >&2
exit 1
fi
REMOTE_PROFILE_STATE=$("$OKDEV_BIN" --config "$CFG_PATH" --session "$SESSION_NAME" exec --no-tty --cmd 'if [ -e /workspace/profiles/run.prof ]; then echo present; else echo absent; fi' || true)
if [[ "$REMOTE_PROFILE_STATE" != "absent" ]]; then
echo "ERROR: expected remoteIgnore to keep profiling data out of remote workspace, got $REMOTE_PROFILE_STATE" >&2
exit 1
fi
echo "remoteIgnore behavior verified"

echo "Verifying repeated okdev up reuses active sync"
SYNC_PID_BEFORE=""
for i in $(seq 1 5); do
Expand Down
Loading