From 6830323896959f3880d46a56632c0fb814a97c30 Mon Sep 17 00:00:00 2001 From: Edward McFarlane Date: Thu, 3 Oct 2024 18:02:50 -0400 Subject: [PATCH] wip --- go.mod | 4 +- go.sum | 12 +- private/buf/bufcli/errors.go | 6 + private/buf/bufcli/flags_args.go | 16 + private/buf/bufprint/bufprint.go | 27 + private/buf/cmd/buf/buf.go | 30 +- .../betaplugindelete.go} | 2 +- .../plugin/betaplugindelete/usage.gen.go | 19 + .../betapluginpush.go} | 2 +- .../plugin/betapluginpush/usage.gen.go | 19 + .../command/plugin/pluginpush/pluginpush.go | 622 ++++++++++++++++++ .../plugin/pluginpush/usage.gen.go | 0 .../plugin/plugincreate/plugincreate.go | 153 +++++ .../registry/plugin/plugincreate/usage.gen.go | 19 + .../plugin/plugindelete/plugindelete.go | 113 ++++ .../registry/plugin/plugindelete/usage.gen.go | 0 .../registry/plugin/plugininfo/plugininfo.go | 121 ++++ .../registry/plugin/plugininfo/usage.gen.go | 19 + .../plugin/pluginupdate/pluginupdate.go | 145 ++++ .../registry/plugin/pluginupdate/usage.gen.go | 19 + private/bufpkg/bufapi/bufapi.go | 55 ++ private/bufpkg/bufplugin/bufplugin.go | 16 + private/bufpkg/bufplugin/commit.go | 72 ++ private/bufpkg/bufplugin/digest.go | 143 ++++ private/bufpkg/bufplugin/errors.go | 68 ++ private/bufpkg/bufplugin/parse.go | 48 ++ private/bufpkg/bufplugin/plugin_full_name.go | 140 ++++ private/bufpkg/bufplugin/plugin_key.go | 106 +++ private/bufpkg/bufplugin/usage.gen.go | 19 + 29 files changed, 2003 insertions(+), 12 deletions(-) rename private/buf/cmd/buf/command/beta/registry/plugin/{plugindelete/plugindelete.go => betaplugindelete/betaplugindelete.go} (99%) create mode 100644 private/buf/cmd/buf/command/beta/registry/plugin/betaplugindelete/usage.gen.go rename private/buf/cmd/buf/command/beta/registry/plugin/{pluginpush/pluginpush.go => betapluginpush/betapluginpush.go} (99%) create mode 100644 private/buf/cmd/buf/command/beta/registry/plugin/betapluginpush/usage.gen.go create mode 100644 private/buf/cmd/buf/command/plugin/pluginpush/pluginpush.go rename private/buf/cmd/buf/command/{beta/registry => }/plugin/pluginpush/usage.gen.go (100%) create mode 100644 private/buf/cmd/buf/command/registry/plugin/plugincreate/plugincreate.go create mode 100644 private/buf/cmd/buf/command/registry/plugin/plugincreate/usage.gen.go create mode 100644 private/buf/cmd/buf/command/registry/plugin/plugindelete/plugindelete.go rename private/buf/cmd/buf/command/{beta => }/registry/plugin/plugindelete/usage.gen.go (100%) create mode 100644 private/buf/cmd/buf/command/registry/plugin/plugininfo/plugininfo.go create mode 100644 private/buf/cmd/buf/command/registry/plugin/plugininfo/usage.gen.go create mode 100644 private/buf/cmd/buf/command/registry/plugin/pluginupdate/pluginupdate.go create mode 100644 private/buf/cmd/buf/command/registry/plugin/pluginupdate/usage.gen.go create mode 100644 private/bufpkg/bufplugin/bufplugin.go create mode 100644 private/bufpkg/bufplugin/commit.go create mode 100644 private/bufpkg/bufplugin/digest.go create mode 100644 private/bufpkg/bufplugin/errors.go create mode 100644 private/bufpkg/bufplugin/parse.go create mode 100644 private/bufpkg/bufplugin/plugin_full_name.go create mode 100644 private/bufpkg/bufplugin/plugin_key.go create mode 100644 private/bufpkg/bufplugin/usage.gen.go diff --git a/go.mod b/go.mod index c48026d0dd..9a08b68f9a 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ toolchain go1.23.2 require ( buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.34.2-20240928190436-5e8abcfd7a7e.2 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-20240920164238-5a7b106cbb87.2 - buf.build/gen/go/bufbuild/registry/connectrpc/go v1.17.0-20240925012807-1610ffa05635.1 - buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.34.2-20240925012807-1610ffa05635.2 + buf.build/gen/go/bufbuild/registry/connectrpc/go v1.17.0-00000000000000-f384556653e6.1 + buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.34.2-00000000000000-f384556653e6.2 buf.build/go/bufplugin v0.5.0 buf.build/go/protoyaml v0.2.0 buf.build/go/spdx v0.2.0 diff --git a/go.sum b/go.sum index 25d83cca46..cb7931823d 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,14 @@ buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.34.2-20240928190436-5e buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.34.2-20240928190436-5e8abcfd7a7e.2/go.mod h1:B+9TKHRYqoAUW57pLjhkLOnBCu0DQYMV+f7imQ9nXwI= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-20240920164238-5a7b106cbb87.2 h1:hl0FrmGlNpQZIGvU1/jDz0lsPDd0BhCE0QDRwPfLZcA= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-20240920164238-5a7b106cbb87.2/go.mod h1:ylS4c28ACSI59oJrOdW4pHS4n0Hw4TgSPHn8rpHl4Yw= -buf.build/gen/go/bufbuild/registry/connectrpc/go v1.17.0-20240925012807-1610ffa05635.1 h1:p4A9QnhBrKjCquBt1mKqfO37QseLwgWqQp+Wb9ZjasE= -buf.build/gen/go/bufbuild/registry/connectrpc/go v1.17.0-20240925012807-1610ffa05635.1/go.mod h1:7WtU+waNF+dyxDsuNaqmG3d0w3y2poNju8cvun1/jLs= -buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.34.2-20240925012807-1610ffa05635.2 h1:3sSS9z8k6zVe7rNNt9R6DN2fOFBVClEflmICIjbXwms= -buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.34.2-20240925012807-1610ffa05635.2/go.mod h1:psseUmlKRo9v5LZJtR/aTpdTLuyp9o3X7rnLT87SZEo= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.17.0-00000000000000-ab00826f5a21.1 h1:PtUgNlDoiuv7ekLj1lOXje+OodTnefL+bexgzBoV3Tc= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.17.0-00000000000000-ab00826f5a21.1/go.mod h1:+e0DBBnL9iGwEIN532lQGiJvY9HODFbjyO9oM8s2aR0= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.17.0-00000000000000-f384556653e6.1 h1:Rh6uADIMohx6myydsQ+VDRovAEs8wxO7CWuggBON0Vo= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.17.0-00000000000000-f384556653e6.1/go.mod h1:LM8iUDMDNerdWwvuUeujsuKbYEM9Tcy9V41/NNQ6n6Y= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.34.2-00000000000000-ab00826f5a21.2 h1:W0B2f9luVLvNBLdH1ajdioI6YS/+XUFrDrmL0/PdmaY= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.34.2-00000000000000-ab00826f5a21.2/go.mod h1:psseUmlKRo9v5LZJtR/aTpdTLuyp9o3X7rnLT87SZEo= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.34.2-00000000000000-f384556653e6.2 h1:u6D0MOYus2seFpswpKRteA6viAWK6rYSATuxDVg7iyQ= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.34.2-00000000000000-f384556653e6.2/go.mod h1:psseUmlKRo9v5LZJtR/aTpdTLuyp9o3X7rnLT87SZEo= buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.34.2-20240828222655-5345c0a56177.2 h1:oSi+Adw4xvIjXrW8eY8QGR3sBdfWeY5HN/RefnRt52M= buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.34.2-20240828222655-5345c0a56177.2/go.mod h1:GjH0gjlY/ns16X8d6eaXV2W+6IFwsO5Ly9WVnzyd1E0= buf.build/go/bufplugin v0.5.0 h1:pmK1AloAMp+4woH5hEisK9qVmDdLySzIKexUUVZLJ2Q= diff --git a/private/buf/bufcli/errors.go b/private/buf/bufcli/errors.go index 7d57f4863a..9c15509b54 100644 --- a/private/buf/bufcli/errors.go +++ b/private/buf/bufcli/errors.go @@ -54,6 +54,12 @@ func NewLabelNameAlreadyExistsError(name string) error { return fmt.Errorf("a label named %q already exists", name) } +// NewPluginNameAlreadyExistsError informs the user that a plugin +// with that name already exists. +func NewPluginNameAlreadyExistsError(name string) error { + return fmt.Errorf("a plugin named %q already exists", name) +} + // NewOrganizationNotFoundError informs the user that an organization with // that name does not exist. func NewOrganizationNotFoundError(name string) error { diff --git a/private/buf/bufcli/flags_args.go b/private/buf/bufcli/flags_args.go index a2238f7c67..e8d2ca56f7 100644 --- a/private/buf/bufcli/flags_args.go +++ b/private/buf/bufcli/flags_args.go @@ -19,6 +19,7 @@ import ( "fmt" modulev1 "buf.build/gen/go/bufbuild/registry/protocolbuffers/go/buf/registry/module/v1" + pluginv1beta1 "buf.build/gen/go/bufbuild/registry/protocolbuffers/go/buf/registry/plugin/v1beta1" "github.com/bufbuild/buf/private/buf/buffetch" "github.com/bufbuild/buf/private/pkg/app" "github.com/bufbuild/buf/private/pkg/app/appcmd" @@ -302,6 +303,21 @@ func VisibilityFlagToVisibilityAllowUnspecified(visibility string) (modulev1.Mod } } +// VisibilityFlagToPluginVisibilityAllowUnspecified parses the given string as a pluginv1.PluginVisibility +// where an empty string will be parsed as unspecified. +func VisibilityFlagToPluginVisibilityAllowUnspecified(visibility string) (pluginv1beta1.PluginVisibility, error) { + switch visibility { + case publicVisibility: + return pluginv1beta1.PluginVisibility_PLUGIN_VISIBILITY_PUBLIC, nil + case privateVisibility: + return pluginv1beta1.PluginVisibility_PLUGIN_VISIBILITY_PRIVATE, nil + case "": + return pluginv1beta1.PluginVisibility_PLUGIN_VISIBILITY_UNSPECIFIED, nil + default: + return 0, fmt.Errorf("invalid visibility: %s", visibility) + } +} + // ArchiveStatusFlagToArchiveStatusFilter parses the given string as a modulev1.ListLabelsRequest_ArchiveFilter. func ArchiveStatusFlagToArchiveStatusFilter(archiveStatus string) (modulev1.ListLabelsRequest_ArchiveFilter, error) { switch archiveStatus { diff --git a/private/buf/bufprint/bufprint.go b/private/buf/bufprint/bufprint.go index 1647a41f93..7ac632ff2c 100644 --- a/private/buf/bufprint/bufprint.go +++ b/private/buf/bufprint/bufprint.go @@ -26,7 +26,9 @@ import ( modulev1 "buf.build/gen/go/bufbuild/registry/protocolbuffers/go/buf/registry/module/v1" ownerv1 "buf.build/gen/go/bufbuild/registry/protocolbuffers/go/buf/registry/owner/v1" + pluginv1beta1 "buf.build/gen/go/bufbuild/registry/protocolbuffers/go/buf/registry/plugin/v1beta1" "github.com/bufbuild/buf/private/bufpkg/bufmodule" + "github.com/bufbuild/buf/private/bufpkg/bufplugin" registryv1alpha1 "github.com/bufbuild/buf/private/gen/proto/go/buf/alpha/registry/v1alpha1" "github.com/bufbuild/buf/private/pkg/protoencoding" "github.com/bufbuild/buf/private/pkg/protostat" @@ -248,6 +250,18 @@ func NewOrganizationEntity(organization *ownerv1.Organization, remote string) En } } +// NewPluginEntity returns a new plugin entity to print. +func NewPluginEntity(plugin *pluginv1beta1.Plugin, pluginFullName bufplugin.PluginFullName) Entity { + return outputPlugin{ + ID: plugin.Id, + Remote: pluginFullName.Registry(), + Owner: pluginFullName.Owner(), + Name: pluginFullName.Name(), + FullName: pluginFullName.String(), + CreateTime: plugin.CreateTime.AsTime(), + } +} + // CuratedPluginPrinter is a printer for curated plugins. type CuratedPluginPrinter interface { PrintCuratedPlugin(ctx context.Context, format Format, plugin *registryv1alpha1.CuratedPlugin) error @@ -456,3 +470,16 @@ type outputOrganization struct { func (o outputOrganization) fullName() string { return o.FullName } + +type outputPlugin struct { + ID string `json:"id,omitempty"` + Remote string `json:"remote,omitempty"` + Owner string `json:"owner,omitempty"` + Name string `json:"name,omitempty"` + FullName string `json:"-" bufprint:"Name"` + CreateTime time.Time `json:"create_time,omitempty" bufprint:"Create Time"` +} + +func (m outputPlugin) fullName() string { + return m.FullName +} diff --git a/private/buf/cmd/buf/buf.go b/private/buf/cmd/buf/buf.go index 8094965342..3ace22c11d 100644 --- a/private/buf/cmd/buf/buf.go +++ b/private/buf/cmd/buf/buf.go @@ -35,8 +35,8 @@ import ( "github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/bufpluginv2" "github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/lsp" "github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/price" - "github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/registry/plugin/plugindelete" - "github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/registry/plugin/pluginpush" + "github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/registry/plugin/betaplugindelete" + "github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/registry/plugin/betapluginpush" "github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/registry/webhook/webhookcreate" "github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/registry/webhook/webhookdelete" "github.com/bufbuild/buf/private/buf/cmd/buf/command/beta/registry/webhook/webhooklist" @@ -62,6 +62,7 @@ import ( "github.com/bufbuild/buf/private/buf/cmd/buf/command/mod/modlsbreakingrules" "github.com/bufbuild/buf/private/buf/cmd/buf/command/mod/modlslintrules" "github.com/bufbuild/buf/private/buf/cmd/buf/command/mod/modopen" + "github.com/bufbuild/buf/private/buf/cmd/buf/command/plugin/pluginpush" "github.com/bufbuild/buf/private/buf/cmd/buf/command/push" "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/commit/commitaddlabel" "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/commit/commitinfo" @@ -81,6 +82,10 @@ import ( "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/organization/organizationdelete" "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/organization/organizationinfo" "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/organization/organizationupdate" + "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/plugin/plugincreate" + "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/plugin/plugindelete" + "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/plugin/plugininfo" + "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/plugin/pluginupdate" "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/registrycc" "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/registrylogin" "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/registrylogout" @@ -224,6 +229,16 @@ func NewRootCommand(name string) *appcmd.Command { moduleupdate.NewCommand("update", builder), }, }, + { + Use: "plugin", + Short: "Manage BSR plugins", + SubCommands: []*appcmd.Command{ + plugincreate.NewCommand("create", builder), + plugininfo.NewCommand("info", builder), + plugindelete.NewCommand("delete", builder), + pluginupdate.NewCommand("update", builder), + }, + }, }, }, { @@ -254,8 +269,8 @@ func NewRootCommand(name string) *appcmd.Command { Use: "plugin", Short: "Manage plugins on the Buf Schema Registry", SubCommands: []*appcmd.Command{ - pluginpush.NewCommand("push", builder), - plugindelete.NewCommand("delete", builder), + betapluginpush.NewCommand("push", builder), + betaplugindelete.NewCommand("delete", builder), }, }, }, @@ -285,6 +300,13 @@ func NewRootCommand(name string) *appcmd.Command { }, }, }, + { + Use: "plugin", + Short: "Work with buf plugins", + SubCommands: []*appcmd.Command{ + pluginpush.NewCommand("push", builder), + }, + }, }, } } diff --git a/private/buf/cmd/buf/command/beta/registry/plugin/plugindelete/plugindelete.go b/private/buf/cmd/buf/command/beta/registry/plugin/betaplugindelete/betaplugindelete.go similarity index 99% rename from private/buf/cmd/buf/command/beta/registry/plugin/plugindelete/plugindelete.go rename to private/buf/cmd/buf/command/beta/registry/plugin/betaplugindelete/betaplugindelete.go index 1cb902872b..fba524099e 100644 --- a/private/buf/cmd/buf/command/beta/registry/plugin/plugindelete/plugindelete.go +++ b/private/buf/cmd/buf/command/beta/registry/plugin/betaplugindelete/betaplugindelete.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugindelete +package betaplugindelete import ( "context" diff --git a/private/buf/cmd/buf/command/beta/registry/plugin/betaplugindelete/usage.gen.go b/private/buf/cmd/buf/command/beta/registry/plugin/betaplugindelete/usage.gen.go new file mode 100644 index 0000000000..9a2d9d14bd --- /dev/null +++ b/private/buf/cmd/buf/command/beta/registry/plugin/betaplugindelete/usage.gen.go @@ -0,0 +1,19 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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. + +// Generated. DO NOT EDIT. + +package betaplugindelete + +import _ "github.com/bufbuild/buf/private/usage" diff --git a/private/buf/cmd/buf/command/beta/registry/plugin/pluginpush/pluginpush.go b/private/buf/cmd/buf/command/beta/registry/plugin/betapluginpush/betapluginpush.go similarity index 99% rename from private/buf/cmd/buf/command/beta/registry/plugin/pluginpush/pluginpush.go rename to private/buf/cmd/buf/command/beta/registry/plugin/betapluginpush/betapluginpush.go index 904768431a..19752e0567 100644 --- a/private/buf/cmd/buf/command/beta/registry/plugin/pluginpush/pluginpush.go +++ b/private/buf/cmd/buf/command/beta/registry/plugin/betapluginpush/betapluginpush.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package pluginpush +package betapluginpush import ( "context" diff --git a/private/buf/cmd/buf/command/beta/registry/plugin/betapluginpush/usage.gen.go b/private/buf/cmd/buf/command/beta/registry/plugin/betapluginpush/usage.gen.go new file mode 100644 index 0000000000..4b0b4d8b75 --- /dev/null +++ b/private/buf/cmd/buf/command/beta/registry/plugin/betapluginpush/usage.gen.go @@ -0,0 +1,19 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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. + +// Generated. DO NOT EDIT. + +package betapluginpush + +import _ "github.com/bufbuild/buf/private/usage" diff --git a/private/buf/cmd/buf/command/plugin/pluginpush/pluginpush.go b/private/buf/cmd/buf/command/plugin/pluginpush/pluginpush.go new file mode 100644 index 0000000000..7ceb26e339 --- /dev/null +++ b/private/buf/cmd/buf/command/plugin/pluginpush/pluginpush.go @@ -0,0 +1,622 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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 pluginpush + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "strings" + + "buf.build/gen/go/bufbuild/registry/connectrpc/go/buf/registry/plugin/v1beta1/pluginv1beta1connect" + pluginv1beta1 "buf.build/gen/go/bufbuild/registry/protocolbuffers/go/buf/registry/plugin/v1beta1" + "connectrpc.com/connect" + "github.com/bufbuild/buf/private/buf/bufcli" + "github.com/bufbuild/buf/private/bufpkg/bufapi" + "github.com/bufbuild/buf/private/bufpkg/bufcas" + "github.com/bufbuild/buf/private/bufpkg/bufplugin" + "github.com/bufbuild/buf/private/bufpkg/bufremoteplugin" + "github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremotepluginconfig" + "github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremoteplugindocker" + "github.com/bufbuild/buf/private/pkg/app/appcmd" + "github.com/bufbuild/buf/private/pkg/app/appext" + "github.com/bufbuild/buf/private/pkg/netrc" + "github.com/bufbuild/buf/private/pkg/slicesext" + "github.com/bufbuild/buf/private/pkg/storage/storageos" + "github.com/bufbuild/buf/private/pkg/syserror" + "github.com/bufbuild/buf/private/pkg/uuidutil" + "github.com/bufbuild/buf/private/pkg/wasm" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + pkgv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "github.com/klauspost/compress/zstd" + "github.com/spf13/pflag" + "go.uber.org/multierr" + "google.golang.org/protobuf/encoding/protojson" +) + +const ( + disableSymlinksFlagName = "disable-symlinks" + labelFlagName = "label" + imageFlagName = "image" + imageConfigFlagName = "image-config" + checkBinaryFlagName = "check-binary" +) + +// NewCommand returns a new Command. +func NewCommand( + name string, + builder appext.SubCommandBuilder, +) *appcmd.Command { + flags := newFlags() + return &appcmd.Command{ + Use: name + " ", + Short: "Push a plugin to a registry", + Long: `The first argument is the plugin full name in the format .`, + Args: appcmd.MaximumNArgs(1), + Run: builder.NewRunFunc( + func(ctx context.Context, container appext.Container) error { + return run(ctx, container, flags) + }, + ), + BindFlags: flags.Bind, + } +} + +type flags struct { + DisableSymlinks bool + Labels []string + Image string + ImageConfig string + CheckBinary string +} + +func newFlags() *flags { + return &flags{} +} + +func (f *flags) Bind(flagSet *pflag.FlagSet) { + bufcli.BindDisableSymlinks(flagSet, &f.DisableSymlinks, disableSymlinksFlagName) + flagSet.StringVar( + &f.Image, + imageFlagName, + "", + "Push the plugin docker image to the registry.", + ) + flagSet.StringVar( + &f.ImageConfig, + imageConfigFlagName, + "", + fmt.Sprintf( + "Set the plugin image config. Must be set if --%s is set.", + imageFlagName, + ), + ) + flagSet.StringVar( + &f.CheckBinary, + checkBinaryFlagName, + "", + "Push the check plugin binary to the registry.", + ) + //flagSet.StringVar( + // &f.BinaryType, + // binaryTypeFlagName, + // "", + // fmt.Sprintf( + // "Set the binary type. Must be set if --binary is set.", + // binaryFlagName, + // ), + //) + flagSet.StringSliceVar( + &f.Labels, + labelFlagName, + nil, + "Associate the label with the plugins pushed. Can be used multiple times.", + ) +} + +func run( + ctx context.Context, + container appext.Container, + flags *flags, +) (retErr error) { + if err := validateFlags(flags); err != nil { + return err + } + // We parse the plugin full name from the user-provided argument. + pluginFullName, err := bufplugin.ParsePluginFullName(container.Arg(0)) + if err != nil { + return appcmd.WrapInvalidArgumentError(err) + } + fmt.Println("pluginFullName", pluginFullName) + + clientConfig, err := bufcli.NewConnectClientConfig(container) + if err != nil { + return err + } + uploadServiceClient := bufapi.NewClientProvider(clientConfig). + PluginV1Beta1UploadServiceClient(pluginFullName.Registry()) + + pluginKey, err := upload(ctx, container, flags, pluginFullName, uploadServiceClient) + if err != nil { + return err + } + // Only one plugin key is returned. + if _, err := fmt.Fprintf(container.Stdout(), "%s\n", pluginKey.String()); err != nil { + return syserror.Wrap(err) + } + return nil +} + +func upload( + ctx context.Context, + container appext.Container, + flags *flags, + pluginFullName bufplugin.PluginFullName, + uploadServiceClient pluginv1beta1connect.UploadServiceClient, +) (_ bufplugin.PluginKey, retErr error) { + switch { + case flags.Image != "": + return uploadImage(ctx, container, flags, pluginFullName, uploadServiceClient) + case flags.CheckBinary != "": + return uploadCheckBinary(ctx, container, flags, pluginFullName, uploadServiceClient) + default: + // This should never happen because the flags are validated. + return nil, syserror.Newf("either --%s or --%s must be set", imageFlagName, checkBinaryFlagName) + } +} + +func uploadCheckBinary( + ctx context.Context, + container appext.Container, + flags *flags, + pluginFullName bufplugin.PluginFullName, + uploadServiceClient pluginv1beta1connect.UploadServiceClient, +) (_ bufplugin.PluginKey, retErr error) { + wasmRuntimeCacheDir, err := bufcli.CreateWasmRuntimeCacheDir(container) + if err != nil { + return nil, err + } + wasmRuntime, err := wasm.NewRuntime(ctx, wasm.WithLocalCacheDir(wasmRuntimeCacheDir)) + if err != nil { + return nil, err + } + defer func() { + retErr = multierr.Append(retErr, wasmRuntime.Close(ctx)) + }() + + // Load the binary from the `--binary` flag. + wasmBinary, err := os.ReadFile(flags.CheckBinary) + if err != nil { + return nil, fmt.Errorf("could not read binary %q: %w", flags.CheckBinary, err) + } + + // Maybe validate the binary is a valid plugin binary? + _, err = wasmRuntime.Compile(ctx, pluginFullName.Name(), wasmBinary) + if err != nil { + return nil, fmt.Errorf("could not compile binary %q: %w", flags.CheckBinary, err) + } + + // Upload the binary to the registry. + content := &pluginv1beta1.UploadRequest_Content{ + PluginRef: &pluginv1beta1.PluginRef{ + Value: &pluginv1beta1.PluginRef_Name_{ + Name: &pluginv1beta1.PluginRef_Name{ + Owner: pluginFullName.Owner(), + Plugin: pluginFullName.Name(), + }, + }, + }, + CompressionType: pluginv1beta1.CompressionType_COMPRESSION_TYPE_ZSTD, + Content: zstdCompress(wasmBinary), + } + + uploadResponse, err := uploadServiceClient.Upload(ctx, connect.NewRequest(&pluginv1beta1.UploadRequest{ + Contents: []*pluginv1beta1.UploadRequest_Content{content}, + })) + if err != nil { + return nil, err + } + if len(uploadResponse.Msg.Commits) != 1 { + return nil, syserror.Newf("unexpected number of commits returned from server: %d", len(uploadResponse.Msg.Commits)) + } + + protoCommit := uploadResponse.Msg.Commits[0] + + commitID, err := uuidutil.FromDashless(protoCommit.Id) + if err != nil { + return nil, err + } + pluginKey, err := bufplugin.NewPluginKey( + pluginFullName, + commitID, + func() (bufplugin.Digest, error) { + return v1beta1ProtoToDigest(protoCommit.Digest) + }, + ) + if err != nil { + return nil, err + } + return pluginKey, nil +} + +// v1beta1ProtoToDigest converts the given proto Digest to a Digest. +// +// Validation is performed to ensure the DigestType is known, and the value +// is a valid digest value for the given DigestType. +func v1beta1ProtoToDigest(protoDigest *pluginv1beta1.Digest) (bufplugin.Digest, error) { + digestType, err := v1beta1ProtoToDigestType(protoDigest.Type) + if err != nil { + return nil, err + } + bufcasDigest, err := bufcas.NewDigest(protoDigest.Value) + if err != nil { + return nil, err + } + return bufplugin.NewDigest(digestType, bufcasDigest) +} + +var ( + v1beta1ProtoDigestTypeToDigestType = map[pluginv1beta1.DigestType]bufplugin.DigestType{ + pluginv1beta1.DigestType_DIGEST_TYPE_P1: bufplugin.DigestTypeP1, + } +) + +func v1beta1ProtoToDigestType(protoDigestType pluginv1beta1.DigestType) (bufplugin.DigestType, error) { + digestType, ok := v1beta1ProtoDigestTypeToDigestType[protoDigestType] + if !ok { + return 0, fmt.Errorf("unknown pluginv1beta1.DigestType: %v", protoDigestType) + } + return digestType, nil +} + +func uploadImage( + ctx context.Context, + container appext.Container, + flags *flags, + pluginFullName bufplugin.PluginFullName, + uploadServiceClient pluginv1beta1connect.UploadServiceClient, +) (_ bufplugin.PluginKey, retErr error) { + if flags.ImageConfig == "" { + return nil, appcmd.NewInvalidArgumentErrorf("--%s is required", imageConfigFlagName) + } + source, err := bufcli.GetInputValue(container, "" /* The input hashtag is not supported here */, ".") + if err != nil { + return nil, err + } + storageProvider := newStorageosProvider(flags.DisableSymlinks) + sourceBucket, err := storageProvider.NewReadWriteBucket(source) + if err != nil { + return nil, err + } + options := []bufremotepluginconfig.ConfigOption{ + bufremotepluginconfig.WithOverrideRemote(pluginFullName.Registry()), + } + pluginConfig, err := bufremotepluginconfig.GetConfigForBucket(ctx, sourceBucket, options...) + if err != nil { + return nil, err + } + + dockerClient, err := bufremoteplugindocker.NewClient(container.Logger(), bufcli.Version) + if err != nil { + return nil, err + } + defer func() { + retErr = multierr.Append(retErr, dockerClient.Close()) + }() + machine, err := netrc.GetMachineForName(container, pluginFullName.Registry()) + if err != nil { + return nil, err + } + authConfig := &bufremoteplugindocker.RegistryAuthConfig{} + if machine != nil { + authConfig.ServerAddress = machine.Name() + authConfig.Username = machine.Login() + authConfig.Password = machine.Password() + } + // Resolve the image reference. + dockerInspectResponse, err := dockerClient.Inspect(ctx, flags.Image) + if err != nil { + return nil, err + } + imageID := dockerInspectResponse.ImageID + + currentImageDigest := "" + { + // TODO: need to resolve the current image digest. + } + imageDigest, err := findExistingDigestForImageID(ctx, pluginFullName, authConfig, imageID, currentImageDigest) + if err != nil { + return nil, err + } + if imageDigest == "" { + imageDigest, err = pushImage(ctx, dockerClient, authConfig, pluginConfig, imageID) + if err != nil { + return nil, err + } + } + // TODO: log image digest wasn't pushed. + + plugin, err := bufremoteplugin.NewPlugin( + pluginConfig.PluginVersion, + pluginConfig.Dependencies, + pluginConfig.Registry, + imageDigest, + pluginConfig.SourceURL, + pluginConfig.Description, + ) + if err != nil { + return nil, err + } + // TODO: upload the image to the BSR + _ = plugin + content := &pluginv1beta1.UploadImageRequest_Content{ + PluginRef: &pluginv1beta1.PluginRef{ + Value: &pluginv1beta1.PluginRef_Name_{ + Name: &pluginv1beta1.PluginRef_Name{ + Owner: pluginFullName.Owner(), + Plugin: pluginFullName.Name(), + }, + }, + }, + Version: "", + Revision: 0, + LicenseUrl: "", + LicenseSpdxIdentifier: "", + CodeGeneration: &pluginv1beta1.CodeGenerationConfig{}, + } + b, _ := protojson.MarshalOptions{Multiline: true}.Marshal(content) + fmt.Println(string(b)) + + return nil, fmt.Errorf("not implemented") +} + +func validateFlags(flags *flags) error { + if err := validateLabelFlags(flags); err != nil { + return err + } + if err := validateTypeFlags(flags); err != nil { + return err + } + return nil +} + +func validateLabelFlags(flags *flags) error { + return validateLabelFlagValues(flags) +} + +func validateTypeFlags(flags *flags) error { + var usedFlags []string + if flags.Image != "" { + usedFlags = append(usedFlags, imageFlagName) + } + if flags.CheckBinary != "" { + usedFlags = append(usedFlags, checkBinaryFlagName) + } + if len(usedFlags) > 1 { + usedFlagsErrStr := strings.Join( + slicesext.Map( + usedFlags, + func(flag string) string { return fmt.Sprintf("--%s", flag) }, + ), + ", ", + ) + return appcmd.NewInvalidArgumentErrorf("These flags cannot be used in combination with one another: %s", usedFlagsErrStr) + } + if flags.Image != "" && flags.ImageConfig == "" { + return appcmd.NewInvalidArgumentErrorf( + "--%s is required if --%s is set", + imageConfigFlagName, + imageFlagName, + ) + } + //if flags.Binary != "" && flags.BinaryType == "" { + // return appcmd.NewInvalidArgumentErrorf( + // "--%s is required if --%s is set", + // binaryTypeFlagName, + // binaryFlagName, + // ) + //} + return nil +} + +func validateLabelFlagValues(flags *flags) error { + for _, label := range flags.Labels { + if label == "" { + return appcmd.NewInvalidArgumentErrorf("--%s requires a non-empty string", labelFlagName) + } + } + return nil +} + +var ( + zstdEncoder, _ = zstd.NewWriter(nil) +) + +func zstdCompress(src []byte) []byte { + return zstdEncoder.EncodeAll(src, make([]byte, 0, len(src))) +} + +func newStorageosProvider(disableSymlinks bool) storageos.Provider { + var options []storageos.ProviderOption + if !disableSymlinks { + options = append(options, storageos.ProviderWithSymlinks()) + } + return storageos.NewProvider(options...) +} + +// pushImage pushes the image to the OCI registry. It returns the digest of the +// pushed image. +func pushImage( + ctx context.Context, + dockerClient bufremoteplugindocker.Client, + authConfig *bufremoteplugindocker.RegistryAuthConfig, + pluginConfig *bufremotepluginconfig.Config, + image string, +) (_ string, retErr error) { + tagResponse, err := dockerClient.Tag(ctx, image, pluginConfig) + if err != nil { + return "", err + } + createdImage := tagResponse.Image + // We tag a Docker image using a unique ID label each time. + // After we're done publishing the image, we delete it to not leave a lot of images left behind. + defer func() { + if _, err := dockerClient.Delete(ctx, createdImage); err != nil { + retErr = multierr.Append(retErr, fmt.Errorf("failed to delete image %q", createdImage)) + } + }() + pushResponse, err := dockerClient.Push(ctx, createdImage, authConfig) + if err != nil { + return "", err + } + return pushResponse.Digest, nil +} + +// findExistingDigestForImageID will query the OCI registry to see if the imageID already exists. +// If an image is found with the same imageID, its digest will be returned (and we'll skip pushing to OCI registry). +// +// It performs the following search: +// +// - GET /v2/{owner}/{plugin}/tags/list +// - For each tag: +// - Fetch image: GET /v2/{owner}/{plugin}/manifests/{tag} +// - If image manifest matches imageID, we can use the image digest for the image. +func findExistingDigestForImageID( + ctx context.Context, + pluginFullName bufplugin.PluginFullName, + authConfig *bufremoteplugindocker.RegistryAuthConfig, + imageID string, + currentImageDigest string, +) (string, error) { + repo, err := name.NewRepository(pluginFullName.String()) + if err != nil { + return "", err + } + auth := &authn.Basic{Username: authConfig.Username, Password: authConfig.Password} + remoteOpts := []remote.Option{remote.WithContext(ctx), remote.WithAuth(auth)} + // First attempt to see if the current image digest matches the image ID + if currentImageDigest != "" { + remoteImageID, _, err := getImageIDAndDigestFromReference(ctx, repo.Digest(currentImageDigest), remoteOpts...) + if err != nil { + return "", err + } + if remoteImageID == imageID { + return currentImageDigest, nil + } + } + // List all tags and check for a match + tags, err := remote.List(repo, remoteOpts...) + if err != nil { + structuredErr := new(transport.Error) + if errors.As(err, &structuredErr) { + if structuredErr.StatusCode == http.StatusUnauthorized { + return "", errors.New("you are not authenticated. For details, visit https://buf.build/docs/bsr/authentication") + } + if structuredErr.StatusCode == http.StatusNotFound { + return "", nil + } + } + return "", err + } + for _, tag := range tags { + remoteImageID, imageDigest, err := getImageIDAndDigestFromReference(ctx, repo.Tag(tag), remoteOpts...) + if err != nil { + return "", err + } + if remoteImageID == imageID { + return imageDigest, nil + } + } + return "", nil +} + +// getImageIDAndDigestFromReference takes an image reference and returns 2 resolved digests: +// +// 1. The image config digest (https://github.com/opencontainers/image-spec/blob/v1.1.0/config.md) +// 2. The image manifest digest (https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md) +// +// The incoming ref is expected to be either an image manifest digest or an image index digest. +func getImageIDAndDigestFromReference( + ctx context.Context, + ref name.Reference, + options ...remote.Option, +) (string, string, error) { + puller, err := remote.NewPuller(options...) + if err != nil { + return "", "", err + } + desc, err := puller.Get(ctx, ref) + if err != nil { + return "", "", err + } + + switch { + case desc.MediaType.IsIndex(): + imageIndex, err := desc.ImageIndex() + if err != nil { + return "", "", fmt.Errorf("failed to get image index: %w", err) + } + indexManifest, err := imageIndex.IndexManifest() + if err != nil { + return "", "", fmt.Errorf("failed to get image manifests: %w", err) + } + var manifest pkgv1.Descriptor + for _, desc := range indexManifest.Manifests { + if p := desc.Platform; p != nil { + // Drop attestations, which don't have a valid platform set. + if p.OS == "unknown" && p.Architecture == "unknown" { + continue + } + manifest = desc + break + } + } + refNameWithoutDigest, _, ok := strings.Cut(ref.Name(), "@") + if !ok { + return "", "", fmt.Errorf("failed to parse reference name %q", ref) + } + repository, err := name.NewRepository(refNameWithoutDigest) + if err != nil { + return "", "", fmt.Errorf("failed to construct repository %q: %w", refNameWithoutDigest, err) + } + // We resolved the image index to an image manifest digest, we can now call this function + // again to resolve the image manifest digest to an image config digest. + return getImageIDAndDigestFromReference( + ctx, + repository.Digest(manifest.Digest.String()), + options..., + ) + case desc.MediaType.IsImage(): + imageManifest, err := desc.Image() + if err != nil { + return "", "", fmt.Errorf("failed to get image: %w", err) + } + imageManifestDigest, err := imageManifest.Digest() + if err != nil { + return "", "", fmt.Errorf("failed to get image digest for %q: %w", ref, err) + } + manifest, err := imageManifest.Manifest() + if err != nil { + return "", "", fmt.Errorf("failed to get image manifest for %q: %w", ref, err) + } + return manifest.Config.Digest.String(), imageManifestDigest.String(), nil + } + return "", "", fmt.Errorf("unsupported media type: %q", desc.MediaType) +} diff --git a/private/buf/cmd/buf/command/beta/registry/plugin/pluginpush/usage.gen.go b/private/buf/cmd/buf/command/plugin/pluginpush/usage.gen.go similarity index 100% rename from private/buf/cmd/buf/command/beta/registry/plugin/pluginpush/usage.gen.go rename to private/buf/cmd/buf/command/plugin/pluginpush/usage.gen.go diff --git a/private/buf/cmd/buf/command/registry/plugin/plugincreate/plugincreate.go b/private/buf/cmd/buf/command/registry/plugin/plugincreate/plugincreate.go new file mode 100644 index 0000000000..7c1c7b30dc --- /dev/null +++ b/private/buf/cmd/buf/command/registry/plugin/plugincreate/plugincreate.go @@ -0,0 +1,153 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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 plugincreate + +import ( + "context" + "fmt" + + ownerv1 "buf.build/gen/go/bufbuild/registry/protocolbuffers/go/buf/registry/owner/v1" + pluginv1beta1 "buf.build/gen/go/bufbuild/registry/protocolbuffers/go/buf/registry/plugin/v1beta1" + "connectrpc.com/connect" + "github.com/bufbuild/buf/private/buf/bufcli" + "github.com/bufbuild/buf/private/buf/bufprint" + "github.com/bufbuild/buf/private/bufpkg/bufapi" + "github.com/bufbuild/buf/private/bufpkg/bufplugin" + "github.com/bufbuild/buf/private/pkg/app/appcmd" + "github.com/bufbuild/buf/private/pkg/app/appext" + "github.com/bufbuild/buf/private/pkg/syserror" + "github.com/spf13/pflag" +) + +const ( + formatFlagName = "format" + visibilityFlagName = "visibility" + defaultLabeFlagName = "default-label-name" + + defaultDefaultLabel = "main" +) + +// NewCommand returns a new Command +func NewCommand( + name string, + builder appext.SubCommandBuilder, +) *appcmd.Command { + flags := newFlags() + return &appcmd.Command{ + Use: name + " ", + Short: "Create a BSR plugin", + Args: appcmd.ExactArgs(1), + Run: builder.NewRunFunc( + func(ctx context.Context, container appext.Container) error { + return run(ctx, container, flags) + }, + ), + BindFlags: flags.Bind, + } +} + +type flags struct { + Format string + Visibility string + DefautlLabel string +} + +func newFlags() *flags { + return &flags{} +} + +func (f *flags) Bind(flagSet *pflag.FlagSet) { + bufcli.BindVisibility(flagSet, &f.Visibility, visibilityFlagName, false) + flagSet.StringVar( + &f.Format, + formatFlagName, + bufprint.FormatText.String(), + fmt.Sprintf(`The output format to use. Must be one of %s`, bufprint.AllFormatsString), + ) + flagSet.StringVar( + &f.DefautlLabel, + defaultLabeFlagName, + defaultDefaultLabel, + "The default label name of the module", + ) +} + +func run( + ctx context.Context, + container appext.Container, + flags *flags, +) error { + pluginFullName, err := bufplugin.ParsePluginFullName(container.Arg(0)) + if err != nil { + return appcmd.WrapInvalidArgumentError(err) + } + fmt.Println("pluginFullName", pluginFullName) + + visibility, err := bufcli.VisibilityFlagToPluginVisibilityAllowUnspecified(flags.Visibility) + if err != nil { + return appcmd.WrapInvalidArgumentError(err) + } + format, err := bufprint.ParseFormat(flags.Format) + if err != nil { + return appcmd.WrapInvalidArgumentError(err) + } + + clientConfig, err := bufcli.NewConnectClientConfig(container) + if err != nil { + return err + } + pluginServiceClient := bufapi.NewClientProvider(clientConfig). + PluginV1Beta1PluginServiceClient(pluginFullName.Registry()) + + pluginResponse, err := pluginServiceClient.CreatePlugins(ctx, connect.NewRequest( + &pluginv1beta1.CreatePluginsRequest{ + Values: []*pluginv1beta1.CreatePluginsRequest_Value{ + { + OwnerRef: &ownerv1.OwnerRef{ + Value: &ownerv1.OwnerRef_Name{ + Name: pluginFullName.Owner(), + }, + }, + Name: pluginFullName.Name(), + Visibility: visibility, + SourceUrl: "", // TODO: flag? + Type: pluginv1beta1.PluginType_PLUGIN_TYPE_CHECK, + }, + }, + }, + )) + if err != nil { + if connect.CodeOf(err) == connect.CodeAlreadyExists { + return bufcli.NewPluginNameAlreadyExistsError(pluginFullName.String()) + } + return err + } + plugins := pluginResponse.Msg.Plugins + if len(plugins) != 1 { + return syserror.Newf("unexpected number of plugins returned from server: %d", len(plugins)) + } + if format == bufprint.FormatText { + _, err = fmt.Fprintf(container.Stdout(), "Created %s.\n", pluginFullName) + if err != nil { + return syserror.Wrap(err) + } + return nil + } + return bufprint.PrintNames( + container.Stdout(), + format, + bufprint.NewPluginEntity(plugins[0], pluginFullName), + ) +} diff --git a/private/buf/cmd/buf/command/registry/plugin/plugincreate/usage.gen.go b/private/buf/cmd/buf/command/registry/plugin/plugincreate/usage.gen.go new file mode 100644 index 0000000000..6934f63116 --- /dev/null +++ b/private/buf/cmd/buf/command/registry/plugin/plugincreate/usage.gen.go @@ -0,0 +1,19 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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. + +// Generated. DO NOT EDIT. + +package plugincreate + +import _ "github.com/bufbuild/buf/private/usage" diff --git a/private/buf/cmd/buf/command/registry/plugin/plugindelete/plugindelete.go b/private/buf/cmd/buf/command/registry/plugin/plugindelete/plugindelete.go new file mode 100644 index 0000000000..1d288c789b --- /dev/null +++ b/private/buf/cmd/buf/command/registry/plugin/plugindelete/plugindelete.go @@ -0,0 +1,113 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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 plugindelete + +import ( + "context" + "fmt" + + "github.com/bufbuild/buf/private/bufpkg/bufplugin" + "github.com/bufbuild/buf/private/pkg/app/appcmd" + "github.com/bufbuild/buf/private/pkg/app/appext" + "github.com/spf13/pflag" +) + +const forceFlagName = "force" + +// NewCommand returns a new Command +func NewCommand( + name string, + builder appext.SubCommandBuilder, +) *appcmd.Command { + flags := newFlags() + return &appcmd.Command{ + Use: name + " ", + Short: "Delete a BSR plugin", + Args: appcmd.ExactArgs(1), + Run: builder.NewRunFunc( + func(ctx context.Context, container appext.Container) error { + return run(ctx, container, flags) + }, + ), + BindFlags: flags.Bind, + } +} + +type flags struct { + Force bool +} + +func newFlags() *flags { + return &flags{} +} + +func (f *flags) Bind(flagSet *pflag.FlagSet) { + flagSet.BoolVar( + &f.Force, + forceFlagName, + false, + "Force deletion without confirming. Use with caution", + ) +} + +func run( + ctx context.Context, + container appext.Container, + flags *flags, +) error { + pluginFullName, err := bufplugin.ParsePluginFullName(container.Arg(0)) + if err != nil { + return appcmd.WrapInvalidArgumentError(err) + } + fmt.Println("pluginFullName", pluginFullName) + /* + clientConfig, err := bufcli.NewConnectClientConfig(container) + if err != nil { + return err + } + if !flags.Force { + if err := bufcli.PromptUserForDelete(container, "entity", moduleFullName.Name()); err != nil { + return err + } + } + moduleServiceClient := bufapi.NewClientProvider(clientConfig).V1ModuleServiceClient(moduleFullName.Registry()) + if _, err := moduleServiceClient.DeleteModules( + ctx, + connect.NewRequest( + &modulev1.DeleteModulesRequest{ + ModuleRefs: []*modulev1.ModuleRef{ + { + Value: &modulev1.ModuleRef_Name_{ + Name: &modulev1.ModuleRef_Name{ + Owner: moduleFullName.Owner(), + Module: moduleFullName.Name(), + }, + }, + }, + }, + }, + ), + ); err != nil { + if connect.CodeOf(err) == connect.CodeNotFound { + return bufcli.NewModuleNotFoundError(container.Arg(0)) + } + return err + } + if _, err := fmt.Fprintf(container.Stdout(), "Deleted %s.\n", moduleFullName); err != nil { + return syserror.Wrap(err) + } + */ + return nil +} diff --git a/private/buf/cmd/buf/command/beta/registry/plugin/plugindelete/usage.gen.go b/private/buf/cmd/buf/command/registry/plugin/plugindelete/usage.gen.go similarity index 100% rename from private/buf/cmd/buf/command/beta/registry/plugin/plugindelete/usage.gen.go rename to private/buf/cmd/buf/command/registry/plugin/plugindelete/usage.gen.go diff --git a/private/buf/cmd/buf/command/registry/plugin/plugininfo/plugininfo.go b/private/buf/cmd/buf/command/registry/plugin/plugininfo/plugininfo.go new file mode 100644 index 0000000000..f8a6812a16 --- /dev/null +++ b/private/buf/cmd/buf/command/registry/plugin/plugininfo/plugininfo.go @@ -0,0 +1,121 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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 plugininfo + +import ( + "context" + "fmt" + + "github.com/bufbuild/buf/private/buf/bufprint" + "github.com/bufbuild/buf/private/bufpkg/bufplugin" + "github.com/bufbuild/buf/private/pkg/app/appcmd" + "github.com/bufbuild/buf/private/pkg/app/appext" + "github.com/spf13/pflag" +) + +const formatFlagName = "format" + +// NewCommand returns a new Command +func NewCommand( + name string, + builder appext.SubCommandBuilder, +) *appcmd.Command { + flags := newFlags() + return &appcmd.Command{ + Use: name + " ", + Short: "Get a BSR plugin", + Args: appcmd.ExactArgs(1), + Run: builder.NewRunFunc( + func(ctx context.Context, container appext.Container) error { + return run(ctx, container, flags) + }, + ), + BindFlags: flags.Bind, + } +} + +type flags struct { + Format string +} + +func newFlags() *flags { + return &flags{} +} + +func (f *flags) Bind(flagSet *pflag.FlagSet) { + flagSet.StringVar( + &f.Format, + formatFlagName, + bufprint.FormatText.String(), + fmt.Sprintf(`The output format to use. Must be one of %s`, bufprint.AllFormatsString), + ) +} + +func run( + ctx context.Context, + container appext.Container, + flags *flags, +) error { + pluginFullName, err := bufplugin.ParsePluginFullName(container.Arg(0)) + if err != nil { + return appcmd.WrapInvalidArgumentError(err) + } + fmt.Println(pluginFullName) + /* + format, err := bufprint.ParseFormat(flags.Format) + if err != nil { + return appcmd.WrapInvalidArgumentError(err) + } + + clientConfig, err := bufcli.NewConnectClientConfig(container) + if err != nil { + return err + } + moduleServiceClient := bufapi.NewClientProvider(clientConfig).V1ModuleServiceClient(moduleFullName.Registry()) + resp, err := moduleServiceClient.GetModules( + ctx, + connect.NewRequest( + &modulev1.GetModulesRequest{ + ModuleRefs: []*modulev1.ModuleRef{ + { + Value: &modulev1.ModuleRef_Name_{ + Name: &modulev1.ModuleRef_Name{ + Owner: moduleFullName.Owner(), + Module: moduleFullName.Name(), + }, + }, + }, + }, + }, + ), + ) + if err != nil { + if connect.CodeOf(err) == connect.CodeNotFound { + return bufcli.NewModuleNotFoundError(container.Arg(0)) + } + return err + } + modules := resp.Msg.Modules + if len(modules) != 1 { + return syserror.Newf("unexpected number of modules returned from server: %d", len(modules)) + } + return bufprint.PrintEntity( + container.Stdout(), + format, + bufprint.NewModuleEntity(modules[0], moduleFullName), + ) + */ + return nil +} diff --git a/private/buf/cmd/buf/command/registry/plugin/plugininfo/usage.gen.go b/private/buf/cmd/buf/command/registry/plugin/plugininfo/usage.gen.go new file mode 100644 index 0000000000..7889165761 --- /dev/null +++ b/private/buf/cmd/buf/command/registry/plugin/plugininfo/usage.gen.go @@ -0,0 +1,19 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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. + +// Generated. DO NOT EDIT. + +package plugininfo + +import _ "github.com/bufbuild/buf/private/usage" diff --git a/private/buf/cmd/buf/command/registry/plugin/pluginupdate/pluginupdate.go b/private/buf/cmd/buf/command/registry/plugin/pluginupdate/pluginupdate.go new file mode 100644 index 0000000000..7df5429fa6 --- /dev/null +++ b/private/buf/cmd/buf/command/registry/plugin/pluginupdate/pluginupdate.go @@ -0,0 +1,145 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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 pluginupdate + +import ( + "context" + "fmt" + + "github.com/bufbuild/buf/private/buf/bufcli" + "github.com/bufbuild/buf/private/bufpkg/bufplugin" + "github.com/bufbuild/buf/private/pkg/app/appcmd" + "github.com/bufbuild/buf/private/pkg/app/appext" + "github.com/spf13/pflag" +) + +const ( + visibilityFlagName = "visibility" + descriptionFlagName = "description" + urlFlagName = "url" + defaultLabelFlagName = "default-label-name" +) + +// NewCommand returns a new Command +func NewCommand(name string, builder appext.SubCommandBuilder) *appcmd.Command { + flags := newFlags() + return &appcmd.Command{ + Use: name + " ", + Short: "Update BSR plugin settings", + Args: appcmd.ExactArgs(1), + Run: builder.NewRunFunc( + func(ctx context.Context, container appext.Container) error { + return run(ctx, container, flags) + }, + ), + BindFlags: flags.Bind, + } +} + +type flags struct { + Visibility string + Description *string + URL *string + DefaultLabel string +} + +func newFlags() *flags { + return &flags{} +} + +func (f *flags) Bind(flagSet *pflag.FlagSet) { + bufcli.BindVisibility(flagSet, &f.Visibility, visibilityFlagName, true) + bufcli.BindStringPointer( + flagSet, + descriptionFlagName, + &f.Description, + "The new description for the plugin", + ) + bufcli.BindStringPointer( + flagSet, + urlFlagName, + &f.URL, + "The new URL for the plugin", + ) + flagSet.StringVar( + &f.DefaultLabel, + defaultLabelFlagName, + "", + "The label that commits are pushed to by default", + ) +} + +func run( + ctx context.Context, + container appext.Container, + flags *flags, +) error { + pluginFullName, err := bufplugin.ParsePluginFullName(container.Arg(0)) + if err != nil { + return appcmd.WrapInvalidArgumentError(err) + } + fmt.Println("pluginFullName", pluginFullName) + /* + visibility, err := bufcli.VisibilityFlagToVisibilityAllowUnspecified(flags.Visibility) + if err != nil { + return appcmd.WrapInvalidArgumentError(err) + } + clientConfig, err := bufcli.NewConnectClientConfig(container) + if err != nil { + return err + } + moduleServiceClient := bufapi.NewClientProvider(clientConfig).V1ModuleServiceClient(moduleFullName.Registry())1 + visibilityUpdate := &visibility + if visibility == modulev1.ModuleVisibility_MODULE_VISIBILITY_UNSPECIFIED { + visibilityUpdate = nil + } + defaultLabelUpdate := &flags.DefaultLabel + if flags.DefaultLabel == "" { + defaultLabelUpdate = nil + } + if _, err := moduleServiceClient.UpdateModules( + ctx, + &connect.Request[modulev1.UpdateModulesRequest]{ + Msg: &modulev1.UpdateModulesRequest{ + Values: []*modulev1.UpdateModulesRequest_Value{ + { + ModuleRef: &modulev1.ModuleRef{ + Value: &modulev1.ModuleRef_Name_{ + Name: &modulev1.ModuleRef_Name{ + Owner: moduleFullName.Owner(), + Module: moduleFullName.Name(), + }, + }, + }, + Description: flags.Description, + Url: flags.URL, + Visibility: visibilityUpdate, + DefaultLabelName: defaultLabelUpdate, + }, + }, + }, + }, + ); err != nil { + if connect.CodeOf(err) == connect.CodeNotFound { + return bufcli.NewModuleNotFoundError(container.Arg(0)) + } + return err + } + if _, err := fmt.Fprintln(container.Stdout(), "Module updated."); err != nil { + return syserror.Wrap(err) + } + */ + return nil +} diff --git a/private/buf/cmd/buf/command/registry/plugin/pluginupdate/usage.gen.go b/private/buf/cmd/buf/command/registry/plugin/pluginupdate/usage.gen.go new file mode 100644 index 0000000000..23136ff2a4 --- /dev/null +++ b/private/buf/cmd/buf/command/registry/plugin/pluginupdate/usage.gen.go @@ -0,0 +1,19 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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. + +// Generated. DO NOT EDIT. + +package pluginupdate + +import _ "github.com/bufbuild/buf/private/usage" diff --git a/private/bufpkg/bufapi/bufapi.go b/private/bufpkg/bufapi/bufapi.go index 61be94c414..a7e009b608 100644 --- a/private/bufpkg/bufapi/bufapi.go +++ b/private/bufpkg/bufapi/bufapi.go @@ -18,6 +18,7 @@ import ( "buf.build/gen/go/bufbuild/registry/connectrpc/go/buf/registry/module/v1/modulev1connect" "buf.build/gen/go/bufbuild/registry/connectrpc/go/buf/registry/module/v1beta1/modulev1beta1connect" "buf.build/gen/go/bufbuild/registry/connectrpc/go/buf/registry/owner/v1/ownerv1connect" + "buf.build/gen/go/bufbuild/registry/connectrpc/go/buf/registry/plugin/v1beta1/pluginv1beta1connect" "github.com/bufbuild/buf/private/pkg/connectclient" ) @@ -136,6 +137,21 @@ type V1Beta1UploadServiceClientProvider interface { V1Beta1UploadServiceClient(registry string) modulev1beta1connect.UploadServiceClient } +// PluginV1Beta1UploadServiceClientProvider provides UploadServiceClients for plugins. +type PluginV1Beta1UploadServiceClientProvider interface { + PluginV1Beta1UploadServiceClient(registry string) pluginv1beta1connect.UploadServiceClient +} + +// PluginV1Beta1PluginServiceClientProvider provides PluginServiceClients for plugins. +type PluginV1Beta1PluginServiceClientProvider interface { + PluginV1Beta1PluginServiceClient(registry string) pluginv1beta1connect.PluginServiceClient +} + +// PluginV1Beta1DownloadServiceClientProvider provides DownloadServiceClients for plugins. +type PluginV1Beta1DownloadServiceClientProvider interface { + PluginV1Beta1DownloadServiceClient(registry string) pluginv1beta1connect.DownloadServiceClient +} + // ClientProvider provides API clients for BSR services. type ClientProvider interface { V1CommitServiceClientProvider @@ -154,6 +170,9 @@ type ClientProvider interface { V1Beta1LabelServiceClientProvider V1Beta1ModuleServiceClientProvider V1Beta1UploadServiceClientProvider + PluginV1Beta1UploadServiceClientProvider + PluginV1Beta1DownloadServiceClientProvider + PluginV1Beta1PluginServiceClientProvider } // NewClientProvider returns a new ClientProvider. @@ -301,6 +320,30 @@ func (c *clientProvider) V1Beta1UploadServiceClient(registry string) modulev1bet ) } +func (c *clientProvider) PluginV1Beta1UploadServiceClient(registry string) pluginv1beta1connect.UploadServiceClient { + return connectclient.Make( + c.clientConfig, + registry, + pluginv1beta1connect.NewUploadServiceClient, + ) +} + +func (c *clientProvider) PluginV1Beta1DownloadServiceClient(registry string) pluginv1beta1connect.DownloadServiceClient { + return connectclient.Make( + c.clientConfig, + registry, + pluginv1beta1connect.NewDownloadServiceClient, + ) +} + +func (c *clientProvider) PluginV1Beta1PluginServiceClient(registry string) pluginv1beta1connect.PluginServiceClient { + return connectclient.Make( + c.clientConfig, + registry, + pluginv1beta1connect.NewPluginServiceClient, + ) +} + type nopClientProvider struct{} func (nopClientProvider) V1CommitServiceClient(registry string) modulev1connect.CommitServiceClient { @@ -366,3 +409,15 @@ func (nopClientProvider) V1Beta1ModuleServiceClient(registry string) modulev1bet func (nopClientProvider) V1Beta1UploadServiceClient(registry string) modulev1beta1connect.UploadServiceClient { return modulev1beta1connect.UnimplementedUploadServiceHandler{} } + +func (nopClientProvider) PluginV1Beta1UploadServiceClient(registry string) pluginv1beta1connect.UploadServiceClient { + return pluginv1beta1connect.UnimplementedUploadServiceHandler{} +} + +func (nopClientProvider) PluginV1Beta1DownloadServiceClient(registry string) pluginv1beta1connect.DownloadServiceClient { + return pluginv1beta1connect.UnimplementedDownloadServiceHandler{} +} + +func (nopClientProvider) PluginV1Beta1PluginServiceClient(registry string) pluginv1beta1connect.PluginServiceClient { + return pluginv1beta1connect.UnimplementedPluginServiceHandler{} +} diff --git a/private/bufpkg/bufplugin/bufplugin.go b/private/bufpkg/bufplugin/bufplugin.go new file mode 100644 index 0000000000..3e18be9213 --- /dev/null +++ b/private/bufpkg/bufplugin/bufplugin.go @@ -0,0 +1,16 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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 bufplugin contains the core primitives for working with Buf plugins. +package bufplugin diff --git a/private/bufpkg/bufplugin/commit.go b/private/bufpkg/bufplugin/commit.go new file mode 100644 index 0000000000..fa1daccddb --- /dev/null +++ b/private/bufpkg/bufplugin/commit.go @@ -0,0 +1,72 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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 bufplugin + +import ( + "sync" + "time" +) + +// Commit represents a Commit on the BSR. +type Commit interface { + // PluginKey returns the PluginKey for the Commit. + PluginKey() PluginKey + // CreateTime returns the time the Commit was created on the BSR. + CreateTime() (time.Time, error) + + isCommit() +} + +// NewCommit returns a new Commit. +func NewCommit( + pluginKey PluginKey, + getCreateTime func() (time.Time, error), +) Commit { + return newCommit( + pluginKey, + getCreateTime, + ) +} + +// *** PRIVATE *** + +type commit struct { + pluginKey PluginKey + getCreateTime func() (time.Time, error) +} + +func newCommit( + pluginKey PluginKey, + getCreateTime func() (time.Time, error), +) *commit { + return &commit{ + pluginKey: pluginKey, + getCreateTime: sync.OnceValues(getCreateTime), + } +} + +func (c *commit) PluginKey() PluginKey { + return c.pluginKey +} + +func (c *commit) CreateTime() (time.Time, error) { + // This may invoke tamper-proofing per newCommit construction. + if _, err := c.pluginKey.Digest(); err != nil { + return time.Time{}, err + } + return c.getCreateTime() +} + +func (*commit) isCommit() {} diff --git a/private/bufpkg/bufplugin/digest.go b/private/bufpkg/bufplugin/digest.go new file mode 100644 index 0000000000..075d70f013 --- /dev/null +++ b/private/bufpkg/bufplugin/digest.go @@ -0,0 +1,143 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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 bufplugin + +import ( + "encoding/hex" + "fmt" + "strconv" + + "github.com/bufbuild/buf/private/bufpkg/bufcas" + "github.com/bufbuild/buf/private/pkg/syserror" +) + +const ( + // DigestTypeP1 represents the p4 plugin digest type. + // + // This is the newest digest type, and should generally be used. The string value + // of this is "p1". + DigestTypeP1 = iota + 1 +) + +var ( + // AllDigestTypes are all known DigestTypes. + AllDigestTypes = []DigestType{ + DigestTypeP1, + } + digestTypeToString = map[DigestType]string{ + DigestTypeP1: "p1", + } + stringToDigestType = map[string]DigestType{ + "p1": DigestTypeP1, + } +) + +// DigestType is a type of digest. +type DigestType int + +// ParseDigestType parses a DigestType from its string representation. +// +// This reverses DigestType.String(). +// +// Returns an error of type *ParseError if thie string could not be parsed. +func ParseDigestType(s string) (DigestType, error) { + d, ok := stringToDigestType[s] + if !ok { + return 0, &ParseError{ + typeString: "plugin digest type", + input: s, + err: fmt.Errorf("unknown type: %q", s), + } + } + return d, nil +} + +// String prints the string representation of the DigestType. +func (d DigestType) String() string { + s, ok := digestTypeToString[d] + if !ok { + return strconv.Itoa(int(d)) + } + return s +} + +// Digest is a digest of some content. +// +// It consists of a DigestType and a digest value. +type Digest interface { + // String() prints typeString:hexValue. + fmt.Stringer + + // Type returns the type of digest. + // Always a valid value. + Type() DigestType + // Value returns the digest value. + // + // Always non-empty. + Value() []byte + + isDigest() +} + +// NewDigest creates a new Digest. +func NewDigest(digestType DigestType, bufcasDigest bufcas.Digest) (Digest, error) { + switch digestType { + case DigestTypeP1: + if bufcasDigest.Type() != bufcas.DigestTypeShake256 { + return nil, syserror.Newf( + "trying to create a %v Digest for a cas Digest of type %v", + digestType, + bufcasDigest.Type(), + ) + } + return newDigest(digestType, bufcasDigest), nil + default: + // This is a system error. + return nil, syserror.Newf("unknown DigestType: %v", digestType) + } +} + +/// *** PRIVATE *** + +type digest struct { + digestType DigestType + bufcasDigest bufcas.Digest + // Cache as we call String pretty often. + // We could do this lazily but not worth it. + stringValue string +} + +// validation should occur outside of this function. +func newDigest(digestType DigestType, bufcasDigest bufcas.Digest) *digest { + return &digest{ + digestType: digestType, + bufcasDigest: bufcasDigest, + stringValue: digestType.String() + ":" + hex.EncodeToString(bufcasDigest.Value()), + } +} + +func (d *digest) Type() DigestType { + return d.digestType +} + +func (d *digest) Value() []byte { + return d.bufcasDigest.Value() +} + +func (d *digest) String() string { + return d.stringValue +} + +func (*digest) isDigest() {} diff --git a/private/bufpkg/bufplugin/errors.go b/private/bufpkg/bufplugin/errors.go new file mode 100644 index 0000000000..a6d52901e7 --- /dev/null +++ b/private/bufpkg/bufplugin/errors.go @@ -0,0 +1,68 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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 bufplugin + +import "strings" + +// ParseError is an error that occurred during parsing. +// +// This is returned by all Parse.* functions in this package. +type ParseError struct { + // typeString is the user-consumable string representing of the type that was attempted to be parsed. + // + // Users cannot rely on this data being structured. + // Examples: "digest", "digest type". + typeString string + // input is the input string that was attempted to be parsed. + input string + // err is the underlying error. + // + // Err may be a *ParseError itself. + // + // This is an error we may give back to the user, use pretty strings that should + // be read. + err error +} + +// Error implements the error interface. +func (p *ParseError) Error() string { + if p == nil { + return "" + } + var builder strings.Builder + _, _ = builder.WriteString(`could not parse`) + if p.typeString != "" { + _, _ = builder.WriteString(` `) + _, _ = builder.WriteString(p.typeString) + } + if p.input != "" { + _, _ = builder.WriteString(` "`) + _, _ = builder.WriteString(p.input) + _, _ = builder.WriteString(`"`) + } + if p.err != nil { + _, _ = builder.WriteString(`: `) + _, _ = builder.WriteString(p.err.Error()) + } + return builder.String() +} + +// Unwrap returns the underlying error. +func (p *ParseError) Unwrap() error { + if p == nil { + return nil + } + return p.err +} diff --git a/private/bufpkg/bufplugin/parse.go b/private/bufpkg/bufplugin/parse.go new file mode 100644 index 0000000000..b31d7d93cd --- /dev/null +++ b/private/bufpkg/bufplugin/parse.go @@ -0,0 +1,48 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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 bufplugin + +import ( + "errors" + "strings" +) + +func parsePluginFullNameComponents(path string) (registry string, owner string, name string, err error) { + slashSplit := strings.Split(path, "/") + if len(slashSplit) != 3 { + return "", "", "", newInvalidPluginFullNameStringError(path) + } + registry = strings.TrimSpace(slashSplit[0]) + if registry == "" { + return "", "", "", newInvalidPluginFullNameStringError(path) + } + owner = strings.TrimSpace(slashSplit[1]) + if owner == "" { + return "", "", "", newInvalidPluginFullNameStringError(path) + } + name = strings.TrimSpace(slashSplit[2]) + if name == "" { + return "", "", "", newInvalidPluginFullNameStringError(path) + } + return registry, owner, name, nil +} + +func newInvalidPluginFullNameStringError(s string) error { + return &ParseError{ + typeString: "plugin name", + input: s, + err: errors.New("must be in the form registry/owner/name"), + } +} diff --git a/private/bufpkg/bufplugin/plugin_full_name.go b/private/bufpkg/bufplugin/plugin_full_name.go new file mode 100644 index 0000000000..96fa295527 --- /dev/null +++ b/private/bufpkg/bufplugin/plugin_full_name.go @@ -0,0 +1,140 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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 bufplugin + +import ( + "errors" + "fmt" + "strings" + + "github.com/bufbuild/buf/private/pkg/netext" +) + +// PluginFullName represents the full name of the Plugin, including its registry, owner, and name. +type PluginFullName interface { + // String returns "registry/owner/name". + fmt.Stringer + + // Registry returns the hostname of the BSR instance that this Plugin is contained within. + Registry() string + // Owner returns the name of the user or organization that owns this Plugin. + Owner() string + // Name returns the name of the Plugin. + Name() string + + isPluginFullName() +} + +// NewPluginFullName returns a new PluginFullName for the given components. +func NewPluginFullName( + registry string, + owner string, + name string, +) (PluginFullName, error) { + return newPluginFullName( + registry, + owner, + name, + ) +} + +// ParsePluginFullName parses a PluginFullName from a string in the form "registry/owner/name". +func ParsePluginFullName(pluginFullNameString string) (PluginFullName, error) { + // parsePluiginFullNameComponents returns ParseErrors. + registry, owner, name, err := parsePluginFullNameComponents(pluginFullNameString) + if err != nil { + return nil, err + } + + // We need to validate the parameters of the PluginFullName. + if err := validatePluginFullNameParameters(registry, owner, name); err != nil { + return nil, &ParseError{ + typeString: "plugin name", + input: pluginFullNameString, + err: err, + } + } + + // We don't rely on constructors for ParseErrors. + return NewPluginFullName(registry, owner, name) +} + +// *** PRIVATE *** + +type pluginFullName struct { + registry string + owner string + name string +} + +func newPluginFullName( + registry string, + owner string, + name string, +) (*pluginFullName, error) { + if err := validatePluginFullNameParameters(registry, owner, name); err != nil { + return nil, err + } + return &pluginFullName{ + registry: registry, + owner: owner, + name: name, + }, nil +} + +func (m *pluginFullName) Registry() string { + return m.registry +} + +func (m *pluginFullName) Owner() string { + return m.owner +} + +func (m *pluginFullName) Name() string { + return m.name +} + +func (m *pluginFullName) String() string { + return m.registry + "/" + m.owner + "/" + m.name +} + +func (*pluginFullName) isPluginFullName() {} + +// TODO(emcfarlane): move this to a shared location. +func validatePluginFullNameParameters( + registry string, + owner string, + name string, +) error { + if registry == "" { + return errors.New("registry is empty") + } + if _, err := netext.ValidateHostname(registry); err != nil { + return fmt.Errorf("registry %q is not a valid hostname: %w", registry, err) + } + if owner == "" { + return errors.New("owner is empty") + } + if strings.Contains(owner, "/") { + return fmt.Errorf("owner %q cannot contain slashes", owner) + } + if name == "" { + return errors.New("name is empty") + } + if strings.Contains(name, "/") { + return fmt.Errorf("name %q cannot contain slashes", name) + } + return nil +} diff --git a/private/bufpkg/bufplugin/plugin_key.go b/private/bufpkg/bufplugin/plugin_key.go new file mode 100644 index 0000000000..3177d9d61f --- /dev/null +++ b/private/bufpkg/bufplugin/plugin_key.go @@ -0,0 +1,106 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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 bufplugin + +import ( + "errors" + "fmt" + "sync" + + "github.com/bufbuild/buf/private/pkg/uuidutil" + "github.com/google/uuid" +) + +// PluginKey provides identifying information for a Plugin. +// +// TODO(emcfarlane) +type PluginKey interface { + // String returns "registry/owner/name:dashlessCommitID". + fmt.Stringer + + // PluginFullName returns the full name of the Plugin. + // + // Always present. + PluginFullName() PluginFullName + // CommitID returns the ID of the Commit. + // + // It is up to the caller to convert this to a dashless ID when necessary. + // + // Always present, that is CommitID() == uuid.Nil will always be false. + CommitID() uuid.UUID + // Digest returns the Plugin digest. + // + // TODO(emcfarlane) + Digest() (Digest, error) + + isPluginKey() +} + +func NewPluginKey( + pluginFullName PluginFullName, + commitID uuid.UUID, + getDigest func() (Digest, error), +) (PluginKey, error) { + return newPluginKey( + pluginFullName, + commitID, + getDigest, + ) +} + +// ** PRIVATE ** + +type pluginKey struct { + pluginFullName PluginFullName + commitID uuid.UUID + + getDigest func() (Digest, error) +} + +func newPluginKey( + pluginFullName PluginFullName, + commitID uuid.UUID, + getDigest func() (Digest, error), +) (*pluginKey, error) { + if pluginFullName == nil { + return nil, errors.New("nil PluginFullName when constructing PluginKey") + } + if commitID == uuid.Nil { + return nil, errors.New("empty commitID when constructing PluginKey") + } + return &pluginKey{ + pluginFullName: pluginFullName, + commitID: commitID, + getDigest: sync.OnceValues(getDigest), + }, nil +} + +func (p *pluginKey) PluginFullName() PluginFullName { + return p.pluginFullName +} + +func (p *pluginKey) CommitID() uuid.UUID { + return p.commitID +} + +func (p *pluginKey) Digest() (Digest, error) { + return p.getDigest() +} + +func (p *pluginKey) String() string { + return p.pluginFullName.String() + ":" + uuidutil.ToDashless(p.commitID) +} + +func (*pluginKey) isPluginKey() {} diff --git a/private/bufpkg/bufplugin/usage.gen.go b/private/bufpkg/bufplugin/usage.gen.go new file mode 100644 index 0000000000..6ffd8bf974 --- /dev/null +++ b/private/bufpkg/bufplugin/usage.gen.go @@ -0,0 +1,19 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// 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. + +// Generated. DO NOT EDIT. + +package bufplugin + +import _ "github.com/bufbuild/buf/private/usage"