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:
Executable
+32
@@ -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}'"
|
||||
+61
-25
@@ -21,38 +21,41 @@ import (
|
||||
|
||||
// Builder — управляет сборкой Docker образов через kaniko Jobs в k8s.
|
||||
type Builder struct {
|
||||
client client.Client
|
||||
builderImage string // образ kaniko
|
||||
registryHost string // куда пушим образ
|
||||
s3Endpoint string // откуда kaniko берёт код
|
||||
s3AccessKey string
|
||||
s3SecretKey string
|
||||
s3Bucket string
|
||||
namespace string // namespace где запускаем build Job'ы
|
||||
client client.Client
|
||||
builderImage string // образ kaniko
|
||||
registryHost string // куда пушим образ
|
||||
registrySecret string // имя k8s Secret с docker-кредами для kaniko
|
||||
s3Endpoint string // откуда kaniko берёт код
|
||||
s3AccessKey string
|
||||
s3SecretKey string
|
||||
s3Bucket string
|
||||
namespace string // namespace где запускаем build Job'ы
|
||||
}
|
||||
|
||||
// Config — параметры для создания Builder'а.
|
||||
type Config struct {
|
||||
BuilderImage string
|
||||
RegistryHost string
|
||||
S3Endpoint string
|
||||
S3AccessKey string
|
||||
S3SecretKey string
|
||||
S3Bucket string
|
||||
Namespace string
|
||||
BuilderImage string
|
||||
RegistryHost string
|
||||
RegistrySecret string // имя k8s Secret с .dockerconfigjson для пуша образов
|
||||
S3Endpoint string
|
||||
S3AccessKey string
|
||||
S3SecretKey string
|
||||
S3Bucket string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// New создаёт новый Builder.
|
||||
func New(c client.Client, cfg Config) *Builder {
|
||||
return &Builder{
|
||||
client: c,
|
||||
builderImage: cfg.BuilderImage,
|
||||
registryHost: cfg.RegistryHost,
|
||||
s3Endpoint: cfg.S3Endpoint,
|
||||
s3AccessKey: cfg.S3AccessKey,
|
||||
s3SecretKey: cfg.S3SecretKey,
|
||||
s3Bucket: cfg.S3Bucket,
|
||||
namespace: cfg.Namespace,
|
||||
client: c,
|
||||
builderImage: cfg.BuilderImage,
|
||||
registryHost: cfg.RegistryHost,
|
||||
registrySecret: cfg.RegistrySecret,
|
||||
s3Endpoint: cfg.S3Endpoint,
|
||||
s3AccessKey: cfg.S3AccessKey,
|
||||
s3SecretKey: cfg.S3SecretKey,
|
||||
s3Bucket: cfg.S3Bucket,
|
||||
namespace: cfg.Namespace,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +99,7 @@ func (b *Builder) Build(ctx context.Context, namespace, funcName, s3Key string)
|
||||
Args: []string{
|
||||
"--context=" + s3ContextURL,
|
||||
"--destination=" + imageRef,
|
||||
"--skip-tls-verify", // для внутреннего registry без TLS
|
||||
"--cache=true", // кешируем слои для ускорения повторных сборок
|
||||
"--cache=true", // кешируем слои для ускорения повторных сборок
|
||||
},
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "AWS_ACCESS_KEY_ID", Value: b.s3AccessKey},
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -118,13 +118,14 @@ func main() {
|
||||
|
||||
// Builder использует k8s client из manager'а
|
||||
bldr := builder.New(mgr.GetClient(), builder.Config{
|
||||
BuilderImage: cfg.BuilderImage,
|
||||
RegistryHost: cfg.RegistryHost,
|
||||
S3Endpoint: cfg.S3Endpoint,
|
||||
S3AccessKey: cfg.S3AccessKey,
|
||||
S3SecretKey: cfg.S3SecretKey,
|
||||
S3Bucket: cfg.S3Bucket,
|
||||
Namespace: "sless",
|
||||
BuilderImage: cfg.BuilderImage,
|
||||
RegistryHost: cfg.RegistryHost,
|
||||
RegistrySecret: cfg.RegistrySecret,
|
||||
S3Endpoint: cfg.S3Endpoint,
|
||||
S3AccessKey: cfg.S3AccessKey,
|
||||
S3SecretKey: cfg.S3SecretKey,
|
||||
S3Bucket: cfg.S3Bucket,
|
||||
Namespace: "sless",
|
||||
})
|
||||
|
||||
if err = (&controllers.FunctionReconciler{
|
||||
|
||||
@@ -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"]
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user