Последние пару недель я работал над Discoblock, нашим новым решением для конфигурирования декларативных томов в Kubernetes с открытым исходным кодом. Для получения более подробной информации вам следует прочесть мой пост. Итак, не вдаваясь в подробности, Discoblocks — это проект на Golang, и он должен поддерживать несколько валидаторов для различных вариантов облачных дисков. Я думаю, что эта область является одним из слабых мест Go, я имею в виду, что любой новый валидатор требует новой сборки бинарника. Легко признать, что это далеко не идеальный вариант, когда нам нужно поддерживать огромное количество облачных драйверов, не говоря уже о будущем.
Один из вариантов — компилировать бинарник с CGO_ENABLED=1
(поведение по умолчанию в любом случае) и загружать библиотеки — в данном случае валидаторы — динамически.
- + Встроенное решение
- — Только скомпилированные языки могут создавать
so
файлы - — Убедиться, что все файлы
so
находятся в контейнере, должно быть кошмаром, любой новый из них требует отладки запущенного контейнера - — Разработчики должны использовать некоторые Linux для разработки, по крайней мере для тестирования
Альтернативным решением в мире Kubernetes является создание небольшого unix socket-based HTTP сервиса для каждого Pod в качестве sidecar, но мы не хотели двигаться в этом направлении из-за сложности.
Третий вариант — подправить код Go для выполнения валидаторов как WebAssembly, точнее, WASI-модулей.
В чем разница между WASM и WASI? Если очень коротко, то WASM предназначен для веб и не поддерживает вызовы функций, что является ключевой особенностью, которая нам нужна.
- + Многие языки могут создавать WASI.
- + Модули WASI не нуждаются в дополнительных зависимостях
- + Это модно 🙂
- — Все еще нуждается в
CGO_ENABLED=1
, но зависит только от ограниченного числа библиотек - — Встроенная компиляция поддерживает только WASM, но не WASI
- Мы должны использовать TinyGo для компиляции модуля WASI
- Поддерживается только несколько типов ввода и вывода числовых данных (обходной путь будет найден позже)
- — Отсутствует встроенное выполнение WASI
- — Интеграция не является бесшовной
- Есть и другие неподдерживаемые функции, пожалуйста, следите за документацией TinyGo.
Время кода, мое любимое:
import (
"fmt"
"os"
"github.com/valyala/fastjson"
)
func main() {}
//export IsStorageClassValid
func IsStorageClassValid() {
json := []byte(os.Getenv("STORAGE_CLASS_JSON"))
if fastjson.Exists(json, "volumeBindingMode") && fastjson.GetString(json, "volumeBindingMode") != "WaitForFirstConsumer" {
fmt.Fprint(os.Stderr, "only volumeBindingMode WaitForFirstConsumer is supported")
fmt.Fprint(os.Stdout, false)
return
}
fmt.Fprint(os.Stdout, true)
}
- Fastjson хорошо работает в WASI, и это быстро 😉
- Пустая
func main() {}
необходима. - Функция не имеет входных параметров, чтобы избежать проблем с типами, вместо этого она считывает переменные окружения
- Нет возвращаемого значения по той же причине, что и одной строкой выше, вместо этого она пишет в стандартный вывод или ошибку
Давайте скомпилируем модуль.
go mod init
docker run -v $(PWD):/go/src/ebs.csi.aws.com -w /go/src/ebs.csi.aws.com tinygo/tinygo:0.23.0 bash -c "go mod tidy && tinygo build -o main.wasm -target wasi --no-debug main.go"
Пришло время реализовать другую сторону истории. Я нашел WebAssembly runtime для Go. Wasmer-go — это полная и зрелая среда выполнения WebAssembly для Go, основанная на Wasmer.
// DriversDir driver location, configure with -ldflags -X github.com/ondat/discoblocks/pkg/drivers.DriversDir=/yourpath
var DriversDir = "/drivers"
func init() {
files, err := os.ReadDir(filepath.Clean(DriversDir))
if err != nil {
log.Fatal(fmt.Errorf("unable to load drivers: %w", err))
}
for _, file := range files {
if !file.IsDir() {
continue
}
driverPath := fmt.Sprintf("%s/%s/main.wasm", DriversDir, file.Name())
if _, err := os.Stat(driverPath); err != nil {
log.Printf("unable to found main.wasm for %s: %s", file.Name(), err.Error())
continue
}
wasmBytes, err := os.ReadFile(filepath.Clean(driverPath))
if err != nil {
log.Fatal(fmt.Errorf("unable to load driver content for %s: %w", driverPath, err))
}
engine := wasmer.NewEngine()
store := wasmer.NewStore(engine)
module, err := wasmer.NewModule(store, wasmBytes)
if err != nil {
log.Fatal(fmt.Errorf("unable to compile module %s: %w", driverPath, err))
}
drivers[file.Name()] = &Driver{
store: store,
module: module,
}
}
}
var drivers = map[string]*Driver{}
// GetDriver returns given service
func GetDriver(name string) *Driver {
return drivers[name]
}
// Driver is the bridge to WASI modules
type Driver struct {
store *wasmer.Store
module *wasmer.Module
}
// IsStorageClassValid validates StorageClass
func (d *Driver) IsStorageClassValid(sc *storagev1.StorageClass) (bool, error) {
rawSc, err := json.Marshal(sc)
if err != nil {
return false, fmt.Errorf("unable to parse StorageClass: %w", err)
}
wasiEnv, instance, err := d.init(map[string]string{
"STORAGE_CLASS_JSON": string(rawSc),
})
if err != nil {
return false, fmt.Errorf("unable to init instance: %w", err)
}
isStorageClassValid, err := instance.Exports.GetRawFunction("IsStorageClassValid")
if err != nil {
return false, fmt.Errorf("unable to find IsStorageClassValid: %w", err)
}
_, err = isStorageClassValid.Native()()
if err != nil {
return false, fmt.Errorf("unable to call IsStorageClassValid: %w", err)
}
errOut := string(wasiEnv.ReadStderr())
if errOut != "" {
return false, fmt.Errorf("function error IsStorageClassValid: %s", errOut)
}
resp, err := strconv.ParseBool(string(wasiEnv.ReadStdout()))
if err != nil {
return false, fmt.Errorf("unable to parse output: %w", err)
}
return resp, nil
}
func (d *Driver) init(envs map[string]string) (*wasmer.WasiEnvironment, *wasmer.Instance, error) {
builder := wasmer.NewWasiStateBuilder("wasi-program").
CaptureStdout().CaptureStderr()
for k, v := range envs {
builder = builder.Environment(k, v)
}
wasiEnv, err := builder.Finalize()
if err != nil {
return nil, nil, fmt.Errorf("unable to build module: %w", err)
}
importObject, err := wasiEnv.GenerateImportObject(d.store, d.module)
if err != nil {
return nil, nil, fmt.Errorf("unable to generate imports: %w", err)
}
instance, err := wasmer.NewInstance(d.module, importObject)
if err != nil {
return nil, nil, fmt.Errorf("unable to create instance: %w", err)
}
start, err := instance.Exports.GetWasiStartFunction()
if err != nil {
return nil, nil, fmt.Errorf("unable to get start: %w", err)
}
_, err = start()
if err != nil {
return nil, nil, fmt.Errorf("unable to start instance: %w", err)
}
return wasiEnv, instance, nil
}
На стороне вызывающей стороны.
driver := drivers.GetDriver(storageClass.Provisioner)
if driver == nil {
return errors.New("driver not found")
}
valid, err := driver.IsStorageClassValid(&storageClass)
if err != nil {
return fmt.Errorf("failed to call driver: %w", err)
} else if !valid {
return fmt.Errorf("invalid StorageClass: %w", err)
}
Есть еще одна вещь: отправьте все в контейнерный образ.
FROM tinygo/tinygo:0.23.0 as drivers
COPY ebs.csi.aws.com/ /go/src/ebs.csi.aws.com
RUN cd /go/src/ebs.csi.aws.com ; go mod tidy && tinygo build -o main.wasm -target wasi --no-debug main.go
...
FROM redhat/ubi8-micro:8.6
COPY --from=drivers /go/src /drivers
COPY --from=builder /go/pkg/mod/github.com/wasmerio/wasmer-go@v1.0.4/wasmer/packaged/lib/linux-amd64/libwasmer.so /lib64
К вашему сведению, поскольку наш бинарник не компилируется статически в один бинарник, мы не можем использовать scratch
или distroless
образы в качестве основы.
Вот и все, друзья!!!