feat: python3.11 base runtime image + DockerHub registry support

- runtimes/python3.11/: Dockerfile + server.py (HTTP wrapper на :8080)
- builder.go: RegistrySecret field, volume mount docker-creds для kaniko, убран --skip-tls-verify
- config.go: добавлен RegistrySecret (env REGISTRY_SECRET, default sless-registry-auth)
- main.go: передаём RegistrySecret в builder.Config
- hack/create-registry-secret.sh: создаёт k8s Secret из DockerHub токена

Образ наружу: naeel/sless-runtime-python3.11:latest (DockerHub)
K8s secret: sless-registry-auth в namespace sless
This commit is contained in:
“Naeel”
2026-03-07 09:32:59 +04:00
parent 33ee17729f
commit c61e822308
6 changed files with 195 additions and 33 deletions
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Изменено: 2026-03-07
# Создаёт k8s Secret sless-registry-auth с DockerHub кредами для kaniko.
# Kaniko монтирует этот secret как /kaniko/.docker/config.json для push образов.
#
# Использование:
# DOCKER_HUB_USER=naeel DOCKER_HUB_TOKEN=<token> ./hack/create-registry-secret.sh
#
# Требуется: kubectl, авторизованный context с доступом к namespace sless.
set -euo pipefail
NAMESPACE="sless"
SECRET_NAME="sless-registry-auth"
if [[ -z "${DOCKER_HUB_USER:-}" ]]; then
echo "ERROR: DOCKER_HUB_USER env var is required"
exit 1
fi
if [[ -z "${DOCKER_HUB_TOKEN:-}" ]]; then
echo "ERROR: DOCKER_HUB_TOKEN env var is required (DockerHub access token, not password)"
exit 1
fi
kubectl create secret docker-registry "${SECRET_NAME}" \
--docker-username="${DOCKER_HUB_USER}" \
--docker-password="${DOCKER_HUB_TOKEN}" \
--docker-server="https://index.docker.io/v1/" \
--namespace="${NAMESPACE}" \
--dry-run=client -o yaml | kubectl apply -f -
echo "OK: secret '${SECRET_NAME}' applied in namespace '${NAMESPACE}'"
+37 -1
View File
@@ -24,6 +24,7 @@ type Builder struct {
client client.Client
builderImage string // образ kaniko
registryHost string // куда пушим образ
registrySecret string // имя k8s Secret с docker-кредами для kaniko
s3Endpoint string // откуда kaniko берёт код
s3AccessKey string
s3SecretKey string
@@ -35,6 +36,7 @@ type Builder struct {
type Config struct {
BuilderImage string
RegistryHost string
RegistrySecret string // имя k8s Secret с .dockerconfigjson для пуша образов
S3Endpoint string
S3AccessKey string
S3SecretKey string
@@ -48,6 +50,7 @@ func New(c client.Client, cfg Config) *Builder {
client: c,
builderImage: cfg.BuilderImage,
registryHost: cfg.RegistryHost,
registrySecret: cfg.RegistrySecret,
s3Endpoint: cfg.S3Endpoint,
s3AccessKey: cfg.S3AccessKey,
s3SecretKey: cfg.S3SecretKey,
@@ -96,7 +99,6 @@ func (b *Builder) Build(ctx context.Context, namespace, funcName, s3Key string)
Args: []string{
"--context=" + s3ContextURL,
"--destination=" + imageRef,
"--skip-tls-verify", // для внутреннего registry без TLS
"--cache=true", // кешируем слои для ускорения повторных сборок
},
Env: []corev1.EnvVar{
@@ -106,8 +108,11 @@ func (b *Builder) Build(ctx context.Context, namespace, funcName, s3Key string)
// Kaniko использует AWS SDK совместимый с S3 — задаём кастомный endpoint
{Name: "AWS_REGION", Value: "us-east-1"},
},
// Монтируем docker-креды из k8s Secret — kaniko читает авторизацию из /kaniko/.docker/config.json
VolumeMounts: b.dockerConfigMounts(),
},
},
Volumes: b.dockerConfigVolumes(),
},
},
},
@@ -152,4 +157,35 @@ func (b *Builder) Cleanup(ctx context.Context, jobName string) error {
return b.client.Delete(ctx, job, &client.DeleteOptions{PropagationPolicy: &propagation})
}
// dockerConfigVolumes возвращает Volume с docker-кредами если RegistrySecret задан.
// Ключ .dockerconfigjson монтируется как config.json — стандартное имя для kaniko.
func (b *Builder) dockerConfigVolumes() []corev1.Volume {
if b.registrySecret == "" {
return nil
}
return []corev1.Volume{
{
Name: "docker-config",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: b.registrySecret,
Items: []corev1.KeyToPath{
{Key: ".dockerconfigjson", Path: "config.json"},
},
},
},
},
}
}
// dockerConfigMounts возвращает VolumeMount для docker-кредов если RegistrySecret задан.
func (b *Builder) dockerConfigMounts() []corev1.VolumeMount {
if b.registrySecret == "" {
return nil
}
return []corev1.VolumeMount{
{Name: "docker-config", MountPath: "/kaniko/.docker"},
}
}
func int32Ptr(i int32) *int32 { return &i }
+12 -1
View File
@@ -26,9 +26,14 @@ type Config struct {
S3Bucket string
S3UseSSL bool
// Registry — адрес внутреннего Docker registry куда пушим собранные образы
// Registry — куда пушим собранные образы функций (DockerHub: "naeel", приватный: "host:port/prefix")
RegistryHost string
// RegistrySecret — имя k8s Secret типа kubernetes.io/dockerconfigjson
// Монтируется в kaniko Job для авторизации в registry.
// Создаётся через hack/create-registry-secret.sh
RegistrySecret string
// FunctionNamespace — префикс namespace для функций пользователей.
// Итоговый namespace: FunctionNamespacePrefix + "-" + functionName
FunctionNamespacePrefix string
@@ -98,6 +103,12 @@ func Load() (*Config, error) {
return nil, fmt.Errorf("REGISTRY_HOST is required")
}
// Secret с docker-кредами для kaniko (опционально — без него push в приватный registry упадёт)
cfg.RegistrySecret = os.Getenv("REGISTRY_SECRET")
if cfg.RegistrySecret == "" {
cfg.RegistrySecret = "sless-registry-auth"
}
// Опциональные параметры с дефолтами
if v := os.Getenv("FUNCTION_NAMESPACE_PREFIX"); v != "" {
cfg.FunctionNamespacePrefix = v
+1
View File
@@ -120,6 +120,7 @@ func main() {
bldr := builder.New(mgr.GetClient(), builder.Config{
BuilderImage: cfg.BuilderImage,
RegistryHost: cfg.RegistryHost,
RegistrySecret: cfg.RegistrySecret,
S3Endpoint: cfg.S3Endpoint,
S3AccessKey: cfg.S3AccessKey,
S3SecretKey: cfg.S3SecretKey,
+15
View File
@@ -0,0 +1,15 @@
# Изменено: 2026-03-07
# Base runtime image для Python 3.11 serverless функций.
# Содержит только HTTP-обёртку (server.py).
# Пользовательский код докапывается kaniko поверх этого образа: COPY . /app/function/
# Почему slim: минимальный размер образа, стандартная библиотека Python не нужна доп. пакетов.
FROM python:3.11-slim
WORKDIR /app
COPY server.py /app/server.py
EXPOSE 8080
CMD ["python", "/app/server.py"]
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
# Изменено: 2026-03-07
# HTTP-обёртка для serverless функций на Python 3.11.
# Загружает handler.py из /app/function/ и вызывает handle(event) на каждый запрос.
# Почему importlib: нужно загружать модуль из фиксированного пути вне sys.path.
import sys
import json
import importlib.util
from http.server import HTTPServer, BaseHTTPRequestHandler
HANDLER_PATH = "/app/function/handler.py"
PORT = 8080
def load_handler():
# Загружаем модуль пользователя динамически — путь известен только в runtime.
spec = importlib.util.spec_from_file_location("handler", HANDLER_PATH)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if not hasattr(module, "handle"):
raise AttributeError(f"handler.py must define a 'handle(event)' function")
return module.handle
_handle = load_handler()
class FunctionHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
# Логи в stderr чтобы не мешать stdout
sys.stderr.write("%s - [%s] %s\n" % (
self.address_string(),
self.log_date_time_string(),
format % args,
))
def do_GET(self):
if self.path == "/health":
self._respond(200, {"status": "ok"})
else:
self._respond(200, _handle({}))
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
try:
event = json.loads(body) if body else {}
except json.JSONDecodeError:
# Если тело не JSON — передаём как строку, не ломаем вызов
event = {"body": body.decode("utf-8", errors="replace")}
result = _handle(event)
self._respond(200, result)
def _respond(self, status, data):
body = json.dumps(data).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", PORT), FunctionHandler)
print(f"sless runtime (python3.11) listening on :{PORT}", flush=True)
server.serve_forever()