go.work replace rule requires explicit require directive in server/go.mod. Patch appended at kaniko build time - no base image rebuild needed.
11 KiB
Решение: поддержка пользовательских 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):
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):
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 не нужен):
// 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.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.goruntimes/go1.23/go.modruntimes/go1.23/Dockerfileinternal/builder/context.go(функцииgenerateDockerfile,runtimeBaseImage)internal/builder/context_test.go