Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support WASM external transformers using Wazero runtime #1172

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions go.mod
Original file line number Diff line number Diff line change
@@ -11,8 +11,8 @@ require (
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/antchfx/xmlquery v1.3.12
github.com/antchfx/xpath v1.2.1
github.com/argoproj/argo-cd/v2 v2.8.4
github.com/argoproj/argo-rollouts v1.6.0
github.com/argoproj/argo-cd/v2 v2.6.11
github.com/argoproj/argo-rollouts v1.2.2
github.com/cloudfoundry-community/go-cfclient/v2 v2.0.0
github.com/cloudfoundry/bosh-cli v6.4.1+incompatible
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5
@@ -41,13 +41,14 @@ require (
github.com/spf13/viper v1.10.1
github.com/tektoncd/pipeline v0.31.1-0.20220112162203-fcca72712ce7
github.com/tektoncd/triggers v0.18.0
github.com/tetratelabs/wazero v1.7.0
github.com/whilp/git-urls v1.0.0
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673
go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd
golang.org/x/crypto v0.10.0
golang.org/x/mod v0.9.0
golang.org/x/text v0.10.0
google.golang.org/grpc v1.57.0
google.golang.org/grpc v1.52.0-dev
google.golang.org/protobuf v1.31.0
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
gopkg.in/yaml.v3 v3.0.1
@@ -61,7 +62,7 @@ require (
// exclude github.com/chai2010/gettext-go v1.0.2

require (
cloud.google.com/go/compute v1.19.1 // indirect
cloud.google.com/go/compute v1.14.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
code.cloudfoundry.org/bytefmt v0.0.0-20211005130812-5bb3c17173e5 // indirect
contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d // indirect
@@ -82,16 +83,15 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/blendle/zapdriver v1.3.1 // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect
github.com/bombsimon/logrusr/v2 v2.0.1 // indirect
github.com/bradleyfalzon/ghinstallation/v2 v2.5.0 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/charlievieth/fs v0.0.2 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cloudfoundry/bosh-utils v0.0.296 // indirect
github.com/containerd/containerd v1.7.5 // indirect
github.com/containerd/containerd v1.6.24 // indirect
github.com/containerd/typeurl v1.0.2 // indirect
github.com/cppforlife/go-patch v0.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
@@ -122,22 +122,23 @@ require (
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-redis/cache/v9 v9.0.0 // indirect
github.com/go-redis/cache/v8 v8.4.2 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/goccy/go-yaml v1.9.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/gnostic v0.6.9 // indirect
github.com/google/go-containerregistry v0.14.0 // indirect
github.com/google/go-containerregistry v0.8.1-0.20220414143355-892d7a808387 // indirect
github.com/google/go-github/v53 v53.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
@@ -173,6 +174,8 @@ require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.7.0 // indirect
github.com/onsi/gomega v1.25.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
github.com/opencontainers/runc v1.1.5 // indirect
@@ -186,7 +189,6 @@ require (
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/prometheus/statsd_exporter v0.21.0 // indirect
github.com/redis/go-redis/v9 v9.0.5 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/russross/blackfriday v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -211,20 +213,19 @@ require (
go.uber.org/goleak v1.1.12 // indirect
go.uber.org/multierr v1.7.0 // indirect
go.uber.org/zap v1.20.0 // indirect
golang.org/x/exp v0.0.0-20210901193431-a062eea981d2 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/oauth2 v0.9.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/term v0.9.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.7.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/api v0.114.0 // indirect
google.golang.org/api v0.103.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
@@ -241,7 +242,6 @@ require (
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
knative.dev/networking v0.0.0-20220412163509-1145ec58c8be // indirect
knative.dev/pkg v0.0.0-20220412134708-e325df66cb51 // indirect
oras.land/oras-go/v2 v2.2.0 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/kustomize/api v0.12.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
119 changes: 45 additions & 74 deletions go.sum

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions transformer/external/executabletransformer.go
Original file line number Diff line number Diff line change
@@ -124,7 +124,6 @@ func (t *Executable) DirectoryDetect(dir string) (services map[string][]transfor
containerDetectOutputPath = env.Value
}
services, err = t.executeDetect(
t.ExecConfig.DirectoryDetectCMD,
containerDetectInputPath,
containerDetectOutputPath,
)
@@ -199,7 +198,6 @@ func (t *Executable) Transform(newArtifacts []transformertypes.Artifact, already
}

func (t *Executable) executeDetect(
cmd environmenttypes.Command,
inputPath string,
outputPath string,
) (services map[string][]transformertypes.Artifact, err error) {
287 changes: 287 additions & 0 deletions transformer/external/wasmtransformer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
/*
* Copyright IBM Corporation 2021
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package external

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"

"github.com/konveyor/move2kube/common"
"github.com/konveyor/move2kube/environment"
transformertypes "github.com/konveyor/move2kube/types/transformer"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
core "k8s.io/kubernetes/pkg/apis/core"
)

const (
wasmEnvDelimiter = "="
detectInputPathWASMEnvKey = "M2K_DETECT_INPUT_PATH"
detectOutputPathWASMEnvKey = "M2K_DETECT_OUTPUT_PATH"
transformInputPathWASMEnvKey = "M2K_TRANSFORM_INPUT_PATH"
transformOutputPathWASMEnvKey = "M2K_TRANSFORM_OUTPUT_PATH"
)

// WASM implements wasm transformer interface and is used for wasm based transformers
type WASM struct {
Config transformertypes.Transformer
Env *environment.Environment
WASMConfig *WASMYamlConfig
}

// WASMYamlConfig is the format of wasm transformer yaml config
type WASMYamlConfig struct {
WASMModule string `yaml:"wasm_module"`
CompileAOT bool `yaml:"compile_aot"`
EnvList []core.EnvVar `yaml:"env,omitempty"`
}

// Init Initializes the transformer
func (t *WASM) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) {
t.Config = tc
t.Env = env
t.WASMConfig = &WASMYamlConfig{}
if err := common.GetObjFromInterface(t.Config.Spec.Config, t.WASMConfig); err != nil {
return fmt.Errorf("unable to load config for Transformer %+v into %T . Error: %w", t.Config.Spec.Config, t.WASMConfig, err)
}

return nil
}

func (t *WASM) prepareEnv() []string {
envList := []string{}
for _, env := range t.WASMConfig.EnvList {
envList = append(envList, env.Name+wasmEnvDelimiter+env.Value)
}
return envList
}

// GetConfig returns the transformer config
func (t *WASM) GetConfig() (transformertypes.Transformer, *environment.Environment) {
return t.Config, t.Env
}

func unpack(combinedResult uint64) (ptr uint32, size uint32) {
// The pointer is the upper 32 bits of the combinedResult.
ptr = uint32(combinedResult >> 32)
// The size is the lower 32 bits of the combinedResult.
size = uint32(combinedResult)
return ptr, size
}

func pack(pointer uint32, size uint32) uint64 {
return (uint64(pointer) << 32) | uint64(size)
}

// DirectoryDetect runs detect in each sub directory
func (t *WASM) DirectoryDetect(dir string) (map[string][]transformertypes.Artifact, error) {
mod, ctx, rt, err := t.initVm([][]string{{dir, dir}})
if err != nil {
return nil, fmt.Errorf("failed to initialize WASM VM: %w", err)
}
defer rt.Close(ctx)
directoryDetectFunc := mod.ExportedFunction("directoryDetect")
malloc := mod.ExportedFunction("malloc")
free := mod.ExportedFunction("free")

allocateResult, err := malloc.Call(ctx, uint64(len(dir)+1))
if err != nil {
return nil, fmt.Errorf("failed to alloc memory for directory: %w", err)
}
dirPointer := int32(allocateResult[0])

defer free.Call(ctx, uint64(dirPointer))

dirBytes := []byte(dir)
dirPointerSize := uint32(len(dirBytes))

if !mod.Memory().Write(uint32(dirPointer), dirBytes) {
return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d",
dirPointer, len(dir)+1, mod.Memory().Size())
}

packedPointerSize := pack(uint32(dirPointer), dirPointerSize)
directoryDetectResultPtrSize, err := directoryDetectFunc.Call(ctx, uint64(packedPointerSize))
if err != nil {
return nil, fmt.Errorf("failed to execute directory detect function: %w", err)
}

directoryDetectResultPtr, directoryDetectResultSize := unpack(directoryDetectResultPtrSize[0])

bytes, ok := mod.Memory().Read(directoryDetectResultPtr, directoryDetectResultSize)
if !ok {
return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d",
directoryDetectResultPtr, directoryDetectResultSize, mod.Memory().Size())
}
services := map[string][]transformertypes.Artifact{}
err = json.Unmarshal(bytes, &services)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal directoryDetect output: %w", err)
}
return services, nil
}

// Transform transforms the artifacts
func (t *WASM) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) {
pathMappings := []transformertypes.PathMapping{}
createdArtifacts := []transformertypes.Artifact{}
data := make(map[string]interface{})
data["newArtifacts"] = newArtifacts
data["oldArtifacts"] = alreadySeenArtifacts
dataByt, err := json.Marshal(data)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal transform input: %w", err)
}
dataStr := string(dataByt)

preopens := []string{}
for _, artifact := range newArtifacts {
for _, paths := range artifact.Paths {
preopens = append(preopens, paths...)
}
}

sort.Slice(preopens, func(i, j int) bool {
l1, l2 := len(preopens[i]), len(preopens[j])
if l1 != l2 {
return l1 < l2
}
return preopens[i] < preopens[j]
})

deduplicatedPreopens := []string{}
for _, path := range preopens {
shouldSkip := false
for _, existingPath := range deduplicatedPreopens {
if strings.HasPrefix(path, existingPath) {
shouldSkip = true
break
}
}

if !shouldSkip {
deduplicatedPreopens = append(deduplicatedPreopens, path)
}
}

finalPreopens := [][]string{}
for _, path := range deduplicatedPreopens {
finalPreopens = append(finalPreopens, []string{path, path})
}

mod, ctx, rt, err := t.initVm(finalPreopens)
if err != nil {
return nil, nil, fmt.Errorf("failed to initialize WASM VM: %w", err)
}
defer rt.Close(ctx)

transformFunc := mod.ExportedFunction("transform")
malloc := mod.ExportedFunction("malloc")
free := mod.ExportedFunction("free")

allocateResult, err := malloc.Call(ctx, uint64(len(dataStr)+1))
if err != nil {
return nil, nil, fmt.Errorf("failed to alloc memory for directory: %w", err)
}
dataPointer := allocateResult[0]
defer free.Call(ctx, dataPointer)

dataBytes := []byte(dataStr)
dataPointerSize := uint32(len(dataBytes))

if !mod.Memory().Write(uint32(dataPointer), []byte(dataStr)) {
return nil, nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d",
dataPointer, len(dataStr)+1, mod.Memory().Size())
}

packedPointerSize := pack(uint32(dataPointer), dataPointerSize)

transformResultPtrSize, err := transformFunc.Call(ctx, packedPointerSize)
if err != nil {
return nil, nil, fmt.Errorf("failed to execute transform function: %w", err)
}

transformResultPtr := uint32(transformResultPtrSize[0] >> 32)
transformResultSize := uint32(transformResultPtrSize[0])

if transformResultPtr != 0 {
defer func() error {
_, err := free.Call(ctx, uint64(transformResultPtr))
if err != nil {
return fmt.Errorf("failed to free pointer memory: %w", err)
}
return nil
}()
}

bytes, ok := mod.Memory().Read(uint32(transformResultPtr), uint32(transformResultSize))
if !ok {
return nil, nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d",
transformResultPtr, transformResultSize, mod.Memory().Size())
}

var output transformertypes.TransformOutput
err = json.Unmarshal(bytes, &output)
if err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal transformer output: %w", err)
}
pathMappings = append(pathMappings, output.PathMappings...)
createdArtifacts = append(createdArtifacts, output.CreatedArtifacts...)
return pathMappings, createdArtifacts, nil
}

func (t *WASM) initVm(preopens [][]string) (api.Module, context.Context, wazero.Runtime, error) {
ctx := context.Background()
var rt wazero.Runtime
if t.WASMConfig.CompileAOT {
rt = wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigCompiler())
} else {
rt = wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigInterpreter())
}
fsconfig := wazero.NewFSConfig()
for i := range preopens {
fsconfig = wazero.NewFSConfig().WithDirMount(preopens[i][0], preopens[i][1])
}

config := wazero.NewModuleConfig().
WithStdout(os.Stdout).WithStderr(os.Stderr).WithFSConfig(fsconfig)

envVars := t.prepareEnv()
for _, envVar := range envVars {
keyValue := strings.SplitN(envVar, wasmEnvDelimiter, 2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we joining the key and value with = delimiter and then splitting again?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also fix the DCO check by signing

if len(keyValue) == 2 {
config = config.WithEnv(keyValue[0], keyValue[1])
}
}

wasi_snapshot_preview1.MustInstantiate(ctx, rt)
wasmBinary, err := os.ReadFile(filepath.Join(t.Env.GetEnvironmentContext(), t.WASMConfig.WASMModule))
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to open wasm file: %w", err)
}
mod, err := rt.InstantiateWithConfig(ctx, wasmBinary, config)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to instantiate wasm binary with the given config: %w", err)
}
return mod, ctx, rt, nil
}
1 change: 1 addition & 0 deletions transformer/transformer.go
Original file line number Diff line number Diff line change
@@ -90,6 +90,7 @@ var (

func init() {
transformerObjs := []Transformer{
new(external.WASM),
new(external.Starlark),
new(external.Executable),