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.
This commit is contained in:
Naeel
2026-03-22 17:28:40 +03:00
parent 088493b7e7
commit c762047234
5 changed files with 556 additions and 32 deletions
+262
View File
@@ -0,0 +1,262 @@
# Решение: поддержка пользовательских 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`