Metadata-Version: 2.4
Name: sync2pod
Version: 0.1.2
Summary: Synchronize local project trees into running Kubernetes pods
Project-URL: Repository, https://github.com/xchen007/sync2pod
Project-URL: Issues, https://github.com/xchen007/sync2pod/issues
Author: xchen007
License-Expression: Apache-2.0
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: <3.14,>=3.10
Requires-Dist: loguru<1,>=0.7
Requires-Dist: watchdog<7,>=4
Description-Content-Type: text/markdown

# sync2pod

`sync2pod` mirrors a local project tree into a running Kubernetes pod and then
keeps it synchronized with bounded, archive-based updates. It is designed for
local development workflows that already have a working `kubectl` command or a
compatible wrapper.

This project is alpha software. Its configuration and command behavior may
change.

## Installation

Install from PyPI:

```bash
python -m pip install sync2pod
```

Build and install a local wheel from source:

```bash
./scripts/build-package.sh
python3 -m pip install dist/sync2pod-0.1.2-py3-none-any.whl
```

Python 3.10 through 3.13 are supported. The wheel installs two equivalent
console commands:

```bash
sync2pod --help
sync_local_to_pod --help
```

The `sync_local_to_pod` name preserves compatibility with existing scripts and
mtools integrations.

## Quick start

Create a filesystem-backed project configuration, inspect it, and run a dry
run before connecting to a pod:

```bash
sync2pod --init-config --local-path /path/to/project --project demo
$EDITOR ~/.sync2pod/demo/sync_config.json
sync2pod --list-projects
sync2pod --project demo --dry-run
sync2pod --project demo
```

Without `--project`, the project name defaults to the final component of
`--local-path` during initialization.

## Configuration

Each normal CLI project is stored as
`~/.sync2pod/<project>/sync_config.json`. Existing JSON project objects remain
compatible with the standalone package. Unknown legacy fields are ignored.

A standard `kubectl` configuration looks like this:

```json
{
  "cluster": "development",
  "namespace": "workspace",
  "pod_label": "app=my-service",
  "remote_parent_path": "/workspace",
  "local_path": "/Users/me/src/my-service",
  "container": "app",
  "kubectl_command": ["kubectl", "--context", "development"],
  "follow_sync": [],
  "exclude_paths": [".git", "node_modules", "*.log"],
  "max_workers": 10,
  "debounce_seconds": 1.0,
  "no_watch": false,
  "skip_verify": false,
  "prune": true,
  "upload_chunk_count": 6,
  "target_chunk_size_mb": 64
}
```

Wrapper commands are represented as an argument array, not a shell string.
`{cluster}` is replaced with the configured `cluster` value:

```json
{
  "cluster": "development",
  "kubectl_command": ["cluster-wrapper", "--target={cluster}", "kubectl"]
}
```

The complete schema is:

| Field | Required | Default | Meaning |
| --- | --- | --- | --- |
| `cluster` | yes | - | Cluster identifier available to wrapper commands. |
| `namespace` | yes | - | Namespace containing the target pod. |
| `pod_label` | yes | - | Label selector used to choose the first running pod. |
| `remote_parent_path` | yes | - | Absolute POSIX parent; the local directory name is appended. |
| `local_path` | yes | - | Existing primary source directory. |
| `container` | no | `null` | Explicit target container for every `exec` and `cp`; omit it for kubectl's default container. |
| `kubectl_command` | no | `["kubectl"]` | Non-empty argument array used before every Kubernetes operation. |
| `follow_sync` | no | `[]` | Additional source directories mapped below the remote project. |
| `exclude_paths` | no | `[]` | Path-name and glob exclusions applied to full and sparse sync. |
| `compress_threshold` | no | `100` | Positive legacy-compatible tuning value. |
| `max_workers` | no | `10` | Maximum sparse-watch worker count. |
| `debug` | no | `false` | Legacy-compatible diagnostic setting. |
| `show_concurrency` | no | `false` | Legacy-compatible concurrency display setting. |
| `no_watch` | no | `false` | Exit after the startup full mirror. |
| `skip_verify` | no | `false` | Skip the interactive configuration confirmation. |
| `debounce_seconds` | no | `1.0` | Positive watch-event debounce interval. |
| `prune` | no | `true` | Legacy-compatible setting; startup remains an authoritative mirror. |
| `upload_chunk_count` | no | `6` | Upper bound used when scheduling archive shards. |
| `target_chunk_size_mb` | no | `64` | Positive target size for startup archive shards. |

`cluster`, `namespace`, `pod_label`, `remote_parent_path`, and `local_path`
must be present and non-empty. Boolean and positive-number fields are validated
strictly.

### Follow mappings and managed symlinks

`follow_sync` maps directories outside the primary tree into named children of
the remote project:

```json
{
  "follow_sync": [
    {
      "local_path": "/Users/me/src/shared-library",
      "alias": "shared",
      "create_symlink": true
    }
  ]
}
```

The remote mapping is `/workspace/my-service/shared` in this example. When
`alias` is omitted or blank, the follow directory name is used. Aliases must be
single safe path segments and must be unique.

With `create_symlink: true`, sync2pod also manages
`<local_path>/shared -> /Users/me/src/shared-library` before synchronization.
It reuses an exact existing symlink but refuses to overwrite a file, directory,
or conflicting symlink. Set `create_symlink` to `false` to keep only the remote
mapping.

## Synchronization behavior

Every normal start performs an authoritative compressed full mirror before
watch mode becomes active. Files are archived locally, copied in a bounded
number of shards, verified, extracted into staging directories, and switched
into place only after all mappings are ready.

Warning: the remote project path is a mirror target, not a merge destination.
Startup replacement and later delete events remove remote content that is not
present locally. Do not point `remote_parent_path/local-directory-name` at
valuable pod data.

While watching, sync2pod batches filesystem events and applies sparse file,
subtree, and deletion actions. Independent work can run concurrently up to
`max_workers`; conflicting ancestor and descendant operations remain ordered.
The selected pod is fixed for the session, so restart the command after the pod
is replaced.

Unconfigured symlinks are archived as symlinks and are never followed outside
the configured roots. Sparse updates reject symlink paths that could escape a
source directory. Device files and other unsupported special files are not a
supported synchronization mechanism.

## Command-line interface

Both console names accept the same legacy-compatible flags:

| Flag | Behavior |
| --- | --- |
| `--init-config` | Create a project configuration. Requires `--local-path`. |
| `--list-projects` | List configured projects. |
| `--project NAME` | Select the project configuration. |
| `--local-path PATH` | Source directory used during initialization. |
| `--force` | Accepted for compatibility; startup already performs a full mirror. |
| `--skip-verify` | Skip the interactive confirmation for this invocation. |
| `--dry-run` | Build local archives and report mappings, counts, bytes, shards, and target without pod operations or symlink mutation. |

For unattended runs, set `skip_verify` in the project configuration or pass
`--skip-verify`.

## Pod requirements

The local machine must provide the configured Kubernetes command and permission
to list pods, execute commands, and copy files in the selected namespace. The
target container must provide:

```text
sh bash find tar md5sum rm mkdir mv cat
```

The process also needs write permission for the remote parent, staging paths,
and final project path.

## mtools and embedded backends

mtools can keep project JSON in its own database instead of
`~/.sync2pod`. It launches the compatibility command with:

```text
SYNC2POD_CONFIG_BACKEND=utools-db
SYNC2POD_CONFIGS_JSON={"project-name": { ... project configuration ... }}
```

When this backend saves configuration, stdout contains one machine-readable
line prefixed with `__SYNC2POD_DB_UPDATE__ `. Watch progress is emitted on
stderr with `__SYNC2POD_PROGRESS__ ` and the JSON keys `pending`, `uploading`,
`session_done`, and `session_failed`. Embedders should consume these control
lines and keep them out of user-facing logs.

The public Python entry point is also available:

```python
from sync2pod import run

run(project_configuration)
```

## Safety boundaries and non-goals

- sync2pod synchronizes into one already-running pod; it does not deploy,
  restart, or supervise workloads.
- It does not provide bidirectional synchronization, conflict resolution, or a
  durable cross-machine journal.
- It does not automatically reselect pods during a watch session.
- It does not interpret shell strings in `kubectl_command`.
- It does not promise preservation of remote-only files inside the mirror
  target.

## Troubleshooting

- **No running pod found:** verify `namespace`, `pod_label`, and the configured
  command by running its equivalent `get pods` request manually.
- **Missing required commands:** add the named tools to the target container;
  minimal images often omit `bash`, `tar`, or `md5sum`.
- **Permission denied:** verify local read access and remote write access,
  including the configured parent directory.
- **Configuration confirmation blocks automation:** pass `--skip-verify` or set
  `skip_verify` to `true`.
- **Follow symlink conflict:** remove or rename the conflicting local path, or
  set `create_symlink` to `false` for that follow mapping.
- **Pod restarted:** stop and relaunch sync2pod so it selects the replacement
  running pod and performs a fresh authoritative mirror.

## Development

```bash
uv sync --all-groups
uv run pytest -m "not performance" --cov=sync2pod --cov-report=term-missing
uv run pytest -m performance
uv build
```

## License

Licensed under the Apache License 2.0.
