From 18d6ef6b291c21c1839741fa7bd3568255d544a5 Mon Sep 17 00:00:00 2001 From: Edward McFarlane Date: Tue, 29 Apr 2025 23:31:29 +0200 Subject: [PATCH] Add buf policy push command --- private/buf/bufcli/uploader.go | 22 ++ private/buf/cmd/buf/buf.go | 8 + .../command/policy/policypush/policypush.go | 240 ++++++++++++++++++ .../command/policy/policypush/usage.gen.go | 19 ++ 4 files changed, 289 insertions(+) create mode 100644 private/buf/cmd/buf/command/policy/policypush/policypush.go create mode 100644 private/buf/cmd/buf/command/policy/policypush/usage.gen.go diff --git a/private/buf/bufcli/uploader.go b/private/buf/bufcli/uploader.go index e1e8c3b0eb..3afbb3e375 100644 --- a/private/buf/bufcli/uploader.go +++ b/private/buf/bufcli/uploader.go @@ -19,8 +19,11 @@ import ( "github.com/bufbuild/buf/private/bufpkg/bufmodule/bufmoduleapi" "github.com/bufbuild/buf/private/bufpkg/bufplugin" "github.com/bufbuild/buf/private/bufpkg/bufplugin/bufpluginapi" + "github.com/bufbuild/buf/private/bufpkg/bufpolicy" + "github.com/bufbuild/buf/private/bufpkg/bufpolicy/bufpolicyapi" "github.com/bufbuild/buf/private/bufpkg/bufregistryapi/bufregistryapimodule" "github.com/bufbuild/buf/private/bufpkg/bufregistryapi/bufregistryapiplugin" + "github.com/bufbuild/buf/private/bufpkg/bufregistryapi/bufregistryapipolicy" "github.com/bufbuild/buf/private/pkg/app/appext" ) @@ -42,6 +45,15 @@ func NewPluginUploader(container appext.Container) (bufplugin.Uploader, error) { return newPluginUploader(container, bufregistryapiplugin.NewClientProvider(clientConfig)), nil } +// NewPolicyUploader returns a new Uploader for Policys. +func NewPolicyUploader(container appext.Container) (bufpolicy.Uploader, error) { + clientConfig, err := NewConnectClientConfig(container) + if err != nil { + return nil, err + } + return newPolicyUploader(container, bufregistryapipolicy.NewClientProvider(clientConfig)), nil +} + func newModuleUploader( container appext.Container, clientProvider bufregistryapimodule.ClientProvider, @@ -63,3 +75,13 @@ func newPluginUploader( clientProvider, ) } + +func newPolicyUploader( + container appext.Container, + clientProvider bufregistryapipolicy.ClientProvider, +) bufpolicy.Uploader { + return bufpolicyapi.NewUploader( + container.Logger(), + clientProvider, + ) +} diff --git a/private/buf/cmd/buf/buf.go b/private/buf/cmd/buf/buf.go index 4a280e2e06..eafb643653 100644 --- a/private/buf/cmd/buf/buf.go +++ b/private/buf/cmd/buf/buf.go @@ -65,6 +65,7 @@ import ( "github.com/bufbuild/buf/private/buf/cmd/buf/command/plugin/pluginprune" "github.com/bufbuild/buf/private/buf/cmd/buf/command/plugin/pluginpush" "github.com/bufbuild/buf/private/buf/cmd/buf/command/plugin/pluginupdate" + "github.com/bufbuild/buf/private/buf/cmd/buf/command/policy/policypush" "github.com/bufbuild/buf/private/buf/cmd/buf/command/push" "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/module/modulecommit/modulecommitaddlabel" "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/module/modulecommit/modulecommitinfo" @@ -195,6 +196,13 @@ func NewRootCommand(name string) *appcmd.Command { pluginprune.NewCommand("prune", builder), }, }, + { + Use: "policy", + Short: "Work with policies", + SubCommands: []*appcmd.Command{ + policypush.NewCommand("push", builder), + }, + }, { Use: "registry", Short: "Manage assets on the Buf Schema Registry", diff --git a/private/buf/cmd/buf/command/policy/policypush/policypush.go b/private/buf/cmd/buf/command/policy/policypush/policypush.go new file mode 100644 index 0000000000..b7bdf2df13 --- /dev/null +++ b/private/buf/cmd/buf/command/policy/policypush/policypush.go @@ -0,0 +1,240 @@ +// Copyright 2020-2025 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 policypush + +import ( + "bytes" + "context" + "fmt" + "os" + "slices" + "strings" + + "github.com/bufbuild/buf/private/buf/bufcli" + "github.com/bufbuild/buf/private/bufpkg/bufparse" + "github.com/bufbuild/buf/private/bufpkg/bufpolicy" + "github.com/bufbuild/buf/private/bufpkg/bufpolicy/bufpolicyconfig" + "github.com/bufbuild/buf/private/pkg/app/appcmd" + "github.com/bufbuild/buf/private/pkg/app/appext" + "github.com/bufbuild/buf/private/pkg/slicesext" + "github.com/bufbuild/buf/private/pkg/syserror" + "github.com/google/uuid" + "github.com/spf13/pflag" +) + +const ( + labelFlagName = "label" + configFlagName = "config" + createFlagName = "create" + createVisibilityFlagName = "create-visibility" +) + +// NewCommand returns a new Command. +func NewCommand( + name string, + builder appext.SubCommandBuilder, +) *appcmd.Command { + flags := newFlags() + return &appcmd.Command{ + Use: name + " ", + Short: "Push a policy to a registry", + Long: `The first argument is the policy 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 { + Labels []string + Config string + Create bool + CreateVisibility string + CreateType string + SourceControlURL string +} + +func newFlags() *flags { + return &flags{} +} + +func (f *flags) Bind(flagSet *pflag.FlagSet) { + bufcli.BindCreateVisibility(flagSet, &f.CreateVisibility, createVisibilityFlagName, createFlagName) + flagSet.StringSliceVar( + &f.Labels, + labelFlagName, + nil, + "Associate the label with the policies pushed. Can be used multiple times.", + ) + flagSet.StringVar( + &f.Config, + configFlagName, + "", + "The path to the policy YAML file to push.", + ) + flagSet.BoolVar( + &f.Create, + createFlagName, + false, + fmt.Sprintf( + "Create the policy if it does not exist. Defaults to creating a private policy on the BSR if --%s is not set.", + createVisibilityFlagName, + ), + ) +} + +func run( + ctx context.Context, + container appext.Container, + flags *flags, +) (retErr error) { + if err := validateFlags(flags); err != nil { + return err + } + // We parse the policy full name from the user-provided argument. + policyFullName, err := bufparse.ParseFullName(container.Arg(0)) + if err != nil { + return appcmd.WrapInvalidArgumentError(err) + } + commit, err := upload(ctx, container, flags, policyFullName) + if err != nil { + return err + } + // Only one commit is returned. + if _, err := fmt.Fprintf(container.Stdout(), "%s\n", commit.PolicyKey().String()); err != nil { + return syserror.Wrap(err) + } + return nil +} + +func upload( + ctx context.Context, + container appext.Container, + flags *flags, + policyFullName bufparse.FullName, +) (_ bufpolicy.Commit, retErr error) { + var policy bufpolicy.Policy + switch { + case flags.Config != "": + // We read the policy YAML file. + data, err := os.ReadFile(flags.Config) + if err != nil { + return nil, fmt.Errorf("could not read policy file %q: %w", flags.Config, err) + } + // Parse the policy YAML file to validate it. + _, err = bufpolicyconfig.ReadBufPolicyYAMLFile(bytes.NewReader(data), flags.Config) + if err != nil { + return nil, fmt.Errorf("unable to validate policy file %q: %w", flags.Config, err) + } + policy, err = bufpolicy.NewPolicy("", policyFullName, flags.Config, uuid.Nil, func() ([]byte, error) { + return data, nil + }) + if err != nil { + return nil, fmt.Errorf("unable to create policy from file %q: %w", flags.Config, err) + } + default: + // This should never happen because the flags are validated. + return nil, syserror.Newf("--%s must be set", configFlagName) + } + uploader, err := bufcli.NewPolicyUploader(container) + if err != nil { + return nil, err + } + var options []bufpolicy.UploadOption + if flags.Create { + createPolicyVisibility, err := bufpolicy.ParsePolicyVisibility(flags.CreateVisibility) + if err != nil { + return nil, err + } + options = append(options, bufpolicy.UploadWithCreateIfNotExist( + createPolicyVisibility, + )) + } + if len(flags.Labels) > 0 { + options = append(options, bufpolicy.UploadWithLabels(flags.Labels...)) + } + if flags.SourceControlURL != "" { + options = append(options, bufpolicy.UploadWithSourceControlURL(flags.SourceControlURL)) + } + commits, err := uploader.Upload(ctx, []bufpolicy.Policy{policy}, options...) + if err != nil { + return nil, err + } + if len(commits) != 1 { + return nil, syserror.Newf("unexpected number of commits returned from server: %d", len(commits)) + } + return commits[0], nil +} + +func validateFlags(flags *flags) error { + if err := validateLabelFlags(flags); err != nil { + return err + } + if err := validateTypeFlags(flags); err != nil { + return err + } + if err := validateCreateFlags(flags); err != nil { + return err + } + return nil +} + +func validateLabelFlags(flags *flags) error { + return validateLabelFlagValues(flags) +} + +func validateTypeFlags(flags *flags) error { + var typeFlags []string + if flags.Config != "" { + typeFlags = append(typeFlags, configFlagName) + } + if len(typeFlags) > 1 { + usedFlagsErrStr := strings.Join( + slicesext.Map( + typeFlags, + 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 len(typeFlags) == 0 { + return appcmd.NewInvalidArgumentErrorf("--%s must be set", configFlagName) + } + return nil +} + +func validateLabelFlagValues(flags *flags) error { + if slices.Contains(flags.Labels, "") { + return appcmd.NewInvalidArgumentErrorf("--%s requires a non-empty string", labelFlagName) + } + return nil +} + +func validateCreateFlags(flags *flags) error { + if flags.Create { + if flags.CreateVisibility == "" { + return appcmd.NewInvalidArgumentErrorf("--%s must be set if --%s is set", createVisibilityFlagName, createFlagName) + } + if _, err := bufpolicy.ParsePolicyVisibility(flags.CreateVisibility); err != nil { + return appcmd.WrapInvalidArgumentError(err) + } + } + return nil +} diff --git a/private/buf/cmd/buf/command/policy/policypush/usage.gen.go b/private/buf/cmd/buf/command/policy/policypush/usage.gen.go new file mode 100644 index 0000000000..1ce6c68477 --- /dev/null +++ b/private/buf/cmd/buf/command/policy/policypush/usage.gen.go @@ -0,0 +1,19 @@ +// Copyright 2020-2025 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 policypush + +import _ "github.com/bufbuild/buf/private/usage"