From f728c992f642b78e492b658e4eff945fd1278dc8 Mon Sep 17 00:00:00 2001 From: xiaojunxiang Date: Thu, 9 Jul 2026 14:16:56 +0800 Subject: [PATCH] feat(cubeproxy): add plaintext gRPC ingress on port 9090 Expose a dedicated HTTP/2 listener so clients can dial the proxy IP directly and route via :authority "-" without DNS. Wire the listener through one-click deploy (CUBE_PROXY_GRPC_PORT), systemd postcheck, release bundle templating, and TKE CLB exposure. Document the access mode alongside existing Host and path-based routing. Add examples/grpc-ingress for native gRPC client usage. Rebased on master after #705 (cube-lifecycle-manager). Addresses review feedback from the closed #679: CLB security group for 9090, grpc_buffering off, client IP forwarding, and specify-protocol in internal mode. Signed-off-by: xiaojunxiang --- CubeProxy/Dockerfile | 2 +- CubeProxy/nginx.conf | 56 ++++++++++++++ deploy/one-click/README.md | 1 + deploy/one-click/README_zh.md | 1 + deploy/one-click/build-release-bundle.sh | 7 +- deploy/one-click/env.example | 1 + .../scripts/one-click/up-cube-proxy.sh | 26 +++++-- .../scripts/systemd/cube-proxy-postcheck.sh | 3 + .../terraform/tencentcloud/create.sh | 4 +- .../one-click/terraform/tencentcloud/main.tf | 8 ++ .../terraform/tencentcloud/tke-addons.tf | 13 +++- .../tests/test_runtime_file_safety.sh | 73 +++++++++++++++++- docs/guide/https-and-domain.md | 26 +++++++ docs/guide/network-hardening.md | 3 +- docs/guide/restrict-public-access.md | 4 + docs/guide/self-build-deploy.md | 1 + docs/guide/service-management.md | 4 +- docs/zh/guide/https-and-domain.md | 25 +++++++ docs/zh/guide/network-hardening.md | 3 +- docs/zh/guide/restrict-public-access.md | 3 + docs/zh/guide/self-build-deploy.md | 1 + docs/zh/guide/service-management.md | 4 +- examples/grpc-ingress/.env.example | 10 +++ examples/grpc-ingress/README.md | 74 +++++++++++++++++++ examples/grpc-ingress/README_zh.md | 73 ++++++++++++++++++ examples/grpc-ingress/env_utils.py | 22 ++++++ examples/grpc-ingress/grpc_plaintext.py | 66 +++++++++++++++++ examples/grpc-ingress/requirements.txt | 3 + 28 files changed, 497 insertions(+), 20 deletions(-) create mode 100644 examples/grpc-ingress/.env.example create mode 100644 examples/grpc-ingress/README.md create mode 100644 examples/grpc-ingress/README_zh.md create mode 100644 examples/grpc-ingress/env_utils.py create mode 100644 examples/grpc-ingress/grpc_plaintext.py create mode 100644 examples/grpc-ingress/requirements.txt diff --git a/CubeProxy/Dockerfile b/CubeProxy/Dockerfile index 5e99b5d82..237fdd8ec 100644 --- a/CubeProxy/Dockerfile +++ b/CubeProxy/Dockerfile @@ -22,7 +22,7 @@ COPY rotate_nginx_log.sh /usr/local/openresty/nginx/sbin/rotate_nginx_log.sh COPY root /etc/crontabs/root COPY start.sh /usr/local/openresty/nginx/sbin/start.sh -EXPOSE 8080 8081 +EXPOSE 8080 8081 9090 STOPSIGNAL SIGQUIT diff --git a/CubeProxy/nginx.conf b/CubeProxy/nginx.conf index ceaf03563..ec8e14e6e 100644 --- a/CubeProxy/nginx.conf +++ b/CubeProxy/nginx.conf @@ -97,6 +97,15 @@ http { keepalive_timeout 80; } + # Plaintext gRPC ingress. Clients dial this IP:port and set :authority to + # "-" (no DNS required). + upstream grpc_backend { + server 0.0.0.1:1235; + balancer_by_lua_file lua/balancer_phase.lua; + keepalive 200; + keepalive_timeout 80; + } + lua_shared_dict local_cache 100m; # Auto-pause / auto-resume coordination dicts. Owned by CubeProxy-sidecar @@ -361,6 +370,53 @@ http { } } + server { + listen 9090 http2 reuseport; + server_name _; + set $cube_proxy_host_ip ""; + set $ins_id ""; + set $container_port ""; + set $backend_ip ""; + set $backend_port ""; + set $host_proxy_port 9090; + # Auto-pause / auto-resume wiring; mirrors the 8081 server. See that block. + set $cube_sidecar_addr "127.0.0.1:8083"; + set $cube_admin_token ""; + + # Mirror of the 8081 server's internal sub-location. + location = /_sidecar_resume { + internal; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Content-Length 0; + proxy_pass_request_body off; + proxy_pass_request_headers off; + proxy_connect_timeout 2s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + proxy_pass http://$cube_sidecar_addr/internal/resume?$args; + } + + location / { + include /usr/local/openresty/nginx/conf/global/global.conf; + + grpc_connect_timeout 3s; + grpc_read_timeout 7206s; + grpc_send_timeout 7206s; + grpc_buffering off; + grpc_set_header X-Real-IP $remote_addr; + grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + rewrite_by_lua_file lua/rewrite_phase.lua; + + grpc_pass grpc://grpc_backend; + + header_filter_by_lua_file lua/header_filter_phase.lua; + + log_by_lua_file lua/log_phase.lua; + } + } + # ── CubeProxy admin server (auto-pause coordination) ────────────────────── # # Bound to loopback only; carries the shared-dict mutation surface used by diff --git a/deploy/one-click/README.md b/deploy/one-click/README.md index c065cbfea..de3b4bef1 100644 --- a/deploy/one-click/README.md +++ b/deploy/one-click/README.md @@ -290,6 +290,7 @@ Other common parameters: ```bash CUBE_PROXY_HTTPS_PORT=443 CUBE_PROXY_HTTP_PORT=80 +CUBE_PROXY_GRPC_PORT=9090 # Deprecated: CUBE_PROXY_HOST_PORT is ignored; configure CUBE_PROXY_HTTP_PORT instead. CUBE_PROXY_CERT_DIR=/usr/local/services/cubetoolbox/cubeproxy/certs CUBE_PROXY_DNS_ANSWER_IP="${CUBE_SANDBOX_NODE_IP}" diff --git a/deploy/one-click/README_zh.md b/deploy/one-click/README_zh.md index 2f26443df..959b7d3a6 100644 --- a/deploy/one-click/README_zh.md +++ b/deploy/one-click/README_zh.md @@ -277,6 +277,7 @@ CUBE_PROXY_DNS_ENABLE=1 ```bash CUBE_PROXY_HTTPS_PORT=443 CUBE_PROXY_HTTP_PORT=80 +CUBE_PROXY_GRPC_PORT=9090 # 已废弃:CUBE_PROXY_HOST_PORT 会被忽略;如需调整启动后检查端口,请配置 CUBE_PROXY_HTTP_PORT。 CUBE_PROXY_CERT_DIR=/usr/local/services/cubetoolbox/cubeproxy/certs CUBE_PROXY_DNS_ANSWER_IP="${CUBE_SANDBOX_NODE_IP}" diff --git a/deploy/one-click/build-release-bundle.sh b/deploy/one-click/build-release-bundle.sh index b1c465155..0d96e0265 100755 --- a/deploy/one-click/build-release-bundle.sh +++ b/deploy/one-click/build-release-bundle.sh @@ -395,6 +395,9 @@ generate_cube_proxy_nginx_template() { # the backend instead of redirecting to HTTPS, because some upstream clients # require plain HTTP. If you need a 301 redirect, override the HTTP server # block in your own deployment. +# +# The gRPC server block (port __CUBE_PROXY_GRPC_PORT__) is a plaintext HTTP/2 +# listener for sandbox gRPC ingress. EOF ) @@ -404,15 +407,17 @@ EOF -e 's|^worker_processes [0-9]\+;|worker_processes auto;|' \ -e 's|^\(\s*listen \)8081\( reuseport;\)|\1__CUBE_PROXY_HTTP_PORT__\2|' \ -e 's|^\(\s*listen \)8080\( ssl reuseport;\)|\1__CUBE_PROXY_HTTPS_PORT__\2|' \ + -e 's|^\(\s*listen \)9090\( http2 reuseport;\)|\1__CUBE_PROXY_GRPC_PORT__\2|' \ -e 's|^\(\s*set \$host_proxy_port \)8081;|\1__CUBE_PROXY_HTTP_PORT__;|' \ -e 's|^\(\s*set \$host_proxy_port \)8080;|\1__CUBE_PROXY_HTTPS_PORT__;|' \ + -e 's|^\(\s*set \$host_proxy_port \)9090;|\1__CUBE_PROXY_GRPC_PORT__;|' \ -e 's|^\(\s*listen \)127\.0\.0\.1:8082;|\1__CUBE_PROXY_ADMIN_LISTEN__:8082;|' \ -e 's|/usr/local/openresty/nginx/certs/cube\.app+3\.pem|/usr/local/openresty/nginx/certs/__CUBE_PROXY_SSL_CERT__|' \ -e 's|/usr/local/openresty/nginx/certs/cube\.app+3-key\.pem|/usr/local/openresty/nginx/certs/__CUBE_PROXY_SSL_KEY__|' \ "${src}" } > "${dst}" - for token in __CUBE_PROXY_HTTP_PORT__ __CUBE_PROXY_HTTPS_PORT__ __CUBE_PROXY_ADMIN_LISTEN__ __CUBE_PROXY_SSL_CERT__ __CUBE_PROXY_SSL_KEY__; do + for token in __CUBE_PROXY_HTTP_PORT__ __CUBE_PROXY_HTTPS_PORT__ __CUBE_PROXY_GRPC_PORT__ __CUBE_PROXY_ADMIN_LISTEN__ __CUBE_PROXY_SSL_CERT__ __CUBE_PROXY_SSL_KEY__; do if ! grep -q -F "${token}" "${dst}"; then die "generated nginx.conf.template is missing placeholder ${token}; upstream CubeProxy/nginx.conf may have changed" fi diff --git a/deploy/one-click/env.example b/deploy/one-click/env.example index 1ee10c4f9..3ef2aba14 100644 --- a/deploy/one-click/env.example +++ b/deploy/one-click/env.example @@ -150,6 +150,7 @@ CUBE_PROXY_CONTAINER_NAME=cube-proxy CUBE_PROXY_HTTPS_PORT=443 # The systemd post-start TCP listener check follows this HTTP proxy port. CUBE_PROXY_HTTP_PORT=80 +CUBE_PROXY_GRPC_PORT=9090 # Deprecated: CUBE_PROXY_HOST_PORT is ignored; configure CUBE_PROXY_HTTP_PORT instead. CUBE_PROXY_CERT_DIR=/usr/local/services/cubetoolbox/cubeproxy/certs CUBE_PROXY_REDIS_IP=127.0.0.1 diff --git a/deploy/one-click/scripts/one-click/up-cube-proxy.sh b/deploy/one-click/scripts/one-click/up-cube-proxy.sh index e7e22b812..2eafb0ea7 100644 --- a/deploy/one-click/scripts/one-click/up-cube-proxy.sh +++ b/deploy/one-click/scripts/one-click/up-cube-proxy.sh @@ -33,6 +33,7 @@ CUBE_PROXY_REDIS_PORT="${CUBE_PROXY_REDIS_PORT:-${CUBE_SANDBOX_REDIS_PORT:-6379} CUBE_PROXY_REDIS_PASSWORD="${CUBE_PROXY_REDIS_PASSWORD:-${CUBE_SANDBOX_REDIS_PASSWORD:-ceuhvu123}}" CUBE_PROXY_HTTPS_PORT="${CUBE_PROXY_HTTPS_PORT:-443}" CUBE_PROXY_HTTP_PORT="${CUBE_PROXY_HTTP_PORT:-80}" +CUBE_PROXY_GRPC_PORT="${CUBE_PROXY_GRPC_PORT:-9090}" CUBE_PROXY_SSL_CERT="${CUBE_PROXY_SSL_CERT:-cube.app+3.pem}" CUBE_PROXY_SSL_KEY="${CUBE_PROXY_SSL_KEY:-cube.app+3-key.pem}" # Address the /admin/* server (port 8082) binds to. Defaults to the node's @@ -142,6 +143,7 @@ render_template_atomic \ "${NGINX_CONF}" \ -e "s/__CUBE_PROXY_HTTPS_PORT__/$(escape_sed "${CUBE_PROXY_HTTPS_PORT}")/g" \ -e "s/__CUBE_PROXY_HTTP_PORT__/$(escape_sed "${CUBE_PROXY_HTTP_PORT}")/g" \ + -e "s/__CUBE_PROXY_GRPC_PORT__/$(escape_sed "${CUBE_PROXY_GRPC_PORT}")/g" \ -e "s/__CUBE_PROXY_ADMIN_LISTEN__/$(escape_sed "${CUBE_PROXY_ADMIN_LISTEN}")/g" \ -e "s/__CUBE_PROXY_SSL_CERT__/$(escape_sed "${CUBE_PROXY_SSL_CERT}")/g" \ -e "s/__CUBE_PROXY_SSL_KEY__/$(escape_sed "${CUBE_PROXY_SSL_KEY}")/g" @@ -179,7 +181,7 @@ docker_rm_if_exists "${CUBE_PROXY_CONTAINER_NAME}" # cube-proxy uses network_mode: host, so HTTP/HTTPS ports must be free on the # host before we attempt to start the container; otherwise the failure mode is # a cryptic "address already in use" from nginx inside the container. -for port in "${CUBE_PROXY_HTTP_PORT}" "${CUBE_PROXY_HTTPS_PORT}"; do +for port in "${CUBE_PROXY_HTTP_PORT}" "${CUBE_PROXY_HTTPS_PORT}" "${CUBE_PROXY_GRPC_PORT}"; do if command_output_contains_fixed_string "LISTEN" ss -lnt "( sport = :${port} )"; then die "port ${port} is already in use; cube-proxy uses host networking and requires it to be free" fi @@ -204,6 +206,7 @@ done http_ready=0 https_ready=0 +grpc_ready=0 for _ in {1..30}; do if [[ "${http_ready}" == "0" ]] && \ command_output_contains_fixed_string "LISTEN" ss -lnt "( sport = :${CUBE_PROXY_HTTP_PORT} )"; then @@ -213,14 +216,27 @@ for _ in {1..30}; do command_output_contains_fixed_string "LISTEN" ss -lnt "( sport = :${CUBE_PROXY_HTTPS_PORT} )"; then https_ready=1 fi - if [[ "${http_ready}" == "1" && "${https_ready}" == "1" ]]; then - log "cube proxy listening on ${CUBE_PROXY_HTTP_PORT} and ${CUBE_PROXY_HTTPS_PORT}" + if [[ "${grpc_ready}" == "0" ]] && \ + command_output_contains_fixed_string "LISTEN" ss -lnt "( sport = :${CUBE_PROXY_GRPC_PORT} )"; then + grpc_ready=1 + fi + if [[ "${http_ready}" == "1" && "${https_ready}" == "1" && "${grpc_ready}" == "1" ]]; then + log "cube proxy listening on ${CUBE_PROXY_HTTP_PORT}, ${CUBE_PROXY_HTTPS_PORT}, and ${CUBE_PROXY_GRPC_PORT}" exit 0 fi sleep 2 done +not_ready=() if [[ "${http_ready}" != "1" ]]; then - die "cube proxy port ${CUBE_PROXY_HTTP_PORT} (HTTP) did not become ready" + not_ready+=("${CUBE_PROXY_HTTP_PORT} (HTTP)") +fi +if [[ "${https_ready}" != "1" ]]; then + not_ready+=("${CUBE_PROXY_HTTPS_PORT} (HTTPS)") +fi +if [[ "${grpc_ready}" != "1" ]]; then + not_ready+=("${CUBE_PROXY_GRPC_PORT} (gRPC)") +fi +if (( ${#not_ready[@]} > 0 )); then + die "cube proxy port(s) did not become ready: ${not_ready[*]}" fi -die "cube proxy port ${CUBE_PROXY_HTTPS_PORT} (HTTPS) did not become ready" diff --git a/deploy/one-click/scripts/systemd/cube-proxy-postcheck.sh b/deploy/one-click/scripts/systemd/cube-proxy-postcheck.sh index 940c82eea..89bfeae1a 100755 --- a/deploy/one-click/scripts/systemd/cube-proxy-postcheck.sh +++ b/deploy/one-click/scripts/systemd/cube-proxy-postcheck.sh @@ -4,6 +4,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/common.sh" postcheck_port="${CUBE_PROXY_HTTP_PORT:-80}" +postcheck_grpc_port="${CUBE_PROXY_GRPC_PORT:-9090}" postcheck_retries="${CUBE_PROXY_POSTCHECK_RETRIES:-30}" postcheck_delay="${CUBE_PROXY_POSTCHECK_DELAY:-2}" deprecated_host_port="${CUBE_PROXY_HOST_PORT:-}" @@ -14,3 +15,5 @@ fi log "checking cube-proxy HTTP tcp port ${postcheck_port}" wait_for_tcp_port "${postcheck_port}" "${postcheck_retries}" "${postcheck_delay}" || die "cube-proxy HTTP tcp port not ready: ${postcheck_port}" +log "checking cube-proxy gRPC tcp port ${postcheck_grpc_port}" +wait_for_tcp_port "${postcheck_grpc_port}" "${postcheck_retries}" "${postcheck_delay}" || die "cube-proxy gRPC tcp port not ready: ${postcheck_grpc_port}" diff --git a/deploy/one-click/terraform/tencentcloud/create.sh b/deploy/one-click/terraform/tencentcloud/create.sh index 0fad609b8..c1e09e7e7 100644 --- a/deploy/one-click/terraform/tencentcloud/create.sh +++ b/deploy/one-click/terraform/tencentcloud/create.sh @@ -4719,7 +4719,7 @@ print_cluster_operator_help() { echo "" echo -e "${CYAN}▶ 2. CLB (load balancer) IPs and ports${NC}" echo -e " ${GREEN}cube-webui${NC} (public, HTTP) : ${webui_ip:-N/A} → port 80" - echo -e " ${GREEN}cube-proxy${NC} (public, TCP) : ${proxy_ip:-N/A} → ports 80, 443" + echo -e " ${GREEN}cube-proxy${NC} (public, TCP) : ${proxy_ip:-N/A} → ports 80, 443, 9090" echo -e " ${GREEN}cube-api${NC} (public, TCP) : ${api_ip:-N/A} → port 3000" echo -e " ${GREEN}cube-master${NC} (VPC-internal) : ${master_ip:-N/A} → port 8089 (reachable from the jumpserver/VPC only)" @@ -5033,7 +5033,7 @@ phase7_health_check() { echo -e " ${CYAN}Summary:${NC}" echo -e " cube-master : ${cm_ip:-N/A}:8089 (VPC-internal)" echo -e " cube-api : ${api_ip:-N/A}:3000 (public)" - echo -e " cube-proxy : ${proxy_ip:-N/A}:80/443 (public)" + echo -e " cube-proxy : ${proxy_ip:-N/A}:80/443/9090 (public)" echo -e " cube-webui : ${webui_ip:-N/A}:80 (public)" return 0 } diff --git a/deploy/one-click/terraform/tencentcloud/main.tf b/deploy/one-click/terraform/tencentcloud/main.tf index 6ccaf27dc..ad3d4e2e4 100644 --- a/deploy/one-click/terraform/tencentcloud/main.tf +++ b/deploy/one-click/terraform/tencentcloud/main.tf @@ -386,6 +386,14 @@ resource "tencentcloud_security_group_rule_set" "clb" { description = "Allow CLB HTTPS (cube-proxy)" } + ingress { + action = "ACCEPT" + cidr_block = var.enable_public_network ? "0.0.0.0/0" : "10.0.0.0/16" + protocol = "TCP" + port = "9090" + description = "Allow cube-proxy plaintext gRPC ingress" + } + ingress { action = "ACCEPT" cidr_block = var.enable_public_network ? "0.0.0.0/0" : "10.0.0.0/16" diff --git a/deploy/one-click/terraform/tencentcloud/tke-addons.tf b/deploy/one-click/terraform/tencentcloud/tke-addons.tf index 3e925c71e..ea0184881 100644 --- a/deploy/one-click/terraform/tencentcloud/tke-addons.tf +++ b/deploy/one-click/terraform/tencentcloud/tke-addons.tf @@ -879,6 +879,11 @@ resource "kubernetes_deployment" "cube_proxy" { container_port = 8081 protocol = "TCP" } + port { + name = "grpc" + container_port = 9090 + protocol = "TCP" + } port { name = "http80" container_port = 80 @@ -1083,7 +1088,7 @@ resource "kubernetes_service" "cube_proxy" { # Public mode: a public CLB billed by traffic (internet-charge-type). # Internal mode (default): pin to a VPC-internal subnet for a private VIP. annotations = merge({ - "service.cloud.tencent.com/specify-protocol" = "{\"80\":{\"protocol\":[\"TCP\"]},\"443\":{\"protocol\":[\"TCP\"]}}" + "service.cloud.tencent.com/specify-protocol" = "{\"80\":{\"protocol\":[\"TCP\"]},\"443\":{\"protocol\":[\"TCP\"]},\"9090\":{\"protocol\":[\"TCP\"]}}" "service.cloud.tencent.com/modification-protection" = "false" "service.cloud.tencent.com/pass-to-target" = "true" "service.cloud.tencent.com/security-groups" = tencentcloud_security_group.clb.id @@ -1122,6 +1127,12 @@ resource "kubernetes_service" "cube_proxy" { target_port = 8080 protocol = "TCP" } + port { + name = "tcp-grpc-9090" + port = 9090 + target_port = 9090 + protocol = "TCP" + } } } diff --git a/deploy/one-click/tests/test_runtime_file_safety.sh b/deploy/one-click/tests/test_runtime_file_safety.sh index 29083df8a..843092b24 100644 --- a/deploy/one-click/tests/test_runtime_file_safety.sh +++ b/deploy/one-click/tests/test_runtime_file_safety.sh @@ -63,7 +63,9 @@ run_cube_proxy_postcheck_case() { local env_content="$1" local listen_port="$2" local expected_port="$3" - local case_dir="${TMP_DIR}/cube-proxy-postcheck-${expected_port}" + local grpc_listen_port="${4:-9090}" + local grpc_expected_port="${5:-9090}" + local case_dir="${TMP_DIR}/cube-proxy-postcheck-${expected_port}-${grpc_expected_port}" local env_file="${case_dir}/.one-click.env" local stub_dir="${case_dir}/bin" local ss_log="${case_dir}/ss.args" @@ -72,18 +74,28 @@ run_cube_proxy_postcheck_case() { printf '%s\n' "${env_content}" > "${env_file}" cat > "${stub_dir}/ss" <<'SH' #!/usr/bin/env bash -printf '%s\n' "$*" > "${SS_ARGS_LOG}" -printf 'LISTEN 0 128 0.0.0.0:%s 0.0.0.0:*\n' "${SS_LISTEN_PORT}" +printf '%s\n' "$*" >> "${SS_ARGS_LOG}" +port="" +for arg in "$@"; do + case "${arg}" in + *"sport = :"*) port="${arg#*sport = :}"; port="${port%% )*}" ;; + esac +done +if [[ "${port}" == "${SS_HTTP_PORT}" || "${port}" == "${SS_GRPC_PORT}" ]]; then + printf 'LISTEN 0 128 0.0.0.0:%s 0.0.0.0:*\n' "${port}" +fi SH chmod +x "${stub_dir}/ss" PATH="${stub_dir}:${PATH}" \ ONE_CLICK_RUNTIME_ENV_FILE="${env_file}" \ SS_ARGS_LOG="${ss_log}" \ - SS_LISTEN_PORT="${listen_port}" \ + SS_HTTP_PORT="${listen_port}" \ + SS_GRPC_PORT="${grpc_listen_port}" \ bash "${ONE_CLICK_DIR}/scripts/systemd/cube-proxy-postcheck.sh" >/dev/null assert_contains "${ss_log}" "sport = :${expected_port}" + assert_contains "${ss_log}" "sport = :${grpc_expected_port}" } test_render_template_replaces_empty_directory() { @@ -791,6 +803,57 @@ CUBE_PROXY_POSTCHECK_DELAY=0" \ 8081 } +test_cube_proxy_postcheck_follows_grpc_port() { + run_cube_proxy_postcheck_case \ + "CUBE_PROXY_HTTP_PORT=8081 +CUBE_PROXY_GRPC_PORT=50051 +CUBE_PROXY_POSTCHECK_RETRIES=1 +CUBE_PROXY_POSTCHECK_DELAY=0" \ + 8081 \ + 8081 \ + 50051 \ + 50051 +} + +test_cube_proxy_postcheck_fails_when_grpc_port_not_ready() { + local case_dir="${TMP_DIR}/cube-proxy-postcheck-grpc-fail" + local env_file="${case_dir}/.one-click.env" + local stub_dir="${case_dir}/bin" + local ss_log="${case_dir}/ss.args" + + mkdir -p "${stub_dir}" + printf '%s\n' \ + "CUBE_PROXY_HTTP_PORT=80 +CUBE_PROXY_GRPC_PORT=9090 +CUBE_PROXY_POSTCHECK_RETRIES=1 +CUBE_PROXY_POSTCHECK_DELAY=0" > "${env_file}" + cat > "${stub_dir}/ss" <<'SH' +#!/usr/bin/env bash +printf '%s\n' "$*" >> "${SS_ARGS_LOG}" +port="" +for arg in "$@"; do + case "${arg}" in + *"sport = :"*) port="${arg#*sport = :}"; port="${port%% )*}" ;; + esac +done +if [[ "${port}" == "${SS_HTTP_PORT}" ]]; then + printf 'LISTEN 0 128 0.0.0.0:%s 0.0.0.0:*\n' "${port}" +fi +SH + chmod +x "${stub_dir}/ss" + + if PATH="${stub_dir}:${PATH}" \ + ONE_CLICK_RUNTIME_ENV_FILE="${env_file}" \ + SS_ARGS_LOG="${ss_log}" \ + SS_HTTP_PORT="80" \ + bash "${ONE_CLICK_DIR}/scripts/systemd/cube-proxy-postcheck.sh" >/dev/null 2>&1; then + fail "expected postcheck to fail when gRPC port is not ready" + fi + + assert_contains "${ss_log}" "sport = :80" + assert_contains "${ss_log}" "sport = :9090" +} + test_postcheck_skips_when_external_host_set() { # When an external endpoint is configured the local container is never # started, so the postcheck must short-circuit with exit 0 instead of @@ -862,6 +925,8 @@ test_cube_proxy_postcheck_follows_http_port test_cube_proxy_postcheck_ignores_https_port test_cube_proxy_postcheck_ignores_deprecated_host_port test_cube_proxy_postcheck_prefers_http_over_deprecated_host_port +test_cube_proxy_postcheck_follows_grpc_port +test_cube_proxy_postcheck_fails_when_grpc_port_not_ready test_postcheck_skips_when_external_host_set test_mask_external_dep_services_remove_then_mask diff --git a/docs/guide/https-and-domain.md b/docs/guide/https-and-domain.md index d09c39ef3..3133fd02c 100644 --- a/docs/guide/https-and-domain.md +++ b/docs/guide/https-and-domain.md @@ -89,6 +89,32 @@ Both modes coexist on every CubeProxy instance and share the same Redis-backed r --- +## gRPC Ingress (Plaintext HTTP/2) + +CubeProxy also exposes a dedicated listener for sandbox gRPC services. The default port is `9090` and can be changed via `CUBE_PROXY_GRPC_PORT` in one-click deployments. + +Clients dial the CubeProxy IP over plaintext HTTP/2 and identify the target sandbox with the gRPC `:authority` pseudo-header, using the same `-` format as host-based HTTP routing: + +``` +- +``` + +For example, to reach sandbox `abc123` on container port `49983` through CubeProxy at `10.0.0.5`: + +``` +dial: 10.0.0.5:9090 +:authority: 49983-abc123 +``` + +This mode is intended for gRPC clients that cannot rely on wildcard DNS or TLS on CubeProxy. It reuses the same Redis-backed routing metadata as host-based HTTP routing. + +When a sandbox is created with `network.allow_public_traffic = false`, the same +`e2b-traffic-access-token` / `cube-traffic-access-token` checks apply on this +listener too. Pass the token as gRPC metadata (or the equivalent request header) +on every call; see [Restrict Public Access](./restrict-public-access.md). + +--- + ## HTTPS Certificate Configuration CubeProxy serves both **HTTPS (port 443) and HTTP (port 80)** out of the box. The E2B SDK uses HTTPS by default. The one-click install pre-installs a `cube.app` test certificate so you can try HTTPS immediately. diff --git a/docs/guide/network-hardening.md b/docs/guide/network-hardening.md index a8a949465..17541eab3 100644 --- a/docs/guide/network-hardening.md +++ b/docs/guide/network-hardening.md @@ -21,7 +21,7 @@ entire page and apply at least one of the hardening strategies below. | CubeAPI | `0.0.0.0` | 3000 | `CUBE_API_BIND` in `.env` | Sandbox lifecycle API | | Cubelet gRPC | `0.0.0.0` | 9999 | `tcp_address` in `Cubelet/config/config.toml` | Node management RPC, **no TLS** | | Cubelet HTTP | `0.0.0.0` | 9998 | `[http] address` in `Cubelet/config/config.toml` | Debug / metrics | -| cube-proxy | `0.0.0.0` | 80 / 443 | `CUBE_PROXY_HTTP_PORT` / `CUBE_PROXY_HTTPS_PORT` | Intentionally public-facing | +| cube-proxy | `0.0.0.0` | 80 / 443 / 9090 | `CUBE_PROXY_HTTP_PORT` / `CUBE_PROXY_HTTPS_PORT` / `CUBE_PROXY_GRPC_PORT` | Intentionally public-facing | | WebUI | `0.0.0.0` | 12088 | `WEB_UI_HOST_PORT` in `.env` (port only) | Dashboard | | MySQL | `127.0.0.1` | 3306 | Hardcoded in compose template | Already loopback-only | | Redis | `127.0.0.1` | 6379 | Hardcoded in compose template | Already loopback-only | @@ -182,6 +182,7 @@ sudo ufw allow from 10.0.0.0/24 to any port 12088 proto tcp # WebUI sudo ufw allow 22/tcp # SSH sudo ufw allow 80/tcp # cube-proxy (public if needed) sudo ufw allow 443/tcp # cube-proxy TLS +sudo ufw allow 9090/tcp # cube-proxy plaintext gRPC sudo ufw enable ``` diff --git a/docs/guide/restrict-public-access.md b/docs/guide/restrict-public-access.md index cdf38ebf4..b04caaabe 100644 --- a/docs/guide/restrict-public-access.md +++ b/docs/guide/restrict-public-access.md @@ -84,6 +84,10 @@ keeps all existing callers working unchanged. | Default — publicly reachable | omit `network`, or `allow_public_traffic=True` | `None` | accepts every request | | Lock down | `network={"allow_public_traffic": False}` | opaque token | rejects requests without a valid token header (403) | +The same enforcement applies to every CubeProxy ingress listener (HTTP, HTTPS, and +the plaintext gRPC port `9090` by default). gRPC callers should send the token as +metadata using `e2b-traffic-access-token` or `cube-traffic-access-token`. + ## Header semantics Both `e2b-traffic-access-token` and `cube-traffic-access-token` accept diff --git a/docs/guide/self-build-deploy.md b/docs/guide/self-build-deploy.md index 145bde307..c551f4629 100644 --- a/docs/guide/self-build-deploy.md +++ b/docs/guide/self-build-deploy.md @@ -316,6 +316,7 @@ You can also point to prebuilt binaries to skip compilation: | `CUBE_PROXY_ENABLE` | `1` | Enable CubeProxy (must be `1` for one-click) | | `CUBE_PROXY_HTTPS_PORT` | `443` | CubeProxy HTTPS listen port | | `CUBE_PROXY_HTTP_PORT` | `80` | CubeProxy HTTP listen port; the systemd post-start TCP listener check follows this port | +| `CUBE_PROXY_GRPC_PORT` | `9090` | CubeProxy plaintext gRPC (HTTP/2) listen port | | `CUBE_PROXY_DNS_ENABLE` | `1` | Enable CoreDNS (must be `1` for one-click) | | `CUBE_PROXY_DNS_ANSWER_IP` | `${CUBE_SANDBOX_NODE_IP}` | IP returned by CoreDNS for `cube.app` | | `CUBE_PROXY_COREDNS_BIND_ADDR` | `127.0.0.54` | CoreDNS bind address | diff --git a/docs/guide/service-management.md b/docs/guide/service-management.md index a62a75bfc..1f23714d1 100644 --- a/docs/guide/service-management.md +++ b/docs/guide/service-management.md @@ -68,7 +68,7 @@ The target lists its child services via `Wants=`; each service declares membersh | `cube-sandbox-network-agent.service` | Host process | `19090` (health) | control / compute | network | | `cube-sandbox-cubelet.service` | Host process | `9999` (gRPC) | control / compute | network-agent + `/data/cubelet` (XFS) | | `cube-sandbox-coredns.service` | Docker container | `127.0.0.54:53` or `169.254.254.53:53` | control | docker | -| `cube-sandbox-cube-proxy.service` | Docker container | `443` (TLS) / `80` | control | docker, redis | +| `cube-sandbox-cube-proxy.service` | Docker container | `443` (TLS) / `80` / `9090` (gRPC) | control | docker, redis | | `cube-sandbox-dns.service` | oneshot (no daemon) | — | control | coredns (`BindsTo`) | | `cube-sandbox-webui.service` | Docker container | `12088` | control | docker, cube-api | @@ -387,7 +387,7 @@ Common root causes: - Container build needs the network (e.g. `cube-proxy`'s `apk update`) and the upstream mirror is flaky — see [Deployment Troubleshooting](./troubleshooting/deployment.md) - `ExecStartPost` health probe timeout (port already in use, upstream not yet ready) -- For `cube-sandbox-cube-proxy.service`, `CUBE_PROXY_HTTP_PORT` is the actual nginx HTTP proxy listener used by the post-start TCP check. `CUBE_PROXY_HOST_PORT` is deprecated and ignored; set `CUBE_PROXY_HTTP_PORT` instead if you need a non-default check port. +- For `cube-sandbox-cube-proxy.service`, `CUBE_PROXY_HTTP_PORT` and `CUBE_PROXY_GRPC_PORT` are the nginx listeners checked by the post-start TCP probe. `CUBE_PROXY_HOST_PORT` is deprecated and ignored; set `CUBE_PROXY_HTTP_PORT` instead if you need a non-default HTTP check port. - `/data/log` or `/data/cubelet` missing / wrong permissions / XFS not mounted ### Dashboard / API unreachable diff --git a/docs/zh/guide/https-and-domain.md b/docs/zh/guide/https-and-domain.md index ae4126cd4..15c31f633 100644 --- a/docs/zh/guide/https-and-domain.md +++ b/docs/zh/guide/https-and-domain.md @@ -89,6 +89,31 @@ CubeProxy 会在转发时剥离 `/sandbox//` 前缀,沙箱内的应 --- +## gRPC 接入(明文 HTTP/2) + +CubeProxy 还提供独立的沙箱 gRPC 接入监听端口。一键部署默认使用 `9090`,可通过 `.env` 中的 `CUBE_PROXY_GRPC_PORT` 调整。 + +客户端以明文 HTTP/2 连接 CubeProxy IP,并通过 gRPC `:authority` 指定目标沙箱,格式与 Host 模式一致: + +``` +- +``` + +例如,经部署在 `10.0.0.5` 的 CubeProxy 访问沙箱 `abc123` 的 `49983` 端口: + +``` +dial: 10.0.0.5:9090 +:authority: 49983-abc123 +``` + +该模式适用于无法使用泛域名 DNS 或在 CubeProxy 侧终止 TLS 的 gRPC 客户端,路由元数据与 Host 模式共用,无需额外配置。 + +若沙箱创建时设置了 `network.allow_public_traffic = false`,本监听端口同样会校验 +`e2b-traffic-access-token` / `cube-traffic-access-token`;gRPC 客户端需在每次请求的 +metadata(或等效 header)中携带 token。详见[限制公网访问](./restrict-public-access.md)。 + +--- + ## HTTPS 证书配置 CubeProxy 开箱提供 **HTTPS(443 端口)和 HTTP(80 端口)** 两种访问方式。E2B SDK 默认使用 HTTPS。Cube 一键安装已预装 `cube.app` 测试证书,可直接体验 HTTPS。 diff --git a/docs/zh/guide/network-hardening.md b/docs/zh/guide/network-hardening.md index 8f8ee6459..6c665e259 100644 --- a/docs/zh/guide/network-hardening.md +++ b/docs/zh/guide/network-hardening.md @@ -18,7 +18,7 @@ Cube Sandbox 的控制面与管理类服务为了便于本地快速体验,部 | CubeAPI | `0.0.0.0` | 3000 | `.env` 中的 `CUBE_API_BIND` | 沙箱生命周期 API | | Cubelet gRPC | `0.0.0.0` | 9999 | `Cubelet/config/config.toml` 的 `tcp_address` | 节点管理 RPC,**无 TLS** | | Cubelet HTTP | `0.0.0.0` | 9998 | `Cubelet/config/config.toml` 的 `[http] address` | 调试 / metrics | -| cube-proxy | `0.0.0.0` | 80 / 443 | `CUBE_PROXY_HTTP_PORT` / `CUBE_PROXY_HTTPS_PORT` | 设计上即面向公网 | +| cube-proxy | `0.0.0.0` | 80 / 443 / 9090 | `CUBE_PROXY_HTTP_PORT` / `CUBE_PROXY_HTTPS_PORT` / `CUBE_PROXY_GRPC_PORT` | 设计上即面向公网 | | WebUI | `0.0.0.0` | 12088 | `.env` 中的 `WEB_UI_HOST_PORT`(仅端口) | 控制台 | | MySQL | `127.0.0.1` | 3306 | compose 模板中硬编码 | 已仅绑回环 | | Redis | `127.0.0.1` | 6379 | compose 模板中硬编码 | 已仅绑回环 | @@ -172,6 +172,7 @@ sudo ufw allow from 10.0.0.0/24 to any port 12088 proto tcp # WebUI sudo ufw allow 22/tcp # SSH sudo ufw allow 80/tcp # cube-proxy(如需公网) sudo ufw allow 443/tcp # cube-proxy TLS +sudo ufw allow 9090/tcp # cube-proxy 明文 gRPC sudo ufw enable ``` diff --git a/docs/zh/guide/restrict-public-access.md b/docs/zh/guide/restrict-public-access.md index 0bb4dc4a1..74d0c2394 100644 --- a/docs/zh/guide/restrict-public-access.md +++ b/docs/zh/guide/restrict-public-access.md @@ -79,6 +79,9 @@ assert resp.status_code == 200 | 默认 —— 公网可达 | 不传 `network`,或 `allow_public_traffic=True` | `None` | 接受所有请求 | | 锁定访问 | `network={"allow_public_traffic": False}` | 不透明 token | 拒绝未携带正确 token 的请求(403) | +CubeProxy 的所有入站监听(HTTP、HTTPS 以及默认明文 gRPC 端口 `9090`)均执行相同校验。 +gRPC 客户端应通过 metadata 发送 `e2b-traffic-access-token` 或 `cube-traffic-access-token`。 + ## Header 语义 `e2b-traffic-access-token` 与 `cube-traffic-access-token` 接受 diff --git a/docs/zh/guide/self-build-deploy.md b/docs/zh/guide/self-build-deploy.md index e943958b5..bf7fb4039 100644 --- a/docs/zh/guide/self-build-deploy.md +++ b/docs/zh/guide/self-build-deploy.md @@ -316,6 +316,7 @@ sudo ./down.sh | `CUBE_PROXY_ENABLE` | `1` | 启用 CubeProxy(一键部署必须为 `1`) | | `CUBE_PROXY_HTTPS_PORT` | `443` | CubeProxy HTTPS 监听端口 | | `CUBE_PROXY_HTTP_PORT` | `80` | CubeProxy HTTP 监听端口;systemd 启动后 TCP listener 检查跟随该端口 | +| `CUBE_PROXY_GRPC_PORT` | `9090` | CubeProxy 明文 gRPC(HTTP/2)监听端口 | | `CUBE_PROXY_DNS_ENABLE` | `1` | 启用 CoreDNS(一键部署必须为 `1`) | | `CUBE_PROXY_DNS_ANSWER_IP` | `${CUBE_SANDBOX_NODE_IP}` | CoreDNS 对 `cube.app` 返回的 IP | | `CUBE_PROXY_COREDNS_BIND_ADDR` | `127.0.0.54` | CoreDNS 绑定地址 | diff --git a/docs/zh/guide/service-management.md b/docs/zh/guide/service-management.md index b61173c7a..921a3723c 100644 --- a/docs/zh/guide/service-management.md +++ b/docs/zh/guide/service-management.md @@ -68,7 +68,7 @@ Target 通过 `Wants=` 列出自己要拉起的 service;service 通过 `PartOf | `cube-sandbox-network-agent.service` | 宿主机进程 | `19090`(health) | control / compute | network | | `cube-sandbox-cubelet.service` | 宿主机进程 | `9999`(gRPC) | control / compute | network-agent + `/data/cubelet`(XFS) | | `cube-sandbox-coredns.service` | Docker 容器 | `127.0.0.54:53` 或 `169.254.254.53:53` | control | docker | -| `cube-sandbox-cube-proxy.service` | Docker 容器 | `443`(TLS)/ `80` | control | docker, redis | +| `cube-sandbox-cube-proxy.service` | Docker 容器 | `443`(TLS)/ `80` / `9090`(gRPC) | control | docker, redis | | `cube-sandbox-dns.service` | oneshot(无常驻进程) | — | control | coredns(`BindsTo`)| | `cube-sandbox-webui.service` | Docker 容器 | `12088` | control | docker, cube-api | @@ -387,7 +387,7 @@ sudo journalctl -u cube-sandbox-.service -n 200 --no-pager - 容器镜像构建依赖外网(如 `cube-proxy` 的 `apk update`)暂时不可达 → 检查网络,或参阅[部署相关排障](./troubleshooting/deployment.md) - `ExecStartPost` 健康端口超时(端口被占用 / 上游服务还没起来) -- 对 `cube-sandbox-cube-proxy.service`,`CUBE_PROXY_HTTP_PORT` 是 nginx 实际 HTTP 代理监听端口,也是启动后 TCP 检查使用的端口。`CUBE_PROXY_HOST_PORT` 已废弃且会被忽略;如果需要改检查端口,请改 `CUBE_PROXY_HTTP_PORT`。 +- 对 `cube-sandbox-cube-proxy.service`,`CUBE_PROXY_HTTP_PORT` 与 `CUBE_PROXY_GRPC_PORT` 是 systemd 启动后 TCP 检查所关注的 nginx 监听端口。`CUBE_PROXY_HOST_PORT` 已废弃且会被忽略;如果需要改 HTTP 检查端口,请改 `CUBE_PROXY_HTTP_PORT`。 - `/data/log` 或 `/data/cubelet` 目录不存在 / 权限不对 / XFS 挂载错位 ### Dashboard / API 无法访问 diff --git a/examples/grpc-ingress/.env.example b/examples/grpc-ingress/.env.example new file mode 100644 index 000000000..8e955e135 --- /dev/null +++ b/examples/grpc-ingress/.env.example @@ -0,0 +1,10 @@ +# Cube API (control plane — create/delete sandbox) +export CUBE_API_URL="http://:3000" +export CUBE_TEMPLATE_ID="" + +# CubeProxy plaintext gRPC ingress +export CUBE_PROXY_NODE_IP="" +export CUBE_PROXY_GRPC_PORT=9090 + +# envd listens on 49983 in standard templates +export ENVD_PORT=49983 diff --git a/examples/grpc-ingress/README.md b/examples/grpc-ingress/README.md new file mode 100644 index 000000000..4b7dc3ad4 --- /dev/null +++ b/examples/grpc-ingress/README.md @@ -0,0 +1,74 @@ +# gRPC Ingress Quickstart + +[中文文档](README_zh.md) + +Minimal example for CubeProxy **plaintext gRPC ingress** on port `9090`. + +The CubeSandbox Python SDK (`commands`, `files`, etc.) talks to envd over +**HTTP/Connect** on CubeProxy HTTP/HTTPS (`80`/`443`, or `CUBE_PROXY_NODE_IP` + +`CUBE_PROXY_PORT_HTTP` with a `Host` header). **No SDK changes are required** for +those APIs. + +Use port `9090` when you have a **native gRPC client** (`grpcio`, `grpc-go`, …) +that cannot rely on wildcard DNS (`*.cube.app`) or TLS on CubeProxy. + +## Flow + +```text + grpc_plaintext.py + │ + ├─ Sandbox.create() ──► CubeAPI (control plane) + │ + └─ grpc.insecure_channel(proxy:9090, authority=-) + │ + ▼ + CubeProxy :9090 + │ + ▼ + envd in sandbox (:49983) +``` + +## Prerequisites + +- A running Cube Sandbox deployment with CubeProxy gRPC ingress enabled +- Python 3.8+ +- A template that exposes envd on port `49983` (standard Cube templates) + +```bash +pip install -r requirements.txt +cp .env.example .env +# edit .env +python grpc_plaintext.py +``` + +## Environment variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `CUBE_API_URL` | yes | — | Cube API base URL | +| `CUBE_TEMPLATE_ID` | yes | — | Sandbox template ID | +| `CUBE_PROXY_NODE_IP` | yes | — | CubeProxy IP (no DNS needed) | +| `CUBE_PROXY_GRPC_PORT` | no | `9090` | Plaintext gRPC listen port | +| `ENVD_PORT` | no | `49983` | Sandbox service port in `:authority` | + +## Restricted sandboxes + +If the sandbox is created with `network={"allow_public_traffic": False}`, +attach the traffic token as gRPC metadata on every RPC: + +```python +metadata = (("cube-traffic-access-token", sandbox.traffic_access_token),) +# stub.SomeMethod(request, metadata=metadata) +``` + +See [Restrict Public Access](../../docs/guide/restrict-public-access.md). + +## Template + +```bash +cubemastercli tpl create-from-image \ + --image cubesandbox-base:latest \ + --expose-port 49983 \ + --probe 49983 \ + --probe-path /health +``` diff --git a/examples/grpc-ingress/README_zh.md b/examples/grpc-ingress/README_zh.md new file mode 100644 index 000000000..1b1ffe8f6 --- /dev/null +++ b/examples/grpc-ingress/README_zh.md @@ -0,0 +1,73 @@ +# gRPC 接入快速示例 + +[English](README.md) + +演示如何通过 CubeProxy **明文 gRPC 接入端口** `9090` 访问沙箱内服务。 + +CubeSandbox Python SDK 的 `commands`、`files` 等 API 走的是 CubeProxy +HTTP/HTTPS (`80`/`443`, 或 `CUBE_PROXY_NODE_IP` + `CUBE_PROXY_PORT_HTTP` 配 `Host` +头) 上的 **HTTP/Connect**。**这些 API 不需要改 SDK**。 + +当你使用 **原生 gRPC 客户端** (`grpcio`、`grpc-go` 等), 且无法使用泛域名 +`*.cube.app` 或在 CubeProxy 上终止 TLS 时, 使用 `9090` 端口。 + +## 流程 + +```text + grpc_plaintext.py + │ + ├─ Sandbox.create() ──► CubeAPI (控制面) + │ + └─ grpc.insecure_channel(proxy:9090, authority=-) + │ + ▼ + CubeProxy :9090 + │ + ▼ + 沙箱内 envd (:49983) +``` + +## 前置条件 + +- 已部署 Cube Sandbox, 且 CubeProxy 已开启 gRPC 接入 +- Python 3.8+ +- 模板暴露 envd 端口 `49983` (标准 Cube 模板即可) + +```bash +pip install -r requirements.txt +cp .env.example .env +# 编辑 .env +python grpc_plaintext.py +``` + +## 环境变量 + +| 变量 | 必填 | 默认 | 说明 | +|------|------|------|------| +| `CUBE_API_URL` | 是 | — | Cube API 地址 | +| `CUBE_TEMPLATE_ID` | 是 | — | 沙箱模板 ID | +| `CUBE_PROXY_NODE_IP` | 是 | — | CubeProxy IP (无需 DNS) | +| `CUBE_PROXY_GRPC_PORT` | 否 | `9090` | 明文 gRPC 监听端口 | +| `ENVD_PORT` | 否 | `49983` | `:authority` 中的沙箱服务端口 | + +## 限制公网访问的沙箱 + +若创建沙箱时设置 `network={"allow_public_traffic": False}`, 每次 RPC 需在 +gRPC metadata 中携带 token: + +```python +metadata = (("cube-traffic-access-token", sandbox.traffic_access_token),) +# stub.SomeMethod(request, metadata=metadata) +``` + +详见[限制公网访问](../../docs/zh/guide/restrict-public-access.md)。 + +## 模板 + +```bash +cubemastercli tpl create-from-image \ + --image cubesandbox-base:latest \ + --expose-port 49983 \ + --probe 49983 \ + --probe-path /health +``` diff --git a/examples/grpc-ingress/env_utils.py b/examples/grpc-ingress/env_utils.py new file mode 100644 index 000000000..f2890e28c --- /dev/null +++ b/examples/grpc-ingress/env_utils.py @@ -0,0 +1,22 @@ +from pathlib import Path + +from dotenv import load_dotenv + + +def load_local_dotenv() -> None: + """Best-effort load of a nearby .env file without overriding real env vars.""" + candidate_paths = [ + Path(__file__).with_name(".env"), + Path.cwd() / ".env", + ] + + seen_paths = set() + for path in candidate_paths: + resolved_path = path.resolve() + if resolved_path in seen_paths: + continue + seen_paths.add(resolved_path) + + if path.is_file(): + load_dotenv(dotenv_path=path, override=False) + return diff --git a/examples/grpc-ingress/grpc_plaintext.py b/examples/grpc-ingress/grpc_plaintext.py new file mode 100644 index 000000000..d66f23afe --- /dev/null +++ b/examples/grpc-ingress/grpc_plaintext.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Tencent Inc. +# SPDX-License-Identifier: Apache-2.0 +# +# Dial a sandbox service through CubeProxy plaintext gRPC ingress (port 9090). +# +# The Python SDK's commands/files APIs use HTTP/Connect on CubeProxy HTTP/HTTPS. +# This script shows the native gRPC client path: +# dial :9090 +# set :authority to - +# +# Run: +# cp .env.example .env # fill in values +# pip install -r requirements.txt +# python grpc_plaintext.py + +import os +import sys + +import grpc +from cubesandbox import Sandbox + +from env_utils import load_local_dotenv + +load_local_dotenv() + +PROXY_IP = os.environ.get("CUBE_PROXY_NODE_IP") +GRPC_PORT = int(os.environ.get("CUBE_PROXY_GRPC_PORT", "9090")) +ENVD_PORT = int(os.environ.get("ENVD_PORT", "49983")) +TEMPLATE_ID = os.environ.get("CUBE_TEMPLATE_ID") + +if not PROXY_IP: + sys.exit("CUBE_PROXY_NODE_IP is required (CubeProxy IP reachable from this host)") +if not TEMPLATE_ID: + sys.exit("CUBE_TEMPLATE_ID is required") + + +def main() -> None: + print(f"creating sandbox from template {TEMPLATE_ID}") + with Sandbox.create(template=TEMPLATE_ID, timeout=300) as sandbox: + authority = f"{ENVD_PORT}-{sandbox.sandbox_id}" + target = f"{PROXY_IP}:{GRPC_PORT}" + print(f"dialing {target} with :authority={authority}") + + channel = grpc.insecure_channel( + target, + options=(("grpc.default_authority", authority),), + ) + try: + grpc.channel_ready_future(channel).result(timeout=15) + print("gRPC channel is READY (CubeProxy routed the connection)") + except grpc.FutureTimeoutError: + sys.exit( + f"timed out connecting to {target} " + f"(is CubeProxy gRPC ingress listening on :{GRPC_PORT}?)" + ) + except grpc.RpcError as exc: + sys.exit(f"gRPC connection failed: {exc}") + finally: + channel.close() + + print("sandbox deleted") + + +if __name__ == "__main__": + main() diff --git a/examples/grpc-ingress/requirements.txt b/examples/grpc-ingress/requirements.txt new file mode 100644 index 000000000..0fccdf568 --- /dev/null +++ b/examples/grpc-ingress/requirements.txt @@ -0,0 +1,3 @@ +grpcio>=1.60 +python-dotenv +cubesandbox