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
34 changes: 34 additions & 0 deletions .github/workflows/cmo-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,37 @@ jobs:
test "$masters" -eq 4
kubectl exec cmo-cmo-data-0 -- valkey-cli -h 127.0.0.1 -c --user admin -a "$pass" --no-auth-warning \
GET cmo:kind | grep -qx before

site:
name: Site, integration, and browser
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Unit tests for the site and the management API
run: |
python3 tools/mgmt/test_cmo_mgmt.py
python3 tools/mgmt/test_cmo_browse.py
python3 tools/flowview/test_flow_feed.py
python3 site/test_site.py
python3 integration/test_integration.py

- name: Install Playwright
working-directory: site/e2e
run: |
npm ci
npx playwright install --with-deps chromium

- name: Browser tests
working-directory: site/e2e
run: npx playwright test

- name: Upload screenshots
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: site-screenshots
path: site/e2e/screenshots/*.png
if-no-files-found: error
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,7 @@ deploy/.env
deploy/.generated/
deploy/results/
dist/
site/e2e/node_modules/
site/e2e/test-results/
site/e2e/playwright-report/
site/e2e/screenshots/
46 changes: 46 additions & 0 deletions INTEGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Adopt Cache Me Outside

The integration directory is the whole package: a Python client, a browser/Node client, a loopback sidecar, and an updater. Version `1.0.0` is recorded in `integration/manifest.json`. Copy that directory into a GoDesk app, or install a `cmo-integration-vX.Y.Z` release. No token or password belongs in the repository.

## Python

```python
import os
from cache_me_outside import Client

client = Client(os.environ["CMO_API"], os.environ["CMO_ADMIN_TOKEN"])
print(client.list_databases())
```

## Browser or Node

```html
<script src="integration/cache-me-outside.js"></script>
<script>
const cache = CacheMeOutside.attach({
api: location.origin,
token: sessionToken
});
const rows = await cache.listDatabases();
</script>
```

`sessionToken` comes from your own sign-in. The script does not contain one.

## Sidecar

```bash
CMO_API=http://127.0.0.1:8088 CMO_ADMIN_TOKEN="$CMO_ADMIN_TOKEN" \
python3 integration/sidecar.py
```

The app then calls `GET http://127.0.0.1:8099/health`. The sidecar binds to loopback only. Cache get and set also need `CMO_URL` and `CMO_PREFIX`; a key outside that prefix is refused before a connection opens.

## Updates

```bash
python3 integration/updater.py --check
python3 integration/updater.py --apply
```

`--apply` reads GitHub releases named `cmo-integration-vX.Y.Z`, downloads `cache-me-outside-integration.tar.gz` and `SHA256SUMS`, and writes files only when the checksum matches. It refuses other hosts, symlinks, and paths that leave the integration directory. When no release is published yet, `--check` reports that and leaves the files alone.
2 changes: 2 additions & 0 deletions docs/cache-me-outside/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Valkey and the Redis code it inherited stay under the BSD 3-Clause License. See
- [Warm restart](warm-restart.md)
- [DRAM + flash hybrid storage](hybrid-storage.md)
- [TTL-grouped storage](ttl-segments.md)
- [Management API and site](management-api.md)
- [Adopting the cache from another GoDesk app](../../INTEGRATION.md)

## What this fork changes

Expand Down
13 changes: 10 additions & 3 deletions docs/cache-me-outside/management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,15 @@ Not done yet:
- **Terraform provider:** the plan also lists one. It needs a separate Go module that calls this API; that is a follow-up.
- **Live test:** the API has not been run against a live server. The tests use a fake ACL backend.

## Web console (plan item 61)
`GET /v1/databases/{name}/keys` is the read-only browser. It scans and reads only keys under `<name>:` and refuses a key outside that prefix. A limit of 0 means unlimited.

Open `/` on the management API to use the console. Paste the admin token, give the new database a name, and click **Create database**. The page then shows the connection URL, a `valkey-cli` command, and the key prefix the database must use. The password is shown once. Each database also has a **New password** button.
## Site

The page keeps the token in memory only; nothing is saved in the browser. It sends a strict CSP header and refuses to be framed. It inserts text with textContent only, never innerHTML.
`python3 site/server.py --demo` serves the site on `127.0.0.1` with an in-memory database, a sample key, and the agent. It will not bind a public address. The real process is the same pages on `cmo_mgmt.py` or `site/server.py --config`, with `CMO_ADMIN_TOKEN` supplied in the environment.

- `/` is the home page.
- `/console` creates, limits, rotates, browses, and deletes databases. The token stays in memory. The password is shown once in a field, not in a link.
- `/flow` is the live data-flow picture. Demo rates are labeled as demo traffic.
- `/integrate` shows the adoption snippets from [INTEGRATION.md](../../INTEGRATION.md).

The agent runs list, create, delete, rotate, limit, browse, explain, and open-page requests without a model. Free-form text uses `OLLAMA_BASE_URL` only. Pages are scripts and stylesheets, so the CSP does not use `unsafe-inline`. The pages refuse to be framed and insert text with textContent only.
43 changes: 43 additions & 0 deletions integration/cache-me-outside.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* Cache Me Outside integration, version 1.0.0. No secrets live in this file. */
(function (root, factory) {
var api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
else root.CacheMeOutside = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
var VERSION = "1.0.0";

function attach(opts) {
if (!opts || !opts.api || !opts.token) throw new Error("api and token are required");
var api = String(opts.api).replace(/\/$/, "");
if (!/^https?:\/\//.test(api)) throw new Error("api must be http or https");
var token = String(opts.token);

function call(method, path, body) {
return fetch(api + path, {
method: method,
headers: {"Authorization": "Bearer " + token, "Content-Type": "application/json"},
body: body ? JSON.stringify(body) : undefined
}).then(function (response) {
return response.json().then(function (payload) {
if (!response.ok) throw new Error(payload.error || String(response.status));
return payload;
});
});
}

return {
version: VERSION,
listDatabases: function () { return call("GET", "/v1/databases").then(function (b) { return b.databases; }); },
createDatabase: function (name, limits) {
return call("POST", "/v1/databases", Object.assign({name: name}, limits || {}));
},
rotatePassword: function (name) { return call("POST", "/v1/databases/" + name + "/rotate-password", {}); },
deleteDatabase: function (name) { return call("DELETE", "/v1/databases/" + name); },
scanKeys: function (name, match) {
return call("GET", "/v1/databases/" + encodeURIComponent(name) + "/keys?match=" + encodeURIComponent(match || "*"));
}
};
}

return {attach: attach, version: VERSION};
});
62 changes: 62 additions & 0 deletions integration/cache_me_outside.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Small client for the Cache Me Outside management API. Standard library only."""
import json
import os
import urllib.error
import urllib.request

_MANIFEST = os.path.join(os.path.dirname(os.path.abspath(__file__)), "manifest.json")


def version():
with open(_MANIFEST) as handle:
return json.load(handle)["version"]


class Client:
def __init__(self, api, token, timeout=15):
if not api or not str(api).startswith(("http://", "https://")):
raise ValueError("api must be an http or https URL")
if not token or not isinstance(token, str):
raise ValueError("token is required")
self.api = api.rstrip("/")
self.token = token
self.timeout = timeout

def _call(self, method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(
self.api + path, data=data, method=method,
headers={"Authorization": "Bearer " + self.token, "Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=self.timeout) as response:
return json.load(response)
except urllib.error.HTTPError as exc:
payload = json.load(exc)
raise RuntimeError(payload.get("error") or str(exc.code)) from None

def list_databases(self):
return self._call("GET", "/v1/databases")["databases"]

def create_database(self, name, **limits):
body = {"name": name}
body.update(limits)
return self._call("POST", "/v1/databases", body)

def get_database(self, name):
return self._call("GET", "/v1/databases/" + name)

def update_limits(self, name, **limits):
return self._call("PATCH", "/v1/databases/" + name, limits)

def rotate_password(self, name):
return self._call("POST", "/v1/databases/%s/rotate-password" % name, {})

def delete_database(self, name):
return self._call("DELETE", "/v1/databases/" + name)

def scan_keys(self, name, match="*", cursor="0"):
return self._call("GET", "/v1/databases/%s/keys?cursor=%s&match=%s" % (name, cursor, match))

def show_key(self, name, key):
from urllib.parse import quote
return self._call("GET", "/v1/databases/%s/keys?key=%s" % (name, quote(key, safe="")))
8 changes: 8 additions & 0 deletions integration/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "cache-me-outside",
"version": "1.0.0",
"api": "v1",
"repository": "https://github.com/GoDeskio/Cache-Me-Outside",
"release_prefix": "cmo-integration-v",
"asset": "cache-me-outside-integration.tar.gz"
}
Loading
Loading