diff --git a/.dockerignore b/.dockerignore index 6cb6e38..85b2135 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,4 +4,5 @@ !core/ !server/ !sqlite/ -!docker-entrypoint.sh +!postgres/ +!entrypoint-* diff --git a/.env b/.env deleted file mode 100644 index b80fa55..0000000 --- a/.env +++ /dev/null @@ -1,3 +0,0 @@ -# Versions must be major.minor -ALPINE_VERSION=3.19 -RUST_VERSION=1.78 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6840527..f992b29 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -6,15 +6,42 @@ on: - '*' jobs: - docker: + sqlite: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Load .env file - uses: xom9ikk/dotenv@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Set up QEMU uses: docker/setup-qemu-action@v3 + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Docker meta + id: meta-sqlite + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/gothenburgbitfactory/taskchampion-sync-server + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=match,pattern=\d.\d.\d,value=latest + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + path: "{context}/Dockerfile-sqlite" + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta-sqlite.outputs.tags }} + labels: ${{ steps.meta-sqlite.outputs.labels }} + postgres: + runs-on: ubuntu-latest + steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to ghcr.io @@ -24,11 +51,11 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Docker meta - id: meta + id: meta-postgres uses: docker/metadata-action@v5 with: images: | - ghcr.io/${{ github.repository }} + ghcr.io/gothenburgbitfactory/taskchampion-sync-server-postgres tags: | type=ref,event=branch type=semver,pattern={{version}} @@ -38,10 +65,8 @@ jobs: uses: docker/build-push-action@v6 with: context: . + path: "{context}/Dockerfile-postgres" platforms: linux/amd64,linux/arm64 push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - build-args: | - ALPINE_VERSION=${{ env.ALPINE_VERSION }} - RUST_VERSION=${{ env.RUST_VERSION }} + tags: ${{ steps.meta-postgres.outputs.tags }} + labels: ${{ steps.meta-postgres.outputs.labels }} diff --git a/Cargo.lock b/Cargo.lock index e32fd8e..0dabf71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -337,7 +337,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -654,7 +654,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -1371,7 +1371,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1634,7 +1634,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1898,6 +1898,7 @@ dependencies = [ "env_logger", "log", "native-tls", + "openssl", "postgres-native-tls", "pretty_assertions", "taskchampion-sync-server-core", @@ -1943,7 +1944,7 @@ dependencies = [ "getrandom", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2385,7 +2386,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2394,16 +2395,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.2", + "windows-targets", ] [[package]] @@ -2412,30 +2404,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -2444,96 +2420,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index ae61945..7fd6fca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,3 +30,6 @@ tokio = { version = "*", features = ["rt", "macros"] } tokio-postgres = { version = "0.7.13", features = ["with-uuid-1"] } bb8 = "0.9.0" bb8-postgres = { version = "0.9.0", features = ["with-uuid-1"] } +openssl = { version = "0.10.73", default-features = false, features = ["vendored"] } +native-tls = { version = "0.2.14", default-features = false, features = ["vendored"] } +postgres-native-tls = "0.5.1" diff --git a/Dockerfile-postgres b/Dockerfile-postgres new file mode 100644 index 0000000..1ff2c04 --- /dev/null +++ b/Dockerfile-postgres @@ -0,0 +1,26 @@ +# Versions must be major.minor +# Default versions are as below +ARG RUST_VERSION=1.85 +ARG ALPINE_VERSION=3.20 + +FROM docker.io/rust:${RUST_VERSION}-alpine${ALPINE_VERSION} AS builder +# perl and make are required to build openssl. +RUN apk -U add libc-dev perl make +COPY Cargo.lock Cargo.toml /data/ +COPY core /data/core/ +COPY server /data/server/ +COPY postgres /data/postgres/ +COPY sqlite /data/sqlite/ +RUN cd /data && \ + cargo build -p taskchampion-sync-server --release --no-default-features --features postgres --bin taskchampion-sync-server-postgres + +FROM docker.io/alpine:${ALPINE_VERSION} +COPY --from=builder /data/target/release/taskchampion-sync-server-postgres /bin +RUN apk add --no-cache su-exec && \ + adduser -u 1092 -S -D -H -h /var/lib/taskchampion-sync-server -s /sbin/nologin -G users \ + -g taskchampion taskchampion && \ + install -d -m1755 -o1092 -g1092 "/var/lib/taskchampion-sync-server" +EXPOSE 8080 +COPY entrypoint-postgres.sh /bin/entrypoint.sh +ENTRYPOINT [ "/bin/entrypoint.sh" ] +CMD [ "/bin/taskchampion-sync-server-postgres" ] diff --git a/Dockerfile b/Dockerfile-sqlite similarity index 73% rename from Dockerfile rename to Dockerfile-sqlite index 630deb7..55455aa 100644 --- a/Dockerfile +++ b/Dockerfile-sqlite @@ -1,16 +1,17 @@ # Versions must be major.minor # Default versions are as below -ARG RUST_VERSION=1.78 -ARG ALPINE_VERSION=3.19 +ARG RUST_VERSION=1.85 +ARG ALPINE_VERSION=3.20 FROM docker.io/rust:${RUST_VERSION}-alpine${ALPINE_VERSION} AS builder +RUN apk -U add libc-dev COPY Cargo.lock Cargo.toml /data/ COPY core /data/core/ COPY server /data/server/ +COPY postgres /data/postgres/ COPY sqlite /data/sqlite/ -RUN apk -U add libc-dev && \ - cd /data && \ - cargo build --release +RUN cd /data && \ + cargo build --release --bin taskchampion-sync-server FROM docker.io/alpine:${ALPINE_VERSION} COPY --from=builder /data/target/release/taskchampion-sync-server /bin @@ -20,6 +21,6 @@ RUN apk add --no-cache su-exec && \ install -d -m1755 -o1092 -g1092 "/var/lib/taskchampion-sync-server" EXPOSE 8080 VOLUME /var/lib/taskchampion-sync-server/data -COPY docker-entrypoint.sh /bin -ENTRYPOINT [ "/bin/docker-entrypoint.sh" ] +COPY entrypoint-sqlite.sh /bin/entrypoint.sh +ENTRYPOINT [ "/bin/entrypoint.sh" ] CMD [ "/bin/taskchampion-sync-server" ] diff --git a/README.md b/README.md index b5c7aee..291edc1 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ cargo build --release After build the binary is located in `target/release/taskchampion-sync-server`. -#### Building the Postgres backend +#### Building the Postgres Backend The storage backend is controlled by Cargo features `postres` and `sqlite`. By default, only the `sqlite` feature is enabled. diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 0ab6c68..3ce3908 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -2,4 +2,10 @@ - [Introduction](./introduction.md) - [Usage](./usage.md) + - [Docker Compose](./usage/docker-compose.md) + - [Docker Images](./usage/docker-images.md) + - [Binaries](./usage/binaries.md) - [Integration](./integration.md) + - [Pre-built Images](./integration/pre-built.md) + - [Rust Crates](./integration/crates.md) + - [Sync Protocol Implementation](./integration/protocol-impl.md) diff --git a/docs/src/integration.md b/docs/src/integration.md index bd6d642..ab82f7a 100644 --- a/docs/src/integration.md +++ b/docs/src/integration.md @@ -1,3 +1,16 @@ # Integration -TBD (pending Postgres support) +Taskchampion-sync-server can be integrated into larger applications, such as +web-based hosting services. + +- Most deployments can simply use the pre-built Docker images to implement the +sync protocol, handling other aspects of the application in separate +containers. See [Pre-built Images](./integration/pre-built.md). + +- More complex deployments may wish to modify or extend the operation of the +server. These can use the Rust crates to build precisely the desired +functionality. See [Rust Crates](./integration/crates.md). + +- If desired, an integration may completely re-implement the [sync +protocol](https://gothenburgbitfactory.org/taskchampion/sync.html). See [Sync +Protocol Implementation](./integration/protocol-impl.md). diff --git a/docs/src/integration/crates.md b/docs/src/integration/crates.md new file mode 100644 index 0000000..6147787 --- /dev/null +++ b/docs/src/integration/crates.md @@ -0,0 +1,17 @@ +# Rust Crates + +This project publishes several Rust crates on `crates.io`: + +- [`taskchampion-sync-server-core`](https://docs.rs/taskchampion-sync-server-core) +implements the core of the protocol +- [`taskchampion-sync-server-storage-sqlite`](https://docs.rs/taskchampion-sync-server-storage-sqlite) +implements an SQLite backend for the core +- [`taskchampion-sync-server-storage-postgres`](https://docs.rs/taskchampion-sync-server-storage-postgres) +implements a Postgres backend for the core + +If you are building an integration with, for example, a custom storage system, +it may be helpful to use the `core` crate and provide a custom implementation +of its `Storage` trait. + +We suggest that any generally useful extensions, such as additional storage +backends, be published as open-source packages. diff --git a/docs/src/integration/pre-built.md b/docs/src/integration/pre-built.md new file mode 100644 index 0000000..377a332 --- /dev/null +++ b/docs/src/integration/pre-built.md @@ -0,0 +1,40 @@ +# Pre-built Images + +The pre-built Postgres Docker image described in [Docker +Images](../usage/docker-images.md) may be adequate for a production deployment. +The image is stateless and can be easily scaled horizontally to increase +capacity. + +## Database Schema + +The schema defined in +[`postgres/schema.sql`](https://github.com/GothenburgBitFactory/taskchampion-sync-server/blob/main/postgres/schema.sql) +must be applied to the database before the container will function. + +The schema is stable, and any changes to the schema will be made in a major +version with migration instructions provided. + +An integration may: + +- Add additional tables to the database +- Add additional columns to the `clients` table. If those columns do not have +default values, ensure the server is configured with `CREATE_CLIENTS=false` as +described below. +- Insert rows into the `clients` table, using default values for all columns +except `client_id` and any application-specific columns. +- Delete rows from the `clients` table. Note that this table is configured to +automatically delete all data associated with a client when the client's row is +deleted. + +## Managing Clients + +By default, taskchampion-sync-server creates a new, empty client when it +receives a connection from an unrecognized client ID. Setting +`CREATE_CLIENTS=false` disables this functionality, and is recommended in +production deployments to avoid abuse. + +In this configuration, it is the responsibility of the integration to create +new client rows when desired, using a statement like `INSERT into clients +(client_id) values ($1)` with the new client ID as a parameter. Similarly, +clients may be deleted, along with all stored task data, using a statement like +`DELETE from clients where client_id = $1`. diff --git a/docs/src/integration/protocol-impl.md b/docs/src/integration/protocol-impl.md new file mode 100644 index 0000000..7ffdcc7 --- /dev/null +++ b/docs/src/integration/protocol-impl.md @@ -0,0 +1,10 @@ +# Sync Protocol Implementation + +The [sync protocol](https://gothenburgbitfactory.org/taskchampion/sync.html) is +an open specification, and can be re-implemented from that specification as +desired. This specification is not battle-tested, so refer to +taskchampion-sync-server's implementation to resolve any ambiguities, and +please create pull requests to resolve the ambiguity in the specification. + +We suggest that new implementations be published as open-source packages where +possible. diff --git a/docs/src/introduction.md b/docs/src/introduction.md index ed8055c..b934860 100644 --- a/docs/src/introduction.md +++ b/docs/src/introduction.md @@ -1,11 +1,30 @@ # Introduction -Taskchampion Sync-Server is an implementation of the TaskChampion [sync +Taskchampion-sync-server is an implementation of the TaskChampion [sync protocol][sync-protocol] server. It supports synchronizing Taskwarrior tasks between multiple systems. -[sync-protocol]: https://gothenburgbitfactory.org/taskchampion/sync.html - The project provides both pre-built images for common use-cases (see [usage](./usage.md)) and Rust libraries that can be used to build more sophisticated applications ([integration](./integration.md)). + +It also serves as a reference implementation: where the +[specification][sync-protocol] is ambiguous, this implementation's +interpretation is favored in resolving the ambiguity. Other implementations of +the protocol should interoperate with this implementation. + +## Sync Overview + +The server identifies each user with a client ID. For example, when +syncing Taskwarrior tasks between a desktop computer and a laptop, both systems +would use the same client ID to indicate that they share the same user's task data. + +Task data is encrypted, and the server does not have access to the encryption +secret. The server sees only encrypted data and cannot read or modify tasks in +any way. + +To perform a sync, a replica first downloads and decrypts any changes that have +been sent to the server since its last sync. It then gathers any local changes, +encrypts them, and uploads them to the server. + +[sync-protocol]: https://gothenburgbitfactory.org/taskchampion/sync.html diff --git a/docs/src/usage.md b/docs/src/usage.md index f5456f1..8f7693a 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -1,87 +1,22 @@ # Usage -## Running the Server +This repository is flexible and can be used in a number of ways, to suit your +needs. -The server is a simple binary that serves HTTP requests on a TCP port. The -server does not implement TLS; for public deployments, the recommendation is to -use a reverse proxy such as Nginx, haproxy, or Apache httpd. +- If you only need a place to sync your tasks, using cloud storage may be +cheaper and easier than running taskchampion-sync-server. See +[task-sync(5)](http://taskwarrior.org/docs/man/task-sync.5/) for details on +cloud storage. -### Using Docker-Compose +- If you have a publicly accessible server, such as a VPS, you can use `docker +compose` to run taskchampion-sync-server as pre-built docker images. See +[Docker Compose](./usage/docker-compose.md). -Every release of the server generates a Docker image in -`ghcr.io/gothenburgbitfactory/taskchampion-sync-server`. The tags include -`latest` for the latest release, and both minor and patch versions, e.g., `0.5` -and `0.5.1`. +- If you would like more control, such as to deploy taskchampion-sync-server +within an orchestration environment such as Kubernetes, you can deploy the +docker images directly. See [Docker Images](./usage/docker-images.md). -The -[`docker-compose.yml`](https://raw.githubusercontent.com/GothenburgBitFactory/taskchampion-sync-server/refs/tags/v0.6.1/docker-compose.yml) -file in this repository is sufficient to run taskchampion-sync-server, -including setting up TLS certificates using Lets Encrypt, thanks to -[Caddy](https://caddyserver.com/). - -You will need a server with ports 80 and 443 open to the Internet and with a -fixed, publicly-resolvable hostname. These ports must be available both to your -Taskwarrior clients and to the Lets Encrypt servers. - -On that server, download `docker-compose.yml` from the link above (it is pinned -to the latest release) into the current directory. Then run - -```sh -TASKCHAMPION_SYNC_SERVER_HOSTNAME=taskwarrior.example.com \ -TASKCHAMPION_SYNC_SERVER_CLIENT_ID=your-client-id \ -docker compose up -``` - -The `TASKCHAMPION_SYNC_SERVER_CLIENT_ID` limits the server to the given client -ID; omit it to allow all client IDs. - -It can take a few minutes to obtain the certificate; the caddy container will -log a message "certificate obtained successfully" when this is complete, or -error messages if the process fails. Once this process is complete, configure -your `.taskrc`'s to point to the server: - -```none -sync.server.url=https://taskwarrior.example.com -sync.server.client_id=your-client-id -sync.encryption_secret=your-encryption-secret -``` - -The docker-compose images store data in a docker volume named -`taskchampion-sync-server_data`. This volume contains all of the task data, as -well as the TLS certificate information. It will persist over restarts, in a -typical Docker installation. The docker containers will start automatically on -system startup. See the docker-compose documentation for more information. - -### Running the Binary - -The server is configured with command-line options. See -`taskchampion-sync-server --help` for full details. - -The `--listen` option specifies the interface and port the server listens on. -It must contain an IP-Address or a DNS name and a port number. This option is -mandatory, but can be repeated to specify multiple interfaces or ports. This -value can be specified in environment variable `LISTEN`, as a comma-separated -list of values. - -The `--data-dir` option specifies where the server should store its data. This -value can be specified in the environment variable `DATA_DIR`. - -By default, the server will allow all clients and create them in the database -on first contact. There are two ways to limit the clients the server will -interact with: - -- To limit the accepted client IDs, specify them in the environment variable -`CLIENT_ID`, as a comma-separated list of UUIDs. Client IDs can be specified -with `--allow-client-id`, but this should not be used on shared systems, as -command line arguments are visible to all users on the system. This convenient -option is suitable for personal and small-scale deployments. - -- To disable the automatic creation of clients, use the `--no-create-clients` -flag or the `CREATE_CLIENTS=false` environment variable. You are now -responsible for creating clients in the database manually, so this option is -more suitable for large scale deployments. - -The server only logs errors by default. To add additional logging output, set -environment variable `RUST_LOG` to `info` to get a log message for every -request, or to `debug` to get more verbose debugging output. +- For even more control, or to avoid the overhead of container images, you can +build and run the taskchampion-sync-server binary directly. See +[Binaries](./usage/binaries.md). diff --git a/docs/src/usage/binaries.md b/docs/src/usage/binaries.md new file mode 100644 index 0000000..ec40a93 --- /dev/null +++ b/docs/src/usage/binaries.md @@ -0,0 +1,49 @@ +# Binaries + +Taskchampion-sync-server is a single binary that serves HTTP requests on a TCP +port. The server does not implement TLS; for public deployments, the +recommendation is to use a reverse proxy such as Nginx, haproxy, or Apache +httpd. + +One binary is provided for each storage backend: + +- `taskchampion-sync-server` (SQLite) +- `taskchampion-sync-server-postgres` (Postgres) + +### Running the Binary + +The server is configured with command-line options or environment variables. +See the `--help` output for full details. + +For the SQLite binary, the `--data-dir` option or `DATA_DIR` environment +variable specifies where the server should store its data. For the Postgres +binary, the `--connection` option or `CONNECTION` environment variable +specifies the connection information, in the form of a [LibPQ-style connection +URI](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS). +The remaining options are common to all binaries. + +The `--listen` option specifies the interface and port the server listens on. +It must contain an IP-Address or a DNS name and a port number. This option is +mandatory, but can be repeated to specify multiple interfaces or ports. This +value can be specified in environment variable `LISTEN`, as a comma-separated +list of values. + +By default, the server will allow all clients and create them in the database +on first contact. There are two ways to limit the clients the server will +interact with: + +- To limit the accepted client IDs, specify them in the environment variable +`CLIENT_ID`, as a comma-separated list of UUIDs. Client IDs can be specified +with `--allow-client-id`, but this should not be used on shared systems, as +command line arguments are visible to all users on the system. This convenient +option is suitable for personal and small-scale deployments. + +- To disable the automatic creation of clients, use the `--no-create-clients` +flag or the `CREATE_CLIENTS=false` environment variable. You are now +responsible for creating clients in the database manually, so this option is +more suitable for large scale deployments. See [Integration](../integration.md) +for more information on such deployments. + +The server only logs errors by default. To add additional logging output, set +environment variable `RUST_LOG` to `info` to get a log message for every +request, or to `debug` to get more verbose debugging output. diff --git a/docs/src/usage/docker-compose.md b/docs/src/usage/docker-compose.md new file mode 100644 index 0000000..598f20e --- /dev/null +++ b/docs/src/usage/docker-compose.md @@ -0,0 +1,43 @@ +# Docker Compose + +The +[`docker-compose.yml`](https://raw.githubusercontent.com/GothenburgBitFactory/taskchampion-sync-server/refs/tags/v0.6.1/docker-compose.yml) +file in this repository is sufficient to run taskchampion-sync-server, +including setting up TLS certificates using Lets Encrypt, thanks to +[Caddy](https://caddyserver.com/). This setup uses the SQLite backend, which is +adequate for one or a few clients. + +You will need a server with ports 80 and 443 open to the Internet and with a +fixed, publicly-resolvable hostname. These ports must be available both to your +Taskwarrior clients and to the Lets Encrypt servers. + +On that server, download `docker-compose.yml` from the link above (it is pinned +to the latest release) into the current directory. Then run + +```sh +TASKCHAMPION_SYNC_SERVER_HOSTNAME=taskwarrior.example.com \ +TASKCHAMPION_SYNC_SERVER_CLIENT_ID=your-client-id \ +docker compose up +``` + +The `TASKCHAMPION_SYNC_SERVER_CLIENT_ID` limits the server to the given client +ID; omit it to allow all client IDs. You may specify multiple client IDs +separated by commas. + +It can take a few minutes to obtain the certificate; the caddy container will +log a message "certificate obtained successfully" when this is complete, or +error messages if the process fails. Once this process is complete, configure +your `.taskrc`'s to point to the server: + +```none +sync.server.url=https://taskwarrior.example.com +sync.server.client_id=your-client-id +sync.encryption_secret=your-encryption-secret +``` + +The docker-compose images store data in a docker volume named +`taskchampion-sync-server_data`. This volume contains all of the task data, as +well as the TLS certificate information. It will persist over restarts, in a +typical Docker installation. The docker containers will start automatically +when the Docker dameon starts. See the docker-compose documentation for more +information. diff --git a/docs/src/usage/docker-images.md b/docs/src/usage/docker-images.md new file mode 100644 index 0000000..09a4c78 --- /dev/null +++ b/docs/src/usage/docker-images.md @@ -0,0 +1,57 @@ +# Docker Images + +Every release of the server generates Docker images. One image is produced for +each storage backend: +- `ghcr.io/gothenburgbitfactory/taskchampion-sync-server` (SQLite) +- `ghcr.io/gothenburgbitfactory/taskchampion-sync-server-postgres` (Postgres) + +The image tags include `latest` for the latest release, and both minor and +patch versions, e.g., `0.5` and `0.5.1`. + +## Running the Image + +At startup, each image applies some default values and runs the relevant binary +directly. Configuration is typically by environment variables, all of which are +documented in the `--help` output of the binaries. These include + +- `RUST_LOG` - log level, one of `trace`, `debug`, `info`, `warn` and `error`. +- `DATA_DIR` (SQLite only; default `/var/lib/taskchampion-sync-server/data`) - +directory for the synced data. +- `CONNECTION` (Postgres only) - Postgres connection information, in the form +of a [LibPQ-style connection +URI](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS). +- `LISTEN` (default `0.0.0.0:8080`) - address and port on which to listen for +HTTP requests. +- `CLIENT_ID` - comma-separated list of client IDs that will be allowed, or +empty to allow all clients. +- `CREATE_CLIENTS` (default `true`) - if true, automatically create clients on +first sync. If this is set to false, it is up to you to initialize clients in +the DB. + +### Example + +```shell +docker run -d \ + --name=taskchampion-sync-server \ + -p 8080:8080 \ + -e RUST_LOG=debug \ + -v /data/taskchampion-sync-server:/var/lib/taskchampion-sync-server/data \ + taskchampion-sync-server +``` + +### Image-Specific Setup + +The SQLite image is configured with `VOLUME +/var/lib/taskchampion-sync-server/data`, persisting the task data in an +anonymous Docker volume. It is recommended to put this on a named volume, or +persistent storage in an environment like Kubernetes, so that it is not +accidentally deleted. + +The Postgres image does not automatically create its database schema. See the +[integration section](../integration/pre-built.md) for more detail. This +implementation is tested with Postgres version 17 but should work with any +recent version. + +Note that the Docker images do not implement TLS. The expectation is that +another component, such as a Kubernetes ingress, will terminate the TLS +connection and proxy HTTP traffic to the taskchampion-sync-server container. diff --git a/entrypoint-postgres.sh b/entrypoint-postgres.sh new file mode 100755 index 0000000..b28e9c2 --- /dev/null +++ b/entrypoint-postgres.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -e +echo "starting entrypoint script..." +if [ "$1" = "/bin/taskchampion-sync-server-postgres" ]; then + : ${DATA_DIR:=/var/lib/taskchampion-sync-server/data} + export DATA_DIR + echo "setting up data directory ${DATA_DIR}" + mkdir -p "${DATA_DIR}" + chown -R taskchampion:users "${DATA_DIR}" + chmod -R 700 "${DATA_DIR}" + + : ${LISTEN:=0.0.0.0:8080} + export LISTEN + echo "Listen set to ${LISTEN}" + + if [ -n "${CLIENT_ID}" ]; then + export CLIENT_ID + echo "Limiting to client ID ${CLIENT_ID}" + else + unset CLIENT_ID + fi + + if [ "$(id -u)" = "0" ]; then + echo "Running server as user 'taskchampion'" + exec su-exec taskchampion "$@" + fi +else + eval "${@}" +fi diff --git a/docker-entrypoint.sh b/entrypoint-sqlite.sh similarity index 100% rename from docker-entrypoint.sh rename to entrypoint-sqlite.sh diff --git a/postgres/Cargo.toml b/postgres/Cargo.toml index ba7b177..010ffbf 100644 --- a/postgres/Cargo.toml +++ b/postgres/Cargo.toml @@ -21,8 +21,9 @@ thiserror.workspace = true tokio-postgres.workspace = true tokio.workspace = true uuid.workspace = true -native-tls = { version = "0.2.14", features = ["vendored"] } -postgres-native-tls = "0.5.1" +openssl.workspace = true +native-tls.workspace = true +postgres-native-tls.workspace = true [dev-dependencies] tempfile.workspace = true diff --git a/postgres/src/lib.rs b/postgres/src/lib.rs index c7b7597..08261a2 100644 --- a/postgres/src/lib.rs +++ b/postgres/src/lib.rs @@ -25,7 +25,7 @@ //! `taskchampion-sync-server` to never call this method. //! - Insert rows into the `clients` table, using default values for all columns except //! `client_id` and application-specific columns. -//! - Delete rows from the `clients` table, using `CASCADE` to ensure any associated data +//! - Delete rows from the `clients` table, noting that any associated task data //! is also deleted. use anyhow::Context;