Files
sless/doc/decisions/go_runtime_modules.md
Naeel c762047234 fix(builder/go1.23): add require sless/fn/handler to server/go.mod at build time
go.work replace rule requires explicit require directive in server/go.mod.
Patch appended at kaniko build time - no base image rebuild needed.
2026-03-22 17:28:40 +03:00

263 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Решение: поддержка пользовательских 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`