Skip to content

Commit 33ea94f

Browse files
committed
Add plugin push command
This adds the `buf plugin push` command to upload Buf plugins to the BSR. Only wASM binary check plugins are supported for now. Plugins must implement the PluginRPC framework. ``` buf plugin push buf.build/organization/plugin --binary plugin.wasm ```
1 parent d12a559 commit 33ea94f

File tree

4 files changed

+364
-4
lines changed

4 files changed

+364
-4
lines changed

private/buf/cmd/buf/buf.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ import (
3535
"github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/bufpluginv2"
3636
"github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/lsp"
3737
"github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/price"
38-
"github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/registry/plugin/plugindelete"
39-
"github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/registry/plugin/pluginpush"
38+
betaplugindelete "github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/registry/plugin/plugindelete"
39+
betapluginpush "github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/registry/plugin/pluginpush"
4040
"github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/registry/webhook/webhookcreate"
4141
"github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/registry/webhook/webhookdelete"
4242
"github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/registry/webhook/webhooklist"
@@ -62,6 +62,7 @@ import (
6262
"github.com/bufbuild/buf/private/buf/cmd/buf/command/mod/modlsbreakingrules"
6363
"github.com/bufbuild/buf/private/buf/cmd/buf/command/mod/modlslintrules"
6464
"github.com/bufbuild/buf/private/buf/cmd/buf/command/mod/modopen"
65+
"github.com/bufbuild/buf/private/buf/cmd/buf/command/plugin/pluginpush"
6566
"github.com/bufbuild/buf/private/buf/cmd/buf/command/push"
6667
"github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/module/modulecommit/modulecommitaddlabel"
6768
"github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/module/modulecommit/modulecommitinfo"
@@ -171,6 +172,13 @@ func NewRootCommand(name string) *appcmd.Command {
171172
modlsbreakingrules.NewCommand("ls-breaking-rules", builder),
172173
},
173174
},
175+
{
176+
Use: "plugin",
177+
Short: "Work with plugins",
178+
SubCommands: []*appcmd.Command{
179+
pluginpush.NewCommand("push", builder),
180+
},
181+
},
174182
{
175183
Use: "registry",
176184
Short: "Manage assets on the Buf Schema Registry",
@@ -282,8 +290,8 @@ func NewRootCommand(name string) *appcmd.Command {
282290
Use: "plugin",
283291
Short: "Manage plugins on the Buf Schema Registry",
284292
SubCommands: []*appcmd.Command{
285-
pluginpush.NewCommand("push", builder),
286-
plugindelete.NewCommand("delete", builder),
293+
betapluginpush.NewCommand("push", builder),
294+
betaplugindelete.NewCommand("delete", builder),
287295
},
288296
},
289297
},
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2020-2024 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package pluginpush
16+
17+
import (
18+
"fmt"
19+
20+
pluginv1beta1 "buf.build/gen/go/bufbuild/registry/protocolbuffers/go/buf/registry/plugin/v1beta1"
21+
"github.com/bufbuild/buf/private/bufpkg/bufcas"
22+
"github.com/bufbuild/buf/private/bufpkg/bufplugin"
23+
)
24+
25+
var (
26+
v1beta1ProtoDigestTypeToDigestType = map[pluginv1beta1.DigestType]bufplugin.DigestType{
27+
pluginv1beta1.DigestType_DIGEST_TYPE_P1: bufplugin.DigestTypeP1,
28+
}
29+
)
30+
31+
func v1beta1ProtoToDigestType(protoDigestType pluginv1beta1.DigestType) (bufplugin.DigestType, error) {
32+
digestType, ok := v1beta1ProtoDigestTypeToDigestType[protoDigestType]
33+
if !ok {
34+
return 0, fmt.Errorf("unknown pluginv1beta1.DigestType: %v", protoDigestType)
35+
}
36+
return digestType, nil
37+
}
38+
39+
// v1beta1ProtoToDigest converts the given proto Digest to a Digest.
40+
//
41+
// Validation is performed to ensure the DigestType is known, and the value
42+
// is a valid digest value for the given DigestType.
43+
func v1beta1ProtoToDigest(protoDigest *pluginv1beta1.Digest) (bufplugin.Digest, error) {
44+
digestType, err := v1beta1ProtoToDigestType(protoDigest.Type)
45+
if err != nil {
46+
return nil, err
47+
}
48+
bufcasDigest, err := bufcas.NewDigest(protoDigest.Value)
49+
if err != nil {
50+
return nil, err
51+
}
52+
return bufplugin.NewDigest(digestType, bufcasDigest)
53+
}
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
// Copyright 2020-2024 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package pluginpush
16+
17+
import (
18+
"context"
19+
"errors"
20+
"fmt"
21+
"os"
22+
"strings"
23+
24+
pluginv1beta1 "buf.build/gen/go/bufbuild/registry/protocolbuffers/go/buf/registry/plugin/v1beta1"
25+
"connectrpc.com/connect"
26+
"github.com/bufbuild/buf/private/buf/bufcli"
27+
"github.com/bufbuild/buf/private/bufpkg/bufparse"
28+
"github.com/bufbuild/buf/private/bufpkg/bufplugin"
29+
"github.com/bufbuild/buf/private/bufpkg/bufregistryapi/bufregistryapiplugin"
30+
"github.com/bufbuild/buf/private/pkg/app/appcmd"
31+
"github.com/bufbuild/buf/private/pkg/app/appext"
32+
"github.com/bufbuild/buf/private/pkg/connectclient"
33+
"github.com/bufbuild/buf/private/pkg/slicesext"
34+
"github.com/bufbuild/buf/private/pkg/syserror"
35+
"github.com/bufbuild/buf/private/pkg/uuidutil"
36+
"github.com/bufbuild/buf/private/pkg/wasm"
37+
"github.com/klauspost/compress/zstd"
38+
"github.com/spf13/pflag"
39+
)
40+
41+
const (
42+
labelFlagName = "label"
43+
binaryFlagName = "binary"
44+
sourceControlURLFlagName = "source-control-url"
45+
)
46+
47+
// NewCommand returns a new Command.
48+
func NewCommand(
49+
name string,
50+
builder appext.SubCommandBuilder,
51+
) *appcmd.Command {
52+
flags := newFlags()
53+
return &appcmd.Command{
54+
Use: name + " <remote/owner/plugin>",
55+
Short: "Push a plugin to a registry",
56+
Long: `The first argument is the plugin full name in the format <remote/owner/plugin>.`,
57+
Args: appcmd.MaximumNArgs(1),
58+
Run: builder.NewRunFunc(
59+
func(ctx context.Context, container appext.Container) error {
60+
return run(ctx, container, flags)
61+
},
62+
),
63+
BindFlags: flags.Bind,
64+
}
65+
}
66+
67+
type flags struct {
68+
Labels []string
69+
Binary string
70+
SourceControlURL string
71+
}
72+
73+
func newFlags() *flags {
74+
return &flags{}
75+
}
76+
77+
func (f *flags) Bind(flagSet *pflag.FlagSet) {
78+
flagSet.StringSliceVar(
79+
&f.Labels,
80+
labelFlagName,
81+
nil,
82+
"Associate the label with the plugins pushed. Can be used multiple times.",
83+
)
84+
flagSet.StringVar(
85+
&f.Binary,
86+
binaryFlagName,
87+
"",
88+
"Push the plugin binary to the registry.",
89+
)
90+
flagSet.StringVar(
91+
&f.SourceControlURL,
92+
sourceControlURLFlagName,
93+
"",
94+
"The URL for viewing the source code of the pushed modules (e.g. the specific commit in source control).",
95+
)
96+
}
97+
98+
func run(
99+
ctx context.Context,
100+
container appext.Container,
101+
flags *flags,
102+
) (retErr error) {
103+
if err := validateFlags(flags); err != nil {
104+
return err
105+
}
106+
// We parse the plugin full name from the user-provided argument.
107+
pluginFullName, err := bufparse.ParseFullName(container.Arg(0))
108+
if err != nil {
109+
return appcmd.WrapInvalidArgumentError(err)
110+
}
111+
112+
clientConfig, err := bufcli.NewConnectClientConfig(container)
113+
if err != nil {
114+
return err
115+
}
116+
pluginKey, err := upload(ctx, container, flags, clientConfig, pluginFullName)
117+
if err != nil {
118+
return err
119+
}
120+
// Only one plugin key is returned.
121+
if _, err := fmt.Fprintf(container.Stdout(), "%s\n", pluginKey.String()); err != nil {
122+
return syserror.Wrap(err)
123+
}
124+
return nil
125+
}
126+
127+
func upload(
128+
ctx context.Context,
129+
container appext.Container,
130+
flags *flags,
131+
clientConfig *connectclient.Config,
132+
pluginFullName bufparse.FullName,
133+
) (_ bufplugin.PluginKey, retErr error) {
134+
switch {
135+
case flags.Binary != "":
136+
return uploadBinary(ctx, container, flags, clientConfig, pluginFullName)
137+
default:
138+
// This should never happen because the flags are validated.
139+
return nil, syserror.Newf("--%s must be set", binaryFlagName)
140+
}
141+
}
142+
143+
func uploadBinary(
144+
ctx context.Context,
145+
container appext.Container,
146+
flags *flags,
147+
clientConfig *connectclient.Config,
148+
pluginFullName bufparse.FullName,
149+
) (pluginKey bufplugin.PluginKey, retErr error) {
150+
uploadServiceClient := bufregistryapiplugin.NewClientProvider(clientConfig).
151+
V1Beta1UploadServiceClient(pluginFullName.Registry())
152+
153+
wasmRuntimeCacheDir, err := bufcli.CreateWasmRuntimeCacheDir(container)
154+
if err != nil {
155+
return nil, err
156+
}
157+
wasmRuntime, err := wasm.NewRuntime(ctx, wasm.WithLocalCacheDir(wasmRuntimeCacheDir))
158+
if err != nil {
159+
return nil, err
160+
}
161+
defer func() {
162+
retErr = errors.Join(retErr, wasmRuntime.Close(ctx))
163+
}()
164+
// Load the binary from the `--binary` flag.
165+
wasmBinary, err := os.ReadFile(flags.Binary)
166+
if err != nil {
167+
return nil, fmt.Errorf("could not read binary %q: %w", flags.Binary, err)
168+
}
169+
compressionType := pluginv1beta1.CompressionType_COMPRESSION_TYPE_ZSTD
170+
compressedWasmBinary, err := zstdCompress(wasmBinary)
171+
if err != nil {
172+
return nil, fmt.Errorf("could not compress binary %q: %w", flags.Binary, err)
173+
}
174+
175+
// Defer validation of the plugin binary to the server, but compile the
176+
// binary locally to catch any errors early.
177+
_, err = wasmRuntime.Compile(ctx, pluginFullName.Name(), wasmBinary)
178+
if err != nil {
179+
return nil, fmt.Errorf("could not compile binary %q: %w", flags.Binary, err)
180+
}
181+
// Upload the binary to the registry.
182+
content := &pluginv1beta1.UploadRequest_Content{
183+
PluginRef: &pluginv1beta1.PluginRef{
184+
Value: &pluginv1beta1.PluginRef_Name_{
185+
Name: &pluginv1beta1.PluginRef_Name{
186+
Owner: pluginFullName.Owner(),
187+
Plugin: pluginFullName.Name(),
188+
},
189+
},
190+
},
191+
CompressionType: compressionType,
192+
Content: compressedWasmBinary,
193+
ScopedLabelRefs: slicesext.Map(flags.Labels, func(label string) *pluginv1beta1.ScopedLabelRef {
194+
return &pluginv1beta1.ScopedLabelRef{
195+
Value: &pluginv1beta1.ScopedLabelRef_Name{
196+
Name: label,
197+
},
198+
}
199+
}),
200+
SourceControlUrl: flags.SourceControlURL,
201+
}
202+
uploadResponse, err := uploadServiceClient.Upload(ctx, connect.NewRequest(&pluginv1beta1.UploadRequest{
203+
Contents: []*pluginv1beta1.UploadRequest_Content{content},
204+
}))
205+
if err != nil {
206+
return nil, err
207+
}
208+
if len(uploadResponse.Msg.Commits) != 1 {
209+
return nil, syserror.Newf("unexpected number of commits returned from server: %d", len(uploadResponse.Msg.Commits))
210+
}
211+
protoCommit := uploadResponse.Msg.Commits[0]
212+
commitID, err := uuidutil.FromDashless(protoCommit.Id)
213+
if err != nil {
214+
return nil, err
215+
}
216+
pluginKey, err = bufplugin.NewPluginKey(
217+
pluginFullName,
218+
commitID,
219+
func() (bufplugin.Digest, error) {
220+
return v1beta1ProtoToDigest(protoCommit.Digest)
221+
},
222+
)
223+
if err != nil {
224+
return nil, err
225+
}
226+
return pluginKey, nil
227+
}
228+
229+
func zstdCompress(data []byte) ([]byte, error) {
230+
encoder, err := zstd.NewWriter(nil)
231+
if err != nil {
232+
return nil, fmt.Errorf("failed to create zstd encoder: %w", err)
233+
}
234+
defer encoder.Close()
235+
return encoder.EncodeAll(data, nil), nil
236+
}
237+
238+
func validateFlags(flags *flags) error {
239+
if err := validateLabelFlags(flags); err != nil {
240+
return err
241+
}
242+
if err := validateTypeFlags(flags); err != nil {
243+
return err
244+
}
245+
return nil
246+
}
247+
248+
func validateLabelFlags(flags *flags) error {
249+
return validateLabelFlagValues(flags)
250+
}
251+
252+
func validateTypeFlags(flags *flags) error {
253+
var typeFlags []string
254+
if flags.Binary != "" {
255+
typeFlags = append(typeFlags, binaryFlagName)
256+
}
257+
if len(typeFlags) > 1 {
258+
usedFlagsErrStr := strings.Join(
259+
slicesext.Map(
260+
typeFlags,
261+
func(flag string) string { return fmt.Sprintf("--%s", flag) },
262+
),
263+
", ",
264+
)
265+
return appcmd.NewInvalidArgumentErrorf("These flags cannot be used in combination with one another: %s", usedFlagsErrStr)
266+
}
267+
if len(typeFlags) == 0 {
268+
return appcmd.NewInvalidArgumentErrorf("--%s must be set", binaryFlagName)
269+
}
270+
return nil
271+
}
272+
273+
func validateLabelFlagValues(flags *flags) error {
274+
for _, label := range flags.Labels {
275+
if label == "" {
276+
return appcmd.NewInvalidArgumentErrorf("--%s requires a non-empty string", labelFlagName)
277+
}
278+
}
279+
return nil
280+
}

private/buf/cmd/buf/command/plugin/pluginpush/usage.gen.go

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)