Files
sless/doc/decisions/go_runtime_modules.md
T
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

11 KiB
Raw Blame History

Решение: поддержка пользовательских 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/fnsless/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/fnsless/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