Skip to content

Commit 4c2b3f5

Browse files
authored
feat: add HaRP support (#247)
resolves #230 todo: adjust NC admin docs --------- Signed-off-by: Anupam Kumar <kyteinsky@gmail.com>
1 parent dc76152 commit 4c2b3f5

8 files changed

Lines changed: 138 additions & 31 deletions

File tree

Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ ADD dockerfile_scripts/install_py11.sh dockerfile_scripts/install_py11.sh
2525
RUN ./dockerfile_scripts/install_py11.sh
2626
ADD dockerfile_scripts/pgsql dockerfile_scripts/pgsql
2727
RUN ./dockerfile_scripts/pgsql/install.sh
28+
ADD dockerfile_scripts/install_frpc.sh dockerfile_scripts/install_frpc.sh
29+
RUN ./dockerfile_scripts/install_frpc.sh
2830
RUN apt-get autoclean
2931
ADD dockerfile_scripts/entrypoint.sh dockerfile_scripts/entrypoint.sh
3032

@@ -48,6 +50,7 @@ COPY config.?pu.yaml .
4850
COPY logger_config.yaml .
4951
COPY logger_config_em.yaml .
5052
COPY hwdetect.sh .
53+
COPY harp_connect.sh .
5154
COPY supervisord.conf /etc/supervisor/supervisord.conf
5255

5356
ENTRYPOINT ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
> An end-to-end example for dev setups on how to build and register the backend manually (with CUDA) is at the end of this readme
1515
>
1616
> See the [NC Admin docs](https://docs.nextcloud.com/server/latest/admin_manual/ai/app_context_chat.html) for requirements and known limitations.
17-
>
18-
> HaRP is not supported in this app yet, please use Docker Socket Proxy (DSP) as the Deploy Daemon in AppAPI.
1917
2018
## Install
2119

appinfo/info.xml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ Install the given apps for Context Chat to work as desired **in the given order*
1919
- Text2Text Task Processing Provider like [llm2 from the External Apps page](https://apps.nextcloud.com/apps/llm2) or [integration_openai from the Apps page](https://apps.nextcloud.com/apps/integration_openai)
2020
2121
Setup background job workers as described here: https://docs.nextcloud.com/server/latest/admin_manual/ai/overview.html#improve-ai-task-pickup-speed
22-
23-
HaRP is not supported in this app yet, please use Docker Socket Proxy (DSP) as the Deploy Daemon in AppAPI.
2422
]]></description>
2523
<version>5.1.0</version>
2624
<licence>agpl</licence>

dockerfile_scripts/install_frpc.sh

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/bash
2+
#
3+
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
4+
# SPDX-License-Identifier: AGPL-3.0-or-later
5+
#
6+
7+
# Download and install FRP client
8+
set -ex; \
9+
ARCH=$(uname -m); \
10+
if [ "$ARCH" = "aarch64" ]; then \
11+
FRP_URL="https://raw.githubusercontent.com/nextcloud/HaRP/dadcb7cfeb7a6d058ca2acb5622807269239f369/exapps_dev/frp_0.61.1_linux_arm64.tar.gz"; \
12+
else \
13+
FRP_URL="https://raw.githubusercontent.com/nextcloud/HaRP/dadcb7cfeb7a6d058ca2acb5622807269239f369/exapps_dev/frp_0.61.1_linux_amd64.tar.gz"; \
14+
fi; \
15+
echo "Downloading FRP client from $FRP_URL"; \
16+
curl -L "$FRP_URL" -o /tmp/frp.tar.gz; \
17+
tar -C /tmp -xzf /tmp/frp.tar.gz; \
18+
mv /tmp/frp_0.61.1_linux_* /tmp/frp; \
19+
cp /tmp/frp/frpc /usr/local/bin/frpc; \
20+
chmod +x /usr/local/bin/frpc; \
21+
rm -rf /tmp/frp /tmp/frp.tar.gz

harp_connect.sh

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/bin/bash
2+
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
set -e
6+
7+
# Only create a config file if HP_SHARED_KEY is set.
8+
if [ -n "$HP_SHARED_KEY" ]; then
9+
echo "HP_SHARED_KEY is set, creating /frpc.toml configuration file..."
10+
if [ -d "/certs/frp" ]; then
11+
echo "Found /certs/frp directory. Creating configuration with TLS certificates."
12+
cat <<EOF > /frpc.toml
13+
serverAddr = "$HP_FRP_ADDRESS"
14+
serverPort = $HP_FRP_PORT
15+
loginFailExit = false
16+
17+
transport.tls.enable = true
18+
transport.tls.certFile = "/certs/frp/client.crt"
19+
transport.tls.keyFile = "/certs/frp/client.key"
20+
transport.tls.trustedCaFile = "/certs/frp/ca.crt"
21+
transport.tls.serverName = "harp.nc"
22+
23+
metadatas.token = "$HP_SHARED_KEY"
24+
25+
[[proxies]]
26+
remotePort = $APP_PORT
27+
type = "tcp"
28+
name = "$APP_ID"
29+
[proxies.plugin]
30+
type = "unix_domain_socket"
31+
unixPath = "/tmp/exapp.sock"
32+
EOF
33+
else
34+
echo "Directory /certs/frp not found. Creating configuration without TLS certificates."
35+
cat <<EOF > /frpc.toml
36+
serverAddr = "$HP_FRP_ADDRESS"
37+
serverPort = $HP_FRP_PORT
38+
loginFailExit = false
39+
40+
transport.tls.enable = false
41+
42+
metadatas.token = "$HP_SHARED_KEY"
43+
44+
[[proxies]]
45+
remotePort = $APP_PORT
46+
type = "tcp"
47+
name = "$APP_ID"
48+
[proxies.plugin]
49+
type = "unix_domain_socket"
50+
unixPath = "/tmp/exapp.sock"
51+
EOF
52+
fi
53+
else
54+
echo "Skipping HaRP configuration as HP_SHARED_KEY is not set."
55+
fi
56+
57+
# If we have a configuration file and the shared key is present, start the FRP client
58+
if [ -f /frpc.toml ] && [ -n "$HP_SHARED_KEY" ]; then
59+
echo "Starting frpc..."
60+
/usr/local/bin/frpc -c /frpc.toml
61+
fi

main.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
from os import getenv
88

99
import uvicorn
10+
from nc_py_api.ex_app import run_app
1011

1112
from context_chat_backend.types import TConfig # isort: skip
1213
from context_chat_backend.controller import app # isort: skip
13-
from context_chat_backend.utils import to_int # isort: skip
1414
from context_chat_backend.logger import get_logging_config, setup_logging # isort: skip
1515

1616
LOGGER_CONFIG_NAME = 'logger_config.yaml'
@@ -56,10 +56,8 @@ def _setup_log_levels(debug: bool):
5656
uv_log_config['loggers']['uvicorn']['handlers'].append('file_json')
5757
uv_log_config['loggers']['uvicorn.access']['handlers'].append('file_json')
5858

59-
uvicorn.run(
60-
app=app,
61-
host=getenv('APP_HOST', '127.0.0.1'),
62-
port=to_int(getenv('APP_PORT'), 9000),
59+
run_app(
60+
uvicorn_app=app,
6361
http='h11',
6462
interface='asgi3',
6563
log_config=uv_log_config,

main_em.py

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,25 @@
2323
MAX_TRIES = 180 # 30 minutes max
2424

2525

26+
def _get_main_app_httpx_client_host() -> tuple[httpx.Client, str]:
27+
"""Get an HTTPX client to connect to the main app, depending on the deployment type.
28+
29+
The second return value is the base URL to use for the main app.
30+
Returns
31+
-------
32+
httpx.Client: The HTTPX client.
33+
str: The base URL to use for the main app.
34+
"""
35+
_headers = {}
36+
sign_request(_headers)
37+
if os.getenv('HP_SHARED_KEY'):
38+
transport = httpx.HTTPTransport(uds=os.getenv('HP_EXAPP_SOCK', '/tmp/exapp.sock')) # noqa: S108
39+
return httpx.Client(transport=transport, headers=_headers), 'main_app'
40+
41+
connect_host = 'localhost' if os.environ['APP_HOST'] in ('0.0.0.0', '::') else os.environ['APP_HOST'] # noqa: S104
42+
return httpx.Client(headers=_headers), f'{connect_host}:{os.environ["APP_PORT"]}'
43+
44+
2645
if __name__ == '__main__':
2746
# intial buffer
2847
sleep(STARTUP_CHECK_SEC)
@@ -47,30 +66,28 @@
4766
_max_tries = MAX_TRIES
4867
_enabled = False
4968
_last_err = None
50-
_headers = {}
51-
sign_request(_headers)
69+
client, base_url = _get_main_app_httpx_client_host()
5270
# wait for the main process to be ready, check the /enabled endpoint
5371
while _max_tries > 0:
54-
with httpx.Client() as client:
55-
try:
56-
ret = client.get(f'http://{os.environ["APP_HOST"]}:{os.environ["APP_PORT"]}/enabled', headers=_headers)
57-
ret.raise_for_status()
58-
59-
if not ret.json().get('enabled', False):
60-
raise RuntimeError('Main app is not enabled, sleeping for a while...')
61-
except (httpx.RequestError, RuntimeError) as e:
62-
print(
63-
f'{MAX_TRIES-_max_tries+1}/{MAX_TRIES}:'
64-
f' [Embedding server] Waiting for the main app to be enabled/ready: {e}',
65-
flush=True,
66-
)
67-
_last_err = e
68-
sleep(STARTUP_CHECK_SEC)
69-
_max_tries -= 1
70-
continue
71-
72-
_enabled = True
73-
break
72+
try:
73+
ret = client.get(f'http://{base_url}/enabled')
74+
ret.raise_for_status()
75+
76+
if not ret.json().get('enabled', False):
77+
raise RuntimeError('Main app is not enabled, sleeping for a while...')
78+
except (httpx.RequestError, RuntimeError) as e:
79+
print(
80+
f'{MAX_TRIES-_max_tries+1}/{MAX_TRIES}:'
81+
f' [Embedding server] Waiting for the main app to be enabled/ready: {e}',
82+
flush=True,
83+
)
84+
_last_err = e
85+
sleep(STARTUP_CHECK_SEC)
86+
_max_tries -= 1
87+
continue
88+
89+
_enabled = True
90+
break
7491

7592
if not _enabled:
7693
logger.error(

supervisord.conf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,14 @@ autorestart=unexpected
3535
startsecs=120
3636
directory=/app
3737
command=python3 -u /app/main_em.py
38+
39+
[program:frpc]
40+
stdout_logfile=/dev/stdout
41+
stdout_logfile_maxbytes=0
42+
stderr_logfile=/dev/stderr
43+
stderr_logfile_maxbytes=0
44+
autostart=true
45+
autorestart=unexpected
46+
startsecs=0
47+
directory=/app
48+
command=/app/harp_connect.sh

0 commit comments

Comments
 (0)