diff --git a/.gitignore b/.gitignore index 77340e7..4b3c13d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ Cargo.lock # MacOS directory conf .DS_Store + +*.cblite2/ diff --git a/Cargo.toml b/Cargo.toml index ffcd1cd..f2910d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,14 @@ tempdir = "*" lazy_static = "1.4.0" regex = "1.10.4" +[dev-dependencies] +serde_json = "1" + +[dev-dependencies.reqwest] +version = "0.12.15" +default-features = false +features = ["blocking", "json"] + [dev-dependencies.cargo-husky] version = "1" default-features = false # Disable features which are enabled by default diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..1fadb2d --- /dev/null +++ b/examples/README.md @@ -0,0 +1,59 @@ +# Running examples with Couchbase Sync Gateway & Server + +Couchbase Lite is often used with replication to a central server, so it can be useful to test the full stack. +The examples in this directory aim at covering these use cases. + +## Setup the Couchbase Sync Gateway & Server + +This process is handled through docker images, with as an entry point the file `docker-conf/docker-compose.yml`. + +The configuration files that might interest you are: +- `docker-conf/couchbase-server-dev/configure-server.sh` -> sets up the cluster, bucket and SG user +- `docker-conf/db-config.json` -> contains the database configuration +- `docker-conf/sync-function.js` -> contains the sync function used by the Sync Gateway + +To start both the Sync Gatewawy and Couchbase Server, move to `docker-conf` through a terminal and use: + +```shell +$ docker-compose up +``` + +It's very long the first time... + +You can then access the Couchbase Server web ui through [http://localhost:8091](http://localhost:8091) (Chrome might not work, Firefox has better support). +Make sure to not have another instance running. + +## Update the config after startup + +You can change a few things through the `curl` command. + +#### Sync function + +Update the file `docker-conf/sync-function.js` and run +```shell +$ curl -XPUT -v "http://localhost:4985/my-db/_config/sync" -H 'Content-Type: application/javascript' --data-binary @docker-conf/sync-function.js +``` + +#### Database config + +Update the file `docker-conf/db-config.json` and run + +```shell +$ curl -XPUT -v "http://localhost:4985/my-db/" -H 'Content-Type: application/json' --data-binary @docker-conf/db-config.json +``` + +## Running an example + +As of now, there is only one example: `sgw_1_cblite`. + +It can be run with the following command: +```shell +$ cargo run --features=enterprise --example sgw_1_cblite +``` + +What it does: +- Create a cblite database `test1` +- Add a user `great_name` to the Sync Gateway +- Retrieve a session token for the user `great_name` from the Sync Gateway +- Start a continuous push & pull replicator +- Create a document, then wait for 5 seconds for the replication to finish diff --git a/examples/docker-conf/couchbase-server-dev/Dockerfile b/examples/docker-conf/couchbase-server-dev/Dockerfile new file mode 100644 index 0000000..209bf4c --- /dev/null +++ b/examples/docker-conf/couchbase-server-dev/Dockerfile @@ -0,0 +1,5 @@ +# hadolint ignore=DL3007 +FROM couchbase/server:enterprise +COPY configure-server.sh /configure-server.sh + +ENTRYPOINT ["/configure-server.sh"] diff --git a/examples/docker-conf/couchbase-server-dev/configure-server.sh b/examples/docker-conf/couchbase-server-dev/configure-server.sh new file mode 100755 index 0000000..a951963 --- /dev/null +++ b/examples/docker-conf/couchbase-server-dev/configure-server.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +export COUCHBASE_ADMINISTRATOR_USERNAME="cb_admin" +export COUCHBASE_ADMINISTRATOR_PASSWORD="cb_admin_pwd" + +export COUCHBASE_BUCKET="my-bucket" + +export COUCHBASE_SG_USERNAME="syncgw" +export COUCHBASE_SG_PASSWORD="syncgw-pwd" +export COUCHBASE_SG_NAME="sg-service-user" + +function retry() { + for i in $(seq 1 10); do + $1 + if [[ $? == 0 ]]; then + return 0 + fi + sleep 1 + done + return 1 +} + +function clusterInit() { + couchbase-cli cluster-init \ + -c 127.0.0.1:8091 \ + --cluster-username $COUCHBASE_ADMINISTRATOR_USERNAME \ + --cluster-password $COUCHBASE_ADMINISTRATOR_PASSWORD \ + --services data,index,query \ + --cluster-ramsize 256 \ + --cluster-index-ramsize 256 \ + --index-storage-setting default + if [[ $? != 0 ]]; then + return 1 + fi +} + +function bucketCreate() { + couchbase-cli bucket-create \ + -c 127.0.0.1:8091 \ + --username $COUCHBASE_ADMINISTRATOR_USERNAME \ + --password $COUCHBASE_ADMINISTRATOR_PASSWORD \ + --bucket-type=couchbase \ + --bucket-ramsize=100 \ + --bucket-replica=0 \ + --bucket $COUCHBASE_BUCKET \ + --wait + if [[ $? != 0 ]]; then + return 1 + fi +} + +function userSgCreate() { + couchbase-cli user-manage \ + -c 127.0.0.1:8091 \ + --username $COUCHBASE_ADMINISTRATOR_USERNAME \ + --password $COUCHBASE_ADMINISTRATOR_PASSWORD \ + --set \ + --rbac-username $COUCHBASE_SG_USERNAME \ + --rbac-password $COUCHBASE_SG_PASSWORD \ + --rbac-name $COUCHBASE_SG_NAME \ + --roles bucket_full_access[*],bucket_admin[*] \ + --auth-domain local + if [[ $? != 0 ]]; then + return 1 + fi +} + +function main() { + /entrypoint.sh couchbase-server & + if [[ $? != 0 ]]; then + echo "Couchbase startup failed. Exiting." >&2 + exit 1 + fi + + # wait for service to come up + until $(curl --output /dev/null --silent --head --fail http://localhost:8091); do + sleep 5 + done + + if couchbase-cli server-list -c 127.0.0.1:8091 --username $COUCHBASE_ADMINISTRATOR_USERNAME --password $COUCHBASE_ADMINISTRATOR_PASSWORD ; then + echo "Couchbase already initialized, skipping initialization" + else + echo "Couchbase is not configured." + echo + + echo "Initializing the cluster...." + retry clusterInit + if [[ $? != 0 ]]; then + echo "Cluster init failed. Exiting." >&2 + exit 1 + fi + echo "Initializing the cluster [OK]" + echo + + echo "Creating the bucket...." + retry bucketCreate + if [[ $? != 0 ]]; then + echo "Bucket create failed. Exiting." >&2 + exit 1 + fi + echo "Creating the bucket [OK]" + echo + + echo "Creating Sync Gateway user...." + retry userSgCreate + if [[ $? != 0 ]]; then + echo "User create failed. Exiting." >&2 + exit 1 + fi + echo "Creating Sync Gateway user [OK]" + echo + + sleep 10 + + fi + + wait +} + +main + diff --git a/examples/docker-conf/db-config.json b/examples/docker-conf/db-config.json new file mode 100644 index 0000000..3f589c7 --- /dev/null +++ b/examples/docker-conf/db-config.json @@ -0,0 +1,8 @@ +{ + "import_docs": true, + "enable_shared_bucket_access": true, + "bucket": "my-bucket", + "num_index_replicas": 0, + "revs_limit": 20, + "allow_conflicts": false +} \ No newline at end of file diff --git a/examples/docker-conf/docker-compose.yml b/examples/docker-conf/docker-compose.yml new file mode 100644 index 0000000..202a386 --- /dev/null +++ b/examples/docker-conf/docker-compose.yml @@ -0,0 +1,47 @@ +services: + cblr-couchbase-server: + ports: + - "8091:8091" # REST(admin), Web console + - "8093:8093" # Query service REST/HTTP traffic + - "11207:11207" # memcached port (TLS) + - "11210:11210" # memcached port + build: + context: ${PWD}/couchbase-server-dev + deploy: + resources: + limits: + memory: 2048M + restart: on-failure + cblr-sync-gateway: + image: couchbase/sync-gateway:enterprise + ports: + - "4984:4984" + - "4985:4985" + deploy: + resources: + limits: + memory: 512M + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:4985"] + interval: 30s + timeout: 10s + retries: 5 + volumes: + - ${PWD}/syncgw-config.json:/etc/sync_gateway/config.json:ro + - ${PWD}/wait-for-couchbase-server.sh:/wait-for-couchbase-server.sh + depends_on: + - cblr-couchbase-server + entrypoint: ["/wait-for-couchbase-server.sh"] + restart: on-failure + cblr-sync-gateway-setup: + image: alpine:3.21.3@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c + depends_on: + cblr-sync-gateway: + condition: service_healthy + volumes: + - ${PWD}/sync-function.js:/sync-function.js + - ${PWD}/db-config.json:/db-config.json + - ${PWD}/update.sh:/update.sh + links: + - "cblr-sync-gateway:sg" + entrypoint: /update.sh diff --git a/examples/docker-conf/sync-function.js b/examples/docker-conf/sync-function.js new file mode 100644 index 0000000..ec263c4 --- /dev/null +++ b/examples/docker-conf/sync-function.js @@ -0,0 +1,19 @@ +function sync(doc, oldDoc, meta) { + console.log("=== New document revision ==="); + console.log("New doc:"); + console.log(doc); + console.log("Old doc:"); + console.log(oldDoc); + console.log("Metadata:"); + console.log(meta); + + if(doc.channels) { + channel(doc.channels); + } + if(doc.expiry) { + // Format: "2022-06-23T05:00:00+01:00" + expiry(doc.expiry); + } + + console.log("=== Document processed ==="); +} diff --git a/examples/docker-conf/syncgw-config.json b/examples/docker-conf/syncgw-config.json new file mode 100644 index 0000000..8451747 --- /dev/null +++ b/examples/docker-conf/syncgw-config.json @@ -0,0 +1,43 @@ +{ + "bootstrap": { + "server": "couchbase://cblr-couchbase-server", + "username": "syncgw", + "password": "syncgw-pwd" + }, + "api": { + "public_interface": ":4984", + "admin_interface": ":4985", + "admin_interface_authentication": false, + "https": {} + }, + "logging": { + "console": { + "rotation": {}, + "log_level": "debug", + "log_keys": [ + "*" + ] + }, + "error": { + "rotation": {} + }, + "warn": { + "rotation": {} + }, + "info": { + "rotation": {} + }, + "debug": { + "rotation": {} + }, + "trace": { + "rotation": {} + }, + "stats": { + "rotation": {} + } + }, + "auth": {}, + "replicator": {}, + "unsupported": {} +} diff --git a/examples/docker-conf/update.sh b/examples/docker-conf/update.sh new file mode 100755 index 0000000..34de32f --- /dev/null +++ b/examples/docker-conf/update.sh @@ -0,0 +1,23 @@ +#!/bin/sh -x + +apk add curl + +export DB_NAME="my-db" + +echo 'START SG Update' +echo + +#Can I bypass the config if it's already done? This command does not work: +#curl -I "http://localhost:4985/my-db/" -w "%{http_code}"` --output >(cat >&3) + +# Setting up database API: https://docs.couchbase.com/sync-gateway/current/rest_api_admin.html#tag/Database-Management/operation/put_db- +echo 'Setting up the database...' +curl -XPUT -v "http://sg:4985/${DB_NAME}/" -H 'Content-Type: application/json' --data-binary @db-config.json +echo + +# Updating sync function API: https://docs.couchbase.com/sync-gateway/current/rest_api_admin.html#tag/Database-Configuration/operation/put_keyspace-_config-sync +# Sync function doc: https://docs.couchbase.com/sync-gateway/current/sync-function.html +echo 'Updating sync function...' +curl -XPUT -v "http://sg:4985/${DB_NAME}/_config/sync" -H 'Content-Type: application/javascript' --data-binary @sync-function.js + +echo 'END SG Update' diff --git a/examples/docker-conf/wait-for-couchbase-server.sh b/examples/docker-conf/wait-for-couchbase-server.sh new file mode 100755 index 0000000..76590da --- /dev/null +++ b/examples/docker-conf/wait-for-couchbase-server.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +echo "Launch SG" +SG_CONFIG_PATH=/etc/sync_gateway/config.json + +COUCHBASE_SERVER_URL="http://cblr-couchbase-server:8091" +SG_AUTH_ARG="syncgw:syncgw-pwd" + +while ! { curl -X GET -u $SG_AUTH_ARG $COUCHBASE_SERVER_URL/pools/default/buckets -H "accept: application/json" -s | grep -q '"status":"healthy"'; }; do + echo "Wait 🕑" + sleep 1 +done +echo "CB ready, starting SG" + +sleep 5 + +/entrypoint.sh -bootstrap.use_tls_server=false $SG_CONFIG_PATH diff --git a/examples/sgw_1_cblite.rs b/examples/sgw_1_cblite.rs new file mode 100644 index 0000000..9498cef --- /dev/null +++ b/examples/sgw_1_cblite.rs @@ -0,0 +1,101 @@ +use std::path::Path; + +use couchbase_lite::*; + +pub const SYNC_GW_URL_ADMIN: &str = "http://localhost:4985/my-db"; +pub const SYNC_GW_URL: &str = "ws://localhost:4984/my-db"; + +fn main() { + let mut db = Database::open( + "test1", + Some(DatabaseConfiguration { + directory: Path::new("./"), + #[cfg(feature = "enterprise")] + encryption_key: None, + }), + ) + .unwrap(); + + add_or_update_user("great_name", vec!["channel1".into()]); + let session_token = Some(get_session("great_name")).unwrap(); + print!("Sync gateway session token: {session_token}"); + + let repl_conf = ReplicatorConfiguration { + database: Some(db.clone()), + endpoint: Endpoint::new_with_url(SYNC_GW_URL).unwrap(), + replicator_type: ReplicatorType::PushAndPull, + continuous: true, + disable_auto_purge: true, // false if we want auto purge when the user loses access to a document + max_attempts: 3, + max_attempt_wait_time: 1, + heartbeat: 60, + authenticator: None, + proxy: None, + headers: vec![( + "Cookie".to_string(), + format!("SyncGatewaySession={session_token}"), + )] + .into_iter() + .collect(), + pinned_server_certificate: None, + trusted_root_certificates: None, + channels: MutableArray::default(), + document_ids: MutableArray::default(), + collections: None, + accept_parent_domain_cookies: false, + }; + let repl_context = ReplicationConfigurationContext::default(); + let mut repl = Replicator::new(repl_conf, Box::new(repl_context)).unwrap(); + + repl.start(false); + + std::thread::sleep(std::time::Duration::from_secs(3)); + + let mut doc = Document::new_with_id("id1"); + doc.set_properties_as_json( + &serde_json::json!({ + "name": "allo2" + }) + .to_string(), + ) + .unwrap(); + db.save_document(&mut doc).unwrap(); + + assert!(db.get_document("id1").is_ok()); + println!("Doc content: {}", doc.properties_as_json()); + + std::thread::sleep(std::time::Duration::from_secs(3)); + + repl.stop(None); + + // Create new user session: https://docs.couchbase.com/sync-gateway/current/rest_api_admin.html#tag/Session/operation/post_db-_session +} + +fn add_or_update_user(name: &str, channels: Vec) { + let url_admin_sg = format!("{SYNC_GW_URL_ADMIN}/_user/"); + let user_to_post = serde_json::json!({ + "name": name, + "password": "very_secure", + "admin_channels": channels + }); + let result = reqwest::blocking::Client::new() + .post(url_admin_sg) + .json(&user_to_post) + .send(); + println!("{result:?}"); +} + +fn get_session(name: &str) -> String { + let url_admin_sg = format!("{SYNC_GW_URL_ADMIN}/_session"); + let to_post = serde_json::json!({ + "name": name, + }); + let result: serde_json::Value = reqwest::blocking::Client::new() + .post(url_admin_sg) + .json(&to_post) + .send() + .unwrap() + .json() + .unwrap(); + result["session_id"].as_str().unwrap().to_string() +}