go.work replace rule requires explicit require directive in server/go.mod. Patch appended at kaniko build time - no base image rebuild needed.
263 lines
11 KiB
Markdown
263 lines
11 KiB
Markdown
# Решение: поддержка пользовательских 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`
|