From 49d48bcd0ef05cc9ff411f8d1bf7f62cc854aa1c Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 6 Dec 2023 06:59:11 +0800 Subject: [PATCH] feat: add notation plugin uninstall command (#842) This PR is based on the spec PR: https://github.com/notaryproject/notation/pull/809. It adds the notation plugin uninstall command. Signed-off-by: Patrick Zheng --- cmd/notation/main.go | 3 +- cmd/notation/plugin/cmd.go | 30 ++++++ cmd/notation/{plugin.go => plugin/list.go} | 13 +-- cmd/notation/plugin/uninstall.go | 107 +++++++++++++++++++++ go.mod | 10 +- go.sum | 20 ++-- internal/osutil/file.go | 4 +- test/e2e/internal/notation/init.go | 37 +++---- test/e2e/run.sh | 5 +- test/e2e/suite/plugin/uninstall.go | 38 ++++++++ 10 files changed, 219 insertions(+), 48 deletions(-) create mode 100644 cmd/notation/plugin/cmd.go rename cmd/notation/{plugin.go => plugin/list.go} (89%) create mode 100644 cmd/notation/plugin/uninstall.go create mode 100644 test/e2e/suite/plugin/uninstall.go diff --git a/cmd/notation/main.go b/cmd/notation/main.go index feb103aa3..f4341da28 100644 --- a/cmd/notation/main.go +++ b/cmd/notation/main.go @@ -17,6 +17,7 @@ import ( "os" "github.com/notaryproject/notation/cmd/notation/cert" + "github.com/notaryproject/notation/cmd/notation/plugin" "github.com/notaryproject/notation/cmd/notation/policy" "github.com/spf13/cobra" ) @@ -40,7 +41,7 @@ func main() { cert.Cmd(), policy.Cmd(), keyCommand(), - pluginCommand(), + plugin.Cmd(), loginCommand(nil), logoutCommand(nil), versionCommand(), diff --git a/cmd/notation/plugin/cmd.go b/cmd/notation/plugin/cmd.go new file mode 100644 index 000000000..39e01bcbf --- /dev/null +++ b/cmd/notation/plugin/cmd.go @@ -0,0 +1,30 @@ +// Copyright The Notary Project Authors. +// 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 plugin + +import "github.com/spf13/cobra" + +func Cmd() *cobra.Command { + command := &cobra.Command{ + Use: "plugin", + Short: "Manage plugins", + } + + command.AddCommand( + listCommand(), + uninstallCommand(nil), + ) + + return command +} diff --git a/cmd/notation/plugin.go b/cmd/notation/plugin/list.go similarity index 89% rename from cmd/notation/plugin.go rename to cmd/notation/plugin/list.go index be2c2f3ad..e715ac233 100644 --- a/cmd/notation/plugin.go +++ b/cmd/notation/plugin/list.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package plugin import ( "fmt" @@ -24,16 +24,7 @@ import ( "github.com/spf13/cobra" ) -func pluginCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "plugin", - Short: "Manage plugins", - } - cmd.AddCommand(pluginListCommand()) - return cmd -} - -func pluginListCommand() *cobra.Command { +func listCommand() *cobra.Command { return &cobra.Command{ Use: "list [flags]", Aliases: []string{"ls"}, diff --git a/cmd/notation/plugin/uninstall.go b/cmd/notation/plugin/uninstall.go new file mode 100644 index 000000000..d60b77e9f --- /dev/null +++ b/cmd/notation/plugin/uninstall.go @@ -0,0 +1,107 @@ +// Copyright The Notary Project Authors. +// 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 plugin + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation-go/plugin" + "github.com/notaryproject/notation/cmd/notation/internal/cmdutil" + "github.com/notaryproject/notation/internal/cmd" + "github.com/spf13/cobra" +) + +type pluginUninstallOpts struct { + cmd.LoggingFlagOpts + pluginName string + confirmed bool +} + +func uninstallCommand(opts *pluginUninstallOpts) *cobra.Command { + if opts == nil { + opts = &pluginUninstallOpts{} + } + command := &cobra.Command{ + Use: "uninstall [flags] ", + Aliases: []string{"remove", "rm"}, + Short: "Uninstall a plugin", + Long: `Uninstall a plugin + +Example - Uninstall plugin: + notation plugin uninstall wabbit-plugin +`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("plugin name is required") + } + if len(args) > 1 { + return errors.New("only one plugin can be removed at a time") + } + opts.pluginName = args[0] + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return uninstallPlugin(cmd, opts) + }, + } + + opts.LoggingFlagOpts.ApplyFlags(command.Flags()) + command.Flags().BoolVarP(&opts.confirmed, "yes", "y", false, "do not prompt for confirmation") + return command +} + +func uninstallPlugin(command *cobra.Command, opts *pluginUninstallOpts) error { + // set logger + ctx := opts.LoggingFlagOpts.InitializeLogger(command.Context()) + pluginName := opts.pluginName + exist, err := checkPluginExistence(ctx, pluginName) + if err != nil { + return fmt.Errorf("failed to check plugin existence: %w", err) + } + if !exist { + return fmt.Errorf("unable to find plugin %s.\nTo view a list of installed plugins, use `notation plugin list`", pluginName) + } + // core process + prompt := fmt.Sprintf("Are you sure you want to uninstall plugin %q?", pluginName) + confirmed, err := cmdutil.AskForConfirmation(os.Stdin, prompt, opts.confirmed) + if err != nil { + return fmt.Errorf("failed when asking for confirmation: %w", err) + } + if !confirmed { + return nil + } + mgr := plugin.NewCLIManager(dir.PluginFS()) + if err := mgr.Uninstall(ctx, pluginName); err != nil { + return fmt.Errorf("failed to uninstall %s: %w", pluginName, err) + } + fmt.Printf("Successfully uninstalled plugin %s\n", pluginName) + return nil +} + +// checkPluginExistence returns true if plugin exists in the system +func checkPluginExistence(ctx context.Context, pluginName string) (bool, error) { + mgr := plugin.NewCLIManager(dir.PluginFS()) + _, err := mgr.Get(ctx, pluginName) + if err != nil { + if errors.Is(err, os.ErrNotExist) { // plugin does not exist + return false, nil + } + return false, err + } + return true, nil +} diff --git a/go.mod b/go.mod index 5bbb4c412..ce90192e9 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,14 @@ go 1.21 require ( github.com/notaryproject/notation-core-go v1.0.1 - github.com/notaryproject/notation-go v1.0.1 + github.com/notaryproject/notation-go v1.0.2-0.20231123031546-5de0d58b21c1 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc5 github.com/oras-project/oras-credentials-go v0.3.1 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 - golang.org/x/term v0.13.0 + golang.org/x/term v0.14.0 oras.land/oras-go/v2 v2.3.1 ) @@ -25,8 +25,8 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/veraison/go-cose v1.1.0 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/mod v0.13.0 // indirect + golang.org/x/crypto v0.15.0 // indirect + golang.org/x/mod v0.14.0 // indirect golang.org/x/sync v0.4.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/sys v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index d30a5638e..4f1f59a82 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/notaryproject/notation-core-go v1.0.1 h1:01doxjDERbd0vocLQrlJdusKrRLNNn50OJzp0c5I4Cw= github.com/notaryproject/notation-core-go v1.0.1/go.mod h1:rayl8WlKgS4YxOZgDO0iGGB4Ef515ZFZUFaZDmsPXgE= -github.com/notaryproject/notation-go v1.0.1 h1:D3fqG3eaBKVESRySV/Tg//MyTg2Q1nTKPh/t2q9LpSw= -github.com/notaryproject/notation-go v1.0.1/go.mod h1:VonyZsbocRQQNIDq/VPV5jKJOQwDH3gvfK4cXNpUA0U= +github.com/notaryproject/notation-go v1.0.2-0.20231123031546-5de0d58b21c1 h1:TuSZ+3Eu3A/XKucl7J95sDT8XoG6t2dEcIipt6ydAls= +github.com/notaryproject/notation-go v1.0.2-0.20231123031546-5de0d58b21c1/go.mod h1:tSCFsAdKAtB7AfKS/BaUf8AXzASA+9TEokMDEDutqPM= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= @@ -51,12 +51,12 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -76,15 +76,15 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/internal/osutil/file.go b/internal/osutil/file.go index 70fdfdf8c..66f1be0fe 100644 --- a/internal/osutil/file.go +++ b/internal/osutil/file.go @@ -72,8 +72,8 @@ func CopyToDir(src, dst string) (int64, error) { if err := os.MkdirAll(dst, 0700); err != nil { return 0, err } - certFile := filepath.Join(dst, filepath.Base(src)) - destination, err := os.Create(certFile) + dstFile := filepath.Join(dst, filepath.Base(src)) + destination, err := os.Create(dstFile) if err != nil { return 0, err } diff --git a/test/e2e/internal/notation/init.go b/test/e2e/internal/notation/init.go index f323e13b7..2e1659917 100644 --- a/test/e2e/internal/notation/init.go +++ b/test/e2e/internal/notation/init.go @@ -33,17 +33,18 @@ const ( ) const ( - envKeyRegistryHost = "NOTATION_E2E_REGISTRY_HOST" - envKeyRegistryUsername = "NOTATION_E2E_REGISTRY_USERNAME" - envKeyRegistryPassword = "NOTATION_E2E_REGISTRY_PASSWORD" - envKeyDomainRegistryHost = "NOTATION_E2E_DOMAIN_REGISTRY_HOST" - envKeyNotationBinPath = "NOTATION_E2E_BINARY_PATH" - envKeyNotationOldBinPath = "NOTATION_E2E_OLD_BINARY_PATH" - envKeyNotationPluginPath = "NOTATION_E2E_PLUGIN_PATH" - envKeyNotationConfigPath = "NOTATION_E2E_CONFIG_PATH" - envKeyOCILayoutPath = "NOTATION_E2E_OCI_LAYOUT_PATH" - envKeyTestRepo = "NOTATION_E2E_TEST_REPO" - envKeyTestTag = "NOTATION_E2E_TEST_TAG" + envKeyRegistryHost = "NOTATION_E2E_REGISTRY_HOST" + envKeyRegistryUsername = "NOTATION_E2E_REGISTRY_USERNAME" + envKeyRegistryPassword = "NOTATION_E2E_REGISTRY_PASSWORD" + envKeyDomainRegistryHost = "NOTATION_E2E_DOMAIN_REGISTRY_HOST" + envKeyNotationBinPath = "NOTATION_E2E_BINARY_PATH" + envKeyNotationOldBinPath = "NOTATION_E2E_OLD_BINARY_PATH" + envKeyNotationPluginPath = "NOTATION_E2E_PLUGIN_PATH" + envKeyNotationPluginTarGzPath = "NOTATION_E2E_PLUGIN_TAR_GZ_PATH" + envKeyNotationConfigPath = "NOTATION_E2E_CONFIG_PATH" + envKeyOCILayoutPath = "NOTATION_E2E_OCI_LAYOUT_PATH" + envKeyTestRepo = "NOTATION_E2E_TEST_REPO" + envKeyTestTag = "NOTATION_E2E_TEST_TAG" ) var ( @@ -51,12 +52,13 @@ var ( NotationBinPath string // NotationOldBinPath is the path of an old version notation binary for // testing forward compatibility. - NotationOldBinPath string - NotationE2EPluginPath string - NotationE2EConfigPath string - NotationE2ELocalKeysDir string - NotationE2ETrustPolicyDir string - NotationE2EConfigJsonDir string + NotationOldBinPath string + NotationE2EPluginPath string + NotationE2EPluginTarGzPath string + NotationE2EConfigPath string + NotationE2ELocalKeysDir string + NotationE2ETrustPolicyDir string + NotationE2EConfigJsonDir string ) var ( @@ -90,6 +92,7 @@ func setUpNotationValues() { // set Notation e2e-plugin path setPathValue(envKeyNotationPluginPath, &NotationE2EPluginPath) + setPathValue(envKeyNotationPluginTarGzPath, &NotationE2EPluginTarGzPath) // set Notation configuration paths setPathValue(envKeyNotationConfigPath, &NotationE2EConfigPath) diff --git a/test/e2e/run.sh b/test/e2e/run.sh index 7d053aa89..6365793ea 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -71,9 +71,9 @@ fi # install dependency go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo@v2.9.5 -# build e2e plugin +# build e2e plugin and tar.gz PLUGIN_NAME=e2e-plugin -( cd $CWD/plugin && go build -o ./bin/$PLUGIN_NAME . && echo "e2e plugin built." ) +( cd $CWD/plugin && go build -o ./bin/$PLUGIN_NAME . && echo "e2e plugin built." && tar --transform="flags=r;s|$PLUGIN_NAME|notation-$PLUGIN_NAME|" -czvf ./bin/$PLUGIN_NAME.tar.gz -C ./bin/ $PLUGIN_NAME) # setup registry case $REGISTRY_NAME in @@ -107,6 +107,7 @@ export NOTATION_E2E_OCI_LAYOUT_PATH=$CWD/testdata/registry/oci_layout export NOTATION_E2E_TEST_REPO=e2e export NOTATION_E2E_TEST_TAG=v1 export NOTATION_E2E_PLUGIN_PATH=$CWD/plugin/bin/$PLUGIN_NAME +export NOTATION_E2E_PLUGIN_TAR_GZ_PATH=$CWD/plugin/bin/$PLUGIN_NAME.tar.gz # run tests ginkgo -r -p -v \ No newline at end of file diff --git a/test/e2e/suite/plugin/uninstall.go b/test/e2e/suite/plugin/uninstall.go new file mode 100644 index 000000000..7a4b26ee5 --- /dev/null +++ b/test/e2e/suite/plugin/uninstall.go @@ -0,0 +1,38 @@ +// Copyright The Notary Project Authors. +// 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 plugin + +import ( + . "github.com/notaryproject/notation/test/e2e/internal/notation" + "github.com/notaryproject/notation/test/e2e/internal/utils" + . "github.com/onsi/ginkgo/v2" +) + +var _ = Describe("notation plugin uninstall", func() { + It("with valid plugin name", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + vhost.SetOption(AddPlugin(NotationE2EPluginPath)) + notation.Exec("plugin", "uninstall", "--yes", "e2e-plugin"). + MatchContent("Successfully uninstalled plugin e2e-plugin\n") + }) + }) + + It("with plugin does not exist", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure().Exec("plugin", "uninstall", "--yes", "non-exist"). + MatchErrContent("Error: unable to find plugin non-exist.\nTo view a list of installed plugins, use `notation plugin list`\n") + }) + }) + +})