Skip to content
Closed
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
54 changes: 54 additions & 0 deletions Dockerfile-ingress-noroot
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright 2025 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

FROM golang:1.24.0 AS builder

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use a Go 1.25+ builder for ingress non-root image

components/ingress/go.mod declares go 1.25.0, but this Dockerfile pins the builder to golang:1.24.0; that causes go mod download/go build in this build stage to fail on the module minimum Go version check. As a result, the newly added non-root ingress image definition is not buildable.

Useful? React with 👍 / 👎.


WORKDIR /build

ARG VERSION=dev
ARG GIT_COMMIT=unknown
ARG BUILD_TIME=unknown

COPY kubernetes ./kubernetes
# Prepare local modules to satisfy replace directives.
COPY components/internal/go.mod components/internal/go.sum ./components/internal/
COPY components/ingress/go.mod components/ingress/go.sum ./components/ingress/

WORKDIR /build

RUN cd components/internal && go mod download
RUN cd components/ingress && go mod download

# Copy sources.
COPY components/internal ./components/internal
COPY components/ingress/. ./components/ingress

WORKDIR /build/components/ingress

RUN CGO_ENABLED=0 go build \
-ldflags "-X 'github.com/alibaba/opensandbox/internal/version.Version=${VERSION}' \
-X 'github.com/alibaba/opensandbox/internal/version.BuildTime=${BUILD_TIME}' \
-X 'github.com/alibaba/opensandbox/internal/version.GitCommit=${GIT_COMMIT}'" \
-o /build/ingress ./main.go

FROM alpine:latest

RUN addgroup -g 2000 -S paasopt && adduser paasopt -D -G paasopt -u 2000

COPY --from=builder /build/ingress .
RUN chown paasopt:paasopt ./ingress

USER 2000

ENTRYPOINT ["./ingress"]
8 changes: 8 additions & 0 deletions components/ingress/Dockerfile-ingress-noroot
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/ingress:v1.0.6
RUN addgroup -g 2000 -S paasopt && adduser paasopt -D -G paasopt -u 2000

RUN chown paasopt:paasopt ./ingress

USER 2000

ENTRYPOINT ["./ingress"]
1 change: 1 addition & 0 deletions downloaded_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name": "opensandbox", "version": "1.0", "type": "demo"}
3 changes: 3 additions & 0 deletions downloaded_readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# OpenSandbox 测试文档

这是一个测试文件,用于演示文件上传功能。
68 changes: 68 additions & 0 deletions server/Dockerfile-noroot
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Copyright 2026 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

FROM python:3.10-slim AS builder

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_PROJECT_ENV=/app/.venv \
UV_LINK_MODE=copy

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*

RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:/root/.cargo/bin:${PATH}"

COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project

COPY src ./src
COPY LICENSE README.md README_zh.md example.config.toml example.config.zh.toml \
example.config.k8s.toml example.config.k8s.zh.toml example.batchsandbox-template.yaml ./
Comment on lines +35 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fix invalid server paths in non-root Dockerfile

This Dockerfile copies src and several example.* files from the server/ root, but those paths do not exist in this repository (server/ contains opensandbox_server/ and examples under opensandbox_server/examples/). As written, image builds fail during COPY before runtime, so the new non-root server image cannot be produced.

Useful? React with 👍 / 👎.


# Install the project itself into the venv (deps already synced)
RUN uv pip install --no-deps --editable .

FROM python:3.10-slim AS runtime

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_PROJECT_ENV=/app/.venv \
PATH="/app/.venv/bin:${PATH}" \
SANDBOX_CONFIG_PATH=/etc/opensandbox/config.toml

RUN groupadd -g 2000 paasopt \
&& useradd -u 2000 -g paasopt -m -s /bin/bash paasopt

WORKDIR /app

COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/src /app/src
COPY --from=builder /app/example.config.k8s.toml /etc/opensandbox/config.toml
COPY --from=builder /app/example.config.k8s.zh.toml /etc/opensandbox/config.zh.toml
COPY --from=builder /app/example.batchsandbox-template.yaml /etc/opensandbox/example.batchsandbox-template.yaml

RUN chown paasopt:paasopt -R /etc/opensandbox/ && chown paasopt:paasopt -R /app/

EXPOSE 8080
USER 2000

ENTRYPOINT ["opensandbox-server"]
CMD ["--config", "/etc/opensandbox/config.toml"]
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from opensandbox_server.services.k8s.workload_provider import WorkloadProvider
from opensandbox_server.services.runtime_resolver import SecureRuntimeResolver


logger = logging.getLogger(__name__)


Expand Down
4 changes: 3 additions & 1 deletion server/opensandbox_server/services/k8s/provider_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
build_security_context_for_sandbox_container,
prep_execd_init_for_egress,
)
from opensandbox_server.services.k8s.resource_utils import calculate_resource_requests
from opensandbox_server.services.k8s.security_context import (
build_security_context_from_dict,
serialize_security_context_to_dict,
Expand Down Expand Up @@ -163,9 +164,10 @@ def _build_main_container(
translated_limits = _translate_resource_limits_for_k8s(resource_limits)
resources = None
if translated_limits:
requests = calculate_resource_requests(translated_limits, fraction=0.25)
resources = V1ResourceRequirements(
limits=translated_limits,
requests=translated_limits,
requests=requests,
)

volume_mounts = [
Expand Down
128 changes: 128 additions & 0 deletions server/opensandbox_server/services/k8s/resource_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Copyright 2025 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Kubernetes resource calculation utilities.
"""

import re
from typing import Dict


def calculate_resource_requests(limits: Dict[str, str], fraction: float = 0.25) -> Dict[str, str]:
"""
Calculate resource requests based on limits.

Args:
limits: Resource limits dict (e.g., {"cpu": "1", "memory": "1Gi"})
fraction: Fraction of limits to use for requests (default: 0.25 = 1/4)

Returns:
Resource requests dict with CPU and memory set to fraction of limits.
Other resources (e.g., gpu) remain unchanged.
"""
requests = {}

for resource, value in limits.items():
if resource in ("cpu", "memory"):
requests[resource] = scale_resource_value(value, fraction)
else:
# For other resources (e.g., gpu), keep the same value
requests[resource] = value

return requests


def scale_resource_value(value: str, factor: float) -> str:
"""
Scale a resource value by the given factor.

Args:
value: Resource value string (e.g., "500m", "1Gi", "2")
factor: Scaling factor (e.g., 0.25 for 1/4)

Returns:
Scaled resource value string
"""
# Parse CPU values
if isinstance(value, (int, float)):
scaled = float(value) * factor
# Return integer if whole number, otherwise float with reasonable precision
if scaled == int(scaled):
return str(int(scaled))
return f"{scaled:.3g}"

value_str = str(value)

# Handle CPU millicores (e.g., "500m")
cpu_match = re.match(r'^(\d+(?:\.\d+)?)m$', value_str)
if cpu_match:
millicores = float(cpu_match.group(1))
scaled_millicores = millicores * factor
# Return as integer millicores if >= 1, otherwise use cores
if scaled_millicores >= 1:
return f"{int(round(scaled_millicores))}m"
else:
# Convert to cores (e.g., 0.25)
cores = scaled_millicores / 1000
return f"{cores:.3g}"
Comment on lines +77 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clamp scaled CPU requests to Kubernetes-valid precision

When a CPU limit is small (for example 1m), this branch converts the scaled value to cores and can emit values below 0.001 CPU (for example 0.00025). Kubernetes rejects CPU quantities with precision finer than 1m, so workloads with low CPU limits can now fail admission even though the original limit was valid.

Useful? React with 👍 / 👎.


# Handle CPU cores without unit (e.g., "1", "0.5")
cpu_cores_match = re.match(r'^(\d+(?:\.\d+)?)$', value_str)
if cpu_cores_match:
cores = float(cpu_cores_match.group(1))
scaled_cores = cores * factor
if scaled_cores == int(scaled_cores):
return str(int(scaled_cores))
return f"{scaled_cores:.3g}"

# Handle memory with units (e.g., "512Mi", "1Gi", "1024Ki")
memory_match = re.match(r'^(\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti|Pi|Ei)$', value_str)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse decimal-SI memory units when scaling requests

The scaler only matches binary memory suffixes (Ki..Ei), so valid Kubernetes decimal-SI values like 2G or 512M bypass scaling and are returned unchanged. After this commit, those requests stay equal to limits while other memory formats are reduced to 25%, creating inconsistent scheduling/quota behavior for equivalent inputs.

Useful? React with 👍 / 👎.

if memory_match:
amount = float(memory_match.group(1))
unit = memory_match.group(2)
scaled_amount = amount * factor

# Try to keep the same unit if result is >= 1
if scaled_amount >= 1:
if scaled_amount == int(scaled_amount):
return f"{int(scaled_amount)}{unit}"
return f"{scaled_amount:.0f}{unit}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve fractional memory when scaling requests

This formatting rounds scaled memory to a whole unit whenever the scaled value is >= 1 (for example, scaling 5Gi by 0.25 yields 1Gi instead of 1.25Gi), which materially deviates from the documented fraction behavior and can over- or under-request memory depending on the input. That distorts scheduling and quota behavior compared to the intended 25% request rule.

Useful? React with 👍 / 👎.

else:
# Convert to smaller unit
unit_map = {"Ki": 1024, "Mi": 1024, "Gi": 1024, "Ti": 1024, "Pi": 1024, "Ei": 1024}
units = ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei"]
current_idx = units.index(unit)

# Convert to next smaller unit
if current_idx > 0:
smaller_unit = units[current_idx - 1]
converted_amount = scaled_amount * unit_map[unit]
if converted_amount == int(converted_amount):
return f"{int(converted_amount)}{smaller_unit}"
return f"{converted_amount:.0f}{smaller_unit}"
else:
# Already at smallest unit (Ki), return as is
return f"{scaled_amount:.0f}{unit}"

# Handle plain numbers (no unit)
try:
num_value = float(value_str)
scaled_value = num_value * factor
if scaled_value == int(scaled_value):
return str(int(scaled_value))
return f"{scaled_value:.3g}"
except ValueError:
# If parsing fails, return original value
return value_str
3 changes: 3 additions & 0 deletions test_download.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
这是从沙箱下载的文件
Download test successful
文件下载功能演示
Loading