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
32 changes: 24 additions & 8 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,44 @@ labels: bug
---

## Describe the bug
A clear and concise description of what the bug is.
What broke, and what were you trying to do?

## To Reproduce
Steps to reproduce the behavior:
1.
2.
3.
1.
2.
3.

## Expected behavior
What you expected to happen.

## Actual behavior
What actually happened (include errors, stack traces, and logs if possible).
What actually happened. Include errors, stack traces, and relevant logs if
possible.

## Diagnostics
If the bundle starts, please run the experimental doctor command and attach the
terminal output plus the generated JSON report:

```sh
docker exec any-sync-bundle any-sync-bundle doctor
```

The report is usually written to `./data/doctor/doctor_<timestamp>.json` on the
host. Please remove secrets before attaching configs or logs.

## Environment
- Deployment: [AIO container | minimal container | binary]
<!-- Will be perfect to attach outout if `--version` -->
<!-- It helps to attach output from `any-sync-bundle --version`. -->
- Version/tag:
- OS/Arch:
- Compose file or start command:
- OS/Arch:
- Storage: [local Badger | S3/MinIO]

## Configuration
<!-- If applicable, add your config file or relevant snippets. -->
<!--
If applicable, add relevant config snippets without private keys or passwords.
-->

## Additional context
Add any other context about the problem here.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

```sh
docker run -d \
--name any-sync-bundle \
-e ANY_SYNC_BUNDLE_INIT_EXTERNAL_ADDRS="192.168.100.9" \
-p 33010:33010 \
-p 33020:33020/udp \
Expand Down Expand Up @@ -145,6 +146,7 @@ Edit `ANY_SYNC_BUNDLE_INIT_EXTERNAL_ADDRS` in the compose file before starting.
| -------------------------- | ----------------------------------------- | ------- |
| `./data/bundle-config.yml` | Service config + private keys | 🔴 Yes |
| `./data/client-config.yml` | Client config (regenerated on each start) | 🟢 No |
| `./data/doctor/*.json` | Experimental diagnostic reports | 🟢 No |

### Storage Options

Expand Down Expand Up @@ -221,6 +223,7 @@ All parameters available as binary flags or environment variables. See `./any-sy
| ------------------ | ---------------------------------------------------------------- |
| `start-bundle` | Start with external MongoDB/Redis |
| `start-all-in-one` | Start with embedded MongoDB/Redis (used in all-in-one container) |
| `doctor` | Run experimental diagnostics against the already running bundle |

### Start Command Flags

Expand All @@ -238,8 +241,39 @@ All parameters available as binary flags or environment variables. See `./any-sy
| `--initial-s3-force-path-style` | Use path-style S3 URLs (required for MinIO) <br> ‣ Default: `false` <br> ‣ Environment Variable: `ANY_SYNC_BUNDLE_INIT_S3_FORCE_PATH_STYLE` |
| `--initial-filenode-default-limit` | Storage limit per space in bytes <br> ‣ Default: `1099511627776` (1 TiB) <br> ‣ Environment Variable: `ANY_SYNC_BUNDLE_INIT_FILENODE_DEFAULT_LIMIT` |

### Doctor Command Flags

| Flag | Description |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--bundle-config`, `-c` | Path to the bundle configuration YAML file, used to locate the doctor socket <br> ‣ Default: `./data/bundle-config.yml` <br> ‣ Environment Variable: `ANY_SYNC_BUNDLE_CONFIG` |

## Operations

### Diagnostics

Run diagnostics from inside the running container:

```sh
docker exec any-sync-bundle any-sync-bundle doctor
```

The command connects to the running bundle over a local Unix socket next to
`bundle-config.yml`. In the default container layout this is `/data/bundle.sock`.

```text
Connecting to running bundle
socket: /data/bundle.sock
status: connected

Experimental:
doctor output and JSON report schema may change between releases.

...

[7/7] Report
written: /data/doctor/doctor_2026-05-22T14-33-10Z.json
```

### Backup & Recovery

**Backup:**
Expand Down
35 changes: 35 additions & 0 deletions cmd/doctor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package cmd

import (
"context"
"fmt"
"path/filepath"

"github.com/urfave/cli/v2"

"github.com/grishy/any-sync-bundle/doctor"
)

func cmdDoctor(ctx context.Context) *cli.Command {
return &cli.Command{
Name: "doctor",
Usage: "Run experimental diagnostics against the already running bundle process",
Description: "EXPERIMENTAL: command output and JSON report schema may change between releases.",
Flags: []cli.Flag{
&cli.PathFlag{
Name: flagStartBundleConfigPath,
Aliases: []string{"c"},
Value: "./data/bundle-config.yml",
EnvVars: []string{"ANY_SYNC_BUNDLE_CONFIG"},
Usage: "Path to the bundle configuration YAML file, used to locate the doctor socket",
},
},
Action: func(cCtx *cli.Context) error {
bundleConfigPath, err := filepath.Abs(cCtx.String(flagStartBundleConfigPath))
if err != nil {
return fmt.Errorf("resolve bundle config path: %w", err)
}
return doctor.RunClient(ctx, doctor.SocketPath(bundleConfigPath), cCtx.App.Writer)
},
}
}
90 changes: 90 additions & 0 deletions cmd/doctor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package cmd

import (
"context"
"flag"
"path/filepath"
"testing"

"github.com/urfave/cli/v2"
)

func TestRootIncludesDoctorCommand(t *testing.T) {
app := Root(context.Background())

if app.Command("doctor") == nil {
t.Fatal("Root() does not include doctor command")
}
}

func TestPrepareBundleConfigKeepsRuntimePaths(t *testing.T) {
dir := t.TempDir()
bundleConfigPath := filepath.Join(dir, "bundle-config.yml")
clientConfigPath := filepath.Join(dir, "client-config.yml")
storagePath := filepath.Join(dir, "storage")
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
for _, cliFlag := range buildStartFlags() {
if err := cliFlag.Apply(flagSet); err != nil {
t.Fatalf("apply flag: %v", err)
}
}
if err := flagSet.Set(flagStartBundleConfigPath, bundleConfigPath); err != nil {
t.Fatalf("set bundle config path: %v", err)
}
if err := flagSet.Set(flagStartClientConfigPath, clientConfigPath); err != nil {
t.Fatalf("set client config path: %v", err)
}
if err := flagSet.Set(flagStartStoragePath, storagePath); err != nil {
t.Fatalf("set storage path: %v", err)
}
cCtx := cli.NewContext(cli.NewApp(), flagSet, nil)

prepared, err := prepareBundleConfig(cCtx)
if err != nil {
t.Fatalf("prepareBundleConfig() error = %v", err)
}

if prepared.Config == nil {
t.Fatal("prepared Config is nil")
}
if prepared.BundleConfigPath != bundleConfigPath {
t.Fatalf("bundle config path = %q, want %q", prepared.BundleConfigPath, bundleConfigPath)
}
if prepared.ClientConfigPath != clientConfigPath {
t.Fatalf("client config path = %q, want %q", prepared.ClientConfigPath, clientConfigPath)
}
}

func TestPrepareBundleConfigStoresAbsoluteRuntimePaths(t *testing.T) {
dir := t.TempDir()
t.Chdir(dir)
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
for _, cliFlag := range buildStartFlags() {
if err := cliFlag.Apply(flagSet); err != nil {
t.Fatalf("apply flag: %v", err)
}
}
if err := flagSet.Set(flagStartBundleConfigPath, "data/bundle-config.yml"); err != nil {
t.Fatalf("set bundle config path: %v", err)
}
if err := flagSet.Set(flagStartClientConfigPath, "data/client-config.yml"); err != nil {
t.Fatalf("set client config path: %v", err)
}
cCtx := cli.NewContext(cli.NewApp(), flagSet, nil)

prepared, err := prepareBundleConfig(cCtx)
if err != nil {
t.Fatalf("prepareBundleConfig() error = %v", err)
}

wantBundleConfigPath := filepath.Join(dir, "data", "bundle-config.yml")
if prepared.BundleConfigPath != wantBundleConfigPath {
t.Fatalf("bundle config path = %q, want %q",
prepared.BundleConfigPath, wantBundleConfigPath)
}
wantClientConfigPath := filepath.Join(dir, "data", "client-config.yml")
if prepared.ClientConfigPath != wantClientConfigPath {
t.Fatalf("client config path = %q, want %q",
prepared.ClientConfigPath, wantClientConfigPath)
}
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func Root(ctx context.Context) *cli.App {
Commands: []*cli.Command{
cmdStartAllInOne(ctx),
cmdStartBundle(ctx),
cmdDoctor(ctx),
},
}
}
Expand Down
Loading
Loading