# Решение: поддержка пользовательских go.mod в Go runtime Создано: 2026-03-22 --- ## Проблема Сейчас Go runtime (`runtimes/go1.23/`) устроен так: ``` /app/ ← корень модуля sless/fn ├── go.mod ← module sless/fn ├── go.sum ├── server.go ← package main, import "sless/fn/handler" └── handler/ ← пользовательский код (копируется kaniko) └── handler.go ← package handler, func Handle(...) ``` `server.go` импортирует `sless/fn/handler` — это просто **поддиректория** внутри того же модуля `sless/fn`. Go собирает всё как единый модуль. Если пользователь кладёт в zip свой `go.mod` — он попадает в `/app/handler/go.mod`. Go не допускает вложенные модули (nested modules в одной сборке), поэтому: - `go build` игнорирует `handler/go.mod` - пользовательские `require` не работают - пользователь может использовать ТОЛЬКО зависимости из runtime-образа (`pgx/v5`) --- ## Анализ вариантов ### Вариант A: Go Workspaces + replace (выбранный) ``` /app/ ├── go.work ← генерируется в Dockerfile (kaniko) ├── server/ ← in base image │ ├── go.mod ← module sless/fn/server │ ├── go.sum │ └── server.go ← package main, import "sless/fn/handler" └── handler/ ← user code (copied by kaniko) ├── go.mod ← ЛЮБОЙ module name (или генерируем если нет) ├── go.sum ← пользовательский └── handler.go ← package handler, func Handle(...) ``` `go.work`: ``` go 1.23 use ./server use ./handler replace sless/fn/handler => ./handler ``` **Ключевое:** `replace sless/fn/handler => ./handler` в go.work позволяет `server.go` импортировать `sless/fn/handler` **независимо от того как пользователь назвал свой модуль**. `go build ./server` компилирует всё через workspace. **Плюсы:** идиоматичный Go; минимальные изменения в server.go; пользователь не обязан соблюдать соглашение по имени модуля. **Минусы:** go.work нужно генерировать в Dockerfile; нельзя тривиально кешировать слои. --- ### Вариант B: Слияние go.mod Во время `PrepareContext` парсим go.mod пользователя, берём из него `require`-строки, добавляем их в runtime go.mod, при билде `go get` стягивает зависимости. **Минус:** `go get` в kaniko требует сетевого доступа к proxy.golang.org (возможно ограничен); сложный парсинг go.mod вручную; риск конфликтов версий. --- ### Вариант C: Server.go копируется В модуль пользователя Пользователь предоставляет полноценный модуль, kaniko копирует `server.go` внутрь, вызывает `go build`. Пользователь объявляет package `handler` сам. **Минус:** ломает текущий интерфейс; пользователь должен знать детали runtime. --- ## Выбранное решение: Вариант A (go.work + replace) --- ## Что нужно изменить ### 1. `runtimes/go1.23/` — реструктуризация **Сейчас:** ``` runtimes/go1.23/ ├── Dockerfile ├── go.mod ← module sless/fn ├── go.sum └── server.go ``` **Станет:** ``` runtimes/go1.23/ ├── Dockerfile ← unchanged: собирает base image ├── server/ │ ├── go.mod ← module sless/fn/server (БЫЛО: sless/fn) │ ├── go.sum │ └── server.go ← unchanged: import "sless/fn/handler" └── README.md ← описание интерфейса для пользователей ``` Изменения: - Создать папку `server/`, перенести `go.mod`, `go.sum`, `server.go` - В `go.mod` переименовать модуль: `sless/fn` → `sless/fn/server` - `Dockerfile` базового образа: копировать `server/` в образ целиком --- ### 2. `internal/builder/context.go` — функция `generateDockerfile` **Сейчас** (go1.23): ```dockerfile FROM naeel/sless-runtime-go1.23:v0.1.2 AS builder WORKDIR /app COPY . /app/handler/ RUN CGO_ENABLED=0 go build -o /server . FROM alpine:3.20 COPY --from=builder /server /server EXPOSE 8080 CMD ["/server"] ``` **Станет** (go1.23): ```dockerfile FROM naeel/sless-runtime-go1.23:v0.1.3 AS builder WORKDIR /app COPY . /app/handler/ # Генерируем go.mod для handler если его нет (стандартное имя нужно для go.work) RUN [ -f /app/handler/go.mod ] || (echo 'module sless/fn/handler\n\ngo 1.23' > /app/handler/go.mod) # Генерируем go.work: use ./server + use ./handler + replace RUN printf 'go 1.23\n\nuse ./server\nuse ./handler\n\nreplace sless/fn/handler => ./handler\n' > /app/go.work RUN CGO_ENABLED=0 GOFLAGS=-mod=mod go build -o /server ./server FROM alpine:3.20 COPY --from=builder /server /server EXPOSE 8080 CMD ["/server"] ``` Изменения в `generateDockerfile()` для `case "go1.23"`: - Обновить referencer базового образа на `v0.1.3` - Добавить RUN-шаги: генерация `go.mod` (если нет), генерация `go.work` - `go build` теперь ссылается на `./server` а не на `.` При наличии у пользователя `go.mod`: используем его (любое имя модуля), `replace` в `go.work` обеспечит resolve import `sless/fn/handler` → `./handler`. Флаг `hasGoMod` в `PrepareContext` остаётся — влияет только на то, нужен ли RUN для генерации `go.mod` в Dockerfile. --- ### 3. Базовый образ `naeel/sless-runtime-go1.23` — v0.1.3 Образ изменится: теперь он содержит `server/` с `go.mod`, `go.sum`, `server.go` вместо этих файлов в корне `/app/`. Сборка образа: ``` cd runtimes/go1.23 docker build -t naeel/sless-runtime-go1.23:v0.1.3 . docker push naeel/sless-runtime-go1.23:v0.1.3 ``` **Важно:** `go.sum` для `server/` нужно обновить под новый `go.mod`. Зависимости `server/go.mod` от pgx остаются — это зависимости runtime, не пользователя. Пользователь может добавить pgx в свой go.mod или не добавлять. --- ### 4. Обновить пример `examples/hello-go` (когда будет воссоздан) Два варианта пользовательского кода: **Без зависимостей** (go.mod не нужен): ```go // handler.go package handler func Handle(event map[string]interface{}) interface{} { return map[string]interface{}{"hello": "world"} } ``` → builder сам сгенерирует минимальный `go.mod` **С зависимостями** (например, pgx напрямую): ``` zip: ├── handler.go ├── go.mod ← module myfunction (любое имя!) └── go.sum ``` ```go // go.mod module myfunction go 1.23 require github.com/jackc/pgx/v5 v5.7.2 ``` → `go.work` с `replace` подхватит этот модуль как `sless/fn/handler` --- ## Порядок выполнения | # | Шаг | Файл | Сложность | |---|-----|------|-----------| | 1 | Создать `runtimes/go1.23/server/`, перенести файлы | `runtimes/go1.23/` | низкая | | 2 | Переименовать модуль в go.mod: `sless/fn` → `sless/fn/server` | `runtimes/go1.23/server/go.mod` | минимальная | | 3 | Обновить `Dockerfile` базового образа | `runtimes/go1.23/Dockerfile` | минимальная | | 4 | Собрать и запушить base image `v0.1.3` | docker push | ~5 мин | | 5 | Обновить `generateDockerfile` go1.23 case | `internal/builder/context.go` | средняя | | 6 | Обновить `runtimeBaseImage` на `v0.1.3` | `internal/builder/context.go` | минимальная | | 7 | Написать unit-тест для нового Dockerfile | `internal/builder/context_test.go` | низкая | | 8 | Обновить `Makefile` / `hack/` если есть правила сборки runtime | `Makefile` | проверить | | 9 | Собрать и выкатить новый operator image | docker build + push | ~10 мин | | 10 | Smoke-test: загрузить zip с `require pgx/v5` → Ready → invoke | bash | ~5 мин | --- ## Что НЕ меняется - Интерфейс пользователя: `func Handle(event map[string]interface{}) interface{}` - `server.go` (package main) — не трогаем - `SLESS_MODE=job` логика — не трогаем - Python 3.11, Node.js 20 runtime — не трогаем - Operator API, контроллеры — не трогаем - Текущая версия образа v0.1.2 продолжает работать для существующих сборок (если не пересобирать) --- ## Риски | Риск | Вероятность | Митигация | |------|-------------|-----------| | `go build` не находит зависимости (нет сети в kaniko) | Средняя | GOPROXY=proxy.golang.org доступен; pgx уже в go.sum сервера | | Конфликт версий (пользователь требует другую версию pgx) | Низкая | Workspace не разделяет зависимости; конфликт только при прямом импорте из server/ | | `go.sum` user кода отсутствует (нет go.sum при commit) | Высокая | Использовать `GONOSUMCHECK=*` или `GOFLAGS=-mod=mod` в Dockerfile | | Увеличение времени сборки (go mod download) | Средняя | Первые сборки медленнее; кеш proxy.golang.org помогает | --- ## Связанные файлы - `runtimes/go1.23/server.go` - `runtimes/go1.23/go.mod` - `runtimes/go1.23/Dockerfile` - `internal/builder/context.go` (функции `generateDockerfile`, `runtimeBaseImage`) - `internal/builder/context_test.go`