Skip to content

Commit

Permalink
Init support for WASM plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
emcfarlane committed Sep 27, 2024
1 parent fd749ca commit d588903
Show file tree
Hide file tree
Showing 24 changed files with 738 additions and 31 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ require (
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
github.com/tetratelabs/wazero v1.8.0
go.lsp.dev/jsonrpc2 v0.10.0
go.lsp.dev/protocol v0.12.0
go.opentelemetry.io/otel v1.30.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down
7 changes: 7 additions & 0 deletions make/buf/all.mk
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ GO_TEST_BINS := $(GO_TEST_BINS) \
private/bufpkg/bufcheck/internal/cmd/buf-plugin-rpc-ext \
private/bufpkg/bufcheck/internal/cmd/buf-plugin-duplicate-category \
private/bufpkg/bufcheck/internal/cmd/buf-plugin-duplicate-rule
GO_TEST_WASM_BINS := $(GO_TEST_WASM_BINS) \
private/bufpkg/bufcheck/internal/cmd/buf-plugin-panic \
private/bufpkg/bufcheck/internal/cmd/buf-plugin-suffix \
private/bufpkg/bufcheck/internal/cmd/buf-plugin-protovalidate-ext \
private/bufpkg/bufcheck/internal/cmd/buf-plugin-rpc-ext \
private/bufpkg/bufcheck/internal/cmd/buf-plugin-duplicate-category \
private/bufpkg/bufcheck/internal/cmd/buf-plugin-duplicate-rule
GO_MOD_VERSION := 1.22
DOCKER_BINS := $(DOCKER_BINS) buf
FILE_IGNORES := $(FILE_IGNORES) \
Expand Down
18 changes: 17 additions & 1 deletion make/go/go.mk
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ GO_BINS ?=
# Settable
GO_TEST_BINS ?=
# Settable
GO_TEST_WASM_BINS ?=
# Settable
GO_GET_PKGS ?=
# Settable
GO_MOD_VERSION ?= 1.21
Expand Down Expand Up @@ -142,7 +144,7 @@ build: prebuild ## Run go build.
pretest::

.PHONY: test
test: pretest installtest ## Run all go tests.
test: pretest installtest installtestwasm ## Run all go tests.
go test $(GO_TEST_FLAGS) $(GOPKGS)

.PHONY: testrace
Expand Down Expand Up @@ -203,3 +205,17 @@ endef

$(foreach gobin,$(sort $(GO_TEST_BINS)),$(eval $(call gotestbinfunc,$(gobin))))
$(foreach gobin,$(sort $(GO_TEST_BINS)),$(eval FILE_IGNORES := $(FILE_IGNORES) $(gobin)/$(notdir $(gobin))))

.PHONY: installtestwasm
installtestwasm::

define gotestwasmfunc
.PHONY: installtestwasm$(notdir $(1))
installtestwasm$(notdir $(1)):
GOOS=wasip1 GOARCH=wasm go build -o $(GOBIN)/$(notdir $(1)).wasm ./$(1)

installtestwasm:: installtestwasm$(notdir $(1))
endef

$(foreach gobin,$(sort $(GO_TEST_WASM_BINS)),$(eval $(call gotestwasmfunc,$(gobin))))
$(foreach gobin,$(sort $(GO_TEST_WASM_BINS)),$(eval FILE_IGNORES := $(FILE_IGNORES) $(gobin)/$(notdir $(gobin))))
13 changes: 13 additions & 0 deletions private/buf/bufcli/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ var (
//
// Normalized.
v3CacheModuleLockRelDirPath = normalpath.Join("v3", "modulelocks")
//v3CachePluginsRelDirPath is the relative path to the plugins cache directory in its newest iteration.
//
// Normalized.
v3CachePluginsRelDirPath = normalpath.Join("v3", "plugins")
)

// NewModuleDataProvider returns a new ModuleDataProvider while creating the
Expand Down Expand Up @@ -135,6 +139,15 @@ func NewCommitProvider(container appext.Container) (bufmodule.CommitProvider, er
)
}

// CreatePluginCacheDir creates the cache directory for plugins.
func CreatePluginCacheDir(container appext.Container) (string, error) {
if err := createCacheDir(container.CacheDirPath(), v3CachePluginsRelDirPath); err != nil {
return "", err
}
fullCacheDirPath := normalpath.Join(container.CacheDirPath(), v3CachePluginsRelDirPath)
return fullCacheDirPath, nil
}

// newWKTStore returns a new bufwktstore.Store while creating the required cache directories.
func newWKTStore(container appext.Container) (bufwktstore.Store, error) {
if err := createCacheDir(container.CacheDirPath(), v3CacheWKTRelDirPath); err != nil {
Expand Down
7 changes: 6 additions & 1 deletion private/buf/buflsp/buflsp.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ func Serve(
}

tracer := tracing.NewTracer(container.Tracer())
checkClient, err := bufcheck.NewClient(container.Logger(), tracer, bufcheck.NewRunnerProvider(command.NewRunner()), bufcheck.ClientWithStderr(container.Stderr()))
checkClient, err := bufcheck.NewClient(
container.Logger(),
tracer,
bufcheck.NewRunnerProvider(command.NewRunner()),
bufcheck.ClientWithStderr(container.Stderr()),
)
if err != nil {
return nil, err
}
Expand Down
7 changes: 6 additions & 1 deletion private/buf/cmd/buf/command/breaking/breaking.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,12 @@ func run(
tracer := tracing.NewTracer(container.Tracer())
var allFileAnnotations []bufanalysis.FileAnnotation
for i, imageWithConfig := range imageWithConfigs {
client, err := bufcheck.NewClient(container.Logger(), tracer, bufcheck.NewRunnerProvider(command.NewRunner()), bufcheck.ClientWithStderr(container.Stderr()))
client, err := bufcheck.NewClient(
container.Logger(),
tracer,
bufcheck.NewRunnerProvider(command.NewRunner()),
bufcheck.ClientWithStderr(container.Stderr()),
)
if err != nil {
return err
}
Expand Down
24 changes: 23 additions & 1 deletion private/buf/cmd/buf/command/lint/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ import (
"github.com/bufbuild/buf/private/buf/bufctl"
"github.com/bufbuild/buf/private/bufpkg/bufanalysis"
"github.com/bufbuild/buf/private/bufpkg/bufcheck"
"github.com/bufbuild/buf/private/bufpkg/bufwasm"
"github.com/bufbuild/buf/private/pkg/app/appcmd"
"github.com/bufbuild/buf/private/pkg/app/appext"
"github.com/bufbuild/buf/private/pkg/command"
"github.com/bufbuild/buf/private/pkg/stringutil"
"github.com/bufbuild/buf/private/pkg/tracing"
"github.com/spf13/pflag"
"go.uber.org/multierr"
)

const (
Expand Down Expand Up @@ -131,10 +133,30 @@ func run(
if err != nil {
return err
}
pluginCacheDir, err := bufcli.CreatePluginCacheDir(container)
if err != nil {
return err
}
wasmRuntime, err := bufwasm.NewRuntime(
ctx,
bufwasm.WithLocalCacheDir(pluginCacheDir),
)
if err != nil {
return err
}
defer func() { retErr = multierr.Append(retErr, wasmRuntime.Release(ctx)) }()
tracer := tracing.NewTracer(container.Tracer())
var allFileAnnotations []bufanalysis.FileAnnotation
for _, imageWithConfig := range imageWithConfigs {
client, err := bufcheck.NewClient(container.Logger(), tracer, bufcheck.NewRunnerProvider(command.NewRunner()), bufcheck.ClientWithStderr(container.Stderr()))
client, err := bufcheck.NewClient(
container.Logger(),
tracer,
bufcheck.NewRunnerProvider(
command.NewRunner(),
bufcheck.RunnerProviderWithWASMRuntime(wasmRuntime),
),
bufcheck.ClientWithStderr(container.Stderr()),
)
if err != nil {
return err
}
Expand Down
35 changes: 19 additions & 16 deletions private/bufpkg/bufcheck/bufcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import (
"buf.build/go/bufplugin/check"
"github.com/bufbuild/buf/private/bufpkg/bufconfig"
"github.com/bufbuild/buf/private/bufpkg/bufimage"
"github.com/bufbuild/buf/private/bufpkg/bufwasm"
"github.com/bufbuild/buf/private/pkg/command"
"github.com/bufbuild/buf/private/pkg/pluginrpcutil"
"github.com/bufbuild/buf/private/pkg/slicesext"
"github.com/bufbuild/buf/private/pkg/syserror"
"github.com/bufbuild/buf/private/pkg/tracing"
Expand Down Expand Up @@ -170,21 +170,24 @@ func (r RunnerProviderFunc) NewRunner(pluginConfig bufconfig.PluginConfig) (plug
}

// NewRunnerProvider returns a new RunnerProvider for the command.Runner.
func NewRunnerProvider(delegate command.Runner) RunnerProvider {
return RunnerProviderFunc(
func(pluginConfig bufconfig.PluginConfig) (pluginrpc.Runner, error) {
if pluginConfig.Type() != bufconfig.PluginConfigTypeLocal {
return nil, syserror.New("only local plugins are supported")
}
path := pluginConfig.Path()
return pluginrpcutil.NewRunner(
delegate,
// We know that Path is of at least length 1.
path[0],
path[1:]...,
), nil
},
)
//
// This implementation should only be used for local applications.
func NewRunnerProvider(
delegate command.Runner,
options ...RunnerProviderOption,
) RunnerProvider {
return newRunnerProvider(delegate, options...)
}

// RunnerProviderOption is an option for NewRunnerProvider.
type RunnerProviderOption func(*runnerProviderOptions)

// RunnerProviderWithWASMRuntime returns a new RunnerProviderOption that
// specifies a WASM runtime. This is required for local WASM plugins.
func RunnerProviderWithWASMRuntime(wasmRuntime bufwasm.Runtime) RunnerProviderOption {
return func(runnerProviderOptions *runnerProviderOptions) {
runnerProviderOptions.wasmRuntime = wasmRuntime
}
}

// NewClient returns a new Client.
Expand Down
55 changes: 49 additions & 6 deletions private/bufpkg/bufcheck/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/bufbuild/buf/private/bufpkg/bufcheck"
"github.com/bufbuild/buf/private/bufpkg/bufimage"
"github.com/bufbuild/buf/private/bufpkg/bufmodule"
"github.com/bufbuild/buf/private/bufpkg/bufwasm"
"github.com/bufbuild/buf/private/pkg/command"
"github.com/bufbuild/buf/private/pkg/storage/storageos"
"github.com/bufbuild/buf/private/pkg/tracing"
Expand Down Expand Up @@ -511,6 +512,7 @@ func TestRunPackageNoImportCycle(t *testing.T) {
require.NoError(t, err)
return newImage
},
false,
bufanalysistesting.NewFileAnnotation(t, "c1.proto", 5, 1, 5, 19, "PACKAGE_NO_IMPORT_CYCLE"),
bufanalysistesting.NewFileAnnotation(t, "d1.proto", 5, 1, 5, 19, "PACKAGE_NO_IMPORT_CYCLE"),
)
Expand Down Expand Up @@ -593,7 +595,8 @@ func TestRunProtovalidate(t *testing.T) {
t,
"protovalidate",
"buf.testing/lint/protovalidate",
nil,
nil, // no image modification
false, // no wasm runtime
bufanalysistesting.NewFileAnnotation(t, "bool.proto", 18, 51, 18, 84, "PROTOVALIDATE"),
bufanalysistesting.NewFileAnnotation(t, "bool.proto", 19, 31, 19, 69, "PROTOVALIDATE"),
bufanalysistesting.NewFileAnnotation(t, "bool.proto", 20, 50, 20, 88, "PROTOVALIDATE"),
Expand Down Expand Up @@ -1026,7 +1029,8 @@ func TestRunV2WorkspaceIgnores(t *testing.T) {
t,
"v2/ignores",
"ignores1",
nil,
nil, // no image modification
false, // no wasm runtime
bufanalysistesting.NewFileAnnotation(t, "bar1/bar.proto", 6, 9, 6, 15, "FIELD_LOWER_SNAKE_CASE"),
bufanalysistesting.NewFileAnnotation(t, "bar1/bar.proto", 9, 9, 9, 12, "MESSAGE_PASCAL_CASE"),
bufanalysistesting.NewFileAnnotation(t, "bar1/bar.proto", 13, 6, 13, 9, "ENUM_PASCAL_CASE"),
Expand All @@ -1050,7 +1054,8 @@ func TestRunV2WorkspaceIgnores(t *testing.T) {
t,
"v2/ignores",
"ignores2",
nil,
nil, // no image modification
false, // no wasm runtime
bufanalysistesting.NewFileAnnotation(t, "bar2/bar.proto", 6, 9, 6, 15, "FIELD_LOWER_SNAKE_CASE"),
bufanalysistesting.NewFileAnnotation(t, "bar2/bar.proto", 9, 9, 9, 12, "MESSAGE_PASCAL_CASE"),
bufanalysistesting.NewFileAnnotation(t, "bar2/bar.proto", 13, 6, 13, 9, "ENUM_PASCAL_CASE"),
Expand All @@ -1062,7 +1067,8 @@ func TestRunV2WorkspaceIgnores(t *testing.T) {
t,
"v2/ignores",
"ignores3",
nil,
nil, // no image modification
false, // no wasm runtime
bufanalysistesting.NewFileAnnotation(t, "bar3/bar.proto", 6, 9, 6, 15, "FIELD_LOWER_SNAKE_CASE"),
bufanalysistesting.NewFileAnnotation(t, "bar3/bar2.proto", 6, 9, 6, 15, "FIELD_LOWER_SNAKE_CASE"),
bufanalysistesting.NewFileAnnotation(t, "bar3/bar2.proto", 9, 9, 9, 13, "MESSAGE_PASCAL_CASE"),
Expand Down Expand Up @@ -1214,6 +1220,30 @@ func TestRunLintCustomPlugins(t *testing.T) {
)
}

func TestRunLintCustomWASMPlugins(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping test in short mode")
}
testLintWithOptions(
t,
"custom_wasm_plugins",
"",
nil, // no image modification
true, // wasm runtime
bufanalysistesting.NewFileAnnotationNoLocation(t, "a.proto", "PACKAGE_DEFINED"),
bufanalysistesting.NewFileAnnotation(t, "a.proto", 8, 1, 10, 2, "SERVICE_BANNED_SUFFIXES"),
bufanalysistesting.NewFileAnnotation(t, "a.proto", 15, 1, 17, 2, "PAGE_REQUEST_HAS_TOKEN"),
bufanalysistesting.NewFileAnnotation(t, "a.proto", 19, 1, 25, 2, "PAGE_RESPONSE_HAS_TOKEN"),
bufanalysistesting.NewFileAnnotation(t, "a.proto", 21, 5, 21, 19, "VALIDATE_ID_DASHLESS"),
bufanalysistesting.NewFileAnnotation(t, "a.proto", 27, 1, 27, 26, "PAGE_REQUEST_HAS_TOKEN"),
bufanalysistesting.NewFileAnnotation(t, "a.proto", 28, 1, 28, 27, "PAGE_RESPONSE_HAS_TOKEN"),
bufanalysistesting.NewFileAnnotation(t, "b.proto", 6, 3, 6, 66, "RPC_BANNED_SUFFIXES"),
bufanalysistesting.NewFileAnnotation(t, "b.proto", 14, 5, 14, 24, "ENUM_VALUE_BANNED_SUFFIXES"),
bufanalysistesting.NewFileAnnotation(t, "b.proto", 19, 5, 19, 23, "FIELD_BANNED_SUFFIXES"),
)
}

func testLint(
t *testing.T,
relDirPath string,
Expand All @@ -1224,6 +1254,7 @@ func testLint(
relDirPath,
"",
nil,
false,
expectedFileAnnotations...,
)
}
Expand All @@ -1234,9 +1265,10 @@ func testLintWithOptions(
// only set if in workspace
moduleFullNameString string,
imageModifier func(bufimage.Image) bufimage.Image,
wasmRuntime bool,
expectedFileAnnotations ...bufanalysis.FileAnnotation,
) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // Increased timeout for WASM runtime
defer cancel()

baseDirPath := filepath.Join("testdata", "lint")
Expand Down Expand Up @@ -1293,7 +1325,18 @@ func testLintWithOptions(

lintConfig := workspace.GetLintConfigForOpaqueID(opaqueID)
require.NotNil(t, lintConfig)
client, err := bufcheck.NewClient(zap.NewNop(), tracing.NopTracer, bufcheck.NewRunnerProvider(command.NewRunner()))
var runnerProviderOptions []bufcheck.RunnerProviderOption
if wasmRuntime {
wasmRuntime, err := bufwasm.NewRuntime(ctx)
require.NoError(t, err)
t.Cleanup(func() { assert.NoError(t, wasmRuntime.Release(ctx)) })
runnerProviderOptions = append(runnerProviderOptions, bufcheck.RunnerProviderWithWASMRuntime(wasmRuntime))
}
client, err := bufcheck.NewClient(
zap.NewNop(),
tracing.NopTracer,
bufcheck.NewRunnerProvider(command.NewRunner(), runnerProviderOptions...),
)
require.NoError(t, err)
err = client.Lint(
ctx,
Expand Down
7 changes: 7 additions & 0 deletions private/bufpkg/bufcheck/multi_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"sort"
"strings"
"sync"
"time"

"buf.build/go/bufplugin/check"
"github.com/bufbuild/buf/private/pkg/slicesext"
Expand Down Expand Up @@ -94,6 +95,7 @@ func (c *multiClient) Check(ctx context.Context, request check.Request) ([]*anno
jobs = append(
jobs,
func(ctx context.Context) error {
start := time.Now()
delegateResponse, err := delegate.Client.Check(ctx, delegateRequest)
if err != nil {
if delegate.PluginName == "" {
Expand All @@ -110,6 +112,7 @@ func (c *multiClient) Check(ctx context.Context, request check.Request) ([]*anno
lock.Lock()
allAnnotations = append(allAnnotations, annotations...)
lock.Unlock()
c.logger.Debug("checked delegate client", zap.String("pluginName", delegate.PluginName), zap.Duration("duration", time.Since(start)))
return nil
},
)
Expand Down Expand Up @@ -151,6 +154,7 @@ func (c *multiClient) getRulesCategoriesAndChunkedIDs(ctx context.Context) (
var rules []Rule
chunkedRuleIDs := make([][]string, len(c.checkClientSpecs))
for i, delegate := range c.checkClientSpecs {
start := time.Now()
delegateCheckRules, err := delegate.Client.ListRules(ctx)
if err != nil {
if delegate.PluginName == "" {
Expand All @@ -165,11 +169,13 @@ func (c *multiClient) getRulesCategoriesAndChunkedIDs(ctx context.Context) (
rules = append(rules, delegateRules...)
// Already sorted.
chunkedRuleIDs[i] = slicesext.Map(delegateRules, Rule.ID)
c.logger.Debug("list rules delegate client", zap.String("pluginName", delegate.PluginName), zap.Duration("duration", time.Since(start)))
}

var categories []Category
chunkedCategoryIDs := make([][]string, len(c.checkClientSpecs))
for i, delegate := range c.checkClientSpecs {
start := time.Now()
delegateCheckCategories, err := delegate.Client.ListCategories(ctx)
if err != nil {
if delegate.PluginName == "" {
Expand All @@ -184,6 +190,7 @@ func (c *multiClient) getRulesCategoriesAndChunkedIDs(ctx context.Context) (
categories = append(categories, delegateCategories...)
// Already sorted.
chunkedCategoryIDs[i] = slicesext.Map(delegateCategories, Category.ID)
c.logger.Debug("list categories delegate client", zap.String("pluginName", delegate.PluginName), zap.Duration("duration", time.Since(start)))
}

if err := validateNoDuplicateRulesOrCategories(rules, categories); err != nil {
Expand Down
Loading

0 comments on commit d588903

Please sign in to comment.